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/di.py
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""A dependency injection container on top of ``veil.Depends``.
|
|
2
|
+
|
|
3
|
+
.. code-block:: python
|
|
4
|
+
|
|
5
|
+
from veil_kit.di import Container, Inject
|
|
6
|
+
|
|
7
|
+
container = Container()
|
|
8
|
+
container.singleton(Settings, lambda: Settings.from_env())
|
|
9
|
+
container.scoped(Session, make_session) # once per request; make_session may yield
|
|
10
|
+
container.transient(Clock, Clock)
|
|
11
|
+
|
|
12
|
+
@container.provider(Repo, scope="scoped")
|
|
13
|
+
def repo(session: Session, settings: Settings): # parameters of registered types are injected
|
|
14
|
+
return Repo(session, settings)
|
|
15
|
+
|
|
16
|
+
@app.get("/items")
|
|
17
|
+
def items(repo: Repo = Inject(Repo)):
|
|
18
|
+
return repo.list()
|
|
19
|
+
|
|
20
|
+
with container.override(Repo, FakeRepo()): # tests
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
A provider is resolved through ``veil.Depends``: its parameters may also be request parameters (``Query``,
|
|
24
|
+
``Header``, ``request: veil.Request``, other ``Depends``) and generators clean up after the request. ``singleton``
|
|
25
|
+
values are created once per process (thread-safe), ``scoped`` once per request, ``transient`` on every use.
|
|
26
|
+
Registrations may come after the routes that use them, until the server starts.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
# F-268: 解決は veil の Depends の木(登録時に組み立て、要求ごとは決まった順に呼ぶ)。差し替えは提供者の入れ替え。
|
|
30
|
+
|
|
31
|
+
import contextlib
|
|
32
|
+
import inspect
|
|
33
|
+
import threading
|
|
34
|
+
|
|
35
|
+
import veil
|
|
36
|
+
|
|
37
|
+
__all__ = ["Container", "Inject", "SINGLETON", "SCOPED", "TRANSIENT"]
|
|
38
|
+
|
|
39
|
+
SINGLETON, SCOPED, TRANSIENT = "singleton", "scoped", "transient"
|
|
40
|
+
|
|
41
|
+
_current = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class _Registration:
|
|
45
|
+
__slots__ = ("key", "factory", "scope", "value", "lock", "has_value", "resolver")
|
|
46
|
+
|
|
47
|
+
def __init__(self, key, factory, scope):
|
|
48
|
+
self.key = key
|
|
49
|
+
self.factory = factory
|
|
50
|
+
self.scope = scope
|
|
51
|
+
self.value = None
|
|
52
|
+
self.has_value = False
|
|
53
|
+
self.lock = threading.Lock()
|
|
54
|
+
self.resolver = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Container:
|
|
58
|
+
"""Providers by type (or any hashable key)."""
|
|
59
|
+
|
|
60
|
+
def __init__(self):
|
|
61
|
+
"""Create an empty container."""
|
|
62
|
+
self._regs = {}
|
|
63
|
+
self._overrides = {}
|
|
64
|
+
self._stubs = {}
|
|
65
|
+
global _current
|
|
66
|
+
_current = self
|
|
67
|
+
|
|
68
|
+
# ---- 登録 ----
|
|
69
|
+
|
|
70
|
+
def register(self, key, factory=None, scope=SCOPED):
|
|
71
|
+
"""Register ``factory`` (a callable, or a value for ``singleton``) for ``key``; ``factory`` defaults to ``key``."""
|
|
72
|
+
if scope not in (SINGLETON, SCOPED, TRANSIENT):
|
|
73
|
+
raise ValueError("scope must be singleton, scoped or transient")
|
|
74
|
+
if factory is None:
|
|
75
|
+
factory = key
|
|
76
|
+
if not callable(factory):
|
|
77
|
+
value = factory
|
|
78
|
+
reg = _Registration(key, None, SINGLETON)
|
|
79
|
+
reg.value, reg.has_value = value, True
|
|
80
|
+
else:
|
|
81
|
+
reg = _Registration(key, factory, scope)
|
|
82
|
+
self._regs[key] = reg
|
|
83
|
+
self._stubs.pop(key, None)
|
|
84
|
+
return self
|
|
85
|
+
|
|
86
|
+
def singleton(self, key, factory=None):
|
|
87
|
+
"""Register a provider created once per process (or a ready value)."""
|
|
88
|
+
return self.register(key, factory, SINGLETON)
|
|
89
|
+
|
|
90
|
+
def scoped(self, key, factory=None):
|
|
91
|
+
"""Register a provider created once per request."""
|
|
92
|
+
return self.register(key, factory, SCOPED)
|
|
93
|
+
|
|
94
|
+
def transient(self, key, factory=None):
|
|
95
|
+
"""Register a provider created on every use."""
|
|
96
|
+
return self.register(key, factory, TRANSIENT)
|
|
97
|
+
|
|
98
|
+
def value(self, key, value):
|
|
99
|
+
"""Register an existing value."""
|
|
100
|
+
reg = _Registration(key, None, SINGLETON)
|
|
101
|
+
reg.value, reg.has_value = value, True
|
|
102
|
+
self._regs[key] = reg
|
|
103
|
+
self._stubs.pop(key, None)
|
|
104
|
+
return self
|
|
105
|
+
|
|
106
|
+
def provider(self, key, scope=SCOPED):
|
|
107
|
+
"""Decorator form of :meth:`register`."""
|
|
108
|
+
|
|
109
|
+
def deco(f):
|
|
110
|
+
self.register(key, f, scope)
|
|
111
|
+
return f
|
|
112
|
+
|
|
113
|
+
return deco
|
|
114
|
+
|
|
115
|
+
# ---- 解決 ----
|
|
116
|
+
|
|
117
|
+
def depends(self, key):
|
|
118
|
+
"""The ``veil.Depends`` marker for ``key`` (``Inject(key)`` uses the current container)."""
|
|
119
|
+
reg = self._regs.get(key)
|
|
120
|
+
scope = reg.scope if reg is not None else SCOPED
|
|
121
|
+
return veil.Depends(self._resolver(key), use_cache=scope != TRANSIENT)
|
|
122
|
+
|
|
123
|
+
def _resolver(self, key):
|
|
124
|
+
stub = self._stubs.get(key)
|
|
125
|
+
if stub is not None:
|
|
126
|
+
return stub
|
|
127
|
+
reg = self._regs.get(key)
|
|
128
|
+
if reg is None:
|
|
129
|
+
# まだ登録されていない: 呼ばれたとき(起動後)に解決する(ジェネレータの提供者は要求の後に閉じる)
|
|
130
|
+
container = self
|
|
131
|
+
|
|
132
|
+
def late(request: veil.Request):
|
|
133
|
+
o = container._overrides.get(key, _MISSING)
|
|
134
|
+
if o is not _MISSING:
|
|
135
|
+
yield o
|
|
136
|
+
return
|
|
137
|
+
reg = container._regs.get(key)
|
|
138
|
+
if reg is None:
|
|
139
|
+
raise LookupError("no provider registered for %r" % (key,))
|
|
140
|
+
if reg.has_value or reg.scope == SINGLETON:
|
|
141
|
+
yield container.get(key)
|
|
142
|
+
return
|
|
143
|
+
v = reg.factory(**container._registered_kwargs(reg.factory))
|
|
144
|
+
if inspect.isgenerator(v):
|
|
145
|
+
yield from _once(v)
|
|
146
|
+
return
|
|
147
|
+
if inspect.isawaitable(v):
|
|
148
|
+
from veil import _run_coro
|
|
149
|
+
|
|
150
|
+
v = _run_coro(v)
|
|
151
|
+
yield v
|
|
152
|
+
|
|
153
|
+
late._veil_inline = True
|
|
154
|
+
stub = late
|
|
155
|
+
else:
|
|
156
|
+
stub = self._make_resolver(reg)
|
|
157
|
+
self._stubs[key] = stub
|
|
158
|
+
return stub
|
|
159
|
+
|
|
160
|
+
def _make_resolver(self, reg):
|
|
161
|
+
container = self
|
|
162
|
+
key = reg.key
|
|
163
|
+
if reg.has_value:
|
|
164
|
+
def value_resolver():
|
|
165
|
+
o = container._overrides.get(key, _MISSING)
|
|
166
|
+
return reg.value if o is _MISSING else o
|
|
167
|
+
|
|
168
|
+
value_resolver._veil_inline = True
|
|
169
|
+
return value_resolver
|
|
170
|
+
factory = reg.factory
|
|
171
|
+
sig = _injected_signature(self, factory)
|
|
172
|
+
regs = self._regs
|
|
173
|
+
kind = "gen" if inspect.isgeneratorfunction(factory) else (
|
|
174
|
+
"agen" if inspect.isasyncgenfunction(factory) else (
|
|
175
|
+
"async" if inspect.iscoroutinefunction(factory) else "sync"))
|
|
176
|
+
|
|
177
|
+
if reg.scope == SINGLETON:
|
|
178
|
+
def resolver(**kw):
|
|
179
|
+
o = container._overrides.get(key, _MISSING)
|
|
180
|
+
if o is not _MISSING:
|
|
181
|
+
return o
|
|
182
|
+
cur = regs[key]
|
|
183
|
+
if not cur.has_value:
|
|
184
|
+
with cur.lock:
|
|
185
|
+
if not cur.has_value:
|
|
186
|
+
v = cur.factory(**kw)
|
|
187
|
+
if inspect.isawaitable(v):
|
|
188
|
+
from veil import _run_coro
|
|
189
|
+
|
|
190
|
+
v = _run_coro(v)
|
|
191
|
+
cur.value, cur.has_value = v, True
|
|
192
|
+
return cur.value
|
|
193
|
+
elif kind == "gen":
|
|
194
|
+
def resolver(**kw):
|
|
195
|
+
o = container._overrides.get(key, _MISSING)
|
|
196
|
+
if o is not _MISSING:
|
|
197
|
+
yield o
|
|
198
|
+
return
|
|
199
|
+
yield from regs[key].factory(**kw)
|
|
200
|
+
elif kind == "agen":
|
|
201
|
+
async def resolver(**kw):
|
|
202
|
+
o = container._overrides.get(key, _MISSING)
|
|
203
|
+
if o is not _MISSING:
|
|
204
|
+
yield o
|
|
205
|
+
return
|
|
206
|
+
async for v in regs[key].factory(**kw):
|
|
207
|
+
yield v
|
|
208
|
+
elif kind == "async":
|
|
209
|
+
async def resolver(**kw):
|
|
210
|
+
o = container._overrides.get(key, _MISSING)
|
|
211
|
+
if o is not _MISSING:
|
|
212
|
+
return o
|
|
213
|
+
return await regs[key].factory(**kw)
|
|
214
|
+
else:
|
|
215
|
+
def resolver(**kw):
|
|
216
|
+
o = container._overrides.get(key, _MISSING)
|
|
217
|
+
if o is not _MISSING:
|
|
218
|
+
return o
|
|
219
|
+
return regs[key].factory(**kw)
|
|
220
|
+
|
|
221
|
+
resolver.__signature__ = sig
|
|
222
|
+
resolver.__annotations__ = {p.name: p.annotation for p in sig.parameters.values()
|
|
223
|
+
if p.annotation is not inspect.Parameter.empty}
|
|
224
|
+
resolver.__name__ = "provide_%s" % getattr(key, "__name__", "value")
|
|
225
|
+
resolver.__qualname__ = resolver.__name__
|
|
226
|
+
resolver.__module__ = getattr(factory, "__module__", __name__)
|
|
227
|
+
if getattr(factory, "_veil_inline", False):
|
|
228
|
+
resolver._veil_inline = True
|
|
229
|
+
return resolver
|
|
230
|
+
|
|
231
|
+
def get(self, key):
|
|
232
|
+
"""Resolve ``key`` outside a request (singletons and providers without request parameters; a generator
|
|
233
|
+
provider gives its first value and its code after ``yield`` is not run)."""
|
|
234
|
+
o = self._overrides.get(key, _MISSING)
|
|
235
|
+
if o is not _MISSING:
|
|
236
|
+
return o
|
|
237
|
+
reg = self._regs.get(key)
|
|
238
|
+
if reg is None:
|
|
239
|
+
raise LookupError("no provider registered for %r" % (key,))
|
|
240
|
+
if reg.has_value:
|
|
241
|
+
return reg.value
|
|
242
|
+
kwargs = self._registered_kwargs(reg.factory)
|
|
243
|
+
if reg.scope == SINGLETON:
|
|
244
|
+
with reg.lock:
|
|
245
|
+
if not reg.has_value:
|
|
246
|
+
reg.value, reg.has_value = reg.factory(**kwargs), True
|
|
247
|
+
return reg.value
|
|
248
|
+
v = reg.factory(**kwargs)
|
|
249
|
+
if inspect.isgenerator(v):
|
|
250
|
+
# 要求の外: 最初の値を返す(yield の後は実行しない)
|
|
251
|
+
return next(v)
|
|
252
|
+
if inspect.isawaitable(v):
|
|
253
|
+
from veil import _run_coro
|
|
254
|
+
|
|
255
|
+
v = _run_coro(v)
|
|
256
|
+
return v
|
|
257
|
+
|
|
258
|
+
def _registered_kwargs(self, factory):
|
|
259
|
+
# 登録された型の引数だけを解決する(要求の引数は無い前提)
|
|
260
|
+
kwargs = {}
|
|
261
|
+
for p in _injected_signature(self, factory).parameters.values():
|
|
262
|
+
d = p.default
|
|
263
|
+
if isinstance(d, veil.Depends) and d.dependency in self._stubs.values():
|
|
264
|
+
kwargs[p.name] = self.get(_key_of(self, d.dependency))
|
|
265
|
+
return kwargs
|
|
266
|
+
|
|
267
|
+
# ---- 差し替え ----
|
|
268
|
+
|
|
269
|
+
@contextlib.contextmanager
|
|
270
|
+
def override(self, key, value):
|
|
271
|
+
"""Use ``value`` for ``key`` inside the ``with`` block (tests); requests in progress may see either."""
|
|
272
|
+
old = self._overrides.get(key, _MISSING)
|
|
273
|
+
self._overrides[key] = value
|
|
274
|
+
try:
|
|
275
|
+
yield value
|
|
276
|
+
finally:
|
|
277
|
+
if old is _MISSING:
|
|
278
|
+
self._overrides.pop(key, None)
|
|
279
|
+
else:
|
|
280
|
+
self._overrides[key] = old
|
|
281
|
+
|
|
282
|
+
def replace(self, key, factory, app=None):
|
|
283
|
+
"""Replace the provider of ``key`` while the server runs (new requests use it).
|
|
284
|
+
|
|
285
|
+
A provider with the same parameters is swapped in place. One with other parameters needs ``app``: the routes
|
|
286
|
+
are rebuilt through ``app.dependency_overrides``.
|
|
287
|
+
"""
|
|
288
|
+
reg = self._regs.get(key)
|
|
289
|
+
if reg is None:
|
|
290
|
+
self.register(key, factory)
|
|
291
|
+
return
|
|
292
|
+
old = self._stubs.get(key)
|
|
293
|
+
if app is not None and old is not None:
|
|
294
|
+
new = self._make_resolver(_Registration(key, factory, reg.scope))
|
|
295
|
+
app.dependency_overrides[old] = new
|
|
296
|
+
return
|
|
297
|
+
if old is not None and _injected_signature(self, factory) != _injected_signature(self, reg.factory):
|
|
298
|
+
raise ValueError("the new provider has other parameters; pass app= to rebuild the routes")
|
|
299
|
+
reg.factory = factory
|
|
300
|
+
reg.has_value = False
|
|
301
|
+
reg.value = None
|
|
302
|
+
|
|
303
|
+
def reset(self, key):
|
|
304
|
+
"""Forget the cached singleton value of ``key``."""
|
|
305
|
+
reg = self._regs.get(key)
|
|
306
|
+
if reg is not None and reg.factory is not None:
|
|
307
|
+
reg.has_value = False
|
|
308
|
+
reg.value = None
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
_MISSING = object()
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _once(gen):
|
|
315
|
+
# ジェネレータの提供者の最初の値を渡し、要求の後に残りを実行する
|
|
316
|
+
v = next(gen)
|
|
317
|
+
try:
|
|
318
|
+
yield v
|
|
319
|
+
finally:
|
|
320
|
+
try:
|
|
321
|
+
next(gen)
|
|
322
|
+
except StopIteration:
|
|
323
|
+
pass
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _key_of(container, resolver):
|
|
327
|
+
for k, v in container._stubs.items():
|
|
328
|
+
if v is resolver:
|
|
329
|
+
return k
|
|
330
|
+
raise LookupError(resolver)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _injected_signature(container, factory):
|
|
334
|
+
"""The signature of ``factory`` with the parameters of registered types turned into ``Depends``."""
|
|
335
|
+
target = factory.__init__ if inspect.isclass(factory) else factory
|
|
336
|
+
try:
|
|
337
|
+
sig = inspect.signature(factory)
|
|
338
|
+
except (TypeError, ValueError):
|
|
339
|
+
return inspect.Signature()
|
|
340
|
+
try:
|
|
341
|
+
import typing
|
|
342
|
+
|
|
343
|
+
hints = typing.get_type_hints(target)
|
|
344
|
+
except Exception: # noqa: BLE001
|
|
345
|
+
hints = {}
|
|
346
|
+
params = []
|
|
347
|
+
for p in sig.parameters.values():
|
|
348
|
+
ann = hints.get(p.name, p.annotation)
|
|
349
|
+
if p.default is inspect.Parameter.empty and ann in container._regs:
|
|
350
|
+
p = p.replace(default=container.depends(ann), annotation=ann)
|
|
351
|
+
elif ann is not inspect.Parameter.empty:
|
|
352
|
+
p = p.replace(annotation=ann)
|
|
353
|
+
params.append(p)
|
|
354
|
+
return sig.replace(parameters=params, return_annotation=inspect.Signature.empty)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def Inject(key, container=None): # noqa: N802
|
|
358
|
+
"""``param: T = Inject(T)``: the ``veil.Depends`` marker for ``key`` in ``container`` (the last created by default)."""
|
|
359
|
+
c = container or _current
|
|
360
|
+
if c is None:
|
|
361
|
+
raise RuntimeError("create a Container before using Inject()")
|
|
362
|
+
return c.depends(key)
|
veil_kit/graphql.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""GraphQL with strawberry (``pip install "veil-api-kit[graphql]"``).
|
|
2
|
+
|
|
3
|
+
.. code-block:: python
|
|
4
|
+
|
|
5
|
+
import strawberry
|
|
6
|
+
from veil_kit.graphql import mount
|
|
7
|
+
|
|
8
|
+
schema = strawberry.Schema(query=Query, mutation=Mutation, subscription=Subscription)
|
|
9
|
+
mount(app, "/graphql", schema)
|
|
10
|
+
|
|
11
|
+
One path serves ``POST`` (JSON, batches too) and ``GET`` (query string), GraphQL over SSE for subscriptions
|
|
12
|
+
(``Accept: text/event-stream``), WebSocket subscriptions (``graphql-transport-ws`` and the older ``graphql-ws``) and
|
|
13
|
+
GraphiQL for a browser ``GET`` (``graphiql=False`` turns it off; the page loads its scripts from a CDN).
|
|
14
|
+
``context_getter(request)`` builds the context passed to resolvers (default: ``{"request": request}``).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
# F-268: examples/api/python-graphql(F-210)を取り込み、WebSocket(F-263)のサブスクリプションを足した
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
import json
|
|
21
|
+
|
|
22
|
+
import veil
|
|
23
|
+
|
|
24
|
+
__all__ = ["mount", "GraphQLRoutes"]
|
|
25
|
+
|
|
26
|
+
_GRAPHQL_RESPONSE = "application/graphql-response+json"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _result(r):
|
|
30
|
+
out = {"data": r.data}
|
|
31
|
+
if r.errors:
|
|
32
|
+
out["errors"] = [e.formatted if hasattr(e, "formatted") else {"message": str(e)} for e in r.errors]
|
|
33
|
+
if getattr(r, "extensions", None):
|
|
34
|
+
out["extensions"] = r.extensions
|
|
35
|
+
return out
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _graphiql_html(path):
|
|
39
|
+
try:
|
|
40
|
+
from strawberry.http.ide import get_graphql_ide_html # noqa: PLC0415
|
|
41
|
+
|
|
42
|
+
return get_graphql_ide_html(graphql_ide="graphiql")
|
|
43
|
+
except ImportError:
|
|
44
|
+
pass
|
|
45
|
+
try:
|
|
46
|
+
from strawberry.utils.graphiql import get_graphiql_html # noqa: PLC0415
|
|
47
|
+
|
|
48
|
+
return get_graphiql_html()
|
|
49
|
+
except ImportError:
|
|
50
|
+
pass
|
|
51
|
+
return (
|
|
52
|
+
"<!doctype html><html><head><title>GraphiQL</title>"
|
|
53
|
+
'<link rel="stylesheet" href="https://unpkg.com/graphiql@3/graphiql.min.css"></head>'
|
|
54
|
+
'<body style="margin:0"><div id="g" style="height:100vh"></div>'
|
|
55
|
+
'<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>'
|
|
56
|
+
'<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>'
|
|
57
|
+
'<script src="https://unpkg.com/graphiql@3/graphiql.min.js"></script>'
|
|
58
|
+
"<script>ReactDOM.createRoot(document.getElementById('g')).render(React.createElement(GraphiQL,"
|
|
59
|
+
"{fetcher:GraphiQL.createFetcher({url:%s})}))</script></body></html>" % json.dumps(path)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class GraphQLRoutes:
|
|
64
|
+
"""The routes of one schema (created by :func:`mount`)."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, schema, path, graphiql=True, context_getter=None, keep_alive=None):
|
|
67
|
+
self.schema = schema
|
|
68
|
+
self.path = path
|
|
69
|
+
self.graphiql = graphiql
|
|
70
|
+
self.context_getter = context_getter or (lambda request: {"request": request})
|
|
71
|
+
self.keep_alive = keep_alive
|
|
72
|
+
|
|
73
|
+
async def _execute(self, body, request):
|
|
74
|
+
ctx = self.context_getter(request)
|
|
75
|
+
if isinstance(ctx, asyncio.Future) or asyncio.iscoroutine(ctx):
|
|
76
|
+
ctx = await ctx
|
|
77
|
+
r = await self.schema.execute(body.get("query") or "", variable_values=body.get("variables"),
|
|
78
|
+
operation_name=body.get("operationName"), context_value=ctx)
|
|
79
|
+
return _result(r)
|
|
80
|
+
|
|
81
|
+
async def _subscribe(self, body, request):
|
|
82
|
+
ctx = self.context_getter(request)
|
|
83
|
+
if asyncio.iscoroutine(ctx):
|
|
84
|
+
ctx = await ctx
|
|
85
|
+
r = await self.schema.subscribe(body.get("query") or "", variable_values=body.get("variables"),
|
|
86
|
+
operation_name=body.get("operationName"), context_value=ctx)
|
|
87
|
+
if hasattr(r, "__aiter__"):
|
|
88
|
+
async for x in r:
|
|
89
|
+
yield _result(x)
|
|
90
|
+
else:
|
|
91
|
+
yield _result(r)
|
|
92
|
+
|
|
93
|
+
async def http(self, request: veil.Request):
|
|
94
|
+
"""``GET`` / ``POST`` on the GraphQL path."""
|
|
95
|
+
accept = request.headers.get("accept") or ""
|
|
96
|
+
if request.method in ("GET", "HEAD"):
|
|
97
|
+
q = request.query_params
|
|
98
|
+
if not q.get("query"):
|
|
99
|
+
if self.graphiql and "text/html" in accept:
|
|
100
|
+
return veil.HTMLResponse(_graphiql_html(self.path))
|
|
101
|
+
raise veil.HTTPException(400, "missing query")
|
|
102
|
+
try:
|
|
103
|
+
variables = json.loads(q["variables"]) if q.get("variables") else None
|
|
104
|
+
except ValueError:
|
|
105
|
+
raise veil.HTTPException(400, "variables is not valid JSON")
|
|
106
|
+
body = {"query": q.get("query"), "variables": variables, "operationName": q.get("operationName")}
|
|
107
|
+
else:
|
|
108
|
+
try:
|
|
109
|
+
body = json.loads(request.body or b"null")
|
|
110
|
+
except ValueError:
|
|
111
|
+
raise veil.HTTPException(400, "the body is not valid JSON")
|
|
112
|
+
if "text/event-stream" in accept:
|
|
113
|
+
if not isinstance(body, dict):
|
|
114
|
+
raise veil.HTTPException(400, "subscriptions over SSE take one operation")
|
|
115
|
+
|
|
116
|
+
async def events():
|
|
117
|
+
async for r in self._subscribe(body, request):
|
|
118
|
+
yield {"event": "next", "data": r}
|
|
119
|
+
yield {"event": "complete", "data": ""}
|
|
120
|
+
|
|
121
|
+
return veil.EventSourceResponse(events())
|
|
122
|
+
if isinstance(body, list):
|
|
123
|
+
out = [await self._execute(b, request) for b in body if isinstance(b, dict)]
|
|
124
|
+
elif isinstance(body, dict):
|
|
125
|
+
out = await self._execute(body, request)
|
|
126
|
+
else:
|
|
127
|
+
raise veil.HTTPException(400, "a GraphQL request is a JSON object")
|
|
128
|
+
media = _GRAPHQL_RESPONSE if _GRAPHQL_RESPONSE in accept else "application/json"
|
|
129
|
+
return veil.Response(json.dumps(out, separators=(",", ":")), 200, media_type=media)
|
|
130
|
+
|
|
131
|
+
async def websocket(self, websocket: veil.WebSocket):
|
|
132
|
+
"""Subscriptions over WebSocket (``graphql-transport-ws`` and ``graphql-ws``)."""
|
|
133
|
+
offered = websocket.subprotocols
|
|
134
|
+
proto = next((p for p in ("graphql-transport-ws", "graphql-ws") if p in offered), None)
|
|
135
|
+
if proto is None:
|
|
136
|
+
await websocket.close(4406)
|
|
137
|
+
return
|
|
138
|
+
await websocket.accept(subprotocol=proto)
|
|
139
|
+
legacy = proto == "graphql-ws"
|
|
140
|
+
ops = {}
|
|
141
|
+
acked = False
|
|
142
|
+
|
|
143
|
+
async def run(op_id, payload):
|
|
144
|
+
try:
|
|
145
|
+
async for r in self._subscribe(payload, websocket):
|
|
146
|
+
await websocket.send_json({"type": "data" if legacy else "next", "id": op_id, "payload": r})
|
|
147
|
+
await websocket.send_json({"type": "complete", "id": op_id})
|
|
148
|
+
except asyncio.CancelledError:
|
|
149
|
+
raise
|
|
150
|
+
except Exception as e: # noqa: BLE001
|
|
151
|
+
await websocket.send_json({"type": "error", "id": op_id, "payload": [{"message": str(e)}]})
|
|
152
|
+
finally:
|
|
153
|
+
ops.pop(op_id, None)
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
while True:
|
|
157
|
+
try:
|
|
158
|
+
msg = await websocket.receive_json()
|
|
159
|
+
except veil.WebSocketDisconnect:
|
|
160
|
+
break
|
|
161
|
+
except (ValueError, TypeError):
|
|
162
|
+
await websocket.close(4400, "invalid message")
|
|
163
|
+
break
|
|
164
|
+
t = msg.get("type") if isinstance(msg, dict) else None
|
|
165
|
+
if t == "connection_init":
|
|
166
|
+
if acked and not legacy:
|
|
167
|
+
await websocket.close(4429, "Too many initialisation requests")
|
|
168
|
+
break
|
|
169
|
+
acked = True
|
|
170
|
+
await websocket.send_json({"type": "connection_ack"})
|
|
171
|
+
elif t == "ping":
|
|
172
|
+
await websocket.send_json({"type": "pong", "payload": msg.get("payload")})
|
|
173
|
+
elif t == "pong":
|
|
174
|
+
pass
|
|
175
|
+
elif t in ("subscribe", "start"):
|
|
176
|
+
if not acked:
|
|
177
|
+
await websocket.close(4401, "Unauthorized")
|
|
178
|
+
break
|
|
179
|
+
op_id = msg.get("id")
|
|
180
|
+
if op_id in ops:
|
|
181
|
+
await websocket.close(4409, "Subscriber for %s already exists" % op_id)
|
|
182
|
+
break
|
|
183
|
+
ops[op_id] = asyncio.get_running_loop().create_task(run(op_id, msg.get("payload") or {}))
|
|
184
|
+
elif t in ("complete", "stop"):
|
|
185
|
+
task = ops.pop(msg.get("id"), None)
|
|
186
|
+
if task is not None:
|
|
187
|
+
task.cancel()
|
|
188
|
+
elif t == "connection_terminate":
|
|
189
|
+
await websocket.close(1000)
|
|
190
|
+
break
|
|
191
|
+
else:
|
|
192
|
+
await websocket.close(4400, "Unknown message type")
|
|
193
|
+
break
|
|
194
|
+
finally:
|
|
195
|
+
for task in list(ops.values()):
|
|
196
|
+
task.cancel()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def mount(app, path, schema, *, graphiql=True, context_getter=None, websocket=True):
|
|
200
|
+
"""Serve ``schema`` (a ``strawberry.Schema``) at ``path`` on ``app``; returns the :class:`GraphQLRoutes`."""
|
|
201
|
+
routes = GraphQLRoutes(schema, path, graphiql, context_getter)
|
|
202
|
+
|
|
203
|
+
async def graphql_http(request: veil.Request):
|
|
204
|
+
return await routes.http(request)
|
|
205
|
+
|
|
206
|
+
graphql_http.__name__ = "graphql" + path.replace("/", "_").replace("{", "").replace("}", "")
|
|
207
|
+
app.route(path, methods=["GET", "POST"], tags=["graphql"], summary="GraphQL endpoint")(graphql_http)
|
|
208
|
+
if websocket:
|
|
209
|
+
async def graphql_ws(websocket: veil.WebSocket):
|
|
210
|
+
await routes.websocket(websocket)
|
|
211
|
+
|
|
212
|
+
graphql_ws.__name__ = graphql_http.__name__ + "_ws"
|
|
213
|
+
# 同じパスの GET と WebSocket は、サーバが Upgrade の有無で振り分ける
|
|
214
|
+
app.websocket(path, subprotocols=["graphql-transport-ws", "graphql-ws"], include_in_schema=False)(graphql_ws)
|
|
215
|
+
return routes
|