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/pydantic.py
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
"""Schemas from Pydantic models, dataclasses and annotated types.
|
|
2
|
+
|
|
3
|
+
Install with ``qstd-openapi[pydantic]`` (Pydantic 2) or
|
|
4
|
+
``qstd-openapi[pydantic-v1]`` (Pydantic 1.10). Models written against the v1
|
|
5
|
+
API inside Pydantic 2 (``pydantic.v1``) are supported as well.
|
|
6
|
+
|
|
7
|
+
Pydantic 1 support is **experimental** and is expected to be removed in a
|
|
8
|
+
future release. Pydantic 1 produces older JSON Schema; its output is
|
|
9
|
+
normalized to what Pydantic 2 would produce: ``definitions`` become components, ``Optional``
|
|
10
|
+
fields allow ``null``, single-value ``Literal`` becomes ``const`` and enums
|
|
11
|
+
get a ``type`` without the boilerplate description.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import copy
|
|
17
|
+
import dataclasses
|
|
18
|
+
import datetime
|
|
19
|
+
import decimal
|
|
20
|
+
import importlib
|
|
21
|
+
import uuid
|
|
22
|
+
|
|
23
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
24
|
+
from types import MappingProxyType
|
|
25
|
+
from typing import Any, Callable, Optional, cast, get_args
|
|
26
|
+
|
|
27
|
+
from qstd_openapi._compat import is_class
|
|
28
|
+
from qstd_openapi.core.schemas import (
|
|
29
|
+
JsonSchema,
|
|
30
|
+
OverrideWithin,
|
|
31
|
+
SchemaContext,
|
|
32
|
+
SchemaMode,
|
|
33
|
+
SchemaRequest,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = ('PydanticSchemas',)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _load() -> tuple[Optional[Any], Optional[Any]]:
|
|
40
|
+
"""``(pydantic v2 module or None, v1 API module or None)``."""
|
|
41
|
+
try:
|
|
42
|
+
pydantic: Any = importlib.import_module('pydantic')
|
|
43
|
+
except ImportError as exc: # pragma: no cover - extra not installed
|
|
44
|
+
raise ImportError(
|
|
45
|
+
'qstd_openapi.pydantic needs Pydantic: install qstd-openapi[pydantic] '
|
|
46
|
+
'or qstd-openapi[pydantic-v1]',
|
|
47
|
+
) from exc
|
|
48
|
+
if str(pydantic.VERSION).startswith('1.'):
|
|
49
|
+
return None, pydantic
|
|
50
|
+
try:
|
|
51
|
+
v1: Any = importlib.import_module('pydantic.v1')
|
|
52
|
+
except ImportError: # pragma: no cover - very old 2.x
|
|
53
|
+
v1 = None
|
|
54
|
+
return pydantic, v1
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class PydanticSchemas:
|
|
58
|
+
"""Schema provider backed by Pydantic.
|
|
59
|
+
|
|
60
|
+
Accepts models, dataclasses, any type ``TypeAdapter`` understands and a
|
|
61
|
+
ready ``TypeAdapter`` instance (Pydantic 2). ``by_alias`` controls whether
|
|
62
|
+
field aliases or attribute names are used as property names; it should
|
|
63
|
+
match how the application serializes data.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, *, by_alias: bool = True) -> None:
|
|
67
|
+
self._v2, self._v1 = _load()
|
|
68
|
+
self.by_alias = by_alias
|
|
69
|
+
self._supported: dict[Any, bool] = {}
|
|
70
|
+
|
|
71
|
+
# --- protocol -------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
def supports(self, target: object) -> bool:
|
|
74
|
+
try:
|
|
75
|
+
key: Any = target
|
|
76
|
+
hash(key)
|
|
77
|
+
except TypeError:
|
|
78
|
+
return False
|
|
79
|
+
if key not in self._supported:
|
|
80
|
+
self._supported[key] = self._check(target)
|
|
81
|
+
return self._supported[key]
|
|
82
|
+
|
|
83
|
+
def generate(
|
|
84
|
+
self,
|
|
85
|
+
requests: Sequence[SchemaRequest],
|
|
86
|
+
context: SchemaContext,
|
|
87
|
+
) -> Sequence[JsonSchema]:
|
|
88
|
+
results: list[Optional[JsonSchema]] = [None] * len(requests)
|
|
89
|
+
v2_indexes = [i for i, r in enumerate(requests) if not self._is_v1(r.target)]
|
|
90
|
+
if v2_indexes:
|
|
91
|
+
for index, schema in zip(
|
|
92
|
+
v2_indexes,
|
|
93
|
+
self._generate_v2([requests[i] for i in v2_indexes], context),
|
|
94
|
+
):
|
|
95
|
+
results[index] = schema
|
|
96
|
+
for index, request in enumerate(requests):
|
|
97
|
+
if results[index] is None:
|
|
98
|
+
results[index] = self._generate_v1(
|
|
99
|
+
request.target,
|
|
100
|
+
request.mode,
|
|
101
|
+
context,
|
|
102
|
+
)
|
|
103
|
+
return cast('list[JsonSchema]', results)
|
|
104
|
+
|
|
105
|
+
# --- detection ------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
def _is_v1(self, target: object) -> bool:
|
|
108
|
+
"""Whether ``target`` (or a type inside ``List[...]`` etc.) uses the v1 API."""
|
|
109
|
+
if self._v1 is None:
|
|
110
|
+
return False
|
|
111
|
+
if self._v2 is None:
|
|
112
|
+
return True
|
|
113
|
+
if is_class(target) and issubclass(target, self._v1.BaseModel):
|
|
114
|
+
return True
|
|
115
|
+
return any(self._is_v1(arg) for arg in get_args(target))
|
|
116
|
+
|
|
117
|
+
def _is_adapter(self, target: object) -> bool:
|
|
118
|
+
return self._v2 is not None and isinstance(target, self._v2.TypeAdapter)
|
|
119
|
+
|
|
120
|
+
def _check(self, target: object) -> bool:
|
|
121
|
+
if isinstance(target, Mapping):
|
|
122
|
+
return False
|
|
123
|
+
if self._is_adapter(target):
|
|
124
|
+
return True
|
|
125
|
+
try:
|
|
126
|
+
if self._is_v1(target):
|
|
127
|
+
self._v1_schema_of(target, '#/{model}')
|
|
128
|
+
else:
|
|
129
|
+
assert self._v2 is not None
|
|
130
|
+
self._v2.TypeAdapter(target)
|
|
131
|
+
except Exception:
|
|
132
|
+
return False
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
# --- Pydantic 2 -----------------------------------------------------
|
|
136
|
+
|
|
137
|
+
def _generate_v2(
|
|
138
|
+
self,
|
|
139
|
+
requests: Sequence[SchemaRequest],
|
|
140
|
+
context: SchemaContext,
|
|
141
|
+
) -> list[JsonSchema]:
|
|
142
|
+
assert self._v2 is not None
|
|
143
|
+
adapter = self._v2.TypeAdapter
|
|
144
|
+
generator = _override_generator(self._v2, context.type_override)
|
|
145
|
+
inputs = [
|
|
146
|
+
(
|
|
147
|
+
index,
|
|
148
|
+
r.mode,
|
|
149
|
+
r.target if self._is_adapter(r.target) else adapter(r.target),
|
|
150
|
+
)
|
|
151
|
+
for index, r in enumerate(requests)
|
|
152
|
+
]
|
|
153
|
+
keys, definitions = adapter.json_schemas(
|
|
154
|
+
inputs,
|
|
155
|
+
by_alias=self.by_alias,
|
|
156
|
+
ref_template=context.ref_template,
|
|
157
|
+
schema_generator=generator,
|
|
158
|
+
)
|
|
159
|
+
for name, schema in cast(
|
|
160
|
+
'Mapping[str, JsonSchema]',
|
|
161
|
+
definitions.get('$defs', {}),
|
|
162
|
+
).items():
|
|
163
|
+
context.add_component(name, schema, 'PydanticSchemas')
|
|
164
|
+
return [
|
|
165
|
+
cast('JsonSchema', keys[(index, r.mode)])
|
|
166
|
+
for index, r in enumerate(requests)
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
# --- Pydantic 1 -----------------------------------------------------
|
|
170
|
+
|
|
171
|
+
def _v1_schema_of(self, target: Any, ref_template: str) -> JsonSchema:
|
|
172
|
+
"""A private copy: Pydantic 1 caches ``Model.schema()`` and we edit the result."""
|
|
173
|
+
assert self._v1 is not None
|
|
174
|
+
if is_class(target) and issubclass(target, self._v1.BaseModel):
|
|
175
|
+
schema = cast(Any, target).schema(
|
|
176
|
+
by_alias=self.by_alias,
|
|
177
|
+
ref_template=ref_template,
|
|
178
|
+
)
|
|
179
|
+
else:
|
|
180
|
+
schema = self._v1.schema_of(
|
|
181
|
+
target,
|
|
182
|
+
by_alias=self.by_alias,
|
|
183
|
+
ref_template=ref_template,
|
|
184
|
+
)
|
|
185
|
+
return cast('JsonSchema', copy.deepcopy(schema))
|
|
186
|
+
|
|
187
|
+
def _generate_v1(
|
|
188
|
+
self,
|
|
189
|
+
target: Any,
|
|
190
|
+
mode: SchemaMode,
|
|
191
|
+
context: SchemaContext,
|
|
192
|
+
) -> JsonSchema:
|
|
193
|
+
assert self._v1 is not None
|
|
194
|
+
schema = self._v1_schema_of(target, context.ref_template)
|
|
195
|
+
definitions = cast('dict[str, JsonSchema]', schema.pop('definitions', {}))
|
|
196
|
+
is_model = is_class(target) and issubclass(target, self._v1.BaseModel)
|
|
197
|
+
if is_model:
|
|
198
|
+
name = target.__name__
|
|
199
|
+
definitions[name] = schema
|
|
200
|
+
result: JsonSchema = {'$ref': context.ref_template.format(model=name)}
|
|
201
|
+
else:
|
|
202
|
+
schema.pop('title', None) # "ParsingModel[...]" wrapper title
|
|
203
|
+
result = {'$ref': schema['$ref']} if '$ref' in schema else schema
|
|
204
|
+
|
|
205
|
+
changed: set[str] = set()
|
|
206
|
+
for model in _v1_models(target, self._v1):
|
|
207
|
+
definition = definitions.get(model.__name__)
|
|
208
|
+
if definition is not None:
|
|
209
|
+
if self._v1_overrides(model, definition, mode, context):
|
|
210
|
+
changed.add(model.__name__)
|
|
211
|
+
self._v1_nullable(model, definition)
|
|
212
|
+
if changed:
|
|
213
|
+
result = _v1_mode_variants(definitions, result, changed, mode, context)
|
|
214
|
+
for name, definition in definitions.items():
|
|
215
|
+
context.add_component(
|
|
216
|
+
name,
|
|
217
|
+
_v1_normalize(definition, root=True),
|
|
218
|
+
'PydanticSchemas',
|
|
219
|
+
)
|
|
220
|
+
return _v1_normalize(result, root=False)
|
|
221
|
+
|
|
222
|
+
def _v1_overrides(
|
|
223
|
+
self,
|
|
224
|
+
model: Any,
|
|
225
|
+
definition: JsonSchema,
|
|
226
|
+
mode: SchemaMode,
|
|
227
|
+
context: SchemaContext,
|
|
228
|
+
) -> bool:
|
|
229
|
+
"""Apply ``type_overrides`` to the fields of one class.
|
|
230
|
+
|
|
231
|
+
Covers fields of the overridden type itself, its sequences and dict
|
|
232
|
+
values (``Optional`` is restored by ``_v1_nullable`` afterwards).
|
|
233
|
+
Returns whether a mode-specific override changed the definition.
|
|
234
|
+
"""
|
|
235
|
+
assert self._v1 is not None
|
|
236
|
+
within: OverrideWithin = (
|
|
237
|
+
'models' if issubclass(model, self._v1.BaseModel) else 'dataclasses'
|
|
238
|
+
)
|
|
239
|
+
properties = cast('dict[str, JsonSchema]', definition.get('properties', {}))
|
|
240
|
+
mode_specific = False
|
|
241
|
+
for field in _v1_fields(model).values():
|
|
242
|
+
key = field.alias if self.by_alias else field.name
|
|
243
|
+
prop = properties.get(key)
|
|
244
|
+
override = context.type_override(field.type_, mode, within)
|
|
245
|
+
if prop is None or override is None:
|
|
246
|
+
continue
|
|
247
|
+
if field.shape == _V1_SINGLETON:
|
|
248
|
+
meta = {k: prop[k] for k in _V1_FIELD_META if k in prop}
|
|
249
|
+
properties[key] = {**override, **meta}
|
|
250
|
+
elif field.shape in _V1_SEQUENCES and 'items' in prop:
|
|
251
|
+
prop['items'] = override
|
|
252
|
+
elif field.shape in _V1_MAPPINGS and 'additionalProperties' in prop:
|
|
253
|
+
prop['additionalProperties'] = override
|
|
254
|
+
else:
|
|
255
|
+
continue
|
|
256
|
+
mode_specific = mode_specific or (
|
|
257
|
+
context.type_override(field.type_, _other(mode), within) != override
|
|
258
|
+
)
|
|
259
|
+
return mode_specific
|
|
260
|
+
|
|
261
|
+
def _v1_nullable(self, model: Any, definition: JsonSchema) -> None:
|
|
262
|
+
"""Pydantic 1 drops ``null`` from ``Optional`` fields; put it back."""
|
|
263
|
+
properties = cast('dict[str, JsonSchema]', definition.get('properties', {}))
|
|
264
|
+
for field in _v1_fields(model).values():
|
|
265
|
+
key = field.alias if self.by_alias else field.name
|
|
266
|
+
prop = properties.get(key)
|
|
267
|
+
if prop is None or not field.allow_none:
|
|
268
|
+
continue
|
|
269
|
+
meta = {
|
|
270
|
+
k: prop.pop(k) for k in ('title', 'description', 'default') if k in prop
|
|
271
|
+
}
|
|
272
|
+
if not field.required and field.default is None:
|
|
273
|
+
meta['default'] = None
|
|
274
|
+
properties[key] = {'anyOf': [prop, {'type': 'null'}], **meta}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
_V1_SINGLETON = 1
|
|
278
|
+
_V1_SEQUENCES = frozenset({2, 3, 6, 7, 8, 11})
|
|
279
|
+
"""``ModelField.shape``: list, set, tuple-ellipsis, sequence, frozenset, deque."""
|
|
280
|
+
_V1_MAPPINGS = frozenset({4, 12, 13})
|
|
281
|
+
"""``ModelField.shape``: mapping, dict, defaultdict."""
|
|
282
|
+
_V1_FIELD_META = ('title', 'description', 'default')
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _other(mode: SchemaMode) -> SchemaMode:
|
|
286
|
+
return 'serialization' if mode == 'validation' else 'validation'
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _v1_mode_variants(
|
|
290
|
+
definitions: dict[str, JsonSchema],
|
|
291
|
+
result: JsonSchema,
|
|
292
|
+
changed: set[str],
|
|
293
|
+
mode: SchemaMode,
|
|
294
|
+
context: SchemaContext,
|
|
295
|
+
) -> JsonSchema:
|
|
296
|
+
"""Rename definitions a mode-specific override changed, as Pydantic 2 does.
|
|
297
|
+
|
|
298
|
+
Pydantic 1 has a single schema per class; when an override applies only to
|
|
299
|
+
requests or only to responses the class gets ``-Input``/``-Output``, and so
|
|
300
|
+
does every definition referring to it.
|
|
301
|
+
"""
|
|
302
|
+
refs = {name: context.ref_template.format(model=name) for name in definitions}
|
|
303
|
+
|
|
304
|
+
def refers(value: Any, names: set[str]) -> bool:
|
|
305
|
+
if isinstance(value, dict):
|
|
306
|
+
items = cast('JsonSchema', value)
|
|
307
|
+
ref = items.get('$ref')
|
|
308
|
+
if any(ref == refs[name] for name in names):
|
|
309
|
+
return True
|
|
310
|
+
return any(refers(item, names) for item in items.values())
|
|
311
|
+
if isinstance(value, list):
|
|
312
|
+
return any(refers(item, names) for item in cast('list[object]', value))
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
grown = True
|
|
316
|
+
while grown:
|
|
317
|
+
grown = False
|
|
318
|
+
for name, definition in definitions.items():
|
|
319
|
+
if name not in changed and refers(definition, changed):
|
|
320
|
+
changed.add(name)
|
|
321
|
+
grown = True
|
|
322
|
+
suffix = '-Input' if mode == 'validation' else '-Output'
|
|
323
|
+
renamed = {
|
|
324
|
+
refs[name]: context.ref_template.format(model=f'{name}{suffix}')
|
|
325
|
+
for name in changed
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
def rewrite(value: Any) -> Any:
|
|
329
|
+
if isinstance(value, dict):
|
|
330
|
+
items = cast('JsonSchema', value)
|
|
331
|
+
return {
|
|
332
|
+
key: (
|
|
333
|
+
renamed.get(item, item)
|
|
334
|
+
if key == '$ref' and isinstance(item, str)
|
|
335
|
+
else rewrite(item)
|
|
336
|
+
)
|
|
337
|
+
for key, item in items.items()
|
|
338
|
+
}
|
|
339
|
+
if isinstance(value, list):
|
|
340
|
+
return [rewrite(item) for item in cast('list[object]', value)]
|
|
341
|
+
return value
|
|
342
|
+
|
|
343
|
+
for name in list(definitions):
|
|
344
|
+
definition = cast('JsonSchema', rewrite(definitions.pop(name)))
|
|
345
|
+
definitions[f'{name}{suffix}' if name in changed else name] = definition
|
|
346
|
+
return cast('JsonSchema', rewrite(result))
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
_CORE_TYPES: Mapping[str, type] = MappingProxyType(
|
|
350
|
+
{
|
|
351
|
+
'str': str,
|
|
352
|
+
'int': int,
|
|
353
|
+
'float': float,
|
|
354
|
+
'bool': bool,
|
|
355
|
+
'bytes': bytes,
|
|
356
|
+
'decimal': decimal.Decimal,
|
|
357
|
+
'datetime': datetime.datetime,
|
|
358
|
+
'date': datetime.date,
|
|
359
|
+
'time': datetime.time,
|
|
360
|
+
'timedelta': datetime.timedelta,
|
|
361
|
+
'uuid': uuid.UUID,
|
|
362
|
+
},
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _scalar_target(schema: Mapping[str, Any]) -> Optional[type]:
|
|
367
|
+
"""The scalar Python type a Pydantic core schema stands for, if known."""
|
|
368
|
+
core_type = schema.get('type')
|
|
369
|
+
return _CORE_TYPES.get(core_type) if isinstance(core_type, str) else None
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
TypeOverrideLookup = Callable[
|
|
373
|
+
[Any, SchemaMode, Optional[OverrideWithin]],
|
|
374
|
+
Optional[JsonSchema],
|
|
375
|
+
]
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _override_generator(v2: Any, lookup: TypeOverrideLookup) -> Any:
|
|
379
|
+
"""A ``GenerateJsonSchema`` applying the document's ``type_overrides``.
|
|
380
|
+
|
|
381
|
+
Keeps a stack of the classes being described, so an override limited to
|
|
382
|
+
model or dataclass fields knows whose field it is looking at.
|
|
383
|
+
"""
|
|
384
|
+
base: Any = importlib.import_module(f'{v2.__name__}.json_schema').GenerateJsonSchema
|
|
385
|
+
|
|
386
|
+
class _Generator(base): # type: ignore[misc, valid-type]
|
|
387
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
388
|
+
parent: Any = super()
|
|
389
|
+
parent.__init__(*args, **kwargs)
|
|
390
|
+
self._within: list[OverrideWithin] = []
|
|
391
|
+
|
|
392
|
+
def _lookup(self, target: Any) -> Optional[JsonSchema]:
|
|
393
|
+
within = self._within[-1] if self._within else None
|
|
394
|
+
return lookup(target, self.mode, within)
|
|
395
|
+
|
|
396
|
+
def _class_schema(self, method: str, within: Any, schema: Any) -> Any:
|
|
397
|
+
# The body of the class's definition: Pydantic still registers it
|
|
398
|
+
# under its name, so references to it stay valid.
|
|
399
|
+
override = self._lookup(schema.get('cls'))
|
|
400
|
+
if override is not None:
|
|
401
|
+
return override
|
|
402
|
+
parent: Any = super()
|
|
403
|
+
if within is not None:
|
|
404
|
+
self._within.append(within)
|
|
405
|
+
try:
|
|
406
|
+
return getattr(parent, method)(schema)
|
|
407
|
+
finally:
|
|
408
|
+
if within is not None:
|
|
409
|
+
self._within.pop()
|
|
410
|
+
|
|
411
|
+
def model_schema(self, schema: Any) -> Any:
|
|
412
|
+
return self._class_schema('model_schema', 'models', schema)
|
|
413
|
+
|
|
414
|
+
def dataclass_schema(self, schema: Any) -> Any:
|
|
415
|
+
return self._class_schema('dataclass_schema', 'dataclasses', schema)
|
|
416
|
+
|
|
417
|
+
def enum_schema(self, schema: Any) -> Any:
|
|
418
|
+
return self._class_schema('enum_schema', None, schema)
|
|
419
|
+
|
|
420
|
+
def generate_inner(self, schema: Any) -> Any:
|
|
421
|
+
target = _scalar_target(schema)
|
|
422
|
+
if target is not None:
|
|
423
|
+
override = self._lookup(target)
|
|
424
|
+
if override is not None:
|
|
425
|
+
return override
|
|
426
|
+
parent: Any = super()
|
|
427
|
+
return parent.generate_inner(schema)
|
|
428
|
+
|
|
429
|
+
return _Generator
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _v1_fields(model: Any) -> Mapping[str, Any]:
|
|
433
|
+
inner = getattr(model, '__pydantic_model__', model)
|
|
434
|
+
return cast('Mapping[str, Any]', getattr(inner, '__fields__', {}))
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _v1_models(target: Any, v1: Any) -> Iterator[Any]:
|
|
438
|
+
"""Model and dataclass classes reachable from ``target``."""
|
|
439
|
+
seen: set[int] = set()
|
|
440
|
+
stack: list[Any] = [target]
|
|
441
|
+
while stack:
|
|
442
|
+
current = stack.pop()
|
|
443
|
+
if id(current) in seen:
|
|
444
|
+
continue
|
|
445
|
+
seen.add(id(current))
|
|
446
|
+
if is_class(current) and (
|
|
447
|
+
issubclass(current, v1.BaseModel) or dataclasses.is_dataclass(current)
|
|
448
|
+
):
|
|
449
|
+
yield current
|
|
450
|
+
for field in _v1_fields(current).values():
|
|
451
|
+
stack.append(field.outer_type_)
|
|
452
|
+
stack.append(field.type_)
|
|
453
|
+
stack.extend(get_args(current))
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _v1_normalize(schema: JsonSchema, *, root: bool) -> JsonSchema:
|
|
457
|
+
"""Bring Pydantic 1 output closer to Pydantic 2 (JSON Schema 2020-12)."""
|
|
458
|
+
result: JsonSchema = {}
|
|
459
|
+
for key, value in schema.items():
|
|
460
|
+
if isinstance(value, dict):
|
|
461
|
+
result[key] = _v1_normalize(cast('JsonSchema', value), root=False)
|
|
462
|
+
elif isinstance(value, list):
|
|
463
|
+
result[key] = [
|
|
464
|
+
(
|
|
465
|
+
_v1_normalize(cast('JsonSchema', item), root=False)
|
|
466
|
+
if isinstance(item, dict)
|
|
467
|
+
else item
|
|
468
|
+
)
|
|
469
|
+
for item in cast('list[object]', value)
|
|
470
|
+
]
|
|
471
|
+
else:
|
|
472
|
+
result[key] = value
|
|
473
|
+
enum_values = result.get('enum')
|
|
474
|
+
if isinstance(enum_values, list):
|
|
475
|
+
values = cast('list[object]', enum_values)
|
|
476
|
+
if root:
|
|
477
|
+
if result.get('description') == 'An enumeration.':
|
|
478
|
+
del result['description']
|
|
479
|
+
if (
|
|
480
|
+
'type' not in result
|
|
481
|
+
and values
|
|
482
|
+
and all(isinstance(v, str) for v in values)
|
|
483
|
+
):
|
|
484
|
+
result['type'] = 'string'
|
|
485
|
+
elif (
|
|
486
|
+
'type' not in result
|
|
487
|
+
and values
|
|
488
|
+
and all(isinstance(v, int) for v in values)
|
|
489
|
+
):
|
|
490
|
+
result['type'] = 'integer'
|
|
491
|
+
elif len(values) == 1:
|
|
492
|
+
result['const'] = values[0]
|
|
493
|
+
del result['enum']
|
|
494
|
+
return result
|