Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 43
feat: simplify async functionality to provider#385
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
e3f2dd0fa380de9548ad629608066c68a9e341ba9783c6f4a8446b9be1247946cfcb900d68182d90190cFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
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 @@ def _typecheck_flag_value(value: typing.Any, flag_type: FlagType) -> None: | ||||||||||
| 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] | ||||||||||
| ||||||||||
| resolution=awaitget_details_callable(*args) # type: ignore[misc] | |
| resolution=get_details_callable(*args) # type: ignore[misc] | |
| ifisawaitable(resolution): | |
| resolution=awaitresolution |
Typing can be a headache here, though.
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.
I actually think this has to be done. If not I think we would get type errors right?
Uh oh!
There was an error while loading. Please reload this page.
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 singleawaitwhen calling self._create_provider_evaluation).Is there any way to share the implementations between the two?