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,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