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