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.
Files changed (122) hide show
  1. pyopenapi_gen/__init__.py +114 -0
  2. pyopenapi_gen/__main__.py +6 -0
  3. pyopenapi_gen/cli.py +86 -0
  4. pyopenapi_gen/context/file_manager.py +52 -0
  5. pyopenapi_gen/context/import_collector.py +382 -0
  6. pyopenapi_gen/context/render_context.py +630 -0
  7. pyopenapi_gen/core/__init__.py +0 -0
  8. pyopenapi_gen/core/auth/base.py +22 -0
  9. pyopenapi_gen/core/auth/plugins.py +89 -0
  10. pyopenapi_gen/core/exceptions.py +25 -0
  11. pyopenapi_gen/core/http_transport.py +219 -0
  12. pyopenapi_gen/core/loader/__init__.py +12 -0
  13. pyopenapi_gen/core/loader/loader.py +158 -0
  14. pyopenapi_gen/core/loader/operations/__init__.py +12 -0
  15. pyopenapi_gen/core/loader/operations/parser.py +155 -0
  16. pyopenapi_gen/core/loader/operations/post_processor.py +60 -0
  17. pyopenapi_gen/core/loader/operations/request_body.py +85 -0
  18. pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
  19. pyopenapi_gen/core/loader/parameters/parser.py +121 -0
  20. pyopenapi_gen/core/loader/responses/__init__.py +10 -0
  21. pyopenapi_gen/core/loader/responses/parser.py +104 -0
  22. pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
  23. pyopenapi_gen/core/loader/schemas/extractor.py +184 -0
  24. pyopenapi_gen/core/pagination.py +64 -0
  25. pyopenapi_gen/core/parsing/__init__.py +13 -0
  26. pyopenapi_gen/core/parsing/common/__init__.py +1 -0
  27. pyopenapi_gen/core/parsing/common/ref_resolution/__init__.py +9 -0
  28. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/__init__.py +0 -0
  29. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/cyclic_properties.py +66 -0
  30. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/direct_cycle.py +33 -0
  31. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/existing_schema.py +22 -0
  32. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +54 -0
  33. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +52 -0
  34. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +50 -0
  35. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +51 -0
  36. pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +86 -0
  37. pyopenapi_gen/core/parsing/common/type_parser.py +74 -0
  38. pyopenapi_gen/core/parsing/context.py +184 -0
  39. pyopenapi_gen/core/parsing/cycle_helpers.py +123 -0
  40. pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
  41. pyopenapi_gen/core/parsing/keywords/all_of_parser.py +77 -0
  42. pyopenapi_gen/core/parsing/keywords/any_of_parser.py +79 -0
  43. pyopenapi_gen/core/parsing/keywords/array_items_parser.py +69 -0
  44. pyopenapi_gen/core/parsing/keywords/one_of_parser.py +72 -0
  45. pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
  46. pyopenapi_gen/core/parsing/schema_finalizer.py +166 -0
  47. pyopenapi_gen/core/parsing/schema_parser.py +610 -0
  48. pyopenapi_gen/core/parsing/transformers/__init__.py +0 -0
  49. pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +285 -0
  50. pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +117 -0
  51. pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
  52. pyopenapi_gen/core/postprocess_manager.py +161 -0
  53. pyopenapi_gen/core/schemas.py +40 -0
  54. pyopenapi_gen/core/streaming_helpers.py +86 -0
  55. pyopenapi_gen/core/telemetry.py +67 -0
  56. pyopenapi_gen/core/utils.py +409 -0
  57. pyopenapi_gen/core/warning_collector.py +83 -0
  58. pyopenapi_gen/core/writers/code_writer.py +135 -0
  59. pyopenapi_gen/core/writers/documentation_writer.py +222 -0
  60. pyopenapi_gen/core/writers/line_writer.py +217 -0
  61. pyopenapi_gen/core/writers/python_construct_renderer.py +274 -0
  62. pyopenapi_gen/core_package_template/README.md +21 -0
  63. pyopenapi_gen/emit/models_emitter.py +143 -0
  64. pyopenapi_gen/emitters/client_emitter.py +51 -0
  65. pyopenapi_gen/emitters/core_emitter.py +181 -0
  66. pyopenapi_gen/emitters/docs_emitter.py +44 -0
  67. pyopenapi_gen/emitters/endpoints_emitter.py +223 -0
  68. pyopenapi_gen/emitters/exceptions_emitter.py +52 -0
  69. pyopenapi_gen/emitters/models_emitter.py +428 -0
  70. pyopenapi_gen/generator/client_generator.py +562 -0
  71. pyopenapi_gen/helpers/__init__.py +1 -0
  72. pyopenapi_gen/helpers/endpoint_utils.py +552 -0
  73. pyopenapi_gen/helpers/type_cleaner.py +341 -0
  74. pyopenapi_gen/helpers/type_helper.py +112 -0
  75. pyopenapi_gen/helpers/type_resolution/__init__.py +1 -0
  76. pyopenapi_gen/helpers/type_resolution/array_resolver.py +57 -0
  77. pyopenapi_gen/helpers/type_resolution/composition_resolver.py +79 -0
  78. pyopenapi_gen/helpers/type_resolution/finalizer.py +89 -0
  79. pyopenapi_gen/helpers/type_resolution/named_resolver.py +174 -0
  80. pyopenapi_gen/helpers/type_resolution/object_resolver.py +212 -0
  81. pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +57 -0
  82. pyopenapi_gen/helpers/type_resolution/resolver.py +48 -0
  83. pyopenapi_gen/helpers/url_utils.py +14 -0
  84. pyopenapi_gen/http_types.py +20 -0
  85. pyopenapi_gen/ir.py +167 -0
  86. pyopenapi_gen/py.typed +1 -0
  87. pyopenapi_gen/types/__init__.py +11 -0
  88. pyopenapi_gen/types/contracts/__init__.py +13 -0
  89. pyopenapi_gen/types/contracts/protocols.py +106 -0
  90. pyopenapi_gen/types/contracts/types.py +30 -0
  91. pyopenapi_gen/types/resolvers/__init__.py +7 -0
  92. pyopenapi_gen/types/resolvers/reference_resolver.py +71 -0
  93. pyopenapi_gen/types/resolvers/response_resolver.py +203 -0
  94. pyopenapi_gen/types/resolvers/schema_resolver.py +367 -0
  95. pyopenapi_gen/types/services/__init__.py +5 -0
  96. pyopenapi_gen/types/services/type_service.py +133 -0
  97. pyopenapi_gen/visit/client_visitor.py +228 -0
  98. pyopenapi_gen/visit/docs_visitor.py +38 -0
  99. pyopenapi_gen/visit/endpoint/__init__.py +1 -0
  100. pyopenapi_gen/visit/endpoint/endpoint_visitor.py +103 -0
  101. pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
  102. pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +121 -0
  103. pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +87 -0
  104. pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
  105. pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +497 -0
  106. pyopenapi_gen/visit/endpoint/generators/signature_generator.py +88 -0
  107. pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +183 -0
  108. pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
  109. pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +76 -0
  110. pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
  111. pyopenapi_gen/visit/exception_visitor.py +52 -0
  112. pyopenapi_gen/visit/model/__init__.py +0 -0
  113. pyopenapi_gen/visit/model/alias_generator.py +89 -0
  114. pyopenapi_gen/visit/model/dataclass_generator.py +197 -0
  115. pyopenapi_gen/visit/model/enum_generator.py +200 -0
  116. pyopenapi_gen/visit/model/model_visitor.py +197 -0
  117. pyopenapi_gen/visit/visitor.py +97 -0
  118. pyopenapi_gen-0.8.3.dist-info/METADATA +224 -0
  119. pyopenapi_gen-0.8.3.dist-info/RECORD +122 -0
  120. pyopenapi_gen-0.8.3.dist-info/WHEEL +4 -0
  121. pyopenapi_gen-0.8.3.dist-info/entry_points.txt +2 -0
  122. 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