pyopenapi-gen 0.8.3__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/__init__.py +114 -0
- pyopenapi_gen/__main__.py +6 -0
- pyopenapi_gen/cli.py +86 -0
- pyopenapi_gen/context/file_manager.py +52 -0
- pyopenapi_gen/context/import_collector.py +382 -0
- pyopenapi_gen/context/render_context.py +630 -0
- pyopenapi_gen/core/__init__.py +0 -0
- pyopenapi_gen/core/auth/base.py +22 -0
- pyopenapi_gen/core/auth/plugins.py +89 -0
- pyopenapi_gen/core/exceptions.py +25 -0
- pyopenapi_gen/core/http_transport.py +219 -0
- pyopenapi_gen/core/loader/__init__.py +12 -0
- pyopenapi_gen/core/loader/loader.py +158 -0
- pyopenapi_gen/core/loader/operations/__init__.py +12 -0
- pyopenapi_gen/core/loader/operations/parser.py +155 -0
- pyopenapi_gen/core/loader/operations/post_processor.py +60 -0
- pyopenapi_gen/core/loader/operations/request_body.py +85 -0
- pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
- pyopenapi_gen/core/loader/parameters/parser.py +121 -0
- pyopenapi_gen/core/loader/responses/__init__.py +10 -0
- pyopenapi_gen/core/loader/responses/parser.py +104 -0
- pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
- pyopenapi_gen/core/loader/schemas/extractor.py +184 -0
- pyopenapi_gen/core/pagination.py +64 -0
- pyopenapi_gen/core/parsing/__init__.py +13 -0
- pyopenapi_gen/core/parsing/common/__init__.py +1 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/__init__.py +9 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/__init__.py +0 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/cyclic_properties.py +66 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/direct_cycle.py +33 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/existing_schema.py +22 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +54 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +52 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +50 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +51 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +86 -0
- pyopenapi_gen/core/parsing/common/type_parser.py +74 -0
- pyopenapi_gen/core/parsing/context.py +184 -0
- pyopenapi_gen/core/parsing/cycle_helpers.py +123 -0
- pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
- pyopenapi_gen/core/parsing/keywords/all_of_parser.py +77 -0
- pyopenapi_gen/core/parsing/keywords/any_of_parser.py +79 -0
- pyopenapi_gen/core/parsing/keywords/array_items_parser.py +69 -0
- pyopenapi_gen/core/parsing/keywords/one_of_parser.py +72 -0
- pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
- pyopenapi_gen/core/parsing/schema_finalizer.py +166 -0
- pyopenapi_gen/core/parsing/schema_parser.py +610 -0
- pyopenapi_gen/core/parsing/transformers/__init__.py +0 -0
- pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +285 -0
- pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +117 -0
- pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
- pyopenapi_gen/core/postprocess_manager.py +161 -0
- pyopenapi_gen/core/schemas.py +40 -0
- pyopenapi_gen/core/streaming_helpers.py +86 -0
- pyopenapi_gen/core/telemetry.py +67 -0
- pyopenapi_gen/core/utils.py +409 -0
- pyopenapi_gen/core/warning_collector.py +83 -0
- pyopenapi_gen/core/writers/code_writer.py +135 -0
- pyopenapi_gen/core/writers/documentation_writer.py +222 -0
- pyopenapi_gen/core/writers/line_writer.py +217 -0
- pyopenapi_gen/core/writers/python_construct_renderer.py +274 -0
- pyopenapi_gen/core_package_template/README.md +21 -0
- pyopenapi_gen/emit/models_emitter.py +143 -0
- pyopenapi_gen/emitters/client_emitter.py +51 -0
- pyopenapi_gen/emitters/core_emitter.py +181 -0
- pyopenapi_gen/emitters/docs_emitter.py +44 -0
- pyopenapi_gen/emitters/endpoints_emitter.py +223 -0
- pyopenapi_gen/emitters/exceptions_emitter.py +52 -0
- pyopenapi_gen/emitters/models_emitter.py +428 -0
- pyopenapi_gen/generator/client_generator.py +562 -0
- pyopenapi_gen/helpers/__init__.py +1 -0
- pyopenapi_gen/helpers/endpoint_utils.py +552 -0
- pyopenapi_gen/helpers/type_cleaner.py +341 -0
- pyopenapi_gen/helpers/type_helper.py +112 -0
- pyopenapi_gen/helpers/type_resolution/__init__.py +1 -0
- pyopenapi_gen/helpers/type_resolution/array_resolver.py +57 -0
- pyopenapi_gen/helpers/type_resolution/composition_resolver.py +79 -0
- pyopenapi_gen/helpers/type_resolution/finalizer.py +89 -0
- pyopenapi_gen/helpers/type_resolution/named_resolver.py +174 -0
- pyopenapi_gen/helpers/type_resolution/object_resolver.py +212 -0
- pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +57 -0
- pyopenapi_gen/helpers/type_resolution/resolver.py +48 -0
- pyopenapi_gen/helpers/url_utils.py +14 -0
- pyopenapi_gen/http_types.py +20 -0
- pyopenapi_gen/ir.py +167 -0
- pyopenapi_gen/py.typed +1 -0
- pyopenapi_gen/types/__init__.py +11 -0
- pyopenapi_gen/types/contracts/__init__.py +13 -0
- pyopenapi_gen/types/contracts/protocols.py +106 -0
- pyopenapi_gen/types/contracts/types.py +30 -0
- pyopenapi_gen/types/resolvers/__init__.py +7 -0
- pyopenapi_gen/types/resolvers/reference_resolver.py +71 -0
- pyopenapi_gen/types/resolvers/response_resolver.py +203 -0
- pyopenapi_gen/types/resolvers/schema_resolver.py +367 -0
- pyopenapi_gen/types/services/__init__.py +5 -0
- pyopenapi_gen/types/services/type_service.py +133 -0
- pyopenapi_gen/visit/client_visitor.py +228 -0
- pyopenapi_gen/visit/docs_visitor.py +38 -0
- pyopenapi_gen/visit/endpoint/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/endpoint_visitor.py +103 -0
- pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +121 -0
- pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +87 -0
- pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
- pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +497 -0
- pyopenapi_gen/visit/endpoint/generators/signature_generator.py +88 -0
- pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +183 -0
- pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +76 -0
- pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
- pyopenapi_gen/visit/exception_visitor.py +52 -0
- pyopenapi_gen/visit/model/__init__.py +0 -0
- pyopenapi_gen/visit/model/alias_generator.py +89 -0
- pyopenapi_gen/visit/model/dataclass_generator.py +197 -0
- pyopenapi_gen/visit/model/enum_generator.py +200 -0
- pyopenapi_gen/visit/model/model_visitor.py +197 -0
- pyopenapi_gen/visit/visitor.py +97 -0
- pyopenapi_gen-0.8.3.dist-info/METADATA +224 -0
- pyopenapi_gen-0.8.3.dist-info/RECORD +122 -0
- pyopenapi_gen-0.8.3.dist-info/WHEEL +4 -0
- pyopenapi_gen-0.8.3.dist-info/entry_points.txt +2 -0
- pyopenapi_gen-0.8.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,88 @@
|
|
1
|
+
"""
|
2
|
+
Helper class for generating the method signature for an endpoint.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from __future__ import annotations
|
6
|
+
|
7
|
+
import logging
|
8
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
9
|
+
|
10
|
+
from pyopenapi_gen.core.utils import NameSanitizer
|
11
|
+
from pyopenapi_gen.core.writers.code_writer import CodeWriter
|
12
|
+
|
13
|
+
# Import necessary helpers from endpoint_utils if they were used directly or indirectly
|
14
|
+
from pyopenapi_gen.helpers.endpoint_utils import get_param_type, get_return_type_unified # Added
|
15
|
+
|
16
|
+
if TYPE_CHECKING:
|
17
|
+
from pyopenapi_gen import IROperation
|
18
|
+
from pyopenapi_gen.context.render_context import RenderContext
|
19
|
+
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
|
22
|
+
|
23
|
+
class EndpointMethodSignatureGenerator:
|
24
|
+
"""Generates the Python method signature for an endpoint operation."""
|
25
|
+
|
26
|
+
def __init__(self, schemas: Optional[Dict[str, Any]] = None) -> None:
|
27
|
+
self.schemas: Dict[str, Any] = schemas or {}
|
28
|
+
|
29
|
+
def generate_signature(
|
30
|
+
self,
|
31
|
+
writer: CodeWriter,
|
32
|
+
op: IROperation,
|
33
|
+
context: RenderContext,
|
34
|
+
ordered_params: List[Dict[str, Any]],
|
35
|
+
) -> None:
|
36
|
+
"""Writes the method signature to the provided CodeWriter."""
|
37
|
+
# Logic from EndpointMethodGenerator._write_method_signature
|
38
|
+
for p_info in ordered_params: # Renamed p to p_info to avoid conflict if IRParameter is named p
|
39
|
+
context.add_typing_imports_for_type(p_info["type"])
|
40
|
+
|
41
|
+
return_type = get_return_type_unified(op, context, self.schemas)
|
42
|
+
context.add_typing_imports_for_type(return_type)
|
43
|
+
|
44
|
+
# Check if AsyncIterator is in return_type or any parameter type
|
45
|
+
# Note: op.parameters contains IRParameter objects, not the dicts in ordered_params directly
|
46
|
+
# We need to re-calculate param_type for op.parameters if we want to be fully independent here
|
47
|
+
# For now, assuming ordered_params covers all type information needed for imports or that context handles it.
|
48
|
+
# If direct access to op.parameters schema is needed, get_param_type might be called again here.
|
49
|
+
# For simplicity, this check will just look at the final return_type string for now.
|
50
|
+
# A more robust solution might involve a richer parameter object passed to this generator.
|
51
|
+
if "AsyncIterator" in return_type:
|
52
|
+
context.add_plain_import("collections.abc")
|
53
|
+
# A more complete check for AsyncIterator in parameters:
|
54
|
+
for param_spec in op.parameters: # Iterate over IROperation's parameters
|
55
|
+
# This get_param_type call might be redundant if ordered_params already has fully resolved types
|
56
|
+
# and context.add_typing_imports_for_type(p_info["type"]) handled it.
|
57
|
+
# However, to be safe and explicit about where AsyncIterator might come from:
|
58
|
+
param_py_type = get_param_type(param_spec, context, self.schemas)
|
59
|
+
if "AsyncIterator" in param_py_type:
|
60
|
+
context.add_plain_import("collections.abc")
|
61
|
+
break # Found one, no need to check further
|
62
|
+
|
63
|
+
args = ["self"]
|
64
|
+
for p_orig in ordered_params:
|
65
|
+
p = p_orig.copy() # Work with a copy
|
66
|
+
arg_str = f"{NameSanitizer.sanitize_method_name(p['name'])}: {p['type']}" # Ensure param name is sanitized
|
67
|
+
if not p.get("required", False):
|
68
|
+
# Default value handling: if default is None, it should be ' = None'
|
69
|
+
# If default is a string, it should be ' = "default_value"'
|
70
|
+
# Otherwise, ' = default_value'
|
71
|
+
default_val = p.get("default")
|
72
|
+
if default_val is None and not p.get("required", False): # Explicitly check for None for Optional types
|
73
|
+
arg_str += f" = None"
|
74
|
+
elif default_val is not None: # Only add if default is not None
|
75
|
+
if isinstance(default_val, str):
|
76
|
+
arg_str += f' = "{default_val}"'
|
77
|
+
else:
|
78
|
+
arg_str += f" = {default_val}"
|
79
|
+
args.append(arg_str)
|
80
|
+
|
81
|
+
actual_return_type = return_type
|
82
|
+
writer.write_function_signature(
|
83
|
+
NameSanitizer.sanitize_method_name(op.operation_id),
|
84
|
+
args,
|
85
|
+
return_type=actual_return_type,
|
86
|
+
async_=True,
|
87
|
+
)
|
88
|
+
writer.indent() # Keep the indent call as the original method did
|
@@ -0,0 +1,183 @@
|
|
1
|
+
"""
|
2
|
+
Helper class for generating URL, query parameters, and header parameters for an endpoint method.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from __future__ import annotations
|
6
|
+
|
7
|
+
import logging
|
8
|
+
import re # For _build_url_with_path_vars
|
9
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
10
|
+
|
11
|
+
from pyopenapi_gen.core.utils import NameSanitizer
|
12
|
+
from pyopenapi_gen.core.writers.code_writer import CodeWriter
|
13
|
+
|
14
|
+
if TYPE_CHECKING:
|
15
|
+
from pyopenapi_gen import IROperation # IRParameter might be needed for op.parameters access
|
16
|
+
from pyopenapi_gen.context.render_context import RenderContext
|
17
|
+
|
18
|
+
logger = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
|
21
|
+
class EndpointUrlArgsGenerator:
|
22
|
+
"""Generates URL, query, and header parameters for an endpoint method."""
|
23
|
+
|
24
|
+
def __init__(self, schemas: Optional[Dict[str, Any]] = None) -> None:
|
25
|
+
self.schemas: Dict[str, Any] = schemas or {}
|
26
|
+
|
27
|
+
def _build_url_with_path_vars(self, path: str) -> str:
|
28
|
+
"""Builds the f-string for URL construction, substituting path variables."""
|
29
|
+
# Ensure m.group(1) is treated as a string for NameSanitizer
|
30
|
+
# Build the URL f-string by substituting path variables
|
31
|
+
formatted_path = re.sub(
|
32
|
+
r"{([^}]+)}", lambda m: f"{{{NameSanitizer.sanitize_method_name(str(m.group(1)))}}}", path
|
33
|
+
)
|
34
|
+
return f'f"{{self.base_url}}{formatted_path}"'
|
35
|
+
|
36
|
+
def _write_query_params(
|
37
|
+
self, writer: CodeWriter, op: IROperation, ordered_params: List[Dict[str, Any]], context: RenderContext
|
38
|
+
) -> None:
|
39
|
+
"""Writes query parameter dictionary construction."""
|
40
|
+
# Logic from EndpointMethodGenerator._write_query_params
|
41
|
+
query_params_to_write = [p for p in ordered_params if p.get("param_in") == "query"]
|
42
|
+
if not query_params_to_write:
|
43
|
+
# writer.write_line("# No query parameters to write") # Optional: for clarity during debugging
|
44
|
+
return
|
45
|
+
|
46
|
+
for i, p in enumerate(query_params_to_write):
|
47
|
+
param_var_name = NameSanitizer.sanitize_method_name(p["name"]) # Ensure name is sanitized
|
48
|
+
original_param_name = p["original_name"]
|
49
|
+
line_end = "," # Always add comma, let formatter handle final one if needed
|
50
|
+
|
51
|
+
if p.get("required", False):
|
52
|
+
writer.write_line(f' "{original_param_name}": {param_var_name}{line_end}')
|
53
|
+
else:
|
54
|
+
# Using dict unpacking for conditional parameters
|
55
|
+
writer.write_line(
|
56
|
+
f' **({{"{original_param_name}": {param_var_name}}} '
|
57
|
+
f"if {param_var_name} is not None else {{}}){line_end}"
|
58
|
+
)
|
59
|
+
|
60
|
+
def _write_header_params(
|
61
|
+
self, writer: CodeWriter, op: IROperation, ordered_params: List[Dict[str, Any]], context: RenderContext
|
62
|
+
) -> None:
|
63
|
+
"""Writes header parameter dictionary construction."""
|
64
|
+
# Logic from EndpointMethodGenerator._write_header_params
|
65
|
+
# Iterate through ordered_params to find header params, op.parameters may not be directly useful here
|
66
|
+
# if ordered_params is the sole source of truth for method params.
|
67
|
+
header_params_to_write = [p for p in ordered_params if p.get("param_in") == "header"]
|
68
|
+
|
69
|
+
for p_info in header_params_to_write:
|
70
|
+
param_var_name = NameSanitizer.sanitize_method_name(
|
71
|
+
p_info["name"]
|
72
|
+
) # Sanitized name used in method signature
|
73
|
+
original_header_name = p_info["original_name"] # Actual header name for the request
|
74
|
+
line_end = ","
|
75
|
+
|
76
|
+
if p_info.get("required", False):
|
77
|
+
writer.write_line(f' "{original_header_name}": {param_var_name}{line_end}')
|
78
|
+
else:
|
79
|
+
# Conditional inclusion for optional headers
|
80
|
+
# This assumes that if an optional header parameter is None, it should not be sent.
|
81
|
+
# If specific behavior (e.g. empty string) is needed for None, logic would adjust.
|
82
|
+
writer.write_line(
|
83
|
+
f' **({{"{original_header_name}": {param_var_name}}} '
|
84
|
+
f"if {param_var_name} is not None else {{}}){line_end}"
|
85
|
+
)
|
86
|
+
|
87
|
+
def generate_url_and_args(
|
88
|
+
self,
|
89
|
+
writer: CodeWriter,
|
90
|
+
op: IROperation,
|
91
|
+
context: RenderContext,
|
92
|
+
ordered_params: List[Dict[str, Any]],
|
93
|
+
primary_content_type: Optional[str],
|
94
|
+
resolved_body_type: Optional[str],
|
95
|
+
) -> bool:
|
96
|
+
"""Writes URL, query, and header parameters. Returns True if header params were written."""
|
97
|
+
# Main logic from EndpointMethodGenerator._write_url_and_args
|
98
|
+
url_expr = self._build_url_with_path_vars(op.path)
|
99
|
+
writer.write_line(f"url = {url_expr}")
|
100
|
+
writer.write_line("") # Add a blank line for readability
|
101
|
+
|
102
|
+
# Query Parameters
|
103
|
+
# Check if any parameter in ordered_params is a query param, not just op.parameters
|
104
|
+
has_spec_query_params = any(p.get("param_in") == "query" for p in ordered_params)
|
105
|
+
if has_spec_query_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
|
+
# writer.indent() # Indentation should be handled by CodeWriter when writing lines
|
110
|
+
self._write_query_params(writer, op, ordered_params, context)
|
111
|
+
# writer.dedent()
|
112
|
+
writer.write_line("}")
|
113
|
+
writer.write_line("") # Add a blank line
|
114
|
+
|
115
|
+
# Header Parameters
|
116
|
+
has_header_params = any(p.get("param_in") == "header" for p in ordered_params)
|
117
|
+
if has_header_params:
|
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
|
+
# writer.indent()
|
122
|
+
self._write_header_params(writer, op, ordered_params, context)
|
123
|
+
# writer.dedent()
|
124
|
+
writer.write_line("}")
|
125
|
+
writer.write_line("") # Add a blank line
|
126
|
+
|
127
|
+
# Request Body related local variables (json_body, files_data, etc.)
|
128
|
+
# This part was in _write_url_and_args in the original, it sets up variables used by _write_request
|
129
|
+
if op.request_body:
|
130
|
+
# Import DataclassSerializer for automatic conversion
|
131
|
+
context.add_import(f"{context.core_package_name}.utils", "DataclassSerializer")
|
132
|
+
|
133
|
+
if primary_content_type == "application/json":
|
134
|
+
body_param_detail = next((p for p in ordered_params if p["name"] == "body"), None)
|
135
|
+
if body_param_detail:
|
136
|
+
actual_body_type_from_signature = body_param_detail["type"]
|
137
|
+
context.add_typing_imports_for_type(actual_body_type_from_signature)
|
138
|
+
writer.write_line(
|
139
|
+
f"json_body: {actual_body_type_from_signature} = DataclassSerializer.serialize(body)"
|
140
|
+
)
|
141
|
+
else:
|
142
|
+
logger.warning(
|
143
|
+
f"Operation {op.operation_id}: 'body' parameter not found in "
|
144
|
+
f"ordered_params for JSON. Defaulting to Any."
|
145
|
+
)
|
146
|
+
context.add_import("typing", "Any")
|
147
|
+
writer.write_line("json_body: Any = DataclassSerializer.serialize(body) # param not found")
|
148
|
+
elif primary_content_type == "multipart/form-data":
|
149
|
+
files_param_details = next((p for p in ordered_params if p["name"] == "files"), None)
|
150
|
+
if files_param_details:
|
151
|
+
actual_files_param_type = files_param_details["type"]
|
152
|
+
context.add_typing_imports_for_type(actual_files_param_type)
|
153
|
+
writer.write_line(f"files_data: {actual_files_param_type} = DataclassSerializer.serialize(files)")
|
154
|
+
else:
|
155
|
+
logger.warning(
|
156
|
+
f"Operation {op.operation_id}: Could not find 'files' parameter details "
|
157
|
+
f"for multipart/form-data. Defaulting type."
|
158
|
+
)
|
159
|
+
context.add_import("typing", "Dict")
|
160
|
+
context.add_import("typing", "IO") # For IO[Any]
|
161
|
+
context.add_import("typing", "Any")
|
162
|
+
writer.write_line(
|
163
|
+
"files_data: Dict[str, IO[Any]] = DataclassSerializer.serialize(files) # type failed"
|
164
|
+
)
|
165
|
+
elif primary_content_type == "application/x-www-form-urlencoded":
|
166
|
+
# form_data is the expected parameter name from EndpointParameterProcessor
|
167
|
+
# resolved_body_type should be Dict[str, Any]
|
168
|
+
if resolved_body_type:
|
169
|
+
writer.write_line(
|
170
|
+
f"form_data_body: {resolved_body_type} = DataclassSerializer.serialize(form_data)"
|
171
|
+
)
|
172
|
+
else: # Should not happen if EndpointParameterProcessor sets it
|
173
|
+
context.add_import("typing", "Dict")
|
174
|
+
context.add_import("typing", "Any")
|
175
|
+
writer.write_line(
|
176
|
+
"form_data_body: Dict[str, Any] = DataclassSerializer.serialize(form_data) # Fallback type"
|
177
|
+
)
|
178
|
+
elif resolved_body_type == "bytes": # e.g. application/octet-stream
|
179
|
+
# bytes_content is the expected parameter name from EndpointParameterProcessor
|
180
|
+
writer.write_line(f"bytes_body: bytes = bytes_content")
|
181
|
+
writer.write_line("") # Add a blank line after body var setup
|
182
|
+
|
183
|
+
return has_header_params
|
@@ -0,0 +1 @@
|
|
1
|
+
# Happiness is not for sale.
|
@@ -0,0 +1,76 @@
|
|
1
|
+
"""
|
2
|
+
Helper class for analyzing an IROperation and registering necessary imports.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from __future__ import annotations
|
6
|
+
|
7
|
+
import logging
|
8
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional # IO for multipart type hint
|
9
|
+
|
10
|
+
# Necessary helpers for type analysis
|
11
|
+
from pyopenapi_gen.helpers.endpoint_utils import (
|
12
|
+
get_param_type,
|
13
|
+
get_request_body_type,
|
14
|
+
get_return_type_unified,
|
15
|
+
)
|
16
|
+
|
17
|
+
if TYPE_CHECKING:
|
18
|
+
from pyopenapi_gen import IROperation # IRParameter for op.parameters type hint
|
19
|
+
from pyopenapi_gen.context.render_context import RenderContext
|
20
|
+
|
21
|
+
logger = logging.getLogger(__name__)
|
22
|
+
|
23
|
+
|
24
|
+
class EndpointImportAnalyzer:
|
25
|
+
"""Analyzes an IROperation to determine and register required imports."""
|
26
|
+
|
27
|
+
def __init__(self, schemas: Optional[Dict[str, Any]] = None) -> None:
|
28
|
+
self.schemas: Dict[str, Any] = schemas or {}
|
29
|
+
|
30
|
+
def analyze_and_register_imports(
|
31
|
+
self,
|
32
|
+
op: IROperation,
|
33
|
+
context: RenderContext,
|
34
|
+
) -> None:
|
35
|
+
"""Analyzes the operation and registers imports with the RenderContext."""
|
36
|
+
for param in op.parameters: # op.parameters are IRParameter objects
|
37
|
+
py_type = get_param_type(param, context, self.schemas)
|
38
|
+
context.add_typing_imports_for_type(py_type)
|
39
|
+
|
40
|
+
if op.request_body:
|
41
|
+
content_types = op.request_body.content.keys()
|
42
|
+
body_param_type: Optional[str] = None
|
43
|
+
if "multipart/form-data" in content_types:
|
44
|
+
# Type for multipart is Dict[str, IO[Any]] which requires IO and Any
|
45
|
+
context.add_import("typing", "Dict")
|
46
|
+
context.add_import("typing", "IO")
|
47
|
+
context.add_import("typing", "Any")
|
48
|
+
# The actual type string "Dict[str, IO[Any]]" will be handled by add_typing_imports_for_type if passed
|
49
|
+
# but ensuring components are imported is key.
|
50
|
+
body_param_type = "Dict[str, IO[Any]]"
|
51
|
+
elif "application/json" in content_types:
|
52
|
+
body_param_type = get_request_body_type(op.request_body, context, self.schemas)
|
53
|
+
elif "application/x-www-form-urlencoded" in content_types:
|
54
|
+
context.add_import("typing", "Dict")
|
55
|
+
context.add_import("typing", "Any")
|
56
|
+
body_param_type = "Dict[str, Any]"
|
57
|
+
elif content_types: # Fallback for other types like application/octet-stream
|
58
|
+
body_param_type = "bytes"
|
59
|
+
|
60
|
+
if body_param_type:
|
61
|
+
context.add_typing_imports_for_type(body_param_type)
|
62
|
+
|
63
|
+
return_type = get_return_type_unified(op, context, self.schemas)
|
64
|
+
context.add_typing_imports_for_type(return_type)
|
65
|
+
|
66
|
+
# Check for AsyncIterator in return type or parameter types
|
67
|
+
async_iterator_found = "AsyncIterator" in return_type
|
68
|
+
if not async_iterator_found:
|
69
|
+
for param_spec in op.parameters: # Iterate over IROperation's parameters
|
70
|
+
param_py_type = get_param_type(param_spec, context, self.schemas) # Re-check type for safety
|
71
|
+
if "AsyncIterator" in param_py_type:
|
72
|
+
async_iterator_found = True
|
73
|
+
break
|
74
|
+
|
75
|
+
if async_iterator_found:
|
76
|
+
context.add_plain_import("collections.abc")
|
@@ -0,0 +1,171 @@
|
|
1
|
+
"""
|
2
|
+
Helper class for processing parameters for an endpoint method.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from __future__ import annotations
|
6
|
+
|
7
|
+
import logging
|
8
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
|
9
|
+
|
10
|
+
from pyopenapi_gen.core.utils import NameSanitizer
|
11
|
+
from pyopenapi_gen.helpers.endpoint_utils import get_param_type, get_request_body_type
|
12
|
+
from pyopenapi_gen.helpers.url_utils import extract_url_variables
|
13
|
+
|
14
|
+
if TYPE_CHECKING:
|
15
|
+
from pyopenapi_gen import IROperation
|
16
|
+
from pyopenapi_gen.context.render_context import RenderContext
|
17
|
+
|
18
|
+
logger = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
|
21
|
+
class EndpointParameterProcessor:
|
22
|
+
"""
|
23
|
+
Processes IROperation parameters and request body to prepare a list of
|
24
|
+
method parameters for the endpoint signature and further processing.
|
25
|
+
"""
|
26
|
+
|
27
|
+
def __init__(self, schemas: Optional[Dict[str, Any]] = None) -> None:
|
28
|
+
self.schemas: Dict[str, Any] = schemas or {}
|
29
|
+
|
30
|
+
def process_parameters(
|
31
|
+
self, op: IROperation, context: RenderContext
|
32
|
+
) -> Tuple[List[Dict[str, Any]], Optional[str], Optional[str]]:
|
33
|
+
"""
|
34
|
+
Prepares and orders parameters for an endpoint method, including path,
|
35
|
+
query, header, and request body parameters.
|
36
|
+
|
37
|
+
Returns:
|
38
|
+
A tuple containing:
|
39
|
+
- ordered_params: List of parameter dictionaries for method signature.
|
40
|
+
- primary_content_type: The dominant content type for the request body.
|
41
|
+
- resolved_body_type: The Python type hint for the request body.
|
42
|
+
"""
|
43
|
+
ordered_params: List[Dict[str, Any]] = []
|
44
|
+
param_details_map: Dict[str, Dict[str, Any]] = {}
|
45
|
+
|
46
|
+
for param in op.parameters:
|
47
|
+
param_name_sanitized = NameSanitizer.sanitize_method_name(param.name)
|
48
|
+
param_info = {
|
49
|
+
"name": param_name_sanitized,
|
50
|
+
"type": get_param_type(param, context, self.schemas),
|
51
|
+
"required": param.required,
|
52
|
+
"default": param.schema.default if param.schema else None,
|
53
|
+
"param_in": param.param_in,
|
54
|
+
"original_name": param.name,
|
55
|
+
}
|
56
|
+
ordered_params.append(param_info)
|
57
|
+
param_details_map[param_name_sanitized] = param_info
|
58
|
+
|
59
|
+
primary_content_type: Optional[str] = None
|
60
|
+
resolved_body_type: Optional[str] = None
|
61
|
+
|
62
|
+
if op.request_body:
|
63
|
+
content_types = op.request_body.content.keys()
|
64
|
+
body_param_name = "body" # Default name
|
65
|
+
context.add_import("typing", "Any") # General fallback
|
66
|
+
body_specific_param_info: Optional[Dict[str, Any]] = None
|
67
|
+
|
68
|
+
if "multipart/form-data" in content_types:
|
69
|
+
primary_content_type = "multipart/form-data"
|
70
|
+
body_param_name = "files"
|
71
|
+
context.add_import("typing", "Dict")
|
72
|
+
context.add_import("typing", "IO")
|
73
|
+
resolved_body_type = "Dict[str, IO[Any]]"
|
74
|
+
body_specific_param_info = {
|
75
|
+
"name": body_param_name,
|
76
|
+
"type": resolved_body_type,
|
77
|
+
"required": op.request_body.required,
|
78
|
+
"default": None,
|
79
|
+
"param_in": "body",
|
80
|
+
"original_name": body_param_name,
|
81
|
+
}
|
82
|
+
elif "application/json" in content_types:
|
83
|
+
primary_content_type = "application/json"
|
84
|
+
body_param_name = "body"
|
85
|
+
resolved_body_type = get_request_body_type(op.request_body, context, self.schemas)
|
86
|
+
body_specific_param_info = {
|
87
|
+
"name": body_param_name,
|
88
|
+
"type": resolved_body_type,
|
89
|
+
"required": op.request_body.required,
|
90
|
+
"default": None,
|
91
|
+
"param_in": "body",
|
92
|
+
"original_name": body_param_name,
|
93
|
+
}
|
94
|
+
elif "application/x-www-form-urlencoded" in content_types:
|
95
|
+
primary_content_type = "application/x-www-form-urlencoded"
|
96
|
+
body_param_name = "form_data"
|
97
|
+
context.add_import("typing", "Dict")
|
98
|
+
resolved_body_type = "Dict[str, Any]"
|
99
|
+
body_specific_param_info = {
|
100
|
+
"name": body_param_name,
|
101
|
+
"type": resolved_body_type,
|
102
|
+
"required": op.request_body.required,
|
103
|
+
"default": None,
|
104
|
+
"param_in": "body",
|
105
|
+
"original_name": body_param_name,
|
106
|
+
}
|
107
|
+
elif content_types: # Fallback for other content types
|
108
|
+
primary_content_type = list(content_types)[0]
|
109
|
+
body_param_name = "bytes_content" # e.g. for application/octet-stream
|
110
|
+
resolved_body_type = "bytes"
|
111
|
+
body_specific_param_info = {
|
112
|
+
"name": body_param_name,
|
113
|
+
"type": resolved_body_type,
|
114
|
+
"required": op.request_body.required,
|
115
|
+
"default": None,
|
116
|
+
"param_in": "body",
|
117
|
+
"original_name": body_param_name,
|
118
|
+
}
|
119
|
+
|
120
|
+
if body_specific_param_info:
|
121
|
+
if body_specific_param_info["name"] not in param_details_map:
|
122
|
+
ordered_params.append(body_specific_param_info)
|
123
|
+
param_details_map[body_specific_param_info["name"]] = body_specific_param_info
|
124
|
+
else:
|
125
|
+
logger.warning(
|
126
|
+
f"Request body parameter name '{body_specific_param_info['name']}' "
|
127
|
+
f"for operation '{op.operation_id}'"
|
128
|
+
f"collides with an existing path/query/header parameter. Check OpenAPI spec."
|
129
|
+
)
|
130
|
+
|
131
|
+
final_ordered_params = self._ensure_path_variables_as_params(op, ordered_params, param_details_map)
|
132
|
+
|
133
|
+
# Sort parameters: required first, then optional.
|
134
|
+
# We use a stable sort by negating 'required' (True becomes -1, False becomes 0).
|
135
|
+
# Parameters with the same required status maintain their relative order.
|
136
|
+
final_ordered_params.sort(key=lambda p: not p["required"])
|
137
|
+
|
138
|
+
return final_ordered_params, primary_content_type, resolved_body_type
|
139
|
+
|
140
|
+
def _ensure_path_variables_as_params(
|
141
|
+
self, op: IROperation, current_params: List[Dict[str, Any]], param_details_map: Dict[str, Dict[str, Any]]
|
142
|
+
) -> List[Dict[str, Any]]:
|
143
|
+
"""
|
144
|
+
Ensures that all variables in the URL path are present in the list of parameters.
|
145
|
+
If a path variable is not already defined as a parameter, it's added as a required string type.
|
146
|
+
This also updates the param_details_map.
|
147
|
+
"""
|
148
|
+
url_vars = extract_url_variables(op.path)
|
149
|
+
|
150
|
+
# Make a copy to modify if necessary
|
151
|
+
updated_params = list(current_params)
|
152
|
+
|
153
|
+
for var in url_vars:
|
154
|
+
sanitized_var_name = NameSanitizer.sanitize_method_name(var)
|
155
|
+
if sanitized_var_name not in param_details_map:
|
156
|
+
path_var_param_info = {
|
157
|
+
"name": sanitized_var_name,
|
158
|
+
"type": "str", # Path variables are typically strings
|
159
|
+
"required": True, # Path variables are always required
|
160
|
+
"default": None,
|
161
|
+
"param_in": "path",
|
162
|
+
"original_name": var,
|
163
|
+
}
|
164
|
+
updated_params.append(path_var_param_info)
|
165
|
+
param_details_map[sanitized_var_name] = path_var_param_info
|
166
|
+
# logger.debug(
|
167
|
+
# f"Added missing path variable '{sanitized_var_name}' "
|
168
|
+
# f"to parameters for operation '{op.operation_id}'."
|
169
|
+
# )
|
170
|
+
|
171
|
+
return updated_params
|
@@ -0,0 +1,52 @@
|
|
1
|
+
from pyopenapi_gen import IRSpec
|
2
|
+
|
3
|
+
from ..context.render_context import RenderContext
|
4
|
+
from ..core.writers.python_construct_renderer import PythonConstructRenderer
|
5
|
+
|
6
|
+
|
7
|
+
class ExceptionVisitor:
|
8
|
+
"""Visitor for rendering exception alias classes from IRSpec."""
|
9
|
+
|
10
|
+
def __init__(self) -> None:
|
11
|
+
self.renderer = PythonConstructRenderer()
|
12
|
+
|
13
|
+
def visit(self, spec: IRSpec, context: RenderContext) -> tuple[str, list[str]]:
|
14
|
+
# Register base exception imports
|
15
|
+
context.add_import(f"{context.core_package_name}.exceptions", "HTTPError")
|
16
|
+
context.add_import(f"{context.core_package_name}.exceptions", "ClientError")
|
17
|
+
context.add_import(f"{context.core_package_name}.exceptions", "ServerError")
|
18
|
+
context.add_import("httpx", "Response")
|
19
|
+
|
20
|
+
# Collect unique numeric status codes
|
21
|
+
codes = sorted(
|
22
|
+
{int(resp.status_code) for op in spec.operations for resp in op.responses if resp.status_code.isdigit()}
|
23
|
+
)
|
24
|
+
|
25
|
+
all_exception_code = []
|
26
|
+
generated_alias_names = []
|
27
|
+
|
28
|
+
# Use renderer to generate each exception class
|
29
|
+
for code in codes:
|
30
|
+
base_class = "ClientError" if code < 500 else "ServerError"
|
31
|
+
class_name = f"Error{code}"
|
32
|
+
generated_alias_names.append(class_name)
|
33
|
+
docstring = f"Exception alias for HTTP {code} responses."
|
34
|
+
|
35
|
+
# Define the __init__ method body
|
36
|
+
init_method_body = [
|
37
|
+
"def __init__(self, response: Response) -> None:",
|
38
|
+
" super().__init__(status_code=response.status_code, message=response.text, response=response)",
|
39
|
+
]
|
40
|
+
|
41
|
+
exception_code = self.renderer.render_class(
|
42
|
+
class_name=class_name,
|
43
|
+
base_classes=[base_class],
|
44
|
+
docstring=docstring,
|
45
|
+
body_lines=init_method_body,
|
46
|
+
context=context,
|
47
|
+
)
|
48
|
+
all_exception_code.append(exception_code)
|
49
|
+
|
50
|
+
# Join the generated class strings
|
51
|
+
final_code = "\n".join(all_exception_code)
|
52
|
+
return final_code, generated_alias_names
|
File without changes
|
@@ -0,0 +1,89 @@
|
|
1
|
+
"""
|
2
|
+
Generates Python code for type aliases from IRSchema objects.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import logging
|
6
|
+
from typing import Dict, Optional
|
7
|
+
|
8
|
+
from pyopenapi_gen import IRSchema
|
9
|
+
from pyopenapi_gen.context.render_context import RenderContext
|
10
|
+
from pyopenapi_gen.core.utils import NameSanitizer
|
11
|
+
from pyopenapi_gen.core.writers.python_construct_renderer import PythonConstructRenderer
|
12
|
+
from pyopenapi_gen.helpers.type_resolution.finalizer import TypeFinalizer
|
13
|
+
from pyopenapi_gen.types.services.type_service import UnifiedTypeService
|
14
|
+
|
15
|
+
logger = logging.getLogger(__name__)
|
16
|
+
|
17
|
+
|
18
|
+
class AliasGenerator:
|
19
|
+
"""Generates Python code for a type alias."""
|
20
|
+
|
21
|
+
def __init__(
|
22
|
+
self,
|
23
|
+
renderer: PythonConstructRenderer,
|
24
|
+
all_schemas: Optional[Dict[str, IRSchema]],
|
25
|
+
):
|
26
|
+
# Pre-condition
|
27
|
+
assert renderer is not None, "PythonConstructRenderer cannot be None"
|
28
|
+
self.renderer = renderer
|
29
|
+
self.all_schemas = all_schemas if all_schemas is not None else {}
|
30
|
+
self.type_service = UnifiedTypeService(self.all_schemas)
|
31
|
+
|
32
|
+
def generate(
|
33
|
+
self,
|
34
|
+
schema: IRSchema,
|
35
|
+
base_name: str,
|
36
|
+
context: RenderContext,
|
37
|
+
) -> str:
|
38
|
+
"""
|
39
|
+
Generates the Python code for a type alias.
|
40
|
+
|
41
|
+
Args:
|
42
|
+
schema: The IRSchema for the alias.
|
43
|
+
base_name: The base name for the alias (e.g., schema.name).
|
44
|
+
context: The render context.
|
45
|
+
|
46
|
+
Returns:
|
47
|
+
The generated Python code string for the type alias.
|
48
|
+
|
49
|
+
Contracts:
|
50
|
+
Pre-conditions:
|
51
|
+
- ``schema`` is not None and ``schema.name`` is not None.
|
52
|
+
- ``base_name`` is a non-empty string.
|
53
|
+
- ``context`` is not None.
|
54
|
+
- The schema should logically represent a type alias
|
55
|
+
(e.g., not have properties if it's not an array of anonymous objects).
|
56
|
+
Post-conditions:
|
57
|
+
- Returns a non-empty string containing valid Python code for a type alias.
|
58
|
+
- ``TypeAlias`` is imported in the context if not already present.
|
59
|
+
"""
|
60
|
+
# Pre-conditions
|
61
|
+
assert schema is not None, "Schema cannot be None for alias generation."
|
62
|
+
assert schema.name is not None, "Schema name must be present for alias generation."
|
63
|
+
assert base_name, "Base name cannot be empty for alias generation."
|
64
|
+
assert context is not None, "RenderContext cannot be None."
|
65
|
+
|
66
|
+
alias_name = NameSanitizer.sanitize_class_name(base_name)
|
67
|
+
target_type = self.type_service.resolve_schema_type(schema, context, required=True, resolve_underlying=True)
|
68
|
+
target_type = TypeFinalizer(context)._clean_type(target_type)
|
69
|
+
|
70
|
+
# logger.debug(f"AliasGenerator: Rendering alias '{alias_name}' for target type '{target_type}'.")
|
71
|
+
|
72
|
+
rendered_code = self.renderer.render_alias(
|
73
|
+
alias_name=alias_name,
|
74
|
+
target_type=target_type,
|
75
|
+
description=schema.description,
|
76
|
+
context=context,
|
77
|
+
)
|
78
|
+
|
79
|
+
# Post-condition
|
80
|
+
assert rendered_code.strip(), "Generated alias code cannot be empty."
|
81
|
+
# PythonConstructRenderer is responsible for adding TypeAlias import
|
82
|
+
# We can check if it was added to context if 'TypeAlias' is in the rendered code
|
83
|
+
if "TypeAlias" in rendered_code:
|
84
|
+
assert (
|
85
|
+
"typing" in context.import_collector.imports
|
86
|
+
and "TypeAlias" in context.import_collector.imports["typing"]
|
87
|
+
), "TypeAlias import was not added to context by renderer."
|
88
|
+
|
89
|
+
return rendered_code
|