Python · Calling

Calling

from ucall import Response, Completed, Client 1.0.0-rc.1 · 60202332

A Client submits calls without driving the network, and polls to drive them and collect what finished. It holds up to connections connections, reusing them across calls, and carries up to pipelined_calls calls on each.

import threading

import ucall

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


@server.post("/echo")
def echo(request: ucall.Request):
    return request.body


@server.post("/sum")
def total(a: int, b: int):
    return a + b


runner = threading.Thread(target=server.run)
runner.start()
base = f"http://{server.addresses[0]}"

client = ucall.Client(connections=4, pipelined_calls=16)
for index in range(64):
    client.submit("POST", f"{base}/echo", bytes([index]), context=index)
remaining = 64
while remaining:
    for completed in client.take_completed(timeout_microseconds=1_000_000):
        assert completed.status == ucall.Status.SUCCESS
        assert bytes(completed.response.body) == bytes([completed.context])
        remaining -= 1

response = client.post(f"{base}/sum", json={"a": 1, "b": 2})
print(response.status_code, response.header("content-type"), bytes(response.body))
server.stop()
runner.join()

submit(verb, url, body, headers, security=..., noise_peer_key=..., context=...) borrows its URL, header lines, body and key until the call completes, and tags it with any context object. A call admitted once completes exactly once, and a client already holding calls unretrieved calls refuses more with ucall.Error and Status.CONTENDED; calls defaults to connections times pipelined_calls. take_completed(timeout_microseconds, capacity) drives every admitted call and returns up to capacity finished ones as Completed objects, waiting up to its timeout for the first, and a zero timeout performs one nonblocking round. A Completed carries the context, the final status, a message on failure, and the response, None on failure.

request, get, post, put, delete and patch submit one call and poll until it finishes, keeping the completions of other calls they retrieve meanwhile for the next take_completed, which hands those out first. They take json= to send a value as JSON with its content type, and raise ucall.Error when the exchange fails. Every calling method also takes this call’s timeout_microseconds=, max_retries= and max_redirects= in place of the client’s, and a noise_private_key= private key in place of its own.

A Client takes log=, log_level= and log_format= as a server does, reporting dials that fail every address, refused handshakes and refused redirects. max_response_bytes= caps the response a call buffers, failing the call with Status.OVERFLOW past it, and zero takes the largest receive buffer the kernel grants a socket. Host names resolve through the library’s own stub resolver, never blocking the calling thread on the system’s, and connections race the answered addresses as Happy Eyeballs does.

Every received response is a Response, whatever its HTTP status, and only a failed exchange is an error; fields() lists its header fields in arrival order as a request’s does. A Response owns its received bytes, so it stays readable after its Client is gone, and supports the buffer protocol as a request does: bytes(response) copies its body and response.body views it. Releasing the client cancels the calls still in flight and discards completions not yet retrieved.

API

ClassResponseOne received HTTP response, which owns its bytes and outlives its client.4
lib.pyi:1347 ↗
class Response(Buffer)

body and the buffer protocol view the received bytes without a copy.

Examples

>>> response = client.post("http://127.0.0.1:8545/sum", json={"a": 1, "b": 2})
>>> response.status_code, bytes(response.body), json.loads(bytes(response))
(200, b'3', 3)
fieldsEvery header field line of the response in arrival order, repeated names apart.
lib.pyi:1376 ↗
def fields() -> list[tuple[bytes, bytes]]

Returns

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

Examples

>>> dict(client.get(url).fields())[b'content-type']
b'text/plain'
status_codeThe HTTP status the server sent.
lib.pyi:1358 ↗
status_code: int
bodyA read-only memoryview of the body, alive as long as it is.
lib.pyi:1361 ↗
body: memoryview
ClassCompletedThe final result of one accepted client call, from Client.take_completed.4
lib.pyi:1389 ↗
class Completed

status is Status.SUCCESS once any response arrived, whatever its HTTP status, and the failure otherwise, such as Status.TIMEOUT, with response None.

Examples

>>> client.submit("GET", "http://127.0.0.1:8545/health", context="health")
>>> for completed in client.take_completed(timeout_microseconds=1_000_000):
...     print(completed.context, completed.status.name)
health SUCCESS
contextThe context given to submit.
lib.pyi:1402 ↗
context: Any
statusThe final Status of the exchange.
lib.pyi:1405 ↗
status: Status
messageThe detail of a failure, or None.
lib.pyi:1408 ↗
message: str | None
responseThe Response, or None on failure.
lib.pyi:1411 ↗
response: Response | None
ClassClientAn HTTP client that belongs to the thread creating it, which alone may submit and poll.9
lib.pyi:1414 ↗
class Client

