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,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .registry import (
|
|
4
|
+
SUPPORTED_STRING_FORMATS,
|
|
5
|
+
clear_pydantic_schema_registry,
|
|
6
|
+
get_schema_exported_variable_name_for_primitive_type,
|
|
7
|
+
get_schema_exported_variable_name_for_string_format,
|
|
8
|
+
register_pydantic_type_to_openapi_schema,
|
|
9
|
+
schema_registry,
|
|
10
|
+
)
|
|
11
|
+
from .to_python import openapi_to_pydantic_code
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"SUPPORTED_STRING_FORMATS",
|
|
15
|
+
"clear_pydantic_schema_registry",
|
|
16
|
+
"get_schema_exported_variable_name_for_primitive_type",
|
|
17
|
+
"get_schema_exported_variable_name_for_string_format",
|
|
18
|
+
"openapi_to_pydantic_code",
|
|
19
|
+
"register_pydantic_type_to_openapi_schema",
|
|
20
|
+
"schema_registry",
|
|
21
|
+
]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from runpy import run_path
|
|
8
|
+
from urllib.request import urlopen
|
|
9
|
+
|
|
10
|
+
from .to_python import openapi_to_pydantic_code
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _load_schema(source: str) -> dict[str, object]:
|
|
14
|
+
if source.startswith("http://") or source.startswith("https://"):
|
|
15
|
+
with urlopen(source) as response: # nosec - user-provided URL
|
|
16
|
+
return json.loads(response.read().decode("utf-8"))
|
|
17
|
+
return json.loads(Path(source).read_text(encoding="utf-8"))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main() -> None:
|
|
21
|
+
parser = argparse.ArgumentParser(
|
|
22
|
+
prog="alt-stack-openapi-pydantic",
|
|
23
|
+
description="Generate Pydantic models from an OpenAPI schema",
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument("input", help="OpenAPI schema file path or URL")
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"-o",
|
|
28
|
+
"--output",
|
|
29
|
+
default="generated-types.py",
|
|
30
|
+
help="Output file path",
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"-r",
|
|
34
|
+
"--registry",
|
|
35
|
+
help="Registry file that registers custom schemas",
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"-i",
|
|
39
|
+
"--include",
|
|
40
|
+
help="Python file to include at top of generated output",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
args = parser.parse_args()
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
if args.registry:
|
|
47
|
+
run_path(args.registry)
|
|
48
|
+
|
|
49
|
+
include_content = None
|
|
50
|
+
if args.include:
|
|
51
|
+
include_content = Path(args.include).read_text(encoding="utf-8")
|
|
52
|
+
|
|
53
|
+
schema = _load_schema(args.input)
|
|
54
|
+
custom_lines = [include_content] if include_content else None
|
|
55
|
+
code = openapi_to_pydantic_code(
|
|
56
|
+
schema,
|
|
57
|
+
custom_import_lines=custom_lines,
|
|
58
|
+
options={"include_routes": True},
|
|
59
|
+
)
|
|
60
|
+
Path(args.output).write_text(code, encoding="utf-8")
|
|
61
|
+
except Exception as exc: # noqa: BLE001
|
|
62
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == "__main__":
|
|
67
|
+
main()
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from urllib.parse import unquote
|
|
5
|
+
|
|
6
|
+
from .types import AnySchema
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def extract_schema_dependencies(schema: AnySchema | None) -> list[str]:
|
|
10
|
+
dependencies: set[str] = set()
|
|
11
|
+
visited: set[int] = set()
|
|
12
|
+
|
|
13
|
+
def traverse(obj: Any) -> None:
|
|
14
|
+
if obj is None or not isinstance(obj, (dict, list)):
|
|
15
|
+
return
|
|
16
|
+
|
|
17
|
+
obj_id = id(obj)
|
|
18
|
+
if obj_id in visited:
|
|
19
|
+
return
|
|
20
|
+
visited.add(obj_id)
|
|
21
|
+
|
|
22
|
+
if isinstance(obj, dict) and isinstance(obj.get("$ref"), str):
|
|
23
|
+
match = obj["$ref"].split("#/components/schemas/")
|
|
24
|
+
if len(match) == 2 and match[1]:
|
|
25
|
+
dependencies.add(unquote(match[1]))
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
if isinstance(obj, list):
|
|
29
|
+
for item in obj:
|
|
30
|
+
traverse(item)
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
if isinstance(obj, dict):
|
|
34
|
+
props = obj.get("properties")
|
|
35
|
+
if isinstance(props, dict):
|
|
36
|
+
for value in props.values():
|
|
37
|
+
traverse(value)
|
|
38
|
+
|
|
39
|
+
for key in [
|
|
40
|
+
"items",
|
|
41
|
+
"oneOf",
|
|
42
|
+
"allOf",
|
|
43
|
+
"anyOf",
|
|
44
|
+
"not",
|
|
45
|
+
"if",
|
|
46
|
+
"then",
|
|
47
|
+
"else",
|
|
48
|
+
"prefixItems",
|
|
49
|
+
"contains",
|
|
50
|
+
"propertyNames",
|
|
51
|
+
"dependentSchemas",
|
|
52
|
+
]:
|
|
53
|
+
if key in obj:
|
|
54
|
+
traverse(obj[key])
|
|
55
|
+
|
|
56
|
+
additional = obj.get("additionalProperties")
|
|
57
|
+
if isinstance(additional, dict):
|
|
58
|
+
traverse(additional)
|
|
59
|
+
|
|
60
|
+
discriminator = obj.get("discriminator")
|
|
61
|
+
if isinstance(discriminator, dict):
|
|
62
|
+
mapping = discriminator.get("mapping")
|
|
63
|
+
if isinstance(mapping, dict):
|
|
64
|
+
for value in mapping.values():
|
|
65
|
+
traverse(value)
|
|
66
|
+
|
|
67
|
+
traverse(schema)
|
|
68
|
+
return list(dependencies)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def topological_sort_schemas(schemas: dict[str, AnySchema]) -> list[str]:
|
|
72
|
+
schema_names = list(schemas.keys())
|
|
73
|
+
dependencies: dict[str, list[str]] = {name: [] for name in schema_names}
|
|
74
|
+
dependents: dict[str, list[str]] = {name: [] for name in schema_names}
|
|
75
|
+
in_degree: dict[str, int] = {name: 0 for name in schema_names}
|
|
76
|
+
|
|
77
|
+
for name, schema in schemas.items():
|
|
78
|
+
deps = extract_schema_dependencies(schema)
|
|
79
|
+
valid_deps = [dep for dep in deps if dep in schemas]
|
|
80
|
+
dependencies[name] = valid_deps
|
|
81
|
+
for dep in valid_deps:
|
|
82
|
+
dependents[dep].append(name)
|
|
83
|
+
|
|
84
|
+
for name, deps in dependencies.items():
|
|
85
|
+
in_degree[name] = len(deps)
|
|
86
|
+
|
|
87
|
+
queue = [name for name, degree in in_degree.items() if degree == 0]
|
|
88
|
+
sorted_names: list[str] = []
|
|
89
|
+
|
|
90
|
+
while queue:
|
|
91
|
+
current = queue.pop(0)
|
|
92
|
+
sorted_names.append(current)
|
|
93
|
+
for dependent in dependents[current]:
|
|
94
|
+
in_degree[dependent] -= 1
|
|
95
|
+
if in_degree[dependent] == 0:
|
|
96
|
+
queue.append(dependent)
|
|
97
|
+
|
|
98
|
+
if len(sorted_names) != len(schema_names):
|
|
99
|
+
for name in schema_names:
|
|
100
|
+
if name not in sorted_names:
|
|
101
|
+
sorted_names.append(name)
|
|
102
|
+
|
|
103
|
+
return sorted_names
|