From e0194224497a284ecab6da3922c39c3b3c0a62ae Mon Sep 17 00:00:00 2001 From: Andrew Helsby Date: Wed, 2 Nov 2022 13:45:48 +0400 Subject: [PATCH 1/9] fix/unit-tests: Add float and int flag methods Signed-off-by: Andrew Helsby Signed-off-by: Andrew Helsby --- open_feature/flag_evaluation/flag_type.py | 2 + open_feature/open_feature_client.py | 70 ++++++++++++++++++++++- 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/open_feature/flag_evaluation/flag_type.py b/open_feature/flag_evaluation/flag_type.py index 1fd52d7c..eec9604c 100644 --- a/open_feature/flag_evaluation/flag_type.py +++ b/open_feature/flag_evaluation/flag_type.py @@ -6,3 +6,5 @@ class FlagType(Enum): STRING = 2 NUMBER = 3 OBJECT = 4 + FLOAT = 5 + INTEGER = 6 diff --git a/open_feature/open_feature_client.py b/open_feature/open_feature_client.py index a685f4b7..136738fe 100644 --- a/open_feature/open_feature_client.py +++ b/open_feature/open_feature_client.py @@ -129,6 +129,64 @@ def get_number_details( flag_evaluation_options, ) + def get_integer_value( + self, + key: str, + default_value: int, + evaluation_context: EvaluationContext = None, + flag_evaluation_options: typing.Any = None, + ) -> int: + return self.get_integer_details( + key, + default_value, + evaluation_context, + flag_evaluation_options, + ).value + + def get_integer_details( + self, + key: str, + default_value: int, + evaluation_context: EvaluationContext = None, + flag_evaluation_options: typing.Any = None, + ) -> FlagEvaluationDetails: + return self.evaluate_flag_details( + FlagType.INTEGER, + key, + default_value, + evaluation_context, + flag_evaluation_options, + ) + + def get_float_value( + self, + key: str, + default_value: float, + evaluation_context: EvaluationContext = None, + flag_evaluation_options: typing.Any = None, + ) -> float: + return self.get_float_details( + key, + default_value, + evaluation_context, + flag_evaluation_options, + ).value + + def get_float_details( + self, + key: str, + default_value: float, + evaluation_context: EvaluationContext = None, + flag_evaluation_options: typing.Any = None, + ) -> FlagEvaluationDetails: + return self.evaluate_flag_details( + FlagType.FLOAT, + key, + default_value, + evaluation_context, + flag_evaluation_options, + ) + def get_object_value( self, flag_key: str, @@ -269,13 +327,23 @@ def create_provider_evaluation( logging.info("No provider configured, using no-op provider.") self.provider = NoOpProvider() - get_details_callable = { + value = get_details_callable = { FlagType.BOOLEAN: self.provider.get_boolean_details, FlagType.NUMBER: self.provider.get_number_details, + FlagType.INTEGER: self.provider.get_number_details, + FlagType.FLOAT: self.provider.get_number_details, FlagType.OBJECT: self.provider.get_object_details, FlagType.STRING: self.provider.get_string_details, }.get(flag_type) + converter = { + FlagType.FLOAT: float, + FlagType.INTEGER: int, + }.get(flag_type) + + if converter: + value.value = converter(value.value) + if not get_details_callable: raise GeneralError(error_message="Unknown flag type") From c3b63949e8ed4d8039c9a4a85020ade0a5f7549e Mon Sep 17 00:00:00 2001 From: Andrew Helsby Date: Wed, 2 Nov 2022 14:29:28 +0400 Subject: [PATCH 2/9] fix/unit-tests: Add the ability for a provider to have hooks Signed-off-by: Andrew Helsby Signed-off-by: Andrew Helsby --- open_feature/open_feature_client.py | 20 +++++++++++++------- open_feature/provider/provider.py | 5 +++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/open_feature/open_feature_client.py b/open_feature/open_feature_client.py index 136738fe..3b39c1d3 100644 --- a/open_feature/open_feature_client.py +++ b/open_feature/open_feature_client.py @@ -3,7 +3,11 @@ from numbers import Number from open_feature.evaluation_context.evaluation_context import EvaluationContext -from open_feature.exception.exceptions import GeneralError +from open_feature.exception.exceptions import ( + GeneralError, + OpenFeatureError, + TypeMismatchError, +) from open_feature.flag_evaluation.error_code import ErrorCode from open_feature.flag_evaluation.flag_evaluation_details import FlagEvaluationDetails from open_feature.flag_evaluation.flag_type import FlagType @@ -259,7 +263,7 @@ def evaluate_flag_details( ) invocation_context.merge(ctx2=evaluation_context) - # merge of: API.context, client.context, invocation.context + # Requirement 3.2.2 merge: API.context->client.context->invocation.context merged_context = ( api_evaluation_context().merge(self.context).merge(invocation_context) ) @@ -271,7 +275,7 @@ def evaluate_flag_details( merged_context, ) - after_hooks(type, hook_context, flag_evaluation, merged_hooks, None) + after_hooks(flag_type, hook_context, flag_evaluation, merged_hooks, None) return flag_evaluation @@ -327,7 +331,7 @@ def create_provider_evaluation( logging.info("No provider configured, using no-op provider.") self.provider = NoOpProvider() - value = get_details_callable = { + get_details_callable = { FlagType.BOOLEAN: self.provider.get_boolean_details, FlagType.NUMBER: self.provider.get_number_details, FlagType.INTEGER: self.provider.get_number_details, @@ -336,15 +340,17 @@ def create_provider_evaluation( FlagType.STRING: self.provider.get_string_details, }.get(flag_type) + value = get_details_callable(*args) + converter = { FlagType.FLOAT: float, FlagType.INTEGER: int, }.get(flag_type) - if converter: - value.value = converter(value.value) + if not isinstance(value.value, converter(value.value)): + raise TypeMismatchError() if not get_details_callable: raise GeneralError(error_message="Unknown flag type") - return get_details_callable(*args) + return value diff --git a/open_feature/provider/provider.py b/open_feature/provider/provider.py index db99373d..753bd2c4 100644 --- a/open_feature/provider/provider.py +++ b/open_feature/provider/provider.py @@ -2,6 +2,7 @@ from numbers import Number from open_feature.evaluation_context.evaluation_context import EvaluationContext +from open_feature.hooks.hook import Hook from open_feature.provider.metadata import Metadata @@ -10,6 +11,10 @@ class AbstractProvider: def get_metadata(self) -> Metadata: pass + @abstractmethod + def get_provider_hooks(self) -> list[Hook]: + return [] + @abstractmethod def get_boolean_details( self, From 4fbf449f887fbe002575422a386fc2a723dd832b Mon Sep 17 00:00:00 2001 From: Andrew Helsby Date: Wed, 2 Nov 2022 15:04:15 +0400 Subject: [PATCH 3/9] fix/unit-tests: Flag evaluation options added for hook merging Signed-off-by: Andrew Helsby Signed-off-by: Andrew Helsby --- .../flag_evaluation/flag_evaluation_options.py | 9 +++++++++ open_feature/open_feature_client.py | 12 ++++++++++-- open_feature/provider/no_op_provider.py | 4 ++++ 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 open_feature/flag_evaluation/flag_evaluation_options.py diff --git a/open_feature/flag_evaluation/flag_evaluation_options.py b/open_feature/flag_evaluation/flag_evaluation_options.py new file mode 100644 index 00000000..0864b279 --- /dev/null +++ b/open_feature/flag_evaluation/flag_evaluation_options.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass, field + +from open_feature.hooks.hook import Hook + + +@dataclass +class FlagEvaluationOptions: + hooks: list[Hook] = field(default_factory=list) + hook_hints: dict = field(default_factory=dict) diff --git a/open_feature/open_feature_client.py b/open_feature/open_feature_client.py index 3b39c1d3..687e38e0 100644 --- a/open_feature/open_feature_client.py +++ b/open_feature/open_feature_client.py @@ -10,6 +10,7 @@ ) from open_feature.flag_evaluation.error_code import ErrorCode from open_feature.flag_evaluation.flag_evaluation_details import FlagEvaluationDetails +from open_feature.flag_evaluation.flag_evaluation_options import FlagEvaluationOptions from open_feature.flag_evaluation.flag_type import FlagType from open_feature.flag_evaluation.reason import Reason from open_feature.hooks.hook import Hook @@ -227,7 +228,7 @@ def evaluate_flag_details( flag_key: str, default_value: typing.Any, evaluation_context: EvaluationContext = None, - flag_evaluation_options: typing.Any = None, + flag_evaluation_options: FlagEvaluationOptions = None, ) -> FlagEvaluationDetails: """ Evaluate the flag requested by the user from the clients provider. @@ -244,6 +245,9 @@ def evaluate_flag_details( if evaluation_context is None: evaluation_context = EvaluationContext() + if flag_evaluation_options is None: + flag_evaluation_options = FlagEvaluationOptions() + hook_context = HookContext( flag_key=flag_key, flag_type=flag_type, @@ -252,7 +256,11 @@ def evaluate_flag_details( client_metadata=None, provider_metadata=None, ) - merged_hooks = [] + merged_hooks = ( + self.provider.get_provider_hooks() + + flag_evaluation_options.hooks + + self.hooks + ) try: # https://github.com/open-feature/spec/blob/main/specification/sections/03-evaluation-context.md diff --git a/open_feature/provider/no_op_provider.py b/open_feature/provider/no_op_provider.py index 8e80786a..06874b48 100644 --- a/open_feature/provider/no_op_provider.py +++ b/open_feature/provider/no_op_provider.py @@ -3,6 +3,7 @@ from open_feature.evaluation_context.evaluation_context import EvaluationContext from open_feature.flag_evaluation.flag_evaluation_details import FlagEvaluationDetails from open_feature.flag_evaluation.reason import Reason +from open_feature.hooks.hook import Hook from open_feature.provider.metadata import Metadata from open_feature.provider.no_op_metadata import NoOpMetadata from open_feature.provider.provider import AbstractProvider @@ -14,6 +15,9 @@ class NoOpProvider(AbstractProvider): def get_metadata(self) -> Metadata: return NoOpMetadata() + def get_provider_hooks(self) -> list[Hook]: + return [] + def get_boolean_details( self, flag_key: str, From 7587d30e734ee70dd07eb172f86841ecfc12c656 Mon Sep 17 00:00:00 2001 From: Andrew Helsby Date: Wed, 2 Nov 2022 15:58:38 +0400 Subject: [PATCH 4/9] fix/unit-tests: Move numeric type methods to a private method within the client Signed-off-by: Andrew Helsby Signed-off-by: Andrew Helsby --- open_feature/open_feature_client.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/open_feature/open_feature_client.py b/open_feature/open_feature_client.py index 687e38e0..89df7a52 100644 --- a/open_feature/open_feature_client.py +++ b/open_feature/open_feature_client.py @@ -25,6 +25,8 @@ from open_feature.provider.no_op_provider import NoOpProvider from open_feature.provider.provider import AbstractProvider +NUMERIC_TYPES = [FlagType.FLOAT, FlagType.INTEGER] + class OpenFeatureClient: def __init__( @@ -350,15 +352,22 @@ def create_provider_evaluation( value = get_details_callable(*args) + if flag_type in NUMERIC_TYPES: + value.value = self._convert_numeric_types(flag_type, value.value) + + if not get_details_callable: + raise GeneralError(error_message="Unknown flag type") + + return value + + @staticmethod + def _convert_numeric_types(flag_type: FlagType, current_value: Number): converter = { FlagType.FLOAT: float, FlagType.INTEGER: int, }.get(flag_type) - if not isinstance(value.value, converter(value.value)): + try: + return converter(current_value) + except ValueError: raise TypeMismatchError() - - if not get_details_callable: - raise GeneralError(error_message="Unknown flag type") - - return value From 769c7dc5f1ac97922dbb3bbdacec79fa91cbcd5a Mon Sep 17 00:00:00 2001 From: Andrew Helsby Date: Wed, 2 Nov 2022 16:41:48 +0400 Subject: [PATCH 5/9] feature/spec-0.2.0: Add tests for new numeric methods Signed-off-by: Andrew Helsby Signed-off-by: Andrew Helsby --- open_feature/open_feature_client.py | 22 +++++++++++----------- tests/test_open_feature_client.py | 4 ++++ 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/open_feature/open_feature_client.py b/open_feature/open_feature_client.py index a792c378..7b7d0098 100644 --- a/open_feature/open_feature_client.py +++ b/open_feature/open_feature_client.py @@ -138,13 +138,13 @@ def get_number_details( def get_integer_value( self, - key: str, + flag_key: str, default_value: int, evaluation_context: EvaluationContext = None, flag_evaluation_options: typing.Any = None, ) -> int: return self.get_integer_details( - key, + flag_key, default_value, evaluation_context, flag_evaluation_options, @@ -152,14 +152,14 @@ def get_integer_value( def get_integer_details( self, - key: str, + flag_key: str, default_value: int, evaluation_context: EvaluationContext = None, flag_evaluation_options: typing.Any = None, ) -> FlagEvaluationDetails: return self.evaluate_flag_details( FlagType.INTEGER, - key, + flag_key, default_value, evaluation_context, flag_evaluation_options, @@ -167,13 +167,13 @@ def get_integer_details( def get_float_value( self, - key: str, + flag_key: str, default_value: float, evaluation_context: EvaluationContext = None, flag_evaluation_options: typing.Any = None, ) -> float: return self.get_float_details( - key, + flag_key, default_value, evaluation_context, flag_evaluation_options, @@ -181,14 +181,14 @@ def get_float_value( def get_float_details( self, - key: str, + flag_key: str, default_value: float, evaluation_context: EvaluationContext = None, flag_evaluation_options: typing.Any = None, ) -> FlagEvaluationDetails: return self.evaluate_flag_details( FlagType.FLOAT, - key, + flag_key, default_value, evaluation_context, flag_evaluation_options, @@ -350,14 +350,14 @@ def _create_provider_evaluation( FlagType.STRING: self.provider.get_string_details, }.get(flag_type) + if not get_details_callable: + raise GeneralError(error_message="Unknown flag type") + value = get_details_callable(*args) if flag_type in NUMERIC_TYPES: value.value = self._convert_numeric_types(flag_type, value.value) - if not get_details_callable: - raise GeneralError(error_message="Unknown flag type") - return value @staticmethod diff --git a/tests/test_open_feature_client.py b/tests/test_open_feature_client.py index 9e7fb5ee..538ca05f 100644 --- a/tests/test_open_feature_client.py +++ b/tests/test_open_feature_client.py @@ -15,6 +15,8 @@ (bool, True, "get_boolean_value"), (str, "String", "get_string_value"), (Number, 100, "get_number_value"), + (int, 100, "get_integer_value"), + (float, 10.23, "get_float_value"), ( dict, { @@ -46,6 +48,8 @@ def test_should_get_flag_value_based_on_method_type( (bool, True, "get_boolean_details"), (str, "String", "get_string_details"), (Number, 100, "get_number_details"), + (int, 100, "get_integer_details"), + (float, 10.23, "get_float_details"), ( dict, { From fbf19eb8ad8806aae190f25268cdcd13909df78c Mon Sep 17 00:00:00 2001 From: Andrew Helsby Date: Wed, 2 Nov 2022 16:52:10 +0400 Subject: [PATCH 6/9] feature/spec-0.2.0: Overwrite init on FlagEvaluationOptions Signed-off-by: Andrew Helsby Signed-off-by: Andrew Helsby --- open_feature/flag_evaluation/flag_evaluation_options.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/open_feature/flag_evaluation/flag_evaluation_options.py b/open_feature/flag_evaluation/flag_evaluation_options.py index 0864b279..35a18e23 100644 --- a/open_feature/flag_evaluation/flag_evaluation_options.py +++ b/open_feature/flag_evaluation/flag_evaluation_options.py @@ -1,9 +1,14 @@ from dataclasses import dataclass, field +from typing import List from open_feature.hooks.hook import Hook @dataclass class FlagEvaluationOptions: - hooks: list[Hook] = field(default_factory=list) + hooks: List[Hook] = field(default_factory=list) hook_hints: dict = field(default_factory=dict) + + def __init__(self): + self.hooks = [] + self.hook_hints = {} From 1c3d840f95cae5aa1c251ad9ab79be22a9a5d2e1 Mon Sep 17 00:00:00 2001 From: Andrew Helsby Date: Wed, 2 Nov 2022 16:57:26 +0400 Subject: [PATCH 7/9] feature/spec-0.2.0: Overwrite init on FlagEvaluationOptions Signed-off-by: Andrew Helsby Signed-off-by: Andrew Helsby --- open_feature/flag_evaluation/flag_evaluation_options.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/open_feature/flag_evaluation/flag_evaluation_options.py b/open_feature/flag_evaluation/flag_evaluation_options.py index 35a18e23..1cdb11b6 100644 --- a/open_feature/flag_evaluation/flag_evaluation_options.py +++ b/open_feature/flag_evaluation/flag_evaluation_options.py @@ -1,13 +1,13 @@ -from dataclasses import dataclass, field -from typing import List +import typing +from dataclasses import dataclass from open_feature.hooks.hook import Hook @dataclass class FlagEvaluationOptions: - hooks: List[Hook] = field(default_factory=list) - hook_hints: dict = field(default_factory=dict) + hooks: typing.List[Hook] + hook_hints: dict def __init__(self): self.hooks = [] From 5c247fd3facfcd9a6ba6c4f771889e19d18c5d1a Mon Sep 17 00:00:00 2001 From: Andrew Helsby Date: Wed, 2 Nov 2022 17:01:09 +0400 Subject: [PATCH 8/9] feature/spec-0.2.0: Remove init on FlagEvaluationOptions Signed-off-by: Andrew Helsby Signed-off-by: Andrew Helsby --- .../flag_evaluation/flag_evaluation_options.py | 10 +++------- open_feature/provider/no_op_provider.py | 3 ++- open_feature/provider/provider.py | 3 ++- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/open_feature/flag_evaluation/flag_evaluation_options.py b/open_feature/flag_evaluation/flag_evaluation_options.py index 1cdb11b6..1a35542d 100644 --- a/open_feature/flag_evaluation/flag_evaluation_options.py +++ b/open_feature/flag_evaluation/flag_evaluation_options.py @@ -1,14 +1,10 @@ import typing -from dataclasses import dataclass +from dataclasses import dataclass, field from open_feature.hooks.hook import Hook @dataclass class FlagEvaluationOptions: - hooks: typing.List[Hook] - hook_hints: dict - - def __init__(self): - self.hooks = [] - self.hook_hints = {} + hooks: typing.List[Hook] = field(default_factory=list) + hook_hints: dict = field(default_factory=dict) diff --git a/open_feature/provider/no_op_provider.py b/open_feature/provider/no_op_provider.py index 06874b48..8dd210d5 100644 --- a/open_feature/provider/no_op_provider.py +++ b/open_feature/provider/no_op_provider.py @@ -1,3 +1,4 @@ +import typing from numbers import Number from open_feature.evaluation_context.evaluation_context import EvaluationContext @@ -15,7 +16,7 @@ class NoOpProvider(AbstractProvider): def get_metadata(self) -> Metadata: return NoOpMetadata() - def get_provider_hooks(self) -> list[Hook]: + def get_provider_hooks(self) -> typing.List[Hook]: return [] def get_boolean_details( diff --git a/open_feature/provider/provider.py b/open_feature/provider/provider.py index 753bd2c4..957beb14 100644 --- a/open_feature/provider/provider.py +++ b/open_feature/provider/provider.py @@ -1,3 +1,4 @@ +import typing from abc import abstractmethod from numbers import Number @@ -12,7 +13,7 @@ def get_metadata(self) -> Metadata: pass @abstractmethod - def get_provider_hooks(self) -> list[Hook]: + def get_provider_hooks(self) -> typing.List[Hook]: return [] @abstractmethod From f52913e853c8724c88f3c12972f62feecea2b43d Mon Sep 17 00:00:00 2001 From: Andrew Helsby Date: Wed, 2 Nov 2022 17:16:18 +0400 Subject: [PATCH 9/9] feature/spec-0.3.0: Update docs on merging contexts to reflect spec Signed-off-by: Andrew Helsby Signed-off-by: Andrew Helsby --- open_feature/open_feature_client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/open_feature/open_feature_client.py b/open_feature/open_feature_client.py index 7b7d0098..047eeed3 100644 --- a/open_feature/open_feature_client.py +++ b/open_feature/open_feature_client.py @@ -268,12 +268,11 @@ def evaluate_flag_details( # https://github.com/open-feature/spec/blob/main/specification/sections/03-evaluation-context.md # Any resulting evaluation context from a before hook will overwrite # duplicate fields defined globally, on the client, or in the invocation. + # Requirement 3.2.2, 4.3.4: API.context->client.context->invocation.context invocation_context = before_hooks( flag_type, hook_context, merged_hooks, None ) invocation_context.merge(ctx2=evaluation_context) - - # Requirement 3.2.2 merge: API.context->client.context->invocation.context merged_context = ( api_evaluation_context().merge(self.context).merge(invocation_context) )