Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
4acc1cd
docs(sponsors): add Byteful and remove expired Hyper Solutions
0x676e67 Sep 29, 2026
4060a7c
feat(runtime): support custom client runtimes
0x676e67 Sep 29, 2026
163d443
refactor(runtime): remove explicit default accessor
0x676e67 Sep 29, 2026
b389351
docs(runtime): describe the generic Tokio runtime wrapper
0x676e67 Sep 29, 2026
c29273c
fix(client): guard interpreter shutdown with try_attach
0x676e67 Sep 29, 2026
33a7000
fix(client): avoid PyO3 attach panics during interpreter shutdown (#621)
0x676e67 Sep 29, 2026
6bcfa52
chore: merge main into runtime branch
0x676e67 Sep 29, 2026
ea58e0d
refactor(runtime): use field annotations in client declarations
0x676e67 Sep 29, 2026
6da54ee
style(python): format code with black
0x676e67 Sep 29, 2026
f552742
style(runtime): separate method declarations with blank lines
0x676e67 Sep 29, 2026
3951e55
refactor(runtime): simplify eager runtime ownership
0x676e67 Sep 29, 2026
0e8b856
refactor(stream): consolidate Python stream adapters
0x676e67 Sep 29, 2026
d186e7c
refactor(runtime): retain selected handles in runtime
0x676e67 Sep 30, 2026
e952bbe
fix(client): propagate blocking client cancellation
0x676e67 Sep 30, 2026
1a4c0e1
refactor(client): simplify cancellation and stream cleanup
0x676e67 Sep 30, 2026
dfe1ae4
ci: bound PyPy tests and expose hang diagnostics
0x676e67 Sep 30, 2026
9382a1f
fix(client): support PyPy coroutine cancellation and upload EOF
0x676e67 Sep 30, 2026
d9b1f36
fix(client): sync upload iterator error handling
0x676e67 Sep 30, 2026
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
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,10 @@ jobs:
*) echo "Expected a pp311 wheel, got: $wheel"; exit 1 ;;
esac
- name: Run tests
run: .venv-pypy/bin/python -m pytest
timeout-minutes: 15
run: .venv-pypy/bin/python -u -m pytest -vv -x -o faulthandler_timeout=60
- name: Upload wheel
if: always()
uses: actions/upload-artifact@v7
with:
name: wheels-linux-x86_64-pypy311
Expand Down
121 changes: 55 additions & 66 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 3 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ abi3-py313 = ["pyo3/abi3-py313"]
abi3-py314 = ["pyo3/abi3-py314"]

