Skip to content
This repository has been archived by the owner on Nov 8, 2024. It is now read-only.

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
schmit committed Mar 14, 2024
1 parent 36a3e18 commit 5c98d88
Show file tree
Hide file tree
Showing 12 changed files with 327 additions and 406 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,5 @@ test-data:
rm -rf ${tempDir}

.PHONY: test
test: test-data
test: # test-data
tox
218 changes: 98 additions & 120 deletions eppo_client/client.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import datetime
import logging
from typing import Any, Dict, Optional
from typing import Any, Dict, Optional, Union
from typing_extensions import deprecated
from numbers import Number
from eppo_client.assignment_logger import AssignmentLogger
from eppo_client.configuration_requestor import (
ExperimentConfigurationRequestor,
)
from eppo_client.constants import POLL_INTERVAL_MILLIS, POLL_JITTER_MILLIS
from eppo_client.models import ValueType
from eppo_client.poller import Poller
from eppo_client.rules import AttributeValue, SubjectAttributes
from eppo_client.sharding import MD5Sharder
from eppo_client.validation import validate_not_blank
from eppo_client.variation_type import VariationType
from eppo_client.eval import FlagEvaluation, Evaluator

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -40,151 +38,128 @@ def get_string_assignment(
self,
subject_key: str,
flag_key: str,
subject_attributes: Optional[SubjectAttributes] = None,
subject_attributes: Optional[Dict[str, Union[str, float, int, bool]]] = None,
default=None,
) -> Optional[str]:
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 default
raise e
return self.get_assignment_variation(
subject_key,
flag_key,
subject_attributes,
ValueType.STRING,
default=default,
)

def get_integer_assignment(
self,
subject_key: str,
flag_key: str,
subject_attributes: Optional[Dict[str, Union[str, float, int, bool]]] = None,
default=None,
) -> Optional[float]:
return self.get_assignment_variation(
subject_key,
flag_key,
subject_attributes,
ValueType.INTEGER,
default=default,
)

def get_float_assignment(
self,
subject_key: str,
flag_key: str,
subject_attributes: Optional[Dict[str, Union[str, float, int, bool]]] = None,
default=None,
) -> Optional[float]:
return self.get_assignment_variation(
subject_key,
flag_key,
subject_attributes,
ValueType.FLOAT,
default=default,
)

@deprecated("get_numeric_assignment is deprecated in favor of get_float_assignment")
def get_numeric_assignment(
self,
subject_key: str,
flag_key: str,
subject_attributes: Optional[SubjectAttributes] = None,
subject_attributes: Optional[Dict[str, Union[str, float, int, bool]]] = None,
default=None,
) -> Optional[Number]:
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 default
raise e
) -> Optional[float]:
return self.get_float_assignment(
subject_key,
flag_key,
subject_attributes,
default=default,
)

def get_boolean_assignment(
self,
subject_key: str,
flag_key: str,
subject_attributes: Optional[SubjectAttributes] = None,
subject_attributes: Optional[Dict[str, Union[str, float, int, bool]]] = None,
default=None,
) -> Optional[bool]:
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 default
raise e
return self.get_assignment_variation(
subject_key,
flag_key,
subject_attributes,
ValueType.BOOLEAN,
default=default,
)

def get_parsed_json_assignment(
self,
subject_key: str,
flag_key: str,
subject_attributes: Optional[SubjectAttributes] = None,
subject_attributes: Optional[Dict[str, Union[str, float, int, bool]]] = None,
default=None,
) -> Optional[Dict[Any, Any]]:
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 default
raise e
return self.get_assignment_variation(
subject_key,
flag_key,
subject_attributes,
ValueType.JSON,
default=default,
)

def get_json_string_assignment(
@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: Optional[SubjectAttributes] = None,
subject_attributes: Optional[Dict[str, Union[str, float, int, bool]]] = None,
default=None,
) -> Optional[str]:
try:
result = self.get_assignment_detail(
subject_key, flag_key, subject_attributes
)
assigned_variation = result.variation
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 default
raise e
return self.get_assignment_variation(
subject_key, flag_key, subject_attributes, default=default
)

