diff --git a/.github/workflows/run-test-harness.yml b/.github/workflows/run-test-harness.yml index cbdb732..b2b1cb1 100644 --- a/.github/workflows/run-test-harness.yml +++ b/.github/workflows/run-test-harness.yml @@ -13,8 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: DevCycleHQ/test-harness@main - env: - SDK_CAPABILITIES: '["clientCustomData","v2Config","EdgeDB","CloudBucketing"]' with: sdks-to-test: python sdk-github-sha: ${{github.event.pull_request.head.sha}} + sdk-capabilities: '["cloud", "edgeDB", "clientCustomData","v2Config", "allVariables", "allFeatures", "evalReason", "cloudEvalReason"]' diff --git a/devcycle_python_sdk/api/bucketing_client.py b/devcycle_python_sdk/api/bucketing_client.py index 1fc9254..698c0fd 100644 --- a/devcycle_python_sdk/api/bucketing_client.py +++ b/devcycle_python_sdk/api/bucketing_client.py @@ -15,6 +15,7 @@ from devcycle_python_sdk.models.feature import Feature from devcycle_python_sdk.models.user import DevCycleUser from devcycle_python_sdk.models.variable import Variable +from devcycle_python_sdk.models.eval_reason import EvalReason from devcycle_python_sdk.util.strings import slash_join logger = logging.getLogger(__name__) @@ -91,11 +92,17 @@ def request(self, method: str, url: str, **kwargs) -> dict: def variable(self, key: str, user: DevCycleUser) -> Variable: data = self.request("POST", self._url("variables", key), json=user.to_json()) + eval_data = data.get("eval") + eval_reason = None + if eval_data is not None and isinstance(eval_data, dict): + eval_reason = EvalReason.from_json(eval_data) + return Variable( _id=data.get("_id"), key=data.get("key", ""), type=data.get("type", ""), value=data.get("value"), + eval=eval_reason, ) def variables(self, user: DevCycleUser) -> Dict[str, Variable]: @@ -109,6 +116,11 @@ def variables(self, user: DevCycleUser) -> Dict[str, Variable]: type=str(value.get("type")), value=value.get("value"), isDefaulted=None, + eval=( + EvalReason.from_json(value.get("eval")) + if value.get("eval") + else None + ), ) return result diff --git a/devcycle_python_sdk/api/local_bucketing.py b/devcycle_python_sdk/api/local_bucketing.py index 4a26b01..5786bf2 100644 --- a/devcycle_python_sdk/api/local_bucketing.py +++ b/devcycle_python_sdk/api/local_bucketing.py @@ -22,7 +22,6 @@ import devcycle_python_sdk.protobuf.utils as pb_utils import devcycle_python_sdk.protobuf.variableForUserParams_pb2 as pb2 from devcycle_python_sdk.exceptions import ( - VariableTypeMismatchError, MalformedConfigError, ) from devcycle_python_sdk.models.bucketed_config import BucketedConfig @@ -324,13 +323,6 @@ def get_variable_for_user_protobuf( sdk_variable = pb2.SDKVariable_PB() sdk_variable.ParseFromString(var_bytes) - if sdk_variable.type != pb_variable_type: - # this situation should never actually happen because the WASM handles - # it internally and returns a null value from the WASM function - # This check is here just in case that logic changes in the future - raise VariableTypeMismatchError( - f"Variable returned does not match requested type: {pb_variable_type}" - ) return pb_utils.create_variable(sdk_variable, default_value) def generate_bucketed_config(self, user: DevCycleUser) -> BucketedConfig: diff --git a/devcycle_python_sdk/bucketing-lib.release.wasm b/devcycle_python_sdk/bucketing-lib.release.wasm index f3c33d6..3e4843f 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/cloud_client.py b/devcycle_python_sdk/cloud_client.py index dd21e63..0a78e97 100644 --- a/devcycle_python_sdk/cloud_client.py +++ b/devcycle_python_sdk/cloud_client.py @@ -14,6 +14,9 @@ BeforeHookError, AfterHookError, ) +from devcycle_python_sdk.models.eval_reason import ( + DefaultReasonDetails, +) from devcycle_python_sdk.models.eval_hook import EvalHook from devcycle_python_sdk.models.eval_hook_context import HookContext from devcycle_python_sdk.models.user import DevCycleUser @@ -121,7 +124,9 @@ def variable(self, user: DevCycleUser, key: str, default_value: Any) -> Variable except NotFoundError: logger.warning(f"DevCycle: Variable not found: {key}") return Variable.create_default_variable( - key=key, default_value=default_value + key=key, + default_value=default_value, + default_reason_detail=DefaultReasonDetails.MISSING_VARIABLE, ) except BeforeHookError as e: self.eval_hooks_manager.run_error(context, e) @@ -130,7 +135,9 @@ def variable(self, user: DevCycleUser, key: str, default_value: Any) -> Variable except Exception as e: logger.error(f"DevCycle: Error evaluating variable: {e}") return Variable.create_default_variable( - key=key, default_value=default_value + key=key, + default_value=default_value, + default_reason_detail=DefaultReasonDetails.ERROR, ) finally: self.eval_hooks_manager.run_finally(context, variable) @@ -143,7 +150,9 @@ def variable(self, user: DevCycleUser, key: str, default_value: Any) -> Variable f"DevCycle: Variable {key} is type {type(variable.value)}, but default value is type {type(default_value)}", ) return Variable.create_default_variable( - key=key, default_value=default_value + key=key, + default_value=default_value, + default_reason_detail=DefaultReasonDetails.TYPE_MISMATCH, ) return variable diff --git a/devcycle_python_sdk/local_client.py b/devcycle_python_sdk/local_client.py index cacaac2..dbf97b5 100644 --- a/devcycle_python_sdk/local_client.py +++ b/devcycle_python_sdk/local_client.py @@ -6,7 +6,6 @@ from devcycle_python_sdk import DevCycleLocalOptions, AbstractDevCycleClient from devcycle_python_sdk.api.local_bucketing import LocalBucketing -from devcycle_python_sdk.exceptions import VariableTypeMismatchError from devcycle_python_sdk.managers.config_manager import EnvironmentConfigManager from devcycle_python_sdk.managers.eval_hooks_manager import ( EvalHooksManager, @@ -17,6 +16,11 @@ from devcycle_python_sdk.models.bucketed_config import BucketedConfig from devcycle_python_sdk.models.eval_hook import EvalHook from devcycle_python_sdk.models.eval_hook_context import HookContext +from devcycle_python_sdk.models.eval_reason import ( + DefaultReasonDetails, + EvalReason, + EvalReasons, +) from devcycle_python_sdk.models.event import DevCycleEvent, EventType from devcycle_python_sdk.models.feature import Feature from devcycle_python_sdk.models.platform_data import default_platform_data @@ -139,7 +143,9 @@ def variable(self, user: DevCycleUser, key: str, default_value: Any) -> Variable logger.warning( f"DevCycle: Unable to track AggVariableDefaulted event for Variable {key}: {e}" ) - return Variable.create_default_variable(key, default_value) + return Variable.create_default_variable( + key, default_value, DefaultReasonDetails.MISSING_CONFIG + ) context = HookContext(key, user, default_value) variable = Variable.create_default_variable( @@ -159,22 +165,28 @@ def variable(self, user: DevCycleUser, key: str, default_value: Any) -> Variable ) if bucketed_variable is not None: variable = bucketed_variable + else: + variable.eval = EvalReason( + reason=EvalReasons.DEFAULT, + details=DefaultReasonDetails.USER_NOT_TARGETED, + ) if before_hook_error is None: self.eval_hooks_manager.run_after(context, variable) else: raise before_hook_error - except VariableTypeMismatchError: - logger.debug("DevCycle: Variable type mismatch, returning default value") - return variable - except BeforeHookError as e: - self.eval_hooks_manager.run_error(context, e) - return variable - except AfterHookError as e: - self.eval_hooks_manager.run_error(context, e) - return variable except Exception as e: - logger.warning(f"DevCycle: Error retrieving variable for user: {e}") + variable.eval = EvalReason( + reason=EvalReasons.DEFAULT, details=DefaultReasonDetails.ERROR + ) + + if isinstance(e, BeforeHookError): + self.eval_hooks_manager.run_error(context, e) + elif isinstance(e, AfterHookError): + self.eval_hooks_manager.run_error(context, e) + else: + logger.warning(f"DevCycle: Error retrieving variable for user: {e}") + return variable finally: self.eval_hooks_manager.run_finally(context, variable) diff --git a/devcycle_python_sdk/models/eval_reason.py b/devcycle_python_sdk/models/eval_reason.py new file mode 100644 index 0000000..ae93984 --- /dev/null +++ b/devcycle_python_sdk/models/eval_reason.py @@ -0,0 +1,40 @@ +from dataclasses import dataclass +from typing import Optional + + +class EvalReasons: + """Evaluation reasons constants""" + + DEFAULT = "DEFAULT" + + +class DefaultReasonDetails: + """Default reason details constants""" + + MISSING_CONFIG = "Missing Config" + USER_NOT_TARGETED = "User Not Targeted" + TYPE_MISMATCH = "Variable Type Mismatch" + MISSING_VARIABLE = "Missing Variable" + ERROR = "Error" + + +@dataclass(order=False) +class EvalReason: + reason: str + details: Optional[str] = None + target_id: Optional[str] = None + + def to_json(self): + return { + key: getattr(self, key) + for key in self.__dataclass_fields__ + if getattr(self, key) is not None + } + + @classmethod + def from_json(cls, data: dict) -> "EvalReason": + return cls( + reason=data["reason"], + details=data.get("details"), + target_id=data.get("target_id"), + ) diff --git a/devcycle_python_sdk/models/variable.py b/devcycle_python_sdk/models/variable.py index bce1268..3bace9b 100644 --- a/devcycle_python_sdk/models/variable.py +++ b/devcycle_python_sdk/models/variable.py @@ -2,6 +2,8 @@ from dataclasses import dataclass from typing import Optional, Any +from .eval_reason import EvalReason, EvalReasons + class TypeEnum: BOOLEAN = "Boolean" @@ -32,16 +34,26 @@ class Variable: isDefaulted: Optional[bool] = False defaultValue: Any = None evalReason: Optional[str] = None + eval: Optional[EvalReason] = None def to_json(self): - return { - key: getattr(self, key) - for key in self.__dataclass_fields__ - if getattr(self, key) is not None - } + result = {} + for key in self.__dataclass_fields__: + value = getattr(self, key) + if value is not None: + if key == "eval" and isinstance(value, EvalReason): + result[key] = value.to_json() + else: + result[key] = value + return result @classmethod def from_json(cls, data: dict) -> "Variable": + eval_data = data.get("eval") + eval_reason = None + if eval_data: + eval_reason = EvalReason.from_json(eval_data) + return cls( _id=data["_id"], key=data["key"], @@ -50,11 +62,20 @@ def from_json(cls, data: dict) -> "Variable": isDefaulted=data.get("isDefaulted", None), defaultValue=data.get("defaultValue"), evalReason=data.get("evalReason"), + eval=eval_reason, ) @staticmethod - def create_default_variable(key: str, default_value: Any) -> "Variable": + def create_default_variable( + key: str, default_value: Any, default_reason_detail: Optional[str] = None + ) -> "Variable": var_type = determine_variable_type(default_value) + if default_reason_detail is not None: + eval_reason = EvalReason( + reason=EvalReasons.DEFAULT, details=default_reason_detail + ) + else: + eval_reason = None return Variable( _id=None, key=key, @@ -62,4 +83,5 @@ def create_default_variable(key: str, default_value: Any) -> "Variable": value=default_value, defaultValue=default_value, isDefaulted=True, + eval=eval_reason, ) diff --git a/devcycle_python_sdk/protobuf/utils.py b/devcycle_python_sdk/protobuf/utils.py index da6e821..c433bf6 100644 --- a/devcycle_python_sdk/protobuf/utils.py +++ b/devcycle_python_sdk/protobuf/utils.py @@ -5,6 +5,7 @@ from typing import Any, Optional from devcycle_python_sdk.models.variable import TypeEnum, Variable +from devcycle_python_sdk.models.eval_reason import EvalReason from devcycle_python_sdk.models.user import DevCycleUser import devcycle_python_sdk.protobuf.variableForUserParams_pb2 as pb2 @@ -82,7 +83,20 @@ def create_dvcuser_pb(user: DevCycleUser) -> pb2.DVCUser_PB: # type: ignore ) +def create_eval_reason_from_pb(eval_reason_pb: pb2.EvalReason_PB) -> EvalReason: # type: ignore + """Convert EvalReason_PB protobuf message to EvalReason object""" + return EvalReason( + reason=eval_reason_pb.reason, + details=eval_reason_pb.details if eval_reason_pb.details else None, + target_id=eval_reason_pb.target_id if eval_reason_pb.target_id else None, + ) + + def create_variable(sdk_variable: pb2.SDKVariable_PB, default_value: Any) -> Variable: # type: ignore + eval_reason_obj = None + if sdk_variable.HasField("eval"): + eval_reason_obj = create_eval_reason_from_pb(sdk_variable.eval) + if sdk_variable.type == pb2.VariableType_PB.Boolean: # type: ignore return Variable( _id=None, @@ -91,6 +105,7 @@ def create_variable(sdk_variable: pb2.SDKVariable_PB, default_value: Any) -> Var type=TypeEnum.BOOLEAN, isDefaulted=False, defaultValue=default_value, + eval=eval_reason_obj, ) elif sdk_variable.type == pb2.VariableType_PB.String: # type: ignore @@ -101,6 +116,7 @@ def create_variable(sdk_variable: pb2.SDKVariable_PB, default_value: Any) -> Var type=TypeEnum.STRING, isDefaulted=False, defaultValue=default_value, + eval=eval_reason_obj, ) elif sdk_variable.type == pb2.VariableType_PB.Number: # type: ignore @@ -111,6 +127,7 @@ def create_variable(sdk_variable: pb2.SDKVariable_PB, default_value: Any) -> Var type=TypeEnum.NUMBER, isDefaulted=False, defaultValue=default_value, + eval=eval_reason_obj, ) elif sdk_variable.type == pb2.VariableType_PB.JSON: # type: ignore @@ -123,6 +140,7 @@ def create_variable(sdk_variable: pb2.SDKVariable_PB, default_value: Any) -> Var type=TypeEnum.JSON, isDefaulted=False, defaultValue=default_value, + eval=eval_reason_obj, ) else: diff --git a/devcycle_python_sdk/protobuf/variableForUserParams_pb2.py b/devcycle_python_sdk/protobuf/variableForUserParams_pb2.py index fdd0257..289f5b9 100644 --- a/devcycle_python_sdk/protobuf/variableForUserParams_pb2.py +++ b/devcycle_python_sdk/protobuf/variableForUserParams_pb2.py @@ -1,11 +1,22 @@ # -*- 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.internal import builder as _builder 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() @@ -13,34 +24,36 @@ -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\"\xac\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*@\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') - -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'variableForUserParams_pb2', globals()) -if _descriptor._USE_C_DESCRIPTORS == False: +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._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' - _VARIABLETYPE_PB._serialized_start=1087 - _VARIABLETYPE_PB._serialized_end=1151 - _CUSTOMDATATYPE._serialized_start=1153 - _CUSTOMDATATYPE._serialized_end=1207 - _NULLABLESTRING._serialized_start=31 - _NULLABLESTRING._serialized_end=78 - _NULLABLEDOUBLE._serialized_start=80 - _NULLABLEDOUBLE._serialized_end=127 - _CUSTOMDATAVALUE._serialized_start=129 - _CUSTOMDATAVALUE._serialized_end=238 - _NULLABLECUSTOMDATA._serialized_start=241 - _NULLABLECUSTOMDATA._serialized_end=388 - _NULLABLECUSTOMDATA_VALUEENTRY._serialized_start=326 - _NULLABLECUSTOMDATA_VALUEENTRY._serialized_end=388 - _VARIABLEFORUSERPARAMS_PB._serialized_start=391 - _VARIABLEFORUSERPARAMS_PB._serialized_end=547 - _DVCUSER_PB._serialized_start=550 - _DVCUSER_PB._serialized_end=910 - _SDKVARIABLE_PB._serialized_start=913 - _SDKVARIABLE_PB._serialized_end=1085 +_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 # @@protoc_insertion_point(module_scope) diff --git a/devcycle_python_sdk/protobuf/variableForUserParams_pb2.pyi b/devcycle_python_sdk/protobuf/variableForUserParams_pb2.pyi index abfd89c..175eee2 100644 --- a/devcycle_python_sdk/protobuf/variableForUserParams_pb2.pyi +++ b/devcycle_python_sdk/protobuf/variableForUserParams_pb2.pyi @@ -4,117 +4,139 @@ from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from typing import ClassVar as _ClassVar, Mapping as _Mapping, Optional as _Optional, Union as _Union -Bool: CustomDataType -Boolean: VariableType_PB DESCRIPTOR: _descriptor.FileDescriptor + +class VariableType_PB(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __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__ = () + Bool: _ClassVar[CustomDataType] + Num: _ClassVar[CustomDataType] + Str: _ClassVar[CustomDataType] + Null: _ClassVar[CustomDataType] +Boolean: VariableType_PB +Number: VariableType_PB +String: VariableType_PB JSON: VariableType_PB -Null: CustomDataType +Bool: CustomDataType Num: CustomDataType -Number: VariableType_PB Str: CustomDataType -String: VariableType_PB +Null: CustomDataType + +class NullableString(_message.Message): + __slots__ = ("value", "isNull") + VALUE_FIELD_NUMBER: _ClassVar[int] + ISNULL_FIELD_NUMBER: _ClassVar[int] + value: str + isNull: bool + def __init__(self, value: _Optional[str] = ..., isNull: bool = ...) -> None: ... + +class NullableDouble(_message.Message): + __slots__ = ("value", "isNull") + VALUE_FIELD_NUMBER: _ClassVar[int] + ISNULL_FIELD_NUMBER: _ClassVar[int] + value: float + isNull: bool + def __init__(self, value: _Optional[float] = ..., isNull: bool = ...) -> None: ... class CustomDataValue(_message.Message): - __slots__ = ["boolValue", "doubleValue", "stringValue", "type"] + __slots__ = ("type", "boolValue", "doubleValue", "stringValue") + TYPE_FIELD_NUMBER: _ClassVar[int] BOOLVALUE_FIELD_NUMBER: _ClassVar[int] DOUBLEVALUE_FIELD_NUMBER: _ClassVar[int] STRINGVALUE_FIELD_NUMBER: _ClassVar[int] - TYPE_FIELD_NUMBER: _ClassVar[int] + type: CustomDataType boolValue: bool doubleValue: float stringValue: str - type: CustomDataType def __init__(self, type: _Optional[_Union[CustomDataType, str]] = ..., boolValue: bool = ..., doubleValue: _Optional[float] = ..., stringValue: _Optional[str] = ...) -> None: ... -class DVCUser_PB(_message.Message): - __slots__ = ["appBuild", "appVersion", "country", "customData", "deviceModel", "email", "language", "name", "privateCustomData", "user_id"] - APPBUILD_FIELD_NUMBER: _ClassVar[int] - APPVERSION_FIELD_NUMBER: _ClassVar[int] - COUNTRY_FIELD_NUMBER: _ClassVar[int] - CUSTOMDATA_FIELD_NUMBER: _ClassVar[int] - DEVICEMODEL_FIELD_NUMBER: _ClassVar[int] - EMAIL_FIELD_NUMBER: _ClassVar[int] - LANGUAGE_FIELD_NUMBER: _ClassVar[int] - NAME_FIELD_NUMBER: _ClassVar[int] - PRIVATECUSTOMDATA_FIELD_NUMBER: _ClassVar[int] - USER_ID_FIELD_NUMBER: _ClassVar[int] - appBuild: NullableDouble - appVersion: NullableString - country: NullableString - customData: NullableCustomData - deviceModel: NullableString - email: NullableString - language: NullableString - name: NullableString - privateCustomData: NullableCustomData - user_id: str - 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 NullableCustomData(_message.Message): - __slots__ = ["isNull", "value"] + __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 value: CustomDataValue def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[CustomDataValue, _Mapping]] = ...) -> None: ... - ISNULL_FIELD_NUMBER: _ClassVar[int] VALUE_FIELD_NUMBER: _ClassVar[int] - isNull: bool + ISNULL_FIELD_NUMBER: _ClassVar[int] value: _containers.MessageMap[str, CustomDataValue] + isNull: bool def __init__(self, value: _Optional[_Mapping[str, CustomDataValue]] = ..., isNull: bool = ...) -> None: ... -class NullableDouble(_message.Message): - __slots__ = ["isNull", "value"] - ISNULL_FIELD_NUMBER: _ClassVar[int] - VALUE_FIELD_NUMBER: _ClassVar[int] - isNull: bool - value: float - def __init__(self, value: _Optional[float] = ..., isNull: bool = ...) -> None: ... +class VariableForUserParams_PB(_message.Message): + __slots__ = ("sdkKey", "variableKey", "variableType", "user", "shouldTrackEvent") + SDKKEY_FIELD_NUMBER: _ClassVar[int] + VARIABLEKEY_FIELD_NUMBER: _ClassVar[int] + VARIABLETYPE_FIELD_NUMBER: _ClassVar[int] + USER_FIELD_NUMBER: _ClassVar[int] + SHOULDTRACKEVENT_FIELD_NUMBER: _ClassVar[int] + sdkKey: str + variableKey: str + variableType: VariableType_PB + user: DVCUser_PB + shouldTrackEvent: bool + 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 NullableString(_message.Message): - __slots__ = ["isNull", "value"] - ISNULL_FIELD_NUMBER: _ClassVar[int] - VALUE_FIELD_NUMBER: _ClassVar[int] - isNull: bool - value: str - def __init__(self, value: _Optional[str] = ..., isNull: bool = ...) -> None: ... +class DVCUser_PB(_message.Message): + __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] + LANGUAGE_FIELD_NUMBER: _ClassVar[int] + COUNTRY_FIELD_NUMBER: _ClassVar[int] + APPBUILD_FIELD_NUMBER: _ClassVar[int] + APPVERSION_FIELD_NUMBER: _ClassVar[int] + DEVICEMODEL_FIELD_NUMBER: _ClassVar[int] + CUSTOMDATA_FIELD_NUMBER: _ClassVar[int] + PRIVATECUSTOMDATA_FIELD_NUMBER: _ClassVar[int] + user_id: str + email: NullableString + name: NullableString + language: NullableString + country: NullableString + appBuild: NullableDouble + appVersion: NullableString + deviceModel: NullableString + customData: NullableCustomData + privateCustomData: NullableCustomData + 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", "boolValue", "doubleValue", "evalReason", "key", "stringValue", "type"] + __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] BOOLVALUE_FIELD_NUMBER: _ClassVar[int] DOUBLEVALUE_FIELD_NUMBER: _ClassVar[int] - EVALREASON_FIELD_NUMBER: _ClassVar[int] - KEY_FIELD_NUMBER: _ClassVar[int] STRINGVALUE_FIELD_NUMBER: _ClassVar[int] - TYPE_FIELD_NUMBER: _ClassVar[int] - _ID_FIELD_NUMBER: _ClassVar[int] + EVALREASON_FIELD_NUMBER: _ClassVar[int] + _FEATURE_FIELD_NUMBER: _ClassVar[int] + EVAL_FIELD_NUMBER: _ClassVar[int] _id: str + type: VariableType_PB + key: str boolValue: bool doubleValue: float - evalReason: NullableString - key: str stringValue: str - type: VariableType_PB - 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]] = ...) -> None: ... - -class VariableForUserParams_PB(_message.Message): - __slots__ = ["sdkKey", "shouldTrackEvent", "user", "variableKey", "variableType"] - SDKKEY_FIELD_NUMBER: _ClassVar[int] - SHOULDTRACKEVENT_FIELD_NUMBER: _ClassVar[int] - USER_FIELD_NUMBER: _ClassVar[int] - VARIABLEKEY_FIELD_NUMBER: _ClassVar[int] - VARIABLETYPE_FIELD_NUMBER: _ClassVar[int] - sdkKey: str - shouldTrackEvent: bool - user: DVCUser_PB - variableKey: str - variableType: VariableType_PB - 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 VariableType_PB(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): - __slots__ = [] + evalReason: NullableString + _feature: NullableString + eval: EvalReason_PB + 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 CustomDataType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): - __slots__ = [] +class EvalReason_PB(_message.Message): + __slots__ = ("reason", "details", "target_id") + REASON_FIELD_NUMBER: _ClassVar[int] + DETAILS_FIELD_NUMBER: _ClassVar[int] + TARGET_ID_FIELD_NUMBER: _ClassVar[int] + reason: str + details: str + target_id: str + def __init__(self, reason: _Optional[str] = ..., details: _Optional[str] = ..., target_id: _Optional[str] = ...) -> None: ... diff --git a/protobuf/variableForUserParams.proto b/protobuf/variableForUserParams.proto index f1f31d2..5320095 100644 --- a/protobuf/variableForUserParams.proto +++ b/protobuf/variableForUserParams.proto @@ -70,4 +70,12 @@ message SDKVariable_PB { double doubleValue = 5; string stringValue = 6; NullableString evalReason = 7; + NullableString _feature = 8; + EvalReason_PB eval = 9; +} + +message EvalReason_PB { + string reason = 1; + string details = 2; + string target_id = 3; } diff --git a/test/api/test_local_bucketing.py b/test/api/test_local_bucketing.py index a01f9f4..207f10c 100644 --- a/test/api/test_local_bucketing.py +++ b/test/api/test_local_bucketing.py @@ -10,6 +10,7 @@ Project, ProjectSettings, ) +from devcycle_python_sdk.models.eval_reason import EvalReason from devcycle_python_sdk.models.feature import Feature from devcycle_python_sdk.models.variable import Variable from devcycle_python_sdk.models.platform_data import default_platform_data @@ -155,6 +156,11 @@ def test_generate_bucketed_config(self): evalReason=None, ) } + expected_eval = EvalReason( + reason="TARGETING_MATCH", + details="All Users", + target_id="63125321d31c601f992288bc", + ) expected_variables = { "a-cool-new-feature": Variable( _id="62fbf6566f1ba302829f9e34", @@ -164,6 +170,7 @@ def test_generate_bucketed_config(self): isDefaulted=None, defaultValue=None, evalReason=None, + eval=expected_eval, ), "string-var": Variable( _id="63125320a4719939fd57cb2b", @@ -173,6 +180,7 @@ def test_generate_bucketed_config(self): isDefaulted=None, defaultValue=None, evalReason=None, + eval=expected_eval, ), "json-var": Variable( _id="64372363125123fca69d3f7b", @@ -186,6 +194,7 @@ def test_generate_bucketed_config(self): isDefaulted=None, defaultValue=None, evalReason=None, + eval=expected_eval, ), "num-var": Variable( _id="65272363125123fca69d3a7d", @@ -195,6 +204,7 @@ def test_generate_bucketed_config(self): isDefaulted=None, defaultValue=None, evalReason=None, + eval=expected_eval, ), "float-var": Variable( _id="61200363125123fca69d3a7a", @@ -204,6 +214,7 @@ def test_generate_bucketed_config(self): isDefaulted=None, defaultValue=None, evalReason=None, + eval=expected_eval, ), } diff --git a/test/test_cloud_client.py b/test/test_cloud_client.py index 5a1c24e..e9e863b 100644 --- a/test/test_cloud_client.py +++ b/test/test_cloud_client.py @@ -5,6 +5,7 @@ from time import time from unittest.mock import patch + from devcycle_python_sdk import DevCycleCloudClient, DevCycleCloudOptions from devcycle_python_sdk.models.eval_hook import EvalHook from devcycle_python_sdk.models.user import DevCycleUser @@ -97,6 +98,8 @@ def test_variable_exceptions(self, mock_variable_call): self.assertIsNotNone(result) self.assertEqual(result.value, "default_value") self.assertTrue(result.isDefaulted) + self.assertEqual(result.eval.reason, "DEFAULT") + self.assertEqual(result.eval.details, "Missing Variable") # other exception - return default mock_variable_call.reset_mock() @@ -105,6 +108,8 @@ def test_variable_exceptions(self, mock_variable_call): self.assertIsNotNone(result) self.assertEqual(result.value, "default_value") self.assertTrue(result.isDefaulted) + self.assertEqual(result.eval.reason, "DEFAULT") + self.assertEqual(result.eval.details, "Error") @patch("devcycle_python_sdk.api.bucketing_client.BucketingAPIClient.variable") def test_variable_type_mismatch(self, mock_variable_call): @@ -116,6 +121,8 @@ def test_variable_type_mismatch(self, mock_variable_call): self.assertIsNotNone(result) self.assertEqual(result.value, "default_value") self.assertTrue(result.isDefaulted) + self.assertEqual(result.eval.reason, "DEFAULT") + self.assertEqual(result.eval.details, "Variable Type Mismatch") @patch("devcycle_python_sdk.api.bucketing_client.BucketingAPIClient.variable") def test_variable_value_defaults(self, mock_variable_call): diff --git a/test/test_local_client.py b/test/test_local_client.py index 2e033e1..7764bd7 100644 --- a/test/test_local_client.py +++ b/test/test_local_client.py @@ -10,6 +10,7 @@ from devcycle_python_sdk.local_client import _validate_user, _validate_sdk_key from devcycle_python_sdk.exceptions import MalformedConfigError from devcycle_python_sdk.models.eval_hook import EvalHook +from devcycle_python_sdk.models.eval_reason import EvalReason from devcycle_python_sdk.models.event import DevCycleEvent from devcycle_python_sdk.models.feature import Feature from devcycle_python_sdk.api.local_bucketing import LocalBucketing @@ -208,6 +209,8 @@ def test_variable_default(self): self.assertEqual(result.defaultValue, test_value) self.assertEqual(result.value, test_value) self.assertEqual(result.type, value_type) + self.assertEqual(result.eval.reason, "DEFAULT") + self.assertEqual(result.eval.details, "User Not Targeted") @responses.activate def test_variable_with_bucketing(self): @@ -241,6 +244,9 @@ def test_variable_with_bucketing(self): self.assertDictEqual(result.value, expected, msg="Test key: " + key) else: self.assertEqual(result.value, expected, msg="Test key: " + key) + self.assertEqual(result.eval.reason, "TARGETING_MATCH") + self.assertEqual(result.eval.details, "All Users") + self.assertEqual(result.eval.target_id, "63125321d31c601f992288bc") @responses.activate def test_variable_with_events(self): @@ -294,7 +300,11 @@ def test_all_variables(self): user = DevCycleUser(user_id="1234") result = self.client.all_variables(user) - + expected_eval = EvalReason( + reason="TARGETING_MATCH", + details="All Users", + target_id="63125321d31c601f992288bc", + ) expected_variables = { "a-cool-new-feature": Variable( _id="62fbf6566f1ba302829f9e34", @@ -304,6 +314,7 @@ def test_all_variables(self): isDefaulted=None, defaultValue=None, evalReason=None, + eval=expected_eval, ), "string-var": Variable( _id="63125320a4719939fd57cb2b", @@ -313,6 +324,7 @@ def test_all_variables(self): isDefaulted=None, defaultValue=None, evalReason=None, + eval=expected_eval, ), "json-var": Variable( _id="64372363125123fca69d3f7b", @@ -326,6 +338,7 @@ def test_all_variables(self): isDefaulted=None, defaultValue=None, evalReason=None, + eval=expected_eval, ), "num-var": Variable( _id="65272363125123fca69d3a7d", @@ -335,6 +348,7 @@ def test_all_variables(self): isDefaulted=None, defaultValue=None, evalReason=None, + eval=expected_eval, ), "float-var": Variable( _id="61200363125123fca69d3a7a", @@ -344,6 +358,7 @@ def test_all_variables(self): isDefaulted=None, defaultValue=None, evalReason=None, + eval=expected_eval, ), } self.assertEqual(result, expected_variables) diff --git a/update_wasm_lib.sh b/update_wasm_lib.sh index 40a363b..93b0472 100755 --- a/update_wasm_lib.sh +++ b/update_wasm_lib.sh @@ -1,6 +1,6 @@ #!/bin/bash -BUCKETING_LIB_VERSION="1.35.1" +BUCKETING_LIB_VERSION="1.40.2" if [[ -n "$1" ]]; then BUCKETING_LIB_VERSION="$1"