pathapi-httpx 0.1.0__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.
- pathapi_httpx-0.1.0/.gitignore +41 -0
- pathapi_httpx-0.1.0/CHANGELOG.md +31 -0
- pathapi_httpx-0.1.0/LICENSE +21 -0
- pathapi_httpx-0.1.0/PKG-INFO +172 -0
- pathapi_httpx-0.1.0/README.md +135 -0
- pathapi_httpx-0.1.0/docs/api/transport.md +23 -0
- pathapi_httpx-0.1.0/docs/benchmarks.md +20 -0
- pathapi_httpx-0.1.0/docs/changelog.md +3 -0
- pathapi_httpx-0.1.0/docs/index.md +47 -0
- pathapi_httpx-0.1.0/examples/async_client.py +48 -0
- pathapi_httpx-0.1.0/mkdocs.yml +52 -0
- pathapi_httpx-0.1.0/pyproject.toml +93 -0
- pathapi_httpx-0.1.0/src/pathapi_httpx/AGENTS.md +149 -0
- pathapi_httpx-0.1.0/src/pathapi_httpx/__init__.py +18 -0
- pathapi_httpx-0.1.0/src/pathapi_httpx/_async.py +156 -0
- pathapi_httpx-0.1.0/src/pathapi_httpx/_common.py +185 -0
- pathapi_httpx-0.1.0/src/pathapi_httpx/_sync.py +134 -0
- pathapi_httpx-0.1.0/src/pathapi_httpx/_version.py +11 -0
- pathapi_httpx-0.1.0/src/pathapi_httpx/py.typed +0 -0
- pathapi_httpx-0.1.0/tests/conftest.py +94 -0
- pathapi_httpx-0.1.0/tests/test_documents.py +180 -0
- pathapi_httpx-0.1.0/tests/test_httpx_async.py +292 -0
- pathapi_httpx-0.1.0/tests/test_httpx_transport.py +264 -0
- pathapi_httpx-0.1.0/tests/test_packaging.py +117 -0
- pathapi_httpx-0.1.0/tests/test_transport_suite.py +71 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Category rule: nothing dot-prefixed at the repo root gets tracked except the
|
|
2
|
+
# files re-included below (a category beats a list). Root-anchored on purpose;
|
|
3
|
+
# nested dotfiles are governed by the specific rules further down.
|
|
4
|
+
/.*
|
|
5
|
+
!/.gitignore
|
|
6
|
+
!/.gitattributes
|
|
7
|
+
!/.github/
|
|
8
|
+
|
|
9
|
+
# Private agent config. No trailing slash on `.agents` on purpose: it may be a
|
|
10
|
+
# symlink, which git treats as a file, so a directory-only `.agents/` would not
|
|
11
|
+
# match it. These stay even though /.* covers the root: they also catch nested
|
|
12
|
+
# copies. *.local.* is an unshared, machine- or user-specific override and is
|
|
13
|
+
# never committed (it is also excluded from the build in pyproject.toml).
|
|
14
|
+
.agents
|
|
15
|
+
*.local.*
|
|
16
|
+
CLAUDE*
|
|
17
|
+
.claude
|
|
18
|
+
|
|
19
|
+
# Build/dist output and generated docs site
|
|
20
|
+
build/
|
|
21
|
+
dist/
|
|
22
|
+
site/
|
|
23
|
+
|
|
24
|
+
# Python
|
|
25
|
+
__pycache__/
|
|
26
|
+
*.py[cod]
|
|
27
|
+
.pytest_cache/
|
|
28
|
+
.hypothesis/
|
|
29
|
+
.mypy_cache/
|
|
30
|
+
.ruff_cache/
|
|
31
|
+
.coverage*
|
|
32
|
+
htmlcov/
|
|
33
|
+
*.egg-info/
|
|
34
|
+
.venv/
|
|
35
|
+
.pyvenv/
|
|
36
|
+
|
|
37
|
+
# OS/editor
|
|
38
|
+
.DS_Store
|
|
39
|
+
Thumbs.db
|
|
40
|
+
.idea/
|
|
41
|
+
.vscode/
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-10-17
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `pathapi_httpx.HttpxTransport` and `pathapi_httpx.AsyncHttpxTransport`, the
|
|
15
|
+
HTTP transports of `pathapi.http` over `httpx.Client` and `httpx.AsyncClient`,
|
|
16
|
+
and `pathapi_httpx.__version__`. They need Python 3.9 or newer,
|
|
17
|
+
`pathapi>=0.1.2,<0.2` and `httpx>=0.28.0,<0.29`.
|
|
18
|
+
- The distribution declares the `httpx` entry of the `pathapi.http_transports`
|
|
19
|
+
and of the `pathapi.http_async_transports` groups. Installing it is all that
|
|
20
|
+
`pathapi.http` needs to send through them:
|
|
21
|
+
`HTTPClient(HttpxTransport(client))`, `AsyncHTTPClient(AsyncHttpxTransport())`,
|
|
22
|
+
`PATHAPI_HTTP_TRANSPORT=httpx` and `PATHAPI_HTTP_ASYNC_TRANSPORT=httpx` use
|
|
23
|
+
them, `pathapi.http.get_transport("httpx")` and
|
|
24
|
+
`pathapi.http.get_async_transport("httpx")` load them, and
|
|
25
|
+
`pathapi.http.transport_names()` and `async_transport_names()` list them.
|
|
26
|
+
- The tests `pathapi` runs over its own transports run over these
|
|
27
|
+
(`python -m pathapi.http.testing --http-transports httpx --http-async-transports httpx`)
|
|
28
|
+
as part of the test suite.
|
|
29
|
+
|
|
30
|
+
[Unreleased]: https://github.com/jose-pr/pathapi-httpx/compare/v0.1.0...HEAD
|
|
31
|
+
[0.1.0]: https://github.com/jose-pr/pathapi-httpx/releases/tag/v0.1.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Jose A
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pathapi-httpx
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The httpx HTTP transports for pathapi: a synchronous one and a native async one, with HTTP/2, your own httpx client and its mounts
|
|
5
|
+
Project-URL: Homepage, https://github.com/jose-pr/pathapi-httpx/
|
|
6
|
+
Project-URL: Documentation, https://jose-pr.github.io/pathapi-httpx/
|
|
7
|
+
Project-URL: Issues, https://github.com/jose-pr/pathapi-httpx/issues
|
|
8
|
+
Author: Jose A
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: httpx<0.29,>=0.28.0
|
|
23
|
+
Requires-Dist: pathapi<0.2,>=0.1.2
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: black; extra == 'dev'
|
|
26
|
+
Requires-Dist: build; extra == 'dev'
|
|
27
|
+
Requires-Dist: cryptography; extra == 'dev'
|
|
28
|
+
Requires-Dist: hatchling; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
31
|
+
Requires-Dist: twine; extra == 'dev'
|
|
32
|
+
Provides-Extra: docs
|
|
33
|
+
Requires-Dist: mkdocs; extra == 'docs'
|
|
34
|
+
Requires-Dist: mkdocs-material; extra == 'docs'
|
|
35
|
+
Requires-Dist: mkdocstrings[python]; extra == 'docs'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# pathapi-httpx
|
|
39
|
+
|
|
40
|
+
[](https://pypi.org/project/pathapi-httpx/)
|
|
41
|
+
[](https://pypi.org/project/pathapi-httpx/)
|
|
42
|
+
[](https://github.com/jose-pr/pathapi-httpx/blob/main/LICENSE)
|
|
43
|
+
[](https://jose-pr.github.io/pathapi-httpx/)
|
|
44
|
+
[](https://github.com/jose-pr/pathapi-httpx/actions/workflows/test.yml)
|
|
45
|
+
|
|
46
|
+
**The `httpx` HTTP transports for [`pathapi`](https://github.com/jose-pr/pathapi).**
|
|
47
|
+
`pathapi.http` sends requests with a transport built on the standard library,
|
|
48
|
+
and from an event loop with that transport on worker threads. Install this
|
|
49
|
+
distribution and `HTTPClient` sends through an `httpx.Client`, and
|
|
50
|
+
`AsyncHTTPClient` through an `httpx.AsyncClient` on the caller's loop, with
|
|
51
|
+
nothing to configure. It is documented at
|
|
52
|
+
<https://jose-pr.github.io/pathapi-httpx/>.
|
|
53
|
+
|
|
54
|
+
## Features
|
|
55
|
+
|
|
56
|
+
| You want | Built-in transports | These transports |
|
|
57
|
+
| --- | --- | --- |
|
|
58
|
+
| Requests from an event loop without a worker thread per request | No: `ThreadedTransport` runs the standard-library transport on threads | Yes: `AsyncHttpxTransport` |
|
|
59
|
+
| HTTP/2 | No | Yes: `HttpxTransport(http2=True)` with `httpx[http2]` |
|
|
60
|
+
| Your own `httpx.Client` or `httpx.AsyncClient`: mounted transports, limits, proxies, `auth` | No | Yes: `HttpxTransport(client)` |
|
|
61
|
+
| `SSL_CERT_FILE`, `SSL_CERT_DIR` and the certifi bundle | The system trust store, or an `ssl_context=` | Yes |
|
|
62
|
+
| Proxies from the environment, `proxy=` and `mounts=` | `http://` proxies through a provider | Yes, as `httpx` does them |
|
|
63
|
+
|
|
64
|
+
- **Credentials belong to one server.** The `auth` of a client is sent to the
|
|
65
|
+
server of the first request and to no other, and the clients of `pathapi.http`
|
|
66
|
+
never take an `Authorization` header to another server on a redirect.
|
|
67
|
+
- **Exactly the headers it is given.** A client's default headers and cookies
|
|
68
|
+
are not added: `HTTPClient` and `AsyncHTTPClient` own the headers of a request.
|
|
69
|
+
- **No third-party text in an error.** A failure of `httpx` is a
|
|
70
|
+
`TimeoutError`, `ConnectionError`, `ssl.SSLError`, `ValueError` or
|
|
71
|
+
`OSError(EIO)`; what the library says, which can quote a URL, is never passed on.
|
|
72
|
+
- **The same promises as the built-in transports.** The tests `pathapi` runs
|
|
73
|
+
over its own transports run over these
|
|
74
|
+
(`python -m pathapi.http.testing --http-transports httpx --http-async-transports httpx`).
|
|
75
|
+
|
|
76
|
+
## Installation
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install pathapi-httpx
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Requires `pathapi` and `httpx`, which pip installs with it. HTTP/2 needs
|
|
83
|
+
`pip install "httpx[http2]"`.
|
|
84
|
+
|
|
85
|
+
## Quick start
|
|
86
|
+
|
|
87
|
+
**Send through an `httpx.Client` from threads or a script:**
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
import httpx
|
|
91
|
+
from pathapi.http import HTTPClient
|
|
92
|
+
from pathapi_httpx import HttpxTransport
|
|
93
|
+
|
|
94
|
+
with httpx.Client() as session:
|
|
95
|
+
with HTTPClient(HttpxTransport(session), timeout=(3, 30)) as client:
|
|
96
|
+
reply = client.request("GET", "http://files.example.org/reports/today.txt")
|
|
97
|
+
print(reply.read().decode())
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The `httpx.Client` stays yours: neither the client nor the transport closes it.
|
|
101
|
+
|
|
102
|
+
**Send from an event loop** with `httpx.AsyncClient` on that loop:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
import asyncio
|
|
106
|
+
|
|
107
|
+
from pathapi.http import AsyncHTTPClient
|
|
108
|
+
from pathapi_httpx import AsyncHttpxTransport
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
async def main():
|
|
112
|
+
async with AsyncHttpxTransport() as transport:
|
|
113
|
+
async with AsyncHTTPClient(transport) as client:
|
|
114
|
+
replies = await asyncio.gather(
|
|
115
|
+
*(
|
|
116
|
+
client.request("GET", f"http://files.example.org/reports/{day}.txt")
|
|
117
|
+
for day in ("mon", "tue", "wed")
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
print([len(await reply.read()) for reply in replies])
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
asyncio.run(main())
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Choose them for the whole process.** `PATHAPI_HTTP_TRANSPORT=httpx` and
|
|
127
|
+
`PATHAPI_HTTP_ASYNC_TRANSPORT=httpx` make every `HTTPClient` and
|
|
128
|
+
`AsyncHTTPClient` that was given no transport send through a new `httpx` client:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
PATHAPI_HTTP_ASYNC_TRANSPORT=httpx python -c "from pathapi.http import AsyncHTTPClient; print(type(AsyncHTTPClient().transport))"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## API overview
|
|
135
|
+
|
|
136
|
+
| Name | Purpose |
|
|
137
|
+
| --- | --- |
|
|
138
|
+
| `pathapi_httpx.HttpxTransport` | The transport: `send()` and `close()` over an `httpx.Client` |
|
|
139
|
+
| `pathapi_httpx.AsyncHttpxTransport` | The async transport: `await send()` and `await aclose()` over an `httpx.AsyncClient` |
|
|
140
|
+
| `pathapi_httpx.__version__` | The installed version |
|
|
141
|
+
|
|
142
|
+
`pathapi.http` finds the classes through the `httpx` entries of its
|
|
143
|
+
`pathapi.http_transports` and `pathapi.http_async_transports` entry-point
|
|
144
|
+
groups, which this distribution declares. The reference is in the
|
|
145
|
+
[API header](https://github.com/jose-pr/pathapi-httpx/blob/main/src/pathapi_httpx/AGENTS.md)
|
|
146
|
+
that ships with the package.
|
|
147
|
+
|
|
148
|
+
## Development
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
python -m venv .venv/3.14-posix-x86_64
|
|
152
|
+
.venv/3.14-posix-x86_64/bin/python -m pip install -e ".[dev]"
|
|
153
|
+
.venv/3.14-posix-x86_64/bin/python -m pytest -q -rs
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`pathapi` 0.1.2 is a dependency; while it is not on PyPI, install it
|
|
157
|
+
editable from its checkout first (`pip install -e ../pathapi`). The test run
|
|
158
|
+
includes `python -m pathapi.http.testing --http-transports httpx
|
|
159
|
+
--http-async-transports httpx`, the tests `pathapi` runs over its own transports.
|
|
160
|
+
|
|
161
|
+
### Releasing
|
|
162
|
+
|
|
163
|
+
This project follows [Semantic Versioning](https://semver.org/) and keeps a
|
|
164
|
+
[`CHANGELOG.md`](CHANGELOG.md). Pushing a tag matching `v*` triggers the release
|
|
165
|
+
workflow: test gate → build (checking the tag names the version built) → a strict
|
|
166
|
+
docs build as a gate → GitHub release → publish. The release workflow never deploys
|
|
167
|
+
the docs site itself: for a final release its last job dispatches the docs workflow
|
|
168
|
+
at the tag, which owns every Pages deploy.
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# pathapi-httpx
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/pathapi-httpx/)
|
|
4
|
+
[](https://pypi.org/project/pathapi-httpx/)
|
|
5
|
+
[](https://github.com/jose-pr/pathapi-httpx/blob/main/LICENSE)
|
|
6
|
+
[](https://jose-pr.github.io/pathapi-httpx/)
|
|
7
|
+
[](https://github.com/jose-pr/pathapi-httpx/actions/workflows/test.yml)
|
|
8
|
+
|
|
9
|
+
**The `httpx` HTTP transports for [`pathapi`](https://github.com/jose-pr/pathapi).**
|
|
10
|
+
`pathapi.http` sends requests with a transport built on the standard library,
|
|
11
|
+
and from an event loop with that transport on worker threads. Install this
|
|
12
|
+
distribution and `HTTPClient` sends through an `httpx.Client`, and
|
|
13
|
+
`AsyncHTTPClient` through an `httpx.AsyncClient` on the caller's loop, with
|
|
14
|
+
nothing to configure. It is documented at
|
|
15
|
+
<https://jose-pr.github.io/pathapi-httpx/>.
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
| You want | Built-in transports | These transports |
|
|
20
|
+
| --- | --- | --- |
|
|
21
|
+
| Requests from an event loop without a worker thread per request | No: `ThreadedTransport` runs the standard-library transport on threads | Yes: `AsyncHttpxTransport` |
|
|
22
|
+
| HTTP/2 | No | Yes: `HttpxTransport(http2=True)` with `httpx[http2]` |
|
|
23
|
+
| Your own `httpx.Client` or `httpx.AsyncClient`: mounted transports, limits, proxies, `auth` | No | Yes: `HttpxTransport(client)` |
|
|
24
|
+
| `SSL_CERT_FILE`, `SSL_CERT_DIR` and the certifi bundle | The system trust store, or an `ssl_context=` | Yes |
|
|
25
|
+
| Proxies from the environment, `proxy=` and `mounts=` | `http://` proxies through a provider | Yes, as `httpx` does them |
|
|
26
|
+
|
|
27
|
+
- **Credentials belong to one server.** The `auth` of a client is sent to the
|
|
28
|
+
server of the first request and to no other, and the clients of `pathapi.http`
|
|
29
|
+
never take an `Authorization` header to another server on a redirect.
|
|
30
|
+
- **Exactly the headers it is given.** A client's default headers and cookies
|
|
31
|
+
are not added: `HTTPClient` and `AsyncHTTPClient` own the headers of a request.
|
|
32
|
+
- **No third-party text in an error.** A failure of `httpx` is a
|
|
33
|
+
`TimeoutError`, `ConnectionError`, `ssl.SSLError`, `ValueError` or
|
|
34
|
+
`OSError(EIO)`; what the library says, which can quote a URL, is never passed on.
|
|
35
|
+
- **The same promises as the built-in transports.** The tests `pathapi` runs
|
|
36
|
+
over its own transports run over these
|
|
37
|
+
(`python -m pathapi.http.testing --http-transports httpx --http-async-transports httpx`).
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install pathapi-httpx
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Requires `pathapi` and `httpx`, which pip installs with it. HTTP/2 needs
|
|
46
|
+
`pip install "httpx[http2]"`.
|
|
47
|
+
|
|
48
|
+
## Quick start
|
|
49
|
+
|
|
50
|
+
**Send through an `httpx.Client` from threads or a script:**
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import httpx
|
|
54
|
+
from pathapi.http import HTTPClient
|
|
55
|
+
from pathapi_httpx import HttpxTransport
|
|
56
|
+
|
|
57
|
+
with httpx.Client() as session:
|
|
58
|
+
with HTTPClient(HttpxTransport(session), timeout=(3, 30)) as client:
|
|
59
|
+
reply = client.request("GET", "http://files.example.org/reports/today.txt")
|
|
60
|
+
print(reply.read().decode())
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The `httpx.Client` stays yours: neither the client nor the transport closes it.
|
|
64
|
+
|
|
65
|
+
**Send from an event loop** with `httpx.AsyncClient` on that loop:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import asyncio
|
|
69
|
+
|
|
70
|
+
from pathapi.http import AsyncHTTPClient
|
|
71
|
+
from pathapi_httpx import AsyncHttpxTransport
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def main():
|
|
75
|
+
async with AsyncHttpxTransport() as transport:
|
|
76
|
+
async with AsyncHTTPClient(transport) as client:
|
|
77
|
+
replies = await asyncio.gather(
|
|
78
|
+
*(
|
|
79
|
+
client.request("GET", f"http://files.example.org/reports/{day}.txt")
|
|
80
|
+
for day in ("mon", "tue", "wed")
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
print([len(await reply.read()) for reply in replies])
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
asyncio.run(main())
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Choose them for the whole process.** `PATHAPI_HTTP_TRANSPORT=httpx` and
|
|
90
|
+
`PATHAPI_HTTP_ASYNC_TRANSPORT=httpx` make every `HTTPClient` and
|
|
91
|
+
`AsyncHTTPClient` that was given no transport send through a new `httpx` client:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
PATHAPI_HTTP_ASYNC_TRANSPORT=httpx python -c "from pathapi.http import AsyncHTTPClient; print(type(AsyncHTTPClient().transport))"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## API overview
|
|
98
|
+
|
|
99
|
+
| Name | Purpose |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| `pathapi_httpx.HttpxTransport` | The transport: `send()` and `close()` over an `httpx.Client` |
|
|
102
|
+
| `pathapi_httpx.AsyncHttpxTransport` | The async transport: `await send()` and `await aclose()` over an `httpx.AsyncClient` |
|
|
103
|
+
| `pathapi_httpx.__version__` | The installed version |
|
|
104
|
+
|
|
105
|
+
`pathapi.http` finds the classes through the `httpx` entries of its
|
|
106
|
+
`pathapi.http_transports` and `pathapi.http_async_transports` entry-point
|
|
107
|
+
groups, which this distribution declares. The reference is in the
|
|
108
|
+
[API header](https://github.com/jose-pr/pathapi-httpx/blob/main/src/pathapi_httpx/AGENTS.md)
|
|
109
|
+
that ships with the package.
|
|
110
|
+
|
|
111
|
+
## Development
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
python -m venv .venv/3.14-posix-x86_64
|
|
115
|
+
.venv/3.14-posix-x86_64/bin/python -m pip install -e ".[dev]"
|
|
116
|
+
.venv/3.14-posix-x86_64/bin/python -m pytest -q -rs
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`pathapi` 0.1.2 is a dependency; while it is not on PyPI, install it
|
|
120
|
+
editable from its checkout first (`pip install -e ../pathapi`). The test run
|
|
121
|
+
includes `python -m pathapi.http.testing --http-transports httpx
|
|
122
|
+
--http-async-transports httpx`, the tests `pathapi` runs over its own transports.
|
|
123
|
+
|
|
124
|
+
### Releasing
|
|
125
|
+
|
|
126
|
+
This project follows [Semantic Versioning](https://semver.org/) and keeps a
|
|
127
|
+
[`CHANGELOG.md`](CHANGELOG.md). Pushing a tag matching `v*` triggers the release
|
|
128
|
+
workflow: test gate → build (checking the tag names the version built) → a strict
|
|
129
|
+
docs build as a gate → GitHub release → publish. The release workflow never deploys
|
|
130
|
+
the docs site itself: for a final release its last job dispatches the docs workflow
|
|
131
|
+
at the tag, which owns every Pages deploy.
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# The transports
|
|
2
|
+
|
|
3
|
+
`HttpxTransport` implements the HTTP transport contract of `pathapi.http`:
|
|
4
|
+
`send()` makes one request with an `httpx.Client` and returns an
|
|
5
|
+
`HTTPResponse`, and `close()` closes a client the transport made.
|
|
6
|
+
`AsyncHttpxTransport` implements the async contract: `await send()` makes one
|
|
7
|
+
request with an `httpx.AsyncClient` on the running event loop and returns an
|
|
8
|
+
`AsyncHTTPResponse`, and `await aclose()` closes a client the transport made.
|
|
9
|
+
`pathapi.http` finds them through the `httpx` entries of its
|
|
10
|
+
`pathapi.http_transports` and `pathapi.http_async_transports` entry-point
|
|
11
|
+
groups, so `PATHAPI_HTTP_TRANSPORT=httpx`,
|
|
12
|
+
`PATHAPI_HTTP_ASYNC_TRANSPORT=httpx`, `pathapi.http.get_transport("httpx")` and
|
|
13
|
+
`pathapi.http.get_async_transport("httpx")` name them.
|
|
14
|
+
|
|
15
|
+
::: pathapi_httpx.HttpxTransport
|
|
16
|
+
options:
|
|
17
|
+
show_if_no_docstring: true
|
|
18
|
+
filters: ["!^_"]
|
|
19
|
+
|
|
20
|
+
::: pathapi_httpx.AsyncHttpxTransport
|
|
21
|
+
options:
|
|
22
|
+
show_if_no_docstring: true
|
|
23
|
+
filters: ["!^_"]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Benchmarks
|
|
2
|
+
|
|
3
|
+
`benchmarks/bench.py` times a batch of concurrent requests through
|
|
4
|
+
`AsyncHTTPClient` against a loopback server that answers each request after a
|
|
5
|
+
fixed delay: with `ThreadedTransport`, the default, which waits for each reply
|
|
6
|
+
on a worker thread, and with `AsyncHttpxTransport`, which waits on the event
|
|
7
|
+
loop. It also times `HTTPClient` with `HttpxTransport` and the built-in
|
|
8
|
+
transport on the same sequential requests.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
python benchmarks/bench.py # run the benchmarks
|
|
12
|
+
python benchmarks/bench.py --help # options; runs nothing
|
|
13
|
+
python benchmarks/bench.py --save # also write benchmarks/results/<name>.json
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Compare two runs on their medians, and only between runs from the same machine
|
|
17
|
+
and interpreter. The result schema is described in
|
|
18
|
+
[`benchmarks/README.md`](https://github.com/jose-pr/pathapi-httpx/blob/main/benchmarks/README.md).
|
|
19
|
+
A run on a developer machine is a sanity check; performance claims come from CI
|
|
20
|
+
runs.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# pathapi-httpx
|
|
2
|
+
|
|
3
|
+
The **`httpx` HTTP transports for [`pathapi`](https://jose-pr.github.io/pathapi/)**.
|
|
4
|
+
`pathapi.http` sends requests with a transport built on the standard
|
|
5
|
+
library, and from an event loop with that transport on worker threads. Install
|
|
6
|
+
this distribution and `HTTPClient` sends through an `httpx.Client`, and
|
|
7
|
+
`AsyncHTTPClient` through an `httpx.AsyncClient` on the caller's loop, with
|
|
8
|
+
nothing to configure.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install pathapi-httpx
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
HTTP/2 needs `pip install "httpx[http2]"`.
|
|
17
|
+
|
|
18
|
+
## 30-second tour
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
import asyncio
|
|
22
|
+
|
|
23
|
+
from pathapi.http import AsyncHTTPClient
|
|
24
|
+
from pathapi_httpx import AsyncHttpxTransport
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def main():
|
|
28
|
+
async with AsyncHttpxTransport() as transport:
|
|
29
|
+
async with AsyncHTTPClient(transport) as client:
|
|
30
|
+
reply = await client.request(
|
|
31
|
+
"GET", "http://files.example.org/reports/today.txt"
|
|
32
|
+
)
|
|
33
|
+
print((await reply.read()).decode())
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
asyncio.run(main())
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`PATHAPI_HTTP_TRANSPORT=httpx` and `PATHAPI_HTTP_ASYNC_TRANSPORT=httpx` make
|
|
40
|
+
every `HTTPClient` and `AsyncHTTPClient` that was given no transport send
|
|
41
|
+
through a new `httpx` client.
|
|
42
|
+
|
|
43
|
+
## Learn more
|
|
44
|
+
|
|
45
|
+
- [The transports](api/transport.md): `HttpxTransport` and `AsyncHttpxTransport`, their arguments and what they guarantee.
|
|
46
|
+
- [Benchmarks](benchmarks.md): concurrent requests through the thread-backed default and `AsyncHttpxTransport` on loopback.
|
|
47
|
+
- [Changelog](changelog.md)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Fetch several files at once from an event loop with `AsyncHttpxTransport`.
|
|
2
|
+
|
|
3
|
+
The script starts a small server on the loopback interface, so it needs no
|
|
4
|
+
network: an `AsyncHTTPClient` over `AsyncHttpxTransport` sends the requests
|
|
5
|
+
together on the running loop, each from the same pool of connections.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import http.server
|
|
12
|
+
import threading
|
|
13
|
+
|
|
14
|
+
from pathapi.http import AsyncHTTPClient
|
|
15
|
+
from pathapi_httpx import AsyncHttpxTransport
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Handler(http.server.BaseHTTPRequestHandler):
|
|
19
|
+
def do_GET(self):
|
|
20
|
+
body = f"contents of {self.path}\n".encode()
|
|
21
|
+
self.send_response(200)
|
|
22
|
+
self.send_header("Content-Length", str(len(body)))
|
|
23
|
+
self.end_headers()
|
|
24
|
+
self.wfile.write(body)
|
|
25
|
+
|
|
26
|
+
def log_message(self, *args):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def main(base: str) -> None:
|
|
31
|
+
async with AsyncHttpxTransport() as transport:
|
|
32
|
+
async with AsyncHTTPClient(transport, timeout=(3, 30)) as client:
|
|
33
|
+
names = ("a.txt", "b.txt", "c.txt")
|
|
34
|
+
replies = await asyncio.gather(
|
|
35
|
+
*(client.request("GET", f"{base}/{name}") for name in names)
|
|
36
|
+
)
|
|
37
|
+
for reply in replies:
|
|
38
|
+
print((await reply.read()).decode(), end="")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
|
43
|
+
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
44
|
+
try:
|
|
45
|
+
asyncio.run(main(f"http://127.0.0.1:{server.server_port}"))
|
|
46
|
+
finally:
|
|
47
|
+
server.shutdown()
|
|
48
|
+
server.server_close()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
site_name: pathapi-httpx
|
|
2
|
+
site_description: The httpx HTTP transports for pathapi
|
|
3
|
+
site_url: https://jose-pr.github.io/pathapi-httpx/
|
|
4
|
+
repo_url: https://github.com/jose-pr/pathapi-httpx
|
|
5
|
+
repo_name: jose-pr/pathapi-httpx
|
|
6
|
+
edit_uri: edit/main/docs/
|
|
7
|
+
|
|
8
|
+
# Constrain the scan to docs/ so pymdownx.snippets + strict nav don't trip on
|
|
9
|
+
# repo-level markdown.
|
|
10
|
+
docs_dir: docs
|
|
11
|
+
|
|
12
|
+
theme:
|
|
13
|
+
name: material
|
|
14
|
+
palette:
|
|
15
|
+
- media: "(prefers-color-scheme: light)"
|
|
16
|
+
scheme: default
|
|
17
|
+
toggle:
|
|
18
|
+
icon: material/brightness-7
|
|
19
|
+
name: Switch to dark mode
|
|
20
|
+
- media: "(prefers-color-scheme: dark)"
|
|
21
|
+
scheme: slate
|
|
22
|
+
toggle:
|
|
23
|
+
icon: material/brightness-4
|
|
24
|
+
name: Switch to light mode
|
|
25
|
+
features:
|
|
26
|
+
- navigation.sections
|
|
27
|
+
- content.code.copy
|
|
28
|
+
|
|
29
|
+
nav:
|
|
30
|
+
- Home: index.md
|
|
31
|
+
- Benchmarks: benchmarks.md
|
|
32
|
+
- API Reference:
|
|
33
|
+
- The transports: api/transport.md
|
|
34
|
+
- Changelog: changelog.md
|
|
35
|
+
|
|
36
|
+
plugins:
|
|
37
|
+
- search
|
|
38
|
+
- mkdocstrings:
|
|
39
|
+
handlers:
|
|
40
|
+
python:
|
|
41
|
+
options:
|
|
42
|
+
show_source: true
|
|
43
|
+
show_root_heading: true
|
|
44
|
+
merge_init_into_class: true
|
|
45
|
+
show_if_no_docstring: true
|
|
46
|
+
|
|
47
|
+
markdown_extensions:
|
|
48
|
+
- admonition
|
|
49
|
+
- tables
|
|
50
|
+
- pymdownx.snippets:
|
|
51
|
+
base_path: ["."]
|
|
52
|
+
- pymdownx.superfences
|