|
@@ -19,27 +19,36 @@ class EvaluationContext:
|
|
feature conditions.
|
|
feature conditions.
|
|
"""
|
|
"""
|
|
|
|
|
|
- def __init__(self, data: EvaluationContextDict):
|
|
|
|
|
|
+ __data: EvaluationContextDict
|
|
|
|
+ __identity_fields: set[str]
|
|
|
|
+ __id: int
|
|
|
|
+
|
|
|
|
+ def __init__(self, data: EvaluationContextDict, identity_fields: set[str] | None = None):
|
|
self.__data = deepcopy(data)
|
|
self.__data = deepcopy(data)
|
|
|
|
+ self.__set_identity_fields(identity_fields)
|
|
|
|
+ self.__id = self.__generate_id()
|
|
|
|
|
|
- def get(self, key: str) -> Any:
|
|
|
|
- return self.__data.get(key)
|
|
|
|
|
|
+ def __set_identity_fields(self, identity_fields: set[str] | None = None):
|
|
|
|
+ trimmed_id_fields = set()
|
|
|
|
+ if identity_fields is not None:
|
|
|
|
+ for field in identity_fields:
|
|
|
|
+ if field in self.__data:
|
|
|
|
+ trimmed_id_fields.add(field)
|
|
|
|
|
|
- def has(self, key: str) -> Any:
|
|
|
|
- return key in self.__data
|
|
|
|
|
|
+ if not trimmed_id_fields:
|
|
|
|
+ trimmed_id_fields.update(self.__data.keys())
|
|
|
|
|
|
- def size(self) -> int:
|
|
|
|
- return len(self.__data)
|
|
|
|
|
|
+ self.__identity_fields = trimmed_id_fields
|
|
|
|
|
|
- def id(self) -> int:
|
|
|
|
|
|
+ def __generate_id(self) -> int:
|
|
"""
|
|
"""
|
|
- Return a hashed identifier for this context
|
|
|
|
|
|
+ Generates and return a hashed identifier for this context
|
|
|
|
|
|
The identifier should be stable for a given context contents.
|
|
The identifier should be stable for a given context contents.
|
|
Identifiers are used to determine rollout groups deterministically
|
|
Identifiers are used to determine rollout groups deterministically
|
|
and consistently.
|
|
and consistently.
|
|
"""
|
|
"""
|
|
- keys = self.__data.keys()
|
|
|
|
|
|
+ keys = list(self.__identity_fields)
|
|
vector = []
|
|
vector = []
|
|
for key in sorted(keys):
|
|
for key in sorted(keys):
|
|
vector.append(key)
|
|
vector.append(key)
|
|
@@ -47,6 +56,23 @@ class EvaluationContext:
|
|
hashed = hashlib.sha1(":".join(vector).encode("utf8"))
|
|
hashed = hashlib.sha1(":".join(vector).encode("utf8"))
|
|
return int.from_bytes(hashed.digest(), byteorder="big")
|
|
return int.from_bytes(hashed.digest(), byteorder="big")
|
|
|
|
|
|
|
|
+ @property
|
|
|
|
+ def id(self) -> int:
|
|
|
|
+ """
|
|
|
|
+ Guard against context mutation by using this virtual property as a
|
|
|
|
+ getter for the private ID field.
|
|
|
|
+ """
|
|
|
|
+ return self.__id
|
|
|
|
+
|
|
|
|
+ def get(self, key: str) -> Any:
|
|
|
|
+ return self.__data.get(key)
|
|
|
|
+
|
|
|
|
+ def has(self, key: str) -> Any:
|
|
|
|
+ return key in self.__data
|
|
|
|
+
|
|
|
|
+ def size(self) -> int:
|
|
|
|
+ return len(self.__data)
|
|
|
|
+
|
|
|
|
|
|
T_CONTEXT_DATA = TypeVar("T_CONTEXT_DATA")
|
|
T_CONTEXT_DATA = TypeVar("T_CONTEXT_DATA")
|
|
|
|
|
|
@@ -61,20 +87,27 @@ class ContextBuilder(Generic[T_CONTEXT_DATA]):
|
|
>>> from flagpole import ContextBuilder, Feature
|
|
>>> from flagpole import ContextBuilder, Feature
|
|
>>> builder = ContextBuilder().add_context_transformer(lambda _dict: dict(foo="bar"))
|
|
>>> builder = ContextBuilder().add_context_transformer(lambda _dict: dict(foo="bar"))
|
|
>>> feature = Feature.from_feature_dictionary(name="foo", feature_dictionary=dict(), context=builder)
|
|
>>> feature = Feature.from_feature_dictionary(name="foo", feature_dictionary=dict(), context=builder)
|
|
- >>> feature.match(dict())
|
|
|
|
|
|
+ >>> feature.match(EvaluationContext(dict()))
|
|
"""
|
|
"""
|
|
|
|
|
|
context_transformers: list[Callable[[T_CONTEXT_DATA], EvaluationContextDict]]
|
|
context_transformers: list[Callable[[T_CONTEXT_DATA], EvaluationContextDict]]
|
|
exception_handler: Callable[[Exception], Any] | None
|
|
exception_handler: Callable[[Exception], Any] | None
|
|
|
|
+ __identity_fields: set[str]
|
|
|
|
|
|
def __init__(self):
|
|
def __init__(self):
|
|
self.context_transformers = []
|
|
self.context_transformers = []
|
|
self.exception_handler = None
|
|
self.exception_handler = None
|
|
|
|
+ self.__identity_fields = set()
|
|
|
|
|
|
def add_context_transformer(
|
|
def add_context_transformer(
|
|
- self, context_transformer: Callable[[T_CONTEXT_DATA], EvaluationContextDict]
|
|
|
|
|
|
+ self,
|
|
|
|
+ context_transformer: Callable[[T_CONTEXT_DATA], EvaluationContextDict],
|
|
|
|
+ identity_fields: list[str] | None = None,
|
|
) -> ContextBuilder[T_CONTEXT_DATA]:
|
|
) -> ContextBuilder[T_CONTEXT_DATA]:
|
|
self.context_transformers.append(context_transformer)
|
|
self.context_transformers.append(context_transformer)
|
|
|
|
+ if identity_fields is not None:
|
|
|
|
+ self.__identity_fields.update(identity_fields)
|
|
|
|
+
|
|
return self
|
|
return self
|
|
|
|
|
|
def add_exception_handler(
|
|
def add_exception_handler(
|
|
@@ -107,4 +140,4 @@ class ContextBuilder(Generic[T_CONTEXT_DATA]):
|
|
else:
|
|
else:
|
|
raise
|
|
raise
|
|
|
|
|
|
- return EvaluationContext(context_data)
|
|
|
|
|
|
+ return EvaluationContext(context_data, self.__identity_fields)
|