Skip to content

Commit 9dac614

Browse files
authored
Add more comparison operators (#112)
* Updates the proto definitions and compiled proto code * make the Version code more resilient in a test environment * implement number operators, consolidate string operations into simple_criterion_evaluators.py * implements Date ops (BEFORE/AFTER) * Add SemanticVersion parsing, comparisons with tests * integrate Semver, add Regex operators * fix argument ordering, expectations in no context value cases * fix one_of to properly handle lists from context and/or criterion * Patch the integration test runner to support updated integration test data format Update the integration test data submodule * Update poetry.lock file * Handle date or datetime in the context for date operations. Example contexts are sent with the RFC formatted datetime, context shape defaults to string for unexpected types * Automatic styling from pre-commit * handle typing errors raised by black/ruff
1 parent d587d54 commit 9dac614

17 files changed

Lines changed: 1807 additions & 533 deletions

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,4 @@ repos:
5151
- types-pyyaml
5252
- types-requests
5353
- google-api-python-client
54+
- pytest

poetry.lock

Lines changed: 400 additions & 329 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

prefab.proto

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,11 @@ message Criterion {
176176
PROP_GREATER_THAN_OR_EQUAL = 19;
177177
PROP_BEFORE = 20;
178178
PROP_AFTER = 21;
179+
PROP_MATCHES = 22;
180+
PROP_DOES_NOT_MATCH = 23;
181+
PROP_SEMVER_LESS_THAN = 24;
182+
PROP_SEMVER_EQUAL = 25;
183+
PROP_SEMVER_GREATER_THAN = 26;
179184
}
180185
string property_name = 1;
181186
CriterionOperator operator = 2;

prefab_cloud_python/_requests.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import importlib
12
from socket import socket
23
from typing import Optional
34

@@ -13,11 +14,16 @@
1314
wait_exponential,
1415
retry_if_exception_type,
1516
)
16-
from importlib.metadata import version
1717

1818
logger = InternalLogger(__name__)
19+
try:
20+
from importlib.metadata import version
21+
22+
Version = version("prefab-cloud-python")
23+
except importlib.metadata.PackageNotFoundError:
24+
Version = "development"
25+
1926

20-
Version = version("prefab-cloud-python")
2127
VersionHeader = "X-PrefabCloud-Client-Version"
2228

2329
DEFAULT_TIMEOUT = 5 # seconds

prefab_cloud_python/config_resolver.py

Lines changed: 46 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55
from .config_value_unwrapper import ConfigValueUnwrapper
66
from .context import Context
77
from ._internal_logging import InternalLogger
8+
from .simple_criterion_evaluators import (
9+
NumericOperators,
10+
StringOperators,
11+
DateOperators,
12+
SemverOperators,
13+
RegexMatchOperators,
14+
)
815
import prefab_pb2 as Prefab
916
import google
1017

@@ -123,67 +130,42 @@ def all_criteria_match(self, conditional_value, props):
123130

124131
def evaluate_criterion(self, criterion, properties):
125132
value_from_properties = properties.get(criterion.property_name)
133+
deepest_value = ConfigValueUnwrapper.deepest_value(
134+
criterion.value_to_match, self.config, properties
135+
)
126136

