mxhttp
Declarative HTTP client on top of msgspec and httpx. Write an API as a class of annotated stub methods and mxhttp will handle the rest (request building, sending, and response decoding).
Install
pip install mxhttp
Usage
from typing import Annotated
import msgspec
from mxhttp import Body, Query, SyncConsumer, get, post
class Item(msgspec.Struct):
id: int
name: str
price: float
class NewItem(msgspec.Struct):
name: str
price: float
class Shop(SyncConsumer):
@get("/items/{item_id}")
def get_item(self, item_id: int) -> Item: ... # type: ignore[empty-body]
@get("/search")
def search(self, q: Annotated[str, Query], limit: Annotated[int, Query] = 20) -> list[Item]: ... # type: ignore[empty-body]
@post("/items")
def create_item(self, item: Annotated[NewItem, Body]) -> Item: ... # type: ignore[empty-body]
shop = Shop("https://api.example.com")
item = shop.get_item(item_id=7)
new = shop.create_item(item=NewItem(name="Gadget", price=4.5))
The method body is never run as it is replaced by the decorator. Parameters are bound based on their Annotated[...] marker:
| Marker class | Request Target | Info |
|---|---|---|
Path (or implicit) |
Path | Matched by parameter name unless annotated explicitly (Path["name"]). Must be a non-nullable str, int, or float. |
Query |
Query | Must be a nullable str, int, float, or bool, or a Sequence of those, sent as key=a&key=b&.... |
Field |
Form Field | application/x-www-form-urlencoded. Accepts same types as Query. |
Part |
Multipart File Part | Forces the whole request to be multipart and any Field params on the same call will become multipart fields as well. Accepts the same types httpx takes for files=. |
Header |
HTTP Header | Must be str, int, float, or bool, but no list of those. |
Cookie |
Cookie | Is superseded by the cookie jar of the client if it already has a same-named cookie, unless override=True is set. Accepts same types as Header |
Body |
JSON Body | Whole object, serialized with msgspec.to_builtins. Can't be a scalar type. |
- Use
Path["name"],Query["name"],Field["name"],Header["name"], orCookie["name"]to bind under a different name than the parameter (e.g. reservedfrom, or a header likeX-Request-Id, unsupported string format arguments like?). None-valuedQuery,Field,Header, andCookieparameters are omitted from the request.Pathparameters cannot be optional as a placeholder cannot be ommited from the URL.- Mismatched marker/type combinations raise a
TypeErroras soon as the class body runs, not at call time.
Decoding the response
The return type defines the reponse decoding:
httpx.Responsefor the raw response.strorbytesfor the corresponding.textor.contentwith no JSON round-trip.pydantic.BaseModelsubclasses via their own.model_validate_json.- Anything else
msgspec.json.decodecan decode:msgspec.Struct, dataclasses,TypedDict,NamedTuple, andlist,dict, or other containers of those. Response[Item]for a small struct with the decodedItemas.dataand the rawhttpx.Responsein.response.- Plain
attrsclasses are decoded bymsgspec, for type hintingattrsis needed as dependency.
For an async client, subclass AsyncConsumer and declare the methods async def, everything else stays the same.
Response handling
By default, every response is checked by response.raise_for_status() before decoding, so errors during the request raise httpx.HTTPStatusError automatically. This behavior can be overriden by @response_handler decorator for the class.
import httpx
from mxhttp import response_handler
def ignore_errors(response: httpx.Response) -> httpx.Response:
return response
@response_handler(ignore_errors)
class Shop(SyncConsumer): ...
The hook runs on every response before decoding.
Streaming responses
Annotate the return type as Iterator[bytes] (sync) or AsyncIterator[bytes] (async) to stream the response body in chunks.
from collections.abc import AsyncIterator, Iterator
class Files(SyncConsumer):
@get("/files/{file_id}")
def download(self, file_id: int) -> Iterator[bytes]: ... # type: ignore[empty-body]
for chunk in shop_files.download(file_id=7):
...
class AsyncFiles(AsyncConsumer)
@get("/files/{file_id}")
def download(self, file_id: int) -> AsyncIterator[bytes]: ... # type: ignore[empty-body]
async for chunk in await shop_async_files.download(file_id=7):
...
httpx already decompresses chunks before responding according to Content-Encoding (gzip/deflate/br/zstd).
Streaming responses run @streaming_response_handler instead of @response_handler (defaults to raise_for_status as well).
The handler can only inspect status line and headers.
from mxhttp import streaming_response_handler
def check_status(response: httpx.Response) -> httpx.Response:
response.raise_for_status()
return response
@streaming_response_handler(check_status)
class Files(SyncConsumer): ...
Server-Sent Events
Annotate the return type as Iterator[Event] (sync) or AsyncIterator[Event] (async) to parse the response as a Server-Sent Events stream instead of raw bytes:
from collections.abc import Iterator
from mxhttp import Event
class Chat(SyncConsumer):
@get("/stream")
def events(self) -> Iterator[Event]: ... # type: ignore[empty-body]
for event in chat.events():
print(event.event, event.data) # event.event defaults to "message"
Event has four attributes, data, event, id, and retry:
datais the raw payload, decode it manually if the server sends JSON.- Multi-line
datafields are joined with\n. idandretrypersist across events once set and reset on reconnect only.- An event without a trailing blank line at the end of the stream is discarded.
SSE streams use @streaming_response_handler matching byte streaming above.
Further configuration
The underlying httpx.Client or httpx.AsyncClient is stored at .session to set default headers, auth, or timeouts.
Typing
The package and all its generators are typed.
Tests
pytest
Acknowledgements
mxhttp is inspired by Uplink but combining it with Python typing features.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mxhttp-1.0.0.tar.gz.
File metadata
- Download URL: mxhttp-1.0.0.tar.gz
- Upload date:
- Size: 24.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
68f122303bad2cf3ca9f47cbf14527eb9bf7634ec4e53df6ab2c3a0810ec118c
|
|
| MD5 |
5f60010811c7aa702b9abd41c9471e14
|
|
| BLAKE2b-256 |
3cb497ed53086d91b2605276e3404a9b496c18fbc92dcff313a68a04b1351ca1
|
Provenance
The following attestation bundles were made for mxhttp-1.0.0.tar.gz:
Publisher:
release.yml on audivir/mxhttp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mxhttp-1.0.0.tar.gz -
Subject digest:
68f122303bad2cf3ca9f47cbf14527eb9bf7634ec4e53df6ab2c3a0810ec118c - Sigstore transparency entry: 2571160953
- Sigstore integration time:
-
Permalink:
audivir/mxhttp@2cf4f82e4a30a0419e3782f914f84ca42870e92b -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/audivir
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@2cf4f82e4a30a0419e3782f914f84ca42870e92b -
Trigger Event:
release
-
Statement type:
File details
Details for the file mxhttp-1.0.0-py3-none-any.whl.
File metadata
- Download URL: mxhttp-1.0.0-py3-none-any.whl
- Upload date:
- Size: 16.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5dc33fc2d016d01625598bc3d4c6348a4445979609ece9df956649703ee0a57f
|
|
| MD5 |
3f4edbb8aef45a35a9d8dbcfd5a13317
|
|
| BLAKE2b-256 |
3ff2d37309877f74e7a123b01b368ab54a0f2c258cf1edc3d7828823f6d592cb
|
Provenance
The following attestation bundles were made for mxhttp-1.0.0-py3-none-any.whl:
Publisher:
release.yml on audivir/mxhttp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mxhttp-1.0.0-py3-none-any.whl -
Subject digest:
5dc33fc2d016d01625598bc3d4c6348a4445979609ece9df956649703ee0a57f - Sigstore transparency entry: 2571160978
- Sigstore integration time:
-
Permalink:
audivir/mxhttp@2cf4f82e4a30a0419e3782f914f84ca42870e92b -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/audivir
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@2cf4f82e4a30a0419e3782f914f84ca42870e92b -
Trigger Event:
release
-
Statement type: