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_api_kit-0.10.0.dist-info/METADATA +103 -0
- veil_api_kit-0.10.0.dist-info/RECORD +17 -0
- veil_api_kit-0.10.0.dist-info/WHEEL +5 -0
- veil_api_kit-0.10.0.dist-info/top_level.txt +1 -0
- veil_kit/__init__.py +21 -0
- veil_kit/cookies.py +145 -0
- veil_kit/cors.py +36 -0
- veil_kit/di.py +362 -0
- veil_kit/graphql.py +215 -0
- veil_kit/grpc.py +525 -0
- veil_kit/py.typed +0 -0
- veil_kit/redirects.py +33 -0
- veil_kit/security.py +377 -0
- veil_kit/sessions.py +243 -0
- veil_kit/staticfiles.py +36 -0
- veil_kit/tasks.py +199 -0
- veil_kit/testing.py +24 -0
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)
|