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,552 @@
|
|
1
|
+
"""
|
2
|
+
Helpers for endpoint code generation: parameter/type analysis, code writing, etc.
|
3
|
+
Used by EndpointVisitor and related emitters.
|
4
|
+
"""
|
5
|
+
|
6
|
+
import logging
|
7
|
+
import re
|
8
|
+
from typing import Any, Dict, List, Optional
|
9
|
+
|
10
|
+
from pyopenapi_gen import IROperation, IRParameter, IRRequestBody, IRResponse, IRSchema
|
11
|
+
from pyopenapi_gen.context.render_context import RenderContext
|
12
|
+
from pyopenapi_gen.http_types import HTTPMethod
|
13
|
+
|
14
|
+
from ..core.utils import NameSanitizer
|
15
|
+
from ..types.services.type_service import UnifiedTypeService
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
def get_params(op: IROperation, context: RenderContext, schemas: Dict[str, IRSchema]) -> List[Dict[str, Any]]:
|
21
|
+
"""
|
22
|
+
Returns a list of dicts with name, type, default, and required for template rendering.
|
23
|
+
Requires the full schema dictionary for type resolution.
|
24
|
+
"""
|
25
|
+
params = []
|
26
|
+
for param in op.parameters:
|
27
|
+
py_type = get_param_type(param, context, schemas)
|
28
|
+
default = None if param.required else "None"
|
29
|
+
params.append(
|
30
|
+
{
|
31
|
+
"name": NameSanitizer.sanitize_method_name(param.name),
|
32
|
+
"type": py_type,
|
33
|
+
"default": default,
|
34
|
+
"required": param.required,
|
35
|
+
}
|
36
|
+
)
|
37
|
+
return params
|
38
|
+
|
39
|
+
|
40
|
+
def get_param_type(param: IRParameter, context: RenderContext, schemas: Dict[str, IRSchema]) -> str:
|
41
|
+
"""Returns the Python type hint for a parameter, resolving references using the schemas dict."""
|
42
|
+
# Use unified service for type resolution
|
43
|
+
type_service = UnifiedTypeService(schemas)
|
44
|
+
py_type = type_service.resolve_schema_type(param.schema, context, required=param.required)
|
45
|
+
|
46
|
+
# Adjust model import path for endpoints (expecting models.<module>)
|
47
|
+
if py_type.startswith(".") and not py_type.startswith(".."): # Simple relative import
|
48
|
+
py_type = "models" + py_type
|
49
|
+
|
50
|
+
# Special handling for file uploads in multipart/form-data
|
51
|
+
if (
|
52
|
+
getattr(param, "in_", None) == "formData"
|
53
|
+
and getattr(param.schema, "type", None) == "string"
|
54
|
+
and getattr(param.schema, "format", None) == "binary"
|
55
|
+
):
|
56
|
+
context.add_import("typing", "IO")
|
57
|
+
context.add_import("typing", "Any")
|
58
|
+
return "IO[Any]"
|
59
|
+
return py_type
|
60
|
+
|
61
|
+
|
62
|
+
def get_request_body_type(body: IRRequestBody, context: RenderContext, schemas: Dict[str, IRSchema]) -> str:
|
63
|
+
"""Returns the Python type hint for a request body, resolving references using the schemas dict."""
|
64
|
+
# Prefer application/json schema if available
|
65
|
+
json_schema = body.content.get("application/json")
|
66
|
+
if json_schema:
|
67
|
+
# Use unified service for type resolution
|
68
|
+
type_service = UnifiedTypeService(schemas)
|
69
|
+
py_type = type_service.resolve_schema_type(json_schema, context, required=body.required)
|
70
|
+
if py_type.startswith(".") and not py_type.startswith(".."):
|
71
|
+
py_type = "models" + py_type
|
72
|
+
|
73
|
+
# If the resolved type is 'Any' for a JSON body, default to Dict[str, Any]
|
74
|
+
if py_type == "Any":
|
75
|
+
context.add_import("typing", "Dict")
|
76
|
+
# context.add_import("typing", "Any") # Already added by the fallback or TypeHelper
|
77
|
+
return "Dict[str, Any]"
|
78
|
+
return py_type
|
79
|
+
# Fallback for other content types (e.g., octet-stream)
|
80
|
+
# TODO: Handle other types more specifically if needed
|
81
|
+
context.add_import("typing", "Any")
|
82
|
+
return "Any"
|
83
|
+
|
84
|
+
|
85
|
+
def get_return_type_unified(
|
86
|
+
op: IROperation,
|
87
|
+
context: RenderContext,
|
88
|
+
schemas: Dict[str, IRSchema],
|
89
|
+
responses: Optional[Dict[str, IRResponse]] = None,
|
90
|
+
) -> str:
|
91
|
+
"""
|
92
|
+
Determines the primary return type hint for an operation using the unified type service.
|
93
|
+
|
94
|
+
Args:
|
95
|
+
op: The operation to resolve
|
96
|
+
context: Render context for imports
|
97
|
+
schemas: Dictionary of all schemas
|
98
|
+
responses: Dictionary of all responses (optional)
|
99
|
+
|
100
|
+
Returns:
|
101
|
+
Python type string
|
102
|
+
"""
|
103
|
+
type_service = UnifiedTypeService(schemas, responses)
|
104
|
+
return type_service.resolve_operation_response_type(op, context)
|
105
|
+
|
106
|
+
|
107
|
+
def get_return_type(
|
108
|
+
op: IROperation,
|
109
|
+
context: RenderContext,
|
110
|
+
schemas: Dict[str, IRSchema],
|
111
|
+
) -> tuple[str | None, bool]:
|
112
|
+
"""
|
113
|
+
DEPRECATED: Use get_return_type_unified instead.
|
114
|
+
|
115
|
+
Now delegates to the unified service for consistent type resolution.
|
116
|
+
Falls back to path-based inference for backward compatibility.
|
117
|
+
|
118
|
+
Returns:
|
119
|
+
A tuple: (Python type hint string, boolean indicating if unwrapping occurred).
|
120
|
+
"""
|
121
|
+
# Delegate to the unified service for all type resolution
|
122
|
+
type_service = UnifiedTypeService(schemas)
|
123
|
+
py_type: str | None = type_service.resolve_operation_response_type(op, context)
|
124
|
+
|
125
|
+
# Backward compatibility: If unified service returns None or "None", try inference
|
126
|
+
if py_type is None or py_type == "None":
|
127
|
+
if op.method == HTTPMethod.GET:
|
128
|
+
# For GET operations, try path-based inference
|
129
|
+
inferred_schema = _infer_type_from_path(op.path, schemas)
|
130
|
+
if inferred_schema and inferred_schema.name:
|
131
|
+
py_type = inferred_schema.name
|
132
|
+
elif op.method == HTTPMethod.PUT and op.request_body:
|
133
|
+
# For PUT operations, try request body inference
|
134
|
+
if op.request_body.content:
|
135
|
+
# Get the first content type (usually application/json)
|
136
|
+
first_content_type = next(iter(op.request_body.content.keys()))
|
137
|
+
request_schema = op.request_body.content[first_content_type]
|
138
|
+
if request_schema and request_schema.name:
|
139
|
+
# First try to find corresponding resource schema (e.g., TenantUpdate -> Tenant)
|
140
|
+
resource_schema = _find_resource_schema(request_schema.name, schemas)
|
141
|
+
if resource_schema and resource_schema.name:
|
142
|
+
py_type = resource_schema.name
|
143
|
+
else:
|
144
|
+
# Fall back to request body type
|
145
|
+
py_type = request_schema.name
|
146
|
+
|
147
|
+
# Convert "None" string back to None for backward compatibility only for GET operations
|
148
|
+
# For other operations, preserve "None" string as expected by tests
|
149
|
+
if py_type == "None" and op.method == HTTPMethod.GET:
|
150
|
+
py_type = None
|
151
|
+
|
152
|
+
# For backward compatibility, return a tuple with unwrapping flag set to False
|
153
|
+
# The unified service handles unwrapping internally
|
154
|
+
return (py_type, False)
|
155
|
+
|
156
|
+
|
157
|
+
def _get_primary_response(op: IROperation) -> Optional[IRResponse]:
|
158
|
+
"""Helper to find the best primary success response."""
|
159
|
+
resp = None
|
160
|
+
# Prioritize 200, 201, 202, 204
|
161
|
+
for code in ["200", "201", "202", "204"]:
|
162
|
+
resp = next((r for r in op.responses if r.status_code == code), None)
|
163
|
+
if resp:
|
164
|
+
return resp
|
165
|
+
# Then other 2xx
|
166
|
+
for r in op.responses:
|
167
|
+
if r.status_code.startswith("2"):
|
168
|
+
return r
|
169
|
+
# Then default
|
170
|
+
resp = next((r for r in op.responses if r.status_code == "default"), None)
|
171
|
+
if resp:
|
172
|
+
return resp
|
173
|
+
# Finally, the first listed response if any
|
174
|
+
if op.responses:
|
175
|
+
return op.responses[0]
|
176
|
+
return None
|
177
|
+
|
178
|
+
|
179
|
+
def _get_response_schema_and_content_type(resp: IRResponse) -> tuple[Optional[IRSchema], Optional[str]]:
|
180
|
+
"""Helper to get the schema and content type from a response."""
|
181
|
+
if not resp.content:
|
182
|
+
return None, None
|
183
|
+
|
184
|
+
content_types = list(resp.content.keys())
|
185
|
+
# Prefer application/json, then event-stream, then any other json, then first
|
186
|
+
mt = next((ct for ct in content_types if ct == "application/json"), None)
|
187
|
+
if not mt:
|
188
|
+
mt = next((ct for ct in content_types if "event-stream" in ct), None)
|
189
|
+
if not mt:
|
190
|
+
mt = next((ct for ct in content_types if "json" in ct), None) # Catch application/vnd.api+json etc.
|
191
|
+
if not mt and content_types:
|
192
|
+
mt = content_types[0]
|
193
|
+
|
194
|
+
if not mt:
|
195
|
+
return None, None
|
196
|
+
|
197
|
+
return resp.content.get(mt), mt
|
198
|
+
|
199
|
+
|
200
|
+
def _find_resource_schema(update_schema_name: str, schemas: Dict[str, IRSchema]) -> Optional[IRSchema]:
|
201
|
+
"""
|
202
|
+
Given an update schema name (e.g. 'TenantUpdate'), try to find the corresponding
|
203
|
+
resource schema (e.g. 'Tenant') in the schemas dictionary.
|
204
|
+
|
205
|
+
Args:
|
206
|
+
update_schema_name: Name of the update schema (typically ends with 'Update')
|
207
|
+
schemas: Dictionary of all available schemas
|
208
|
+
|
209
|
+
Returns:
|
210
|
+
Resource schema if found, None otherwise
|
211
|
+
"""
|
212
|
+
if not update_schema_name or not update_schema_name.endswith("Update"):
|
213
|
+
return None
|
214
|
+
|
215
|
+
# Extract base resource name (e.g., "TenantUpdate" -> "Tenant")
|
216
|
+
base_name = update_schema_name[:-6] # Remove "Update" suffix
|
217
|
+
|
218
|
+
# Look for a matching resource schema in the schemas dictionary
|
219
|
+
for schema_name, schema in schemas.items():
|
220
|
+
if schema_name == base_name or getattr(schema, "name", "") == base_name:
|
221
|
+
return schema
|
222
|
+
|
223
|
+
return None
|
224
|
+
|
225
|
+
|
226
|
+
def _infer_type_from_path(path: str, schemas: Dict[str, IRSchema]) -> Optional[IRSchema]:
|
227
|
+
"""
|
228
|
+
Infers a response type from a path. This is used when a response schema is not specified.
|
229
|
+
|
230
|
+
Example:
|
231
|
+
- '/tenants/{tenant_id}/feedback' might infer a 'Feedback' or 'FeedbackResponse' type
|
232
|
+
- '/users/{user_id}/settings' might infer a 'Settings' or 'SettingsResponse' type
|
233
|
+
|
234
|
+
Args:
|
235
|
+
path: The endpoint path
|
236
|
+
schemas: Dictionary of all available schemas
|
237
|
+
|
238
|
+
Returns:
|
239
|
+
A schema that might be appropriate for the response, or None if no match found
|
240
|
+
"""
|
241
|
+
# Extract resource name from the end of the path
|
242
|
+
path_parts = path.rstrip("/").split("/")
|
243
|
+
resource_name = path_parts[-1] # Get the last part of the path
|
244
|
+
|
245
|
+
# Check if the last part is an ID parameter (like {id} or {user_id})
|
246
|
+
if resource_name.startswith("{") and resource_name.endswith("}"):
|
247
|
+
# If the path ends with an ID, use the second-to-last part
|
248
|
+
if len(path_parts) >= 2:
|
249
|
+
resource_name = path_parts[-2]
|
250
|
+
|
251
|
+
# Clean up and singularize the resource name
|
252
|
+
# Remove any special characters and convert to camel case
|
253
|
+
clean_name = "".join(word.title() for word in re.findall(r"[a-zA-Z0-9]+", resource_name))
|
254
|
+
|
255
|
+
# If the resource name is plural (most common API convention), make it singular
|
256
|
+
if clean_name.endswith("s") and len(clean_name) > 2:
|
257
|
+
singular_name = clean_name[:-1]
|
258
|
+
else:
|
259
|
+
singular_name = clean_name
|
260
|
+
|
261
|
+
# Common suffix patterns for response types
|
262
|
+
candidates = [
|
263
|
+
f"{singular_name}",
|
264
|
+
f"{singular_name}Response",
|
265
|
+
f"{singular_name}ListResponse" if path.endswith("/") or not resource_name.startswith("{") else None,
|
266
|
+
f"{singular_name}Item",
|
267
|
+
f"{singular_name}Data",
|
268
|
+
f"{clean_name}",
|
269
|
+
f"{clean_name}Response",
|
270
|
+
f"{clean_name}ListResponse" if path.endswith("/") or not resource_name.startswith("{") else None,
|
271
|
+
f"{clean_name}Item",
|
272
|
+
f"{clean_name}Data",
|
273
|
+
]
|
274
|
+
|
275
|
+
# Filter out None values
|
276
|
+
candidates = [c for c in candidates if c]
|
277
|
+
|
278
|
+
# Look for matching schemas
|
279
|
+
for candidate in candidates:
|
280
|
+
for schema_name, schema in schemas.items():
|
281
|
+
if schema_name == candidate or getattr(schema, "name", "") == candidate:
|
282
|
+
return schema
|
283
|
+
|
284
|
+
# Try a more generic approach if no specific match found
|
285
|
+
for schema_name, schema in schemas.items():
|
286
|
+
schema_name_lower = schema_name.lower()
|
287
|
+
resource_name_lower = resource_name.lower()
|
288
|
+
|
289
|
+
if resource_name_lower in schema_name_lower and (
|
290
|
+
"response" in schema_name_lower or "result" in schema_name_lower or "dto" in schema_name_lower
|
291
|
+
):
|
292
|
+
return schema
|
293
|
+
|
294
|
+
# Heuristic: if the path ends with a pluralized version of a known schema, infer list of that schema
|
295
|
+
# Example: /users and a User schema -> List[User]
|
296
|
+
if singular_name in schemas:
|
297
|
+
# Create a temporary array schema for type resolution
|
298
|
+
return IRSchema(type="array", items=schemas[singular_name])
|
299
|
+
|
300
|
+
# Heuristic: if the path ends with a singular version of a known schema, infer that schema
|
301
|
+
# Example: /user/{id} or /users/{id} and a User schema -> User
|
302
|
+
# Covers /resource_name/{param} or /plural_resource_name/{param}
|
303
|
+
# More robustly, check if any part of the path matches a schema name (singular or plural)
|
304
|
+
# and if it has a path parameter immediately following it.
|
305
|
+
path_parts = [part for part in path.split("/") if part]
|
306
|
+
for i, part in enumerate(path_parts):
|
307
|
+
# potential_schema_name_singular = NameSanitizer.pascal_case(NameSanitizer.singularize(part))
|
308
|
+
# HACK: Use a simplified version until NameSanitizer.pascal_case and singularize are available
|
309
|
+
# This is a placeholder and might not be correct for all cases.
|
310
|
+
potential_schema_name_singular = part.capitalize() # Simplified placeholder
|
311
|
+
if potential_schema_name_singular in schemas and (
|
312
|
+
i + 1 < len(path_parts) and path_parts[i + 1].startswith("{")
|
313
|
+
):
|
314
|
+
return schemas[potential_schema_name_singular]
|
315
|
+
|
316
|
+
return None
|
317
|
+
|
318
|
+
|
319
|
+
def format_method_args(params: list[dict[str, Any]]) -> str:
|
320
|
+
"""
|
321
|
+
Format a list of parameter dicts into a Python function argument string (excluding
|
322
|
+
'self'). Each dict must have: name, type, default (None or string), required (bool).
|
323
|
+
Required params come first, then optional params with defaults.
|
324
|
+
"""
|
325
|
+
required = [p for p in params if p.get("required", True)]
|
326
|
+
optional = [p for p in params if not p.get("required", True)]
|
327
|
+
arg_strs = []
|
328
|
+
for p in required:
|
329
|
+
arg_strs.append(f"{p['name']}: {p['type']}")
|
330
|
+
for p in optional:
|
331
|
+
default = p["default"]
|
332
|
+
arg_strs.append(f"{p['name']}: {p['type']} = {default}")
|
333
|
+
return ", ".join(arg_strs)
|
334
|
+
|
335
|
+
|
336
|
+
def get_model_stub_args(schema: IRSchema, context: RenderContext, present_args: set[str]) -> str:
|
337
|
+
"""
|
338
|
+
Generate a string of arguments for instantiating a model.
|
339
|
+
For each required field, use the variable if present in present_args, otherwise use
|
340
|
+
a safe default. For optional fields, use None.
|
341
|
+
"""
|
342
|
+
if not hasattr(schema, "properties") or not schema.properties:
|
343
|
+
return ""
|
344
|
+
args = []
|
345
|
+
for prop, pschema in schema.properties.items():
|
346
|
+
is_required = prop in getattr(schema, "required", [])
|
347
|
+
py_type = pschema.type if hasattr(pschema, "type") else None
|
348
|
+
if prop in present_args:
|
349
|
+
args.append(f"{prop}={prop}")
|
350
|
+
elif is_required:
|
351
|
+
# Safe defaults for required fields
|
352
|
+
if py_type == "string":
|
353
|
+
args.append(f'{prop}=""')
|
354
|
+
elif py_type == "integer":
|
355
|
+
args.append(f"{prop}=0")
|
356
|
+
elif py_type == "number":
|
357
|
+
args.append(f"{prop}=0.0")
|
358
|
+
elif py_type == "boolean":
|
359
|
+
args.append(f"{prop}=False")
|
360
|
+
else:
|
361
|
+
args.append(f"{prop}=...")
|
362
|
+
else:
|
363
|
+
args.append(f"{prop}=None")
|
364
|
+
return ", ".join(args)
|
365
|
+
|
366
|
+
|
367
|
+
def merge_params_with_model_fields(
|
368
|
+
op: IROperation,
|
369
|
+
model_schema: IRSchema,
|
370
|
+
context: RenderContext,
|
371
|
+
schemas: Dict[str, IRSchema],
|
372
|
+
) -> List[Dict[str, Any]]:
|
373
|
+
"""
|
374
|
+
Merge endpoint parameters with required model fields for function signatures.
|
375
|
+
- Ensures all required model fields are present as parameters (without duplication).
|
376
|
+
- Endpoint parameters take precedence if names overlap.
|
377
|
+
- Returns a list of dicts: {name, type, default, required}.
|
378
|
+
|
379
|
+
Args:
|
380
|
+
op: The IROperation (endpoint operation).
|
381
|
+
model_schema: The IRSchema for the model (request body or return type).
|
382
|
+
context: The RenderContext for imports/type resolution.
|
383
|
+
schemas: Dictionary of all named schemas.
|
384
|
+
|
385
|
+
Returns:
|
386
|
+
List of parameter dicts suitable for use in endpoint method signatures.
|
387
|
+
"""
|
388
|
+
# Get endpoint parameters (already sanitized)
|
389
|
+
endpoint_params = get_params(op, context, schemas)
|
390
|
+
endpoint_param_names = {p["name"] for p in endpoint_params}
|
391
|
+
merged_params = list(endpoint_params)
|
392
|
+
|
393
|
+
# Add required model fields not already present
|
394
|
+
if hasattr(model_schema, "properties") and model_schema.properties:
|
395
|
+
for prop, pschema in model_schema.properties.items():
|
396
|
+
is_required = prop in getattr(model_schema, "required", [])
|
397
|
+
if not is_required:
|
398
|
+
continue # Only add required fields
|
399
|
+
sanitized_name = NameSanitizer.sanitize_method_name(prop)
|
400
|
+
if sanitized_name in endpoint_param_names:
|
401
|
+
continue # Already present as endpoint param
|
402
|
+
|
403
|
+
# Use the unified service to get the type
|
404
|
+
type_service = UnifiedTypeService(schemas)
|
405
|
+
py_type = type_service.resolve_schema_type(pschema, context, required=True)
|
406
|
+
if py_type.startswith(".") and not py_type.startswith(".."):
|
407
|
+
py_type = "models" + py_type
|
408
|
+
|
409
|
+
merged_params.append(
|
410
|
+
{
|
411
|
+
"name": sanitized_name,
|
412
|
+
"type": py_type,
|
413
|
+
"default": None,
|
414
|
+
"required": True,
|
415
|
+
}
|
416
|
+
)
|
417
|
+
return merged_params
|
418
|
+
|
419
|
+
|
420
|
+
def get_type_for_specific_response(
|
421
|
+
operation_path: str,
|
422
|
+
resp_ir: IRResponse,
|
423
|
+
all_schemas: dict[str, IRSchema],
|
424
|
+
ctx: RenderContext,
|
425
|
+
return_unwrap_data_property: bool = False,
|
426
|
+
) -> str:
|
427
|
+
"""Get the Python type for a specific response."""
|
428
|
+
# If the response content is empty or None, return None
|
429
|
+
if not hasattr(resp_ir, "content") or not resp_ir.content:
|
430
|
+
return "None"
|
431
|
+
|
432
|
+
# Unwrap data property if needed
|
433
|
+
final_py_type = get_python_type_for_response_body(resp_ir, all_schemas, ctx)
|
434
|
+
|
435
|
+
if return_unwrap_data_property:
|
436
|
+
wrapper_schema = get_schema_from_response(resp_ir, all_schemas)
|
437
|
+
if wrapper_schema and hasattr(wrapper_schema, "properties") and wrapper_schema.properties.get("data"):
|
438
|
+
# We have a data property we can unwrap
|
439
|
+
data_schema = wrapper_schema.properties["data"]
|
440
|
+
|
441
|
+
wrapper_type_str = final_py_type
|
442
|
+
|
443
|
+
# Handle array unwrapping
|
444
|
+
if hasattr(data_schema, "type") and data_schema.type == "array" and hasattr(data_schema, "items"):
|
445
|
+
# Need to unwrap array in data property
|
446
|
+
|
447
|
+
# Extract the item type from the array items
|
448
|
+
parent_schema_name = (
|
449
|
+
getattr(wrapper_schema, "name", "") + "Data" if hasattr(wrapper_schema, "name") else ""
|
450
|
+
)
|
451
|
+
|
452
|
+
# Make sure items is not None before passing to type service
|
453
|
+
if data_schema.items:
|
454
|
+
type_service = UnifiedTypeService(all_schemas)
|
455
|
+
item_type = type_service.resolve_schema_type(data_schema.items, ctx, required=True)
|
456
|
+
else:
|
457
|
+
ctx.add_import("typing", "Any")
|
458
|
+
item_type = "Any"
|
459
|
+
|
460
|
+
# Handle problematic array item types
|
461
|
+
if not item_type or item_type == "Any":
|
462
|
+
# Try to get a better type from the item schema directly
|
463
|
+
parent_schema_name = (
|
464
|
+
getattr(wrapper_schema, "name", "") + "DataItem" if hasattr(wrapper_schema, "name") else ""
|
465
|
+
)
|
466
|
+
|
467
|
+
# Make sure items is not None before passing to type service
|
468
|
+
if data_schema.items:
|
469
|
+
type_service = UnifiedTypeService(all_schemas)
|
470
|
+
item_type = type_service.resolve_schema_type(data_schema.items, ctx, required=True)
|
471
|
+
else:
|
472
|
+
ctx.add_import("typing", "Any")
|
473
|
+
item_type = "Any"
|
474
|
+
|
475
|
+
# Build the final List type
|
476
|
+
ctx.add_import("typing", "List")
|
477
|
+
final_type = f"List[{item_type}]"
|
478
|
+
return final_type
|
479
|
+
|
480
|
+
else:
|
481
|
+
# Unwrap non-array data property
|
482
|
+
parent_schema_name = (
|
483
|
+
getattr(wrapper_schema, "name", "") + "Data" if hasattr(wrapper_schema, "name") else ""
|
484
|
+
)
|
485
|
+
type_service = UnifiedTypeService(all_schemas)
|
486
|
+
return type_service.resolve_schema_type(data_schema, ctx, required=True)
|
487
|
+
|
488
|
+
# For AsyncIterator/streaming handlers, add appropriate type annotation
|
489
|
+
if getattr(resp_ir, "is_stream", False):
|
490
|
+
if _is_binary_stream_content(resp_ir):
|
491
|
+
# Binary stream returns bytes chunks
|
492
|
+
ctx.add_import("typing", "AsyncIterator")
|
493
|
+
return "AsyncIterator[bytes]"
|
494
|
+
|
495
|
+
# If it's not a binary stream, try to determine the item type for the event stream
|
496
|
+
item_type_opt = _get_item_type_from_schema(resp_ir, all_schemas, ctx)
|
497
|
+
if item_type_opt:
|
498
|
+
ctx.add_import("typing", "AsyncIterator")
|
499
|
+
return f"AsyncIterator[{item_type_opt}]"
|
500
|
+
|
501
|
+
# Default for unknown event streams
|
502
|
+
ctx.add_import("typing", "AsyncIterator")
|
503
|
+
ctx.add_import("typing", "Dict")
|
504
|
+
ctx.add_import("typing", "Any")
|
505
|
+
return "AsyncIterator[Dict[str, Any]]"
|
506
|
+
|
507
|
+
return final_py_type
|
508
|
+
|
509
|
+
|
510
|
+
def _is_binary_stream_content(resp_ir: IRResponse) -> bool:
|
511
|
+
"""Check if the response is a binary stream based on content type."""
|
512
|
+
if not hasattr(resp_ir, "content") or not resp_ir.content:
|
513
|
+
return False
|
514
|
+
|
515
|
+
content_types = resp_ir.content.keys()
|
516
|
+
return any(
|
517
|
+
ct in ("application/octet-stream", "application/pdf", "image/", "audio/", "video/")
|
518
|
+
or ct.startswith("image/")
|
519
|
+
or ct.startswith("audio/")
|
520
|
+
or ct.startswith("video/")
|
521
|
+
for ct in content_types
|
522
|
+
)
|
523
|
+
|
524
|
+
|
525
|
+
def _get_item_type_from_schema(
|
526
|
+
resp_ir: IRResponse, all_schemas: dict[str, IRSchema], ctx: RenderContext
|
527
|
+
) -> Optional[str]:
|
528
|
+
"""Extract item type from schema for streaming responses."""
|
529
|
+
schema, _ = _get_response_schema_and_content_type(resp_ir)
|
530
|
+
if not schema:
|
531
|
+
return None
|
532
|
+
|
533
|
+
# For event streams, we want the schema type
|
534
|
+
type_service = UnifiedTypeService(all_schemas)
|
535
|
+
return type_service.resolve_schema_type(schema, ctx, required=True)
|
536
|
+
|
537
|
+
|
538
|
+
def get_python_type_for_response_body(resp_ir: IRResponse, all_schemas: dict[str, IRSchema], ctx: RenderContext) -> str:
|
539
|
+
"""Get the Python type for a response body without unwrapping."""
|
540
|
+
schema, _ = _get_response_schema_and_content_type(resp_ir)
|
541
|
+
if not schema:
|
542
|
+
ctx.add_import("typing", "Any")
|
543
|
+
return "Any"
|
544
|
+
|
545
|
+
type_service = UnifiedTypeService(all_schemas)
|
546
|
+
return type_service.resolve_schema_type(schema, ctx, required=True)
|
547
|
+
|
548
|
+
|
549
|
+
def get_schema_from_response(resp_ir: IRResponse, all_schemas: dict[str, IRSchema]) -> Optional[IRSchema]:
|
550
|
+
"""Get the schema from a response object."""
|
551
|
+
schema, _ = _get_response_schema_and_content_type(resp_ir)
|
552
|
+
return schema
|