Raises

  • TypeError: A missing connections, or an argument of the wrong type.
  • Error: The backend cannot start.

Examples

>>> client = ucall.Client(connections=4, pipelined_calls=16)
>>> client.get("http://127.0.0.1:8545/health").status_code
200
Client
lib.pyi:1451 ↗
Client(
    *,
    connections: int,
    pipelined_calls: int = 0,
    calls: int = 0,
    connect_timeout_microseconds: int = 0,
    call_timeout_microseconds: int = 0,
    noise_private_key: bytes | None = None,
    peer_authorizer: Callable[[bytes], bool] | None = None,
    backend: Backend | str = Backend.AUTOMATIC,
    cpu_class: CPUClass | str = CPUClass.UNKNOWN,
    log: Callable[[LogLevel, str], object] | None = None,
    log_level: LogLevel | str = LogLevel.DEBUG,
    log_format: LogFormat | str = LogFormat.HUMAN,
    max_response_bytes: int = 0,
    server: Server | None = None,
    event_loop: int = 0,
)
requestSends one exchange and waits for its response on this thread.
lib.pyi:1470 ↗
def request(
    verb: Verb | str,
    url: str | bytes,
    /,
    body: Buffer = b'',
    *,
    json: object = None,
    headers: Sequence[str | bytes] = (),
    security: Security | str = Security.NONE,
    noise_peer_key: bytes | None = None,
    noise_private_key: bytes | None = None,
    timeout_microseconds: int = 0,
    max_retries: int = 0,
    max_redirects: int = 0,
) -> Response

It polls in slices of 100 ms, checking for signals between them, and keeps every other call it finishes meanwhile for the next take_completed.

Parameters

verbVerb | strThe method, such as Verb.POST or "post".
urlstr | bytesThe full URL, "http://" or "http+noise://" and the server.
bodyBufferAny bytes-like object to send.
jsonobjectA value to send as JSON in place of body, with its content type; None sends body.
headersSequence[str | bytes]"name: value" lines.
securitySecurity | strThe handshake for this call. Defaults to Security.NONE.
noise_peer_keybytes | NoneThe server's 32-byte static key, for Security.NOISE_IK and Security.NOISE_KK.
noise_private_keybytes | NoneThis call's 32-byte Noise private key in place of the client's own, as Security.NOISE_KK names the caller to the server.
timeout_microsecondsintThe budget of this call, queueing and redirects included; 0 takes the client's call_timeout_microseconds.
max_retriesintConnection attempts before sending; 0 takes 3.
max_redirectsintRedirect hops followed before failing; 0 takes 5.

Returns

The response, whatever its HTTP status.

Raises

Examples

>>> response = client.request("POST", "http://127.0.0.1:8545/sum", json={"a": 1, "b": 2})
>>> response.status_code
200
getSends a GET and waits for its response, as request does.
lib.pyi:1520 ↗
def get(
    url: str | bytes,
    /,
    *,
    body: Buffer = b'',
    json: object = None,
    headers: Sequence[str | bytes] = (),
    security: Security | str = Security.NONE,
    noise_peer_key: bytes | None = None,
    noise_private_key: bytes | None = None,
    timeout_microseconds: int = 0,
    max_retries: int = 0,
    max_redirects: int = 0,
) -> Response

Parameters

urlstr | bytesThe full URL.

Returns

The response, whatever its HTTP status.

Raises

  • Error: The exchange failed.

Examples

>>> bytes(client.get("http://127.0.0.1:8545/sum?a=1&b=2").body)
b'3'
postSends a POST and waits for its response, as request does.
lib.pyi:1551 ↗
def post(
    url: str | bytes,
    /,
    body: Buffer = b'',
    *,
    json: object = None,
    headers: Sequence[str | bytes] = (),
    security: Security | str = Security.NONE,
    noise_peer_key: bytes | None = None,
    noise_private_key: bytes | None = None,
    timeout_microseconds: int = 0,
    max_retries: int = 0,
    max_redirects: int = 0,
) -> Response

Parameters

urlstr | bytesThe full URL.
bodyBufferAny bytes-like object to send.

Returns

The response, whatever its HTTP status.

Raises

  • Error: The exchange failed.

Examples

