grpc-server-kit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. grpc_server_kit/__init__.py +34 -0
  2. grpc_server_kit/__version__.py +1 -0
  3. grpc_server_kit/aio/__init__.py +44 -0
  4. grpc_server_kit/aio/app.py +368 -0
  5. grpc_server_kit/aio/builder.py +63 -0
  6. grpc_server_kit/aio/dishka/__init__.py +28 -0
  7. grpc_server_kit/aio/dishka/bundle.py +70 -0
  8. grpc_server_kit/aio/dishka/interceptors.py +116 -0
  9. grpc_server_kit/aio/dishka/metrics.py +50 -0
  10. grpc_server_kit/aio/dishka/observability.py +86 -0
  11. grpc_server_kit/aio/dishka/server.py +51 -0
  12. grpc_server_kit/aio/health/__init__.py +36 -0
  13. grpc_server_kit/aio/health/cache.py +89 -0
  14. grpc_server_kit/aio/health/checkers/__init__.py +16 -0
  15. grpc_server_kit/aio/health/checkers/postgres.py +115 -0
  16. grpc_server_kit/aio/health/checkers/redis.py +88 -0
  17. grpc_server_kit/aio/health/checkers/utils.py +97 -0
  18. grpc_server_kit/aio/health/orchestrator.py +122 -0
  19. grpc_server_kit/aio/health/protocols.py +43 -0
  20. grpc_server_kit/aio/health/servicer.py +229 -0
  21. grpc_server_kit/aio/interceptors/__init__.py +62 -0
  22. grpc_server_kit/aio/interceptors/base.py +235 -0
  23. grpc_server_kit/aio/interceptors/context.py +179 -0
  24. grpc_server_kit/aio/interceptors/exception_handler.py +167 -0
  25. grpc_server_kit/aio/interceptors/metadata.py +57 -0
  26. grpc_server_kit/aio/interceptors/metrics.py +104 -0
  27. grpc_server_kit/aio/interceptors/protocols.py +54 -0
  28. grpc_server_kit/aio/interceptors/request_logger.py +103 -0
  29. grpc_server_kit/aio/interceptors/sentry.py +102 -0
  30. grpc_server_kit/aio/interceptors/tracing.py +130 -0
  31. grpc_server_kit/aio/observability/__init__.py +5 -0
  32. grpc_server_kit/aio/observability/tracing.py +48 -0
  33. grpc_server_kit/aio/runtime.py +206 -0
  34. grpc_server_kit/aio/server.py +191 -0
  35. grpc_server_kit/config.py +100 -0
  36. grpc_server_kit/constants.py +34 -0
  37. grpc_server_kit/credentials.py +112 -0
  38. grpc_server_kit/dishka/__init__.py +38 -0
  39. grpc_server_kit/dishka/settings.py +41 -0
  40. grpc_server_kit/interceptors/__init__.py +15 -0
  41. grpc_server_kit/interceptors/constants.py +44 -0
  42. grpc_server_kit/interceptors/utils.py +74 -0
  43. grpc_server_kit/observability/__init__.py +12 -0
  44. grpc_server_kit/observability/metrics.py +116 -0
  45. grpc_server_kit/observability/naming.py +26 -0
  46. grpc_server_kit/observability/protocols.py +86 -0
  47. grpc_server_kit/observability/sentry.py +63 -0
  48. grpc_server_kit/options.py +136 -0
  49. grpc_server_kit/protocols.py +114 -0
  50. grpc_server_kit/py.typed +0 -0
  51. grpc_server_kit/server.py +59 -0
  52. grpc_server_kit/settings.py +151 -0
  53. grpc_server_kit/signals.py +228 -0
  54. grpc_server_kit-0.1.0.dist-info/METADATA +484 -0
  55. grpc_server_kit-0.1.0.dist-info/RECORD +57 -0
  56. grpc_server_kit-0.1.0.dist-info/WHEEL +4 -0
  57. grpc_server_kit-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,34 @@
