Python · Requests

Requests

import ucallfrom ucall import Request, RequestPart, Deferred, … 1.0.0-rc.1 · 60202332

A Request is what a handler annotated to take one receives. Its reads copy out of the received bytes: verb, path, query_string, path_parameter(name), query(name) and header(name), with peer naming where it came from. path, query_string, path parameters and query values stay percent-encoded as sent, header names match case-insensitively, a lookup that finds nothing answers None, and the body arrives whole, decoded from chunked framing if it was sent that way. fields() lists every header field in arrival order as (name, value) bytes, names lowercased and values trimmed, as a proxy forwards a head. peer is a tuple of the interface’s index in the server’s arguments, the client’s 32-byte static key, 32 zero bytes on a plaintext interface, and its address: an (ipaddress, port) pair, "" for an AF_UNIX peer, or None.

import ucall

server = ucall.Server(ucall.Interface("127.0.0.1:0"))


@server.post("/orders/{order}")
def order(request: ucall.Request):
    token = request.header("authorization")
    if token is None:
        request.reply_error(401, "sign in first\n")
        return
    receipt = f"{request.path_parameter('order')} by {token}".encode()
    request.reply(receipt, status=201, headers=("content-type: text/plain", "cache-control: no-store"))

request.body is a read-only memoryview over the received bytes, and the request itself supports the buffer protocol, so bytes(request) copies the body and numpy.frombuffer(request, numpy.int8) views it without a copy. Both views stay valid until the reply, which frees those bytes, so a reply is refused with BufferError while any of them is alive. The one view a reply accepts is request.body itself passed as the reply body, which it releases first, so request.reply(request.body) echoes without a copy.

reply(body, status, headers) sends any bytes-like body, copied before it returns, with “name: value” header lines, and Content-Length, Date and Server are generated; reply_error(status, message) sends a plain-text error. Either consumes the request, so a second reply raises ucall.Error with Status.ALREADY_EXISTS, and later reads raise ucall.Error or, for the body, BufferError. A request may be kept past its handler and answered later, from any thread, once, while responses still leave each connection in request order. A request released without a reply answers 500. A reply to a closed connection still consumes the request silently; any other failed reply raises ucall.Error and keeps it.

request.path_segments is a tuple of the path’s components as bytes, and request.parts a tuple of ucall.RequestPart(name, content_type, data) for a multipart/form-data body. Each part’s data views the received bytes like body does, and keeps the request alive and unanswered until every such view is gone.

API

Types

BodyChunksThe iterator DetachedRequest.body_chunks returns, yielding memoryviews of the body.
__init__.py:928 ↗
class BodyChunks(Iterator[memoryview], AsyncIterator[memoryview])

async for returns a chunk already decoded at once and waits for one still arriving in the running loop's default executor; for waits on the calling thread.

Examples

>>> async for chunk in detached.body_chunks():  # or: for chunk in detached.body_chunks()
...     digest.update(chunk)
ClassRequestOne received HTTP request, retained until a reply consumes it.14
lib.pyi:426 ↗
class Request(Buffer)

A handler taking one parameter annotated Request receives it on the polling thread, and may keep it to answer later from any thread, once, or detach it to a worker for the replies that may defer. body, parts and the buffer protocol view the received bytes without a copy until the reply, which is refused while such a view is alive, except the body passed as the reply body itself. A request released without a reply answers 500.

Examples

>>> @server.put("/echo/{name}")
... def echo(request: ucall.Request):
...     interface_index, static_key = request.peer
...     greeting = f"{request.verb.name} {request.path_parameter('name')}"
...     request.reply(request.body, headers=(f"x-greeting: {greeting}",))
*× 2Every header field line in arrival order, repeated names apart, as a proxy forwards a head.
lib.pyi:477 ↗
def fields() -> list[tuple[bytes, bytes]]

Returns

list[tuple[bytes, bytes]]: Each name, lowercased as the parser keeps it, with its value trimmed.

Examples

>>> [name for name, _ in request.fields()]
[b'host', b'accept']
*× 2Looks up a query parameter of the request by its case-sensitive name.
lib.pyi:488 ↗
def query(name: str | bytes, /) -> str | None

Parameters

namestr | bytesThe parameter name.

Returns

str | None: The value still percent-encoded, which urllib.parse.unquote_plus decodes, or None when the query has no such parameter.

Examples

