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,165 @@
1
+ """Unified type resolution service."""
2
+
3
+ import logging
4
+
5
+ from pyopenapi_gen import IROperation, IRResponse, IRSchema
6
+ from pyopenapi_gen.context.render_context import RenderContext
7
+
8
+ from ..contracts.types import ResolvedType
9
+ from ..resolvers import OpenAPIReferenceResolver, OpenAPIResponseResolver, OpenAPISchemaResolver
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class RenderContextAdapter:
15
+ """Adapter to make RenderContext compatible with TypeContext protocol."""
16
+
17
+ def __init__(self, render_context: RenderContext):
18
+ self.render_context = render_context
19
+
20
+ def add_import(self, module: str, name: str) -> None:
21
+ """Add an import to the context."""
22
+ self.render_context.add_import(module, name)
23
+
24
+ def add_conditional_import(self, condition: str, module: str, name: str) -> None:
25
+ """Add a conditional import (e.g., TYPE_CHECKING)."""
26
+ self.render_context.add_conditional_import(condition, module, name)
27
+
28
+
29
+ class UnifiedTypeService:
30
+ """
31
+ Unified service for all type resolution needs.
32
+
33
+ This is the main entry point for converting OpenAPI schemas, responses,
34
+ and operations to Python type strings.
35
+ """
36
+
37
+ def __init__(self, schemas: dict[str, IRSchema], responses: dict[str, IRResponse] | None = None):
38
+ """
39
+ Initialize the type service.
40
+
41
+ Args:
42
+ schemas: Dictionary of all schemas by name
43
+ responses: Dictionary of all responses by name (optional)
44
+ """
45
+ self.ref_resolver = OpenAPIReferenceResolver(schemas, responses)
46
+ self.schema_resolver = OpenAPISchemaResolver(self.ref_resolver)
47
+ self.response_resolver = OpenAPIResponseResolver(self.ref_resolver, self.schema_resolver)
48
+
49
+ def resolve_schema_type(
50
+ self, schema: IRSchema, context: RenderContext, required: bool = True, resolve_underlying: bool = False
51
+ ) -> str:
52
+ """
53
+ Resolve a schema to a Python type string.
54
+
55
+ Args:
56
+ schema: The schema to resolve
57
+ context: Render context for imports
58
+ required: Whether the field is required
59
+ resolve_underlying: If True, resolve underlying type for aliases instead of schema name
60
+
61
+ Returns:
62
+ Python type string
63
+ """
64
+ # Check if the schema itself is nullable
65
+ # If schema.is_nullable=True, it should be Optional regardless of required
66
+ effective_required = required and not getattr(schema, "is_nullable", False)
67
+
68
+ type_context = RenderContextAdapter(context)
69
+ resolved = self.schema_resolver.resolve_schema(schema, type_context, effective_required, resolve_underlying)
70
+
71
+ # DEBUG: Log resolved type before formatting
72
+ if resolved.python_type and "[" in resolved.python_type:
73
+ bracket_count_open = resolved.python_type.count("[")
74
+ bracket_count_close = resolved.python_type.count("]")
75
+ if bracket_count_open != bracket_count_close:
76
+ logger.warning(
77
+ f"BRACKET MISMATCH BEFORE FORMAT: '{resolved.python_type}', "
78
+ f"schema_name: {getattr(schema, 'name', 'anonymous')}, "
79
+ f"schema_type: {getattr(schema, 'type', None)}, "
80
+ f"is_optional: {resolved.is_optional}"
81
+ )
82
+
83
+ formatted = self._format_resolved_type(resolved, context)
84
+
85
+ # DEBUG: Log final formatted type
86
+ if formatted and "[" in formatted:
87
+ bracket_count_open = formatted.count("[")
88
+ bracket_count_close = formatted.count("]")
89
+ if bracket_count_open != bracket_count_close:
90
+ logger.warning(f"BRACKET MISMATCH AFTER FORMAT: '{formatted}', " f"original: '{resolved.python_type}'")
91
+
92
+ return formatted
93
+
94
+ def resolve_operation_response_type(self, operation: IROperation, context: RenderContext) -> str:
95
+ """
96
+ Resolve an operation's response to a Python type string.
97
+
98
+ Args:
99
+ operation: The operation to resolve
100
+ context: Render context for imports
101
+
102
+ Returns:
103
+ Python type string
104
+ """
105
+ type_context = RenderContextAdapter(context)
106
+ resolved = self.response_resolver.resolve_operation_response(operation, type_context)
107
+ return self._format_resolved_type(resolved, context)
108
+
109
+ def resolve_response_type(self, response: IRResponse, context: RenderContext) -> str:
110
+ """
111
+ Resolve a specific response to a Python type string.
112
+
113
+ Args:
114
+ response: The response to resolve
115
+ context: Render context for imports
116
+
117
+ Returns:
118
+ Python type string
119
+ """
120
+ type_context = RenderContextAdapter(context)
121
+ resolved = self.response_resolver.resolve_specific_response(response, type_context)
122
+ return self._format_resolved_type(resolved, context)
123
+
124
+ def _format_resolved_type(self, resolved: ResolvedType, context: RenderContext | None = None) -> str:
125
+ """Format a ResolvedType into a Python type string.
126
+
127
+ Architecture Guarantee: This method produces ONLY modern Python 3.10+ syntax (X | None).
128
+ Optional[X] is NEVER generated - unified type system uses | None exclusively.
129
+ """
130
+ python_type = resolved.python_type
131
+
132
+ # SANITY CHECK: Unified system should never produce Optional[X] internally
133
+ if python_type.startswith("Optional["):
134
+ logger.error(
135
+ f"❌ ARCHITECTURE VIOLATION: Resolver produced legacy Optional[X]: {python_type}. "
136
+ f"Unified type system must generate X | None directly. "
137
+ f"This indicates a bug in schema/response/reference resolver."
138
+ )
139
+ # This should never happen in our unified system
140
+ raise ValueError(
141
+ f"Type resolver produced legacy Optional[X] syntax: {python_type}. "
142
+ f"Unified type system must use X | None exclusively."
143
+ )
144
+
145
+ # Quote forward references BEFORE adding | None so we get: "DataSource" | None not "DataSource | None"
146
+ if resolved.is_forward_ref and not python_type.startswith('"'):
147
+ logger.debug(
148
+ f'Quoting forward ref: {python_type} -> "{python_type}" '
149
+ f"(is_forward_ref={resolved.is_forward_ref}, needs_import={resolved.needs_import})"
150
+ )
151
+ python_type = f'"{python_type}"'
152
+
153
+ # Add modern | None syntax if needed
154
+ # Modern Python 3.10+ uses | None syntax without needing Optional import
155
+ if resolved.is_optional and not python_type.endswith("| None"):
156
+ python_type = f"{python_type} | None"
157
+
158
+ # DEBUG: Check for malformed type strings
159
+ if python_type.count("[") != python_type.count("]"):
160
+ logger.warning(
161
+ f"MALFORMED TYPE: Bracket mismatch in '{python_type}'. "
162
+ f"Original: '{resolved.python_type}', is_optional: {resolved.is_optional}"
163
+ )
164
+
165
+ return python_type
@@ -0,0 +1,5 @@
1
+ """Response handling strategies."""
2
+
3
+ from .response_strategy import ResponseStrategy, ResponseStrategyResolver
4
+
5
+ __all__ = ["ResponseStrategy", "ResponseStrategyResolver"]
@@ -0,0 +1,310 @@
1
+ """Unified response handling strategy.
2
+
3
+ This module provides a single source of truth for how operation responses should be handled,
4
+ eliminating the scattered responsibility that was causing Data_ vs proper schema name issues.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from dataclasses import dataclass
11
+
12
+ from pyopenapi_gen import IROperation, IRResponse, IRSchema
13
+ from pyopenapi_gen.context.render_context import RenderContext
14
+ from pyopenapi_gen.types.services.type_service import UnifiedTypeService
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @dataclass
20
+ class ResponseStrategy:
21
+ """Unified strategy for handling a specific operation's response.
22
+
23
+ This class encapsulates all decisions about how to handle a response:
24
+ - What type to use in method signatures (matches OpenAPI schema exactly)
25
+ - Which schema to use for deserialization
26
+ - How to generate the response handling code
27
+ """
28
+
29
+ return_type: str # The Python type for method signature
30
+ response_schema: IRSchema | None # The response schema as defined in OpenAPI spec
31
+ is_streaming: bool # Whether this is a streaming response
32
+
33
+ # Additional context for code generation
34
+ response_ir: IRResponse | None # The original response IR
35
+
36
+ # Multi-content-type support
37
+ content_type_mapping: dict[str, str] | None = None # Maps content-type to Python type for Union responses
38
+
39
+
40
+ class ResponseStrategyResolver:
41
+ """Single source of truth for response handling decisions.
42
+
43
+ This resolver examines an operation and its responses to determine the optimal
44
+ strategy for handling the response. It replaces the scattered logic that was
45
+ previously spread across multiple components.
46
+ """
47
+
48
+ def __init__(self, schemas: dict[str, IRSchema]):
49
+ self.schemas = schemas
50
+ self.type_service = UnifiedTypeService(schemas)
51
+
52
+ def resolve(self, operation: IROperation, context: RenderContext) -> ResponseStrategy:
53
+ """Determine how to handle this operation's response.
54
+
55
+ Uses the response schema exactly as defined in the OpenAPI spec,
56
+ with no unwrapping logic. What you see in the spec is what you get.
57
+
58
+ Args:
59
+ operation: The operation to analyze
60
+ context: Render context for type resolution
61
+
62
+ Returns:
63
+ A ResponseStrategy that all components should use consistently
64
+ """
65
+ primary_response = self._get_primary_response(operation)
66
+
67
+ if not primary_response:
68
+ return ResponseStrategy(return_type="None", response_schema=None, is_streaming=False, response_ir=None)
69
+
70
+ # Handle responses without content (e.g., 204)
71
+ if not hasattr(primary_response, "content") or not primary_response.content:
72
+ return ResponseStrategy(
73
+ return_type="None", response_schema=None, is_streaming=False, response_ir=primary_response
74
+ )
75
+
76
+ # Handle streaming responses
77
+ if hasattr(primary_response, "stream") and primary_response.stream:
78
+ return self._resolve_streaming_strategy(primary_response, context)
79
+
80
+ # Check if response has multiple content types
81
+ content_types = list(primary_response.content.keys()) if primary_response.content else []
82
+ if len(content_types) > 1:
83
+ return self._resolve_multi_content_type_strategy(primary_response, context)
84
+
85
+ # Single content type - get the response schema
86
+ response_schema = self._get_response_schema(primary_response)
87
+
88
+ # If no schema provided, try to infer type from content-type
89
+ if not response_schema:
90
+ # Try to infer type from content-type (e.g., text/plain → str, application/pdf → bytes)
91
+ if content_types:
92
+ inferred_type = self._resolve_content_type_to_python_type(content_types[0], None, context)
93
+ if inferred_type:
94
+ return ResponseStrategy(
95
+ return_type=inferred_type,
96
+ response_schema=None,
97
+ is_streaming=False,
98
+ response_ir=primary_response,
99
+ )
100
+
101
+ # No schema and couldn't infer type
102
+ return ResponseStrategy(
103
+ return_type="None", response_schema=None, is_streaming=False, response_ir=primary_response
104
+ )
105
+
106
+ # Use the response schema as-is from the OpenAPI spec
107
+ return_type = self.type_service.resolve_schema_type(response_schema, context, required=True)
108
+
109
+ return ResponseStrategy(
110
+ return_type=return_type, response_schema=response_schema, is_streaming=False, response_ir=primary_response
111
+ )
112
+
113
+ def _get_primary_response(self, operation: IROperation) -> IRResponse | None:
114
+ """Get the primary success response from an operation."""
115
+ if not operation.responses:
116
+ return None
117
+
118
+ # Priority order: 200, 201, 202, 204, other 2xx, default
119
+ for code in ["200", "201", "202", "204"]:
120
+ for response in operation.responses:
121
+ if response.status_code == code:
122
+ return response
123
+
124
+ # Other 2xx responses
125
+ for response in operation.responses:
126
+ if response.status_code.startswith("2"):
127
+ return response
128
+
129
+ # Default response
130
+ for response in operation.responses:
131
+ if response.status_code == "default":
132
+ return response
133
+
134
+ # First response as fallback
135
+ return operation.responses[0] if operation.responses else None
136
+
137
+ def _get_response_schema(self, response: IRResponse) -> IRSchema | None:
138
+ """Get the schema from a response's content."""
139
+ if not response.content:
140
+ return None
141
+
142
+ # Prefer application/json
143
+ content_types = list(response.content.keys())
144
+ content_type = None
145
+
146
+ if "application/json" in content_types:
147
+ content_type = "application/json"
148
+ elif any("json" in ct for ct in content_types):
149
+ content_type = next(ct for ct in content_types if "json" in ct)
150
+ elif content_types:
151
+ content_type = content_types[0]
152
+
153
+ if not content_type:
154
+ return None
155
+
156
+ return response.content.get(content_type)
157
+
158
+ def _resolve_streaming_strategy(self, response: IRResponse, context: RenderContext) -> ResponseStrategy:
159
+ """Resolve strategy for streaming responses."""
160
+ # Add AsyncIterator import
161
+ context.add_import("typing", "AsyncIterator")
162
+
163
+ # Determine the item type for the stream
164
+ if not response.content:
165
+ # Binary stream with no specific content type
166
+ return ResponseStrategy(
167
+ return_type="AsyncIterator[bytes]", response_schema=None, is_streaming=True, response_ir=response
168
+ )
169
+
170
+ # Check for binary content types
171
+ content_types = list(response.content.keys())
172
+ is_binary = any(
173
+ ct in ["application/octet-stream", "application/pdf"] or ct.startswith(("image/", "audio/", "video/"))
174
+ for ct in content_types
175
+ )
176
+
177
+ if is_binary:
178
+ return ResponseStrategy(
179
+ return_type="AsyncIterator[bytes]", response_schema=None, is_streaming=True, response_ir=response
180
+ )
181
+
182
+ # For event streams (text/event-stream) or JSON streams
183
+ is_event_stream = any("event-stream" in ct for ct in content_types)
184
+ if is_event_stream:
185
+ context.add_import("typing", "Dict")
186
+ context.add_import("typing", "Any")
187
+ return ResponseStrategy(
188
+ return_type="AsyncIterator[dict[str, Any]]",
189
+ response_schema=None,
190
+ is_streaming=True,
191
+ response_ir=response,
192
+ )
193
+
194
+ # For other streaming content, try to resolve the schema
195
+ schema = self._get_response_schema(response)
196
+ if schema:
197
+ schema_type = self.type_service.resolve_schema_type(schema, context, required=True)
198
+ return ResponseStrategy(
199
+ return_type=f"AsyncIterator[{schema_type}]",
200
+ response_schema=schema,
201
+ is_streaming=True,
202
+ response_ir=response,
203
+ )
204
+
205
+ # Default to bytes if we can't determine the type
206
+ return ResponseStrategy(
207
+ return_type="AsyncIterator[bytes]", response_schema=None, is_streaming=True, response_ir=response
208
+ )
209
+
210
+ def _resolve_multi_content_type_strategy(self, response: IRResponse, context: RenderContext) -> ResponseStrategy:
211
+ """Resolve strategy for responses with multiple content types.
212
+
213
+ When a response defines multiple content types, generate a Union return type
214
+ and store the mapping for content-type-based response handling.
215
+
216
+ Args:
217
+ response: The response with multiple content types
218
+ context: Render context for type resolution
219
+
220
+ Returns:
221
+ ResponseStrategy with Union return type and content_type_mapping
222
+ """
223
+ if not response.content:
224
+ return ResponseStrategy(return_type="None", response_schema=None, is_streaming=False, response_ir=response)
225
+
226
+ content_types = list(response.content.keys())
227
+ logger.info(f"Detected response with multiple content types: {content_types}")
228
+
229
+ # Resolve each content type to its Python type
230
+ content_type_mapping: dict[str, str] = {}
231
+ resolved_types: list[str] = []
232
+
233
+ for content_type in content_types:
234
+ python_type = self._resolve_content_type_to_python_type(
235
+ content_type, response.content[content_type], context
236
+ )
237
+
238
+ if python_type:
239
+ content_type_mapping[content_type] = python_type
240
+ if python_type not in resolved_types: # Avoid duplicates in Union
241
+ resolved_types.append(python_type)
242
+
243
+ # If no types were resolved, default to None
244
+ if not resolved_types:
245
+ return ResponseStrategy(return_type="None", response_schema=None, is_streaming=False, response_ir=response)
246
+
247
+ # If only one unique type, no need for Union
248
+ if len(resolved_types) == 1:
249
+ return ResponseStrategy(
250
+ return_type=resolved_types[0],
251
+ response_schema=response.content.get(content_types[0]),
252
+ is_streaming=False,
253
+ response_ir=response,
254
+ content_type_mapping=content_type_mapping,
255
+ )
256
+
257
+ # Create Union type
258
+ context.add_import("typing", "Union")
259
+ union_type = f"Union[{', '.join(resolved_types)}]"
260
+
261
+ logger.info(f"Generated Union return type: {union_type}")
262
+
263
+ return ResponseStrategy(
264
+ return_type=union_type,
265
+ response_schema=None, # No single schema for Union
266
+ is_streaming=False,
267
+ response_ir=response,
268
+ content_type_mapping=content_type_mapping,
269
+ )
270
+
271
+ def _resolve_content_type_to_python_type(
272
+ self, content_type: str, schema: IRSchema | None, context: RenderContext
273
+ ) -> str | None:
274
+ """Resolve a content type and its schema to a Python type.
275
+
276
+ Args:
277
+ content_type: The HTTP content type (e.g., 'application/json')
278
+ schema: The schema for this content type (may be None)
279
+ context: Render context for type resolution
280
+
281
+ Returns:
282
+ Python type string or None if cannot be resolved
283
+ """
284
+ # Handle binary content types
285
+ if content_type in ["application/octet-stream", "application/pdf"] or content_type.startswith(
286
+ ("image/", "audio/", "video/")
287
+ ):
288
+ return "bytes"
289
+
290
+ # Handle text content types
291
+ if content_type.startswith("text/"):
292
+ # text/html, text/plain, etc. can be either str or bytes depending on the schema
293
+ if schema and hasattr(schema, "type"):
294
+ if schema.type == "string" and hasattr(schema, "format") and schema.format == "binary":
295
+ return "bytes"
296
+ return "str"
297
+ # Text content without schema should be str by default (text is naturally string-based)
298
+ return "str"
299
+
300
+ # Handle JSON content types
301
+ if "json" in content_type and schema:
302
+ return self.type_service.resolve_schema_type(schema, context, required=True)
303
+
304
+ # Handle other content types with schema
305
+ if schema:
306
+ return self.type_service.resolve_schema_type(schema, context, required=True)
307
+
308
+ # Unknown content type
309
+ logger.warning(f"Unknown content type '{content_type}', defaulting to bytes")
310
+ return "bytes"