Skip to content

Commit ea02ddc

Browse files
authored
Implement the Python SDK with full spec conformance (#85)
PR: #85
1 parent d3ff3f4 commit ea02ddc

426 files changed

Lines changed: 52764 additions & 2105 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/workflows/ci.yml

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,28 @@ jobs:
3737
- name: Run pytest
3838
run: uv run pytest -q
3939

40+
# Blocking requirement-traceability audit. Collection-only run: the
41+
# TraceabilityPlugin (registered in the root conftest) collects every
42+
# @pytest.mark.req(...) marker, writes the matrix artifact, and — because
43+
# DEXPACE_TRACEABILITY_GATE is set — fails the run if any MUST-level
44+
# requirement in an enabled subsystem is neither covered by a marker nor
45+
# listed N/A in docs/traceability-na-ids.txt (the deviations-ledger bridge).
46+
- name: Run traceability audit (blocking)
47+
env:
48+
DEXPACE_TRACEABILITY_ENABLE: "seams,http,bodies,io,context,pipeline,recovery,redirect,retry,auth,xcut,serde,pagination,sse,observability,config,transport,async,nfr"
49+
DEXPACE_TRACEABILITY_NA: docs/traceability-na-ids.txt
50+
DEXPACE_TRACEABILITY_MATRIX: build/traceability-matrix.json
51+
DEXPACE_TRACEABILITY_GATE: "1"
52+
run: uv run pytest -q --co
53+
54+
- name: Upload traceability matrix
55+
if: always()
56+
uses: actions/upload-artifact@v4
57+
with:
58+
name: traceability-matrix-py${{ matrix.python-version }}
59+
path: build/traceability-matrix.json
60+
if-no-files-found: error
61+
4062
- name: Run mypy
4163
run: uv run mypy --strict
4264

@@ -45,3 +67,223 @@ jobs:
4567

4668
- name: Run ruff format --check
4769
run: uv run ruff format --check
70+
71+
- name: Run import-linter
72+
run: uv run lint-imports
73+
74+
packaging:
75+
name: packaging invariants
76+
runs-on: ubuntu-latest
77+
steps:
78+
- name: Checkout
79+
uses: actions/checkout@v6
80+
81+
- name: Install uv
82+
uses: astral-sh/setup-uv@v8.2.0
83+
with:
84+
enable-cache: true
85+
python-version: "3.12"
86+
87+
- name: Check namespace layout
88+
run: uv run --no-project python tools/check_namespace_packages.py
89+
90+
- name: Check MIT headers
91+
run: uv run --no-project python tools/check_mit_headers.py
92+
93+
- name: Check runtime dependency budget
94+
run: uv run --no-project python tools/check_dependency_audit.py
95+
96+
- name: Build core + stdlib wheels
97+
run: |
98+
uv build --package dexpace-sdk-core --wheel -o dist
99+
uv build --package dexpace-sdk-http-stdlib --wheel -o dist
100+
101+
- name: Smoke-import wheels in a fresh venv
102+
run: |
103+
uv venv /tmp/wheel-smoke
104+
uv pip install --python /tmp/wheel-smoke \
105+
dist/dexpace_sdk_core-*.whl \
106+
dist/dexpace_sdk_http_stdlib-*.whl
107+
/tmp/wheel-smoke/bin/python tools/smoke_wheel_import.py
108+
109+
coverage:
110+
name: coverage gate
111+
runs-on: ubuntu-latest
112+
steps:
113+
- name: Checkout
114+
uses: actions/checkout@v6
115+
116+
- name: Install uv
117+
uses: astral-sh/setup-uv@v8.2.0
118+
with:
119+
enable-cache: true
120+
python-version: "3.12"
121+
122+
- name: Sync workspace
123+
run: uv sync
124+
125+
# Blocking aggregate line+branch coverage gate. The 80% floor lives in
126+
# [tool.coverage.report].fail_under in pyproject.toml; pytest-cov exits
127+
# non-zero when the total drops below it.
128+
- name: Run coverage gate
129+
run: uv run pytest -q --cov --cov-report=term-missing
130+
131+
typecheck-divergence:
132+
name: typecheck divergence (pyright, non-blocking)
133+
runs-on: ubuntu-latest
134+
# Advisory early-warning lane: pyright cross-checks mypy's verdict so a
135+
# divergence between the two checkers surfaces early. This is NOT a gate —
136+
# `mypy --strict` in the `test` job remains the sole blocking type check.
137+
# `continue-on-error` at both the job and step level keeps a red result
138+
# (e.g. pyright's stricter completeness scoring) from ever failing CI.
139+
# pyright is layered in per-run via `uv run --with`, so it stays out of the
140+
# committed dependency set and uv.lock.
141+
continue-on-error: true
142+
steps:
143+
- name: Checkout
144+
uses: actions/checkout@v6
145+
146+
- name: Install uv
147+
uses: astral-sh/setup-uv@v8.2.0
148+
with:
149+
enable-cache: true
150+
python-version: "3.14"
151+
152+
- name: Sync workspace
153+
run: uv sync
154+
155+
- name: pyright --verifytypes (advisory)
156+
continue-on-error: true
157+
run: uv run --with pyright pyright --verifytypes dexpace.sdk.core --ignoreexternal
158+
159+
- name: pyright typing-conformance corpus (advisory)
160+
continue-on-error: true
161+
run: uv run --with pyright pyright packages/dexpace-sdk-core/tests/typing_conformance.py
162+
163+
version-skew:
164+
name: version skew (Python ${{ matrix.python-version }}, ${{ matrix.resolution }})
165+
runs-on: ubuntu-latest
166+
strategy:
167+
fail-fast: false
168+
matrix:
169+
python-version: ["3.12", "3.13", "3.14"]
170+
resolution: ["highest", "lowest-direct"]
171+
steps:
172+
- name: Checkout
173+
uses: actions/checkout@v6
174+
175+
- name: Install uv
176+
uses: astral-sh/setup-uv@v8.2.0
177+
with:
178+
enable-cache: true
179+
python-version: ${{ matrix.python-version }}
180+
181+
# Resolve the workspace at either edge of its declared dependency window.
182+
# `highest` exercises the newest adapters against head core (the standard
183+
# release runtime). `lowest-direct` pins every *direct* dependency to its
184+
# declared floor: core to the compatible-release lower bound each adapter
185+
# and the TCK pin (dexpace-sdk-core>=0.1,<0.2) and each transport to its
186+
# minimum (httpx>=0.27, aiohttp>=3.9, requests>=2.32, furl>=2.1.3). Running
187+
# both edges across Python 3.12-3.14 turns the forward-compatibility
188+
# promise -- newest adapters and the generated petstore canary run against
189+
# the oldest supported core -- into a passing pipeline, not a prose claim.
190+
- name: Sync workspace (${{ matrix.resolution }} resolution)
191+
run: uv sync --resolution ${{ matrix.resolution }}
192+
193+
# Newest adapters (built from HEAD) plus the TCK conformance battery, run
194+
# against the resolved core. `--no-sync` keeps the environment exactly as
195+
# the resolution step left it rather than re-resolving to the default edge.
196+
- name: Adapters + TCK conformance
197+
run: >
198+
uv run --no-sync pytest -q
199+
packages/dexpace-sdk-http-stdlib/tests
200+
packages/dexpace-sdk-http-httpx/tests
201+
packages/dexpace-sdk-http-aiohttp/tests
202+
packages/dexpace-sdk-http-requests/tests
203+
packages/dexpace-sdk-tck/src/dexpace/sdk/tck
204+
205+
# The petstore canary is the "generated at vN.M runs on vN.x" proof: a
206+
# generated SDK driven end to end over the certified core. Running it at
207+
# both dependency edges proves the generated output keeps working against
208+
# both head core and the oldest supported core.
209+
- name: Petstore canary
210+
run: uv run --no-sync pytest -q examples/petstore/tests
211+
212+
free-threaded:
213+
name: free-threaded CPython (${{ matrix.python-version }})
214+
runs-on: ubuntu-latest
215+
strategy:
216+
fail-fast: false
217+
matrix:
218+
python-version: ["3.13t", "3.14t"]
219+
# Exercises the codebase's explicit-lock claims (no GIL assumption) on a real
220+
# GIL-disabled interpreter, not just under code review. Only the
221+
# lock-sensitive core suites run here, and each spins real threads: the
222+
# single-use body once-guards, the single-flight token cache, the
223+
# ContextStore cap-drain, the emit-once logger latch, and the SSE
224+
# cross-thread close. Core is pure Python (only furl), so the environment
225+
# holds core plus the test runner alone -- no C-extension transports, which
226+
# do not yet ship free-threaded wheels.
227+
env:
228+
# Fail loudly if an imported extension would force the GIL back on rather
229+
# than silently re-enabling it and voiding the exercise.
230+
PYTHON_GIL: "0"
231+
steps:
232+
- name: Checkout
233+
uses: actions/checkout@v6
234+
235+
- name: Install uv
236+
uses: astral-sh/setup-uv@v8.2.0
237+
with:
238+
enable-cache: true
239+
240+
- name: Install free-threaded interpreter
241+
run: uv python install ${{ matrix.python-version }}
242+
243+
- name: Create free-threaded environment
244+
run: uv venv --python ${{ matrix.python-version }} .venv-ft
245+
246+
# `--only-binary hypothesis` uses its universal wheel instead of building
247+
# its sdist, whose build pulls a Rust/PyO3 toolchain with no free-threaded
248+
# support yet. hypothesis is imported by the core test conftest.
249+
- name: Install core + test runner
250+
run: >
251+
uv pip install --python .venv-ft --only-binary hypothesis
252+
./packages/dexpace-sdk-core
253+
pytest pytest-asyncio hypothesis
254+
255+
- name: Verify the interpreter is GIL-disabled
256+
run: .venv-ft/bin/python -c "import sys; assert not sys._is_gil_enabled(), 'GIL is enabled'; print(sys.version)"
257+
258+
- name: Run lock-sensitive suites (GIL disabled)
259+
run: >
260+
.venv-ft/bin/python -m pytest -q
261+
packages/dexpace-sdk-core/tests/http/test_single_use_bodies.py
262+
packages/dexpace-sdk-core/tests/auth/test_token_cache.py
263+
packages/dexpace-sdk-core/tests/context/test_context_store.py
264+
packages/dexpace-sdk-core/tests/instrumentation/test_logger.py
265+
packages/dexpace-sdk-core/tests/sse/test_connection_lifecycle.py
266+
267+
version-source:
268+
name: single version train
269+
runs-on: ubuntu-latest
270+
# One version train for every published package (NFR-14): the version lives
271+
# once in each pyproject and is mirrored in uv.lock, and a built wheel
272+
# carries that real version -- never a placeholder such as 0.0.0 or
273+
# "unknown" (NFR-15). A bump touches all published packages together or this
274+
# gate fails.
275+
steps:
276+
- name: Checkout
277+
uses: actions/checkout@v6
278+
279+
- name: Install uv
280+
uses: astral-sh/setup-uv@v8.2.0
281+
with:
282+
enable-cache: true
283+
python-version: "3.12"
284+
285+
- name: Build a wheel to inspect its metadata version
286+
run: uv build --package dexpace-sdk-core --wheel -o dist
287+
288+
- name: Verify one version train (pyproject + uv.lock + wheel)
289+
run: uv run --no-project python tools/check_version_train.py --wheel-dir dist

.github/workflows/release.yml

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
name: Release
2+
3+
# Publishes the whole version train to PyPI via trusted publishing (OIDC), with
4+
# PEP 740 attestations and reproducible, byte-stable wheels. A tag push builds
5+
# and publishes; a manual run defaults to a dry run that builds and verifies the
6+
# artifacts without uploading anything.
7+
on:
8+
push:
9+
tags: ["v*"]
10+
workflow_dispatch:
11+
inputs:
12+
dry_run:
13+
description: "Build and verify only; skip the PyPI upload."
14+
type: boolean
15+
default: true
16+
17+
permissions:
18+
contents: read
19+
20+
jobs:
21+
build:
22+
name: build + verify artifacts
23+
runs-on: ubuntu-latest
24+
steps:
25+
- name: Checkout
26+
# Full history so the commit timestamp is available for a reproducible
27+
# SOURCE_DATE_EPOCH.
28+
uses: actions/checkout@v6
29+
with:
30+
fetch-depth: 0
31+
32+
- name: Install uv
33+
uses: astral-sh/setup-uv@v8.2.0
34+
with:
35+
enable-cache: true
36+
python-version: "3.12"
37+
38+
# Reproducible builds (NFR-12): pin every archive timestamp to the tagged
39+
# commit's authored date and fix the hash seed so the build carries no
40+
# run-to-run entropy. hatchling honours SOURCE_DATE_EPOCH and writes wheel
41+
# entries in a stable order, so identical inputs yield byte-identical
42+
# wheels.
43+
- name: Pin reproducible-build environment
44+
run: |
45+
echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> "$GITHUB_ENV"
46+
echo "PYTHONHASHSEED=0" >> "$GITHUB_ENV"
47+
48+
# Build each published distribution explicitly so the unpublished
49+
# workspace root never lands in dist/. Sdists ship alongside wheels.
50+
- name: Build wheels + sdists
51+
run: |
52+
for pkg in \
53+
dexpace-sdk-core \
54+
dexpace-sdk-http-stdlib \
55+
dexpace-sdk-http-httpx \
56+
dexpace-sdk-http-aiohttp \
57+
dexpace-sdk-http-requests \
58+
dexpace-sdk-tck; do
59+
uv build --package "$pkg" --out-dir dist
60+
done
61+
62+
# NFR-14/15: every artifact carries the single, real train version -- never
63+
# a placeholder. Blocks the upload if any wheel disagrees.
64+
- name: Verify one version train (pyproject + uv.lock + wheels)
65+
run: uv run --no-project python tools/check_version_train.py --wheel-dir dist
66+
67+
- name: List artifacts
68+
run: ls -1 dist | sort
69+
70+
- name: Upload artifacts
71+
uses: actions/upload-artifact@v4
72+
with:
73+
name: release-dist
74+
path: dist/
75+
if-no-files-found: error
76+
77+
publish:
78+
name: publish to PyPI (trusted publishing)
79+
needs: build
80+
runs-on: ubuntu-latest
81+
# A tag push publishes; a manual run publishes only when dry_run is turned
82+
# off. The default manual run is a build-and-verify dry run.
83+
if: >-
84+
github.event_name == 'push' ||
85+
(github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run == 'false')
86+
environment:
87+
name: pypi
88+
url: https://pypi.org/project/dexpace-sdk-core/
89+
permissions:
90+
# OIDC token for PyPI trusted publishing; no long-lived API token needed.
91+
id-token: write
92+
steps:
93+
- name: Download artifacts
94+
uses: actions/download-artifact@v4
95+
with:
96+
name: release-dist
97+
path: dist/
98+
99+
- name: Publish to PyPI
100+
uses: pypa/gh-action-pypi-publish@release/v1
101+
with:
102+
# PEP 740 provenance attestations, signed with the OIDC identity
103+
# (NFR-16).
104+
attestations: true
105+
print-hash: true

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,6 @@ dmypy.json
8383

8484
# Cython debug symbols
8585
cython_debug/
86+
87+
# Subagent-driven-development scratch (progress ledger, task briefs/reports)
88+
.superpowers/

0 commit comments

Comments
 (0)