Skip to content

Commit d67c24b

Browse files
feat(python)!: New python provider with inprocess support (#4861)
* feat(python): New python provider with inprocess support Signed-off-by: Thomas Poignant <thomas.poignant@gofeatureflag.org> * move ci to uv Signed-off-by: Thomas Poignant <thomas.poignant@gofeatureflag.org> * use read and write wasmtime functions Signed-off-by: Thomas Poignant <thomas.poignant@gofeatureflag.org> * fix(python-provider): run async flag resolution in thread via asyncio.to_thread Async resolve_*_details_async methods now use asyncio.to_thread() to run sync evaluation in a thread, avoiding event loop blocking. Update test docstring to match. * chore(python-provider): remove unused deps (pylru, websocket-client, rel) from pyproject * fix(python-provider): default unknown flags to trackable in inprocess evaluator When a flag is not found in _is_trackable, log warning and return True so unknown flags remain visible in exporters. Update uv.lock. * feat(python-provider): add WASM store pool for concurrent in-process evaluation - Add wasm_pool_size option (default 10) to control number of wasmtime Store instances; Store is not thread-safe, so a pool allows parallel evaluations. - Refactor EvaluateWasm to use a Queue of Store slots and _create_slot helper. - InProcessEvaluator passes pool_size into EvaluateWasm (default 4 when unset). - Add test_evaluate_concurrent_threads to assert concurrent evaluation works. * chore(python-provider): default wasm pool size to 10 * HEAD Signed-off-by: Thomas Poignant <thomas.poignant@gofeatureflag.org> * fix(python-provider): make event_publisher tests deterministic for CI Co-authored-by: Cursor <cursoragent@cursor.com> * docs: update AGENTS.md to reflect current codebase structure The file had several outdated references: phantom ffclient/ directory, non-existent modules/evaluation, missing providers (PHP, Ruby, Swift), missing exporters (Logs, OpenTelemetry), incorrect Exporter interface signature, and missing Makefile targets. Co-authored-by: Cursor <cursoragent@cursor.com> * test(python-provider): add in-process evaluation tests Port the Java InProcessEvaluation test class to Python, exercising the full provider stack (provider -> InProcessEvaluator -> WASM) with mock flag configurations for all flag types, disabled flags, error paths, and scheduled rollouts. Co-authored-by: Cursor <cursoragent@cursor.com> * move mock files Signed-off-by: Thomas Poignant <thomas.poignant@gofeatureflag.org> * manage all errors Signed-off-by: Thomas Poignant <thomas.poignant@gofeatureflag.org> * set as prerelease Signed-off-by: Thomas Poignant <thomas.poignant@gofeatureflag.org> * fix: remove @staticmethod from _raise_for_error_code to fix missing 'details' argument --------- Signed-off-by: Thomas Poignant <thomas.poignant@gofeatureflag.org> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 468a0c8 commit d67c24b

79 files changed

Lines changed: 6479 additions & 1890 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/release-please/release-please-config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@
8080
"extra-files": [
8181
"pyproject.toml",
8282
"README.md"
83-
]
83+
],
84+
"prerelease": true
8485
}
8586
}
8687
}

.github/workflows/ci.yml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ jobs:
191191
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
192192
with:
193193
fetch-depth: 100
194+
submodules: recursive
194195
- name: Get changed files in the docs folder
195196
id: changed-files-specific
196197
uses: marceloprado/has-changed-path@df1b7a3161b8fb9fd8c90403c66a9e66dfde50cb # v1.0.1
@@ -199,21 +200,21 @@ jobs:
199200
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
200201
with:
201202
python-version: 3.9
202-
- name: Setup Poetry
203+
- name: Install uv
203204
if: steps.changed-files-specific.outputs.changed == 'true'
204-
uses: abatilo/actions-poetry@3765cf608f2d4a72178a9fc5b918668e542b89b1 # v4.0.0
205-
- name: Poetry install
205+
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
206+
- name: uv sync
206207
if: steps.changed-files-specific.outputs.changed == 'true'
207208
working-directory: ./openfeature/providers/python-provider
208-
run: poetry install
209+
run: uv sync
209210
- name: lint black
210211
if: steps.changed-files-specific.outputs.changed == 'true'
211212
working-directory: ./openfeature/providers/python-provider
212-
run: poetry run black . --check
213+
run: uv run black . --check
213214
- name: Pytest
214215
if: steps.changed-files-specific.outputs.changed == 'true'
215216
working-directory: ./openfeature/providers/python-provider
216-
run: poetry run pytest
217+
run: uv run pytest
217218
GraddleWrapperValidation:
218219
runs-on: ubuntu-latest
219220
permissions:

