Skip to content

feat: spec-0.2.0 #38

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Nov 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e019422
fix/unit-tests: Add float and int flag methods
ajhelsby Nov 2, 2022
c3b6394
fix/unit-tests: Add the ability for a provider to have hooks
ajhelsby Nov 2, 2022
4fbf449
fix/unit-tests: Flag evaluation options added for hook merging
ajhelsby Nov 2, 2022
7587d30
fix/unit-tests: Move numeric type methods to a private method within …
ajhelsby Nov 2, 2022
145a15b
Merge branch 'main' into feature/spec-0.2.0
ajhelsby Nov 2, 2022
769c7dc
feature/spec-0.2.0: Add tests for new numeric methods
ajhelsby Nov 2, 2022
fbf19eb
feature/spec-0.2.0: Overwrite init on FlagEvaluationOptions
ajhelsby Nov 2, 2022
1c3d840
feature/spec-0.2.0: Overwrite init on FlagEvaluationOptions
ajhelsby Nov 2, 2022
5c247fd
feature/spec-0.2.0: Remove init on FlagEvaluationOptions
ajhelsby Nov 2, 2022
78fb954
feature/spec-0.2.0: Ensure before_hooks are evaluated in the opposite…
ajhelsby Nov 2, 2022
c037b31
feature/spec-0.2.0: Remove number flag evaluation in favour of strong…
ajhelsby Nov 3, 2022
9fe8a5f
feature/spec-0.2.0: Check flag type after provider response
ajhelsby Nov 3, 2022
e350ec9
feature/spec-0.2.0: Remove unnecessary static method
matthewelwell Nov 4, 2022
534a95d
feature/spec-0.2.0: Update docstring parameter name
matthewelwell Nov 4, 2022
aa8f6fc
feature/spec-0.2.0: Fix typing of int and float methods
ajhelsby Nov 9, 2022
c053c9f
Merge remote-tracking branch 'origin/feature/spec-0.2.0' into feature…
ajhelsby Nov 9, 2022
7a1aff2
feature/spec-0.2.0: Change provider methods to resolves rather than gets
ajhelsby Nov 9, 2022
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
10 changes: 10 additions & 0 deletions open_feature/flag_evaluation/flag_evaluation_options.py
Original file line number Diff line number Diff line change
@@ -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)
9 changes: 5 additions & 4 deletions open_feature/flag_evaluation/flag_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@


class FlagType(Enum):
BOOLEAN = 1
STRING = 2
NUMBER = 3
OBJECT = 4
BOOLEAN = bool
STRING = str
OBJECT = dict
FLOAT = float
INTEGER = int
120 changes: 90 additions & 30 deletions open_feature/open_feature_client.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import logging
import typing
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
Expand All @@ -20,6 +24,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__(
Expand All @@ -44,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,
Expand All @@ -59,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,
Expand All @@ -74,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,
Expand All @@ -89,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,
Expand All @@ -99,30 +105,58 @@ def get_string_details(
flag_evaluation_options,
)

def get_number_value(
def get_integer_value(
self,
flag_key: str,
default_value: int,
evaluation_context: EvaluationContext = None,
flag_evaluation_options: FlagEvaluationOptions = 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: Number,
default_value: int,
evaluation_context: EvaluationContext = None,
flag_evaluation_options: typing.Any = None,
) -> Number:
flag_evaluation_options: FlagEvaluationOptions = None,
) -> FlagEvaluationDetails:
return self.evaluate_flag_details(
FlagType.NUMBER,
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: FlagEvaluationOptions = None,
) -> float:
return self.get_float_details(
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
).value

def get_number_details(
def get_float_details(
self,
flag_key: str,
default_value: Number,
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.NUMBER,
FlagType.FLOAT,
flag_key,
default_value,
evaluation_context,
Expand All @@ -134,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,
Expand All @@ -149,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,
Expand All @@ -165,13 +199,13 @@ 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.

: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
Expand All @@ -182,6 +216,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,
Expand All @@ -190,7 +227,22 @@ def evaluate_flag_details(
client_metadata=None,
provider_metadata=None,
)
merged_hooks = self.hooks
# 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
)

try:
# https://github.com/open-feature/spec/blob/main/specification/sections/03-evaluation-context.md
Expand All @@ -201,7 +253,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)
)
Expand All @@ -213,12 +265,14 @@ def evaluate_flag_details(
merged_context,
)

after_hooks(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,
Expand All @@ -229,7 +283,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,
Expand All @@ -240,7 +294,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,
Expand Down Expand Up @@ -270,13 +324,19 @@ def _create_provider_evaluation(
self.provider = NoOpProvider()

get_details_callable = {
FlagType.BOOLEAN: self.provider.get_boolean_details,
FlagType.NUMBER: self.provider.get_number_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:
raise GeneralError(error_message="Unknown flag type")

return get_details_callable(*args)
value = get_details_callable(*args)

if not isinstance(value.value, flag_type.value):
raise TypeMismatchError()

return value
29 changes: 23 additions & 6 deletions open_feature/provider/no_op_provider.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from numbers import Number
import typing

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
Expand All @@ -14,7 +15,10 @@ class NoOpProvider(AbstractProvider):
def get_metadata(self) -> Metadata:
return NoOpMetadata()

def get_boolean_details(
def get_provider_hooks(self) -> typing.List[Hook]:
return []

def resolve_boolean_details(
self,
flag_key: str,
default_value: bool,
Expand All @@ -27,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,
Expand All @@ -40,10 +44,23 @@ def get_string_details(
variant=PASSED_IN_DEFAULT,
)

def get_number_details(
def resolve_integer_details(
self,
flag_key: str,
default_value: int,
evaluation_context: EvaluationContext = None,
):
return FlagEvaluationDetails(
flag_key=flag_key,
value=default_value,
reason=Reason.DEFAULT,
variant=PASSED_IN_DEFAULT,
)

def resolve_float_details(
self,
flag_key: str,
default_value: Number,
default_value: float,
evaluation_context: EvaluationContext = None,
):
return FlagEvaluationDetails(
Expand All @@ -53,7 +70,7 @@ def get_number_details(
variant=PASSED_IN_DEFAULT,
)

def get_object_details(
def resolve_object_details(
self,
flag_key: str,
default_value: dict,
Expand Down
Loading