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.
Files changed (47) hide show
  1. alt_stack_openapi_pydantic-1.6.2/LICENSE +21 -0
  2. alt_stack_openapi_pydantic-1.6.2/PKG-INFO +95 -0
  3. alt_stack_openapi_pydantic-1.6.2/README.md +71 -0
  4. alt_stack_openapi_pydantic-1.6.2/pyproject.toml +61 -0
  5. alt_stack_openapi_pydantic-1.6.2/setup.cfg +4 -0
  6. alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/__init__.py +21 -0
  7. alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/cli.py +67 -0
  8. alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/dependencies.py +103 -0
  9. alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/emission.py +725 -0
  10. alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/lowering.py +116 -0
  11. alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/py.typed +1 -0
  12. alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/registry.py +161 -0
  13. alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/rendering.py +197 -0
  14. alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/routes.py +179 -0
  15. alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/schema_dedup.py +141 -0
  16. alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/to_python.py +12 -0
  17. alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/type_render.py +15 -0
  18. alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/types/__init__.py +5 -0
  19. alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/types/array.py +39 -0
  20. alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/types/boolean.py +14 -0
  21. alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/types/intersection.py +20 -0
  22. alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/types/number.py +39 -0
  23. alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/types/object.py +15 -0
  24. alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/types/string.py +64 -0
  25. alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/types/types.py +6 -0
  26. alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/types/union.py +37 -0
  27. alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/utils.py +32 -0
  28. alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic.egg-info/PKG-INFO +95 -0
  29. alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic.egg-info/SOURCES.txt +45 -0
  30. alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic.egg-info/dependency_links.txt +1 -0
  31. alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic.egg-info/entry_points.txt +2 -0
  32. alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic.egg-info/requires.txt +8 -0
  33. alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic.egg-info/top_level.txt +1 -0
  34. alt_stack_openapi_pydantic-1.6.2/tests/test_array.py +24 -0
  35. alt_stack_openapi_pydantic-1.6.2/tests/test_boolean.py +10 -0
  36. alt_stack_openapi_pydantic-1.6.2/tests/test_convert_schema.py +162 -0
  37. alt_stack_openapi_pydantic-1.6.2/tests/test_dependencies.py +116 -0
  38. alt_stack_openapi_pydantic-1.6.2/tests/test_generated_client.py +208 -0
  39. alt_stack_openapi_pydantic-1.6.2/tests/test_intersection.py +12 -0
  40. alt_stack_openapi_pydantic-1.6.2/tests/test_master_openapi.py +391 -0
  41. alt_stack_openapi_pydantic-1.6.2/tests/test_number.py +36 -0
  42. alt_stack_openapi_pydantic-1.6.2/tests/test_object.py +15 -0
  43. alt_stack_openapi_pydantic-1.6.2/tests/test_registry.py +102 -0
  44. alt_stack_openapi_pydantic-1.6.2/tests/test_route_typing.py +415 -0
  45. alt_stack_openapi_pydantic-1.6.2/tests/test_string.py +123 -0
  46. alt_stack_openapi_pydantic-1.6.2/tests/test_to_python.py +1008 -0
  47. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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