.github/workflows/release-python-provider.yml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,13 @@ jobs:
1313
runs-on: ubuntu-latest
1414
steps:
1515
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
16-
- name: Build and publish to PyPi
17-
uses: JRubics/poetry-publish@4b3306307f536bbfcb559603629b3b4f6aef5ab8 # v2.1
16+
- name: Install uv
17+
uses: astral-sh/setup-uv@bd01f18f5d15746b30239de8373e6f36c5be2f19 # v6.3.0
1818
with:
19-
package_directory: ./openfeature/providers/python-provider
20-
pypi_token: ${{ secrets.pypi_token }}
19+
version: "latest"
20+
- name: Build package
21+
working-directory: ./openfeature/providers/python-provider
22+
run: uv build
23+
- name: Publish to PyPi
24+
working-directory: ./openfeature/providers/python-provider
25+
run: uv publish --token ${{ secrets.pypi_token }}

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "openfeature/providers/python-provider/wasm-releases"]
2+
path = openfeature/providers/python-provider/wasm-releases
3+
url = https://github.com/go-feature-flag/wasm-releases.git

AGENTS.md

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ make help # Show all available commands
2121

2222
**Key Features:**
2323
- Feature flag implementation with OpenFeature standard support
24-
- Multiple storage backends (S3, HTTP, Kubernetes, MongoDB, Redis, etc.)
24+
- Multiple storage backends (S3, HTTP, Kubernetes, MongoDB, Redis, GCS, Azure, PostgreSQL, etc.)
2525
- Complex rollout strategies (A/B testing, progressive, scheduled)
26-
- Data export and notification systems
26+
- Data export, notification, and tracking systems
2727

2828
## 🏗️ Architecture
2929

@@ -36,28 +36,37 @@ OpenFeature SDKs → Relay Proxy (cmd/relayproxy/) → GO Module (ffclient)
3636
**Data Flow:** Retrievers fetch configs → Cache stores flags → Change detection triggers Notifiers → Evaluation generates Events → Exporters send data
3737

3838
**Key Components:**
39-
- **`ffclient`**: Core Go module for direct integration
39+
- **`ffclient` (root package)**: Core Go module for direct integration (root `.go` files are `package ffclient`)
4040
- **`cmd/relayproxy/`**: HTTP API server (uses Echo + Zap logging)
41-
- **`retriever/`**: Flag configuration sources (file, HTTP, S3, K8s, MongoDB, Redis, GitHub, GitLab, Bitbucket, PostgreSQL, Azure)
42-
- **`exporter/`**: Data export destinations (S3, File, Kafka, Kinesis, Webhook, GCS, Pub/Sub, SQS, Azure)
43-
- **`notifier/`**: Change notifications (Slack, Webhook, Discord, Teams, Logs)
44-
- **`modules/core`**: Core logic modules used by OpenFeature providers and WASM
41+
- **`retriever/`**: Flag configuration sources (File, HTTP, S3, K8s, MongoDB, Redis, GitHub, GitLab, Bitbucket, PostgreSQL, Azure Blob Storage, GCS)
42+
- **`exporter/`**: Data export destinations (S3, File, Kafka, Kinesis, Webhook, GCS, Pub/Sub, SQS, Azure, Logs, OpenTelemetry)
43+
- **`notifier/`**: Change notifications (Slack, Webhook, Discord, Microsoft Teams, Logs)
44+
- **`modules/core`**: Core logic module used by OpenFeature providers and WASM
4545

