traffical 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- traffical-0.3.0/.github/workflows/ci.yml +95 -0
- traffical-0.3.0/.github/workflows/publish.yml +25 -0
- traffical-0.3.0/.github/workflows/release-please.yml +39 -0
- traffical-0.3.0/.gitignore +13 -0
- traffical-0.3.0/.release-please-manifest.json +3 -0
- traffical-0.3.0/AGENTS.md +29 -0
- traffical-0.3.0/CHANGELOG.md +27 -0
- traffical-0.3.0/CONTRIBUTING.md +120 -0
- traffical-0.3.0/LICENSE +21 -0
- traffical-0.3.0/PKG-INFO +283 -0
- traffical-0.3.0/README.md +260 -0
- traffical-0.3.0/SECURITY.md +35 -0
- traffical-0.3.0/examples/fastapi_app.py +78 -0
- traffical-0.3.0/examples/quickstart_async.py +57 -0
- traffical-0.3.0/examples/quickstart_sync.py +55 -0
- traffical-0.3.0/pyproject.toml +62 -0
- traffical-0.3.0/release-please-config.json +13 -0
- traffical-0.3.0/src/traffical/__init__.py +45 -0
- traffical-0.3.0/src/traffical/_fork.py +41 -0
- traffical-0.3.0/src/traffical/async_client.py +335 -0
- traffical-0.3.0/src/traffical/client.py +637 -0
- traffical-0.3.0/src/traffical/config/__init__.py +38 -0
- traffical-0.3.0/src/traffical/config/poller.py +289 -0
- traffical-0.3.0/src/traffical/config/readiness.py +128 -0
- traffical-0.3.0/src/traffical/config/source.py +199 -0
- traffical-0.3.0/src/traffical/engine/__init__.py +57 -0
- traffical-0.3.0/src/traffical/engine/conditions.py +159 -0
- traffical-0.3.0/src/traffical/engine/contextual.py +141 -0
- traffical-0.3.0/src/traffical/engine/hashing.py +55 -0
- traffical-0.3.0/src/traffical/engine/ids.py +51 -0
- traffical-0.3.0/src/traffical/engine/resolution.py +404 -0
- traffical-0.3.0/src/traffical/engine/strings.py +90 -0
- traffical-0.3.0/src/traffical/engine/types.py +489 -0
- traffical-0.3.0/src/traffical/engine/weighted.py +40 -0
- traffical-0.3.0/src/traffical/events/__init__.py +45 -0
- traffical-0.3.0/src/traffical/events/assignments.py +177 -0
- traffical-0.3.0/src/traffical/events/dedup.py +98 -0
- traffical-0.3.0/src/traffical/events/flusher.py +378 -0
- traffical-0.3.0/src/traffical/events/logger.py +204 -0
- traffical-0.3.0/src/traffical/events/queue.py +65 -0
- traffical-0.3.0/src/traffical/options.py +88 -0
- traffical-0.3.0/src/traffical/py.typed +0 -0
- traffical-0.3.0/tests/client/.gitkeep +0 -0
- traffical-0.3.0/tests/client/test_client_async.py +158 -0
- traffical-0.3.0/tests/client/test_client_sync.py +521 -0
- traffical-0.3.0/tests/client/test_config_poller.py +368 -0
- traffical-0.3.0/tests/client/test_config_source.py +210 -0
- traffical-0.3.0/tests/client/test_config_store.py +95 -0
- traffical-0.3.0/tests/client/test_events_async.py +240 -0
- traffical-0.3.0/tests/client/test_events_dedup.py +116 -0
- traffical-0.3.0/tests/client/test_events_flusher.py +234 -0
- traffical-0.3.0/tests/client/test_events_pipeline.py +138 -0
- traffical-0.3.0/tests/client/test_events_queue.py +61 -0
- traffical-0.3.0/tests/conformance/.gitkeep +0 -0
- traffical-0.3.0/tests/conformance/test_events_conformance.py +213 -0
- traffical-0.3.0/tests/conformance/test_spec_vectors.py +333 -0
- traffical-0.3.0/tests/conftest.py +37 -0
- traffical-0.3.0/tests/engine/.gitkeep +0 -0
- traffical-0.3.0/tests/engine/test_conditions.py +312 -0
- traffical-0.3.0/tests/engine/test_contextual.py +377 -0
- traffical-0.3.0/tests/engine/test_hashing.py +133 -0
- traffical-0.3.0/tests/engine/test_resolution.py +869 -0
- traffical-0.3.0/tests/engine/test_strings.py +48 -0
- traffical-0.3.0/tests/engine/test_weighted.py +100 -0
- traffical-0.3.0/tests/test_exports.py +40 -0
- traffical-0.3.0/uv.lock +798 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
env:
|
|
9
|
+
# The published npm pin the SDK conforms to. The stale-pin gate below fails
|
|
10
|
+
# CI when a newer sdk-spec tag exists, so this must be bumped in lockstep.
|
|
11
|
+
SDK_SPEC_VERSION: "0.7.0"
|
|
12
|
+
# CI checks out this published sdk-spec tag for the fixtures + schemas.
|
|
13
|
+
SDK_SPEC_REF: "v0.7.0"
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
test:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
strategy:
|
|
19
|
+
fail-fast: false
|
|
20
|
+
matrix:
|
|
21
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v6
|
|
24
|
+
|
|
25
|
+
- name: Check out sdk-spec (fixtures + schemas)
|
|
26
|
+
uses: actions/checkout@v6
|
|
27
|
+
with:
|
|
28
|
+
repository: traffical/sdk-spec
|
|
29
|
+
ref: ${{ env.SDK_SPEC_REF }}
|
|
30
|
+
path: .sdk-spec
|
|
31
|
+
|
|
32
|
+
- name: Point tests at the sdk-spec checkout
|
|
33
|
+
run: echo "TRAFFICAL_SDK_SPEC_PATH=${GITHUB_WORKSPACE}/.sdk-spec" >> "$GITHUB_ENV"
|
|
34
|
+
|
|
35
|
+
- uses: astral-sh/setup-uv@v8.2.0
|
|
36
|
+
with:
|
|
37
|
+
python-version: ${{ matrix.python-version }}
|
|
38
|
+
|
|
39
|
+
- name: Install dependencies
|
|
40
|
+
# --frozen (not --locked): install exactly the locked dependency
|
|
41
|
+
# versions without asserting uv.lock matches pyproject. release-please
|
|
42
|
+
# bumps the project version in pyproject.toml but not in uv.lock, so
|
|
43
|
+
# --locked would fail on every release PR over the project's own version.
|
|
44
|
+
run: uv sync --frozen
|
|
45
|
+
|
|
46
|
+
- name: Tests (unit + sdk-spec conformance)
|
|
47
|
+
run: uv run pytest
|
|
48
|
+
|
|
49
|
+
- name: Type check (mypy --strict)
|
|
50
|
+
run: uv run mypy src/ tests/
|
|
51
|
+
|
|
52
|
+
- name: Lint
|
|
53
|
+
run: uv run ruff check .
|
|
54
|
+
|
|
55
|
+
- name: Format check
|
|
56
|
+
run: uv run ruff format --check .
|
|
57
|
+
|
|
58
|
+
spec-pin-check:
|
|
59
|
+
# Stale-pin gate: fail when the sdk-spec pin is behind the latest published
|
|
60
|
+
# tag, so the conformance suite cannot silently lag the spec.
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
steps:
|
|
63
|
+
- name: Fail if SDK_SPEC_VERSION is behind the latest sdk-spec tag
|
|
64
|
+
run: |
|
|
65
|
+
set -euo pipefail
|
|
66
|
+
pinned="${SDK_SPEC_VERSION#v}"
|
|
67
|
+
latest="$(git ls-remote --tags --refs https://github.com/traffical/sdk-spec \
|
|
68
|
+
| sed -E 's#.*/tags/v?##' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n1 || true)"
|
|
69
|
+
if [ -z "$latest" ]; then
|
|
70
|
+
echo "No published sdk-spec tags found (pre-release); skipping stale-pin gate."
|
|
71
|
+
exit 0
|
|
72
|
+
fi
|
|
73
|
+
newest="$(printf '%s\n%s\n' "$pinned" "$latest" | sort -V | tail -n1)"
|
|
74
|
+
echo "pinned=$pinned latest=$latest"
|
|
75
|
+
if [ "$latest" != "$pinned" ] && [ "$newest" = "$latest" ]; then
|
|
76
|
+
echo "::error::sdk-spec pin $pinned is behind the latest published tag $latest. Bump SDK_SPEC_VERSION."
|
|
77
|
+
exit 1
|
|
78
|
+
fi
|
|
79
|
+
echo "sdk-spec pin is current."
|
|
80
|
+
|
|
81
|
+
build:
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
steps:
|
|
84
|
+
- uses: actions/checkout@v6
|
|
85
|
+
- uses: astral-sh/setup-uv@v8.2.0
|
|
86
|
+
- name: Build wheel and sdist
|
|
87
|
+
run: uv build
|
|
88
|
+
- name: Check wheel contents
|
|
89
|
+
run: |
|
|
90
|
+
uv run --no-project --with check-wheel-contents check-wheel-contents dist/*.whl
|
|
91
|
+
unzip -l dist/*.whl | grep -q "traffical/py.typed"
|
|
92
|
+
- uses: actions/upload-artifact@v7
|
|
93
|
+
with:
|
|
94
|
+
name: dist
|
|
95
|
+
path: dist/
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Token-based publish. Runs on a GitHub Release (for user-created releases) and
|
|
4
|
+
# via manual dispatch (e.g. to re-publish a release whose auto-publish did not
|
|
5
|
+
# fire). Releases cut by release-please publish inline in release-please.yml —
|
|
6
|
+
# see the note there — because a release created with GITHUB_TOKEN cannot trigger
|
|
7
|
+
# this `release`-triggered workflow.
|
|
8
|
+
on:
|
|
9
|
+
release:
|
|
10
|
+
types: [published]
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
publish:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v6
|
|
18
|
+
- uses: astral-sh/setup-uv@v8.2.0
|
|
19
|
+
- name: Build wheel and sdist
|
|
20
|
+
run: uv build
|
|
21
|
+
- name: Publish
|
|
22
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
23
|
+
with:
|
|
24
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
25
|
+
skip-existing: true
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: Release Please
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: write
|
|
9
|
+
pull-requests: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
release-please:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: googleapis/release-please-action@v5
|
|
16
|
+
id: release
|
|
17
|
+
with:
|
|
18
|
+
# Optional RELEASE_PLEASE_TOKEN (a PAT) makes CI run on the release PR;
|
|
19
|
+
# the default GITHUB_TOKEN cannot trigger downstream workflows.
|
|
20
|
+
token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}
|
|
21
|
+
|
|
22
|
+
# Publish to PyPI in THIS run when release-please cuts a release. Doing it
|
|
23
|
+
# inline (rather than a separate `release`-triggered workflow) avoids
|
|
24
|
+
# GitHub's anti-recursion rule: a release created with GITHUB_TOKEN cannot
|
|
25
|
+
# trigger a downstream `release:` workflow, so publish.yml would never fire
|
|
26
|
+
# for an automated release.
|
|
27
|
+
- uses: actions/checkout@v6
|
|
28
|
+
if: ${{ steps.release.outputs.release_created }}
|
|
29
|
+
- uses: astral-sh/setup-uv@v8.2.0
|
|
30
|
+
if: ${{ steps.release.outputs.release_created }}
|
|
31
|
+
- name: Build wheel and sdist
|
|
32
|
+
if: ${{ steps.release.outputs.release_created }}
|
|
33
|
+
run: uv build
|
|
34
|
+
- name: Publish to PyPI
|
|
35
|
+
if: ${{ steps.release.outputs.release_created }}
|
|
36
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
37
|
+
with:
|
|
38
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
39
|
+
skip-existing: true
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Agent Guidelines for the Traffical Python SDK
|
|
2
|
+
|
|
3
|
+
## Important Files
|
|
4
|
+
|
|
5
|
+
- **`CONTRIBUTING.md`** — full development workflow (uv, pytest, mypy, ruff, conformance fixtures).
|
|
6
|
+
Always read and follow it when making changes.
|
|
7
|
+
|
|
8
|
+
## Hard Rules
|
|
9
|
+
|
|
10
|
+
- `src/traffical/engine/` must stay pure: stdlib only, no I/O, no clocks, no globals, frozen dataclasses.
|
|
11
|
+
- Public client methods are fail-open — they return the caller's defaults instead of raising.
|
|
12
|
+
- The cross-language contract in `../sdk-spec` is authoritative. If a conformance vector fails, fix the
|
|
13
|
+
code, never the fixture.
|
|
14
|
+
- Never manually edit the version (`pyproject.toml` + `SDK_VERSION` in `src/traffical/options.py`)
|
|
15
|
+
outside a dedicated release commit.
|
|
16
|
+
|
|
17
|
+
## Workflow
|
|
18
|
+
|
|
19
|
+
Run from the package root:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv sync
|
|
23
|
+
uv run pytest
|
|
24
|
+
uv run mypy src/traffical # --strict, configured in pyproject
|
|
25
|
+
uv run ruff check
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
All three must be green before committing. Conformance fixtures resolve via `TRAFFICAL_SDK_SPEC_PATH`,
|
|
29
|
+
defaulting to the sibling `../sdk-spec` (see `tests/conftest.py`).
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.3.0](https://github.com/traffical/python-sdk/compare/v0.2.0...v0.3.0) (2026-07-15)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* adopt 0.7.0 public API contract (A1) + exposure dedup (S4) + track options
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* adopt 0.7.0 public API contract (A1) + exposure dedup (S4) + track options ([eccbf9d](https://github.com/traffical/python-sdk/commit/eccbf9d929a4a8392a203c6aae31e2772391271b))
|
|
13
|
+
* **config:** honor suggestedRefreshMs from the config response ([1862de9](https://github.com/traffical/python-sdk/commit/1862de95bc613834ff326003ec0ad2d8cc9ba032))
|
|
14
|
+
* emit propensity, modelVersion, configVersion per sdk-spec 0.6.0 ([d7eb8de](https://github.com/traffical/python-sdk/commit/d7eb8de5683b7b6b37dfe0c2cd9f07d614b3df95))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* **ci:** use uv sync --frozen; release pre-1.0 (bump-minor-pre-major) ([#3](https://github.com/traffical/python-sdk/issues/3)) ([fc99d8a](https://github.com/traffical/python-sdk/commit/fc99d8aed53496b73e39a21e817d65b79cdd43cb))
|
|
20
|
+
* **config:** discard malformed bundles at ingest (S8) ([255858c](https://github.com/traffical/python-sdk/commit/255858c1f93b4601c8160ab57ecdf1fb75fe898d))
|
|
21
|
+
* **engine:** align condition/stringify/exposure semantics with spec 0.7.0 ([2c00c7b](https://github.com/traffical/python-sdk/commit/2c00c7bc4b625f60b7bef839e1e652bfdd529ba7))
|
|
22
|
+
* **events:** exposure event carries full assignment map (S4) ([2b01705](https://github.com/traffical/python-sdk/commit/2b01705354230d8c71bf491d5c0bbe91b3ce3229))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Documentation
|
|
26
|
+
|
|
27
|
+
* **ci:** pin sdk-spec 0.7.0, add stale-pin gate, SECURITY.md, README refresh ([ff01a73](https://github.com/traffical/python-sdk/commit/ff01a735d3ae436368d7da24ee97df0cf9fbe342))
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Contributing to the Traffical Python SDK
|
|
2
|
+
|
|
3
|
+
Thank you for your interest in contributing!
|
|
4
|
+
|
|
5
|
+
## Getting started
|
|
6
|
+
|
|
7
|
+
### Prerequisites
|
|
8
|
+
|
|
9
|
+
- Python 3.10+
|
|
10
|
+
- [uv](https://docs.astral.sh/uv/) (manages the venv and dev dependencies via `pyproject.toml`)
|
|
11
|
+
|
|
12
|
+
### Setup
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
cd python-sdk
|
|
16
|
+
uv sync
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Everyday commands
|
|
20
|
+
|
|
21
|
+
Run everything from the `python-sdk` package root:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv run pytest # full suite (unit + conformance)
|
|
25
|
+
uv run pytest tests/engine # just the pure-engine tests
|
|
26
|
+
uv run pytest tests/conformance
|
|
27
|
+
uv run mypy src/traffical # must pass --strict (configured in pyproject)
|
|
28
|
+
uv run ruff check # lint
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
All three (pytest, mypy, ruff) must be green before a PR is mergeable.
|
|
32
|
+
|
|
33
|
+
## Conformance fixtures (sdk-spec)
|
|
34
|
+
|
|
35
|
+
The cross-language contract lives in the [sdk-spec](https://github.com/traffical/sdk-spec) repository:
|
|
36
|
+
JSON schemas plus deterministic test vectors (`test-vectors/fixtures/bundle_*.json` +
|
|
37
|
+
`expected_*.json`). `tests/conftest.py` resolves the spec checkout in this order:
|
|
38
|
+
|
|
39
|
+
1. `TRAFFICAL_SDK_SPEC_PATH` environment variable, if set:
|
|
40
|
+
```bash
|
|
41
|
+
TRAFFICAL_SDK_SPEC_PATH=/path/to/sdk-spec uv run pytest tests/conformance
|
|
42
|
+
```
|
|
43
|
+
2. Otherwise the sibling directory `../sdk-spec` (the layout of the traffical-sdk workspace).
|
|
44
|
+
|
|
45
|
+
A pinned `tests/sdk-spec` git submodule (like the PHP SDK's) is planned for the standalone repository;
|
|
46
|
+
the `conftest.py` resolution order will stay the same, with the submodule becoming the default.
|
|
47
|
+
|
|
48
|
+
If a conformance test fails, treat the spec as the source of truth: fix the engine, not the fixture.
|
|
49
|
+
Spec changes go through the sdk-spec repository first.
|
|
50
|
+
|
|
51
|
+
## Project layout
|
|
52
|
+
|
|
53
|
+
- `src/traffical/engine/` — pure resolution engine: stdlib only, no I/O, no clocks, no globals;
|
|
54
|
+
frozen dataclasses for models. Keep it that way.
|
|
55
|
+
- `src/traffical/config/` — bundle sources, store/readiness, background pollers.
|
|
56
|
+
- `src/traffical/events/` — event queue, flushers, dedup, BYO assignment logger.
|
|
57
|
+
- `src/traffical/client.py` / `async_client.py` — the public sync/async clients.
|
|
58
|
+
- `tests/` — `engine/`, `client/`, `conformance/` plus `conftest.py` (fixture loading).
|
|
59
|
+
- `examples/` — runnable offline examples; keep them working when the public API changes.
|
|
60
|
+
|
|
61
|
+
## Code style
|
|
62
|
+
|
|
63
|
+
- Full type annotations; `mypy --strict` clean.
|
|
64
|
+
- PEP 8 / snake_case public API; the surface mirrors the JS and PHP SDKs
|
|
65
|
+
(`get_params`, `decide`, `track_exposure`, `track`, `flush_events`, `close`, `wait_for_ready`).
|
|
66
|
+
- No comments that narrate the obvious.
|
|
67
|
+
- Fail-open: public client methods must never raise into application code; defaults win on errors.
|
|
68
|
+
|
|
69
|
+
## Pull request process
|
|
70
|
+
|
|
71
|
+
1. Ensure `uv run pytest`, `uv run mypy src/traffical`, and `uv run ruff check` pass.
|
|
72
|
+
2. Update README/examples if the public surface changed.
|
|
73
|
+
3. New behavior needs tests; cross-SDK behavior needs a pointer to the matching JS/PHP reference or
|
|
74
|
+
sdk-spec section in the PR description.
|
|
75
|
+
|
|
76
|
+
## Releases
|
|
77
|
+
|
|
78
|
+
Versioning and publishing are automated with [release-please](https://github.com/googleapis/release-please)
|
|
79
|
+
(the Python-world equivalent of the changesets flow used by `js-sdk`) and PyPI
|
|
80
|
+
[trusted publishing](https://docs.pypi.org/trusted-publishers/). Never bump versions by hand.
|
|
81
|
+
|
|
82
|
+
### How it works
|
|
83
|
+
|
|
84
|
+
1. **Commit messages drive version bumps** ([Conventional Commits](https://www.conventionalcommits.org/)):
|
|
85
|
+
- `fix: ...` → patch (0.1.0 → 0.1.1)
|
|
86
|
+
- `feat: ...` → minor (0.1.0 → 0.2.0)
|
|
87
|
+
- `feat!: ...` or a `BREAKING CHANGE:` footer → major (pre-1.0: minor)
|
|
88
|
+
- `chore:`/`docs:`/`test:`/`refactor:` → no release
|
|
89
|
+
Where changesets asks "what kind of change is this?" at `bunx changeset` time, here you answer it
|
|
90
|
+
in the commit (squash-merge) message. PR titles should follow the same convention since PRs are
|
|
91
|
+
squash-merged.
|
|
92
|
+
2. **Release PR**: on every push to `main`, the `release-please` workflow opens/updates a release PR
|
|
93
|
+
(the analog of changesets' "Version Packages" PR) that bumps `version` in `pyproject.toml` and
|
|
94
|
+
`SDK_VERSION` in `src/traffical/options.py` (via the `x-release-please-version` annotation) and
|
|
95
|
+
updates `CHANGELOG.md`.
|
|
96
|
+
3. **Merging the release PR** tags `vX.Y.Z` and creates a GitHub release.
|
|
97
|
+
4. **The release triggers `publish.yml`**, which builds with `uv build` and publishes to PyPI via
|
|
98
|
+
trusted publishing (OIDC; the `pypi` GitHub environment, no long-lived API token).
|
|
99
|
+
|
|
100
|
+
Version state lives in `.release-please-manifest.json` + `release-please-config.json`.
|
|
101
|
+
|
|
102
|
+
### One-time setup (maintainers)
|
|
103
|
+
|
|
104
|
+
- On [pypi.org](https://pypi.org/manage/account/publishing/): add a **pending trusted publisher** for
|
|
105
|
+
project `traffical` → owner `traffical`, repository `python-sdk`, workflow `publish.yml`,
|
|
106
|
+
environment `pypi`. The first publish claims the name.
|
|
107
|
+
- In the GitHub repo: create the `pypi` environment (optionally with required reviewers as a
|
|
108
|
+
publish gate). Optional: a `RELEASE_PLEASE_TOKEN` PAT secret so CI runs on release PRs
|
|
109
|
+
(the default `GITHUB_TOKEN` cannot trigger downstream workflows).
|
|
110
|
+
|
|
111
|
+
> **Warning:** publishing to PyPI makes the source public (the sdist contains the full package).
|
|
112
|
+
> Do not publish a release while this repository is meant to stay private.
|
|
113
|
+
|
|
114
|
+
### Release gates
|
|
115
|
+
|
|
116
|
+
- CI (`ci.yml`) must be green: full test suite including all sdk-spec conformance vector suites
|
|
117
|
+
against the pinned spec version (`SDK_SPEC_VERSION` in `ci.yml`, fetched from the published
|
|
118
|
+
`@traffical/sdk-spec` npm package), `mypy --strict`, `ruff check`, `ruff format --check`, and a
|
|
119
|
+
wheel/sdist build whose contents are checked (`py.typed` present, no test files).
|
|
120
|
+
- Publishing to PyPI happens from CI only, never from developer machines.
|
traffical-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Traffical
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
traffical-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: traffical
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Traffical Python SDK: deterministic experimentation and parameter resolution
|
|
5
|
+
Project-URL: Homepage, https://traffical.io
|
|
6
|
+
Project-URL: Repository, https://github.com/traffical/python-sdk
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: ab-testing,experimentation,feature-flags,traffical
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: httpx<1.0,>=0.27
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# Traffical Python SDK
|
|
25
|
+
|
|
26
|
+
[Traffical](https://traffical.io) is one control plane for experiments, feature flags, and adaptive
|
|
27
|
+
optimization. Instead of hard-coding decisions, you expose **typed parameters** — numbers, strings, booleans,
|
|
28
|
+
and JSON, not just on/off toggles — and control behavior across web, mobile, push, and backend from a single
|
|
29
|
+
place. Parameters are resolved **locally in the SDK** (sub-millisecond, no network round trips at runtime), and
|
|
30
|
+
metrics are computed **warehouse-native**, against data you own. Start with a feature flag, graduate it to an
|
|
31
|
+
A/B test, and let adaptive optimization shift traffic to the winning variant — all on the same parameter,
|
|
32
|
+
without a deploy.
|
|
33
|
+
|
|
34
|
+
This package is the **official Python SDK** (Python 3.10+) for Traffical, with first-class sync and asyncio
|
|
35
|
+
clients.
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- **Local, in-process evaluation** — resolve a cached config bundle with no per-decision network call.
|
|
40
|
+
- **Typed parameters with safe defaults** — bool / string / number / JSON, each with a caller-provided
|
|
41
|
+
fallback; every public call is fail-open (your defaults always win on errors).
|
|
42
|
+
- **Layered experiments & targeting** — Google-style layered isolation, condition/attribute segmentation, and
|
|
43
|
+
progressive (percentage) rollouts.
|
|
44
|
+
- **Adaptive optimization** — contextual-bandit scoring evaluated client-side.
|
|
45
|
+
- **BYO warehouse-native assignment logging** — route structured assignment rows through your own pipeline
|
|
46
|
+
(a DB, a queue, Segment, RudderStack) so assignment data never has to leave your infrastructure.
|
|
47
|
+
- **Event tracking** — exposure, decision, and custom track events, batched and flushed on a background
|
|
48
|
+
thread (or asyncio task), with retries and an atexit safety net.
|
|
49
|
+
- **Fork-safe by construction** — works under gunicorn pre-fork, celery, and anything else that calls
|
|
50
|
+
`os.fork()` after client creation.
|
|
51
|
+
- **Pure, deterministic engine** — `traffical.engine` is stdlib-only (no I/O, no clocks, no globals) and
|
|
52
|
+
conformance-tested against the cross-language SDK spec.
|
|
53
|
+
|
|
54
|
+
## Modes
|
|
55
|
+
|
|
56
|
+
- **Bundle mode (default)** — the SDK fetches a config bundle, refreshes it on a background poller, and
|
|
57
|
+
resolves every parameter locally. No per-decision network call.
|
|
58
|
+
- **Server mode** — resolution is delegated to the Traffical edge via `POST /v1/resolve`. Use it when you
|
|
59
|
+
want zero client-side evaluation logic.
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install traffical
|
|
65
|
+
# or
|
|
66
|
+
uv add traffical
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Requires Python 3.10+. The only runtime dependency is [httpx](https://www.python-httpx.org/).
|
|
70
|
+
|
|
71
|
+
## Quick start
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from traffical import TrafficalClient, TrafficalClientOptions
|
|
75
|
+
|
|
76
|
+
client = TrafficalClient(
|
|
77
|
+
TrafficalClientOptions(
|
|
78
|
+
api_key="sk_...",
|
|
79
|
+
project_id="proj_...",
|
|
80
|
+
env="production",
|
|
81
|
+
org_id="org_...",
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
client.wait_for_ready(timeout=5.0)
|
|
85
|
+
|
|
86
|
+
# Resolve parameters with defaults as the fallback.
|
|
87
|
+
params = client.get_params(
|
|
88
|
+
context={"userId": "user-abc", "country": "US"},
|
|
89
|
+
defaults={"ui.primaryColor": "#000000", "pricing.discount": 0},
|
|
90
|
+
)
|
|
91
|
+
color = params["ui.primaryColor"]
|
|
92
|
+
|
|
93
|
+
# decide() returns a Decision (assignments + metadata). Call track_exposure()
|
|
94
|
+
# when the user actually sees the treatment.
|
|
95
|
+
decision = client.decide({"userId": "user-abc"}, {"ui.primaryColor": "#000000"})
|
|
96
|
+
variant = decision.assignments["ui.primaryColor"]
|
|
97
|
+
client.track_exposure(decision)
|
|
98
|
+
|
|
99
|
+
# Custom analytics events, attributed to the decision. Numeric metrics go in
|
|
100
|
+
# the value / values options (not properties):
|
|
101
|
+
client.track("checkout_completed", value=49.0, decision_id=decision.decision_id)
|
|
102
|
+
|
|
103
|
+
client.close() # flushes pending events; also usable as a context manager
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`TrafficalClient` supports `with`-statement usage; `close()` is idempotent and flushes the event queue.
|
|
107
|
+
|
|
108
|
+
## Async quick start
|
|
109
|
+
|
|
110
|
+
`AsyncTrafficalClient` has the same surface with awaitable `get_params` / `decide` / `flush_events` /
|
|
111
|
+
`close`. Background work runs on the event loop; enter the context manager (or call `start()`) from a
|
|
112
|
+
running loop.
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
import asyncio
|
|
116
|
+
|
|
117
|
+
from traffical import AsyncTrafficalClient, TrafficalClientOptions
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
async def main() -> None:
|
|
121
|
+
async with AsyncTrafficalClient(
|
|
122
|
+
TrafficalClientOptions(
|
|
123
|
+
api_key="sk_...",
|
|
124
|
+
project_id="proj_...",
|
|
125
|
+
env="production",
|
|
126
|
+
org_id="org_...",
|
|
127
|
+
)
|
|
128
|
+
) as client:
|
|
129
|
+
await client.wait_for_ready(timeout=5.0)
|
|
130
|
+
decision = await client.decide({"userId": "user-abc"}, {"ui.primaryColor": "#000000"})
|
|
131
|
+
client.track_exposure(decision)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
asyncio.run(main())
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Server evaluation mode
|
|
138
|
+
|
|
139
|
+
Delegate resolution to the edge worker instead of evaluating a local bundle. Each `get_params()` /
|
|
140
|
+
`decide()` performs a `POST /v1/resolve`:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
options = TrafficalClientOptions(
|
|
144
|
+
api_key="sk_...",
|
|
145
|
+
project_id="proj_...",
|
|
146
|
+
env="production",
|
|
147
|
+
org_id="org_...",
|
|
148
|
+
evaluation_mode="server",
|
|
149
|
+
)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
In server mode `wait_for_ready()` returns immediately (there is no bundle to wait for) and resolve failures
|
|
153
|
+
fall back to your defaults.
|
|
154
|
+
|
|
155
|
+
## Warehouse-native assignment logging
|
|
156
|
+
|
|
157
|
+
Pass an `assignment_logger` callable to route structured per-layer rows through your own pipeline. Each
|
|
158
|
+
`AssignmentLogEntry` carries the stable `policy_key` / `allocation_key` used for warehouse joins, plus
|
|
159
|
+
`unit_key`, `layer_id`, `decision_id`, timestamps and SDK metadata. Combine with `disable_cloud_events=True`
|
|
160
|
+
to keep assignment data entirely on your own infrastructure:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from traffical import AssignmentLogEntry, TrafficalClient, TrafficalClientOptions
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def log_assignment(entry: AssignmentLogEntry) -> None:
|
|
167
|
+
my_warehouse.insert("assignments", entry.__dict__) # DB, queue, CDP, ...
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
client = TrafficalClient(
|
|
171
|
+
TrafficalClientOptions(
|
|
172
|
+
api_key="sk_...",
|
|
173
|
+
project_id="proj_...",
|
|
174
|
+
env="production",
|
|
175
|
+
org_id="org_...",
|
|
176
|
+
assignment_logger=log_assignment,
|
|
177
|
+
disable_cloud_events=True, # keep assignment data on your own infra
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The logger fires on every `decide()` (type `"decision"`) and `track_exposure()` (type `"exposure"`),
|
|
183
|
+
independent of `track_decisions`, decision deduplication, and `disable_cloud_events`. Entries are
|
|
184
|
+
deduplicated per `unit:policy:allocation:type` with a 1-hour TTL; set
|
|
185
|
+
`deduplicate_assignment_logger=False` to receive every row.
|
|
186
|
+
|
|
187
|
+
`AssignmentLogEntry.to_row()` maps the entry onto a flat, warehouse-friendly
|
|
188
|
+
snake_case row. Two field names are deliberately renamed for the warehouse
|
|
189
|
+
convention: the propensity is emitted as **`propensity`** (not `probability`),
|
|
190
|
+
and the entry `id` as **`assignment_id`**. Off-policy training columns
|
|
191
|
+
(`bucket`, `propensity`, `model_version`, `config_version`) are included, and
|
|
192
|
+
any filtered context (`properties`) is spread to the top level as CUPED
|
|
193
|
+
covariates.
|
|
194
|
+
|
|
195
|
+
## Options reference
|
|
196
|
+
|
|
197
|
+
`TrafficalClientOptions` is a frozen dataclass; construct it with keyword arguments.
|
|
198
|
+
|
|
199
|
+
| Option | Default | Description |
|
|
200
|
+
|--------|---------|-------------|
|
|
201
|
+
| `api_key`, `project_id`, `env`, `org_id` | — | Required scoping + auth |
|
|
202
|
+
| `base_url` | `https://sdk.traffical.io` | Control-plane base URL |
|
|
203
|
+
| `evaluation_mode` | `"bundle"` | `"bundle"` (local) or `"server"` (`POST /v1/resolve`) |
|
|
204
|
+
| `refresh_interval_seconds` | `60.0` | Bundle refresh interval (seconds); `<= 0` fetches once, no background refresh |
|
|
205
|
+
| `config_timeout_seconds` | `10.0` | HTTP timeout for config-bundle fetch (seconds) |
|
|
206
|
+
| `events_timeout_seconds` | `10.0` | HTTP timeout for event delivery (seconds) |
|
|
207
|
+
| `resolve_timeout_seconds` | `5.0` | HTTP timeout for server-mode resolve calls (seconds) |
|
|
208
|
+
| `track_decisions` | `True` | Emit decision events on `decide()` |
|
|
209
|
+
| `decision_dedup_ttl_seconds` | `3600.0` | Decision-event dedup window (seconds) |
|
|
210
|
+
| `deduplicate_exposures` | `True` | Session-dedup exposures per unit/policy/allocation |
|
|
211
|
+
| `exposure_session_ttl_seconds` | `1800.0` | Exposure session-dedup window (seconds; 30 min) |
|
|
212
|
+
| `batch_size` | `10` | Auto-flush threshold |
|
|
213
|
+
| `flush_interval_seconds` | `30.0` | Background flush interval (seconds) |
|
|
214
|
+
| `max_event_queue_size` | `1000` | Bounded event queue; overflow drops oldest |
|
|
215
|
+
| `event_max_retries` | `3` | Delivery retries per batch |
|
|
216
|
+
| `assignment_logger` | `None` | BYO warehouse logger, `(AssignmentLogEntry) -> None` |
|
|
217
|
+
| `deduplicate_assignment_logger` | `True` | Dedup logger rows per unit/policy/allocation/type |
|
|
218
|
+
| `disable_cloud_events` | `False` | Stop sending events to Traffical |
|
|
219
|
+
| `local_config` | `None` | Bootstrap/offline `ConfigBundle` (or raw `dict`) |
|
|
220
|
+
| `config_source` | `None` | Custom bundle source (`FileConfigSource`, `InlineConfigSource`, `CallableConfigSource`, or your own) |
|
|
221
|
+
| `transport`, `async_transport` | `None` | httpx transport seams (e.g. `MockTransport` in tests) |
|
|
222
|
+
|
|
223
|
+
## Fork safety (gunicorn, celery, …)
|
|
224
|
+
|
|
225
|
+
Pre-fork servers `os.fork()` after your app module — and your client — are created. Background threads do
|
|
226
|
+
not survive a fork, and inherited locks can be held by threads that no longer exist. The SDK handles this
|
|
227
|
+
automatically via `os.register_at_fork`:
|
|
228
|
+
|
|
229
|
+
- locks in the event queue, flusher, dedup caches, and config poller are re-armed in the child,
|
|
230
|
+
- dead background threads (event flusher, config poller) are dropped and restarted lazily on next use,
|
|
231
|
+
- each child process flushes its own events; an atexit/`weakref.finalize` safety net stops flushers of
|
|
232
|
+
clients that are garbage-collected without `close()`.
|
|
233
|
+
|
|
234
|
+
What you should know:
|
|
235
|
+
|
|
236
|
+
- Creating the client at module import (pre-fork) is fine and recommended — workers share nothing at runtime.
|
|
237
|
+
- Call `close()` (or `flush_events()`) in worker shutdown hooks if you need a hard delivery guarantee.
|
|
238
|
+
- The asyncio client is bound to the event loop it was started on; create it per process/loop, not pre-fork.
|
|
239
|
+
|
|
240
|
+
## Cross-language conformance
|
|
241
|
+
|
|
242
|
+
The Python SDK shares the language-agnostic [Traffical SDK spec](https://github.com/traffical/sdk-spec)
|
|
243
|
+
(0.7.0) with the JS/TS and PHP SDKs: the same SHA-256 v2 (UTF-8 byte) assignment hashing, the same layered
|
|
244
|
+
resolution engine, and the same contextual-bandit scoring. Every release is gated on the spec's
|
|
245
|
+
deterministic conformance vectors — all resolution fixture suites (`basic`, `conditions`,
|
|
246
|
+
`conditions_omitted`, `contextual`, `contextual_boundary`, `contextual_gamma_zero`,
|
|
247
|
+
`contextual_high_floor`, `edge_policies`, `per_layer_unit_key`, `empty_unit_key`, `numeric_unit_key`,
|
|
248
|
+
`unicode`, plus the entity-weight and resolve vectors) — so a given unit buckets identically on every
|
|
249
|
+
platform.
|
|
250
|
+
|
|
251
|
+
The 0.7.0 drift-remediation vectors lock the cross-SDK behavior decisions this SDK implements: empty layer
|
|
252
|
+
`unitKey` overrides skip the layer (S1), numeric unit keys stringify via ECMAScript `Number::toString`
|
|
253
|
+
(S2), strict condition typing with `.length` paths (S3), single-event exposure with session dedup (S4),
|
|
254
|
+
omitted relational values never match (S5), the `safeGamma`/`effectiveFloor` softmax guards (S6), and
|
|
255
|
+
`modelVersion = generatedAt ?? modelVersion` (S7). Emitted exposure/decision/track payloads are also
|
|
256
|
+
validated against `events.schema.json` and the `events_conformance.json` vectors.
|
|
257
|
+
|
|
258
|
+
## Examples
|
|
259
|
+
|
|
260
|
+
Runnable scripts live in [examples/](examples/): a sync quick start, an asyncio variant, and a FastAPI
|
|
261
|
+
integration. They run offline against a local bundle file (`FileConfigSource`), so no API key is needed.
|
|
262
|
+
|
|
263
|
+
## Development
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
uv sync # create the venv + install dev deps
|
|
267
|
+
uv run pytest # full suite (unit + conformance)
|
|
268
|
+
uv run mypy src/traffical # strict type checking
|
|
269
|
+
uv run ruff check # lint
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Conformance fixtures are loaded from the sdk-spec checkout, resolved from the `TRAFFICAL_SDK_SPEC_PATH`
|
|
273
|
+
environment variable and defaulting to the sibling `../sdk-spec` directory:
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
TRAFFICAL_SDK_SPEC_PATH=/path/to/sdk-spec uv run pytest tests/conformance
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the full workflow.
|
|
280
|
+
|
|
281
|
+
## License
|
|
282
|
+
|
|
283
|
+
MIT — see [LICENSE](LICENSE).
|