Skip to content

Client

Channel

A Channel is one connection, shared by any number of clients:

from fastgrpc3 import TLS, BearerToken, Channel

async with Channel(
    "orders.example.test:443",
    tls=TLS(ca="ca.pem"),  # TLS() alone trusts the system roots
    auth=BearerToken("sk_example_not_a_real_secret"),
    default_timeout=5.0,
) as channel:
    ...
Argument Default
tls — (required) TLS(ca=...), MTLS(cert, key, ca) or Insecure()
auth None BearerToken(...): authorization: Bearer … on every call; refused on an insecure channel
default_timeout 30 s per-call timeout when the call gives none; None for no timeout
retry None RetryPolicy(methods=("orders.v1.Orders/GetOrder",), max_attempts=3), idempotent methods only
keepalive None Keepalive(...)
compression, max_send_bytes, max_recv_bytes, max_metadata_bytes as on the server

Over an insecure channel, credentials are refused: an auth, and metadata keys such as authorization, x-api-key or cookie raise ValueError instead of being sent in clear.

Typed clients

from fastgrpc3 import GrpcClient


class OrdersClient(GrpcClient):
    package = "orders.v1"
    service = "Orders"

    get_order = GrpcClient.unary("GetOrder", GetOrderRequest, Order)
    watch = GrpcClient.unary_stream("Watch", GetOrderRequest, Order)
    upload = GrpcClient.stream_unary("Upload", Line, Summary)
    chat = GrpcClient.stream_stream("Chat", Message, Message)

In proto-first mode, fastgrpc3 generate writes this class for you (OrdersClient in orders_service.py). The models are the server's, or copies with the same field numbers. If the models leave their numbers to proto.lock, set lock_file = "proto.lock" on the class, pointing at the server's lock.

orders = OrdersClient(channel)

order = await orders.get_order(GetOrderRequest(order_number="A"))  # await a unary call

async for update in orders.watch(GetOrderRequest(order_number="A")):  # iterate a stream
    ...

summary = await orders.upload([Line(sku="a"), Line(sku="b")])  # an iterable or async iterator


async def messages() -> AsyncIterator[Message]:
    yield Message(text="hi")


async for reply in orders.chat(messages()):
    ...

Every method takes timeout= (seconds) and metadata= (a Metadata, a dict or pairs). Methods are typed: mypy checks the request type and knows the response type.

Errors

A failed call raises GrpcError, with the status code, the message and the error details:

from fastgrpc3 import ErrorInfo, GrpcError, StatusCode

try:
    await orders.get_order(GetOrderRequest(order_number="?"))
except GrpcError as error:
    if error.code == StatusCode.NOT_FOUND:
        info = error.detail(ErrorInfo)  # the first ErrorInfo detail, or None

The standard google.rpc details are decoded; a detail of your own needs @register_detail on its model. An unknown detail stays an AnyMessage (its type_url and bytes).

The client decodes responses itself: a response that does not decode or validate raises GrpcError(INTERNAL, "invalid response payload"), instead of silently giving None.

For a server you trust, OrdersClient(channel, validate_responses=False) builds the response models without Pydantic validation: the values keep their proto types, but the models' constraints and validators do not run. Large responses decode about 45 % faster (see Performance); models with a JSON-carried field are still validated.

Deadlines

A call made while serving a request inherits what is left of that request's deadline: the timeout is the smallest of the call's timeout (or the channel's default_timeout) and the time left. A chain of services therefore shares one deadline, with nothing to pass around.