diff --git a/eppo_client/__init__.py b/eppo_client/__init__.py index 64f4cc8..575524d 100644 --- a/eppo_client/__init__.py +++ b/eppo_client/__init__.py @@ -10,7 +10,7 @@ from eppo_client.http_client import HttpClient, SdkParams from eppo_client.read_write_lock import ReadWriteLock -__version__ = "1.2.3" +__version__ = "1.3.0" __client: Optional[EppoClient] = None __lock = ReadWriteLock() diff --git a/eppo_client/assignment_logger.py b/eppo_client/assignment_logger.py index c9dd3f6..309b26a 100644 --- a/eppo_client/assignment_logger.py +++ b/eppo_client/assignment_logger.py @@ -1,6 +1,10 @@ from typing import Dict +from eppo_client.base_model import BaseModel +from pydantic import ConfigDict -class AssignmentLogger: +class AssignmentLogger(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + def log_assignment(self, assignment_event: Dict): pass diff --git a/eppo_client/base_model.py b/eppo_client/base_model.py index 1541f7a..efbd618 100644 --- a/eppo_client/base_model.py +++ b/eppo_client/base_model.py @@ -1,14 +1,6 @@ -from pydantic import BaseModel - - -def to_camel(s: str): - words = s.split("_") - if len(words) > 1: - return words[0] + "".join([w.capitalize() for w in words[1:]]) - return words[0] +from pydantic import ConfigDict, BaseModel +from pydantic.alias_generators import to_camel class SdkBaseModel(BaseModel): - class Config: - alias_generator = to_camel - allow_population_by_field_name = True + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) diff --git a/eppo_client/client.py b/eppo_client/client.py index 100db4b..b043b6f 100644 --- a/eppo_client/client.py +++ b/eppo_client/client.py @@ -42,7 +42,7 @@ def get_string_assignment( subject_key, flag_key, subject_attributes, VariationType.STRING ) return ( - assigned_variation.typedValue + assigned_variation.typed_value if assigned_variation is not None else assigned_variation ) @@ -54,7 +54,7 @@ def get_numeric_assignment( subject_key, flag_key, subject_attributes, VariationType.NUMERIC ) return ( - assigned_variation.typedValue + assigned_variation.typed_value if assigned_variation is not None else assigned_variation ) @@ -66,7 +66,7 @@ def get_boolean_assignment( subject_key, flag_key, subject_attributes, VariationType.BOOLEAN ) return ( - assigned_variation.typedValue + assigned_variation.typed_value if assigned_variation is not None else assigned_variation ) @@ -78,7 +78,7 @@ def get_parsed_json_assignment( subject_key, flag_key, subject_attributes, VariationType.JSON ) return ( - assigned_variation.typedValue + assigned_variation.typed_value if assigned_variation is not None else assigned_variation ) @@ -221,7 +221,7 @@ def _get_subject_variation_override( return VariationDto( name="override", value=experiment_config.overrides[subject_hash], - typedValue=experiment_config.typedOverrides[subject_hash], + typed_value=experiment_config.typed_overrides[subject_hash], shard_range=ShardRange(start=0, end=10000), ) return None diff --git a/eppo_client/config.py b/eppo_client/config.py index 404e2c3..404897b 100644 --- a/eppo_client/config.py +++ b/eppo_client/config.py @@ -11,7 +11,3 @@ class Config(SdkBaseModel): def _validate(self): validate_not_blank("api_key", self.api_key) - - class Config: - # needed for the AssignmentLogger class which is not of type SdkBaseModel - arbitrary_types_allowed = True diff --git a/eppo_client/configuration_requestor.py b/eppo_client/configuration_requestor.py index fdcdd61..a8fb2ee 100644 --- a/eppo_client/configuration_requestor.py +++ b/eppo_client/configuration_requestor.py @@ -4,7 +4,6 @@ from eppo_client.configuration_store import ConfigurationStore from eppo_client.http_client import HttpClient from eppo_client.rules import Rule - from eppo_client.shard import ShardRange logger = logging.getLogger(__name__) @@ -13,7 +12,7 @@ class VariationDto(SdkBaseModel): name: str value: str - typedValue: Any + typed_value: Any = None shard_range: ShardRange @@ -25,9 +24,9 @@ class AllocationDto(SdkBaseModel): class ExperimentConfigurationDto(SdkBaseModel): subject_shards: int enabled: bool - name: Optional[str] + name: Optional[str] = None overrides: Dict[str, str] = {} - typedOverrides: Dict[str, Any] = {} + typed_overrides: Dict[str, Any] = {} rules: List[Rule] = [] allocations: Dict[str, AllocationDto] diff --git a/eppo_client/rules.py b/eppo_client/rules.py index dd2ad95..f0e0d68 100644 --- a/eppo_client/rules.py +++ b/eppo_client/rules.py @@ -19,7 +19,7 @@ class OperatorType(Enum): class Condition(SdkBaseModel): operator: OperatorType attribute: str - value: Any + value: Any = None class Rule(SdkBaseModel): diff --git a/eppo_client/variation_type.py b/eppo_client/variation_type.py index be63693..0b7a013 100644 --- a/eppo_client/variation_type.py +++ b/eppo_client/variation_type.py @@ -14,18 +14,18 @@ def is_expected_type( cls, assigned_variation: VariationDto, expected_variation_type: str ) -> bool: if expected_variation_type == cls.STRING: - return isinstance(assigned_variation.typedValue, str) + return isinstance(assigned_variation.typed_value, str) elif expected_variation_type == cls.NUMERIC: - return isinstance(assigned_variation.typedValue, Number) and not isinstance( - assigned_variation.typedValue, bool - ) + return isinstance( + assigned_variation.typed_value, Number + ) and not isinstance(assigned_variation.typed_value, bool) elif expected_variation_type == cls.BOOLEAN: - return isinstance(assigned_variation.typedValue, bool) + return isinstance(assigned_variation.typed_value, bool) elif expected_variation_type == cls.JSON: try: parsed_json = json.loads(assigned_variation.value) - json.dumps(assigned_variation.typedValue) - return parsed_json == assigned_variation.typedValue + json.dumps(assigned_variation.typed_value) + return parsed_json == assigned_variation.typed_value except (json.JSONDecodeError, TypeError): pass return False diff --git a/requirements.txt b/requirements.txt index 3902a05..8371266 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ -pydantic==1.10.* +pydantic==2.4.* +pydantic-settings==2.0.* requests==2.31.* cachetools==5.3.* types-cachetools==5.3.* diff --git a/setup.cfg b/setup.cfg index d0c522d..eff59e3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,6 +19,7 @@ packages = eppo_client python_requires = >=3.6 include_package_data=True install_requires = - pydantic<2 + pydantic + pydantic-settings requests cachetools diff --git a/test/client_test.py b/test/client_test.py index dcddda9..11deb15 100644 --- a/test/client_test.py +++ b/test/client_test.py @@ -75,12 +75,12 @@ def test_assign_subject_not_in_sample(mock_config_requestor): VariationDto( name="control", value="control", - shardRange=ShardRange(start=0, end=10000), + shard_range=ShardRange(start=0, end=10000), ) ], ) mock_config_requestor.get_configuration.return_value = ExperimentConfigurationDto( - subjectShards=10000, + subject_shards=10000, enabled=True, name="recommendation_algo", overrides=dict(), @@ -101,14 +101,14 @@ def test_log_assignment(mock_config_requestor, mock_logger): VariationDto( name="control", value="control", - shardRange=ShardRange(start=0, end=10000), + shard_range=ShardRange(start=0, end=10000), ) ], ) mock_config_requestor.get_configuration.return_value = ExperimentConfigurationDto( allocations={"allocation": allocation}, rules=[Rule(conditions=[], allocation_key="allocation")], - subjectShards=10000, + subject_shards=10000, enabled=True, name="recommendation_algo", overrides=dict(), @@ -129,12 +129,12 @@ def test_get_assignment_handles_logging_exception(mock_config_requestor, mock_lo VariationDto( name="control", value="control", - shardRange=ShardRange(start=0, end=10000), + shard_range=ShardRange(start=0, end=10000), ) ], ) mock_config_requestor.get_configuration.return_value = ExperimentConfigurationDto( - subjectShards=10000, + subject_shards=10000, allocations={"allocation": allocation}, enabled=True, rules=[Rule(conditions=[], allocation_key="allocation")], @@ -145,6 +145,7 @@ def test_get_assignment_handles_logging_exception(mock_config_requestor, mock_lo client = EppoClient( config_requestor=mock_config_requestor, assignment_logger=mock_logger ) + assert client.get_assignment("user-1", "experiment-key-1") == "control" @@ -156,7 +157,7 @@ def test_assign_subject_with_with_attributes_and_rules(mock_config_requestor): VariationDto( name="control", value="control", - shardRange=ShardRange(start=0, end=10000), + shard_range=ShardRange(start=0, end=10000), ) ], ) @@ -165,7 +166,7 @@ def test_assign_subject_with_with_attributes_and_rules(mock_config_requestor): ) text_rule = Rule(conditions=[matches_email_condition], allocation_key="allocation") mock_config_requestor.get_configuration.return_value = ExperimentConfigurationDto( - subjectShards=10000, + subject_shards=10000, allocations={"allocation": allocation}, enabled=True, name="experiment-key-1", @@ -196,18 +197,18 @@ def test_with_subject_in_overrides(mock_config_requestor): VariationDto( name="control", value="control", - shardRange=ShardRange(start=0, end=10000), + shard_range=ShardRange(start=0, end=10000), ) ], ) mock_config_requestor.get_configuration.return_value = ExperimentConfigurationDto( - subjectShards=10000, + subject_shards=10000, allocations={"allocation": allocation}, enabled=True, rules=[Rule(conditions=[], allocation_key="allocation")], name="recommendation_algo", overrides={"d6d7705392bc7af633328bea8c4c6904": "override-variation"}, - typedOverrides={"d6d7705392bc7af633328bea8c4c6904": "override-variation"}, + typed_overrides={"d6d7705392bc7af633328bea8c4c6904": "override-variation"}, ) client = EppoClient( config_requestor=mock_config_requestor, assignment_logger=AssignmentLogger() @@ -223,18 +224,18 @@ def test_with_subject_in_overrides_exp_disabled(mock_config_requestor): VariationDto( name="control", value="control", - shardRange=ShardRange(start=0, end=10000), + shard_range=ShardRange(start=0, end=10000), ) ], ) mock_config_requestor.get_configuration.return_value = ExperimentConfigurationDto( - subjectShards=10000, + subject_shards=10000, allocations={"allocation": allocation}, enabled=False, rules=[Rule(conditions=[], allocation_key="allocation")], name="recommendation_algo", overrides={"d6d7705392bc7af633328bea8c4c6904": "override-variation"}, - typedOverrides={"d6d7705392bc7af633328bea8c4c6904": "override-variation"}, + typed_overrides={"d6d7705392bc7af633328bea8c4c6904": "override-variation"}, ) client = EppoClient( config_requestor=mock_config_requestor, assignment_logger=AssignmentLogger() @@ -266,6 +267,7 @@ def get_assignments(test_case): "boolean": client.get_boolean_assignment, "json": client.get_json_string_assignment, }[test_case["valueType"]] + return [ get_typed_assignment(subjectKey, test_case["experiment"]) for subjectKey in test_case.get("subjects", [])