pyopenapi-gen 0.8.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. pyopenapi_gen/__init__.py +114 -0
  2. pyopenapi_gen/__main__.py +6 -0
  3. pyopenapi_gen/cli.py +86 -0
  4. pyopenapi_gen/context/file_manager.py +52 -0
  5. pyopenapi_gen/context/import_collector.py +382 -0
  6. pyopenapi_gen/context/render_context.py +630 -0
  7. pyopenapi_gen/core/__init__.py +0 -0
  8. pyopenapi_gen/core/auth/base.py +22 -0
  9. pyopenapi_gen/core/auth/plugins.py +89 -0
  10. pyopenapi_gen/core/exceptions.py +25 -0
  11. pyopenapi_gen/core/http_transport.py +219 -0
  12. pyopenapi_gen/core/loader/__init__.py +12 -0
  13. pyopenapi_gen/core/loader/loader.py +158 -0
  14. pyopenapi_gen/core/loader/operations/__init__.py +12 -0
  15. pyopenapi_gen/core/loader/operations/parser.py +155 -0
  16. pyopenapi_gen/core/loader/operations/post_processor.py +60 -0
  17. pyopenapi_gen/core/loader/operations/request_body.py +85 -0
  18. pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
  19. pyopenapi_gen/core/loader/parameters/parser.py +121 -0
  20. pyopenapi_gen/core/loader/responses/__init__.py +10 -0
  21. pyopenapi_gen/core/loader/responses/parser.py +104 -0
  22. pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
  23. pyopenapi_gen/core/loader/schemas/extractor.py +184 -0
  24. pyopenapi_gen/core/pagination.py +64 -0
  25. pyopenapi_gen/core/parsing/__init__.py +13 -0
  26. pyopenapi_gen/core/parsing/common/__init__.py +1 -0
  27. pyopenapi_gen/core/parsing/common/ref_resolution/__init__.py +9 -0
  28. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/__init__.py +0 -0
  29. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/cyclic_properties.py +66 -0
  30. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/direct_cycle.py +33 -0
  31. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/existing_schema.py +22 -0
  32. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +54 -0
  33. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +52 -0
  34. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +50 -0
  35. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +51 -0
  36. pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +86 -0
  37. pyopenapi_gen/core/parsing/common/type_parser.py +74 -0
  38. pyopenapi_gen/core/parsing/context.py +184 -0
  39. pyopenapi_gen/core/parsing/cycle_helpers.py +123 -0
  40. pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
  41. pyopenapi_gen/core/parsing/keywords/all_of_parser.py +77 -0
  42. pyopenapi_gen/core/parsing/keywords/any_of_parser.py +79 -0
  43. pyopenapi_gen/core/parsing/keywords/array_items_parser.py +69 -0
  44. pyopenapi_gen/core/parsing/keywords/one_of_parser.py +72 -0
  45. pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
  46. pyopenapi_gen/core/parsing/schema_finalizer.py +166 -0
  47. pyopenapi_gen/core/parsing/schema_parser.py +610 -0
  48. pyopenapi_gen/core/parsing/transformers/__init__.py +0 -0
  49. pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +285 -0
  50. pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +117 -0
  51. pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
  52. pyopenapi_gen/core/postprocess_manager.py +161 -0
  53. pyopenapi_gen/core/schemas.py +40 -0
  54. pyopenapi_gen/core/streaming_helpers.py +86 -0
  55. pyopenapi_gen/core/telemetry.py +67 -0
  56. pyopenapi_gen/core/utils.py +409 -0
  57. pyopenapi_gen/core/warning_collector.py +83 -0
  58. pyopenapi_gen/core/writers/code_writer.py +135 -0
  59. pyopenapi_gen/core/writers/documentation_writer.py +222 -0
  60. pyopenapi_gen/core/writers/line_writer.py +217 -0
  61. pyopenapi_gen/core/writers/python_construct_renderer.py +274 -0
  62. pyopenapi_gen/core_package_template/README.md +21 -0
  63. pyopenapi_gen/emit/models_emitter.py +143 -0
  64. pyopenapi_gen/emitters/client_emitter.py +51 -0
  65. pyopenapi_gen/emitters/core_emitter.py +181 -0
  66. pyopenapi_gen/emitters/docs_emitter.py +44 -0
  67. pyopenapi_gen/emitters/endpoints_emitter.py +223 -0
  68. pyopenapi_gen/emitters/exceptions_emitter.py +52 -0
  69. pyopenapi_gen/emitters/models_emitter.py +428 -0
  70. pyopenapi_gen/generator/client_generator.py +562 -0
  71. pyopenapi_gen/helpers/__init__.py +1 -0
  72. pyopenapi_gen/helpers/endpoint_utils.py +552 -0
  73. pyopenapi_gen/helpers/type_cleaner.py +341 -0
  74. pyopenapi_gen/helpers/type_helper.py +112 -0
  75. pyopenapi_gen/helpers/type_resolution/__init__.py +1 -0
  76. pyopenapi_gen/helpers/type_resolution/array_resolver.py +57 -0
  77. pyopenapi_gen/helpers/type_resolution/composition_resolver.py +79 -0
  78. pyopenapi_gen/helpers/type_resolution/finalizer.py +89 -0
  79. pyopenapi_gen/helpers/type_resolution/named_resolver.py +174 -0
  80. pyopenapi_gen/helpers/type_resolution/object_resolver.py +212 -0
  81. pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +57 -0
  82. pyopenapi_gen/helpers/type_resolution/resolver.py +48 -0
  83. pyopenapi_gen/helpers/url_utils.py +14 -0
  84. pyopenapi_gen/http_types.py +20 -0
  85. pyopenapi_gen/ir.py +167 -0
  86. pyopenapi_gen/py.typed +1 -0
  87. pyopenapi_gen/types/__init__.py +11 -0
  88. pyopenapi_gen/types/contracts/__init__.py +13 -0
  89. pyopenapi_gen/types/contracts/protocols.py +106 -0
  90. pyopenapi_gen/types/contracts/types.py +30 -0
  91. pyopenapi_gen/types/resolvers/__init__.py +7 -0
  92. pyopenapi_gen/types/resolvers/reference_resolver.py +71 -0
  93. pyopenapi_gen/types/resolvers/response_resolver.py +203 -0
  94. pyopenapi_gen/types/resolvers/schema_resolver.py +367 -0
  95. pyopenapi_gen/types/services/__init__.py +5 -0
  96. pyopenapi_gen/types/services/type_service.py +133 -0
  97. pyopenapi_gen/visit/client_visitor.py +228 -0
  98. pyopenapi_gen/visit/docs_visitor.py +38 -0
  99. pyopenapi_gen/visit/endpoint/__init__.py +1 -0
  100. pyopenapi_gen/visit/endpoint/endpoint_visitor.py +103 -0
  101. pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
  102. pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +121 -0
  103. pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +87 -0
  104. pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
  105. pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +497 -0
  106. pyopenapi_gen/visit/endpoint/generators/signature_generator.py +88 -0
  107. pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +183 -0
  108. pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
  109. pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +76 -0
  110. pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
  111. pyopenapi_gen/visit/exception_visitor.py +52 -0
  112. pyopenapi_gen/visit/model/__init__.py +0 -0
  113. pyopenapi_gen/visit/model/alias_generator.py +89 -0
  114. pyopenapi_gen/visit/model/dataclass_generator.py +197 -0
  115. pyopenapi_gen/visit/model/enum_generator.py +200 -0
  116. pyopenapi_gen/visit/model/model_visitor.py +197 -0
  117. pyopenapi_gen/visit/visitor.py +97 -0
  118. pyopenapi_gen-0.8.3.dist-info/METADATA +224 -0
  119. pyopenapi_gen-0.8.3.dist-info/RECORD +122 -0
  120. pyopenapi_gen-0.8.3.dist-info/WHEEL +4 -0
  121. pyopenapi_gen-0.8.3.dist-info/entry_points.txt +2 -0
  122. pyopenapi_gen-0.8.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,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