pyopenapi-gen 0.14.0__py3-none-any.whl → 0.14.1__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.
- pyopenapi_gen/cli.py +3 -3
- pyopenapi_gen/context/import_collector.py +10 -10
- pyopenapi_gen/context/render_context.py +13 -13
- pyopenapi_gen/core/auth/plugins.py +7 -7
- pyopenapi_gen/core/http_status_codes.py +2 -4
- pyopenapi_gen/core/http_transport.py +19 -19
- pyopenapi_gen/core/loader/operations/parser.py +2 -2
- pyopenapi_gen/core/loader/operations/request_body.py +3 -3
- pyopenapi_gen/core/loader/parameters/parser.py +3 -3
- pyopenapi_gen/core/loader/responses/parser.py +2 -2
- pyopenapi_gen/core/loader/schemas/extractor.py +4 -4
- pyopenapi_gen/core/pagination.py +3 -3
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +3 -3
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +2 -2
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +3 -3
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +3 -3
- pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +2 -2
- pyopenapi_gen/core/parsing/common/type_parser.py +2 -3
- pyopenapi_gen/core/parsing/context.py +10 -10
- pyopenapi_gen/core/parsing/cycle_helpers.py +5 -2
- pyopenapi_gen/core/parsing/keywords/all_of_parser.py +5 -5
- pyopenapi_gen/core/parsing/keywords/any_of_parser.py +4 -4
- pyopenapi_gen/core/parsing/keywords/array_items_parser.py +4 -4
- pyopenapi_gen/core/parsing/keywords/one_of_parser.py +4 -4
- pyopenapi_gen/core/parsing/keywords/properties_parser.py +5 -5
- pyopenapi_gen/core/parsing/schema_finalizer.py +15 -15
- pyopenapi_gen/core/parsing/schema_parser.py +44 -25
- pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +4 -4
- pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +7 -4
- pyopenapi_gen/core/parsing/unified_cycle_detection.py +10 -10
- pyopenapi_gen/core/schemas.py +10 -10
- pyopenapi_gen/core/streaming_helpers.py +5 -7
- pyopenapi_gen/core/telemetry.py +4 -4
- pyopenapi_gen/core/utils.py +7 -7
- pyopenapi_gen/core/writers/code_writer.py +2 -2
- pyopenapi_gen/core/writers/documentation_writer.py +18 -18
- pyopenapi_gen/core/writers/line_writer.py +3 -3
- pyopenapi_gen/core/writers/python_construct_renderer.py +10 -10
- pyopenapi_gen/emit/models_emitter.py +2 -2
- pyopenapi_gen/emitters/core_emitter.py +3 -5
- pyopenapi_gen/emitters/endpoints_emitter.py +12 -12
- pyopenapi_gen/emitters/exceptions_emitter.py +4 -3
- pyopenapi_gen/emitters/models_emitter.py +6 -6
- pyopenapi_gen/generator/client_generator.py +6 -6
- pyopenapi_gen/helpers/endpoint_utils.py +16 -18
- pyopenapi_gen/helpers/type_cleaner.py +66 -53
- pyopenapi_gen/helpers/type_helper.py +7 -7
- pyopenapi_gen/helpers/type_resolution/array_resolver.py +4 -4
- pyopenapi_gen/helpers/type_resolution/composition_resolver.py +5 -5
- pyopenapi_gen/helpers/type_resolution/finalizer.py +38 -22
- pyopenapi_gen/helpers/type_resolution/named_resolver.py +4 -5
- pyopenapi_gen/helpers/type_resolution/object_resolver.py +11 -11
- pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +1 -2
- pyopenapi_gen/helpers/type_resolution/resolver.py +2 -3
- pyopenapi_gen/ir.py +32 -34
- pyopenapi_gen/types/contracts/protocols.py +5 -5
- pyopenapi_gen/types/contracts/types.py +2 -3
- pyopenapi_gen/types/resolvers/reference_resolver.py +4 -4
- pyopenapi_gen/types/resolvers/response_resolver.py +6 -4
- pyopenapi_gen/types/resolvers/schema_resolver.py +32 -16
- pyopenapi_gen/types/services/type_service.py +55 -9
- pyopenapi_gen/types/strategies/response_strategy.py +6 -7
- pyopenapi_gen/visit/client_visitor.py +5 -7
- pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +7 -7
- pyopenapi_gen/visit/endpoint/generators/request_generator.py +5 -5
- pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +38 -17
- pyopenapi_gen/visit/endpoint/generators/signature_generator.py +4 -4
- pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +17 -17
- pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +8 -8
- pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +13 -13
- pyopenapi_gen/visit/model/alias_generator.py +1 -4
- pyopenapi_gen/visit/model/dataclass_generator.py +139 -10
- pyopenapi_gen/visit/model/model_visitor.py +2 -3
- pyopenapi_gen/visit/visitor.py +3 -3
- {pyopenapi_gen-0.14.0.dist-info → pyopenapi_gen-0.14.1.dist-info}/METADATA +1 -1
- pyopenapi_gen-0.14.1.dist-info/RECORD +132 -0
- pyopenapi_gen-0.14.0.dist-info/RECORD +0 -132
- {pyopenapi_gen-0.14.0.dist-info → pyopenapi_gen-0.14.1.dist-info}/WHEEL +0 -0
- {pyopenapi_gen-0.14.0.dist-info → pyopenapi_gen-0.14.1.dist-info}/entry_points.txt +0 -0
- {pyopenapi_gen-0.14.0.dist-info → pyopenapi_gen-0.14.1.dist-info}/licenses/LICENSE +0 -0
@@ -5,7 +5,7 @@ Helper class for generating response handling logic for an endpoint method.
|
|
5
5
|
from __future__ import annotations
|
6
6
|
|
7
7
|
import logging
|
8
|
-
from typing import TYPE_CHECKING, Any,
|
8
|
+
from typing import TYPE_CHECKING, Any, TypedDict
|
9
9
|
|
10
10
|
from pyopenapi_gen.core.http_status_codes import get_exception_class_name
|
11
11
|
from pyopenapi_gen.core.writers.code_writer import CodeWriter
|
@@ -44,8 +44,8 @@ class DefaultCase(TypedDict):
|
|
44
44
|
class EndpointResponseHandlerGenerator:
|
45
45
|
"""Generates the response handling logic for an endpoint method."""
|
46
46
|
|
47
|
-
def __init__(self, schemas:
|
48
|
-
self.schemas:
|
47
|
+
def __init__(self, schemas: dict[str, Any] | None = None) -> None:
|
48
|
+
self.schemas: dict[str, Any] = schemas or {}
|
49
49
|
|
50
50
|
def _is_type_alias_to_array(self, type_name: str) -> bool:
|
51
51
|
"""
|
@@ -130,7 +130,7 @@ class EndpointResponseHandlerGenerator:
|
|
130
130
|
Determine if a type should use BaseSchema deserialization.
|
131
131
|
|
132
132
|
Args:
|
133
|
-
type_name: The Python type name (e.g., "User", "List[User]", "
|
133
|
+
type_name: The Python type name (e.g., "User", "List[User]", "User | None")
|
134
134
|
|
135
135
|
Returns:
|
136
136
|
True if the type should use BaseSchema .from_dict() deserialization
|
@@ -138,14 +138,14 @@ class EndpointResponseHandlerGenerator:
|
|
138
138
|
# Extract the base type name from complex types
|
139
139
|
base_type = type_name
|
140
140
|
|
141
|
-
# Handle List[Type],
|
141
|
+
# Handle List[Type], Type | None, etc.
|
142
142
|
if "[" in base_type and "]" in base_type:
|
143
|
-
# Extract the inner type from List[Type],
|
143
|
+
# Extract the inner type from List[Type], Type | None, etc.
|
144
144
|
start_bracket = base_type.find("[")
|
145
145
|
end_bracket = base_type.rfind("]")
|
146
146
|
inner_type = base_type[start_bracket + 1 : end_bracket]
|
147
147
|
|
148
|
-
# For Union types like
|
148
|
+
# For Union types like User | None -> Union[User, None], take the first type
|
149
149
|
if ", " in inner_type:
|
150
150
|
inner_type = inner_type.split(", ")[0]
|
151
151
|
|
@@ -168,8 +168,9 @@ class EndpointResponseHandlerGenerator:
|
|
168
168
|
}:
|
169
169
|
return False
|
170
170
|
|
171
|
-
# Skip typing constructs
|
172
|
-
|
171
|
+
# Skip typing constructs
|
172
|
+
# Note: Modern Python 3.10+ uses | None instead of Optional[X]
|
173
|
+
if base_type.startswith(("dict[", "List[", "Union[", "Tuple[", "dict[", "list[", "tuple[")):
|
173
174
|
return False
|
174
175
|
|
175
176
|
# Check if this is a type alias (array or non-array) - these should NOT use BaseSchema
|
@@ -180,8 +181,7 @@ class EndpointResponseHandlerGenerator:
|
|
180
181
|
# Check if it's a model type (contains a dot indicating it's from models package)
|
181
182
|
# or if it's a simple class name that's likely a generated model (starts with uppercase)
|
182
183
|
return "." in base_type or (
|
183
|
-
base_type[0].isupper()
|
184
|
-
and base_type not in {"Dict", "List", "Optional", "Union", "Tuple", "dict", "list", "tuple"}
|
184
|
+
base_type[0].isupper() and base_type not in {"Dict", "List", "Union", "Tuple", "dict", "list", "tuple"}
|
185
185
|
)
|
186
186
|
|
187
187
|
def _get_base_schema_deserialization_code(self, return_type: str, data_expr: str) -> str:
|
@@ -203,8 +203,29 @@ class EndpointResponseHandlerGenerator:
|
|
203
203
|
item_type = return_type[5:-1] # Remove 'list[' and ']'
|
204
204
|
return f"[{item_type}.from_dict(item) for item in {data_expr}]"
|
205
205
|
elif return_type.startswith("Optional["):
|
206
|
-
#
|
206
|
+
# SANITY CHECK: Unified type system should never produce Optional[X]
|
207
|
+
logger.error(
|
208
|
+
f"❌ ARCHITECTURE VIOLATION: Received legacy Optional[X] type in response handler: {return_type}. "
|
209
|
+
f"Unified type system must generate X | None directly."
|
210
|
+
)
|
211
|
+
# Defensive conversion (but this indicates a serious bug upstream)
|
207
212
|
inner_type = return_type[9:-1] # Remove 'Optional[' and ']'
|
213
|
+
logger.warning(f"⚠️ Converting to modern syntax internally for: {inner_type} | None")
|
214
|
+
|
215
|
+
# Check if inner type is also a list
|
216
|
+
if inner_type.startswith("List[") or inner_type.startswith("list["):
|
217
|
+
list_code = self._get_base_schema_deserialization_code(inner_type, data_expr)
|
218
|
+
return f"{list_code} if {data_expr} is not None else None"
|
219
|
+
else:
|
220
|
+
return f"{inner_type}.from_dict({data_expr}) if {data_expr} is not None else None"
|
221
|
+
elif " | None" in return_type or return_type.endswith("| None"):
|
222
|
+
# Handle Model | None types (modern Python 3.10+ syntax)
|
223
|
+
# Extract base type from "X | None" pattern
|
224
|
+
if " | None" in return_type:
|
225
|
+
inner_type = return_type.replace(" | None", "").strip()
|
226
|
+
else:
|
227
|
+
inner_type = return_type.replace("| None", "").strip()
|
228
|
+
|
208
229
|
# Check if inner type is also a list
|
209
230
|
if inner_type.startswith("List[") or inner_type.startswith("list["):
|
210
231
|
list_code = self._get_base_schema_deserialization_code(inner_type, data_expr)
|
@@ -230,7 +251,7 @@ class EndpointResponseHandlerGenerator:
|
|
230
251
|
return_type: str,
|
231
252
|
context: RenderContext,
|
232
253
|
op: IROperation,
|
233
|
-
response_ir:
|
254
|
+
response_ir: IRResponse | None = None,
|
234
255
|
) -> str:
|
235
256
|
"""Determines the code snippet to extract/transform the response body."""
|
236
257
|
# Handle None, StreamingResponse, Iterator, etc.
|
@@ -243,7 +264,7 @@ class EndpointResponseHandlerGenerator:
|
|
243
264
|
if return_type == "AsyncIterator[bytes]":
|
244
265
|
context.add_import(f"{context.core_package_name}.streaming_helpers", "iter_bytes")
|
245
266
|
return "iter_bytes(response)"
|
246
|
-
elif "
|
267
|
+
elif "dict[str, Any]" in return_type or "dict" in return_type.lower():
|
247
268
|
# For event streams that return Dict objects
|
248
269
|
context.add_import(f"{context.core_package_name}.streaming_helpers", "iter_sse_events_text")
|
249
270
|
return "sse_json_stream_marker" # Special marker handled by _write_parsed_return
|
@@ -260,7 +281,7 @@ class EndpointResponseHandlerGenerator:
|
|
260
281
|
return "iter_bytes(response)"
|
261
282
|
|
262
283
|
# Special case for "data: Any" unwrapping when the actual schema has no fields/properties
|
263
|
-
if return_type in {"
|
284
|
+
if return_type in {"dict[str, Any]", "dict[str, object]", "object", "Any"}:
|
264
285
|
context.add_import("typing", "Dict")
|
265
286
|
context.add_import("typing", "Any")
|
266
287
|
|
@@ -273,7 +294,7 @@ class EndpointResponseHandlerGenerator:
|
|
273
294
|
return "response.json() # Type is Any"
|
274
295
|
elif return_type == "None":
|
275
296
|
return "None" # This will be handled by generate_response_handling directly
|
276
|
-
else: # Includes schema-defined models, List[],
|
297
|
+
else: # Includes schema-defined models, List[], dict[], Optional[]
|
277
298
|
context.add_typing_imports_for_type(return_type) # Ensure model itself is imported
|
278
299
|
|
279
300
|
# Check if we should use BaseSchema deserialization instead of cast()
|
@@ -434,7 +455,7 @@ class EndpointResponseHandlerGenerator:
|
|
434
455
|
context.add_import("typing", "cast")
|
435
456
|
writer.write_line(f"return cast({strategy.return_type}, response.json())")
|
436
457
|
|
437
|
-
def _get_response_schema(self, response_ir: IRResponse) ->
|
458
|
+
def _get_response_schema(self, response_ir: IRResponse) -> IRSchema | None:
|
438
459
|
"""Extract the schema from a response IR."""
|
439
460
|
if not response_ir.content:
|
440
461
|
return None
|
@@ -5,7 +5,7 @@ Helper class for generating the method signature for an endpoint.
|
|
5
5
|
from __future__ import annotations
|
6
6
|
|
7
7
|
import logging
|
8
|
-
from typing import TYPE_CHECKING, Any,
|
8
|
+
from typing import TYPE_CHECKING, Any, List
|
9
9
|
|
10
10
|
from pyopenapi_gen.core.utils import NameSanitizer
|
11
11
|
from pyopenapi_gen.core.writers.code_writer import CodeWriter
|
@@ -24,15 +24,15 @@ logger = logging.getLogger(__name__)
|
|
24
24
|
class EndpointMethodSignatureGenerator:
|
25
25
|
"""Generates the Python method signature for an endpoint operation."""
|
26
26
|
|
27
|
-
def __init__(self, schemas:
|
28
|
-
self.schemas:
|
27
|
+
def __init__(self, schemas: dict[str, Any] | None = None) -> None:
|
28
|
+
self.schemas: dict[str, Any] = schemas or {}
|
29
29
|
|
30
30
|
def generate_signature(
|
31
31
|
self,
|
32
32
|
writer: CodeWriter,
|
33
33
|
op: IROperation,
|
34
34
|
context: RenderContext,
|
35
|
-
ordered_params: List[
|
35
|
+
ordered_params: List[dict[str, Any]],
|
36
36
|
strategy: ResponseStrategy,
|
37
37
|
) -> None:
|
38
38
|
"""Writes the method signature to the provided CodeWriter."""
|
@@ -6,7 +6,7 @@ from __future__ import annotations
|
|
6
6
|
|
7
7
|
import logging
|
8
8
|
import re # For _build_url_with_path_vars
|
9
|
-
from typing import TYPE_CHECKING, Any,
|
9
|
+
from typing import TYPE_CHECKING, Any, List
|
10
10
|
|
11
11
|
from pyopenapi_gen.core.utils import NameSanitizer
|
12
12
|
from pyopenapi_gen.core.writers.code_writer import CodeWriter
|
@@ -21,8 +21,8 @@ logger = logging.getLogger(__name__)
|
|
21
21
|
class EndpointUrlArgsGenerator:
|
22
22
|
"""Generates URL, query, and header parameters for an endpoint method."""
|
23
23
|
|
24
|
-
def __init__(self, schemas:
|
25
|
-
self.schemas:
|
24
|
+
def __init__(self, schemas: dict[str, Any] | None = None) -> None:
|
25
|
+
self.schemas: dict[str, Any] = schemas or {}
|
26
26
|
|
27
27
|
def _build_url_with_path_vars(self, path: str) -> str:
|
28
28
|
"""Builds the f-string for URL construction, substituting path variables."""
|
@@ -34,7 +34,7 @@ class EndpointUrlArgsGenerator:
|
|
34
34
|
return f'f"{{self.base_url}}{formatted_path}"'
|
35
35
|
|
36
36
|
def _write_query_params(
|
37
|
-
self, writer: CodeWriter, op: IROperation, ordered_params: List[
|
37
|
+
self, writer: CodeWriter, op: IROperation, ordered_params: List[dict[str, Any]], context: RenderContext
|
38
38
|
) -> None:
|
39
39
|
"""Writes query parameter dictionary construction."""
|
40
40
|
# Logic from EndpointMethodGenerator._write_query_params
|
@@ -58,7 +58,7 @@ class EndpointUrlArgsGenerator:
|
|
58
58
|
)
|
59
59
|
|
60
60
|
def _write_header_params(
|
61
|
-
self, writer: CodeWriter, op: IROperation, ordered_params: List[
|
61
|
+
self, writer: CodeWriter, op: IROperation, ordered_params: List[dict[str, Any]], context: RenderContext
|
62
62
|
) -> None:
|
63
63
|
"""Writes header parameter dictionary construction."""
|
64
64
|
# Logic from EndpointMethodGenerator._write_header_params
|
@@ -89,9 +89,9 @@ class EndpointUrlArgsGenerator:
|
|
89
89
|
writer: CodeWriter,
|
90
90
|
op: IROperation,
|
91
91
|
context: RenderContext,
|
92
|
-
ordered_params: List[
|
93
|
-
primary_content_type:
|
94
|
-
resolved_body_type:
|
92
|
+
ordered_params: List[dict[str, Any]],
|
93
|
+
primary_content_type: str | None,
|
94
|
+
resolved_body_type: str | None,
|
95
95
|
) -> bool:
|
96
96
|
"""Writes URL, query, and header parameters. Returns True if header params were written."""
|
97
97
|
# Main logic from EndpointMethodGenerator._write_url_and_args
|
@@ -103,9 +103,9 @@ class EndpointUrlArgsGenerator:
|
|
103
103
|
# Check if any parameter in ordered_params is a query param, not just op.parameters
|
104
104
|
has_spec_query_params = any(p.get("param_in") == "query" for p in ordered_params)
|
105
105
|
if has_spec_query_params:
|
106
|
-
context.add_import("typing", "Any") # For
|
107
|
-
context.add_import("typing", "Dict") # For
|
108
|
-
writer.write_line("params:
|
106
|
+
context.add_import("typing", "Any") # For dict[str, Any]
|
107
|
+
context.add_import("typing", "Dict") # For dict[str, Any]
|
108
|
+
writer.write_line("params: dict[str, Any] = {")
|
109
109
|
# writer.indent() # Indentation should be handled by CodeWriter when writing lines
|
110
110
|
self._write_query_params(writer, op, ordered_params, context)
|
111
111
|
# writer.dedent()
|
@@ -115,9 +115,9 @@ class EndpointUrlArgsGenerator:
|
|
115
115
|
# Header Parameters
|
116
116
|
has_header_params = any(p.get("param_in") == "header" for p in ordered_params)
|
117
117
|
if has_header_params:
|
118
|
-
context.add_import("typing", "Any") # For
|
119
|
-
context.add_import("typing", "Dict") # For
|
120
|
-
writer.write_line("headers:
|
118
|
+
context.add_import("typing", "Any") # For dict[str, Any]
|
119
|
+
context.add_import("typing", "Dict") # For dict[str, Any]
|
120
|
+
writer.write_line("headers: dict[str, Any] = {")
|
121
121
|
# writer.indent()
|
122
122
|
self._write_header_params(writer, op, ordered_params, context)
|
123
123
|
# writer.dedent()
|
@@ -160,11 +160,11 @@ class EndpointUrlArgsGenerator:
|
|
160
160
|
context.add_import("typing", "IO") # For IO[Any]
|
161
161
|
context.add_import("typing", "Any")
|
162
162
|
writer.write_line(
|
163
|
-
"files_data:
|
163
|
+
"files_data: dict[str, IO[Any]] = DataclassSerializer.serialize(files) # type failed"
|
164
164
|
)
|
165
165
|
elif primary_content_type == "application/x-www-form-urlencoded":
|
166
166
|
# form_data is the expected parameter name from EndpointParameterProcessor
|
167
|
-
# resolved_body_type should be
|
167
|
+
# resolved_body_type should be dict[str, Any]
|
168
168
|
if resolved_body_type:
|
169
169
|
writer.write_line(
|
170
170
|
f"form_data_body: {resolved_body_type} = DataclassSerializer.serialize(form_data)"
|
@@ -173,7 +173,7 @@ class EndpointUrlArgsGenerator:
|
|
173
173
|
context.add_import("typing", "Dict")
|
174
174
|
context.add_import("typing", "Any")
|
175
175
|
writer.write_line(
|
176
|
-
"form_data_body:
|
176
|
+
"form_data_body: dict[str, Any] = DataclassSerializer.serialize(form_data) # Fallback type"
|
177
177
|
)
|
178
178
|
elif resolved_body_type == "bytes": # e.g. application/octet-stream
|
179
179
|
# bytes_content is the expected parameter name from EndpointParameterProcessor
|
@@ -5,7 +5,7 @@ Helper class for analyzing an IROperation and registering necessary imports.
|
|
5
5
|
from __future__ import annotations
|
6
6
|
|
7
7
|
import logging
|
8
|
-
from typing import TYPE_CHECKING, Any
|
8
|
+
from typing import TYPE_CHECKING, Any # IO for multipart type hint
|
9
9
|
|
10
10
|
# Necessary helpers for type analysis
|
11
11
|
from pyopenapi_gen.helpers.endpoint_utils import (
|
@@ -24,8 +24,8 @@ logger = logging.getLogger(__name__)
|
|
24
24
|
class EndpointImportAnalyzer:
|
25
25
|
"""Analyzes an IROperation to determine and register required imports."""
|
26
26
|
|
27
|
-
def __init__(self, schemas:
|
28
|
-
self.schemas:
|
27
|
+
def __init__(self, schemas: dict[str, Any] | None = None) -> None:
|
28
|
+
self.schemas: dict[str, Any] = schemas or {}
|
29
29
|
|
30
30
|
def analyze_and_register_imports(
|
31
31
|
self,
|
@@ -40,21 +40,21 @@ class EndpointImportAnalyzer:
|
|
40
40
|
|
41
41
|
if op.request_body:
|
42
42
|
content_types = op.request_body.content.keys()
|
43
|
-
body_param_type:
|
43
|
+
body_param_type: str | None = None
|
44
44
|
if "multipart/form-data" in content_types:
|
45
|
-
# Type for multipart is
|
45
|
+
# Type for multipart is dict[str, IO[Any]] which requires IO and Any
|
46
46
|
context.add_import("typing", "Dict")
|
47
47
|
context.add_import("typing", "IO")
|
48
48
|
context.add_import("typing", "Any")
|
49
|
-
# The actual type string "
|
49
|
+
# The actual type string "dict[str, IO[Any]]" will be handled by add_typing_imports_for_type if passed
|
50
50
|
# but ensuring components are imported is key.
|
51
|
-
body_param_type = "
|
51
|
+
body_param_type = "dict[str, IO[Any]]"
|
52
52
|
elif "application/json" in content_types:
|
53
53
|
body_param_type = get_request_body_type(op.request_body, context, self.schemas)
|
54
54
|
elif "application/x-www-form-urlencoded" in content_types:
|
55
55
|
context.add_import("typing", "Dict")
|
56
56
|
context.add_import("typing", "Any")
|
57
|
-
body_param_type = "
|
57
|
+
body_param_type = "dict[str, Any]"
|
58
58
|
elif content_types: # Fallback for other types like application/octet-stream
|
59
59
|
body_param_type = "bytes"
|
60
60
|
|
@@ -5,7 +5,7 @@ Helper class for processing parameters for an endpoint method.
|
|
5
5
|
from __future__ import annotations
|
6
6
|
|
7
7
|
import logging
|
8
|
-
from typing import TYPE_CHECKING, Any,
|
8
|
+
from typing import TYPE_CHECKING, Any, List, Tuple
|
9
9
|
|
10
10
|
from pyopenapi_gen.core.utils import NameSanitizer
|
11
11
|
from pyopenapi_gen.helpers.endpoint_utils import get_param_type, get_request_body_type
|
@@ -24,12 +24,12 @@ class EndpointParameterProcessor:
|
|
24
24
|
method parameters for the endpoint signature and further processing.
|
25
25
|
"""
|
26
26
|
|
27
|
-
def __init__(self, schemas:
|
28
|
-
self.schemas:
|
27
|
+
def __init__(self, schemas: dict[str, Any] | None = None) -> None:
|
28
|
+
self.schemas: dict[str, Any] = schemas or {}
|
29
29
|
|
30
30
|
def process_parameters(
|
31
31
|
self, op: IROperation, context: RenderContext
|
32
|
-
) -> Tuple[List[
|
32
|
+
) -> Tuple[List[dict[str, Any]], str | None, str | None]:
|
33
33
|
"""
|
34
34
|
Prepares and orders parameters for an endpoint method, including path,
|
35
35
|
query, header, and request body parameters.
|
@@ -40,8 +40,8 @@ class EndpointParameterProcessor:
|
|
40
40
|
- primary_content_type: The dominant content type for the request body.
|
41
41
|
- resolved_body_type: The Python type hint for the request body.
|
42
42
|
"""
|
43
|
-
ordered_params: List[
|
44
|
-
param_details_map:
|
43
|
+
ordered_params: List[dict[str, Any]] = []
|
44
|
+
param_details_map: dict[str, dict[str, Any]] = {}
|
45
45
|
|
46
46
|
for param in op.parameters:
|
47
47
|
param_name_sanitized = NameSanitizer.sanitize_method_name(param.name)
|
@@ -56,21 +56,21 @@ class EndpointParameterProcessor:
|
|
56
56
|
ordered_params.append(param_info)
|
57
57
|
param_details_map[param_name_sanitized] = param_info
|
58
58
|
|
59
|
-
primary_content_type:
|
60
|
-
resolved_body_type:
|
59
|
+
primary_content_type: str | None = None
|
60
|
+
resolved_body_type: str | None = None
|
61
61
|
|
62
62
|
if op.request_body:
|
63
63
|
content_types = op.request_body.content.keys()
|
64
64
|
body_param_name = "body" # Default name
|
65
65
|
context.add_import("typing", "Any") # General fallback
|
66
|
-
body_specific_param_info:
|
66
|
+
body_specific_param_info: dict[str, Any] | None = None
|
67
67
|
|
68
68
|
if "multipart/form-data" in content_types:
|
69
69
|
primary_content_type = "multipart/form-data"
|
70
70
|
body_param_name = "files"
|
71
71
|
context.add_import("typing", "Dict")
|
72
72
|
context.add_import("typing", "IO")
|
73
|
-
resolved_body_type = "
|
73
|
+
resolved_body_type = "dict[str, IO[Any]]"
|
74
74
|
body_specific_param_info = {
|
75
75
|
"name": body_param_name,
|
76
76
|
"type": resolved_body_type,
|
@@ -95,7 +95,7 @@ class EndpointParameterProcessor:
|
|
95
95
|
primary_content_type = "application/x-www-form-urlencoded"
|
96
96
|
body_param_name = "form_data"
|
97
97
|
context.add_import("typing", "Dict")
|
98
|
-
resolved_body_type = "
|
98
|
+
resolved_body_type = "dict[str, Any]"
|
99
99
|
body_specific_param_info = {
|
100
100
|
"name": body_param_name,
|
101
101
|
"type": resolved_body_type,
|
@@ -138,8 +138,8 @@ class EndpointParameterProcessor:
|
|
138
138
|
return final_ordered_params, primary_content_type, resolved_body_type
|
139
139
|
|
140
140
|
def _ensure_path_variables_as_params(
|
141
|
-
self, op: IROperation, current_params: List[
|
142
|
-
) -> List[
|
141
|
+
self, op: IROperation, current_params: List[dict[str, Any]], param_details_map: dict[str, dict[str, Any]]
|
142
|
+
) -> List[dict[str, Any]]:
|
143
143
|
"""
|
144
144
|
Ensures that all variables in the URL path are present in the list of parameters.
|
145
145
|
If a path variable is not already defined as a parameter, it's added as a required string type.
|
@@ -3,13 +3,11 @@ Generates Python code for type aliases from IRSchema objects.
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
import logging
|
6
|
-
from typing import Dict, Optional
|
7
6
|
|
8
7
|
from pyopenapi_gen import IRSchema
|
9
8
|
from pyopenapi_gen.context.render_context import RenderContext
|
10
9
|
from pyopenapi_gen.core.utils import NameSanitizer
|
11
10
|
from pyopenapi_gen.core.writers.python_construct_renderer import PythonConstructRenderer
|
12
|
-
from pyopenapi_gen.helpers.type_resolution.finalizer import TypeFinalizer
|
13
11
|
from pyopenapi_gen.types.services.type_service import UnifiedTypeService
|
14
12
|
|
15
13
|
logger = logging.getLogger(__name__)
|
@@ -21,7 +19,7 @@ class AliasGenerator:
|
|
21
19
|
def __init__(
|
22
20
|
self,
|
23
21
|
renderer: PythonConstructRenderer,
|
24
|
-
all_schemas:
|
22
|
+
all_schemas: dict[str, IRSchema] | None,
|
25
23
|
):
|
26
24
|
# Pre-condition
|
27
25
|
if renderer is None:
|
@@ -70,7 +68,6 @@ class AliasGenerator:
|
|
70
68
|
|
71
69
|
alias_name = NameSanitizer.sanitize_class_name(base_name)
|
72
70
|
target_type = self.type_service.resolve_schema_type(schema, context, required=True, resolve_underlying=True)
|
73
|
-
target_type = TypeFinalizer(context)._clean_type(target_type)
|
74
71
|
|
75
72
|
# logger.debug(f"AliasGenerator: Rendering alias '{alias_name}' for target type '{target_type}'.")
|
76
73
|
|
@@ -4,7 +4,7 @@ Generates Python code for dataclasses from IRSchema objects.
|
|
4
4
|
|
5
5
|
import json
|
6
6
|
import logging
|
7
|
-
from typing import Any,
|
7
|
+
from typing import Any, List, Tuple
|
8
8
|
|
9
9
|
from pyopenapi_gen import IRSchema
|
10
10
|
from pyopenapi_gen.context.render_context import RenderContext
|
@@ -22,7 +22,7 @@ class DataclassGenerator:
|
|
22
22
|
def __init__(
|
23
23
|
self,
|
24
24
|
renderer: PythonConstructRenderer,
|
25
|
-
all_schemas:
|
25
|
+
all_schemas: dict[str, IRSchema] | None,
|
26
26
|
):
|
27
27
|
"""
|
28
28
|
Initialize a new DataclassGenerator.
|
@@ -37,7 +37,130 @@ class DataclassGenerator:
|
|
37
37
|
self.all_schemas = all_schemas if all_schemas is not None else {}
|
38
38
|
self.type_service = UnifiedTypeService(self.all_schemas)
|
39
39
|
|
40
|
-
def
|
40
|
+
def _is_arbitrary_json_object(self, schema: IRSchema) -> bool:
|
41
|
+
"""
|
42
|
+
Check if schema represents an arbitrary JSON object (additionalProperties but no properties).
|
43
|
+
|
44
|
+
Args:
|
45
|
+
schema: The schema to check.
|
46
|
+
|
47
|
+
Returns:
|
48
|
+
True if this is an arbitrary JSON object that should use wrapper class.
|
49
|
+
"""
|
50
|
+
return (
|
51
|
+
schema.type == "object"
|
52
|
+
and not schema.properties # No defined properties
|
53
|
+
and (
|
54
|
+
schema.additional_properties is True # Explicit true
|
55
|
+
or isinstance(schema.additional_properties, IRSchema) # Schema for additional props
|
56
|
+
)
|
57
|
+
)
|
58
|
+
|
59
|
+
def _generate_json_wrapper_class(
|
60
|
+
self,
|
61
|
+
class_name: str,
|
62
|
+
schema: IRSchema,
|
63
|
+
context: RenderContext,
|
64
|
+
) -> str:
|
65
|
+
"""
|
66
|
+
Generate a wrapper class for arbitrary JSON objects.
|
67
|
+
|
68
|
+
This wrapper class preserves all JSON data and provides dict-like access,
|
69
|
+
preventing data loss when deserializing API responses with arbitrary properties.
|
70
|
+
|
71
|
+
Args:
|
72
|
+
class_name: Name of the wrapper class.
|
73
|
+
schema: The schema (should have additionalProperties but no properties).
|
74
|
+
context: Render context for imports.
|
75
|
+
|
76
|
+
Returns:
|
77
|
+
Python code for the wrapper class.
|
78
|
+
"""
|
79
|
+
# Register required imports
|
80
|
+
context.add_import("dataclasses", "dataclass")
|
81
|
+
context.add_import("dataclasses", "field")
|
82
|
+
context.add_import("typing", "Any")
|
83
|
+
context.add_import("..core.schemas", "BaseSchema")
|
84
|
+
|
85
|
+
description = schema.description or "Generic JSON value object that preserves arbitrary data."
|
86
|
+
|
87
|
+
# Generate the wrapper class code
|
88
|
+
code = f'''__all__ = ["{class_name}"]
|
89
|
+
|
90
|
+
@dataclass
|
91
|
+
class {class_name}(BaseSchema):
|
92
|
+
"""
|
93
|
+
{description}
|
94
|
+
|
95
|
+
This class wraps arbitrary JSON objects with no defined schema,
|
96
|
+
preserving all data during serialization/deserialization.
|
97
|
+
"""
|
98
|
+
|
99
|
+
_data: dict[str, Any] = field(default_factory=dict, repr=False)
|
100
|
+
|
101
|
+
@classmethod
|
102
|
+
def from_dict(cls, data: dict[str, Any]) -> '{class_name}':
|
103
|
+
"""
|
104
|
+
Create {class_name} from dictionary, preserving all data.
|
105
|
+
|
106
|
+
Args:
|
107
|
+
data: Dictionary with arbitrary keys and values.
|
108
|
+
|
109
|
+
Returns:
|
110
|
+
{class_name} instance containing the data.
|
111
|
+
"""
|
112
|
+
instance = cls(_data=data)
|
113
|
+
return instance
|
114
|
+
|
115
|
+
def to_dict(self, exclude_none: bool = False) -> dict[str, Any]:
|
116
|
+
"""
|
117
|
+
Convert back to dictionary.
|
118
|
+
|
119
|
+
Args:
|
120
|
+
exclude_none: If True, exclude None values from output.
|
121
|
+
|
122
|
+
Returns:
|
123
|
+
Dictionary representation of the data.
|
124
|
+
"""
|
125
|
+
if exclude_none:
|
126
|
+
return {{k: v for k, v in self._data.items() if v is not None}}
|
127
|
+
return self._data.copy()
|
128
|
+
|
129
|
+
def get(self, key: str, default: Any = None) -> Any:
|
130
|
+
"""Dict-like get method for backward compatibility."""
|
131
|
+
return self._data.get(key, default)
|
132
|
+
|
133
|
+
def __getitem__(self, key: str) -> Any:
|
134
|
+
"""Dict-like item access."""
|
135
|
+
return self._data[key]
|
136
|
+
|
137
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
138
|
+
"""Dict-like item setting."""
|
139
|
+
self._data[key] = value
|
140
|
+
|
141
|
+
def __contains__(self, key: str) -> bool:
|
142
|
+
"""Dict-like 'in' operator."""
|
143
|
+
return key in self._data
|
144
|
+
|
145
|
+
def __bool__(self) -> bool:
|
146
|
+
"""Truthy if contains data."""
|
147
|
+
return bool(self._data)
|
148
|
+
|
149
|
+
def keys(self) -> Any:
|
150
|
+
"""Return dictionary keys."""
|
151
|
+
return self._data.keys()
|
152
|
+
|
153
|
+
def values(self) -> Any:
|
154
|
+
"""Return dictionary values."""
|
155
|
+
return self._data.values()
|
156
|
+
|
157
|
+
def items(self) -> Any:
|
158
|
+
"""Return dictionary items."""
|
159
|
+
return self._data.items()
|
160
|
+
'''
|
161
|
+
return code
|
162
|
+
|
163
|
+
def _get_field_default(self, ps: IRSchema, context: RenderContext) -> str | None:
|
41
164
|
"""
|
42
165
|
Determines the default value expression string for a dataclass field.
|
43
166
|
This method is called for fields determined to be optional.
|
@@ -88,7 +211,7 @@ class DataclassGenerator:
|
|
88
211
|
"""Check if field mapping is required between API and Python field names."""
|
89
212
|
return api_field != python_field
|
90
213
|
|
91
|
-
def _generate_field_mappings(self, properties:
|
214
|
+
def _generate_field_mappings(self, properties: dict[str, Any], sanitized_names: dict[str, str]) -> dict[str, str]:
|
92
215
|
"""Generate field mappings for BaseSchema configuration."""
|
93
216
|
mappings = {}
|
94
217
|
for api_name, python_name in sanitized_names.items():
|
@@ -96,7 +219,7 @@ class DataclassGenerator:
|
|
96
219
|
mappings[api_name] = python_name
|
97
220
|
return mappings
|
98
221
|
|
99
|
-
def _has_any_mappings(self, properties:
|
222
|
+
def _has_any_mappings(self, properties: dict[str, Any], sanitized_names: dict[str, str]) -> bool:
|
100
223
|
"""Check if any field mappings are needed."""
|
101
224
|
return bool(self._generate_field_mappings(properties, sanitized_names))
|
102
225
|
|
@@ -137,9 +260,17 @@ class DataclassGenerator:
|
|
137
260
|
raise ValueError("RenderContext cannot be None.")
|
138
261
|
# Additional check for schema type might be too strict here, as ModelVisitor decides eligibility.
|
139
262
|
|
263
|
+
# Check if this is an arbitrary JSON object that needs wrapper class
|
264
|
+
if self._is_arbitrary_json_object(schema):
|
265
|
+
logger.info(
|
266
|
+
f"DataclassGenerator: Schema '{base_name}' is an arbitrary JSON object. "
|
267
|
+
"Generating wrapper class to preserve data."
|
268
|
+
)
|
269
|
+
return self._generate_json_wrapper_class(base_name, schema, context)
|
270
|
+
|
140
271
|
class_name = base_name
|
141
|
-
fields_data: List[Tuple[str, str,
|
142
|
-
field_mappings:
|
272
|
+
fields_data: List[Tuple[str, str, str | None, str | None]] = []
|
273
|
+
field_mappings: dict[str, str] = {}
|
143
274
|
|
144
275
|
if schema.type == "array" and schema.items:
|
145
276
|
field_name_for_array_content = "items"
|
@@ -147,7 +278,6 @@ class DataclassGenerator:
|
|
147
278
|
raise ValueError("Schema items must be present for array type dataclass field.")
|
148
279
|
|
149
280
|
list_item_py_type = self.type_service.resolve_schema_type(schema.items, context, required=True)
|
150
|
-
list_item_py_type = TypeFinalizer(context)._clean_type(list_item_py_type)
|
151
281
|
field_type_str = f"List[{list_item_py_type}]"
|
152
282
|
|
153
283
|
final_field_type_str = TypeFinalizer(context).finalize(
|
@@ -191,9 +321,8 @@ class DataclassGenerator:
|
|
191
321
|
field_mappings[prop_name] = field_name
|
192
322
|
|
193
323
|
py_type = self.type_service.resolve_schema_type(prop_schema, context, required=is_required)
|
194
|
-
py_type = TypeFinalizer(context)._clean_type(py_type)
|
195
324
|
|
196
|
-
default_expr:
|
325
|
+
default_expr: str | None = None
|
197
326
|
if not is_required:
|
198
327
|
default_expr = self._get_field_default(prop_schema, context)
|
199
328
|
|
@@ -6,7 +6,6 @@ defined in OpenAPI specifications, supporting type aliases, enums, and dataclass
|
|
6
6
|
"""
|
7
7
|
|
8
8
|
import logging
|
9
|
-
from typing import Dict, Optional
|
10
9
|
|
11
10
|
from pyopenapi_gen import IRSchema
|
12
11
|
from pyopenapi_gen.context.render_context import RenderContext
|
@@ -36,7 +35,7 @@ class ModelVisitor(Visitor[IRSchema, str]):
|
|
36
35
|
- All necessary imports for the generated model are registered in the context.
|
37
36
|
"""
|
38
37
|
|
39
|
-
def __init__(self, schemas:
|
38
|
+
def __init__(self, schemas: dict[str, IRSchema] | None = None) -> None:
|
40
39
|
"""
|
41
40
|
Initialize a new ModelVisitor.
|
42
41
|
|
@@ -156,7 +155,7 @@ class ModelVisitor(Visitor[IRSchema, str]):
|
|
156
155
|
|
157
156
|
return self.formatter.format(rendered_code)
|
158
157
|
|
159
|
-
def _get_field_default(self, ps: IRSchema, context: RenderContext) ->
|
158
|
+
def _get_field_default(self, ps: IRSchema, context: RenderContext) -> str | None:
|
160
159
|
"""
|
161
160
|
Determines the default value expression string for a dataclass field.
|
162
161
|
This method is called for fields determined to be optional.
|