httpr 0.1.17__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.
Files changed (58) hide show
  1. httpr-0.1.17/.github/copilot-instructions.md +240 -0
  2. httpr-0.1.17/.github/workflows/CI.yml +386 -0
  3. httpr-0.1.17/.github/workflows/CI.yml.backup +362 -0
  4. httpr-0.1.17/.github/workflows/copilot-setup-steps.yml +62 -0
  5. httpr-0.1.17/.github/workflows/mkdocs.yml +30 -0
  6. httpr-0.1.17/.github/workflows/set_version.py +56 -0
  7. httpr-0.1.17/.gitignore +86 -0
  8. httpr-0.1.17/.pre-commit-config.yaml +32 -0
  9. httpr-0.1.17/CLAUDE.md +88 -0
  10. httpr-0.1.17/Cargo.lock +2180 -0
  11. httpr-0.1.17/Cargo.toml +55 -0
  12. httpr-0.1.17/LICENSE +21 -0
  13. httpr-0.1.17/PKG-INFO +49 -0
  14. httpr-0.1.17/README.md +291 -0
  15. httpr-0.1.17/benchmark/README.md +17 -0
  16. httpr-0.1.17/benchmark/benchmark.py +270 -0
  17. httpr-0.1.17/benchmark/generate_image.py +138 -0
  18. httpr-0.1.17/benchmark/pyproject.toml +23 -0
  19. httpr-0.1.17/benchmark/requirements.txt +11 -0
  20. httpr-0.1.17/benchmark/server.py +74 -0
  21. httpr-0.1.17/benchmark.jpg +0 -0
  22. httpr-0.1.17/docs/advanced/cookies.md +344 -0
  23. httpr-0.1.17/docs/advanced/index.md +171 -0
  24. httpr-0.1.17/docs/advanced/proxy.md +307 -0
  25. httpr-0.1.17/docs/advanced/ssl-tls.md +294 -0
  26. httpr-0.1.17/docs/api/async-client.md +20 -0
  27. httpr-0.1.17/docs/api/client.md +20 -0
  28. httpr-0.1.17/docs/api/functions.md +55 -0
  29. httpr-0.1.17/docs/api/index.md +112 -0
  30. httpr-0.1.17/docs/api/response.md +254 -0
  31. httpr-0.1.17/docs/index.md +174 -0
  32. httpr-0.1.17/docs/quickstart.md +288 -0
  33. httpr-0.1.17/docs/tutorial/async.md +357 -0
  34. httpr-0.1.17/docs/tutorial/authentication.md +286 -0
  35. httpr-0.1.17/docs/tutorial/index.md +99 -0
  36. httpr-0.1.17/docs/tutorial/making-requests.md +369 -0
  37. httpr-0.1.17/docs/tutorial/response-handling.md +308 -0
  38. httpr-0.1.17/docs/writings/index.md +1 -0
  39. httpr-0.1.17/docs/writings/posts/2025-02-24-python-http-clients-suck.md +9 -0
  40. httpr-0.1.17/httpr/__init__.py +870 -0
  41. httpr-0.1.17/httpr/httpr.pyi +101 -0
  42. httpr-0.1.17/httpr/py.typed +0 -0
  43. httpr-0.1.17/httpr.code-workspace +7 -0
  44. httpr-0.1.17/mkdocs.yml +119 -0
  45. httpr-0.1.17/pyproject.toml +48 -0
  46. httpr-0.1.17/scratch.ipynb +114 -0
  47. httpr-0.1.17/src/lib.rs +445 -0
  48. httpr-0.1.17/src/response.rs +199 -0
  49. httpr-0.1.17/src/traits.rs +87 -0
  50. httpr-0.1.17/src/utils.rs +250 -0
  51. httpr-0.1.17/tests/conftest.py +33 -0
  52. httpr-0.1.17/tests/httpx_conns.py +155 -0
  53. httpr-0.1.17/tests/test_asyncclient.py +24 -0
  54. httpr-0.1.17/tests/test_client.py +280 -0
  55. httpr-0.1.17/tests/test_defs.py +248 -0
  56. httpr-0.1.17/tests/test_docs.py +316 -0
  57. httpr-0.1.17/tests/test_ssl.py +126 -0
  58. httpr-0.1.17/uv.lock +3811 -0
