Skip to content
Merged
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
31 changes: 31 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[run]
source =
splitio/

omit =
tests/*
*/__init__.py

branch = True

relative_files = True

[report]
# Regexes for lines to exclude from consideration
exclude_lines =
# Have to re-enable the standard pragma
pragma: no cover

# Don't complain about missing debug-only code:
def __repr__
if self\.debug

# Don't complain if tests don't hit defensive assertion code:
raise AssertionError
raise NotImplementedError

# Don't complain if non-runnable code isn't run:
if 0:
if __name__ == .__main__.:

precision = 2
Binary file added .github/.DS_Store
Binary file not shown.
1 change: 1 addition & 0 deletions .github/workflows/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @splitio/sdk
73 changes: 73 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: ci
on:
push:
branches:
- main
- development
pull_request:
branches:
- main
- development

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
test:
name: Test
runs-on: ubuntu-22.04
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0

- name: Setup Python
uses: actions/setup-python@v3
with:
python-version: '3.9.13'

- name: Install dependencies
run: |
sudo apt update
sudo apt-get install -y libkrb5-dev
pip install -U setuptools pip wheel
pip install -e .[cpphash,redis,uwsgi]
pip install pytest --quiet
pip install mock
pip install pytest-asyncio
pip install -r requirements.txt

- name: Run tests
run: cd tests; pytest -v

- name: Set VERSION env
run: echo "VERSION=$(cat setup.py | grep "version=" | cut -d'"' -f2)" >> $GITHUB_ENV

- name: SonarQube Scan (Push)
if: github.event_name == 'push'
uses: SonarSource/sonarcloud-github-action@v1.9
env:
SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
projectBaseDir: .
args: >
-Dsonar.host.url=${{ secrets.SONARQUBE_HOST }}
-Dsonar.projectVersion=${{ env.VERSION }}

