alt-stack-openapi-pydantic 1.6.2__tar.gz
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-1.6.2/LICENSE +21 -0
- alt_stack_openapi_pydantic-1.6.2/PKG-INFO +95 -0
- alt_stack_openapi_pydantic-1.6.2/README.md +71 -0
- alt_stack_openapi_pydantic-1.6.2/pyproject.toml +61 -0
- alt_stack_openapi_pydantic-1.6.2/setup.cfg +4 -0
- alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/__init__.py +21 -0
- alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/cli.py +67 -0
- alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/dependencies.py +103 -0
- alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/emission.py +725 -0
- alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/lowering.py +116 -0
- alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/py.typed +1 -0
- alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/registry.py +161 -0
- alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/rendering.py +197 -0
- alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/routes.py +179 -0
- alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/schema_dedup.py +141 -0
- alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/to_python.py +12 -0
- alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/type_render.py +15 -0
- alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/types/__init__.py +5 -0
- alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/types/array.py +39 -0
- alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/types/boolean.py +14 -0
- alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/types/intersection.py +20 -0
- alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/types/number.py +39 -0
- alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/types/object.py +15 -0
- alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/types/string.py +64 -0
- alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/types/types.py +6 -0
- alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/types/union.py +37 -0
- alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/utils.py +32 -0
- alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic.egg-info/PKG-INFO +95 -0
- alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic.egg-info/SOURCES.txt +45 -0
- alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic.egg-info/dependency_links.txt +1 -0
- alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic.egg-info/entry_points.txt +2 -0
- alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic.egg-info/requires.txt +8 -0
- alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic.egg-info/top_level.txt +1 -0
- alt_stack_openapi_pydantic-1.6.2/tests/test_array.py +24 -0
- alt_stack_openapi_pydantic-1.6.2/tests/test_boolean.py +10 -0
- alt_stack_openapi_pydantic-1.6.2/tests/test_convert_schema.py +162 -0
- alt_stack_openapi_pydantic-1.6.2/tests/test_dependencies.py +116 -0
- alt_stack_openapi_pydantic-1.6.2/tests/test_generated_client.py +208 -0
- alt_stack_openapi_pydantic-1.6.2/tests/test_intersection.py +12 -0
- alt_stack_openapi_pydantic-1.6.2/tests/test_master_openapi.py +391 -0
- alt_stack_openapi_pydantic-1.6.2/tests/test_number.py +36 -0
- alt_stack_openapi_pydantic-1.6.2/tests/test_object.py +15 -0
- alt_stack_openapi_pydantic-1.6.2/tests/test_registry.py +102 -0
- alt_stack_openapi_pydantic-1.6.2/tests/test_route_typing.py +415 -0
- alt_stack_openapi_pydantic-1.6.2/tests/test_string.py +123 -0
- alt_stack_openapi_pydantic-1.6.2/tests/test_to_python.py +1008 -0
- alt_stack_openapi_pydantic-1.6.2/tests/test_union.py +28 -0
|
@@ -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,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,71 @@
|
|
|
1
|
+
# `alt-stack-openapi-pydantic`
|
|
2
|
+
|
|
3
|
+
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`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
python -m pip install alt-stack-openapi-pydantic
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Consumers of a generated SDK install the runtime package instead:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
python -m pip install alt-stack-http-client-httpx
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Generate
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
alt-stack-openapi-pydantic ./openapi.json --output ./generated_types.py
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`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.
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from generated_types import User
|
|
27
|
+
|
|
28
|
+
user = User.model_validate({"id": "u_1", "name": "Ada"})
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
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:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from generated_types import Request, Response
|
|
35
|
+
|
|
36
|
+
Request["/users"]["POST"]["body"] # type[CreateUserBody]
|
|
37
|
+
Response["/users/{id}"]["GET"]["200"] # type[User]
|
|
38
|
+
Response["/users/{id}"]["GET"]["999"] # static error: unknown status
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Generated modules also include an asyncio `HttpxApiClient` with one typed method per route, built on `alt_stack_http_client_httpx`:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from alt_stack_http_client_httpx import ApiSuccess
|
|
45
|
+
|
|
46
|
+
from generated_types import GetUsersIdParams, HttpxApiClient, Request, Response
|
|
47
|
+
|
|
48
|
+
async with HttpxApiClient(
|
|
49
|
+
"https://api.example.com", request_map=Request, response_map=Response
|
|
50
|
+
) as client:
|
|
51
|
+
result = await client.get("/users/{id}", params=GetUsersIdParams(id="u_1"))
|
|
52
|
+
|
|
53
|
+
if isinstance(result, ApiSuccess):
|
|
54
|
+
result.body # User; unknown paths, missing params, and wrong models are static errors
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
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.
|
|
58
|
+
|
|
59
|
+
## Development
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
uv run --project packages/python-pydantic-openapi --extra dev pytest packages/python-pydantic-openapi/tests
|
|
63
|
+
uv run --project packages/python-pydantic-openapi --extra dev ruff check packages/python-pydantic-openapi
|
|
64
|
+
uv run --project packages/python-pydantic-openapi --extra dev ty check packages/python-pydantic-openapi
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Documentation
|
|
68
|
+
|
|
69
|
+
- [Code generation Quickstart](../../apps/docs/docs/codegen/quickstart.md)
|
|
70
|
+
- [Common Patterns](../../apps/docs/docs/codegen/common-patterns.md)
|
|
71
|
+
- [Python/Pydantic API Documentation](../../apps/docs/docs/codegen/api/pydantic-openapi.md)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "alt-stack-openapi-pydantic"
|
|
3
|
+
version = "1.6.2"
|
|
4
|
+
description = "Generate Pydantic models, typed route maps, and an asyncio client from OpenAPI"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Programming Language :: Python :: 3",
|
|
11
|
+
"Programming Language :: Python :: 3.11",
|
|
12
|
+
"Programming Language :: Python :: 3.12",
|
|
13
|
+
"Typing :: Typed",
|
|
14
|
+
]
|
|
15
|
+
dependencies = ["pydantic[email]>=2.7.0"]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Homepage = "https://github.com/anthonyaltieri/alt-stack"
|
|
19
|
+
Repository = "https://github.com/anthonyaltieri/alt-stack"
|
|
20
|
+
Issues = "https://github.com/anthonyaltieri/alt-stack/issues"
|
|
21
|
+
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
dev = [
|
|
24
|
+
"alt-stack-http-client-httpx",
|
|
25
|
+
"pyrefly>=1.0.0",
|
|
26
|
+
"pytest>=8.0.0",
|
|
27
|
+
"ruff>=0.6.0",
|
|
28
|
+
"ty>=0.0.78",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
alt-stack-openapi-pydantic = "alt_stack_openapi_pydantic.cli:main"
|
|
33
|
+
|
|
34
|
+
[build-system]
|
|
35
|
+
requires = ["setuptools>=68", "wheel"]
|
|
36
|
+
build-backend = "setuptools.build_meta"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools]
|
|
39
|
+
package-dir = { "" = "src" }
|
|
40
|
+
include-package-data = true
|
|
41
|
+
|
|
42
|
+
[tool.setuptools.packages.find]
|
|
43
|
+
where = ["src"]
|
|
44
|
+
include = ["alt_stack_openapi_pydantic*"]
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.package-data]
|
|
47
|
+
"alt_stack_openapi_pydantic" = ["py.typed"]
|
|
48
|
+
|
|
49
|
+
[tool.uv.sources]
|
|
50
|
+
alt-stack-http-client-httpx = { path = "../python-http-client-httpx" }
|
|
51
|
+
|
|
52
|
+
[tool.ruff]
|
|
53
|
+
line-length = 100
|
|
54
|
+
target-version = "py311"
|
|
55
|
+
|
|
56
|
+
[tool.ruff.lint]
|
|
57
|
+
select = ["E", "F", "I"]
|
|
58
|
+
|
|
59
|
+
[tool.ruff.format]
|
|
60
|
+
quote-style = "double"
|
|
61
|
+
indent-style = "space"
|
|
@@ -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
|