@deprecated(
"get_assignment is deprecated in favor of the typed get_<type>_assignment methods"
)
def get_assignment(
def get_assignment_variation(
self,
subject_key: str,
flag_key: str,
subject_attributes: Optional[SubjectAttributes] = None,
subject_attributes: Optional[Dict[str, Union[str, float, int, bool]]] = None,
expected_variation_type: Optional[ValueType] = None,
default=None,
) -> Optional[str]:
):
try:
result = self.get_assignment_detail(
subject_key, flag_key, subject_attributes
)
if not result:
return default
assigned_variation = result.variation
return (
assigned_variation.value
if assigned_variation is not None
else assigned_variation
)
if not check_type_match(
assigned_variation.value_type, expected_variation_type
):
raise TypeError(
"Variation value does not have the correct type. Found: {assigned_variation.value_type} != {expected_variation_type}"
)
return assigned_variation.value
except ValueError as e:
# allow ValueError to bubble up as it is a validation error
raise e
Expand All @@ -198,8 +173,7 @@ def get_assignment_detail(
self,
subject_key: str,
flag_key: str,
subject_attributes: Optional[SubjectAttributes] = None,
expected_variation_type: Optional[str] = None,
subject_attributes: Optional[Dict[str, Union[str, float, int, bool]]] = None,
) -> Optional[FlagEvaluation]:
"""Maps a subject to a variation for a given experiment
Returns None if the subject is not part of the experiment sample.
Expand All @@ -224,13 +198,6 @@ def get_assignment_detail(

result = self.__evaluator.evaluate_flag(flag, subject_key, subject_attributes)

if expected_variation_type is not None and result.variation:
variation_is_expected_type = VariationType.is_expected_type(
result.variation, expected_variation_type
)
if not variation_is_expected_type:
return None

assignment_event = {
**result.extra_logging,
"allocation": result.allocation_key,
Expand All @@ -248,8 +215,19 @@ def get_assignment_detail(
logger.error("[Eppo SDK] Error logging assignment event: " + str(e))
return result

def get_flag_keys(self):
return self.__config_requestor.get_flag_keys()

def _shutdown(self):
"""Stops all background processes used by the client
Do not use the client after calling this method.
"""
self.__poller.stop()


def check_type_match(value_type, expected_type):
return (
expected_type is None
or value_type == expected_type
or value_type == expected_type.value
)
15 changes: 9 additions & 6 deletions eppo_client/configuration_requestor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
logger = logging.getLogger(__name__)


RAC_ENDPOINT = "/randomized_assignment/v3/config"
UFC_ENDPOINT = "/flag_config/v1/config"


class ExperimentConfigurationRequestor:
Expand All @@ -19,16 +19,19 @@ def __init__(
self.__http_client = http_client
self.__config_store = config_store

def get_configuration(self, experiment_key: str) -> Optional[Flag]:
def get_configuration(self, flag_key: str) -> Optional[Flag]:
if self.__http_client.is_unauthorized():
raise ValueError("Unauthorized: please check your API key")
return self.__config_store.get_configuration(experiment_key)
return self.__config_store.get_configuration(flag_key)

def get_flag_keys(self):
return self.__config_store.get_keys()

def fetch_and_store_configurations(self) -> Dict[str, Flag]:
try:
configs = cast(dict, self.__http_client.get(RAC_ENDPOINT).get("flags", {}))
for exp_key, exp_config in configs.items():
configs[exp_key] = Flag(**exp_config)
configs = cast(dict, self.__http_client.get(UFC_ENDPOINT).get("flags", {}))
for flag_key, flag_config in configs.items():
configs[flag_key] = Flag(**flag_config)
self.__config_store.set_configurations(configs)
return configs
except Exception as e:
Expand Down
3 changes: 3 additions & 0 deletions eppo_client/configuration_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ def set_configurations(self, configs: Dict[str, T]):
self.__cache[key] = config
finally:
self.__lock.release_write()

def get_keys(self):
return list(self.__cache.keys())
11 changes: 7 additions & 4 deletions eppo_client/eval.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Dict, Optional
from typing import Dict, Optional, Union
from eppo_client.sharding import Sharder
from eppo_client.models import Range, Shard, Variation
from eppo_client.models import Flag, Range, Shard, Variation
from eppo_client.rules import matches_rule
from dataclasses import dataclass
import datetime
Expand All @@ -10,7 +10,7 @@
class FlagEvaluation:
flag_key: str
subject_key: str
subject_attributes: Dict[str, str | int | float | bool]
subject_attributes: Dict[str, Union[str, float, int, bool]]
allocation_key: str
variation: Variation
extra_logging: Dict[str, str]
Expand All @@ -22,7 +22,10 @@ class Evaluator:
sharder: Sharder

def evaluate_flag(
self, flag, subject_key, subject_attributes
self,
flag: Flag,
subject_key: str,
subject_attributes: Dict[str, Union[str, float, int, bool]],
) -> Optional[FlagEvaluation]:
if not flag.enabled:
return none_result(flag.key, subject_key, subject_attributes)
Expand Down
2 changes: 1 addition & 1 deletion eppo_client/http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def get(self, resource: str) -> Any:
try:
response = self.__session.get(
self.__base_url + resource,
params=self.__sdk_params.dict(),
params=self.__sdk_params.model_dump(),
timeout=REQUEST_TIMEOUT_SECONDS,
)
self.__is_unauthorized = response.status_code == HTTPStatus.UNAUTHORIZED
Expand Down
Loading

0 comments on commit 5c98d88

Please sign in to comment.