[dependencies]
tokio = "1.52.2"
pingora-runtime = "0.9.0"
tokio = { version = "1.52.2", features = ["rt-multi-thread", "sync", "time", "net"] }
tokio-util = { version = "0.7.18", features = ["rt"] }
pyo3 = { version = "0.29.0", features = [
"indexmap",
Expand All @@ -37,11 +38,6 @@ pyo3 = { version = "0.29.0", features = [
"generate-import-lib",
"experimental-async",
] }
pyo3-async-runtimes = { version = "0.29.0", features = [
"tokio-runtime",
"unstable-streams",
] }
pin-project-lite = "0.2.16"
futures-util = { version = "0.3.33", default-features = false }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1"
Expand Down Expand Up @@ -92,4 +88,5 @@ debug = false
incremental = false
lto = "fat"
opt-level = 3
panic = "abort"
strip = true
1 change: 1 addition & 0 deletions docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ nav:
- Modules:
- wreq: api/wreq.md
- wreq.blocking: api/blocking.md
- wreq.runtime: api/runtime.md
- wreq.header: api/header.md
- wreq.cookie: api/cookie.md
- wreq.exceptions: api/exceptions.md
Expand Down
8 changes: 8 additions & 0 deletions docs/source/api/runtime.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# wreq.runtime

Runtime configuration for asynchronous and blocking clients. `Runtime` is also
available as `wreq.Runtime`.

::: wreq.runtime.Runtime
options:
show_root_heading: true
63 changes: 63 additions & 0 deletions docs/source/guide/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@

Send data using async generators for streaming uploads:

Async upload generators run on the caller's running event loop with its context variables.
Their exceptions fail the request. When an upload ends, the generator is closed;
cancelling or dropping the upload schedules producer cancellation and cleanup on that loop.
Keep the loop running until generator cleanup has finished. This also applies to async multipart parts.
Construct async-generator `Part` objects inside a running event loop; their producers
start at construction, with bounded buffering before the request consumes them.
Use synchronous iterators for blocking uploads. A blocking call on the producer's
event-loop thread prevents async generators from progressing.

```python
import asyncio
import wreq
Expand Down Expand Up @@ -87,6 +96,60 @@ if __name__ == "__main__":
asyncio.run(main())
```

### Custom runtimes

Clients share a global multi-thread runtime when `runtime` is omitted or `None`.
It starts on first use. Construct a `Runtime` to
start a separate worker pool for an async or blocking client:

```python
from datetime import timedelta

from wreq import Client
from wreq.runtime import Runtime

runtime = Runtime(
workers=1,
work_steal=False,
thread_name="http-client",
max_blocking_threads=8,
thread_keep_alive=timedelta(seconds=10),
)
client = Client(runtime=runtime)
```

With `work_steal=False`, workers use independent single-thread Tokio runtimes.
Each client is assigned one worker for its lifetime; requests, response reads,
streams and WebSocket operations use that worker. With multiple workers, newly
created clients select a worker randomly and keep that selection. This is not
CPU pinning. Sharing the same `Runtime` between clients is supported, and
`client.runtime` returns the shared runtime object.

`workers=None` uses the available CPU parallelism, or 1 if it cannot be determined.
Custom runtimes start their threads during construction, before any client is
bound or request is sent.

`thread_name=None` uses the package name, `wreq-python`, as the thread name.

`thread_keep_alive` accepts a nonnegative `datetime.timedelta`.
`max_blocking_threads` and `thread_keep_alive` default to Tokio's settings
(512 and 10 seconds). In
no-steal mode these limits apply to **each worker's** blocking pool, not the pool
as a whole. Python async upload generators still run on the caller's event loop.
Standalone multipart file preparation and upload-task cleanup can use the
shared runtime; a dedicated client runtime does not isolate Python's GIL or
every process resource. DNS resolvers are owned by individual clients so their
connections are not shared across runtimes.

Closing a client cancels pending requests and rejects new requests with
`asyncio.CancelledError`, for both async and blocking APIs. It does not shut down
the runtime or invalidate existing responses and WebSockets.
Clients, responses, streams and active tasks share ownership. Dropping the last
owner automatically releases a custom runtime without synchronously waiting for
its workers; already running blocking work may finish later. The default runtime
is shared for the process lifetime. Zero thread counts, NUL characters in thread
names and negative durations raise `ValueError`.

### TLS Key Logging

Capture TLS keys for debugging with tools like Wireshark:
Expand Down
21 changes: 21 additions & 0 deletions docs/source/guide/blocking.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,27 @@ if __name__ == "__main__":
main()
```

### Custom Runtime

The blocking client accepts the same `Runtime` as the async client. Without one,
it uses the shared global multi-thread runtime.

```python
from wreq.blocking import Client
from wreq.runtime import Runtime

runtime = Runtime(workers=1, work_steal=False)
with Client(runtime=runtime) as client:
with client.get("https://httpbin.io/get") as response:
print(response.text())
```

Network work runs on the selected worker while the calling thread waits.
`client.runtime` is read-only. Closing the client does not shut down a shared
runtime; it cancels pending requests and rejects new ones with
`asyncio.CancelledError`. See [custom runtimes](advanced.md#custom-runtimes) for
configuration and lifetime details.

### Cookies

```python
Expand Down
Loading
Loading