Skip to content
This repository was archived by the owner on Nov 8, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion eppo_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
6 changes: 5 additions & 1 deletion eppo_client/assignment_logger.py
Original file line number Diff line number Diff line change
@@ -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)
Comment on lines +6 to +7
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AssignmentLogger does not inherit from SdkBaseModel because it has unique serialization requirements from the json dto objects


def log_assignment(self, assignment_event: Dict):
pass
14 changes: 3 additions & 11 deletions eppo_client/base_model.py
Original file line number Diff line number Diff line change
@@ -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]
Comment on lines -2 to -8
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

glad they have an official version publish! our to_camel had a bug in it preventing test cases from passing

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)
Comment on lines 5 to +6
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SdkBaseModel is the parent class of all json dto classes

10 changes: 5 additions & 5 deletions eppo_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand All @@ -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
)
Expand All @@ -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
)
Expand All @@ -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
)
Expand Down Expand Up @@ -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),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

python classes now have snake case, as they're supposed to

)
return None
Expand Down
4 changes: 0 additions & 4 deletions eppo_client/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 3 additions & 4 deletions eppo_client/configuration_requestor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand All @@ -13,7 +12,7 @@
class VariationDto(SdkBaseModel):
name: str
value: str
typedValue: Any
typed_value: Any = None
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pydantic required this

shard_range: ShardRange


Expand All @@ -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]

Expand Down
2 changes: 1 addition & 1 deletion eppo_client/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class OperatorType(Enum):
class Condition(SdkBaseModel):
operator: OperatorType
attribute: str
value: Any
value: Any = None


class Rule(SdkBaseModel):
Expand Down
14 changes: 7 additions & 7 deletions eppo_client/variation_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pydantic==1.10.*
pydantic==2.4.*
pydantic-settings==2.0.*
requests==2.31.*
cachetools==5.3.*
types-cachetools==5.3.*
Expand Down
3 changes: 2 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ packages = eppo_client
python_requires = >=3.6
include_package_data=True
install_requires =
pydantic<2
pydantic
pydantic-settings
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is there a setup.cfg and requirements.txt file?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't know what the intention was here, but I believe the convention is about abstract vs concrete dependencies

https://martin-thoma.com/python-requirements/#abstract-dependencies

requests
cachetools
30 changes: 16 additions & 14 deletions test/client_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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(),
Expand All @@ -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")],
Expand All @@ -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"


Expand All @@ -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),
)
],
)
Expand All @@ -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",
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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", [])
Expand Down