From bebce942f9f6cbb603adc483b6b6185d22d61ff7 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 21 Sep 2023 00:47:22 -0700 Subject: [PATCH 1/6] upgrade to pydantic 2.3 (FF-957) --- eppo_client/assignment_logger.py | 6 +++++- eppo_client/base_model.py | 6 ++---- eppo_client/client.py | 10 ++++----- eppo_client/config.py | 5 +---- eppo_client/configuration_requestor.py | 7 +++---- eppo_client/rules.py | 2 +- eppo_client/variation_type.py | 12 +++++------ requirements.txt | 2 +- test/client_test.py | 29 +++++++++++++------------- 9 files changed, 38 insertions(+), 41 deletions(-) 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..275043f 100644 --- a/eppo_client/base_model.py +++ b/eppo_client/base_model.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel +from pydantic import ConfigDict, BaseModel def to_camel(s: str): @@ -9,6 +9,4 @@ def to_camel(s: str): 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..b248bd3 100644 --- a/eppo_client/config.py +++ b/eppo_client/config.py @@ -2,6 +2,7 @@ from eppo_client.base_model import SdkBaseModel from eppo_client.validation import validate_not_blank +from pydantic import ConfigDict class Config(SdkBaseModel): @@ -11,7 +12,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..2fcbf26 100644 --- a/eppo_client/variation_type.py +++ b/eppo_client/variation_type.py @@ -14,18 +14,16 @@ 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) 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..6a0fc58 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -pydantic==1.10.* +pydantic==2.3.* requests==2.31.* cachetools==5.3.* types-cachetools==5.3.* diff --git a/test/client_test.py b/test/client_test.py index dcddda9..0f1dbae 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() From dca179b54a9a145caec21a1b5b1578211252997c Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Wed, 4 Oct 2023 20:20:15 -0700 Subject: [PATCH 2/6] remove unused import --- eppo_client/config.py | 1 - test/client_test.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/eppo_client/config.py b/eppo_client/config.py index b248bd3..404897b 100644 --- a/eppo_client/config.py +++ b/eppo_client/config.py @@ -2,7 +2,6 @@ from eppo_client.base_model import SdkBaseModel from eppo_client.validation import validate_not_blank -from pydantic import ConfigDict class Config(SdkBaseModel): diff --git a/test/client_test.py b/test/client_test.py index 0f1dbae..11deb15 100644 --- a/test/client_test.py +++ b/test/client_test.py @@ -267,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", []) From a1b235554fbc4763006853def5053469832b1e3e Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Wed, 4 Oct 2023 20:39:10 -0700 Subject: [PATCH 3/6] fix revert --- eppo_client/variation_type.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/eppo_client/variation_type.py b/eppo_client/variation_type.py index 2fcbf26..0b7a013 100644 --- a/eppo_client/variation_type.py +++ b/eppo_client/variation_type.py @@ -16,7 +16,9 @@ def is_expected_type( if expected_variation_type == cls.STRING: return isinstance(assigned_variation.typed_value, str) elif expected_variation_type == cls.NUMERIC: - return isinstance(assigned_variation.typed_value, Number) + 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.typed_value, bool) elif expected_variation_type == cls.JSON: From 767e1f523502d2b69df71cd0460fa780edabf468 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Wed, 4 Oct 2023 21:04:25 -0700 Subject: [PATCH 4/6] replace to_camel custom code with pydantic alias generator --- eppo_client/base_model.py | 8 +------- requirements.txt | 3 ++- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/eppo_client/base_model.py b/eppo_client/base_model.py index 275043f..efbd618 100644 --- a/eppo_client/base_model.py +++ b/eppo_client/base_model.py @@ -1,11 +1,5 @@ from pydantic import ConfigDict, 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.alias_generators import to_camel class SdkBaseModel(BaseModel): diff --git a/requirements.txt b/requirements.txt index 6a0fc58..8371266 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ -pydantic==2.3.* +pydantic==2.4.* +pydantic-settings==2.0.* requests==2.31.* cachetools==5.3.* types-cachetools==5.3.* From fe3ca6851963189379b67f99286ae5d483b824c5 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Wed, 4 Oct 2023 21:10:12 -0700 Subject: [PATCH 5/6] setup.cfg? --- setup.cfg | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From 4ee89e1d303fbebebbc0a887adf5c38fa0aa1ceb Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 5 Oct 2023 09:42:24 -0700 Subject: [PATCH 6/6] version 1.3.0 --- eppo_client/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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()