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..1a35542d --- /dev/null +++ b/open_feature/flag_evaluation/flag_evaluation_options.py @@ -0,0 +1,10 @@ +import typing +from dataclasses import dataclass, field + +from open_feature.hooks.hook import Hook + + +@dataclass +class FlagEvaluationOptions: + hooks: typing.List[Hook] = field(default_factory=list) + hook_hints: dict = field(default_factory=dict) 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 8be455ef..047eeed3 100644 --- a/open_feature/open_feature_client.py +++ b/open_feature/open_feature_client.py @@ -3,9 +3,14 @@ from numbers import Number from open_feature.evaluation_context.evaluation_context import EvaluationContext -from open_feature.exception.exceptions import GeneralError, OpenFeatureError +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_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 @@ -20,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__( @@ -129,6 +136,64 @@ def get_number_details( flag_evaluation_options, ) + def get_integer_value( + self, + flag_key: str, + default_value: int, + evaluation_context: EvaluationContext = None, + flag_evaluation_options: typing.Any = None, + ) -> int: + return self.get_integer_details( + flag_key, + default_value, + evaluation_context, + flag_evaluation_options, + ).value + + def get_integer_details( + self, + flag_key: str, + default_value: int, + evaluation_context: EvaluationContext = None, + flag_evaluation_options: typing.Any = None, + ) -> FlagEvaluationDetails: + return self.evaluate_flag_details( + FlagType.INTEGER, + flag_key, + default_value, + evaluation_context, + flag_evaluation_options, + ) + + def get_float_value( + self, + flag_key: str, + default_value: float, + evaluation_context: EvaluationContext = None, + flag_evaluation_options: typing.Any = None, + ) -> float: + return self.get_float_details( + flag_key, + default_value, + evaluation_context, + flag_evaluation_options, + ).value + + def get_float_details( + self, + flag_key: str, + default_value: float, + evaluation_context: EvaluationContext = None, + flag_evaluation_options: typing.Any = None, + ) -> FlagEvaluationDetails: + return self.evaluate_flag_details( + FlagType.FLOAT, + flag_key, + default_value, + evaluation_context, + flag_evaluation_options, + ) + def get_object_value( self, flag_key: str, @@ -165,7 +230,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. @@ -182,6 +247,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, @@ -190,18 +258,21 @@ def evaluate_flag_details( client_metadata=None, provider_metadata=None, ) - merged_hooks = self.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 # 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) - - # merge of: API.context, client.context, invocation.context merged_context = ( api_evaluation_context().merge(self.context).merge(invocation_context) ) @@ -213,7 +284,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 @@ -272,6 +343,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.OBJECT: self.provider.get_object_details, FlagType.STRING: self.provider.get_string_details, }.get(flag_type) @@ -279,4 +352,21 @@ def _create_provider_evaluation( if not get_details_callable: raise GeneralError(error_message="Unknown flag type") - return get_details_callable(*args) + value = get_details_callable(*args) + + if flag_type in NUMERIC_TYPES: + value.value = self._convert_numeric_types(flag_type, value.value) + + return value + + @staticmethod + def _convert_numeric_types(flag_type: FlagType, current_value: Number): + converter = { + FlagType.FLOAT: float, + FlagType.INTEGER: int, + }.get(flag_type) + + try: + return converter(current_value) + except ValueError: + raise TypeMismatchError() diff --git a/open_feature/provider/no_op_provider.py b/open_feature/provider/no_op_provider.py index 8e80786a..8dd210d5 100644 --- a/open_feature/provider/no_op_provider.py +++ b/open_feature/provider/no_op_provider.py @@ -1,8 +1,10 @@ +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 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 +16,9 @@ class NoOpProvider(AbstractProvider): def get_metadata(self) -> Metadata: return NoOpMetadata() + def get_provider_hooks(self) -> typing.List[Hook]: + return [] + def get_boolean_details( self, flag_key: str, diff --git a/open_feature/provider/provider.py b/open_feature/provider/provider.py index db99373d..957beb14 100644 --- a/open_feature/provider/provider.py +++ b/open_feature/provider/provider.py @@ -1,7 +1,9 @@ +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 from open_feature.provider.metadata import Metadata @@ -10,6 +12,10 @@ class AbstractProvider: def get_metadata(self) -> Metadata: pass + @abstractmethod + def get_provider_hooks(self) -> typing.List[Hook]: + return [] + @abstractmethod def get_boolean_details( self, 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, {