alt-stack-openapi-pydantic 1.6.2__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.
- alt_stack_openapi_pydantic/__init__.py +21 -0
- alt_stack_openapi_pydantic/cli.py +67 -0
- alt_stack_openapi_pydantic/dependencies.py +103 -0
- alt_stack_openapi_pydantic/emission.py +725 -0
- alt_stack_openapi_pydantic/lowering.py +116 -0
- alt_stack_openapi_pydantic/py.typed +1 -0
- alt_stack_openapi_pydantic/registry.py +161 -0
- alt_stack_openapi_pydantic/rendering.py +197 -0
- alt_stack_openapi_pydantic/routes.py +179 -0
- alt_stack_openapi_pydantic/schema_dedup.py +141 -0
- alt_stack_openapi_pydantic/to_python.py +12 -0
- alt_stack_openapi_pydantic/type_render.py +15 -0
- alt_stack_openapi_pydantic/types/__init__.py +5 -0
- alt_stack_openapi_pydantic/types/array.py +39 -0
- alt_stack_openapi_pydantic/types/boolean.py +14 -0
- alt_stack_openapi_pydantic/types/intersection.py +20 -0
- alt_stack_openapi_pydantic/types/number.py +39 -0
- alt_stack_openapi_pydantic/types/object.py +15 -0
- alt_stack_openapi_pydantic/types/string.py +64 -0
- alt_stack_openapi_pydantic/types/types.py +6 -0
- alt_stack_openapi_pydantic/types/union.py +37 -0
- alt_stack_openapi_pydantic/utils.py +32 -0
- alt_stack_openapi_pydantic-1.6.2.dist-info/METADATA +95 -0
- alt_stack_openapi_pydantic-1.6.2.dist-info/RECORD +28 -0
- alt_stack_openapi_pydantic-1.6.2.dist-info/WHEEL +5 -0
- alt_stack_openapi_pydantic-1.6.2.dist-info/entry_points.txt +2 -0
- alt_stack_openapi_pydantic-1.6.2.dist-info/licenses/LICENSE +21 -0
- alt_stack_openapi_pydantic-1.6.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .rendering import needs_named_model
|
|
7
|
+
from .types import AnySchema
|
|
8
|
+
from .utils import to_pascal_case
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(slots=True)
|
|
12
|
+
class NamedSchema:
|
|
13
|
+
name: str
|
|
14
|
+
schema: AnySchema
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(slots=True)
|
|
18
|
+
class LoweringContext:
|
|
19
|
+
used_names: set[str] = field(default_factory=set)
|
|
20
|
+
|
|
21
|
+
def reserve_name(self, base_name: str) -> str:
|
|
22
|
+
if base_name not in self.used_names:
|
|
23
|
+
self.used_names.add(base_name)
|
|
24
|
+
return base_name
|
|
25
|
+
|
|
26
|
+
index = 2
|
|
27
|
+
while f"{base_name}_{index}" in self.used_names:
|
|
28
|
+
index += 1
|
|
29
|
+
|
|
30
|
+
reserved = f"{base_name}_{index}"
|
|
31
|
+
self.used_names.add(reserved)
|
|
32
|
+
return reserved
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def lower_named_schema(
|
|
36
|
+
context: LoweringContext,
|
|
37
|
+
name: str,
|
|
38
|
+
schema: AnySchema,
|
|
39
|
+
) -> list[NamedSchema]:
|
|
40
|
+
lowered, hoisted = _lower_schema(context, name, schema)
|
|
41
|
+
return [*hoisted, NamedSchema(name=name, schema=lowered)]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _lower_schema(
|
|
45
|
+
context: LoweringContext,
|
|
46
|
+
parent_name: str,
|
|
47
|
+
schema: AnySchema,
|
|
48
|
+
) -> tuple[AnySchema, list[NamedSchema]]:
|
|
49
|
+
if not isinstance(schema, dict):
|
|
50
|
+
return schema, []
|
|
51
|
+
|
|
52
|
+
lowered = dict(schema)
|
|
53
|
+
hoisted: list[NamedSchema] = []
|
|
54
|
+
|
|
55
|
+
properties = lowered.get("properties")
|
|
56
|
+
if isinstance(properties, dict):
|
|
57
|
+
lowered_properties: dict[str, AnySchema] = {}
|
|
58
|
+
for prop_name, prop_schema in properties.items():
|
|
59
|
+
child_name = f"{parent_name}{to_pascal_case(prop_name)}"
|
|
60
|
+
lowered_child, child_hoisted = _lower_child_schema(context, child_name, prop_schema)
|
|
61
|
+
lowered_properties[prop_name] = lowered_child
|
|
62
|
+
hoisted.extend(child_hoisted)
|
|
63
|
+
lowered["properties"] = lowered_properties
|
|
64
|
+
|
|
65
|
+
items = lowered.get("items")
|
|
66
|
+
if isinstance(items, dict):
|
|
67
|
+
lowered_items, item_hoisted = _lower_child_schema(
|
|
68
|
+
context,
|
|
69
|
+
f"{parent_name}Item",
|
|
70
|
+
items,
|
|
71
|
+
)
|
|
72
|
+
lowered["items"] = lowered_items
|
|
73
|
+
hoisted.extend(item_hoisted)
|
|
74
|
+
|
|
75
|
+
additional = lowered.get("additionalProperties")
|
|
76
|
+
if isinstance(additional, dict):
|
|
77
|
+
lowered_additional, additional_hoisted = _lower_child_schema(
|
|
78
|
+
context,
|
|
79
|
+
f"{parent_name}Value",
|
|
80
|
+
additional,
|
|
81
|
+
)
|
|
82
|
+
lowered["additionalProperties"] = lowered_additional
|
|
83
|
+
hoisted.extend(additional_hoisted)
|
|
84
|
+
|
|
85
|
+
for keyword, suffix in (("oneOf", "Option"), ("anyOf", "Option"), ("allOf", "Part")):
|
|
86
|
+
value = lowered.get(keyword)
|
|
87
|
+
if not isinstance(value, list):
|
|
88
|
+
continue
|
|
89
|
+
lowered_items_list: list[AnySchema] = []
|
|
90
|
+
for index, item in enumerate(value, start=1):
|
|
91
|
+
child_name = f"{parent_name}{suffix}{index}"
|
|
92
|
+
lowered_item, item_hoisted = _lower_child_schema(context, child_name, item)
|
|
93
|
+
lowered_items_list.append(lowered_item)
|
|
94
|
+
hoisted.extend(item_hoisted)
|
|
95
|
+
lowered[keyword] = lowered_items_list
|
|
96
|
+
|
|
97
|
+
return lowered, hoisted
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _lower_child_schema(
|
|
101
|
+
context: LoweringContext,
|
|
102
|
+
child_name: str,
|
|
103
|
+
schema: AnySchema,
|
|
104
|
+
) -> tuple[AnySchema, list[NamedSchema]]:
|
|
105
|
+
if not isinstance(schema, dict):
|
|
106
|
+
return schema, []
|
|
107
|
+
|
|
108
|
+
if needs_named_model(schema):
|
|
109
|
+
reserved_name = context.reserve_name(child_name)
|
|
110
|
+
lowered_schema, hoisted = _lower_schema(context, reserved_name, schema)
|
|
111
|
+
ref_schema: dict[str, Any] = {"$ref": f"#/components/schemas/{reserved_name}"}
|
|
112
|
+
if schema.get("nullable") is True:
|
|
113
|
+
ref_schema["nullable"] = True
|
|
114
|
+
return ref_schema, [*hoisted, NamedSchema(name=reserved_name, schema=lowered_schema)]
|
|
115
|
+
|
|
116
|
+
return _lower_schema(context, child_name, schema)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Literal, TypeAlias, TypedDict, cast
|
|
4
|
+
|
|
5
|
+
SUPPORTED_STRING_FORMATS = [
|
|
6
|
+
"color-hex",
|
|
7
|
+
"date",
|
|
8
|
+
"date-time",
|
|
9
|
+
"email",
|
|
10
|
+
"iso-date",
|
|
11
|
+
"iso-date-time",
|
|
12
|
+
"objectid",
|
|
13
|
+
"uri",
|
|
14
|
+
"url",
|
|
15
|
+
"uuid",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
SupportedStringFormat = Literal[
|
|
19
|
+
"color-hex",
|
|
20
|
+
"date",
|
|
21
|
+
"date-time",
|
|
22
|
+
"email",
|
|
23
|
+
"iso-date",
|
|
24
|
+
"iso-date-time",
|
|
25
|
+
"objectid",
|
|
26
|
+
"uri",
|
|
27
|
+
"url",
|
|
28
|
+
"uuid",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PydanticOpenApiRegistrationString(TypedDict):
|
|
33
|
+
schema_exported_variable_name: str
|
|
34
|
+
type: Literal["string"]
|
|
35
|
+
format: SupportedStringFormat
|
|
36
|
+
description: str | None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PydanticOpenApiRegistrationStrings(TypedDict):
|
|
40
|
+
schema_exported_variable_name: str
|
|
41
|
+
type: Literal["string"]
|
|
42
|
+
formats: list[SupportedStringFormat]
|
|
43
|
+
description: str | None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class PydanticOpenApiRegistrationPrimitive(TypedDict):
|
|
47
|
+
schema_exported_variable_name: str
|
|
48
|
+
type: Literal["number", "integer", "boolean"]
|
|
49
|
+
description: str | None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if TYPE_CHECKING:
|
|
53
|
+
PydanticOpenApiRegistration: TypeAlias = (
|
|
54
|
+
PydanticOpenApiRegistrationString
|
|
55
|
+
| PydanticOpenApiRegistrationStrings
|
|
56
|
+
| PydanticOpenApiRegistrationPrimitive
|
|
57
|
+
)
|
|
58
|
+
else:
|
|
59
|
+
PydanticOpenApiRegistration = dict[str, Any]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class PydanticSchemaRegistry:
|
|
63
|
+
def __init__(self) -> None:
|
|
64
|
+
self._string_format_to_name: dict[str, str] = {}
|
|
65
|
+
self._primitive_type_to_name: dict[str, str] = {}
|
|
66
|
+
self._schema_ids: dict[int, PydanticOpenApiRegistration] = {}
|
|
67
|
+
|
|
68
|
+
def register(self, schema: Any, registration: PydanticOpenApiRegistration) -> None:
|
|
69
|
+
reg_type = registration["type"]
|
|
70
|
+
registration_dict = cast(dict[str, Any], registration)
|
|
71
|
+
|
|
72
|
+
if reg_type == "string":
|
|
73
|
+
format_value = registration_dict.get("format")
|
|
74
|
+
if isinstance(format_value, str):
|
|
75
|
+
self._register_string_format(format_value, registration)
|
|
76
|
+
self._schema_ids[id(schema)] = registration
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
format_values = registration_dict.get("formats")
|
|
80
|
+
if isinstance(format_values, list):
|
|
81
|
+
for format_item in format_values:
|
|
82
|
+
if isinstance(format_item, str):
|
|
83
|
+
self._register_string_format(format_item, registration)
|
|
84
|
+
self._schema_ids[id(schema)] = registration
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
if reg_type in {"number", "integer", "boolean"}:
|
|
88
|
+
name = registration["schema_exported_variable_name"]
|
|
89
|
+
existing = self._primitive_type_to_name.get(reg_type)
|
|
90
|
+
if existing and existing != name:
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"duplicate Pydantic OpenAPI registration for type '{reg_type}'",
|
|
93
|
+
)
|
|
94
|
+
self._primitive_type_to_name[reg_type] = name
|
|
95
|
+
self._schema_ids[id(schema)] = registration
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
raise ValueError("unsupported registration type")
|
|
99
|
+
|
|
100
|
+
def _register_string_format(
|
|
101
|
+
self,
|
|
102
|
+
fmt: SupportedStringFormat | str,
|
|
103
|
+
registration: PydanticOpenApiRegistration,
|
|
104
|
+
) -> None:
|
|
105
|
+
if fmt not in SUPPORTED_STRING_FORMATS:
|
|
106
|
+
raise ValueError(f"unsupported string format registration: {fmt!r}")
|
|
107
|
+
name = registration["schema_exported_variable_name"]
|
|
108
|
+
existing = self._string_format_to_name.get(fmt)
|
|
109
|
+
if existing and existing != name:
|
|
110
|
+
raise ValueError(
|
|
111
|
+
f"duplicate Pydantic OpenAPI registration for (type, format)=('string', '{fmt}')",
|
|
112
|
+
)
|
|
113
|
+
self._string_format_to_name[fmt] = name
|
|
114
|
+
|
|
115
|
+
def clear(self) -> None:
|
|
116
|
+
self._string_format_to_name.clear()
|
|
117
|
+
self._primitive_type_to_name.clear()
|
|
118
|
+
self._schema_ids.clear()
|
|
119
|
+
|
|
120
|
+
def get_schema_exported_variable_name_for_string_format(
|
|
121
|
+
self, format_value: SupportedStringFormat | str
|
|
122
|
+
) -> str | None:
|
|
123
|
+
if format_value not in SUPPORTED_STRING_FORMATS:
|
|
124
|
+
return None
|
|
125
|
+
return self._string_format_to_name.get(format_value)
|
|
126
|
+
|
|
127
|
+
def get_schema_exported_variable_name_for_primitive_type(
|
|
128
|
+
self, type_value: Literal["number", "integer", "boolean"]
|
|
129
|
+
) -> str | None:
|
|
130
|
+
return self._primitive_type_to_name.get(type_value)
|
|
131
|
+
|
|
132
|
+
def is_registered(self, schema: Any) -> bool:
|
|
133
|
+
return id(schema) in self._schema_ids
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
schema_registry = PydanticSchemaRegistry()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def register_pydantic_type_to_openapi_schema(
|
|
140
|
+
schema: Any,
|
|
141
|
+
registration: PydanticOpenApiRegistration,
|
|
142
|
+
) -> None:
|
|
143
|
+
schema_registry.register(schema, registration)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def clear_pydantic_schema_registry() -> None:
|
|
147
|
+
schema_registry.clear()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def get_schema_exported_variable_name_for_string_format(
|
|
151
|
+
format_value: SupportedStringFormat | str,
|
|
152
|
+
) -> str | None:
|
|
153
|
+
return schema_registry.get_schema_exported_variable_name_for_string_format(
|
|
154
|
+
format_value,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def get_schema_exported_variable_name_for_primitive_type(
|
|
159
|
+
type_value: Literal["number", "integer", "boolean"],
|
|
160
|
+
) -> str | None:
|
|
161
|
+
return schema_registry.get_schema_exported_variable_name_for_primitive_type(type_value)
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
from urllib.parse import unquote
|
|
6
|
+
|
|
7
|
+
from .registry import (
|
|
8
|
+
get_schema_exported_variable_name_for_primitive_type,
|
|
9
|
+
get_schema_exported_variable_name_for_string_format,
|
|
10
|
+
)
|
|
11
|
+
from .type_render import format_openapi_metadata
|
|
12
|
+
from .types import AnySchema
|
|
13
|
+
from .types.array import convert_openapi_array_to_pydantic
|
|
14
|
+
from .types.boolean import convert_openapi_boolean_to_pydantic
|
|
15
|
+
from .types.intersection import convert_openapi_intersection_to_pydantic
|
|
16
|
+
from .types.number import convert_openapi_number_to_pydantic
|
|
17
|
+
from .types.object import convert_openapi_object_to_pydantic
|
|
18
|
+
from .types.string import convert_openapi_string_to_pydantic
|
|
19
|
+
from .types.union import convert_openapi_union_to_pydantic
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(slots=True)
|
|
23
|
+
class RenderContext:
|
|
24
|
+
root_model_names: set[str] = field(default_factory=set)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def decode_component_ref(ref: str) -> str:
|
|
28
|
+
if ref.startswith("#/components/schemas/"):
|
|
29
|
+
return unquote(ref.split("#/components/schemas/")[1])
|
|
30
|
+
return ref
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def is_freeform_object_schema(schema: AnySchema) -> bool:
|
|
34
|
+
if not isinstance(schema, dict):
|
|
35
|
+
return False
|
|
36
|
+
if schema.get("type") != "object" and "properties" not in schema:
|
|
37
|
+
return False
|
|
38
|
+
return not schema.get("properties") and schema.get("additionalProperties") is not False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def is_object_model_schema(schema: AnySchema) -> bool:
|
|
42
|
+
if not isinstance(schema, dict):
|
|
43
|
+
return False
|
|
44
|
+
properties = schema.get("properties")
|
|
45
|
+
if schema.get("type") == "object" or properties is not None:
|
|
46
|
+
if isinstance(properties, dict) and properties:
|
|
47
|
+
return True
|
|
48
|
+
return schema.get("additionalProperties") is False
|
|
49
|
+
|
|
50
|
+
parts = schema.get("allOf")
|
|
51
|
+
if not isinstance(parts, list) or not parts:
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
saw_object_part = False
|
|
55
|
+
for part in parts:
|
|
56
|
+
if not isinstance(part, dict):
|
|
57
|
+
return False
|
|
58
|
+
if isinstance(part.get("$ref"), str):
|
|
59
|
+
saw_object_part = True
|
|
60
|
+
continue
|
|
61
|
+
if part.get("type") == "object" or "properties" in part:
|
|
62
|
+
saw_object_part = True
|
|
63
|
+
continue
|
|
64
|
+
return False
|
|
65
|
+
return saw_object_part
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def needs_named_model(schema: AnySchema) -> bool:
|
|
69
|
+
return is_object_model_schema(schema)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def schema_to_type_expr(
|
|
73
|
+
schema: AnySchema | None,
|
|
74
|
+
*,
|
|
75
|
+
context: RenderContext | None = None,
|
|
76
|
+
) -> str:
|
|
77
|
+
if not isinstance(schema, dict):
|
|
78
|
+
return "Any"
|
|
79
|
+
|
|
80
|
+
render_context = context or RenderContext()
|
|
81
|
+
|
|
82
|
+
if isinstance(schema.get("$ref"), str):
|
|
83
|
+
ref = schema["$ref"]
|
|
84
|
+
if not ref.startswith("#/components/schemas/"):
|
|
85
|
+
return wrap_nullable("Any", schema)
|
|
86
|
+
expr = decode_component_ref(ref)
|
|
87
|
+
return wrap_nullable(expr, schema)
|
|
88
|
+
|
|
89
|
+
if isinstance(schema.get("oneOf"), list):
|
|
90
|
+
expr = convert_openapi_union_to_pydantic(
|
|
91
|
+
schema,
|
|
92
|
+
lambda child: schema_to_type_expr(child, context=render_context),
|
|
93
|
+
)
|
|
94
|
+
return wrap_nullable(expr, schema)
|
|
95
|
+
|
|
96
|
+
if isinstance(schema.get("anyOf"), list):
|
|
97
|
+
expr = convert_openapi_union_to_pydantic(
|
|
98
|
+
{"oneOf": schema["anyOf"], "discriminator": schema.get("discriminator")},
|
|
99
|
+
lambda child: schema_to_type_expr(child, context=render_context),
|
|
100
|
+
)
|
|
101
|
+
return wrap_nullable(expr, schema)
|
|
102
|
+
|
|
103
|
+
if isinstance(schema.get("allOf"), list):
|
|
104
|
+
expr = convert_openapi_intersection_to_pydantic(
|
|
105
|
+
schema,
|
|
106
|
+
lambda child: schema_to_type_expr(child, context=render_context),
|
|
107
|
+
)
|
|
108
|
+
return wrap_nullable(expr, schema)
|
|
109
|
+
|
|
110
|
+
schema_type = schema.get("type")
|
|
111
|
+
|
|
112
|
+
if schema_type == "string":
|
|
113
|
+
expr = convert_openapi_string_to_pydantic(schema)
|
|
114
|
+
return wrap_nullable(expr, schema)
|
|
115
|
+
|
|
116
|
+
if schema_type in {"number", "integer"}:
|
|
117
|
+
expr = convert_openapi_number_to_pydantic(schema)
|
|
118
|
+
return wrap_nullable(expr, schema)
|
|
119
|
+
|
|
120
|
+
if schema_type == "boolean":
|
|
121
|
+
expr = convert_openapi_boolean_to_pydantic(schema)
|
|
122
|
+
return wrap_nullable(expr, schema)
|
|
123
|
+
|
|
124
|
+
if schema_type == "array":
|
|
125
|
+
expr = convert_openapi_array_to_pydantic(
|
|
126
|
+
schema,
|
|
127
|
+
lambda child: schema_to_type_expr(child, context=render_context),
|
|
128
|
+
)
|
|
129
|
+
return wrap_nullable(expr, schema)
|
|
130
|
+
|
|
131
|
+
if schema_type == "object" or "properties" in schema:
|
|
132
|
+
expr = convert_openapi_object_to_pydantic(
|
|
133
|
+
schema,
|
|
134
|
+
lambda child: schema_to_type_expr(child, context=render_context),
|
|
135
|
+
)
|
|
136
|
+
return wrap_nullable(expr, schema)
|
|
137
|
+
|
|
138
|
+
if isinstance(schema.get("enum"), list):
|
|
139
|
+
values = ", ".join(repr(value) for value in schema["enum"])
|
|
140
|
+
expr = f"Literal[{values}]"
|
|
141
|
+
return wrap_nullable(expr, schema)
|
|
142
|
+
|
|
143
|
+
return wrap_nullable("Any", schema)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def wrap_nullable(expr: str, schema: dict[str, Any]) -> str:
|
|
147
|
+
if schema.get("nullable") is True:
|
|
148
|
+
return f"Optional[{expr}]"
|
|
149
|
+
return expr
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def registered_output_alias(schema: AnySchema, context: RenderContext | None = None) -> str | None:
|
|
153
|
+
if not isinstance(schema, dict):
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
if schema.get("type") == "string" and isinstance(schema.get("format"), str):
|
|
157
|
+
custom = get_schema_exported_variable_name_for_string_format(schema["format"])
|
|
158
|
+
if custom:
|
|
159
|
+
return custom
|
|
160
|
+
|
|
161
|
+
primitive_type = schema.get("type")
|
|
162
|
+
if primitive_type in {"number", "integer", "boolean"}:
|
|
163
|
+
custom = get_schema_exported_variable_name_for_primitive_type(primitive_type)
|
|
164
|
+
if custom:
|
|
165
|
+
return custom
|
|
166
|
+
|
|
167
|
+
if context is None:
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
if isinstance(schema.get("$ref"), str):
|
|
171
|
+
name = decode_component_ref(schema["$ref"])
|
|
172
|
+
if name in context.root_model_names:
|
|
173
|
+
return name
|
|
174
|
+
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def root_model_annotation(type_expr: str) -> str:
|
|
179
|
+
return f"RootModel[{type_expr}]"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def extra_field_annotation(value_type: str) -> str:
|
|
183
|
+
return f"dict[str, {value_type}]"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
__all__ = [
|
|
187
|
+
"RenderContext",
|
|
188
|
+
"decode_component_ref",
|
|
189
|
+
"extra_field_annotation",
|
|
190
|
+
"format_openapi_metadata",
|
|
191
|
+
"is_freeform_object_schema",
|
|
192
|
+
"is_object_model_schema",
|
|
193
|
+
"needs_named_model",
|
|
194
|
+
"registered_output_alias",
|
|
195
|
+
"root_model_annotation",
|
|
196
|
+
"schema_to_type_expr",
|
|
197
|
+
]
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, cast
|
|
5
|
+
|
|
6
|
+
from .types import AnySchema
|
|
7
|
+
|
|
8
|
+
HttpMethod = str
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class RouteParameter:
|
|
13
|
+
name: str
|
|
14
|
+
location: str
|
|
15
|
+
required: bool
|
|
16
|
+
schema: AnySchema
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class RouteInfo:
|
|
21
|
+
path: str
|
|
22
|
+
method: HttpMethod
|
|
23
|
+
parameters: list[RouteParameter]
|
|
24
|
+
request_body: AnySchema | None
|
|
25
|
+
responses: dict[str, AnySchema]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class RouteSchemaNames:
|
|
30
|
+
params_schema_name: str | None = None
|
|
31
|
+
query_schema_name: str | None = None
|
|
32
|
+
headers_schema_name: str | None = None
|
|
33
|
+
body_schema_name: str | None = None
|
|
34
|
+
response_schema_name: str | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _to_upper_method(method: str) -> HttpMethod:
|
|
38
|
+
upper = method.upper()
|
|
39
|
+
if upper in {"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"}:
|
|
40
|
+
return upper
|
|
41
|
+
return "GET"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _to_pascal_case(value: str) -> str:
|
|
45
|
+
parts = [part for part in "".join(c if c.isalnum() else " " for c in value).split()]
|
|
46
|
+
return "".join(part[:1].upper() + part[1:].lower() for part in parts)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def build_route_path_name(path: str) -> str:
|
|
50
|
+
path_parts = []
|
|
51
|
+
for part in path.split("/"):
|
|
52
|
+
if not part:
|
|
53
|
+
continue
|
|
54
|
+
if part.startswith("{") and part.endswith("}"):
|
|
55
|
+
path_parts.append(part[1:-1])
|
|
56
|
+
else:
|
|
57
|
+
path_parts.append(part)
|
|
58
|
+
return "".join(_to_pascal_case(part) for part in path_parts)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def build_route_schema_name(path: str, method: HttpMethod, suffix: str) -> str:
|
|
62
|
+
method_prefix = method[:1] + method[1:].lower()
|
|
63
|
+
return "".join([method_prefix, build_route_path_name(path), suffix])
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def parse_openapi_paths(openapi: dict[str, Any]) -> list[RouteInfo]:
|
|
67
|
+
paths_obj = openapi.get("paths")
|
|
68
|
+
if not isinstance(paths_obj, dict):
|
|
69
|
+
return []
|
|
70
|
+
paths = cast(dict[str, Any], paths_obj)
|
|
71
|
+
|
|
72
|
+
routes: list[RouteInfo] = []
|
|
73
|
+
methods = ["get", "post", "put", "patch", "delete", "head", "options"]
|
|
74
|
+
|
|
75
|
+
for path, path_item_obj in paths.items():
|
|
76
|
+
if not isinstance(path_item_obj, dict):
|
|
77
|
+
continue
|
|
78
|
+
path_item = cast(dict[str, Any], path_item_obj)
|
|
79
|
+
|
|
80
|
+
for method in methods:
|
|
81
|
+
operation_obj = path_item.get(method)
|
|
82
|
+
if not isinstance(operation_obj, dict):
|
|
83
|
+
continue
|
|
84
|
+
operation = cast(dict[str, Any], operation_obj)
|
|
85
|
+
|
|
86
|
+
parameters: list[RouteParameter] = []
|
|
87
|
+
responses: dict[str, AnySchema] = {}
|
|
88
|
+
|
|
89
|
+
for params_src in (path_item.get("parameters"), operation.get("parameters")):
|
|
90
|
+
if isinstance(params_src, list):
|
|
91
|
+
for param in params_src:
|
|
92
|
+
if isinstance(param, dict):
|
|
93
|
+
parameters.append(
|
|
94
|
+
RouteParameter(
|
|
95
|
+
name=str(param.get("name", "")),
|
|
96
|
+
location=str(param.get("in", "query")),
|
|
97
|
+
required=bool(param.get("required")),
|
|
98
|
+
schema=param.get("schema", {}),
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
request_body = None
|
|
103
|
+
rb = operation.get("requestBody")
|
|
104
|
+
if isinstance(rb, dict):
|
|
105
|
+
content = rb.get("content")
|
|
106
|
+
if isinstance(content, dict):
|
|
107
|
+
json_content = content.get("application/json")
|
|
108
|
+
if isinstance(json_content, dict):
|
|
109
|
+
request_body = json_content.get("schema", {})
|
|
110
|
+
|
|
111
|
+
responses_obj = operation.get("responses")
|
|
112
|
+
if isinstance(responses_obj, dict):
|
|
113
|
+
for status_code, response in responses_obj.items():
|
|
114
|
+
if not isinstance(response, dict):
|
|
115
|
+
continue
|
|
116
|
+
content = response.get("content")
|
|
117
|
+
if isinstance(content, dict):
|
|
118
|
+
json_content = content.get("application/json")
|
|
119
|
+
if isinstance(json_content, dict):
|
|
120
|
+
schema = json_content.get("schema")
|
|
121
|
+
if schema is not None:
|
|
122
|
+
responses[str(status_code)] = schema
|
|
123
|
+
|
|
124
|
+
routes.append(
|
|
125
|
+
RouteInfo(
|
|
126
|
+
path=str(path),
|
|
127
|
+
method=_to_upper_method(method),
|
|
128
|
+
parameters=parameters,
|
|
129
|
+
request_body=request_body,
|
|
130
|
+
responses=responses,
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return routes
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def generate_route_schema_names(route: RouteInfo) -> RouteSchemaNames:
|
|
138
|
+
path_params = [p for p in route.parameters if p.location == "path"]
|
|
139
|
+
query_params = [p for p in route.parameters if p.location == "query"]
|
|
140
|
+
header_params = [p for p in route.parameters if p.location == "header"]
|
|
141
|
+
success_statuses = [status for status in route.responses if status.startswith("2")]
|
|
142
|
+
|
|
143
|
+
result = RouteSchemaNames()
|
|
144
|
+
if success_statuses:
|
|
145
|
+
result.response_schema_name = build_route_schema_name(
|
|
146
|
+
route.path,
|
|
147
|
+
route.method,
|
|
148
|
+
"Response",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if path_params:
|
|
152
|
+
result.params_schema_name = build_route_schema_name(
|
|
153
|
+
route.path,
|
|
154
|
+
route.method,
|
|
155
|
+
"Params",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
if query_params:
|
|
159
|
+
result.query_schema_name = build_route_schema_name(
|
|
160
|
+
route.path,
|
|
161
|
+
route.method,
|
|
162
|
+
"Query",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if header_params:
|
|
166
|
+
result.headers_schema_name = build_route_schema_name(
|
|
167
|
+
route.path,
|
|
168
|
+
route.method,
|
|
169
|
+
"Headers",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if route.request_body is not None:
|
|
173
|
+
result.body_schema_name = build_route_schema_name(
|
|
174
|
+
route.path,
|
|
175
|
+
route.method,
|
|
176
|
+
"Body",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return result
|