diff --git a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/provider.py b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/provider.py index 15c86f6..dca9182 100644 --- a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/provider.py +++ b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/provider.py @@ -24,6 +24,7 @@ import typing from openfeature.evaluation_context import EvaluationContext +from openfeature.event import ProviderEventDetails from openfeature.flag_evaluation import FlagResolutionDetails from openfeature.provider.metadata import Metadata from openfeature.provider.provider import AbstractProvider @@ -75,7 +76,12 @@ def setup_resolver(self) -> AbstractResolver: if self.config.resolver_type == ResolverType.GRPC: return GrpcResolver(self.config) elif self.config.resolver_type == ResolverType.IN_PROCESS: - return InProcessResolver(self.config, self) + return InProcessResolver( + self.config, + self.emit_provider_ready, + self.emit_provider_error, + self.emit_provider_configuration_changed, + ) else: raise ValueError( f"`resolver_type` parameter invalid: {self.config.resolver_type}" @@ -92,6 +98,11 @@ def get_metadata(self) -> Metadata: """Returns provider metadata""" return Metadata(name="FlagdProvider") + def flag_store_updated_callback(self, flag_keys: typing.List[str]) -> None: + self.emit_provider_configuration_changed( + ProviderEventDetails(flags_changed=flag_keys) + ) + def resolve_boolean_details( self, key: str, diff --git a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/in_process.py b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/in_process.py index e0ea96b..37c1608 100644 --- a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/in_process.py +++ b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/in_process.py @@ -4,9 +4,9 @@ from json_logic import builtins, jsonLogic # type: ignore[import-untyped] from openfeature.evaluation_context import EvaluationContext +from openfeature.event import ProviderEventDetails from openfeature.exception import FlagNotFoundError, ParseError from openfeature.flag_evaluation import FlagResolutionDetails, Reason -from openfeature.provider.provider import AbstractProvider from ..config import Config from .process.connector import FlagStateConnector @@ -27,19 +27,32 @@ class InProcessResolver: "sem_ver": sem_ver, } - def __init__(self, config: Config, provider: AbstractProvider): + def __init__( + self, + config: Config, + emit_provider_ready: typing.Callable[[ProviderEventDetails], None], + emit_provider_error: typing.Callable[[ProviderEventDetails], None], + emit_provider_configuration_changed: typing.Callable[ + [ProviderEventDetails], None + ], + ): self.config = config - self.provider = provider - self.flag_store = FlagStore(provider) + self.flag_store = FlagStore(emit_provider_configuration_changed) self.connector: FlagStateConnector = ( FileWatcher( self.config.offline_flag_source_path, - self.provider, self.flag_store, + emit_provider_ready, + emit_provider_error, self.config.offline_poll_interval_seconds, ) if self.config.offline_flag_source_path - else GrpcWatcher(self.config, self.provider, self.flag_store) + else GrpcWatcher( + self.config, + self.flag_store, + emit_provider_ready, + emit_provider_error, + ) ) def initialize(self, evaluation_context: EvaluationContext) -> None: diff --git a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/file_watcher.py b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/file_watcher.py index a33601c..934c433 100644 --- a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/file_watcher.py +++ b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/file_watcher.py @@ -10,7 +10,6 @@ from openfeature.evaluation_context import EvaluationContext from openfeature.event import ProviderEventDetails from openfeature.exception import ParseError, ProviderNotReadyError -from openfeature.provider.provider import AbstractProvider from ..connector import FlagStateConnector from ..flags import FlagStore @@ -22,12 +21,14 @@ class FileWatcher(FlagStateConnector): def __init__( self, file_path: str, - provider: AbstractProvider, flag_store: FlagStore, + emit_provider_ready: typing.Callable[[ProviderEventDetails], None], + emit_provider_error: typing.Callable[[ProviderEventDetails], None], poll_interval_seconds: float = 1.0, ): self.file_path = file_path - self.provider = provider + self.emit_provider_ready = emit_provider_ready + self.emit_provider_error = emit_provider_error self.poll_interval_seconds = poll_interval_seconds self.last_modified = 0.0 @@ -83,7 +84,7 @@ def _load_data(self, modified_time: typing.Optional[float] = None) -> None: self.flag_store.update(data) if self.should_emit_ready_on_success: - self.provider.emit_provider_ready( + self.emit_provider_ready( ProviderEventDetails( message="Reloading file contents recovered from error state" ) @@ -95,4 +96,4 @@ def _load_data(self, modified_time: typing.Optional[float] = None) -> None: def handle_error(self, error_message: str) -> None: logger.exception(error_message) self.should_emit_ready_on_success = True - self.provider.emit_provider_error(ProviderEventDetails(message=error_message)) + self.emit_provider_error(ProviderEventDetails(message=error_message)) diff --git a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py index 9afbd6a..f6ea3ec 100644 --- a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py +++ b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py @@ -2,13 +2,13 @@ import logging import threading import time +import typing import grpc from openfeature.evaluation_context import EvaluationContext from openfeature.event import ProviderEventDetails from openfeature.exception import ErrorCode, ParseError, ProviderNotReadyError -from openfeature.provider.provider import AbstractProvider from ....config import Config from ....proto.flagd.sync.v1 import sync_pb2, sync_pb2_grpc @@ -22,9 +22,12 @@ class GrpcWatcher(FlagStateConnector): MAX_BACK_OFF = 120 def __init__( - self, config: Config, provider: AbstractProvider, flag_store: FlagStore + self, + config: Config, + flag_store: FlagStore, + emit_provider_ready: typing.Callable[[ProviderEventDetails], None], + emit_provider_error: typing.Callable[[ProviderEventDetails], None], ): - self.provider = provider self.flag_store = flag_store channel_factory = grpc.secure_channel if config.tls else grpc.insecure_channel self.channel = channel_factory(f"{config.host}:{config.port}") @@ -32,6 +35,8 @@ def __init__( self.timeout = config.timeout self.retry_backoff_seconds = config.retry_backoff_seconds self.selector = config.selector + self.emit_provider_ready = emit_provider_ready + self.emit_provider_error = emit_provider_error self.connected = False @@ -71,7 +76,7 @@ def sync_flags(self) -> None: self.flag_store.update(json.loads(flag_str)) if not self.connected: - self.provider.emit_provider_ready( + self.emit_provider_ready( ProviderEventDetails( message="gRPC sync connection established" ) @@ -94,7 +99,7 @@ def sync_flags(self) -> None: ) self.connected = False - self.provider.emit_provider_error( + self.emit_provider_error( ProviderEventDetails( message=f"gRPC sync disconnected, reconnecting in {retry_delay}s", error_code=ErrorCode.GENERAL, diff --git a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/flags.py b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/flags.py index c8fb618..889edac 100644 --- a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/flags.py +++ b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/flags.py @@ -5,15 +5,16 @@ from openfeature.event import ProviderEventDetails from openfeature.exception import ParseError -from openfeature.provider.provider import AbstractProvider class FlagStore: def __init__( self, - provider: AbstractProvider, + emit_provider_configuration_changed: typing.Callable[ + [ProviderEventDetails], None + ], ): - self.provider = provider + self.emit_provider_configuration_changed = emit_provider_configuration_changed self.flags: typing.Mapping[str, "Flag"] = {} def get_flag(self, key: str) -> typing.Optional["Flag"]: @@ -34,7 +35,7 @@ def update(self, flags_data: dict) -> None: raise ParseError("`flags` key of configuration must be a dictionary") self.flags = {key: Flag.from_dict(key, data) for key, data in flags.items()} - self.provider.emit_provider_configuration_changed( + self.emit_provider_configuration_changed( ProviderEventDetails(flags_changed=list(self.flags.keys())) ) diff --git a/providers/openfeature-provider-flagd/tests/test_file_store.py b/providers/openfeature-provider-flagd/tests/test_file_store.py index f44c9b8..12a1d97 100644 --- a/providers/openfeature-provider-flagd/tests/test_file_store.py +++ b/providers/openfeature-provider-flagd/tests/test_file_store.py @@ -8,7 +8,6 @@ FileWatcher, ) from openfeature.contrib.provider.flagd.resolvers.process.flags import Flag, FlagStore -from openfeature.provider.provider import AbstractProvider def create_client(provider: FlagdProvider): @@ -24,9 +23,13 @@ def create_client(provider: FlagdProvider): ], ) def test_file_load(file_name: str): - provider = Mock(spec=AbstractProvider) - flag_store = FlagStore(provider) - file_watcher = FileWatcher(f"tests/flags/{file_name}", provider, flag_store) + emit_provider_configuration_changed = Mock() + emit_provider_ready = Mock() + emit_provider_error = Mock() + flag_store = FlagStore(emit_provider_configuration_changed) + file_watcher = FileWatcher( + f"tests/flags/{file_name}", flag_store, emit_provider_ready, emit_provider_error + ) file_watcher.initialize(None) flag = flag_store.get_flag("basic-flag") diff --git a/providers/openfeature-provider-flagd/tests/test_grpc_sync_connector.py b/providers/openfeature-provider-flagd/tests/test_grpc_sync_connector.py index 00d08a2..e68e717 100644 --- a/providers/openfeature-provider-flagd/tests/test_grpc_sync_connector.py +++ b/providers/openfeature-provider-flagd/tests/test_grpc_sync_connector.py @@ -30,9 +30,13 @@ def sync(request): ), ) def test_invalid_payload(flag_configuration: str): - fake_provider = MagicMock() - flag_store = FlagStore(fake_provider) - watcher = GrpcWatcher(Config(timeout=0.2), fake_provider, flag_store) + emit_provider_configuration_changed = MagicMock() + emit_provider_ready = MagicMock() + emit_provider_error = MagicMock() + flag_store = FlagStore(emit_provider_configuration_changed) + watcher = GrpcWatcher( + Config(timeout=0.2), flag_store, emit_provider_ready, emit_provider_error + ) fake_sync_flags = fake_grpc_service(flag_configuration) with patch.object(watcher.stub, "SyncFlags", fake_sync_flags), pytest.raises(