Skip to content

Commit 2a89743

Browse files
committed
adjustments
1 parent 6db9e93 commit 2a89743

14 files changed

Lines changed: 648 additions & 117 deletions

File tree

‎.github/workflows/checks.yml‎

Lines changed: 96 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,117 @@ name: Checks
33
on:
44
push:
55
branches: ['**']
6+
pull_request:
7+
8+
permissions:
9+
contents: read
10+
11+
concurrency:
12+
group: checks-${{ github.workflow }}-${{ github.ref }}
13+
cancel-in-progress: true
614

715
jobs:
8-
checks:
16+
tests:
17+
name: Python ${{ matrix.python-version }}
918
runs-on: ubuntu-latest
19+
timeout-minutes: 10
20+
strategy:
21+
fail-fast: false
22+
matrix:
23+
python-version: ['3.10', '3.14']
1024

1125
steps:
12-
- uses: actions/checkout@v4
26+
- uses: actions/checkout@v6
1327

14-
- uses: actions/setup-python@v5
28+
- uses: actions/setup-python@v6
1529
with:
16-
python-version: '3.13'
30+
python-version: ${{ matrix.python-version }}
31+
cache: pip
32+
33+
- name: Install development dependencies
34+
run: python -m pip install -e '.[dev]'
35+
36+
- name: Check
37+
run: make check
1738

18-
- name: Test
19-
run: make test
39+
package:
40+
runs-on: ubuntu-latest
41+
timeout-minutes: 10
42+
43+
steps:
44+
- uses: actions/checkout@v6
2045

