Skip to content
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
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,4 @@ repos:
- types-pyyaml
- types-requests
- google-api-python-client
- pytest
729 changes: 400 additions & 329 deletions poetry.lock

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions prefab.proto
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ message Criterion {
PROP_GREATER_THAN_OR_EQUAL = 19;
PROP_BEFORE = 20;
PROP_AFTER = 21;
PROP_MATCHES = 22;
PROP_DOES_NOT_MATCH = 23;
PROP_SEMVER_LESS_THAN = 24;
PROP_SEMVER_EQUAL = 25;
PROP_SEMVER_GREATER_THAN = 26;
}
string property_name = 1;
CriterionOperator operator = 2;
Expand Down
10 changes: 8 additions & 2 deletions prefab_cloud_python/_requests.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import importlib
from socket import socket
from typing import Optional

Expand All @@ -13,11 +14,16 @@
wait_exponential,
retry_if_exception_type,
)
from importlib.metadata import version

logger = InternalLogger(__name__)
try:
from importlib.metadata import version

Version = version("prefab-cloud-python")
except importlib.metadata.PackageNotFoundError:
Version = "development"


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

DEFAULT_TIMEOUT = 5 # seconds
Expand Down
102 changes: 46 additions & 56 deletions prefab_cloud_python/config_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
from .config_value_unwrapper import ConfigValueUnwrapper
from .context import Context
from ._internal_logging import InternalLogger
from .simple_criterion_evaluators import (
NumericOperators,
StringOperators,
DateOperators,
SemverOperators,
RegexMatchOperators,
)
import prefab_pb2 as Prefab
import google

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

def evaluate_criterion(self, criterion, properties):
value_from_properties = properties.get(criterion.property_name)
deepest_value = ConfigValueUnwrapper.deepest_value(
criterion.value_to_match, self.config, properties
)

