pyopenapi-gen 2.7.2__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 (137) hide show
  1. pyopenapi_gen/__init__.py +224 -0
  2. pyopenapi_gen/__main__.py +6 -0
  3. pyopenapi_gen/cli.py +62 -0
  4. pyopenapi_gen/context/CLAUDE.md +284 -0
  5. pyopenapi_gen/context/file_manager.py +52 -0
  6. pyopenapi_gen/context/import_collector.py +382 -0
  7. pyopenapi_gen/context/render_context.py +726 -0
  8. pyopenapi_gen/core/CLAUDE.md +224 -0
  9. pyopenapi_gen/core/__init__.py +0 -0
  10. pyopenapi_gen/core/auth/base.py +22 -0
  11. pyopenapi_gen/core/auth/plugins.py +89 -0
  12. pyopenapi_gen/core/cattrs_converter.py +810 -0
  13. pyopenapi_gen/core/exceptions.py +20 -0
  14. pyopenapi_gen/core/http_status_codes.py +218 -0
  15. pyopenapi_gen/core/http_transport.py +222 -0
  16. pyopenapi_gen/core/loader/__init__.py +12 -0
  17. pyopenapi_gen/core/loader/loader.py +174 -0
  18. pyopenapi_gen/core/loader/operations/__init__.py +12 -0
  19. pyopenapi_gen/core/loader/operations/parser.py +161 -0
  20. pyopenapi_gen/core/loader/operations/post_processor.py +62 -0
  21. pyopenapi_gen/core/loader/operations/request_body.py +90 -0
  22. pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
  23. pyopenapi_gen/core/loader/parameters/parser.py +186 -0
  24. pyopenapi_gen/core/loader/responses/__init__.py +10 -0
  25. pyopenapi_gen/core/loader/responses/parser.py +111 -0
  26. pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
  27. pyopenapi_gen/core/loader/schemas/extractor.py +275 -0
  28. pyopenapi_gen/core/pagination.py +64 -0
  29. pyopenapi_gen/core/parsing/__init__.py +13 -0
  30. pyopenapi_gen/core/parsing/common/__init__.py +1 -0
  31. pyopenapi_gen/core/parsing/common/ref_resolution/__init__.py +9 -0
  32. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/__init__.py +0 -0
  33. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/cyclic_properties.py +66 -0
  34. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/direct_cycle.py +33 -0
  35. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/existing_schema.py +22 -0
  36. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +54 -0
  37. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +52 -0
  38. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +50 -0
  39. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +51 -0
  40. pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +86 -0
  41. pyopenapi_gen/core/parsing/common/type_parser.py +73 -0
  42. pyopenapi_gen/core/parsing/context.py +187 -0
  43. pyopenapi_gen/core/parsing/cycle_helpers.py +126 -0
  44. pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
  45. pyopenapi_gen/core/parsing/keywords/all_of_parser.py +81 -0
  46. pyopenapi_gen/core/parsing/keywords/any_of_parser.py +84 -0
  47. pyopenapi_gen/core/parsing/keywords/array_items_parser.py +72 -0
  48. pyopenapi_gen/core/parsing/keywords/one_of_parser.py +77 -0
  49. pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
  50. pyopenapi_gen/core/parsing/schema_finalizer.py +169 -0
  51. pyopenapi_gen/core/parsing/schema_parser.py +804 -0
  52. pyopenapi_gen/core/parsing/transformers/__init__.py +0 -0
  53. pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +285 -0
  54. pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +120 -0
  55. pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
  56. pyopenapi_gen/core/postprocess_manager.py +260 -0
  57. pyopenapi_gen/core/spec_fetcher.py +148 -0
  58. pyopenapi_gen/core/streaming_helpers.py +84 -0
  59. pyopenapi_gen/core/telemetry.py +69 -0
  60. pyopenapi_gen/core/utils.py +456 -0
  61. pyopenapi_gen/core/warning_collector.py +83 -0
  62. pyopenapi_gen/core/writers/code_writer.py +135 -0
  63. pyopenapi_gen/core/writers/documentation_writer.py +222 -0
  64. pyopenapi_gen/core/writers/line_writer.py +217 -0
  65. pyopenapi_gen/core/writers/python_construct_renderer.py +321 -0
  66. pyopenapi_gen/core_package_template/README.md +21 -0
  67. pyopenapi_gen/emit/models_emitter.py +143 -0
  68. pyopenapi_gen/emitters/CLAUDE.md +286 -0
  69. pyopenapi_gen/emitters/client_emitter.py +51 -0
  70. pyopenapi_gen/emitters/core_emitter.py +181 -0
  71. pyopenapi_gen/emitters/docs_emitter.py +44 -0
  72. pyopenapi_gen/emitters/endpoints_emitter.py +247 -0
  73. pyopenapi_gen/emitters/exceptions_emitter.py +187 -0
  74. pyopenapi_gen/emitters/mocks_emitter.py +185 -0
  75. pyopenapi_gen/emitters/models_emitter.py +426 -0
  76. pyopenapi_gen/generator/CLAUDE.md +352 -0
  77. pyopenapi_gen/generator/client_generator.py +567 -0
  78. pyopenapi_gen/generator/exceptions.py +7 -0
  79. pyopenapi_gen/helpers/CLAUDE.md +325 -0
  80. pyopenapi_gen/helpers/__init__.py +1 -0
  81. pyopenapi_gen/helpers/endpoint_utils.py +532 -0
  82. pyopenapi_gen/helpers/type_cleaner.py +334 -0
  83. pyopenapi_gen/helpers/type_helper.py +112 -0
  84. pyopenapi_gen/helpers/type_resolution/__init__.py +1 -0
  85. pyopenapi_gen/helpers/type_resolution/array_resolver.py +57 -0
  86. pyopenapi_gen/helpers/type_resolution/composition_resolver.py +79 -0
  87. pyopenapi_gen/helpers/type_resolution/finalizer.py +105 -0
  88. pyopenapi_gen/helpers/type_resolution/named_resolver.py +172 -0
  89. pyopenapi_gen/helpers/type_resolution/object_resolver.py +216 -0
  90. pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +109 -0
  91. pyopenapi_gen/helpers/type_resolution/resolver.py +47 -0
  92. pyopenapi_gen/helpers/url_utils.py +14 -0
  93. pyopenapi_gen/http_types.py +20 -0
  94. pyopenapi_gen/ir.py +165 -0
  95. pyopenapi_gen/py.typed +1 -0
  96. pyopenapi_gen/types/CLAUDE.md +140 -0
  97. pyopenapi_gen/types/__init__.py +11 -0
  98. pyopenapi_gen/types/contracts/__init__.py +13 -0
  99. pyopenapi_gen/types/contracts/protocols.py +106 -0
  100. pyopenapi_gen/types/contracts/types.py +28 -0
  101. pyopenapi_gen/types/resolvers/__init__.py +7 -0
  102. pyopenapi_gen/types/resolvers/reference_resolver.py +71 -0
  103. pyopenapi_gen/types/resolvers/response_resolver.py +177 -0
  104. pyopenapi_gen/types/resolvers/schema_resolver.py +498 -0
  105. pyopenapi_gen/types/services/__init__.py +5 -0
  106. pyopenapi_gen/types/services/type_service.py +165 -0
  107. pyopenapi_gen/types/strategies/__init__.py +5 -0
  108. pyopenapi_gen/types/strategies/response_strategy.py +310 -0
  109. pyopenapi_gen/visit/CLAUDE.md +272 -0
  110. pyopenapi_gen/visit/client_visitor.py +477 -0
  111. pyopenapi_gen/visit/docs_visitor.py +38 -0
  112. pyopenapi_gen/visit/endpoint/__init__.py +1 -0
  113. pyopenapi_gen/visit/endpoint/endpoint_visitor.py +292 -0
  114. pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
  115. pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +123 -0
  116. pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +222 -0
  117. pyopenapi_gen/visit/endpoint/generators/mock_generator.py +140 -0
  118. pyopenapi_gen/visit/endpoint/generators/overload_generator.py +252 -0
  119. pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
  120. pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +705 -0
  121. pyopenapi_gen/visit/endpoint/generators/signature_generator.py +83 -0
  122. pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +207 -0
  123. pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
  124. pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +78 -0
  125. pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
  126. pyopenapi_gen/visit/exception_visitor.py +90 -0
  127. pyopenapi_gen/visit/model/__init__.py +0 -0
  128. pyopenapi_gen/visit/model/alias_generator.py +93 -0
  129. pyopenapi_gen/visit/model/dataclass_generator.py +553 -0
  130. pyopenapi_gen/visit/model/enum_generator.py +212 -0
  131. pyopenapi_gen/visit/model/model_visitor.py +198 -0
  132. pyopenapi_gen/visit/visitor.py +97 -0
  133. pyopenapi_gen-2.7.2.dist-info/METADATA +1169 -0
  134. pyopenapi_gen-2.7.2.dist-info/RECORD +137 -0
  135. pyopenapi_gen-2.7.2.dist-info/WHEEL +4 -0
  136. pyopenapi_gen-2.7.2.dist-info/entry_points.txt +2 -0
  137. pyopenapi_gen-2.7.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,83 @@
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, List
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
14
+ from pyopenapi_gen.helpers.endpoint_utils import get_param_type
15
+ from pyopenapi_gen.types.strategies.response_strategy import ResponseStrategy
16
+
17
+ if TYPE_CHECKING:
18
+ from pyopenapi_gen import IROperation
19
+ from pyopenapi_gen.context.render_context import RenderContext
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class EndpointMethodSignatureGenerator:
25
+ """Generates the Python method signature for an endpoint operation."""
26
+
27
+ def __init__(self, schemas: dict[str, Any] | None = None) -> None:
28
+ self.schemas: dict[str, Any] = schemas or {}
29
+
30
+ def generate_signature(
31
+ self,
32
+ writer: CodeWriter,
33
+ op: IROperation,
34
+ context: RenderContext,
35
+ ordered_params: List[dict[str, Any]],
36
+ strategy: ResponseStrategy,
37
+ ) -> None:
38
+ """Writes the method signature to the provided CodeWriter."""
39
+ # Logic from EndpointMethodGenerator._write_method_signature
40
+ for p_info in ordered_params: # Renamed p to p_info to avoid conflict if IRParameter is named p
41
+ context.add_typing_imports_for_type(p_info["type"])
42
+
43
+ # Use strategy return type instead of computing it again
44
+ return_type = strategy.return_type
45
+ context.add_typing_imports_for_type(return_type)
46
+
47
+ # Check if AsyncIterator is in return_type or any parameter type
48
+ # Note: op.parameters contains IRParameter objects, not the dicts in ordered_params directly
49
+ # We need to re-calculate param_type for op.parameters if we want to be fully independent here
50
+ # For now, assuming ordered_params covers all type information needed for imports or that context handles it.
51
+ # If direct access to op.parameters schema is needed, get_param_type might be called again here.
52
+ # For simplicity, this check will just look at the final return_type string for now.
53
+ # A more robust solution might involve a richer parameter object passed to this generator.
54
+ if "AsyncIterator" in return_type:
55
+ context.add_plain_import("collections.abc")
56
+ # A more complete check for AsyncIterator in parameters:
57
+ for param_spec in op.parameters: # Iterate over IROperation's parameters
58
+ # This get_param_type call might be redundant if ordered_params already has fully resolved types
59
+ # and context.add_typing_imports_for_type(p_info["type"]) handled it.
60
+ # However, to be safe and explicit about where AsyncIterator might come from:
61
+ param_py_type = get_param_type(param_spec, context, self.schemas)
62
+ if "AsyncIterator" in param_py_type:
63
+ context.add_plain_import("collections.abc")
64
+ break # Found one, no need to check further
65
+
66
+ args = ["self"]
67
+ for p_orig in ordered_params:
68
+ p = p_orig.copy() # Work with a copy
69
+ arg_str = f"{NameSanitizer.sanitize_method_name(p['name'])}: {p['type']}" # Ensure param name is sanitized
70
+ if not p.get("required", False):
71
+ # For optional parameters, always default to None to avoid type mismatches
72
+ # (e.g., enum-typed params with string defaults)
73
+ arg_str += " = None"
74
+ args.append(arg_str)
75
+
76
+ actual_return_type = return_type
77
+ writer.write_function_signature(
78
+ NameSanitizer.sanitize_method_name(op.operation_id),
79
+ args,
80
+ return_type=actual_return_type,
81
+ async_=True,
82
+ )
83
+ writer.indent() # Keep the indent call as the original method did
@@ -0,0 +1,207 @@
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, List
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: dict[str, Any] | None = 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
+ # Import DataclassSerializer since we use it for parameter serialization
47
+ context.add_import(f"{context.core_package_name}.utils", "DataclassSerializer")
48
+
49
+ for i, p in enumerate(query_params_to_write):
50
+ param_var_name = NameSanitizer.sanitize_method_name(p["name"]) # Ensure name is sanitized
51
+ original_param_name = p["original_name"]
52
+ line_end = "," # Always add comma, let formatter handle final one if needed
53
+
54
+ if p.get("required", False):
55
+ writer.write_line(
56
+ f' "{original_param_name}": DataclassSerializer.serialize({param_var_name}){line_end}'
57
+ )
58
+ else:
59
+ # Using dict unpacking for conditional parameters
60
+ writer.write_line(
61
+ f' **({{"{original_param_name}": DataclassSerializer.serialize({param_var_name})}} '
62
+ f"if {param_var_name} is not None else {{}}){line_end}"
63
+ )
64
+
65
+ def _write_header_params(
66
+ self, writer: CodeWriter, op: IROperation, ordered_params: List[dict[str, Any]], context: RenderContext
67
+ ) -> None:
68
+ """Writes header parameter dictionary construction."""
69
+ # Logic from EndpointMethodGenerator._write_header_params
70
+ # Iterate through ordered_params to find header params, op.parameters may not be directly useful here
71
+ # if ordered_params is the sole source of truth for method params.
72
+ header_params_to_write = [p for p in ordered_params if p.get("param_in") == "header"]
73
+
74
+ # Import DataclassSerializer since we use it for parameter serialization
75
+ if header_params_to_write:
76
+ context.add_import(f"{context.core_package_name}.utils", "DataclassSerializer")
77
+
78
+ for p_info in header_params_to_write:
79
+ param_var_name = NameSanitizer.sanitize_method_name(
80
+ p_info["name"]
81
+ ) # Sanitized name used in method signature
82
+ original_header_name = p_info["original_name"] # Actual header name for the request
83
+ line_end = ","
84
+
85
+ if p_info.get("required", False):
86
+ writer.write_line(
87
+ f' "{original_header_name}": DataclassSerializer.serialize({param_var_name}){line_end}'
88
+ )
89
+ else:
90
+ # Conditional inclusion for optional headers
91
+ # This assumes that if an optional header parameter is None, it should not be sent.
92
+ # If specific behavior (e.g. empty string) is needed for None, logic would adjust.
93
+ writer.write_line(
94
+ f' **({{"{original_header_name}": DataclassSerializer.serialize({param_var_name})}} '
95
+ f"if {param_var_name} is not None else {{}}){line_end}"
96
+ )
97
+
98
+ def generate_url_and_args(
99
+ self,
100
+ writer: CodeWriter,
101
+ op: IROperation,
102
+ context: RenderContext,
103
+ ordered_params: List[dict[str, Any]],
104
+ primary_content_type: str | None,
105
+ resolved_body_type: str | None,
106
+ ) -> bool:
107
+ """Writes URL, query, and header parameters. Returns True if header params were written."""
108
+ # Main logic from EndpointMethodGenerator._write_url_and_args
109
+
110
+ # Serialize path parameters before URL construction
111
+ # This ensures enums, dates, and other complex types are converted to strings
112
+ # before f-string interpolation in the URL
113
+ path_params = [p for p in ordered_params if p.get("param_in") == "path"]
114
+ if path_params:
115
+ # Import DataclassSerializer since we use it for parameter serialization
116
+ context.add_import(f"{context.core_package_name}.utils", "DataclassSerializer")
117
+ for p in path_params:
118
+ param_var_name = NameSanitizer.sanitize_method_name(p["name"])
119
+ writer.write_line(f"{param_var_name} = DataclassSerializer.serialize({param_var_name})")
120
+ writer.write_line("") # Blank line after path param serialization
121
+
122
+ url_expr = self._build_url_with_path_vars(op.path)
123
+ writer.write_line(f"url = {url_expr}")
124
+ writer.write_line("") # Add a blank line for readability
125
+
126
+ # Query Parameters
127
+ # Check if any parameter in ordered_params is a query param, not just op.parameters
128
+ has_spec_query_params = any(p.get("param_in") == "query" for p in ordered_params)
129
+ if has_spec_query_params:
130
+ context.add_import("typing", "Any") # For dict[str, Any]
131
+ context.add_import("typing", "Dict") # For dict[str, Any]
132
+ writer.write_line("params: dict[str, Any] = {")
133
+ # writer.indent() # Indentation should be handled by CodeWriter when writing lines
134
+ self._write_query_params(writer, op, ordered_params, context)
135
+ # writer.dedent()
136
+ writer.write_line("}")
137
+ writer.write_line("") # Add a blank line
138
+
139
+ # Header Parameters
140
+ has_header_params = any(p.get("param_in") == "header" for p in ordered_params)
141
+ if has_header_params:
142
+ context.add_import("typing", "Any") # For dict[str, Any]
143
+ context.add_import("typing", "Dict") # For dict[str, Any]
144
+ writer.write_line("headers: dict[str, Any] = {")
145
+ # writer.indent()
146
+ self._write_header_params(writer, op, ordered_params, context)
147
+ # writer.dedent()
148
+ writer.write_line("}")
149
+ writer.write_line("") # Add a blank line
150
+
151
+ # Request Body related local variables (json_body, files_data, etc.)
152
+ # This part was in _write_url_and_args in the original, it sets up variables used by _write_request
153
+ if op.request_body:
154
+ # Import DataclassSerializer for automatic conversion
155
+ context.add_import(f"{context.core_package_name}.utils", "DataclassSerializer")
156
+
157
+ if primary_content_type == "application/json":
158
+ body_param_detail = next((p for p in ordered_params if p["name"] == "body"), None)
159
+ if body_param_detail:
160
+ actual_body_type_from_signature = body_param_detail["type"]
161
+ context.add_typing_imports_for_type(actual_body_type_from_signature)
162
+ writer.write_line(
163
+ f"json_body: {actual_body_type_from_signature} = DataclassSerializer.serialize(body)"
164
+ )
165
+ else:
166
+ logger.warning(
167
+ f"Operation {op.operation_id}: 'body' parameter not found in "
168
+ f"ordered_params for JSON. Defaulting to Any."
169
+ )
170
+ context.add_import("typing", "Any")
171
+ writer.write_line("json_body: Any = DataclassSerializer.serialize(body) # param not found")
172
+ elif primary_content_type == "multipart/form-data":
173
+ files_param_details = next((p for p in ordered_params if p["name"] == "files"), None)
174
+ if files_param_details:
175
+ actual_files_param_type = files_param_details["type"]
176
+ context.add_typing_imports_for_type(actual_files_param_type)
177
+ writer.write_line(f"files_data: {actual_files_param_type} = DataclassSerializer.serialize(files)")
178
+ else:
179
+ logger.warning(
180
+ f"Operation {op.operation_id}: Could not find 'files' parameter details "
181
+ f"for multipart/form-data. Defaulting type."
182
+ )
183
+ context.add_import("typing", "Dict")
184
+ context.add_import("typing", "IO") # For IO[Any]
185
+ context.add_import("typing", "Any")
186
+ writer.write_line(
187
+ "files_data: dict[str, IO[Any]] = DataclassSerializer.serialize(files) # type failed"
188
+ )
189
+ elif primary_content_type == "application/x-www-form-urlencoded":
190
+ # form_data is the expected parameter name from EndpointParameterProcessor
191
+ # resolved_body_type should be dict[str, Any]
192
+ if resolved_body_type:
193
+ writer.write_line(
194
+ f"form_data_body: {resolved_body_type} = DataclassSerializer.serialize(form_data)"
195
+ )
196
+ else: # Should not happen if EndpointParameterProcessor sets it
197
+ context.add_import("typing", "Dict")
198
+ context.add_import("typing", "Any")
199
+ writer.write_line(
200
+ "form_data_body: dict[str, Any] = DataclassSerializer.serialize(form_data) # Fallback type"
201
+ )
202
+ elif resolved_body_type == "bytes": # e.g. application/octet-stream
203
+ # bytes_content is the expected parameter name from EndpointParameterProcessor
204
+ writer.write_line(f"bytes_body: bytes = bytes_content")
205
+ writer.write_line("") # Add a blank line after body var setup
206
+
207
+ return has_header_params
@@ -0,0 +1 @@
1
+ # Happiness is not for sale.
@@ -0,0 +1,78 @@
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 # 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
+ )
15
+
16
+ if TYPE_CHECKING:
17
+ from pyopenapi_gen import IROperation # IRParameter for op.parameters type hint
18
+ from pyopenapi_gen.context.render_context import RenderContext
19
+ from pyopenapi_gen.types.strategies.response_strategy import ResponseStrategy
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: dict[str, Any] | None = 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
+ response_strategy: ResponseStrategy,
35
+ ) -> None:
36
+ """Analyzes the operation and registers imports with the RenderContext."""
37
+ for param in op.parameters: # op.parameters are IRParameter objects
38
+ py_type = get_param_type(param, context, self.schemas)
39
+ context.add_typing_imports_for_type(py_type)
40
+
41
+ if op.request_body:
42
+ content_types = op.request_body.content.keys()
43
+ body_param_type: str | None = None
44
+ if "multipart/form-data" in content_types:
45
+ # Type for multipart is dict[str, IO[Any]] which requires IO and Any
46
+ context.add_import("typing", "Dict")
47
+ context.add_import("typing", "IO")
48
+ context.add_import("typing", "Any")
49
+ # The actual type string "dict[str, IO[Any]]" will be handled by add_typing_imports_for_type if passed
50
+ # but ensuring components are imported is key.
51
+ body_param_type = "dict[str, IO[Any]]"
52
+ elif "application/json" in content_types:
53
+ body_param_type = get_request_body_type(op.request_body, context, self.schemas)
54
+ elif "application/x-www-form-urlencoded" in content_types:
55
+ context.add_import("typing", "Dict")
56
+ context.add_import("typing", "Any")
57
+ body_param_type = "dict[str, Any]"
58
+ elif content_types: # Fallback for other types like application/octet-stream
59
+ body_param_type = "bytes"
60
+
61
+ if body_param_type:
62
+ context.add_typing_imports_for_type(body_param_type)
63
+
64
+ # Use the response strategy's return type for import analysis
65
+ return_type = response_strategy.return_type
66
+ context.add_typing_imports_for_type(return_type)
67
+
68
+ # Check for AsyncIterator in return type or parameter types
69
+ async_iterator_found = "AsyncIterator" in return_type
70
+ if not async_iterator_found:
71
+ for param_spec in op.parameters: # Iterate over IROperation's parameters
72
+ param_py_type = get_param_type(param_spec, context, self.schemas) # Re-check type for safety
73
+ if "AsyncIterator" in param_py_type:
74
+ async_iterator_found = True
75
+ break
76
+
77
+ if async_iterator_found:
78
+ 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, List, 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: dict[str, Any] | None = 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]], str | None, str | None]:
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: str | None = None
60
+ resolved_body_type: str | None = 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: dict[str, Any] | None = 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,90 @@
1
+ from pyopenapi_gen import IRSpec
2
+
3
+ from ..context.render_context import RenderContext
4
+ from ..core.http_status_codes import (
5
+ get_exception_class_name,
6
+ get_status_name,
7
+ is_client_error,
8
+ is_error_code,
9
+ is_server_error,
10
+ )
11
+ from ..core.writers.python_construct_renderer import PythonConstructRenderer
12
+
13
+
14
+ class ExceptionVisitor:
15
+ """Visitor for rendering exception alias classes from IRSpec.
16
+
17
+ This visitor generates exception classes only for error status codes (4xx and 5xx).
18
+ Success codes (2xx) are intentionally excluded as they represent successful responses.
19
+ """
20
+
21
+ def __init__(self) -> None:
22
+ self.renderer = PythonConstructRenderer()
23
+
24
+ def visit(self, spec: IRSpec, context: RenderContext) -> tuple[str, list[str], list[int]]:
25
+ """Generate exception classes from IRSpec.
26
+
27
+ Args:
28
+ spec: The IRSpec containing operations and responses
29
+ context: Render context for imports and code generation
30
+
31
+ Returns:
32
+ Tuple of (generated_code, exception_class_names, status_codes_list)
33
+ """
34
+ # Register base exception imports (only the ones we actually use)
35
+ # Note: HTTPError is not used in exception_aliases.py, so we don't import it
36
+ context.add_import("httpx", "Response") # Third-party import first (Ruff I001)
37
+ context.add_import(f"{context.core_package_name}.exceptions", "ClientError")
38
+ context.add_import(f"{context.core_package_name}.exceptions", "ServerError")
39
+
40
+ # Collect unique numeric error status codes (4xx and 5xx only)
41
+ all_codes = {
42
+ int(resp.status_code) for op in spec.operations for resp in op.responses if resp.status_code.isdigit()
43
+ }
44
+ error_codes = sorted([code for code in all_codes if is_error_code(code)])
45
+
46
+ all_exception_code = []
47
+ generated_alias_names = []
48
+
49
+ # Use renderer to generate each exception class
50
+ for code in error_codes:
51
+ # Determine base class using helper functions
52
+ if is_client_error(code):
53
+ base_class = "ClientError"
54
+ elif is_server_error(code):
55
+ base_class = "ServerError"
56
+ else:
57
+ # Should not happen since we filtered to 4xx/5xx, but be defensive
58
+ continue
59
+
60
+ # Get human-readable exception class name (e.g., NotFoundError instead of Error404)
61
+ class_name = get_exception_class_name(code)
62
+ generated_alias_names.append(class_name)
63
+
64
+ # Get human-readable status name for documentation
65
+ status_name = get_status_name(code)
66
+ docstring = f"HTTP {code} {status_name}.\n\nRaised when the server responds with a {code} status code."
67
+
68
+ # Define the __init__ method body
69
+ init_method_body = [
70
+ "def __init__(self, response: Response) -> None:",
71
+ f' """Initialise {class_name} with the HTTP response.',
72
+ "", # Empty line without trailing whitespace (Ruff W293)
73
+ " Args:",
74
+ " response: The httpx Response object that triggered this exception",
75
+ ' """',
76
+ " super().__init__(status_code=response.status_code, message=response.text, response=response)",
77
+ ]
78
+
79
+ exception_code = self.renderer.render_class(
80
+ class_name=class_name,
81
+ base_classes=[base_class],
82
+ docstring=docstring,
83
+ body_lines=init_method_body,
84
+ context=context,
85
+ )
86
+ all_exception_code.append(exception_code)
87
+
88
+ # Join the generated class strings with 2 blank lines between classes (PEP 8 / Ruff E302)
89
+ final_code = "\n\n\n".join(all_exception_code)
90
+ return final_code, generated_alias_names, error_codes
File without changes