From b1313d1b63c4240000d32989563c958706a61a39 Mon Sep 17 00:00:00 2001 From: Lisa Date: Sat, 6 Jul 2024 08:50:13 -0400 Subject: [PATCH 01/12] Return the default value for a bandit flag if no actions. Cast provided categorical attributes to strings. --- eppo_client/client.py | 20 ++++++++++++++++---- eppo_client/version.py | 2 +- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/eppo_client/client.py b/eppo_client/client.py index c1cde48..464a0c0 100644 --- a/eppo_client/client.py +++ b/eppo_client/client.py @@ -313,7 +313,7 @@ def get_bandit_action_detail( actions: Union[ActionContexts, ActionAttributes], default: str, ) -> BanditResult: - subject_attributes = convert_subject_context_to_attributes(subject_context) + subject_attributes = convert_subject_context_to_context_attributes(subject_context) action_contexts = convert_actions_to_action_contexts(actions) # get experiment assignment @@ -339,6 +339,10 @@ def get_bandit_action_detail( ) return BanditResult(variation, None) + # if no actions are given - a valid use case - return the default value + if len(actions) == 0: + return BanditResult(default, None) + evaluation = self.__bandit_evaluator.evaluate_bandit( flag_key, subject_key, @@ -443,18 +447,26 @@ def check_value_type_match( return False -def convert_subject_context_to_attributes( +def convert_subject_context_to_context_attributes( subject_context: Union[ContextAttributes, Attributes] ) -> ContextAttributes: if isinstance(subject_context, dict): return ContextAttributes.from_dict(subject_context) - return subject_context + + stringified_categorical_attributes = { + key: str(value) for key, value in subject_context.categorical_attributes.items() + } + + return ContextAttributes( + numeric_attributes=subject_context.numeric_attributes, + categorical_attributes=stringified_categorical_attributes + ) def convert_actions_to_action_contexts( actions: Union[ActionContexts, ActionAttributes] ) -> ActionContexts: return { - k: ContextAttributes.from_dict(v) if isinstance(v, dict) else v + k: convert_subject_context_to_context_attributes(v) for k, v in actions.items() } diff --git a/eppo_client/version.py b/eppo_client/version.py index 91b5628..99950aa 100644 --- a/eppo_client/version.py +++ b/eppo_client/version.py @@ -1,4 +1,4 @@ # Note to developers: When ready to bump to 4.0, please change # the `POLL_INTERVAL_SECONDS` constant in `eppo_client/constants.py` # to 30 seconds to match the behavior of the other server SDKs. -__version__ = "3.5.0" +__version__ = "3.5.1" From 4441e5083d410937b9c474166da4a458b3a240bb Mon Sep 17 00:00:00 2001 From: Lisa Date: Mon, 8 Jul 2024 09:27:03 -0400 Subject: [PATCH 02/12] Formatting --- eppo_client/client.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/eppo_client/client.py b/eppo_client/client.py index 464a0c0..5b7541f 100644 --- a/eppo_client/client.py +++ b/eppo_client/client.py @@ -313,7 +313,9 @@ def get_bandit_action_detail( actions: Union[ActionContexts, ActionAttributes], default: str, ) -> BanditResult: - subject_attributes = convert_subject_context_to_context_attributes(subject_context) + subject_attributes = convert_subject_context_to_context_attributes( + subject_context + ) action_contexts = convert_actions_to_action_contexts(actions) # get experiment assignment @@ -459,7 +461,7 @@ def convert_subject_context_to_context_attributes( return ContextAttributes( numeric_attributes=subject_context.numeric_attributes, - categorical_attributes=stringified_categorical_attributes + categorical_attributes=stringified_categorical_attributes, ) @@ -467,6 +469,5 @@ def convert_actions_to_action_contexts( actions: Union[ActionContexts, ActionAttributes] ) -> ActionContexts: return { - k: convert_subject_context_to_context_attributes(v) - for k, v in actions.items() + k: convert_subject_context_to_context_attributes(v) for k, v in actions.items() } From e13c9b389e2d9cd31eeeaef80911a1e8d48c53e8 Mon Sep 17 00:00:00 2001 From: Aaron Silverman Date: Thu, 18 Jul 2024 16:58:44 -0600 Subject: [PATCH 03/12] update for revisted zero-action strategy --- eppo_client/client.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/eppo_client/client.py b/eppo_client/client.py index 5b7541f..50c3dd0 100644 --- a/eppo_client/client.py +++ b/eppo_client/client.py @@ -261,13 +261,15 @@ def get_bandit_action( actions (ActionContexts | ActionAttributes): The dictionary that maps action keys to their context of actions with their contexts. 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. + default (str): The default variation to use if an error is encountered retrieving the + assigned variation. Returns: BanditResult: The result containing either the bandit action if the subject is part of the bandit, or the assignment if they are not. The BanditResult includes: - variation (str): The assignment key indicating the subject's variation. - - action (str): The key of the selected action if the subject is part of the bandit. + - action (str | null): The key of the selected action if the subject was assigned one + by the bandit. Example: result = client.get_bandit_action( @@ -286,10 +288,10 @@ def get_bandit_action( }, "default" ) - if result.action is None: - do_variation(result.variation) + if result.action: + do_action(result.variation) else: - do_action(result.action) + do_status_quo() """ try: return self.get_bandit_action_detail( @@ -341,9 +343,9 @@ def get_bandit_action_detail( ) return BanditResult(variation, None) - # if no actions are given - a valid use case - return the default value + # if no actions are given - a valid use case - return the variation with no action if len(actions) == 0: - return BanditResult(default, None) + return BanditResult(variation, None) evaluation = self.__bandit_evaluator.evaluate_bandit( flag_key, From f0b9fd31c725223badf6b771a57bfe5d51b63707 Mon Sep 17 00:00:00 2001 From: Aaron Silverman Date: Thu, 18 Jul 2024 18:17:23 -0600 Subject: [PATCH 04/12] return variation if that has been computed --- eppo_client/client.py | 151 ++++++++++++++++++++----------------- test/client_bandit_test.py | 53 +++++++++++-- 2 files changed, 130 insertions(+), 74 deletions(-) diff --git a/eppo_client/client.py b/eppo_client/client.py index 50c3dd0..bb6b04b 100644 --- a/eppo_client/client.py +++ b/eppo_client/client.py @@ -293,103 +293,107 @@ def get_bandit_action( else: do_status_quo() """ + variation = default + action = None try: - return self.get_bandit_action_detail( + subject_attributes = convert_context_attributes_to_attributes(subject_context) + + # get experiment assignment + # ignoring type because Dict[str, str] satisfies Dict[str, str | ...] but mypy does not understand + variation = self.get_string_assignment( flag_key, subject_key, - subject_context, - actions, + subject_attributes, default, ) + + if variation in self.get_bandit_keys(): + action = self.evaluate_bandit_action( + flag_key, + variation, # for now, we assume the variation value is always equal to the bandit key + subject_key, + subject_context, + actions, + ) except Exception as e: if self.__is_graceful_mode: logger.error("[Eppo SDK] Error getting bandit action: " + str(e)) - return BanditResult(default, None) - raise e + else: + raise e + + return BanditResult(variation, action) - def get_bandit_action_detail( + def evaluate_bandit_action( self, flag_key: str, + bandit_key: str, subject_key: str, subject_context: Union[ContextAttributes, Attributes], actions: Union[ActionContexts, ActionAttributes], - default: str, - ) -> BanditResult: - subject_attributes = convert_subject_context_to_context_attributes( - subject_context - ) - action_contexts = convert_actions_to_action_contexts(actions) - - # get experiment assignment - # ignoring type because Dict[str, str] satisfies Dict[str, str | ...] but mypy does not understand - variation = self.get_string_assignment( - flag_key, - subject_key, - subject_attributes.categorical_attributes - | subject_attributes.numeric_attributes, # type: ignore - default, - ) - - # if the variation is not the bandit key, then the subject is not allocated in the bandit - if variation not in self.get_bandit_keys(): - return BanditResult(variation, None) - - # for now, assume that the variation is equal to the bandit key - bandit_data = self.__config_requestor.get_bandit_model(variation) + ) -> str | None: + # if no actions are given--a valid use case--return the variation with no action + if len(actions) == 0: + return None + + bandit_data = self.__config_requestor.get_bandit_model(bandit_key) if not bandit_data: logger.warning( f"[Eppo SDK] No assigned action. Bandit not found for flag: {flag_key}" ) - return BanditResult(variation, None) - - # if no actions are given - a valid use case - return the variation with no action - if len(actions) == 0: - return BanditResult(variation, None) + return None + + subject_context_attributes = convert_subject_context_to_context_attributes( + subject_context + ) + action_contexts = convert_actions_to_action_contexts(actions) evaluation = self.__bandit_evaluator.evaluate_bandit( flag_key, subject_key, - subject_attributes, + subject_context_attributes, action_contexts, bandit_data.bandit_model_data, ) # log bandit action - bandit_event = { - "flagKey": flag_key, - "banditKey": bandit_data.bandit_key, - "subject": subject_key, - "action": evaluation.action_key if evaluation else None, - "actionProbability": evaluation.action_weight if evaluation else None, - "optimalityGap": evaluation.optimality_gap if evaluation else None, - "modelVersion": bandit_data.bandit_model_version if evaluation else None, - "timestamp": datetime.datetime.utcnow().isoformat(), - "subjectNumericAttributes": ( - subject_attributes.numeric_attributes - if evaluation.subject_attributes - else {} - ), - "subjectCategoricalAttributes": ( - subject_attributes.categorical_attributes - if evaluation.subject_attributes - else {} - ), - "actionNumericAttributes": ( - evaluation.action_attributes.numeric_attributes - if evaluation.action_attributes - else {} - ), - "actionCategoricalAttributes": ( - evaluation.action_attributes.categorical_attributes - if evaluation.action_attributes - else {} - ), - "metaData": {"sdkLanguage": "python", "sdkVersion": __version__}, - } - self.__assignment_logger.log_bandit_action(bandit_event) + try: + bandit_event = { + "flagKey": flag_key, + "banditKey": bandit_data.bandit_key, + "subject": subject_key, + "action": evaluation.action_key if evaluation else None, + "actionProbability": evaluation.action_weight if evaluation else None, + "optimalityGap": evaluation.optimality_gap if evaluation else None, + "modelVersion": bandit_data.bandit_model_version if evaluation else None, + "timestamp": datetime.datetime.utcnow().isoformat(), + "subjectNumericAttributes": ( + subject_context_attributes.numeric_attributes + if evaluation.subject_attributes + else {} + ), + "subjectCategoricalAttributes": ( + subject_context_attributes.categorical_attributes + if evaluation.subject_attributes + else {} + ), + "actionNumericAttributes": ( + evaluation.action_attributes.numeric_attributes + if evaluation.action_attributes + else {} + ), + "actionCategoricalAttributes": ( + evaluation.action_attributes.categorical_attributes + if evaluation.action_attributes + else {} + ), + "metaData": {"sdkLanguage": "python", "sdkVersion": __version__}, + } + self.__assignment_logger.log_bandit_action(bandit_event) + except Exception as e: + logger.warn("[Eppo SDK] Error logging bandit event: " + str(e)) - return BanditResult(variation, evaluation.action_key if evaluation else None) + return evaluation.action_key def get_flag_keys(self): """ @@ -413,6 +417,9 @@ def get_bandit_keys(self): This can be useful to debug the initialization process. """ return self.__config_requestor.get_bandit_keys() + + def set_is_graceful_mode(self, is_graceful_mode: bool): + self.__is_graceful_mode = is_graceful_mode def is_initialized(self): """ @@ -450,6 +457,14 @@ def check_value_type_match( return isinstance(value, bool) return False +def convert_context_attributes_to_attributes( + subject_context: Union[ContextAttributes, Attributes] +) -> Attributes: + if isinstance(subject_context, dict): + return subject_context + + return subject_context.numeric_attributes | subject_context.categorical_attributes + def convert_subject_context_to_context_attributes( subject_context: Union[ContextAttributes, Attributes] diff --git a/test/client_bandit_test.py b/test/client_bandit_test.py index 64cb7c6..f5b981b 100644 --- a/test/client_bandit_test.py +++ b/test/client_bandit_test.py @@ -6,7 +6,8 @@ import os from time import sleep from typing import Dict, List -from eppo_client.bandit import BanditResult, ContextAttributes +from unittest.mock import patch +from eppo_client.bandit import BanditEvaluator, BanditResult, ContextAttributes import httpretty # type: ignore import pytest @@ -80,16 +81,23 @@ def init_fixture(): httpretty.disable() httpretty.reset() +@pytest.fixture(autouse=True) +def clear_event_arrays(): + # Reset graceful mode to off + get_instance().set_is_graceful_mode(False) + # Clear captured logger events + mock_assignment_logger.assignment_events.clear() + mock_assignment_logger.bandit_events.clear() def test_is_initialized(): client = get_instance() assert client.is_initialized(), "Client should be initialized" -def test_get_bandit_action_bandit_does_not_exist(): +def test_get_bandit_action_flag_not_exist(): client = get_instance() result = client.get_bandit_action( - "nonexistent_bandit", + "nonexistent_flag", "subject_key", DEFAULT_SUBJECT_ATTRIBUTES, {}, @@ -98,12 +106,45 @@ def test_get_bandit_action_bandit_does_not_exist(): assert result == BanditResult("default_variation", None) -def test_get_bandit_action_flag_without_bandit(): +def test_get_bandit_action_flag_has_no_bandit(): client = get_instance() result = client.get_bandit_action( - "a_flag", "subject_key", DEFAULT_SUBJECT_ATTRIBUTES, {}, "default_variation" + "non_bandit_flag", "subject_key", DEFAULT_SUBJECT_ATTRIBUTES, {}, "default_variation" ) - assert result == BanditResult("default_variation", None) + assert result == BanditResult("control", None) + +@patch.object(BanditEvaluator, 'evaluate_bandit', side_effect=Exception("Mocked Exception")) +def test_get_bandit_action_bandit_error(mock_bandit_evaluator): + client = get_instance() + client.set_is_graceful_mode(True) + actions = { + "adidas": ContextAttributes( + numeric_attributes={"discount": 0.1}, + categorical_attributes={"from": "germany"}, + ), + "nike": ContextAttributes( + numeric_attributes={"discount": 0.2}, categorical_attributes={"from": "usa"} + ), + } + + result = client.get_bandit_action( + "banner_bandit_flag_uk_only", + "alice", + DEFAULT_SUBJECT_ATTRIBUTES, + actions, + "default_variation", + ) + assert result.variation == "banner_bandit" + assert result.action is None + + # testing assignment logger + assignment_log_statement = mock_assignment_logger.assignment_events[-1] + assert assignment_log_statement["featureFlag"] == "banner_bandit_flag_uk_only" + assert assignment_log_statement["variation"] == "banner_bandit" + assert assignment_log_statement["subject"] == "alice" + + # testing bandit logger + assert len(mock_assignment_logger.bandit_events) == 0 def test_get_bandit_action_with_subject_attributes(): From 13e466a299f8c3b463c235d79d6f094bf9c5b6a0 Mon Sep 17 00:00:00 2001 From: Aaron Silverman Date: Thu, 18 Jul 2024 18:26:48 -0600 Subject: [PATCH 05/12] changes from self-review of PR --- eppo_client/client.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/eppo_client/client.py b/eppo_client/client.py index bb6b04b..3e0cbdb 100644 --- a/eppo_client/client.py +++ b/eppo_client/client.py @@ -298,8 +298,7 @@ def get_bandit_action( try: subject_attributes = convert_context_attributes_to_attributes(subject_context) - # get experiment assignment - # ignoring type because Dict[str, str] satisfies Dict[str, str | ...] but mypy does not understand + # first, get experiment assignment variation = self.get_string_assignment( flag_key, subject_key, @@ -308,6 +307,7 @@ def get_bandit_action( ) if variation in self.get_bandit_keys(): + # next, if assigned a bandit, get the selected action action = self.evaluate_bandit_action( flag_key, variation, # for now, we assume the variation value is always equal to the bandit key @@ -343,7 +343,7 @@ def evaluate_bandit_action( ) return None - subject_context_attributes = convert_subject_context_to_context_attributes( + subject_context_attributes = convert_attributes_to_context_attributes( subject_context ) action_contexts = convert_actions_to_action_contexts(actions) @@ -466,7 +466,7 @@ def convert_context_attributes_to_attributes( return subject_context.numeric_attributes | subject_context.categorical_attributes -def convert_subject_context_to_context_attributes( +def convert_attributes_to_context_attributes( subject_context: Union[ContextAttributes, Attributes] ) -> ContextAttributes: if isinstance(subject_context, dict): @@ -486,5 +486,5 @@ def convert_actions_to_action_contexts( actions: Union[ActionContexts, ActionAttributes] ) -> ActionContexts: return { - k: convert_subject_context_to_context_attributes(v) for k, v in actions.items() + k: convert_attributes_to_context_attributes(v) for k, v in actions.items() } From 19e5a28d8118a18a5aee60dfbab177f97c2e3db4 Mon Sep 17 00:00:00 2001 From: Aaron Silverman Date: Thu, 18 Jul 2024 18:33:01 -0600 Subject: [PATCH 06/12] test for bandit logger error --- test/client_bandit_test.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/client_bandit_test.py b/test/client_bandit_test.py index f5b981b..d307f79 100644 --- a/test/client_bandit_test.py +++ b/test/client_bandit_test.py @@ -203,6 +203,32 @@ def test_get_bandit_action_with_subject_attributes(): == chosen_action.categorical_attributes ) +@patch.object(MockAssignmentLogger, 'log_bandit_action', side_effect=Exception("Mocked Exception")) +def test_get_bandit_action_bandit_logger_error(patched_mock_assignment_logger): + client = get_instance() + actions = { + "adidas": ContextAttributes( + numeric_attributes={"discount": 0.1}, + categorical_attributes={"from": "germany"}, + ), + "nike": ContextAttributes( + numeric_attributes={"discount": 0.2}, categorical_attributes={"from": "usa"} + ), + } + result = client.get_bandit_action( + "banner_bandit_flag_uk_only", + "alice", + DEFAULT_SUBJECT_ATTRIBUTES, + actions, + "default_variation", + ) + assert result.variation == "banner_bandit" + assert result.action in ["adidas", "nike"] + + # assignment should have still been logged + assert len(mock_assignment_logger.assignment_events) == 1 + assert len(mock_assignment_logger.bandit_events) == 0 + @pytest.mark.parametrize("test_case", test_data) def test_bandit_generic_test_cases(test_case): From 4b0ef384aeb92b3d396c0cec7a512484f6d7d4d6 Mon Sep 17 00:00:00 2001 From: Aaron Silverman Date: Fri, 19 Jul 2024 17:27:10 -0600 Subject: [PATCH 07/12] fix flake8 lintint errors --- eppo_client/client.py | 11 ++++++----- test/client_bandit_test.py | 8 ++++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/eppo_client/client.py b/eppo_client/client.py index 3e0cbdb..b1ccd3b 100644 --- a/eppo_client/client.py +++ b/eppo_client/client.py @@ -310,7 +310,7 @@ def get_bandit_action( # next, if assigned a bandit, get the selected action action = self.evaluate_bandit_action( flag_key, - variation, # for now, we assume the variation value is always equal to the bandit key + variation, # for now, we assume the variation value is always equal to the bandit key subject_key, subject_context, actions, @@ -334,7 +334,7 @@ def evaluate_bandit_action( # if no actions are given--a valid use case--return the variation with no action if len(actions) == 0: return None - + bandit_data = self.__config_requestor.get_bandit_model(bandit_key) if not bandit_data: @@ -342,7 +342,7 @@ def evaluate_bandit_action( f"[Eppo SDK] No assigned action. Bandit not found for flag: {flag_key}" ) return None - + subject_context_attributes = convert_attributes_to_context_attributes( subject_context ) @@ -417,7 +417,7 @@ def get_bandit_keys(self): This can be useful to debug the initialization process. """ return self.__config_requestor.get_bandit_keys() - + def set_is_graceful_mode(self, is_graceful_mode: bool): self.__is_graceful_mode = is_graceful_mode @@ -457,12 +457,13 @@ def check_value_type_match( return isinstance(value, bool) return False + def convert_context_attributes_to_attributes( subject_context: Union[ContextAttributes, Attributes] ) -> Attributes: if isinstance(subject_context, dict): return subject_context - + return subject_context.numeric_attributes | subject_context.categorical_attributes diff --git a/test/client_bandit_test.py b/test/client_bandit_test.py index d307f79..90f32fe 100644 --- a/test/client_bandit_test.py +++ b/test/client_bandit_test.py @@ -81,14 +81,16 @@ def init_fixture(): httpretty.disable() httpretty.reset() + @pytest.fixture(autouse=True) def clear_event_arrays(): # Reset graceful mode to off get_instance().set_is_graceful_mode(False) - # Clear captured logger events + # Clear captured logger events mock_assignment_logger.assignment_events.clear() mock_assignment_logger.bandit_events.clear() + def test_is_initialized(): client = get_instance() assert client.is_initialized(), "Client should be initialized" @@ -113,6 +115,7 @@ def test_get_bandit_action_flag_has_no_bandit(): ) assert result == BanditResult("control", None) + @patch.object(BanditEvaluator, 'evaluate_bandit', side_effect=Exception("Mocked Exception")) def test_get_bandit_action_bandit_error(mock_bandit_evaluator): client = get_instance() @@ -203,6 +206,7 @@ def test_get_bandit_action_with_subject_attributes(): == chosen_action.categorical_attributes ) + @patch.object(MockAssignmentLogger, 'log_bandit_action', side_effect=Exception("Mocked Exception")) def test_get_bandit_action_bandit_logger_error(patched_mock_assignment_logger): client = get_instance() @@ -228,7 +232,7 @@ def test_get_bandit_action_bandit_logger_error(patched_mock_assignment_logger): # assignment should have still been logged assert len(mock_assignment_logger.assignment_events) == 1 assert len(mock_assignment_logger.bandit_events) == 0 - + @pytest.mark.parametrize("test_case", test_data) def test_bandit_generic_test_cases(test_case): From 80470e84d285ca4f174d5415e90c8a1f7596d125 Mon Sep 17 00:00:00 2001 From: Aaron Silverman Date: Fri, 19 Jul 2024 17:29:15 -0600 Subject: [PATCH 08/12] black formatter --- eppo_client/client.py | 12 +++++++----- test/client_bandit_test.py | 14 +++++++++++--- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/eppo_client/client.py b/eppo_client/client.py index b1ccd3b..d851451 100644 --- a/eppo_client/client.py +++ b/eppo_client/client.py @@ -296,7 +296,9 @@ def get_bandit_action( variation = default action = None try: - subject_attributes = convert_context_attributes_to_attributes(subject_context) + subject_attributes = convert_context_attributes_to_attributes( + subject_context + ) # first, get experiment assignment variation = self.get_string_assignment( @@ -365,7 +367,9 @@ def evaluate_bandit_action( "action": evaluation.action_key if evaluation else None, "actionProbability": evaluation.action_weight if evaluation else None, "optimalityGap": evaluation.optimality_gap if evaluation else None, - "modelVersion": bandit_data.bandit_model_version if evaluation else None, + "modelVersion": ( + bandit_data.bandit_model_version if evaluation else None + ), "timestamp": datetime.datetime.utcnow().isoformat(), "subjectNumericAttributes": ( subject_context_attributes.numeric_attributes @@ -486,6 +490,4 @@ def convert_attributes_to_context_attributes( def convert_actions_to_action_contexts( actions: Union[ActionContexts, ActionAttributes] ) -> ActionContexts: - return { - k: convert_attributes_to_context_attributes(v) for k, v in actions.items() - } + return {k: convert_attributes_to_context_attributes(v) for k, v in actions.items()} diff --git a/test/client_bandit_test.py b/test/client_bandit_test.py index 90f32fe..0674f96 100644 --- a/test/client_bandit_test.py +++ b/test/client_bandit_test.py @@ -111,12 +111,18 @@ def test_get_bandit_action_flag_not_exist(): def test_get_bandit_action_flag_has_no_bandit(): client = get_instance() result = client.get_bandit_action( - "non_bandit_flag", "subject_key", DEFAULT_SUBJECT_ATTRIBUTES, {}, "default_variation" + "non_bandit_flag", + "subject_key", + DEFAULT_SUBJECT_ATTRIBUTES, + {}, + "default_variation", ) assert result == BanditResult("control", None) -@patch.object(BanditEvaluator, 'evaluate_bandit', side_effect=Exception("Mocked Exception")) +@patch.object( + BanditEvaluator, "evaluate_bandit", side_effect=Exception("Mocked Exception") +) def test_get_bandit_action_bandit_error(mock_bandit_evaluator): client = get_instance() client.set_is_graceful_mode(True) @@ -207,7 +213,9 @@ def test_get_bandit_action_with_subject_attributes(): ) -@patch.object(MockAssignmentLogger, 'log_bandit_action', side_effect=Exception("Mocked Exception")) +@patch.object( + MockAssignmentLogger, "log_bandit_action", side_effect=Exception("Mocked Exception") +) def test_get_bandit_action_bandit_logger_error(patched_mock_assignment_logger): client = get_instance() actions = { From 95356c992cb4cfd4f9080e133e449b339a68e4b4 Mon Sep 17 00:00:00 2001 From: Aaron Silverman Date: Fri, 19 Jul 2024 17:32:18 -0600 Subject: [PATCH 09/12] return type ignore hint --- eppo_client/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eppo_client/client.py b/eppo_client/client.py index d851451..5d0e5c1 100644 --- a/eppo_client/client.py +++ b/eppo_client/client.py @@ -468,7 +468,8 @@ def convert_context_attributes_to_attributes( if isinstance(subject_context, dict): return subject_context - return subject_context.numeric_attributes | subject_context.categorical_attributes + # ignoring type because Dict[str, str] satisfies Dict[str, str | ...] but mypy does not understand + return subject_context.numeric_attributes | subject_context.categorical_attributes # type: ignore def convert_attributes_to_context_attributes( From a97e8aa043a9a8601722a31aceee992fd03499d2 Mon Sep 17 00:00:00 2001 From: Aaron Silverman Date: Fri, 19 Jul 2024 17:36:23 -0600 Subject: [PATCH 10/12] fix type hint --- eppo_client/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eppo_client/client.py b/eppo_client/client.py index 5d0e5c1..6e6c1ef 100644 --- a/eppo_client/client.py +++ b/eppo_client/client.py @@ -332,7 +332,7 @@ def evaluate_bandit_action( subject_key: str, subject_context: Union[ContextAttributes, Attributes], actions: Union[ActionContexts, ActionAttributes], - ) -> str | None: + ) -> Union[str, None]: # if no actions are given--a valid use case--return the variation with no action if len(actions) == 0: return None From 726d8c53ce0c61fa93d166b4eb8defe855ad4e31 Mon Sep 17 00:00:00 2001 From: Aaron Silverman Date: Fri, 19 Jul 2024 17:41:33 -0600 Subject: [PATCH 11/12] feedback from PR --- eppo_client/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eppo_client/client.py b/eppo_client/client.py index 6e6c1ef..7b21f46 100644 --- a/eppo_client/client.py +++ b/eppo_client/client.py @@ -268,7 +268,7 @@ def get_bandit_action( BanditResult: The result containing either the bandit action if the subject is part of the bandit, or the assignment if they are not. The BanditResult includes: - variation (str): The assignment key indicating the subject's variation. - - action (str | null): The key of the selected action if the subject was assigned one + - action (Optional[str]): The key of the selected action if the subject was assigned one by the bandit. Example: From 83ab6173e29153eb7d55c9a3f568a75890a1e1f8 Mon Sep 17 00:00:00 2001 From: Aaron Silverman Date: Wed, 24 Jul 2024 09:07:19 -0600 Subject: [PATCH 12/12] update type in comment per PR suggestion --- eppo_client/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eppo_client/client.py b/eppo_client/client.py index 7b21f46..f266641 100644 --- a/eppo_client/client.py +++ b/eppo_client/client.py @@ -256,9 +256,9 @@ 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 (ActionContexts | ActionAttributes): The subject context. + subject_context (Union[ContextAttributes, Attributes]): The subject context. If supplying an ActionAttributes, it gets converted to an ActionContexts instance - actions (ActionContexts | ActionAttributes): The dictionary that maps action keys + actions (Union[ActionContexts, ActionAttributes]): The dictionary that maps action keys to their context of actions with their contexts. If supplying an ActionAttributes, it gets converted to an ActionContexts instance. default (str): The default variation to use if an error is encountered retrieving the