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,103 @@
1
+ """
2
+ Helper class for generating the HTTP request call for an endpoint method.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ from typing import TYPE_CHECKING, Any, Dict, Optional
9
+
10
+ from pyopenapi_gen.core.writers.code_writer import CodeWriter
11
+
12
+ # No specific utils needed yet, but NameSanitizer might be if param names are manipulated here
13
+
14
+ if TYPE_CHECKING:
15
+ from pyopenapi_gen import IROperation
16
+ from pyopenapi_gen.context.render_context import RenderContext # For context.add_import if needed
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class EndpointRequestGenerator:
22
+ """Generates the self._transport.request(...) call 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 generate_request_call(
28
+ self,
29
+ writer: CodeWriter,
30
+ op: IROperation,
31
+ context: RenderContext, # Pass context for potential import needs
32
+ has_header_params: bool,
33
+ primary_content_type: Optional[str],
34
+ # resolved_body_type: Optional[str], # May not be directly needed here if logic relies on var names
35
+ ) -> None:
36
+ """Writes the self._transport.request call to the CodeWriter."""
37
+ # Logic from EndpointMethodGenerator._write_request
38
+ args_list = []
39
+
40
+ # Determine if 'params' argument is needed for query parameters
41
+ # This relies on UrlArgsGenerator having created a 'params' dict if query params exist.
42
+ # A more robust way could be to check op.parameters directly, but this keeps coupling loose.
43
+ if any(p.param_in == "query" for p in op.parameters): # Check IROperation directly for query params
44
+ args_list.append("params=params")
45
+ else:
46
+ args_list.append("params=None")
47
+
48
+ # Determine 'json' and 'data' arguments based on request body
49
+ if op.request_body:
50
+ if primary_content_type == "application/json":
51
+ args_list.append("json=json_body") # Assumes json_body is defined
52
+ # args_list.append("data=None") # Not strictly needed if json is present, httpx handles it
53
+ elif primary_content_type and "multipart/form-data" in primary_content_type:
54
+ # For multipart, httpx uses 'files' or 'data' depending on content.
55
+ # UrlArgsGenerator created 'files_data'. Httpx typically uses 'files=' for file uploads.
56
+ # Let's assume 'files_data' is a dict suitable for 'files=' or 'data='
57
+ # If 'files_data' is specifically for file-like objects, 'files=files_data' is better.
58
+ # If it can also contain plain data, 'data=files_data' might be used by httpx.
59
+ # For simplicity and common use with files:
60
+ args_list.append("files=files_data") # Assumes files_data is defined
61
+ elif primary_content_type == "application/x-www-form-urlencoded":
62
+ args_list.append("data=form_data_body") # Assumes form_data_body is defined
63
+ elif primary_content_type: # Other types, like application/octet-stream
64
+ args_list.append("data=bytes_body") # Assumes bytes_body is defined
65
+ # else: # No specific content type handled, might mean no body or unhandled type
66
+ # args_list.append("json=None")
67
+ # args_list.append("data=None")
68
+ else: # No request body
69
+ args_list.append("json=None")
70
+ args_list.append("data=None")
71
+
72
+ # Determine 'headers' argument
73
+ if has_header_params: # This flag comes from UrlArgsGenerator
74
+ args_list.append("headers=headers") # Assumes headers dict is defined
75
+ else:
76
+ args_list.append("headers=None")
77
+
78
+ positional_args_str = f'"{op.method.upper()}", url' # url variable is assumed to be defined
79
+ keyword_args_str = ", ".join(args_list)
80
+
81
+ # Check length for formatting (120 is a common line length limit)
82
+ # Account for "response = await self._transport.request(" and ")" and surrounding spaces/indentation
83
+ # A rough estimate, effective line length for arguments should be less than ~120 - ~40 = 80
84
+ effective_args_len = len(positional_args_str) + len(", ") + len(keyword_args_str)
85
+
86
+ base_call_len = len("response = await self._transport.request()") + 2 # +2 for (,)
87
+
88
+ if base_call_len + effective_args_len <= 100: # Adjusted for typical black formatting preference
89
+ writer.write_line(f"response = await self._transport.request({positional_args_str}, {keyword_args_str})")
90
+ else:
91
+ writer.write_line(f"response = await self._transport.request(")
92
+ writer.indent()
93
+ writer.write_line(f"{positional_args_str},")
94
+ # Filter out "*=None" for cleaner multi-line calls if they are truly None and not just assigned None
95
+ # This might be overly complex here; httpx handles None correctly.
96
+ # Sticking to original logic for now.
97
+ num_args = len(args_list)
98
+ for i, arg in enumerate(args_list):
99
+ line_end = "," if i < num_args - 1 else ""
100
+ writer.write_line(f"{arg}{line_end}")
101
+ writer.dedent()
102
+ writer.write_line(")")
103
+ writer.write_line("") # Add a blank line for readability after the request call
@@ -0,0 +1,497 @@
1
+ """
2
+ Helper class for generating response handling logic for an endpoint method.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ import re # For parsing Union types, etc.
9
+ from typing import TYPE_CHECKING, Any, Dict, Optional, TypedDict
10
+
11
+ from pyopenapi_gen.core.writers.code_writer import CodeWriter
12
+ from pyopenapi_gen.helpers.endpoint_utils import (
13
+ _get_primary_response,
14
+ get_return_type_unified,
15
+ get_type_for_specific_response, # Added new helper
16
+ )
17
+ from pyopenapi_gen.types.services.type_service import UnifiedTypeService
18
+
19
+ if TYPE_CHECKING:
20
+ from pyopenapi_gen import IROperation, IRResponse
21
+ from pyopenapi_gen.context.render_context import RenderContext
22
+ else:
23
+ # For runtime, we need to import for TypedDict
24
+ from pyopenapi_gen import IRResponse
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class StatusCase(TypedDict):
30
+ """Type definition for status code case data."""
31
+
32
+ status_code: int
33
+ type: str # 'primary_success', 'success', or 'error'
34
+ return_type: str
35
+ needs_unwrap: bool
36
+ response_ir: IRResponse
37
+
38
+
39
+ class DefaultCase(TypedDict):
40
+ """Type definition for default case data."""
41
+
42
+ response_ir: IRResponse
43
+ return_type: str
44
+ needs_unwrap: bool
45
+
46
+
47
+ class EndpointResponseHandlerGenerator:
48
+ """Generates the response handling logic for an endpoint method."""
49
+
50
+ def __init__(self, schemas: Optional[Dict[str, Any]] = None) -> None:
51
+ self.schemas: Dict[str, Any] = schemas or {}
52
+
53
+ def _get_extraction_code(
54
+ self,
55
+ return_type: str,
56
+ context: RenderContext,
57
+ op: IROperation,
58
+ needs_unwrap: bool,
59
+ response_ir: Optional[IRResponse] = None,
60
+ ) -> str:
61
+ """Determines the code snippet to extract/transform the response body."""
62
+ # Handle None, StreamingResponse, Iterator, etc.
63
+ if return_type is None or return_type == "None":
64
+ return "None" # This will be directly used in the return statement
65
+
66
+ # Handle streaming responses
67
+ if return_type.startswith("AsyncIterator["):
68
+ # Check if it's a bytes stream or other type of stream
69
+ if return_type == "AsyncIterator[bytes]":
70
+ context.add_import(f"{context.core_package_name}.streaming_helpers", "iter_bytes")
71
+ return "iter_bytes(response)"
72
+ elif "Dict[str, Any]" in return_type or "dict" in return_type.lower():
73
+ # For event streams that return Dict objects
74
+ context.add_import(f"{context.core_package_name}.streaming_helpers", "iter_sse_events_text")
75
+ return "sse_json_stream_marker" # Special marker handled by _write_parsed_return
76
+ else:
77
+ # Model streaming - likely an SSE model stream
78
+ # Extract the model type and check if content type is text/event-stream
79
+ model_type = return_type[13:-1] # Remove 'AsyncIterator[' and ']'
80
+ if response_ir and "text/event-stream" in response_ir.content:
81
+ context.add_import(f"{context.core_package_name}.streaming_helpers", "iter_sse_events_text")
82
+ return "sse_json_stream_marker" # Special marker for SSE
83
+
84
+ # Default to bytes streaming for other types
85
+ context.add_import(f"{context.core_package_name}.streaming_helpers", "iter_bytes")
86
+ return "iter_bytes(response)"
87
+
88
+ # Special case for "data: Any" unwrapping when the actual schema has no fields/properties
89
+ if return_type in {"Dict[str, Any]", "Dict[str, object]", "object", "Any"}:
90
+ context.add_import("typing", "Dict")
91
+ context.add_import("typing", "Any")
92
+
93
+ if return_type == "str":
94
+ return "response.text"
95
+ elif return_type == "bytes":
96
+ return "response.content"
97
+ elif return_type == "Any":
98
+ context.add_import("typing", "Any")
99
+ return "response.json() # Type is Any"
100
+ elif return_type == "None":
101
+ return "None" # This will be handled by generate_response_handling directly
102
+ else: # Includes schema-defined models, List[], Dict[], Optional[]
103
+ context.add_import("typing", "cast")
104
+ context.add_typing_imports_for_type(return_type) # Ensure model itself is imported
105
+
106
+ if needs_unwrap:
107
+ # Special handling for List unwrapping - ensure we have the correct imports
108
+ if return_type.startswith("List["):
109
+ # Extract the item type from List[ItemType]
110
+ item_type = return_type[5:-1] # Remove 'List[' and ']'
111
+ context.add_import("typing", "List")
112
+ if "." in item_type:
113
+ # Ensure we have the proper import for the item type
114
+ context.add_typing_imports_for_type(item_type)
115
+ # Handle unwrapping of List directly
116
+ return (
117
+ f"raw_data = response.json().get('data')\n"
118
+ f"if raw_data is None:\n"
119
+ f" raise ValueError(\"Expected 'data' key in response but found None\")\n"
120
+ f"return cast({return_type}, raw_data)"
121
+ )
122
+ # Standard unwrapping for single object
123
+ return (
124
+ f"raw_data = response.json().get('data')\n"
125
+ f"if raw_data is None:\n"
126
+ f" raise ValueError(\"Expected 'data' key in response but found None\")\n"
127
+ f"return cast({return_type}, raw_data)"
128
+ )
129
+ else:
130
+ return f"cast({return_type}, response.json())"
131
+
132
+ def generate_response_handling(
133
+ self,
134
+ writer: CodeWriter,
135
+ op: IROperation,
136
+ context: RenderContext,
137
+ ) -> None:
138
+ """Writes the response parsing and return logic to the CodeWriter, including status code dispatch."""
139
+ writer.write_line("# Check response status code and handle accordingly")
140
+
141
+ # Sort responses: specific 2xx, then default (if configured for success), then errors
142
+ # This simplified sorting might need adjustment based on how 'default' is treated
143
+ # For now, we'll explicitly find the primary success path first.
144
+
145
+ primary_success_ir = _get_primary_response(op)
146
+
147
+ is_primary_actually_success = False
148
+ if primary_success_ir: # Explicit check for None to help linter
149
+ is_2xx = primary_success_ir.status_code.startswith("2")
150
+ is_default_with_content = primary_success_ir.status_code == "default" and bool(
151
+ primary_success_ir.content
152
+ ) # Ensure this part is boolean
153
+ is_primary_actually_success = is_2xx or is_default_with_content
154
+
155
+ # Determine if the primary success response will be handled by the first dedicated block
156
+ # This first block only handles numeric (2xx) success codes.
157
+ is_primary_handled_by_first_block = (
158
+ primary_success_ir
159
+ and is_primary_actually_success
160
+ and primary_success_ir.status_code.isdigit() # Key change: first block only for numeric codes
161
+ and primary_success_ir.status_code.startswith("2") # Ensure it's 2xx
162
+ )
163
+
164
+ other_responses = sorted(
165
+ [
166
+ r for r in op.responses if not (r == primary_success_ir and is_primary_handled_by_first_block)
167
+ ], # If primary is handled by first block, exclude it from others
168
+ key=lambda r: (
169
+ not r.status_code.startswith("2"), # False for 2xx (comes first)
170
+ r.status_code != "default", # False for default (comes after 2xx, before errors)
171
+ r.status_code, # Then sort by status_code string
172
+ ),
173
+ )
174
+
175
+ # Collect all status codes and their handlers for the match statement
176
+ status_cases: list[StatusCase] = []
177
+
178
+ # 1. Handle primary success response IF IT IS TRULY A SUCCESS RESPONSE AND NUMERIC (2xx)
179
+ if is_primary_handled_by_first_block:
180
+ assert primary_success_ir is not None # Add assertion to help linter
181
+ # No try-except needed here as isdigit() and startswith("2") already checked
182
+ status_code_val = int(primary_success_ir.status_code)
183
+
184
+ # This is the return_type for the *entire operation*, based on its primary success response
185
+ # First try the fallback method for backward compatibility
186
+ return_type_for_op = get_return_type_unified(op, context, self.schemas)
187
+ needs_unwrap_for_op = False # Default to False
188
+
189
+ # If we have proper schemas, try to get unwrapping information from unified service
190
+ if self.schemas and hasattr(list(self.schemas.values())[0] if self.schemas else None, "type"):
191
+ try:
192
+ type_service = UnifiedTypeService(self.schemas)
193
+ return_type_for_op, needs_unwrap_for_op = type_service.resolve_operation_response_with_unwrap_info(
194
+ op, context
195
+ )
196
+ except Exception:
197
+ # Fall back to the original approach if there's an issue
198
+ needs_unwrap_for_op = False
199
+
200
+ status_cases.append(
201
+ StatusCase(
202
+ status_code=status_code_val,
203
+ type="primary_success",
204
+ return_type=return_type_for_op,
205
+ needs_unwrap=needs_unwrap_for_op,
206
+ response_ir=primary_success_ir,
207
+ )
208
+ )
209
+
210
+ # 2. Handle other specific responses (other 2xx, then default, then errors)
211
+ default_case: Optional[DefaultCase] = None
212
+ for resp_ir in other_responses:
213
+ # Determine if this response IR defines a success type different from the primary
214
+ # This is complex. For now, if it's 2xx, we'll try to parse it.
215
+ # If it's an error, we raise.
216
+
217
+ current_return_type_str: str = "None" # Default for e.g. 204 or error cases
218
+ current_needs_unwrap: bool = False
219
+
220
+ if resp_ir.status_code.startswith("2"):
221
+ if not resp_ir.content: # e.g. 204
222
+ current_return_type_str = "None"
223
+ else:
224
+ # We need a way to get the type for *this specific* resp_ir if its schema differs
225
+ # from the primary operation return type.
226
+ # Call the new helper for this specific response
227
+ current_return_type_str = get_type_for_specific_response(
228
+ operation_path=getattr(op, "path", ""),
229
+ resp_ir=resp_ir,
230
+ all_schemas=self.schemas,
231
+ ctx=context,
232
+ return_unwrap_data_property=True,
233
+ )
234
+ current_needs_unwrap = (
235
+ "data" in current_return_type_str.lower() or "item" in current_return_type_str.lower()
236
+ )
237
+
238
+ if resp_ir.status_code == "default":
239
+ # Determine type for default response if it has content
240
+ default_return_type_str = "None"
241
+ default_needs_unwrap = False
242
+ if resp_ir.content:
243
+ # If 'default' is primary success, get_return_type_unified(op,...) might give its type.
244
+ # We use the operation's global/primary return type if default has content.
245
+ op_global_return_type = get_return_type_unified(op, context, self.schemas)
246
+ op_global_needs_unwrap = False # Unified service handles unwrapping internally
247
+ # Only use this if the global type is not 'None', otherwise keep default_return_type_str as 'None'.
248
+ if op_global_return_type != "None":
249
+ default_return_type_str = op_global_return_type
250
+ default_needs_unwrap = op_global_needs_unwrap
251
+
252
+ default_case = DefaultCase(
253
+ response_ir=resp_ir, return_type=default_return_type_str, needs_unwrap=default_needs_unwrap
254
+ )
255
+ continue # Handle default separately
256
+
257
+ try:
258
+ status_code_val = int(resp_ir.status_code)
259
+ case_type = "success" if resp_ir.status_code.startswith("2") else "error"
260
+
261
+ status_cases.append(
262
+ StatusCase(
263
+ status_code=status_code_val,
264
+ type=case_type,
265
+ return_type=current_return_type_str,
266
+ needs_unwrap=current_needs_unwrap,
267
+ response_ir=resp_ir,
268
+ )
269
+ )
270
+ except ValueError:
271
+ logger.warning(f"Skipping non-integer status code in other_responses: {resp_ir.status_code}")
272
+
273
+ # Generate the match statement
274
+ if status_cases or default_case:
275
+ writer.write_line("match response.status_code:")
276
+ writer.indent()
277
+
278
+ # Generate cases for specific status codes
279
+ for case in status_cases:
280
+ writer.write_line(f"case {case['status_code']}:")
281
+ writer.indent()
282
+
283
+ if case["type"] == "primary_success":
284
+ # If get_return_type determined a specific type (not "None"),
285
+ # we should attempt to parse the response accordingly. This handles cases
286
+ # where the type was inferred even if the spec lacked explicit content for the 2xx.
287
+ # If get_return_type says "None" (e.g., for a 204 or truly no content), then return None.
288
+ if case["return_type"] == "None":
289
+ writer.write_line("return None")
290
+ else:
291
+ self._write_parsed_return(
292
+ writer, op, context, case["return_type"], case["needs_unwrap"], case["response_ir"]
293
+ )
294
+ elif case["type"] == "success":
295
+ # Other 2xx success
296
+ if case["return_type"] == "None" or not case["response_ir"].content:
297
+ writer.write_line("return None")
298
+ else:
299
+ self._write_parsed_return(
300
+ writer, op, context, case["return_type"], case["needs_unwrap"], case["response_ir"]
301
+ )
302
+ elif case["type"] == "error":
303
+ # Error codes (3xx, 4xx, 5xx)
304
+ error_class_name = f"Error{case['status_code']}"
305
+ context.add_import(
306
+ f"{context.core_package_name}", error_class_name
307
+ ) # Import from top-level core package
308
+ writer.write_line(f"raise {error_class_name}(response=response)")
309
+
310
+ writer.dedent()
311
+
312
+ # Handle default case if it exists
313
+ if default_case:
314
+ # Default response case - catch all remaining status codes
315
+ if default_case["response_ir"].content and default_case["return_type"] != "None":
316
+ # Default case with content (success)
317
+ writer.write_line("case _ if response.status_code >= 0: # Default response catch-all")
318
+ writer.indent()
319
+ self._write_parsed_return(
320
+ writer,
321
+ op,
322
+ context,
323
+ default_case["return_type"],
324
+ default_case["needs_unwrap"],
325
+ default_case["response_ir"],
326
+ )
327
+ writer.dedent()
328
+ else:
329
+ # Default case without content (error)
330
+ writer.write_line("case _: # Default error response")
331
+ writer.indent()
332
+ context.add_import(f"{context.core_package_name}.exceptions", "HTTPError")
333
+ default_description = default_case["response_ir"].description or "Unknown default error"
334
+ writer.write_line(
335
+ f"raise HTTPError(response=response, "
336
+ f'message="Default error: {default_description}", '
337
+ f"status_code=response.status_code)"
338
+ )
339
+ writer.dedent()
340
+ else:
341
+ # Final catch-all for unhandled status codes
342
+ writer.write_line("case _:")
343
+ writer.indent()
344
+ context.add_import(f"{context.core_package_name}.exceptions", "HTTPError")
345
+ writer.write_line(
346
+ "raise HTTPError("
347
+ "response=response, "
348
+ 'message="Unhandled status code", '
349
+ "status_code=response.status_code)"
350
+ )
351
+ writer.dedent()
352
+
353
+ writer.dedent() # End of match statement
354
+ else:
355
+ # Fallback if no responses are defined
356
+ writer.write_line("match response.status_code:")
357
+ writer.indent()
358
+ writer.write_line("case _:")
359
+ writer.indent()
360
+ context.add_import(f"{context.core_package_name}.exceptions", "HTTPError")
361
+ writer.write_line(
362
+ f'raise HTTPError(response=response, message="Unhandled status code", status_code=response.status_code)'
363
+ )
364
+ writer.dedent()
365
+ writer.dedent()
366
+
367
+ # All code paths should be covered by the match statement above
368
+ # But add an explicit assertion for mypy's satisfaction
369
+ writer.write_line("# All paths above should return or raise - this should never execute")
370
+ context.add_import("typing", "NoReturn")
371
+ writer.write_line("assert False, 'Unexpected code path' # pragma: no cover")
372
+ writer.write_line("") # Add a blank line for readability
373
+
374
+ def _write_parsed_return(
375
+ self,
376
+ writer: CodeWriter,
377
+ op: IROperation,
378
+ context: RenderContext,
379
+ return_type: str,
380
+ needs_unwrap: bool,
381
+ response_ir: Optional[IRResponse] = None,
382
+ ) -> None:
383
+ """Helper to write the actual return statement with parsing/extraction logic."""
384
+
385
+ # This section largely reuses the logic from the original generate_response_handling
386
+ # adapted to be callable for a specific return_type and response context.
387
+
388
+ is_op_with_inferred_type = return_type != "None" and not any(
389
+ r.content for r in op.responses if r.status_code.startswith("2")
390
+ ) # This might need adjustment if called for a specific non-primary response.
391
+
392
+ if return_type.startswith("Union["):
393
+ context.add_import("typing", "Union")
394
+ context.add_import("typing", "cast")
395
+ # Corrected regex to parse "Union[TypeA, TypeB]"
396
+ match = re.match(r"Union\[([A-Za-z0-9_]+),\s*([A-Za-z0-9_]+)\]", return_type)
397
+ if match:
398
+ type1_str = match.group(1).strip()
399
+ type2_str = match.group(2).strip()
400
+ context.add_typing_imports_for_type(type1_str)
401
+ context.add_typing_imports_for_type(type2_str)
402
+ writer.write_line("try:")
403
+ writer.indent()
404
+ # Pass response_ir to _get_extraction_code if available
405
+ extraction_code_type1 = self._get_extraction_code(type1_str, context, op, needs_unwrap, response_ir)
406
+ if "\n" in extraction_code_type1: # Multi-line extraction
407
+ lines = extraction_code_type1.split("\n")
408
+ for line in lines[:-1]: # all but 'return ...'
409
+ writer.write_line(line)
410
+ writer.write_line(lines[-1].replace("return ", "return_value = "))
411
+ writer.write_line("return return_value")
412
+ else:
413
+ writer.write_line(f"return {extraction_code_type1}")
414
+
415
+ writer.dedent()
416
+ writer.write_line("except Exception: # Attempt to parse as the second type")
417
+ writer.indent()
418
+ extraction_code_type2 = self._get_extraction_code(type2_str, context, op, needs_unwrap, response_ir)
419
+ if "\n" in extraction_code_type2: # Multi-line extraction
420
+ lines = extraction_code_type2.split("\n")
421
+ for line in lines[:-1]:
422
+ writer.write_line(line)
423
+ writer.write_line(lines[-1].replace("return ", "return_value = "))
424
+ writer.write_line("return return_value")
425
+ else:
426
+ writer.write_line(f"return {extraction_code_type2}")
427
+ writer.dedent()
428
+ else:
429
+ logger.warning(
430
+ f"Could not parse Union components with regex: {return_type}. Falling back to cast(Any, ...)"
431
+ )
432
+ context.add_import("typing", "Any")
433
+ writer.write_line(f"return cast(Any, response.json())")
434
+
435
+ elif return_type == "None": # Explicit None, e.g. for 204 or when specific response has no content
436
+ writer.write_line("return None")
437
+ elif is_op_with_inferred_type: # This condition may need re-evaluation in this context
438
+ context.add_typing_imports_for_type(return_type)
439
+ context.add_import("typing", "cast")
440
+ writer.write_line(f"return cast({return_type}, response.json())")
441
+ else:
442
+ context.add_typing_imports_for_type(return_type)
443
+ extraction_code_str = self._get_extraction_code(return_type, context, op, needs_unwrap, response_ir)
444
+
445
+ if extraction_code_str == "sse_json_stream_marker": # SSE handling
446
+ context.add_plain_import("json")
447
+ context.add_import(f"{context.core_package_name}.streaming_helpers", "iter_sse_events_text")
448
+ # The actual yield loop must be outside, this function is about the *return value* for one branch.
449
+ # This indicates that SSE streaming might need to be handled more holistically.
450
+ # For now, if we hit this, it means get_return_type decided on AsyncIterator for an SSE.
451
+ # The method signature is already async iterator.
452
+ # The dispatcher should yield from the iter_sse_events_text.
453
+ # This implies that the `if response.status_code == ...:` block itself needs to be `async for ... yield`
454
+ # This refactoring is getting deeper.
455
+ # Quick fix: if it's sse_json_stream_marker, we write the loop here.
456
+ writer.write_line(f"async for chunk in iter_sse_events_text(response):")
457
+ writer.indent()
458
+ writer.write_line("yield json.loads(chunk)") # Assuming item_type for SSE is JSON decodable
459
+ writer.dedent()
460
+ writer.write_line(
461
+ "return # Explicit return for async generator"
462
+ ) # Ensure function ends if it's a generator path
463
+ elif extraction_code_str == "iter_bytes(response)" or (
464
+ return_type.startswith("AsyncIterator[") and "Iterator" in return_type
465
+ ):
466
+ # Handle streaming responses - either binary (bytes) or event-stream (Dict[str, Any])
467
+ context.add_import(f"{context.core_package_name}.streaming_helpers", "iter_bytes")
468
+ if return_type == "AsyncIterator[bytes]":
469
+ # Binary streaming
470
+ writer.write_line(f"async for chunk in iter_bytes(response):")
471
+ writer.indent()
472
+ writer.write_line("yield chunk")
473
+ writer.dedent()
474
+ elif "Dict[str, Any]" in return_type or "dict" in return_type.lower():
475
+ # Event-stream or JSON streaming
476
+ context.add_plain_import("json")
477
+ context.add_import(f"{context.core_package_name}.streaming_helpers", "iter_sse_events_text")
478
+ writer.write_line(f"async for chunk in iter_sse_events_text(response):")
479
+ writer.indent()
480
+ writer.write_line("yield json.loads(chunk)")
481
+ writer.dedent()
482
+ else:
483
+ # Other streaming type
484
+ writer.write_line(f"async for chunk in iter_bytes(response):")
485
+ writer.indent()
486
+ writer.write_line("yield chunk")
487
+ writer.dedent()
488
+ writer.write_line("return # Explicit return for async generator")
489
+
490
+ elif "\n" in extraction_code_str: # Multi-line extraction code (e.g. data unwrap)
491
+ # The _get_extraction_code for unwrap already includes "return cast(...)"
492
+ for line in extraction_code_str.split("\n"):
493
+ writer.write_line(line)
494
+ else: # Single line extraction code
495
+ if return_type != "None": # Should already be handled, but as safety
496
+ writer.write_line(f"return {extraction_code_str}")
497
+ # writer.write_line("") # Blank line might be added by the caller of this helper