veil-api-kit 0.10.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.
veil_kit/grpc.py ADDED
@@ -0,0 +1,525 @@
1
+ """gRPC: a channel for the stubs generated by ``grpc_tools.protoc``, and the server helpers of ``veil.grpc``.
2
+
3
+ .. code-block:: python
4
+
5
+ import demo_pb2, demo_pb2_grpc # python -m grpc_tools.protoc ... --grpc_python_out=.
6
+ from veil_kit import grpc as vgrpc
7
+
8
+ channel = vgrpc.insecure_channel("127.0.0.1:50051")
9
+ stub = demo_pb2_grpc.DemoStub(channel)
10
+ reply = stub.SayHello(demo_pb2.HelloRequest(name="veil"), timeout=5)
11
+
12
+ # asyncio (the stubs of grpc.aio work the same way)
13
+ async with vgrpc.aio.insecure_channel("127.0.0.1:50051") as ch:
14
+ reply = await demo_pb2_grpc.DemoStub(ch).SayHello(demo_pb2.HelloRequest(name="veil"))
15
+
16
+ The channel runs on veil's own HTTP/2 client (Rust, on a client thread of its own): grpcio is not needed (only
17
+ protobuf for the messages). Errors raise :class:`RpcError` (a ``grpc.RpcError`` when grpcio is installed) with
18
+ ``code()``, ``details()`` and ``trailing_metadata()``. Servers: :class:`veil.grpc.Server` takes the generated
19
+ ``add_XxxServicer_to_server`` functions (re-exported here).
20
+ """
21
+
22
+ # F-268: grpcio の Channel と同じ口(生成スタブがそのまま使える)。通信は Rust(veil::api::grpc::Channel)。
23
+
24
+ import asyncio
25
+ import os
26
+ import tempfile
27
+
28
+ from veil.grpc import Server, ServicerContext, StatusCode, add_health # noqa: F401 再公開
29
+
30
+ __all__ = [
31
+ "Channel",
32
+ "insecure_channel",
33
+ "secure_channel",
34
+ "ssl_channel_credentials",
35
+ "RpcError",
36
+ "StatusCode",
37
+ "Server",
38
+ "ServicerContext",
39
+ "add_health",
40
+ "aio",
41
+ ]
42
+
43
+ try: # grpcio があれば、その例外・状態コードの型に合わせる(except grpc.RpcError で捕まえられる)
44
+ import grpc as _grpcio
45
+
46
+ _RpcBase = _grpcio.RpcError
47
+ _CODES = {c.value[0]: c for c in _grpcio.StatusCode}
48
+ except ImportError: # pragma: no cover
49
+ _grpcio = None
50
+ _RpcBase = Exception
51
+ _CODES = {}
52
+
53
+
54
+ def _code(n):
55
+ return _CODES.get(n) or StatusCode(n)
56
+
57
+
58
+ class RpcError(_RpcBase):
59
+ """A call ended with a status other than OK."""
60
+
61
+ def __init__(self, code, details, initial=(), trailing=()):
62
+ super().__init__("%s: %s" % (_code(code).name, details))
63
+ self._code = code
64
+ self._details = details
65
+ self._initial = tuple(initial)
66
+ self._trailing = tuple(trailing)
67
+
68
+ def code(self):
69
+ """The status code."""
70
+ return _code(self._code)
71
+
72
+ def details(self):
73
+ """The status message."""
74
+ return self._details
75
+
76
+ def initial_metadata(self):
77
+ """The response headers."""
78
+ return self._initial
79
+
80
+ def trailing_metadata(self):
81
+ """The trailers."""
82
+ return self._trailing
83
+
84
+ def debug_error_string(self):
85
+ """Same as ``str(error)``."""
86
+ return str(self)
87
+
88
+
89
+ class _CallInfo:
90
+ """What ``with_call`` returns next to the response (``grpc.Call``)."""
91
+
92
+ def __init__(self, code, details, initial, trailing):
93
+ self._code, self._details, self._initial, self._trailing = code, details, tuple(initial), tuple(trailing)
94
+
95
+ def code(self):
96
+ """The status code."""
97
+ return _code(self._code)
98
+
99
+ def details(self):
100
+ """The status message."""
101
+ return self._details
102
+
103
+ def initial_metadata(self):
104
+ """The response headers."""
105
+ return self._initial
106
+
107
+ def trailing_metadata(self):
108
+ """The trailers."""
109
+ return self._trailing
110
+
111
+
112
+ class ChannelCredentials:
113
+ """TLS settings for :func:`secure_channel` (from :func:`ssl_channel_credentials`)."""
114
+
115
+ def __init__(self, ca=None, cert=None, key=None):
116
+ self.ca = ca
117
+ self.cert = cert
118
+ self.key = key
119
+
120
+
121
+ def _to_file(pem, suffix):
122
+ if pem is None:
123
+ return None
124
+ if isinstance(pem, str) and not pem.lstrip().startswith("-----"):
125
+ return pem # 既にパス
126
+ data = pem.encode() if isinstance(pem, str) else pem
127
+ fd, path = tempfile.mkstemp(suffix=suffix, prefix="veil-grpc-")
128
+ with os.fdopen(fd, "wb") as f:
129
+ f.write(data)
130
+ return path
131
+
132
+
133
+ def ssl_channel_credentials(root_certificates=None, private_key=None, certificate_chain=None):
134
+ """TLS credentials (PEM ``bytes`` or file paths): trusted CAs (default: the system's) and a client certificate."""
135
+ return ChannelCredentials(_to_file(root_certificates, ".ca.pem"), _to_file(certificate_chain, ".crt.pem"),
136
+ _to_file(private_key, ".key.pem"))
137
+
138
+
139
+ def _url(target, secure):
140
+ if "://" in target:
141
+ return target
142
+ return ("https://" if secure else "http://") + target
143
+
144
+
145
+ def _metadata(md):
146
+ out = []
147
+ for k, v in md or ():
148
+ k = str(k).lower()
149
+ out.append((k, v if isinstance(v, (bytes, bytearray)) else str(v)))
150
+ return out
151
+
152
+
153
+ class Channel:
154
+ """A channel (``grpc.Channel``); prefer :func:`insecure_channel` / :func:`secure_channel`."""
155
+
156
+ def __init__(self, target, credentials=None, options=None, compression=None):
157
+ """Connect lazily to ``target`` (``host:port`` or a URL)."""
158
+ from veil import _veil
159
+
160
+ self.target = target
161
+ secure = credentials is not None
162
+ ca = [credentials.ca] if secure and credentials.ca else None
163
+ cert = (credentials.cert, credentials.key) if secure and credentials.cert else None
164
+ gzip = compression is not None and getattr(compression, "name", str(compression)).lower().endswith("gzip")
165
+ max_msg = None
166
+ for k, v in options or ():
167
+ if k in ("grpc.max_receive_message_length", "grpc.max_message_length"):
168
+ max_msg = int(v)
169
+ self._n = _veil.GrpcChannel(_url(target, secure), ca, cert, gzip, max_msg)
170
+
171
+ # ---- grpcio の Channel の口 ----
172
+
173
+ def unary_unary(self, method, request_serializer=None, response_deserializer=None, **_):
174
+ """A callable for a unary method."""
175
+ return _Unary(self, method, request_serializer, response_deserializer)
176
+
177
+ def unary_stream(self, method, request_serializer=None, response_deserializer=None, **_):
178
+ """A callable for a server-streaming method."""
179
+ return _UnaryStream(self, method, request_serializer, response_deserializer)
180
+
181
+ def stream_unary(self, method, request_serializer=None, response_deserializer=None, **_):
182
+ """A callable for a client-streaming method."""
183
+ return _StreamUnary(self, method, request_serializer, response_deserializer)
184
+
185
+ def stream_stream(self, method, request_serializer=None, response_deserializer=None, **_):
186
+ """A callable for a bidirectional method."""
187
+ return _StreamStream(self, method, request_serializer, response_deserializer)
188
+
189
+ def subscribe(self, callback, try_to_connect=False):
190
+ """Accepted for compatibility (connectivity is not reported)."""
191
+
192
+ def unsubscribe(self, callback):
193
+ """Accepted for compatibility."""
194
+
195
+ def close(self):
196
+ """Close the channel (connections close when idle)."""
197
+
198
+ def __enter__(self):
199
+ return self
200
+
201
+ def __exit__(self, *exc):
202
+ self.close()
203
+
204
+ # ---- 呼び出し ----
205
+
206
+ def _start(self, method, metadata, timeout):
207
+ return self._n.call(method, _metadata(metadata), timeout)
208
+
209
+
210
+ def _ser(f, m):
211
+ return m if f is None else f(m)
212
+
213
+
214
+ def _de(f, b):
215
+ return b if f is None else f(b)
216
+
217
+
218
+ def _finish(call):
219
+ code, details, initial, trailing = call.status()
220
+ if code != 0:
221
+ raise RpcError(code, details, initial, trailing)
222
+ return _CallInfo(code, details, initial, trailing)
223
+
224
+
225
+ class _Callable:
226
+ def __init__(self, ch, method, ser, de):
227
+ self._ch, self._method, self._ser, self._de = ch, method, ser, de
228
+
229
+
230
+ class _Unary(_Callable):
231
+ def with_call(self, request, timeout=None, metadata=None, **_):
232
+ """Call and return ``(response, call)``."""
233
+ call = self._ch._start(self._method, metadata, timeout)
234
+ call.send(_ser(self._ser, request))
235
+ call.done_sending()
236
+ data = call.recv()
237
+ info = _finish(call)
238
+ if data is None:
239
+ raise RpcError(13, "the server sent no response message")
240
+ return _de(self._de, data), info
241
+
242
+ def __call__(self, request, timeout=None, metadata=None, **kw):
243
+ """Call and return the response."""
244
+ return self.with_call(request, timeout, metadata, **kw)[0]
245
+
246
+ def future(self, request, timeout=None, metadata=None, **kw):
247
+ """Call on a thread and return a ``concurrent.futures.Future``."""
248
+ import concurrent.futures
249
+
250
+ from veil._aio import _executor
251
+
252
+ f = _executor().submit(self, request, timeout, metadata)
253
+ assert isinstance(f, concurrent.futures.Future)
254
+ return f
255
+
256
+
257
+ class _ResponseIterator:
258
+ def __init__(self, call, de):
259
+ self._call, self._de, self._info = call, de, None
260
+
261
+ def __iter__(self):
262
+ return self
263
+
264
+ def __next__(self):
265
+ data = self._call.recv()
266
+ if data is None:
267
+ self._info = _finish(self._call)
268
+ raise StopIteration
269
+ return _de(self._de, data)
270
+
271
+ def cancel(self):
272
+ """Cancel the call."""
273
+ self._call.cancel()
274
+
275
+ def code(self):
276
+ """The status code (after the last message)."""
277
+ return self._info.code() if self._info else None
278
+
279
+ def details(self):
280
+ """The status message."""
281
+ return self._info.details() if self._info else None
282
+
283
+ def trailing_metadata(self):
284
+ """The trailers (after the last message)."""
285
+ return self._info.trailing_metadata() if self._info else None
286
+
287
+ def initial_metadata(self):
288
+ """The response headers."""
289
+ return tuple(self._call.initial_metadata())
290
+
291
+
292
+ class _UnaryStream(_Callable):
293
+ def __call__(self, request, timeout=None, metadata=None, **_):
294
+ """Call and return an iterator of responses."""
295
+ call = self._ch._start(self._method, metadata, timeout)
296
+ call.send(_ser(self._ser, request))
297
+ call.done_sending()
298
+ return _ResponseIterator(call, self._de)
299
+
300
+
301
+ def _pump(call, ser, it):
302
+ try:
303
+ for m in it:
304
+ if not call.send(_ser(ser, m)):
305
+ break
306
+ finally:
307
+ call.done_sending()
308
+
309
+
310
+ class _StreamUnary(_Callable):
311
+ def with_call(self, request_iterator, timeout=None, metadata=None, **_):
312
+ """Send the requests and return ``(response, call)``."""
313
+ call = self._ch._start(self._method, metadata, timeout)
314
+ _pump(call, self._ser, request_iterator)
315
+ data = call.recv()
316
+ info = _finish(call)
317
+ if data is None:
318
+ raise RpcError(13, "the server sent no response message")
319
+ return _de(self._de, data), info
320
+
321
+ def __call__(self, request_iterator, timeout=None, metadata=None, **kw):
322
+ """Send the requests and return the response."""
323
+ return self.with_call(request_iterator, timeout, metadata, **kw)[0]
324
+
325
+
326
+ class _StreamStream(_Callable):
327
+ def __call__(self, request_iterator, timeout=None, metadata=None, **_):
328
+ """Send the requests (on a thread) and return an iterator of responses."""
329
+ import threading
330
+
331
+ call = self._ch._start(self._method, metadata, timeout)
332
+ threading.Thread(target=_pump, args=(call, self._ser, request_iterator), daemon=True).start()
333
+ return _ResponseIterator(call, self._de)
334
+
335
+
336
+ def insecure_channel(target, options=None, compression=None):
337
+ """A channel over plain HTTP/2 (h2c)."""
338
+ return Channel(target, None, options, compression)
339
+
340
+
341
+ def secure_channel(target, credentials=None, options=None, compression=None):
342
+ """A channel over TLS (``credentials`` from :func:`ssl_channel_credentials`; default: the system's CAs)."""
343
+ return Channel(target, credentials or ChannelCredentials(), options, compression)
344
+
345
+
346
+ # ===================== asyncio(grpc.aio と同じ口) =====================
347
+
348
+
349
+ def _future_of(register):
350
+ loop = asyncio.get_running_loop()
351
+ fut = loop.create_future()
352
+
353
+ def done(v):
354
+ loop.call_soon_threadsafe(lambda: fut.done() or fut.set_result(v))
355
+
356
+ register(done)
357
+ return fut
358
+
359
+
360
+ async def _arecv(call):
361
+ return await _future_of(call.recv_cb)
362
+
363
+
364
+ async def _astatus(call):
365
+ code, details, initial, trailing = await _future_of(call.status_cb)
366
+ if code != 0:
367
+ raise RpcError(code, details, initial, trailing)
368
+ return _CallInfo(code, details, initial, trailing)
369
+
370
+
371
+ class _AioUnaryCall:
372
+ def __init__(self, coro):
373
+ self._coro = coro
374
+
375
+ def __await__(self):
376
+ return self._coro.__await__()
377
+
378
+
379
+ class _AioStreamCall:
380
+ def __init__(self, call, de, pump=None, ser=None):
381
+ self._call, self._de, self._pump, self._ser, self._info = call, de, pump, ser, None
382
+
383
+ def __aiter__(self):
384
+ return self
385
+
386
+ async def __anext__(self):
387
+ m = await self.read()
388
+ if m is None:
389
+ raise StopAsyncIteration
390
+ return m
391
+
392
+ async def read(self):
393
+ """The next response, or ``None`` at the end."""
394
+ data = await _arecv(self._call)
395
+ if data is None:
396
+ self._info = await _astatus(self._call)
397
+ return None
398
+ return _de(self._de, data)
399
+
400
+ async def write(self, request):
401
+ """Send one request (bidirectional calls created without a request iterator)."""
402
+ self._call.send(_ser(self._ser, request))
403
+
404
+ async def done_writing(self):
405
+ """Finish sending."""
406
+ self._call.done_sending()
407
+
408
+ def cancel(self):
409
+ """Cancel the call."""
410
+ self._call.cancel()
411
+ if self._pump is not None:
412
+ self._pump.cancel()
413
+
414
+ async def code(self):
415
+ """The status code."""
416
+ return self._info.code() if self._info else None
417
+
418
+ async def trailing_metadata(self):
419
+ """The trailers."""
420
+ return self._info.trailing_metadata() if self._info else None
421
+
422
+
423
+ async def _apump(call, ser, it):
424
+ try:
425
+ if hasattr(it, "__aiter__"):
426
+ async for m in it:
427
+ if not call.send(_ser(ser, m)):
428
+ break
429
+ else:
430
+ for m in it:
431
+ if not call.send(_ser(ser, m)):
432
+ break
433
+ finally:
434
+ call.done_sending()
435
+
436
+
437
+ class _AioChannel(Channel):
438
+ """An asyncio channel (``grpc.aio.Channel``)."""
439
+
440
+ def unary_unary(self, method, request_serializer=None, response_deserializer=None, **_):
441
+ """An awaitable callable for a unary method."""
442
+ ch = self
443
+
444
+ def call(request, timeout=None, metadata=None, **kw):
445
+ async def run():
446
+ c = ch._start(method, metadata, timeout)
447
+ c.send(_ser(request_serializer, request))
448
+ c.done_sending()
449
+ data = await _arecv(c)
450
+ await _astatus(c)
451
+ if data is None:
452
+ raise RpcError(13, "the server sent no response message")
453
+ return _de(response_deserializer, data)
454
+
455
+ return _AioUnaryCall(run())
456
+
457
+ return call
458
+
459
+ def unary_stream(self, method, request_serializer=None, response_deserializer=None, **_):
460
+ """A callable returning an async iterator of responses."""
461
+ ch = self
462
+
463
+ def call(request, timeout=None, metadata=None, **kw):
464
+ c = ch._start(method, metadata, timeout)
465
+ c.send(_ser(request_serializer, request))
466
+ c.done_sending()
467
+ return _AioStreamCall(c, response_deserializer)
468
+
469
+ return call
470
+
471
+ def stream_unary(self, method, request_serializer=None, response_deserializer=None, **_):
472
+ """An awaitable callable taking an (async) iterator of requests."""
473
+ ch = self
474
+
475
+ def call(request_iterator, timeout=None, metadata=None, **kw):
476
+ async def run():
477
+ c = ch._start(method, metadata, timeout)
478
+ await _apump(c, request_serializer, request_iterator)
479
+ data = await _arecv(c)
480
+ await _astatus(c)
481
+ if data is None:
482
+ raise RpcError(13, "the server sent no response message")
483
+ return _de(response_deserializer, data)
484
+
485
+ return _AioUnaryCall(run())
486
+
487
+ return call
488
+
489
+ def stream_stream(self, method, request_serializer=None, response_deserializer=None, **_):
490
+ """A callable taking an (async) iterator of requests and returning an async iterator of responses."""
491
+ ch = self
492
+
493
+ def call(request_iterator=None, timeout=None, metadata=None, **kw):
494
+ c = ch._start(method, metadata, timeout)
495
+ pump = None
496
+ if request_iterator is not None:
497
+ pump = asyncio.get_running_loop().create_task(_apump(c, request_serializer, request_iterator))
498
+ return _AioStreamCall(c, response_deserializer, pump, request_serializer)
499
+
500
+ return call
501
+
502
+ async def __aenter__(self):
503
+ return self
504
+
505
+ async def __aexit__(self, *exc):
506
+ self.close()
507
+
508
+ async def channel_ready(self):
509
+ """Accepted for compatibility (connections are made on the first call)."""
510
+
511
+
512
+ class aio: # noqa: N801 grpc.aio と同じ名前
513
+ """``veil_kit.grpc.aio.insecure_channel`` / ``secure_channel`` (``grpc.aio`` style)."""
514
+
515
+ Channel = _AioChannel
516
+
517
+ @staticmethod
518
+ def insecure_channel(target, options=None, compression=None):
519
+ """An asyncio channel over plain HTTP/2 (h2c)."""
520
+ return _AioChannel(target, None, options, compression)
521
+
522
+ @staticmethod
523
+ def secure_channel(target, credentials=None, options=None, compression=None):
524
+ """An asyncio channel over TLS."""
525
+ return _AioChannel(target, credentials or ChannelCredentials(), options, compression)
veil_kit/py.typed ADDED
File without changes
veil_kit/redirects.py ADDED
@@ -0,0 +1,33 @@
1
+ """Redirects that only go to this site or to listed hosts (protection against open redirects)."""
2
+
3
+ # F-268
4
+
5
+ import urllib.parse
6
+
7
+ import veil
8
+
9
+ __all__ = ["is_safe_url", "safe_redirect"]
10
+
11
+
12
+ def is_safe_url(url, allowed_hosts=(), require_https=False):
13
+ """True when ``url`` is a path on this site (``/x``, not ``//host`` or ``/\\host``) or an ``http(s)`` URL whose host
14
+ is in ``allowed_hosts`` (URLs with user information are refused)."""
15
+ if not url or any(c in url for c in "\r\n\t\x00") or "\\" in url:
16
+ return False
17
+ if url.startswith("/"):
18
+ return not url.startswith("//")
19
+ try:
20
+ u = urllib.parse.urlsplit(url)
21
+ except ValueError:
22
+ return False
23
+ if u.scheme not in (("https",) if require_https else ("http", "https")):
24
+ return False
25
+ if u.username is not None or u.password is not None:
26
+ return False
27
+ host = (u.hostname or "").lower()
28
+ return bool(host) and host in {h.lower() for h in allowed_hosts}
29
+
30
+
31
+ def safe_redirect(url, allowed_hosts=(), fallback="/", status_code=303):
32
+ """A :class:`veil.RedirectResponse` to ``url`` when :func:`is_safe_url`, otherwise to ``fallback``."""
33
+ return veil.RedirectResponse(url if is_safe_url(url, allowed_hosts) else fallback, status_code)