diff --git a/eppo_client/bandit.py b/eppo_client/bandit.py index f353fb8..2d05e82 100644 --- a/eppo_client/bandit.py +++ b/eppo_client/bandit.py @@ -10,7 +10,7 @@ ) from eppo_client.rules import to_string from eppo_client.sharders import Sharder -from eppo_client.types import AttributesDict +from eppo_client.types import Attributes logger = logging.getLogger(__name__) @@ -21,7 +21,7 @@ class BanditEvaluationError(Exception): @dataclass -class Attributes: +class ContextAttributes: numeric_attributes: Dict[str, float] categorical_attributes: Dict[str, str] @@ -31,22 +31,23 @@ def empty(cls): Create an empty Attributes instance with no numeric or categorical attributes. Returns: - Attributes: An instance of the Attributes class with empty dictionaries + ContextAttributes: An instance of the ContextAttributes class with empty dictionaries for numeric and categorical attributes. """ return cls({}, {}) @classmethod - def from_dict(cls, attributes: AttributesDict): + def from_dict(cls, attributes: Attributes): """ - Create an Attributes instance from a dictionary of attributes. + Create an ContextAttributes instance from a dictionary of attributes. Args: attributes (Dict[str, Union[float, int, bool, str]]): A dictionary where keys are attribute names - and values are attribute values which can be of type float, int, bool, or str. + and values are attribute values which can be of type float, int, bool, or str. Returns: - Attributes: An instance of the Attributes class with numeric and categorical attributes separated. + ContextAttributes: An instance of the ContextAttributes class + with numeric and categorical attributes separated. """ numeric_attributes = { key: float(value) @@ -61,17 +62,17 @@ def from_dict(cls, attributes: AttributesDict): return cls(numeric_attributes, categorical_attributes) -ActionContexts = Dict[str, Attributes] -ActionContextsDict = Dict[str, AttributesDict] +ActionContexts = Dict[str, ContextAttributes] +ActionAttributes = Dict[str, Attributes] @dataclass class BanditEvaluation: flag_key: str subject_key: str - subject_attributes: Attributes + subject_attributes: ContextAttributes action_key: Optional[str] - action_attributes: Optional[Attributes] + action_attributes: Optional[ContextAttributes] action_score: float action_weight: float gamma: float @@ -88,7 +89,7 @@ def to_string(self) -> str: def null_evaluation( - flag_key: str, subject_key: str, subject_attributes: Attributes, gamma: float + flag_key: str, subject_key: str, subject_attributes: ContextAttributes, gamma: float ): return BanditEvaluation( flag_key, subject_key, subject_attributes, None, None, 0.0, 0.0, gamma, 0.0 @@ -104,7 +105,7 @@ def evaluate_bandit( self, flag_key: str, subject_key: str, - subject_attributes: Attributes, + subject_attributes: ContextAttributes, actions: ActionContexts, bandit_model: BanditModelData, ) -> BanditEvaluation: @@ -138,7 +139,7 @@ def evaluate_bandit( def score_actions( self, - subject_attributes: Attributes, + subject_attributes: ContextAttributes, actions: ActionContexts, bandit_model: BanditModelData, ) -> Dict[str, float]: @@ -209,8 +210,8 @@ def select_action(self, flag_key, subject_key, action_weights) -> str: def score_action( - subject_attributes: Attributes, - action_attributes: Attributes, + subject_attributes: ContextAttributes, + action_attributes: ContextAttributes, coefficients: BanditCoefficients, ) -> float: score = coefficients.intercept diff --git a/eppo_client/client.py b/eppo_client/client.py index d2318f8..640482a 100644 --- a/eppo_client/client.py +++ b/eppo_client/client.py @@ -4,10 +4,10 @@ from typing import Any, Dict, Optional, Union from eppo_client.assignment_logger import AssignmentLogger from eppo_client.bandit import ( - ActionContextsDict, + ActionAttributes, BanditEvaluator, BanditResult, - Attributes, + ContextAttributes, ActionContexts, ) from eppo_client.configuration_requestor import ( @@ -17,7 +17,7 @@ from eppo_client.models import VariationType from eppo_client.poller import Poller from eppo_client.sharders import MD5Sharder -from eppo_client.types import AttributesDict, ValueType +from eppo_client.types import Attributes, ValueType from eppo_client.validation import validate_not_blank from eppo_client.eval import FlagEvaluation, Evaluator, none_result from eppo_client.version import __version__ @@ -49,7 +49,7 @@ def get_string_assignment( self, flag_key: str, subject_key: str, - subject_attributes: AttributesDict, + subject_attributes: Attributes, default: str, ) -> str: return self.get_assignment_variation( @@ -64,7 +64,7 @@ def get_integer_assignment( self, flag_key: str, subject_key: str, - subject_attributes: AttributesDict, + subject_attributes: Attributes, default: int, ) -> int: return self.get_assignment_variation( @@ -79,7 +79,7 @@ def get_numeric_assignment( self, flag_key: str, subject_key: str, - subject_attributes: AttributesDict, + subject_attributes: Attributes, default: float, ) -> float: # convert to float in case we get an int @@ -97,7 +97,7 @@ def get_boolean_assignment( self, flag_key: str, subject_key: str, - subject_attributes: AttributesDict, + subject_attributes: Attributes, default: bool, ) -> bool: return self.get_assignment_variation( @@ -112,7 +112,7 @@ def get_json_assignment( self, flag_key: str, subject_key: str, - subject_attributes: AttributesDict, + subject_attributes: Attributes, default: Dict[Any, Any], ) -> Dict[Any, Any]: json_value = self.get_assignment_variation( @@ -131,7 +131,7 @@ def get_assignment_variation( self, flag_key: str, subject_key: str, - subject_attributes: AttributesDict, + subject_attributes: Attributes, default: Optional[ValueType], expected_variation_type: VariationType, ): @@ -155,7 +155,7 @@ def get_assignment_detail( self, flag_key: str, subject_key: str, - subject_attributes: AttributesDict, + subject_attributes: Attributes, expected_variation_type: VariationType, ) -> FlagEvaluation: """Maps a subject to a variation for a given flag @@ -231,8 +231,8 @@ def get_bandit_action( self, flag_key: str, subject_key: str, - subject_context: Union[Attributes, AttributesDict], - actions: Union[ActionContexts, ActionContextsDict], + subject_context: Union[ContextAttributes, Attributes], + actions: Union[ActionContexts, ActionAttributes], default: str, ) -> BanditResult: """ @@ -250,11 +250,11 @@ def get_bandit_action( Args: flag_key (str): The feature flag key that contains the bandit as one of the variations. subject_key (str): The key identifying the subject. - subject_context (Attributes | AttributesDict): The subject context. - If supplying an AttributesDict, it gets converted to an Attributes instance - actions (ActionContexts | ActionContextsDict): The dictionary that maps action keys + subject_context (ActionContexts | ActionAttributes): The subject context. + If supplying an ActionAttributes, it gets converted to an ActionContexts instance + actions (ActionContexts | ActionAttributes): The dictionary that maps action keys to their context of actions with their contexts. - If supplying an AttributesDict, it gets converted to an Attributes instance. + If supplying an ActionAttributes, it gets converted to an ActionContexts instance. default (str): The default variation to use if the subject is not part of the bandit. Returns: @@ -267,13 +267,16 @@ def get_bandit_action( result = client.get_bandit_action( "flag_key", "subject_key", - Attributes( + ContextAttributes( numeric_attributes={"age": 25}, categorical_attributes={"country": "USA"}), { - "action1": Attributes(numeric_attributes={"price": 10.0}, categorical_attributes={"category": "A"}), + "action1": ContextAttributes( + numeric_attributes={"price": 10.0}, + categorical_attributes={"category": "A"} + ), "action2": {"price": 10.0, "category": "B"} - "action3": Attributes.empty(), + "action3": ContextAttributes.empty(), }, "default" ) @@ -300,8 +303,8 @@ def get_bandit_action_detail( self, flag_key: str, subject_key: str, - subject_context: Union[Attributes, AttributesDict], - actions: Union[ActionContexts, ActionContextsDict], + subject_context: Union[ContextAttributes, Attributes], + actions: Union[ActionContexts, ActionAttributes], default: str, ) -> BanditResult: subject_attributes = convert_subject_context_to_attributes(subject_context) @@ -428,17 +431,17 @@ def check_value_type_match( def convert_subject_context_to_attributes( - subject_context: Union[Attributes, AttributesDict] -) -> Attributes: + subject_context: Union[ContextAttributes, Attributes] +) -> ContextAttributes: if isinstance(subject_context, dict): - return Attributes.from_dict(subject_context) + return ContextAttributes.from_dict(subject_context) return subject_context def convert_actions_to_action_contexts( - actions: Union[ActionContexts, ActionContextsDict] + actions: Union[ActionContexts, ActionAttributes] ) -> ActionContexts: return { - k: Attributes.from_dict(v) if isinstance(v, dict) else v + k: ContextAttributes.from_dict(v) if isinstance(v, dict) else v for k, v in actions.items() } diff --git a/eppo_client/eval.py b/eppo_client/eval.py index e769b17..89ddd77 100644 --- a/eppo_client/eval.py +++ b/eppo_client/eval.py @@ -5,7 +5,7 @@ from dataclasses import dataclass import datetime -from eppo_client.types import AttributesDict +from eppo_client.types import Attributes @dataclass @@ -13,7 +13,7 @@ class FlagEvaluation: flag_key: str variation_type: VariationType subject_key: str - subject_attributes: AttributesDict + subject_attributes: Attributes allocation_key: Optional[str] variation: Optional[Variation] extra_logging: Dict[str, str] @@ -28,7 +28,7 @@ def evaluate_flag( self, flag: Flag, subject_key: str, - subject_attributes: AttributesDict, + subject_attributes: Attributes, ) -> FlagEvaluation: if not flag.enabled: return none_result( @@ -93,7 +93,7 @@ def none_result( flag_key: str, variation_type: VariationType, subject_key: str, - subject_attributes: AttributesDict, + subject_attributes: Attributes, ) -> FlagEvaluation: return FlagEvaluation( flag_key=flag_key, diff --git a/eppo_client/rules.py b/eppo_client/rules.py index fc181bf..85f8feb 100644 --- a/eppo_client/rules.py +++ b/eppo_client/rules.py @@ -7,7 +7,7 @@ import semver from eppo_client.models import SdkBaseModel -from eppo_client.types import AttributeType, ConditionValueType, AttributesDict +from eppo_client.types import AttributeType, ConditionValueType, Attributes class OperatorType(Enum): @@ -32,16 +32,14 @@ class Rule(SdkBaseModel): conditions: List[Condition] -def matches_rule(rule: Rule, subject_attributes: AttributesDict) -> bool: +def matches_rule(rule: Rule, subject_attributes: Attributes) -> bool: return all( evaluate_condition(condition, subject_attributes) for condition in rule.conditions ) -def evaluate_condition( - condition: Condition, subject_attributes: AttributesDict -) -> bool: +def evaluate_condition(condition: Condition, subject_attributes: Attributes) -> bool: subject_value = subject_attributes.get(condition.attribute, None) if condition.operator == OperatorType.IS_NULL: if condition.value: diff --git a/eppo_client/types.py b/eppo_client/types.py index 1bd4a10..f159183 100644 --- a/eppo_client/types.py +++ b/eppo_client/types.py @@ -3,5 +3,5 @@ ValueType = Union[str, int, float, bool] AttributeType = Union[str, int, float, bool, None] ConditionValueType = Union[AttributeType, List[AttributeType]] -AttributesDict = Dict[str, AttributeType] +Attributes = Dict[str, AttributeType] Action = str diff --git a/eppo_client/version.py b/eppo_client/version.py index 1173108..1da6a55 100644 --- a/eppo_client/version.py +++ b/eppo_client/version.py @@ -1 +1 @@ -__version__ = "3.2.0" +__version__ = "3.2.1" diff --git a/example/03_bandit.py b/example/03_bandit.py index eff8a07..1527014 100644 --- a/example/03_bandit.py +++ b/example/03_bandit.py @@ -43,15 +43,15 @@ async def bandit(name: str, country: str, age: int): bandit_result = client.get_bandit_action( "shoe-bandit", name, - eppo_client.bandit.Attributes( + eppo_client.bandit.ContextAttributes( numeric_attributes={"age": age}, categorical_attributes={"country": country} ), { - "nike": eppo_client.bandit.Attributes( + "nike": eppo_client.bandit.ContextAttributes( numeric_attributes={"brand_affinity": 2.3}, categorical_attributes={"aspect_ratio": "16:9"}, ), - "adidas": eppo_client.bandit.Attributes( + "adidas": eppo_client.bandit.ContextAttributes( numeric_attributes={"brand_affinity": 0.2}, categorical_attributes={"aspect_ratio": "16:9"}, ), diff --git a/test/bandit_test.py b/test/bandit_test.py index d63c76e..32f6641 100644 --- a/test/bandit_test.py +++ b/test/bandit_test.py @@ -3,7 +3,7 @@ from eppo_client.sharders import MD5Sharder, DeterministicSharder from eppo_client.bandit import ( - Attributes, + ContextAttributes, score_numeric_attributes, score_categorical_attributes, BanditEvaluator, @@ -222,15 +222,15 @@ def test_evaluate_bandit(): # Mock data flag_key = "test_flag" subject_key = "test_subject" - subject_attributes = Attributes( + subject_attributes = ContextAttributes( numeric_attributes={"age": 25.0}, categorical_attributes={"location": "US"} ) action_contexts = { - "action1": Attributes( + "action1": ContextAttributes( numeric_attributes={"price": 10.0}, categorical_attributes={"category": "A"}, ), - "action2": Attributes( + "action2": ContextAttributes( numeric_attributes={"price": 20.0}, categorical_attributes={"category": "B"}, ), @@ -325,7 +325,7 @@ def test_bandit_no_action_contexts(): # Mock data flag_key = "test_flag" subject_key = "test_subject" - subject_attributes = Attributes( + subject_attributes = ContextAttributes( numeric_attributes={"age": 25.0}, categorical_attributes={"location": "US"} ) coefficients = { @@ -378,7 +378,7 @@ def test_bandit_no_action_contexts(): flag_key, subject_key, subject_attributes, - {"action1": Attributes.empty(), "action2": Attributes.empty()}, + {"action1": ContextAttributes.empty(), "action2": ContextAttributes.empty()}, bandit_model, ) diff --git a/test/client_bandit_test.py b/test/client_bandit_test.py index d8a1e6a..a3176ae 100644 --- a/test/client_bandit_test.py +++ b/test/client_bandit_test.py @@ -6,7 +6,7 @@ import os from time import sleep from typing import Dict, List -from eppo_client.bandit import BanditResult, Attributes +from eppo_client.bandit import BanditResult, ContextAttributes import httpretty # type: ignore import pytest @@ -28,7 +28,7 @@ MOCK_BASE_URL = "http://localhost:4001/api" -DEFAULT_SUBJECT_ATTRIBUTES = Attributes( +DEFAULT_SUBJECT_ATTRIBUTES = ContextAttributes( numeric_attributes={"age": 30}, categorical_attributes={"country": "UK"} ) @@ -110,11 +110,11 @@ def test_get_bandit_action_with_subject_attributes(): # tests that allocation filtering based on subject attributes works correctly client = get_instance() actions = { - "adidas": Attributes( + "adidas": ContextAttributes( numeric_attributes={"discount": 0.1}, categorical_attributes={"from": "germany"}, ), - "nike": Attributes( + "nike": ContextAttributes( numeric_attributes={"discount": 0.2}, categorical_attributes={"from": "usa"} ), } @@ -174,14 +174,14 @@ def test_bandit_generic_test_cases(test_case): result = client.get_bandit_action( flag, subject["subjectKey"], - Attributes( + ContextAttributes( numeric_attributes=subject["subjectAttributes"]["numeric_attributes"], categorical_attributes=subject["subjectAttributes"][ "categorical_attributes" ], ), { - action["actionKey"]: Attributes( + action["actionKey"]: ContextAttributes( action["numericAttributes"], action["categoricalAttributes"] ) for action in subject["actions"]