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.
@@ -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