21-
##
22-
# Example project
23-
#
24-
- uses: actions/setup-node@v4
46+
- uses: actions/setup-python@v6
47+
with:
48+
python-version: '3.14'
49+
cache: pip
50+
51+
- name: Install development dependencies
52+
run: python -m pip install -e '.[dev]'
53+
54+
- name: Build and validate distributions
55+
run: |
56+
python -m build
57+
python -m twine check dist/*
58+
59+
- name: Verify source distribution contents
60+
run: |
61+
tar -tzf dist/featurevisor-*.tar.gz | grep '/conformance/sdk-v3.json$'
62+
tar -tzf dist/featurevisor-*.tar.gz | grep '/tests/test_sdk.py$'
63+
tar -tzf dist/featurevisor-*.tar.gz | grep '/tests_openfeature/test_provider.py$'
64+
65+
- name: Verify base wheel installation
66+
run: |
67+
python -m venv /tmp/featurevisor-base
68+
/tmp/featurevisor-base/bin/python -m pip install --no-deps dist/featurevisor-*.whl
69+
/tmp/featurevisor-base/bin/python -c 'import featurevisor; assert featurevisor.__all__ == ["Featurevisor", "FeaturevisorChildInstance", "FeaturevisorModule", "create_featurevisor"]'
70+
/tmp/featurevisor-base/bin/python -c 'from importlib.util import find_spec; assert find_spec("openfeature") is None'
71+
/tmp/featurevisor-base/bin/python -c 'from pathlib import Path; import featurevisor; assert Path(featurevisor.__file__).with_name("py.typed").is_file()'
72+
/tmp/featurevisor-base/bin/python - <<'PY'
73+
try:
74+
import featurevisor.openfeature
75+
except ModuleNotFoundError as exc:
76+
assert "featurevisor[openfeature]" in str(exc)
77+
else:
78+
raise AssertionError("OpenFeature import unexpectedly succeeded without its optional dependency")
79+
PY
80+
/tmp/featurevisor-base/bin/python -m unittest discover -s tests -v
81+
/tmp/featurevisor-base/bin/python -m pip check
82+
83+
- name: Verify OpenFeature wheel installation
84+
run: |
85+
python -m venv /tmp/featurevisor-openfeature
86+
WHEEL=$(find dist -name 'featurevisor-*.whl' -print -quit)
87+
/tmp/featurevisor-openfeature/bin/python -m pip install "${WHEEL}[openfeature]"
88+
/tmp/featurevisor-openfeature/bin/python -m unittest discover -s tests_openfeature -v
89+
/tmp/featurevisor-openfeature/bin/python -m pip check
90+
91+
example-project:
92+
runs-on: ubuntu-latest
93+
timeout-minutes: 15
94+
95+
steps:
96+
- uses: actions/checkout@v6
97+
98+
- uses: actions/setup-python@v6
99+
with:
100+
python-version: '3.14'
101+
cache: pip
102+
103+
- uses: actions/setup-node@v6
25104
with:
26105
node-version-file: '.nvmrc'
27106

28-
- name: Setup Featurevisor example-1 project
107+
- name: Install Python SDK
108+
run: python -m pip install -e .
109+
110+
- name: Set up Featurevisor v3 example-1 project
29111
run: |
30112
mkdir example-1
31-
(cd example-1 && npx @featurevisor/cli@2.x init --example=1)
113+
(cd example-1 && npx --yes @featurevisor/cli@3.x init --example=1)
32114
(cd example-1 && npm install)
33115
(cd example-1 && npx featurevisor build)
34116
(cd example-1 && npx featurevisor test)
35117
36-
- name: Run Featurevisor project tests against Python SDK
37-
run: PYTHONPATH=src python3 -m featurevisor test --projectDirectoryPath=./example-1
118+
- name: Run project tests against Python SDK
119+
run: python -m featurevisor test --projectDirectoryPath=./example-1 --onlyFailures

‎.github/workflows/publish.yml‎

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,45 @@ on:
66
- 'v*.*.*'
77

88
permissions:
9+
contents: read
910
id-token: write
10-
contents: write
1111

1212
jobs:
1313
publish:
1414
runs-on: ubuntu-latest
15+
timeout-minutes: 15
1516

1617
steps:
1718
- name: Check out
18-
uses: actions/checkout@v4
19+
uses: actions/checkout@v6
1920

20-
- name: Install build tools
21-
run: pip install hatch
21+
- name: Set up Python
22+
uses: actions/setup-python@v6
23+
with:
24+
python-version: '3.14'
25+
cache: pip
2226

23-
- name: Build package
24-
run: hatch build
27+
- name: Install development dependencies
28+
run: python -m pip install -e '.[dev]'
29+
30+
- name: Verify tag matches package version
31+
run: test "${GITHUB_REF_NAME#v}" = "$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')"
32+
33+
- name: Run checks
34+
run: make check
35+
36+
- name: Build and validate package
37+
run: |
38+
python -m build
39+
python -m twine check dist/*
40+
41+
- name: Verify built OpenFeature package
42+
run: |
43+
python -m venv /tmp/featurevisor-release
44+
WHEEL=$(find dist -name 'featurevisor-*.whl' -print -quit)
45+
/tmp/featurevisor-release/bin/python -m pip install "${WHEEL}[openfeature]"
46+
/tmp/featurevisor-release/bin/python -m unittest discover -s tests_openfeature -v
47+
/tmp/featurevisor-release/bin/python -m pip check
2548
2649
- name: Publish to PyPI
2750
uses: pypa/gh-action-pypi-publish@release/v1

‎MANIFEST.in‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
include LICENSE
2+
include README.md
3+
recursive-include conformance *.json
4+
recursive-include tests *.py
5+
recursive-include tests_openfeature *.py

‎Makefile‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,18 @@ setup-golang-sdk:
2525
update-golang-sdk:
2626
(cd featurevisor-go && git pull origin main)
2727

28-
.PHONY: test test-openfeature
28+
.PHONY: test test-openfeature typecheck check
2929
test:
3030
PYTHONPATH=src python3 -m unittest discover -s tests -v
3131

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

35+
typecheck:
36+
python3 -m mypy
37+
38+
check: test test-openfeature typecheck
39+
3540
.PHONY: test-example-1
3641
test-example-1:
3742
PYTHONPATH=src python3 -m unittest discover -s tests -v

‎README.md‎

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ This SDK is compatible with Featurevisor v3 projects and v2 datafiles.
4545
- [Child instance](#child-instance)
4646
- [Close](#close)
4747
- [OpenFeature](#openfeature)
48+
- [Installation](#installation-1)
49+
- [Provider setup](#provider-setup)
50+
- [Flag key mapping](#flag-key-mapping)
51+
- [Context mapping](#context-mapping)
52+
- [Resolution details](#resolution-details)
53+
- [Tracking](#tracking)
54+
- [Using an existing Featurevisor instance](#using-an-existing-featurevisor-instance)
4855
- [CLI usage](#cli-usage)
4956
- [Test](#test)
5057
- [Benchmark](#benchmark)
@@ -583,19 +590,25 @@ python -m featurevisor assess-distribution \
583590

584591
## OpenFeature
585592

586-
Install Featurevisor with its optional OpenFeature dependency:
593+
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.
594+
595+
### Installation
587596

588597
```bash
589598
pip install "featurevisor[openfeature]"
590599
```
591600

601+
If the extra is not installed, importing `featurevisor.openfeature` reports the installation command needed to enable it.
602+
603+
### Provider setup
604+
592605
```python
593606
from featurevisor.openfeature import FeaturevisorOpenFeatureProvider
594607
from openfeature import api
595608
from openfeature.evaluation_context import EvaluationContext
596609

597610
provider = FeaturevisorOpenFeatureProvider({"datafile": datafile_content})
598-
api.set_provider(provider)
611+
api.set_provider_and_wait(provider)
599612

600613
client = api.get_client()
601614
enabled = client.get_boolean_value(
@@ -605,11 +618,77 @@ enabled = client.get_boolean_value(
605618
)
606619
```
607620

608-
Use `checkout` for a flag, `checkout:variation` for its variation, and `checkout:title` for its `title` variable. Boolean variables use the boolean resolver. Sequences, mappings, and JSON variables use the object resolver.
621+
Call `api.shutdown()` during application shutdown. This closes a Featurevisor instance created by the provider and releases provider subscriptions.
622+
623+
### Flag key mapping
624+
625+
| OpenFeature key | Featurevisor evaluation |
626+
| --- | --- |
627+
| `checkout` | Boolean flag for `checkout` |
628+
| `checkout:variation` | Variation value for `checkout` |
629+
| `checkout:title` | Variable `title` for `checkout` |
630+
631+
Boolean variables use the boolean resolver. Integer and double variables use their matching numeric resolvers. Arrays, objects, and JSON variables use the object resolver.
632+
633+
The first separator divides the feature key from the selector. Use `key_separator` and `variation_key` when project keys require a different grammar:
634+
635+
```python
636+
provider = FeaturevisorOpenFeatureProvider(
637+
{"datafile": datafile_content},
638+
key_separator="/",
639+
variation_key="$variation",
640+
)
641+
```
642+
643+
This makes `checkout/$variation` the variation key and `checkout/title` a variable key.
609644

610-
OpenFeature's targeting key maps to `userId` by default. `targeting_key_field`, `key_separator`, and `variation_key` can customize the mapping.
645+
### Context mapping
611646

612-
You can also reuse an existing Featurevisor instance:
647+
OpenFeature's targeting key maps to `userId` by default. Use `targeting_key_field` to map it to another Featurevisor context field:
648+
649+
```python
650+
provider = FeaturevisorOpenFeatureProvider(
651+
{"datafile": datafile_content},
652+
targeting_key_field="accountId",
653+
)
654+
```
655+
656+
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.
657+
658+
### Resolution details
659+
660+
The provider maps Featurevisor evaluation results to OpenFeature details:
661+
662+
| Featurevisor result | OpenFeature result |
663+
| --- | --- |
664+
| Required, forced, sticky, or rule match | `TARGETING_MATCH` |
665+
| Traffic allocation | `SPLIT` |
666+
| Disabled variation or variable | `DISABLED` |
667+
| No match or variable default | `DEFAULT` |
668+
| Missing feature, variable, or variations | `ERROR` with `FLAG_NOT_FOUND` |
669+
| Wrong resolver type | `ERROR` with `TYPE_MISMATCH` |
670+
| Invalid datafile | `ERROR` with `PARSE_ERROR` |
671+
| Evaluation failure | `ERROR` with `GENERAL` |
672+
673+
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.
674+
675+
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.
676+
677+
### Tracking
678+
679+
Tracking is a no-op unless `on_track` is configured:
680+
681+
```python
682+
def handle_track(name, context, details):
683+
print(name, context, details)
684+
685+
provider = FeaturevisorOpenFeatureProvider(
686+
{"datafile": datafile_content},
687+
on_track=handle_track,
688+
)
689+
```
690+
691+
### Using an existing Featurevisor instance
613692

614693
```python
615694
from featurevisor import create_featurevisor
@@ -635,14 +714,17 @@ This repository assumes:
635714
Run the local test suite:
636715

637716
```bash
638-
make test
717+
python -m pip install -e '.[dev]'
718+
make check
639719
```
640720

721+
`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`.
722+
641723
Run the example project integration directly:
642724

643725
```bash
644726
PYTHONPATH=src python3 -m featurevisor test \
645-
--projectDirectoryPath=/Users/fahad/Projects/featurevisor/featurevisor/examples/example-1 \
727+
--projectDirectoryPath=../featurevisor/examples/example-1 \
646728
--onlyFailures
647729
```
648730

0 commit comments

Comments
 (0)