1
+ """grpc-server-kit — batteries-optional async gRPC server toolkit."""
2
+
3
+ from . import aio
4
+ from .__version__ import __version__
5
+ from .aio.app import GrpcApp
6
+ from .config import GrpcServerConfig
7
+ from .credentials import load_server_credentials
8
+ from .options import COMPRESSION_ALGORITHMS, build_grpc_options
9
+ from .protocols import (
10
+ GrpcAsyncServerProtocol,
11
+ GrpcServerProtocol,
12
+ GrpcServerSettingsProtocol,
13
+ GrpcSettingsProtocol,
14
+ GrpcSslSettingsProtocol,
15
+ )
16
+ from .server import bind_server_port
17
+ from .signals import setup_signal_handlers
18
+
19
+ __all__ = [
20
+ "COMPRESSION_ALGORITHMS",
21
+ "GrpcApp",
22
+ "GrpcAsyncServerProtocol",
23
+ "GrpcServerConfig",
24
+ "GrpcServerProtocol",
25
+ "GrpcServerSettingsProtocol",
26
+ "GrpcSettingsProtocol",
27
+ "GrpcSslSettingsProtocol",
28
+ "__version__",
29
+ "aio",
30
+ "bind_server_port",
31
+ "build_grpc_options",
32
+ "load_server_credentials",
33
+ "setup_signal_handlers",
34
+ ]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,44 @@
1
+ """Async (aio) gRPC server public API."""
2
+
3
+ from ..config import GrpcServerConfig
4
+ from ..credentials import load_server_credentials
5
+ from ..options import COMPRESSION_ALGORITHMS, build_grpc_options
6
+ from ..protocols import (
7
+ GrpcAsyncServerProtocol,
8
+ GrpcServerProtocol,
9
+ GrpcServerSettingsProtocol,
10
+ GrpcSettingsProtocol,
11
+ GrpcSslSettingsProtocol,
12
+ )
13
+ from ..server import bind_server_port
14
+ from ..signals import reset_signal_handlers, setup_signal_handlers
15
+ from .app import GrpcApp
16
+ from .builder import AsyncGrpcServerBuilder
17
+ from .runtime import ServerLifecycleManager, run_async_grpc_server
18
+ from .server import (
19
+ AsyncServer,
20
+ create_async_grpc_server,
21
+ create_base_async_grpc_server,
22
+ )
23
+
24
+ __all__ = [
25
+ "COMPRESSION_ALGORITHMS",
26
+ "AsyncGrpcServerBuilder",
27
+ "AsyncServer",
28
+ "GrpcApp",
29
+ "GrpcAsyncServerProtocol",
30
+ "GrpcServerConfig",
31
+ "GrpcServerProtocol",
32
+ "GrpcServerSettingsProtocol",
33
+ "GrpcSettingsProtocol",
34
+ "GrpcSslSettingsProtocol",
35
+ "ServerLifecycleManager",
36
+ "bind_server_port",
37
+ "build_grpc_options",
38
+ "create_async_grpc_server",
39
+ "create_base_async_grpc_server",
40
+ "load_server_credentials",
41
+ "reset_signal_handlers",
42
+ "run_async_grpc_server",
43
+ "setup_signal_handlers",
44
+ ]
@@ -0,0 +1,368 @@
1
+ """The one-object happy path for running an async gRPC server.
2
+
3
+ :class:`GrpcApp` collapses builder + port binding + lifecycle into a single
4
+ facade::
5
+
6
+ import asyncio
7
+ from grpc_server_kit.aio import GrpcApp
8
+
9
+ app = GrpcApp(port=50051)
10
+ app.add_servicer(MyServicer(), add_MyServiceServicer_to_server)
11
+ asyncio.run(app.run())
12
+
13
+ For finer control (DI containers, custom wiring) use the underlying pieces:
14
+ :class:`~grpc_server_kit.aio.builder.AsyncGrpcServerBuilder`,
15
+ :func:`~grpc_server_kit.server.bind_server_port`, and
16
+ :class:`~grpc_server_kit.aio.runtime.ServerLifecycleManager`.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import asyncio
22
+ import logging
23
+ from typing import TYPE_CHECKING, Any, Protocol
24
+
25
+ from ..config import GrpcServerConfig
26
+ from ..constants import DEFAULT_HEALTH_CACHE_TTL, DEFAULT_HEALTH_CHECK_TIMEOUT, HEALTH_SERVICE_NAME
27
+ from ..server import bind_server_port
28
+ from .runtime import ServerLifecycleManager
29
+ from .server import AsyncServer, create_async_grpc_server
30
+
31
+ if TYPE_CHECKING:
32
+ from collections.abc import Callable, Sequence
33
+ from types import TracebackType
34
+
35
+ import grpc
36
+
37
+ from ..protocols import GrpcServerSettingsProtocol
38
+ from .health.protocols import AsyncHealthChecker
39
+
40
+ __all__ = ["GrpcApp"]
41
+
42
+ logger = logging.getLogger(__name__)
43
+
44
+ _UNSET: Any = object()
45
+
46
+
47
+ class _HealthDefaultsProtocol(Protocol):
48
+ """Duck-typed shape of a ``health`` settings block (see BaseHealthSettings)."""
49
+
50
+ @property
51
+ def cache_ttl(self) -> float: ...
52
+ @property
53
+ def check_timeout(self) -> float: ...
54
+
55
+
56
+ class GrpcApp:
57
+ """Async gRPC application: configuration, servicers, and lifecycle in one place.
58
+
59
+ The app is configured with plain method calls, then :meth:`run` builds the
60
+ server, binds the port (TLS-aware), installs signal handlers, and serves
61
+ until termination or :meth:`request_shutdown`.
62
+
63
+ A GrpcApp instance is **single-use**: once its server has run and stopped
64
+ (via :meth:`run` or the async context manager), create a new instance to
65
+ serve again — gRPC servers cannot be restarted.
66
+
67
+ Args:
68
+ settings: Full server settings (anything satisfying
69
+ ``GrpcServerSettingsProtocol``). When omitted, a default
70
+ :class:`~grpc_server_kit.config.GrpcServerConfig` is created from
71
+ ``host`` / ``port``.
72
+ host: Bind host shortcut (only when ``settings`` is omitted).
73
+ port: Bind port shortcut (only when ``settings`` is omitted; 0 binds an
74
+ ephemeral port exposed via :attr:`bound_port`).
75
+ interceptors: Initial interceptor chain (outermost first).
76
+ """
77
+
78
+ def __init__(
79
+ self,
80
+ settings: GrpcServerSettingsProtocol | None = None,
81
+ *,
82
+ host: str | None = None,
83
+ port: int | None = None,
84
+ interceptors: Sequence[grpc.aio.ServerInterceptor] | None = None,
85
+ ) -> None:
86
+ if settings is not None and (host is not None or port is not None):
87
+ raise ValueError("Pass either settings or host/port shortcuts, not both")
88
+ if settings is None:
89
+ shortcut_kwargs: dict[str, Any] = {}
90
+ if host is not None:
91
+ shortcut_kwargs["host"] = host
92
+ if port is not None:
93
+ shortcut_kwargs["port"] = port
94
+ settings = GrpcServerConfig(**shortcut_kwargs)
95
+
96
+ self._settings = settings
97
+ self._interceptors: list[grpc.aio.ServerInterceptor] = list(interceptors or [])
98
+ self._registrars: list[Callable[[grpc.aio.Server], None]] = []
99
+ self._reflection_names: list[str] | None = None
100
+ self._channelz_enabled = settings.enable_channelz
101
+ self._health_kwargs: dict[str, Any] | None = None
102
+ self._server: AsyncServer | None = None
103
+ self._manager: ServerLifecycleManager | None = None
104
+ self._bound_port: int | None = None
105
+ self._finished = False
106
+ self._shutdown_requested: str | None = None
107
+
108
+ # -- configuration ----------------------------------------------------------------
109
+
110
+ def add_servicer(self, servicer: object, add_to_server: Callable[[Any, Any], Any]) -> None:
111
+ """Add a servicer via its generated ``add_*Servicer_to_server`` function.
112
+
113
+ Example::
114
+
115
+ app.add_servicer(MyServicer(), add_MyServiceServicer_to_server)
116
+ """
117
+ self._ensure_not_built()
118
+ self._registrars.append(lambda server: add_to_server(servicer, server))
119
+
120
+ def register(self, callback: Callable[[grpc.aio.Server], None]) -> None:
121
+ """Add a registration callback receiving the raw ``grpc.aio.Server``.
122
+
123
+ Escape hatch for registrations that need the server object directly
124
+ (generic handlers, third-party integrations).
125
+ """
126
+ self._ensure_not_built()
127
+ self._registrars.append(callback)
128
+
129
+ def add_interceptors(self, interceptors: Sequence[grpc.aio.ServerInterceptor]) -> None:
130
+ """Append interceptors to the chain (outermost first)."""
131
+ self._ensure_not_built()
132
+ self._interceptors.extend(interceptors)
133
+
134
+ def enable_reflection(self, service_names: Sequence[str]) -> None:
135
+ """Enable server reflection for the given fully-qualified service names.
136
+
137
+ Requires the ``[reflection]`` extra.
138
+ """
139
+ self._ensure_not_built()
140
+ self._reflection_names = list(service_names)
141
+
142
+ def enable_channelz(self) -> None:
143
+ """Enable channelz debugging. Requires the ``[channelz]`` extra."""
144
+ self._ensure_not_built()
145
+ self._channelz_enabled = True
146
+
147
+ def enable_health(
148
+ self,
149
+ checkers: Sequence[AsyncHealthChecker] | None = None,
150
+ *,
151
+ cache_ttl: float | None = _UNSET,
152
+ check_timeout: float = _UNSET,
153
+ service_names: Sequence[str] | None = None,
154
+ ) -> None:
155
+ """Enable the gRPC Health Checking Protocol v1 servicer.
156
+
157
+ Requires the ``[health]`` extra. When ``cache_ttl`` / ``check_timeout``
158
+ are not passed explicitly, they are read from ``settings.health`` if the
159
+ app's settings object carries a health block (see
160
+ :class:`~grpc_server_kit.settings.BaseHealthSettings`), else from the
161
+ kit defaults.
162
+
163
+ Args:
164
+ checkers: Dependency checkers (``async check() -> bool``); with none,
165
+ the service always reports SERVING.
166
+ cache_ttl: TTL for cached check results in seconds. ``0`` and
167
+ ``None`` both disable caching.
168
+ check_timeout: Timeout for one health check run in seconds.
169
+ service_names: Additional service names to report health for.
170
+
171
+ Raises:
172
+ ValueError: If ``cache_ttl`` is negative or ``check_timeout`` is not positive.
173
+ """
174
+ self._ensure_not_built()
175
+
176
+ health_defaults: _HealthDefaultsProtocol | None = getattr(self._settings, "health", None)
177
+ if cache_ttl is _UNSET:
178
+ cache_ttl = health_defaults.cache_ttl if health_defaults is not None else DEFAULT_HEALTH_CACHE_TTL
179
+ if check_timeout is _UNSET:
180
+ check_timeout = (
181
+ health_defaults.check_timeout if health_defaults is not None else DEFAULT_HEALTH_CHECK_TIMEOUT
182
+ )
183
+
184
+ if cache_ttl is not None and cache_ttl < 0:
185
+ raise ValueError(f"cache_ttl must be non-negative or None, got {cache_ttl}")
186
+ if check_timeout <= 0:
187
+ raise ValueError(f"check_timeout must be positive, got {check_timeout}")
188
+
189
+ self._health_kwargs = {
190
+ "checkers": list(checkers) if checkers else None,
191
+ # 0 naturally means "no caching" — normalize it to None here so a
192
+ # settings-valid 0 can never crash HealthCache at build time.
193
+ "cache_ttl": cache_ttl if cache_ttl else None,
194
+ "check_timeout": check_timeout,
195
+ "service_names": list(service_names) if service_names else None,
196
+ }
197
+
198
+ # -- state ------------------------------------------------------------------------
199
+
200
+ @property
201
+ def settings(self) -> GrpcServerSettingsProtocol:
202
+ """The settings this app was configured with."""
203
+ return self._settings
204
+
205
+ @property
206
+ def server(self) -> AsyncServer:
207
+ """The built server (available after :meth:`build` / :meth:`run`)."""
208
+ if self._server is None:
209
+ raise RuntimeError("Server is not built yet; call build() or run() first")
210
+ return self._server
211
+
212
+ @property
213
+ def bound_port(self) -> int:
214
+ """The actually bound port (useful with ``port=0``)."""
215
+ if self._bound_port is None:
216
+ raise RuntimeError("Port is not bound yet; call build() or run() first")
217
+ return self._bound_port
218
+
219
+ def request_shutdown(self, reason: str = "requested") -> None:
220
+ """Request a graceful stop of this app.
221
+
222
+ Idempotent and safe at any point of the lifecycle — stopping something
223
+ that is not running is not an error, and any "is it still running?"
224
+ check by the caller would be racy anyway (the server can terminate on
225
+ its own between the check and the call):
226
+
227
+ - while serving: the :meth:`run` loop drains and returns;
228
+ - before :meth:`run`: the request is remembered and honored as soon as
229
+ the server starts, so a signal racing startup is never lost;
230
+ - after the app has stopped: no-op.
231
+ """
232
+ if self._shutdown_requested is None:
233
+ self._shutdown_requested = reason
234
+ if self._manager is not None:
235
+ self._manager.request_shutdown(reason)
236
+ else:
237
+ logger.debug("Shutdown requested while not serving", extra={"reason": reason})
238
+
239
+ # -- lifecycle --------------------------------------------------------------------
240
+
241
+ def build(self) -> AsyncServer:
242
+ """Build the server, register servicers/health, and bind the port.
243
+
244
+ Idempotent: subsequent calls return the already-built server. Nothing is
245
+ cached on failure, so a failed build (e.g. a busy port or a bad TLS
246
+ file) can simply be retried after fixing the cause.
247
+ """
248
+ self._ensure_not_finished()
249
+ if self._server is not None:
250
+ return self._server
251
+
252
+ registrars = list(self._registrars)
253
+ reflection_names = list(self._reflection_names) if self._reflection_names is not None else None
254
+
255
+ if self._health_kwargs is not None:
256
+ registrars.append(self._make_health_registrar(self._health_kwargs))
257
+ if reflection_names is not None and HEALTH_SERVICE_NAME not in reflection_names:
258
+ reflection_names.append(HEALTH_SERVICE_NAME)
259
+
260
+ enable_reflection = reflection_names is not None or self._settings.enable_reflection
261
+ if enable_reflection and not reflection_names:
262
+ raise ValueError(
263
+ "Reflection is enabled but no service names are configured; "
264
+ "call app.enable_reflection([...]) with your fully-qualified service names"
265
+ )
266
+
267
+ def register_all(server: grpc.aio.Server) -> None:
268
+ for registrar in registrars:
269
+ registrar(server)
270
+
271
+ # Build into locals; publish to self only after the port is bound so a
272
+ # bind failure leaves the app clean and retryable.
273
+ server = create_async_grpc_server(
274
+ interceptors=self._interceptors,
275
+ settings=self._settings,
276
+ register_servicers=register_all,
277
+ enable_reflection=enable_reflection,
278
+ reflection_service_names=reflection_names,
279
+ enable_channelz=self._channelz_enabled,
280
+ )
281
+ bound_port = bind_server_port(server, self._settings)
282
+
283
+ self._server = server
284
+ self._bound_port = bound_port
285
+ return server
286
+
287
+ async def run(self, *, setup_signals: bool = True) -> None:
288
+ """Build (if needed) and serve until termination, signal, or shutdown request."""
289
+ self._ensure_not_finished()
290
+ if self._manager is not None:
291
+ raise RuntimeError("Server is already running")
292
+
293
+ server = self.build()
294
+ self._manager = ServerLifecycleManager(
295
+ server=server,
296
+ address=f"{self._settings.host}:{self.bound_port}",
297
+ grace_period=self._settings.grace_period,
298
+ )
299
+ if self._shutdown_requested is not None:
300
+ # A shutdown requested before serving started (e.g. SIGTERM racing
301
+ # a slow startup) is honored instead of being dropped.
302
+ self._manager.request_shutdown(self._shutdown_requested)
303
+ try:
304
+ await self._manager.run(setup_signals=setup_signals)
305
+ finally:
306
+ self._manager = None
307
+ self._finished = True
308
+
309
+ def run_sync(self, *, setup_signals: bool = True) -> None:
310
+ """Blocking convenience wrapper: ``asyncio.run(app.run())``."""
311
+ asyncio.run(self.run(setup_signals=setup_signals))
312
+
313
+ async def __aenter__(self) -> GrpcApp:
314
+ """Start the server without signal handling (embedding/tests)."""
315
+ self._ensure_not_finished()
316
+ server = self.build()
317
+ await server.start()
318
+ logger.info("gRPC server started", extra={"address": f"{self._settings.host}:{self.bound_port}"})
319
+ return self
320
+
321
+ async def __aexit__(
322
+ self,
323
+ exc_type: type[BaseException] | None,
324
+ exc_val: BaseException | None,
325
+ exc_tb: TracebackType | None,
326
+ ) -> None:
327
+ """Stop the server with the configured grace period."""
328
+ self._finished = True
329
+ await self.server.stop(self._settings.grace_period)
330
+
331
+ # -- internals --------------------------------------------------------------------
332
+
333
+ def _ensure_not_built(self) -> None:
334
+ if self._server is not None:
335
+ raise RuntimeError("Server is already built; configure the app before build()/run()")
336
+
337
+ def _ensure_not_finished(self) -> None:
338
+ if self._finished:
339
+ raise RuntimeError(
340
+ "This GrpcApp has already served and stopped; gRPC servers cannot restart — create a new GrpcApp"
341
+ )
342
+
343
+ @staticmethod
344
+ def _make_health_registrar(health_kwargs: dict[str, Any]) -> Callable[[grpc.aio.Server], None]:
345
+ try:
346
+ # Deferred import: the [health] extra is optional for the core app.
347
+ from grpc_health.v1 import health_pb2_grpc # noqa: PLC0415
348
+
349
+ from .health import AsyncDynamicHealthServicer, HealthCache # noqa: PLC0415
350
+ except ImportError as exc:
351
+ raise ImportError(
352
+ "grpcio-health-checking is not installed. "
353
+ "Please install it with 'pip install grpcio-health-checking' "
354
+ "or use 'grpc-server-kit[health]' optional dependency."
355
+ ) from exc
356
+
357
+ cache_ttl = health_kwargs["cache_ttl"]
358
+ servicer = AsyncDynamicHealthServicer(
359
+ checkers=health_kwargs["checkers"],
360
+ cache=HealthCache(ttl=cache_ttl) if cache_ttl is not None else None,
361
+ service_names=health_kwargs["service_names"],
362
+ check_timeout=health_kwargs["check_timeout"],
363
+ )
364
+
365
+ def register(server: grpc.aio.Server) -> None:
366
+ health_pb2_grpc.add_HealthServicer_to_server(servicer, server)
367
+
368
+ return register
@@ -0,0 +1,63 @@
1
+ """Type-safe fluent builder for async gRPC servers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Self
6
+
7
+ from .server import create_async_grpc_server
8
+
9
+ if TYPE_CHECKING:
10
+ from collections.abc import Callable, Sequence
11
+
12
+ import grpc
13
+
14
+ from ..protocols import GrpcServerSettingsProtocol
15
+ from .server import AsyncServer
16
+
17
+
18
+ class AsyncGrpcServerBuilder:
19
+ """Fluent builder for an :class:`AsyncServer`.
20
+
21
+ Advanced API — for the common path prefer :class:`grpc_server_kit.aio.GrpcApp`.
22
+ """
23
+
24
+ def __init__(self, settings: GrpcServerSettingsProtocol) -> None:
25
+ self._settings = settings
26
+ self._interceptors: list[grpc.aio.ServerInterceptor] = []
27
+ self._register_servicers: Callable[[grpc.aio.Server], None] | None = None
28
+ self._enable_reflection = False
29
+ self._reflection_service_names: Sequence[str] | None = None
30
+ self._enable_channelz = False
31
+
32
+ def with_interceptors(self, interceptors: list[grpc.aio.ServerInterceptor]) -> Self:
33
+ """Add interceptors to the server."""
34
+ self._interceptors.extend(interceptors)
35
+ return self
36
+
37
+ def with_servicers(self, register_servicers: Callable[[grpc.aio.Server], None]) -> Self:
38
+ """Set the servicer registration callback (optional — servicers can also be
39
+ registered later on the built server)."""
40
+ self._register_servicers = register_servicers
41
+ return self
42
+
43
+ def with_reflection(self, service_names: Sequence[str]) -> Self:
44
+ """Enable reflection for the given services."""
45
+ self._enable_reflection = True
46
+ self._reflection_service_names = service_names
47
+ return self
48
+
49
+ def with_channelz(self) -> Self:
50
+ """Enable channelz for the server."""
51
+ self._enable_channelz = True
52
+ return self
53
+
54
+ def build(self) -> AsyncServer:
55
+ """Build and return the configured AsyncServer."""
56
+ return create_async_grpc_server(
57
+ interceptors=self._interceptors,
58
+ settings=self._settings,
59
+ register_servicers=self._register_servicers,
60
+ enable_reflection=self._enable_reflection,
61
+ reflection_service_names=self._reflection_service_names,
62
+ enable_channelz=self._enable_channelz,
63
+ )
@@ -0,0 +1,28 @@
1
+ """Async Dishka integration for the gRPC server (``[dishka]`` extra).
2
+
3
+ - ``DishkaAioInterceptor`` — request-scoped container per RPC.
4
+ - ``AsyncGrpcServerProvider`` — builds the configured ``AsyncServer``.
5
+ - ``grpc_server_providers`` + the granular providers — the standard interceptor
6
+ chain, metrics, and observability seams in one registration.
7
+
8
+ Combine with the shared helpers in :mod:`grpc_server_kit.dishka`
9
+ (``GrpcServerSettingsProvider``, ``GrpcioProvider``, ``inject``, ``FromDishka``).
10
+ """
11
+
12
+ from dishka.integrations.grpcio import DishkaAioInterceptor
13
+
14
+ from .bundle import grpc_server_providers
15
+ from .interceptors import GrpcServerInterceptorsProvider
16
+ from .metrics import PrometheusGrpcServerMetricsProvider
17
+ from .observability import SentryAdapterProvider, TracerAdapterProvider
18
+ from .server import AsyncGrpcServerProvider
19
+
20
+ __all__ = [
21
+ "AsyncGrpcServerProvider",
22
+ "DishkaAioInterceptor",
23
+ "GrpcServerInterceptorsProvider",
24
+ "PrometheusGrpcServerMetricsProvider",
25
+ "SentryAdapterProvider",
26
+ "TracerAdapterProvider",
27
+ "grpc_server_providers",
28
+ ]
@@ -0,0 +1,70 @@
1
+ """One-call bundle of the standard gRPC server Dishka providers (``[dishka]`` extra)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+
7
+ import grpc
8
+ from dishka import Provider
9
+
10
+ from grpc_server_kit.aio.dishka.interceptors import GrpcServerInterceptorsProvider
11
+ from grpc_server_kit.aio.dishka.metrics import PrometheusGrpcServerMetricsProvider
12
+ from grpc_server_kit.aio.dishka.observability import SentryAdapterProvider, TracerAdapterProvider
13
+ from grpc_server_kit.aio.dishka.server import AsyncGrpcServerProvider
14
+ from grpc_server_kit.aio.interceptors import ErrorDetailFactory, HeaderConfig
15
+ from grpc_server_kit.dishka.settings import GrpcServerSettingsProvider
16
+ from grpc_server_kit.protocols import GrpcServerSettingsProtocol
17
+
18
+
19
+ def grpc_server_providers(
20
+ settings: GrpcServerSettingsProtocol,
21
+ *,
22
+ service_name: str,
23
+ error_status_map: dict[type[Exception], grpc.StatusCode] | None = None,
24
+ detail_factory: ErrorDetailFactory | None = None,
25
+ header_configs: Sequence[HeaderConfig] | None = None,
26
+ metrics_enabled: bool | None = None,
27
+ metrics_prefix: str | None = None,
28
+ sentry_enabled: bool = True,
29
+ tracing_enabled: bool = True,
30
+ reflection_service_names: Sequence[str] | None = None,
31
+ ) -> tuple[Provider, ...]:
32
+ """Return the standard set of gRPC server providers for one-line registration.
33
+
34
+ Register the result in a Dishka container together with ``GrpcioProvider()`` and your
35
+ servicer/domain providers::
36
+
37
+ from grpc_server_kit.dishka import GrpcioProvider
38
+ from grpc_server_kit.aio.dishka import grpc_server_providers
39
+
40
+ container = make_async_container(
41
+ *grpc_server_providers(settings.grpc, service_name="my.pkg.MyService"),
42
+ GrpcioProvider(),
43
+ MyDomainProvider(),
44
+ )
45
+
46
+ Then resolve ``AsyncServer`` from the container, register your servicers on it,
47
+ ``bind_server_port(server, settings.grpc)`` and ``run_async_grpc_server(server, address=...)``.
48
+
49
+ Args:
50
+ metrics_enabled: Explicit metrics toggle. When None (default), read from
51
+ ``settings.metrics_enabled`` if the settings object has that field,
52
+ else True.
53
+ """
54
+ if metrics_enabled is None:
55
+ metrics_enabled = bool(getattr(settings, "metrics_enabled", True))
56
+ return (
57
+ GrpcServerSettingsProvider(settings, service_name=service_name),
58
+ PrometheusGrpcServerMetricsProvider(enabled=metrics_enabled, prefix=metrics_prefix),
59
+ SentryAdapterProvider(enabled=sentry_enabled),
60
+ TracerAdapterProvider(enabled=tracing_enabled),
61
+ GrpcServerInterceptorsProvider(
62
+ error_status_map=error_status_map,
63
+ detail_factory=detail_factory,
64
+ header_configs=header_configs,
65
+ ),
66
+ AsyncGrpcServerProvider(reflection_service_names=reflection_service_names),
67
+ )
68
+
69
+
70
+ __all__ = ["grpc_server_providers"]