From 289b47b7febbb00fe813a0dfcd1abd764b867da4 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Tue, 14 Nov 2023 01:16:58 -0300 Subject: [PATCH 01/13] feat: implement provider events Signed-off-by: Federico Bond --- README.md | 23 ++++++- openfeature/api.py | 39 ++++++++++++ openfeature/client.py | 7 +++ openfeature/event.py | 100 +++++++++++++++++++++++++++++++ openfeature/provider/provider.py | 20 +++++++ tests/test_api.py | 49 ++++++++++++++- tests/test_client.py | 54 +++++++++++++++++ 7 files changed, 288 insertions(+), 4 deletions(-) create mode 100644 openfeature/event.py diff --git a/README.md b/README.md index 47731700..18017e1e 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ print("Value: " + str(flag_value)) | ✅ | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | | ✅ | [Logging](#logging) | Integrate with popular logging packages. | | ✅ | [Domains](#domains) | Logically bind clients with providers. | -| ❌ | [Eventing](#eventing) | React to state changes in the provider or flag management system. | +| ✅ | [Eventing](#eventing) | React to state changes in the provider or flag management system. | | ✅ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | | ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | @@ -214,7 +214,26 @@ For more details, please refer to the [providers](#providers) section. ### Eventing -Events are not yet available in the Python SDK. Progress on this feature can be tracked [here](https://github.com/open-feature/python-sdk/issues/125). +Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions. Initialization events (PROVIDER_READY on success, PROVIDER_ERROR on failure) are dispatched for every provider. Some providers support additional events, such as PROVIDER_CONFIGURATION_CHANGED. + +Please refer to the documentation of the provider you're using to see what events are supported. + +```python +from openfeature import api +from openfeature.provider import ProviderEvent + +def on_provider_ready(event_details: EventDetails): + print(f"Provider {event_details.provider_name} is ready") + +api.add_handler(ProviderEvent.PROVIDER_READY, on_provider_ready) + +client = api.get_client() + +def on_provider_ready(event_details: EventDetails): + print(f"Provider {event_details.provider_name} is ready") + +client.add_handler(ProviderEvent.PROVIDER_READY, on_provider_ready) +``` ### Shutdown diff --git a/openfeature/api.py b/openfeature/api.py index f4574545..ce49e6f0 100644 --- a/openfeature/api.py +++ b/openfeature/api.py @@ -2,18 +2,29 @@ from openfeature.client import OpenFeatureClient from openfeature.evaluation_context import EvaluationContext +from openfeature.event import ( + EventHandler, + EventSupport, + ProviderEvent, + ProviderEventDetails, +) from openfeature.exception import GeneralError from openfeature.hook import Hook from openfeature.provider import FeatureProvider from openfeature.provider.metadata import Metadata +from openfeature.provider.no_op_provider import NoOpProvider from openfeature.provider.registry import ProviderRegistry +_provider: FeatureProvider = NoOpProvider() + _evaluation_context = EvaluationContext() _hooks: typing.List[Hook] = [] _provider_registry: ProviderRegistry = ProviderRegistry() +_event_support: EventSupport = EventSupport() + def get_client( domain: typing.Optional[str] = None, version: typing.Optional[str] = None @@ -67,3 +78,31 @@ def get_hooks() -> typing.List[Hook]: def shutdown() -> None: _provider_registry.shutdown() + + +def add_handler(event: ProviderEvent, handler: EventHandler) -> None: + _event_support.add_global_handler(event, handler) + + +def remove_handler(event: ProviderEvent, handler: EventHandler) -> None: + _event_support.remove_global_handler(event, handler) + + +def _add_client_handler( + client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler +) -> None: + _event_support.add_client_handler(client, event, handler) + + +def _remove_client_handler( + client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler +) -> None: + _event_support.remove_client_handler(client, event, handler) + + +def _run_handlers_for_provider( + provider: FeatureProvider, + event: ProviderEvent, + provider_details: ProviderEventDetails, +) -> None: + _event_support.run_handlers_for_provider(provider, event, provider_details) diff --git a/openfeature/client.py b/openfeature/client.py index b27866ce..4b2f3a91 100644 --- a/openfeature/client.py +++ b/openfeature/client.py @@ -4,6 +4,7 @@ from openfeature import api from openfeature.evaluation_context import EvaluationContext +from openfeature.event import EventHandler, ProviderEvent from openfeature.exception import ( ErrorCode, GeneralError, @@ -440,6 +441,12 @@ def _create_provider_evaluation( error_message=resolution.error_message, ) + def add_handler(self, event: ProviderEvent, handler: EventHandler) -> None: + api._add_client_handler(self, event, handler) + + def remove_handler(self, event: ProviderEvent, handler: EventHandler) -> None: + api._remove_client_handler(self, event, handler) + def _typecheck_flag_value(value: typing.Any, flag_type: FlagType) -> None: type_map: TypeMap = { diff --git a/openfeature/event.py b/openfeature/event.py new file mode 100644 index 00000000..6e02d2c6 --- /dev/null +++ b/openfeature/event.py @@ -0,0 +1,100 @@ +from collections import defaultdict +from dataclasses import dataclass, field +from enum import Enum +from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Union + +from openfeature.provider import FeatureProvider + +if TYPE_CHECKING: + from openfeature.client import OpenFeatureClient + + +class ProviderEvent(Enum): + PROVIDER_READY = "PROVIDER_READY" + PROVIDER_CONFIGURATION_CHANGED = "PROVIDER_CONFIGURATION_CHANGED" + PROVIDER_ERROR = "PROVIDER_ERROR" + PROVIDER_STALE = "PROVIDER_STALE" + + +@dataclass +class ProviderEventDetails: + flags_changed: Optional[List[str]] = None + message: Optional[str] = None + metadata: Dict[str, Union[bool, str, int, float]] = field(default_factory=dict) + + +@dataclass +class EventDetails(ProviderEventDetails): + provider_name: str = "" + flags_changed: Optional[List[str]] = None + message: Optional[str] = None + metadata: Dict[str, Union[bool, str, int, float]] = field(default_factory=dict) + + @classmethod + def from_provider_event_details( + cls, provider_name: str, details: ProviderEventDetails + ) -> "EventDetails": + return cls( + provider_name=provider_name, + flags_changed=details.flags_changed, + message=details.message, + metadata=details.metadata, + ) + + +EventHandler = Callable[[EventDetails], None] + + +class EventSupport: + _global_handlers: Dict[ProviderEvent, List[EventHandler]] + _client_handlers: Dict["OpenFeatureClient", Dict[ProviderEvent, List[EventHandler]]] + + def __init__(self) -> None: + self._global_handlers = defaultdict(list) + self._client_handlers = defaultdict(lambda: defaultdict(list)) + + def run_client_handlers( + self, client: "OpenFeatureClient", event: ProviderEvent, details: EventDetails + ) -> None: + for handler in self._client_handlers[client][event]: + handler(details) + + def run_global_handlers(self, event: ProviderEvent, details: EventDetails) -> None: + for handler in self._global_handlers[event]: + handler(details) + + def add_client_handler( + self, client: "OpenFeatureClient", event: ProviderEvent, handler: EventHandler + ) -> None: + handlers = self._client_handlers[client][event] + handlers.append(handler) + + def remove_client_handler( + self, client: "OpenFeatureClient", event: ProviderEvent, handler: EventHandler + ) -> None: + handlers = self._client_handlers[client][event] + handlers.remove(handler) + + def add_global_handler(self, event: ProviderEvent, handler: EventHandler) -> None: + self._global_handlers[event].append(handler) + + def remove_global_handler( + self, event: ProviderEvent, handler: EventHandler + ) -> None: + self._global_handlers[event].remove(handler) + + def run_handlers_for_provider( + self, + provider: FeatureProvider, + event: ProviderEvent, + provider_details: ProviderEventDetails, + ) -> None: + details = EventDetails.from_provider_event_details( + provider.get_metadata().name, provider_details + ) + # run the global handlers + self.run_global_handlers(event, details) + # run the handlers for clients associated to this provider + for client in self._client_handlers: + if client.provider == provider: + self.run_client_handlers(client, event, details) diff --git a/openfeature/provider/provider.py b/openfeature/provider/provider.py index ebad417f..70486e96 100644 --- a/openfeature/provider/provider.py +++ b/openfeature/provider/provider.py @@ -2,6 +2,7 @@ from abc import abstractmethod from openfeature.evaluation_context import EvaluationContext +from openfeature.event import ProviderEvent, ProviderEventDetails from openfeature.flag_evaluation import FlagResolutionDetails from openfeature.hook import Hook from openfeature.provider import FeatureProvider @@ -66,3 +67,22 @@ def resolve_object_details( evaluation_context: typing.Optional[EvaluationContext] = None, ) -> FlagResolutionDetails[typing.Union[dict, list]]: pass + + def emit_provider_ready(self, details: ProviderEventDetails) -> None: + self.emit(ProviderEvent.PROVIDER_READY, details) + + def emit_provider_configuration_changed( + self, details: ProviderEventDetails + ) -> None: + self.emit(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, details) + + def emit_provider_error(self, details: ProviderEventDetails) -> None: + self.emit(ProviderEvent.PROVIDER_ERROR, details) + + def emit_provider_stale(self, details: ProviderEventDetails) -> None: + self.emit(ProviderEvent.PROVIDER_STALE, details) + + def emit(self, event: ProviderEvent, details: ProviderEventDetails) -> None: + from openfeature.api import _run_handlers_for_provider + + _run_handlers_for_provider(self, event, details) diff --git a/tests/test_api.py b/tests/test_api.py index 3756f85c..be852dac 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -3,6 +3,7 @@ import pytest from openfeature.api import ( + add_handler, add_hooks, clear_hooks, clear_providers, @@ -10,16 +11,17 @@ get_evaluation_context, get_hooks, get_provider_metadata, + remove_handler, set_evaluation_context, set_provider, shutdown, ) from openfeature.evaluation_context import EvaluationContext +from openfeature.event import EventDetails, ProviderEvent, ProviderEventDetails from openfeature.exception import ErrorCode, GeneralError from openfeature.hook import Hook -from openfeature.provider.metadata import Metadata +from openfeature.provider import FeatureProvider, Metadata from openfeature.provider.no_op_provider import NoOpProvider -from openfeature.provider.provider import FeatureProvider def test_should_not_raise_exception_with_noop_client(): @@ -228,3 +230,46 @@ def test_clear_providers_shutdowns_every_provider_and_resets_default_provider(): provider_1.shutdown.assert_called_once() provider_2.shutdown.assert_called_once() assert isinstance(get_client().provider, NoOpProvider) + + +def test_provider_events(): + spy = MagicMock() + + add_handler(ProviderEvent.PROVIDER_READY, spy.provider_ready) + add_handler( + ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, spy.provider_configuration_changed + ) + add_handler(ProviderEvent.PROVIDER_ERROR, spy.provider_error) + add_handler(ProviderEvent.PROVIDER_STALE, spy.provider_stale) + + provider = NoOpProvider() + + provider_details = ProviderEventDetails(message="message") + details = EventDetails.from_provider_event_details( + provider.get_metadata().name, provider_details + ) + + provider.emit_provider_ready(provider_details) + provider.emit_provider_configuration_changed(provider_details) + provider.emit_provider_error(provider_details) + provider.emit_provider_stale(provider_details) + + spy.provider_ready.assert_called_once_with(details) + spy.provider_configuration_changed.assert_called_once_with(details) + spy.provider_error.assert_called_once_with(details) + spy.provider_stale.assert_called_once_with(details) + + +def test_add_remove_event_handler(): + provider = NoOpProvider() + set_provider(provider) + + spy = MagicMock() + + add_handler(ProviderEvent.PROVIDER_READY, spy.provider_ready) + remove_handler(ProviderEvent.PROVIDER_READY, spy.provider_ready) + + provider_details = ProviderEventDetails(message="message") + provider.emit_provider_ready(provider_details) + + spy.provider_ready.assert_not_called() diff --git a/tests/test_client.py b/tests/test_client.py index 43223d99..52cfe3cd 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -4,6 +4,7 @@ from openfeature.api import add_hooks, clear_hooks, get_client, set_provider from openfeature.client import OpenFeatureClient +from openfeature.event import EventDetails, ProviderEvent, ProviderEventDetails from openfeature.exception import ErrorCode, OpenFeatureError from openfeature.flag_evaluation import FlagResolutionDetails, Reason from openfeature.hook import Hook @@ -260,3 +261,56 @@ def test_should_run_error_hooks_if_provider_returns_resolution_with_error_code() assert flag_details.reason == Reason.ERROR assert flag_details.error_code == ErrorCode.PROVIDER_FATAL spy_hook.error.assert_called_once() + + +def test_provider_events(): + provider = NoOpProvider() + set_provider(provider) + + other_provider = NoOpProvider() + set_provider(other_provider, "my-domain") + + provider_details = ProviderEventDetails(message="message") + details = EventDetails.from_provider_event_details( + provider.get_metadata().name, provider_details + ) + + def emit_all_events(provider): + provider.emit_provider_ready(provider_details) + provider.emit_provider_configuration_changed(provider_details) + provider.emit_provider_error(provider_details) + provider.emit_provider_stale(provider_details) + + spy = MagicMock() + + client = get_client() + client.add_handler(ProviderEvent.PROVIDER_READY, spy.provider_ready) + client.add_handler( + ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, spy.provider_configuration_changed + ) + client.add_handler(ProviderEvent.PROVIDER_ERROR, spy.provider_error) + client.add_handler(ProviderEvent.PROVIDER_STALE, spy.provider_stale) + + emit_all_events(provider) + emit_all_events(other_provider) + + spy.provider_ready.assert_called_once_with(details) + spy.provider_configuration_changed.assert_called_once_with(details) + spy.provider_error.assert_called_once_with(details) + spy.provider_stale.assert_called_once_with(details) + + +def test_add_remove_event_handler(): + provider = NoOpProvider() + set_provider(provider) + + spy = MagicMock() + + client = get_client() + client.add_handler(ProviderEvent.PROVIDER_READY, spy.provider_ready) + client.remove_handler(ProviderEvent.PROVIDER_READY, spy.provider_ready) + + provider_details = ProviderEventDetails(message="message") + provider.emit_provider_ready(provider_details) + + spy.provider_ready.assert_not_called() From e31c1ac5d5019293b81c1b041fc27590f9e047f5 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Thu, 29 Feb 2024 20:59:37 -0300 Subject: [PATCH 02/13] feat: add error_code field to EventDetails and ProviderEventDetails Signed-off-by: Federico Bond --- openfeature/event.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openfeature/event.py b/openfeature/event.py index 6e02d2c6..3db7829e 100644 --- a/openfeature/event.py +++ b/openfeature/event.py @@ -3,6 +3,7 @@ from enum import Enum from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Union +from openfeature.exception import ErrorCode from openfeature.provider import FeatureProvider if TYPE_CHECKING: @@ -20,6 +21,7 @@ class ProviderEvent(Enum): class ProviderEventDetails: flags_changed: Optional[List[str]] = None message: Optional[str] = None + error_code: Optional[ErrorCode] = None metadata: Dict[str, Union[bool, str, int, float]] = field(default_factory=dict) @@ -28,6 +30,7 @@ class EventDetails(ProviderEventDetails): provider_name: str = "" flags_changed: Optional[List[str]] = None message: Optional[str] = None + error_code: Optional[ErrorCode] = None metadata: Dict[str, Union[bool, str, int, float]] = field(default_factory=dict) @classmethod @@ -38,6 +41,7 @@ def from_provider_event_details( provider_name=provider_name, flags_changed=details.flags_changed, message=details.message, + error_code=details.error_code, metadata=details.metadata, ) From 47d7e1acce46bfddd66a6cb6194929bdaca484e1 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Sun, 3 Mar 2024 13:40:13 -0300 Subject: [PATCH 03/13] fix: replace strings with postponed evaluation of annotations Signed-off-by: Federico Bond --- openfeature/event.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/openfeature/event.py b/openfeature/event.py index 3db7829e..b27b7f90 100644 --- a/openfeature/event.py +++ b/openfeature/event.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from collections import defaultdict from dataclasses import dataclass, field from enum import Enum @@ -36,7 +38,7 @@ class EventDetails(ProviderEventDetails): @classmethod def from_provider_event_details( cls, provider_name: str, details: ProviderEventDetails - ) -> "EventDetails": + ) -> EventDetails: return cls( provider_name=provider_name, flags_changed=details.flags_changed, @@ -51,14 +53,14 @@ def from_provider_event_details( class EventSupport: _global_handlers: Dict[ProviderEvent, List[EventHandler]] - _client_handlers: Dict["OpenFeatureClient", Dict[ProviderEvent, List[EventHandler]]] + _client_handlers: Dict[OpenFeatureClient, Dict[ProviderEvent, List[EventHandler]]] def __init__(self) -> None: self._global_handlers = defaultdict(list) self._client_handlers = defaultdict(lambda: defaultdict(list)) def run_client_handlers( - self, client: "OpenFeatureClient", event: ProviderEvent, details: EventDetails + self, client: OpenFeatureClient, event: ProviderEvent, details: EventDetails ) -> None: for handler in self._client_handlers[client][event]: handler(details) @@ -68,13 +70,13 @@ def run_global_handlers(self, event: ProviderEvent, details: EventDetails) -> No handler(details) def add_client_handler( - self, client: "OpenFeatureClient", event: ProviderEvent, handler: EventHandler + self, client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler ) -> None: handlers = self._client_handlers[client][event] handlers.append(handler) def remove_client_handler( - self, client: "OpenFeatureClient", event: ProviderEvent, handler: EventHandler + self, client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler ) -> None: handlers = self._client_handlers[client][event] handlers.remove(handler) From aca435fc388fd2c84add84f743aed3d1b9b79bb9 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Sun, 3 Mar 2024 13:46:35 -0300 Subject: [PATCH 04/13] feat: run handlers immediately if provider already in associated state Signed-off-by: Federico Bond --- openfeature/event.py | 13 +++++++++++++ tests/test_api.py | 45 ++++++++++++++++++++++++++++++++++++++------ tests/test_client.py | 16 ++++++++++------ 3 files changed, 62 insertions(+), 12 deletions(-) diff --git a/openfeature/event.py b/openfeature/event.py index b27b7f90..b3fda379 100644 --- a/openfeature/event.py +++ b/openfeature/event.py @@ -75,6 +75,8 @@ def add_client_handler( handlers = self._client_handlers[client][event] handlers.append(handler) + self._run_immediate_handler(client, event, handler) + def remove_client_handler( self, client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler ) -> None: @@ -84,6 +86,10 @@ def remove_client_handler( def add_global_handler(self, event: ProviderEvent, handler: EventHandler) -> None: self._global_handlers[event].append(handler) + from openfeature.api import get_client + + self._run_immediate_handler(get_client(), event, handler) + def remove_global_handler( self, event: ProviderEvent, handler: EventHandler ) -> None: @@ -104,3 +110,10 @@ def run_handlers_for_provider( for client in self._client_handlers: if client.provider == provider: self.run_client_handlers(client, event, details) + + def _run_immediate_handler( + self, client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler + ) -> None: + if event == ProviderEvent.PROVIDER_READY: + # providers are assumed ready because provider status is not yet implemented + handler(EventDetails(provider_name=client.provider.get_metadata().name)) diff --git a/tests/test_api.py b/tests/test_api.py index be852dac..34d6dd2c 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -249,12 +249,12 @@ def test_provider_events(): provider.get_metadata().name, provider_details ) - provider.emit_provider_ready(provider_details) provider.emit_provider_configuration_changed(provider_details) provider.emit_provider_error(provider_details) provider.emit_provider_stale(provider_details) - spy.provider_ready.assert_called_once_with(details) + # NOTE: provider_ready is called immediately after adding the handler + spy.provider_ready.assert_called_once() spy.provider_configuration_changed.assert_called_once_with(details) spy.provider_error.assert_called_once_with(details) spy.provider_stale.assert_called_once_with(details) @@ -266,10 +266,43 @@ def test_add_remove_event_handler(): spy = MagicMock() - add_handler(ProviderEvent.PROVIDER_READY, spy.provider_ready) - remove_handler(ProviderEvent.PROVIDER_READY, spy.provider_ready) + add_handler( + ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, spy.provider_configuration_changed + ) + remove_handler( + ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, spy.provider_configuration_changed + ) provider_details = ProviderEventDetails(message="message") - provider.emit_provider_ready(provider_details) + provider.emit_provider_configuration_changed(provider_details) + + spy.provider_configuration_changed.assert_not_called() + + +# Requirement 5.3.3 +def test_handlers_attached_to_provider_already_in_associated_state_should_run_immediately(): + # Given + provider = NoOpProvider() + set_provider(provider) + spy = MagicMock() + + # When + add_handler(ProviderEvent.PROVIDER_READY, spy.provider_ready) + + # Then + spy.provider_ready.assert_called_once() - spy.provider_ready.assert_not_called() + +def test_provider_ready_handlers_run_if_provider_initialize_function_terminates_normally(): + # Given + provider = NoOpProvider() + set_provider(provider) + + spy = MagicMock() + add_handler(ProviderEvent.PROVIDER_READY, spy.provider_ready) + + # When + provider.initialize(get_evaluation_context()) + + # Then + spy.provider_ready.assert_called_once() diff --git a/tests/test_client.py b/tests/test_client.py index 52cfe3cd..88e086dc 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -276,7 +276,6 @@ def test_provider_events(): ) def emit_all_events(provider): - provider.emit_provider_ready(provider_details) provider.emit_provider_configuration_changed(provider_details) provider.emit_provider_error(provider_details) provider.emit_provider_stale(provider_details) @@ -294,7 +293,8 @@ def emit_all_events(provider): emit_all_events(provider) emit_all_events(other_provider) - spy.provider_ready.assert_called_once_with(details) + # NOTE: provider_ready is called immediately after adding the handler + spy.provider_ready.assert_called_once() spy.provider_configuration_changed.assert_called_once_with(details) spy.provider_error.assert_called_once_with(details) spy.provider_stale.assert_called_once_with(details) @@ -307,10 +307,14 @@ def test_add_remove_event_handler(): spy = MagicMock() client = get_client() - client.add_handler(ProviderEvent.PROVIDER_READY, spy.provider_ready) - client.remove_handler(ProviderEvent.PROVIDER_READY, spy.provider_ready) + client.add_handler( + ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, spy.provider_configuration_changed + ) + client.remove_handler( + ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, spy.provider_configuration_changed + ) provider_details = ProviderEventDetails(message="message") - provider.emit_provider_ready(provider_details) + provider.emit_provider_configuration_changed(provider_details) - spy.provider_ready.assert_not_called() + spy.provider_configuration_changed.assert_not_called() From faa17517af7afad2bb3ba1684d988db7b48bcbcb Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Fri, 8 Mar 2024 12:02:04 +1100 Subject: [PATCH 05/13] feat: remove unused _provider from openfeature.api Signed-off-by: Federico Bond --- openfeature/api.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openfeature/api.py b/openfeature/api.py index ce49e6f0..3bd40ff2 100644 --- a/openfeature/api.py +++ b/openfeature/api.py @@ -12,11 +12,8 @@ from openfeature.hook import Hook from openfeature.provider import FeatureProvider from openfeature.provider.metadata import Metadata -from openfeature.provider.no_op_provider import NoOpProvider from openfeature.provider.registry import ProviderRegistry -_provider: FeatureProvider = NoOpProvider() - _evaluation_context = EvaluationContext() _hooks: typing.List[Hook] = [] From ba56db16b3dee6ffe179c534c685e087f60f21d5 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Fri, 8 Mar 2024 12:07:19 +1100 Subject: [PATCH 06/13] test: add some comments to test cases Signed-off-by: Federico Bond --- tests/test_api.py | 7 +++++++ tests/test_client.py | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/tests/test_api.py b/tests/test_api.py index 34d6dd2c..5bb9c91a 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -233,6 +233,7 @@ def test_clear_providers_shutdowns_every_provider_and_resets_default_provider(): def test_provider_events(): + # Given spy = MagicMock() add_handler(ProviderEvent.PROVIDER_READY, spy.provider_ready) @@ -249,10 +250,12 @@ def test_provider_events(): provider.get_metadata().name, provider_details ) + # When provider.emit_provider_configuration_changed(provider_details) provider.emit_provider_error(provider_details) provider.emit_provider_stale(provider_details) + # Then # NOTE: provider_ready is called immediately after adding the handler spy.provider_ready.assert_called_once() spy.provider_configuration_changed.assert_called_once_with(details) @@ -261,6 +264,7 @@ def test_provider_events(): def test_add_remove_event_handler(): + # Given provider = NoOpProvider() set_provider(provider) @@ -274,8 +278,11 @@ def test_add_remove_event_handler(): ) provider_details = ProviderEventDetails(message="message") + + # When provider.emit_provider_configuration_changed(provider_details) + # Then spy.provider_configuration_changed.assert_not_called() diff --git a/tests/test_client.py b/tests/test_client.py index 88e086dc..2e236de8 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -264,6 +264,7 @@ def test_should_run_error_hooks_if_provider_returns_resolution_with_error_code() def test_provider_events(): + # Given provider = NoOpProvider() set_provider(provider) @@ -290,9 +291,11 @@ def emit_all_events(provider): client.add_handler(ProviderEvent.PROVIDER_ERROR, spy.provider_error) client.add_handler(ProviderEvent.PROVIDER_STALE, spy.provider_stale) + # When emit_all_events(provider) emit_all_events(other_provider) + # Then # NOTE: provider_ready is called immediately after adding the handler spy.provider_ready.assert_called_once() spy.provider_configuration_changed.assert_called_once_with(details) @@ -301,6 +304,7 @@ def emit_all_events(provider): def test_add_remove_event_handler(): + # Given provider = NoOpProvider() set_provider(provider) @@ -315,6 +319,9 @@ def test_add_remove_event_handler(): ) provider_details = ProviderEventDetails(message="message") + + # When provider.emit_provider_configuration_changed(provider_details) + # Then spy.provider_configuration_changed.assert_not_called() From 2dabd8123bfb6cbd178cd6ba5f1c55bca8439272 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Fri, 8 Mar 2024 12:12:57 +1100 Subject: [PATCH 07/13] test: add provider event late binding test cases Signed-off-by: Federico Bond --- tests/test_client.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index 2e236de8..dc25abee 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -325,3 +325,34 @@ def test_add_remove_event_handler(): # Then spy.provider_configuration_changed.assert_not_called() + + +# Requirement 5.1.2, Requirement 5.1.3 +def test_provider_event_late_binding(): + # Given + provider = NoOpProvider() + set_provider(provider, "my-domain") + other_provider = NoOpProvider() + + spy = MagicMock() + + client = get_client("my-domain") + client.add_handler( + ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, spy.provider_configuration_changed + ) + + set_provider(other_provider, "my-domain") + + provider_details = ProviderEventDetails(message="message from provider") + other_provider_details = ProviderEventDetails(message="message from other provider") + + details = EventDetails.from_provider_event_details( + other_provider.get_metadata().name, other_provider_details + ) + + # When + provider.emit_provider_configuration_changed(provider_details) + other_provider.emit_provider_configuration_changed(other_provider_details) + + # Then + spy.provider_configuration_changed.assert_called_once_with(details) From 3324a6cad8733f26ba8c194ea2b81b60241ba33a Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Fri, 8 Mar 2024 16:02:07 +1100 Subject: [PATCH 08/13] fix: fix status handlers running immediately if provider already in associated state Signed-off-by: Federico Bond --- openfeature/api.py | 3 ++- openfeature/event.py | 18 +++++++++++++++--- openfeature/provider/registry.py | 17 +++++++++++++++-- tests/conftest.py | 5 ++--- 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/openfeature/api.py b/openfeature/api.py index 3bd40ff2..7704d382 100644 --- a/openfeature/api.py +++ b/openfeature/api.py @@ -39,7 +39,8 @@ def set_provider( def clear_providers() -> None: - return _provider_registry.clear_providers() + _provider_registry.clear_providers() + _event_support.clear() def get_provider_metadata(domain: typing.Optional[str] = None) -> Metadata: diff --git a/openfeature/event.py b/openfeature/event.py index b3fda379..70868a31 100644 --- a/openfeature/event.py +++ b/openfeature/event.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Union from openfeature.exception import ErrorCode -from openfeature.provider import FeatureProvider +from openfeature.provider import FeatureProvider, ProviderStatus if TYPE_CHECKING: from openfeature.client import OpenFeatureClient @@ -16,9 +16,18 @@ class ProviderEvent(Enum): PROVIDER_READY = "PROVIDER_READY" PROVIDER_CONFIGURATION_CHANGED = "PROVIDER_CONFIGURATION_CHANGED" PROVIDER_ERROR = "PROVIDER_ERROR" + PROVIDER_FATAL = "PROVIDER_FATAL" PROVIDER_STALE = "PROVIDER_STALE" +_provider_status_to_event = { + ProviderStatus.READY: ProviderEvent.PROVIDER_READY, + ProviderStatus.ERROR: ProviderEvent.PROVIDER_ERROR, + ProviderStatus.FATAL: ProviderEvent.PROVIDER_FATAL, + ProviderStatus.STALE: ProviderEvent.PROVIDER_STALE, +} + + @dataclass class ProviderEventDetails: flags_changed: Optional[List[str]] = None @@ -114,6 +123,9 @@ def run_handlers_for_provider( def _run_immediate_handler( self, client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler ) -> None: - if event == ProviderEvent.PROVIDER_READY: - # providers are assumed ready because provider status is not yet implemented + if event == _provider_status_to_event.get(client.get_provider_status()): handler(EventDetails(provider_name=client.provider.get_metadata().name)) + + def clear(self) -> None: + self._global_handlers.clear() + self._client_handlers.clear() diff --git a/openfeature/provider/registry.py b/openfeature/provider/registry.py index 779ee569..18af1962 100644 --- a/openfeature/provider/registry.py +++ b/openfeature/provider/registry.py @@ -1,6 +1,10 @@ import typing from openfeature.evaluation_context import EvaluationContext +from openfeature.event import ( + ProviderEventDetails, + _provider_status_to_event, +) from openfeature.exception import ErrorCode, GeneralError, OpenFeatureError from openfeature.provider import FeatureProvider, ProviderStatus from openfeature.provider.no_op_provider import NoOpProvider @@ -14,8 +18,9 @@ class ProviderRegistry: def __init__(self) -> None: self._default_provider = NoOpProvider() self._providers = {} - self._provider_status = {} - self._set_provider_status(self._default_provider, ProviderStatus.NOT_READY) + self._provider_status = { + self._default_provider: ProviderStatus.READY, + } def set_provider(self, domain: str, provider: FeatureProvider) -> None: if provider is None: @@ -50,6 +55,9 @@ def clear_providers(self) -> None: self.shutdown() self._providers.clear() self._default_provider = NoOpProvider() + self._provider_status = { + self._default_provider: ProviderStatus.READY, + } def shutdown(self) -> None: for provider in {self._default_provider, *self._providers.values()}: @@ -90,3 +98,8 @@ def _set_provider_status( self, provider: FeatureProvider, status: ProviderStatus ) -> None: self._provider_status[provider] = status + + if event := _provider_status_to_event.get(status): + from openfeature.api import _run_handlers_for_provider + + _run_handlers_for_provider(provider, event, ProviderEventDetails()) diff --git a/tests/conftest.py b/tests/conftest.py index b97e5478..1f0a7982 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,13 +5,12 @@ @pytest.fixture(autouse=True) -def clear_provider(): +def clear_providers(): """ For tests that use set_provider(), we need to clear the provider to avoid issues in other tests. """ - yield - _provider = None + api.clear_providers() @pytest.fixture() From 349da5c0254b1c7011c4c415458bbe4b3d9677cc Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Mon, 11 Mar 2024 10:36:30 +1100 Subject: [PATCH 09/13] refactor: reuse provider property in OpenFeatureClient Signed-off-by: Federico Bond --- openfeature/client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openfeature/client.py b/openfeature/client.py index 4b2f3a91..5b04f755 100644 --- a/openfeature/client.py +++ b/openfeature/client.py @@ -85,8 +85,7 @@ def provider(self) -> FeatureProvider: return api._provider_registry.get_provider(self.domain) def get_provider_status(self) -> ProviderStatus: - provider = api._provider_registry.get_provider(self.domain) - return api._provider_registry.get_provider_status(provider) + return api._provider_registry.get_provider_status(self.provider) def get_metadata(self) -> ClientMetadata: return ClientMetadata(domain=self.domain) From 7997eef2ab3f1c3b3814057feb3514b42ba2cb5d Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Thu, 14 Mar 2024 08:42:32 +1100 Subject: [PATCH 10/13] refactor: move _provider_status_to_event to ProviderEvent.from_provider_status Signed-off-by: Federico Bond --- openfeature/event.py | 20 ++++++++++++-------- openfeature/provider/registry.py | 4 ++-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/openfeature/event.py b/openfeature/event.py index 70868a31..f6d0b3a5 100644 --- a/openfeature/event.py +++ b/openfeature/event.py @@ -3,7 +3,7 @@ from collections import defaultdict from dataclasses import dataclass, field from enum import Enum -from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Union +from typing import TYPE_CHECKING, Callable, ClassVar, Dict, List, Optional, Union from openfeature.exception import ErrorCode from openfeature.provider import FeatureProvider, ProviderStatus @@ -19,13 +19,17 @@ class ProviderEvent(Enum): PROVIDER_FATAL = "PROVIDER_FATAL" PROVIDER_STALE = "PROVIDER_STALE" + __status__: ClassVar[Dict[ProviderStatus, str]] = { + ProviderStatus.READY: PROVIDER_READY, + ProviderStatus.ERROR: PROVIDER_ERROR, + ProviderStatus.FATAL: PROVIDER_FATAL, + ProviderStatus.STALE: PROVIDER_STALE, + } -_provider_status_to_event = { - ProviderStatus.READY: ProviderEvent.PROVIDER_READY, - ProviderStatus.ERROR: ProviderEvent.PROVIDER_ERROR, - ProviderStatus.FATAL: ProviderEvent.PROVIDER_FATAL, - ProviderStatus.STALE: ProviderEvent.PROVIDER_STALE, -} + @classmethod + def from_provider_status(cls, status: ProviderStatus) -> Optional[ProviderEvent]: + value = ProviderEvent.__status__.get(status) + return ProviderEvent[value] if value else None @dataclass @@ -123,7 +127,7 @@ def run_handlers_for_provider( def _run_immediate_handler( self, client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler ) -> None: - if event == _provider_status_to_event.get(client.get_provider_status()): + if event == ProviderEvent.from_provider_status(client.get_provider_status()): handler(EventDetails(provider_name=client.provider.get_metadata().name)) def clear(self) -> None: diff --git a/openfeature/provider/registry.py b/openfeature/provider/registry.py index 18af1962..f15ddb45 100644 --- a/openfeature/provider/registry.py +++ b/openfeature/provider/registry.py @@ -2,8 +2,8 @@ from openfeature.evaluation_context import EvaluationContext from openfeature.event import ( + ProviderEvent, ProviderEventDetails, - _provider_status_to_event, ) from openfeature.exception import ErrorCode, GeneralError, OpenFeatureError from openfeature.provider import FeatureProvider, ProviderStatus @@ -99,7 +99,7 @@ def _set_provider_status( ) -> None: self._provider_status[provider] = status - if event := _provider_status_to_event.get(status): + if event := ProviderEvent.from_provider_status(status): from openfeature.api import _run_handlers_for_provider _run_handlers_for_provider(provider, event, ProviderEventDetails()) From e49d3751882409a198e4fdd4e21fb7e957c15947 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Thu, 14 Mar 2024 09:20:46 +1100 Subject: [PATCH 11/13] refactor: move EventSupport class to an internal module Signed-off-by: Federico Bond --- openfeature/_event_support.py | 86 +++++++++++++++++++++++++++++++++++ openfeature/api.py | 2 +- openfeature/event.py | 79 +------------------------------- 3 files changed, 89 insertions(+), 78 deletions(-) create mode 100644 openfeature/_event_support.py diff --git a/openfeature/_event_support.py b/openfeature/_event_support.py new file mode 100644 index 00000000..c15d68b0 --- /dev/null +++ b/openfeature/_event_support.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +from collections import defaultdict +from typing import TYPE_CHECKING, Dict, List + +from openfeature.event import ( + EventDetails, + EventHandler, + ProviderEvent, + ProviderEventDetails, +) +from openfeature.provider import FeatureProvider + +if TYPE_CHECKING: + from openfeature.client import OpenFeatureClient + + +class EventSupport: + _global_handlers: Dict[ProviderEvent, List[EventHandler]] + _client_handlers: Dict[OpenFeatureClient, Dict[ProviderEvent, List[EventHandler]]] + + def __init__(self) -> None: + self._global_handlers = defaultdict(list) + self._client_handlers = defaultdict(lambda: defaultdict(list)) + + def run_client_handlers( + self, client: OpenFeatureClient, event: ProviderEvent, details: EventDetails + ) -> None: + for handler in self._client_handlers[client][event]: + handler(details) + + def run_global_handlers(self, event: ProviderEvent, details: EventDetails) -> None: + for handler in self._global_handlers[event]: + handler(details) + + def add_client_handler( + self, client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler + ) -> None: + handlers = self._client_handlers[client][event] + handlers.append(handler) + + self._run_immediate_handler(client, event, handler) + + def remove_client_handler( + self, client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler + ) -> None: + handlers = self._client_handlers[client][event] + handlers.remove(handler) + + def add_global_handler(self, event: ProviderEvent, handler: EventHandler) -> None: + self._global_handlers[event].append(handler) + + from openfeature.api import get_client + + self._run_immediate_handler(get_client(), event, handler) + + def remove_global_handler( + self, event: ProviderEvent, handler: EventHandler + ) -> None: + self._global_handlers[event].remove(handler) + + def run_handlers_for_provider( + self, + provider: FeatureProvider, + event: ProviderEvent, + provider_details: ProviderEventDetails, + ) -> None: + details = EventDetails.from_provider_event_details( + provider.get_metadata().name, provider_details + ) + # run the global handlers + self.run_global_handlers(event, details) + # run the handlers for clients associated to this provider + for client in self._client_handlers: + if client.provider == provider: + self.run_client_handlers(client, event, details) + + def _run_immediate_handler( + self, client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler + ) -> None: + if event == ProviderEvent.from_provider_status(client.get_provider_status()): + handler(EventDetails(provider_name=client.provider.get_metadata().name)) + + def clear(self) -> None: + self._global_handlers.clear() + self._client_handlers.clear() diff --git a/openfeature/api.py b/openfeature/api.py index 7704d382..bbbda081 100644 --- a/openfeature/api.py +++ b/openfeature/api.py @@ -1,10 +1,10 @@ import typing +from openfeature._event_support import EventSupport from openfeature.client import OpenFeatureClient from openfeature.evaluation_context import EvaluationContext from openfeature.event import ( EventHandler, - EventSupport, ProviderEvent, ProviderEventDetails, ) diff --git a/openfeature/event.py b/openfeature/event.py index f6d0b3a5..18ef4e1b 100644 --- a/openfeature/event.py +++ b/openfeature/event.py @@ -1,15 +1,11 @@ from __future__ import annotations -from collections import defaultdict from dataclasses import dataclass, field from enum import Enum -from typing import TYPE_CHECKING, Callable, ClassVar, Dict, List, Optional, Union +from typing import Callable, ClassVar, Dict, List, Optional, Union from openfeature.exception import ErrorCode -from openfeature.provider import FeatureProvider, ProviderStatus - -if TYPE_CHECKING: - from openfeature.client import OpenFeatureClient +from openfeature.provider import ProviderStatus class ProviderEvent(Enum): @@ -62,74 +58,3 @@ def from_provider_event_details( EventHandler = Callable[[EventDetails], None] - - -class EventSupport: - _global_handlers: Dict[ProviderEvent, List[EventHandler]] - _client_handlers: Dict[OpenFeatureClient, Dict[ProviderEvent, List[EventHandler]]] - - def __init__(self) -> None: - self._global_handlers = defaultdict(list) - self._client_handlers = defaultdict(lambda: defaultdict(list)) - - def run_client_handlers( - self, client: OpenFeatureClient, event: ProviderEvent, details: EventDetails - ) -> None: - for handler in self._client_handlers[client][event]: - handler(details) - - def run_global_handlers(self, event: ProviderEvent, details: EventDetails) -> None: - for handler in self._global_handlers[event]: - handler(details) - - def add_client_handler( - self, client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler - ) -> None: - handlers = self._client_handlers[client][event] - handlers.append(handler) - - self._run_immediate_handler(client, event, handler) - - def remove_client_handler( - self, client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler - ) -> None: - handlers = self._client_handlers[client][event] - handlers.remove(handler) - - def add_global_handler(self, event: ProviderEvent, handler: EventHandler) -> None: - self._global_handlers[event].append(handler) - - from openfeature.api import get_client - - self._run_immediate_handler(get_client(), event, handler) - - def remove_global_handler( - self, event: ProviderEvent, handler: EventHandler - ) -> None: - self._global_handlers[event].remove(handler) - - def run_handlers_for_provider( - self, - provider: FeatureProvider, - event: ProviderEvent, - provider_details: ProviderEventDetails, - ) -> None: - details = EventDetails.from_provider_event_details( - provider.get_metadata().name, provider_details - ) - # run the global handlers - self.run_global_handlers(event, details) - # run the handlers for clients associated to this provider - for client in self._client_handlers: - if client.provider == provider: - self.run_client_handlers(client, event, details) - - def _run_immediate_handler( - self, client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler - ) -> None: - if event == ProviderEvent.from_provider_status(client.get_provider_status()): - handler(EventDetails(provider_name=client.provider.get_metadata().name)) - - def clear(self) -> None: - self._global_handlers.clear() - self._client_handlers.clear() From cda7245eb7585656c854eabf8596ff134ce56fe7 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Thu, 14 Mar 2024 09:26:17 +1100 Subject: [PATCH 12/13] refactor: replace EventSupport class with module-level functions Signed-off-by: Federico Bond --- openfeature/_event_support.py | 141 ++++++++++++++++--------------- openfeature/api.py | 25 +----- openfeature/client.py | 6 +- openfeature/provider/provider.py | 5 +- openfeature/provider/registry.py | 5 +- 5 files changed, 80 insertions(+), 102 deletions(-) diff --git a/openfeature/_event_support.py b/openfeature/_event_support.py index c15d68b0..32cdb453 100644 --- a/openfeature/_event_support.py +++ b/openfeature/_event_support.py @@ -15,72 +15,75 @@ from openfeature.client import OpenFeatureClient -class EventSupport: - _global_handlers: Dict[ProviderEvent, List[EventHandler]] - _client_handlers: Dict[OpenFeatureClient, Dict[ProviderEvent, List[EventHandler]]] - - def __init__(self) -> None: - self._global_handlers = defaultdict(list) - self._client_handlers = defaultdict(lambda: defaultdict(list)) - - def run_client_handlers( - self, client: OpenFeatureClient, event: ProviderEvent, details: EventDetails - ) -> None: - for handler in self._client_handlers[client][event]: - handler(details) - - def run_global_handlers(self, event: ProviderEvent, details: EventDetails) -> None: - for handler in self._global_handlers[event]: - handler(details) - - def add_client_handler( - self, client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler - ) -> None: - handlers = self._client_handlers[client][event] - handlers.append(handler) - - self._run_immediate_handler(client, event, handler) - - def remove_client_handler( - self, client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler - ) -> None: - handlers = self._client_handlers[client][event] - handlers.remove(handler) - - def add_global_handler(self, event: ProviderEvent, handler: EventHandler) -> None: - self._global_handlers[event].append(handler) - - from openfeature.api import get_client - - self._run_immediate_handler(get_client(), event, handler) - - def remove_global_handler( - self, event: ProviderEvent, handler: EventHandler - ) -> None: - self._global_handlers[event].remove(handler) - - def run_handlers_for_provider( - self, - provider: FeatureProvider, - event: ProviderEvent, - provider_details: ProviderEventDetails, - ) -> None: - details = EventDetails.from_provider_event_details( - provider.get_metadata().name, provider_details - ) - # run the global handlers - self.run_global_handlers(event, details) - # run the handlers for clients associated to this provider - for client in self._client_handlers: - if client.provider == provider: - self.run_client_handlers(client, event, details) - - def _run_immediate_handler( - self, client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler - ) -> None: - if event == ProviderEvent.from_provider_status(client.get_provider_status()): - handler(EventDetails(provider_name=client.provider.get_metadata().name)) - - def clear(self) -> None: - self._global_handlers.clear() - self._client_handlers.clear() +_global_handlers: Dict[ProviderEvent, List[EventHandler]] = defaultdict(list) +_client_handlers: Dict[ + OpenFeatureClient, Dict[ProviderEvent, List[EventHandler]] +] = defaultdict(lambda: defaultdict(list)) + + +def run_client_handlers( + client: OpenFeatureClient, event: ProviderEvent, details: EventDetails +) -> None: + for handler in _client_handlers[client][event]: + handler(details) + + +def run_global_handlers(event: ProviderEvent, details: EventDetails) -> None: + for handler in _global_handlers[event]: + handler(details) + + +def add_client_handler( + client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler +) -> None: + handlers = _client_handlers[client][event] + handlers.append(handler) + + _run_immediate_handler(client, event, handler) + + +def remove_client_handler( + client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler +) -> None: + handlers = _client_handlers[client][event] + handlers.remove(handler) + + +def add_global_handler(event: ProviderEvent, handler: EventHandler) -> None: + _global_handlers[event].append(handler) + + from openfeature.api import get_client + + _run_immediate_handler(get_client(), event, handler) + + +def remove_global_handler(event: ProviderEvent, handler: EventHandler) -> None: + _global_handlers[event].remove(handler) + + +def run_handlers_for_provider( + provider: FeatureProvider, + event: ProviderEvent, + provider_details: ProviderEventDetails, +) -> None: + details = EventDetails.from_provider_event_details( + provider.get_metadata().name, provider_details + ) + # run the global handlers + run_global_handlers(event, details) + # run the handlers for clients associated to this provider + for client in _client_handlers: + if client.provider == provider: + run_client_handlers(client, event, details) + + +def _run_immediate_handler( + client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler +) -> None: + if event == ProviderEvent.from_provider_status(client.get_provider_status()): + handler(EventDetails(provider_name=client.provider.get_metadata().name)) + + +def clear() -> None: + _global_handlers.clear() + _client_handlers.clear() diff --git a/openfeature/api.py b/openfeature/api.py index bbbda081..4460cc70 100644 --- a/openfeature/api.py +++ b/openfeature/api.py @@ -1,12 +1,11 @@ import typing -from openfeature._event_support import EventSupport +from openfeature import _event_support from openfeature.client import OpenFeatureClient from openfeature.evaluation_context import EvaluationContext from openfeature.event import ( EventHandler, ProviderEvent, - ProviderEventDetails, ) from openfeature.exception import GeneralError from openfeature.hook import Hook @@ -20,8 +19,6 @@ _provider_registry: ProviderRegistry = ProviderRegistry() -_event_support: EventSupport = EventSupport() - def get_client( domain: typing.Optional[str] = None, version: typing.Optional[str] = None @@ -84,23 +81,3 @@ def add_handler(event: ProviderEvent, handler: EventHandler) -> None: def remove_handler(event: ProviderEvent, handler: EventHandler) -> None: _event_support.remove_global_handler(event, handler) - - -def _add_client_handler( - client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler -) -> None: - _event_support.add_client_handler(client, event, handler) - - -def _remove_client_handler( - client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler -) -> None: - _event_support.remove_client_handler(client, event, handler) - - -def _run_handlers_for_provider( - provider: FeatureProvider, - event: ProviderEvent, - provider_details: ProviderEventDetails, -) -> None: - _event_support.run_handlers_for_provider(provider, event, provider_details) diff --git a/openfeature/client.py b/openfeature/client.py index 5b04f755..f08749d6 100644 --- a/openfeature/client.py +++ b/openfeature/client.py @@ -2,7 +2,7 @@ import typing from dataclasses import dataclass -from openfeature import api +from openfeature import _event_support, api from openfeature.evaluation_context import EvaluationContext from openfeature.event import EventHandler, ProviderEvent from openfeature.exception import ( @@ -441,10 +441,10 @@ def _create_provider_evaluation( ) def add_handler(self, event: ProviderEvent, handler: EventHandler) -> None: - api._add_client_handler(self, event, handler) + _event_support.add_client_handler(self, event, handler) def remove_handler(self, event: ProviderEvent, handler: EventHandler) -> None: - api._remove_client_handler(self, event, handler) + _event_support.remove_client_handler(self, event, handler) def _typecheck_flag_value(value: typing.Any, flag_type: FlagType) -> None: diff --git a/openfeature/provider/provider.py b/openfeature/provider/provider.py index 70486e96..2e5da576 100644 --- a/openfeature/provider/provider.py +++ b/openfeature/provider/provider.py @@ -1,6 +1,7 @@ import typing from abc import abstractmethod +from openfeature._event_support import run_handlers_for_provider from openfeature.evaluation_context import EvaluationContext from openfeature.event import ProviderEvent, ProviderEventDetails from openfeature.flag_evaluation import FlagResolutionDetails @@ -83,6 +84,4 @@ def emit_provider_stale(self, details: ProviderEventDetails) -> None: self.emit(ProviderEvent.PROVIDER_STALE, details) def emit(self, event: ProviderEvent, details: ProviderEventDetails) -> None: - from openfeature.api import _run_handlers_for_provider - - _run_handlers_for_provider(self, event, details) + run_handlers_for_provider(self, event, details) diff --git a/openfeature/provider/registry.py b/openfeature/provider/registry.py index f15ddb45..8d764465 100644 --- a/openfeature/provider/registry.py +++ b/openfeature/provider/registry.py @@ -1,5 +1,6 @@ import typing +from openfeature._event_support import run_handlers_for_provider from openfeature.evaluation_context import EvaluationContext from openfeature.event import ( ProviderEvent, @@ -100,6 +101,4 @@ def _set_provider_status( self._provider_status[provider] = status if event := ProviderEvent.from_provider_status(status): - from openfeature.api import _run_handlers_for_provider - - _run_handlers_for_provider(provider, event, ProviderEventDetails()) + run_handlers_for_provider(provider, event, ProviderEventDetails()) From d4420f3434d35f73ad5f9a003996075ae8c6d07c Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Fri, 22 Mar 2024 07:32:05 +1100 Subject: [PATCH 13/13] style: fix code style --- openfeature/_event_support.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openfeature/_event_support.py b/openfeature/_event_support.py index 32cdb453..5842b0f4 100644 --- a/openfeature/_event_support.py +++ b/openfeature/_event_support.py @@ -16,9 +16,9 @@ _global_handlers: Dict[ProviderEvent, List[EventHandler]] = defaultdict(list) -_client_handlers: Dict[ - OpenFeatureClient, Dict[ProviderEvent, List[EventHandler]] -] = defaultdict(lambda: defaultdict(list)) +_client_handlers: Dict[OpenFeatureClient, Dict[ProviderEvent, List[EventHandler]]] = ( + defaultdict(lambda: defaultdict(list)) +) def run_client_handlers(