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

import typing

from openfeature._event_support import EventSupport
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 (
NoOpTransactionContextPropagator,
TransactionContextPropagator,
)

if typing.TYPE_CHECKING:
from openfeature.client import OpenFeatureClient


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] = []
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:
from openfeature.client import OpenFeatureClient # noqa: PLC0415

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 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:
self._provider_registry.shutdown()

# --- 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

# --- 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

# --- Transaction context ---

def set_transaction_context_propagator(
self, transaction_context_propagator: TransactionContextPropagator
) -> None:
self._transaction_context_propagator = transaction_context_propagator

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)


def _create_default_api() -> OpenFeatureAPI:
"""Create the default global API instance, wired to legacy module-level singletons.

The default API reuses the module-level ``_default_event_support`` and
``provider_registry`` so that backward-compatible module-level functions
continue to work against the same state.
"""
from openfeature._event_support import _default_event_support # noqa: PLC0415
from openfeature.provider._registry import provider_registry # noqa: PLC0415

api = OpenFeatureAPI.__new__(OpenFeatureAPI)
api._hooks = []
api._evaluation_context = EvaluationContext()
api._transaction_context_propagator = NoOpTransactionContextPropagator()
api._event_support = _default_event_support
api._provider_registry = provider_registry

# Wire the registry to this API's event support and context getter
provider_registry._event_support = _default_event_support
provider_registry._evaluation_context_getter = api.get_evaluation_context

return api


_default_api = _create_default_api()
163 changes: 107 additions & 56 deletions openfeature/_event_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import threading
import typing
from collections import defaultdict
from collections.abc import Callable

from openfeature.event import (
EventDetails,
Expand All @@ -16,93 +17,143 @@
from openfeature.client import OpenFeatureClient


_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))
)


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:
for handler in self._client_handlers[client][event]:
handler(details)

def run_global_handlers(self, event: ProviderEvent, details: EventDetails) -> None:
with self._global_lock:
for handler in self._global_handlers[event]:
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
)
self.run_global_handlers(event, details)
with self._client_lock:
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:
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()):
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()


# Default instance used by the global singleton API
_default_event_support = EventSupport()


# Backward-compatible module-level functions delegating to the default instance
def run_client_handlers(
client: OpenFeatureClient, event: ProviderEvent, details: EventDetails
) -> None:
with _client_lock:
for handler in _client_handlers[client][event]:
handler(details)
_default_event_support.run_client_handlers(client, event, details)


def run_global_handlers(event: ProviderEvent, details: EventDetails) -> None:
with _global_lock:
for handler in _global_handlers[event]:
handler(details)
_default_event_support.run_global_handlers(event, 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)
_default_event_support.add_client_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)
_default_event_support.remove_client_handler(client, event, 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)
_default_event_support.add_global_handler(event, handler, get_client)


def remove_global_handler(event: ProviderEvent, handler: EventHandler) -> None:
with _global_lock:
_global_handlers[event].remove(handler)
_default_event_support.remove_global_handler(event, 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:
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:
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()):
handler(EventDetails(provider_name=client.provider.get_metadata().name))
_default_event_support.run_handlers_for_provider(provider, event, provider_details)


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