httpware 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.
- httpware-0.1.0/PKG-INFO +94 -0
- httpware-0.1.0/README.md +62 -0
- httpware-0.1.0/pyproject.toml +113 -0
- httpware-0.1.0/src/httpware/__init__.py +67 -0
- httpware-0.1.0/src/httpware/_internal/__init__.py +1 -0
- httpware-0.1.0/src/httpware/_internal/chain.py +39 -0
- httpware-0.1.0/src/httpware/_internal/import_checker.py +6 -0
- httpware-0.1.0/src/httpware/client.py +620 -0
- httpware-0.1.0/src/httpware/config.py +40 -0
- httpware-0.1.0/src/httpware/decoders/__init__.py +18 -0
- httpware-0.1.0/src/httpware/decoders/msgspec.py +32 -0
- httpware-0.1.0/src/httpware/decoders/pydantic.py +29 -0
- httpware-0.1.0/src/httpware/errors.py +194 -0
- httpware-0.1.0/src/httpware/middleware/__init__.py +89 -0
- httpware-0.1.0/src/httpware/py.typed +0 -0
- httpware-0.1.0/src/httpware/request.py +55 -0
- httpware-0.1.0/src/httpware/response.py +69 -0
- httpware-0.1.0/src/httpware/transports/__init__.py +27 -0
- httpware-0.1.0/src/httpware/transports/httpx2.py +180 -0
- httpware-0.1.0/src/httpware/transports/recorded.py +84 -0
httpware-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: httpware
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Resilience-first async HTTP client framework for Python
|
|
5
|
+
Keywords: http,async,client,resilience,retry,circuit-breaker,middleware,httpx,pydantic
|
|
6
|
+
Author: Artur Shiriev
|
|
7
|
+
Author-email: Artur Shiriev <me@shiriev.ru>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
15
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
16
|
+
Classifier: Framework :: AsyncIO
|
|
17
|
+
Requires-Dist: httpx2>=2.0.0,<3.0
|
|
18
|
+
Requires-Dist: pydantic>=2.0,<3.0
|
|
19
|
+
Requires-Dist: httpware[msgspec,otel,niquests] ; extra == 'all'
|
|
20
|
+
Requires-Dist: msgspec>=0.18 ; extra == 'msgspec'
|
|
21
|
+
Requires-Dist: niquests>=3.18 ; extra == 'niquests'
|
|
22
|
+
Requires-Dist: opentelemetry-api>=1.20 ; extra == 'otel'
|
|
23
|
+
Requires-Dist: opentelemetry-sdk>=1.20 ; extra == 'otel'
|
|
24
|
+
Requires-Python: >=3.11, <4
|
|
25
|
+
Project-URL: repository, https://github.com/modern-python/httpware
|
|
26
|
+
Project-URL: docs, https://httpware.readthedocs.io
|
|
27
|
+
Provides-Extra: all
|
|
28
|
+
Provides-Extra: msgspec
|
|
29
|
+
Provides-Extra: niquests
|
|
30
|
+
Provides-Extra: otel
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# httpware
|
|
34
|
+
|
|
35
|
+
[](https://github.com/modern-python/httpware/actions/workflows/ci.yml)
|
|
36
|
+
[](https://pypi.org/project/httpware/)
|
|
37
|
+
[](https://pypi.org/project/httpware/)
|
|
38
|
+
[](https://opensource.org/licenses/MIT)
|
|
39
|
+
|
|
40
|
+
**Async HTTP client framework for Python.**
|
|
41
|
+
|
|
42
|
+
`httpware` is a typed, async HTTP client library built on `httpx2` with a protocol-based seam so the transport is swappable. Middleware composes via an onion model. Pydantic and msgspec response decoding ship out of the box. `RecordedTransport` replaces respx for transport-level tests.
|
|
43
|
+
|
|
44
|
+
> **Status:** Pre-1.0 (0.1.0 alpha). Public API is subject to change between minor releases until v1.0. Resilience middleware (retry / timeout / bulkhead), streaming, and observability are not yet shipped — track progress on GitHub.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install httpware
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Optional extras:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install httpware[msgspec] # MsgspecDecoder
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
(`otel`, `niquests`, and `all` extras are declared but their integrations have not shipped yet.)
|
|
59
|
+
|
|
60
|
+
## Quickstart
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from httpware import AsyncClient
|
|
64
|
+
from pydantic import BaseModel
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class User(BaseModel):
|
|
68
|
+
id: int
|
|
69
|
+
name: str
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def main() -> None:
|
|
73
|
+
async with AsyncClient(base_url="https://api.example.com") as client:
|
|
74
|
+
user = await client.get("/users/1", response_model=User)
|
|
75
|
+
print(user.name)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## What ships in 0.1.0
|
|
79
|
+
|
|
80
|
+
- **`AsyncClient`** — eight HTTP method shortcuts (`get`, `post`, `put`, `patch`, `delete`, `head`, `options`, `request`) with typed `response_model` overloads; per-call overrides for `headers`, `params`, `cookies`, `timeout`, `json`, `content`; httpx-style `base_url` join; `with_options(...)` returns a view sharing the same transport.
|
|
81
|
+
- **Transport-agnostic seam.** `httpx2` is confined to `httpware.transports.httpx2.Httpx2Transport`. Implement the `Transport` protocol to swap backends.
|
|
82
|
+
- **Middleware foundation.** `Middleware` protocol, `Next` type alias, recursive-closure `compose()` chain composition, and phase decorators (`@before_request`, `@after_response`, `@on_error`).
|
|
83
|
+
- **Pluggable response decoding.** `PydanticDecoder` (default) with cached `TypeAdapter`; `MsgspecDecoder` via `httpware[msgspec]`.
|
|
84
|
+
- **`RecordedTransport`** — built-in test double with a route table, observed-request list, and `aclose_calls` counter.
|
|
85
|
+
- **Status-keyed exception hierarchy** — `StatusError`, 4xx / 5xx subclasses, plain typed fields (`status: int`, `body: bytes`, `headers`, `json`, `request_method`, `request_url`). Pickleable; userinfo redacted in `__repr__`.
|
|
86
|
+
- **No `httpx2` exception types** leak through `httpware`. The transport seam maps them to `httpware` exceptions.
|
|
87
|
+
|
|
88
|
+
## Part of `modern-python`
|
|
89
|
+
|
|
90
|
+
Browse the full list of templates and libraries in [`modern-python`](https://github.com/modern-python) — see the org profile for the categorized index.
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT — see [LICENSE](./LICENSE).
|
httpware-0.1.0/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# httpware
|
|
2
|
+
|
|
3
|
+
[](https://github.com/modern-python/httpware/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/httpware/)
|
|
5
|
+
[](https://pypi.org/project/httpware/)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
**Async HTTP client framework for Python.**
|
|
9
|
+
|
|
10
|
+
`httpware` is a typed, async HTTP client library built on `httpx2` with a protocol-based seam so the transport is swappable. Middleware composes via an onion model. Pydantic and msgspec response decoding ship out of the box. `RecordedTransport` replaces respx for transport-level tests.
|
|
11
|
+
|
|
12
|
+
> **Status:** Pre-1.0 (0.1.0 alpha). Public API is subject to change between minor releases until v1.0. Resilience middleware (retry / timeout / bulkhead), streaming, and observability are not yet shipped — track progress on GitHub.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install httpware
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Optional extras:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install httpware[msgspec] # MsgspecDecoder
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
(`otel`, `niquests`, and `all` extras are declared but their integrations have not shipped yet.)
|
|
27
|
+
|
|
28
|
+
## Quickstart
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from httpware import AsyncClient
|
|
32
|
+
from pydantic import BaseModel
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class User(BaseModel):
|
|
36
|
+
id: int
|
|
37
|
+
name: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def main() -> None:
|
|
41
|
+
async with AsyncClient(base_url="https://api.example.com") as client:
|
|
42
|
+
user = await client.get("/users/1", response_model=User)
|
|
43
|
+
print(user.name)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## What ships in 0.1.0
|
|
47
|
+
|
|
48
|
+
- **`AsyncClient`** — eight HTTP method shortcuts (`get`, `post`, `put`, `patch`, `delete`, `head`, `options`, `request`) with typed `response_model` overloads; per-call overrides for `headers`, `params`, `cookies`, `timeout`, `json`, `content`; httpx-style `base_url` join; `with_options(...)` returns a view sharing the same transport.
|
|
49
|
+
- **Transport-agnostic seam.** `httpx2` is confined to `httpware.transports.httpx2.Httpx2Transport`. Implement the `Transport` protocol to swap backends.
|
|
50
|
+
- **Middleware foundation.** `Middleware` protocol, `Next` type alias, recursive-closure `compose()` chain composition, and phase decorators (`@before_request`, `@after_response`, `@on_error`).
|
|
51
|
+
- **Pluggable response decoding.** `PydanticDecoder` (default) with cached `TypeAdapter`; `MsgspecDecoder` via `httpware[msgspec]`.
|
|
52
|
+
- **`RecordedTransport`** — built-in test double with a route table, observed-request list, and `aclose_calls` counter.
|
|
53
|
+
- **Status-keyed exception hierarchy** — `StatusError`, 4xx / 5xx subclasses, plain typed fields (`status: int`, `body: bytes`, `headers`, `json`, `request_method`, `request_url`). Pickleable; userinfo redacted in `__repr__`.
|
|
54
|
+
- **No `httpx2` exception types** leak through `httpware`. The transport seam maps them to `httpware` exceptions.
|
|
55
|
+
|
|
56
|
+
## Part of `modern-python`
|
|
57
|
+
|
|
58
|
+
Browse the full list of templates and libraries in [`modern-python`](https://github.com/modern-python) — see the org profile for the categorized index.
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "httpware"
|
|
3
|
+
description = "Resilience-first async HTTP client framework for Python"
|
|
4
|
+
authors = [{ name = "Artur Shiriev", email = "me@shiriev.ru" }]
|
|
5
|
+
requires-python = ">=3.11,<4"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
keywords = [
|
|
9
|
+
"http",
|
|
10
|
+
"async",
|
|
11
|
+
"client",
|
|
12
|
+
"resilience",
|
|
13
|
+
"retry",
|
|
14
|
+
"circuit-breaker",
|
|
15
|
+
"middleware",
|
|
16
|
+
"httpx",
|
|
17
|
+
"pydantic",
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
"Topic :: Software Development :: Libraries",
|
|
26
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
27
|
+
"Framework :: AsyncIO",
|
|
28
|
+
]
|
|
29
|
+
version = "0.1.0"
|
|
30
|
+
dependencies = [
|
|
31
|
+
"httpx2>=2.0.0,<3.0",
|
|
32
|
+
"pydantic>=2.0,<3.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
msgspec = ["msgspec>=0.18"]
|
|
37
|
+
otel = [
|
|
38
|
+
"opentelemetry-api>=1.20",
|
|
39
|
+
"opentelemetry-sdk>=1.20",
|
|
40
|
+
]
|
|
41
|
+
niquests = ["niquests>=3.18"]
|
|
42
|
+
all = ["httpware[msgspec,otel,niquests]"]
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
repository = "https://github.com/modern-python/httpware"
|
|
46
|
+
docs = "https://httpware.readthedocs.io"
|
|
47
|
+
|
|
48
|
+
[build-system]
|
|
49
|
+
requires = ["uv_build>=0.11,<0.12"]
|
|
50
|
+
build-backend = "uv_build"
|
|
51
|
+
|
|
52
|
+
[tool.uv.build-backend]
|
|
53
|
+
module-name = "httpware"
|
|
54
|
+
module-root = "src"
|
|
55
|
+
|
|
56
|
+
[dependency-groups]
|
|
57
|
+
dev = [
|
|
58
|
+
"pytest",
|
|
59
|
+
"pytest-cov",
|
|
60
|
+
"pytest-asyncio",
|
|
61
|
+
"pytest-repeat",
|
|
62
|
+
"pytest-benchmark",
|
|
63
|
+
"hypothesis",
|
|
64
|
+
]
|
|
65
|
+
lint = [
|
|
66
|
+
"ruff",
|
|
67
|
+
"ty",
|
|
68
|
+
"eof-fixer",
|
|
69
|
+
"typing-extensions",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
[tool.ruff]
|
|
73
|
+
fix = true
|
|
74
|
+
unsafe-fixes = true
|
|
75
|
+
line-length = 120
|
|
76
|
+
target-version = "py311"
|
|
77
|
+
extend-exclude = ["docs"]
|
|
78
|
+
|
|
79
|
+
[tool.ruff.lint]
|
|
80
|
+
select = ["ALL"]
|
|
81
|
+
ignore = [
|
|
82
|
+
"D1", # allow missing docstrings
|
|
83
|
+
"S101", # allow asserts
|
|
84
|
+
"TCH", # ignore flake8-type-checking
|
|
85
|
+
"FBT", # allow boolean args
|
|
86
|
+
"D203", # "one-blank-line-before-class" conflicting with D211
|
|
87
|
+
"D213", # "multi-line-summary-second-line" conflicting with D212
|
|
88
|
+
"COM812", # flake8-commas "Trailing comma missing"
|
|
89
|
+
"ISC001", # flake8-implicit-str-concat
|
|
90
|
+
]
|
|
91
|
+
isort.lines-after-imports = 2
|
|
92
|
+
isort.no-lines-before = ["standard-library", "local-folder"]
|
|
93
|
+
pylint.max-args = 10 # HTTP-method APIs are kwarg-rich (headers, params, cookies, timeout, json, content, response_model, …); default 5 is too strict.
|
|
94
|
+
|
|
95
|
+
[tool.ruff.lint.per-file-ignores]
|
|
96
|
+
# AsyncClient's HTTP-method `timeout=` is a config-value parameter forwarded to the transport,
|
|
97
|
+
# not asyncio.timeout territory. The rule fires on 24+ method signatures in this one file with
|
|
98
|
+
# the same false-positive justification; per-file disable is cleaner than 24 per-line noqa.
|
|
99
|
+
"src/httpware/client.py" = ["ASYNC109"]
|
|
100
|
+
|
|
101
|
+
[tool.pytest.ini_options]
|
|
102
|
+
addopts = "--cov=src/httpware --cov-report term-missing -m 'not perf'"
|
|
103
|
+
asyncio_mode = "auto"
|
|
104
|
+
pythonpath = ["src"]
|
|
105
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
106
|
+
markers = [
|
|
107
|
+
"perf: assertive performance tests (skipped by default; run with `pytest -m perf`)",
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
[tool.coverage]
|
|
111
|
+
run.concurrency = ["thread"]
|
|
112
|
+
run.omit = ["benchmarks/*"]
|
|
113
|
+
report.exclude_also = ["if typing.TYPE_CHECKING:"]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""httpware — resilience-first async HTTP client framework for Python."""
|
|
2
|
+
|
|
3
|
+
from httpware.client import AsyncClient
|
|
4
|
+
from httpware.config import ClientConfig, Limits, Timeout
|
|
5
|
+
from httpware.decoders import ResponseDecoder
|
|
6
|
+
from httpware.decoders.pydantic import PydanticDecoder
|
|
7
|
+
from httpware.errors import (
|
|
8
|
+
STATUS_TO_EXCEPTION,
|
|
9
|
+
BadRequestError,
|
|
10
|
+
ClientError,
|
|
11
|
+
ClientStatusError,
|
|
12
|
+
ConflictError,
|
|
13
|
+
ForbiddenError,
|
|
14
|
+
InternalServerError,
|
|
15
|
+
NotFoundError,
|
|
16
|
+
RateLimitedError,
|
|
17
|
+
ServerStatusError,
|
|
18
|
+
ServiceUnavailableError,
|
|
19
|
+
StatusError,
|
|
20
|
+
TimeoutError, # noqa: A004
|
|
21
|
+
TransportError,
|
|
22
|
+
UnauthorizedError,
|
|
23
|
+
UnprocessableEntityError,
|
|
24
|
+
)
|
|
25
|
+
from httpware.middleware import Middleware, Next, after_response, before_request, on_error
|
|
26
|
+
from httpware.request import Request
|
|
27
|
+
from httpware.response import Response, StreamResponse
|
|
28
|
+
from httpware.transports import Transport
|
|
29
|
+
from httpware.transports.httpx2 import Httpx2Transport
|
|
30
|
+
from httpware.transports.recorded import RecordedTransport
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"STATUS_TO_EXCEPTION",
|
|
35
|
+
"AsyncClient",
|
|
36
|
+
"BadRequestError",
|
|
37
|
+
"ClientConfig",
|
|
38
|
+
"ClientError",
|
|
39
|
+
"ClientStatusError",
|
|
40
|
+
"ConflictError",
|
|
41
|
+
"ForbiddenError",
|
|
42
|
+
"Httpx2Transport",
|
|
43
|
+
"InternalServerError",
|
|
44
|
+
"Limits",
|
|
45
|
+
"Middleware",
|
|
46
|
+
"Next",
|
|
47
|
+
"NotFoundError",
|
|
48
|
+
"PydanticDecoder",
|
|
49
|
+
"RateLimitedError",
|
|
50
|
+
"RecordedTransport",
|
|
51
|
+
"Request",
|
|
52
|
+
"Response",
|
|
53
|
+
"ResponseDecoder",
|
|
54
|
+
"ServerStatusError",
|
|
55
|
+
"ServiceUnavailableError",
|
|
56
|
+
"StatusError",
|
|
57
|
+
"StreamResponse",
|
|
58
|
+
"Timeout",
|
|
59
|
+
"TimeoutError",
|
|
60
|
+
"Transport",
|
|
61
|
+
"TransportError",
|
|
62
|
+
"UnauthorizedError",
|
|
63
|
+
"UnprocessableEntityError",
|
|
64
|
+
"after_response",
|
|
65
|
+
"before_request",
|
|
66
|
+
"on_error",
|
|
67
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Private cross-module helpers (not part of the public API)."""
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Middleware chain composition — wires a middleware list against a Transport.
|
|
2
|
+
|
|
3
|
+
Private helper. AsyncClient calls `compose` at construction time and stores the
|
|
4
|
+
returned `Next` callable; per-request dispatch awaits that callable.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
|
|
9
|
+
from httpware.middleware import Middleware, Next
|
|
10
|
+
from httpware.request import Request
|
|
11
|
+
from httpware.response import Response
|
|
12
|
+
from httpware.transports import Transport
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def compose(middlewares: Sequence[Middleware], transport: Transport) -> Next:
|
|
16
|
+
"""Fold `middlewares` into a single `Next` callable terminating at `transport`.
|
|
17
|
+
|
|
18
|
+
The outermost middleware in the input sequence is the first to receive the
|
|
19
|
+
request; its `next` argument forwards to the next middleware, and so on,
|
|
20
|
+
until the innermost middleware's `next` calls `transport.__call__`. An
|
|
21
|
+
empty sequence returns `transport.__call__` directly.
|
|
22
|
+
|
|
23
|
+
The returned callable is reusable across many requests; it captures
|
|
24
|
+
references to `middlewares` and `transport` by closure.
|
|
25
|
+
"""
|
|
26
|
+
chain: Next = transport.__call__
|
|
27
|
+
for middleware in reversed(middlewares):
|
|
28
|
+
chain = _wrap(middleware, chain)
|
|
29
|
+
return chain
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _wrap(middleware: Middleware, next_call: Next) -> Next:
|
|
33
|
+
async def _call(request: Request) -> Response:
|
|
34
|
+
return await middleware(request, next_call)
|
|
35
|
+
|
|
36
|
+
return _call
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
__all__ = ["compose"]
|