From a021dc26c361af6faeaf1aa225a2b9423a5045ca Mon Sep 17 00:00:00 2001 From: Oleksii Shmalko Date: Tue, 20 Aug 2024 20:58:56 +0300 Subject: [PATCH 1/2] fix: add timezone to timestamps --- eppo_client/client.py | 8 ++++++-- test/client_bandit_test.py | 32 ++++++++++++++++++++++++++++++++ test/client_test.py | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/eppo_client/client.py b/eppo_client/client.py index f266641..6fd5606 100644 --- a/eppo_client/client.py +++ b/eppo_client/client.py @@ -222,7 +222,7 @@ def get_assignment_detail( "featureFlag": flag_key, "variation": result.variation.key if result and result.variation else None, "subject": subject_key, - "timestamp": datetime.datetime.utcnow().isoformat(), + "timestamp": _utcnow().isoformat(), "subjectAttributes": subject_attributes, "metaData": {"sdkLanguage": "python", "sdkVersion": __version__}, } @@ -370,7 +370,7 @@ def evaluate_bandit_action( "modelVersion": ( bandit_data.bandit_model_version if evaluation else None ), - "timestamp": datetime.datetime.utcnow().isoformat(), + "timestamp": _utcnow().isoformat(), "subjectNumericAttributes": ( subject_context_attributes.numeric_attributes if evaluation.subject_attributes @@ -492,3 +492,7 @@ 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()} + + +def _utcnow() -> datetime.datetime: + return datetime.datetime.now(datetime.timezone.utc) diff --git a/test/client_bandit_test.py b/test/client_bandit_test.py index 0674f96..bb01904 100644 --- a/test/client_bandit_test.py +++ b/test/client_bandit_test.py @@ -2,6 +2,7 @@ # making client_test.py too long. +import datetime import json import os from time import sleep @@ -156,6 +157,37 @@ def test_get_bandit_action_bandit_error(mock_bandit_evaluator): assert len(mock_assignment_logger.bandit_events) == 0 +def test_bandit_event_has_utc_timestamp(): + # tests that allocation filtering based on subject attributes works correctly + 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"} + ), + } + client.get_bandit_action( + "banner_bandit_flag_uk_only", + "alice", + DEFAULT_SUBJECT_ATTRIBUTES, + actions, + "default_variation", + ) + + # testing assignment logger + assignment_event = mock_assignment_logger.assignment_events[-1] + timestamp = datetime.datetime.fromisoformat(assignment_event["timestamp"]) + assert timestamp.tzinfo == datetime.timezone.utc + + # testing bandit logger + bandit_event = mock_assignment_logger.bandit_events[-1] + timestamp = datetime.datetime.fromisoformat(bandit_event["timestamp"]) + assert timestamp.tzinfo == datetime.timezone.utc + + def test_get_bandit_action_with_subject_attributes(): # tests that allocation filtering based on subject attributes works correctly client = get_instance() diff --git a/test/client_test.py b/test/client_test.py index 9d340dc..aab976e 100644 --- a/test/client_test.py +++ b/test/client_test.py @@ -1,4 +1,5 @@ import json +import datetime import os from time import sleep from unittest.mock import patch @@ -113,6 +114,40 @@ def test_log_assignment(mock_config_requestor, mock_logger): assert mock_logger.log_assignment.call_count == 1 +@patch("eppo_client.assignment_logger.AssignmentLogger") +@patch("eppo_client.configuration_requestor.ExperimentConfigurationRequestor") +def test_assignment_event_has_utc_timestamp(mock_config_requestor, mock_logger): + flag = Flag( + key="flag-key", + enabled=True, + variation_type=VariationType.STRING, + variations={"control": Variation(key="control", value="control")}, + allocations=[ + Allocation( + key="allocation", + splits=[ + Split( + variation_key="control", + shards=[Shard(salt="salt", ranges=[Range(start=0, end=10000)])], + ) + ], + do_log=True, + ) + ], + total_shards=10_000, + ) + + mock_config_requestor.get_configuration.return_value = flag + client = EppoClient( + config_requestor=mock_config_requestor, assignment_logger=mock_logger + ) + client.get_string_assignment("falg-key", "user-1", {}, "default value") + + event = mock_logger.log_assignment.call_args.args[0] + timestamp = datetime.datetime.fromisoformat(event["timestamp"]) + assert timestamp.tzinfo == datetime.timezone.utc + + @patch("eppo_client.assignment_logger.AssignmentLogger") @patch("eppo_client.configuration_requestor.ExperimentConfigurationRequestor") def test_get_assignment_handles_logging_exception(mock_config_requestor, mock_logger): From c1f717cd95a8b20817b04203343e7fc9365e3839 Mon Sep 17 00:00:00 2001 From: Oleksii Shmalko Date: Wed, 21 Aug 2024 18:56:56 +0300 Subject: [PATCH 2/2] chore: version bump --- eppo_client/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eppo_client/version.py b/eppo_client/version.py index 99950aa..7198df6 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.1" +__version__ = "3.5.2"