Introspection¶
Discover an RPC service's methods, schemas, and parameter types at runtime or statically.
Usage¶
Static introspection (no server needed)¶
Use rpc_methods() to inspect a Protocol class, or describe_rpc() for a human-readable summary:
from vgi_rpc import describe_rpc, rpc_methods
methods = rpc_methods(Calculator)
for name, info in methods.items():
print(f"{name}: {info.method_type.value}, params={info.params_schema}")
print(describe_rpc(Calculator))
Runtime introspection (over a connection you already hold)¶
An RpcServer built with enable_describe=True hosts the built-in
vgi_rpc.Reflection.v1 protocol beside its application protocols. Ask it
through the proxy you already have — on any transport — with
list_protocols() and describe_protocol():
from vgi_rpc import connect, describe_protocol, list_protocols
with connect(Calculator, ["python", "worker.py"]) as proxy:
for p in list_protocols(proxy):
print(p.name, p.version, p.hash)
desc = describe_protocol(proxy, "Calculator")
for name, method in desc.methods.items():
print(f"{name}: {method.method_type.value}")
proxy.add(a=1.0, b=2.0) # the connection is still yours
Both reuse the target's connection rather than opening one. A proxy from
connect, serve_pipe, unix_connect, tcp_connect, iroh_connect,
WorkerPool.connect or RpcConnection is rebound to reflection over the same
byte stream — the server routes each request by its vgi_rpc.protocol key — so
do not call them while a stream is open on that connection. A proxy from
http_connect or httpi_connect shares its httpx2.Client, prefix, auth,
retry and response-budget settings. The proxy may be bound to any protocol the
server hosts; only its connection matters. A raw RpcTransport is accepted
too.
list_protocols() returns one HostedProtocol per hosted protocol, in the
server's order: application protocols in registration order (the primary
first), then the framework's own (vgi_rpc.Reflection.v1, and
vgi_rpc.Identity.v1 on an HTTP server that hosts it). hash is the
protocol's canonical SHA-256, identical in every port for the same wire
surface, so a client that cached a description for that hash can skip
describe_protocol(). Use the listing to discover an optional protocol before
calling it, rather than calling it and reading an error.
describe_protocol() makes two calls — list_protocols and then
describe(name) — and returns a ServiceDescription. A name the server does
not host is an RpcError with error_kind == "protocol_not_supported".
Servers without reflection. A server built without enable_describe=True
(the default), or one older than reflection, answers "not hosted" (protocol_not_supported, an
unknown method, UNIMPLEMENTED, or over HTTP a bare 404). Both functions raise
ReflectionNotSupportedError for that answer — a subclass of RpcError
carrying the server's error fields — and nothing else: no listing is inferred,
because only the caller knows which protocol it expected the server to speak.
The connection remains usable. Any other failure propagates as itself.
introspect(transport) and http_introspect(url) remain for callers that
have only a raw transport or a URL and want the primary protocol described in
one call:
The ServiceDescription carries language-neutral method metadata — name, method type, request/result/header Arrow schemas, and stream flags — everything a dynamic client needs to invoke methods without the Python Protocol class. It also includes protocol_hash (a SHA-256 digest identifying the contract) and protocol_version.
Discovery takes two round trips — list_protocols to learn what the server hosts, then describe for one of them. With a server free to host several protocols there is no longer a single "the" protocol to describe without first asking; both introspect() and http_introspect() accept an explicit protocol to skip the discovery hop.
The retired __describe__ method is gone: introspection is the ordinary vgi_rpc.Reflection.v1 protocol, dispatched through a normal binding, so it gets access logging, telemetry and the normal error envelope like any other call. A server that externalizes payloads externalizes reflection replies too, so an HTTP client needs its external_location configured to read them.
Python-flavoured fields (parameter type names, default values, docstrings) are not on the wire, having been dropped for cross-language neutrality. Consumers that need those human-readable details import the Protocol source class directly rather than reconstructing them from the wire. DESCRIBE_VERSION is reported as "5" and is vestigial — the protocol's major version is part of its own name now, so there is no separate format number to negotiate.
For stream methods that declare a header type (Stream[S, H]), MethodDescription includes has_header (bool) and header_schema (pa.Schema | None) fields describing the header's Arrow schema. Streams also carry is_exchange (bool | None) — True for exchange (bidi), False for producer, None for unary. Header fields are False / None for methods without headers.
API Reference¶
Functions¶
list_protocols
¶
list_protocols(target: object) -> list[HostedProtocol]
List the protocols a server hosts, over a connection the caller holds.
One round trip -- vgi_rpc.Reflection.v1.list_protocols -- on target's
own connection; nothing new is opened and nothing is closed. Over HTTP
the call shares the proxy's httpx2.Client, prefix, auth, retry and
response-budget settings; over every other transport it shares the
proxy's byte stream, which the server demultiplexes by each request's
protocol key. On a pipe-like transport, do not call it while a stream is
open on the same connection: the two would interleave on one channel.
| PARAMETER | DESCRIPTION |
|---|---|
target
|
A proxy from any connect function --
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
One
|
class:
TYPE:
|
order
|
application protocols first, primary leading, then the
TYPE:
|
list[HostedProtocol]
|
framework's own. |
| RAISES | DESCRIPTION |
|---|---|
ReflectionNotSupportedError
|
The server does not host reflection
(built without |
RpcError
|
The server answered with any other error, or the transport failed. |
TypeError
|
target is neither a proxy nor an |
Source code in vgi_rpc/introspect.py
describe_protocol
¶
describe_protocol(
target: object, name: str
) -> ServiceDescription
Describe one hosted protocol, over a connection the caller holds.
Two round trips on target's connection: list_protocols (for the
server identity the description carries, and to tell "no reflection"
apart from "no such protocol") then describe(name). The connection
rules are those of :func:list_protocols.
| PARAMETER | DESCRIPTION |
|---|---|
target
|
A proxy from any connect function, or a raw
TYPE:
|
name
|
The protocol's wire name, as :func:
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
A
|
class:
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
ReflectionNotSupportedError
|
The server does not host reflection. |
RpcError
|
The server does not host name ( |
Source code in vgi_rpc/introspect.py
introspect
¶
introspect(
transport: RpcTransport,
ipc_validation: IpcValidation = FULL,
protocol: str | None = None,
) -> ServiceDescription
Describe a server's protocol over any RpcTransport.
Two round trips: list_protocols to learn what is hosted, then
describe on one of them. The first is unavoidable now that a server
may host several protocols -- there is no longer a single "the" protocol to
ask about without asking.
| PARAMETER | DESCRIPTION |
|---|---|
transport
|
An open
TYPE:
|
ipc_validation
|
Validation level for incoming IPC batches.
TYPE:
|
protocol
|
Which protocol to describe. Defaults to the first hosted one that is not reflection itself, which is the primary.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
ServiceDescription
|
A |
| RAISES | DESCRIPTION |
|---|---|
RpcError
|
If the server does not support reflection or returns an error. |
ValueError
|
If the server hosts no application protocol. |
Source code in vgi_rpc/introspect.py
rpc_methods
cached
¶
rpc_methods(protocol: type) -> Mapping[str, RpcMethodInfo]
Introspect a Protocol class and return RpcMethodInfo for each method.
Skips underscore-prefixed names and non-callable attributes.
Source code in vgi_rpc/rpc/_types.py
976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 | |
describe_rpc
¶
describe_rpc(
protocol: type,
*,
methods: Mapping[str, RpcMethodInfo] | None = None
) -> str
Return a human-readable description of an RPC protocol's methods.
Source code in vgi_rpc/rpc/__init__.py
Data Classes¶
HostedProtocol
dataclass
¶
HostedProtocol(
name: str,
version: str,
hash: str,
deprecated: bool = False,
deprecation_message: str = "",
features: tuple[str, ...] = (),
)
One protocol a server hosts, as vgi_rpc.Reflection.v1 lists it.
A client-side view of the wire ProtocolSummary, returned by
:func:list_protocols in the server's order: application protocols in
registration order (the primary first), then the framework's own
(vgi_rpc.Reflection.v1, and vgi_rpc.Identity.v1 on an HTTP server
that hosts it).
| ATTRIBUTE | DESCRIPTION |
|---|---|
name |
The protocol's wire name -- its routing key, carrying its major
version, e.g.
TYPE:
|
version |
Its declared semver, or
TYPE:
|
hash |
SHA-256 of its canonical description, as 64 lowercase hex
characters. Equal hashes mean an identical wire surface, in any
port, so a caller holding a cached description for this hash can
skip :func:
TYPE:
|
deprecated |
Whether callers should migrate off this protocol.
TYPE:
|
deprecation_message |
What to migrate to; empty unless
TYPE:
|
features |
Capability tokens the protocol announces.
TYPE:
|
ServiceDescription
dataclass
¶
ServiceDescription(
protocol_name: str,
request_version: str,
describe_version: str,
protocol_hash: str,
server_id: str,
methods: Mapping[str, MethodDescription],
protocol_version: str = "",
)
Complete description of an RPC service from introspection.
| ATTRIBUTE | DESCRIPTION |
|---|---|
protocol_name |
Name of the Protocol class.
TYPE:
|
request_version |
Wire protocol version.
TYPE:
|
describe_version |
Introspection format version.
TYPE:
|
protocol_hash |
SHA-256 hex digest of the canonical describe payload. Stable across server processes that expose the same Protocol; changes when any wire-relevant detail changes. Use this as the schema-registry key when decoding archived access-log records.
TYPE:
|
server_id |
Server instance identifier.
TYPE:
|
methods |
Mapping of method name to
TYPE:
|
protocol_version |
Application protocol surface version declared by
the Protocol class (canonical semver MAJOR.MINOR.PATCH), or empty
string when the Protocol opts out. Diagnostic — actual enforcement
happens via the per-request
TYPE:
|
__str__
¶
Return a human-readable summary of the service.
Source code in vgi_rpc/introspect.py
MethodDescription
dataclass
¶
MethodDescription(
name: str,
method_type: MethodType,
has_return: bool,
params_schema: Schema,
result_schema: Schema,
has_header: bool = False,
header_schema: Schema | None = None,
is_exchange: bool | None = None,
)
Description of a single RPC method from introspection.
The wire format carries only language-neutral data: name, method type, schemas, and stream flags. Rich Python-flavoured metadata (parameter type names, defaults, docstrings) lives in the Protocol source class — consumers needing that information import the Protocol directly rather than reconstructing it from the wire.
For STREAM methods, result_schema reflects the Protocol-level
return type (always empty). The actual stream output schema is
determined at runtime by the implementation and cannot be reported
statically.
| ATTRIBUTE | DESCRIPTION |
|---|---|
name |
Method name as it appears on the Protocol.
TYPE:
|
method_type |
Whether this is UNARY or STREAM.
TYPE:
|
has_return |
TYPE:
|
params_schema |
Arrow schema for request parameters.
TYPE:
|
result_schema |
Arrow schema for the response (unary) or empty (streams).
TYPE:
|
has_header |
TYPE:
|
header_schema |
Arrow schema for the header, or
TYPE:
|
is_exchange |
For streams,
TYPE:
|
Exceptions¶
ReflectionNotSupportedError
¶
ReflectionNotSupportedError(
error_type: str,
error_message: str,
remote_traceback: str,
*,
request_id: str = "",
error_code: str = "",
error_kind: str = "",
error_details: list[dict[str, Any]] | None = None
)
Bases: RpcError
The server does not host vgi_rpc.Reflection.v1.
Raised by :func:list_protocols and :func:describe_protocol when the
server answers the reflection call with "not hosted" rather than with a
listing: a server built without enable_describe=True (the default),
or one that predates reflection. Such a server still serves its own protocol, so this
is a statement about discovery, not about the connection -- the
connection remains usable.
A subclass of :class:~vgi_rpc.rpc.RpcError carrying the server's
original error fields, so code that already catches RpcError keeps
working; catch this class to branch on "cannot discover" specifically.
Source code in vgi_rpc/rpc/_common.py
from_rpc_error
classmethod
¶
from_rpc_error(
error: RpcError,
) -> ReflectionNotSupportedError
Wrap the server's "not hosted" answer, keeping every field.
| PARAMETER | DESCRIPTION |
|---|---|
error
|
The error the reflection call raised.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
ReflectionNotSupportedError
|
An instance carrying error's type, message, traceback, request |
ReflectionNotSupportedError
|
id, code, kind and details. |
Source code in vgi_rpc/introspect.py
Constants¶
DESCRIBE_VERSION
module-attribute
¶
Introspection format version.
Vestigial since v5: introspection is vgi_rpc.Reflection.v1 now, a protocol
whose major version is part of its own name, so there is no separate format
number to negotiate. Reported for the benefit of readers who still look for
it, and it will not move again.
History
- v5: introspection became a protocol.
__describe__,_DESCRIBE_SCHEMA,build_describe_batchandparse_describe_batchare gone; the payload is a generated schema and the hash is taken over the decoded description. - v4: dropped Python-flavoured fields; introduced
protocol_hash. - v3: added
param_docs_json. - v2: added
has_header,header_schema_ipc,is_exchange.