simple-grpcio 0.1.5__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.
- simple_grpc/__init__.py +13 -0
- simple_grpc/applications.py +436 -0
- simple_grpc/dispatch.py +380 -0
- simple_grpc/exceptions.py +130 -0
- simple_grpc/logging.py +183 -0
- simple_grpc/models.py +243 -0
- simple_grpc/params.py +89 -0
- simple_grpc/reloading.py +176 -0
- simple_grpc/utils.py +72 -0
- simple_grpcio-0.1.5.dist-info/METADATA +172 -0
- simple_grpcio-0.1.5.dist-info/RECORD +12 -0
- simple_grpcio-0.1.5.dist-info/WHEEL +4 -0
simple_grpc/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from collections.abc import Iterator
|
|
2
|
+
|
|
3
|
+
from google.protobuf.message import Message
|
|
4
|
+
from grpc import ServicerContext as Context, StatusCode as status
|
|
5
|
+
|
|
6
|
+
from .applications import SimpleGRPC
|
|
7
|
+
from .params import Depends
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
Request = Message # Requests are application-defined proto message classes, handled opaquely,
|
|
11
|
+
RequestIterator = Iterator[Message] # so Any is the honest annotation.
|
|
12
|
+
|
|
13
|
+
# __all__ = ["Context", "Depends", "Request", "RequestIterator", "SimpleGRPC", "status"]
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
from collections.abc import Callable, Iterator
|
|
2
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import sys
|
|
6
|
+
from types import ModuleType
|
|
7
|
+
from typing import Any, Unpack
|
|
8
|
+
|
|
9
|
+
from google.protobuf import message_factory
|
|
10
|
+
from google.protobuf.message import Message
|
|
11
|
+
import grpc
|
|
12
|
+
from grpc_tools import protoc
|
|
13
|
+
|
|
14
|
+
import simple_grpc.dispatch as dispatch
|
|
15
|
+
from simple_grpc.logging import (
|
|
16
|
+
LoggingConfig,
|
|
17
|
+
configure_logging,
|
|
18
|
+
logger,
|
|
19
|
+
style,
|
|
20
|
+
)
|
|
21
|
+
from simple_grpc.models import ProtoConfig, ServerCredentials, ServerOptions
|
|
22
|
+
from simple_grpc.params import INJECTABLE, Depends
|
|
23
|
+
import simple_grpc.utils as utils
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SimpleGRPC:
|
|
27
|
+
"""
|
|
28
|
+
`SimpleGRPC` app class, the main entrypoint to use SimpleGRPC.
|
|
29
|
+
|
|
30
|
+
Handlers are attached with :meth:`rpc` and may declare any subset of the
|
|
31
|
+
injectable parameters (by name): ``request`` (the request message, or the
|
|
32
|
+
request iterator on streaming RPCs), ``request_iterator`` (alias, same
|
|
33
|
+
object) and ``context``.
|
|
34
|
+
|
|
35
|
+
Handlers may also declare dependencies with
|
|
36
|
+
``Depends(...)`` parameter defaults. Dependencies (app-wide ones passed to
|
|
37
|
+
the constructor, or signature ones declared on the handler) are
|
|
38
|
+
plain functions resolved automatically at call time, yielding pre/post
|
|
39
|
+
interceptor-like behavior for generator dependencies.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
# Parameter names a handler may declare for dependency injection.
|
|
43
|
+
INJECTABLE = INJECTABLE # just for compatibility
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
*,
|
|
48
|
+
thread_pool: ThreadPoolExecutor | None = None,
|
|
49
|
+
raw_protos: dict[str, str] | None = None,
|
|
50
|
+
proto_path: str | Path | None = None,
|
|
51
|
+
proto_files: list[str] | None = None,
|
|
52
|
+
service_path: str | Path | None = None,
|
|
53
|
+
port: int = 50051,
|
|
54
|
+
dependencies: list[Depends] | None = None,
|
|
55
|
+
credentials: ServerCredentials | None = None,
|
|
56
|
+
logging_config: LoggingConfig | None = None,
|
|
57
|
+
**server_options: Unpack[ServerOptions],
|
|
58
|
+
) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Store the compile config; handlers attach later via :meth:`rpc`.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
ValueError: If ``raw_protos`` is passed together with
|
|
64
|
+
``proto_path`` or ``proto_files`` - provide protos either as
|
|
65
|
+
inline content or as files on disk, not both.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
thread_pool: Executor used to serve requests. Defaults to a new
|
|
69
|
+
``ThreadPoolExecutor``.
|
|
70
|
+
raw_protos: Proto content as inline strings, keyed by the stem to
|
|
71
|
+
give the generated modules (``{"calc": "syntax..."}`` ->
|
|
72
|
+
``calc_pb2``). Materialized into a content-addressed dir under
|
|
73
|
+
``tempfile.gettempdir()``; identical content maps to the
|
|
74
|
+
identical dir, so a client constructing the app with the same
|
|
75
|
+
content finds the generated code without coordination.
|
|
76
|
+
proto_path: Directory containing ``.proto`` files. ``None``
|
|
77
|
+
(default) resolves to the raw-proto dir in raw mode, else
|
|
78
|
+
``"protos"``.
|
|
79
|
+
proto_files: Proto files to compile; ``None`` (default) discovers
|
|
80
|
+
all ``*.proto`` in ``proto_path`` in file mode, or uses the
|
|
81
|
+
``raw_protos`` keys in raw mode.
|
|
82
|
+
service_path: Output directory for the generated ``*_pb2*.py``.
|
|
83
|
+
``None`` (default) resolves to the raw-proto dir in raw mode,
|
|
84
|
+
else ``"grpc_services"``. An explicitly passed value wins in
|
|
85
|
+
both modes (the client passes the same one when rendezvousing).
|
|
86
|
+
port: Port the server binds with ``dev()``/``run()``.
|
|
87
|
+
credentials: TLS credentials; each PEM field takes a ``Path`` or
|
|
88
|
+
the content itself. Given, ``_start`` binds with
|
|
89
|
+
``add_secure_port`` (and ``ssl_server_credentials``); absent,
|
|
90
|
+
the port stays insecure.
|
|
91
|
+
logging_config: Logging knobs (access log, level, colors);
|
|
92
|
+
defaults to ``LoggingConfig()``.
|
|
93
|
+
dependencies: App-wide ``Depends`` resolved before every RPC,
|
|
94
|
+
applied automatically to all handlers. Generator dependencies
|
|
95
|
+
wrap each call (pre/`yield`/post).
|
|
96
|
+
**server_options: Forwarded verbatim to ``grpc.server()`` at
|
|
97
|
+
startup (``options``, ``compression``, ``interceptors``,
|
|
98
|
+
``maximum_concurrent_rpcs``, ``xds``, ...). Passing
|
|
99
|
+
``thread_pool`` here raises ``TypeError``. ``handlers``
|
|
100
|
+
bypasses this library's servicer registration and is only
|
|
101
|
+
useful for exotic ``GenericRpcHandler`` setups.
|
|
102
|
+
"""
|
|
103
|
+
self.thread_pool = thread_pool or ThreadPoolExecutor()
|
|
104
|
+
self.proto_config = ProtoConfig.config_proto_files(
|
|
105
|
+
raw_protos=raw_protos,
|
|
106
|
+
proto_path=proto_path,
|
|
107
|
+
proto_files=proto_files,
|
|
108
|
+
service_path=service_path,
|
|
109
|
+
)
|
|
110
|
+
self.port = port
|
|
111
|
+
self.credentials = credentials
|
|
112
|
+
self.logging_config = logging_config or LoggingConfig()
|
|
113
|
+
self._handlers: dict[str, dict[str, Callable]] = {}
|
|
114
|
+
self._schemaless_handlers: dict[str, dict[str, Callable]] = {}
|
|
115
|
+
self._app_dependencies: tuple[Depends, ...] = tuple(dependencies or ())
|
|
116
|
+
self._server_options: ServerOptions = server_options
|
|
117
|
+
|
|
118
|
+
def _start(self) -> None:
|
|
119
|
+
"""Compile, load generated modules, register services, and serve."""
|
|
120
|
+
|
|
121
|
+
configure_logging(self.logging_config)
|
|
122
|
+
|
|
123
|
+
logger.info("Starting simple_grpc (pid %s)", os.getpid())
|
|
124
|
+
mods: dict[str, tuple[ModuleType, ModuleType]] = {}
|
|
125
|
+
|
|
126
|
+
if self._handlers:
|
|
127
|
+
self._compile()
|
|
128
|
+
proto = self.proto_config
|
|
129
|
+
logger.info(
|
|
130
|
+
"Compiled %d proto file(s) from '%s' into '%s'",
|
|
131
|
+
len(proto.proto_files),
|
|
132
|
+
proto.proto_path,
|
|
133
|
+
proto.service_path,
|
|
134
|
+
)
|
|
135
|
+
mods = self._load_generated()
|
|
136
|
+
|
|
137
|
+
server = grpc.server(self.thread_pool, **self._server_options)
|
|
138
|
+
self._register_services(mods, server)
|
|
139
|
+
self._register_schemaless(server)
|
|
140
|
+
registered = {**self._handlers, **self._schemaless_handlers}
|
|
141
|
+
services = ", ".join(f"{svc} ({', '.join(m)})" for svc, m in registered.items())
|
|
142
|
+
logger.info("Registered services: %s", services or "(none)")
|
|
143
|
+
|
|
144
|
+
if self.credentials:
|
|
145
|
+
server.add_secure_port(f"[::]:{self.port}", self.credentials.to_server_credentials())
|
|
146
|
+
tls = "(TLS)"
|
|
147
|
+
else:
|
|
148
|
+
server.add_insecure_port(f"[::]:{self.port}")
|
|
149
|
+
tls = f"({style('No', self.logging_config.use_colors, bold=True)} TLS)"
|
|
150
|
+
|
|
151
|
+
server.start()
|
|
152
|
+
logger.info("Serving on [::]:%d %s - press CTRL+C to quit", self.port, tls)
|
|
153
|
+
server.wait_for_termination()
|
|
154
|
+
|
|
155
|
+
def _compile(self) -> None:
|
|
156
|
+
"""
|
|
157
|
+
Run protoc against the configured proto files.
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
RuntimeError: If ``protoc`` exits with a non-zero status.
|
|
161
|
+
"""
|
|
162
|
+
if not self.proto_config.service_path.exists(): # type: ignore[reportOptionalMemberAccess]
|
|
163
|
+
raise ValueError(f"service_path '{self.proto_config.service_path}' does not exist.")
|
|
164
|
+
if len(self.proto_config.proto_files) == 0:
|
|
165
|
+
raise RuntimeError(
|
|
166
|
+
f"no proto files in directory: {self.proto_config.proto_path} - "
|
|
167
|
+
f"proto-backed @app.rpc handlers require a .proto file; or use "
|
|
168
|
+
f"@app.schemaless_rpc for endpoints without a schema"
|
|
169
|
+
)
|
|
170
|
+
if protoc.main(self.proto_config.to_compiler_args()) != 0:
|
|
171
|
+
raise RuntimeError(f"protoc failed for {self.proto_config.proto_files}")
|
|
172
|
+
|
|
173
|
+
def _load_generated(self) -> dict[str, tuple[ModuleType, ModuleType]]:
|
|
174
|
+
"""
|
|
175
|
+
Import the freshly compiled ``*_pb2``/``*_pb2_grpc`` modules.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Mapping of proto stem to ``(pb2_module, pb2_grpc_module)``.
|
|
179
|
+
"""
|
|
180
|
+
sp = self.proto_config.service_path
|
|
181
|
+
mods: dict[str, tuple[ModuleType, ModuleType]] = {}
|
|
182
|
+
|
|
183
|
+
for pf in self.proto_config.proto_files:
|
|
184
|
+
name = Path(pf).stem
|
|
185
|
+
pb2 = utils.module_from_file(sp / f"{name}_pb2.py", f"{name}_pb2")
|
|
186
|
+
# _pb2_grpc does a bare `import <name>_pb2`, so seed sys.modules;
|
|
187
|
+
# pop first so a recompile in the dev loop never serves stale code.
|
|
188
|
+
sys.modules.pop(f"{name}_pb2", None)
|
|
189
|
+
sys.modules[f"{name}_pb2"] = pb2
|
|
190
|
+
pb2_grpc = utils.module_from_file(sp / f"{name}_pb2_grpc.py", f"{name}_pb2_grpc") # type: ignore
|
|
191
|
+
mods[name] = (pb2, pb2_grpc)
|
|
192
|
+
|
|
193
|
+
return mods
|
|
194
|
+
|
|
195
|
+
def _build_servicer(self, service: str, pb2: ModuleType, pb2_grpc: ModuleType) -> Any:
|
|
196
|
+
"""
|
|
197
|
+
Subclass the generated servicer so each RPC dispatches to its handler.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
service: Service name.
|
|
201
|
+
pb2: Generated messages module.
|
|
202
|
+
pb2_grpc: Generated service classes module.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
An instance of the generated servicer subclass.
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
ValueError: If a registered handler names an unknown RPC method.
|
|
209
|
+
"""
|
|
210
|
+
base = getattr(pb2_grpc, f"{service}Servicer")
|
|
211
|
+
handlers = self._handlers[service]
|
|
212
|
+
unknown = set(handlers) - set(vars(base))
|
|
213
|
+
if unknown:
|
|
214
|
+
available = list(pb2.DESCRIPTOR.services_by_name[service].methods_by_name)
|
|
215
|
+
raise ValueError(
|
|
216
|
+
f"service {service!r} has no RPC methods: {sorted(unknown)}. Available: {available}"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
methods = pb2.DESCRIPTOR.services_by_name[service].methods_by_name
|
|
220
|
+
package = pb2.DESCRIPTOR.package
|
|
221
|
+
prefix = f"{package}.{service}" if package else service
|
|
222
|
+
overrides = {
|
|
223
|
+
name: self._wrap_handler(
|
|
224
|
+
handler,
|
|
225
|
+
message_factory.GetMessageClass(methods[name].output_type),
|
|
226
|
+
self._app_dependencies,
|
|
227
|
+
path=f"/{prefix}/{name}",
|
|
228
|
+
)
|
|
229
|
+
for name, handler in handlers.items()
|
|
230
|
+
}
|
|
231
|
+
return type("SimpleGRPCServicer", (base,), overrides)()
|
|
232
|
+
|
|
233
|
+
def _register_services(
|
|
234
|
+
self, mods: dict[str, tuple[ModuleType, ModuleType]], server: grpc.Server
|
|
235
|
+
) -> None:
|
|
236
|
+
"""
|
|
237
|
+
Attach a dispatching servicer to the server for each service.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
mods: Mapping returned by :meth:`_load_generated`.
|
|
241
|
+
server: The server receiving the servicers.
|
|
242
|
+
"""
|
|
243
|
+
for service in self._handlers:
|
|
244
|
+
pb2, pb2_grpc = utils.find_service(mods, service)
|
|
245
|
+
servicer = self._build_servicer(service, pb2, pb2_grpc)
|
|
246
|
+
getattr(pb2_grpc, f"add_{service}Servicer_to_server")(servicer, server)
|
|
247
|
+
|
|
248
|
+
def _register_schemaless(self, server: grpc.Server) -> None:
|
|
249
|
+
"""
|
|
250
|
+
Attach one generic RPC handler per schemaless service.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
server: The server receiving the generic RPC handlers.
|
|
254
|
+
"""
|
|
255
|
+
for service, methods in self._schemaless_handlers.items():
|
|
256
|
+
method_handlers = {
|
|
257
|
+
name: dispatch.schemaless_method_handler(
|
|
258
|
+
handler,
|
|
259
|
+
dependencies=self._app_dependencies,
|
|
260
|
+
path=f"/{service}/{name}",
|
|
261
|
+
)
|
|
262
|
+
for name, handler in methods.items()
|
|
263
|
+
}
|
|
264
|
+
server.add_generic_rpc_handlers(
|
|
265
|
+
(grpc.method_handlers_generic_handler(service, method_handlers),)
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
@classmethod
|
|
269
|
+
def _stream(
|
|
270
|
+
cls,
|
|
271
|
+
result,
|
|
272
|
+
response_cls: type[Message],
|
|
273
|
+
context: grpc.ServicerContext,
|
|
274
|
+
tails,
|
|
275
|
+
path: str = "",
|
|
276
|
+
) -> Iterator[Any]:
|
|
277
|
+
"""Delegate to :func:`simple_grpc.dispatch.stream`."""
|
|
278
|
+
return dispatch.stream(result, response_cls, context, tails, path=path)
|
|
279
|
+
|
|
280
|
+
@classmethod
|
|
281
|
+
def _wrap_handler(
|
|
282
|
+
cls,
|
|
283
|
+
handler: Callable,
|
|
284
|
+
response_cls: type[Message],
|
|
285
|
+
dependencies: tuple[Depends, ...] = (),
|
|
286
|
+
path: str = "",
|
|
287
|
+
) -> Callable:
|
|
288
|
+
"""Delegate to :func:`simple_grpc.dispatch.wrap_handler`."""
|
|
289
|
+
return dispatch.wrap_handler(handler, response_cls, dependencies=dependencies, path=path)
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def protos(self) -> dict[str, str]:
|
|
293
|
+
"""
|
|
294
|
+
Raw content of every configured ``.proto`` file, keyed by file name.
|
|
295
|
+
|
|
296
|
+
Read on access, so edits to the proto files show up under hot reload.
|
|
297
|
+
"""
|
|
298
|
+
return {
|
|
299
|
+
pf: (self.proto_config.proto_path / pf).read_text(encoding="utf-8")
|
|
300
|
+
for pf in self.proto_config.proto_files
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
def rpc(
|
|
304
|
+
self,
|
|
305
|
+
service: str,
|
|
306
|
+
method: str,
|
|
307
|
+
) -> Callable[[Callable], Callable]:
|
|
308
|
+
"""Decorate a function as the handler for one RPC method.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
service: Proto service name, e.g. ``"RouteGuide"``.
|
|
312
|
+
method: RPC method name within the service, e.g. ``"GetFeature"``.
|
|
313
|
+
Dependencies may be declared via ``Depends(...)`` parameter
|
|
314
|
+
defaults; app-wide and signature dependencies run in that
|
|
315
|
+
order, outermost first.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
A decorator that registers the function and returns it unchanged.
|
|
319
|
+
"""
|
|
320
|
+
|
|
321
|
+
def decorator(func: Callable) -> Callable:
|
|
322
|
+
"""Register ``func`` under ``(service, method)``.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
func: The decorated handler function.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
The same function, unchanged.
|
|
329
|
+
"""
|
|
330
|
+
self._handlers.setdefault(service, {})[method] = func
|
|
331
|
+
return func
|
|
332
|
+
|
|
333
|
+
return decorator
|
|
334
|
+
|
|
335
|
+
def schemaless_rpc(
|
|
336
|
+
self,
|
|
337
|
+
service: str,
|
|
338
|
+
method: str,
|
|
339
|
+
) -> Callable[[Callable], Callable]:
|
|
340
|
+
"""
|
|
341
|
+
Decorate a function as the handler for one RPC, with no proto schema.
|
|
342
|
+
|
|
343
|
+
The endpoint is served without any ``.proto`` definition, using
|
|
344
|
+
``google.protobuf.Struct`` as the wire format on both sides: the
|
|
345
|
+
handler receives the request as a plain dict and returns a plain
|
|
346
|
+
dict (or a ``Struct`` message, or ``None``), exactly like the dict
|
|
347
|
+
contract of :meth:`rpc`.
|
|
348
|
+
|
|
349
|
+
The RPC flavor is inferred from the handler shape: declaring
|
|
350
|
+
``request_iterator`` makes it request-streaming; being a generator
|
|
351
|
+
function (or annotating an ``Iterator``/``Generator``/``Iterable``
|
|
352
|
+
return) makes it response-streaming; anything else is unary.
|
|
353
|
+
Dependencies may be declared via ``Depends(...)`` parameter
|
|
354
|
+
defaults, like :meth:`rpc`.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
service: Service name as clients address it, e.g.
|
|
358
|
+
``"Greeter"`` or a dotted ``"pkg.Greeter"``. The full method
|
|
359
|
+
path is ``/{service}/{method}``.
|
|
360
|
+
method: RPC method name within the service.
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
A decorator that registers the function and returns it unchanged.
|
|
364
|
+
"""
|
|
365
|
+
|
|
366
|
+
def decorator(func: Callable) -> Callable:
|
|
367
|
+
"""Register ``func`` under ``(service, method)``.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
func: The decorated handler function.
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
The same function, unchanged.
|
|
374
|
+
"""
|
|
375
|
+
self._schemaless_handlers.setdefault(service, {})[method] = func
|
|
376
|
+
return func
|
|
377
|
+
|
|
378
|
+
return decorator
|
|
379
|
+
|
|
380
|
+
def dev(
|
|
381
|
+
self,
|
|
382
|
+
reload: bool = True,
|
|
383
|
+
reload_dirs: list[str | Path] | None = None,
|
|
384
|
+
reload_excludes: list[str | Path] | None = None,
|
|
385
|
+
) -> None:
|
|
386
|
+
"""
|
|
387
|
+
Serve with hot reload: editing watched files restarts the server.
|
|
388
|
+
|
|
389
|
+
The app runs in a spawned child process, rebuilt from its module on
|
|
390
|
+
every restart (fresh imports, fresh ``protoc`` compile). By default
|
|
391
|
+
the working directory is watched, excluding the proto and service
|
|
392
|
+
directories plus the generated ``*_pb2*.py`` (protoc rewrites them
|
|
393
|
+
at every child start, which would otherwise loop reloads forever).
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
reload: Serve in-process like :meth:`run` when False.
|
|
397
|
+
reload_dirs: Directories to watch; defaults to the working
|
|
398
|
+
directory.
|
|
399
|
+
reload_excludes: Extra glob patterns and/or directories to
|
|
400
|
+
ignore, on top of the defaults (proto/service directories,
|
|
401
|
+
``*_pb2.py``/``*_pb2_grpc.py``).
|
|
402
|
+
|
|
403
|
+
Raises:
|
|
404
|
+
ImportError: If ``watchfiles`` is not installed.
|
|
405
|
+
RuntimeError: If the app is not bound to a module-level
|
|
406
|
+
variable, or if ``protoc`` exits with a non-zero status.
|
|
407
|
+
"""
|
|
408
|
+
configure_logging(self.logging_config)
|
|
409
|
+
|
|
410
|
+
if not reload:
|
|
411
|
+
self._start()
|
|
412
|
+
return
|
|
413
|
+
|
|
414
|
+
try:
|
|
415
|
+
from simple_grpc import reloading
|
|
416
|
+
except ImportError as exc:
|
|
417
|
+
raise ImportError(
|
|
418
|
+
"hot reload requires watchfiles: install it with 'pip install watchfiles'"
|
|
419
|
+
) from exc
|
|
420
|
+
|
|
421
|
+
module_ref, attr = reloading.locate_app(self, sys._getframe(1))
|
|
422
|
+
reloading.serve_with_reload(
|
|
423
|
+
self,
|
|
424
|
+
module_ref,
|
|
425
|
+
attr,
|
|
426
|
+
reload_dirs=reload_dirs,
|
|
427
|
+
reload_excludes=reload_excludes,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
def run(self) -> None:
|
|
431
|
+
"""Serve like :meth:`dev`; compile-skipping is deferred until needed.
|
|
432
|
+
|
|
433
|
+
Raises:
|
|
434
|
+
RuntimeError: If ``protoc`` exits with a non-zero status.
|
|
435
|
+
"""
|
|
436
|
+
self._start()
|