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
110 changes: 96 additions & 14 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,117 @@ name: Checks
on:
push:
branches: ['**']
pull_request:

permissions:
contents: read

concurrency:
group: checks-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
checks:
tests:
name: Python ${{ matrix.python-version }}
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
python-version: ['3.10', '3.14']

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- uses: actions/setup-python@v5
- uses: actions/setup-python@v6
with:
python-version: '3.13'
python-version: ${{ matrix.python-version }}
cache: pip

- name: Install development dependencies
run: python -m pip install -e '.[dev]'

- name: Check
run: make check

- name: Test
run: make test
package:
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- uses: actions/checkout@v6

##
# Example project
#
- uses: actions/setup-node@v4
- uses: actions/setup-python@v6
with:
python-version: '3.14'
cache: pip

- name: Install development dependencies
run: python -m pip install -e '.[dev]'

- name: Build and validate distributions
run: |
python -m build
python -m twine check dist/*

- name: Verify source distribution contents
run: |
tar -tzf dist/featurevisor-*.tar.gz | grep '/conformance/sdk-v3.json$'
tar -tzf dist/featurevisor-*.tar.gz | grep '/tests/test_sdk.py$'
tar -tzf dist/featurevisor-*.tar.gz | grep '/tests_openfeature/test_provider.py$'

- name: Verify base wheel installation
run: |
python -m venv /tmp/featurevisor-base
/tmp/featurevisor-base/bin/python -m pip install --no-deps dist/featurevisor-*.whl
/tmp/featurevisor-base/bin/python -c 'import featurevisor; assert featurevisor.__all__ == ["Featurevisor", "FeaturevisorChildInstance", "FeaturevisorModule", "create_featurevisor"]'
/tmp/featurevisor-base/bin/python -c 'from importlib.util import find_spec; assert find_spec("openfeature") is None'
/tmp/featurevisor-base/bin/python -c 'from pathlib import Path; import featurevisor; assert Path(featurevisor.__file__).with_name("py.typed").is_file()'
/tmp/featurevisor-base/bin/python - <<'PY'
try:
import featurevisor.openfeature
except ModuleNotFoundError as exc:
assert "featurevisor[openfeature]" in str(exc)
else:
raise AssertionError("OpenFeature import unexpectedly succeeded without its optional dependency")
PY
/tmp/featurevisor-base/bin/python -m unittest discover -s tests -v
/tmp/featurevisor-base/bin/python -m pip check

- name: Verify OpenFeature wheel installation
run: |
python -m venv /tmp/featurevisor-openfeature
WHEEL=$(find dist -name 'featurevisor-*.whl' -print -quit)
/tmp/featurevisor-openfeature/bin/python -m pip install "${WHEEL}[openfeature]"
/tmp/featurevisor-openfeature/bin/python -m unittest discover -s tests_openfeature -v
/tmp/featurevisor-openfeature/bin/python -m pip check

example-project:
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- uses: actions/checkout@v6

- uses: actions/setup-python@v6
with:
python-version: '3.14'
cache: pip

- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'

- name: Setup Featurevisor example-1 project
- name: Install Python SDK
run: python -m pip install -e .

- name: Set up Featurevisor v3 example-1 project
run: |
mkdir example-1
(cd example-1 && npx @featurevisor/cli@2.x init --example=1)
(cd example-1 && npx --yes @featurevisor/cli@3.x init --example=1)
(cd example-1 && npm install)
(cd example-1 && npx featurevisor build)
(cd example-1 && npx featurevisor test)

- name: Run Featurevisor project tests against Python SDK
run: PYTHONPATH=src python3 -m featurevisor test --projectDirectoryPath=./example-1
- name: Run project tests against Python SDK
run: python -m featurevisor test --projectDirectoryPath=./example-1 --onlyFailures
35 changes: 29 additions & 6 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,45 @@ on:
- 'v*.*.*'

permissions:
contents: read
id-token: write
contents: write

jobs:
publish:
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- name: Check out
uses: actions/checkout@v4
uses: actions/checkout@v6

- name: Install build tools
run: pip install hatch
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.14'
cache: pip

- name: Build package
run: hatch build
- name: Install development dependencies
run: python -m pip install -e '.[dev]'

- name: Verify tag matches package version
run: test "${GITHUB_REF_NAME#v}" = "$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')"

- name: Run checks
run: make check

- name: Build and validate package
run: |
python -m build
python -m twine check dist/*

- name: Verify built OpenFeature package
run: |
python -m venv /tmp/featurevisor-release
WHEEL=$(find dist -name 'featurevisor-*.whl' -print -quit)
/tmp/featurevisor-release/bin/python -m pip install "${WHEEL}[openfeature]"
/tmp/featurevisor-release/bin/python -m unittest discover -s tests_openfeature -v
/tmp/featurevisor-release/bin/python -m pip check

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
5 changes: 5 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
include LICENSE
include README.md
recursive-include conformance *.json
recursive-include tests *.py
recursive-include tests_openfeature *.py
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,18 @@ setup-golang-sdk:
update-golang-sdk:
(cd featurevisor-go && git pull origin main)

.PHONY: test
.PHONY: test test-openfeature typecheck check
test:
PYTHONPATH=src python3 -m unittest discover -s tests -v

test-openfeature:
PYTHONPATH=src python3 -m unittest discover -s tests_openfeature -v

typecheck:
python3 -m mypy

check: test test-openfeature typecheck

.PHONY: test-example-1
test-example-1:
PYTHONPATH=src python3 -m unittest discover -s tests -v
Expand Down
128 changes: 126 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ This SDK is compatible with Featurevisor v3 projects and v2 datafiles.
- [Registering modules](#registering-modules)
- [Child instance](#child-instance)
- [Close](#close)
- [OpenFeature](#openfeature)
- [Installation](#installation-1)
- [Provider setup](#provider-setup)
- [Flag key mapping](#flag-key-mapping)
- [Context mapping](#context-mapping)
- [Resolution details](#resolution-details)
- [Tracking](#tracking)
- [Using an existing Featurevisor instance](#using-an-existing-featurevisor-instance)
- [CLI usage](#cli-usage)
- [Test](#test)
- [Benchmark](#benchmark)
Expand Down Expand Up @@ -580,6 +588,119 @@ python -m featurevisor assess-distribution \
--populateUuid=deviceId
```

## OpenFeature

The provider targets OpenFeature specification `0.8.0` through OpenFeature Python SDK `0.10.x`. OpenFeature remains optional and is not installed or imported by the base Featurevisor package.

### Installation

```bash
pip install "featurevisor[openfeature]"
```

If the extra is not installed, importing `featurevisor.openfeature` reports the installation command needed to enable it.

### Provider setup

```python
from featurevisor.openfeature import FeaturevisorOpenFeatureProvider
from openfeature import api
from openfeature.evaluation_context import EvaluationContext

provider = FeaturevisorOpenFeatureProvider({"datafile": datafile_content})
api.set_provider_and_wait(provider)

client = api.get_client()
enabled = client.get_boolean_value(
"checkout",
False,
EvaluationContext(targeting_key="user-123", attributes={"country": "nl"}),
)
```

Call `api.shutdown()` during application shutdown. This closes a Featurevisor instance created by the provider and releases provider subscriptions.

### Flag key mapping

| OpenFeature key | Featurevisor evaluation |
| --- | --- |
| `checkout` | Boolean flag for `checkout` |
| `checkout:variation` | Variation value for `checkout` |
| `checkout:title` | Variable `title` for `checkout` |

Boolean variables use the boolean resolver. Integer and double variables use their matching numeric resolvers. Arrays, objects, and JSON variables use the object resolver.

The first separator divides the feature key from the selector. Use `key_separator` and `variation_key` when project keys require a different grammar:

```python
provider = FeaturevisorOpenFeatureProvider(
{"datafile": datafile_content},
key_separator="/",
variation_key="$variation",
)
```

This makes `checkout/$variation` the variation key and `checkout/title` a variable key.

### Context mapping

OpenFeature's targeting key maps to `userId` by default. Use `targeting_key_field` to map it to another Featurevisor context field:

```python
provider = FeaturevisorOpenFeatureProvider(
{"datafile": datafile_content},
targeting_key_field="accountId",
)
```

OpenFeature context attributes are copied without mutating the incoming context. Nested arrays and mappings are preserved. Datetimes are normalized to UTC ISO strings, matching the JavaScript provider.

### Resolution details

The provider maps Featurevisor evaluation results to OpenFeature details:

| Featurevisor result | OpenFeature result |
| --- | --- |
| Required, forced, sticky, or rule match | `TARGETING_MATCH` |
| Traffic allocation | `SPLIT` |
| Disabled variation or variable | `DISABLED` |
| No match or variable default | `DEFAULT` |
| Missing feature, variable, or variations | `ERROR` with `FLAG_NOT_FOUND` |
| Wrong resolver type | `ERROR` with `TYPE_MISMATCH` |
| Invalid datafile | `ERROR` with `PARSE_ERROR` |
| Evaluation failure | `ERROR` with `GENERAL` |

Errors return the default value supplied to OpenFeature. A malformed datafile uses the stable message `Could not parse datafile`. A later successful `set_datafile` call clears the parse error.

Resolution metadata can include `featureKey`, `variableKey`, `featurevisorReason`, `revision`, `schemaVersion`, `ruleKey`, `bucketKey`, `bucketValue`, `forceIndex`, and `variableOverrideIndex`. The selected variation is exposed as the OpenFeature variant when available.

### Tracking

Tracking is a no-op unless `on_track` is configured:

```python
def handle_track(name, context, details):
print(name, context, details)

provider = FeaturevisorOpenFeatureProvider(
{"datafile": datafile_content},
on_track=handle_track,
)
```

### Using an existing Featurevisor instance

```python
from featurevisor import create_featurevisor

featurevisor = create_featurevisor({"datafile": datafile_content})
provider = FeaturevisorOpenFeatureProvider(featurevisor=featurevisor)
```

The caller owns an instance passed this way. Provider shutdown does not close it. Call `featurevisor.close()` when every consumer is finished with it. When the provider creates the instance from options, the provider owns and closes it. If both are supplied, `featurevisor` takes precedence over the options dictionary.

See the [OpenFeature provider guide](https://featurevisor.com/docs/sdks/openfeature/) for resolution reasons, errors, metadata, tracking, lifecycle, and providers for other languages.

<!-- FEATUREVISOR_DOCS_END -->

## Development
Expand All @@ -593,14 +714,17 @@ This repository assumes:
Run the local test suite:

```bash
make test
python -m pip install -e '.[dev]'
make check
```

`make check` runs the base SDK tests, OpenFeature provider tests, and static type checking. You can also run them separately with `make test`, `make test-openfeature`, and `make typecheck`.

Run the example project integration directly:

```bash
PYTHONPATH=src python3 -m featurevisor test \
--projectDirectoryPath=/Users/fahad/Projects/featurevisor/featurevisor/examples/example-1 \
--projectDirectoryPath=../featurevisor/examples/example-1 \
--onlyFailures
```

Expand Down
Loading