otterapi 0.0.5__py3-none-any.whl → 0.0.6__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.
- README.md +581 -8
- otterapi/__init__.py +73 -0
- otterapi/cli.py +327 -29
- otterapi/codegen/__init__.py +115 -0
- otterapi/codegen/ast_utils.py +134 -5
- otterapi/codegen/client.py +1271 -0
- otterapi/codegen/codegen.py +1736 -0
- otterapi/codegen/dataframes.py +392 -0
- otterapi/codegen/emitter.py +473 -0
- otterapi/codegen/endpoints.py +2597 -343
- otterapi/codegen/pagination.py +1026 -0
- otterapi/codegen/schema.py +593 -0
- otterapi/codegen/splitting.py +1397 -0
- otterapi/codegen/types.py +1345 -0
- otterapi/codegen/utils.py +180 -1
- otterapi/config.py +1017 -24
- otterapi/exceptions.py +231 -0
- otterapi/openapi/__init__.py +46 -0
- otterapi/openapi/v2/__init__.py +86 -0
- otterapi/openapi/v2/spec.json +1607 -0
- otterapi/openapi/v2/v2.py +1776 -0
- otterapi/openapi/v3/__init__.py +131 -0
- otterapi/openapi/v3/spec.json +1651 -0
- otterapi/openapi/v3/v3.py +1557 -0
- otterapi/openapi/v3_1/__init__.py +133 -0
- otterapi/openapi/v3_1/spec.json +1411 -0
- otterapi/openapi/v3_1/v3_1.py +798 -0
- otterapi/openapi/v3_2/__init__.py +133 -0
- otterapi/openapi/v3_2/spec.json +1666 -0
- otterapi/openapi/v3_2/v3_2.py +777 -0
- otterapi/tests/__init__.py +3 -0
- otterapi/tests/fixtures/__init__.py +455 -0
- otterapi/tests/test_ast_utils.py +680 -0
- otterapi/tests/test_codegen.py +610 -0
- otterapi/tests/test_dataframe.py +1038 -0
- otterapi/tests/test_exceptions.py +493 -0
- otterapi/tests/test_openapi_support.py +616 -0
- otterapi/tests/test_openapi_upgrade.py +215 -0
- otterapi/tests/test_pagination.py +1101 -0
- otterapi/tests/test_splitting_config.py +319 -0
- otterapi/tests/test_splitting_integration.py +427 -0
- otterapi/tests/test_splitting_resolver.py +512 -0
- otterapi/tests/test_splitting_tree.py +525 -0
- otterapi-0.0.6.dist-info/METADATA +627 -0
- otterapi-0.0.6.dist-info/RECORD +48 -0
- {otterapi-0.0.5.dist-info → otterapi-0.0.6.dist-info}/WHEEL +1 -1
- otterapi/codegen/generator.py +0 -358
- otterapi/codegen/openapi_processor.py +0 -27
- otterapi/codegen/type_generator.py +0 -559
- otterapi-0.0.5.dist-info/METADATA +0 -54
- otterapi-0.0.5.dist-info/RECORD +0 -16
- {otterapi-0.0.5.dist-info → otterapi-0.0.6.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,1557 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Annotated, Any
|
|
5
|
+
|
|
6
|
+
from pydantic import (
|
|
7
|
+
AnyUrl,
|
|
8
|
+
BaseModel,
|
|
9
|
+
ConfigDict,
|
|
10
|
+
EmailStr,
|
|
11
|
+
Field,
|
|
12
|
+
PositiveFloat,
|
|
13
|
+
RootModel,
|
|
14
|
+
StringConstraints,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Import OpenAPI 3.1 models for upgrade functionality
|
|
18
|
+
from otterapi.openapi.v3_1 import v3_1 as openapi_v3_1
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class WarningCollector:
|
|
22
|
+
"""Helper class to collect and deduplicate warnings during upgrade.
|
|
23
|
+
|
|
24
|
+
Instead of generating per-occurrence warnings that can flood output for large APIs,
|
|
25
|
+
this class tracks warning counts and generates summary messages.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
self._counts: dict[str, int] = {}
|
|
30
|
+
self._unique_warnings: list[str] = []
|
|
31
|
+
|
|
32
|
+
def add(self, warning_key: str, count: int = 1) -> None:
|
|
33
|
+
"""Add a warning that should be counted and summarized."""
|
|
34
|
+
self._counts[warning_key] = self._counts.get(warning_key, 0) + count
|
|
35
|
+
|
|
36
|
+
def add_unique(self, warning: str) -> None:
|
|
37
|
+
"""Add a warning that should appear as-is (not deduplicated)."""
|
|
38
|
+
self._unique_warnings.append(warning)
|
|
39
|
+
|
|
40
|
+
def get_warnings(self) -> list[str]:
|
|
41
|
+
"""Get the final list of deduplicated/summarized warnings."""
|
|
42
|
+
result: list[str] = []
|
|
43
|
+
|
|
44
|
+
# Add unique warnings first
|
|
45
|
+
result.extend(self._unique_warnings)
|
|
46
|
+
|
|
47
|
+
# Add summarized warnings
|
|
48
|
+
warning_templates = {
|
|
49
|
+
'nullable_to_type_array': 'Converting nullable field to type array for schema',
|
|
50
|
+
'nullable_without_type': 'Schema has nullable=true without type, converting to type: [null]',
|
|
51
|
+
'exclusive_maximum': 'Converting exclusiveMaximum from boolean to numeric',
|
|
52
|
+
'exclusive_minimum': 'Converting exclusiveMinimum from boolean to numeric',
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for key, count in self._counts.items():
|
|
56
|
+
if key in warning_templates:
|
|
57
|
+
msg = warning_templates[key]
|
|
58
|
+
if count == 1:
|
|
59
|
+
result.append(msg)
|
|
60
|
+
else:
|
|
61
|
+
result.append(f'{msg} ({count} occurrences)')
|
|
62
|
+
else:
|
|
63
|
+
# Generic key not in templates
|
|
64
|
+
if count == 1:
|
|
65
|
+
result.append(key)
|
|
66
|
+
else:
|
|
67
|
+
result.append(f'{key} ({count} occurrences)')
|
|
68
|
+
|
|
69
|
+
return result
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Reference(
|
|
73
|
+
RootModel[dict[Annotated[str, StringConstraints(pattern=r'^\$ref$')], str]]
|
|
74
|
+
):
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Contact(BaseModel):
|
|
79
|
+
model_config = ConfigDict(extra='forbid')
|
|
80
|
+
|
|
81
|
+
name: str | None = None
|
|
82
|
+
url: str | None = None
|
|
83
|
+
email: EmailStr | None = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class License(BaseModel):
|
|
87
|
+
model_config = ConfigDict(extra='forbid')
|
|
88
|
+
|
|
89
|
+
name: str
|
|
90
|
+
url: str | None = None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ServerVariable(BaseModel):
|
|
94
|
+
model_config = ConfigDict(extra='forbid')
|
|
95
|
+
|
|
96
|
+
enum: list[str] | None = None
|
|
97
|
+
default: str
|
|
98
|
+
description: str | None = None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Type(Enum):
|
|
102
|
+
array = 'array'
|
|
103
|
+
boolean = 'boolean'
|
|
104
|
+
integer = 'integer'
|
|
105
|
+
number = 'number'
|
|
106
|
+
object = 'object'
|
|
107
|
+
string = 'string'
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class Discriminator(BaseModel):
|
|
111
|
+
propertyName: str
|
|
112
|
+
mapping: dict[str, str] | None = None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class XML(BaseModel):
|
|
116
|
+
model_config = ConfigDict(extra='forbid')
|
|
117
|
+
|
|
118
|
+
name: str | None = None
|
|
119
|
+
namespace: AnyUrl | None = None
|
|
120
|
+
prefix: str | None = None
|
|
121
|
+
attribute: bool | None = False
|
|
122
|
+
wrapped: bool | None = False
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class Example(BaseModel):
|
|
126
|
+
model_config = ConfigDict(extra='forbid')
|
|
127
|
+
|
|
128
|
+
summary: str | None = None
|
|
129
|
+
description: str | None = None
|
|
130
|
+
value: Any | None = None
|
|
131
|
+
externalValue: str | None = None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class Style(Enum):
|
|
135
|
+
simple = 'simple'
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class SecurityRequirement(RootModel[dict[str, list[str]]]):
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class ExternalDocumentation(BaseModel):
|
|
143
|
+
model_config = ConfigDict(extra='forbid')
|
|
144
|
+
|
|
145
|
+
description: str | None = None
|
|
146
|
+
url: str
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class ExampleXORExamples(RootModel[Any]):
|
|
150
|
+
root: Any = Field(..., description='Example and examples are mutually exclusive')
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class SchemaXORContent1(BaseModel):
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class SchemaXORContent(RootModel[Any | SchemaXORContent1]):
|
|
158
|
+
root: Any | SchemaXORContent1 = Field(
|
|
159
|
+
...,
|
|
160
|
+
description='Schema and content are mutually exclusive, at least one is required',
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class In(Enum):
|
|
165
|
+
path = 'path'
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class Style1(Enum):
|
|
169
|
+
matrix = 'matrix'
|
|
170
|
+
label = 'label'
|
|
171
|
+
simple = 'simple'
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class Required(Enum):
|
|
175
|
+
bool_True = True
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class PathParameter(BaseModel):
|
|
179
|
+
in_: In | None = Field(None, alias='in')
|
|
180
|
+
style: Style1 | None = 'simple'
|
|
181
|
+
required: Required
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class In1(Enum):
|
|
185
|
+
query = 'query'
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class Style2(Enum):
|
|
189
|
+
form = 'form'
|
|
190
|
+
spaceDelimited = 'spaceDelimited'
|
|
191
|
+
pipeDelimited = 'pipeDelimited'
|
|
192
|
+
deepObject = 'deepObject'
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class QueryParameter(BaseModel):
|
|
196
|
+
in_: In1 | None = Field(None, alias='in')
|
|
197
|
+
style: Style2 | None = 'form'
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class In2(Enum):
|
|
201
|
+
header = 'header'
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class Style3(Enum):
|
|
205
|
+
simple = 'simple'
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class HeaderParameter(BaseModel):
|
|
209
|
+
in_: In2 | None = Field(None, alias='in')
|
|
210
|
+
style: Style3 | None = 'simple'
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class In3(Enum):
|
|
214
|
+
cookie = 'cookie'
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class Style4(Enum):
|
|
218
|
+
form = 'form'
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class CookieParameter(BaseModel):
|
|
222
|
+
in_: In3 | None = Field(None, alias='in')
|
|
223
|
+
style: Style4 | None = 'form'
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class Type1(Enum):
|
|
227
|
+
apiKey = 'apiKey'
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class In4(Enum):
|
|
231
|
+
header = 'header'
|
|
232
|
+
query = 'query'
|
|
233
|
+
cookie = 'cookie'
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class APIKeySecurityScheme(BaseModel):
|
|
237
|
+
model_config = ConfigDict(extra='forbid')
|
|
238
|
+
|
|
239
|
+
type: Type1
|
|
240
|
+
name: str
|
|
241
|
+
in_: In4 = Field(..., alias='in')
|
|
242
|
+
description: str | None = None
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class Type2(Enum):
|
|
246
|
+
http = 'http'
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class HTTPSecurityScheme1(BaseModel):
|
|
250
|
+
model_config = ConfigDict(extra='forbid')
|
|
251
|
+
|
|
252
|
+
scheme: Annotated[str, StringConstraints(pattern=r'^[Bb][Ee][Aa][Rr][Ee][Rr]$')]
|
|
253
|
+
bearerFormat: str | None = None
|
|
254
|
+
description: str | None = None
|
|
255
|
+
type: Type2
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class HTTPSecurityScheme2(BaseModel):
|
|
259
|
+
model_config = ConfigDict(extra='forbid')
|
|
260
|
+
|
|
261
|
+
scheme: str
|
|
262
|
+
bearerFormat: str | None = None
|
|
263
|
+
description: str | None = None
|
|
264
|
+
type: Type2
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class HTTPSecurityScheme(RootModel[HTTPSecurityScheme1 | HTTPSecurityScheme2]):
|
|
268
|
+
pass
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class Type4(Enum):
|
|
272
|
+
oauth2 = 'oauth2'
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class Type5(Enum):
|
|
276
|
+
openIdConnect = 'openIdConnect'
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class OpenIdConnectSecurityScheme(BaseModel):
|
|
280
|
+
model_config = ConfigDict(extra='forbid')
|
|
281
|
+
|
|
282
|
+
type: Type5
|
|
283
|
+
openIdConnectUrl: str
|
|
284
|
+
description: str | None = None
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class ImplicitOAuthFlow(BaseModel):
|
|
288
|
+
model_config = ConfigDict(extra='forbid')
|
|
289
|
+
|
|
290
|
+
authorizationUrl: str
|
|
291
|
+
refreshUrl: str | None = None
|
|
292
|
+
scopes: dict[str, str]
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class PasswordOAuthFlow(BaseModel):
|
|
296
|
+
model_config = ConfigDict(extra='forbid')
|
|
297
|
+
|
|
298
|
+
tokenUrl: str
|
|
299
|
+
refreshUrl: str | None = None
|
|
300
|
+
scopes: dict[str, str]
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class ClientCredentialsFlow(BaseModel):
|
|
304
|
+
model_config = ConfigDict(extra='forbid')
|
|
305
|
+
|
|
306
|
+
tokenUrl: str
|
|
307
|
+
refreshUrl: str | None = None
|
|
308
|
+
scopes: dict[str, str]
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class AuthorizationCodeOAuthFlow(BaseModel):
|
|
312
|
+
model_config = ConfigDict(extra='forbid')
|
|
313
|
+
|
|
314
|
+
authorizationUrl: str
|
|
315
|
+
tokenUrl: str
|
|
316
|
+
refreshUrl: str | None = None
|
|
317
|
+
scopes: dict[str, str]
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class Callback(RootModel[dict[Annotated[str, StringConstraints(pattern=r'^x-')], Any]]):
|
|
321
|
+
pass
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class Style5(Enum):
|
|
325
|
+
form = 'form'
|
|
326
|
+
spaceDelimited = 'spaceDelimited'
|
|
327
|
+
pipeDelimited = 'pipeDelimited'
|
|
328
|
+
deepObject = 'deepObject'
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class Info(BaseModel):
|
|
332
|
+
model_config = ConfigDict(extra='forbid')
|
|
333
|
+
|
|
334
|
+
title: str
|
|
335
|
+
description: str | None = None
|
|
336
|
+
termsOfService: str | None = None
|
|
337
|
+
contact: Contact | None = None
|
|
338
|
+
license: License | None = None
|
|
339
|
+
version: str
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class Server(BaseModel):
|
|
343
|
+
model_config = ConfigDict(extra='forbid')
|
|
344
|
+
|
|
345
|
+
url: str
|
|
346
|
+
description: str | None = None
|
|
347
|
+
variables: dict[str, ServerVariable] | None = None
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class Schema(BaseModel):
|
|
351
|
+
model_config = ConfigDict(extra='forbid')
|
|
352
|
+
|
|
353
|
+
title: str | None = None
|
|
354
|
+
multipleOf: PositiveFloat | None = None
|
|
355
|
+
maximum: float | None = None
|
|
356
|
+
exclusiveMaximum: bool | None = False
|
|
357
|
+
minimum: float | None = None
|
|
358
|
+
exclusiveMinimum: bool | None = False
|
|
359
|
+
maxLength: Annotated[int, Field(ge=0)] | None = None
|
|
360
|
+
minLength: Annotated[int, Field(ge=0)] | None = 0
|
|
361
|
+
pattern: str | None = None
|
|
362
|
+
maxItems: Annotated[int, Field(ge=0)] | None = None
|
|
363
|
+
minItems: Annotated[int, Field(ge=0)] | None = 0
|
|
364
|
+
uniqueItems: bool | None = False
|
|
365
|
+
maxProperties: Annotated[int, Field(ge=0)] | None = None
|
|
366
|
+
minProperties: Annotated[int, Field(ge=0)] | None = 0
|
|
367
|
+
required: list[str] | None = Field(None, min_length=1)
|
|
368
|
+
enum: list | None = Field(None, min_length=1)
|
|
369
|
+
type: Type | None = None
|
|
370
|
+
not_: Schema | Reference | None = Field(None, alias='not')
|
|
371
|
+
allOf: list[Schema | Reference] | None = None
|
|
372
|
+
oneOf: list[Schema | Reference] | None = None
|
|
373
|
+
anyOf: list[Schema | Reference] | None = None
|
|
374
|
+
items: Schema | Reference | None = None
|
|
375
|
+
properties: dict[str, Schema | Reference] | None = None
|
|
376
|
+
additionalProperties: Schema | Reference | bool | None = True
|
|
377
|
+
description: str | None = None
|
|
378
|
+
format: str | None = None
|
|
379
|
+
default: Any | None = None
|
|
380
|
+
nullable: bool | None = False
|
|
381
|
+
discriminator: Discriminator | None = None
|
|
382
|
+
readOnly: bool | None = False
|
|
383
|
+
writeOnly: bool | None = False
|
|
384
|
+
example: Any | None = None
|
|
385
|
+
externalDocs: ExternalDocumentation | None = None
|
|
386
|
+
deprecated: bool | None = False
|
|
387
|
+
xml: XML | None = None
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class Tag(BaseModel):
|
|
391
|
+
model_config = ConfigDict(extra='forbid')
|
|
392
|
+
|
|
393
|
+
name: str
|
|
394
|
+
description: str | None = None
|
|
395
|
+
externalDocs: ExternalDocumentation | None = None
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
class OAuthFlows(BaseModel):
|
|
399
|
+
model_config = ConfigDict(extra='forbid')
|
|
400
|
+
|
|
401
|
+
implicit: ImplicitOAuthFlow | None = None
|
|
402
|
+
password: PasswordOAuthFlow | None = None
|
|
403
|
+
clientCredentials: ClientCredentialsFlow | None = None
|
|
404
|
+
authorizationCode: AuthorizationCodeOAuthFlow | None = None
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
class Link(BaseModel):
|
|
408
|
+
model_config = ConfigDict(extra='forbid')
|
|
409
|
+
|
|
410
|
+
operationId: str | None = None
|
|
411
|
+
operationRef: str | None = None
|
|
412
|
+
parameters: dict[str, Any] | None = None
|
|
413
|
+
requestBody: Any | None = None
|
|
414
|
+
description: str | None = None
|
|
415
|
+
server: Server | None = None
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
class OAuth2SecurityScheme(BaseModel):
|
|
419
|
+
model_config = ConfigDict(extra='forbid')
|
|
420
|
+
|
|
421
|
+
type: Type4
|
|
422
|
+
flows: OAuthFlows
|
|
423
|
+
description: str | None = None
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
class SecurityScheme(
|
|
427
|
+
RootModel[
|
|
428
|
+
APIKeySecurityScheme
|
|
429
|
+
| HTTPSecurityScheme
|
|
430
|
+
| OAuth2SecurityScheme
|
|
431
|
+
| OpenIdConnectSecurityScheme
|
|
432
|
+
]
|
|
433
|
+
):
|
|
434
|
+
pass
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
class OpenAPI(BaseModel):
|
|
438
|
+
model_config = ConfigDict(extra='forbid')
|
|
439
|
+
|
|
440
|
+
openapi: Annotated[str, StringConstraints(pattern=r'^3\.0\.\d(-.+)?$')]
|
|
441
|
+
info: Info
|
|
442
|
+
externalDocs: ExternalDocumentation | None = None
|
|
443
|
+
servers: list[Server] | None = None
|
|
444
|
+
security: list[SecurityRequirement] | None = None
|
|
445
|
+
tags: list[Tag] | None = None
|
|
446
|
+
paths: Paths
|
|
447
|
+
components: Components | None = None
|
|
448
|
+
|
|
449
|
+
def upgrade(self) -> tuple[openapi_v3_1.OpenAPI, list[str]]:
|
|
450
|
+
"""
|
|
451
|
+
Upgrade this OpenAPI 3.0 specification to OpenAPI 3.1.
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
A tuple of (OpenAPI 3.1 model, list of warnings)
|
|
455
|
+
|
|
456
|
+
Key changes in 3.1:
|
|
457
|
+
- nullable property removed, use type arrays instead
|
|
458
|
+
- exclusiveMaximum/exclusiveMinimum changed from boolean to numeric
|
|
459
|
+
- New jsonSchemaDialect field
|
|
460
|
+
- Version updated to 3.1.x
|
|
461
|
+
|
|
462
|
+
Warnings are deduplicated and summarized to avoid overwhelming output
|
|
463
|
+
for large APIs.
|
|
464
|
+
"""
|
|
465
|
+
warning_collector = WarningCollector()
|
|
466
|
+
|
|
467
|
+
# Convert basic metadata
|
|
468
|
+
info = self._convert_info_to_3_1()
|
|
469
|
+
|
|
470
|
+
# Convert components
|
|
471
|
+
components = (
|
|
472
|
+
self._convert_components_to_3_1(warning_collector)
|
|
473
|
+
if self.components
|
|
474
|
+
else None
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
# Convert paths
|
|
478
|
+
paths = self._convert_paths_to_3_1(warning_collector) if self.paths else None
|
|
479
|
+
|
|
480
|
+
# Convert servers
|
|
481
|
+
servers = (
|
|
482
|
+
[self._convert_server_to_3_1(s) for s in self.servers]
|
|
483
|
+
if self.servers
|
|
484
|
+
else None
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
# Convert security requirements
|
|
488
|
+
security = None
|
|
489
|
+
if self.security:
|
|
490
|
+
security = [
|
|
491
|
+
openapi_v3_1.SecurityRequirement(
|
|
492
|
+
root={k: v for k, v in req.root.items()}
|
|
493
|
+
)
|
|
494
|
+
for req in self.security
|
|
495
|
+
]
|
|
496
|
+
|
|
497
|
+
# Convert tags
|
|
498
|
+
tags = None
|
|
499
|
+
if self.tags:
|
|
500
|
+
tags = [
|
|
501
|
+
openapi_v3_1.Tag(
|
|
502
|
+
name=tag.name,
|
|
503
|
+
description=tag.description,
|
|
504
|
+
externalDocs=self._convert_external_docs_to_3_1(tag.externalDocs)
|
|
505
|
+
if tag.externalDocs
|
|
506
|
+
else None,
|
|
507
|
+
)
|
|
508
|
+
for tag in self.tags
|
|
509
|
+
]
|
|
510
|
+
|
|
511
|
+
# Convert external docs
|
|
512
|
+
external_docs = (
|
|
513
|
+
self._convert_external_docs_to_3_1(self.externalDocs)
|
|
514
|
+
if self.externalDocs
|
|
515
|
+
else None
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
# Build OpenAPI 3.1 object
|
|
519
|
+
openapi_3_1 = openapi_v3_1.OpenAPI(
|
|
520
|
+
openapi='3.1.0',
|
|
521
|
+
info=info,
|
|
522
|
+
jsonSchemaDialect='https://spec.openapis.org/oas/3.1/dialect/base',
|
|
523
|
+
servers=servers,
|
|
524
|
+
paths=paths,
|
|
525
|
+
components=components,
|
|
526
|
+
security=security,
|
|
527
|
+
tags=tags,
|
|
528
|
+
externalDocs=external_docs,
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
return openapi_3_1, warning_collector.get_warnings()
|
|
532
|
+
|
|
533
|
+
def _convert_info_to_3_1(self) -> openapi_v3_1.Info:
|
|
534
|
+
"""Convert Info object from OpenAPI 3.0 to 3.1."""
|
|
535
|
+
contact = None
|
|
536
|
+
if self.info.contact:
|
|
537
|
+
contact = openapi_v3_1.Contact(
|
|
538
|
+
name=self.info.contact.name,
|
|
539
|
+
url=self.info.contact.url,
|
|
540
|
+
email=self.info.contact.email,
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
license_obj = None
|
|
544
|
+
if self.info.license:
|
|
545
|
+
license_obj = openapi_v3_1.License(
|
|
546
|
+
name=self.info.license.name, url=self.info.license.url
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
return openapi_v3_1.Info(
|
|
550
|
+
title=self.info.title,
|
|
551
|
+
version=self.info.version,
|
|
552
|
+
summary=None, # New in 3.1, not present in 3.0
|
|
553
|
+
description=self.info.description,
|
|
554
|
+
termsOfService=self.info.termsOfService,
|
|
555
|
+
contact=contact,
|
|
556
|
+
license=license_obj,
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
def _convert_server_to_3_1(self, server: Server) -> openapi_v3_1.Server:
|
|
560
|
+
"""Convert Server object from OpenAPI 3.0 to 3.1."""
|
|
561
|
+
variables = None
|
|
562
|
+
if server.variables:
|
|
563
|
+
variables = {
|
|
564
|
+
name: openapi_v3_1.ServerVariable(
|
|
565
|
+
enum=var.enum, default=var.default, description=var.description
|
|
566
|
+
)
|
|
567
|
+
for name, var in server.variables.items()
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return openapi_v3_1.Server(
|
|
571
|
+
url=server.url, description=server.description, variables=variables
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
def _convert_external_docs_to_3_1(
|
|
575
|
+
self, docs: ExternalDocumentation
|
|
576
|
+
) -> openapi_v3_1.ExternalDocumentation:
|
|
577
|
+
"""Convert ExternalDocumentation from OpenAPI 3.0 to 3.1."""
|
|
578
|
+
return openapi_v3_1.ExternalDocumentation(
|
|
579
|
+
url=docs.url, description=docs.description
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
def _convert_components_to_3_1(
|
|
583
|
+
self, warnings: WarningCollector
|
|
584
|
+
) -> openapi_v3_1.Components:
|
|
585
|
+
"""Convert Components object from OpenAPI 3.0 to 3.1."""
|
|
586
|
+
if not self.components:
|
|
587
|
+
return None
|
|
588
|
+
|
|
589
|
+
# Convert schemas
|
|
590
|
+
schemas = None
|
|
591
|
+
if self.components.schemas:
|
|
592
|
+
schemas = {}
|
|
593
|
+
for name, schema in self.components.schemas.items():
|
|
594
|
+
schemas[name] = self._convert_schema_or_ref_to_3_1(schema, warnings)
|
|
595
|
+
|
|
596
|
+
# Convert responses
|
|
597
|
+
responses = None
|
|
598
|
+
if self.components.responses:
|
|
599
|
+
responses = {}
|
|
600
|
+
for name, response in self.components.responses.items():
|
|
601
|
+
responses[name] = self._convert_response_or_ref_to_3_1(
|
|
602
|
+
response, warnings
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
# Convert parameters
|
|
606
|
+
parameters = None
|
|
607
|
+
if self.components.parameters:
|
|
608
|
+
parameters = {}
|
|
609
|
+
for name, param in self.components.parameters.items():
|
|
610
|
+
parameters[name] = self._convert_parameter_or_ref_to_3_1(
|
|
611
|
+
param, warnings
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
# Convert examples
|
|
615
|
+
examples = None
|
|
616
|
+
if self.components.examples:
|
|
617
|
+
examples = {}
|
|
618
|
+
for name, example in self.components.examples.items():
|
|
619
|
+
examples[name] = self._convert_example_or_ref_to_3_1(example)
|
|
620
|
+
|
|
621
|
+
# Convert request bodies
|
|
622
|
+
request_bodies = None
|
|
623
|
+
if self.components.requestBodies:
|
|
624
|
+
request_bodies = {}
|
|
625
|
+
for name, body in self.components.requestBodies.items():
|
|
626
|
+
request_bodies[name] = self._convert_request_body_or_ref_to_3_1(
|
|
627
|
+
body, warnings
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
# Convert headers
|
|
631
|
+
headers = None
|
|
632
|
+
if self.components.headers:
|
|
633
|
+
headers = {}
|
|
634
|
+
for name, header in self.components.headers.items():
|
|
635
|
+
headers[name] = self._convert_header_or_ref_to_3_1(header, warnings)
|
|
636
|
+
|
|
637
|
+
# Convert security schemes
|
|
638
|
+
security_schemes = None
|
|
639
|
+
if self.components.securitySchemes:
|
|
640
|
+
security_schemes = {}
|
|
641
|
+
for name, scheme in self.components.securitySchemes.items():
|
|
642
|
+
security_schemes[name] = self._convert_security_scheme_or_ref_to_3_1(
|
|
643
|
+
scheme
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
# Convert links
|
|
647
|
+
links = None
|
|
648
|
+
if self.components.links:
|
|
649
|
+
links = {}
|
|
650
|
+
for name, link in self.components.links.items():
|
|
651
|
+
links[name] = self._convert_link_or_ref_to_3_1(link)
|
|
652
|
+
|
|
653
|
+
# Convert callbacks
|
|
654
|
+
callbacks = None
|
|
655
|
+
if self.components.callbacks:
|
|
656
|
+
callbacks = {}
|
|
657
|
+
for name, callback in self.components.callbacks.items():
|
|
658
|
+
callbacks[name] = self._convert_callback_or_ref_to_3_1(
|
|
659
|
+
callback, warnings
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
return openapi_v3_1.Components(
|
|
663
|
+
schemas=schemas,
|
|
664
|
+
responses=responses,
|
|
665
|
+
parameters=parameters,
|
|
666
|
+
examples=examples,
|
|
667
|
+
requestBodies=request_bodies,
|
|
668
|
+
headers=headers,
|
|
669
|
+
securitySchemes=security_schemes,
|
|
670
|
+
links=links,
|
|
671
|
+
callbacks=callbacks,
|
|
672
|
+
pathItems=None, # New in 3.1, not in 3.0
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
def _convert_schema_or_ref_to_3_1(
|
|
676
|
+
self, schema_or_ref: Schema | Reference, warnings: WarningCollector
|
|
677
|
+
) -> openapi_v3_1.Schema | openapi_v3_1.Reference:
|
|
678
|
+
"""Convert a Schema or Reference from OpenAPI 3.0 to 3.1."""
|
|
679
|
+
if isinstance(schema_or_ref, Reference):
|
|
680
|
+
return self._convert_reference_to_3_1(schema_or_ref)
|
|
681
|
+
return self._convert_schema_to_3_1(schema_or_ref, warnings)
|
|
682
|
+
|
|
683
|
+
def _convert_reference_to_3_1(self, ref: Reference) -> openapi_v3_1.Reference:
|
|
684
|
+
"""Convert a Reference from OpenAPI 3.0 to 3.1."""
|
|
685
|
+
# Extract the $ref value from the RootModel
|
|
686
|
+
ref_value = ref.root.get('$ref', '')
|
|
687
|
+
return openapi_v3_1.Reference(
|
|
688
|
+
**{'$ref': ref_value, 'summary': None, 'description': None}
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
def _convert_schema_to_3_1(
|
|
692
|
+
self, schema: Schema, warnings: WarningCollector
|
|
693
|
+
) -> openapi_v3_1.Schema:
|
|
694
|
+
"""Convert a Schema from OpenAPI 3.0 to 3.1."""
|
|
695
|
+
# Handle nullable conversion
|
|
696
|
+
type_value = None
|
|
697
|
+
if schema.type:
|
|
698
|
+
# Convert v3.Type enum to v3_1.Type enum
|
|
699
|
+
type_3_1 = openapi_v3_1.Type(schema.type.value)
|
|
700
|
+
if schema.nullable:
|
|
701
|
+
# Convert nullable: true to type array
|
|
702
|
+
warnings.add('nullable_to_type_array')
|
|
703
|
+
type_value = [type_3_1, openapi_v3_1.Type.null]
|
|
704
|
+
else:
|
|
705
|
+
type_value = type_3_1
|
|
706
|
+
elif schema.nullable:
|
|
707
|
+
# nullable without type - preserve nullable semantics for composition schemas
|
|
708
|
+
type_value = [openapi_v3_1.Type.null]
|
|
709
|
+
warnings.add('nullable_without_type')
|
|
710
|
+
|
|
711
|
+
# Handle exclusiveMaximum/exclusiveMinimum conversion
|
|
712
|
+
exclusive_maximum = None
|
|
713
|
+
if schema.exclusiveMaximum and schema.maximum is not None:
|
|
714
|
+
# In 3.0, exclusiveMaximum is boolean, in 3.1 it's numeric
|
|
715
|
+
warnings.add('exclusive_maximum')
|
|
716
|
+
exclusive_maximum = schema.maximum
|
|
717
|
+
elif not schema.exclusiveMaximum and schema.maximum is not None:
|
|
718
|
+
exclusive_maximum = None
|
|
719
|
+
|
|
720
|
+
exclusive_minimum = None
|
|
721
|
+
if schema.exclusiveMinimum and schema.minimum is not None:
|
|
722
|
+
warnings.add('exclusive_minimum')
|
|
723
|
+
exclusive_minimum = schema.minimum
|
|
724
|
+
elif not schema.exclusiveMinimum and schema.minimum is not None:
|
|
725
|
+
exclusive_minimum = None
|
|
726
|
+
|
|
727
|
+
# Use maximum/minimum only if not exclusive
|
|
728
|
+
maximum = schema.maximum if not schema.exclusiveMaximum else None
|
|
729
|
+
minimum = schema.minimum if not schema.exclusiveMinimum else None
|
|
730
|
+
|
|
731
|
+
# Convert nested schemas
|
|
732
|
+
not_ = (
|
|
733
|
+
self._convert_schema_or_ref_to_3_1(schema.not_, warnings)
|
|
734
|
+
if schema.not_
|
|
735
|
+
else None
|
|
736
|
+
)
|
|
737
|
+
allOf = (
|
|
738
|
+
[self._convert_schema_or_ref_to_3_1(s, warnings) for s in schema.allOf]
|
|
739
|
+
if schema.allOf
|
|
740
|
+
else None
|
|
741
|
+
)
|
|
742
|
+
oneOf = (
|
|
743
|
+
[self._convert_schema_or_ref_to_3_1(s, warnings) for s in schema.oneOf]
|
|
744
|
+
if schema.oneOf
|
|
745
|
+
else None
|
|
746
|
+
)
|
|
747
|
+
anyOf = (
|
|
748
|
+
[self._convert_schema_or_ref_to_3_1(s, warnings) for s in schema.anyOf]
|
|
749
|
+
if schema.anyOf
|
|
750
|
+
else None
|
|
751
|
+
)
|
|
752
|
+
items = (
|
|
753
|
+
self._convert_schema_or_ref_to_3_1(schema.items, warnings)
|
|
754
|
+
if schema.items
|
|
755
|
+
else None
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
properties = None
|
|
759
|
+
if schema.properties:
|
|
760
|
+
properties = {
|
|
761
|
+
name: self._convert_schema_or_ref_to_3_1(prop, warnings)
|
|
762
|
+
for name, prop in schema.properties.items()
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
additional_properties = None
|
|
766
|
+
if schema.additionalProperties is not None:
|
|
767
|
+
if isinstance(schema.additionalProperties, bool):
|
|
768
|
+
additional_properties = schema.additionalProperties
|
|
769
|
+
else:
|
|
770
|
+
additional_properties = self._convert_schema_or_ref_to_3_1(
|
|
771
|
+
schema.additionalProperties, warnings
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
# Convert discriminator
|
|
775
|
+
discriminator = None
|
|
776
|
+
if schema.discriminator:
|
|
777
|
+
discriminator = openapi_v3_1.Discriminator(
|
|
778
|
+
propertyName=schema.discriminator.propertyName,
|
|
779
|
+
mapping=schema.discriminator.mapping,
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
# Convert XML
|
|
783
|
+
xml = None
|
|
784
|
+
if schema.xml:
|
|
785
|
+
xml = openapi_v3_1.XML(
|
|
786
|
+
name=schema.xml.name,
|
|
787
|
+
namespace=schema.xml.namespace,
|
|
788
|
+
prefix=schema.xml.prefix,
|
|
789
|
+
attribute=schema.xml.attribute,
|
|
790
|
+
wrapped=schema.xml.wrapped,
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
# Convert external docs
|
|
794
|
+
external_docs = (
|
|
795
|
+
self._convert_external_docs_to_3_1(schema.externalDocs)
|
|
796
|
+
if schema.externalDocs
|
|
797
|
+
else None
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
# Build schema dict excluding None values for optional fields
|
|
801
|
+
schema_dict = {
|
|
802
|
+
'title': schema.title,
|
|
803
|
+
'multipleOf': schema.multipleOf,
|
|
804
|
+
'maximum': maximum,
|
|
805
|
+
'exclusiveMaximum': exclusive_maximum,
|
|
806
|
+
'minimum': minimum,
|
|
807
|
+
'exclusiveMinimum': exclusive_minimum,
|
|
808
|
+
'maxLength': schema.maxLength,
|
|
809
|
+
'minLength': schema.minLength,
|
|
810
|
+
'pattern': schema.pattern,
|
|
811
|
+
'maxItems': schema.maxItems,
|
|
812
|
+
'minItems': schema.minItems,
|
|
813
|
+
'uniqueItems': schema.uniqueItems,
|
|
814
|
+
'maxProperties': schema.maxProperties,
|
|
815
|
+
'minProperties': schema.minProperties,
|
|
816
|
+
'required': schema.required,
|
|
817
|
+
'enum': schema.enum,
|
|
818
|
+
'type': type_value,
|
|
819
|
+
'not': not_,
|
|
820
|
+
'allOf': allOf,
|
|
821
|
+
'oneOf': oneOf,
|
|
822
|
+
'anyOf': anyOf,
|
|
823
|
+
'items': items,
|
|
824
|
+
'prefixItems': None,
|
|
825
|
+
'properties': properties,
|
|
826
|
+
'additionalProperties': additional_properties,
|
|
827
|
+
'patternProperties': None,
|
|
828
|
+
'format': schema.format,
|
|
829
|
+
'description': schema.description,
|
|
830
|
+
'default': schema.default,
|
|
831
|
+
'discriminator': discriminator,
|
|
832
|
+
'readOnly': schema.readOnly,
|
|
833
|
+
'writeOnly': schema.writeOnly,
|
|
834
|
+
'example': schema.example,
|
|
835
|
+
'examples': None,
|
|
836
|
+
'externalDocs': external_docs,
|
|
837
|
+
'deprecated': schema.deprecated,
|
|
838
|
+
'xml': xml,
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
# Remove None values to avoid extra_forbid issues
|
|
842
|
+
schema_dict = {k: v for k, v in schema_dict.items() if v is not None}
|
|
843
|
+
|
|
844
|
+
return openapi_v3_1.Schema.model_validate(schema_dict)
|
|
845
|
+
|
|
846
|
+
def _convert_response_or_ref_to_3_1(
|
|
847
|
+
self, response_or_ref: Response | Reference, warnings: WarningCollector
|
|
848
|
+
) -> openapi_v3_1.Response | openapi_v3_1.Reference:
|
|
849
|
+
"""Convert a Response or Reference from OpenAPI 3.0 to 3.1."""
|
|
850
|
+
if isinstance(response_or_ref, Reference):
|
|
851
|
+
return self._convert_reference_to_3_1(response_or_ref)
|
|
852
|
+
return self._convert_response_to_3_1(response_or_ref, warnings)
|
|
853
|
+
|
|
854
|
+
def _convert_response_to_3_1(
|
|
855
|
+
self, response: Response, warnings: WarningCollector
|
|
856
|
+
) -> openapi_v3_1.Response:
|
|
857
|
+
"""Convert a Response from OpenAPI 3.0 to 3.1."""
|
|
858
|
+
headers = None
|
|
859
|
+
if response.headers:
|
|
860
|
+
headers = {}
|
|
861
|
+
for name, header in response.headers.items():
|
|
862
|
+
headers[name] = self._convert_header_or_ref_to_3_1(header, warnings)
|
|
863
|
+
|
|
864
|
+
content = None
|
|
865
|
+
if response.content:
|
|
866
|
+
content = {}
|
|
867
|
+
for media_type, media_type_obj in response.content.items():
|
|
868
|
+
content[media_type] = self._convert_media_type_to_3_1(
|
|
869
|
+
media_type_obj, warnings
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
links = None
|
|
873
|
+
if response.links:
|
|
874
|
+
links = {}
|
|
875
|
+
for name, link in response.links.items():
|
|
876
|
+
links[name] = self._convert_link_or_ref_to_3_1(link)
|
|
877
|
+
|
|
878
|
+
return openapi_v3_1.Response(
|
|
879
|
+
description=response.description,
|
|
880
|
+
headers=headers,
|
|
881
|
+
content=content,
|
|
882
|
+
links=links,
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
def _convert_media_type_to_3_1(
|
|
886
|
+
self, media_type: MediaType, warnings: WarningCollector
|
|
887
|
+
) -> openapi_v3_1.MediaType:
|
|
888
|
+
"""Convert a MediaType from OpenAPI 3.0 to 3.1."""
|
|
889
|
+
schema_ = None
|
|
890
|
+
if media_type.schema_:
|
|
891
|
+
schema_ = self._convert_schema_or_ref_to_3_1(media_type.schema_, warnings)
|
|
892
|
+
|
|
893
|
+
examples = None
|
|
894
|
+
if media_type.examples:
|
|
895
|
+
examples = {}
|
|
896
|
+
for name, example in media_type.examples.items():
|
|
897
|
+
examples[name] = self._convert_example_or_ref_to_3_1(example)
|
|
898
|
+
|
|
899
|
+
encoding = None
|
|
900
|
+
if media_type.encoding:
|
|
901
|
+
encoding = {}
|
|
902
|
+
for name, enc in media_type.encoding.items():
|
|
903
|
+
encoding[name] = self._convert_encoding_to_3_1(enc, warnings)
|
|
904
|
+
|
|
905
|
+
media_type_dict = {
|
|
906
|
+
'schema': schema_,
|
|
907
|
+
'example': media_type.example,
|
|
908
|
+
'examples': examples,
|
|
909
|
+
'encoding': encoding,
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
# Remove None values
|
|
913
|
+
media_type_dict = {k: v for k, v in media_type_dict.items() if v is not None}
|
|
914
|
+
|
|
915
|
+
return openapi_v3_1.MediaType.model_validate(media_type_dict)
|
|
916
|
+
|
|
917
|
+
def _convert_encoding_to_3_1(
|
|
918
|
+
self, encoding: Encoding, warnings: WarningCollector
|
|
919
|
+
) -> openapi_v3_1.Encoding:
|
|
920
|
+
"""Convert an Encoding from OpenAPI 3.0 to 3.1."""
|
|
921
|
+
headers = None
|
|
922
|
+
if encoding.headers:
|
|
923
|
+
headers = {}
|
|
924
|
+
for name, header in encoding.headers.items():
|
|
925
|
+
headers[name] = self._convert_header_or_ref_to_3_1(header, warnings)
|
|
926
|
+
|
|
927
|
+
return openapi_v3_1.Encoding(
|
|
928
|
+
contentType=encoding.contentType,
|
|
929
|
+
headers=headers,
|
|
930
|
+
style=encoding.style,
|
|
931
|
+
explode=encoding.explode,
|
|
932
|
+
allowReserved=encoding.allowReserved,
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
def _convert_parameter_or_ref_to_3_1(
|
|
936
|
+
self, param_or_ref: Parameter | Reference, warnings: WarningCollector
|
|
937
|
+
) -> openapi_v3_1.Parameter | openapi_v3_1.Reference:
|
|
938
|
+
"""Convert a Parameter or Reference from OpenAPI 3.0 to 3.1."""
|
|
939
|
+
if isinstance(param_or_ref, Reference):
|
|
940
|
+
return self._convert_reference_to_3_1(param_or_ref)
|
|
941
|
+
return self._convert_parameter_to_3_1(param_or_ref, warnings)
|
|
942
|
+
|
|
943
|
+
def _convert_parameter_to_3_1(
|
|
944
|
+
self, param: Parameter, warnings: WarningCollector
|
|
945
|
+
) -> openapi_v3_1.Parameter:
|
|
946
|
+
"""Convert a Parameter from OpenAPI 3.0 to 3.1."""
|
|
947
|
+
schema_ = None
|
|
948
|
+
if param.schema_:
|
|
949
|
+
schema_ = self._convert_schema_or_ref_to_3_1(param.schema_, warnings)
|
|
950
|
+
|
|
951
|
+
content = None
|
|
952
|
+
if param.content:
|
|
953
|
+
content = {}
|
|
954
|
+
for media_type, media_type_obj in param.content.items():
|
|
955
|
+
content[media_type] = self._convert_media_type_to_3_1(
|
|
956
|
+
media_type_obj, warnings
|
|
957
|
+
)
|
|
958
|
+
|
|
959
|
+
examples = None
|
|
960
|
+
if param.examples:
|
|
961
|
+
examples = {}
|
|
962
|
+
for name, example in param.examples.items():
|
|
963
|
+
examples[name] = self._convert_example_or_ref_to_3_1(example)
|
|
964
|
+
|
|
965
|
+
param_dict = {
|
|
966
|
+
'name': param.name,
|
|
967
|
+
'in': param.in_,
|
|
968
|
+
'description': param.description,
|
|
969
|
+
'required': param.required,
|
|
970
|
+
'deprecated': param.deprecated,
|
|
971
|
+
'allowEmptyValue': param.allowEmptyValue,
|
|
972
|
+
'style': param.style,
|
|
973
|
+
'explode': param.explode,
|
|
974
|
+
'allowReserved': param.allowReserved,
|
|
975
|
+
'schema': schema_,
|
|
976
|
+
'content': content,
|
|
977
|
+
'example': param.example,
|
|
978
|
+
'examples': examples,
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
# Remove None values
|
|
982
|
+
param_dict = {k: v for k, v in param_dict.items() if v is not None}
|
|
983
|
+
|
|
984
|
+
return openapi_v3_1.Parameter.model_validate(param_dict)
|
|
985
|
+
|
|
986
|
+
def _convert_header_or_ref_to_3_1(
|
|
987
|
+
self, header_or_ref: Header | Reference, warnings: WarningCollector
|
|
988
|
+
) -> openapi_v3_1.Header | openapi_v3_1.Reference:
|
|
989
|
+
"""Convert a Header or Reference from OpenAPI 3.0 to 3.1."""
|
|
990
|
+
if isinstance(header_or_ref, Reference):
|
|
991
|
+
return self._convert_reference_to_3_1(header_or_ref)
|
|
992
|
+
return self._convert_header_to_3_1(header_or_ref, warnings)
|
|
993
|
+
|
|
994
|
+
def _convert_header_to_3_1(
|
|
995
|
+
self, header: Header, warnings: WarningCollector
|
|
996
|
+
) -> openapi_v3_1.Header:
|
|
997
|
+
"""Convert a Header from OpenAPI 3.0 to 3.1."""
|
|
998
|
+
schema_ = None
|
|
999
|
+
if header.schema_:
|
|
1000
|
+
schema_ = self._convert_schema_or_ref_to_3_1(header.schema_, warnings)
|
|
1001
|
+
|
|
1002
|
+
content = None
|
|
1003
|
+
if header.content:
|
|
1004
|
+
content = {}
|
|
1005
|
+
for media_type, media_type_obj in header.content.items():
|
|
1006
|
+
content[media_type] = self._convert_media_type_to_3_1(
|
|
1007
|
+
media_type_obj, warnings
|
|
1008
|
+
)
|
|
1009
|
+
|
|
1010
|
+
examples = None
|
|
1011
|
+
if header.examples:
|
|
1012
|
+
examples = {}
|
|
1013
|
+
for name, example in header.examples.items():
|
|
1014
|
+
examples[name] = self._convert_example_or_ref_to_3_1(example)
|
|
1015
|
+
|
|
1016
|
+
header_dict = {
|
|
1017
|
+
'description': header.description,
|
|
1018
|
+
'required': header.required,
|
|
1019
|
+
'deprecated': header.deprecated,
|
|
1020
|
+
'allowEmptyValue': header.allowEmptyValue,
|
|
1021
|
+
'style': header.style,
|
|
1022
|
+
'explode': header.explode,
|
|
1023
|
+
'allowReserved': header.allowReserved,
|
|
1024
|
+
'schema': schema_,
|
|
1025
|
+
'content': content,
|
|
1026
|
+
'example': header.example,
|
|
1027
|
+
'examples': examples,
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
# Remove None values
|
|
1031
|
+
header_dict = {k: v for k, v in header_dict.items() if v is not None}
|
|
1032
|
+
|
|
1033
|
+
return openapi_v3_1.Header.model_validate(header_dict)
|
|
1034
|
+
|
|
1035
|
+
def _convert_example_or_ref_to_3_1(
|
|
1036
|
+
self, example: Example | Reference
|
|
1037
|
+
) -> openapi_v3_1.Example | openapi_v3_1.Reference:
|
|
1038
|
+
"""Convert an Example or Reference from OpenAPI 3.0 to 3.1."""
|
|
1039
|
+
if isinstance(example, Reference):
|
|
1040
|
+
return self._convert_reference_to_3_1(example)
|
|
1041
|
+
return openapi_v3_1.Example(
|
|
1042
|
+
summary=example.summary,
|
|
1043
|
+
description=example.description,
|
|
1044
|
+
value=example.value,
|
|
1045
|
+
externalValue=example.externalValue,
|
|
1046
|
+
)
|
|
1047
|
+
|
|
1048
|
+
def _convert_request_body_or_ref_to_3_1(
|
|
1049
|
+
self, body_or_ref: RequestBody | Reference, warnings: WarningCollector
|
|
1050
|
+
) -> openapi_v3_1.RequestBody | openapi_v3_1.Reference:
|
|
1051
|
+
"""Convert a RequestBody or Reference from OpenAPI 3.0 to 3.1."""
|
|
1052
|
+
if isinstance(body_or_ref, Reference):
|
|
1053
|
+
return self._convert_reference_to_3_1(body_or_ref)
|
|
1054
|
+
|
|
1055
|
+
content = {}
|
|
1056
|
+
for media_type, media_type_obj in body_or_ref.content.items():
|
|
1057
|
+
content[media_type] = self._convert_media_type_to_3_1(
|
|
1058
|
+
media_type_obj, warnings
|
|
1059
|
+
)
|
|
1060
|
+
|
|
1061
|
+
return openapi_v3_1.RequestBody(
|
|
1062
|
+
description=body_or_ref.description,
|
|
1063
|
+
content=content,
|
|
1064
|
+
required=body_or_ref.required,
|
|
1065
|
+
)
|
|
1066
|
+
|
|
1067
|
+
def _convert_link_or_ref_to_3_1(
|
|
1068
|
+
self, link: Link | Reference
|
|
1069
|
+
) -> openapi_v3_1.Link | openapi_v3_1.Reference:
|
|
1070
|
+
"""Convert a Link or Reference from OpenAPI 3.0 to 3.1."""
|
|
1071
|
+
if isinstance(link, Reference):
|
|
1072
|
+
return self._convert_reference_to_3_1(link)
|
|
1073
|
+
|
|
1074
|
+
server = self._convert_server_to_3_1(link.server) if link.server else None
|
|
1075
|
+
|
|
1076
|
+
return openapi_v3_1.Link(
|
|
1077
|
+
operationId=link.operationId,
|
|
1078
|
+
operationRef=link.operationRef,
|
|
1079
|
+
parameters=link.parameters,
|
|
1080
|
+
requestBody=link.requestBody,
|
|
1081
|
+
description=link.description,
|
|
1082
|
+
server=server,
|
|
1083
|
+
)
|
|
1084
|
+
|
|
1085
|
+
def _convert_security_scheme_or_ref_to_3_1(
|
|
1086
|
+
self, scheme: SecurityScheme | Reference
|
|
1087
|
+
) -> openapi_v3_1.SecurityScheme | openapi_v3_1.Reference:
|
|
1088
|
+
"""Convert a SecurityScheme or Reference from OpenAPI 3.0 to 3.1."""
|
|
1089
|
+
if isinstance(scheme, Reference):
|
|
1090
|
+
return self._convert_reference_to_3_1(scheme)
|
|
1091
|
+
|
|
1092
|
+
# SecurityScheme is a RootModel with Union, need to access root
|
|
1093
|
+
sec_scheme = scheme.root
|
|
1094
|
+
|
|
1095
|
+
if isinstance(sec_scheme, APIKeySecurityScheme):
|
|
1096
|
+
inner = openapi_v3_1.APIKeySecurityScheme.model_validate(
|
|
1097
|
+
{
|
|
1098
|
+
'type': openapi_v3_1.Type1.apiKey,
|
|
1099
|
+
'name': sec_scheme.name,
|
|
1100
|
+
'in': sec_scheme.in_.value,
|
|
1101
|
+
'description': sec_scheme.description,
|
|
1102
|
+
}
|
|
1103
|
+
)
|
|
1104
|
+
return openapi_v3_1.SecurityScheme(root=inner)
|
|
1105
|
+
elif isinstance(sec_scheme, HTTPSecurityScheme):
|
|
1106
|
+
http_scheme = sec_scheme.root
|
|
1107
|
+
# Determine if it's Bearer or non-Bearer based on scheme pattern
|
|
1108
|
+
scheme_value = http_scheme.scheme
|
|
1109
|
+
if scheme_value.lower() == 'bearer':
|
|
1110
|
+
inner_http = openapi_v3_1.HTTPSecurityScheme1(
|
|
1111
|
+
type=openapi_v3_1.Type2.http,
|
|
1112
|
+
scheme=scheme_value,
|
|
1113
|
+
bearerFormat=http_scheme.bearerFormat,
|
|
1114
|
+
description=http_scheme.description,
|
|
1115
|
+
)
|
|
1116
|
+
else:
|
|
1117
|
+
inner_http = openapi_v3_1.HTTPSecurityScheme2(
|
|
1118
|
+
type=openapi_v3_1.Type2.http,
|
|
1119
|
+
scheme=scheme_value,
|
|
1120
|
+
description=http_scheme.description,
|
|
1121
|
+
)
|
|
1122
|
+
inner = openapi_v3_1.HTTPSecurityScheme(root=inner_http)
|
|
1123
|
+
return openapi_v3_1.SecurityScheme(root=inner)
|
|
1124
|
+
elif isinstance(sec_scheme, OAuth2SecurityScheme):
|
|
1125
|
+
flows = sec_scheme.flows
|
|
1126
|
+
oauth_flows = openapi_v3_1.OAuthFlows(
|
|
1127
|
+
implicit=openapi_v3_1.ImplicitOAuthFlow(
|
|
1128
|
+
authorizationUrl=flows.implicit.authorizationUrl,
|
|
1129
|
+
refreshUrl=flows.implicit.refreshUrl,
|
|
1130
|
+
scopes=flows.implicit.scopes,
|
|
1131
|
+
)
|
|
1132
|
+
if flows.implicit
|
|
1133
|
+
else None,
|
|
1134
|
+
password=openapi_v3_1.PasswordOAuthFlow(
|
|
1135
|
+
tokenUrl=flows.password.tokenUrl,
|
|
1136
|
+
refreshUrl=flows.password.refreshUrl,
|
|
1137
|
+
scopes=flows.password.scopes,
|
|
1138
|
+
)
|
|
1139
|
+
if flows.password
|
|
1140
|
+
else None,
|
|
1141
|
+
clientCredentials=openapi_v3_1.ClientCredentialsFlow(
|
|
1142
|
+
tokenUrl=flows.clientCredentials.tokenUrl,
|
|
1143
|
+
refreshUrl=flows.clientCredentials.refreshUrl,
|
|
1144
|
+
scopes=flows.clientCredentials.scopes,
|
|
1145
|
+
)
|
|
1146
|
+
if flows.clientCredentials
|
|
1147
|
+
else None,
|
|
1148
|
+
authorizationCode=openapi_v3_1.AuthorizationCodeOAuthFlow(
|
|
1149
|
+
authorizationUrl=flows.authorizationCode.authorizationUrl,
|
|
1150
|
+
tokenUrl=flows.authorizationCode.tokenUrl,
|
|
1151
|
+
refreshUrl=flows.authorizationCode.refreshUrl,
|
|
1152
|
+
scopes=flows.authorizationCode.scopes,
|
|
1153
|
+
)
|
|
1154
|
+
if flows.authorizationCode
|
|
1155
|
+
else None,
|
|
1156
|
+
)
|
|
1157
|
+
inner = openapi_v3_1.OAuth2SecurityScheme(
|
|
1158
|
+
type=openapi_v3_1.Type4.oauth2,
|
|
1159
|
+
flows=oauth_flows,
|
|
1160
|
+
description=sec_scheme.description,
|
|
1161
|
+
)
|
|
1162
|
+
return openapi_v3_1.SecurityScheme(root=inner)
|
|
1163
|
+
elif isinstance(sec_scheme, OpenIdConnectSecurityScheme):
|
|
1164
|
+
inner = openapi_v3_1.OpenIdConnectSecurityScheme(
|
|
1165
|
+
type=openapi_v3_1.Type5.openIdConnect,
|
|
1166
|
+
openIdConnectUrl=sec_scheme.openIdConnectUrl,
|
|
1167
|
+
description=sec_scheme.description,
|
|
1168
|
+
)
|
|
1169
|
+
return openapi_v3_1.SecurityScheme(root=inner)
|
|
1170
|
+
|
|
1171
|
+
raise ValueError(f'Unknown security scheme type: {type(sec_scheme)}')
|
|
1172
|
+
|
|
1173
|
+
def _convert_callback_or_ref_to_3_1(
|
|
1174
|
+
self, callback_or_ref: Callback | Reference, warnings: WarningCollector
|
|
1175
|
+
) -> openapi_v3_1.Callback | openapi_v3_1.Reference:
|
|
1176
|
+
"""Convert a Callback or Reference from OpenAPI 3.0 to 3.1."""
|
|
1177
|
+
if isinstance(callback_or_ref, Reference):
|
|
1178
|
+
return self._convert_reference_to_3_1(callback_or_ref)
|
|
1179
|
+
|
|
1180
|
+
# Callback is a RootModel, just pass through the root
|
|
1181
|
+
return openapi_v3_1.Callback(root=callback_or_ref.root)
|
|
1182
|
+
|
|
1183
|
+
def _convert_paths_to_3_1(self, warnings: WarningCollector) -> openapi_v3_1.Paths:
|
|
1184
|
+
"""Convert Paths from OpenAPI 3.0 to 3.1."""
|
|
1185
|
+
if not self.paths:
|
|
1186
|
+
return None
|
|
1187
|
+
|
|
1188
|
+
# Paths is a RootModel containing a dict
|
|
1189
|
+
paths_dict = {}
|
|
1190
|
+
|
|
1191
|
+
if hasattr(self.paths, 'root') and isinstance(self.paths.root, dict):
|
|
1192
|
+
for path, path_item in self.paths.root.items():
|
|
1193
|
+
if path.startswith('x-'):
|
|
1194
|
+
# Vendor extension, pass through
|
|
1195
|
+
paths_dict[path] = path_item
|
|
1196
|
+
else:
|
|
1197
|
+
# Convert PathItem
|
|
1198
|
+
paths_dict[path] = self._convert_path_item_to_3_1(
|
|
1199
|
+
path_item, warnings
|
|
1200
|
+
)
|
|
1201
|
+
|
|
1202
|
+
return openapi_v3_1.Paths(root=paths_dict)
|
|
1203
|
+
|
|
1204
|
+
def _convert_path_item_to_3_1(
|
|
1205
|
+
self, path_item: PathItem, warnings: WarningCollector
|
|
1206
|
+
) -> openapi_v3_1.PathItem:
|
|
1207
|
+
"""Convert a PathItem from OpenAPI 3.0 to 3.1."""
|
|
1208
|
+
parameters = None
|
|
1209
|
+
if path_item.parameters:
|
|
1210
|
+
parameters = [
|
|
1211
|
+
self._convert_parameter_or_ref_to_3_1(p, warnings)
|
|
1212
|
+
for p in path_item.parameters
|
|
1213
|
+
]
|
|
1214
|
+
|
|
1215
|
+
servers = None
|
|
1216
|
+
if path_item.servers:
|
|
1217
|
+
servers = [self._convert_server_to_3_1(s) for s in path_item.servers]
|
|
1218
|
+
|
|
1219
|
+
# Convert operations
|
|
1220
|
+
get = (
|
|
1221
|
+
self._convert_operation_to_3_1(path_item.get, warnings)
|
|
1222
|
+
if path_item.get
|
|
1223
|
+
else None
|
|
1224
|
+
)
|
|
1225
|
+
put = (
|
|
1226
|
+
self._convert_operation_to_3_1(path_item.put, warnings)
|
|
1227
|
+
if path_item.put
|
|
1228
|
+
else None
|
|
1229
|
+
)
|
|
1230
|
+
post = (
|
|
1231
|
+
self._convert_operation_to_3_1(path_item.post, warnings)
|
|
1232
|
+
if path_item.post
|
|
1233
|
+
else None
|
|
1234
|
+
)
|
|
1235
|
+
delete = (
|
|
1236
|
+
self._convert_operation_to_3_1(path_item.delete, warnings)
|
|
1237
|
+
if path_item.delete
|
|
1238
|
+
else None
|
|
1239
|
+
)
|
|
1240
|
+
options = (
|
|
1241
|
+
self._convert_operation_to_3_1(path_item.options, warnings)
|
|
1242
|
+
if path_item.options
|
|
1243
|
+
else None
|
|
1244
|
+
)
|
|
1245
|
+
head = (
|
|
1246
|
+
self._convert_operation_to_3_1(path_item.head, warnings)
|
|
1247
|
+
if path_item.head
|
|
1248
|
+
else None
|
|
1249
|
+
)
|
|
1250
|
+
patch = (
|
|
1251
|
+
self._convert_operation_to_3_1(path_item.patch, warnings)
|
|
1252
|
+
if path_item.patch
|
|
1253
|
+
else None
|
|
1254
|
+
)
|
|
1255
|
+
trace = (
|
|
1256
|
+
self._convert_operation_to_3_1(path_item.trace, warnings)
|
|
1257
|
+
if path_item.trace
|
|
1258
|
+
else None
|
|
1259
|
+
)
|
|
1260
|
+
|
|
1261
|
+
path_item_dict = {
|
|
1262
|
+
'$ref': path_item.field_ref,
|
|
1263
|
+
'summary': path_item.summary,
|
|
1264
|
+
'description': path_item.description,
|
|
1265
|
+
'get': get,
|
|
1266
|
+
'put': put,
|
|
1267
|
+
'post': post,
|
|
1268
|
+
'delete': delete,
|
|
1269
|
+
'options': options,
|
|
1270
|
+
'head': head,
|
|
1271
|
+
'patch': patch,
|
|
1272
|
+
'trace': trace,
|
|
1273
|
+
'servers': servers,
|
|
1274
|
+
'parameters': parameters,
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
# Remove None values
|
|
1278
|
+
path_item_dict = {k: v for k, v in path_item_dict.items() if v is not None}
|
|
1279
|
+
|
|
1280
|
+
return openapi_v3_1.PathItem.model_validate(path_item_dict)
|
|
1281
|
+
|
|
1282
|
+
def _convert_operation_to_3_1(
|
|
1283
|
+
self, operation: Operation, warnings: WarningCollector
|
|
1284
|
+
) -> openapi_v3_1.Operation:
|
|
1285
|
+
"""Convert an Operation from OpenAPI 3.0 to 3.1."""
|
|
1286
|
+
external_docs = (
|
|
1287
|
+
self._convert_external_docs_to_3_1(operation.externalDocs)
|
|
1288
|
+
if operation.externalDocs
|
|
1289
|
+
else None
|
|
1290
|
+
)
|
|
1291
|
+
|
|
1292
|
+
parameters = None
|
|
1293
|
+
if operation.parameters:
|
|
1294
|
+
parameters = [
|
|
1295
|
+
self._convert_parameter_or_ref_to_3_1(p, warnings)
|
|
1296
|
+
for p in operation.parameters
|
|
1297
|
+
]
|
|
1298
|
+
|
|
1299
|
+
request_body = None
|
|
1300
|
+
if operation.requestBody:
|
|
1301
|
+
request_body = self._convert_request_body_or_ref_to_3_1(
|
|
1302
|
+
operation.requestBody, warnings
|
|
1303
|
+
)
|
|
1304
|
+
|
|
1305
|
+
responses = (
|
|
1306
|
+
self._convert_responses_to_3_1(operation.responses, warnings)
|
|
1307
|
+
if operation.responses
|
|
1308
|
+
else None
|
|
1309
|
+
)
|
|
1310
|
+
|
|
1311
|
+
callbacks = None
|
|
1312
|
+
if operation.callbacks:
|
|
1313
|
+
callbacks = {}
|
|
1314
|
+
for name, callback in operation.callbacks.items():
|
|
1315
|
+
callbacks[name] = self._convert_callback_or_ref_to_3_1(
|
|
1316
|
+
callback, warnings
|
|
1317
|
+
)
|
|
1318
|
+
|
|
1319
|
+
security = None
|
|
1320
|
+
if operation.security:
|
|
1321
|
+
security = [
|
|
1322
|
+
openapi_v3_1.SecurityRequirement(
|
|
1323
|
+
root={k: v for k, v in req.root.items()}
|
|
1324
|
+
)
|
|
1325
|
+
for req in operation.security
|
|
1326
|
+
]
|
|
1327
|
+
|
|
1328
|
+
servers = None
|
|
1329
|
+
if operation.servers:
|
|
1330
|
+
servers = [self._convert_server_to_3_1(s) for s in operation.servers]
|
|
1331
|
+
|
|
1332
|
+
return openapi_v3_1.Operation(
|
|
1333
|
+
tags=operation.tags,
|
|
1334
|
+
summary=operation.summary,
|
|
1335
|
+
description=operation.description,
|
|
1336
|
+
externalDocs=external_docs,
|
|
1337
|
+
operationId=operation.operationId,
|
|
1338
|
+
parameters=parameters,
|
|
1339
|
+
requestBody=request_body,
|
|
1340
|
+
responses=responses,
|
|
1341
|
+
callbacks=callbacks,
|
|
1342
|
+
deprecated=operation.deprecated,
|
|
1343
|
+
security=security,
|
|
1344
|
+
servers=servers,
|
|
1345
|
+
)
|
|
1346
|
+
|
|
1347
|
+
def _convert_responses_to_3_1(
|
|
1348
|
+
self, responses: Responses, warnings: WarningCollector
|
|
1349
|
+
) -> openapi_v3_1.Responses:
|
|
1350
|
+
"""Convert Responses from OpenAPI 3.0 to 3.1."""
|
|
1351
|
+
converted_responses = {}
|
|
1352
|
+
for status_code, response in responses.root.items():
|
|
1353
|
+
converted_responses[status_code] = self._convert_response_or_ref_to_3_1(
|
|
1354
|
+
response, warnings
|
|
1355
|
+
)
|
|
1356
|
+
return openapi_v3_1.Responses(root=converted_responses)
|
|
1357
|
+
|
|
1358
|
+
|
|
1359
|
+
class Components(BaseModel):
|
|
1360
|
+
model_config = ConfigDict(extra='forbid')
|
|
1361
|
+
|
|
1362
|
+
schemas: (
|
|
1363
|
+
dict[
|
|
1364
|
+
Annotated[str, StringConstraints(pattern=r'^[a-zA-Z0-9\.\-_]+$')],
|
|
1365
|
+
Schema | Reference,
|
|
1366
|
+
]
|
|
1367
|
+
| None
|
|
1368
|
+
) = None
|
|
1369
|
+
responses: (
|
|
1370
|
+
dict[
|
|
1371
|
+
Annotated[str, StringConstraints(pattern=r'^[a-zA-Z0-9\.\-_]+$')],
|
|
1372
|
+
Reference | Response,
|
|
1373
|
+
]
|
|
1374
|
+
| None
|
|
1375
|
+
) = None
|
|
1376
|
+
parameters: (
|
|
1377
|
+
dict[
|
|
1378
|
+
Annotated[str, StringConstraints(pattern=r'^[a-zA-Z0-9\.\-_]+$')],
|
|
1379
|
+
Reference | Parameter,
|
|
1380
|
+
]
|
|
1381
|
+
| None
|
|
1382
|
+
) = None
|
|
1383
|
+
examples: (
|
|
1384
|
+
dict[
|
|
1385
|
+
Annotated[str, StringConstraints(pattern=r'^[a-zA-Z0-9\.\-_]+$')],
|
|
1386
|
+
Reference | Example,
|
|
1387
|
+
]
|
|
1388
|
+
| None
|
|
1389
|
+
) = None
|
|
1390
|
+
requestBodies: (
|
|
1391
|
+
dict[
|
|
1392
|
+
Annotated[str, StringConstraints(pattern=r'^[a-zA-Z0-9\.\-_]+$')],
|
|
1393
|
+
Reference | RequestBody,
|
|
1394
|
+
]
|
|
1395
|
+
| None
|
|
1396
|
+
) = None
|
|
1397
|
+
headers: (
|
|
1398
|
+
dict[
|
|
1399
|
+
Annotated[str, StringConstraints(pattern=r'^[a-zA-Z0-9\.\-_]+$')],
|
|
1400
|
+
Reference | Header,
|
|
1401
|
+
]
|
|
1402
|
+
| None
|
|
1403
|
+
) = None
|
|
1404
|
+
securitySchemes: (
|
|
1405
|
+
dict[
|
|
1406
|
+
Annotated[str, StringConstraints(pattern=r'^[a-zA-Z0-9\.\-_]+$')],
|
|
1407
|
+
Reference | SecurityScheme,
|
|
1408
|
+
]
|
|
1409
|
+
| None
|
|
1410
|
+
) = None
|
|
1411
|
+
links: (
|
|
1412
|
+
dict[
|
|
1413
|
+
Annotated[str, StringConstraints(pattern=r'^[a-zA-Z0-9\.\-_]+$')],
|
|
1414
|
+
Reference | Link,
|
|
1415
|
+
]
|
|
1416
|
+
| None
|
|
1417
|
+
) = None
|
|
1418
|
+
callbacks: (
|
|
1419
|
+
dict[
|
|
1420
|
+
Annotated[str, StringConstraints(pattern=r'^[a-zA-Z0-9\.\-_]+$')],
|
|
1421
|
+
Reference | Callback,
|
|
1422
|
+
]
|
|
1423
|
+
| None
|
|
1424
|
+
) = None
|
|
1425
|
+
|
|
1426
|
+
|
|
1427
|
+
class Response(BaseModel):
|
|
1428
|
+
model_config = ConfigDict(extra='forbid')
|
|
1429
|
+
|
|
1430
|
+
description: str
|
|
1431
|
+
headers: dict[str, Header | Reference] | None = None
|
|
1432
|
+
content: dict[str, MediaType] | None = None
|
|
1433
|
+
links: dict[str, Link | Reference] | None = None
|
|
1434
|
+
|
|
1435
|
+
|
|
1436
|
+
class MediaType(BaseModel):
|
|
1437
|
+
model_config = ConfigDict(extra='forbid')
|
|
1438
|
+
|
|
1439
|
+
schema_: Schema | Reference | None = Field(None, alias='schema')
|
|
1440
|
+
example: Any | None = None
|
|
1441
|
+
examples: dict[str, Example | Reference] | None = None
|
|
1442
|
+
encoding: dict[str, Encoding] | None = None
|
|
1443
|
+
|
|
1444
|
+
|
|
1445
|
+
class Header(BaseModel):
|
|
1446
|
+
model_config = ConfigDict(extra='forbid')
|
|
1447
|
+
|
|
1448
|
+
description: str | None = None
|
|
1449
|
+
required: bool | None = False
|
|
1450
|
+
deprecated: bool | None = False
|
|
1451
|
+
allowEmptyValue: bool | None = False
|
|
1452
|
+
style: Style | None = 'simple'
|
|
1453
|
+
explode: bool | None = None
|
|
1454
|
+
allowReserved: bool | None = False
|
|
1455
|
+
schema_: Schema | Reference | None = Field(None, alias='schema')
|
|
1456
|
+
content: dict[str, MediaType] | None = None
|
|
1457
|
+
example: Any | None = None
|
|
1458
|
+
examples: dict[str, Example | Reference] | None = None
|
|
1459
|
+
|
|
1460
|
+
|
|
1461
|
+
class Paths(RootModel[dict[str, 'PathItem']]):
|
|
1462
|
+
"""Paths object containing path items and optional extensions.
|
|
1463
|
+
|
|
1464
|
+
Keys should be path templates (starting with /) or extensions (starting with x-).
|
|
1465
|
+
"""
|
|
1466
|
+
|
|
1467
|
+
pass
|
|
1468
|
+
|
|
1469
|
+
|
|
1470
|
+
class PathItem(BaseModel):
|
|
1471
|
+
model_config = ConfigDict(extra='forbid')
|
|
1472
|
+
|
|
1473
|
+
field_ref: str | None = Field(None, alias='$ref')
|
|
1474
|
+
summary: str | None = None
|
|
1475
|
+
description: str | None = None
|
|
1476
|
+
get: Operation | None = None
|
|
1477
|
+
put: Operation | None = None
|
|
1478
|
+
post: Operation | None = None
|
|
1479
|
+
delete: Operation | None = None
|
|
1480
|
+
options: Operation | None = None
|
|
1481
|
+
head: Operation | None = None
|
|
1482
|
+
patch: Operation | None = None
|
|
1483
|
+
trace: Operation | None = None
|
|
1484
|
+
servers: list[Server] | None = None
|
|
1485
|
+
parameters: list[Parameter | Reference] | None = None
|
|
1486
|
+
|
|
1487
|
+
|
|
1488
|
+
class Operation(BaseModel):
|
|
1489
|
+
model_config = ConfigDict(extra='forbid')
|
|
1490
|
+
|
|
1491
|
+
tags: list[str] | None = None
|
|
1492
|
+
summary: str | None = None
|
|
1493
|
+
description: str | None = None
|
|
1494
|
+
externalDocs: ExternalDocumentation | None = None
|
|
1495
|
+
operationId: str | None = None
|
|
1496
|
+
parameters: list[Parameter | Reference] | None = None
|
|
1497
|
+
requestBody: RequestBody | Reference | None = None
|
|
1498
|
+
responses: Responses
|
|
1499
|
+
callbacks: dict[str, Callback | Reference] | None = None
|
|
1500
|
+
deprecated: bool | None = False
|
|
1501
|
+
security: list[SecurityRequirement] | None = None
|
|
1502
|
+
servers: list[Server] | None = None
|
|
1503
|
+
|
|
1504
|
+
|
|
1505
|
+
class Responses(RootModel[dict[str, Response | Reference]]):
|
|
1506
|
+
"""Responses object containing response definitions by HTTP status code.
|
|
1507
|
+
|
|
1508
|
+
Keys are HTTP status codes (200, 400, etc.) or 'default'.
|
|
1509
|
+
"""
|
|
1510
|
+
|
|
1511
|
+
pass
|
|
1512
|
+
|
|
1513
|
+
|
|
1514
|
+
class Parameter(BaseModel):
|
|
1515
|
+
model_config = ConfigDict(extra='forbid')
|
|
1516
|
+
|
|
1517
|
+
name: str
|
|
1518
|
+
in_: str = Field(..., alias='in')
|
|
1519
|
+
description: str | None = None
|
|
1520
|
+
required: bool | None = False
|
|
1521
|
+
deprecated: bool | None = False
|
|
1522
|
+
allowEmptyValue: bool | None = False
|
|
1523
|
+
style: str | None = None
|
|
1524
|
+
explode: bool | None = None
|
|
1525
|
+
allowReserved: bool | None = False
|
|
1526
|
+
schema_: Schema | Reference | None = Field(None, alias='schema')
|
|
1527
|
+
content: dict[str, MediaType] | None = None
|
|
1528
|
+
example: Any | None = None
|
|
1529
|
+
examples: dict[str, Example | Reference] | None = None
|
|
1530
|
+
|
|
1531
|
+
|
|
1532
|
+
class RequestBody(BaseModel):
|
|
1533
|
+
model_config = ConfigDict(extra='forbid')
|
|
1534
|
+
|
|
1535
|
+
description: str | None = None
|
|
1536
|
+
content: dict[str, MediaType]
|
|
1537
|
+
required: bool | None = False
|
|
1538
|
+
|
|
1539
|
+
|
|
1540
|
+
class Encoding(BaseModel):
|
|
1541
|
+
model_config = ConfigDict(extra='forbid')
|
|
1542
|
+
|
|
1543
|
+
contentType: str | None = None
|
|
1544
|
+
headers: dict[str, Header | Reference] | None = None
|
|
1545
|
+
style: Style5 | None = None
|
|
1546
|
+
explode: bool | None = None
|
|
1547
|
+
allowReserved: bool | None = False
|
|
1548
|
+
|
|
1549
|
+
|
|
1550
|
+
Schema.model_rebuild()
|
|
1551
|
+
OpenAPI.model_rebuild()
|
|
1552
|
+
Components.model_rebuild()
|
|
1553
|
+
Response.model_rebuild()
|
|
1554
|
+
MediaType.model_rebuild()
|
|
1555
|
+
Paths.model_rebuild()
|
|
1556
|
+
PathItem.model_rebuild()
|
|
1557
|
+
Operation.model_rebuild()
|