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.
Files changed (52) hide show
  1. README.md +581 -8
  2. otterapi/__init__.py +73 -0
  3. otterapi/cli.py +327 -29
  4. otterapi/codegen/__init__.py +115 -0
  5. otterapi/codegen/ast_utils.py +134 -5
  6. otterapi/codegen/client.py +1271 -0
  7. otterapi/codegen/codegen.py +1736 -0
  8. otterapi/codegen/dataframes.py +392 -0
  9. otterapi/codegen/emitter.py +473 -0
  10. otterapi/codegen/endpoints.py +2597 -343
  11. otterapi/codegen/pagination.py +1026 -0
  12. otterapi/codegen/schema.py +593 -0
  13. otterapi/codegen/splitting.py +1397 -0
  14. otterapi/codegen/types.py +1345 -0
  15. otterapi/codegen/utils.py +180 -1
  16. otterapi/config.py +1017 -24
  17. otterapi/exceptions.py +231 -0
  18. otterapi/openapi/__init__.py +46 -0
  19. otterapi/openapi/v2/__init__.py +86 -0
  20. otterapi/openapi/v2/spec.json +1607 -0
  21. otterapi/openapi/v2/v2.py +1776 -0
  22. otterapi/openapi/v3/__init__.py +131 -0
  23. otterapi/openapi/v3/spec.json +1651 -0
  24. otterapi/openapi/v3/v3.py +1557 -0
  25. otterapi/openapi/v3_1/__init__.py +133 -0
  26. otterapi/openapi/v3_1/spec.json +1411 -0
  27. otterapi/openapi/v3_1/v3_1.py +798 -0
  28. otterapi/openapi/v3_2/__init__.py +133 -0
  29. otterapi/openapi/v3_2/spec.json +1666 -0
  30. otterapi/openapi/v3_2/v3_2.py +777 -0
  31. otterapi/tests/__init__.py +3 -0
  32. otterapi/tests/fixtures/__init__.py +455 -0
  33. otterapi/tests/test_ast_utils.py +680 -0
  34. otterapi/tests/test_codegen.py +610 -0
  35. otterapi/tests/test_dataframe.py +1038 -0
  36. otterapi/tests/test_exceptions.py +493 -0
  37. otterapi/tests/test_openapi_support.py +616 -0
  38. otterapi/tests/test_openapi_upgrade.py +215 -0
  39. otterapi/tests/test_pagination.py +1101 -0
  40. otterapi/tests/test_splitting_config.py +319 -0
  41. otterapi/tests/test_splitting_integration.py +427 -0
  42. otterapi/tests/test_splitting_resolver.py +512 -0
  43. otterapi/tests/test_splitting_tree.py +525 -0
  44. otterapi-0.0.6.dist-info/METADATA +627 -0
  45. otterapi-0.0.6.dist-info/RECORD +48 -0
  46. {otterapi-0.0.5.dist-info → otterapi-0.0.6.dist-info}/WHEEL +1 -1
  47. otterapi/codegen/generator.py +0 -358
  48. otterapi/codegen/openapi_processor.py +0 -27
  49. otterapi/codegen/type_generator.py +0 -559
  50. otterapi-0.0.5.dist-info/METADATA +0 -54
  51. otterapi-0.0.5.dist-info/RECORD +0 -16
  52. {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()