@@ -0,0 +1,240 @@
1
+ # httpr - AI Coding Agent Instructions
2
+
3
+ ## Project Overview
4
+
5
+ **httpr** is a high-performance HTTP client for Python built in Rust using PyO3 and reqwest. It's designed as a drop-in replacement for `httpx` and `requests` with significantly better performance through Rust's native speed.
6
+
7
+ ### Directory Structure
8
+
9
+ ```
10
+ httpr/
11
+ ├── .github/ # GitHub workflows and configurations
12
+ │ ├── workflows/ # CI/CD pipelines (testing, building, releasing)
13
+ │ └── copilot-instructions.md # This file
14
+ ├── src/ # Rust source code
15
+ │ ├── lib.rs # Main RClient class, PyO3 module definition
16
+ │ ├── response.rs # Response object with CaseInsensitiveHeaderMap
17
+ │ ├── traits.rs # Python↔Rust type conversion traits
18
+ │ └── utils.rs # CA certificates, encoding detection
19
+ ├── httpr/ # Python package
20
+ │ ├── __init__.py # Client and AsyncClient classes
21
+ │ ├── httpr.pyi # Type stubs for IDE support
22
+ │ └── py.typed # PEP 561 marker for type checking
23
+ ├── tests/ # Python tests using pytest
24
+ ├── benchmark/ # Performance benchmarking suite
25
+ ├── docs/ # MkDocs documentation
26
+ ├── Cargo.toml # Rust dependencies and build config
27
+ ├── pyproject.toml # Python project metadata and dependencies
28
+ └── README.md # User-facing documentation
29
+ ```
30
+
31
+ ### Architecture
32
+
33
+ - **Rust Core** (`src/`): PyO3-based HTTP client wrapping reqwest
34
+ - `lib.rs`: Main `RClient` class with sync request handling via Tokio single-threaded runtime
35
+ - `response.rs`: `Response` object with `CaseInsensitiveHeaderMap` for HTTP headers
36
+ - `traits.rs`: Conversion traits between Python/Rust types (IndexMap ↔ HeaderMap)
37
+ - `utils.rs`: CA certificate loading, encoding detection
38
+ - **Python Wrapper** (`httpr/`): Thin Python layer over Rust bindings
39
+ - `__init__.py`: `Client` (sync) and `AsyncClient` classes with context manager support
40
+ - `AsyncClient` uses `asyncio.run_in_executor()` to wrap sync Rust calls - NOT native async
41
+ - `httpr.pyi`: Type stubs for IDE support
42
+ - **Build System**: Maturin (PyO3 build tool) compiles Rust → Python wheels
43
+
44
+ ### Key Architectural Decisions
45
+
46
+ 1. **Single Tokio Runtime**: Uses `LazyLock<Runtime>` with `new_current_thread()` - all async operations run on one thread
47
+ 2. **Async is Sync**: `AsyncClient` runs sync Rust code in thread executor, NOT native async Rust
48
+ 3. **Case-Insensitive Headers**: Custom `CaseInsensitiveHeaderMap` struct maintains original casing while allowing case-insensitive lookups (required for HTTP/2)
49
+ 4. **Zero Python Dependencies**: All functionality in Rust; no runtime Python dependencies
50
+
51
+ ### Key Dependencies
52
+
53
+ **Rust (Cargo.toml)**:
54
+ - `pyo3` (0.23.4): Python bindings, enables calling Rust from Python
55
+ - `reqwest` (0.12): HTTP client library, provides core networking functionality
56
+ - `tokio` (1.43): Async runtime for Rust (single-threaded mode)
57
+ - `anyhow`: Error handling with context
58
+ - `indexmap` + `foldhash`: Fast hash maps for headers/params
59
+ - `rustls-tls`: TLS implementation (no OpenSSL dependency)
60
+ - Compression: `gzip`, `brotli`, `zstd`, `deflate` support
61
+
62
+ **Python (pyproject.toml)**:
63
+ - **Zero runtime dependencies** - all functionality in Rust
64
+ - Dev dependencies: `pytest`, `pytest-asyncio`, `mypy`, `ruff`, `maturin`
65
+ - Supports Python 3.9+
66
+
67
+ ## Development Workflows
68
+
69
+ ### Building & Testing
70
+
71
+ ```bash
72
+ # Install development dependencies (includes maturin)
73
+ uv sync --extra dev
74
+
75
+ # Build Rust extension in development mode (fast compilation)
76
+ uv run maturin develop
77
+
78
+ # Run tests (requires httpbin.org access)
79
+ uv run pytest tests/
80
+
81
+ # Type checking
82
+ uv run mypy httpr/
83
+
84
+ # Linting
85
+ uv run ruff check httpr/
86
+ ```
87
+
88
+ ### Benchmarking
89
+
90
+ ```bash
91
+ cd benchmark/
92
+ uv run uvicorn server:app # Terminal 1: Start test server
93
+ uv run python benchmark.py # Terminal 2: Run benchmarks
94
+ ```
95
+
96
+ The benchmark compares httpr against requests, httpx, curl_cffi, pycurl, tls_client, and aiohttp.
97
+
98
+ ### Release Process
99
+
100
+ 1. Tag with version: `git tag v0.1.x`
101
+ 2. Push tag: `git push origin v0.1.x`
102
+ 3. CI auto-builds wheels for Linux (x86_64, aarch64, armv7), macOS (Intel/ARM), Windows
103
+ 4. Maturin publishes to PyPI
104
+
105
+ ### CI/CD
106
+
107
+ **Workflows** (`.github/workflows/`):
108
+ - `CI.yml`: Main CI pipeline - runs tests, linting, type checking on pull requests
109
+ - `mkdocs.yml`: Documentation deployment to GitHub Pages
110
+ - Platform matrix testing: Linux (multiple architectures), macOS (Intel/ARM), Windows
111
+ - Automated wheel building via `maturin` for all platforms
112
+ - Test suite runs against live httpbin.org endpoints
113
+
114
+ ### Debugging
115
+
116
+ **Rust Debugging**:
117
+ ```bash
118
+ # Enable Rust logging
119
+ RUST_LOG=debug uv run pytest tests/test_client.py -v
120
+
121
+ # Build with debug symbols (slower but debuggable)
122
+ uv run maturin develop --release=false
123
+ ```
124
+
125
+ **Python Debugging**:
126
+ ```python
127
+ # Standard Python debugging works
128
+ import httpr
129
+ client = httpr.Client()
130
+ breakpoint() # Python debugger
131
+ response = client.get("https://httpbin.org/get")
132
+ ```
133
+
134
+ **Common Issues**:
135
+ - "Module not found" after Rust changes → Run `uv run maturin develop`
136
+ - SSL errors → Check `HTTPR_CA_BUNDLE` environment variable
137
+ - Test failures → httpbin.org may be down; tests use `@retry()` decorator
138
+ - Import errors → Ensure virtual environment is activated
139
+
140
+ ## Project-Specific Conventions
141
+
142
+ ### Python-Rust Interface Patterns
143
+
144
+ - **All Python params converted to strings**: `Client.request()` converts param values to strings before passing to Rust
145
+ - **Method validation in Python**: HTTP method validation happens in Python wrapper, not Rust
146
+ - **Optional chaining**: Client stores default `auth`, `params`, `headers` - per-request values override via `.or()`
147
+
148
+ ### Type System
149
+
150
+ - Rust uses `IndexMap<String, String, RandomState>` (foldhash) for dicts
151
+ - Python type hints via `TypedDict` (`RequestParams`, `ClientRequestParams`)
152
+ - Use `Unpack` for `**kwargs` typing (supports Python 3.9+ via typing_extensions)
153
+
154
+ ### Error Handling
155
+
156
+ - Rust errors bubble up via `anyhow::Result` → PyO3 exceptions
157
+ - No fine-grained error types yet (all raise generic `Exception`)
158
+ - Tests use `@retry()` decorator to handle flaky httpbin.org responses
159
+
160
+ ### Testing Patterns
161
+
162
+ ```python
163
+ # Sync tests
164
+ def test_client_feature():
165
+ client = httpr.Client(auth=("user", "pass"))
166
+ response = client.get("https://httpbin.org/anything")
167
+ assert response.status_code == 200
168
+
169
+ # Async tests
170
+ @pytest.mark.asyncio
171
+ async def test_asyncclient_feature():
172
+ async with httpr.AsyncClient() as client:
173
+ response = await client.get("https://httpbin.org/anything")
174
+ assert response.status_code == 200
175
+ ```
176
+
177
+ Tests depend on httpbin.org for validation - use `@retry()` wrapper for flaky network requests.
178
+
179
+ ## Critical Implementation Details
180
+
181
+ ### SSL/TLS Configuration
182
+
183
+ - CA certs loaded via `HTTPR_CA_BUNDLE` env var (set before client init)
184
+ - `ca_cert_file` param internally sets `HTTPR_CA_BUNDLE`
185
+ - mTLS via `client_pem` parameter (PEM format identity file)
186
+ - `verify=False` enables `danger_accept_invalid_certs()`
187
+
188
+ ### Headers Behavior
189
+
190
+ - Headers are **lowercased** internally (HTTP/2 requirement)
191
+ - `client.headers` getter excludes `Cookie` header
192
+ - `client.cookies` getter/setter extracts from `Cookie` header
193
+ - Use `CaseInsensitiveHeaderMap` in Rust for lookups
194
+
195
+ ### Request Body Handling
196
+
197
+ - Mutually exclusive: `content` (bytes), `data` (form), `json` (JSON), `files` (multipart)
198
+ - `data` and `json` use `pythonize::depythonize()` for Python → Rust serde_json::Value
199
+ - `files` dict maps field names to file paths (auto-reads and sends as multipart)
200
+
201
+ ### Proxy Support
202
+
203
+ - Set via `proxy` param or `HTTPR_PROXY` env var
204
+ - Changing `client.proxy` rebuilds entire reqwest client (expensive operation)
205
+
206
+ ## Common Tasks
207
+
208
+ ### Adding New Request Parameters
209
+
210
+ 1. Add to `RequestParams` TypedDict in `httpr.pyi`
211
+ 2. Add parameter to `RClient.request()` signature in `src/lib.rs`
212
+ 3. Handle parameter in request builder chain
213
+ 4. Update docstrings in both Python and Rust
214
+
215
+ ### Modifying Response Properties
216
+
217
+ 1. Add Rust implementation in `src/response.rs`
218
+ 2. Add `#[pymethods]` getter/method
219
+ 3. Add type hint to `Response` class in `httpr.pyi`
220
+
221
+ ### Performance Optimization
222
+
223
+ - Check `Cargo.toml` for reqwest feature flags (enable compression codecs, http2, etc.)
224
+ - Profile with `cargo flamegraph` on Rust side
225
+ - Benchmark against other clients using `benchmark/benchmark.py`
226
+
227
+ ## What NOT to Do
228
+
229
+ - Don't add Python dependencies (defeats "zero dependencies" goal)
230
+ - Don't use native async Rust in request path (breaks single-threaded runtime model)
231
+ - Don't modify headers case-sensitivity behavior (HTTP/2 spec requirement)
232
+ - Don't remove reqwest compression features (users expect automatic gzip/brotli)
233
+ - Don't skip `maturin develop` after Rust changes (Python won't see updates)
234
+
235
+ ## Resources
236
+
237
+ - [PyO3 User Guide](https://pyo3.rs/)
238
+ - [reqwest docs](https://docs.rs/reqwest/)
239
+ - [Maturin Guide](https://www.maturin.rs/)
240
+ - Benchmark results: `benchmark.jpg` in repo root
@@ -0,0 +1,386 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "*"
7
+ pull_request:
8
+ schedule:
9
+ # Run weekly on Sundays at midnight UTC
10
+ - cron: "0 0 * * 0"
11
+ workflow_dispatch:
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ jobs:
17
+ # Fast test job - runs on PRs with Python version matrix
18
+ test:
19
+ runs-on: ubuntu-22.04
20
+ if: github.event_name == 'pull_request'
21
+ strategy:
22
+ matrix:
23
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+ - uses: astral-sh/setup-uv@v7
27
+ with:
28
+ python-version: ${{ matrix.python-version }}
29
+ enable-cache: false
30
+ - name: Build and test
31
+ run: |
32
+ uv sync --extra dev
33
+ uv run maturin develop
34
+ uv run pytest
35
+
36
+ # Docs build test - runs on PRs to catch broken docs early
37
+ docs:
38
+ runs-on: ubuntu-latest
39
+ if: github.event_name == 'pull_request'
40
+ steps:
41
+ - uses: actions/checkout@v4
42
+ - uses: astral-sh/setup-uv@v7
43
+ with:
44
+ python-version: "3.12"
45
+ enable-cache: false
46
+ - name: Build docs
47
+ run: |
48
+ uv sync --extra docs
49
+ uv run mkdocs build --strict
50
+
51
+ # Build jobs - run on tags, weekly schedule, or manual dispatch
52
+ linux:
53
+ runs-on: ${{ matrix.platform.runner }}
54
+ if: github.event_name != 'pull_request'
55
+ strategy:
56
+ matrix:
57
+ platform:
58
+ - runner: ubuntu-22.04
59
+ target: x86_64
60
+ - runner: ubuntu-22.04
61
+ target: x86
62
+ - runner: ubuntu-24.04-arm
63
+ target: aarch64
64
+ - runner: ubuntu-22.04
65
+ target: armv7
66
+ # - runner: ubuntu-22.04
67
+ # target: s390x
68
+ # - runner: ubuntu-22.04
69
+ # target: ppc64le
70
+ steps:
71
+ - uses: actions/checkout@v4
72
+ - uses: astral-sh/setup-uv@v7
73
+ with:
74
+ python-version: "3.12"
75
+ enable-cache: false
76
+ # Only modify pyproject.toml temporarily for this job if running from a tag
77
+ - name: Update version in pyproject.toml
78
+ if: ${{ startsWith(github.ref, 'refs/tags/') }}
79
+ run: |
80
+ TAG_VERSION=${GITHUB_REF#refs/tags/v}
81
+ uvx --with toml python .github/workflows/set_version.py $TAG_VERSION
82
+ - name: Build wheels
83
+ uses: PyO3/maturin-action@v1
84
+ with:
85
+ target: ${{ matrix.platform.target }}
86
+ args: --release --out dist
87
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
88
+ manylinux: auto
89
+ - name: Build free-threaded wheels
90
+ uses: PyO3/maturin-action@v1
91
+ with:
92
+ target: ${{ matrix.platform.target }}
93
+ args: --release --out dist -i python3.13t
94
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
95
+ manylinux: auto
96
+ - name: Upload wheels
97
+ uses: actions/upload-artifact@v4
98
+ with:
99
+ name: wheels-linux-${{ matrix.platform.target }}
100
+ path: dist
101
+ - name: pytest
102
+ if: ${{ matrix.platform.target == 'x86_64' || matrix.platform.target == 'aarch64' }}
103
+ shell: bash
104
+ env:
105
+ RUSTC_WRAPPER: ""
106
+ run: |
107
+ set -e
108
+ uv sync --extra dev --find-links dist --reinstall
109
+ uv run pytest
110
+ - name: pytest
111
+ if: ${{ matrix.platform.target == 'armv7' }}
112
+ uses: uraimo/run-on-arch-action@v2
113
+ with:
114
+ arch: ${{ matrix.platform.target }}
115
+ distro: ubuntu22.04
116
+ githubToken: ${{ github.token }}
117
+ install: |
118
+ apt-get update
119
+ apt-get install -y --no-install-recommends python3 python3-pip python3-dev build-essential libffi-dev
120
+ pip3 install -U pip
121
+ run: |
122
+ set -e
123
+ pip3 install httpr --find-links dist --no-index --force-reinstall
124
+ pip3 install certifi pytest pytest-asyncio pytest-httpbin mypy ruff maturin trustme
125
+ pytest
126
+
127
+ musllinux:
128
+ runs-on: ${{ matrix.platform.runner }}
129
+ if: github.event_name != 'pull_request'
130
+ strategy:
131
+ matrix:
132
+ platform:
133
+ - runner: ubuntu-22.04
134
+ target: x86_64
135
+ - runner: ubuntu-22.04
136
+ target: x86
137
+ - runner: ubuntu-24.04-arm
138
+ target: aarch64
139
+ # - runner: ubuntu-22.04
140
+ # target: armv7
141
+ steps:
142
+ - uses: actions/checkout@v4
143
+ - uses: astral-sh/setup-uv@v7
144
+ with:
145
+ python-version: "3.12"
146
+ enable-cache: false
147
+ # Only modify pyproject.toml temporarily for this job if running from a tag
148
+ - name: Update version in pyproject.toml
149
+ if: ${{ startsWith(github.ref, 'refs/tags/') }}
150
+ run: |
151
+ TAG_VERSION=${GITHUB_REF#refs/tags/v}
152
+ uvx --with toml python .github/workflows/set_version.py $TAG_VERSION
153
+ - name: Build wheels
154
+ uses: PyO3/maturin-action@v1
155
+ with:
156
+ target: ${{ matrix.platform.target }}
157
+ args: --release --out dist
158
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
159
+ manylinux: musllinux_1_2
160
+ - name: Build free-threaded wheels
161
+ uses: PyO3/maturin-action@v1
162
+ with:
163
+ target: ${{ matrix.platform.target }}
164
+ args: --release --out dist -i python3.13t
165
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
166
+ manylinux: musllinux_1_2
167
+ - name: Upload wheels
168
+ uses: actions/upload-artifact@v4
169
+ with:
170
+ name: wheels-musllinux-${{ matrix.platform.target }}
171
+ path: dist
172
+ - name: pytest
173
+ if: ${{ matrix.platform.target == 'x86_64' || matrix.platform.target == 'aarch64' }}
174
+ uses: addnab/docker-run-action@v3
175
+ with:
176
+ image: alpine:latest
177
+ options: -v ${{ github.workspace }}:/io -w /io
178
+ run: |
179
+ set -e
180
+ apk add py3-pip py3-virtualenv python3-dev build-base libffi-dev
181
+ python3 -m virtualenv .venv
182
+ source .venv/bin/activate
183
+ pip install httpr --find-links dist --no-index --force-reinstall
184
+ pip install certifi pytest pytest-asyncio pytest-httpbin mypy ruff maturin trustme
185
+ pytest
186
+
187
+ windows:
188
+ runs-on: ${{ matrix.platform.runner }}
189
+ if: github.event_name != 'pull_request'
190
+ strategy:
191
+ matrix:
192
+ platform:
193
+ - runner: windows-latest
194
+ target: x64
195
+ - runner: windows-latest
196
+ target: x86
197
+ steps:
198
+ - uses: actions/checkout@v4
199
+ - uses: actions/setup-python@v5
200
+ with:
201
+ python-version: "3.12"
202
+ architecture: ${{ matrix.platform.target }}
203
+ - uses: astral-sh/setup-uv@v7
204
+ with:
205
+ python-version: "3.12"
206
+ enable-cache: false
207
+ # Only modify pyproject.toml temporarily for this job if running from a tag
208
+ - name: Update version in pyproject.toml
209
+ if: ${{ startsWith(github.ref, 'refs/tags/') }}
210
+ shell: pwsh
211
+ run: |
212
+ $TAG_VERSION = $env:GITHUB_REF -replace 'refs/tags/v', ''
213
+ uvx --with toml python .github/workflows/set_version.py $TAG_VERSION
214
+ - name: Build wheels
215
+ uses: PyO3/maturin-action@v1
216
+ with:
217
+ target: ${{ matrix.platform.target }}
218
+ args: --release --out dist -i python3.12
219
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
220
+ - uses: actions/setup-python@v5
221
+ with:
222
+ python-version: 3.13t
223
+ architecture: ${{ matrix.platform.target }}
224
+ - name: Build free-threaded wheels
225
+ uses: PyO3/maturin-action@v1
226
+ with:
227
+ target: ${{ matrix.platform.target }}
228
+ args: --release --out dist -i python3.13t
229
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
230
+ - name: Upload wheels
231
+ uses: actions/upload-artifact@v4
232
+ with:
233
+ name: wheels-windows-${{ matrix.platform.target }}
234
+ path: dist
235
+ - name: pytest
236
+ shell: bash
237
+ env:
238
+ RUSTC_WRAPPER: ""
239
+ run: |
240
+ set -e
241
+ uv sync --extra dev --find-links dist --reinstall
242
+ uv run pytest
243
+
244
+ macos:
245
+ runs-on: ${{ matrix.platform.runner }}
246
+ if: github.event_name != 'pull_request'
247
+ strategy:
248
+ matrix:
249
+ platform:
250
+ - runner: macos-15
251
+ target: x86_64
252
+ - runner: macos-14
253
+ target: aarch64
254
+ steps:
255
+ - uses: actions/checkout@v4
256
+ - uses: astral-sh/setup-uv@v7
257
+ with:
258
+ python-version: "3.12"
259
+ enable-cache: false
260
+ # Only modify pyproject.toml temporarily for this job if running from a tag
261
+ - name: Update version in pyproject.toml
262
+ if: ${{ startsWith(github.ref, 'refs/tags/') }}
263
+ run: |
264
+ TAG_VERSION=${GITHUB_REF#refs/tags/v}
265
+ uvx --with toml python .github/workflows/set_version.py $TAG_VERSION
266
+ - name: Build wheels
267
+ uses: PyO3/maturin-action@v1
268
+ with:
269
+ target: ${{ matrix.platform.target }}
270
+ args: --release --out dist
271
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
272
+ - name: Build free-threaded wheels
273
+ uses: PyO3/maturin-action@v1
274
+ with:
275
+ target: ${{ matrix.platform.target }}
276
+ args: --release --out dist -i python3.13t
277
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
278
+ - name: Upload wheels
279
+ uses: actions/upload-artifact@v4
280
+ with:
281
+ name: wheels-macos-${{ matrix.platform.target }}
282
+ path: dist
283
+ - name: pytest
284
+ env:
285
+ RUSTC_WRAPPER: ""
286
+ run: |
287
+ set -e
288
+ uv sync --extra dev --find-links dist --reinstall
289
+ uv run pytest
290
+
291
+ sdist:
292
+ runs-on: ubuntu-latest
293
+ if: github.event_name != 'pull_request'
294
+ steps:
295
+ - uses: actions/checkout@v4
296
+ - uses: astral-sh/setup-uv@v7
297
+ with:
298
+ python-version: "3.12"
299
+ enable-cache: false
300
+ # Only modify pyproject.toml temporarily for this job if running from a tag
301
+ - name: Update version in pyproject.toml
302
+ if: ${{ startsWith(github.ref, 'refs/tags/') }}
303
+ run: |
304
+ TAG_VERSION=${GITHUB_REF#refs/tags/v}
305
+ uvx --with toml python .github/workflows/set_version.py $TAG_VERSION
306
+ - name: Build sdist
307
+ uses: PyO3/maturin-action@v1
308
+ with:
309
+ command: sdist
310
+ args: --out dist
311
+ - name: Upload sdist
312
+ uses: actions/upload-artifact@v4
313
+ with:
314
+ name: wheels-sdist
315
+ path: dist
316
+
317
+ release:
318
+ name: Release
319
+ environment: pypi
320
+ runs-on: ubuntu-latest
321
+ if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }}
322
+ needs: [linux, musllinux, windows, macos, sdist]
323
+ permissions:
324
+ # Use to sign the release artifacts
325
+ id-token: write
326
+ # Used to upload release artifacts
327
+ contents: write
328
+ # Used to generate artifact attestation
329
+ attestations: write
330
+ steps:
331
+ - uses: actions/download-artifact@v4
332
+ - name: Generate artifact attestation
333
+ uses: actions/attest-build-provenance@v2
334
+ with:
335
+ subject-path: "wheels-*/*"
336
+ - name: Publish to PyPI
337
+ if: ${{ startsWith(github.ref, 'refs/tags/') }}
338
+ uses: PyO3/maturin-action@v1
339
+ env:
340
+ MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
341
+ with:
342
+ command: upload
343
+ args: --non-interactive --skip-existing wheels-*/*
344
+
345
+ benchmark:
346
+ permissions:
347
+ contents: write
348
+ runs-on: ubuntu-latest
349
+ needs: [linux]
350
+ steps:
351
+ - uses: actions/checkout@v4
352
+ with:
353
+ fetch-depth: 0
354
+ - uses: astral-sh/setup-uv@v7
355
+ with:
356
+ python-version: "3.12"
357
+ enable-cache: false
358
+ - name: Download wheels
359
+ uses: actions/download-artifact@v4
360
+ with:
361
+ name: wheels-linux-x86_64
362
+ - name: Install dependencies
363
+ run: uv sync --package benchmark --find-links ./ --reinstall
364
+ - name: Start Uvicorn server
365
+ run: |
366
+ uv run --package benchmark uvicorn benchmark.server:app --host 0.0.0.0 --port 8000 &
367
+ sleep 10
368
+ - name: Run benchmark
369
+ run: uv run --package benchmark python benchmark/benchmark.py
370
+ - name: Generate and commit benchmark image
371
+ if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'schedule'
372
+ run: |
373
+ uv run --package benchmark python benchmark/generate_image.py
374
+ git config --global user.name 'github-actions[bot]'
375
+ git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com'
376
+ git add *.jpg
377
+ if ! git diff --quiet --cached; then
378
+ if [ "${{ github.event_name }}" = "schedule" ]; then
379
+ git commit -m "Update benchmark image (weekly)"
380
+ else
381
+ git commit -m "Update benchmark image for ${{ github.ref_name }}"
382
+ fi
383
+ git push
384
+ else
385
+ echo "No changes to commit"
386
+ fi