diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 8bee34f..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,28 +0,0 @@ - - -## [1.0.0] - 2022-06-08 - -#### New Features: -* Subject attributes: an optional `subject_attributes` param is added to the `get_assignment` function. The subject attributes may contains custom metadata about the subject. These attributes are used for evaluating any targeting rules defined on the experiment. -``` -client.get_assignment(", "", { "email": "user@example.com" }); -``` - -#### Breaking Changes: -* The EppoClient `assign()` function is renamed to `get_assignment()` - -## [0.0.3] - 2022-05-11 - -#### New Features -* Implemented allow list for subject-variation overrides \ No newline at end of file diff --git a/eppo_client/__init__.py b/eppo_client/__init__.py index 9caa004..67196d9 100644 --- a/eppo_client/__init__.py +++ b/eppo_client/__init__.py @@ -10,7 +10,7 @@ from eppo_client.http_client import HttpClient, SdkParams from eppo_client.read_write_lock import ReadWriteLock -__version__ = "1.3.0" +__version__ = "1.3.1" __client: Optional[EppoClient] = None __lock = ReadWriteLock() diff --git a/eppo_client/rules.py b/eppo_client/rules.py index f0e0d68..84b1055 100644 --- a/eppo_client/rules.py +++ b/eppo_client/rules.py @@ -1,5 +1,6 @@ import numbers import re +import semver from enum import Enum from typing import Any, List @@ -55,9 +56,13 @@ def evaluate_condition(subject_attributes: dict, condition: Condition) -> bool: value.lower() for value in condition.value ] else: - return isinstance( - subject_value, numbers.Number - ) and evaluate_numeric_condition(subject_value, condition) + # Numeric operator: value could be numeric or semver. + if isinstance(subject_value, numbers.Number): + return evaluate_numeric_condition(subject_value, condition) + elif is_valid_semver(subject_value): + return compare_semver( + subject_value, condition.value, condition.operator + ) return False @@ -70,4 +75,31 @@ def evaluate_numeric_condition(subject_value: numbers.Number, condition: Conditi return subject_value < condition.value elif condition.operator == OperatorType.LTE: return subject_value <= condition.value + + return False + + +def is_valid_semver(value: str): + try: + # Parse the string. If it's a valid semver, it will return without errors. + semver.VersionInfo.parse(value) + return True + except ValueError: + # If a ValueError is raised, the string is not a valid semver. + return False + + +def compare_semver(attribute_value: Any, condition_value: Any, operator: OperatorType): + if not is_valid_semver(attribute_value) or not is_valid_semver(condition_value): + return False + + if operator == OperatorType.GT: + return semver.compare(attribute_value, condition_value) > 0 + elif operator == OperatorType.GTE: + return semver.compare(attribute_value, condition_value) >= 0 + elif operator == OperatorType.LT: + return semver.compare(attribute_value, condition_value) < 0 + elif operator == OperatorType.LTE: + return semver.compare(attribute_value, condition_value) <= 0 + return False diff --git a/requirements.txt b/requirements.txt index 8371266..ea377c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ requests==2.31.* cachetools==5.3.* types-cachetools==5.3.* types-requests==2.31.* +semver==3.0.* diff --git a/test/rules_test.py b/test/rules_test.py index 733a615..65089d2 100644 --- a/test/rules_test.py +++ b/test/rules_test.py @@ -58,6 +58,23 @@ def test_find_matching_rule_with_numeric_value_and_regex(): assert find_matching_rule({"age": 99}, [rule]) == rule +def test_find_matching_rule_with_semver(): + semver_greater_than_condition = Condition( + operator=OperatorType.GTE, value="1.0.0", attribute="version" + ) + semver_less_than_condition = Condition( + operator=OperatorType.LTE, value="2.0.0", attribute="version" + ) + semver_rule = Rule( + allocation_key="allocation", + conditions=[semver_less_than_condition, semver_greater_than_condition], + ) + + assert find_matching_rule({"version": "1.1.0"}, [semver_rule]) is semver_rule + assert find_matching_rule({"version": "2.0.0"}, [semver_rule]) is semver_rule + assert find_matching_rule({"version": "2.1.0"}, [semver_rule]) is None + + def test_one_of_operator_with_boolean(): oneOfRule = Rule( allocation_key="allocation",