4646
## 📁 Directory Structure
4747

4848
**Root Level:**
49-
- `ffclient/`: Core client package
50-
- `cmd/`: Applications (relayproxy, cli, lint, editor, wasm)
49+
- Root `.go` files: Core `ffclient` package (`variation.go`, `feature_flag.go`, `config.go`, `tracking.go`, `variation_all_flags.go`, `config_exporter.go`)
50+
- `cmd/`: Applications (relayproxy, cli, lint, editor, jsonschema-generator, wasm)
5151
- `retriever/`, `exporter/`, `notifier/`: Integration packages
52-
- `modules/`: Separate Go modules (core, evaluation)
52+
- `modules/`: Separate Go modules (`core` only)
5353
- `internal/`: Internal packages (cache, flagstate, notification, signer)
54-
- `openfeature/providers/`: Some providers (Kotlin, Python) - most are in OpenFeature contrib repos
54+
- `ffcontext/`: Evaluation context package
55+
- `ffuser/`: User context (legacy)
56+
- `model/`: DTO models
57+
- `cmdhelpers/`: Shared CLI helpers (errors, retriever config)
58+
- `utils/`: Utilities (fflog, string helpers, constants)
59+
- `openfeature/providers/`: In-repo providers (Kotlin, Python, PHP, Ruby, Swift)
60+
- `openfeature/provider_tests/`: Integration tests for providers (Go, JS, Java, .NET)
5561
- `testutils/`, `testdata/`, `examples/`, `website/`
5662

5763
**Key Files:**
5864
- `variation.go`: Flag evaluation methods
59-
- `feature_flag.go`: Core logic
65+
- `variation_all_flags.go`: Bulk evaluation of all flags
66+
- `feature_flag.go`: Core logic and initialization
6067
- `config.go`: Configuration structure
68+
- `config_exporter.go`: Exporter configuration
69+
- `tracking.go`: Tracking/experimentation support
6170
- `Makefile`: Primary interface (use `make help`)
6271

6372
## 🔑 Key Concepts
@@ -72,7 +81,7 @@ OpenFeature SDKs → Relay Proxy (cmd/relayproxy/) → GO Module (ffclient)
7281

7382
**Interfaces:**
7483
- `Retriever`: `Retrieve(ctx context.Context) ([]byte, error)`
75-
- `Exporter`: `Export(ctx context.Context, events []FeatureEvent) error`, `IsBulk() bool`
84+
- `Exporter`: `Export(context.Context, *fflog.FFLogger, []ExportableEvent) error` + `IsBulk() bool`
7685
- `Notifier`: `Notify(cache DiffCache) error`
7786

7887
## 🛠️ Common Tasks
@@ -86,11 +95,11 @@ OpenFeature SDKs → Relay Proxy (cmd/relayproxy/) → GO Module (ffclient)
8695
6. Update docs
8796

8897
**OpenFeature Providers:**
89-
- Most providers in OpenFeature contrib repos (Go, JS, Java, .NET, Ruby, Swift, PHP)
90-
- Some in this repo: Kotlin (`kotlin-provider/`), Python (`python-provider/`)
91-
- Providers use `modules/core` for evaluation logic
98+
- Most providers in OpenFeature contrib repos (Go, JS, Java, .NET)
99+
- In this repo: Kotlin (`kotlin-provider/`), Python (`python-provider/`), PHP (`php-provider/`), Ruby (`ruby-provider/`), Swift (`swift-provider/`)
100+
- Providers use `modules/core` for evaluation logic
92101