>>> @server.get("/search")
... def search(request: ucall.Request):
...     return urllib.parse.unquote_plus(request.query("q") or "")
path_*_parameter× 2Looks up a placeholder the endpoint's URI template captured.
lib.pyi:503 ↗
def path__parameter(name: str | bytes, /) -> str | None

Parameters

namestr | bytesThe placeholder name without braces, "path" for {*path}.

Returns

str | None: The captured text as sent, or None when the template has no such placeholder.

Examples

>>> @server.get("/files/{*path}")
... def files(request: ucall.Request):
...     return request.path_parameter("path")
*× 2Answers the request, once, from any thread; a peer that disconnected consumes it silently.
lib.pyi:518 ↗
def reply(
    body: Buffer = b'',
    status: int = 200,
    headers: Sequence[str | bytes] = (),
) -> None

Parameters

bodyBufferAny bytes-like object, copied before the call returns; the request's own body passes without a copy and is released by the reply.
statusintThe HTTP status. Defaults to 200.
headersSequence[str | bytes]"name: value" lines; the server adds Content-Length, Date and Server itself.

Raises

Examples

>>> @server.post("/echo")
... def echo(request: ucall.Request):
...     request.reply(request.body, headers=("content-type: application/octet-stream",))
reply_*_error× 2Answers the request with an error status and a plain-text message, once.
lib.pyi:537 ↗
def reply__error(status: int, message: str | bytes) -> None

Parameters

statusintThe HTTP status, 400 or above.
messagestr | bytesThe body, sent as text/plain.

Raises

Examples

>>> @server.get("/teapot")
... def teapot(request: ucall.Request):
...     request.reply_error(418, "short and stout\n")
detachHands the request to another thread, the one way to the verbs whose replies may defer.
lib.pyi:553 ↗
def detach() -> DetachedRequest

The DetachedRequest owns the request from then on, and this object answers nothing. A Deferred waits when released, and a wait on the polling thread would wait for the loop running it, so pass the detached request to a worker rather than replying here.

Raises

Examples

>>> @server.get("/videos/{name}")
... def video(request: ucall.Request):
...     workers.submit(send_video, request.detach(), request.path_parameter("name"))
verbThe Verb it arrived with, never Verb.ANY.
lib.pyi:443 ↗
verb: Verb
pathThe path as sent, still percent-encoded, without the query.
lib.pyi:446 ↗
path: str
bodyA read-only memoryview of the body, valid until the reply.
lib.pyi:449 ↗
body: memoryview
peerThe index of the interface it arrived on, the peer's 32-byte Noise key, zeros on plaintext, and its address: (ipaddress, port), "" for AF_UNIX, or None.
lib.pyi:452 ↗
peer: tuple[int, bytes, tuple[IPv4Address | IPv6Address, int] | str | None]
partsThe RequestParts of a multipart/form-data body, else empty.
lib.pyi:455 ↗
parts: tuple[RequestPart, ...]
path_segmentsThe path's components between slashes, as sent.
lib.pyi:458 ↗
path_segments: tuple[bytes, ...]
query_stringThe query after the first ?, still percent-encoded, or "" without one.
lib.pyi:461 ↗
query_string: str
ClassRequestPartOne part of a multipart/form-data request body, from Request.parts.3
lib.pyi:571 ↗
class RequestPart

data views the received bytes without a copy and keeps the request alive, so the request refuses its reply until every such view is gone, as it does for views over body.

Examples

>>> @server.post("/upload")
... def upload(request: ucall.Request):
...     return {part.name: len(part.data) for part in request.parts}
nameThe name from Content-Disposition.
lib.pyi:583 ↗
name: str
content_typeThe part's Content-Type, empty if absent.
lib.pyi:586 ↗
content_type: str
dataA read-only memoryview of the payload, which refuses the request's reply while alive.
lib.pyi:589 ↗
data: memoryview
ClassDeferredA reply still sending file or path parts, from reply_parts or ReplyStream.end.2
lib.pyi:655 ↗
class Deferred(Awaitable[None])

It owns the reply's outcome and keeps the File parts it borrows alive until the outcome is final. await it in a coroutine, or call wait(); released unfinished, it waits, so keep it off the polling thread, as Request.detach does.

Examples

>>> deferred = detached.reply_parts([b"--frame\r\n", ucall.Path(0, "intro.mp4")])
>>> deferred.wait()  # or: await deferred
statusReads the reply's outcome without waiting.
lib.pyi:666 ↗
def status() -> Status

Returns

Status.DEFERRED while parts still send, then the final one.