>>> bytes(client.post("http://127.0.0.1:8545/sum", json={"a": 1, "b": 2}).body)
b'3.0'
putSends a PUT and waits for its response, as request does.
lib.pyi:1583 ↗
def put(
    url: str | bytes,
    /,
    body: Buffer = b'',
    *,
    json: object = None,
    headers: Sequence[str | bytes] = (),
    security: Security | str = Security.NONE,
    noise_peer_key: bytes | None = None,
    noise_private_key: bytes | None = None,
    timeout_microseconds: int = 0,
    max_retries: int = 0,
    max_redirects: int = 0,
) -> Response

Parameters

urlstr | bytesThe full URL.
bodyBufferAny bytes-like object to send.

Returns

The response, whatever its HTTP status.

Raises

  • Error: The exchange failed.

Examples

>>> client.put("http://127.0.0.1:8545/files/notes.txt", b"hello").status_code
200
deleteSends a DELETE and waits for its response, as request does.
lib.pyi:1615 ↗
def delete(
    url: str | bytes,
    /,
    *,
    body: Buffer = b'',
    json: object = None,
    headers: Sequence[str | bytes] = (),
    security: Security | str = Security.NONE,
    noise_peer_key: bytes | None = None,
    noise_private_key: bytes | None = None,
    timeout_microseconds: int = 0,
    max_retries: int = 0,
    max_redirects: int = 0,
) -> Response

Parameters

urlstr | bytesThe full URL.

Returns

The response, whatever its HTTP status.

Raises

  • Error: The exchange failed.

Examples

>>> client.delete("http://127.0.0.1:8545/files/notes.txt").status_code
204
patchSends a PATCH and waits for its response, as request does.
lib.pyi:1646 ↗
def patch(
    url: str | bytes,
    /,
    body: Buffer = b'',
    *,
    json: object = None,
    headers: Sequence[str | bytes] = (),
    security: Security | str = Security.NONE,
    noise_peer_key: bytes | None = None,
    noise_private_key: bytes | None = None,
    timeout_microseconds: int = 0,
    max_retries: int = 0,
    max_redirects: int = 0,
) -> Response

Parameters

urlstr | bytesThe full URL.
bodyBufferAny bytes-like object to send.

Returns

The response, whatever its HTTP status.

Raises

  • Error: The exchange failed.

Examples

>>> client.patch("http://127.0.0.1:8545/users/ada", json={"alias": "countess"}).status_code
200
submitQueues one exchange without driving I/O.
lib.pyi:1678 ↗
def submit(
    verb: Verb | str,
    url: str | bytes,
    /,
    body: Buffer = b'',
    headers: Sequence[str | bytes] = (),
    *,
    security: Security | str = Security.NONE,
    noise_peer_key: bytes | None = None,
    noise_private_key: bytes | None = None,
    timeout_microseconds: int = 0,
    max_retries: int = 0,
    max_redirects: int = 0,
    context: object = None,
) -> None

take_completed later returns exactly one Completed carrying context.

Parameters

verbVerb | strThe method, such as Verb.POST or "post".
urlstr | bytesThe full URL, "http://" or "http+noise://" and the server.
bodyBufferAny bytes-like object, borrowed until the call completes.
headersSequence[str | bytes]"name: value" lines; the client adds Host, Content-Length and User-Agent itself.
securitySecurity | strThe handshake for this call. Defaults to Security.NONE, and an "http+noise://" URL calls over Noise XX.
noise_peer_keybytes | NoneThe server's 32-byte static key, required for Security.NOISE_IK and Security.NOISE_KK.
noise_private_keybytes | NoneThis call's 32-byte Noise private key in place of the client's own, as Security.NOISE_KK names the caller to the server.
timeout_microsecondsintThe budget of this call, queueing and redirects included; 0 takes the client's call_timeout_microseconds.
max_retriesintConnection attempts before sending; 0 takes 3.
max_redirectsintRedirect hops followed before failing; 0 takes 5.
contextobjectAnything, handed back as Completed.context.

Raises

Examples

>>> for index in range(4):
...     client.submit("POST", "http://127.0.0.1:8545/echo", b"payload", context=index)
take_completedDrives the client's I/O on this thread and returns the calls that finished.
lib.pyi:1724 ↗
def take_completed(
    timeout_microseconds: int = 0,
    capacity: int = 64,
) -> list[Completed]

Completions a blocking call retrieved for other calls come first, without driving any I/O.

Parameters

timeout_microsecondsintThe longest wait for the first, in microseconds; 0 polls once.
capacityintThe most to return. Defaults to 64.

Returns

list[Completed]: The finished calls in the order they finished, maybe none.

Raises

Examples

>>> finished = []
>>> while len(finished) != 4:
...     finished += client.take_completed(timeout_microseconds=1_000_000)