Skip to content
Open
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
135 changes: 135 additions & 0 deletions openfeature/_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
from __future__ import annotations

from openfeature._event_support import EventSupport
Comment thread
toddbaert marked this conversation as resolved.
from openfeature.client import OpenFeatureClient
from openfeature.evaluation_context import EvaluationContext
from openfeature.event import EventHandler, ProviderEvent
from openfeature.exception import GeneralError
from openfeature.hook import Hook
from openfeature.provider import FeatureProvider, ProviderStatus
from openfeature.provider._registry import ProviderRegistry
from openfeature.provider.metadata import Metadata
from openfeature.transaction_context import TransactionContextPropagator
from openfeature.transaction_context.no_op_transaction_context_propagator import (
NoOpTransactionContextPropagator,
)


class OpenFeatureAPI:
"""An independent OpenFeature API instance with its own isolated state.

Each instance maintains its own providers, evaluation context, hooks,
event handlers, and transaction context propagator; fully separate from
the global singleton and from other instances.
"""

def __init__(self) -> None:
self._hooks: list[Hook] = []
Comment thread
toddbaert marked this conversation as resolved.
self._evaluation_context = EvaluationContext()
self._transaction_context_propagator: TransactionContextPropagator = (
NoOpTransactionContextPropagator()
)
self._event_support = EventSupport()
self._provider_registry = ProviderRegistry(
event_support=self._event_support,
evaluation_context_getter=self.get_evaluation_context,
)

# --- Client creation ---

def get_client(
self, domain: str | None = None, version: str | None = None
) -> OpenFeatureClient:
return OpenFeatureClient(domain=domain, version=version, api=self)

# --- Provider management ---

def set_provider(
self, provider: FeatureProvider, domain: str | None = None
) -> None:
if domain is None:
self._provider_registry.set_default_provider(provider)
else:
self._provider_registry.set_provider(domain, provider)

def set_provider_and_wait(
self, provider: FeatureProvider, domain: str | None = None
) -> None:
if domain is None:
self._provider_registry.set_default_provider(provider, wait_for_init=True)
else:
self._provider_registry.set_provider(domain, provider, wait_for_init=True)

def get_provider_metadata(self, domain: str | None = None) -> Metadata:
return self._provider_registry.get_provider(domain).get_metadata()

def get_provider(self, domain: str | None = None) -> FeatureProvider:
return self._provider_registry.get_provider(domain)

def get_provider_status(self, provider: FeatureProvider) -> ProviderStatus:
return self._provider_registry.get_provider_status(provider)

def clear_providers(self) -> None:
self._provider_registry.clear_providers()
self._event_support.clear()

def shutdown(self) -> None:
# shutdown -> remove providers -> set default provider to NoOp -> remove event handlers
self.clear_providers()
# remove hooks
self.clear_hooks()
# set evaluation context to default
self._evaluation_context = EvaluationContext()
# set propagator to NoOp
self._transaction_context_propagator = NoOpTransactionContextPropagator()

# --- Hooks ---

def add_hooks(self, hooks: list[Hook]) -> None:
self._hooks = self._hooks + hooks

def clear_hooks(self) -> None:
self._hooks = []

def get_hooks(self) -> list[Hook]:
return self._hooks
Comment thread
toddbaert marked this conversation as resolved.

# --- Evaluation context ---

def get_evaluation_context(self) -> EvaluationContext:
return self._evaluation_context

def set_evaluation_context(self, evaluation_context: EvaluationContext) -> None:
if evaluation_context is None:
raise GeneralError(error_message="No api level evaluation context")
self._evaluation_context = evaluation_context
Comment thread
toddbaert marked this conversation as resolved.

def clear_evaluation_context(self) -> None:
self.set_evaluation_context(EvaluationContext())

# --- Transaction context ---

def set_transaction_context_propagator(
self, transaction_context_propagator: TransactionContextPropagator
) -> None:
self._transaction_context_propagator = transaction_context_propagator
Comment thread
toddbaert marked this conversation as resolved.

def clear_transaction_context_propagator(self) -> None:
self.set_transaction_context_propagator(NoOpTransactionContextPropagator())

def get_transaction_context(self) -> EvaluationContext:
return self._transaction_context_propagator.get_transaction_context()

def set_transaction_context(self, evaluation_context: EvaluationContext) -> None:
self._transaction_context_propagator.set_transaction_context(evaluation_context)

# --- Event handlers ---

def add_handler(self, event: ProviderEvent, handler: EventHandler) -> None:
self._event_support.add_global_handler(event, handler, self.get_client)

def remove_handler(self, event: ProviderEvent, handler: EventHandler) -> None:
self._event_support.remove_global_handler(event, handler)


_default_api = OpenFeatureAPI()
212 changes: 110 additions & 102 deletions openfeature/_event_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import threading
import typing
from collections import defaultdict
from collections.abc import Callable
from concurrent.futures import ThreadPoolExecutor
from logging import getLogger
Comment thread
toddbaert marked this conversation as resolved.

Expand All @@ -23,103 +24,6 @@
_event_executor = ThreadPoolExecutor(thread_name_prefix="openfeature-event-handler")
atexit.register(_event_executor.shutdown, wait=True)

