# Calling

> UCall for Python

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.

```python
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.

`from ucall import Response, Completed, Client`

## Response

One received HTTP response, which owns its bytes and outlives its client.

```python
class Response(Buffer)
```

[`body`](#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)
```

## header

Looks up a header of the response by its case-insensitive name.

```python
def header(name: str | bytes, /) -> str | None
```

- `name`: The header name, such as "Content-Type".

Examples: ```
>>> response.header("content-type")
'application/json'
```

## fields

Every header field line of the response in arrival order, repeated names apart.

```python
def fields() -> list[tuple[bytes, bytes]]
```

Examples: ```
>>> dict(client.get(url).fields())[b'content-type']
b'text/plain'
```

## status_code

The HTTP status the server sent.

```python
status_code: int
```

## body

A read-only memoryview of the body, alive as long as it is.

```python
body: memoryview
```

## Completed

The final result of one accepted client call, from [`Client.take_completed`](#take_completed).

```python
class Completed
```

[`status`](https://www.unum.cloud/docs/ucall/python/errors.md#status) is [`Status.SUCCESS`](https://www.unum.cloud/docs/ucall/python/errors.md#SUCCESS) once any response arrived, whatever its HTTP status, and the
failure otherwise, such as [`Status.TIMEOUT`](https://www.unum.cloud/docs/ucall/python/errors.md#TIMEOUT), with [`response`](#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
```

## context

The [`context`](#context) given to [`submit`](#submit).

```python
context: Any
```

## status

The final [`Status`](https://www.unum.cloud/docs/ucall/python/errors.md#Status) of the exchange.

```python
status: Status
```

## message

The detail of a failure, or None.

```python
message: str | None
```

## response

The [`Response`](#Response), or None on failure.

```python
response: Response | None
```

## Client

An HTTP client that belongs to the thread creating it, which alone may submit and poll.

```python
class Client
```

Raises: - `TypeError`: A missing [`connections`](https://www.unum.cloud/docs/ucall/python/serving.md#connections), or an argument of the wrong type.

- [`Error`](https://www.unum.cloud/docs/ucall/python/errors.md#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

```python
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,
)
```

## request

Sends one exchange and waits for its response on this thread.

```python
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`](#take_completed).

- `verb`: The method, such as [`Verb.POST`](https://www.unum.cloud/docs/ucall/python/serving.md#POST) or "post".
- `url`: The full URL, "http://" or "http+noise://" and the server.
- `body`: Any bytes-like object to send.
- `json`: A value to send as JSON in place of [`body`](#body), with its content
type; None sends [`body`](#body).
- `headers`: "name: value" lines.
- `security`: The handshake for this call. Defaults to [`Security.NONE`](https://www.unum.cloud/docs/ucall/python/noise-keys.md#NONE).
- `noise_peer_key`: The server's 32-byte static key, for [`Security.NOISE_IK`](https://www.unum.cloud/docs/ucall/python/noise-keys.md#NOISE_IK)
and [`Security.NOISE_KK`](https://www.unum.cloud/docs/ucall/python/noise-keys.md#NOISE_KK).
- `noise_private_key`: This call's 32-byte Noise private key in place of the
client's own, as [`Security.NOISE_KK`](https://www.unum.cloud/docs/ucall/python/noise-keys.md#NOISE_KK) names the caller to the server.
- `timeout_microseconds`: The budget of this call, queueing and redirects included;
0 takes the client's `call_timeout_microseconds`.
- `max_retries`: Connection attempts before sending; 0 takes 3.
- `max_redirects`: Redirect hops followed before failing; 0 takes 5.

Raises: - [`Error`](https://www.unum.cloud/docs/ucall/python/errors.md#Error): The exchange failed, such as [`Status.CONNECTION_FAILED`](https://www.unum.cloud/docs/ucall/python/errors.md#CONNECTION_FAILED) or [`Status.TIMEOUT`](https://www.unum.cloud/docs/ucall/python/errors.md#TIMEOUT).

- `KeyboardInterrupt`: Ctrl+C arrived while waiting; the call completes later, unclaimed.

Examples: ```
>>> response = client.request("POST", "http://127.0.0.1:8545/sum", json={"a": 1, "b": 2})
>>> response.status_code
200
```

## get

Sends a GET and waits for its response, as [`request`](#request) does.

```python
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
```

- `url`: The full URL.

Raises: - [`Error`](https://www.unum.cloud/docs/ucall/python/errors.md#Error): The exchange failed.

Examples: ```
>>> bytes(client.get("http://127.0.0.1:8545/sum?a=1&#x26;b=2").body)
b'3'
```

## post

Sends a POST and waits for its response, as [`request`](#request) does.

```python
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
```

- `url`: The full URL.
- `body`: Any bytes-like object to send.

Raises: - [`Error`](https://www.unum.cloud/docs/ucall/python/errors.md#Error): The exchange failed.

Examples: ```
>>> bytes(client.post("http://127.0.0.1:8545/sum", json={"a": 1, "b": 2}).body)
b'3.0'
```

## put

Sends a PUT and waits for its response, as [`request`](#request) does.

```python
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
```

- `url`: The full URL.
- `body`: Any bytes-like object to send.

Raises: - [`Error`](https://www.unum.cloud/docs/ucall/python/errors.md#Error): The exchange failed.

Examples: ```
>>> client.put("http://127.0.0.1:8545/files/notes.txt", b"hello").status_code
200
```

## delete

Sends a DELETE and waits for its response, as [`request`](#request) does.

```python
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
```

- `url`: The full URL.

Raises: - [`Error`](https://www.unum.cloud/docs/ucall/python/errors.md#Error): The exchange failed.

Examples: ```
>>> client.delete("http://127.0.0.1:8545/files/notes.txt").status_code
204
```

## patch

Sends a PATCH and waits for its response, as [`request`](#request) does.

```python
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
```

- `url`: The full URL.
- `body`: Any bytes-like object to send.

Raises: - [`Error`](https://www.unum.cloud/docs/ucall/python/errors.md#Error): The exchange failed.

Examples: ```
>>> client.patch("http://127.0.0.1:8545/users/ada", json={"alias": "countess"}).status_code
200
```

## submit

Queues one exchange without driving I/O.

```python
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`](#take_completed) later returns exactly one [`Completed`](#Completed) carrying [`context`](#context).

- `verb`: The method, such as [`Verb.POST`](https://www.unum.cloud/docs/ucall/python/serving.md#POST) or "post".
- `url`: The full URL, "http://" or "http+noise://" and the server.
- `body`: Any bytes-like object, borrowed until the call completes.
- `headers`: "name: value" lines; the client adds Host,
Content-Length and User-Agent itself.
- `security`: The handshake for this call. Defaults to [`Security.NONE`](https://www.unum.cloud/docs/ucall/python/noise-keys.md#NONE),
and an "http+noise://" URL calls over Noise XX.
- `noise_peer_key`: The server's 32-byte static key, required for
[`Security.NOISE_IK`](https://www.unum.cloud/docs/ucall/python/noise-keys.md#NOISE_IK) and [`Security.NOISE_KK`](https://www.unum.cloud/docs/ucall/python/noise-keys.md#NOISE_KK).
- `noise_private_key`: This call's 32-byte Noise private key in place of the
client's own, as [`Security.NOISE_KK`](https://www.unum.cloud/docs/ucall/python/noise-keys.md#NOISE_KK) names the caller to the server.
- `timeout_microseconds`: The budget of this call, queueing and redirects included;
0 takes the client's `call_timeout_microseconds`.
- `max_retries`: Connection attempts before sending; 0 takes 3.
- `max_redirects`: Redirect hops followed before failing; 0 takes 5.
- `context`: Anything, handed back as [`Completed.context`](#context).

Raises: - [`Error`](https://www.unum.cloud/docs/ucall/python/errors.md#Error): The call was refused and will not complete, such as [`Status.CONTENDED`](https://www.unum.cloud/docs/ucall/python/errors.md#CONTENDED) once
`calls` are in flight, or [`Status.INVALID_ARGUMENT`](https://www.unum.cloud/docs/ucall/python/errors.md#INVALID_ARGUMENT) from another thread.

Examples: ```
>>> for index in range(4):
...     client.submit("POST", "http://127.0.0.1:8545/echo", b"payload", context=index)
```

## take_completed

Drives the client's I/O on this thread and returns the calls that finished.

```python
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.

- `timeout_microseconds`: The longest wait for the first, in microseconds;
0 polls once.
- `capacity`: The most to return. Defaults to 64.

Raises: - [`Error`](https://www.unum.cloud/docs/ucall/python/errors.md#Error): An I/O failure, or [`Status.INVALID_ARGUMENT`](https://www.unum.cloud/docs/ucall/python/errors.md#INVALID_ARGUMENT) from another thread.

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

