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
4 changes: 4 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@
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

This package replaces the previous `split-openfeature-provider` Python provider in [Pypi](https://pypi.org/project/split-openfeature-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.
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
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
64 changes: 64 additions & 0 deletions split_openfeature_provider/split_client_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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()

Expand Down
82 changes: 81 additions & 1 deletion split_openfeature_provider/split_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 []

Expand Down