_global_lock = threading.RLock()
_global_handlers: dict[ProviderEvent, list[EventHandler]] = defaultdict(list)

_client_lock = threading.RLock()
_client_handlers: dict[OpenFeatureClient, dict[ProviderEvent, list[EventHandler]]] = (
defaultdict(lambda: defaultdict(list))
)


def run_client_handlers(
client: OpenFeatureClient, event: ProviderEvent, details: EventDetails
) -> None:
with _client_lock:
handlers_by_event = _client_handlers.get(client)
if handlers_by_event is None:
return

handlers = tuple(handlers_by_event.get(event, ()))

for handler in handlers:
_submit_handler(handler, details)


def run_global_handlers(event: ProviderEvent, details: EventDetails) -> None:
with _global_lock:
handlers = tuple(_global_handlers.get(event, ()))

for handler in handlers:
_submit_handler(handler, details)


def add_client_handler(
client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler
) -> None:
with _client_lock:
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:
with _client_lock:
handlers = _client_handlers[client][event]
handlers.remove(handler)


def add_global_handler(event: ProviderEvent, handler: EventHandler) -> None:
with _global_lock:
_global_handlers[event].append(handler)

from openfeature.api import get_client # noqa: PLC0415

_run_immediate_handler(get_client(), event, handler)


def remove_global_handler(event: ProviderEvent, handler: EventHandler) -> None:
with _global_lock:
_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
with _client_lock:
clients = tuple(
client for client in _client_handlers if client.provider == provider
)

for client in clients:
run_client_handlers(client, event, details)


def _run_immediate_handler(
client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler
) -> None:
status_to_event = {
ProviderStatus.READY: ProviderEvent.PROVIDER_READY,
ProviderStatus.ERROR: ProviderEvent.PROVIDER_ERROR,
ProviderStatus.FATAL: ProviderEvent.PROVIDER_ERROR,
ProviderStatus.STALE: ProviderEvent.PROVIDER_STALE,
}
if event == status_to_event.get(client.get_provider_status()):
_submit_handler(
handler, EventDetails(provider_name=client.provider.get_metadata().name)
)


def _submit_handler(handler: EventHandler, details: EventDetails) -> None:
_event_executor.submit(_run_handler, handler, details)
Expand All @@ -132,8 +36,112 @@ def _run_handler(handler: EventHandler, details: EventDetails) -> None:
logger.exception("Unhandled exception in OpenFeature event handler")


def clear() -> None:
with _global_lock:
_global_handlers.clear()
with _client_lock:
_client_handlers.clear()
class EventSupport:
"""Per-API-instance event handler storage and dispatch."""

def __init__(self) -> None:
self._global_lock = threading.RLock()
self._global_handlers: dict[ProviderEvent, list[EventHandler]] = defaultdict(
list
)

self._client_lock = threading.RLock()
self._client_handlers: dict[
OpenFeatureClient, dict[ProviderEvent, list[EventHandler]]
] = defaultdict(lambda: defaultdict(list))

def run_client_handlers(
self, client: OpenFeatureClient, event: ProviderEvent, details: EventDetails
) -> None:
with self._client_lock:
handlers_by_event = self._client_handlers.get(client)
if handlers_by_event is None:
return

handlers = tuple(handlers_by_event.get(event, ()))

for handler in handlers:
_submit_handler(handler, details)

def run_global_handlers(self, event: ProviderEvent, details: EventDetails) -> None:
with self._global_lock:
handlers = tuple(self._global_handlers.get(event, ()))

for handler in handlers:
_submit_handler(handler, details)

def add_client_handler(
self, client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler
) -> None:
with self._client_lock:
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:
with self._client_lock:
handlers = self._client_handlers[client][event]
handlers.remove(handler)

def add_global_handler(
self,
event: ProviderEvent,
handler: EventHandler,
get_client: Callable[[], OpenFeatureClient],
) -> None:
with self._global_lock:
self._global_handlers[event].append(handler)

self._run_immediate_handler(get_client(), event, handler)

def remove_global_handler(
self, event: ProviderEvent, handler: EventHandler
) -> None:
with self._global_lock:
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
with self._client_lock:
clients = tuple(
client
for client in self._client_handlers
if client.provider == provider
)

for client in clients:
self.run_client_handlers(client, event, details)

def _run_immediate_handler(
self, client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler
) -> None:
status_to_event = {
ProviderStatus.READY: ProviderEvent.PROVIDER_READY,
ProviderStatus.ERROR: ProviderEvent.PROVIDER_ERROR,
ProviderStatus.FATAL: ProviderEvent.PROVIDER_ERROR,
ProviderStatus.STALE: ProviderEvent.PROVIDER_STALE,
}
if event == status_to_event.get(client.get_provider_status()):
_submit_handler(
handler,
EventDetails(provider_name=client.provider.get_metadata().name),
)

def clear(self) -> None:
with self._global_lock:
self._global_handlers.clear()
with self._client_lock:
self._client_handlers.clear()
Loading
Loading