-
Notifications
You must be signed in to change notification settings - Fork 21
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
feat: simplify async functionality to provider #385
Changes from all commits
e3f2dd0
fa380de
9548ad6
2960806
6c68a9e
341ba97
83c6f4a
8446b9b
e124794
6cfcb90
0d68182
d90190c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -33,6 +33,7 @@ | |||||||||
__all__ = [ | ||||||||||
"ClientMetadata", | ||||||||||
"OpenFeatureClient", | ||||||||||
"AsyncOpenFeatureClient", | ||||||||||
] | ||||||||||
|
||||||||||
logger = logging.getLogger("openfeature") | ||||||||||
|
@@ -464,3 +465,353 @@ | |||||||||
raise GeneralError(error_message="Unknown flag type") | ||||||||||
if not isinstance(value, _type): | ||||||||||
raise TypeMismatchError(f"Expected type {_type} but got {type(value)}") | ||||||||||
|
||||||||||
|
||||||||||
class AsyncOpenFeatureClient(OpenFeatureClient): | ||||||||||
async def get_boolean_value( | ||||||||||
self, | ||||||||||
flag_key: str, | ||||||||||
default_value: bool, | ||||||||||
evaluation_context: typing.Optional[EvaluationContext] = None, | ||||||||||
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None, | ||||||||||
) -> bool: | ||||||||||
details = await self.get_boolean_details( | ||||||||||
flag_key, | ||||||||||
default_value, | ||||||||||
evaluation_context, | ||||||||||
flag_evaluation_options, | ||||||||||
) | ||||||||||
return details.value | ||||||||||
|
||||||||||
async def get_boolean_details( | ||||||||||
self, | ||||||||||
flag_key: str, | ||||||||||
default_value: bool, | ||||||||||
evaluation_context: typing.Optional[EvaluationContext] = None, | ||||||||||
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None, | ||||||||||
) -> FlagEvaluationDetails[bool]: | ||||||||||
return await self.evaluate_flag_details( | ||||||||||
FlagType.BOOLEAN, | ||||||||||
flag_key, | ||||||||||
default_value, | ||||||||||
evaluation_context, | ||||||||||
flag_evaluation_options, | ||||||||||
) | ||||||||||
|
||||||||||
async def get_string_value( | ||||||||||
self, | ||||||||||
flag_key: str, | ||||||||||
default_value: str, | ||||||||||
evaluation_context: typing.Optional[EvaluationContext] = None, | ||||||||||
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None, | ||||||||||
) -> str: | ||||||||||
details = await self.get_string_details( | ||||||||||
flag_key, | ||||||||||
default_value, | ||||||||||
evaluation_context, | ||||||||||
flag_evaluation_options, | ||||||||||
) | ||||||||||
return details.value | ||||||||||
|
||||||||||
async def get_string_details( | ||||||||||
self, | ||||||||||
flag_key: str, | ||||||||||
default_value: str, | ||||||||||
evaluation_context: typing.Optional[EvaluationContext] = None, | ||||||||||
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None, | ||||||||||
) -> FlagEvaluationDetails[str]: | ||||||||||
return await self.evaluate_flag_details( | ||||||||||
FlagType.STRING, | ||||||||||
flag_key, | ||||||||||
default_value, | ||||||||||
evaluation_context, | ||||||||||
flag_evaluation_options, | ||||||||||
) | ||||||||||
|
||||||||||
async def get_integer_value( | ||||||||||
self, | ||||||||||
flag_key: str, | ||||||||||
default_value: int, | ||||||||||
evaluation_context: typing.Optional[EvaluationContext] = None, | ||||||||||
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None, | ||||||||||
) -> int: | ||||||||||
details = await self.get_integer_details( | ||||||||||
flag_key, | ||||||||||
default_value, | ||||||||||
evaluation_context, | ||||||||||
flag_evaluation_options, | ||||||||||
) | ||||||||||
return details.value | ||||||||||
|
||||||||||
async def get_integer_details( | ||||||||||
self, | ||||||||||
flag_key: str, | ||||||||||
default_value: int, | ||||||||||
evaluation_context: typing.Optional[EvaluationContext] = None, | ||||||||||
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None, | ||||||||||
) -> FlagEvaluationDetails[int]: | ||||||||||
return await self.evaluate_flag_details( | ||||||||||
FlagType.INTEGER, | ||||||||||
flag_key, | ||||||||||
default_value, | ||||||||||
evaluation_context, | ||||||||||
flag_evaluation_options, | ||||||||||
) | ||||||||||
|
||||||||||
async def get_float_value( | ||||||||||
self, | ||||||||||
flag_key: str, | ||||||||||
default_value: float, | ||||||||||
evaluation_context: typing.Optional[EvaluationContext] = None, | ||||||||||
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None, | ||||||||||
) -> float: | ||||||||||
details = await self.get_float_details( | ||||||||||
flag_key, | ||||||||||
default_value, | ||||||||||
evaluation_context, | ||||||||||
flag_evaluation_options, | ||||||||||
) | ||||||||||
return details.value | ||||||||||
|
||||||||||
async def get_float_details( | ||||||||||
self, | ||||||||||
flag_key: str, | ||||||||||
default_value: float, | ||||||||||
evaluation_context: typing.Optional[EvaluationContext] = None, | ||||||||||
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None, | ||||||||||
) -> FlagEvaluationDetails[float]: | ||||||||||
return await self.evaluate_flag_details( | ||||||||||
FlagType.FLOAT, | ||||||||||
flag_key, | ||||||||||
default_value, | ||||||||||
evaluation_context, | ||||||||||
flag_evaluation_options, | ||||||||||
) | ||||||||||
|
||||||||||
async def get_object_value( | ||||||||||
self, | ||||||||||
flag_key: str, | ||||||||||
default_value: typing.Union[dict, list], | ||||||||||
evaluation_context: typing.Optional[EvaluationContext] = None, | ||||||||||
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None, | ||||||||||
) -> typing.Union[dict, list]: | ||||||||||
details = await self.get_object_details( | ||||||||||
flag_key, | ||||||||||
default_value, | ||||||||||
evaluation_context, | ||||||||||
flag_evaluation_options, | ||||||||||
) | ||||||||||
return details.value | ||||||||||
|
||||||||||
async def get_object_details( | ||||||||||
self, | ||||||||||
flag_key: str, | ||||||||||
default_value: typing.Union[dict, list], | ||||||||||
evaluation_context: typing.Optional[EvaluationContext] = None, | ||||||||||
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None, | ||||||||||
) -> FlagEvaluationDetails[typing.Union[dict, list]]: | ||||||||||
return await self.evaluate_flag_details( | ||||||||||
FlagType.OBJECT, | ||||||||||
flag_key, | ||||||||||
default_value, | ||||||||||
evaluation_context, | ||||||||||
flag_evaluation_options, | ||||||||||
) | ||||||||||
|
||||||||||
async def evaluate_flag_details( # noqa: PLR0915 | ||||||||||
self, | ||||||||||
flag_type: FlagType, | ||||||||||
flag_key: str, | ||||||||||
default_value: typing.Any, | ||||||||||
evaluation_context: typing.Optional[EvaluationContext] = None, | ||||||||||
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None, | ||||||||||
) -> FlagEvaluationDetails[typing.Any]: | ||||||||||
""" | ||||||||||
Evaluate the flag requested by the user from the clients provider. | ||||||||||
|
||||||||||
:param flag_type: the type of the flag being returned | ||||||||||
: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 | ||||||||||
:return: a FlagEvaluationDetails object with the fully evaluated flag from a | ||||||||||
provider | ||||||||||
""" | ||||||||||
if evaluation_context is None: | ||||||||||
evaluation_context = EvaluationContext() | ||||||||||
|
||||||||||
if flag_evaluation_options is None: | ||||||||||
flag_evaluation_options = FlagEvaluationOptions() | ||||||||||
|
||||||||||
provider = self.provider # call this once to maintain a consistent reference | ||||||||||
evaluation_hooks = flag_evaluation_options.hooks | ||||||||||
hook_hints = flag_evaluation_options.hook_hints | ||||||||||
|
||||||||||
hook_context = HookContext( | ||||||||||
flag_key=flag_key, | ||||||||||
flag_type=flag_type, | ||||||||||
default_value=default_value, | ||||||||||
evaluation_context=evaluation_context, | ||||||||||
client_metadata=self.get_metadata(), | ||||||||||
provider_metadata=provider.get_metadata(), | ||||||||||
) | ||||||||||
# Hooks need to be handled in different orders at different stages | ||||||||||
# in the flag evaluation | ||||||||||
# before: API, Client, Invocation, Provider | ||||||||||
merged_hooks = ( | ||||||||||
api.get_hooks() | ||||||||||
+ self.hooks | ||||||||||
+ evaluation_hooks | ||||||||||
+ provider.get_provider_hooks() | ||||||||||
) | ||||||||||
# after, error, finally: Provider, Invocation, Client, API | ||||||||||
reversed_merged_hooks = merged_hooks[:] | ||||||||||
reversed_merged_hooks.reverse() | ||||||||||
|
||||||||||
status = self.get_provider_status() | ||||||||||
if status == ProviderStatus.NOT_READY: | ||||||||||
error_hooks( | ||||||||||
flag_type, | ||||||||||
hook_context, | ||||||||||
ProviderNotReadyError(), | ||||||||||
reversed_merged_hooks, | ||||||||||
hook_hints, | ||||||||||
) | ||||||||||
return FlagEvaluationDetails( | ||||||||||
flag_key=flag_key, | ||||||||||
value=default_value, | ||||||||||
reason=Reason.ERROR, | ||||||||||
error_code=ErrorCode.PROVIDER_NOT_READY, | ||||||||||
) | ||||||||||
if status == ProviderStatus.FATAL: | ||||||||||
error_hooks( | ||||||||||
flag_type, | ||||||||||
hook_context, | ||||||||||
ProviderFatalError(), | ||||||||||
reversed_merged_hooks, | ||||||||||
hook_hints, | ||||||||||
) | ||||||||||
return FlagEvaluationDetails( | ||||||||||
flag_key=flag_key, | ||||||||||
value=default_value, | ||||||||||
reason=Reason.ERROR, | ||||||||||
error_code=ErrorCode.PROVIDER_FATAL, | ||||||||||
) | ||||||||||
|
||||||||||
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, hook_hints | ||||||||||
) | ||||||||||
|
||||||||||
invocation_context = invocation_context.merge(ctx2=evaluation_context) | ||||||||||
# Requirement 3.2.2 merge: API.context->client.context->invocation.context | ||||||||||
merged_context = ( | ||||||||||
api.get_evaluation_context() | ||||||||||
.merge(self.context) | ||||||||||
.merge(invocation_context) | ||||||||||
) | ||||||||||
|
||||||||||
flag_evaluation = await self._create_provider_evaluation( | ||||||||||
provider, | ||||||||||
flag_type, | ||||||||||
flag_key, | ||||||||||
default_value, | ||||||||||
merged_context, | ||||||||||
) | ||||||||||
|
||||||||||
after_hooks( | ||||||||||
flag_type, | ||||||||||
hook_context, | ||||||||||
flag_evaluation, | ||||||||||
reversed_merged_hooks, | ||||||||||
hook_hints, | ||||||||||
) | ||||||||||
|
||||||||||
return flag_evaluation | ||||||||||
|
||||||||||
except OpenFeatureError as err: | ||||||||||
error_hooks(flag_type, hook_context, err, reversed_merged_hooks, hook_hints) | ||||||||||
|
||||||||||
return FlagEvaluationDetails( | ||||||||||
flag_key=flag_key, | ||||||||||
value=default_value, | ||||||||||
reason=Reason.ERROR, | ||||||||||
error_code=err.error_code, | ||||||||||
error_message=err.error_message, | ||||||||||
) | ||||||||||
# Catch any type of exception here since the user can provide any exception | ||||||||||
# in the error hooks | ||||||||||
except Exception as err: # pragma: no cover | ||||||||||
logger.exception( | ||||||||||
"Unable to correctly evaluate flag with key: '%s'", flag_key | ||||||||||
) | ||||||||||
|
||||||||||
error_hooks(flag_type, hook_context, err, reversed_merged_hooks, hook_hints) | ||||||||||
|
||||||||||
error_message = getattr(err, "error_message", str(err)) | ||||||||||
return FlagEvaluationDetails( | ||||||||||
flag_key=flag_key, | ||||||||||
value=default_value, | ||||||||||
reason=Reason.ERROR, | ||||||||||
error_code=ErrorCode.GENERAL, | ||||||||||
error_message=error_message, | ||||||||||
) | ||||||||||
|
||||||||||
finally: | ||||||||||
after_all_hooks(flag_type, hook_context, reversed_merged_hooks, hook_hints) | ||||||||||
|
||||||||||
async def _create_provider_evaluation( | ||||||||||
self, | ||||||||||
provider: FeatureProvider, | ||||||||||
flag_type: FlagType, | ||||||||||
flag_key: str, | ||||||||||
default_value: typing.Any, | ||||||||||
evaluation_context: typing.Optional[EvaluationContext] = None, | ||||||||||
) -> FlagEvaluationDetails[typing.Any]: | ||||||||||
""" | ||||||||||
Asynchronous encapsulated method to create a FlagEvaluationDetail from a specific provider. | ||||||||||
|
||||||||||
:param flag_type: the type of the flag being returned | ||||||||||
:param 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 | ||||||||||
:return: a FlagEvaluationDetails object with the fully evaluated flag from a | ||||||||||
provider | ||||||||||
""" | ||||||||||
args = ( | ||||||||||
flag_key, | ||||||||||
default_value, | ||||||||||
evaluation_context, | ||||||||||
) | ||||||||||
|
||||||||||
get_details_callables: typing.Mapping[FlagType, GetDetailCallable] = { | ||||||||||
FlagType.BOOLEAN: provider.resolve_boolean_details, | ||||||||||
FlagType.INTEGER: provider.resolve_integer_details, | ||||||||||
FlagType.FLOAT: provider.resolve_float_details, | ||||||||||
FlagType.OBJECT: provider.resolve_object_details, | ||||||||||
FlagType.STRING: provider.resolve_string_details, | ||||||||||
} | ||||||||||
|
||||||||||
get_details_callable = get_details_callables.get(flag_type) | ||||||||||
if not get_details_callable: | ||||||||||
raise GeneralError(error_message="Unknown flag type") | ||||||||||
|
||||||||||
resolution = await get_details_callable(*args) # type: ignore[misc] | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be helpful to check whether It might look like this, but I didn't test it
Suggested change
Typing can be a headache here, though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I actually think this has to be done. If not I think we would get type errors right? |
||||||||||
resolution.raise_for_error() | ||||||||||
|
||||||||||
# we need to check the get_args to be compatible with union types. | ||||||||||
_typecheck_flag_value(resolution.value, flag_type) | ||||||||||
|
||||||||||
return FlagEvaluationDetails( | ||||||||||
flag_key=flag_key, | ||||||||||
value=resolution.value, | ||||||||||
variant=resolution.variant, | ||||||||||
flag_metadata=resolution.flag_metadata or {}, | ||||||||||
reason=resolution.reason, | ||||||||||
error_code=resolution.error_code, | ||||||||||
error_message=resolution.error_message, | ||||||||||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there a lot of duplication of complex spec-compliance logic between the async and sync versions of
evaluation_flag_details
(~140 lines of code, with really the only difference being a singleawait
when calling self._create_provider_evaluation).Is there any way to share the implementations between the two?