alt-stack-openapi-pydantic 1.6.2__tar.gz → 1.6.4__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/src/alt_stack_openapi_pydantic.egg-info → alt_stack_openapi_pydantic-1.6.4}/PKG-INFO +1 -1
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/pyproject.toml +1 -1
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/src/alt_stack_openapi_pydantic/emission.py +2 -1
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/src/alt_stack_openapi_pydantic/rendering.py +57 -5
- alt_stack_openapi_pydantic-1.6.4/src/alt_stack_openapi_pydantic/type_render.py +38 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/src/alt_stack_openapi_pydantic/types/boolean.py +4 -2
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/src/alt_stack_openapi_pydantic/types/number.py +6 -6
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/src/alt_stack_openapi_pydantic/types/string.py +4 -4
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4/src/alt_stack_openapi_pydantic.egg-info}/PKG-INFO +1 -1
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/tests/test_boolean.py +6 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/tests/test_convert_schema.py +36 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/tests/test_number.py +25 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/tests/test_string.py +5 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/tests/test_to_python.py +89 -0
- alt_stack_openapi_pydantic-1.6.2/src/alt_stack_openapi_pydantic/type_render.py +0 -15
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/LICENSE +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/README.md +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/setup.cfg +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/src/alt_stack_openapi_pydantic/__init__.py +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/src/alt_stack_openapi_pydantic/cli.py +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/src/alt_stack_openapi_pydantic/dependencies.py +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/src/alt_stack_openapi_pydantic/lowering.py +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/src/alt_stack_openapi_pydantic/py.typed +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/src/alt_stack_openapi_pydantic/registry.py +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/src/alt_stack_openapi_pydantic/routes.py +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/src/alt_stack_openapi_pydantic/schema_dedup.py +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/src/alt_stack_openapi_pydantic/to_python.py +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/src/alt_stack_openapi_pydantic/types/__init__.py +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/src/alt_stack_openapi_pydantic/types/array.py +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/src/alt_stack_openapi_pydantic/types/intersection.py +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/src/alt_stack_openapi_pydantic/types/object.py +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/src/alt_stack_openapi_pydantic/types/types.py +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/src/alt_stack_openapi_pydantic/types/union.py +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/src/alt_stack_openapi_pydantic/utils.py +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/src/alt_stack_openapi_pydantic.egg-info/SOURCES.txt +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/src/alt_stack_openapi_pydantic.egg-info/dependency_links.txt +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/src/alt_stack_openapi_pydantic.egg-info/entry_points.txt +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/src/alt_stack_openapi_pydantic.egg-info/requires.txt +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/src/alt_stack_openapi_pydantic.egg-info/top_level.txt +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/tests/test_array.py +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/tests/test_dependencies.py +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/tests/test_generated_client.py +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/tests/test_intersection.py +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/tests/test_master_openapi.py +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/tests/test_object.py +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/tests/test_registry.py +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/tests/test_route_typing.py +0 -0
- {alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/tests/test_union.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alt-stack-openapi-pydantic
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.4
|
|
4
4
|
Summary: Generate Pydantic models, typed route maps, and an asyncio client from OpenAPI
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/anthonyaltieri/alt-stack
|
|
@@ -9,6 +9,7 @@ from .rendering import (
|
|
|
9
9
|
RenderContext,
|
|
10
10
|
decode_component_ref,
|
|
11
11
|
extra_field_annotation,
|
|
12
|
+
is_nullable_schema,
|
|
12
13
|
is_object_model_schema,
|
|
13
14
|
root_model_annotation,
|
|
14
15
|
schema_to_type_expr,
|
|
@@ -266,7 +267,7 @@ def _build_model_lines(
|
|
|
266
267
|
if is_required:
|
|
267
268
|
default_expr = f" = Field(alias={alias!r})" if alias else ""
|
|
268
269
|
else:
|
|
269
|
-
if not
|
|
270
|
+
if not is_nullable_schema(prop_schema):
|
|
270
271
|
type_expr = f"Annotated[Optional[{type_expr}], _omit_not_null]"
|
|
271
272
|
default_expr = f" = Field(default=None, alias={alias!r})" if alias else " = None"
|
|
272
273
|
|
|
@@ -8,7 +8,7 @@ from .registry import (
|
|
|
8
8
|
get_schema_exported_variable_name_for_primitive_type,
|
|
9
9
|
get_schema_exported_variable_name_for_string_format,
|
|
10
10
|
)
|
|
11
|
-
from .type_render import format_openapi_metadata
|
|
11
|
+
from .type_render import format_openapi_metadata, literal_type_expr
|
|
12
12
|
from .types import AnySchema
|
|
13
13
|
from .types.array import convert_openapi_array_to_pydantic
|
|
14
14
|
from .types.boolean import convert_openapi_boolean_to_pydantic
|
|
@@ -79,6 +79,13 @@ def schema_to_type_expr(
|
|
|
79
79
|
|
|
80
80
|
render_context = context or RenderContext()
|
|
81
81
|
|
|
82
|
+
# OpenAPI 3.1 spells "may be null" three ways; all of them render as Optional[...].
|
|
83
|
+
if schema.get("type") == "null":
|
|
84
|
+
return "None"
|
|
85
|
+
without_null = strip_null_member(schema)
|
|
86
|
+
if without_null is not None:
|
|
87
|
+
return f"Optional[{schema_to_type_expr(without_null, context=render_context)}]"
|
|
88
|
+
|
|
82
89
|
if isinstance(schema.get("$ref"), str):
|
|
83
90
|
ref = schema["$ref"]
|
|
84
91
|
if not ref.startswith("#/components/schemas/"):
|
|
@@ -135,10 +142,9 @@ def schema_to_type_expr(
|
|
|
135
142
|
)
|
|
136
143
|
return wrap_nullable(expr, schema)
|
|
137
144
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return wrap_nullable(expr, schema)
|
|
145
|
+
literal = literal_type_expr(schema)
|
|
146
|
+
if literal is not None:
|
|
147
|
+
return wrap_nullable(literal, schema)
|
|
142
148
|
|
|
143
149
|
return wrap_nullable("Any", schema)
|
|
144
150
|
|
|
@@ -149,6 +155,52 @@ def wrap_nullable(expr: str, schema: dict[str, Any]) -> str:
|
|
|
149
155
|
return expr
|
|
150
156
|
|
|
151
157
|
|
|
158
|
+
def _is_null_schema(schema: Any) -> bool:
|
|
159
|
+
return isinstance(schema, dict) and schema.get("type") == "null" and len(schema) <= 2
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def strip_null_member(schema: dict[str, Any]) -> dict[str, Any] | None:
|
|
163
|
+
"""Return the schema without its null alternative, or None when it has none.
|
|
164
|
+
|
|
165
|
+
Handles ``type: ["string", "null"]`` and ``anyOf``/``oneOf`` compositions that include a
|
|
166
|
+
``{"type": "null"}`` member. A composition left with one member collapses to that member so
|
|
167
|
+
the caller renders ``Optional[Member]`` rather than ``Optional[Union[Member]]``.
|
|
168
|
+
"""
|
|
169
|
+
type_value = schema.get("type")
|
|
170
|
+
if isinstance(type_value, list) and "null" in type_value:
|
|
171
|
+
remaining = [item for item in type_value if item != "null"]
|
|
172
|
+
stripped = dict(schema)
|
|
173
|
+
if len(remaining) == 1:
|
|
174
|
+
stripped["type"] = remaining[0]
|
|
175
|
+
else:
|
|
176
|
+
stripped.pop("type")
|
|
177
|
+
stripped["anyOf"] = [{"type": item} for item in remaining]
|
|
178
|
+
return stripped
|
|
179
|
+
for keyword in ("anyOf", "oneOf"):
|
|
180
|
+
members = schema.get(keyword)
|
|
181
|
+
if isinstance(members, list) and any(_is_null_schema(member) for member in members):
|
|
182
|
+
remaining = [member for member in members if not _is_null_schema(member)]
|
|
183
|
+
stripped = {key: value for key, value in schema.items() if key != keyword}
|
|
184
|
+
if len(remaining) == 1 and isinstance(remaining[0], dict):
|
|
185
|
+
merged = dict(remaining[0])
|
|
186
|
+
for key in ("description", "title", "default"):
|
|
187
|
+
if key in stripped and key not in merged:
|
|
188
|
+
merged[key] = stripped[key]
|
|
189
|
+
return merged
|
|
190
|
+
stripped[keyword] = remaining
|
|
191
|
+
return stripped
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def is_nullable_schema(schema: Any) -> bool:
|
|
196
|
+
"""True when the schema admits null: ``nullable: true``, ``type: "null"``, or a null member."""
|
|
197
|
+
if not isinstance(schema, dict):
|
|
198
|
+
return False
|
|
199
|
+
if schema.get("nullable") is True or schema.get("type") == "null":
|
|
200
|
+
return True
|
|
201
|
+
return strip_null_member(schema) is not None
|
|
202
|
+
|
|
203
|
+
|
|
152
204
|
def registered_output_alias(schema: AnySchema, context: RenderContext | None = None) -> str | None:
|
|
153
205
|
if not isinstance(schema, dict):
|
|
154
206
|
return None
|
|
@@ -0,0 +1,38 @@
|
|
|
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}]"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def literal_type_expr(schema: dict[str, Any]) -> str | None:
|
|
19
|
+
"""Render ``enum`` values, or a single-value ``const``, as ``Literal[...]``.
|
|
20
|
+
|
|
21
|
+
OpenAPI 3.1 documents (and Pydantic-exported schemas) express a fixed value with
|
|
22
|
+
``const``; treating it as a one-member enum keeps discriminated unions valid, because
|
|
23
|
+
Pydantic requires the discriminator field of every member to be a ``Literal``.
|
|
24
|
+
"""
|
|
25
|
+
values = schema.get("enum")
|
|
26
|
+
if not isinstance(values, list):
|
|
27
|
+
if "const" not in schema:
|
|
28
|
+
return None
|
|
29
|
+
values = [schema["const"]]
|
|
30
|
+
rendered = ", ".join(repr(value) for value in values)
|
|
31
|
+
return f"Literal[{rendered}]"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def integer_bound(value: Any, base: str) -> Any:
|
|
35
|
+
"""Render an integer field's bound as an ``int`` when the document spells it ``0.0``."""
|
|
36
|
+
if base == "int" and isinstance(value, float) and value.is_integer():
|
|
37
|
+
return int(value)
|
|
38
|
+
return value
|
|
@@ -3,11 +3,13 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
5
|
from ..registry import get_schema_exported_variable_name_for_primitive_type
|
|
6
|
-
from ..type_render import wrap_annotated
|
|
6
|
+
from ..type_render import literal_type_expr, wrap_annotated
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def convert_openapi_boolean_to_pydantic(schema: dict[str, Any]) -> str:
|
|
10
|
-
|
|
10
|
+
literal = literal_type_expr(schema)
|
|
11
|
+
if literal is not None:
|
|
12
|
+
return literal
|
|
11
13
|
custom = get_schema_exported_variable_name_for_primitive_type("boolean")
|
|
12
14
|
if custom:
|
|
13
15
|
return custom
|
|
@@ -3,13 +3,13 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
5
|
from ..registry import get_schema_exported_variable_name_for_primitive_type
|
|
6
|
-
from ..type_render import format_openapi_metadata, wrap_annotated
|
|
6
|
+
from ..type_render import format_openapi_metadata, integer_bound, literal_type_expr, wrap_annotated
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def convert_openapi_number_to_pydantic(schema: dict[str, Any]) -> str:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return
|
|
10
|
+
literal = literal_type_expr(schema)
|
|
11
|
+
if literal is not None:
|
|
12
|
+
return literal
|
|
13
13
|
|
|
14
14
|
schema_type = schema.get("type")
|
|
15
15
|
if schema_type in {"number", "integer", "boolean"}:
|
|
@@ -24,9 +24,9 @@ def convert_openapi_number_to_pydantic(schema: dict[str, Any]) -> str:
|
|
|
24
24
|
|
|
25
25
|
field_args: list[str] = ["strict=True"]
|
|
26
26
|
if isinstance(minimum, (int, float)):
|
|
27
|
-
field_args.append(f"ge={minimum}")
|
|
27
|
+
field_args.append(f"ge={integer_bound(minimum, base)}")
|
|
28
28
|
if isinstance(maximum, (int, float)):
|
|
29
|
-
field_args.append(f"le={maximum}")
|
|
29
|
+
field_args.append(f"le={integer_bound(maximum, base)}")
|
|
30
30
|
|
|
31
31
|
meta: dict[str, Any] = {}
|
|
32
32
|
if isinstance(minimum, (int, float)):
|
|
@@ -3,13 +3,13 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
5
|
from ..registry import get_schema_exported_variable_name_for_string_format
|
|
6
|
-
from ..type_render import format_openapi_metadata, wrap_annotated
|
|
6
|
+
from ..type_render import format_openapi_metadata, literal_type_expr, wrap_annotated
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def convert_openapi_string_to_pydantic(schema: dict[str, Any]) -> str:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return
|
|
10
|
+
literal = literal_type_expr(schema)
|
|
11
|
+
if literal is not None:
|
|
12
|
+
return literal
|
|
13
13
|
|
|
14
14
|
fmt = schema.get("format")
|
|
15
15
|
if isinstance(fmt, str) and fmt:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alt-stack-openapi-pydantic
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.4
|
|
4
4
|
Summary: Generate Pydantic models, typed route maps, and an asyncio client from OpenAPI
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/anthonyaltieri/alt-stack
|
|
@@ -8,3 +8,9 @@ def test_boolean_basic() -> None:
|
|
|
8
8
|
convert_openapi_boolean_to_pydantic({"type": "boolean"})
|
|
9
9
|
== "Annotated[bool, Field(strict=True)]"
|
|
10
10
|
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_const_boolean_is_a_single_literal() -> None:
|
|
14
|
+
assert (
|
|
15
|
+
convert_openapi_boolean_to_pydantic({"type": "boolean", "const": True}) == "Literal[True]"
|
|
16
|
+
)
|
{alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/tests/test_convert_schema.py
RENAMED
|
@@ -160,3 +160,39 @@ def test_string_constraints() -> None:
|
|
|
160
160
|
)
|
|
161
161
|
def test_string_formats(schema: dict, expected: str) -> None:
|
|
162
162
|
assert convert_schema_to_pydantic_string(schema) == expected
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_untyped_const_and_enum_render_as_literals() -> None:
|
|
166
|
+
assert convert_schema_to_pydantic_string({"const": "only"}) == "Literal['only']"
|
|
167
|
+
assert convert_schema_to_pydantic_string({"enum": ["a", 1]}) == "Literal['a', 1]"
|
|
168
|
+
assert (
|
|
169
|
+
convert_schema_to_pydantic_string({"const": "only", "nullable": True})
|
|
170
|
+
== "Optional[Literal['only']]"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_null_members_render_as_optional() -> None:
|
|
175
|
+
assert (
|
|
176
|
+
convert_schema_to_pydantic_string({"anyOf": [{"type": "string"}, {"type": "null"}]})
|
|
177
|
+
== "Optional[Annotated[str, Field(strict=True)]]"
|
|
178
|
+
)
|
|
179
|
+
assert (
|
|
180
|
+
convert_schema_to_pydantic_string({"oneOf": [{"type": "integer"}, {"type": "null"}]})
|
|
181
|
+
== "Optional[Annotated[int, Field(strict=True)]]"
|
|
182
|
+
)
|
|
183
|
+
assert (
|
|
184
|
+
convert_schema_to_pydantic_string({"type": ["string", "null"]})
|
|
185
|
+
== "Optional[Annotated[str, Field(strict=True)]]"
|
|
186
|
+
)
|
|
187
|
+
assert (
|
|
188
|
+
convert_schema_to_pydantic_string(
|
|
189
|
+
{"anyOf": [{"type": "string"}, {"type": "integer"}, {"type": "null"}]}
|
|
190
|
+
)
|
|
191
|
+
== "Optional[Union[Annotated[str, Field(strict=True)], Annotated[int, Field(strict=True)]]]"
|
|
192
|
+
)
|
|
193
|
+
assert (
|
|
194
|
+
convert_schema_to_pydantic_string(
|
|
195
|
+
{"anyOf": [{"$ref": "#/components/schemas/User"}, {"type": "null"}]}
|
|
196
|
+
)
|
|
197
|
+
== "Optional[User]"
|
|
198
|
+
)
|
|
@@ -27,6 +27,31 @@ def test_integer_enum() -> None:
|
|
|
27
27
|
assert result == "Literal[100, 200]"
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
def test_const_integer_is_a_single_literal() -> None:
|
|
31
|
+
result = convert_openapi_number_to_pydantic({"type": "integer", "const": 1})
|
|
32
|
+
assert result == "Literal[1]"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_integer_bounds_written_as_floats_render_as_ints() -> None:
|
|
36
|
+
result = convert_openapi_number_to_pydantic(
|
|
37
|
+
{"type": "integer", "minimum": 0.0, "maximum": 10.0}
|
|
38
|
+
)
|
|
39
|
+
expected = (
|
|
40
|
+
"Annotated[int, Field(strict=True, ge=0, le=10, "
|
|
41
|
+
"json_schema_extra={'openapi': {'maximum': 10.0, 'minimum': 0.0}})]"
|
|
42
|
+
)
|
|
43
|
+
assert result == expected
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_number_bounds_keep_their_float_form() -> None:
|
|
47
|
+
result = convert_openapi_number_to_pydantic({"type": "number", "minimum": 0.0})
|
|
48
|
+
expected = (
|
|
49
|
+
"Annotated[float, Field(strict=True, ge=0.0, "
|
|
50
|
+
"json_schema_extra={'openapi': {'minimum': 0.0}})]"
|
|
51
|
+
)
|
|
52
|
+
assert result == expected
|
|
53
|
+
|
|
54
|
+
|
|
30
55
|
def test_min_max() -> None:
|
|
31
56
|
result = convert_openapi_number_to_pydantic({"type": "integer", "minimum": 1, "maximum": 10})
|
|
32
57
|
expected = (
|
|
@@ -23,6 +23,11 @@ def test_enum_string() -> None:
|
|
|
23
23
|
assert result == "Literal['red', 'green']"
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def test_const_string_is_a_single_literal() -> None:
|
|
27
|
+
result = convert_openapi_string_to_pydantic({"type": "string", "const": "plaintext"})
|
|
28
|
+
assert result == "Literal['plaintext']"
|
|
29
|
+
|
|
30
|
+
|
|
26
31
|
def test_format_email() -> None:
|
|
27
32
|
result = convert_openapi_string_to_pydantic({"type": "string", "format": "email"})
|
|
28
33
|
assert (
|
{alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/tests/test_to_python.py
RENAMED
|
@@ -292,6 +292,43 @@ def test_optional_non_nullable_fields_reject_explicit_none() -> None:
|
|
|
292
292
|
module.Item.model_validate({"description": None})
|
|
293
293
|
|
|
294
294
|
|
|
295
|
+
def test_optional_null_member_fields_accept_explicit_none() -> None:
|
|
296
|
+
openapi = {
|
|
297
|
+
"components": {
|
|
298
|
+
"schemas": {
|
|
299
|
+
"Bundle": {
|
|
300
|
+
"type": "object",
|
|
301
|
+
"properties": {
|
|
302
|
+
"access_token": {"type": "string"},
|
|
303
|
+
"refresh_token": {"anyOf": [{"type": "string"}, {"type": "null"}]},
|
|
304
|
+
"expires_in": {"type": ["integer", "null"]},
|
|
305
|
+
"scope": {"type": "string"},
|
|
306
|
+
},
|
|
307
|
+
"required": ["access_token"],
|
|
308
|
+
"additionalProperties": False,
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
code = openapi_to_pydantic_code(openapi)
|
|
315
|
+
assert "refresh_token: Optional[Annotated[str, Field(strict=True)]] = None" in code
|
|
316
|
+
assert "expires_in: Optional[Annotated[int, Field(strict=True)]] = None" in code
|
|
317
|
+
assert (
|
|
318
|
+
"scope: Annotated[Optional[Annotated[str, Field(strict=True)]], _omit_not_null] = None"
|
|
319
|
+
in code
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
module = _load_module(code)
|
|
323
|
+
bundle = module.Bundle.model_validate(
|
|
324
|
+
{"access_token": "a", "refresh_token": None, "expires_in": None}
|
|
325
|
+
)
|
|
326
|
+
assert bundle.refresh_token is None and bundle.expires_in is None
|
|
327
|
+
assert module.Bundle.model_validate({"access_token": "a"}).refresh_token is None
|
|
328
|
+
with pytest.raises(ValidationError):
|
|
329
|
+
module.Bundle.model_validate({"access_token": "a", "scope": None})
|
|
330
|
+
|
|
331
|
+
|
|
295
332
|
def test_allof_wrapper_around_root_model_imports() -> None:
|
|
296
333
|
openapi = {
|
|
297
334
|
"components": {
|
|
@@ -1006,3 +1043,55 @@ def test_intersection_helper_is_inlined_only_when_used() -> None:
|
|
|
1006
1043
|
|
|
1007
1044
|
plain = openapi_to_pydantic_code({"components": {"schemas": {}}, "paths": {}})
|
|
1008
1045
|
assert "def all_of" not in plain
|
|
1046
|
+
|
|
1047
|
+
|
|
1048
|
+
def test_const_discriminators_generate_a_valid_discriminated_union() -> None:
|
|
1049
|
+
openapi = {
|
|
1050
|
+
"components": {
|
|
1051
|
+
"schemas": {
|
|
1052
|
+
"Plaintext": {
|
|
1053
|
+
"type": "object",
|
|
1054
|
+
"additionalProperties": False,
|
|
1055
|
+
"properties": {
|
|
1056
|
+
"mode": {"type": "string", "const": "plaintext"},
|
|
1057
|
+
"v": {"type": "integer", "const": 1},
|
|
1058
|
+
"data": {"type": "string"},
|
|
1059
|
+
},
|
|
1060
|
+
"required": ["mode", "v", "data"],
|
|
1061
|
+
},
|
|
1062
|
+
"Encrypted": {
|
|
1063
|
+
"type": "object",
|
|
1064
|
+
"additionalProperties": False,
|
|
1065
|
+
"properties": {
|
|
1066
|
+
"mode": {"type": "string", "const": "kms"},
|
|
1067
|
+
"v": {"type": "integer", "const": 1},
|
|
1068
|
+
"ct": {"type": "string"},
|
|
1069
|
+
},
|
|
1070
|
+
"required": ["mode", "v", "ct"],
|
|
1071
|
+
},
|
|
1072
|
+
"Envelope": {
|
|
1073
|
+
"oneOf": [
|
|
1074
|
+
{"$ref": "#/components/schemas/Plaintext"},
|
|
1075
|
+
{"$ref": "#/components/schemas/Encrypted"},
|
|
1076
|
+
],
|
|
1077
|
+
"discriminator": {
|
|
1078
|
+
"propertyName": "mode",
|
|
1079
|
+
"mapping": {
|
|
1080
|
+
"plaintext": "#/components/schemas/Plaintext",
|
|
1081
|
+
"kms": "#/components/schemas/Encrypted",
|
|
1082
|
+
},
|
|
1083
|
+
},
|
|
1084
|
+
},
|
|
1085
|
+
}
|
|
1086
|
+
},
|
|
1087
|
+
"paths": {},
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
module = _load_module(openapi_to_pydantic_code(openapi))
|
|
1091
|
+
|
|
1092
|
+
envelope = module.Envelope.model_validate({"mode": "plaintext", "v": 1, "data": "x"})
|
|
1093
|
+
assert isinstance(envelope.root, module.Plaintext)
|
|
1094
|
+
with pytest.raises(ValidationError):
|
|
1095
|
+
module.Envelope.model_validate({"mode": "other", "v": 1, "data": "x"})
|
|
1096
|
+
with pytest.raises(ValidationError):
|
|
1097
|
+
module.Plaintext.model_validate({"mode": "plaintext", "v": 2, "data": "x"})
|
|
@@ -1,15 +0,0 @@
|
|
|
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}]"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/tests/test_dependencies.py
RENAMED
|
File without changes
|
{alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/tests/test_generated_client.py
RENAMED
|
File without changes
|
{alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/tests/test_intersection.py
RENAMED
|
File without changes
|
{alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/tests/test_master_openapi.py
RENAMED
|
File without changes
|
|
File without changes
|
{alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/tests/test_registry.py
RENAMED
|
File without changes
|
{alt_stack_openapi_pydantic-1.6.2 → alt_stack_openapi_pydantic-1.6.4}/tests/test_route_typing.py
RENAMED
|
File without changes
|
|
File without changes
|