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.
- pyureq-0.1.7/.github/workflows/benchmarks.yml +102 -0
- pyureq-0.1.7/.github/workflows/ci.yml +185 -0
- pyureq-0.1.7/.gitignore +44 -0
- pyureq-0.1.7/CHANGELOG.md +204 -0
- pyureq-0.1.7/Cargo.lock +832 -0
- pyureq-0.1.7/Cargo.toml +27 -0
- pyureq-0.1.7/PKG-INFO +356 -0
- pyureq-0.1.7/README.md +327 -0
- pyureq-0.1.7/benchmarks/bench.py +337 -0
- pyureq-0.1.7/benchmarks/bench_gil.py +292 -0
- pyureq-0.1.7/benchmarks/bench_stress.py +371 -0
- pyureq-0.1.7/benchmarks/bench_threads.py +345 -0
- pyureq-0.1.7/pyproject.toml +42 -0
- pyureq-0.1.7/python/pyureq/__init__.py +126 -0
- pyureq-0.1.7/python/pyureq/_pyureq.pyi +218 -0
- pyureq-0.1.7/python/pyureq/exceptions.py +83 -0
- pyureq-0.1.7/python/pyureq/models.py +204 -0
- pyureq-0.1.7/python/pyureq/sessions.py +295 -0
- pyureq-0.1.7/python/pyureq/structures.py +64 -0
- pyureq-0.1.7/src/lib.rs +569 -0
- pyureq-0.1.7/tests/__init__.py +0 -0
- pyureq-0.1.7/tests/conftest.py +25 -0
- pyureq-0.1.7/tests/local_server.py +253 -0
- pyureq-0.1.7/tests/test_basic.py +228 -0
- pyureq-0.1.7/tests/test_compat.py +254 -0
- pyureq-0.1.7/tests/test_duplicate_headers.py +260 -0
- pyureq-0.1.7/tests/test_edge_cases.py +322 -0
- pyureq-0.1.7/tests/test_regressions.py +304 -0
|
@@ -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
|
pyureq-0.1.7/.gitignore
ADDED
|
@@ -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
|