feat: implement api-level hooks (#139)

Signed-off-by: Federico Bond <federicobond@gmail.com>
This commit is contained in:
Federico Bond 2023-07-10 13:56:48 -03:00 committed by GitHub
parent 571f5eb3bf
commit 7fe511ffe0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 77 additions and 4 deletions

View File

@ -0,0 +1,21 @@
import typing
from open_feature.hooks.hook import Hook
_hooks: typing.List[Hook] = []
def add_api_hooks(hooks: typing.List[Hook]):
global _hooks
_hooks = _hooks + hooks
def clear_api_hooks():
global _hooks
_hooks = []
def api_hooks() -> typing.List[Hook]:
global _hooks
return _hooks

View File

@ -14,6 +14,7 @@ from open_feature.flag_evaluation.flag_evaluation_options import FlagEvaluationO
from open_feature.flag_evaluation.flag_type import FlagType
from open_feature.flag_evaluation.reason import Reason
from open_feature.flag_evaluation.resolution_details import FlagResolutionDetails
from open_feature.hooks import api_hooks
from open_feature.hooks.hook import Hook
from open_feature.hooks.hook_context import HookContext
from open_feature.hooks.hook_support import (
@ -257,13 +258,14 @@ class OpenFeatureClient:
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 + evaluation_hooks + self.provider.get_provider_hooks()
api_hooks()
+ self.hooks
+ evaluation_hooks
+ self.provider.get_provider_hooks()
)
# after, error, finally: Provider, Invocation, Client, API
reversed_merged_hooks = merged_hooks[:]

View File

@ -137,7 +137,24 @@ See [here](https://openfeature.dev/ecosystem) for a catalog of available provide
### Hooks:
TBD (See Issue [#72](https://github.com/open-feature/python-sdk/issues/72))
A hook is a mechanism that allows for adding arbitrary behavior at well-defined points of the flag evaluation life-cycle. Use cases include validating the resolved flag value, modifying or adding data to the evaluation context, logging, telemetry, and tracking.
```python
from open_feature.hooks.hook import Hook
class MyHook(Hook):
def after(self, hook_context: HookContext, details: FlagEvaluationDetails, hints: dict):
print("This runs after the flag has been evaluated")
# set global hooks at the API-level
from open_feature.hooks import add_api_hooks
add_api_hooks([MyHook()])
# or configure them in the client
client = OpenFeatureClient()
client.add_hooks([MyHook()])
```
See [here](https://openfeature.dev/ecosystem) for a catalog of available hooks.

18
tests/hooks/test_init.py Normal file
View File

@ -0,0 +1,18 @@
from unittest.mock import MagicMock
from open_feature.hooks.hook import Hook
from open_feature.hooks import add_api_hooks, clear_api_hooks, api_hooks
def test_should_add_hooks_to_api_hooks():
# Given
hook_1 = MagicMock(spec=Hook)
hook_2 = MagicMock(spec=Hook)
clear_api_hooks()
# When
add_api_hooks([hook_1])
add_api_hooks([hook_2])
# Then
assert api_hooks() == [hook_1, hook_2]

View File

@ -5,6 +5,7 @@ import pytest
from open_feature.exception.error_code import ErrorCode
from open_feature.exception.exceptions import OpenFeatureError
from open_feature.flag_evaluation.reason import Reason
from open_feature.hooks import clear_api_hooks, add_api_hooks
from open_feature.hooks.hook import Hook
from open_feature.open_feature_client import OpenFeatureClient
from open_feature.provider.no_op_provider import NoOpProvider
@ -144,3 +145,17 @@ def test_should_return_client_metadata_with_name():
# Then
assert metadata is not None
assert metadata.name == "my-client"
def test_should_call_api_level_hooks(no_op_provider_client):
# Given
clear_api_hooks()
api_hook = MagicMock(spec=Hook)
add_api_hooks([api_hook])
# When
no_op_provider_client.get_boolean_details(flag_key="Key", default_value=True)
# Then
api_hook.before.assert_called_once()
api_hook.after.assert_called_once()