Python · Files and Streams

Files and Streams

from ucall import File, Path, ReplyStream 1.0.0-rc.1 · 60202332

Handlers run on the polling thread, so a reply or a pull that finishes later runs on a worker: request.detach() hands the request over as a ucall.DetachedRequest, which reads and replies like a Request and alone has the three verbs below.

VerbReturnsFinal when
reply_parts(parts, status, headers)None for bytes alone, else a ucall.Deferredevery part is sent
reply_stream(status, headers)a ucall.ReplyStream with append(parts), end()end() returns None or a Deferred
body_chunks()an iterator and async iterator of memoryviewsthe body ended

§ Serving a File

import concurrent.futures

import ucall

workers = concurrent.futures.ThreadPoolExecutor()
server = ucall.Server(ucall.Interface("0.0.0.0:8080", reply_directories=["/srv/videos"]))


def send_video(detached: ucall.DetachedRequest, name: str):
    deferred = detached.reply_parts([ucall.Path(0, name)])
    deferred.wait()  # or: await deferred


@server.get("/videos/{name}")
def video(request: ucall.Request):
    workers.submit(send_video, request.detach(), request.path_parameter("name"))

reply_parts concatenates bytes-like objects, ucall.Path(directory, path, offset=0, length=0) and ucall.File(descriptor, offset=0, length=0) into one body whose Content-Length is measured from the files. A Path resolves beneath the named reply_directories entry, and the event loop opens, reads and closes it unbuffered. A File is the caller’s regular file opened with os.O_DIRECT, or an object whose fileno() answers one; UCall never closes it, and the Deferred keeps it open until the reply is final. deferred.status() reads the outcome without waiting, deferred.wait() blocks without the interpreter lock and raises the failure, such as Status.NOT_FOUND after a 404, and await deferred runs that wait on the running loop’s default executor. Releasing an unfinished Deferred waits for it, which is why only a detached request makes one.

§ Streaming a Reply

def send_rows(detached: ucall.DetachedRequest, rows):
    stream = detached.reply_stream(headers=("content-type: text/csv",))
    for row in rows:
        stream.append([row.encode()])
    stream.end()

append queues more parts and raises ucall.Error with Status.WOULD_BLOCK while the peer lags behind what is queued; end() answers None or a Deferred as reply_parts does, and a stream released without end() ends itself.

§ Streaming an Upload

import hashlib


def store(detached: ucall.DetachedRequest):
    digest = hashlib.sha256()
    for chunk in detached.body_chunks():  # or: async for chunk in detached.body_chunks()
        digest.update(chunk)
    detached.reply(digest.hexdigest().encode())


@server.post("/uploads", body=ucall.BodyDelivery.STREAMED)
def upload(request: ucall.Request):
    workers.submit(store, request.detach())

An endpoint registered with body=ucall.BodyDelivery.STREAMED, on add_endpoint, route and every verb decorator, runs its handler once the headers arrived. body_chunks() yields each chunk as a memoryview valid until the next pull, which releases it, and waits for bytes without the interpreter lock. async for over it returns a chunk already decoded at once and runs the wait for one still arriving on the running loop’s default executor, as await deferred does.

API

ClassFileA byte range of a file the caller opened, as one part of DetachedRequest.reply_parts.4
lib.pyi:592 ↗
class File

The event loop reads it unbuffered after the call returns, so the reply defers, and the Deferred keeps this part, and through it a file object, alive until it is final. UCall never closes the descriptor.

Examples

>>> part = ucall.File(os.open("/srv/videos/intro.mp4", os.O_RDONLY | os.O_DIRECT))
File
lib.pyi:608 ↗
File(descriptor: Any, offset: int = 0, length: int = 0)
descriptorThe descriptor or its file object.
lib.pyi:610 ↗
descriptor: Any
offsetThe range's first byte.
lib.pyi:613 ↗
offset: int
lengthThe range's bytes; 0 runs to the end.
lib.pyi:616 ↗
length: int
ClassPathA byte range of a file beneath a reply directory, as one part of reply_parts.5
lib.pyi:619 ↗
class Path

The event loop opens it unbuffered beneath the directory and closes it once sent, so the reply defers. Absolute paths, ".." and any resolution leaving the directory are refused.

Examples

>>> part = ucall.Path(0, "intro.mp4", offset=1 << 20)
Path
lib.pyi:635 ↗
Path(
    directory: int,
    path: str | bytes | os.PathLike[str] | os.PathLike[bytes],
    offset: int = 0,
    length: int = 0,
)
directoryThe position of the reply directory.
lib.pyi:643 ↗
directory: int
pathThe file beneath the directory.
lib.pyi:646 ↗
path: str | bytes
offsetThe range's first byte.
lib.pyi:649 ↗
offset: int
lengthThe range's bytes; 0 runs to the end.
lib.pyi:652 ↗
length: int
ClassReplyStreamA reply sent in parts, from DetachedRequest.reply_stream, ended once.2
lib.pyi:886 ↗
class ReplyStream

Released without end(), it ends the reply itself and waits for any file parts to send.

Examples

>>> stream = detached.reply_stream(headers=("content-type: application/x-ndjson",))
>>> for row in rows:
...     stream.append([json.dumps(row).encode(), b"\n"])
>>> stream.end()
appendQueues parts after the ones before, all of them or none.
lib.pyi:897 ↗
def append(parts: Sequence[Buffer | File | Path], /) -> None

Bytes-like parts are copied before the call returns; File parts stay alive with the stream until its end is final.

Parameters

partsSequence[Buffer | File | Path]The next parts of the body.

Raises

Examples

>>> stream.append([b"data: ", payload, b"\n\n"])
endCompletes the reply, consuming the request whatever happens.
lib.pyi:913 ↗
def end() -> Deferred | None

Returns

Deferred | None: The Deferred of a stream that appended file or path parts, which may already be final, or None for bytes alone.

Raises

Examples

>>> deferred = stream.end()