93-
**Modules (`modules/core`:**
102+
**Modules (`modules/core`):**
94103
- Core logic separated for reuse by OpenFeature providers and WASM module
95104
- `modules/core`: Flag structures, context, models, utilities
96105
- Allows independent versioning and smaller dependency trees
@@ -117,7 +126,7 @@ func TestFunction(t *testing.T) {
117126
}
118127
```
119128

120-
**Commands:** `make test`, `make coverage`, `make bench`
129+
**Commands:** `make test`, `make coverage`, `make bench`, `make provider-tests`
121130
**Coverage:** Aim for 90%+, use `testify/assert`, mock external deps
122131

123132
## 📝 Code Patterns
@@ -155,11 +164,11 @@ pre-commit install # Install pre-commit hooks
155164
```
156165

157166
**Common Commands:**
158-
- **Build:** `make build`, `make build-relayproxy`, `make build-cli`, `make build-wasm`
159-
- **Dev:** `make watch-relayproxy`, `make watch-doc`
160-
- **Test:** `make test`, `make coverage`, `make bench`
167+
- **Build:** `make build`, `make build-relayproxy`, `make build-cli`, `make build-wasm`, `make build-wasi`, `make build-editor-api`, `make build-jsonschema-generator`, `make build-modules`
168+
- **Dev:** `make watch-relayproxy`, `make watch-doc`, `make serve-doc`
169+
- **Test:** `make test`, `make coverage`, `make bench`, `make provider-tests`
161170
- **Quality:** `make lint`, `make tidy`, `make vendor`
162-
- **Utils:** `make clean`, `make swagger`, `make generate-helm-docs`
171+
- **Utils:** `make clean`, `make swagger`, `make generate-helm-docs`, `make bump-helm-chart-version`, `make bump-wasm-contrib`
163172

164173
**Code Quality:**
165174
- Use `make lint` (golangci-lint)
@@ -168,9 +177,9 @@ pre-commit install # Install pre-commit hooks
168177

169178
## 🔍 Code Navigation
170179

171-
**Flag Evaluation:** `variation.go``feature_flag.go``internal/cache/``internal/flagstate/`
172-
**API Endpoints:** `cmd/relayproxy/api/routes_*.go``controller/``model/`
173-
**Configuration:** `config.go` (ffclient), `cmd/relayproxy/config/config.go` (relay proxy)
180+
**Flag Evaluation:** `variation.go``feature_flag.go``internal/cache/``internal/flagstate/`
181+
**API Endpoints:** `cmd/relayproxy/api/routes_*.go``controller/``model/`
182+
**Configuration:** `config.go` (ffclient), `cmd/relayproxy/config/config.go` (relay proxy)
174183
**Flag Format:** `.schema/flag-schema.json`, `testdata/flag-config.*`, https://gofeatureflag.org/docs
175184

176185
## 🔗 Important Files
@@ -185,11 +194,10 @@ pre-commit install # Install pre-commit hooks
185194

186195
**Key Libraries:** Echo (HTTP), Koanf (config), OpenTelemetry (observability), Prometheus (metrics), Testcontainers (integration tests)
187196

188-
**Monorepo Modules:**
189-
- Main module (root): Core library
197+
**Monorepo Modules (Go workspace):**
198+
- Main module (root): Core `ffclient` library
190199
- `modules/core`: Core data structures (used by providers/WASM/relayproxy)
191-
- `cmd/wasm`: WebAssembly evaluation
192-
- `openfeature/providers/`: Some providers (most in contrib repos)
200+
- `cmd/wasm`: WebAssembly/WASI evaluation
193201

194202
## 📚 Resources
195203

@@ -200,8 +208,8 @@ pre-commit install # Install pre-commit hooks
200208

201209
## 🎯 Quick Reference
202210

203-
**Entry Points:** `ffclient.Init()`, `cmd/relayproxy/main.go`, `cmd/cli/main.go`
204-
**Interfaces:** `Retriever`, `Exporter`, `Notifier`, `Cache`
211+
**Entry Points:** `ffclient.Init()`, `cmd/relayproxy/main.go`, `cmd/cli/main.go`
212+
**Interfaces:** `Retriever`, `Exporter`, `Notifier`, `Cache`
205213
**Config:** YAML/JSON/TOML flags, `ffclient.Config`, `cmd/relayproxy/config/config.go`
206214

207215
## ⚠️ What Agents Must NEVER Do

openfeature/providers/python-provider/.gitignore

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,10 @@ ipython_config.py
9494
# install all needed dependencies.
9595
#Pipfile.lock
9696

97-
# poetry
98-
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99-
# This is especially recommended for binary packages to ensure reproducibility, and is more
100-
# commonly ignored for libraries.
101-
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102-
#poetry.lock
97+
# uv
98+
# uv.lock is recommended to be included in version control for reproducibility.
99+
# https://docs.astral.sh/uv/concepts/projects/layout/#the-lockfile
100+
#uv.lock
103101

104102
# pdm
105103
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
@@ -158,3 +156,6 @@ cython_debug/
158156
# and can be added to the global gitignore or merged into this file. For a more nuclear
159157
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
160158
.idea/
159+
160+
./gofeatureflag_python_provider/wasm/_wasi_version.txt
161+
./gofeatureflag_python_provider/wasm/gofeatureflag-evaluation_*.wasi
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# GO Feature Flag Python Provider
2+
3+
OpenFeature Python provider for [GO Feature Flag](https://gofeatureflag.org).
4+
5+
## Project Overview
6+
7+
This is a Python package that implements the OpenFeature provider interface to connect to a GO Feature Flag relay proxy. It enables Python applications to evaluate feature flags using the OpenFeature SDK.
8+
9+
## Architecture
10+
11+
```
12+
gofeatureflag_python_provider/
13+
├── __init__.py # Package exports
14+
├── provider.py # Main GoFeatureFlagProvider class (AbstractProvider implementation)
15+
├── options.py # GoFeatureFlagOptions configuration class
16+
├── hooks/ # OpenFeature hooks
17+
│ ├── __init__.py
18+
│ ├── data_collector.py # Hook for collecting flag evaluation usage data
19+
│ └── enrich_evaluation_context.py # Hook that adds gofeatureflag metadata to context before evaluation
20+
├── metadata.py # Provider metadata
21+
├── request_data_collector.py # Data models for usage collection
22+
├── request_flag_evaluation.py # Request models for flag evaluation API calls
23+
└── response_flag_evaluation.py # Response models for flag evaluation API calls
24+
25+
tests/
26+
├── test_gofeatureflag_python_provider.py # Main provider tests
27+
├── test_enrich_evaluation_context_hook.py # EnrichEvaluationContextHook tests
28+
├── test_provider_graceful_exit.py # Shutdown/cleanup tests
29+
├── test_websocket_cache_invalidation.py # WebSocket cache invalidation tests
30+
├── mock_responses/ # JSON mock responses for testing
31+
├── config.goff.yaml # Test flag configuration
32+
└── docker-compose.yml # Test infrastructure
33+
```
34+
35+
## Key Components
36+
37+
### GoFeatureFlagProvider (`provider.py`)
38+
- Extends `AbstractProvider` from OpenFeature SDK
39+
- Implements all resolve methods: `resolve_boolean_details`, `resolve_string_details`, `resolve_integer_details`, `resolve_float_details`, `resolve_object_details`
40+
- Uses `generic_go_feature_flag_resolver` for all flag types
41+
- Features:
42+
- LRU cache for flag evaluations (`pylru`)
43+
- WebSocket connection for cache invalidation
44+
- Data collection for usage analytics
45+
46+
### GoFeatureFlagOptions (`options.py`)
47+
Configuration options:
48+
- `endpoint` (required): URL of the GO Feature Flag relay proxy
49+
- `cache_size`: Max cached flags (default: 10000)
50+
- `data_flush_interval`: Interval to flush usage data in ms (default: 60000)
51+
- `disable_data_collection`: Turn off usage tracking (default: false)
52+
- `reconnect_interval`: WebSocket reconnect interval in seconds (default: 60)
53+
- `disable_cache_invalidation`: Disable WebSocket cache invalidation (default: false)
54+
- `api_key`: API key for authenticated requests
55+
- `exporter_metadata`: Custom metadata for evaluation events
56+
- `debug`: Enable debug logging (default: false)
57+
- `urllib3_pool_manager`: Custom HTTP client
58+
59+
### DataCollectorHook (`hooks/data_collector.py`)
60+
- OpenFeature Hook implementation for collecting usage data
61+
- Tracks flag evaluations via `after()` and `error()` hooks
62+
- Flushes data to `/v1/data/collector` endpoint periodically
63+
64+
### EnrichEvaluationContextHook (`hooks/enrich_evaluation_context.py`)
65+
- Enriches the evaluation context with a `gofeatureflag` attribute (from `exporter_metadata`) before flag resolution
66+
- Used by the relay proxy for analytics or filtering; registered automatically by the provider
67+
68+
## Development
69+
70+
### Prerequisites
71+
- Python 3.9+
72+
- uv (package manager)
73+
74+
### Setup
75+
```bash
76+
# Install dependencies
77+
uv sync
78+
79+
# Run a command in the virtual environment
80+
uv run <command>
81+
```
82+
83+
### Running Tests
84+
```bash
85+
# Run all tests
86+
uv run pytest
87+
88+
# Run specific test file
89+
uv run pytest tests/test_gofeatureflag_python_provider.py
90+
91+
# Run with verbose output
92+
uv run pytest -v
93+
```
94+
95+
### Code Style
96+
```bash
97+
# Format code with black
98+
uv run black gofeatureflag_python_provider tests
99+
```
100+
101+
## Key Patterns
102+
103+
### Pydantic Models
104+
- All data classes extend Pydantic `BaseModel` for validation
105+
- Request/response models use `model_dump_json()` for serialization
106+
- Use `model_validate_json()` for deserialization
107+
108+
### HTTP Communication
109+
- Uses `urllib3.PoolManager` for HTTP requests
110+
- POST to `/v1/feature/{flag_key}/eval` for flag evaluation
111+
- POST to `/v1/data/collector` for usage data
112+
- WebSocket at `/ws/v1/flag/change` for cache invalidation
113+
114+
### Caching Strategy
115+
- LRU cache keyed by `{flag_key}:{evaluation_context_hash()}`
116+
- Cache cleared on WebSocket message (flag config changed)
117+
- Set `cacheable` field in response determines if result is cached
118+
119+
### Error Handling
120+
- `FlagNotFoundError`: Flag doesn't exist (404)
121+
- `InvalidContextError`: Invalid evaluation context (400)
122+
- `TypeMismatchError`: Response type doesn't match expected type
123+
- `GeneralError`: Other errors (500+)
124+
125+
## API Reference
126+
127+
The provider communicates with the GO Feature Flag relay proxy:
128+
129+
| Endpoint | Method | Purpose |
130+
|----------|--------|---------|
131+
| `/v1/feature/{flag}/eval` | POST | Evaluate a flag |
132+
| `/v1/data/collector` | POST | Send usage data |
133+
| `/ws/v1/flag/change` | WebSocket | Cache invalidation notifications |
134+
135+
## Dependencies
136+
137+
Core:
138+
- `openfeature-sdk`: OpenFeature Python SDK
139+
- `pydantic`: Data validation
140+
- `urllib3`: HTTP client
141+
- `pylru`: LRU cache implementation
142+
- `websocket-client`: WebSocket support
143+
- `rel`: WebSocket reconnection handling
144+
145+
Dev:
146+
- `pytest`: Testing framework
147+
- `black`: Code formatter
148+
- `pytest-docker`: Docker-based integration tests
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@AGENTS.md

openfeature/providers/python-provider/clean.py

Whitespace-only changes.

0 commit comments

Comments
 (0)