From e0194224497a284ecab6da3922c39c3b3c0a62ae Mon Sep 17 00:00:00 2001 From: Andrew Helsby Date: Wed, 2 Nov 2022 13:45:48 +0400 Subject: [PATCH 01/15] 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 02/15] 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 03/15] 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 04/15] 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 05/15] 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 06/15] 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 07/15] 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 08/15] 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 78fb954341333949814bfb9e7c670157fb4b76af Mon Sep 17 00:00:00 2001 From: Andrew Helsby Date: Wed, 2 Nov 2022 23:35:58 +0400 Subject: [PATCH 09/15] feature/spec-0.2.0: Ensure before_hooks are evaluated in the opposite order Signed-off-by: Andrew Helsby Signed-off-by: Andrew Helsby --- open_feature/open_feature_client.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/open_feature/open_feature_client.py b/open_feature/open_feature_client.py index 7b7d0098..f2479c27 100644 --- a/open_feature/open_feature_client.py +++ b/open_feature/open_feature_client.py @@ -258,7 +258,18 @@ def evaluate_flag_details( client_metadata=None, provider_metadata=None, ) + # Todo add api level hooks + # https://github.com/open-feature/spec/blob/main/specification/sections/04-hooks.md#requirement-442 + # Hooks need to be handled in different orders at different stages + # in the flag evaluation + # before: API, Client, Invocation, Provider merged_hooks = ( + self.hooks + + flag_evaluation_options.hooks + + self.provider.get_provider_hooks() + ) + # after, error, finally: Provider, Invocation, Client, API + reversed_merged_hooks = ( self.provider.get_provider_hooks() + flag_evaluation_options.hooks + self.hooks @@ -285,12 +296,14 @@ def evaluate_flag_details( merged_context, ) - after_hooks(flag_type, hook_context, flag_evaluation, merged_hooks, None) + after_hooks( + flag_type, hook_context, flag_evaluation, reversed_merged_hooks, None + ) return flag_evaluation except OpenFeatureError as e: - error_hooks(flag_type, hook_context, e, merged_hooks, None) + error_hooks(flag_type, hook_context, e, reversed_merged_hooks, None) return FlagEvaluationDetails( flag_key=flag_key, value=default_value, @@ -301,7 +314,7 @@ def evaluate_flag_details( # Catch any type of exception here since the user can provide any exception # in the error hooks except Exception as e: # noqa - error_hooks(flag_type, hook_context, e, merged_hooks, None) + error_hooks(flag_type, hook_context, e, reversed_merged_hooks, None) error_message = getattr(e, "error_message", str(e)) return FlagEvaluationDetails( flag_key=flag_key, @@ -312,7 +325,7 @@ def evaluate_flag_details( ) finally: - after_all_hooks(flag_type, hook_context, merged_hooks, None) + after_all_hooks(flag_type, hook_context, reversed_merged_hooks, None) def _create_provider_evaluation( self, From c037b3131d09ad0cc12be51f9ecc536db69731b4 Mon Sep 17 00:00:00 2001 From: Andrew Helsby Date: Thu, 3 Nov 2022 16:14:00 +0400 Subject: [PATCH 10/15] feature/spec-0.2.0: Remove number flag evaluation in favour of strongly typed counterparts Signed-off-by: Andrew Helsby Signed-off-by: Andrew Helsby --- open_feature/flag_evaluation/flag_type.py | 7 ++--- open_feature/open_feature_client.py | 38 ++--------------------- open_feature/provider/no_op_provider.py | 17 ++++++++-- open_feature/provider/provider.py | 14 +++++++-- readme.md | 3 +- tests/provider/test_no_op_provider.py | 14 +++++++-- tests/test_open_feature_client.py | 3 -- 7 files changed, 46 insertions(+), 50 deletions(-) diff --git a/open_feature/flag_evaluation/flag_type.py b/open_feature/flag_evaluation/flag_type.py index eec9604c..52d19d05 100644 --- a/open_feature/flag_evaluation/flag_type.py +++ b/open_feature/flag_evaluation/flag_type.py @@ -4,7 +4,6 @@ class FlagType(Enum): BOOLEAN = 1 STRING = 2 - NUMBER = 3 - OBJECT = 4 - FLOAT = 5 - INTEGER = 6 + OBJECT = 3 + FLOAT = 4 + INTEGER = 5 diff --git a/open_feature/open_feature_client.py b/open_feature/open_feature_client.py index f2479c27..635a5417 100644 --- a/open_feature/open_feature_client.py +++ b/open_feature/open_feature_client.py @@ -1,6 +1,5 @@ import logging import typing -from numbers import Number from open_feature.evaluation_context.evaluation_context import EvaluationContext from open_feature.exception.exceptions import ( @@ -106,36 +105,6 @@ def get_string_details( flag_evaluation_options, ) - def get_number_value( - self, - flag_key: str, - default_value: Number, - evaluation_context: EvaluationContext = None, - flag_evaluation_options: typing.Any = None, - ) -> Number: - return self.evaluate_flag_details( - FlagType.NUMBER, - flag_key, - default_value, - evaluation_context, - flag_evaluation_options, - ).value - - def get_number_details( - self, - flag_key: str, - default_value: Number, - evaluation_context: EvaluationContext = None, - flag_evaluation_options: typing.Any = None, - ) -> FlagEvaluationDetails: - return self.evaluate_flag_details( - FlagType.NUMBER, - flag_key, - default_value, - evaluation_context, - flag_evaluation_options, - ) - def get_integer_value( self, flag_key: str, @@ -356,9 +325,8 @@ def _create_provider_evaluation( 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.INTEGER: self.provider.get_integer_details, + FlagType.FLOAT: self.provider.get_float_details, FlagType.OBJECT: self.provider.get_object_details, FlagType.STRING: self.provider.get_string_details, }.get(flag_type) @@ -374,7 +342,7 @@ def _create_provider_evaluation( return value @staticmethod - def _convert_numeric_types(flag_type: FlagType, current_value: Number): + def _convert_numeric_types(flag_type: FlagType, current_value): converter = { FlagType.FLOAT: float, FlagType.INTEGER: int, diff --git a/open_feature/provider/no_op_provider.py b/open_feature/provider/no_op_provider.py index 8dd210d5..8916ffd0 100644 --- a/open_feature/provider/no_op_provider.py +++ b/open_feature/provider/no_op_provider.py @@ -45,9 +45,22 @@ def get_string_details( variant=PASSED_IN_DEFAULT, ) - def get_number_details( + def get_integer_details( self, - flag_key: str, + flag_key: int, + default_value: Number, + evaluation_context: EvaluationContext = None, + ): + return FlagEvaluationDetails( + flag_key=flag_key, + value=default_value, + reason=Reason.DEFAULT, + variant=PASSED_IN_DEFAULT, + ) + + def get_float_details( + self, + flag_key: float, default_value: Number, evaluation_context: EvaluationContext = None, ): diff --git a/open_feature/provider/provider.py b/open_feature/provider/provider.py index 957beb14..e93b221b 100644 --- a/open_feature/provider/provider.py +++ b/open_feature/provider/provider.py @@ -1,6 +1,5 @@ import typing from abc import abstractmethod -from numbers import Number from open_feature.evaluation_context.evaluation_context import EvaluationContext from open_feature.hooks.hook import Hook @@ -35,10 +34,19 @@ def get_string_details( pass @abstractmethod - def get_number_details( + def get_integer_details( self, flag_key: str, - default_value: Number, + default_value: int, + evaluation_context: EvaluationContext = EvaluationContext(), + ): + pass + + @abstractmethod + def get_float_details( + self, + flag_key: str, + default_value: float, evaluation_context: EvaluationContext = EvaluationContext(), ): pass diff --git a/readme.md b/readme.md index 64405d0a..9486b40d 100644 --- a/readme.md +++ b/readme.md @@ -19,7 +19,8 @@ While Boolean provides the simplest introduction, we offer a variety of flag typ # Depending on the flag type, use one of the methods below flag_key = "PROVIDER_FLAG" boolean_result = open_feature_client.get_boolean_value(key=flag_key,default_value=False) -number_result = open_feature_client.get_number_value(key=flag_key,default_value=-1) +integer_result = open_feature_client.get_integer_value(key=flag_key,default_value=-1) +float_result = open_feature_client.get_float_value(key=flag_key,default_value=-1) string_result = open_feature_client.get_string_value(key=flag_key,default_value="") object_result = open_feature_client.get_object_value(key=flag_key,default_value={}) ``` diff --git a/tests/provider/test_no_op_provider.py b/tests/provider/test_no_op_provider.py index 1e1e4b96..a615bb5c 100644 --- a/tests/provider/test_no_op_provider.py +++ b/tests/provider/test_no_op_provider.py @@ -23,16 +23,26 @@ def test_should_get_boolean_flag_from_no_op(): assert isinstance(flag.value, bool) -def test_should_get_number_flag_from_no_op(): +def test_should_get_integer_flag_from_no_op(): # Given # When - flag = NoOpProvider().get_number_details(flag_key="Key", default_value=100) + flag = NoOpProvider().get_integer_details(flag_key="Key", default_value=100) # Then assert flag is not None assert flag.value == 100 assert isinstance(flag.value, Number) +def test_should_get_float_flag_from_no_op(): + # Given + # When + flag = NoOpProvider().get_float_details(flag_key="Key", default_value=10.23) + # Then + assert flag is not None + assert flag.value == 10.23 + assert isinstance(flag.value, Number) + + def test_should_get_string_flag_from_no_op(): # Given # When diff --git a/tests/test_open_feature_client.py b/tests/test_open_feature_client.py index 538ca05f..c5ff42f1 100644 --- a/tests/test_open_feature_client.py +++ b/tests/test_open_feature_client.py @@ -1,4 +1,3 @@ -from numbers import Number from unittest.mock import MagicMock import pytest @@ -14,7 +13,6 @@ ( (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"), ( @@ -47,7 +45,6 @@ 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"), ( From 9fe8a5f6bad17fd986bbb6310bcdfe9af8f30601 Mon Sep 17 00:00:00 2001 From: Andrew Helsby Date: Thu, 3 Nov 2022 17:49:29 +0400 Subject: [PATCH 11/15] feature/spec-0.2.0: Check flag type after provider response Signed-off-by: Andrew Helsby Signed-off-by: Andrew Helsby --- open_feature/flag_evaluation/flag_type.py | 10 +++++----- open_feature/open_feature_client.py | 14 +++----------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/open_feature/flag_evaluation/flag_type.py b/open_feature/flag_evaluation/flag_type.py index 52d19d05..1deada9a 100644 --- a/open_feature/flag_evaluation/flag_type.py +++ b/open_feature/flag_evaluation/flag_type.py @@ -2,8 +2,8 @@ class FlagType(Enum): - BOOLEAN = 1 - STRING = 2 - OBJECT = 3 - FLOAT = 4 - INTEGER = 5 + BOOLEAN = bool + STRING = str + OBJECT = dict + FLOAT = float + INTEGER = int diff --git a/open_feature/open_feature_client.py b/open_feature/open_feature_client.py index 635a5417..e043e368 100644 --- a/open_feature/open_feature_client.py +++ b/open_feature/open_feature_client.py @@ -336,19 +336,11 @@ 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) + self._check_flag_type(flag_type, value.value) return value @staticmethod - def _convert_numeric_types(flag_type: FlagType, current_value): - converter = { - FlagType.FLOAT: float, - FlagType.INTEGER: int, - }.get(flag_type) - - try: - return converter(current_value) - except ValueError: + def _check_flag_type(flag_type: FlagType, current_value): + if not isinstance(current_value, flag_type.value): raise TypeMismatchError() From e350ec93bfd2bb28831fbe5bdb97fdec92059611 Mon Sep 17 00:00:00 2001 From: Matthew Elwell Date: Fri, 4 Nov 2022 16:38:22 +0000 Subject: [PATCH 12/15] feature/spec-0.2.0: Remove unnecessary static method Signed-off-by: Matthew Elwell --- open_feature/open_feature_client.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/open_feature/open_feature_client.py b/open_feature/open_feature_client.py index e043e368..db893395 100644 --- a/open_feature/open_feature_client.py +++ b/open_feature/open_feature_client.py @@ -336,11 +336,7 @@ def _create_provider_evaluation( value = get_details_callable(*args) - self._check_flag_type(flag_type, value.value) + if not isinstance(value.value, flag_type.value): + raise TypeMismatchError() return value - - @staticmethod - def _check_flag_type(flag_type: FlagType, current_value): - if not isinstance(current_value, flag_type.value): - raise TypeMismatchError() From 534a95d49c43bbcb750192bb6cfdb03c5fb5ed7f Mon Sep 17 00:00:00 2001 From: Matthew Elwell Date: Fri, 4 Nov 2022 16:39:05 +0000 Subject: [PATCH 13/15] feature/spec-0.2.0: Update docstring parameter name Signed-off-by: Matthew Elwell --- open_feature/open_feature_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/open_feature/open_feature_client.py b/open_feature/open_feature_client.py index db893395..11b1bd46 100644 --- a/open_feature/open_feature_client.py +++ b/open_feature/open_feature_client.py @@ -205,7 +205,7 @@ def evaluate_flag_details( Evaluate the flag requested by the user from the clients provider. :param flag_type: the type of the flag being returned - :param key: the string key of the selected flag + :param flag_key: the string key of the selected flag :param default_value: backup value returned if no result found by the provider :param evaluation_context: Information for the purposes of flag evaluation :param flag_evaluation_options: Additional flag evaluation information From aa8f6fc4877c1eceab15df3d0b6d4d5a7f9d46b2 Mon Sep 17 00:00:00 2001 From: Andrew Helsby Date: Wed, 9 Nov 2022 17:33:23 +0400 Subject: [PATCH 14/15] feature/spec-0.2.0: Fix typing of int and float methods Signed-off-by: Andrew Helsby Signed-off-by: Andrew Helsby --- open_feature/open_feature_client.py | 20 ++++++++++---------- open_feature/provider/no_op_provider.py | 9 ++++----- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/open_feature/open_feature_client.py b/open_feature/open_feature_client.py index e043e368..eacba5b8 100644 --- a/open_feature/open_feature_client.py +++ b/open_feature/open_feature_client.py @@ -50,7 +50,7 @@ def get_boolean_value( flag_key: str, default_value: bool, evaluation_context: EvaluationContext = None, - flag_evaluation_options: typing.Any = None, + flag_evaluation_options: FlagEvaluationOptions = None, ) -> bool: return self.evaluate_flag_details( FlagType.BOOLEAN, @@ -65,7 +65,7 @@ def get_boolean_details( flag_key: str, default_value: bool, evaluation_context: EvaluationContext = None, - flag_evaluation_options: typing.Any = None, + flag_evaluation_options: FlagEvaluationOptions = None, ) -> FlagEvaluationDetails: return self.evaluate_flag_details( FlagType.BOOLEAN, @@ -80,7 +80,7 @@ def get_string_value( flag_key: str, default_value: str, evaluation_context: EvaluationContext = None, - flag_evaluation_options: typing.Any = None, + flag_evaluation_options: FlagEvaluationOptions = None, ) -> str: return self.evaluate_flag_details( FlagType.STRING, @@ -95,7 +95,7 @@ def get_string_details( flag_key: str, default_value: str, evaluation_context: EvaluationContext = None, - flag_evaluation_options: typing.Any = None, + flag_evaluation_options: FlagEvaluationOptions = None, ) -> FlagEvaluationDetails: return self.evaluate_flag_details( FlagType.STRING, @@ -110,7 +110,7 @@ def get_integer_value( flag_key: str, default_value: int, evaluation_context: EvaluationContext = None, - flag_evaluation_options: typing.Any = None, + flag_evaluation_options: FlagEvaluationOptions = None, ) -> int: return self.get_integer_details( flag_key, @@ -124,7 +124,7 @@ def get_integer_details( flag_key: str, default_value: int, evaluation_context: EvaluationContext = None, - flag_evaluation_options: typing.Any = None, + flag_evaluation_options: FlagEvaluationOptions = None, ) -> FlagEvaluationDetails: return self.evaluate_flag_details( FlagType.INTEGER, @@ -139,7 +139,7 @@ def get_float_value( flag_key: str, default_value: float, evaluation_context: EvaluationContext = None, - flag_evaluation_options: typing.Any = None, + flag_evaluation_options: FlagEvaluationOptions = None, ) -> float: return self.get_float_details( flag_key, @@ -153,7 +153,7 @@ def get_float_details( flag_key: str, default_value: float, evaluation_context: EvaluationContext = None, - flag_evaluation_options: typing.Any = None, + flag_evaluation_options: FlagEvaluationOptions = None, ) -> FlagEvaluationDetails: return self.evaluate_flag_details( FlagType.FLOAT, @@ -168,7 +168,7 @@ def get_object_value( flag_key: str, default_value: dict, evaluation_context: EvaluationContext = None, - flag_evaluation_options: typing.Any = None, + flag_evaluation_options: FlagEvaluationOptions = None, ) -> dict: return self.evaluate_flag_details( FlagType.OBJECT, @@ -183,7 +183,7 @@ def get_object_details( flag_key: str, default_value: dict, evaluation_context: EvaluationContext = None, - flag_evaluation_options: typing.Any = None, + flag_evaluation_options: FlagEvaluationOptions = None, ) -> FlagEvaluationDetails: return self.evaluate_flag_details( FlagType.OBJECT, diff --git a/open_feature/provider/no_op_provider.py b/open_feature/provider/no_op_provider.py index 8916ffd0..67a336ab 100644 --- a/open_feature/provider/no_op_provider.py +++ b/open_feature/provider/no_op_provider.py @@ -1,5 +1,4 @@ import typing -from numbers import Number from open_feature.evaluation_context.evaluation_context import EvaluationContext from open_feature.flag_evaluation.flag_evaluation_details import FlagEvaluationDetails @@ -47,8 +46,8 @@ def get_string_details( def get_integer_details( self, - flag_key: int, - default_value: Number, + flag_key: str, + default_value: int, evaluation_context: EvaluationContext = None, ): return FlagEvaluationDetails( @@ -60,8 +59,8 @@ def get_integer_details( def get_float_details( self, - flag_key: float, - default_value: Number, + flag_key: str, + default_value: float, evaluation_context: EvaluationContext = None, ): return FlagEvaluationDetails( From 7a1aff2fd17a3d669bbeea232af35503403d2a43 Mon Sep 17 00:00:00 2001 From: Andrew Helsby Date: Wed, 9 Nov 2022 17:52:18 +0400 Subject: [PATCH 15/15] feature/spec-0.2.0: Change provider methods to resolves rather than gets Signed-off-by: Andrew Helsby Signed-off-by: Andrew Helsby --- open_feature/open_feature_client.py | 10 +++++----- open_feature/provider/no_op_provider.py | 10 +++++----- open_feature/provider/provider.py | 10 +++++----- tests/provider/test_no_op_provider.py | 22 ++++++++++++---------- 4 files changed, 27 insertions(+), 25 deletions(-) diff --git a/open_feature/open_feature_client.py b/open_feature/open_feature_client.py index 26054fe1..bd8c54ce 100644 --- a/open_feature/open_feature_client.py +++ b/open_feature/open_feature_client.py @@ -324,11 +324,11 @@ def _create_provider_evaluation( self.provider = NoOpProvider() get_details_callable = { - FlagType.BOOLEAN: self.provider.get_boolean_details, - FlagType.INTEGER: self.provider.get_integer_details, - FlagType.FLOAT: self.provider.get_float_details, - FlagType.OBJECT: self.provider.get_object_details, - FlagType.STRING: self.provider.get_string_details, + FlagType.BOOLEAN: self.provider.resolve_boolean_details, + FlagType.INTEGER: self.provider.resolve_integer_details, + FlagType.FLOAT: self.provider.resolve_float_details, + FlagType.OBJECT: self.provider.resolve_object_details, + FlagType.STRING: self.provider.resolve_string_details, }.get(flag_type) if not get_details_callable: diff --git a/open_feature/provider/no_op_provider.py b/open_feature/provider/no_op_provider.py index 67a336ab..f3bdda4a 100644 --- a/open_feature/provider/no_op_provider.py +++ b/open_feature/provider/no_op_provider.py @@ -18,7 +18,7 @@ def get_metadata(self) -> Metadata: def get_provider_hooks(self) -> typing.List[Hook]: return [] - def get_boolean_details( + def resolve_boolean_details( self, flag_key: str, default_value: bool, @@ -31,7 +31,7 @@ def get_boolean_details( variant=PASSED_IN_DEFAULT, ) - def get_string_details( + def resolve_string_details( self, flag_key: str, default_value: str, @@ -44,7 +44,7 @@ def get_string_details( variant=PASSED_IN_DEFAULT, ) - def get_integer_details( + def resolve_integer_details( self, flag_key: str, default_value: int, @@ -57,7 +57,7 @@ def get_integer_details( variant=PASSED_IN_DEFAULT, ) - def get_float_details( + def resolve_float_details( self, flag_key: str, default_value: float, @@ -70,7 +70,7 @@ def get_float_details( variant=PASSED_IN_DEFAULT, ) - def get_object_details( + def resolve_object_details( self, flag_key: str, default_value: dict, diff --git a/open_feature/provider/provider.py b/open_feature/provider/provider.py index e93b221b..b755374f 100644 --- a/open_feature/provider/provider.py +++ b/open_feature/provider/provider.py @@ -16,7 +16,7 @@ def get_provider_hooks(self) -> typing.List[Hook]: return [] @abstractmethod - def get_boolean_details( + def resolve_boolean_details( self, flag_key: str, default_value: bool, @@ -25,7 +25,7 @@ def get_boolean_details( pass @abstractmethod - def get_string_details( + def resolve_string_details( self, flag_key: str, default_value: str, @@ -34,7 +34,7 @@ def get_string_details( pass @abstractmethod - def get_integer_details( + def resolve_integer_details( self, flag_key: str, default_value: int, @@ -43,7 +43,7 @@ def get_integer_details( pass @abstractmethod - def get_float_details( + def resolve_float_details( self, flag_key: str, default_value: float, @@ -52,7 +52,7 @@ def get_float_details( pass @abstractmethod - def get_object_details( + def resolve_object_details( self, flag_key: str, default_value: dict, diff --git a/tests/provider/test_no_op_provider.py b/tests/provider/test_no_op_provider.py index a615bb5c..26e9a9dc 100644 --- a/tests/provider/test_no_op_provider.py +++ b/tests/provider/test_no_op_provider.py @@ -13,47 +13,47 @@ def test_should_return_no_op_provider_metadata(): assert metadata.is_default_provider -def test_should_get_boolean_flag_from_no_op(): +def test_should_resolve_boolean_flag_from_no_op(): # Given # When - flag = NoOpProvider().get_boolean_details(flag_key="Key", default_value=True) + flag = NoOpProvider().resolve_boolean_details(flag_key="Key", default_value=True) # Then assert flag is not None assert flag.value assert isinstance(flag.value, bool) -def test_should_get_integer_flag_from_no_op(): +def test_should_resolve_integer_flag_from_no_op(): # Given # When - flag = NoOpProvider().get_integer_details(flag_key="Key", default_value=100) + flag = NoOpProvider().resolve_integer_details(flag_key="Key", default_value=100) # Then assert flag is not None assert flag.value == 100 assert isinstance(flag.value, Number) -def test_should_get_float_flag_from_no_op(): +def test_should_resolve_float_flag_from_no_op(): # Given # When - flag = NoOpProvider().get_float_details(flag_key="Key", default_value=10.23) + flag = NoOpProvider().resolve_float_details(flag_key="Key", default_value=10.23) # Then assert flag is not None assert flag.value == 10.23 assert isinstance(flag.value, Number) -def test_should_get_string_flag_from_no_op(): +def test_should_resolve_string_flag_from_no_op(): # Given # When - flag = NoOpProvider().get_string_details(flag_key="Key", default_value="String") + flag = NoOpProvider().resolve_string_details(flag_key="Key", default_value="String") # Then assert flag is not None assert flag.value == "String" assert isinstance(flag.value, str) -def test_should_get_object_flag_from_no_op(): +def test_should_resolve_object_flag_from_no_op(): # Given return_value = { "String": "string", @@ -61,7 +61,9 @@ def test_should_get_object_flag_from_no_op(): "Boolean": True, } # When - flag = NoOpProvider().get_object_details(flag_key="Key", default_value=return_value) + flag = NoOpProvider().resolve_object_details( + flag_key="Key", default_value=return_value + ) # Then assert flag is not None assert flag.value == return_value