if criterion.operator in [OPS.LOOKUP_KEY_IN, OPS.PROP_IS_ONE_OF]:
return self.matches(criterion, value_from_properties, properties)
return self.one_of(criterion, value_from_properties, properties)
if criterion.operator in [OPS.LOOKUP_KEY_NOT_IN, OPS.PROP_IS_NOT_ONE_OF]:
return not self.matches(criterion, value_from_properties, properties)
return not self.one_of(criterion, value_from_properties, properties)
if criterion.operator == OPS.IN_SEG:
return self.in_segment(criterion, properties)
if criterion.operator == OPS.NOT_IN_SEG:
return not self.in_segment(criterion, properties)
if criterion.operator in [
OPS.PROP_ENDS_WITH_ONE_OF,
OPS.PROP_DOES_NOT_END_WITH_ONE_OF,
]:
negative = criterion.operator == OPS.PROP_DOES_NOT_END_WITH_ONE_OF
if value_from_properties is None:
return self.negate(negative, False)
return self.negate(
negative,
any(
[
str(value_from_properties).endswith(ending)
for ending in criterion.value_to_match.string_list.values
]
),
)
if criterion.operator in [
OPS.PROP_STARTS_WITH_ONE_OF,
OPS.PROP_DOES_NOT_START_WITH_ONE_OF,
]:
negative = criterion.operator == OPS.PROP_DOES_NOT_START_WITH_ONE_OF
if value_from_properties is None:
return self.negate(negative, False)
return self.negate(
negative,
any(
[
str(value_from_properties).startswith(beginning)
for beginning in criterion.value_to_match.string_list.values
]
),
)
if criterion.operator in [
OPS.PROP_CONTAINS_ONE_OF,
OPS.PROP_DOES_NOT_CONTAIN_ONE_OF,
]:
negative = criterion.operator == OPS.PROP_DOES_NOT_CONTAIN_ONE_OF
if value_from_properties is None:
return self.negate(negative, False)
return self.negate(
negative,
any(
[
string in str(value_from_properties)
for string in criterion.value_to_match.string_list.values
]
),
if criterion.operator in StringOperators.SUPPORTED_OPERATORS:
return StringOperators.evaluate(
value_from_properties, criterion.operator, deepest_value.unwrap()
)
if criterion.operator == OPS.HIERARCHICAL_MATCH:
return value_from_properties.startswith(criterion.value_to_match.string)
if criterion.operator == OPS.ALWAYS_TRUE:
return True
if criterion.operator in DateOperators.SUPPORTED_OPERATORS:
return DateOperators.evaluate(
value_from_properties, criterion.operator, deepest_value.unwrap()
)
if criterion.operator in NumericOperators.SUPPORTED_OPERATORS:
return NumericOperators.evaluate(
value_from_properties, criterion.operator, deepest_value.unwrap()
)
if criterion.operator in RegexMatchOperators.SUPPORTED_OPERATORS:
return RegexMatchOperators.evaluate(
value_from_properties, criterion.operator, deepest_value.unwrap()
)
if criterion.operator in SemverOperators.SUPPORTED_OPERATORS:
return SemverOperators.evaluate(
value_from_properties, criterion.operator, deepest_value.unwrap()
)

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

def matches(self, criterion, value, properties):
@staticmethod
def _ensure_list(value):
return (
value
if isinstance(value, (list, google._upb._message.RepeatedScalarContainer))
else [value]
)

def one_of(self, criterion, value, properties):
criterion_value_or_values = ConfigValueUnwrapper.deepest_value(
criterion.value_to_match, self.config.key, properties
criterion.value_to_match, self.config, properties
).unwrap()
if isinstance(
criterion_value_or_values, google._upb._message.RepeatedScalarContainer
) or isinstance(criterion_value_or_values, list):
return str(value) in criterion_value_or_values
return value == criterion_value_or_values

criterion_values = self._ensure_list(criterion_value_or_values)
values = self._ensure_list(value)

return any(str(v1) == str(v2) for v1 in criterion_values for v2 in values)

def in_segment(self, criterion, properties):
return (
Expand Down
12 changes: 10 additions & 2 deletions prefab_cloud_python/config_value_unwrapper.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import json


from typing import TYPE_CHECKING, ForwardRef

if TYPE_CHECKING:
from .config_resolver import ConfigResolver
else:
ConfigResolver = ForwardRef("ConfigResolver")

from .weighted_value_resolver import WeightedValueResolver
from .config_value_wrapper import ConfigValueWrapper
from .context import Context
Expand Down Expand Up @@ -77,8 +85,8 @@ def reportable_value(self):
@staticmethod
def deepest_value(
config_value: Prefab.ConfigValue,
config,
resolver,
config: Prefab.Config,
resolver: ConfigResolver,
context=Context.get_current(),
):
if config_value and config_value.WhichOneof("type") == "weighted_values":
Expand Down
28 changes: 24 additions & 4 deletions prefab_cloud_python/config_value_wrapper.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,39 @@
from datetime import date, datetime, timezone

import prefab_pb2 as Prefab


class ConfigValueWrapper:
@staticmethod
def wrap(value, confidential=None):
if type(value) == int:
value_type = type(value)

if value_type == int:
return Prefab.ConfigValue(int=value, confidential=confidential)
elif type(value) == float:
elif value_type == float:
return Prefab.ConfigValue(double=value, confidential=confidential)
elif type(value) == bool:
elif value_type == bool:
return Prefab.ConfigValue(bool=value, confidential=confidential)
elif type(value) == list:
elif value_type == list:
return Prefab.ConfigValue(
string_list=Prefab.StringList(values=[str(x) for x in value]),
confidential=confidential,
)
elif value_type == datetime:
return Prefab.ConfigValue(
string=ConfigValueWrapper._format_date_time(value),
confidential=confidential,
)
elif value_type == date:
return Prefab.ConfigValue(
string=ConfigValueWrapper._format_date_time(
datetime.combine(value, datetime.min.time(), timezone.utc)
),
confidential=confidential,
)
else:
return Prefab.ConfigValue(string=value, confidential=confidential)

@staticmethod
def _format_date_time(value: datetime):
return value.strftime("%Y-%m-%dT%H:%M:%SZ")
7 changes: 3 additions & 4 deletions prefab_cloud_python/context_shape.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import json

MAPPING = {int: 1, str: 2, float: 4, bool: 5, list: 10, json: 16}
MAPPING = {int: 1, str: 2, float: 4, bool: 5, list: 10, dict: 16}


class ContextShape:
@staticmethod
def field_type_number(value):
return MAPPING.setdefault(type(value), MAPPING[str])
return MAPPING.get(type(value), 2) # default to string type
Loading
Loading