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: 2 additions & 0 deletions eppo_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def init(config: Config) -> EppoClient:
http_client=http_client, config_store=config_store
)
assignment_logger = config.assignment_logger
is_graceful_mode = config.is_graceful_mode
global __client
global __lock
try:
Expand All @@ -48,6 +49,7 @@ def init(config: Config) -> EppoClient:
__client = EppoClient(
config_requestor=config_requestor,
assignment_logger=assignment_logger,
is_graceful_mode=is_graceful_mode,
)
return __client
finally:
Expand Down
152 changes: 104 additions & 48 deletions eppo_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ def __init__(
self,
config_requestor: ExperimentConfigurationRequestor,
assignment_logger: AssignmentLogger,
is_graceful_mode: bool = True,
):
self.__config_requestor = config_requestor
self.__assignment_logger = assignment_logger
self.__is_graceful_mode = is_graceful_mode
self.__poller = Poller(
interval_millis=POLL_INTERVAL_MILLIS,
jitter_millis=POLL_JITTER_MILLIS,
Expand All @@ -38,77 +40,131 @@ def __init__(
def get_string_assignment(
self, subject_key: str, flag_key: str, subject_attributes=dict()
) -> Optional[str]:
assigned_variation = self.get_assignment_variation(
subject_key, flag_key, subject_attributes, VariationType.STRING
)
return (
assigned_variation.typed_value
if assigned_variation is not None
else assigned_variation
)
try:
assigned_variation = self.get_assignment_variation(
subject_key, flag_key, subject_attributes, VariationType.STRING
)
return (
assigned_variation.typed_value
if assigned_variation is not None
else assigned_variation
)
except ValueError as e:
# allow ValueError to bubble up as it is a validation error
raise e
except Exception as e:
if self.__is_graceful_mode:
logger.error("[Eppo SDK] Error getting assignment: " + str(e))
return None
raise e

def get_numeric_assignment(
self, subject_key: str, flag_key: str, subject_attributes=dict()
) -> Optional[Number]:
assigned_variation = self.get_assignment_variation(
subject_key, flag_key, subject_attributes, VariationType.NUMERIC
)
return (
assigned_variation.typed_value
if assigned_variation is not None
else assigned_variation
)
try:
assigned_variation = self.get_assignment_variation(
subject_key, flag_key, subject_attributes, VariationType.NUMERIC
)
return (
assigned_variation.typed_value
if assigned_variation is not None
else assigned_variation
)
except ValueError as e:
# allow ValueError to bubble up as it is a validation error
raise e
except Exception as e:
if self.__is_graceful_mode:
logger.error("[Eppo SDK] Error getting assignment: " + str(e))
return None
raise e

def get_boolean_assignment(
self, subject_key: str, flag_key: str, subject_attributes=dict()
) -> Optional[bool]:
assigned_variation = self.get_assignment_variation(
subject_key, flag_key, subject_attributes, VariationType.BOOLEAN
)
return (
assigned_variation.typed_value
if assigned_variation is not None
else assigned_variation
)
try:
assigned_variation = self.get_assignment_variation(
subject_key, flag_key, subject_attributes, VariationType.BOOLEAN
)
return (
assigned_variation.typed_value
if assigned_variation is not None
else assigned_variation
)
except ValueError as e:
# allow ValueError to bubble up as it is a validation error
raise e
except Exception as e:
if self.__is_graceful_mode:
logger.error("[Eppo SDK] Error getting assignment: " + str(e))
return None
raise e

def get_parsed_json_assignment(
self, subject_key: str, flag_key: str, subject_attributes=dict()
) -> Optional[Dict[Any, Any]]:
assigned_variation = self.get_assignment_variation(
subject_key, flag_key, subject_attributes, VariationType.JSON
)
return (
assigned_variation.typed_value
if assigned_variation is not None
else assigned_variation
)
try:
assigned_variation = self.get_assignment_variation(
subject_key, flag_key, subject_attributes, VariationType.JSON
)
return (
assigned_variation.typed_value
if assigned_variation is not None
else assigned_variation
)
except ValueError as e:
# allow ValueError to bubble up as it is a validation error
raise e
except Exception as e:
if self.__is_graceful_mode:
logger.error("[Eppo SDK] Error getting assignment: " + str(e))
return None
raise e

def get_json_string_assignment(
self, subject_key: str, flag_key: str, subject_attributes=dict()
) -> Optional[str]:
assigned_variation = self.get_assignment_variation(
subject_key, flag_key, subject_attributes, VariationType.JSON
)
return (
assigned_variation.value
if assigned_variation is not None
else assigned_variation
)
try:
assigned_variation = self.get_assignment_variation(
subject_key, flag_key, subject_attributes, VariationType.JSON
)
return (
assigned_variation.value
if assigned_variation is not None
else assigned_variation
)
except ValueError as e:
# allow ValueError to bubble up as it is a validation error
raise e
except Exception as e:
if self.__is_graceful_mode:
logger.error("[Eppo SDK] Error getting assignment: " + str(e))
return None
raise e

@deprecated(
"get_assignment is deprecated in favor of the typed get_<type>_assignment methods"
)
def get_assignment(
self, subject_key: str, flag_key: str, subject_attributes=dict()
) -> Optional[str]:
assigned_variation = self.get_assignment_variation(
subject_key, flag_key, subject_attributes
)
return (
assigned_variation.value
if assigned_variation is not None
else assigned_variation
)
try:
assigned_variation = self.get_assignment_variation(
subject_key, flag_key, subject_attributes
)
return (
assigned_variation.value
if assigned_variation is not None
else assigned_variation
)
except ValueError as e:
# allow ValueError to bubble up as it is a validation error
raise e
except Exception as e:
if self.__is_graceful_mode:
logger.error("[Eppo SDK] Error getting assignment: " + str(e))
return None
raise e

def get_assignment_variation(
self,
Expand Down
1 change: 1 addition & 0 deletions eppo_client/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class Config(SdkBaseModel):
api_key: str
base_url: str = "https://fscdn.eppo.cloud/api"
assignment_logger: AssignmentLogger
is_graceful_mode: bool = True
Copy link
Contributor

Choose a reason for hiding this comment

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

Just wondering, is this intended for customers to set? I.e. will this need an update in the docs?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes they can set this, for example in dev mode


def _validate(self):
validate_not_blank("api_key", self.api_key)
39 changes: 39 additions & 0 deletions test/client_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,45 @@ def test_with_null_experiment_config(mock_config_requestor):
assert client.get_assignment("user-1", "experiment-key-1") is None


@patch("eppo_client.configuration_requestor.ExperimentConfigurationRequestor")
@patch.object(EppoClient, "get_assignment_variation")
def test_graceful_mode_on(mock_get_assignment_variation, mock_config_requestor):
mock_get_assignment_variation.side_effect = Exception("This is a mock exception!")

client = EppoClient(
config_requestor=mock_config_requestor,
assignment_logger=AssignmentLogger(),
is_graceful_mode=True,
)

assert client.get_assignment("user-1", "experiment-key-1") is None
assert client.get_boolean_assignment("user-1", "experiment-key-1") is None
assert client.get_json_string_assignment("user-1", "experiment-key-1") is None
assert client.get_numeric_assignment("user-1", "experiment-key-1") is None
assert client.get_string_assignment("user-1", "experiment-key-1") is None
assert client.get_parsed_json_assignment("user-1", "experiment-key-1") is None


@patch("eppo_client.configuration_requestor.ExperimentConfigurationRequestor")
@patch.object(EppoClient, "get_assignment_variation")
def test_graceful_mode_off(mock_get_assignment_variation, mock_config_requestor):
mock_get_assignment_variation.side_effect = Exception("This is a mock exception!")

client = EppoClient(
config_requestor=mock_config_requestor,
assignment_logger=AssignmentLogger(),
is_graceful_mode=False,
)

with pytest.raises(Exception):
client.get_assignment("user-1", "experiment-key-1")
client.get_boolean_assignment("user-1", "experiment-key-1")
client.get_json_string_assignment("user-1", "experiment-key-1")
client.get_numeric_assignment("user-1", "experiment-key-1")
client.get_string_assignment("user-1", "experiment-key-1")
client.get_parsed_json_assignment("user-1", "experiment-key-1")


@pytest.mark.parametrize("test_case", test_data)
def test_assign_subject_in_sample(test_case):
print("---- Test case for {} Experiment".format(test_case["experiment"]))
Expand Down