pyopenapi-gen 2.7.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyopenapi_gen/__init__.py +224 -0
- pyopenapi_gen/__main__.py +6 -0
- pyopenapi_gen/cli.py +62 -0
- pyopenapi_gen/context/CLAUDE.md +284 -0
- pyopenapi_gen/context/file_manager.py +52 -0
- pyopenapi_gen/context/import_collector.py +382 -0
- pyopenapi_gen/context/render_context.py +726 -0
- pyopenapi_gen/core/CLAUDE.md +224 -0
- pyopenapi_gen/core/__init__.py +0 -0
- pyopenapi_gen/core/auth/base.py +22 -0
- pyopenapi_gen/core/auth/plugins.py +89 -0
- pyopenapi_gen/core/cattrs_converter.py +810 -0
- pyopenapi_gen/core/exceptions.py +20 -0
- pyopenapi_gen/core/http_status_codes.py +218 -0
- pyopenapi_gen/core/http_transport.py +222 -0
- pyopenapi_gen/core/loader/__init__.py +12 -0
- pyopenapi_gen/core/loader/loader.py +174 -0
- pyopenapi_gen/core/loader/operations/__init__.py +12 -0
- pyopenapi_gen/core/loader/operations/parser.py +161 -0
- pyopenapi_gen/core/loader/operations/post_processor.py +62 -0
- pyopenapi_gen/core/loader/operations/request_body.py +90 -0
- pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
- pyopenapi_gen/core/loader/parameters/parser.py +186 -0
- pyopenapi_gen/core/loader/responses/__init__.py +10 -0
- pyopenapi_gen/core/loader/responses/parser.py +111 -0
- pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
- pyopenapi_gen/core/loader/schemas/extractor.py +275 -0
- pyopenapi_gen/core/pagination.py +64 -0
- pyopenapi_gen/core/parsing/__init__.py +13 -0
- pyopenapi_gen/core/parsing/common/__init__.py +1 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/__init__.py +9 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/__init__.py +0 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/cyclic_properties.py +66 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/direct_cycle.py +33 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/existing_schema.py +22 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +54 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +52 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +50 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +51 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +86 -0
- pyopenapi_gen/core/parsing/common/type_parser.py +73 -0
- pyopenapi_gen/core/parsing/context.py +187 -0
- pyopenapi_gen/core/parsing/cycle_helpers.py +126 -0
- pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
- pyopenapi_gen/core/parsing/keywords/all_of_parser.py +81 -0
- pyopenapi_gen/core/parsing/keywords/any_of_parser.py +84 -0
- pyopenapi_gen/core/parsing/keywords/array_items_parser.py +72 -0
- pyopenapi_gen/core/parsing/keywords/one_of_parser.py +77 -0
- pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
- pyopenapi_gen/core/parsing/schema_finalizer.py +169 -0
- pyopenapi_gen/core/parsing/schema_parser.py +804 -0
- pyopenapi_gen/core/parsing/transformers/__init__.py +0 -0
- pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +285 -0
- pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +120 -0
- pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
- pyopenapi_gen/core/postprocess_manager.py +260 -0
- pyopenapi_gen/core/spec_fetcher.py +148 -0
- pyopenapi_gen/core/streaming_helpers.py +84 -0
- pyopenapi_gen/core/telemetry.py +69 -0
- pyopenapi_gen/core/utils.py +456 -0
- pyopenapi_gen/core/warning_collector.py +83 -0
- pyopenapi_gen/core/writers/code_writer.py +135 -0
- pyopenapi_gen/core/writers/documentation_writer.py +222 -0
- pyopenapi_gen/core/writers/line_writer.py +217 -0
- pyopenapi_gen/core/writers/python_construct_renderer.py +321 -0
- pyopenapi_gen/core_package_template/README.md +21 -0
- pyopenapi_gen/emit/models_emitter.py +143 -0
- pyopenapi_gen/emitters/CLAUDE.md +286 -0
- pyopenapi_gen/emitters/client_emitter.py +51 -0
- pyopenapi_gen/emitters/core_emitter.py +181 -0
- pyopenapi_gen/emitters/docs_emitter.py +44 -0
- pyopenapi_gen/emitters/endpoints_emitter.py +247 -0
- pyopenapi_gen/emitters/exceptions_emitter.py +187 -0
- pyopenapi_gen/emitters/mocks_emitter.py +185 -0
- pyopenapi_gen/emitters/models_emitter.py +426 -0
- pyopenapi_gen/generator/CLAUDE.md +352 -0
- pyopenapi_gen/generator/client_generator.py +567 -0
- pyopenapi_gen/generator/exceptions.py +7 -0
- pyopenapi_gen/helpers/CLAUDE.md +325 -0
- pyopenapi_gen/helpers/__init__.py +1 -0
- pyopenapi_gen/helpers/endpoint_utils.py +532 -0
- pyopenapi_gen/helpers/type_cleaner.py +334 -0
- pyopenapi_gen/helpers/type_helper.py +112 -0
- pyopenapi_gen/helpers/type_resolution/__init__.py +1 -0
- pyopenapi_gen/helpers/type_resolution/array_resolver.py +57 -0
- pyopenapi_gen/helpers/type_resolution/composition_resolver.py +79 -0
- pyopenapi_gen/helpers/type_resolution/finalizer.py +105 -0
- pyopenapi_gen/helpers/type_resolution/named_resolver.py +172 -0
- pyopenapi_gen/helpers/type_resolution/object_resolver.py +216 -0
- pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +109 -0
- pyopenapi_gen/helpers/type_resolution/resolver.py +47 -0
- pyopenapi_gen/helpers/url_utils.py +14 -0
- pyopenapi_gen/http_types.py +20 -0
- pyopenapi_gen/ir.py +165 -0
- pyopenapi_gen/py.typed +1 -0
- pyopenapi_gen/types/CLAUDE.md +140 -0
- pyopenapi_gen/types/__init__.py +11 -0
- pyopenapi_gen/types/contracts/__init__.py +13 -0
- pyopenapi_gen/types/contracts/protocols.py +106 -0
- pyopenapi_gen/types/contracts/types.py +28 -0
- pyopenapi_gen/types/resolvers/__init__.py +7 -0
- pyopenapi_gen/types/resolvers/reference_resolver.py +71 -0
- pyopenapi_gen/types/resolvers/response_resolver.py +177 -0
- pyopenapi_gen/types/resolvers/schema_resolver.py +498 -0
- pyopenapi_gen/types/services/__init__.py +5 -0
- pyopenapi_gen/types/services/type_service.py +165 -0
- pyopenapi_gen/types/strategies/__init__.py +5 -0
- pyopenapi_gen/types/strategies/response_strategy.py +310 -0
- pyopenapi_gen/visit/CLAUDE.md +272 -0
- pyopenapi_gen/visit/client_visitor.py +477 -0
- pyopenapi_gen/visit/docs_visitor.py +38 -0
- pyopenapi_gen/visit/endpoint/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/endpoint_visitor.py +292 -0
- pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +123 -0
- pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +222 -0
- pyopenapi_gen/visit/endpoint/generators/mock_generator.py +140 -0
- pyopenapi_gen/visit/endpoint/generators/overload_generator.py +252 -0
- pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
- pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +705 -0
- pyopenapi_gen/visit/endpoint/generators/signature_generator.py +83 -0
- pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +207 -0
- pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +78 -0
- pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
- pyopenapi_gen/visit/exception_visitor.py +90 -0
- pyopenapi_gen/visit/model/__init__.py +0 -0
- pyopenapi_gen/visit/model/alias_generator.py +93 -0
- pyopenapi_gen/visit/model/dataclass_generator.py +553 -0
- pyopenapi_gen/visit/model/enum_generator.py +212 -0
- pyopenapi_gen/visit/model/model_visitor.py +198 -0
- pyopenapi_gen/visit/visitor.py +97 -0
- pyopenapi_gen-2.7.2.dist-info/METADATA +1169 -0
- pyopenapi_gen-2.7.2.dist-info/RECORD +137 -0
- pyopenapi_gen-2.7.2.dist-info/WHEEL +4 -0
- pyopenapi_gen-2.7.2.dist-info/entry_points.txt +2 -0
- pyopenapi_gen-2.7.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,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,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"
|