httpware 0.1.0__py3-none-any.whl

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,84 @@
1
+ """RecordedTransport — built-in Transport test double."""
2
+
3
+ from collections.abc import Mapping
4
+ from contextlib import AbstractAsyncContextManager
5
+
6
+ from httpware.request import Request
7
+ from httpware.response import Response, StreamResponse
8
+
9
+
10
+ class RecordedTransport:
11
+ """Built-in Transport test double.
12
+
13
+ Construct with a route table mapping (method, url) → Response | BaseException.
14
+ `await transport(request)` looks up `(request.method.upper(), request.url)`; on
15
+ match returns the Response or raises the Exception. On no-match, uses the
16
+ `default` (Response, BaseException, or RuntimeError("No route for METHOD URL")
17
+ when None).
18
+
19
+ Every call appends the Request to `transport.requests`. Tests can assert on
20
+ `transport.last_request`, iterate `transport.requests`, or count
21
+ `transport.aclose_calls` for lifecycle assertions.
22
+
23
+ Routes fire indefinitely — the same (method, url) yields the same canned
24
+ Response on every match. To express "different replies on repeat calls",
25
+ swap the route between calls via `add_route(...)` or construct a new
26
+ transport per call.
27
+
28
+ Route and default values may be `BaseException` (not just `Exception`) so
29
+ test code can express `asyncio.CancelledError`, `SystemExit`, or
30
+ `KeyboardInterrupt` — useful for cancellation/shutdown propagation tests.
31
+ These do NOT get caught by user code's `except Exception:`.
32
+
33
+ `stream()` raises NotImplementedError; streaming lands in Epic 4 (Story 4-1).
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ routes: Mapping[tuple[str, str], Response | BaseException] | None = None,
39
+ *,
40
+ default: Response | BaseException | None = None,
41
+ ) -> None:
42
+ self._routes: dict[tuple[str, str], Response | BaseException] = (
43
+ {(m.upper(), u): v for (m, u), v in routes.items()} if routes is not None else {}
44
+ )
45
+ self._default = default
46
+ self.requests: list[Request] = []
47
+ self.aclose_calls = 0
48
+
49
+ @property
50
+ def last_request(self) -> Request | None:
51
+ """The most recently observed Request, or None if no calls have been made."""
52
+ return self.requests[-1] if self.requests else None
53
+
54
+ def add_route(
55
+ self,
56
+ method: str,
57
+ url: str,
58
+ response_or_exception: Response | BaseException,
59
+ ) -> None:
60
+ """Add or replace a route entry."""
61
+ self._routes[(method.upper(), url)] = response_or_exception
62
+
63
+ async def __call__(self, request: Request) -> Response:
64
+ self.requests.append(request)
65
+ key = (request.method.upper(), request.url)
66
+ result: Response | BaseException | None
67
+ result = self._routes.get(key, self._default)
68
+ if isinstance(result, BaseException):
69
+ raise result
70
+ if result is None:
71
+ msg = f"No route for {request.method} {request.url}"
72
+ raise RuntimeError(msg)
73
+ return result
74
+
75
+ def stream(
76
+ self,
77
+ request: Request,
78
+ ) -> AbstractAsyncContextManager[StreamResponse]:
79
+ """Streaming not implemented in v0 — landing in Epic 4 (Story 4-1)."""
80
+ msg = "RecordedTransport.stream() is not implemented; streaming lands in Epic 4"
81
+ raise NotImplementedError(msg)
82
+
83
+ async def aclose(self) -> None:
84
+ self.aclose_calls += 1
@@ -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
+ [![Test](https://github.com/modern-python/httpware/actions/workflows/ci.yml/badge.svg)](https://github.com/modern-python/httpware/actions/workflows/ci.yml)
36
+ [![PyPI version](https://badge.fury.io/py/httpware.svg)](https://pypi.org/project/httpware/)
37
+ [![Python versions](https://img.shields.io/pypi/pyversions/httpware.svg)](https://pypi.org/project/httpware/)
38
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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).
@@ -0,0 +1,20 @@
1
+ httpware/__init__.py,sha256=ou3ZG7AJNUWby5VT8eUDIEVhWs__5eA_t6TteXsMSCg,1733
2
+ httpware/_internal/__init__.py,sha256=xRyN_CoQ1XZCzo2h15x2v5vZd3buM5n5MsMi4qGfj5o,65
3
+ httpware/_internal/chain.py,sha256=u5IUs-LLMfCIrMSYT3srHwY3AsJc2_5yFRnKx7gdYFE,1383
4
+ httpware/_internal/import_checker.py,sha256=XWgqP9TJO9O3TjYAMPsoAR8cLxRVmMSMCMuIxKCvzUY,195
5
+ httpware/client.py,sha256=c9dSj0IAU7zsOmtLLn2h22EGu1BVwMPaFampIoYwOU0,19829
6
+ httpware/config.py,sha256=MmtLbb8C8x3r3nAPWo9oyR843xeUqnMcJ2APjyMnB1A,1190
7
+ httpware/decoders/__init__.py,sha256=ZriYOpgse0cd9K_R4u0Bo8xKQdOcafhvcIdUmBa_sFA,470
8
+ httpware/decoders/msgspec.py,sha256=4ohALyeumX3DcosFEDVScZ_GtzdO7LaIzgPG0mhNhsE,1066
9
+ httpware/decoders/pydantic.py,sha256=fxwkc1_njsWG8yD6FbHJHLX1khy-jYq7GNsUpHnm0H0,756
10
+ httpware/errors.py,sha256=yvQ6Z1MujYhxguVI_Q26mim7xbAu2MKuEuDUXfm0gmo,6079
11
+ httpware/middleware/__init__.py,sha256=6jR0s_lKmd3dDaNg4vxbDDEUlAA06C6ypvDd9ZcXp_c,3262
12
+ httpware/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ httpware/request.py,sha256=LISPQH5FbxM5P3anycw0kFd8VxOA1fDjNfnjr8E1e3I,2458
14
+ httpware/response.py,sha256=VbavHF5ZCX3VnMXbQfoI3U91Qvqe263drGRKGV-NATI,1998
15
+ httpware/transports/__init__.py,sha256=tZ414G7aXs8MAHs5hqubRBmrrdF_naRlw5Jp89gBqro,831
16
+ httpware/transports/httpx2.py,sha256=gTMfM98_UOq2mK_ZV7k8DZJw8XjTldI2z_TmsezhQxg,7082
17
+ httpware/transports/recorded.py,sha256=RV_82s4bqTSXzYbfp7npWCKLwqT51UYWAq-1YVph63c,3302
18
+ httpware-0.1.0.dist-info/WHEEL,sha256=Q9FtwzuR2QE37l-JIkuyklGnJJiCBHKnsPVQ9vzCMzQ,81
19
+ httpware-0.1.0.dist-info/METADATA,sha256=EkZq_GMtftccfjoTxLK4tgdEnA5Xy1I9ZJbIrArIGzs,4430
20
+ httpware-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.17
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any