pyopenapi-gen 0.8.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyopenapi_gen/__init__.py +114 -0
- pyopenapi_gen/__main__.py +6 -0
- pyopenapi_gen/cli.py +86 -0
- pyopenapi_gen/context/file_manager.py +52 -0
- pyopenapi_gen/context/import_collector.py +382 -0
- pyopenapi_gen/context/render_context.py +630 -0
- pyopenapi_gen/core/__init__.py +0 -0
- pyopenapi_gen/core/auth/base.py +22 -0
- pyopenapi_gen/core/auth/plugins.py +89 -0
- pyopenapi_gen/core/exceptions.py +25 -0
- pyopenapi_gen/core/http_transport.py +219 -0
- pyopenapi_gen/core/loader/__init__.py +12 -0
- pyopenapi_gen/core/loader/loader.py +158 -0
- pyopenapi_gen/core/loader/operations/__init__.py +12 -0
- pyopenapi_gen/core/loader/operations/parser.py +155 -0
- pyopenapi_gen/core/loader/operations/post_processor.py +60 -0
- pyopenapi_gen/core/loader/operations/request_body.py +85 -0
- pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
- pyopenapi_gen/core/loader/parameters/parser.py +121 -0
- pyopenapi_gen/core/loader/responses/__init__.py +10 -0
- pyopenapi_gen/core/loader/responses/parser.py +104 -0
- pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
- pyopenapi_gen/core/loader/schemas/extractor.py +184 -0
- pyopenapi_gen/core/pagination.py +64 -0
- pyopenapi_gen/core/parsing/__init__.py +13 -0
- pyopenapi_gen/core/parsing/common/__init__.py +1 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/__init__.py +9 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/__init__.py +0 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/cyclic_properties.py +66 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/direct_cycle.py +33 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/existing_schema.py +22 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +54 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +52 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +50 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +51 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +86 -0
- pyopenapi_gen/core/parsing/common/type_parser.py +74 -0
- pyopenapi_gen/core/parsing/context.py +184 -0
- pyopenapi_gen/core/parsing/cycle_helpers.py +123 -0
- pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
- pyopenapi_gen/core/parsing/keywords/all_of_parser.py +77 -0
- pyopenapi_gen/core/parsing/keywords/any_of_parser.py +79 -0
- pyopenapi_gen/core/parsing/keywords/array_items_parser.py +69 -0
- pyopenapi_gen/core/parsing/keywords/one_of_parser.py +72 -0
- pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
- pyopenapi_gen/core/parsing/schema_finalizer.py +166 -0
- pyopenapi_gen/core/parsing/schema_parser.py +610 -0
- pyopenapi_gen/core/parsing/transformers/__init__.py +0 -0
- pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +285 -0
- pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +117 -0
- pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
- pyopenapi_gen/core/postprocess_manager.py +161 -0
- pyopenapi_gen/core/schemas.py +40 -0
- pyopenapi_gen/core/streaming_helpers.py +86 -0
- pyopenapi_gen/core/telemetry.py +67 -0
- pyopenapi_gen/core/utils.py +409 -0
- pyopenapi_gen/core/warning_collector.py +83 -0
- pyopenapi_gen/core/writers/code_writer.py +135 -0
- pyopenapi_gen/core/writers/documentation_writer.py +222 -0
- pyopenapi_gen/core/writers/line_writer.py +217 -0
- pyopenapi_gen/core/writers/python_construct_renderer.py +274 -0
- pyopenapi_gen/core_package_template/README.md +21 -0
- pyopenapi_gen/emit/models_emitter.py +143 -0
- pyopenapi_gen/emitters/client_emitter.py +51 -0
- pyopenapi_gen/emitters/core_emitter.py +181 -0
- pyopenapi_gen/emitters/docs_emitter.py +44 -0
- pyopenapi_gen/emitters/endpoints_emitter.py +223 -0
- pyopenapi_gen/emitters/exceptions_emitter.py +52 -0
- pyopenapi_gen/emitters/models_emitter.py +428 -0
- pyopenapi_gen/generator/client_generator.py +562 -0
- pyopenapi_gen/helpers/__init__.py +1 -0
- pyopenapi_gen/helpers/endpoint_utils.py +552 -0
- pyopenapi_gen/helpers/type_cleaner.py +341 -0
- pyopenapi_gen/helpers/type_helper.py +112 -0
- pyopenapi_gen/helpers/type_resolution/__init__.py +1 -0
- pyopenapi_gen/helpers/type_resolution/array_resolver.py +57 -0
- pyopenapi_gen/helpers/type_resolution/composition_resolver.py +79 -0
- pyopenapi_gen/helpers/type_resolution/finalizer.py +89 -0
- pyopenapi_gen/helpers/type_resolution/named_resolver.py +174 -0
- pyopenapi_gen/helpers/type_resolution/object_resolver.py +212 -0
- pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +57 -0
- pyopenapi_gen/helpers/type_resolution/resolver.py +48 -0
- pyopenapi_gen/helpers/url_utils.py +14 -0
- pyopenapi_gen/http_types.py +20 -0
- pyopenapi_gen/ir.py +167 -0
- pyopenapi_gen/py.typed +1 -0
- pyopenapi_gen/types/__init__.py +11 -0
- pyopenapi_gen/types/contracts/__init__.py +13 -0
- pyopenapi_gen/types/contracts/protocols.py +106 -0
- pyopenapi_gen/types/contracts/types.py +30 -0
- pyopenapi_gen/types/resolvers/__init__.py +7 -0
- pyopenapi_gen/types/resolvers/reference_resolver.py +71 -0
- pyopenapi_gen/types/resolvers/response_resolver.py +203 -0
- pyopenapi_gen/types/resolvers/schema_resolver.py +367 -0
- pyopenapi_gen/types/services/__init__.py +5 -0
- pyopenapi_gen/types/services/type_service.py +133 -0
- pyopenapi_gen/visit/client_visitor.py +228 -0
- pyopenapi_gen/visit/docs_visitor.py +38 -0
- pyopenapi_gen/visit/endpoint/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/endpoint_visitor.py +103 -0
- pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +121 -0
- pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +87 -0
- pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
- pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +497 -0
- pyopenapi_gen/visit/endpoint/generators/signature_generator.py +88 -0
- pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +183 -0
- pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +76 -0
- pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
- pyopenapi_gen/visit/exception_visitor.py +52 -0
- pyopenapi_gen/visit/model/__init__.py +0 -0
- pyopenapi_gen/visit/model/alias_generator.py +89 -0
- pyopenapi_gen/visit/model/dataclass_generator.py +197 -0
- pyopenapi_gen/visit/model/enum_generator.py +200 -0
- pyopenapi_gen/visit/model/model_visitor.py +197 -0
- pyopenapi_gen/visit/visitor.py +97 -0
- pyopenapi_gen-0.8.3.dist-info/METADATA +224 -0
- pyopenapi_gen-0.8.3.dist-info/RECORD +122 -0
- pyopenapi_gen-0.8.3.dist-info/WHEEL +4 -0
- pyopenapi_gen-0.8.3.dist-info/entry_points.txt +2 -0
- pyopenapi_gen-0.8.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,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
|