-
Notifications
You must be signed in to change notification settings - Fork 0
Add events metadata #25
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
base: development
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,2 @@ | ||
| openfeature_sdk==0.8.3 | ||
| splitio_client[cpphash,asyncio]==10.5.1 | ||
| splitio_client[cpphash,asyncio]>=10.5.1 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,13 +2,23 @@ | |
| from splitio.exceptions import TimeoutException | ||
| import logging | ||
|
|
||
| try: | ||
| from splitio.models.events import SdkEvent | ||
| except ImportError: | ||
| SdkEvent = None # type: ignore # Split < 10.6: no events API | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| # Sentinel for block_until_ready timeout (not a Split SdkEvent) | ||
| SPLIT_EVENT_BUR_TIMEOUT = "block_until_ready_timeout" | ||
|
|
||
|
|
||
| class SplitClientWrapper(): | ||
|
|
||
| def __init__(self, initial_context): | ||
| self.sdk_ready = False | ||
| self.split_client = None | ||
| self._event_receiver = None | ||
|
|
||
| if not self._validate_context(initial_context): | ||
| raise AttributeError() | ||
|
|
@@ -39,13 +49,15 @@ def __init__(self, initial_context): | |
| self.sdk_ready = True | ||
| except TimeoutException: | ||
| _LOGGER.debug("Split SDK timed out") | ||
| self._notify_receiver(SPLIT_EVENT_BUR_TIMEOUT, None) | ||
|
|
||
| self.split_client = self._factory.client() | ||
|
|
||
| async def create(self): | ||
| if self._initial_context.get("SplitClient") != None: | ||
| self.split_client = self._initial_context.get("SplitClient") | ||
| self._factory = self.split_client._factory | ||
| await self._register_split_events_async() | ||
| return | ||
|
|
||
| try: | ||
|
|
@@ -54,8 +66,10 @@ async def create(self): | |
| self.sdk_ready = True | ||
| except TimeoutException: | ||
| _LOGGER.debug("Split SDK timed out") | ||
| self._notify_receiver(SPLIT_EVENT_BUR_TIMEOUT, None) | ||
|
|
||
| self.split_client = self._factory.client() | ||
| await self._register_split_events_async() | ||
|
|
||
| def is_sdk_ready(self): | ||
| if self.sdk_ready: | ||
|
|
@@ -69,9 +83,59 @@ def is_sdk_ready(self): | |
|
|
||
| return self.sdk_ready | ||
|
|
||
| def set_event_receiver(self, receiver): | ||
| """Set the receiver that will be notified of Split SDK events (e.g. the provider).""" | ||
| self._event_receiver = receiver | ||
|
|
||
| def register_for_split_events(self): | ||
| """Register for Split SDK events (SDK_READY, SDK_UPDATE). Pass the provider as receiver (or call set_event_receiver first).""" | ||
| self._register_split_events() | ||
|
|
||
| def unregister_for_split_events(self): | ||
| """Stop receiving Split SDK events.""" | ||
| self._event_receiver = None | ||
|
|
||
| def _notify_receiver(self, split_event, event_metadata): | ||
| if self._event_receiver is None: | ||
| _LOGGER.debug("Split event %s: no receiver registered", split_event) | ||
| return | ||
| try: | ||
| self._event_receiver._on_split_event(split_event, event_metadata) | ||
| except Exception as ex: | ||
| _LOGGER.debug("Split event callback error: %s", ex) | ||
|
|
||
| def _register_split_events(self): | ||
| if self._factory is None: | ||
| _LOGGER.warning("SplitClientWrapper: _factory is None, cannot register for SDK events") | ||
| return | ||
| if SdkEvent is None: | ||
| _LOGGER.debug("SplitClientWrapper: SdkEvent not available (Split SDK < 10.6?), skipping event registration") | ||
| return | ||
| try: | ||
| em = self._factory._events_manager | ||
| if not hasattr(em, "register"): | ||
| _LOGGER.warning("SplitClientWrapper: events_manager has no register method") | ||
| return | ||
| em.register(SdkEvent.SDK_READY, lambda m: self._notify_receiver(SdkEvent.SDK_READY, m)) | ||
| em.register(SdkEvent.SDK_UPDATE, lambda m: self._notify_receiver(SdkEvent.SDK_UPDATE, m)) | ||
| _LOGGER.info("SplitClientWrapper: registered for SDK_READY and SDK_UPDATE") | ||
| except Exception as ex: | ||
| _LOGGER.warning("Could not register Split events: %s", ex) | ||
|
|
||
| def destroy(self, destroy_event=None): | ||
| self._factory.destroy(destroy_event) | ||
|
|
||
| async def _register_split_events_async(self): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we need to use different handler for async, keep in mind async mode does not support lambda, for ex: |
||
| if self._factory is None or SdkEvent is None: | ||
| return | ||
| try: | ||
| em = self._factory._events_manager | ||
| if hasattr(em, "register"): | ||
| await em.register(SdkEvent.SDK_READY, lambda m: self._notify_receiver(SdkEvent.SDK_READY, m)) | ||
| await em.register(SdkEvent.SDK_UPDATE, lambda m: self._notify_receiver(SdkEvent.SDK_UPDATE, m)) | ||
| except Exception as ex: | ||
| _LOGGER.debug("Could not register Split events: %s", ex) | ||
|
|
||
| async def destroy_async(self): | ||
| await self._factory.destroy() | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,15 +7,95 @@ | |
| from openfeature.exception import ErrorCode, GeneralError, ParseError, OpenFeatureError, TargetingKeyMissingError | ||
| from openfeature.flag_evaluation import Reason, FlagResolutionDetails | ||
| from openfeature.provider import AbstractProvider, Metadata | ||
| from split_openfeature_provider.split_client_wrapper import SplitClientWrapper | ||
| from openfeature.event import ProviderEventDetails | ||
| from split_openfeature_provider.split_client_wrapper import SplitClientWrapper, SPLIT_EVENT_BUR_TIMEOUT | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| try: | ||
| from splitio.models.events import SdkEvent | ||
| except ImportError: | ||
| SdkEvent = None # type: ignore | ||
|
|
||
|
|
||
| def _flags_changed_from_sdk_update(event_metadata): | ||
| """ | ||
| Extract list of updated flag/split names from Split SDK_UPDATE event metadata. | ||
| OpenFeature expects flags_changed: list[str] for PROVIDER_CONFIGURATION_CHANGED. | ||
| Handles: dict with "names", object with .metadata, or object with get_names() (Split EventsMetadata). | ||
| """ | ||
| if event_metadata is None: | ||
| return None | ||
| if hasattr(event_metadata, "metadata") and getattr(event_metadata, "metadata", None) is not None: | ||
| event_metadata = getattr(event_metadata, "metadata") | ||
| if isinstance(event_metadata, dict): | ||
| val = event_metadata.get("names") | ||
| if isinstance(val, list): | ||
| return [str(x) for x in val if x is not None] | ||
| return None | ||
| if hasattr(event_metadata, "get_names"): | ||
| names = event_metadata.get_names() | ||
| if names is not None: | ||
| return [str(x) for x in names if x is not None] | ||
| return None | ||
|
|
||
|
|
||
| def _metadata_from_split(split_event, event_metadata): | ||
| """Build OpenFeature event metadata dict from Split event (and optional Split metadata).""" | ||
| meta = {"split_event": getattr(split_event, "value", str(split_event))} | ||
| if event_metadata is not None and isinstance(event_metadata, dict): | ||
| for k, v in event_metadata.items(): | ||
| if isinstance(v, (bool, str, int, float)): | ||
| meta["split_%s" % k] = v | ||
| # Split may pass an object with get_type/get_names (e.g. EventsMetadata) | ||
| if event_metadata is not None and hasattr(event_metadata, "get_type"): | ||
| t = event_metadata.get_type() | ||
| meta["split_type"] = getattr(t, "value", str(t)) | ||
| if event_metadata is not None and hasattr(event_metadata, "get_names"): | ||
| names = event_metadata.get_names() | ||
| meta["split_names"] = list(names) if names is not None else [] | ||
| return meta | ||
|
|
||
|
|
||
| class SplitProviderBase(AbstractProvider): | ||
|
|
||
| def get_metadata(self) -> Metadata: | ||
| return Metadata("Split") | ||
|
|
||
| def attach(self, on_emit): | ||
| super().attach(on_emit) | ||
| self._split_client_wrapper.set_event_receiver(self) | ||
| self._split_client_wrapper.register_for_split_events() | ||
|
|
||
| def detach(self): | ||
| self._split_client_wrapper.unregister_for_split_events() | ||
| super().detach() | ||
|
|
||
| def _on_split_event(self, split_event, event_metadata): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add an async call with the same code |
||
| """Map Split SDK events to OpenFeature provider events with OpenFeature-friendly details.""" | ||
| _LOGGER.debug("SplitProvider: _on_split_event received %s", split_event) | ||
| if split_event == SPLIT_EVENT_BUR_TIMEOUT: | ||
| self.emit_provider_error(ProviderEventDetails( | ||
| message="Block until ready timed out", | ||
| error_code=ErrorCode.PROVIDER_NOT_READY, | ||
| metadata=_metadata_from_split(split_event, event_metadata), | ||
| )) | ||
| return | ||
| if SdkEvent is None: | ||
| return | ||
| if split_event == SdkEvent.SDK_READY: | ||
| self.emit_provider_ready(ProviderEventDetails( | ||
| metadata=_metadata_from_split(split_event, event_metadata), | ||
| )) | ||
| elif split_event == SdkEvent.SDK_UPDATE: | ||
| flags_changed = _flags_changed_from_sdk_update(event_metadata) | ||
| details = ProviderEventDetails( | ||
| flags_changed=flags_changed, | ||
| metadata=_metadata_from_split(split_event, event_metadata), | ||
| ) | ||
| _LOGGER.info("SplitProvider: emitting PROVIDER_CONFIGURATION_CHANGED flags_changed=%s", flags_changed) | ||
| self.emit_provider_configuration_changed(details) | ||
|
|
||
| def get_provider_hooks(self) -> typing.List[Hook]: | ||
| return [] | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need an async call for this method:
async def _notify_receiver(...)
# same code
await self._event_receiver._on_split_event_async(...)