127137
if criterion.operator in [OPS.LOOKUP_KEY_IN, OPS.PROP_IS_ONE_OF]:
128-
return self.matches(criterion, value_from_properties, properties)
138+
return self.one_of(criterion, value_from_properties, properties)
129139
if criterion.operator in [OPS.LOOKUP_KEY_NOT_IN, OPS.PROP_IS_NOT_ONE_OF]:
130-
return not self.matches(criterion, value_from_properties, properties)
140+
return not self.one_of(criterion, value_from_properties, properties)
131141
if criterion.operator == OPS.IN_SEG:
132142
return self.in_segment(criterion, properties)
133143
if criterion.operator == OPS.NOT_IN_SEG:
134144
return not self.in_segment(criterion, properties)
135-
if criterion.operator in [
136-
OPS.PROP_ENDS_WITH_ONE_OF,
137-
OPS.PROP_DOES_NOT_END_WITH_ONE_OF,
138-
]:
139-
negative = criterion.operator == OPS.PROP_DOES_NOT_END_WITH_ONE_OF
140-
if value_from_properties is None:
141-
return self.negate(negative, False)
142-
return self.negate(
143-
negative,
144-
any(
145-
[
146-
str(value_from_properties).endswith(ending)
147-
for ending in criterion.value_to_match.string_list.values
148-
]
149-
),
150-
)
151-
if criterion.operator in [
152-
OPS.PROP_STARTS_WITH_ONE_OF,
153-
OPS.PROP_DOES_NOT_START_WITH_ONE_OF,
154-
]:
155-
negative = criterion.operator == OPS.PROP_DOES_NOT_START_WITH_ONE_OF
156-
if value_from_properties is None:
157-
return self.negate(negative, False)
158-
return self.negate(
159-
negative,
160-
any(
161-
[
162-
str(value_from_properties).startswith(beginning)
163-
for beginning in criterion.value_to_match.string_list.values
164-
]
165-
),
166-
)
167-
if criterion.operator in [
168-
OPS.PROP_CONTAINS_ONE_OF,
169-
OPS.PROP_DOES_NOT_CONTAIN_ONE_OF,
170-
]:
171-
negative = criterion.operator == OPS.PROP_DOES_NOT_CONTAIN_ONE_OF
172-
if value_from_properties is None:
173-
return self.negate(negative, False)
174-
return self.negate(
175-
negative,
176-
any(
177-
[
178-
string in str(value_from_properties)
179-
for string in criterion.value_to_match.string_list.values
180-
]
181-
),
145+
if criterion.operator in StringOperators.SUPPORTED_OPERATORS:
146+
return StringOperators.evaluate(
147+
value_from_properties, criterion.operator, deepest_value.unwrap()
182148
)
183149
if criterion.operator == OPS.HIERARCHICAL_MATCH:
184150
return value_from_properties.startswith(criterion.value_to_match.string)
185151
if criterion.operator == OPS.ALWAYS_TRUE:
186152
return True
153+
if criterion.operator in DateOperators.SUPPORTED_OPERATORS:
154+
return DateOperators.evaluate(
155+
value_from_properties, criterion.operator, deepest_value.unwrap()
156+
)
157+
if criterion.operator in NumericOperators.SUPPORTED_OPERATORS:
158+
return NumericOperators.evaluate(
159+
value_from_properties, criterion.operator, deepest_value.unwrap()
160+
)
161+
if criterion.operator in RegexMatchOperators.SUPPORTED_OPERATORS:
162+
return RegexMatchOperators.evaluate(
163+
value_from_properties, criterion.operator, deepest_value.unwrap()
164+
)
165+
if criterion.operator in SemverOperators.SUPPORTED_OPERATORS:
166+
return SemverOperators.evaluate(
167+
value_from_properties, criterion.operator, deepest_value.unwrap()
168+
)
187169

188170
logger.info(f"Unknown criterion operator {criterion.operator}")
189171
return False
@@ -192,15 +174,23 @@ def evaluate_criterion(self, criterion, properties):
192174
def negate(negate, value):
193175
return not value if negate else value
194176

195-
def matches(self, criterion, value, properties):
177+
@staticmethod
178+
def _ensure_list(value):
179+
return (
180+
value
181+
if isinstance(value, (list, google._upb._message.RepeatedScalarContainer))
182+
else [value]
183+
)
184+
185+
def one_of(self, criterion, value, properties):
196186
criterion_value_or_values = ConfigValueUnwrapper.deepest_value(
197-
criterion.value_to_match, self.config.key, properties
187+
criterion.value_to_match, self.config, properties
198188
).unwrap()
199-
if isinstance(
200-
criterion_value_or_values, google._upb._message.RepeatedScalarContainer
201-
) or isinstance(criterion_value_or_values, list):
202-
return str(value) in criterion_value_or_values
203-
return value == criterion_value_or_values
189+
190+
criterion_values = self._ensure_list(criterion_value_or_values)
191+
values = self._ensure_list(value)
192+
193+
return any(str(v1) == str(v2) for v1 in criterion_values for v2 in values)
204194

205195
def in_segment(self, criterion, properties):
206196
return (

prefab_cloud_python/config_value_unwrapper.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import json
22

3+
4+
from typing import TYPE_CHECKING, ForwardRef
5+
6+
if TYPE_CHECKING:
7+
from .config_resolver import ConfigResolver
8+
else:
9+
ConfigResolver = ForwardRef("ConfigResolver")
10+
311
from .weighted_value_resolver import WeightedValueResolver
412
from .config_value_wrapper import ConfigValueWrapper
513
from .context import Context
@@ -77,8 +85,8 @@ def reportable_value(self):
7785
@staticmethod
7886
def deepest_value(
7987
config_value: Prefab.ConfigValue,
80-
config,
81-
resolver,
88+
config: Prefab.Config,
89+
resolver: ConfigResolver,
8290
context=Context.get_current(),
8391
):
8492
if config_value and config_value.WhichOneof("type") == "weighted_values":
Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,39 @@
1+
from datetime import date, datetime, timezone
2+
13
import prefab_pb2 as Prefab
24

35

46
class ConfigValueWrapper:
57
@staticmethod
68
def wrap(value, confidential=None):
7-
if type(value) == int:
9+
value_type = type(value)
10+
11+
if value_type == int:
812
return Prefab.ConfigValue(int=value, confidential=confidential)
9-
elif type(value) == float:
13+
elif value_type == float:
1014
return Prefab.ConfigValue(double=value, confidential=confidential)
11-
elif type(value) == bool:
15+
elif value_type == bool:
1216
return Prefab.ConfigValue(bool=value, confidential=confidential)
13-
elif type(value) == list:
17+
elif value_type == list:
1418
return Prefab.ConfigValue(
1519
string_list=Prefab.StringList(values=[str(x) for x in value]),
1620
confidential=confidential,
1721
)
22+
elif value_type == datetime:
23+
return Prefab.ConfigValue(
24+
string=ConfigValueWrapper._format_date_time(value),
25+
confidential=confidential,
26+
)
27+
elif value_type == date:
28+
return Prefab.ConfigValue(
29+
string=ConfigValueWrapper._format_date_time(
30+
datetime.combine(value, datetime.min.time(), timezone.utc)
31+
),
32+
confidential=confidential,
33+
)
1834
else:
1935
return Prefab.ConfigValue(string=value, confidential=confidential)
36+
37+
@staticmethod
38+
def _format_date_time(value: datetime):
39+
return value.strftime("%Y-%m-%dT%H:%M:%SZ")
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import json
2-
3-
MAPPING = {int: 1, str: 2, float: 4, bool: 5, list: 10, json: 16}
1+
MAPPING = {int: 1, str: 2, float: 4, bool: 5, list: 10, dict: 16}
42

53

64
class ContextShape:
5+
@staticmethod
76
def field_type_number(value):
8-
return MAPPING.setdefault(type(value), MAPPING[str])
7+
return MAPPING.get(type(value), 2) # default to string type

0 commit comments

Comments
 (0)