From 3ed520a60c5c27fbeb2083a3ed7a400524a0b1d5 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sat, 11 Jul 2026 15:45:09 +0300 Subject: [PATCH] docs: add gRPC integration usage page Usage page for modern-di-grpc (server-side DI via grpc ServerInterceptor) plus its mkdocs nav entries. Covers DIInterceptor/DIAioInterceptor, per-RPC REQUEST scope across all four RPC types, @inject's sync/async/async-gen shapes, auto-registered ServicerContext injection, and user-owned root lifecycle. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/integrations/grpc.md | 188 ++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 2 + 2 files changed, 190 insertions(+) create mode 100644 docs/integrations/grpc.md diff --git a/docs/integrations/grpc.md b/docs/integrations/grpc.md new file mode 100644 index 0000000..fac4d0b --- /dev/null +++ b/docs/integrations/grpc.md @@ -0,0 +1,188 @@ +# Usage with `gRPC` + +## How to use + +### 1. Install `modern-di-grpc` + +=== "uv" + + ```bash + uv add modern-di-grpc + ``` + +=== "pip" + + ```bash + pip install modern-di-grpc + ``` + +=== "poetry" + + ```bash + poetry add modern-di-grpc + ``` + +### 2. Apply to your application (sync server) + +`DIInterceptor` is a `grpc.ServerInterceptor`; pass it to `grpc.server(...)`. It +opens one `Scope.REQUEST` child container per RPC and resolves `FromDI`-annotated +parameters of `@inject`-decorated servicer methods. + +```python +import typing +from concurrent import futures + +import grpc +from modern_di import Container, Group, Scope, providers +from modern_di_grpc import DIInterceptor, FromDI, inject + +from myapp import greeter_pb2, greeter_pb2_grpc # your generated stubs + + +class Settings: + def __init__(self) -> None: + self.greeting = "hello" + + +class Greeter: + def __init__(self, settings: Settings) -> None: # auto-injected by type + self._settings = settings + + def greet(self, name: str) -> str: + return f"{self._settings.greeting}, {name}" + + +class AppGroup(Group): + settings = providers.Factory(Settings, scope=Scope.APP, cache=True) + greeter = providers.Factory(Greeter, scope=Scope.REQUEST) + + +class GreeterService(greeter_pb2_grpc.GreeterServicer): + @inject + def SayHello( + self, + request: greeter_pb2.HelloRequest, + context: grpc.ServicerContext, + greeter: typing.Annotated[Greeter, FromDI(Greeter)], # resolve by type + ) -> greeter_pb2.HelloReply: + return greeter_pb2.HelloReply(message=greeter.greet(request.name)) + + +container = Container(groups=[AppGroup], validate=True) +server = grpc.server( + futures.ThreadPoolExecutor(max_workers=10), + interceptors=[DIInterceptor(container)], +) +greeter_pb2_grpc.add_GreeterServicer_to_server(GreeterService(), server) +server.add_insecure_port("[::]:50051") +server.start() +server.wait_for_termination() +``` + +Constructing `DIInterceptor(container)` registers the `ServicerContext` context +provider on the container automatically — no separate setup call. + +### 3. Async server (`grpc.aio`) + +`DIAioInterceptor` is the async twin — pass it to `grpc.aio.server(...)` and write +`async def` servicer methods (server-streaming methods as `async` generators): + +```python +import grpc +from modern_di_grpc import DIAioInterceptor, FromDI, inject + + +class GreeterService(greeter_pb2_grpc.GreeterServicer): + @inject + async def SayHello( + self, + request: greeter_pb2.HelloRequest, + context: grpc.aio.ServicerContext, + greeter: typing.Annotated[Greeter, FromDI(Greeter)], + ) -> greeter_pb2.HelloReply: + return greeter_pb2.HelloReply(message=greeter.greet(request.name)) + + +server = grpc.aio.server(interceptors=[DIAioInterceptor(container)]) +``` + +`@inject` adapts to the method it decorates — sync method, `async def`, or async +generator (server-streaming) — so the same decorator works on any of the four RPC +types on either server. + +## Scopes + +The integration opens one `Scope.REQUEST` child container **per RPC call**, for +all four RPC types (unary-unary, server-streaming, client-streaming, bidi). The +child is created when the RPC starts and closed when it ends — for a streaming +RPC it stays open for the whole stream and closes after the last message, +including on the error and client-cancellation paths. REQUEST-scoped providers +(and their finalizers) live for exactly one RPC. APP-scoped providers persist for +the life of the container. + +There is no `Scope.SESSION` for gRPC — a streaming RPC is one method invocation, +modelled as a single REQUEST-scoped unit of work. + +## Injecting the `ServicerContext` + +The `ServicerContext` is injectable at `Scope.REQUEST` — the interceptor +registers `grpc_context_provider` on the container when constructed, and seeds the +live context per RPC. A factory can depend on it to read RPC metadata, the +deadline, or the peer: + +```python +import grpc +from modern_di import Group, Scope, providers + + +def make_caller(context: grpc.ServicerContext | None = None) -> str: + return context.peer() if context is not None else "unknown" + + +class AppGroup(Group): + caller = providers.Factory(make_caller, scope=Scope.REQUEST) +``` + +The `| None = None` default lets the provider construct at validation time, when +no context is set. The protobuf request `Message` is **not** exposed as a provider +(that would add a `protobuf` dependency); the request is already a servicer-method +argument. + +## Root container lifecycle + +gRPC has no server startup/shutdown hook, so the **root container's lifecycle is +yours to own** (as with Flask). Create the container open, pass it to the +interceptor, and close it after the server stops to run APP-scoped finalizers: + +```python +server.stop(grace=5).wait() +container.close_sync() # or: await container.close_async() on grpc.aio +``` + +## Resolving without `@inject` + +Inside a servicer method (or anything it calls during the RPC), +`fetch_di_container()` returns the current RPC's child container: + +```python +from modern_di_grpc import fetch_di_container + +container = fetch_di_container() # raises LookupError outside an intercepted RPC +``` + +## `*args` / `**kwargs` + +Unlike the Celery/Typer decorator integrations, gRPC always calls a servicer +method as `(request, context)`, so `@inject` needs no signature rewrite and +imposes no restriction on the method signature beyond the injected parameters. + +## API + +| Symbol | Description | +|---|---| +| `DIInterceptor(container)` | `grpc.ServerInterceptor` for the sync thread-pool server. Opens a `Scope.REQUEST` child per RPC (`close_sync`); auto-registers `grpc_context_provider`. | +| `DIAioInterceptor(container)` | `grpc.aio.ServerInterceptor` for the async server. Same, with `close_async`. | +| `FromDI(provider_or_type)` | Marker for `Annotated[T, FromDI(...)]` in servicer-method signatures; accepts a provider instance or a plain type. | +| `@inject` | Decorates a servicer method to resolve its `FromDI` parameters from the current RPC's child container; adapts to sync / async / async-generator methods. | +| `fetch_di_container()` | Returns the current RPC's child container (raises `LookupError` outside an RPC). | +| `grpc_context_provider` | `ContextProvider` exposing `grpc.ServicerContext` at `Scope.REQUEST`; auto-registered by the interceptor. | diff --git a/mkdocs.yml b/mkdocs.yml index 87cfe24..69ae864 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -28,6 +28,7 @@ nav: - FastAPI: integrations/fastapi.md - FastStream: integrations/faststream.md - Flask: integrations/flask.md + - gRPC: integrations/grpc.md - Litestar: integrations/litestar.md - Starlette: integrations/starlette.md - taskiq: integrations/taskiq.md @@ -170,6 +171,7 @@ plugins: - integrations/fastapi.md: Usage with FastAPI - integrations/faststream.md: Usage with FastStream - integrations/flask.md: Usage with Flask + - integrations/grpc.md: Usage with gRPC - integrations/litestar.md: Usage with Litestar - integrations/starlette.md: Usage with Starlette - integrations/taskiq.md: Usage with taskiq