11import typing as t
2+ from functools import cached_property
23
34import pydantic
45import pydantic .alias_generators
@@ -15,7 +16,7 @@ class OASBase(pydantic.BaseModel, alias_generator=to_alias, extra="forbid"):
1516
1617class ExtensibleOASBase (OASBase , extra = "allow" ):
1718 @pydantic .model_validator (mode = "after" )
18- def _check_extensions (self ) -> t .Self :
19+ def _check_extensions (self ) -> " t.Self" :
1920 if self .__pydantic_extra__ is not None :
2021 invalid_keys = [
2122 key for key in self .__pydantic_extra__ .keys () if not key .startswith ("x-" )
@@ -30,8 +31,31 @@ def _check_extensions(self) -> t.Self:
3031 return self
3132
3233
34+ OperationName = t .Literal [
35+ "get" ,
36+ "put" ,
37+ "post" ,
38+ "patch" ,
39+ "delete" ,
40+ "options" ,
41+ "head" ,
42+ "trace" ,
43+ ]
44+
45+ METHODS : list [OperationName ] = [
46+ "get" ,
47+ "put" ,
48+ "post" ,
49+ "patch" ,
50+ "delete" ,
51+ "options" ,
52+ "head" ,
53+ "trace" ,
54+ ]
55+
56+
3357class Reference (OASBase ):
34- _ref : t .Annotated [str , pydantic .Field (alias = "$ref" )]
58+ ref : t .Annotated [str , pydantic .Field (alias = "$ref" )]
3559 summary : str | None = None
3660 description : str | None = None
3761
@@ -164,11 +188,48 @@ class XML(ExtensibleOASBase):
164188 wrapped : bool = False
165189
166190
167- class Schema (OASBase , extra = "allow" ):
191+ class SchemaBase (OASBase , extra = "allow" ):
168192 discriminator : Discriminator | None = None
169193 xml : XML | None = None
170194 external_docs : ExternalDocumentation | None = None
171195 example : t .Any = None
196+ examples : dict [str , Example | Reference ] | None = None
197+ nullable : bool = False
198+
199+
200+ class AllOfSchema (SchemaBase ):
201+ all_of : list ["Schema" ]
202+
203+
204+ class AnyOfSchema (SchemaBase ):
205+ any_of : list ["Schema" ]
206+
207+
208+ class OneOfSchema (SchemaBase ):
209+ one_of : list ["Schema" ]
210+
211+
212+ class TypeSchema (SchemaBase ):
213+ type_ : str | list [str ]
214+
215+ min_items : int | None = None
216+ max_items : int | None = None
217+ unique_items : bool = False
218+ items : "Schema | None" = None
219+
220+ minimum : int | float | None = None
221+ exclusive_minimum : bool = False
222+ maximum : int | float | None = None
223+ exclusive_maximum : bool = False
224+ multiple_of : int | None = None
225+ format_ : str | None = None
226+ enum : list [str ] | None = None
227+ properties : dict [str , "Schema" ] | None = None
228+ additional_properties : "Schema" = True
229+ required : list [str ] | None = None
230+
231+
232+ Schema = bool | Reference | TypeSchema | AllOfSchema | AnyOfSchema | OneOfSchema | SchemaBase
172233
173234
174235class Link (ExtensibleOASBase ):
@@ -192,18 +253,38 @@ class Encoding(ExtensibleOASBase):
192253 # TODO These only apply to RFC6570
193254 style : (
194255 t .Literal [
195- "simple" , "form" , "matrix" , "label" , "spaceDelimited" , "pipeDelimited" , "deepObject"
256+ "simple" ,
257+ "form" ,
258+ "matrix" ,
259+ "label" ,
260+ "spaceDelimited" ,
261+ "pipeDelimited" ,
262+ "deepObject" ,
196263 ]
197264 | None
198265 ) = None
199- explode : t .Annotated [bool , pydantic .Field (default_factory = lambda data : data ["style" ] == "form" )]
266+ explode : t .Annotated [
267+ bool ,
268+ pydantic .Field (
269+ # default_factory=lambda data: data["style"] == "form"
270+ ),
271+ ]
200272 allow_reserved : bool = False
201273
274+ @pydantic .model_validator (mode = "before" )
275+ @classmethod
276+ def explode_default (cls , data : t .Any ) -> t .Any :
277+ # This is a workaround for pydantic < 2.10 .
278+ # default_factory didn't have the extra data argument.
279+ data ["explode" ] = data .get ("style" ) == "form"
280+ return data
281+
202282
203283class MediaType (ExtensibleOASBase ):
204284 schema_ : Schema
205285 example : t .Any = None
206286 examples : dict [str , Example | Reference ] | None = None
287+ # TODO encoding seems to be reserved to request body.
207288 encoding : dict [str , Encoding ] = {}
208289
209290
@@ -237,7 +318,7 @@ class ParameterBase(ExtensibleOASBase):
237318 allow_empty_value : bool = False
238319
239320 @pydantic .model_validator (mode = "after" )
240- def _path_required (self ) -> t .Self :
321+ def _path_required (self ) -> " t.Self" :
241322 if self .in_ == "path" :
242323 assert self .required
243324 return self
@@ -251,17 +332,42 @@ class SchemaParameter(ParameterBase):
251332 schema_ : Schema
252333 style : t .Annotated [
253334 t .Literal [
254- "simple" , "form" , "matrix" , "label" , "spaceDelimited" , "pipeDelimited" , "deepObject"
335+ "simple" ,
336+ "form" ,
337+ "matrix" ,
338+ "label" ,
339+ "spaceDelimited" ,
340+ "pipeDelimited" ,
341+ "deepObject" ,
255342 ],
256343 pydantic .Field (
257- default_factory = lambda data : "simple" if data ["in_" ] in ["header" , "path" ] else "form"
344+ # default_factory=lambda data: (
345+ # "simple" if data.get("in_") in ["header", "path"] else "form"
346+ # )
347+ ),
348+ ]
349+ explode : t .Annotated [
350+ bool ,
351+ pydantic .Field (
352+ # default_factory=lambda data: data["style"] == "form"
258353 ),
259354 ]
260- explode : t .Annotated [bool , pydantic .Field (default_factory = lambda data : data ["style" ] == "form" )]
261355 allowed_reserved : bool = False
262356 example : t .Any = None
263357 examples : dict [str , Example | Reference ] | None = None
264358
359+ @pydantic .model_validator (mode = "before" )
360+ @classmethod
361+ def explode_default (cls , data : t .Any ) -> t .Any :
362+ # This is a workaround for pydantic < 2.10 .
363+ # default_factory didn't have the extra data argument.
364+ if "schema" in data :
365+ if data .get ("style" ) is None :
366+ data ["style" ] = "simple" if data .get ("in" ) in ["header" , "path" ] else "form"
367+ if data .get ("explode" ) is None :
368+ data ["explode" ] = data ["style" ] == "form"
369+ return data
370+
265371
266372Parameter = ContentParameter | SchemaParameter
267373
@@ -275,7 +381,7 @@ class RequestBody(ExtensibleOASBase):
275381class Response (ExtensibleOASBase ):
276382 description : str
277383 headers : dict [str , Header | Reference ] | None = None
278- content : dict [str , MediaType ] | None = None
384+ content : dict [str , MediaType ] = {}
279385 links : dict [str , Link | Reference ] | None = None
280386
281387
@@ -291,16 +397,17 @@ class Operation(ExtensibleOASBase):
291397 description : str | None = None
292398 external_docs : ExternalDocumentation | None = None
293399 operation_id : str
294- parameters : list [Parameter | Reference ] | None = None
400+ parameters : list [Parameter | Reference ] = []
295401 request_body : RequestBody | Reference | None = None
296- responses : Responses | None = None
402+ responses : Responses = {}
297403 callbacks : dict [str , Callback | Reference ] | None = None
298404 deprecated : bool = False
299405 security : SecurityRequirements | None = None
300406 servers : list [Server ] | None = None
301407
302408
303- class PathItem (Reference , ExtensibleOASBase ):
409+ class PathItem (ExtensibleOASBase ):
410+ # TODO $ref
304411 get : Operation | None = None
305412 put : Operation | None = None
306413 post : Operation | None = None
@@ -310,17 +417,17 @@ class PathItem(Reference, ExtensibleOASBase):
310417 head : Operation | None = None
311418 trace : Operation | None = None
312419 servers : list [Server ] | None = None
313- parameters : list [Parameter | Reference ] | None = None
420+ parameters : list [Parameter | Reference ] = []
314421
315422
316423class Components (ExtensibleOASBase ):
317- schemas : dict [str , Schema ] | None = None
424+ schemas : dict [str , Schema ] = {}
318425 responses : dict [str , Response | Reference ] | None = None
319- parameters : dict [str , Parameter | Reference ] | None = None
426+ parameters : dict [str , Parameter | Reference ] = {}
320427 examples : dict [str , Example | Reference ] | None = None
321428 request_bodies : dict [str , RequestBody | Reference ] | None = None
322429 headers : dict [str , Header | Reference ] | None = None
323- security_schemes : dict [str , SecurityScheme | Reference ] | None = None
430+ security_schemes : dict [str , SecurityScheme | Reference ] = {}
324431 links : dict [str , Link | Reference ] | None = None
325432 callbacks : dict [str , Callback | Reference ] | None = None
326433 path_items : dict [str , PathItem ] | None = None
@@ -333,9 +440,19 @@ class OpenAPISpec(ExtensibleOASBase):
333440 servers : list [Server ] | None = None
334441 # In the specification there is a Paths Object,
335442 # probably because paths as keys can get extra validation.
336- paths : dict [str , PathItem ] | None = None
443+ paths : dict [str , PathItem ] = {}
337444 webhooks : dict [str , PathItem ] | None = None
338- components : Components | None = None
445+ components : Components = Components ()
339446 security : SecurityRequirements | None = None
340447 tags : list [Tag ] | None = None
341448 external_docs : ExternalDocumentation | None = None
449+
450+ # @pydantic.computed_field # type: ignore[prop-decorator]
451+ @cached_property
452+ def operations (self ) -> dict [str , tuple [OperationName , str ]]:
453+ return {
454+ operation .operation_id : (method , path )
455+ for path , path_item in self .paths .items ()
456+ for method , operation in ((method , getattr (path_item , method )) for method in METHODS )
457+ if operation is not None
458+ }
0 commit comments