pyureq 0.1.7__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.
@@ -0,0 +1,102 @@
1
+ name: Benchmarks
2
+
3
+ on:
4
+ push:
5
+ branches: ["**"]
6
+ workflow_dispatch:
7
+ inputs:
8
+ iterations:
9
+ description: "Iterations per scenario (bench.py and bench_gil.py)"
10
+ default: "200"
11
+ required: false
12
+ thread_requests:
13
+ description: "Requests per thread (bench_threads.py)"
14
+ default: "10"
15
+ required: false
16
+ stress_threads:
17
+ description: "Thread sweep for bench_stress.py (comma-separated)"
18
+ default: "50,100,200"
19
+ required: false
20
+
21
+ jobs:
22
+ benchmark:
23
+ name: Benchmark (${{ matrix.os }})
24
+ runs-on: ${{ matrix.os }}
25
+ strategy:
26
+ fail-fast: false
27
+ matrix:
28
+ os: [ubuntu-latest, windows-latest]
29
+ defaults:
30
+ run:
31
+ shell: bash
32
+ # Force UTF-8 I/O on all platforms so Unicode box-drawing characters in
33
+ # benchmark output don't crash on Windows' default cp1252 console encoding.
34
+ env:
35
+ PYTHONUTF8: "1"
36
+
37
+ steps:
38
+ - uses: actions/checkout@v4
39
+
40
+ - name: Set up Python
41
+ uses: actions/setup-python@v5
42
+ with:
43
+ python-version: "3.12"
44
+
45
+ - name: Install Rust toolchain
46
+ uses: dtolnay/rust-toolchain@stable
47
+
48
+ - name: Cache Rust build artifacts
49
+ uses: Swatinem/rust-cache@v2
50
+
51
+ - name: Build pyureq wheel
52
+ uses: PyO3/maturin-action@v1
53
+ with:
54
+ command: build
55
+ args: --release --out dist
56
+
57
+ - name: Install pyureq
58
+ run: pip install dist/*.whl
59
+
60
+ - name: Install benchmark dependencies
61
+ run: pip install requests httpx curl_cffi flask
62
+
63
+ - name: Create results directory
64
+ run: mkdir -p results
65
+
66
+ - name: Run latency benchmark (bench.py)
67
+ run: |
68
+ python benchmarks/bench.py \
69
+ --iterations ${{ github.event.inputs.iterations || '50' }} \
70
+ --warmup 3 \
71
+ --json results/bench_latency.json \
72
+ | tee results/bench_latency.txt
73
+
74
+ - name: Run GIL benchmark (bench_gil.py)
75
+ run: |
76
+ python benchmarks/bench_gil.py \
77
+ --iterations ${{ github.event.inputs.iterations || '30' }} \
78
+ | tee results/bench_gil.txt
79
+
80
+ - name: Run concurrency benchmark (bench_threads.py)
81
+ run: |
82
+ python benchmarks/bench_threads.py \
83
+ --threads 100 \
84
+ --requests ${{ github.event.inputs.thread_requests || '3' }} \
85
+ | tee results/bench_threads.txt
86
+
87
+ - name: Run stress test (bench_stress.py)
88
+ run: |
89
+ python benchmarks/bench_stress.py \
90
+ --duration 5 \
91
+ --warmup 1 \
92
+ --threads ${{ github.event.inputs.stress_threads || '50,100,200' }} \
93
+ --json results/bench_stress.json \
94
+ | tee results/bench_stress.txt
95
+
96
+ - name: Upload benchmark results
97
+ if: always()
98
+ uses: actions/upload-artifact@v4
99
+ with:
100
+ name: benchmark-results-${{ matrix.os }}-${{ github.sha }}
101
+ path: results/
102
+ retention-days: 90
@@ -0,0 +1,185 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: ["**"]
6
+ tags: ["v*"]
7
+ pull_request:
8
+ branches: ["**"]
9
+
10
+ jobs:
11
+ lint:
12
+ name: Lint
13
+ runs-on: ubuntu-latest
14
+
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - name: Set up Python
19
+ uses: actions/setup-python@v5
20
+ with:
21
+ python-version: "3.12"
22
+
23
+ - name: Install ruff
24
+ run: pip install ruff
25
+
26
+ - name: Lint Python (ruff)
27
+ run: ruff check python/
28
+
29
+ - name: Install Rust toolchain (with clippy + rustfmt)
30
+ shell: bash
31
+ run: |
32
+ for attempt in 1 2 3 4; do
33
+ rustup toolchain install stable --profile minimal --no-self-update \
34
+ --component clippy --component rustfmt \
35
+ && rustup default stable \
36
+ && break
37
+ echo "Attempt $attempt failed, retrying in $((attempt * 15))s..."
38
+ sleep $((attempt * 15))
39
+ done
40
+
41
+ - name: Cache Rust build artifacts
42
+ uses: Swatinem/rust-cache@v2
43
+
44
+ - name: Lint Rust (clippy)
45
+ run: cargo clippy -- -D warnings
46
+
47
+ - name: Check Rust formatting
48
+ run: cargo fmt --check
49
+
50
+ test:
51
+ name: Test (Python ${{ matrix.python-version }})
52
+ runs-on: ubuntu-latest
53
+ strategy:
54
+ matrix:
55
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
56
+
57
+ steps:
58
+ - uses: actions/checkout@v4
59
+
60
+ - name: Set up Python ${{ matrix.python-version }}
61
+ uses: actions/setup-python@v5
62
+ with:
63
+ python-version: ${{ matrix.python-version }}
64
+
65
+ - name: Install Rust toolchain
66
+ shell: bash
67
+ run: |
68
+ for attempt in 1 2 3 4; do
69
+ rustup toolchain install stable --profile minimal --no-self-update \
70
+ && rustup default stable \
71
+ && break
72
+ echo "Attempt $attempt failed, retrying in $((attempt * 15))s..."
73
+ sleep $((attempt * 15))
74
+ done
75
+
76
+ - name: Cache Rust build artifacts
77
+ uses: Swatinem/rust-cache@v2
78
+
79
+ - name: Install maturin
80
+ run: pip install maturin
81
+
82
+ - name: Build and install wheel
83
+ run: |
84
+ maturin build --release
85
+ pip install target/wheels/*.whl
86
+
87
+ - name: Install test dependencies
88
+ run: pip install pytest pytest-cov flask
89
+
90
+ - name: Run tests with coverage
91
+ run: pytest tests/ -v --cov=pyureq --cov-report=xml --cov-report=term-missing
92
+
93
+ - name: Upload coverage report
94
+ uses: actions/upload-artifact@v4
95
+ with:
96
+ name: coverage-py${{ matrix.python-version }}
97
+ path: coverage.xml
98
+
99
+ build-wheels:
100
+ name: Build wheels (${{ matrix.os }})
101
+ runs-on: ${{ matrix.os }}
102
+ strategy:
103
+ matrix:
104
+ os: [ubuntu-latest, macos-latest, windows-latest]
105
+
106
+ steps:
107
+ - uses: actions/checkout@v4
108
+
109
+ - name: Install Rust toolchain
110
+ shell: bash
111
+ run: |
112
+ for attempt in 1 2 3 4; do
113
+ rustup toolchain install stable --profile minimal --no-self-update \
114
+ && rustup default stable \
115
+ && break
116
+ echo "Attempt $attempt failed, retrying in $((attempt * 15))s..."
117
+ sleep $((attempt * 15))
118
+ done
119
+
120
+ - name: Cache Rust build artifacts
121
+ uses: Swatinem/rust-cache@v2
122
+
123
+ - name: Build wheels
124
+ uses: PyO3/maturin-action@v1
125
+ with:
126
+ command: build
127
+ # abi3-py39 + generate-import-lib: one cp39-abi3 wheel per platform,
128
+ # no Python interpreter required (works on Windows without python3.9).
129
+ args: --release --out dist
130
+
131
+ - name: Upload wheels
132
+ uses: actions/upload-artifact@v4
133
+ with:
134
+ name: wheels-${{ matrix.os }}
135
+ path: dist/
136
+
137
+ release:
138
+ name: Publish to PyPI + GitHub Release
139
+ runs-on: ubuntu-latest
140
+ needs: [lint, test, build-wheels]
141
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
142
+ environment: pypi
143
+ permissions:
144
+ id-token: write
145
+ contents: write # required for GitHub Release creation
146
+
147
+ steps:
148
+ - uses: actions/checkout@v4
149
+
150
+ - uses: actions/download-artifact@v4
151
+ with:
152
+ pattern: wheels-*
153
+ merge-multiple: true
154
+ path: dist/
155
+
156
+ - name: Publish to PyPI
157
+ uses: pypa/gh-action-pypi-publish@release/v1
158
+
159
+ - name: Create GitHub Release
160
+ uses: softprops/action-gh-release@v2
161
+ with:
162
+ files: dist/*
163
+ generate_release_notes: true
164
+ body: |
165
+ ## Installation
166
+
167
+ ```
168
+ pip install pyureq==${{ github.ref_name }}
169
+ ```
170
+
171
+ Full changelog: https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md
172
+
173
+ - name: Update repository About description
174
+ env:
175
+ GH_TOKEN: ${{ github.token }}
176
+ run: |
177
+ gh api --method PATCH /repos/${{ github.repository }} \
178
+ -f description="A requests-compatible Python HTTP library powered by Rust's ureq" \
179
+ -f homepage="https://pypi.org/project/pyureq/" \
180
+ -f topics[]="python" \
181
+ -f topics[]="rust" \
182
+ -f topics[]="http" \
183
+ -f topics[]="requests" \
184
+ -f topics[]="pyo3" \
185
+ -f topics[]="ureq" || true
@@ -0,0 +1,44 @@
1
+ # Generated by Cargo
2
+ # will have compiled files and executables
3
+ debug
4
+ target
5
+
6
+ # These are backup files generated by rustfmt
7
+ **/*.rs.bk
8
+
9
+ # MSVC Windows builds of rustc generate these, which store debugging information
10
+ *.pdb
11
+
12
+ # Generated by cargo mutants
13
+ # Contains mutation testing data
14
+ **/mutants.out*/
15
+
16
+ # Python
17
+ __pycache__/
18
+ *.py[cod]
19
+ *.egg-info/
20
+ dist/
21
+ *.whl
22
+
23
+ # RustRover
24
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
25
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
26
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
27
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
28
+ #.idea/
29
+
30
+ # Secrets / credentials
31
+ .env
32
+ .env.*
33
+ *.key
34
+ *.pem
35
+ *.p12
36
+ *.pfx
37
+
38
+ # Benchmark output
39
+ results/
40
+
41
+ # Coverage reports
42
+ coverage.xml
43
+ .coverage
44
+ htmlcov/
@@ -0,0 +1,204 @@
1
+ # Changelog
2
+
3
+ All notable changes to pyureq are documented here.
4
+ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
+
6
+ ---
7
+
8
+ ## [0.1.7] — 2026-08-06
9
+
10
+ ### Fixed
11
+ - **Headers you set explicitly were sent twice.** When a request supplied a
12
+ header that pyureq also derives on its own, both copies went out on the
13
+ wire:
14
+
15
+ | you pass | pyureq also derives | result |
16
+ |---|---|---|
17
+ | `headers={"Content-Type": ...}` + `json=` / `data=` | `Content-Type` | sent 2× |
18
+ | `headers={"Cookie": ...}` + `cookies=` | `Cookie` | sent 2× |
19
+ | `headers={"Authorization": ...}` + `auth=` | `Authorization` | sent 2× |
20
+
21
+ This is a regression introduced in 0.1.6 by the ureq 2 → ureq 3 migration.
22
+ ureq 2's `Request::set` **replaced** an existing header; ureq 3 builds
23
+ through `http::request::Builder::header`, which calls `try_append` and
24
+ therefore **appends**. The three call sites were migrated verbatim, so each
25
+ derived header stacked on top of the caller's.
26
+
27
+ Most servers tolerate a repeated header, which is why this went unnoticed —
28
+ it only breaks against strict gateways. Crypto.com's exchange API answers a
29
+ duplicated `Content-Type` with `{"code": 50001, "message": "ERR_INTERNAL"}`,
30
+ a generic error that points nowhere near the real cause; the same request
31
+ through `requests` succeeded, making it look like a server-side or
32
+ authentication problem.
33
+
34
+ pyureq now follows `requests`' rule: a derived header is only applied when
35
+ the caller did not supply one, so an explicit header always wins. Matching
36
+ is case-insensitive (`content-type` and `Content-Type` are the same header
37
+ per RFC 9110). Deriving still works unchanged when you pass no header of
38
+ your own.
39
+
40
+ ### Testing
41
+ - New `tests/test_duplicate_headers.py` (12 tests) asserts on the **raw
42
+ request bytes** read off a socket. The existing WSGI/Flask test server
43
+ collapses repeated headers into a single mapping entry, so it structurally
44
+ could not observe this bug. Verified by rebuilding with the fix reverted:
45
+ 8 of the 12 fail, while the four "derived headers still work" cases keep
46
+ passing — the suite targets the defect rather than pinning current
47
+ behaviour. Full suite: 135 passing.
48
+
49
+ ---
50
+
51
+ ## [0.1.6] — 2026-08-01
52
+
53
+ ### Fixed
54
+ - **Timeouts were reported as `ConnectionError` instead of `Timeout`.** The
55
+ transport error was classified by searching the OS error message for
56
+ `"timed out"`/`"deadline"`. Windows reports `WSAETIMEDOUT` (os error 10060)
57
+ with a message containing neither phrase, so every timeout on Windows raised
58
+ `ConnectionError`. Errors are now matched on ureq's typed `Error::Timeout`
59
+ variant, so classification is platform-independent.
60
+ - **`Session` did not pool connections.** A new agent — and therefore a new
61
+ connection pool — was constructed for *every* request, so five sequential
62
+ `Session.get()` calls opened five TCP connections despite the documented
63
+ keep-alive behaviour. `Session` now holds one long-lived agent, so repeated
64
+ requests to the same host reuse a single connection.
65
+ - **`deflate` responses came back still compressed.** The default
66
+ `Accept-Encoding` advertised `gzip, deflate`, but the Rust core only decodes
67
+ gzip and brotli. A server honouring `deflate` returned raw zlib bytes, and
68
+ `.text` / `.json()` failed with `UnicodeDecodeError`. `deflate` is no longer
69
+ advertised. (Same class of bug as the brotli fix in 0.1.3.)
70
+ - **Per-request `verify=` was ignored on the `Session` path.** `verify` was
71
+ never forwarded to the Rust client, so `session.get(url, verify=False)`
72
+ still performed full certificate verification. It is now honoured, and a
73
+ per-request `verify` gets its own connection pool: ureq keys pooled
74
+ connections on `(scheme, authority, proxy)` without regard to TLS config, so
75
+ sharing one pool would have let a `verify=False` connection be reused to
76
+ serve a later `verify=True` request — silently skipping validation.
77
+
78
+ ### Changed
79
+ - Upgraded **ureq 2.12 → 3.3** and **pyo3 0.23 → 0.29**.
80
+ - Dropped the unused `serde_json` dependency and ureq's default features, which
81
+ were compiling a second TLS stack (rustls + ring + webpki) that the crate
82
+ never used. TLS is native-tls only, so the OS trust store stays authoritative
83
+ — preserving the 0.1.5 `UnknownIssuer` fix.
84
+ - Response bodies are no longer capped at ureq 3's default 10 MB read limit.
85
+ - The extension is now declared free-threading safe (`gil_used = false`); it
86
+ holds no Python state while performing I/O.
87
+ - Building from source now requires Rust 1.85 or newer (a ureq 3 requirement).
88
+ Installing from a published wheel is unaffected.
89
+
90
+ ### Added
91
+ - Regression tests covering all four fixes above, including a hermetic
92
+ self-signed HTTPS fixture for the `verify` cases — no network access needed.
93
+ - Type stubs now match the real extension signatures; `RustClient.__init__` was
94
+ missing `timeout`/`no_proxy_all` and `RustClient.request` was missing `proxy`.
95
+
96
+ [0.1.7]: https://github.com/vuongphu/pyureq/releases/tag/v0.1.7
97
+ [0.1.6]: https://github.com/vuongphu/pyureq/releases/tag/v0.1.6
98
+
99
+ ---
100
+
101
+ ## [0.1.5] — 2026-04-05
102
+
103
+ ### Fixed
104
+ - **TLS `UnknownIssuer` error on some HTTPS domains** — The `native-tls`
105
+ backend (via `schannel 0.1.28` on Windows) changed how it initialises the
106
+ TLS connector, causing certificate chain validation to fail for domains whose
107
+ intermediate CA had not previously been cached by Windows. The fix is to
108
+ always construct an explicit `TlsConnector` (even when `verify=True`) so that
109
+ the OS trust store is properly loaded on every platform (Windows CryptoAPI,
110
+ macOS Security framework, Linux OpenSSL).
111
+
112
+ ### Added
113
+ - `verify` now accepts a **path string** to a PEM CA-bundle file, matching
114
+ `requests` semantics:
115
+ - `verify=True` (default) — system / OS CA store
116
+ - `verify=False` — skip all certificate verification
117
+ - `verify="/path/to/ca-bundle.pem"` — use the supplied PEM file
118
+
119
+ [0.1.5]: https://github.com/vuongphu/pyureq/releases/tag/v0.1.5
120
+
121
+ ---
122
+
123
+ ## [0.1.4] — 2026-02-28
124
+
125
+ ### Added
126
+ - Proxy support via the `proxies` parameter, matching the `requests` API:
127
+ `proxies={"http": "http://host:port", "https": "http://user:pass@host:port"}`.
128
+ HTTP, HTTPS, SOCKS4, SOCKS4A, SOCKS5, and SOCKS5H proxy URLs are all
129
+ accepted. `Session.proxies` dict is also supported for session-wide defaults.
130
+
131
+ [0.1.4]: https://github.com/vuongphu/pyureq/releases/tag/v0.1.4
132
+
133
+ ---
134
+
135
+ ## [0.1.3] — 2026-02-28
136
+
137
+ ### Fixed
138
+ - Remove `br` from the default `Accept-Encoding` session header. pyureq was
139
+ advertising brotli support it could not fulfil, causing servers to return
140
+ brotli-compressed bodies that the Rust backend cannot decompress. The raw
141
+ bytes then raised `UnicodeDecodeError` when callers accessed `.text` or
142
+ `.json()`.
143
+
144
+ [0.1.3]: https://github.com/vuongphu/pyureq/releases/tag/v0.1.3
145
+
146
+ ---
147
+
148
+ ## [0.1.2] — 2026-02-28
149
+
150
+ ### Fixed
151
+ - Bytes header values are now decoded with latin-1 (matching `requests`
152
+ behaviour) instead of `str()`, which produced `"b'...'"` and caused HMAC
153
+ signature verification failures (`40009 sign signature error`) on APIs such
154
+ as Bitget.
155
+
156
+ ### Removed
157
+ - `SessionPool` / `pool.py` — removed from the public API.
158
+
159
+ [0.1.2]: https://github.com/vuongphu/pyureq/releases/tag/v0.1.2
160
+
161
+ ---
162
+
163
+ ## [0.1.0] — 2026-02-28
164
+
165
+ ### Added
166
+
167
+ - Initial release of **pyureq** — a `requests`-compatible Python HTTP library
168
+ backed by Rust's [ureq](https://github.com/algesten/ureq) crate.
169
+ - Full `requests`-compatible surface:
170
+ - `pyureq.get / post / put / patch / delete / head / options / request`
171
+ - `Session` with connection pooling, shared headers, cookies, auth, and
172
+ timeout defaults
173
+ - `Response` with `.status_code`, `.text`, `.content`, `.json()`,
174
+ `.headers`, `.url`, `.ok`, `.elapsed`, `.raise_for_status()`, and more
175
+ - `CaseInsensitiveDict` for response headers
176
+ - Complete `requests.exceptions` hierarchy
177
+ (`Timeout`, `HTTPError`, `ConnectionError`, `TooManyRedirects`, …)
178
+ - Rust core (`_pyureq` extension module) built with
179
+ [PyO3](https://github.com/PyO3/pyo3) + ureq 2.x:
180
+ - GIL released for the entire network call (connection → TLS → send →
181
+ recv → response parsing)
182
+ - `RustClient` for persistent connection pooling per `Session`
183
+ - One-shot stateless `http_request` for top-level convenience functions
184
+ - `native-tls` for TLS (system TLS on macOS/Windows, bundled OpenSSL
185
+ on Linux)
186
+ - `verify=False` support via `danger_accept_invalid_certs`
187
+ - Basic Auth, custom headers, cookies, query params, timeout, redirects,
188
+ and proxy disable (`proxies={"http": "", "https": ""}`)
189
+ - Pre-built `abi3-py39` wheels for Linux, macOS, Windows (x86-64 and
190
+ ARM64 on macOS)
191
+ - Distributed as `pyureq` on PyPI; import name is `pyureq`
192
+ - GitHub Actions CI: lint (ruff + clippy), test matrix (Python 3.9–3.13),
193
+ cross-platform wheel build, coverage report, and automated PyPI publish
194
+ on `v*` tag push via OIDC Trusted Publishing
195
+
196
+ ### Performance (vs requests, on local server)
197
+
198
+ | Metric | pyureq | requests |
199
+ |--------|--------|----------|
200
+ | Mean latency (single-thread) | 1.49 ms | 2.75 ms (+85%) |
201
+ | Throughput (1 000 threads) | 268 req/s | 203 req/s (+32%) |
202
+ | GIL hold per request | ~0.3 ms | ~1.2 ms |
203
+
204
+ [0.1.0]: https://github.com/vuongphu/pyureq/releases/tag/v0.1.0