From 7c96501845a82d95c67c86d86f0ea19035b38ade Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Fri, 27 Feb 2026 17:26:15 -0300 Subject: [PATCH 1/3] Add events with metadata --- CHANGES.txt | 4 + README.md | 5 +- requirements.txt | 2 +- .../split_client_wrapper.py | 64 +++++++++++++++ split_openfeature_provider/split_provider.py | 82 ++++++++++++++++++- 5 files changed, 153 insertions(+), 4 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index b792f39..fee4327 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,9 @@ CHANGES +1.1.0 (Feb 27 2026) +- Split SDK 10.5.1 remains supported. Provider lifecycle events (PROVIDER_READY, PROVIDER_CONFIGURATION_CHANGED, PROVIDER_ERROR) require Split SDK 10.6.0 or later; on 10.5.1 the provider works as before without emitting those events. +- Provider now emits OpenFeature provider events (PROVIDER_READY, PROVIDER_CONFIGURATION_CHANGED, PROVIDER_ERROR) when Split SDK 10.6+ fires ready/update/timeout. Event details include OpenFeature-friendly metadata (see docs/EVENTS_MAPPING.md). + 1.0.0 (Nov 10 2025) - BREAKING CHANGE: Passing the SplitClient object to Provider constructor is now only through the initialization context dictionary - BREAKING CHANGE: Provider will throw exception when ObjectDetail and ObjectValue evaluation is used, since it will attempt to parse the treatment as a JSON structure. diff --git a/README.md b/README.md index 32c717d..7f22f74 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,8 @@ This Provider is designed to allow the use of OpenFeature with Split, the platform for controlled rollouts, serving features to your users via the Split feature flag to manage your complete customer experience. ## Compatibility -This SDK is compatible with Python 3.9 and higher. +- Python 3.9 and higher. +- **Split SDK**: [Split Python SDK](https://github.com/splitio/python-client) **10.5.1 or later**. Provider lifecycle events (PROVIDER_READY, PROVIDER_CONFIGURATION_CHANGED, PROVIDER_ERROR) require **10.6.0 or later**; on 10.5.1 the provider works without emitting those events. ## Getting started @@ -13,7 +14,7 @@ This package replaces the previous `split-openfeature-provider` Python provider ### Pip Installation ```python -pip install split-openfeature-provider==1.0.0 +pip install split-openfeature-provider==1.1.0 ``` ### Configure it Below is a simple example that describes using the Split Provider. Please see the [OpenFeature Documentation](https://docs.openfeature.dev/docs/reference/concepts/evaluation-api) for details on how to use the OpenFeature SDK. diff --git a/requirements.txt b/requirements.txt index 42865f3..d1362f6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ openfeature_sdk==0.8.3 -splitio_client[cpphash,asyncio]==10.5.1 +splitio_client[cpphash,asyncio]>=10.5.1 diff --git a/split_openfeature_provider/split_client_wrapper.py b/split_openfeature_provider/split_client_wrapper.py index 048340a..04a4457 100644 --- a/split_openfeature_provider/split_client_wrapper.py +++ b/split_openfeature_provider/split_client_wrapper.py @@ -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,6 +49,7 @@ 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() @@ -46,6 +57,7 @@ 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): + 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() diff --git a/split_openfeature_provider/split_provider.py b/split_openfeature_provider/split_provider.py index e57bcbd..45ac2f5 100644 --- a/split_openfeature_provider/split_provider.py +++ b/split_openfeature_provider/split_provider.py @@ -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): + """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 [] From e0a2428a9eda16171f9446041f43ac2ff63e76ad Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Thu, 5 Mar 2026 17:58:53 -0300 Subject: [PATCH 2/3] Add async calls --- .../split_client_wrapper.py | 20 +- split_openfeature_provider/split_provider.py | 25 ++ tests/test_async_events.py | 274 ++++++++++++++++++ 3 files changed, 316 insertions(+), 3 deletions(-) create mode 100644 tests/test_async_events.py diff --git a/split_openfeature_provider/split_client_wrapper.py b/split_openfeature_provider/split_client_wrapper.py index 04a4457..9ef0052 100644 --- a/split_openfeature_provider/split_client_wrapper.py +++ b/split_openfeature_provider/split_client_wrapper.py @@ -66,7 +66,7 @@ async def create(self): self.sdk_ready = True except TimeoutException: _LOGGER.debug("Split SDK timed out") - self._notify_receiver(SPLIT_EVENT_BUR_TIMEOUT, None) + await self._notify_receiver_async(SPLIT_EVENT_BUR_TIMEOUT, None) self.split_client = self._factory.client() await self._register_split_events_async() @@ -104,6 +104,16 @@ def _notify_receiver(self, split_event, event_metadata): except Exception as ex: _LOGGER.debug("Split event callback error: %s", ex) + async def _notify_receiver_async(self, split_event, event_metadata): + """Async version for use when the receiver is used in asyncio context (e.g. async event registration).""" + if self._event_receiver is None: + _LOGGER.debug("Split event %s: no receiver registered", split_event) + return + try: + await self._event_receiver._on_split_event_async(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") @@ -131,8 +141,12 @@ async def _register_split_events_async(self): 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)) + async def handler_ready(m): + await self._notify_receiver_async(SdkEvent.SDK_READY, m) + async def handler_update(m): + await self._notify_receiver_async(SdkEvent.SDK_UPDATE, m) + await em.register(SdkEvent.SDK_READY, handler_ready) + await em.register(SdkEvent.SDK_UPDATE, handler_update) except Exception as ex: _LOGGER.debug("Could not register Split events: %s", ex) diff --git a/split_openfeature_provider/split_provider.py b/split_openfeature_provider/split_provider.py index 45ac2f5..f009111 100644 --- a/split_openfeature_provider/split_provider.py +++ b/split_openfeature_provider/split_provider.py @@ -96,6 +96,31 @@ def _on_split_event(self, split_event, event_metadata): _LOGGER.info("SplitProvider: emitting PROVIDER_CONFIGURATION_CHANGED flags_changed=%s", flags_changed) self.emit_provider_configuration_changed(details) + async def _on_split_event_async(self, split_event, event_metadata): + """Async version for asyncio path; same logic as _on_split_event (emit_* are sync).""" + _LOGGER.debug("SplitProvider: _on_split_event_async 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 [] diff --git a/tests/test_async_events.py b/tests/test_async_events.py new file mode 100644 index 0000000..87aca00 --- /dev/null +++ b/tests/test_async_events.py @@ -0,0 +1,274 @@ +import pytest +import unittest +from unittest.mock import MagicMock, AsyncMock, patch, call +from openfeature.exception import ErrorCode +from openfeature.event import ProviderEventDetails + +from split_openfeature_provider import SplitProvider, SplitProviderAsync +from split_openfeature_provider.split_client_wrapper import SplitClientWrapper, SPLIT_EVENT_BUR_TIMEOUT + +try: + from splitio.models.events import SdkEvent +except ImportError: + SdkEvent = None + + +class MockEventReceiver: + """Mock event receiver for testing async event notifications.""" + def __init__(self): + self.events = [] + + async def _on_split_event_async(self, split_event, event_metadata): + self.events.append({"event": split_event, "metadata": event_metadata}) + + +class TestSplitClientWrapperAsyncEvents: + """Tests for async event notification in SplitClientWrapper.""" + + @pytest.mark.asyncio + async def test_notify_receiver_async_with_receiver(self): + """Test that _notify_receiver_async calls the receiver's async method.""" + wrapper = SplitClientWrapper({"SdkKey": "test", "ConfigOptions": {}, "ThreadingMode": "asyncio"}) + receiver = MockEventReceiver() + wrapper.set_event_receiver(receiver) + + # Call the async notify method + test_metadata = {"test": "data"} + await wrapper._notify_receiver_async("test_event", test_metadata) + + # Verify the receiver was notified + assert len(receiver.events) == 1 + assert receiver.events[0]["event"] == "test_event" + assert receiver.events[0]["metadata"] == test_metadata + + @pytest.mark.asyncio + async def test_notify_receiver_async_without_receiver(self): + """Test that _notify_receiver_async handles missing receiver gracefully.""" + wrapper = SplitClientWrapper({"SdkKey": "test", "ConfigOptions": {}, "ThreadingMode": "asyncio"}) + + # Should not raise an exception when no receiver is set + await wrapper._notify_receiver_async("test_event", None) + + @pytest.mark.asyncio + async def test_notify_receiver_async_with_exception(self): + """Test that _notify_receiver_async handles receiver exceptions gracefully.""" + wrapper = SplitClientWrapper({"SdkKey": "test", "ConfigOptions": {}, "ThreadingMode": "asyncio"}) + + # Create a receiver that raises an exception + receiver = MagicMock() + receiver._on_split_event_async = AsyncMock(side_effect=Exception("Test exception")) + wrapper.set_event_receiver(receiver) + + # Should not raise the exception + await wrapper._notify_receiver_async("test_event", None) + + @pytest.mark.asyncio + async def test_timeout_calls_notify_receiver_async(self): + """Test that timeout during async create calls _notify_receiver_async.""" + from splitio.exceptions import TimeoutException + + receiver = MockEventReceiver() + wrapper = SplitClientWrapper({ + "ReadyBlockTime": 0.1, + "SdkKey": "invalid_key", + "ConfigOptions": {}, + "ThreadingMode": "asyncio" + }) + wrapper.set_event_receiver(receiver) + + # Mock get_factory_async to raise TimeoutException + with patch('split_openfeature_provider.split_client_wrapper.get_factory_async') as mock_factory: + mock_factory_instance = AsyncMock() + mock_factory_instance.block_until_ready = AsyncMock(side_effect=TimeoutException("timeout")) + mock_factory_instance.client = MagicMock(return_value=MagicMock()) + mock_factory.return_value = mock_factory_instance + + await wrapper.create() + + # Verify that the receiver was notified of the timeout + assert len(receiver.events) == 1 + assert receiver.events[0]["event"] == SPLIT_EVENT_BUR_TIMEOUT + assert receiver.events[0]["metadata"] is None + + +@pytest.mark.skipif(SdkEvent is None, reason="SdkEvent not available in this Split SDK version") +class TestSplitProviderAsyncEvents: + """Tests for async event handling in SplitProvider.""" + + @pytest.mark.asyncio + async def test_on_split_event_async_sdk_ready(self): + """Test that _on_split_event_async emits provider ready event.""" + # Create a mock client + mock_client = MagicMock() + provider = SplitProviderAsync({"SplitClient": mock_client}) + + # Mock the emit_provider_ready method + provider.emit_provider_ready = MagicMock() + + # Call the async event handler + test_metadata = {"timestamp": 123456} + await provider._on_split_event_async(SdkEvent.SDK_READY, test_metadata) + + # Verify that emit_provider_ready was called + assert provider.emit_provider_ready.called + call_args = provider.emit_provider_ready.call_args[0][0] + assert isinstance(call_args, ProviderEventDetails) + assert call_args.metadata is not None + assert "split_event" in call_args.metadata + + @pytest.mark.asyncio + async def test_on_split_event_async_sdk_update(self): + """Test that _on_split_event_async emits provider configuration changed event.""" + # Create a mock client + mock_client = MagicMock() + provider = SplitProviderAsync({"SplitClient": mock_client}) + + # Mock the emit_provider_configuration_changed method + provider.emit_provider_configuration_changed = MagicMock() + + # Call the async event handler with metadata containing changed flags + test_metadata = {"names": ["flag1", "flag2"]} + await provider._on_split_event_async(SdkEvent.SDK_UPDATE, test_metadata) + + # Verify that emit_provider_configuration_changed was called + assert provider.emit_provider_configuration_changed.called + call_args = provider.emit_provider_configuration_changed.call_args[0][0] + assert isinstance(call_args, ProviderEventDetails) + assert call_args.flags_changed == ["flag1", "flag2"] + assert call_args.metadata is not None + assert "split_event" in call_args.metadata + + @pytest.mark.asyncio + async def test_on_split_event_async_bur_timeout(self): + """Test that _on_split_event_async emits provider error on timeout.""" + # Create a mock client + mock_client = MagicMock() + provider = SplitProviderAsync({"SplitClient": mock_client}) + + # Mock the emit_provider_error method + provider.emit_provider_error = MagicMock() + + # Call the async event handler with timeout event + await provider._on_split_event_async(SPLIT_EVENT_BUR_TIMEOUT, None) + + # Verify that emit_provider_error was called + assert provider.emit_provider_error.called + call_args = provider.emit_provider_error.call_args[0][0] + assert isinstance(call_args, ProviderEventDetails) + assert call_args.error_code == ErrorCode.PROVIDER_NOT_READY + assert "Block until ready timed out" in call_args.message + assert call_args.metadata is not None + + @pytest.mark.asyncio + async def test_on_split_event_async_with_event_object_metadata(self): + """Test that _on_split_event_async handles EventsMetadata objects.""" + # Create a mock client + mock_client = MagicMock() + provider = SplitProviderAsync({"SplitClient": mock_client}) + + # Mock the emit_provider_configuration_changed method + provider.emit_provider_configuration_changed = MagicMock() + + # Create a mock EventsMetadata object + mock_metadata = MagicMock() + mock_metadata.metadata = None # Ensure the metadata attribute is None + mock_metadata.get_names.return_value = ["flag_a", "flag_b"] + mock_metadata.get_type.return_value = MagicMock(value="SPLIT_UPDATE") + + # Call the async event handler + await provider._on_split_event_async(SdkEvent.SDK_UPDATE, mock_metadata) + + # Verify that emit_provider_configuration_changed was called + assert provider.emit_provider_configuration_changed.called + call_args = provider.emit_provider_configuration_changed.call_args[0][0] + assert isinstance(call_args, ProviderEventDetails) + assert call_args.flags_changed == ["flag_a", "flag_b"] + assert "split_names" in call_args.metadata + assert call_args.metadata["split_names"] == ["flag_a", "flag_b"] + + @pytest.mark.asyncio + async def test_on_split_event_async_update_with_null_names(self): + """Test that _on_split_event_async handles SDK_UPDATE with no flag names.""" + # Create a mock client + mock_client = MagicMock() + provider = SplitProviderAsync({"SplitClient": mock_client}) + + # Mock the emit_provider_configuration_changed method + provider.emit_provider_configuration_changed = MagicMock() + + # Call the async event handler with no metadata + await provider._on_split_event_async(SdkEvent.SDK_UPDATE, None) + + # Verify that emit_provider_configuration_changed was called + assert provider.emit_provider_configuration_changed.called + call_args = provider.emit_provider_configuration_changed.call_args[0][0] + assert isinstance(call_args, ProviderEventDetails) + assert call_args.flags_changed is None + + +class TestSyncProviderEvents: + """Tests for sync event handling in SplitProvider (for completeness).""" + + def test_on_split_event_sdk_ready(self): + """Test that _on_split_event emits provider ready event.""" + if SdkEvent is None: + pytest.skip("SdkEvent not available") + + # Create a mock client + mock_client = MagicMock() + provider = SplitProvider({"SplitClient": mock_client}) + + # Mock the emit_provider_ready method + provider.emit_provider_ready = MagicMock() + + # Call the sync event handler + test_metadata = {"timestamp": 123456} + provider._on_split_event(SdkEvent.SDK_READY, test_metadata) + + # Verify that emit_provider_ready was called + assert provider.emit_provider_ready.called + call_args = provider.emit_provider_ready.call_args[0][0] + assert isinstance(call_args, ProviderEventDetails) + assert call_args.metadata is not None + assert "split_event" in call_args.metadata + + def test_on_split_event_sdk_update(self): + """Test that _on_split_event emits provider configuration changed event.""" + if SdkEvent is None: + pytest.skip("SdkEvent not available") + + # Create a mock client + mock_client = MagicMock() + provider = SplitProvider({"SplitClient": mock_client}) + + # Mock the emit_provider_configuration_changed method + provider.emit_provider_configuration_changed = MagicMock() + + # Call the sync event handler with metadata containing changed flags + test_metadata = {"names": ["flag1", "flag2"]} + provider._on_split_event(SdkEvent.SDK_UPDATE, test_metadata) + + # Verify that emit_provider_configuration_changed was called + assert provider.emit_provider_configuration_changed.called + call_args = provider.emit_provider_configuration_changed.call_args[0][0] + assert isinstance(call_args, ProviderEventDetails) + assert call_args.flags_changed == ["flag1", "flag2"] + + def test_on_split_event_bur_timeout(self): + """Test that _on_split_event emits provider error on timeout.""" + # Create a mock client + mock_client = MagicMock() + provider = SplitProvider({"SplitClient": mock_client}) + + # Mock the emit_provider_error method + provider.emit_provider_error = MagicMock() + + # Call the sync event handler with timeout event + provider._on_split_event(SPLIT_EVENT_BUR_TIMEOUT, None) + + # Verify that emit_provider_error was called + assert provider.emit_provider_error.called + call_args = provider.emit_provider_error.call_args[0][0] + assert isinstance(call_args, ProviderEventDetails) + assert call_args.error_code == ErrorCode.PROVIDER_NOT_READY + assert "Block until ready timed out" in call_args.message From a3ec6028358a6a4bc7d49569d31839f08bb7c3b0 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Fri, 6 Mar 2026 15:18:29 -0300 Subject: [PATCH 3/3] Update readme --- README.md | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7f22f74..a4d287a 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Below is a simple example that describes using the Split Provider. Please see th from openfeature import api from split_openfeature_provider import SplitProvider config = { - 'impressionsMode': 'OPTIMIZED', + 'impressionsMode': 'optimized', 'impressionsRefreshRate': 30, } provider = SplitProvider({"SdkKey": "YOUR_API_KEY", "ConfigOptions": config, "ReadyBlockTime": 5}) @@ -37,7 +37,7 @@ from split_openfeature_provider import SplitProvider from splitio import get_factory config = { - 'impressionsMode': 'OPTIMIZED', + 'impressionsMode': 'optimized', 'impressionsRefreshRate': 30, } factory = get_factory("YOUR_API_KEY", config=config) @@ -45,6 +45,34 @@ factory.block_until_ready(5) api.set_provider(SplitProvider({"SplitClient": factory.client()})) ``` +## Example + +A minimal end-to-end example (sync): initialize the provider, set a targeting context, and evaluate a flag. + +```python +from openfeature import api +from openfeature.evaluation_context import EvaluationContext +from split_openfeature_provider import SplitProvider + +# Initialize with your SDK key (or use "localhost" + splitFile for local YAML) +provider = SplitProvider({ + "SdkKey": "YOUR_API_KEY", + "ConfigOptions": {"impressionsMode": "optimized"}, + "ReadyBlockTime": 5, +}) +api.set_provider(provider) + +# Get a client and set targeting context +client = api.get_client() +client.context = EvaluationContext(targeting_key="user-123") + +# Evaluate flags +show_new_ui = client.get_boolean_value("my_feature_flag", False) +print("show_new_ui:", show_new_ui) +``` + +With **asyncio**, use `SplitProviderAsync`, `await provider.create()`, and `await client.get_boolean_value_async(...)` as shown in the Asyncio mode section below. + ## Use of OpenFeature with Split After the initial setup you can use OpenFeature according to their [documentation](https://docs.openfeature.dev/docs/reference/concepts/evaluation-api/). @@ -78,10 +106,10 @@ Example below shows using the provider in asyncio from openfeature import api from split_openfeature_provider import SplitProviderAsync config = { - 'impressionsMode': 'OPTIMIZED', + 'impressionsMode': 'optimized', 'impressionsRefreshRate': 30, } -provider = SplitProvider({"SdkKey": "YOUR_API_KEY", "ConfigOptions": config, "ReadyBlockTime": 5}) +provider = SplitProviderAsync({"SdkKey": "YOUR_API_KEY", "ConfigOptions": config, "ReadyBlockTime": 5}) await provider.create() api.set_provider(provider) ``` @@ -93,7 +121,7 @@ from split_openfeature_provider import SplitProviderAsync from splitio import get_factory_async config = { - 'impressionsMode': 'OPTIMIZED', + 'impressionsMode': 'optimized', 'impressionsRefreshRate': 30, } factory = get_factory_async("YOUR_API_KEY", config=config)