English | 中文
An outbound RPC call in tRPC-Python is assembled as Options and then executed by Client.invoke. A protobuf stub (proxy) is only a thin wrapper over that path. The proxy itself does not keep network connections.
The public client API is in trpc_sdk.client (Client, get_client, and Options). The call context is trpc_sdk.abc.TrpcContext.
On each call, get_options builds the final Options in this order. A later step overwrites the same field from an earlier step:
- Framework defaults. The selector is
TrpcSelector, serialization isSerializationType.PB, and compression isCompressType.Noop. - YAML. The callee service name on the
TrpcMessageselects the matching entry underclientintrpc_python.yaml. - Caller
Options. TheOptionspassed in code for this call are applied last.
flowchart LR
caller["Caller Options"] --> merge["get_options merges config"]
yaml["YAML client config"] --> merge
merge --> select["select_node"]
select --> filters["Filter chain"]
filters --> trip["encode, transport, decode"]
Node selection (select_node) finishes before the client filter chain. The chain wraps serialization, compression, frame encoding, transport, and response decoding. How to write a filter is in the Filter Plugin.
create_server(conf_path) already calls TrpcConfig.load. Inside a handler, pass the ctx the handler received into the downstream proxy. The upstream deadline and the propagated metadata stay on that context:
from trpc_sdk.abc import TrpcContext
from trpc_sdk.client import Options
from trpc_sdk.serialization import SerializationType
class GreeterServicer(rpc.GreeterServicer):
"""Downstream call from inside a request handler."""
async def SayHello(self, ctx: TrpcContext, request: pb.HelloRequest) -> pb.HelloReply:
"""Forward SayHello to another Greeter."""
proxy = rpc.GreeterClientProxyImpl()
req = pb.HelloRequest()
req.msg = request.msg
options = (
Options()
.set_target("ip://127.0.0.1:8000")
.set_protocol("trpc")
.set_serialization_type(SerializationType.PB)
.set_network("tcp")
)
return await proxy.asyncSayHello(ctx, req, options)GreeterClientProxyImpl defaults to need_clone_ctx=True. It clones a client TrpcMessage before the call, so this outbound call does not modify the server request that is being handled.
A script that does not start a Server must call TrpcConfig.load itself and build a TrpcContext itself. If you use a custom plugin, import that plugin module before TrpcConfig.load so it is registered:
from trpc_sdk.abc import TrpcContext
from trpc_sdk.client import Options
from trpc_sdk.config import TrpcConfig
from trpc_sdk.serialization import SerializationType
TrpcConfig.load("./trpc_python.yaml", "utf-8")
async def say_hello() -> pb.HelloReply:
"""Call Greeter from a standalone client."""
proxy = rpc.GreeterClientProxyImpl()
req = pb.HelloRequest()
req.msg = "hello"
options = (
Options()
.set_target("ip://127.0.0.1:8000")
.set_protocol("trpc")
.set_serialization_type(SerializationType.PB)
.set_network("tcp")
.set_namespace("Development")
.set_timeout(3000)
)
ctx = TrpcContext()
return await proxy.asyncSayHello(ctx, req, options)When both target and protocol are set on Options in code, trpc_python.yaml does not need a client.service entry of the same name. When no entry matches the callee service name, the framework falls back to the shared timeout, namespace, and filter on the top-level client section.
The generated GreeterClientProxyImpl writes the full RPC path, the callee service name, and the method name onto the TrpcMessage, then sends the request with get_client():
-
Async:
await proxy.asyncSayHello(ctx, req, options)callsClient.invoke. -
Sync:
proxy.SayHello(ctx, req, options)callsClient.invoke_sync. -
Custom service name (
service_name).GreeterClientProxyImpl(service_name=...)defaults to the service name in the.protofile, and that name is the YAML lookup key. A custom name replaces both the YAML lookup key and the service name in the RPC path. Withfix_rpc_name=False, only the YAML lookup key changes. The RPC path that is sent stays the original proto text. -
optionsshape. Pass one chainedOptions()object, or a list ofOptions. The framework applies the list onto this request in order:
options = [
Options().set_target("ip://127.0.0.1:8000"),
Options().set_protocol("trpc"),
Options().set_timeout(3000),
]
reply = proxy.SayHello(ctx, req, options)- Cheap to construct. A proxy is lightweight and can be created for each call. The
Clientreturned byget_client()is a process-wide singleton. The connection pool lives intransport, not on the proxy. - Generic HTTP RPC. The same stub can carry this RPC over HTTP with
Options().set_protocol("http"). To send a standard HTTP request by URL without a stub, see the HTTP Client.
get_client().invoke(...) requires the caller to set the callee service name, the RPC path, and the method name on the TrpcMessage, and to pass the response class rsp_cls. The framework builds an empty instance with rsp_cls() and fills it by deserialization:
from trpc_sdk.abc import TrpcContext
from trpc_sdk.client import Options
from trpc_sdk.client import get_client
ctx = TrpcContext()
msg = ctx.get_message()
msg.set_callee_service_name("trpc.test.helloworld.Greeter")
msg.set_client_rpc_name("/trpc.test.helloworld.Greeter/SayHello")
msg.set_callee_method("SayHello")
options = Options().set_target("ip://127.0.0.1:8000").set_protocol("trpc")
reply = await get_client().invoke(ctx, req, pb.HelloReply, options)A set_* in code overrides YAML, and YAML overrides the framework defaults. The table lists the common methods. Options also has connection-pool, TLS, and streaming-window settings.
| Method | Role |
|---|---|
set_target(target) |
Addressing target scheme://endpoint. Decides which Selector plugin is used |
set_service_name(name) |
Override the service name from YAML, and use it as the endpoint of this selection |
set_protocol(protocol) |
Protocol name. Looks up and binds the codec, checker, and transport |
set_network(network) |
Transport network: tcp or udp |
set_serialization_type(t) |
Serialization type. Default SerializationType.PB |
set_compress_type(t) |
Compression type. Default CompressType.Noop |
set_timeout(ms) |
Timeout of this client call, in milliseconds |
set_namespace(ns) |
Namespace of the callee service |
set_metadata(key, val) |
Add one cross-process key-value pair. val must be bytes |
set_protocol(name) looks up a registered codec, checker, and transport by that name. If no codec of that name is registered, all three stay at the values they had before the call. If a codec is found but no transport of the same name is registered, only the codec and the checker are replaced. The transport stays the default implementation.
In trpc_python.yaml, the top-level client section is shared by every downstream service. client.service[] is one downstream service. It maps to ClientServiceConfig: YAML name becomes service_name, filter becomes filters, loadbalance becomes load_balance, and circuitbreaker becomes circuit_breaker.
client:
timeout: 1000
namespace: Development
filter:
- trace
service:
- callee: trpc.test.helloworld.Greeter
name: trpc.test.helloworld.Greeter
target: ip://127.0.0.1:8000
network: tcp
protocol: trpc
timeout: 800
serialization: 0
compression: 0A name in filter must be a registered client filter. trace in the example only marks the position. This repository does not ship a plugin under that name.
- Base fields. When a
serviceomitsnamespace,network, orprotocol, it inherits the value from the outerclientsection. Whenclient.namespaceis also omitted, it inheritsglobal.namespace. - Naming sub-plugins. When a
serviceomitstarget, itsdiscovery,loadbalance, andcircuitbreakerare also inherited from the outerclientsection. - Filter merge. The outer
client.filteris inserted in front of that service'sfilterlist. A duplicate name keeps only the earlier copy. - Timeout. The outer
client.timeoutis stored only on the fallback configuration. It applies only when noclient.service[]entry matches the service name. When a matchedserviceomitstimeout, the timeout stays0and this call does not set another client deadline. - Lookup key (
calleeandname). Whencalleeis set, it is the lookup key. Otherwisenameis the lookup key. The callee service name the stub writes onto the message must match that key, or the YAML entry is not applied.Options.set_service_nameruns after YAML is loaded. It overrides the selectionendpointand writes the new name back onto the message.
Connection-pool and short-connection fields (short_conn, dial_timeout, idle_time, max_conn_lifetime, max_idle_conn, max_active_conn, is_wait_active_conn, and HTTP's disable_reuse_http_session) are in Client Connection Modes.
The client addresses a peer in two ways:
- No
target(TrpcSelectorchain). The default selector isTrpcSelector. It picks one node through discovery, the service router, load balance, and the circuit breaker. The selectionendpointstarts as the callee service name on the message. When the matching YAMLservicesetsname, thatnamereplaces it. targetset (oneSelector). The value must bescheme://endpointand contain exactly one://. The framework looks up theNAMING_SELECTORplugin registered under that scheme and passes theendpointafter://to it. A bad format, or a selection that does not produce a valid address, raisesClientRouteError.
Schemes that ship with this repository:
ip,http, andhttps: parse theendpointashost:portand do not query DNS. Several addresses are separated by commas, and one is picked at random.dns: also accepts a comma-separated list and picks one at random, then resolves the host name with DNS.dsn: the full string after://is placed inNode.addressas-is.
An external naming plugin registers its own scheme as NAMING_SELECTOR. See How to Write a Naming Plugin.
Filter names in YAML become filter instances when the client configuration is loaded, and they are attached to this call's Options. Code can also append an already constructed client filter with set_filter or set_filter_list:
options = Options().set_target("ip://127.0.0.1:8000").set_protocol("trpc").set_filter(trace_filter)- The default is to append to the list already attached.
override=Trueclears that list and keeps only the instances passed this time. - A filter earlier in the list sits on the outside: its pre-logic runs first, and its post-logic runs last. Node selection finishes before the chain is entered.
- Async (preferred).
await proxy.asyncSayHello(...)andawait Client.invoke(...)are coroutines.awaitthem on the event loop that is already running. - Sync.
proxy.SayHello(...)andClient.invoke_sync(...)block the current thread until the call finishes. If this thread has no event loop yet, the framework creates one and binds it to the thread. Do not call the synchronous entry when this thread's event loop is already running, for example from inside anasync def. That raises an event-loop conflict.
- Timeout. Both
set_timeoutand YAMLtimeoutare in milliseconds.0means this call does not set another client deadline. When the value is greater than0, the framework takes the earlier of that value and the upstreamctx.deadline. If the client timeout is earlier, or the upstream context has no deadline, a later timeout is charged to the downstream call. If the upstream deadline is earlier, the timeout is charged to the current service and is not reported to theSelectoras a downstream node failure. See Timeout Control. - Metadata.
Options().set_metadata(key, val)(valisbytes) merges the pair intoclient_meta_dataof this request. The codec writes it into the frame header sent downstream. The server reads it fromserver_meta_data. See Metadata Propagation.
options = Options().set_metadata("key", b"value")The defaults are SerializationType.PB and CompressType.Noop. Other common serializations are SerializationType.JSON and SerializationType.Noop. Compression types are in trpc_sdk.compressor.CompressType, for example Gzip. YAML serialization and compression are the integer enum values. 0 is PB and no compression, respectively.
In a proxy or forwarding path, the request body may already be serialized bytes (for example an encoded protobuf payload). This process should not serialize it again, but the frame header should still tell the peer to parse the body as protobuf. Use trpc_sdk.serialization.Body together with set_current_serialization_type:
set_serialization_type(SerializationType.PB)is written on the frame header. It tells the peer that the body format is PB.set_current_serialization_type(SerializationType.Noop)only decides that this process usesNoopSerializerto read and write raw bytes. It does not change the serialization type announced on the frame header.
from trpc_sdk.abc import TrpcContext
from trpc_sdk.abc import create_message
from trpc_sdk.client import Client
from trpc_sdk.client import Options
from trpc_sdk.serialization import Body
from trpc_sdk.serialization import SerializationType
ctx = TrpcContext()
create_message(ctx)
rsp = await Client().invoke(
ctx,
Body(data=raw_bytes),
Body,
[
Options()
.set_target("ip://127.0.0.1:8000")
.set_protocol("trpc")
.set_serialization_type(SerializationType.PB)
.set_current_serialization_type(SerializationType.Noop),
],
)
payload = rsp.dataWhen serializing a request, NoopSerializer accepts a Body or bare bytes. When deserializing a response, it writes bytes only into Body.data, so rsp_cls must be Body.
After select_node picks a node, it writes the node address to message.remote_addr of this call. Stub code clones the context by default, so the ctx the caller passed in does not show that change.
To read the downstream address and the call cost after the call, construct a Node first. service_name is required; pass "" before the call. Pass the node with Options.set_selector_node(node). When the call finishes, the framework fills address, service_name, and cost_time on that object:
from trpc_sdk.abc import Node
node = Node(service_name="")
options = (
Options()
.set_target("ip://127.0.0.1:8000")
.set_protocol("trpc")
.set_selector_node(node)
)
reply = await proxy.asyncSayHello(ctx, req, options)
print(node.address, node.cost_time)- HTTP and SSE. The built-in HTTP client and the SSE streaming client are in the HTTP Client and the HTTP SSE Client.
- Ecosystem protocols. Redis, MySQL, Kafka, TAF, MCP, A2A, and vector databases come from separate plugin repositories. How a database plugin is attached is in How to Write a Database Plugin.