Skip to content
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: implement api-level hooks #139

Merged
merged 1 commit into from
Jul 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 21 additions & 0 deletions open_feature/hooks/__init__.py
Original file line number Diff line number Diff line change
@@ -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
8 changes: 5 additions & 3 deletions open_feature/open_feature_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
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 (
Expand Down Expand Up @@ -257,13 +258,14 @@ def evaluate_flag_details(
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[:]
Expand Down
19 changes: 18 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
18 changes: 18 additions & 0 deletions tests/hooks/test_init.py
Original file line number Diff line number Diff line change
@@ -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]
15 changes: 15 additions & 0 deletions tests/test_open_feature_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
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
Expand Down Expand Up @@ -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()