grpc-server-kit for AI agents¶
One page holding everything a coding assistant needs to wire and run a
grpc.aioserver with grpc-server-kit correctly, plus a map of where the rest of the documentation keeps the details it leaves out. Give an agent this page rather than the whole site.
| Package | grpc-server-kit on PyPI, import root grpc_server_kit |
| Requires | Python 3.12+, grpcio>=1.78,<2 — the only hard dependency |
| Install | pip install grpc-server-kit · extras: reflection, channelz, settings, health, postgres, redis, metrics, tracing, sentry, dishka, all |
| Async | grpc_server_kit.aio, over grpc.aio — this is the whole server API |
| Sync | There is no sync server. bind_server_port accepts a sync grpc.Server, and config / options / credentials / signals are transport-agnostic; everything else is grpc.aio |
| Source | https://github.com/bedrock-python/grpc-server-kit |
How to read this page¶
Every page of this site is also served as raw Markdown at its own URL with .md in
place of the trailing slash — this page is /agents.md, the health guide is
/guide/health.md — so anything the map below points at can be fetched as plain text
rather than scraped out of HTML. The Copy page control at the top of a page does
the same thing for a human with a chat window open. The one exception is the API
reference: its Markdown is a single instruction to a docstring renderer rather than the
API, so it carries neither the control nor a .md twin — read it as HTML, or read the
docstrings in the source.
Top to bottom before writing code. Rules that hold or break the code is the section correctness lives in — those are the things the library will not save you from. Every name used below is in the public API; if you need something not listed here, fetch the page the documentation map points at rather than guessing a method that sounds plausible.
Scope¶
It does everything around a grpc.aio server that is not your service logic:
validated channel options from a settings object, TLS/mTLS credential loading with
permission and PEM checks, port binding, SIGINT/SIGTERM handling and a graceful drain,
a streaming-aware interceptor base class plus six shipped interceptors, the gRPC Health
Checking Protocol v1 with a single-flight TTL cache and Postgres/Redis checkers,
observability seams declared as structural protocols, and ready-made Dishka providers.
It does not compile protobufs — protoc/buf and the generated
add_*Servicer_to_server functions are yours; it has no client side at all (no channel,
stub or retry helpers); it has no sync server facade; it runs no scheduler and no HTTP
server; it never calls sentry_sdk.init, sets up an OpenTelemetry TracerProvider or
starts a Prometheus /metrics endpoint — those are process-wide and stay with your
application; and it does not restart a stopped server, because gRPC cannot.
Mental model¶
Four nouns, in the order a request meets them.
- Settings — one object satisfying
GrpcServerSettingsProtocol. Two shipped shapes are interchangeable everywhere:GrpcServerConfig(stdlib dataclass, core) andBaseGrpcServerSettings(pydantic model,[settings]extra). Both validate at construction; a misconfigured server must not start.build_grpc_options(settings)turns them into thegrpc.channel options. GrpcApp— the facade. You configure it with plain method calls, thenbuild()creates the server, registers everything and binds the port, andrun()installs signal handlers and serves until termination. It is a one-way sequence: configure, build, run, done.- The pieces underneath, for DI containers and custom lifecycles:
AsyncGrpcServerBuilder(settings).with_interceptors(...).with_servicers(...).build()returns anAsyncServer(a typed wrapper overgrpc.aio.Server— generatedadd_*Servicer_to_serverfunctions accept it directly, and.raw_serveris the underlying object). Thenbind_server_port(server, settings)andrun_async_grpc_server(server, address=...). ServerLifecycleManager— start, serve, drain, stop, restore signal handlers.request_shutdown(reason)stops it programmatically; it is idempotent, keeps the first reason, and a request made before the run starts is remembered rather than lost.
Interceptors are a list handed to grpc.aio.server() at construction, outermost
first. They cannot be added afterwards, which is why every GrpcApp configuration
method refuses to run once the server is built.
Everything past the core is an opt-in extra, and each optional subpackage imports its
dependency at module level: grpc_server_kit.aio.health needs [health],
grpc_server_kit.settings needs [settings], grpc_server_kit.dishka and
grpc_server_kit.aio.dishka need [dishka]. Importing one without its extra raises
ImportError at import time. The observability modules are the exception — they import
safely and raise only when you construct the adapter.
Wiring¶
import asyncio
from grpc_server_kit import GrpcApp, GrpcServerConfig
from grpc_server_kit.aio.interceptors import (
AsyncExceptionHandlerInterceptor,
AsyncRequestLoggerInterceptor,
)
from my_pkg_pb2_grpc import add_MyServiceServicer_to_server
config = GrpcServerConfig(host="[::]", port=50051, grace_period=10.0)
app = GrpcApp(
config,
interceptors=[ # outermost first
AsyncRequestLoggerInterceptor(),
AsyncExceptionHandlerInterceptor(), # innermost of the two
],
)
app.add_servicer(MyServicer(), add_MyServiceServicer_to_server)
app.enable_reflection(["my.pkg.MyService"]) # [reflection] extra
app.enable_health(checkers=[MyChecker()]) # [health] extra
asyncio.run(app.run()) # build + bind + SIGINT/SIGTERM + graceful drain
Without a settings object, GrpcApp(host=..., port=...) builds a default
GrpcServerConfig for you — but passing settings and a shortcut raises
ValueError.
For tests and embedding, the async context manager starts the server without touching signal handlers and stops it with the configured grace period:
The same server without the facade:
from grpc_server_kit import bind_server_port
from grpc_server_kit.aio import AsyncGrpcServerBuilder, run_async_grpc_server
server = (
AsyncGrpcServerBuilder(config)
.with_interceptors(interceptors)
.with_servicers(lambda s: add_MyServiceServicer_to_server(MyServicer(), s))
.with_reflection(["my.pkg.MyService"])
.build()
)
port = bind_server_port(server, config) # honors config.ssl_enabled
await run_async_grpc_server(server, address=f"{config.host}:{port}")
The API¶
grpc_server_kit (package root)¶
| Name | What it is |
|---|---|
GrpcApp |
The facade — re-exported from grpc_server_kit.aio.app |
GrpcServerConfig |
Settings dataclass, core |
build_grpc_options(settings) |
list[tuple[str, int]] of gRPC channel options |
COMPRESSION_ALGORITHMS |
{"none": 0, "deflate": 1, "gzip": 2} |
load_server_credentials(settings, strict=True) |
grpc.ServerCredentials from the TLS files |
bind_server_port(server, settings) |
Binds, TLS-aware; returns the actual port |
setup_signal_handlers(callback) |
Installs SIGINT/SIGTERM on a process-global manager |
GrpcSettingsProtocol, GrpcSslSettingsProtocol, GrpcServerSettingsProtocol, GrpcServerProtocol, GrpcAsyncServerProtocol |
The structural seams |
GrpcServiceName |
NewType("GrpcServiceName", str) — the DI key for the fully-qualified service name |
__version__ |
The version string |
grpc_server_kit.aio¶
Everything above plus AsyncGrpcServerBuilder, AsyncServer,
create_async_grpc_server, create_base_async_grpc_server, ServerLifecycleManager,
run_async_grpc_server, SignalManager, reset_signal_handlers and
reset_signal_handlers_async. The subpackages
(aio.interceptors, aio.health, aio.observability, aio.dishka) are not
re-exported here — import them by their own module path.
GrpcApp¶
GrpcApp(settings=None, *, host=None, port=None, interceptors=None).
| Method | When | What it does |
|---|---|---|
add_servicer(servicer, add_to_server) |
before build | Registers via the generated add_*Servicer_to_server |
register(callback) |
before build | Escape hatch: the callback gets the raw grpc.aio.Server |
add_interceptors(interceptors) |
before build | Appends to the chain (outermost first) |
enable_reflection(service_names) |
before build | [reflection] extra; names are required |
enable_channelz() |
before build | [channelz] extra |
enable_health(checkers=None, *, cache_ttl=…, check_timeout=…, service_names=None) |
before build | [health] extra; see below |
build() |
— | Builds, registers, binds. Idempotent; caches nothing on failure |
await run(*, setup_signals=True) |
— | Builds if needed, then serves until termination |
run_sync(*, setup_signals=True) |
— | asyncio.run(self.run(...)) |
request_shutdown(reason="requested") |
any time | Idempotent graceful stop; honored even if requested before run() |
async with app: |
— | Starts without signal handling, stops with the grace period |
.settings / .server / .bound_port |
after build for the last two | The settings, the AsyncServer, the actually bound port |
enable_health defaults cache_ttl and check_timeout from settings.health when the
settings object carries that block (BaseGrpcServerSettings does), otherwise from
DEFAULT_HEALTH_CACHE_TTL (5.0) and DEFAULT_HEALTH_CHECK_TIMEOUT (10.0). A cache_ttl
of 0 or None disables caching.
AsyncGrpcServerBuilder¶
with_interceptors(list) extends the chain, with_servicers(callback) sets it
(a second call replaces the first), with_reflection(names), with_channelz(), and
build() -> AsyncServer. All the with_* methods return self.
Lifecycle¶
| Name | Signature |
|---|---|
ServerLifecycleManager |
(server, address, grace_period=5.0, signal_manager=None) |
await manager.run(setup_signals=True) |
Serves until termination, request, or signal |
await manager.stop() |
Drains, then restores the previous signal handlers |
manager.request_shutdown(reason="requested") |
From the event loop; from a thread use loop.call_soon_threadsafe |
await run_async_grpc_server(server, *, address, grace_period=5.0, setup_signals=True, signal_manager=None) |
The one-call form |
SignalManager (grpc_server_kit.signals, re-exported from grpc_server_kit.aio) |
setup(callback), reset(), await reset_async(); the module-level setup_signal_handlers / reset_signal_handlers / await reset_signal_handlers_async() drive one process-global manager |
Interceptors¶
Subclass AsyncServerInterceptor and implement around_call as an async generator that
yields exactly once. RpcCall is the frozen view of one RPC: method_name, request,
context, request_streaming, response_streaming.
from collections.abc import AsyncIterator
from grpc_server_kit.aio.interceptors import AsyncServerInterceptor, RpcCall
class TimingInterceptor(AsyncServerInterceptor):
async def around_call(self, call: RpcCall) -> AsyncIterator[None]:
start = time.perf_counter()
try:
yield # the RPC, or the FULL response stream
finally:
log.info("%s took %.3fs", call.method_name, time.perf_counter() - start)
The six shipped interceptors, in canonical order (outermost first):
| # | Constructor | Notes |
|---|---|---|
| 1 | AsyncMetricsInterceptor(metrics=None, service_name="unknown", *, skip_methods=SKIPPED_HEALTH_METHODS) |
metrics=None no-ops; empty service_name raises ValueError |
| 2 | AsyncContextInterceptor(header_configs, bind_method_name=True, bind_structlog=True, method_key="grpc_method", *, skip_methods=()) |
skip_methods defaults to empty: it binds values the handler itself may read |
| 3 | AsyncRequestLoggerInterceptor(*, log_peer=False, log_request_on_error=False, skip_methods=SKIPPED_HEALTH_METHODS) |
Peer is logged as a protocol name only, never an IP |
| 4 | AsyncTracingInterceptor(service_name, tracer=None, *, skip_methods=SKIPPED_HEALTH_METHODS) |
service_name is required; tracer=None no-ops |
| 5 | AsyncExceptionHandlerInterceptor(error_status_map=None, *, detail_factory=None, merge_defaults=True) |
Your map wins over the defaults it is merged into; no skip_methods — every RPC gets its exceptions mapped |
| 6 | AsyncSentryInterceptor(sentry=None, *, capture_filter=None, skip_methods=SKIPPED_HEALTH_METHODS) |
Must sit after the exception handler in the list |
AsyncServerInterceptor(*, skip_methods=()) is the base; skip_methods holds full RPC
names such as "/grpc.health.v1.Health/Check", and a skipped method gets gRPC's original
unwrapped handler back — zero overhead, not a fast path.
HeaderConfig(header_name, context_var_name, context_setter=None, default_factory=None,
validator=None, required=False, allow_empty=False) configures one header for
AsyncContextInterceptor. A missing, empty or invalid value aborts the RPC with
INVALID_ARGUMENT only when required=True; otherwise it is skipped.
Also exported from grpc_server_kit.aio.interceptors:
split_method_name("/pkg.Service/Method") -> ("pkg.Service", "Method"),
get_metadata_dict(context) (per-RPC cached), default_error_detail(exc, status),
find_mapped_status(exc_type, error_status_map), GRPC_DEFAULT_ERROR_STATUS_MAP,
GRPC_SAFE_ERROR_MESSAGES, the ErrorDetailFactory type alias, and the seam protocols
GrpcServerMetricsProtocol, TracerProtocol, SpanProtocol, SpanAttributeValue,
ErrorReporterProtocol.
grpc_server_kit.interceptors (transport-agnostic) holds SKIPPED_HEALTH_METHODS,
X_REQUEST_ID, is_server_error(code) and
resolve_status_code(exc, context, default=grpc.StatusCode.UNKNOWN).
GRPC_DEFAULT_ERROR_STATUS_MAP maps ValueError → INVALID_ARGUMENT, PermissionError
→ PERMISSION_DENIED, NotImplementedError → UNIMPLEMENTED, TimeoutError →
DEADLINE_EXCEEDED, FileNotFoundError → NOT_FOUND. Anything unmapped resolves to
INTERNAL. TypeError is deliberately absent — it is a server bug, not a client one.
Health (grpc_server_kit.aio.health, [health] extra)¶
| Name | Signature |
|---|---|
AsyncDynamicHealthServicer |
(checkers=None, cache=None, service_names=None, check_interval=5.0, heartbeat_interval=60.0, check_timeout=10.0) |
HealthCache |
(ttl=5.0) — TTL freshness plus single-flight; ttl <= 0 raises ValueError |
check_async_overall_health |
(checkers=None, cache=None, timeout=10.0) -> ServingStatus |
AsyncHealthChecker |
Protocol: async check(self) -> bool |
AsyncHealthCacheProtocol |
Protocol: async get_or_set(check_func, now=None) |
DatabaseHealthChecker |
(session_maker, timeout=5.0) — SELECT 1, [postgres] extra |
RedisHealthChecker |
(redis_client, timeout=5.0) — PING, [redis] extra |
FunctionalHealthChecker |
(check_func, resource=None, timeout=5.0) — the adapter both are built on |
handle_check_exceptions(check_name) |
Decorator normalising a check's failures to False |
check_async_database_health, check_async_redis_health |
The bare functions behind the two checkers |
SessionMakerProtocol, RedisClientProtocol |
The duck-typed client shapes |
Observability¶
| Name | Module | Extra |
|---|---|---|
GrpcServerMetrics(prefix=None, buckets=DEFAULT_GRPC_BUCKETS, registry=None) |
observability.metrics |
metrics |
get_grpc_server_metrics(prefix=None, buckets=None) |
observability.metrics |
metrics |
DEFAULT_GRPC_BUCKETS, make_metric_name(name, prefix=None) |
observability.metrics / .naming |
core |
SentrySdkAdapter() |
observability.sentry |
sentry |
instrument_aio_server(**kwargs) / uninstrument_aio_server() |
aio.observability |
tracing |
SpanProtocol, TracerProtocol, ErrorReporterProtocol, SpanAttributeValue |
observability.protocols |
core |
Two metrics, frozen: grpc_requests_total (Counter; labels service, method,
status, grpc_code) and grpc_request_duration_seconds (Histogram; labels service,
method).
Dishka ([dishka] extra)¶
grpc_server_kit.dishka re-exports inject, FromDishka, GrpcioProvider and
from_context, and adds GrpcServerSettingsProvider(settings, *, service_name).
grpc_server_kit.aio.dishka adds DishkaAioInterceptor, AsyncGrpcServerProvider,
GrpcServerInterceptorsProvider, PrometheusGrpcServerMetricsProvider,
SentryAdapterProvider, TracerAdapterProvider, and the bundle:
grpc_server_providers(
settings, *, service_name,
error_status_map=None, detail_factory=None, header_configs=None,
metrics_enabled=None, metrics_prefix=None,
sentry_enabled=True, tracing_enabled=True,
reflection_service_names=None,
) -> tuple[Provider, ...]
GrpcServerInterceptorsProvider takes include_metrics / include_context /
include_request_logger / include_tracing / include_sentry /
include_exception_handler toggles and extra_outer / extra_inner slots, and appends
DishkaAioInterceptor innermost so the request scope opens per RPC.
Settings fields¶
GrpcServerConfig and BaseGrpcServerSettings carry the same fields with the same
defaults: host="[::]", port=50051, ssl_enabled=False, ssl_cert_file,
ssl_key_file, ssl_ca_file, ssl_client_auth=False, ssl_max_cert_size=None (1 MB),
keepalive_time_ms=7_200_000, keepalive_timeout_ms=20_000,
keepalive_permit_without_calls=False,
http2_min_recv_ping_interval_without_data_ms=300_000, http2_max_pings_without_data=2,
max_concurrent_rpcs=None, max_connection_idle_ms=None, max_connection_age_ms=None,
max_connection_age_grace_ms=None, max_send_message_length=4 MiB,
max_receive_message_length=4 MiB, max_metadata_size=8 KiB,
compression_algorithm=None, initial_stream_window_size=65535,
initial_connection_window_size=65535, enable_reflection=False,
enable_channelz=False, grace_period=5.0, metrics_enabled=False.
BaseGrpcServerSettings additionally nests health: BaseHealthSettings
(cache_ttl=5.0, check_timeout=10.0); GrpcServerConfig has no health block.
The kit's defaults live in grpc_server_kit.constants as DEFAULT_* names, alongside
HEALTH_SERVICE_NAME = "grpc.health.v1.Health".
Rules that hold or break the code¶
- Configure before
build().add_servicer,register,add_interceptors,enable_reflection,enable_channelzandenable_healthall raiseRuntimeErroronce the server exists. Interceptors especially are handed togrpc.aio.server()at construction — there is no adding one to a built server, at any layer. - A
GrpcAppis single-use. Afterrun()returns, or after the async context manager exits, the app is finished and every entry point raisesRuntimeError. gRPC servers cannot restart; make a newGrpcApp. AServerLifecycleManager, by contrast, is reusable across sequential runs. - The interceptor list is outermost first, and position is semantics, not style.
AsyncSentryInterceptormust come afterAsyncExceptionHandlerInterceptorin the list — closer to the handler — because the handler converts raw exceptions intogrpc.aio.AbortError, and Sentry never captures anAbortError. Put it first and it reports nothing, ever, with no error to tell you. context.abort()raisesgrpc.aio.AbortError, which is not agrpc.RpcError.except grpc.RpcError:does not catch a deliberate abort;except Exception:does. An interceptor that logs and re-raises everything will fire on every intentional abort unless it filtersAbortErrorout first.around_callis an async generator, not a coroutine. It must contain exactly oneyield; the RPC — including the complete consumption of a response stream — happens there. A body with noyieldanywhere is a plain coroutine, and every RPC through that interceptor dies withTypeError: 'coroutine' object is not an async iterator; a body that returns before reaching itsyieldraisesRuntimeError: generator didn't yield. Neither is caught for you.- Reflection needs names.
settings.enable_reflection = Truewith noenable_reflection([...])call fails the build withValueError.enable_health()addsgrpc.health.v1.Healthto the reflection list only when you already gave it names; it never turns reflection on by itself. grace_period=Nonemeans abort immediately, not wait forever. That is gRPC's own semantics forstop(None). The settings objects typegrace_periodas a non-negative float and default it to 5.0; only the lower-levelServerLifecycleManager/run_async_grpc_server/AsyncServer.stopacceptNone.- The private key must be owner-only on Unix.
load_server_credentialsraisesPermissionErrorfor a key with any group or world bits set (chmod 600), andbind_server_portalways calls it in strict mode — there is no way to relax that through the settings. Passstrict=Falseyourself only if you are callingload_server_credentialsdirectly and accept the warning instead. port=0binds an ephemeral port, and onlybind_server_portknows which. Read it back fromapp.bound_port(or the return value ofbind_server_port); before the build it raisesRuntimeError, andsettings.portstill says0.- Optional subpackages import their extra at module level.
import grpc_server_kit.aio.healthraisesImportErrorwithout[health], and the same holds forgrpc_server_kit.settings([settings]) and both dishka packages ([dishka]). Guard the import, do not guard the call. - A health checker returns
booland does not raise for "unhealthy". A raised exception is logged and folded intoNOT_SERVINGall the same, but it costs you the per-checker signal.heartbeat_intervalmust be>= check_interval,check_timeoutandcheck_intervalmust be positive — all validated eagerly at construction. HealthCache(ttl=0)raises;enable_health(cache_ttl=0)does not. The app normalises a falsy TTL to "no cache" before constructing anything, which is why a settings-validcache_ttl=0is safe there and fatal here.with_servicersreplaces,with_interceptorsextends. Callingwith_servicerstwice on the same builder silently drops the first callback. Register several servicers from one callback, or register them on the builtAsyncServerafterwards.- The Dishka bundle hands you a server with no servicers on it.
AsyncGrpcServerProviderdeliberately stops at interceptors, options and reflection/channelz: resolveAsyncServerfrom the container, register your servicers on it, thenbind_server_portandrun_async_grpc_server. - Signal handlers need the main thread. Off it,
SignalManager.setuplogs a warning and gives up rather than crashing — the server then runs with no signal-driven shutdown at all.setup_signal_handlers()uses one process-global manager;GrpcAppandServerLifecycleManagereach use their own, and restore the exact handlers that were installed before them. - The metrics
methodlabel is the full RPC name (/pkg.Service/Method), and theservicelabel is the constructor'sservice_name, not the service parsed out of the call.get_grpc_server_metricscaches by prefix and raisesValueErrorif you ask for a cached prefix again with differentbuckets. AsyncTracingInterceptordoes not readtraceparent. It opens a new span with OTel semconv attributes; incoming trace context comes frominstrument_aio_server()([tracing]). The two compose — use both for real distributed tracing.- The settings shapes are structural, not nominal. Anything with the right
read-only properties satisfies
GrpcServerSettingsProtocol; there is no base class to inherit and no registration step.GrpcServerConfigiskw_only=Trueandslots=True, so every field is a keyword and unknown ones are aTypeError. build_grpc_optionsrejects bad values loudly. A negative option or an unsupportedcompression_algorithmraisesValueError;Nonemeans "use the kit default". The algorithm name is matched case-insensitively againstCOMPRESSION_ALGORITHMS, and"none"is one of them — gRPC's explicit no-compression algorithm, which is not the same thing as leaving the fieldNone.
Common mistakes¶
# WRONG — configuring an app that has already been built
app = GrpcApp(port=50051)
await app.run()
app.add_servicer(Other(), add_OtherServicer_to_server) # RuntimeError
await app.run() # RuntimeError: single-use
# RIGHT
app = GrpcApp(port=50051)
app.add_servicer(MyServicer(), add_MyServiceServicer_to_server)
app.add_servicer(Other(), add_OtherServicer_to_server)
await app.run()
# WRONG — Sentry outside the exception handler: it will capture nothing, silently
interceptors = [
AsyncSentryInterceptor(sentry=SentrySdkAdapter()),
AsyncExceptionHandlerInterceptor(),
]
# RIGHT — outermost first, so Sentry sits closer to the handler
interceptors = [
AsyncExceptionHandlerInterceptor(),
AsyncSentryInterceptor(sentry=SentrySdkAdapter()),
]
# WRONG — around_call written as a coroutine
class Mine(AsyncServerInterceptor):
async def around_call(self, call: RpcCall) -> None:
log.info("start")
return # no yield: every RPC raises TypeError instead
# RIGHT — an async generator with exactly one yield
class Mine(AsyncServerInterceptor):
async def around_call(self, call: RpcCall) -> AsyncIterator[None]:
log.info("start")
try:
yield
finally:
log.info("done")
# WRONG — expecting RpcError to cover a deliberate abort
try:
yield
except grpc.RpcError: # AbortError is NOT an RpcError
metrics.failure()
raise
# RIGHT — narrowest first
try:
yield
except (asyncio.CancelledError, KeyboardInterrupt, SystemExit):
raise
except grpc.aio.AbortError:
raise # deliberate; the context carries the real status
except grpc.RpcError:
raise
except Exception:
metrics.failure()
raise
# WRONG — reading the port off the settings after asking for an ephemeral one
app = GrpcApp(host="127.0.0.1", port=0)
await app.run()
client = f"127.0.0.1:{app.settings.port}" # 0
# RIGHT
async with GrpcApp(host="127.0.0.1", port=0) as app:
client = f"127.0.0.1:{app.bound_port}"
# WRONG — guarding an optional extra at the call site; the import already failed
from grpc_server_kit.aio.health import DatabaseHealthChecker # ImportError, right here
def make_checker(session_maker):
try:
return DatabaseHealthChecker(session_maker)
except ImportError: # never reached — the module never loaded
return None
# RIGHT — install the extra, or guard the import itself
try:
from grpc_server_kit.aio.health import DatabaseHealthChecker
except ImportError:
DatabaseHealthChecker = None
# WRONG — two servicer callbacks on one builder; the first is dropped
builder.with_servicers(lambda s: add_AServicer_to_server(A(), s))
builder.with_servicers(lambda s: add_BServicer_to_server(B(), s))
# RIGHT — one callback registers everything
def register(server):
add_AServicer_to_server(A(), server)
add_BServicer_to_server(B(), server)
builder.with_servicers(register)
Errors¶
The kit defines no exception classes of its own. Do not import or catch a
GrpcServerKitError — there isn't one. Everything it raises is a builtin or a gRPC type:
| Raised | When |
|---|---|
ValueError |
Invalid settings (host, port, grace_period, max_concurrent_rpcs, TLS combinations), a negative channel option, an unsupported compression algorithm, an invalid metric prefix, an invalid reflection service name, reflection with no names, settings together with a host/port shortcut, an out-of-range health interval or timeout, a metrics prefix re-requested with different buckets |
RuntimeError |
Configuring or running a GrpcApp out of order: already built, already running, already finished, or reading .server / .bound_port before the build |
ImportError |
An optional subpackage or adapter used without its extra — [health], [settings], [dishka], [reflection], [channelz], [metrics], [sentry], [tracing] |
FileNotFoundError |
A TLS certificate, key or CA file that is not there |
PermissionError |
A private key readable beyond its owner, or a TLS file that cannot be read |
OSError |
Any other I/O failure reading a TLS file |
grpc.aio.AbortError |
Raised by context.abort() — by your handler, by AsyncExceptionHandlerInterceptor, by AsyncContextInterceptor on a missing required header, or by the health servicer aborting a broken Watch |
grpc.RpcError |
gRPC's own transport-level errors, passed through untouched |
Pydantic's ValidationError replaces ValueError when the settings object is a
BaseGrpcServerSettings.
Documentation map¶
Fetch a page when the task is the one named beside it.
| Page | Read it when |
|---|---|
| Home | installing, choosing extras, the one-paragraph pitch |
| Quick start | the first server end to end: servicers, chain, health, reflection, embedding |
| Configuration | picking a settings shape, TLS/mTLS, channel tuning, shutdown semantics |
| Interceptors | around_call, streaming coverage, skip_methods, the handler cache, AbortError, writing your own |
| Health | the Health v1 servicer, writing checkers, the TTL cache, Check vs Watch, Kubernetes probes |
| Observability | the four seam protocols, Prometheus metrics, tracing attributes and propagation, Sentry scopes |
| Advanced | the pieces under GrpcApp, Dishka providers, signals and lifecycle |
| API reference | an exact signature, field or docstring — HTML only, see above |
| Changelog | what changed between versions |