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.
- pyopenapi_gen/__init__.py +224 -0
- pyopenapi_gen/__main__.py +6 -0
- pyopenapi_gen/cli.py +62 -0
- pyopenapi_gen/context/CLAUDE.md +284 -0
- pyopenapi_gen/context/file_manager.py +52 -0
- pyopenapi_gen/context/import_collector.py +382 -0
- pyopenapi_gen/context/render_context.py +726 -0
- pyopenapi_gen/core/CLAUDE.md +224 -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/cattrs_converter.py +810 -0
- pyopenapi_gen/core/exceptions.py +20 -0
- pyopenapi_gen/core/http_status_codes.py +218 -0
- pyopenapi_gen/core/http_transport.py +222 -0
- pyopenapi_gen/core/loader/__init__.py +12 -0
- pyopenapi_gen/core/loader/loader.py +174 -0
- pyopenapi_gen/core/loader/operations/__init__.py +12 -0
- pyopenapi_gen/core/loader/operations/parser.py +161 -0
- pyopenapi_gen/core/loader/operations/post_processor.py +62 -0
- pyopenapi_gen/core/loader/operations/request_body.py +90 -0
- pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
- pyopenapi_gen/core/loader/parameters/parser.py +186 -0
- pyopenapi_gen/core/loader/responses/__init__.py +10 -0
- pyopenapi_gen/core/loader/responses/parser.py +111 -0
- pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
- pyopenapi_gen/core/loader/schemas/extractor.py +275 -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 +73 -0
- pyopenapi_gen/core/parsing/context.py +187 -0
- pyopenapi_gen/core/parsing/cycle_helpers.py +126 -0
- pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
- pyopenapi_gen/core/parsing/keywords/all_of_parser.py +81 -0
- pyopenapi_gen/core/parsing/keywords/any_of_parser.py +84 -0
- pyopenapi_gen/core/parsing/keywords/array_items_parser.py +72 -0
- pyopenapi_gen/core/parsing/keywords/one_of_parser.py +77 -0
- pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
- pyopenapi_gen/core/parsing/schema_finalizer.py +169 -0
- pyopenapi_gen/core/parsing/schema_parser.py +804 -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 +120 -0
- pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
- pyopenapi_gen/core/postprocess_manager.py +260 -0
- pyopenapi_gen/core/spec_fetcher.py +148 -0
- pyopenapi_gen/core/streaming_helpers.py +84 -0
- pyopenapi_gen/core/telemetry.py +69 -0
- pyopenapi_gen/core/utils.py +456 -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 +321 -0
- pyopenapi_gen/core_package_template/README.md +21 -0
- pyopenapi_gen/emit/models_emitter.py +143 -0
- pyopenapi_gen/emitters/CLAUDE.md +286 -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 +247 -0
- pyopenapi_gen/emitters/exceptions_emitter.py +187 -0
- pyopenapi_gen/emitters/mocks_emitter.py +185 -0
- pyopenapi_gen/emitters/models_emitter.py +426 -0
- pyopenapi_gen/generator/CLAUDE.md +352 -0
- pyopenapi_gen/generator/client_generator.py +567 -0
- pyopenapi_gen/generator/exceptions.py +7 -0
- pyopenapi_gen/helpers/CLAUDE.md +325 -0
- pyopenapi_gen/helpers/__init__.py +1 -0
- pyopenapi_gen/helpers/endpoint_utils.py +532 -0
- pyopenapi_gen/helpers/type_cleaner.py +334 -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 +105 -0
- pyopenapi_gen/helpers/type_resolution/named_resolver.py +172 -0
- pyopenapi_gen/helpers/type_resolution/object_resolver.py +216 -0
- pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +109 -0
- pyopenapi_gen/helpers/type_resolution/resolver.py +47 -0
- pyopenapi_gen/helpers/url_utils.py +14 -0
- pyopenapi_gen/http_types.py +20 -0
- pyopenapi_gen/ir.py +165 -0
- pyopenapi_gen/py.typed +1 -0
- pyopenapi_gen/types/CLAUDE.md +140 -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 +28 -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 +177 -0
- pyopenapi_gen/types/resolvers/schema_resolver.py +498 -0
- pyopenapi_gen/types/services/__init__.py +5 -0
- pyopenapi_gen/types/services/type_service.py +165 -0
- pyopenapi_gen/types/strategies/__init__.py +5 -0
- pyopenapi_gen/types/strategies/response_strategy.py +310 -0
- pyopenapi_gen/visit/CLAUDE.md +272 -0
- pyopenapi_gen/visit/client_visitor.py +477 -0
- pyopenapi_gen/visit/docs_visitor.py +38 -0
- pyopenapi_gen/visit/endpoint/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/endpoint_visitor.py +292 -0
- pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +123 -0
- pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +222 -0
- pyopenapi_gen/visit/endpoint/generators/mock_generator.py +140 -0
- pyopenapi_gen/visit/endpoint/generators/overload_generator.py +252 -0
- pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
- pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +705 -0
- pyopenapi_gen/visit/endpoint/generators/signature_generator.py +83 -0
- pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +207 -0
- pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +78 -0
- pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
- pyopenapi_gen/visit/exception_visitor.py +90 -0
- pyopenapi_gen/visit/model/__init__.py +0 -0
- pyopenapi_gen/visit/model/alias_generator.py +93 -0
- pyopenapi_gen/visit/model/dataclass_generator.py +553 -0
- pyopenapi_gen/visit/model/enum_generator.py +212 -0
- pyopenapi_gen/visit/model/model_visitor.py +198 -0
- pyopenapi_gen/visit/visitor.py +97 -0
- pyopenapi_gen-2.7.2.dist-info/METADATA +1169 -0
- pyopenapi_gen-2.7.2.dist-info/RECORD +137 -0
- pyopenapi_gen-2.7.2.dist-info/WHEEL +4 -0
- pyopenapi_gen-2.7.2.dist-info/entry_points.txt +2 -0
- pyopenapi_gen-2.7.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,705 @@
|
|
|
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
|
+
from typing import TYPE_CHECKING, Any, TypedDict
|
|
9
|
+
|
|
10
|
+
from pyopenapi_gen.core.http_status_codes import get_exception_class_name
|
|
11
|
+
from pyopenapi_gen.core.writers.code_writer import CodeWriter
|
|
12
|
+
from pyopenapi_gen.helpers.endpoint_utils import (
|
|
13
|
+
_get_primary_response,
|
|
14
|
+
)
|
|
15
|
+
from pyopenapi_gen.types.services.type_service import UnifiedTypeService
|
|
16
|
+
from pyopenapi_gen.types.strategies.response_strategy import ResponseStrategy
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from pyopenapi_gen import IROperation, IRResponse, IRSchema
|
|
20
|
+
from pyopenapi_gen.context.render_context import RenderContext
|
|
21
|
+
else:
|
|
22
|
+
# For runtime, we need to import for TypedDict
|
|
23
|
+
from pyopenapi_gen import IRResponse, IRSchema
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class StatusCase(TypedDict):
|
|
29
|
+
"""Type definition for status code case data."""
|
|
30
|
+
|
|
31
|
+
status_code: int
|
|
32
|
+
type: str # 'primary_success', 'success', or 'error'
|
|
33
|
+
return_type: str
|
|
34
|
+
response_ir: IRResponse
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class DefaultCase(TypedDict):
|
|
38
|
+
"""Type definition for default case data."""
|
|
39
|
+
|
|
40
|
+
response_ir: IRResponse
|
|
41
|
+
return_type: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class EndpointResponseHandlerGenerator:
|
|
45
|
+
"""Generates the response handling logic for an endpoint method."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, schemas: dict[str, Any] | None = None) -> None:
|
|
48
|
+
self.schemas: dict[str, Any] = schemas or {}
|
|
49
|
+
|
|
50
|
+
def _register_cattrs_import(self, context: RenderContext) -> None:
|
|
51
|
+
"""Register the cattrs structure_from_dict import."""
|
|
52
|
+
context.add_import(f"{context.core_package_name}.cattrs_converter", "structure_from_dict")
|
|
53
|
+
|
|
54
|
+
def _is_type_alias_to_array(self, type_name: str) -> bool:
|
|
55
|
+
"""
|
|
56
|
+
Check if a type name corresponds to a type alias that resolves to a List/array type.
|
|
57
|
+
|
|
58
|
+
This helps distinguish between:
|
|
59
|
+
- Type aliases: AgentHistoryListResponse = List[AgentHistory] (should use array deserialization)
|
|
60
|
+
- Dataclasses: class AgentHistoryListResponse: ... (should use structure_from_dict())
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
type_name: The Python type name (e.g., "AgentHistoryListResponse")
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
True if this is a type alias that resolves to List[SomeType]
|
|
67
|
+
"""
|
|
68
|
+
# Extract base type name without generics
|
|
69
|
+
base_type = type_name
|
|
70
|
+
if "[" in base_type:
|
|
71
|
+
base_type = base_type[: base_type.find("[")]
|
|
72
|
+
|
|
73
|
+
# Look up the schema for this type name
|
|
74
|
+
if base_type in self.schemas:
|
|
75
|
+
schema = self.schemas[base_type]
|
|
76
|
+
# Check if it's a type alias using ModelVisitor's logic:
|
|
77
|
+
# - Has a name
|
|
78
|
+
# - No properties (not an object with fields)
|
|
79
|
+
# - Not an enum
|
|
80
|
+
# - Type is not "object" (which would be a dataclass)
|
|
81
|
+
# - Type is "array" (indicating it's an array type alias)
|
|
82
|
+
is_type_alias = bool(
|
|
83
|
+
getattr(schema, "name", None)
|
|
84
|
+
and not getattr(schema, "properties", None)
|
|
85
|
+
and not getattr(schema, "enum", None)
|
|
86
|
+
and getattr(schema, "type", None) != "object"
|
|
87
|
+
)
|
|
88
|
+
is_array_type = getattr(schema, "type", None) == "array"
|
|
89
|
+
return is_type_alias and is_array_type
|
|
90
|
+
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
def _is_type_alias_to_primitive(self, type_name: str) -> bool:
|
|
94
|
+
"""
|
|
95
|
+
Check if a type name corresponds to a type alias that resolves to a primitive type.
|
|
96
|
+
|
|
97
|
+
This helps distinguish between:
|
|
98
|
+
- Type aliases: StringAlias = str (should use cast())
|
|
99
|
+
- Dataclasses: class MyModel: ... (should use .from_dict())
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
type_name: The Python type name (e.g., "StringAlias")
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
True if this is a type alias that resolves to a primitive type (str, int, float, bool)
|
|
106
|
+
"""
|
|
107
|
+
# Extract base type name without generics
|
|
108
|
+
base_type = type_name
|
|
109
|
+
if "[" in base_type:
|
|
110
|
+
base_type = base_type[: base_type.find("[")]
|
|
111
|
+
|
|
112
|
+
# Look up the schema for this type name
|
|
113
|
+
if base_type in self.schemas:
|
|
114
|
+
schema = self.schemas[base_type]
|
|
115
|
+
# Check if it's a type alias using ModelVisitor's logic:
|
|
116
|
+
# - Has a name
|
|
117
|
+
# - No properties (not an object with fields)
|
|
118
|
+
# - Not an enum
|
|
119
|
+
# - Type is not "object" (which would be a dataclass)
|
|
120
|
+
# - Type is a primitive (string, integer, number, boolean)
|
|
121
|
+
is_type_alias = bool(
|
|
122
|
+
getattr(schema, "name", None)
|
|
123
|
+
and not getattr(schema, "properties", None)
|
|
124
|
+
and not getattr(schema, "enum", None)
|
|
125
|
+
and getattr(schema, "type", None) != "object"
|
|
126
|
+
)
|
|
127
|
+
is_primitive_type = getattr(schema, "type", None) in ("string", "integer", "number", "boolean")
|
|
128
|
+
return is_type_alias and is_primitive_type
|
|
129
|
+
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
def _extract_array_item_type(self, type_name: str) -> str:
|
|
133
|
+
"""
|
|
134
|
+
Extract the item type from an array type or type alias.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
type_name: Type name like "List[ItemType]" or "AgentListResponse" (alias to List[Item])
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
The item type as a string (e.g., "ItemType", "AgentListResponseItem")
|
|
141
|
+
"""
|
|
142
|
+
# If it's already in List[X] format, extract X
|
|
143
|
+
if type_name.startswith("List[") or type_name.startswith("list["):
|
|
144
|
+
start_bracket = type_name.find("[")
|
|
145
|
+
end_bracket = type_name.rfind("]")
|
|
146
|
+
return type_name[start_bracket + 1 : end_bracket].strip()
|
|
147
|
+
|
|
148
|
+
# If it's a type alias, look up the schema and get the items type
|
|
149
|
+
base_type = type_name
|
|
150
|
+
if "[" in base_type:
|
|
151
|
+
base_type = base_type[: base_type.find("[")]
|
|
152
|
+
|
|
153
|
+
if base_type in self.schemas:
|
|
154
|
+
schema = self.schemas[base_type]
|
|
155
|
+
if getattr(schema, "type", None) == "array" and getattr(schema, "items", None):
|
|
156
|
+
# Get items schema and resolve its type
|
|
157
|
+
items_schema = schema.items
|
|
158
|
+
# Check if items is a reference or inline schema
|
|
159
|
+
if hasattr(items_schema, "name") and items_schema.name:
|
|
160
|
+
return str(items_schema.name)
|
|
161
|
+
elif hasattr(items_schema, "type"):
|
|
162
|
+
# Inline schema - map to Python type
|
|
163
|
+
return str(items_schema.type)
|
|
164
|
+
|
|
165
|
+
# Fallback: return the original type name (might be primitive List[str])
|
|
166
|
+
return type_name
|
|
167
|
+
|
|
168
|
+
def _is_dataclass_type(self, type_name: str) -> bool:
|
|
169
|
+
"""
|
|
170
|
+
Check if a type name refers to a dataclass (not a primitive or type alias).
|
|
171
|
+
|
|
172
|
+
This is used to determine if a type needs structure_from_dict() deserialisation.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
type_name: The Python type name (e.g., "User", "AgentListResponseItem")
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
True if the type is a dataclass (has properties or type="object")
|
|
179
|
+
"""
|
|
180
|
+
# Skip obvious primitives and built-ins
|
|
181
|
+
if type_name in {
|
|
182
|
+
"str",
|
|
183
|
+
"int",
|
|
184
|
+
"float",
|
|
185
|
+
"bool",
|
|
186
|
+
"bytes",
|
|
187
|
+
"None",
|
|
188
|
+
"Any",
|
|
189
|
+
"Dict",
|
|
190
|
+
"List",
|
|
191
|
+
"dict",
|
|
192
|
+
"list",
|
|
193
|
+
"tuple",
|
|
194
|
+
}:
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
# Extract base type name without generics
|
|
198
|
+
base_type = type_name
|
|
199
|
+
if "[" in base_type:
|
|
200
|
+
base_type = base_type[: base_type.find("[")]
|
|
201
|
+
|
|
202
|
+
# Look up in schemas
|
|
203
|
+
if base_type in self.schemas:
|
|
204
|
+
schema = self.schemas[base_type]
|
|
205
|
+
# Dataclasses have properties or are object type
|
|
206
|
+
return getattr(schema, "type", None) == "object" or bool(getattr(schema, "properties", None))
|
|
207
|
+
|
|
208
|
+
# Heuristic: uppercase names are likely models (not primitives)
|
|
209
|
+
return base_type[0].isupper() and base_type not in {"Dict", "List", "Union", "Tuple", "Optional"}
|
|
210
|
+
|
|
211
|
+
def _should_use_cattrs_structure(self, type_name: str) -> bool:
|
|
212
|
+
"""
|
|
213
|
+
Determine if a type should use cattrs deserialization.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
type_name: The Python type name (e.g., "User", "List[User]", "User | None")
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
True if the type should use structure_from_dict() deserialization
|
|
220
|
+
"""
|
|
221
|
+
# Extract the base type name from complex types
|
|
222
|
+
base_type = type_name
|
|
223
|
+
|
|
224
|
+
# Handle List[Type], Type | None, etc.
|
|
225
|
+
if "[" in base_type and "]" in base_type:
|
|
226
|
+
# Extract the inner type from List[Type], Type | None, etc.
|
|
227
|
+
start_bracket = base_type.find("[")
|
|
228
|
+
end_bracket = base_type.rfind("]")
|
|
229
|
+
inner_type = base_type[start_bracket + 1 : end_bracket]
|
|
230
|
+
|
|
231
|
+
# For Union types like User | None -> Union[User, None], take the first type
|
|
232
|
+
if ", " in inner_type:
|
|
233
|
+
inner_type = inner_type.split(", ")[0]
|
|
234
|
+
|
|
235
|
+
base_type = inner_type.strip()
|
|
236
|
+
|
|
237
|
+
# Skip primitive types and built-ins (both uppercase and lowercase)
|
|
238
|
+
if base_type in {
|
|
239
|
+
"str",
|
|
240
|
+
"int",
|
|
241
|
+
"float",
|
|
242
|
+
"bool",
|
|
243
|
+
"bytes",
|
|
244
|
+
"None",
|
|
245
|
+
"Any",
|
|
246
|
+
"Dict",
|
|
247
|
+
"List",
|
|
248
|
+
"dict",
|
|
249
|
+
"list",
|
|
250
|
+
"tuple",
|
|
251
|
+
}:
|
|
252
|
+
return False
|
|
253
|
+
|
|
254
|
+
# Skip typing constructs
|
|
255
|
+
# Note: Modern Python 3.10+ uses | None instead of Optional[X]
|
|
256
|
+
if base_type.startswith(("dict[", "List[", "Union[", "Tuple[", "dict[", "list[", "tuple[")):
|
|
257
|
+
return False
|
|
258
|
+
|
|
259
|
+
# Check if this is a primitive type alias - these should use cast()
|
|
260
|
+
if self._is_type_alias_to_primitive(type_name):
|
|
261
|
+
return False
|
|
262
|
+
|
|
263
|
+
# NEW LOGIC: For array type aliases, check if the item type needs deserialisation
|
|
264
|
+
if self._is_type_alias_to_array(type_name):
|
|
265
|
+
# Extract item type from List[ItemType] or from the type alias schema
|
|
266
|
+
item_type = self._extract_array_item_type(type_name)
|
|
267
|
+
# Check if item type is a dataclass that needs .from_dict()
|
|
268
|
+
return self._is_dataclass_type(item_type)
|
|
269
|
+
|
|
270
|
+
# All custom model types use cattrs via Meta class for automatic field mapping
|
|
271
|
+
# Check if it's a model type (contains a dot indicating it's from models package)
|
|
272
|
+
# or if it's a simple class name that's likely a generated model (starts with uppercase)
|
|
273
|
+
return "." in base_type or (
|
|
274
|
+
base_type[0].isupper() and base_type not in {"Dict", "List", "Union", "Tuple", "dict", "list", "tuple"}
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def _register_imports_for_type(self, return_type: str, context: RenderContext) -> None:
|
|
278
|
+
"""
|
|
279
|
+
Register necessary imports for a return type, including item types for arrays.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
return_type: The return type (e.g., "AgentListResponse", "List[User]", "User")
|
|
283
|
+
context: Render context for import registration
|
|
284
|
+
"""
|
|
285
|
+
# Register the type itself
|
|
286
|
+
context.add_typing_imports_for_type(return_type)
|
|
287
|
+
|
|
288
|
+
# For array type aliases, also register the item type
|
|
289
|
+
if self._is_type_alias_to_array(return_type):
|
|
290
|
+
item_type = self._extract_array_item_type(return_type)
|
|
291
|
+
context.add_typing_imports_for_type(item_type)
|
|
292
|
+
|
|
293
|
+
def _get_cattrs_deserialization_code(self, return_type: str, data_expr: str) -> str:
|
|
294
|
+
"""
|
|
295
|
+
Generate cattrs deserialization code for a given type.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
return_type: The return type (e.g., "User", "List[User]", "list[User]", "AgentListResponse")
|
|
299
|
+
data_expr: The expression containing the raw data to deserialize
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
Code string for deserializing the data using structure_from_dict()
|
|
303
|
+
"""
|
|
304
|
+
# Check if this is an array type alias (e.g., AgentListResponse = List[AgentListResponseItem])
|
|
305
|
+
if self._is_type_alias_to_array(return_type):
|
|
306
|
+
# Use the type alias directly - cattrs handles List[Type] natively
|
|
307
|
+
return f"structure_from_dict({data_expr}, {return_type})"
|
|
308
|
+
|
|
309
|
+
if return_type.startswith("List[") or return_type.startswith("list["):
|
|
310
|
+
# Handle List[Model] or list[Model] types - cattrs handles this natively
|
|
311
|
+
return f"structure_from_dict({data_expr}, {return_type})"
|
|
312
|
+
elif return_type.startswith("Optional["):
|
|
313
|
+
# SANITY CHECK: Unified type system should never produce Optional[X]
|
|
314
|
+
logger.error(
|
|
315
|
+
f"❌ ARCHITECTURE VIOLATION: Received legacy Optional[X] type in response handler: {return_type}. "
|
|
316
|
+
f"Unified type system must generate X | None directly."
|
|
317
|
+
)
|
|
318
|
+
# Defensive conversion (but this indicates a serious bug upstream)
|
|
319
|
+
inner_type = return_type[9:-1] # Remove 'Optional[' and ']'
|
|
320
|
+
logger.warning(f"⚠️ Converting to modern syntax internally for: {inner_type} | None")
|
|
321
|
+
|
|
322
|
+
# Check if inner type is also a list
|
|
323
|
+
if inner_type.startswith("List[") or inner_type.startswith("list["):
|
|
324
|
+
list_code = self._get_cattrs_deserialization_code(inner_type, data_expr)
|
|
325
|
+
return f"{list_code} if {data_expr} is not None else None"
|
|
326
|
+
else:
|
|
327
|
+
return f"structure_from_dict({data_expr}, {inner_type}) if {data_expr} is not None else None"
|
|
328
|
+
elif " | None" in return_type or return_type.endswith("| None"):
|
|
329
|
+
# Handle Model | None types (modern Python 3.10+ syntax)
|
|
330
|
+
# Extract base type from "X | None" pattern
|
|
331
|
+
if " | None" in return_type:
|
|
332
|
+
inner_type = return_type.replace(" | None", "").strip()
|
|
333
|
+
else:
|
|
334
|
+
inner_type = return_type.replace("| None", "").strip()
|
|
335
|
+
|
|
336
|
+
# Check if inner type is also a list
|
|
337
|
+
if inner_type.startswith("List[") or inner_type.startswith("list["):
|
|
338
|
+
list_code = self._get_cattrs_deserialization_code(inner_type, data_expr)
|
|
339
|
+
return f"{list_code} if {data_expr} is not None else None"
|
|
340
|
+
else:
|
|
341
|
+
return f"structure_from_dict({data_expr}, {inner_type}) if {data_expr} is not None else None"
|
|
342
|
+
else:
|
|
343
|
+
# Handle simple Model types only - this should not be called for list types
|
|
344
|
+
if "[" in return_type and "]" in return_type:
|
|
345
|
+
# This is a complex type that we missed - should not happen
|
|
346
|
+
raise ValueError(f"Unsupported complex type for cattrs deserialization: {return_type}")
|
|
347
|
+
|
|
348
|
+
# Safety check: catch the specific issue we're debugging
|
|
349
|
+
if return_type.startswith("list[") or return_type.startswith("List["):
|
|
350
|
+
raise ValueError(
|
|
351
|
+
f"CRITICAL BUG: List type {return_type} reached simple type handler! This should never happen."
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
return f"structure_from_dict({data_expr}, {return_type})"
|
|
355
|
+
|
|
356
|
+
def _get_extraction_code(
|
|
357
|
+
self,
|
|
358
|
+
return_type: str,
|
|
359
|
+
context: RenderContext,
|
|
360
|
+
op: IROperation,
|
|
361
|
+
response_ir: IRResponse | None = None,
|
|
362
|
+
) -> str:
|
|
363
|
+
"""Determines the code snippet to extract/transform the response body."""
|
|
364
|
+
# Handle None, StreamingResponse, Iterator, etc.
|
|
365
|
+
if return_type is None or return_type == "None":
|
|
366
|
+
return "None" # This will be directly used in the return statement
|
|
367
|
+
|
|
368
|
+
# Handle streaming responses
|
|
369
|
+
if return_type.startswith("AsyncIterator["):
|
|
370
|
+
# Check if it's a bytes stream or other type of stream
|
|
371
|
+
if return_type == "AsyncIterator[bytes]":
|
|
372
|
+
context.add_import(f"{context.core_package_name}.streaming_helpers", "iter_bytes")
|
|
373
|
+
return "iter_bytes(response)"
|
|
374
|
+
elif "dict[str, Any]" in return_type or "dict" in return_type.lower():
|
|
375
|
+
# For event streams that return Dict objects
|
|
376
|
+
context.add_import(f"{context.core_package_name}.streaming_helpers", "iter_sse_events_text")
|
|
377
|
+
return "sse_json_stream_marker" # Special marker handled by _write_parsed_return
|
|
378
|
+
else:
|
|
379
|
+
# Model streaming - likely an SSE model stream
|
|
380
|
+
# Extract the model type and check if content type is text/event-stream
|
|
381
|
+
model_type = return_type[13:-1] # Remove 'AsyncIterator[' and ']'
|
|
382
|
+
if response_ir and "text/event-stream" in response_ir.content:
|
|
383
|
+
context.add_import(f"{context.core_package_name}.streaming_helpers", "iter_sse_events_text")
|
|
384
|
+
return "sse_json_stream_marker" # Special marker for SSE streaming
|
|
385
|
+
|
|
386
|
+
# Default to bytes streaming for other types
|
|
387
|
+
context.add_import(f"{context.core_package_name}.streaming_helpers", "iter_bytes")
|
|
388
|
+
return "iter_bytes(response)"
|
|
389
|
+
|
|
390
|
+
# Special case for "data: Any" unwrapping when the actual schema has no fields/properties
|
|
391
|
+
if return_type in {"dict[str, Any]", "dict[str, object]", "object", "Any"}:
|
|
392
|
+
context.add_import("typing", "Dict")
|
|
393
|
+
context.add_import("typing", "Any")
|
|
394
|
+
|
|
395
|
+
if return_type == "str":
|
|
396
|
+
return "response.text"
|
|
397
|
+
elif return_type == "bytes":
|
|
398
|
+
return "response.content"
|
|
399
|
+
elif return_type == "Any":
|
|
400
|
+
context.add_import("typing", "Any")
|
|
401
|
+
return "response.json() # Type is Any"
|
|
402
|
+
elif return_type == "None":
|
|
403
|
+
return "None" # This will be handled by generate_response_handling directly
|
|
404
|
+
else: # Includes schema-defined models, List[], dict[], Optional[]
|
|
405
|
+
context.add_typing_imports_for_type(return_type) # Ensure model itself is imported
|
|
406
|
+
|
|
407
|
+
# Check if we should use cattrs deserialization instead of cast()
|
|
408
|
+
use_base_schema = self._should_use_cattrs_structure(return_type)
|
|
409
|
+
|
|
410
|
+
if not use_base_schema:
|
|
411
|
+
# Fallback to cast() for non-dataclass types
|
|
412
|
+
context.add_import("typing", "cast")
|
|
413
|
+
|
|
414
|
+
# Direct deserialization using schemas as-is (no unwrapping)
|
|
415
|
+
if use_base_schema:
|
|
416
|
+
deserialization_code = self._get_cattrs_deserialization_code(return_type, "response.json()")
|
|
417
|
+
return deserialization_code
|
|
418
|
+
else:
|
|
419
|
+
return f"cast({return_type}, response.json())"
|
|
420
|
+
|
|
421
|
+
def generate_response_handling(
|
|
422
|
+
self,
|
|
423
|
+
writer: CodeWriter,
|
|
424
|
+
op: IROperation,
|
|
425
|
+
context: RenderContext,
|
|
426
|
+
strategy: ResponseStrategy,
|
|
427
|
+
) -> None:
|
|
428
|
+
"""Writes the response parsing and return logic to the CodeWriter, using the unified response strategy."""
|
|
429
|
+
writer.write_line("# Check response status code and handle accordingly")
|
|
430
|
+
|
|
431
|
+
# Generate the match statement for status codes
|
|
432
|
+
writer.write_line("match response.status_code:")
|
|
433
|
+
writer.indent()
|
|
434
|
+
|
|
435
|
+
# Handle the primary success response first
|
|
436
|
+
primary_success_ir = _get_primary_response(op)
|
|
437
|
+
processed_primary_success = False
|
|
438
|
+
if (
|
|
439
|
+
primary_success_ir
|
|
440
|
+
and primary_success_ir.status_code.isdigit()
|
|
441
|
+
and primary_success_ir.status_code.startswith("2")
|
|
442
|
+
):
|
|
443
|
+
status_code_val = int(primary_success_ir.status_code)
|
|
444
|
+
writer.write_line(f"case {status_code_val}:")
|
|
445
|
+
writer.indent()
|
|
446
|
+
|
|
447
|
+
if strategy.return_type == "None":
|
|
448
|
+
writer.write_line("return None")
|
|
449
|
+
else:
|
|
450
|
+
self._write_strategy_based_return(writer, strategy, context)
|
|
451
|
+
|
|
452
|
+
writer.dedent()
|
|
453
|
+
processed_primary_success = True
|
|
454
|
+
|
|
455
|
+
# Handle other responses (exclude primary only if it was actually processed)
|
|
456
|
+
other_responses = [r for r in op.responses if not (processed_primary_success and r == primary_success_ir)]
|
|
457
|
+
for resp_ir in other_responses:
|
|
458
|
+
if resp_ir.status_code.isdigit():
|
|
459
|
+
status_code_val = int(resp_ir.status_code)
|
|
460
|
+
writer.write_line(f"case {status_code_val}:")
|
|
461
|
+
writer.indent()
|
|
462
|
+
|
|
463
|
+
if resp_ir.status_code.startswith("2"):
|
|
464
|
+
# Other 2xx success responses - resolve each response individually
|
|
465
|
+
if not resp_ir.content:
|
|
466
|
+
writer.write_line("return None")
|
|
467
|
+
else:
|
|
468
|
+
# Resolve the specific return type for this response
|
|
469
|
+
resp_schema = self._get_response_schema(resp_ir)
|
|
470
|
+
if resp_schema:
|
|
471
|
+
# Use response.json() directly - no automatic unwrapping
|
|
472
|
+
data_expr = "response.json()"
|
|
473
|
+
|
|
474
|
+
type_service = UnifiedTypeService(self.schemas)
|
|
475
|
+
response_type = type_service.resolve_schema_type(resp_schema, context)
|
|
476
|
+
if self._should_use_cattrs_structure(response_type):
|
|
477
|
+
deserialization_code = self._get_cattrs_deserialization_code(response_type, data_expr)
|
|
478
|
+
writer.write_line(f"return {deserialization_code}")
|
|
479
|
+
self._register_imports_for_type(response_type, context)
|
|
480
|
+
else:
|
|
481
|
+
context.add_import("typing", "cast")
|
|
482
|
+
writer.write_line(f"return cast({response_type}, {data_expr})")
|
|
483
|
+
else:
|
|
484
|
+
writer.write_line("return None")
|
|
485
|
+
else:
|
|
486
|
+
# Error responses - use human-readable exception names
|
|
487
|
+
error_class_name = get_exception_class_name(status_code_val)
|
|
488
|
+
context.add_import(f"{context.core_package_name}", error_class_name)
|
|
489
|
+
writer.write_line(f"raise {error_class_name}(response=response)")
|
|
490
|
+
|
|
491
|
+
writer.dedent()
|
|
492
|
+
|
|
493
|
+
# Handle default case
|
|
494
|
+
default_response = next((r for r in op.responses if r.status_code == "default"), None)
|
|
495
|
+
if default_response:
|
|
496
|
+
writer.write_line("case _: # Default response")
|
|
497
|
+
writer.indent()
|
|
498
|
+
if default_response.content and strategy.return_type != "None":
|
|
499
|
+
self._write_strategy_based_return(writer, strategy, context)
|
|
500
|
+
else:
|
|
501
|
+
context.add_import(f"{context.core_package_name}.exceptions", "HTTPError")
|
|
502
|
+
writer.write_line(
|
|
503
|
+
'raise HTTPError(response=response, message="Default error", status_code=response.status_code)'
|
|
504
|
+
)
|
|
505
|
+
writer.dedent()
|
|
506
|
+
else:
|
|
507
|
+
# Final catch-all
|
|
508
|
+
writer.write_line("case _:")
|
|
509
|
+
writer.indent()
|
|
510
|
+
context.add_import(f"{context.core_package_name}.exceptions", "HTTPError")
|
|
511
|
+
writer.write_line(
|
|
512
|
+
'raise HTTPError(response=response, message="Unhandled status code", status_code=response.status_code)'
|
|
513
|
+
)
|
|
514
|
+
writer.dedent()
|
|
515
|
+
|
|
516
|
+
writer.dedent() # End of match statement
|
|
517
|
+
|
|
518
|
+
# All code paths should be covered by the match statement above
|
|
519
|
+
writer.write_line("# All paths above should return or raise - this should never execute")
|
|
520
|
+
context.add_import("typing", "NoReturn")
|
|
521
|
+
writer.write_line("raise RuntimeError('Unexpected code path') # pragma: no cover")
|
|
522
|
+
writer.write_line("") # Add a blank line for readability
|
|
523
|
+
|
|
524
|
+
def _write_strategy_based_return(
|
|
525
|
+
self,
|
|
526
|
+
writer: CodeWriter,
|
|
527
|
+
strategy: ResponseStrategy,
|
|
528
|
+
context: RenderContext,
|
|
529
|
+
) -> None:
|
|
530
|
+
"""Write the return statement based on the response strategy.
|
|
531
|
+
|
|
532
|
+
This method implements the strategy pattern for response handling,
|
|
533
|
+
ensuring consistent behavior between signature and implementation.
|
|
534
|
+
"""
|
|
535
|
+
if strategy.is_streaming:
|
|
536
|
+
# Handle streaming responses
|
|
537
|
+
if "AsyncIterator[bytes]" in strategy.return_type:
|
|
538
|
+
context.add_import(f"{context.core_package_name}.streaming_helpers", "iter_bytes")
|
|
539
|
+
writer.write_line("async for chunk in iter_bytes(response):")
|
|
540
|
+
writer.indent()
|
|
541
|
+
writer.write_line("yield chunk")
|
|
542
|
+
writer.dedent()
|
|
543
|
+
writer.write_line("return # Explicit return for async generator")
|
|
544
|
+
else:
|
|
545
|
+
# Handle other streaming types
|
|
546
|
+
context.add_plain_import("json")
|
|
547
|
+
context.add_import(f"{context.core_package_name}.streaming_helpers", "iter_sse_events_text")
|
|
548
|
+
writer.write_line("async for chunk in iter_sse_events_text(response):")
|
|
549
|
+
writer.indent()
|
|
550
|
+
writer.write_line("yield json.loads(chunk)")
|
|
551
|
+
writer.dedent()
|
|
552
|
+
writer.write_line("return # Explicit return for async generator")
|
|
553
|
+
return
|
|
554
|
+
|
|
555
|
+
# Use response.json() directly - no automatic unwrapping
|
|
556
|
+
data_expr = "response.json()"
|
|
557
|
+
|
|
558
|
+
# Handle responses using the schema
|
|
559
|
+
if strategy.return_type.startswith("Union["):
|
|
560
|
+
# Check if this is a multi-content-type Union (has content_type_mapping)
|
|
561
|
+
if strategy.content_type_mapping:
|
|
562
|
+
# Generate Content-Type header checking code
|
|
563
|
+
self._write_content_type_conditional_handling(writer, context, strategy)
|
|
564
|
+
else:
|
|
565
|
+
# Traditional Union handling with try/except fallback
|
|
566
|
+
self._write_union_response_handling(writer, context, strategy.return_type, data_expr)
|
|
567
|
+
elif self._should_use_cattrs_structure(strategy.return_type):
|
|
568
|
+
# Register cattrs import
|
|
569
|
+
context.add_import(f"{context.core_package_name}.cattrs_converter", "structure_from_dict")
|
|
570
|
+
deserialization_code = self._get_cattrs_deserialization_code(strategy.return_type, data_expr)
|
|
571
|
+
writer.write_line(f"return {deserialization_code}")
|
|
572
|
+
self._register_imports_for_type(strategy.return_type, context)
|
|
573
|
+
else:
|
|
574
|
+
context.add_import("typing", "cast")
|
|
575
|
+
writer.write_line(f"return cast({strategy.return_type}, {data_expr})")
|
|
576
|
+
|
|
577
|
+
def _get_response_schema(self, response_ir: IRResponse) -> IRSchema | None:
|
|
578
|
+
"""Extract the schema from a response IR."""
|
|
579
|
+
if not response_ir.content:
|
|
580
|
+
return None
|
|
581
|
+
|
|
582
|
+
# Prefer application/json, then first available content type
|
|
583
|
+
content_types = list(response_ir.content.keys())
|
|
584
|
+
preferred_content_type = next((ct for ct in content_types if ct == "application/json"), None)
|
|
585
|
+
if not preferred_content_type:
|
|
586
|
+
preferred_content_type = content_types[0] if content_types else None
|
|
587
|
+
|
|
588
|
+
if preferred_content_type:
|
|
589
|
+
return response_ir.content.get(preferred_content_type)
|
|
590
|
+
|
|
591
|
+
return None
|
|
592
|
+
|
|
593
|
+
def _write_union_response_handling(
|
|
594
|
+
self, writer: CodeWriter, context: RenderContext, return_type: str, data_expr: str
|
|
595
|
+
) -> None:
|
|
596
|
+
"""Write try/except logic for Union types."""
|
|
597
|
+
# Parse Union[TypeA, TypeB] to extract the types
|
|
598
|
+
if not return_type.startswith("Union[") or not return_type.endswith("]"):
|
|
599
|
+
raise ValueError(f"Invalid Union type format: {return_type}")
|
|
600
|
+
|
|
601
|
+
union_content = return_type[6:-1] # Remove 'Union[' and ']'
|
|
602
|
+
types = [t.strip() for t in union_content.split(",")]
|
|
603
|
+
|
|
604
|
+
if len(types) < 2:
|
|
605
|
+
raise ValueError(f"Union type must have at least 2 types: {return_type}")
|
|
606
|
+
|
|
607
|
+
# Add Union import
|
|
608
|
+
context.add_import("typing", "Union")
|
|
609
|
+
|
|
610
|
+
# Generate try/except blocks for each type
|
|
611
|
+
first_type = types[0]
|
|
612
|
+
remaining_types = types[1:]
|
|
613
|
+
|
|
614
|
+
# Try the first type
|
|
615
|
+
writer.write_line("try:")
|
|
616
|
+
writer.indent()
|
|
617
|
+
if self._should_use_cattrs_structure(first_type):
|
|
618
|
+
context.add_typing_imports_for_type(first_type)
|
|
619
|
+
deserialization_code = self._get_cattrs_deserialization_code(first_type, data_expr)
|
|
620
|
+
writer.write_line(f"return {deserialization_code}")
|
|
621
|
+
else:
|
|
622
|
+
context.add_import("typing", "cast")
|
|
623
|
+
writer.write_line(f"return cast({first_type}, {data_expr})")
|
|
624
|
+
writer.dedent()
|
|
625
|
+
|
|
626
|
+
# Add except blocks for remaining types
|
|
627
|
+
for i, type_name in enumerate(remaining_types):
|
|
628
|
+
is_last = i == len(remaining_types) - 1
|
|
629
|
+
if is_last:
|
|
630
|
+
writer.write_line("except Exception: # Attempt to parse as the final type")
|
|
631
|
+
else:
|
|
632
|
+
writer.write_line("except Exception: # Attempt to parse as the next type")
|
|
633
|
+
writer.indent()
|
|
634
|
+
if self._should_use_cattrs_structure(type_name):
|
|
635
|
+
context.add_typing_imports_for_type(type_name)
|
|
636
|
+
deserialization_code = self._get_cattrs_deserialization_code(type_name, data_expr)
|
|
637
|
+
if is_last:
|
|
638
|
+
writer.write_line(f"return {deserialization_code}")
|
|
639
|
+
else:
|
|
640
|
+
writer.write_line("try:")
|
|
641
|
+
writer.indent()
|
|
642
|
+
writer.write_line(f"return {deserialization_code}")
|
|
643
|
+
writer.dedent()
|
|
644
|
+
else:
|
|
645
|
+
context.add_import("typing", "cast")
|
|
646
|
+
writer.write_line(f"return cast({type_name}, {data_expr})")
|
|
647
|
+
writer.dedent()
|
|
648
|
+
|
|
649
|
+
def _write_content_type_conditional_handling(
|
|
650
|
+
self, writer: CodeWriter, context: RenderContext, strategy: ResponseStrategy
|
|
651
|
+
) -> None:
|
|
652
|
+
"""Write Content-Type header checking code for multi-content-type responses.
|
|
653
|
+
|
|
654
|
+
When a response has multiple content types, generate conditional code that checks
|
|
655
|
+
the Content-Type header and returns the appropriate type.
|
|
656
|
+
|
|
657
|
+
Args:
|
|
658
|
+
writer: Code writer for output
|
|
659
|
+
context: Render context for imports
|
|
660
|
+
strategy: Response strategy with content_type_mapping
|
|
661
|
+
"""
|
|
662
|
+
if not strategy.content_type_mapping:
|
|
663
|
+
raise ValueError("content_type_mapping is required for Content-Type conditional handling")
|
|
664
|
+
|
|
665
|
+
# Extract content type without parameters and normalize to lowercase for case-insensitive comparison
|
|
666
|
+
# (e.g., "Application/JSON; charset=utf-8" -> "application/json")
|
|
667
|
+
# RFC 7230: HTTP header field names are case-insensitive
|
|
668
|
+
writer.write_line('content_type = response.headers.get("content-type", "").split(";")[0].strip().lower()')
|
|
669
|
+
writer.write_line("")
|
|
670
|
+
|
|
671
|
+
# Generate if/elif/else chain for each content type
|
|
672
|
+
content_type_items = list(strategy.content_type_mapping.items())
|
|
673
|
+
|
|
674
|
+
for i, (content_type, python_type) in enumerate(content_type_items):
|
|
675
|
+
is_first = i == 0
|
|
676
|
+
is_last = i == len(content_type_items) - 1
|
|
677
|
+
|
|
678
|
+
# Write conditional statement with lowercase content-type (case-insensitive comparison)
|
|
679
|
+
content_type_lower = content_type.lower()
|
|
680
|
+
if is_first:
|
|
681
|
+
writer.write_line(f'if content_type == "{content_type_lower}":')
|
|
682
|
+
elif not is_last:
|
|
683
|
+
writer.write_line(f'elif content_type == "{content_type_lower}":')
|
|
684
|
+
else:
|
|
685
|
+
# Last item - use else for fallback
|
|
686
|
+
writer.write_line("else: # Default/fallback content type")
|
|
687
|
+
|
|
688
|
+
writer.indent()
|
|
689
|
+
|
|
690
|
+
# Generate return statement based on python_type
|
|
691
|
+
if python_type == "bytes":
|
|
692
|
+
writer.write_line("return response.content")
|
|
693
|
+
elif python_type == "str":
|
|
694
|
+
writer.write_line("return response.text")
|
|
695
|
+
elif self._should_use_cattrs_structure(python_type):
|
|
696
|
+
# Complex type - use cattrs deserialization
|
|
697
|
+
context.add_typing_imports_for_type(python_type)
|
|
698
|
+
deserialization_code = self._get_cattrs_deserialization_code(python_type, "response.json()")
|
|
699
|
+
writer.write_line(f"return {deserialization_code}")
|
|
700
|
+
else:
|
|
701
|
+
# Simple type - use cast
|
|
702
|
+
context.add_import("typing", "cast")
|
|
703
|
+
writer.write_line(f"return cast({python_type}, response.json())")
|
|
704
|
+
|
|
705
|
+
writer.dedent()
|