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,141 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, TypedDict
|
|
6
|
+
|
|
7
|
+
from .types import AnySchema
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _sort_object_deep(obj: Any) -> Any:
|
|
11
|
+
if obj is None or not isinstance(obj, (dict, list)):
|
|
12
|
+
return obj
|
|
13
|
+
if isinstance(obj, list):
|
|
14
|
+
return [_sort_object_deep(item) for item in obj]
|
|
15
|
+
sorted_obj: dict[str, Any] = {}
|
|
16
|
+
for key in sorted(obj.keys()):
|
|
17
|
+
sorted_obj[key] = _sort_object_deep(obj[key])
|
|
18
|
+
return sorted_obj
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_schema_fingerprint(schema: AnySchema) -> str:
|
|
22
|
+
return json.dumps(_sort_object_deep(schema), sort_keys=True)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class SchemaRegistry:
|
|
27
|
+
fingerprint_to_name: dict[str, str]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def create_schema_registry() -> SchemaRegistry:
|
|
31
|
+
return SchemaRegistry(fingerprint_to_name={})
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class RegisterSchemaResult:
|
|
36
|
+
is_new: bool
|
|
37
|
+
canonical_name: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def register_schema(
|
|
41
|
+
registry: SchemaRegistry,
|
|
42
|
+
name: str,
|
|
43
|
+
schema: AnySchema,
|
|
44
|
+
) -> RegisterSchemaResult:
|
|
45
|
+
fingerprint = get_schema_fingerprint(schema)
|
|
46
|
+
existing = registry.fingerprint_to_name.get(fingerprint)
|
|
47
|
+
if existing:
|
|
48
|
+
return RegisterSchemaResult(is_new=False, canonical_name=existing)
|
|
49
|
+
registry.fingerprint_to_name[fingerprint] = name
|
|
50
|
+
return RegisterSchemaResult(is_new=True, canonical_name=name)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def pre_register_schema(
|
|
54
|
+
registry: SchemaRegistry,
|
|
55
|
+
name: str,
|
|
56
|
+
fingerprint: str,
|
|
57
|
+
) -> None:
|
|
58
|
+
registry.fingerprint_to_name[fingerprint] = name
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def extract_error_code(schema: AnySchema) -> str | None:
|
|
62
|
+
props = schema.get("properties")
|
|
63
|
+
if not isinstance(props, dict):
|
|
64
|
+
return None
|
|
65
|
+
error_obj = props.get("error")
|
|
66
|
+
if not isinstance(error_obj, dict):
|
|
67
|
+
return None
|
|
68
|
+
error_props = error_obj.get("properties")
|
|
69
|
+
if not isinstance(error_props, dict):
|
|
70
|
+
return None
|
|
71
|
+
code_schema = error_props.get("code")
|
|
72
|
+
if not isinstance(code_schema, dict):
|
|
73
|
+
return None
|
|
74
|
+
code_enum = code_schema.get("enum")
|
|
75
|
+
if isinstance(code_enum, list) and len(code_enum) == 1:
|
|
76
|
+
return str(code_enum[0])
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def error_code_to_pascal_case(code: str) -> str:
|
|
81
|
+
return "".join(part.capitalize() for part in code.split("_"))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def generate_common_error_schema_name(error_code: str) -> str:
|
|
85
|
+
return f"{error_code_to_pascal_case(error_code)}Error"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class CommonSchema:
|
|
90
|
+
name: str
|
|
91
|
+
schema: AnySchema
|
|
92
|
+
fingerprint: str
|
|
93
|
+
count: int
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class NamedSchema(TypedDict):
|
|
97
|
+
name: str
|
|
98
|
+
schema: AnySchema
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def find_common_schemas(
|
|
102
|
+
schemas: list[NamedSchema],
|
|
103
|
+
min_count: int = 2,
|
|
104
|
+
) -> list[CommonSchema]:
|
|
105
|
+
fingerprints: dict[str, dict[str, Any]] = {}
|
|
106
|
+
|
|
107
|
+
for item in schemas:
|
|
108
|
+
name = item["name"]
|
|
109
|
+
schema = item["schema"]
|
|
110
|
+
fingerprint = get_schema_fingerprint(schema)
|
|
111
|
+
existing = fingerprints.get(fingerprint)
|
|
112
|
+
if existing:
|
|
113
|
+
existing["names"].append(name)
|
|
114
|
+
else:
|
|
115
|
+
fingerprints[fingerprint] = {
|
|
116
|
+
"schema": schema,
|
|
117
|
+
"names": [name],
|
|
118
|
+
"error_code": extract_error_code(schema),
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
common: list[CommonSchema] = []
|
|
122
|
+
for fingerprint, data in fingerprints.items():
|
|
123
|
+
names = data["names"]
|
|
124
|
+
if len(names) < min_count:
|
|
125
|
+
continue
|
|
126
|
+
error_code = data["error_code"]
|
|
127
|
+
name = (
|
|
128
|
+
generate_common_error_schema_name(error_code)
|
|
129
|
+
if isinstance(error_code, str)
|
|
130
|
+
else names[0]
|
|
131
|
+
)
|
|
132
|
+
common.append(
|
|
133
|
+
CommonSchema(
|
|
134
|
+
name=name,
|
|
135
|
+
schema=data["schema"],
|
|
136
|
+
fingerprint=fingerprint,
|
|
137
|
+
count=len(names),
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return sorted(common, key=lambda item: item.count, reverse=True)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .emission import openapi_to_pydantic_code
|
|
4
|
+
from .rendering import schema_to_type_expr
|
|
5
|
+
from .types import AnySchema
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def convert_schema_to_pydantic_string(schema: AnySchema | None) -> str:
|
|
9
|
+
return schema_to_type_expr(schema)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
__all__ = ["convert_schema_to_pydantic_string", "openapi_to_pydantic_code"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def format_openapi_metadata(meta: dict[str, Any]) -> str:
|
|
7
|
+
ordered = {key: meta[key] for key in sorted(meta)}
|
|
8
|
+
return f"json_schema_extra={{'openapi': {ordered!r}}}"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def wrap_annotated(base: str, metadata: list[str]) -> str:
|
|
12
|
+
if not metadata:
|
|
13
|
+
return base
|
|
14
|
+
parts = ", ".join(metadata)
|
|
15
|
+
return f"Annotated[{base}, {parts}]"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
from ..type_render import format_openapi_metadata, wrap_annotated
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def convert_openapi_array_to_pydantic(
|
|
9
|
+
schema: dict[str, Any],
|
|
10
|
+
convert_schema: Callable[[dict[str, Any]], str],
|
|
11
|
+
) -> str:
|
|
12
|
+
items = schema.get("items")
|
|
13
|
+
item_type = "Any"
|
|
14
|
+
if isinstance(items, dict):
|
|
15
|
+
item_type = convert_schema(items)
|
|
16
|
+
|
|
17
|
+
base = f"list[{item_type}]"
|
|
18
|
+
|
|
19
|
+
min_items = schema.get("minItems")
|
|
20
|
+
max_items = schema.get("maxItems")
|
|
21
|
+
|
|
22
|
+
field_args: list[str] = []
|
|
23
|
+
if isinstance(min_items, int):
|
|
24
|
+
field_args.append(f"min_length={min_items}")
|
|
25
|
+
if isinstance(max_items, int):
|
|
26
|
+
field_args.append(f"max_length={max_items}")
|
|
27
|
+
|
|
28
|
+
meta: dict[str, Any] = {}
|
|
29
|
+
if isinstance(min_items, int):
|
|
30
|
+
meta["minItems"] = min_items
|
|
31
|
+
if isinstance(max_items, int):
|
|
32
|
+
meta["maxItems"] = max_items
|
|
33
|
+
if meta:
|
|
34
|
+
field_args.append(format_openapi_metadata(meta))
|
|
35
|
+
|
|
36
|
+
if field_args:
|
|
37
|
+
return wrap_annotated(base, [f"Field({', '.join(field_args)})"])
|
|
38
|
+
|
|
39
|
+
return base
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..registry import get_schema_exported_variable_name_for_primitive_type
|
|
6
|
+
from ..type_render import wrap_annotated
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def convert_openapi_boolean_to_pydantic(schema: dict[str, Any]) -> str:
|
|
10
|
+
_ = schema
|
|
11
|
+
custom = get_schema_exported_variable_name_for_primitive_type("boolean")
|
|
12
|
+
if custom:
|
|
13
|
+
return custom
|
|
14
|
+
return wrap_annotated("bool", ["Field(strict=True)"])
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def convert_openapi_intersection_to_pydantic(
|
|
7
|
+
schema: dict[str, Any],
|
|
8
|
+
convert_schema: Callable[[dict[str, Any]], str],
|
|
9
|
+
) -> str:
|
|
10
|
+
items = schema.get("allOf")
|
|
11
|
+
if not isinstance(items, list):
|
|
12
|
+
return "Any"
|
|
13
|
+
|
|
14
|
+
parts = [convert_schema(item) for item in items if isinstance(item, dict)]
|
|
15
|
+
if not parts:
|
|
16
|
+
return "Any"
|
|
17
|
+
if len(parts) == 1:
|
|
18
|
+
return parts[0]
|
|
19
|
+
|
|
20
|
+
return f"all_of({', '.join(parts)})"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..registry import get_schema_exported_variable_name_for_primitive_type
|
|
6
|
+
from ..type_render import format_openapi_metadata, wrap_annotated
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def convert_openapi_number_to_pydantic(schema: dict[str, Any]) -> str:
|
|
10
|
+
if isinstance(schema.get("enum"), list):
|
|
11
|
+
values = ", ".join(repr(value) for value in schema["enum"])
|
|
12
|
+
return f"Literal[{values}]"
|
|
13
|
+
|
|
14
|
+
schema_type = schema.get("type")
|
|
15
|
+
if schema_type in {"number", "integer", "boolean"}:
|
|
16
|
+
custom = get_schema_exported_variable_name_for_primitive_type(schema_type)
|
|
17
|
+
if custom:
|
|
18
|
+
return custom
|
|
19
|
+
|
|
20
|
+
base = "int" if schema.get("type") == "integer" else "float"
|
|
21
|
+
|
|
22
|
+
minimum = schema.get("minimum")
|
|
23
|
+
maximum = schema.get("maximum")
|
|
24
|
+
|
|
25
|
+
field_args: list[str] = ["strict=True"]
|
|
26
|
+
if isinstance(minimum, (int, float)):
|
|
27
|
+
field_args.append(f"ge={minimum}")
|
|
28
|
+
if isinstance(maximum, (int, float)):
|
|
29
|
+
field_args.append(f"le={maximum}")
|
|
30
|
+
|
|
31
|
+
meta: dict[str, Any] = {}
|
|
32
|
+
if isinstance(minimum, (int, float)):
|
|
33
|
+
meta["minimum"] = minimum
|
|
34
|
+
if isinstance(maximum, (int, float)):
|
|
35
|
+
meta["maximum"] = maximum
|
|
36
|
+
if meta:
|
|
37
|
+
field_args.append(format_openapi_metadata(meta))
|
|
38
|
+
|
|
39
|
+
return wrap_annotated(base, [f"Field({', '.join(field_args)})"])
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def convert_openapi_object_to_pydantic(
|
|
7
|
+
schema: dict[str, Any],
|
|
8
|
+
convert_schema: Callable[[dict[str, Any]], str],
|
|
9
|
+
) -> str:
|
|
10
|
+
additional = schema.get("additionalProperties")
|
|
11
|
+
|
|
12
|
+
if isinstance(additional, dict):
|
|
13
|
+
return f"dict[str, {convert_schema(additional)}]"
|
|
14
|
+
|
|
15
|
+
return "dict[str, Any]"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..registry import get_schema_exported_variable_name_for_string_format
|
|
6
|
+
from ..type_render import format_openapi_metadata, wrap_annotated
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def convert_openapi_string_to_pydantic(schema: dict[str, Any]) -> str:
|
|
10
|
+
if isinstance(schema.get("enum"), list):
|
|
11
|
+
values = ", ".join(repr(value) for value in schema["enum"])
|
|
12
|
+
return f"Literal[{values}]"
|
|
13
|
+
|
|
14
|
+
fmt = schema.get("format")
|
|
15
|
+
if isinstance(fmt, str) and fmt:
|
|
16
|
+
custom_schema = get_schema_exported_variable_name_for_string_format(fmt)
|
|
17
|
+
if custom_schema:
|
|
18
|
+
return custom_schema
|
|
19
|
+
else:
|
|
20
|
+
fmt = None
|
|
21
|
+
|
|
22
|
+
base = "str"
|
|
23
|
+
if fmt == "email":
|
|
24
|
+
base = "EmailStr"
|
|
25
|
+
elif fmt in {"url", "uri"}:
|
|
26
|
+
base = "AnyUrl"
|
|
27
|
+
elif fmt == "uuid":
|
|
28
|
+
base = "UUID"
|
|
29
|
+
|
|
30
|
+
min_length = schema.get("minLength")
|
|
31
|
+
max_length = schema.get("maxLength")
|
|
32
|
+
pattern = schema.get("pattern")
|
|
33
|
+
pattern_is_explicit = isinstance(pattern, str)
|
|
34
|
+
|
|
35
|
+
if fmt == "color-hex" and not pattern_is_explicit:
|
|
36
|
+
pattern = "^[a-fA-F0-9]{6}$"
|
|
37
|
+
|
|
38
|
+
field_args: list[str] = []
|
|
39
|
+
if base == "str":
|
|
40
|
+
field_args.append("strict=True")
|
|
41
|
+
if isinstance(min_length, int):
|
|
42
|
+
field_args.append(f"min_length={min_length}")
|
|
43
|
+
if isinstance(max_length, int):
|
|
44
|
+
field_args.append(f"max_length={max_length}")
|
|
45
|
+
if isinstance(pattern, str):
|
|
46
|
+
field_args.append(f"pattern={pattern!r}")
|
|
47
|
+
|
|
48
|
+
meta: dict[str, Any] = {}
|
|
49
|
+
if isinstance(fmt, str):
|
|
50
|
+
meta["format"] = fmt
|
|
51
|
+
if isinstance(pattern, str) and pattern_is_explicit:
|
|
52
|
+
meta["pattern"] = pattern
|
|
53
|
+
if isinstance(min_length, int):
|
|
54
|
+
meta["minLength"] = min_length
|
|
55
|
+
if isinstance(max_length, int):
|
|
56
|
+
meta["maxLength"] = max_length
|
|
57
|
+
|
|
58
|
+
if meta:
|
|
59
|
+
field_args.append(format_openapi_metadata(meta))
|
|
60
|
+
|
|
61
|
+
if field_args:
|
|
62
|
+
return wrap_annotated(base, [f"Field({', '.join(field_args)})"])
|
|
63
|
+
|
|
64
|
+
return base
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
from ..type_render import format_openapi_metadata, wrap_annotated
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def convert_openapi_union_to_pydantic(
|
|
9
|
+
schema: dict[str, Any],
|
|
10
|
+
convert_schema: Callable[[dict[str, Any]], str],
|
|
11
|
+
) -> str:
|
|
12
|
+
items = schema.get("oneOf")
|
|
13
|
+
if not isinstance(items, list):
|
|
14
|
+
return "Any"
|
|
15
|
+
|
|
16
|
+
item_types = [convert_schema(item) for item in items if isinstance(item, dict)]
|
|
17
|
+
if not item_types:
|
|
18
|
+
return "Any"
|
|
19
|
+
|
|
20
|
+
union_expr = f"Union[{', '.join(item_types)}]" if len(item_types) > 1 else item_types[0]
|
|
21
|
+
|
|
22
|
+
discriminator = schema.get("discriminator")
|
|
23
|
+
field_args: list[str] = []
|
|
24
|
+
meta: dict[str, Any] = {}
|
|
25
|
+
if isinstance(discriminator, dict):
|
|
26
|
+
property_name = discriminator.get("propertyName")
|
|
27
|
+
if isinstance(property_name, str) and property_name:
|
|
28
|
+
field_args.append(f"discriminator={property_name!r}")
|
|
29
|
+
meta["discriminator"] = discriminator
|
|
30
|
+
|
|
31
|
+
if meta:
|
|
32
|
+
field_args.append(format_openapi_metadata(meta))
|
|
33
|
+
|
|
34
|
+
if field_args:
|
|
35
|
+
return wrap_annotated(union_expr, [f"Field({', '.join(field_args)})"])
|
|
36
|
+
|
|
37
|
+
return union_expr
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import keyword
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
_VALID_IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def to_pascal_case(value: str) -> str:
|
|
10
|
+
parts = re.split(r"[^A-Za-z0-9]+", value)
|
|
11
|
+
return "".join(part[:1].upper() + part[1:] for part in parts if part)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def to_snake_case(value: str) -> str:
|
|
15
|
+
value = re.sub(r"[^A-Za-z0-9]+", "_", value).strip("_")
|
|
16
|
+
if not value:
|
|
17
|
+
return "field"
|
|
18
|
+
value = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", value)
|
|
19
|
+
return value.lower()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def safe_identifier(name: str, used: set[str]) -> tuple[str, bool]:
|
|
23
|
+
candidate = to_snake_case(name)
|
|
24
|
+
if not _VALID_IDENTIFIER.match(candidate) or keyword.iskeyword(candidate):
|
|
25
|
+
candidate = f"_{candidate}"
|
|
26
|
+
base = candidate
|
|
27
|
+
idx = 1
|
|
28
|
+
while candidate in used:
|
|
29
|
+
idx += 1
|
|
30
|
+
candidate = f"{base}_{idx}"
|
|
31
|
+
used.add(candidate)
|
|
32
|
+
return candidate, candidate != name
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: alt-stack-openapi-pydantic
|
|
3
|
+
Version: 1.6.2
|
|
4
|
+
Summary: Generate Pydantic models, typed route maps, and an asyncio client from OpenAPI
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/anthonyaltieri/alt-stack
|
|
7
|
+
Project-URL: Repository, https://github.com/anthonyaltieri/alt-stack
|
|
8
|
+
Project-URL: Issues, https://github.com/anthonyaltieri/alt-stack/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Typing :: Typed
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: pydantic[email]>=2.7.0
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: alt-stack-http-client-httpx; extra == "dev"
|
|
19
|
+
Requires-Dist: pyrefly>=1.0.0; extra == "dev"
|
|
20
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
21
|
+
Requires-Dist: ruff>=0.6.0; extra == "dev"
|
|
22
|
+
Requires-Dist: ty>=0.0.78; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# `alt-stack-openapi-pydantic`
|
|
26
|
+
|
|
27
|
+
Generate Python 3.11+ Pydantic 2 models, statically typed `Request`/`Response` route maps, and an asyncio `HttpxApiClient` from OpenAPI JSON. This is the Python counterpart of `@alt-stack/zod-openapi`; the generated client runs on `alt-stack-http-client-httpx`.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
python -m pip install alt-stack-openapi-pydantic
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Consumers of a generated SDK install the runtime package instead:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
python -m pip install alt-stack-http-client-httpx
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Generate
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
alt-stack-openapi-pydantic ./openapi.json --output ./generated_types.py
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`input` may be a local JSON path or HTTP(S) URL. Use `--registry` to execute custom type mappings and `--include` to insert imports/definitions into output.
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from generated_types import User
|
|
51
|
+
|
|
52
|
+
user = User.model_validate({"id": "u_1", "name": "Ada"})
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The CLI enables route generation and emits `Request`/`Response` dictionaries whose leaves are Pydantic model classes. The dictionaries carry generated `TypedDict` annotations, so type checkers resolve literal lookups to the exact model and reject unknown paths, methods, request parts, and status codes:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from generated_types import Request, Response
|
|
59
|
+
|
|
60
|
+
Request["/users"]["POST"]["body"] # type[CreateUserBody]
|
|
61
|
+
Response["/users/{id}"]["GET"]["200"] # type[User]
|
|
62
|
+
Response["/users/{id}"]["GET"]["999"] # static error: unknown status
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Generated modules also include an asyncio `HttpxApiClient` with one typed method per route, built on `alt_stack_http_client_httpx`:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from alt_stack_http_client_httpx import ApiSuccess
|
|
69
|
+
|
|
70
|
+
from generated_types import GetUsersIdParams, HttpxApiClient, Request, Response
|
|
71
|
+
|
|
72
|
+
async with HttpxApiClient(
|
|
73
|
+
"https://api.example.com", request_map=Request, response_map=Response
|
|
74
|
+
) as client:
|
|
75
|
+
result = await client.get("/users/{id}", params=GetUsersIdParams(id="u_1"))
|
|
76
|
+
|
|
77
|
+
if isinstance(result, ApiSuccess):
|
|
78
|
+
result.body # User; unknown paths, missing params, and wrong models are static errors
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Generated modules that use non-object intersections inline a small `all_of` validator, so they depend only on `pydantic` and `alt-stack-http-client-httpx` at runtime.
|
|
82
|
+
|
|
83
|
+
## Development
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
uv run --project packages/python-pydantic-openapi --extra dev pytest packages/python-pydantic-openapi/tests
|
|
87
|
+
uv run --project packages/python-pydantic-openapi --extra dev ruff check packages/python-pydantic-openapi
|
|
88
|
+
uv run --project packages/python-pydantic-openapi --extra dev ty check packages/python-pydantic-openapi
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Documentation
|
|
92
|
+
|
|
93
|
+
- [Code generation Quickstart](../../apps/docs/docs/codegen/quickstart.md)
|
|
94
|
+
- [Common Patterns](../../apps/docs/docs/codegen/common-patterns.md)
|
|
95
|
+
- [Python/Pydantic API Documentation](../../apps/docs/docs/codegen/api/pydantic-openapi.md)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
alt_stack_openapi_pydantic/__init__.py,sha256=fFC0seuk0nMLp7ocTUuHyvTR3RJoPK8dLCPDWQ7a8_s,665
|
|
2
|
+
alt_stack_openapi_pydantic/cli.py,sha256=wyjMEXo4mHTcmrQzBLz0vMEopKZ152Mqi9QoRP99TGY,1909
|
|
3
|
+
alt_stack_openapi_pydantic/dependencies.py,sha256=27kkVQo8l5jSfytoXpPW6k_gJu_QAL-KSKS1B0ABGMY,3192
|
|
4
|
+
alt_stack_openapi_pydantic/emission.py,sha256=z5O1j8-q9FoZbzMdt9ib3JzrpCLO8m08-pWNr3P3tqI,27463
|
|
5
|
+
alt_stack_openapi_pydantic/lowering.py,sha256=p9fOcDPCNwNiYvsTNnxXHbF30ytx6KusPmW3Y9R-kns,3721
|
|
6
|
+
alt_stack_openapi_pydantic/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
7
|
+
alt_stack_openapi_pydantic/registry.py,sha256=11v1K_6yAKjfMp4hFTNFD7VIxV1LMNFgHbESZBoiL6k,5085
|
|
8
|
+
alt_stack_openapi_pydantic/rendering.py,sha256=Tx1YvO9npLMtmeQ8sR-UR24YqAFlyFRceNT-aDdisJw,6264
|
|
9
|
+
alt_stack_openapi_pydantic/routes.py,sha256=A_NPx6zZGKpZbTl5rIDdf7YR083_vHNg49eMRCalKpk,5814
|
|
10
|
+
alt_stack_openapi_pydantic/schema_dedup.py,sha256=nR8VrXFooeV02xDDDdRDCD5FsMYLFq2kMpdz8KqOo28,3751
|
|
11
|
+
alt_stack_openapi_pydantic/to_python.py,sha256=x4wDyXvaOV_I4UjmpeYEF_72AL3G5NGrusn1ZncJWSk,346
|
|
12
|
+
alt_stack_openapi_pydantic/type_render.py,sha256=n2j0iPy5daNPMOWQwwYo5hF3ok2vuSf0Ls8FGiVhoiI,408
|
|
13
|
+
alt_stack_openapi_pydantic/utils.py,sha256=91xMmdRwQP0WMPJSg7n2GrQq5Uqk2cLI3auq3Uq2n4g,907
|
|
14
|
+
alt_stack_openapi_pydantic/types/__init__.py,sha256=MUlvT6Woc4YFbN82M7-k7_3k5rXztfdfPw3TTNzoVwM,134
|
|
15
|
+
alt_stack_openapi_pydantic/types/array.py,sha256=KXsC_l2kz4sUMIaTIWZiZKDasYK7cuJ9yvkPNv1w8LI,1071
|
|
16
|
+
alt_stack_openapi_pydantic/types/boolean.py,sha256=otAaBJmsaZTBV-8-gVDS9OIqIaacz3hlbO5nO6PTq6o,438
|
|
17
|
+
alt_stack_openapi_pydantic/types/intersection.py,sha256=iGGVPzbJAYMhG3ktalWPgm1hbicN528ZgzoRTAnxrvI,506
|
|
18
|
+
alt_stack_openapi_pydantic/types/number.py,sha256=39lakX2P-6V1Ipf-qrY5jJ2mnSEyjBRpYDYShQ2e1IU,1323
|
|
19
|
+
alt_stack_openapi_pydantic/types/object.py,sha256=BDQyu77Ouz2M-bj7YLkNDsTxzPyg4Ap_7iMNtlr7wXg,379
|
|
20
|
+
alt_stack_openapi_pydantic/types/string.py,sha256=qNsNXCbJNmzjebkqOYUKi0tapiZ5USqCUT7Tv6fWkXE,1962
|
|
21
|
+
alt_stack_openapi_pydantic/types/types.py,sha256=2rbEHIxFMrRFHw72tUuOCim2vzIUErfJuclliUUahiQ,124
|
|
22
|
+
alt_stack_openapi_pydantic/types/union.py,sha256=qWIMXaqN4D4x2PnYEu0w-H3TOHG5VkXMZbF6-7F6Agc,1158
|
|
23
|
+
alt_stack_openapi_pydantic-1.6.2.dist-info/licenses/LICENSE,sha256=OvI3tgOAzCyCFOjcQ42mFXk6SKQTicTDrUgbqofOXGY,1072
|
|
24
|
+
alt_stack_openapi_pydantic-1.6.2.dist-info/METADATA,sha256=0imf_TzJfH8e-3ySDrSgHxeHlYFLe61cirWQgnuWrOg,3765
|
|
25
|
+
alt_stack_openapi_pydantic-1.6.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
26
|
+
alt_stack_openapi_pydantic-1.6.2.dist-info/entry_points.txt,sha256=n_oPzTHD1-LolT8dLBmubt9ajr9-A-rokIx-CpT6Nlg,83
|
|
27
|
+
alt_stack_openapi_pydantic-1.6.2.dist-info/top_level.txt,sha256=qUyCFcP-72UcOp06tPOxJLqxzRXMr7OOhenSuZKIaqo,27
|
|
28
|
+
alt_stack_openapi_pydantic-1.6.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Anthony Altieri
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
alt_stack_openapi_pydantic
|