Examples

>>> deferred.status() in [ucall.Status.DEFERRED, ucall.Status.SUCCESS]
True
waitBlocks until every part is sent, without the interpreter lock.
lib.pyi:676 ↗
def wait() -> None

Raises

Examples

>>> deferred.wait()
ClassDetachedRequestA request taken off its polling thread by Request.detach, owning the replies that may defer.10
lib.pyi:688 ↗
class DetachedRequest(Buffer)

It reads and answers as Request does, and alone offers reply_parts, reply_stream and body_chunks, whose Deferred replies and waiting pulls belong on a worker thread. Released without a reply, it answers 500.

Examples

>>> def send_video(detached: ucall.DetachedRequest, name: str):
...     deferred = detached.reply_parts([b"--frame\r\n", ucall.Path(0, name)])
...     deferred.wait()  # or: await deferred
reply_partsAnswers the request, once, with parts concatenated into the body.
lib.pyi:811 ↗
def reply_parts(
    parts: Sequence[Buffer | File | Path],
    status: int = 200,
    headers: Sequence[str | bytes] = (),
) -> Deferred | None

Bytes-like parts are copied before the call returns, and the reply is then final. A File or Path part is read by the event loop afterwards, sized first so Content-Length is measured, and its reply defers: one failing before the head replaces the reply with 404 or 416, and one failing after it resets the connection.

Parameters

partsSequence[Buffer | File | Path]The body, in order.
statusintThe HTTP status. Defaults to 200.
headersSequence[str | bytes]"name: value" lines; the server adds Content-Length, Date and Server itself.

Returns

Deferred | None: The Deferred of a reply with file or path parts, which may already be final, or None for bytes alone, final on return.

Raises

  • BufferError: A view over the request's bytes is still alive.
  • Error: A refused part, such as a descriptor without O_DIRECT, or a request already answered, with Status.ALREADY_EXISTS.

Examples

>>> deferred = detached.reply_parts([b"--frame\r\n", ucall.Path(0, "intro.mp4")])
>>> deferred.wait()  # or: await deferred
reply_streamAnswers the request with a body sent in parts, emitting the status line and headers now.
lib.pyi:843 ↗
def reply_stream(
    status: int = 200,
    headers: Sequence[str | bytes] = (),
) -> ReplyStream

The body is chunked for HTTP/1.1 peers, unless headers declare Content-Length, which the parts must then sum to. A peer that left already still opens the stream, whose parts drop.

Parameters

statusintThe HTTP status. Defaults to 200.
headersSequence[str | bytes]"name: value" lines.

Returns

The open reply, which owns the request from then on.

Raises

  • BufferError: A view over the request's bytes is still alive.
  • Error: A malformed header, or a request already answered, with Status.ALREADY_EXISTS.

Examples

>>> stream = detached.reply_stream(headers=("content-type: text/event-stream",))
>>> stream.append([b"data: 1\n\n"])
>>> stream.end()
body_chunksIterates the request body as it arrives, each chunk the bytes decoded since the last pull.
lib.pyi:865 ↗
def body_chunks() -> BodyChunks

Each chunk is a read-only memoryview valid until the next pull, which releases it, and a pull waits for bytes without the interpreter lock. On a buffered endpoint the first chunk is the whole body.

Returns

The iterator, ending once the body did.

Raises

Examples

>>> async for chunk in detached.body_chunks():  # or: for chunk in detached.body_chunks()
...     digest.update(chunk)
verbThe Verb it arrived with, never Verb.ANY.
lib.pyi:701 ↗
verb: Verb
pathThe path as sent, still percent-encoded, without the query.
lib.pyi:704 ↗
path: str
bodyA read-only memoryview of the body, valid until the reply.
lib.pyi:707 ↗
body: memoryview
peerThe index of the interface it arrived on, the peer's 32-byte Noise key, zeros on plaintext, and its address: (ipaddress, port), "" for AF_UNIX, or None.
lib.pyi:710 ↗
peer: tuple[int, bytes, tuple[IPv4Address | IPv6Address, int] | str | None]
partsThe RequestParts of a multipart/form-data body, else empty.
lib.pyi:713 ↗
parts: tuple[RequestPart, ...]
path_segmentsThe path's components between slashes, as sent.
lib.pyi:716 ↗
path_segments: tuple[bytes, ...]
query_stringThe query after the first ?, still percent-encoded, or "" without one.
lib.pyi:719 ↗
query_string: str