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
155 changes: 62 additions & 93 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,138 +1,107 @@
name: CI

on:
workflow_dispatch:
push:
branches: [main]
pull_request:

jobs:
fast:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13"]
steps:
- name: Check out repository
uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: "3.12"

- name: Install package and dev dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"

- name: Format
run: python -m ruff format --check .

- name: Lint
run: python -m ruff check .

- name: Run unit tests
run: python -m pytest tests/unit
python-version: ${{ matrix.python-version }}
cache: pip
- run: python -m pip install --upgrade pip
- run: python -m pip install -e ".[dev]"
- run: python -m ruff format --check .
- run: python -m ruff check .
- run: python -m pyright src
- run: python -m pytest tests/unit --cov=fitz_py --cov-report=term-missing

package:
runs-on: ubuntu-latest
needs: fast
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: "3.12"

- name: Build distributions
run: |
python -m pip install --upgrade pip build
python -m build

- name: Smoke install built wheel
cache: pip
- run: python -m pip install --upgrade pip build
- run: python -m build
- name: Smoke test wheel
run: |
python -m venv artifacts/smoke/.venv
. artifacts/smoke/.venv/bin/activate
python -m pip install --upgrade pip
python -m pip install dist/*.whl
python - <<'PY'
python -m venv artifacts/smoke
artifacts/smoke/bin/python -m pip install dist/*.whl
artifacts/smoke/bin/python - <<'PY'
from fitz_py import Client, ClientConfig

client = Client(ClientConfig(url="ws://localhost:4190/ws"))
print(type(client).__name__)
print(type(client.kv()).__name__)
client = Client(ClientConfig(url="tcp://localhost:4191"))
assert client.kv is client.kv
assert client.queue is client.queue
PY

- name: Upload distributions
uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v4
with:
name: fitz-py-dist
path: dist/*

spec:
runs-on: ubuntu-latest
broker:
needs: fast
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
transport: [ws, tcp]
auth_mode: [anonymous, valid_jwt]
transport: [tcp, ws]
auth-mode: [anonymous, valid_jwt]
env:
FITZ_BROKER_ANON_TCP_ADDR: localhost:4191
FITZ_BROKER_ANON_WS_ADDR: ws://localhost:4190/ws
FITZ_BROKER_AUTH_TCP_ADDR: localhost:4091
FITZ_BROKER_AUTH_WS_ADDR: ws://localhost:4090/ws
FITZ_BROKER_JWT_HMAC_SECRET: test-secret-key
FITZ_BROKER_JWT_AUDIENCE: fitz
CONFORMANCE_OUTPUT: artifacts/conformance-results.json
FITZ_BROKER_JWT_TENANT: dev
CONFORMANCE_TRANSPORT: ${{ matrix.transport }}
CONFORMANCE_AUTH_MODE: ${{ matrix.auth-mode }}
CONFORMANCE_OUTPUT: artifacts/conformance-${{ matrix.transport }}-${{ matrix.auth-mode }}.json
steps:
- name: Check out repository
uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: "3.12"

- name: Install package and dev dependencies
cache: pip
- run: python -m pip install --upgrade pip
- run: python -m pip install -e ".[dev]"
- run: docker compose up -d
- name: Wait for brokers
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"

- name: Start broker stack
run: docker compose -f ../fitz-go/compose.yml up -d

- name: Run conformance suite
env:
CONFORMANCE_TRANSPORT: ${{ matrix.transport }}
CONFORMANCE_AUTH_MODE: ${{ matrix.auth_mode }}
run: python -m pytest tests/conformance -v

- name: Run invalid JWT auth scenario
run: |
CONFORMANCE_TRANSPORT=${{ matrix.transport }} CONFORMANCE_AUTH_MODE=anonymous python -m pytest tests/conformance/test_conformance.py -k cs002 -v

- name: Enforce full conformance
for port in 4090 4091 4190 4191; do
timeout 60 bash -c "until (echo > /dev/tcp/127.0.0.1/$port) 2>/dev/null; do sleep 1; done"
done
- run: python -m pytest tests/integration -v
- run: python -m pytest tests/conformance -v
- name: Enforce canonical conformance
run: |
python - <<'PY'
import json
import json, os
from pathlib import Path
required = {f"CS-{index:03d}" for index in range(1, 16)}
path = Path("artifacts/conformance-results.json")
data = json.loads(path.read_text(encoding="utf-8"))
observed = {s["scenario_id"] for s in data["scenarios"]}
missing = sorted(required - observed)
failing = sorted(s["scenario_id"] for s in data["scenarios"] if s["scenario_id"] in required and s["verdict"] != "pass")
print(f"transport={data['transport']} auth={data['auth_mode']} p0={data['p0_pass_rate']:.0%} p1={data['p1_pass_rate']:.0%} failing={failing} missing={missing}")
if data.get("overall_status") != "pass" or failing or missing:
raise SystemExit(1)
data = json.loads(Path(os.environ["CONFORMANCE_OUTPUT"]).read_text())
required = {f"CS-{index:03d}" for index in range(1, 18)}
observed = {item["scenario_id"] for item in data["scenarios"] if item["verdict"] == "pass"}
assert data["overall_status"] == "pass"
assert observed == required, sorted(required - observed)
PY

- name: Upload conformance results
- uses: actions/upload-artifact@v4
if: always()
uses: actions/upload-artifact@v4
with:
name: fitz-py-conformance-${{ matrix.transport }}-${{ matrix.auth_mode }}
path: artifacts/conformance-results.json

- name: Stop broker stack
if: always()
run: docker compose -f ../fitz-go/compose.yml down --volumes
name: conformance-${{ matrix.transport }}-${{ matrix.auth-mode }}
path: artifacts/conformance-*.json
if-no-files-found: warn
- if: always()
run: docker compose down --volumes
154 changes: 58 additions & 96 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,135 +1,97 @@
# fitz-py

`fitz-py` is the async-first Python SDK for Fitz.

## Install
`fitz-py` is the typed, asyncio-native Python client for the Fitz broker. Version 0.2 is a
deliberate clean break: clients are configured once, domain clients are cached properties, streamed
results are async iterators, and network/runtime queues are bounded.

```bash
python -m pip install cntryl-fitz
```

## Quick Start
## Connect

```python
from fitz_py import Client, ClientConfig

client = Client(
async with Client(
ClientConfig(
url="ws://localhost:4190/ws",
token_provider=lambda: "",
)
)
) as client:
async with await client.kv.begin("kv://example/app/users") as tx:
await tx.put(b"alice", b"active")
await tx.commit()
```

`Client.close()` is permanent and idempotent. Reconnect is enabled by default after the first
successful authentication; an authentication rejection permanently closes the client. Configure
timeouts, bounded concurrency, retry, heartbeat, logging, metrics, and lifecycle events with the
frozen policies on `ClientConfig`.

## Domains

await client.connect()
- `client.kv`: transactions, scans, range deletes, durability, and mutation subscriptions.
- `client.queue`: delayed enqueue, broker-native long-poll reserve, fenced items, and availability.
- `client.rpc`: streamed calls and wildcard worker registrations with bounded handler dispatch.
- `client.lease`: queued fenced acquisition, query, change subscriptions, and managed renewal.
- `client.notice`: fire-and-forget publish and one-wire/many-consumer subscriptions.
- `client.stream`: append sessions, filtered replay, global cursors/watermarks, and commit events.
- `client.schedule`: delivery modes, total-count pagination, cancel, and routed notifications.

tx = await client.kv().begin("kv://example/app/users")
result = await tx.get(b"key")
await tx.rollback()
Subscriptions are independently closable async iterators:

await client.close()
```python
async with await client.notice.subscribe("notice://example/app/*") as notices:
async for notice in notices:
print(notice.route, notice.body)
```

The package is `asyncio`-first and does not provide a synchronous wrapper.
Reserve waits are performed by the broker rather than local polling:

```python
items = await client.queue.reserve("queue://example/work/*", lease=30, wait=10)
for item in items:
try:
await process(item.body)
except Exception:
raise
else:
await item.complete()
```

## Stream replay
Managed leases renew at one third of their TTL and preserve both renewal and release failures:

```python
from fitz_py import StreamFilterClause, StreamFilterSet

stream_filter = StreamFilterSet(clauses=[StreamFilterClause(kind="Equals", value="proj.alpha")])

records = await client.stream().read(
"stream://example/app/events",
start_offset=0,
limit=100,
stream_filter=stream_filter,
)
page = await client.stream().read_page(
"stream://example/app/events",
start_offset=0,
limit=100,
stream_filter=stream_filter,
)

# `read()` preserves the compatibility event-only shape.
# `read_page()` exposes filtered markers and cursor metadata.
assert page.cursor.last_resource_offset >= 0
async with client.lease.hold("lease://example/jobs/leader", ttl=30, wait=10) as lease:
await run_leader(lease.token)
```

## Parity Goals
## Errors and cancellation

`fitz-py` tracks the Fitz client behavior implemented in `fitz-go` and `fitz-ts`.
The Python SDK now exposes the same seven domains, typed Fitz/domain errors, retryability
helpers via `is_retryable()`, reconnect-aware subscription restoration, and extended
integration/conformance coverage for queue, lease, notice, and schedule lifecycle flows.
All library failures derive from `FitzError`. Transport, connection, timeout, protocol, bounded
queue, stale-handle, and domain failures have stable string codes and structured context. Task
cancellation is preserved. Requests cancelled after transmission leave a FIFO tombstone so a late
reply cannot corrupt the next same-type request.

## Verification

Fast local checks:

```bash
python -m pip install -e ".[dev]"
python -m ruff format --check .
python -m ruff check .
python -m pyright src
python -m pytest tests/unit
```

Hot-path microbenchmarks:

```bash
python artifacts/benchmarks/hotpath.py --iterations 10000
# or
hatch run bench-hotpath
```

One-shot verification:
docker compose up -d
python -m pytest tests/integration
python -m pytest tests/conformance
docker compose down --volumes

```bash
hatch run verify
```

The local quality bar is formatter first, then lint, then unit tests.

Broker-backed verification:

```bash
docker compose -f ../fitz-go/compose.yml up -d
python -m pytest tests/integration -v
CONFORMANCE_TRANSPORT=ws CONFORMANCE_AUTH_MODE=anonymous \
CONFORMANCE_OUTPUT=artifacts/conformance-results.json \
python -m pytest tests/conformance -v
docker compose -f ../fitz-go/compose.yml down --volumes
```

Package smoke verification:

```bash
python -m benchmarks.hotpath
python -m build
python -m pip install dist/*.whl
```

The conformance harness writes JSON results to `artifacts/conformance-results.json` by default.
It currently executes 19 scenarios: CS-001..CS-015 mirror the shared cross-language suite,
and CS-016..CS-019 cover fitz-py local lifecycle checks.

## Project Layout

- `src/fitz_py`: package code
- `tests/unit`: fast unit coverage
- `tests/integration`: broker-backed integration coverage
- `tests/conformance`: release-gate conformance coverage

## Canonical Docs

Canonical client behavior is defined by the server-owned docs in the Fitz repository:

- [CLIENT_SPEC.md](../fitz/docs/clients/CLIENT_SPEC.md)
- [CLIENT_ACCEPTANCE_CRITERIA.md](../fitz/docs/clients/CLIENT_ACCEPTANCE_CRITERIA.md)
- [CLIENT_IMPLEMENTATION_GUIDE.md](../fitz/docs/clients/CLIENT_IMPLEMENTATION_GUIDE.md)
- [CONNECTION_FLOW.md](../fitz/docs/clients/CONNECTION_FLOW.md)

## Documentation

- [`docs/README.md`](docs/README.md)
- [`CLIENT_SPEC.md`](CLIENT_SPEC.md)
- [`CLIENT_ACCEPTANCE_CRITERIA.md`](CLIENT_ACCEPTANCE_CRITERIA.md)
The repository owns its broker Compose stack and a vendored copy of the canonical 17-scenario
cross-language suite. CI runs Python 3.11-3.13, wheel smoke tests, TCP/WebSocket, and
anonymous/JWT broker legs. Canonical behavior remains owned by the Fitz server documentation.
1 change: 1 addition & 0 deletions benchmarks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Fitz Python benchmark suite."""
Loading
Loading