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.
- grpc_server_kit/__init__.py +34 -0
- grpc_server_kit/__version__.py +1 -0
- grpc_server_kit/aio/__init__.py +44 -0
- grpc_server_kit/aio/app.py +368 -0
- grpc_server_kit/aio/builder.py +63 -0
- grpc_server_kit/aio/dishka/__init__.py +28 -0
- grpc_server_kit/aio/dishka/bundle.py +70 -0
- grpc_server_kit/aio/dishka/interceptors.py +116 -0
- grpc_server_kit/aio/dishka/metrics.py +50 -0
- grpc_server_kit/aio/dishka/observability.py +86 -0
- grpc_server_kit/aio/dishka/server.py +51 -0
- grpc_server_kit/aio/health/__init__.py +36 -0
- grpc_server_kit/aio/health/cache.py +89 -0
- grpc_server_kit/aio/health/checkers/__init__.py +16 -0
- grpc_server_kit/aio/health/checkers/postgres.py +115 -0
- grpc_server_kit/aio/health/checkers/redis.py +88 -0
- grpc_server_kit/aio/health/checkers/utils.py +97 -0
- grpc_server_kit/aio/health/orchestrator.py +122 -0
- grpc_server_kit/aio/health/protocols.py +43 -0
- grpc_server_kit/aio/health/servicer.py +229 -0
- grpc_server_kit/aio/interceptors/__init__.py +62 -0
- grpc_server_kit/aio/interceptors/base.py +235 -0
- grpc_server_kit/aio/interceptors/context.py +179 -0
- grpc_server_kit/aio/interceptors/exception_handler.py +167 -0
- grpc_server_kit/aio/interceptors/metadata.py +57 -0
- grpc_server_kit/aio/interceptors/metrics.py +104 -0
- grpc_server_kit/aio/interceptors/protocols.py +54 -0
- grpc_server_kit/aio/interceptors/request_logger.py +103 -0
- grpc_server_kit/aio/interceptors/sentry.py +102 -0
- grpc_server_kit/aio/interceptors/tracing.py +130 -0
- grpc_server_kit/aio/observability/__init__.py +5 -0
- grpc_server_kit/aio/observability/tracing.py +48 -0
- grpc_server_kit/aio/runtime.py +206 -0
- grpc_server_kit/aio/server.py +191 -0
- grpc_server_kit/config.py +100 -0
- grpc_server_kit/constants.py +34 -0
- grpc_server_kit/credentials.py +112 -0
- grpc_server_kit/dishka/__init__.py +38 -0
- grpc_server_kit/dishka/settings.py +41 -0
- grpc_server_kit/interceptors/__init__.py +15 -0
- grpc_server_kit/interceptors/constants.py +44 -0
- grpc_server_kit/interceptors/utils.py +74 -0
- grpc_server_kit/observability/__init__.py +12 -0
- grpc_server_kit/observability/metrics.py +116 -0
- grpc_server_kit/observability/naming.py +26 -0
- grpc_server_kit/observability/protocols.py +86 -0
- grpc_server_kit/observability/sentry.py +63 -0
- grpc_server_kit/options.py +136 -0
- grpc_server_kit/protocols.py +114 -0
- grpc_server_kit/py.typed +0 -0
- grpc_server_kit/server.py +59 -0
- grpc_server_kit/settings.py +151 -0
- grpc_server_kit/signals.py +228 -0
- grpc_server_kit-0.1.0.dist-info/METADATA +484 -0
- grpc_server_kit-0.1.0.dist-info/RECORD +57 -0
- grpc_server_kit-0.1.0.dist-info/WHEEL +4 -0
- 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"]
|