qstd-openapi 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.
- qstd_openapi/__init__.py +56 -0
- qstd_openapi/__main__.py +103 -0
- qstd_openapi/_compat.py +16 -0
- qstd_openapi/contrib/__init__.py +1 -0
- qstd_openapi/contrib/app_errors.py +173 -0
- qstd_openapi/core/__init__.py +56 -0
- qstd_openapi/core/document.py +1029 -0
- qstd_openapi/core/documents.py +296 -0
- qstd_openapi/core/error_responses.py +98 -0
- qstd_openapi/core/filters.py +81 -0
- qstd_openapi/core/schemas.py +554 -0
- qstd_openapi/core/sources.py +107 -0
- qstd_openapi/dialects/__init__.py +19 -0
- qstd_openapi/dialects/base.py +47 -0
- qstd_openapi/dialects/openapi30.py +312 -0
- qstd_openapi/dialects/openapi31.py +82 -0
- qstd_openapi/errors.py +117 -0
- qstd_openapi/fastapi.py +441 -0
- qstd_openapi/markers.py +57 -0
- qstd_openapi/meta/__init__.py +55 -0
- qstd_openapi/meta/merge.py +271 -0
- qstd_openapi/meta/model.py +182 -0
- qstd_openapi/meta/storage.py +112 -0
- qstd_openapi/openapi.py +656 -0
- qstd_openapi/py.typed +0 -0
- qstd_openapi/pydantic.py +494 -0
- qstd_openapi/sanic.py +343 -0
- qstd_openapi/serialization.py +63 -0
- qstd_openapi/tags.py +58 -0
- qstd_openapi/testing.py +98 -0
- qstd_openapi/ui.py +109 -0
- qstd_openapi-0.1.0.dist-info/METADATA +324 -0
- qstd_openapi-0.1.0.dist-info/RECORD +36 -0
- qstd_openapi-0.1.0.dist-info/WHEEL +5 -0
- qstd_openapi-0.1.0.dist-info/licenses/LICENSE +21 -0
- qstd_openapi-0.1.0.dist-info/top_level.txt +1 -0
qstd_openapi/openapi.py
ADDED
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
"""Declarative API: decorators, ``describe`` and ``attach``.
|
|
2
|
+
|
|
3
|
+
Every decorator here is ``describe`` with a single option, so all of them
|
|
4
|
+
follow the same rules. Decorators return the decorated object unchanged;
|
|
5
|
+
metadata is stored on the object itself (see :mod:`qstd_openapi.meta.storage`)
|
|
6
|
+
and is only interpreted when a document is built.
|
|
7
|
+
|
|
8
|
+
Usage::
|
|
9
|
+
|
|
10
|
+
from qstd_openapi import openapi
|
|
11
|
+
|
|
12
|
+
@openapi.tag('Users')
|
|
13
|
+
@openapi.errors(UserAlreadyExistsError)
|
|
14
|
+
@openapi.response(UserDTO, status=201)
|
|
15
|
+
async def register_user(request, body): ...
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from collections.abc import Hashable, Iterable, Mapping, Sequence
|
|
21
|
+
from enum import Enum
|
|
22
|
+
from typing import Any, Callable, Optional, TypeVar, Union, cast
|
|
23
|
+
|
|
24
|
+
from typing_extensions import TypeAlias, TypedDict, Unpack
|
|
25
|
+
|
|
26
|
+
from qstd_openapi.markers import File, FileList, FormFields
|
|
27
|
+
from qstd_openapi.meta.model import (
|
|
28
|
+
BodyPart,
|
|
29
|
+
ErrorRef,
|
|
30
|
+
Example,
|
|
31
|
+
Examples,
|
|
32
|
+
OperationPatch,
|
|
33
|
+
Parameter,
|
|
34
|
+
ParameterLocation,
|
|
35
|
+
ParameterModel,
|
|
36
|
+
ResponseHeader,
|
|
37
|
+
ResponsePart,
|
|
38
|
+
Security,
|
|
39
|
+
StatusCode,
|
|
40
|
+
Webhook,
|
|
41
|
+
)
|
|
42
|
+
from qstd_openapi.meta.storage import attach_patch
|
|
43
|
+
|
|
44
|
+
__all__ = (
|
|
45
|
+
'DescribeOptions',
|
|
46
|
+
'DocumentationOptions',
|
|
47
|
+
'ErrorLike',
|
|
48
|
+
'Example',
|
|
49
|
+
'ExampleValue',
|
|
50
|
+
'OperationOptions',
|
|
51
|
+
'Parameter',
|
|
52
|
+
'ParameterModel',
|
|
53
|
+
'ResponseHeader',
|
|
54
|
+
'ResponsePart',
|
|
55
|
+
'SchemaLike',
|
|
56
|
+
'Security',
|
|
57
|
+
'SecurityValue',
|
|
58
|
+
'Webhook',
|
|
59
|
+
'attach',
|
|
60
|
+
'body',
|
|
61
|
+
'body_binary',
|
|
62
|
+
'body_form_data_file',
|
|
63
|
+
'body_form_data_files',
|
|
64
|
+
'body_one_of',
|
|
65
|
+
'cookie',
|
|
66
|
+
'cookies',
|
|
67
|
+
'deprecated',
|
|
68
|
+
'describe',
|
|
69
|
+
'description',
|
|
70
|
+
'errors',
|
|
71
|
+
'exclude',
|
|
72
|
+
'extra',
|
|
73
|
+
'header',
|
|
74
|
+
'headers',
|
|
75
|
+
'no_content',
|
|
76
|
+
'operation_id',
|
|
77
|
+
'params',
|
|
78
|
+
'path',
|
|
79
|
+
'query',
|
|
80
|
+
'response',
|
|
81
|
+
'response_file',
|
|
82
|
+
'response_header',
|
|
83
|
+
'responses',
|
|
84
|
+
'scope',
|
|
85
|
+
'security',
|
|
86
|
+
'summary',
|
|
87
|
+
'tag',
|
|
88
|
+
'webhook',
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
F = TypeVar('F', bound=Callable[..., Any])
|
|
92
|
+
|
|
93
|
+
JSON = 'application/json'
|
|
94
|
+
|
|
95
|
+
SecurityValue: TypeAlias = Union[str, Mapping[str, Sequence[str]], Security]
|
|
96
|
+
"""A scheme name, ``{scheme: [scopes]}`` (all required together) or ``Security``."""
|
|
97
|
+
|
|
98
|
+
SchemaLike: TypeAlias = object
|
|
99
|
+
"""Anything a schema provider understands: a Pydantic model, a dataclass, a
|
|
100
|
+
type such as ``list[UserDTO]`` or ``Optional[int]``, a ``TypeAdapter``, a raw
|
|
101
|
+
JSON Schema dict (with ``markers.Ref`` inside) or a marker (``File``...)."""
|
|
102
|
+
|
|
103
|
+
ErrorLike: TypeAlias = type
|
|
104
|
+
"""An error class (usually an exception) for the document's error providers.
|
|
105
|
+
|
|
106
|
+
``ErrorProvider.supports`` takes any object at runtime; the annotation is a
|
|
107
|
+
class so that a string or an exception instance is a type error."""
|
|
108
|
+
|
|
109
|
+
ExampleValue: TypeAlias = object
|
|
110
|
+
"""An example value, or :class:`Example` with a summary and a description."""
|
|
111
|
+
|
|
112
|
+
ExamplesArg: TypeAlias = Optional[Mapping[str, ExampleValue]]
|
|
113
|
+
|
|
114
|
+
ResponseValue: TypeAlias = Union[SchemaLike, Sequence[SchemaLike], ResponsePart, None]
|
|
115
|
+
"""``responses={status: ...}`` value: a schema, alternatives (``oneOf``), ``None``
|
|
116
|
+
(no body) or a ``ResponsePart``."""
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class OperationOptions(TypedDict, total=False):
|
|
120
|
+
"""Operation fields that framework route decorators may accept themselves."""
|
|
121
|
+
|
|
122
|
+
summary: str
|
|
123
|
+
description: str
|
|
124
|
+
operation_id: str
|
|
125
|
+
deprecated: bool
|
|
126
|
+
tags: Union[str, Iterable[str]]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class DocumentationOptions(TypedDict, total=False):
|
|
130
|
+
"""Options only the library understands (never passed to a framework)."""
|
|
131
|
+
|
|
132
|
+
exclude: bool
|
|
133
|
+
webhook: Union[str, Webhook]
|
|
134
|
+
scope: Union[Hashable, Iterable[Hashable]]
|
|
135
|
+
security: Union[SecurityValue, Iterable[SecurityValue]]
|
|
136
|
+
body: SchemaLike
|
|
137
|
+
body_media_type: str
|
|
138
|
+
body_examples: Mapping[str, ExampleValue]
|
|
139
|
+
"""Named examples of the request body: ``{'name': value | Example}``."""
|
|
140
|
+
query: SchemaLike
|
|
141
|
+
"""A model whose fields become query parameters."""
|
|
142
|
+
path: SchemaLike
|
|
143
|
+
headers: SchemaLike
|
|
144
|
+
cookies: SchemaLike
|
|
145
|
+
parameters: Iterable[Union[Parameter, ParameterModel]]
|
|
146
|
+
response: SchemaLike
|
|
147
|
+
"""Schema of the ``200`` response."""
|
|
148
|
+
response_media_type: str
|
|
149
|
+
response_examples: Mapping[StatusCode, Mapping[str, ExampleValue]]
|
|
150
|
+
"""Named examples per response status: ``{201: {'name': value | Example}}``."""
|
|
151
|
+
responses: Mapping[StatusCode, ResponseValue]
|
|
152
|
+
"""``{status: schema | [schema, ...] | None | ResponsePart}``."""
|
|
153
|
+
errors: Iterable[ErrorLike]
|
|
154
|
+
errors_media_type: str
|
|
155
|
+
extra: Mapping[str, Any]
|
|
156
|
+
"""Raw OpenAPI operation fields, written for the dialect being built."""
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class DescribeOptions(OperationOptions, DocumentationOptions, total=False):
|
|
160
|
+
"""Options shared by :func:`describe`, :func:`attach` and router wrappers."""
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def attach(target: F, /, **options: Unpack[DescribeOptions]) -> F:
|
|
164
|
+
"""Attach metadata to ``target`` and return it unchanged.
|
|
165
|
+
|
|
166
|
+
Meant for project decorators, middlewares and validators that document
|
|
167
|
+
what they add, e.g. ``openapi.attach(wrapper, errors=[...])``.
|
|
168
|
+
"""
|
|
169
|
+
attach_patch(target, _build_patch(options), 'attach')
|
|
170
|
+
return target
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def describe(**options: Unpack[DescribeOptions]) -> Callable[[F], F]:
|
|
174
|
+
"""Attach several documentation options with one decorator."""
|
|
175
|
+
return _decorator(_build_patch(options), 'describe')
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# --- single-purpose decorators ---------------------------------------------
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def tag(*names: str) -> Callable[[F], F]:
|
|
182
|
+
"""Add one or more tags to an operation."""
|
|
183
|
+
return _decorator(OperationPatch(tags=names), 'tag')
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def summary(text: str) -> Callable[[F], F]:
|
|
187
|
+
"""Set the operation summary."""
|
|
188
|
+
return _decorator(OperationPatch(summary=text), 'summary')
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def description(text: str) -> Callable[[F], F]:
|
|
192
|
+
"""Set the operation description."""
|
|
193
|
+
return _decorator(OperationPatch(description=text), 'description')
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def operation_id(value: str) -> Callable[[F], F]:
|
|
197
|
+
"""Set an explicit OpenAPI ``operationId``."""
|
|
198
|
+
return _decorator(OperationPatch(operation_id=value), 'operation_id')
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def deprecated(value: bool = True) -> Callable[[F], F]:
|
|
202
|
+
"""Mark an operation as deprecated (or clear the flag with ``False``)."""
|
|
203
|
+
return _decorator(OperationPatch(deprecated=value), 'deprecated')
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def exclude(value: bool = True) -> Callable[[F], F]:
|
|
207
|
+
"""Exclude an operation from documents (or restore it with ``False``)."""
|
|
208
|
+
return _decorator(OperationPatch(exclude=value), 'exclude')
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def scope(*scopes: Hashable) -> Callable[[F], F]:
|
|
212
|
+
"""Label an operation for selection by :class:`ScopeFilter`."""
|
|
213
|
+
return _decorator(OperationPatch(scopes=scopes), 'scope')
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def security(
|
|
217
|
+
scheme: SecurityValue,
|
|
218
|
+
scopes: Optional[Sequence[str]] = None,
|
|
219
|
+
) -> Callable[[F], F]:
|
|
220
|
+
"""Add one security requirement (several decorators mean alternatives)."""
|
|
221
|
+
return _decorator(
|
|
222
|
+
OperationPatch(security=(_security(scheme, scopes),)),
|
|
223
|
+
'security',
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def webhook(
|
|
228
|
+
name: str,
|
|
229
|
+
method: str = 'post',
|
|
230
|
+
*,
|
|
231
|
+
scope: Union[Hashable, Iterable[Hashable], None] = None,
|
|
232
|
+
) -> Callable[[F], F]:
|
|
233
|
+
"""Mark a function that *sends* a webhook; it is documented, not routed."""
|
|
234
|
+
return _decorator(
|
|
235
|
+
OperationPatch(webhook=Webhook(name, method.lower()), scopes=_as_tuple(scope)),
|
|
236
|
+
'webhook',
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def body(
|
|
241
|
+
schema: SchemaLike,
|
|
242
|
+
media_type: str = JSON,
|
|
243
|
+
*,
|
|
244
|
+
examples: ExamplesArg = None,
|
|
245
|
+
) -> Callable[[F], F]:
|
|
246
|
+
"""Describe one request body schema and optional named examples."""
|
|
247
|
+
part = BodyPart(media_type, schema, _examples(examples))
|
|
248
|
+
return _decorator(OperationPatch(body=(part,)), 'body')
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def body_one_of(
|
|
252
|
+
*schemas: SchemaLike,
|
|
253
|
+
media_type: str = JSON,
|
|
254
|
+
examples: ExamplesArg = None,
|
|
255
|
+
) -> Callable[[F], F]:
|
|
256
|
+
"""Describe alternative request body schemas combined with ``oneOf``."""
|
|
257
|
+
parts = [BodyPart(media_type, s) for s in schemas]
|
|
258
|
+
if examples:
|
|
259
|
+
parts.append(BodyPart(media_type, None, _examples(examples)))
|
|
260
|
+
return _decorator(OperationPatch(body=tuple(parts)), 'body_one_of')
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def body_binary(media_type: str = '*/*') -> Callable[[F], F]:
|
|
264
|
+
"""Describe an arbitrary binary request body."""
|
|
265
|
+
return _decorator(
|
|
266
|
+
OperationPatch(body=(BodyPart(media_type, File()),)),
|
|
267
|
+
'body_binary',
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def body_form_data_file(
|
|
272
|
+
name: str = 'file',
|
|
273
|
+
*,
|
|
274
|
+
description: Optional[str] = None,
|
|
275
|
+
required: bool = True,
|
|
276
|
+
media_type: str = 'multipart/form-data',
|
|
277
|
+
) -> Callable[[F], F]:
|
|
278
|
+
"""Describe one file field in a multipart form body."""
|
|
279
|
+
schema = FormFields.of({name: File()}, (name,) if required else (), description)
|
|
280
|
+
return _decorator(
|
|
281
|
+
OperationPatch(body=(BodyPart(media_type, schema),)),
|
|
282
|
+
'body_form_data_file',
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def body_form_data_files(
|
|
287
|
+
name: str = 'files',
|
|
288
|
+
*,
|
|
289
|
+
max_items: Optional[int] = None,
|
|
290
|
+
description: Optional[str] = None,
|
|
291
|
+
required: bool = True,
|
|
292
|
+
media_type: str = 'multipart/form-data',
|
|
293
|
+
) -> Callable[[F], F]:
|
|
294
|
+
"""Describe a list of files in a multipart form body."""
|
|
295
|
+
schema = FormFields.of(
|
|
296
|
+
{name: FileList(max_items)},
|
|
297
|
+
(name,) if required else (),
|
|
298
|
+
description,
|
|
299
|
+
)
|
|
300
|
+
return _decorator(
|
|
301
|
+
OperationPatch(body=(BodyPart(media_type, schema),)),
|
|
302
|
+
'body_form_data_files',
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def query(
|
|
307
|
+
model_or_name: Union[str, SchemaLike],
|
|
308
|
+
schema: SchemaLike = str,
|
|
309
|
+
*,
|
|
310
|
+
required: Optional[bool] = None,
|
|
311
|
+
description: Optional[str] = None,
|
|
312
|
+
deprecated: bool = False,
|
|
313
|
+
examples: ExamplesArg = None,
|
|
314
|
+
) -> Callable[[F], F]:
|
|
315
|
+
"""``query(Model)`` expands the model's fields; ``query('page', int)`` adds one."""
|
|
316
|
+
return _parameter_decorator(
|
|
317
|
+
'query',
|
|
318
|
+
'query',
|
|
319
|
+
model_or_name,
|
|
320
|
+
schema,
|
|
321
|
+
required,
|
|
322
|
+
description,
|
|
323
|
+
deprecated,
|
|
324
|
+
examples,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def path(
|
|
329
|
+
model_or_name: Union[str, SchemaLike],
|
|
330
|
+
schema: SchemaLike = str,
|
|
331
|
+
*,
|
|
332
|
+
description: Optional[str] = None,
|
|
333
|
+
deprecated: bool = False,
|
|
334
|
+
examples: ExamplesArg = None,
|
|
335
|
+
) -> Callable[[F], F]:
|
|
336
|
+
"""Add a required path parameter or expand the fields of a model."""
|
|
337
|
+
return _parameter_decorator(
|
|
338
|
+
'path',
|
|
339
|
+
'path',
|
|
340
|
+
model_or_name,
|
|
341
|
+
schema,
|
|
342
|
+
True,
|
|
343
|
+
description,
|
|
344
|
+
deprecated,
|
|
345
|
+
examples,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
params = path
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def header(
|
|
353
|
+
name: str,
|
|
354
|
+
schema: SchemaLike = str,
|
|
355
|
+
*,
|
|
356
|
+
required: Optional[bool] = None,
|
|
357
|
+
description: Optional[str] = None,
|
|
358
|
+
deprecated: bool = False,
|
|
359
|
+
examples: ExamplesArg = None,
|
|
360
|
+
) -> Callable[[F], F]:
|
|
361
|
+
"""Add one header parameter."""
|
|
362
|
+
return _parameter_decorator(
|
|
363
|
+
'header',
|
|
364
|
+
'header',
|
|
365
|
+
name,
|
|
366
|
+
schema,
|
|
367
|
+
required,
|
|
368
|
+
description,
|
|
369
|
+
deprecated,
|
|
370
|
+
examples,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def headers(model: SchemaLike) -> Callable[[F], F]:
|
|
375
|
+
"""Expand a model's fields into header parameters."""
|
|
376
|
+
return _decorator(
|
|
377
|
+
OperationPatch(parameters=(ParameterModel('header', model),)),
|
|
378
|
+
'headers',
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def cookie(
|
|
383
|
+
name: str,
|
|
384
|
+
schema: SchemaLike = str,
|
|
385
|
+
*,
|
|
386
|
+
required: Optional[bool] = None,
|
|
387
|
+
description: Optional[str] = None,
|
|
388
|
+
deprecated: bool = False,
|
|
389
|
+
examples: ExamplesArg = None,
|
|
390
|
+
) -> Callable[[F], F]:
|
|
391
|
+
"""Add one cookie parameter."""
|
|
392
|
+
return _parameter_decorator(
|
|
393
|
+
'cookie',
|
|
394
|
+
'cookie',
|
|
395
|
+
name,
|
|
396
|
+
schema,
|
|
397
|
+
required,
|
|
398
|
+
description,
|
|
399
|
+
deprecated,
|
|
400
|
+
examples,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def cookies(model: SchemaLike) -> Callable[[F], F]:
|
|
405
|
+
"""Expand a model's fields into cookie parameters."""
|
|
406
|
+
return _decorator(
|
|
407
|
+
OperationPatch(parameters=(ParameterModel('cookie', model),)),
|
|
408
|
+
'cookies',
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def response(
|
|
413
|
+
schema: SchemaLike,
|
|
414
|
+
status: StatusCode = 200,
|
|
415
|
+
media_type: str = JSON,
|
|
416
|
+
description: Optional[str] = None,
|
|
417
|
+
*,
|
|
418
|
+
examples: ExamplesArg = None,
|
|
419
|
+
) -> Callable[[F], F]:
|
|
420
|
+
"""Describe one response schema and optional named examples."""
|
|
421
|
+
part = ResponsePart(
|
|
422
|
+
status,
|
|
423
|
+
media_type,
|
|
424
|
+
schema,
|
|
425
|
+
description,
|
|
426
|
+
examples=_examples(examples),
|
|
427
|
+
)
|
|
428
|
+
return _decorator(OperationPatch(responses=(part,)), 'response')
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def responses(
|
|
432
|
+
*schemas: SchemaLike,
|
|
433
|
+
status: StatusCode = 200,
|
|
434
|
+
media_type: str = JSON,
|
|
435
|
+
description: Optional[str] = None,
|
|
436
|
+
examples: ExamplesArg = None,
|
|
437
|
+
) -> Callable[[F], F]:
|
|
438
|
+
"""Several alternative bodies (``oneOf``) for one status."""
|
|
439
|
+
parts = [ResponsePart(status, media_type, s, description) for s in schemas]
|
|
440
|
+
if examples:
|
|
441
|
+
parts.append(ResponsePart(status, media_type, examples=_examples(examples)))
|
|
442
|
+
return _decorator(OperationPatch(responses=tuple(parts)), 'responses')
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def response_file(
|
|
446
|
+
media_type: str = '*/*',
|
|
447
|
+
status: StatusCode = 200,
|
|
448
|
+
description: Optional[str] = None,
|
|
449
|
+
) -> Callable[[F], F]:
|
|
450
|
+
"""Describe a binary response."""
|
|
451
|
+
return _decorator(
|
|
452
|
+
OperationPatch(
|
|
453
|
+
responses=(ResponsePart(status, media_type, File(), description),),
|
|
454
|
+
),
|
|
455
|
+
'response_file',
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def response_header(
|
|
460
|
+
name: str,
|
|
461
|
+
schema: SchemaLike = str,
|
|
462
|
+
*,
|
|
463
|
+
status: StatusCode = 200,
|
|
464
|
+
description: Optional[str] = None,
|
|
465
|
+
required: bool = False,
|
|
466
|
+
) -> Callable[[F], F]:
|
|
467
|
+
"""Add a header to the response for one status."""
|
|
468
|
+
part = ResponsePart(
|
|
469
|
+
status,
|
|
470
|
+
headers=(ResponseHeader(name, schema, description, required),),
|
|
471
|
+
)
|
|
472
|
+
return _decorator(OperationPatch(responses=(part,)), 'response_header')
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def no_content(
|
|
476
|
+
status: StatusCode = 204,
|
|
477
|
+
description: Optional[str] = None,
|
|
478
|
+
) -> Callable[[F], F]:
|
|
479
|
+
"""Describe a response status without a body."""
|
|
480
|
+
return _decorator(
|
|
481
|
+
OperationPatch(responses=(ResponsePart(status, description=description),)),
|
|
482
|
+
'no_content',
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def errors(*errors: ErrorLike, media_type: str = JSON) -> Callable[[F], F]:
|
|
487
|
+
"""Add error classes handled by the document's error providers."""
|
|
488
|
+
return _decorator(
|
|
489
|
+
OperationPatch(errors=tuple(ErrorRef(e, media_type) for e in errors)),
|
|
490
|
+
'errors',
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def extra(fields: Mapping[str, Any]) -> Callable[[F], F]:
|
|
495
|
+
"""Raw OpenAPI operation fields, written for the dialect being built."""
|
|
496
|
+
return _decorator(OperationPatch(extra=(fields,)), 'extra')
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
# --- helpers ----------------------------------------------------------------
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _decorator(patch: OperationPatch, api: str) -> Callable[[F], F]:
|
|
503
|
+
def decorate(target: F) -> F:
|
|
504
|
+
attach_patch(target, patch, api)
|
|
505
|
+
return target
|
|
506
|
+
|
|
507
|
+
return decorate
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _parameter_decorator(
|
|
511
|
+
api: str,
|
|
512
|
+
location: ParameterLocation,
|
|
513
|
+
model_or_name: Union[str, SchemaLike],
|
|
514
|
+
schema: SchemaLike,
|
|
515
|
+
required: Optional[bool],
|
|
516
|
+
description: Optional[str],
|
|
517
|
+
deprecated: bool,
|
|
518
|
+
examples: ExamplesArg = None,
|
|
519
|
+
) -> Callable[[F], F]:
|
|
520
|
+
parameter: Union[Parameter, ParameterModel]
|
|
521
|
+
if isinstance(model_or_name, str):
|
|
522
|
+
parameter = Parameter(
|
|
523
|
+
location,
|
|
524
|
+
model_or_name,
|
|
525
|
+
schema,
|
|
526
|
+
required,
|
|
527
|
+
description,
|
|
528
|
+
deprecated,
|
|
529
|
+
_examples(examples),
|
|
530
|
+
)
|
|
531
|
+
elif examples:
|
|
532
|
+
raise TypeError(
|
|
533
|
+
f'{api}(Model, examples=...): examples belong to one parameter; '
|
|
534
|
+
f'use {api}(name, schema, examples=...) or examples in the model',
|
|
535
|
+
)
|
|
536
|
+
else:
|
|
537
|
+
parameter = ParameterModel(location, model_or_name)
|
|
538
|
+
return _decorator(OperationPatch(parameters=(parameter,)), api)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _examples(examples: ExamplesArg) -> Examples:
|
|
542
|
+
if not examples:
|
|
543
|
+
return ()
|
|
544
|
+
return tuple(
|
|
545
|
+
(name, value if isinstance(value, Example) else Example(value))
|
|
546
|
+
for name, value in examples.items()
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _as_tuple(value: Any) -> tuple[Any, ...]:
|
|
551
|
+
"""One value or an iterable of values; strings and enum members are single."""
|
|
552
|
+
if value is None:
|
|
553
|
+
return ()
|
|
554
|
+
if isinstance(value, (str, bytes, Enum)) or not isinstance(value, Iterable):
|
|
555
|
+
return (value,)
|
|
556
|
+
return tuple(cast('Iterable[Any]', value))
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _security(value: SecurityValue, scopes: Optional[Sequence[str]] = None) -> Security:
|
|
560
|
+
if isinstance(value, Security):
|
|
561
|
+
if scopes is not None:
|
|
562
|
+
raise TypeError('scopes cannot be combined with a Security object')
|
|
563
|
+
return value
|
|
564
|
+
if isinstance(value, str):
|
|
565
|
+
return Security(((value, tuple(scopes or ())),))
|
|
566
|
+
if scopes is not None:
|
|
567
|
+
raise TypeError('scopes can only be given together with a single scheme name')
|
|
568
|
+
return Security(tuple((name, tuple(values)) for name, values in value.items()))
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _security_list(value: Any) -> tuple[Security, ...]:
|
|
572
|
+
if isinstance(value, (str, Security, Mapping)):
|
|
573
|
+
return (_security(cast('SecurityValue', value)),)
|
|
574
|
+
return tuple(_security(item) for item in cast('Iterable[SecurityValue]', value))
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def _responses(
|
|
578
|
+
statuses: Mapping[StatusCode, ResponseValue],
|
|
579
|
+
media_type: str,
|
|
580
|
+
) -> list[ResponsePart]:
|
|
581
|
+
parts: list[ResponsePart] = []
|
|
582
|
+
for status, value in statuses.items():
|
|
583
|
+
if value is None:
|
|
584
|
+
parts.append(ResponsePart(status))
|
|
585
|
+
elif isinstance(value, ResponsePart):
|
|
586
|
+
parts.append(value)
|
|
587
|
+
elif isinstance(value, (list, tuple)):
|
|
588
|
+
schemas = cast('Sequence[Any]', value)
|
|
589
|
+
parts.extend(ResponsePart(status, media_type, s) for s in schemas)
|
|
590
|
+
else:
|
|
591
|
+
parts.append(ResponsePart(status, media_type, value))
|
|
592
|
+
return parts
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _build_patch(options: DescribeOptions) -> OperationPatch:
|
|
596
|
+
unknown = set(options) - set(DescribeOptions.__annotations__)
|
|
597
|
+
if unknown:
|
|
598
|
+
raise TypeError(f'Unknown describe options: {", ".join(sorted(unknown))}')
|
|
599
|
+
|
|
600
|
+
parameters: list[Union[Parameter, ParameterModel]] = list(
|
|
601
|
+
options.get('parameters', ()),
|
|
602
|
+
)
|
|
603
|
+
locations: tuple[tuple[str, ParameterLocation], ...] = (
|
|
604
|
+
('query', 'query'),
|
|
605
|
+
('path', 'path'),
|
|
606
|
+
('headers', 'header'),
|
|
607
|
+
('cookies', 'cookie'),
|
|
608
|
+
)
|
|
609
|
+
for key, location in locations:
|
|
610
|
+
model = options.get(key)
|
|
611
|
+
if model is not None:
|
|
612
|
+
parameters.append(ParameterModel(location, model))
|
|
613
|
+
|
|
614
|
+
body_parts: tuple[BodyPart, ...] = ()
|
|
615
|
+
body_examples = _examples(options.get('body_examples'))
|
|
616
|
+
if options.get('body') is not None or body_examples:
|
|
617
|
+
body_parts = (
|
|
618
|
+
BodyPart(
|
|
619
|
+
options.get('body_media_type', JSON),
|
|
620
|
+
options.get('body'),
|
|
621
|
+
body_examples,
|
|
622
|
+
),
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
response_media = options.get('response_media_type', JSON)
|
|
626
|
+
response_parts: list[ResponsePart] = []
|
|
627
|
+
if options.get('response') is not None:
|
|
628
|
+
response_parts.append(
|
|
629
|
+
ResponsePart(200, response_media, options.get('response')),
|
|
630
|
+
)
|
|
631
|
+
response_parts.extend(_responses(options.get('responses', {}), response_media))
|
|
632
|
+
for status, examples in options.get('response_examples', {}).items():
|
|
633
|
+
response_parts.append(
|
|
634
|
+
ResponsePart(status, response_media, examples=_examples(examples)),
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
hook = options.get('webhook')
|
|
638
|
+
errors_media = options.get('errors_media_type', JSON)
|
|
639
|
+
extra_fields = options.get('extra')
|
|
640
|
+
|
|
641
|
+
return OperationPatch(
|
|
642
|
+
summary=options.get('summary'),
|
|
643
|
+
description=options.get('description'),
|
|
644
|
+
operation_id=options.get('operation_id'),
|
|
645
|
+
deprecated=options.get('deprecated'),
|
|
646
|
+
exclude=options.get('exclude'),
|
|
647
|
+
webhook=Webhook(hook) if isinstance(hook, str) else hook,
|
|
648
|
+
tags=_as_tuple(options.get('tags')),
|
|
649
|
+
scopes=_as_tuple(options.get('scope')),
|
|
650
|
+
security=_security_list(options['security']) if 'security' in options else (),
|
|
651
|
+
parameters=tuple(parameters),
|
|
652
|
+
body=body_parts,
|
|
653
|
+
responses=tuple(response_parts),
|
|
654
|
+
errors=tuple(ErrorRef(e, errors_media) for e in options.get('errors', ())),
|
|
655
|
+
extra=(extra_fields,) if extra_fields is not None else (),
|
|
656
|
+
)
|
qstd_openapi/py.typed
ADDED
|
File without changes
|