- name: SonarQube Scan (Pull Request)
if: github.event_name == 'pull_request'
uses: SonarSource/sonarcloud-github-action@v1.9
env:
SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
projectBaseDir: .
args: >
-Dsonar.host.url=${{ secrets.SONARQUBE_HOST }}
-Dsonar.projectVersion=${{ env.VERSION }}
-Dsonar.pullrequest.key=${{ github.event.pull_request.number }}
-Dsonar.pullrequest.branch=${{ github.event.pull_request.head.ref }}
-Dsonar.pullrequest.base=${{ github.event.pull_request.base.ref }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,6 @@ dmypy.json

# IDE
.idea/

# Other
.DS_Store
9 changes: 9 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
0.0.1
- First release. Up to date with spec 0.5.1 and python sdk 0.0.6

0.1.0
- Up to date with spec 0.8.0 and python sdk 0.8.1. Using split client 10.2.0

1.0.0 (Nov 5 2026)
- 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.
- Upgraded Split SDK to 10.5.1
- Upgraded OpenFeature SDK to 0.8.3
- Added support for asyncio mode
- Added ability to pass Ready Timeout and ConfigurationOptions to Provider initialization
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright © 2022 Split Software, Inc.
Copyright © 2025 Split Software, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
90 changes: 82 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
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 and higher.
This SDK is compatible with Python 3.9 and higher.

## Getting started
### Pip Installation
Expand All @@ -18,21 +18,28 @@ Below is a simple example that describes using the Split Provider. Please see th
```python
from openfeature import api
from split_openfeature import SplitProvider

api.set_provider(SplitProvider(api_key="YOUR_API_KEY"))
config = {
'impressionsMode': 'OPTIMIZED',
'impressionsRefreshRate': 30,
}
provider = SplitProvider({"SdkKey": "YOUR_API_KEY", "ConfigOptions": config, "ReadyBlockTime": 5})
api.set_provider(provider)
```

If you are more familiar with Split or want access to other initialization options, you can provide a Split `client` to the constructor. See the [Split Java SDK Documentation](https://help.split.io/hc/en-us/articles/360020405151-Java-SDK) for more information.
If you are more familiar with Split or want access to other initialization options, you can provide a Split `client` to the constructor. See the [Harness Split Python SDK Documentation](https://developer.harness.io/docs/feature-management-experimentation/sdks-and-infrastructure/server-side-sdks/python-sdk/) for more information.
```python
from openfeature import api
from split_openfeature import SplitProvider
from splitio import get_factory

factory = get_factory("YOUR_API_KEY", config=config_file)
config = {
'impressionsMode': 'OPTIMIZED',
'impressionsRefreshRate': 30,
}
factory = get_factory("YOUR_API_KEY", config=config)
factory.block_until_ready(5)
api.set_provider(SplitProvider(client=factory.client()))
api.set_provider(SplitProvider({"SplitClient": factory.client()}))
```
where config_file is the Split config file you want to use

## 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/).
Expand All @@ -56,9 +63,76 @@ or at the OpenFeatureAPI level
```python
context = EvaluationContext(targeting_key="TARGETING_KEY")
api.set_evaluation_context(context)
````
```
If the context was set at the client or api level, it is not required to provide it during flag evaluation.

### Asyncio mode
The provider supports asyncio mode as well, using the asyncio mode in Split SDK.
Example below shows using the provider in asyncio

```python
from openfeature import api
from split_openfeature import SplitProviderAsync
config = {
'impressionsMode': 'OPTIMIZED',
'impressionsRefreshRate': 30,
}
provider = SplitProvider({"SdkKey": "YOUR_API_KEY", "ConfigOptions": config, "ReadyBlockTime": 5})
await provider.create()
api.set_provider(provider)
```

Example below show how to create the Split Client externally and pass it to Provider
```python
from openfeature import api
from split_openfeature import SplitProviderAsync
from splitio import get_factory_async

config = {
'impressionsMode': 'OPTIMIZED',
'impressionsRefreshRate': 30,
}
factory = get_factory_async("YOUR_API_KEY", config=config)
await factory.block_until_ready(5)
provider = SplitProviderAsync({"SplitClient": factory.client()})
await provider.create()
api.set_provider(provider)
```

Example below fetching the treatment in asyncio mode
```python
from openfeature import api
from openfeature.evaluation_context import EvaluationContext

client = api.get_client("CLIENT_NAME")

context = EvaluationContext(targeting_key="TARGETING_KEY")
value = await client.get_boolean_value_async("FLAG_NAME", False, context)
```
### Logging
Split Provider use `logging` library, Each module has it's own logger, the root being split_provider. Below is an example of simple usage which will set all libraries using `logging` including the provider, to use `DEBUG` mode.
```python
import logging

logging.basicConfig(level=logging.DEBUG)
```

### Shutting down Split SDK factory
Currently OpenFeature SDK does not provide override for provider shutdown, when using internal split client object, the Split SDK will not shutdown properly. We recommend using the example below before terminating the OpenFeature object

```python
from threading import Event

destroy_event = Event()
provider._split_client_wrapper._factory.destroy(destroy_event)
destroy_event.wait()
```

Below the example for asyncio mode
```python
await provider._split_client_wrapper._factory.destroy()
```

## Submitting issues

The Split team monitors all issues submitted to this [issue tracker](https://github.com/splitio/split-openfeature-provider-python/issues). We encourage you to use this issue tracker to submit any bug reports, feedback, and feature enhancements. We'll do our best to respond in a timely manner.
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==10.5.1
splitio_client[cpphash,asyncio]==10.5.1
4 changes: 1 addition & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
setuptools.setup(
name="split_openfeature",
version="1.0.0",
author="Robert Grassian",
author_email="robert.grassian@split.io",
description="The official Python Split Provider for OpenFeature",
long_description=long_description,
long_description_content_type="text/markdown",
Expand All @@ -17,5 +15,5 @@
"Programming Language :: Python :: 3",
'Topic :: Software Development :: Libraries'
],
python_requires='>=3.5'
python_requires='>=3.9'
)
10 changes: 10 additions & 0 deletions sonar-project.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
sonar.projectName=split_openfeature
sonar.projectKey=split_openfeature
sonar.python.version=3.9
sonar.sources=split_openfeature
sonar.tests=tests
sonar.text.excluded.file.suffixes=.csv
sonar.python.coverage.reportPaths=coverage.xml
sonar.coverage.exclusions=**/__init__.py
sonar.links.ci=https://github.com/splitio/split-openfeature-provider-python
sonar.links.scm=https://github.com/splitio/split-openfeature-provider-python/actions
6 changes: 6 additions & 0 deletions split_openfeature/split_client_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ def is_sdk_ready(self):

return self.sdk_ready

def destroy(self, destroy_event=None):
self._factory.destroy(destroy_event)

async def destroy_async(self):
await self._factory.destroy()

async def is_sdk_ready_async(self):
if self.sdk_ready:
return True
Expand Down
4 changes: 2 additions & 2 deletions split_openfeature/split_provider.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import typing
import logging
import json

from openfeature.hook import Hook
from openfeature.evaluation_context import EvaluationContext
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.split_client_wrapper import SplitClientWrapper
import json

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -159,7 +159,7 @@ def resolve_float_details(self, flag_key: str, default_value: float,
def resolve_object_details(self, flag_key: str, default_value: dict,
evaluation_context: EvaluationContext = EvaluationContext()):
return self._evaluate_treatment(flag_key, evaluation_context, default_value)

class SplitProviderAsync(SplitProviderBase):
def __init__(self, initial_context):
if isinstance(initial_context, dict):
Expand Down
Loading