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,177 @@
|
|
|
1
|
+
"""Response type resolver implementation."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
from pyopenapi_gen import IROperation, IRResponse, IRSchema
|
|
10
|
+
|
|
11
|
+
from ..contracts.protocols import ReferenceResolver, ResponseTypeResolver, SchemaTypeResolver, TypeContext
|
|
12
|
+
from ..contracts.types import ResolvedType
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class OpenAPIResponseResolver(ResponseTypeResolver):
|
|
18
|
+
"""Resolves operation responses to Python types."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, ref_resolver: ReferenceResolver, schema_resolver: SchemaTypeResolver):
|
|
21
|
+
"""
|
|
22
|
+
Initialize response resolver.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
ref_resolver: Reference resolver for handling $ref
|
|
26
|
+
schema_resolver: Schema resolver for handling schemas
|
|
27
|
+
"""
|
|
28
|
+
self.ref_resolver = ref_resolver
|
|
29
|
+
self.schema_resolver = schema_resolver
|
|
30
|
+
|
|
31
|
+
def resolve_operation_response(self, operation: IROperation, context: TypeContext) -> ResolvedType:
|
|
32
|
+
"""
|
|
33
|
+
Resolve an operation's primary response to a Python type.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
operation: The operation to resolve
|
|
37
|
+
context: Type resolution context
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Resolved Python type information
|
|
41
|
+
"""
|
|
42
|
+
primary_response = self._get_primary_response(operation)
|
|
43
|
+
|
|
44
|
+
if not primary_response:
|
|
45
|
+
return ResolvedType(python_type="None")
|
|
46
|
+
|
|
47
|
+
return self.resolve_specific_response(primary_response, context)
|
|
48
|
+
|
|
49
|
+
def resolve_specific_response(self, response: IRResponse, context: TypeContext) -> ResolvedType:
|
|
50
|
+
"""
|
|
51
|
+
Resolve a specific response to a Python type.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
response: The response to resolve
|
|
55
|
+
context: Type resolution context
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Resolved Python type information
|
|
59
|
+
"""
|
|
60
|
+
# Handle response references
|
|
61
|
+
if hasattr(response, "ref") and response.ref:
|
|
62
|
+
return self._resolve_response_reference(response.ref, context)
|
|
63
|
+
|
|
64
|
+
# Handle streaming responses (check before content validation)
|
|
65
|
+
if hasattr(response, "stream") and response.stream:
|
|
66
|
+
return self._resolve_streaming_response(response, context)
|
|
67
|
+
|
|
68
|
+
# Handle responses without content (e.g., 204)
|
|
69
|
+
if not hasattr(response, "content") or not response.content:
|
|
70
|
+
return ResolvedType(python_type="None")
|
|
71
|
+
|
|
72
|
+
# Get the content schema
|
|
73
|
+
schema = self._get_response_schema(response)
|
|
74
|
+
if not schema:
|
|
75
|
+
return ResolvedType(python_type="None")
|
|
76
|
+
|
|
77
|
+
# Resolve the schema directly (no unwrapping)
|
|
78
|
+
resolved = self.schema_resolver.resolve_schema(schema, context, required=True)
|
|
79
|
+
return resolved
|
|
80
|
+
|
|
81
|
+
def _resolve_response_reference(self, ref: str, context: TypeContext) -> ResolvedType:
|
|
82
|
+
"""Resolve a response $ref."""
|
|
83
|
+
target_response = self.ref_resolver.resolve_response_ref(ref)
|
|
84
|
+
if not target_response:
|
|
85
|
+
logger.warning(f"Could not resolve response reference: {ref}")
|
|
86
|
+
return ResolvedType(python_type="None")
|
|
87
|
+
|
|
88
|
+
return self.resolve_specific_response(target_response, context)
|
|
89
|
+
|
|
90
|
+
def _get_primary_response(self, operation: IROperation) -> IRResponse | None:
|
|
91
|
+
"""Get the primary success response from an operation."""
|
|
92
|
+
if not operation.responses:
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
# Priority order: 200, 201, 202, 204, other 2xx, default
|
|
96
|
+
for code in ["200", "201", "202", "204"]:
|
|
97
|
+
for response in operation.responses:
|
|
98
|
+
if response.status_code == code:
|
|
99
|
+
return response
|
|
100
|
+
|
|
101
|
+
# Other 2xx responses
|
|
102
|
+
for response in operation.responses:
|
|
103
|
+
if response.status_code.startswith("2"):
|
|
104
|
+
return response
|
|
105
|
+
|
|
106
|
+
# Default response
|
|
107
|
+
for response in operation.responses:
|
|
108
|
+
if response.status_code == "default":
|
|
109
|
+
return response
|
|
110
|
+
|
|
111
|
+
# First response as fallback
|
|
112
|
+
return operation.responses[0] if operation.responses else None
|
|
113
|
+
|
|
114
|
+
def _get_response_schema(self, response: IRResponse) -> IRSchema | None:
|
|
115
|
+
"""Get the schema from a response's content."""
|
|
116
|
+
if not response.content:
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
# Prefer application/json
|
|
120
|
+
content_types = list(response.content.keys())
|
|
121
|
+
content_type = None
|
|
122
|
+
|
|
123
|
+
if "application/json" in content_types:
|
|
124
|
+
content_type = "application/json"
|
|
125
|
+
elif any("json" in ct for ct in content_types):
|
|
126
|
+
content_type = next(ct for ct in content_types if "json" in ct)
|
|
127
|
+
elif content_types:
|
|
128
|
+
content_type = content_types[0]
|
|
129
|
+
|
|
130
|
+
if not content_type:
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
return response.content.get(content_type)
|
|
134
|
+
|
|
135
|
+
def _resolve_streaming_response(self, response: IRResponse, context: TypeContext) -> ResolvedType:
|
|
136
|
+
"""
|
|
137
|
+
Resolve a streaming response to an AsyncIterator type.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
response: The streaming response
|
|
141
|
+
context: Type resolution context
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
ResolvedType with AsyncIterator type
|
|
145
|
+
"""
|
|
146
|
+
# Add AsyncIterator import
|
|
147
|
+
context.add_import("typing", "AsyncIterator")
|
|
148
|
+
|
|
149
|
+
# Determine the item type for the stream
|
|
150
|
+
if not response.content:
|
|
151
|
+
# Binary stream with no specific content type
|
|
152
|
+
return ResolvedType(python_type="AsyncIterator[bytes]")
|
|
153
|
+
|
|
154
|
+
# Check for binary content types
|
|
155
|
+
content_types = list(response.content.keys())
|
|
156
|
+
is_binary = any(
|
|
157
|
+
ct in ["application/octet-stream", "application/pdf"] or ct.startswith(("image/", "audio/", "video/"))
|
|
158
|
+
for ct in content_types
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if is_binary:
|
|
162
|
+
return ResolvedType(python_type="AsyncIterator[bytes]")
|
|
163
|
+
|
|
164
|
+
# For event streams (text/event-stream) or JSON streams
|
|
165
|
+
is_event_stream = any("event-stream" in ct for ct in content_types)
|
|
166
|
+
if is_event_stream:
|
|
167
|
+
context.add_import("typing", "Any")
|
|
168
|
+
return ResolvedType(python_type="AsyncIterator[dict[str, Any]]")
|
|
169
|
+
|
|
170
|
+
# For other streaming content, try to resolve the schema
|
|
171
|
+
schema = self._get_response_schema(response)
|
|
172
|
+
if schema:
|
|
173
|
+
resolved = self.schema_resolver.resolve_schema(schema, context, required=True)
|
|
174
|
+
return ResolvedType(python_type=f"AsyncIterator[{resolved.python_type}]")
|
|
175
|
+
|
|
176
|
+
# Default to bytes if we can't determine the type
|
|
177
|
+
return ResolvedType(python_type="AsyncIterator[bytes]")
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
"""Schema type resolver implementation."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from pyopenapi_gen import IRSchema
|
|
6
|
+
|
|
7
|
+
from ..contracts.protocols import ReferenceResolver, SchemaTypeResolver, TypeContext
|
|
8
|
+
from ..contracts.types import ResolvedType, TypeResolutionError
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OpenAPISchemaResolver(SchemaTypeResolver):
|
|
14
|
+
"""Resolves IRSchema objects to Python types."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, ref_resolver: ReferenceResolver):
|
|
17
|
+
"""
|
|
18
|
+
Initialize schema resolver.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
ref_resolver: Reference resolver for handling $ref
|
|
22
|
+
"""
|
|
23
|
+
self.ref_resolver = ref_resolver
|
|
24
|
+
|
|
25
|
+
def resolve_schema(
|
|
26
|
+
self, schema: IRSchema, context: TypeContext, required: bool = True, resolve_underlying: bool = False
|
|
27
|
+
) -> ResolvedType:
|
|
28
|
+
"""
|
|
29
|
+
Resolve a schema to a Python type.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
schema: The schema to resolve
|
|
33
|
+
context: Type resolution context
|
|
34
|
+
required: Whether the field is required
|
|
35
|
+
resolve_underlying: If True, resolve underlying type for aliases instead of schema name
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Resolved Python type information
|
|
39
|
+
"""
|
|
40
|
+
if not schema:
|
|
41
|
+
return self._resolve_any(context)
|
|
42
|
+
|
|
43
|
+
# Handle references
|
|
44
|
+
if hasattr(schema, "ref") and schema.ref:
|
|
45
|
+
return self._resolve_reference(schema.ref, context, required, resolve_underlying)
|
|
46
|
+
|
|
47
|
+
# Handle null schemas (schemas with no type and no generation_name) - resolve to Any inline
|
|
48
|
+
# These are schemas from null schema nodes in the OpenAPI spec
|
|
49
|
+
# IMPORTANT: Don't treat schemas with names that exist in the registry as null schemas
|
|
50
|
+
schema_type = getattr(schema, "type", None)
|
|
51
|
+
if (
|
|
52
|
+
schema_type is None
|
|
53
|
+
and not (hasattr(schema, "generation_name") and schema.generation_name)
|
|
54
|
+
and not (hasattr(schema, "any_of") and schema.any_of)
|
|
55
|
+
and not (hasattr(schema, "one_of") and schema.one_of)
|
|
56
|
+
and not (hasattr(schema, "all_of") and schema.all_of)
|
|
57
|
+
and not (schema.name and schema.name in getattr(self.ref_resolver, "schemas", {}))
|
|
58
|
+
):
|
|
59
|
+
# This is a null schema - respect the required parameter for optionality
|
|
60
|
+
return self._resolve_null(context, required)
|
|
61
|
+
|
|
62
|
+
# Handle named schemas with generation_name (fully processed schemas)
|
|
63
|
+
# Skip for boolean enums which should be resolved inline as Literal types
|
|
64
|
+
schema_type = getattr(schema, "type", None)
|
|
65
|
+
is_boolean_enum = schema_type == "boolean" and hasattr(schema, "enum") and schema.enum
|
|
66
|
+
if schema.name and hasattr(schema, "generation_name") and schema.generation_name and not is_boolean_enum:
|
|
67
|
+
# If resolve_underlying is True, skip named schema resolution for type aliases
|
|
68
|
+
# and resolve the underlying primitive type instead
|
|
69
|
+
if not resolve_underlying:
|
|
70
|
+
return self._resolve_named_schema(schema, context, required)
|
|
71
|
+
|
|
72
|
+
# Handle composition types (any_of, all_of, one_of)
|
|
73
|
+
# These are processed for inline compositions or when generating the alias for a named composition
|
|
74
|
+
# Check for the attribute existence, not just truthiness, to handle empty lists
|
|
75
|
+
if hasattr(schema, "any_of") and schema.any_of is not None:
|
|
76
|
+
return self._resolve_any_of(schema, context, required, resolve_underlying)
|
|
77
|
+
elif hasattr(schema, "all_of") and schema.all_of is not None:
|
|
78
|
+
return self._resolve_all_of(schema, context, required, resolve_underlying)
|
|
79
|
+
elif hasattr(schema, "one_of") and schema.one_of is not None:
|
|
80
|
+
return self._resolve_one_of(schema, context, required, resolve_underlying)
|
|
81
|
+
|
|
82
|
+
# Handle named schemas without generation_name (fallback for references)
|
|
83
|
+
if schema.name and schema.name in self.ref_resolver.schemas:
|
|
84
|
+
target_schema = self.ref_resolver.schemas[schema.name]
|
|
85
|
+
# Avoid infinite recursion if it's the same object
|
|
86
|
+
if target_schema is not schema:
|
|
87
|
+
return self.resolve_schema(target_schema, context, required, resolve_underlying)
|
|
88
|
+
|
|
89
|
+
# Handle by type (schema_type was already extracted above for boolean enum check)
|
|
90
|
+
# Check if schema.type refers to a named schema (backward compatibility)
|
|
91
|
+
if (
|
|
92
|
+
schema_type
|
|
93
|
+
and hasattr(self.ref_resolver, "schemas")
|
|
94
|
+
and hasattr(self.ref_resolver.schemas, "__contains__")
|
|
95
|
+
and schema_type in self.ref_resolver.schemas
|
|
96
|
+
):
|
|
97
|
+
target_schema = self.ref_resolver.schemas[schema_type]
|
|
98
|
+
if target_schema and hasattr(target_schema, "name") and target_schema.name: # It's a named schema reference
|
|
99
|
+
return self.resolve_schema(target_schema, context, required, resolve_underlying)
|
|
100
|
+
|
|
101
|
+
if schema_type == "string":
|
|
102
|
+
return self._resolve_string(schema, context, required)
|
|
103
|
+
elif schema_type == "integer":
|
|
104
|
+
return self._resolve_integer(context, required)
|
|
105
|
+
elif schema_type == "number":
|
|
106
|
+
return self._resolve_number(context, required)
|
|
107
|
+
elif schema_type == "boolean":
|
|
108
|
+
return self._resolve_boolean(schema, context, required)
|
|
109
|
+
elif schema_type == "array":
|
|
110
|
+
return self._resolve_array(schema, context, required, resolve_underlying)
|
|
111
|
+
elif schema_type == "object":
|
|
112
|
+
return self._resolve_object(schema, context, required)
|
|
113
|
+
elif schema_type == "null":
|
|
114
|
+
return self._resolve_null(context, required)
|
|
115
|
+
else:
|
|
116
|
+
# Gather detailed information about the problematic schema
|
|
117
|
+
schema_details = {
|
|
118
|
+
"type": schema_type,
|
|
119
|
+
"name": getattr(schema, "name", None),
|
|
120
|
+
"ref": getattr(schema, "ref", None),
|
|
121
|
+
"properties": list(getattr(schema, "properties", {}).keys()) if hasattr(schema, "properties") else None,
|
|
122
|
+
"enum": getattr(schema, "enum", None),
|
|
123
|
+
"description": getattr(schema, "description", None),
|
|
124
|
+
"generation_name": getattr(schema, "generation_name", None),
|
|
125
|
+
"is_nullable": getattr(schema, "is_nullable", None),
|
|
126
|
+
"any_of": len(getattr(schema, "any_of", []) or []) if hasattr(schema, "any_of") else 0,
|
|
127
|
+
"all_of": len(getattr(schema, "all_of", []) or []) if hasattr(schema, "all_of") else 0,
|
|
128
|
+
"one_of": len(getattr(schema, "one_of", []) or []) if hasattr(schema, "one_of") else 0,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Remove None values for cleaner output
|
|
132
|
+
schema_details = {k: v for k, v in schema_details.items() if v is not None}
|
|
133
|
+
|
|
134
|
+
# Create detailed error message
|
|
135
|
+
error_msg = f"Unknown schema type '{schema_type}' encountered."
|
|
136
|
+
if schema_details.get("name"):
|
|
137
|
+
error_msg += f" Schema name: '{schema_details['name']}'."
|
|
138
|
+
if schema_details.get("ref"):
|
|
139
|
+
error_msg += f" Reference: '{schema_details['ref']}'."
|
|
140
|
+
|
|
141
|
+
# Log full details with actionable advice
|
|
142
|
+
logger.warning(f"{error_msg} Full details: {schema_details}")
|
|
143
|
+
|
|
144
|
+
# Provide specific guidance based on the unknown type
|
|
145
|
+
if schema_type == "Any":
|
|
146
|
+
logger.info(
|
|
147
|
+
"Schema type 'Any' will be mapped to typing.Any. Consider using a more specific type in your OpenAPI spec."
|
|
148
|
+
)
|
|
149
|
+
elif schema_type == "None" or schema_type is None:
|
|
150
|
+
logger.info(
|
|
151
|
+
"Schema type 'None' detected - likely an optional field or null type. This will be mapped to Any | None."
|
|
152
|
+
)
|
|
153
|
+
return self._resolve_null(context, required)
|
|
154
|
+
elif schema_type and isinstance(schema_type, str):
|
|
155
|
+
# Unknown string type - provide helpful suggestions
|
|
156
|
+
logger.info(f"Unknown type '{schema_type}' - common issues:")
|
|
157
|
+
logger.info(" 1. Typo in type name (should be: string, integer, number, boolean, array, object)")
|
|
158
|
+
logger.info(" 2. Using a schema name as type (should use $ref instead)")
|
|
159
|
+
logger.info(" 3. Custom type not supported by OpenAPI (consider using allOf/oneOf/anyOf)")
|
|
160
|
+
logger.info(f" Location: Check your OpenAPI spec for schemas with type='{schema_type}'")
|
|
161
|
+
|
|
162
|
+
return self._resolve_any(context, required)
|
|
163
|
+
|
|
164
|
+
def _resolve_reference(
|
|
165
|
+
self, ref: str, context: TypeContext, required: bool, resolve_underlying: bool = False
|
|
166
|
+
) -> ResolvedType:
|
|
167
|
+
"""Resolve a $ref to its target schema."""
|
|
168
|
+
target_schema = self.ref_resolver.resolve_ref(ref)
|
|
169
|
+
if not target_schema:
|
|
170
|
+
raise TypeResolutionError(f"Could not resolve reference: {ref}")
|
|
171
|
+
|
|
172
|
+
return self.resolve_schema(target_schema, context, required, resolve_underlying)
|
|
173
|
+
|
|
174
|
+
def _resolve_named_schema(self, schema: IRSchema, context: TypeContext, required: bool) -> ResolvedType:
|
|
175
|
+
"""Resolve a named schema (model/enum)."""
|
|
176
|
+
class_name = schema.generation_name
|
|
177
|
+
module_stem = getattr(schema, "final_module_stem", None)
|
|
178
|
+
|
|
179
|
+
if not module_stem:
|
|
180
|
+
logger.warning(f"Named schema '{schema.name}' missing final_module_stem attribute.")
|
|
181
|
+
logger.info(f" This usually means the schema wasn't properly processed during parsing.")
|
|
182
|
+
logger.info(
|
|
183
|
+
f" Check if '{schema.name}' is defined in components/schemas or if it's an inline schema that should be promoted."
|
|
184
|
+
)
|
|
185
|
+
logger.info(f" The schema will be treated as 'Any' type for now.")
|
|
186
|
+
return ResolvedType(python_type=class_name or "Any", is_optional=not required)
|
|
187
|
+
|
|
188
|
+
# Check if we're trying to import from the same module (self-import)
|
|
189
|
+
# This only applies when using a RenderContextAdapter with a real RenderContext
|
|
190
|
+
current_file = None
|
|
191
|
+
try:
|
|
192
|
+
# Only check for self-import if we have a RenderContextAdapter with real RenderContext
|
|
193
|
+
if (
|
|
194
|
+
hasattr(context, "render_context")
|
|
195
|
+
and hasattr(context.render_context, "current_file")
|
|
196
|
+
and
|
|
197
|
+
# Ensure this is not a mock by checking if it has the expected methods
|
|
198
|
+
hasattr(context.render_context, "add_import")
|
|
199
|
+
):
|
|
200
|
+
current_file = context.render_context.current_file
|
|
201
|
+
except (AttributeError, TypeError):
|
|
202
|
+
# If anything goes wrong with attribute access, assume not a self-reference
|
|
203
|
+
current_file = None
|
|
204
|
+
|
|
205
|
+
# Only treat as self-reference if the EXACT filename matches (not just a suffix)
|
|
206
|
+
# Bug fix: Use basename comparison to avoid false positives with similar names
|
|
207
|
+
# e.g., "vector_index_with_embedding_response_data.py" should NOT match "embedding_response_data.py"
|
|
208
|
+
is_self_import = False
|
|
209
|
+
if current_file and isinstance(current_file, str):
|
|
210
|
+
import os
|
|
211
|
+
|
|
212
|
+
current_filename = os.path.basename(current_file)
|
|
213
|
+
expected_filename = f"{module_stem}.py"
|
|
214
|
+
is_self_import = current_filename == expected_filename
|
|
215
|
+
|
|
216
|
+
if is_self_import:
|
|
217
|
+
# This is a self-import (importing from the same file), so skip the import
|
|
218
|
+
# and mark as forward reference to require quoting
|
|
219
|
+
logger.debug(
|
|
220
|
+
f"Self-import detected: current_file={current_file}, module_stem={module_stem}, "
|
|
221
|
+
f"class_name={class_name}, returning forward ref WITHOUT import"
|
|
222
|
+
)
|
|
223
|
+
return ResolvedType(python_type=class_name or "Any", is_optional=not required, is_forward_ref=True)
|
|
224
|
+
|
|
225
|
+
# Use the render context's relative path calculation to determine proper import path
|
|
226
|
+
import_module = f"..models.{module_stem}" # Default fallback
|
|
227
|
+
|
|
228
|
+
# If we have access to RenderContext, use its proper relative path calculation
|
|
229
|
+
if hasattr(context, "render_context") and hasattr(
|
|
230
|
+
context.render_context, "calculate_relative_path_for_internal_module"
|
|
231
|
+
):
|
|
232
|
+
try:
|
|
233
|
+
# Calculate relative path from current location to the target model module
|
|
234
|
+
target_module_relative = f"models.{module_stem}"
|
|
235
|
+
relative_path = context.render_context.calculate_relative_path_for_internal_module(
|
|
236
|
+
target_module_relative
|
|
237
|
+
)
|
|
238
|
+
if relative_path:
|
|
239
|
+
import_module = relative_path
|
|
240
|
+
except Exception as e:
|
|
241
|
+
# If relative path calculation fails, fall back to the default
|
|
242
|
+
logger.debug(f"Failed to calculate relative path for {module_stem}, using default: {e}")
|
|
243
|
+
|
|
244
|
+
logger.debug(
|
|
245
|
+
f"Adding import for named schema: module={import_module}, name={class_name}, "
|
|
246
|
+
f"current_file={current_file}, module_stem={module_stem}"
|
|
247
|
+
)
|
|
248
|
+
context.add_import(import_module, class_name or "Any")
|
|
249
|
+
|
|
250
|
+
return ResolvedType(
|
|
251
|
+
python_type=class_name or "Any",
|
|
252
|
+
needs_import=True,
|
|
253
|
+
import_module=import_module,
|
|
254
|
+
import_name=class_name or "Any",
|
|
255
|
+
is_optional=not required,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
def _resolve_string(self, schema: IRSchema, context: TypeContext, required: bool) -> ResolvedType:
|
|
259
|
+
"""Resolve string type, handling enums and formats."""
|
|
260
|
+
# Check if this is a properly processed enum (has generation_name)
|
|
261
|
+
if hasattr(schema, "enum") and schema.enum:
|
|
262
|
+
# Check if this enum was properly processed (has generation_name)
|
|
263
|
+
if hasattr(schema, "generation_name") and schema.generation_name:
|
|
264
|
+
# This is a properly processed enum, it should have been handled earlier
|
|
265
|
+
# by _resolve_named_schema. If we're here, it might be during initial processing.
|
|
266
|
+
# Return the enum type name
|
|
267
|
+
return ResolvedType(python_type=schema.generation_name, is_optional=not required)
|
|
268
|
+
else:
|
|
269
|
+
# This is an unprocessed inline enum - log warning with details
|
|
270
|
+
enum_values = schema.enum[:5] if len(schema.enum) > 5 else schema.enum
|
|
271
|
+
more = f" (and {len(schema.enum) - 5} more)" if len(schema.enum) > 5 else ""
|
|
272
|
+
context_info = f"name='{schema.name}'" if schema.name else "unnamed"
|
|
273
|
+
logger.warning(
|
|
274
|
+
f"Found inline enum in string schema that wasn't promoted: "
|
|
275
|
+
f"{context_info}, type={schema.type}, enum_values={enum_values}{more}. "
|
|
276
|
+
f"This will be treated as plain 'str' instead of a proper Enum type."
|
|
277
|
+
)
|
|
278
|
+
return ResolvedType(python_type="str", is_optional=not required)
|
|
279
|
+
|
|
280
|
+
# Handle string formats
|
|
281
|
+
format_type = getattr(schema, "format", None)
|
|
282
|
+
if format_type:
|
|
283
|
+
format_mapping = {
|
|
284
|
+
"date": "date",
|
|
285
|
+
"date-time": "datetime",
|
|
286
|
+
"time": "time",
|
|
287
|
+
"uuid": "UUID",
|
|
288
|
+
"email": "str",
|
|
289
|
+
"uri": "str",
|
|
290
|
+
"hostname": "str",
|
|
291
|
+
"ipv4": "str",
|
|
292
|
+
"ipv6": "str",
|
|
293
|
+
"binary": "bytes",
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
python_type = format_mapping.get(format_type, "str")
|
|
297
|
+
|
|
298
|
+
# Add appropriate imports for special types
|
|
299
|
+
if python_type == "date":
|
|
300
|
+
context.add_import("datetime", "date")
|
|
301
|
+
elif python_type == "datetime":
|
|
302
|
+
context.add_import("datetime", "datetime")
|
|
303
|
+
elif python_type == "time":
|
|
304
|
+
context.add_import("datetime", "time")
|
|
305
|
+
elif python_type == "UUID":
|
|
306
|
+
context.add_import("uuid", "UUID")
|
|
307
|
+
|
|
308
|
+
return ResolvedType(python_type=python_type, is_optional=not required)
|
|
309
|
+
|
|
310
|
+
return ResolvedType(python_type="str", is_optional=not required)
|
|
311
|
+
|
|
312
|
+
def _resolve_integer(self, context: TypeContext, required: bool) -> ResolvedType:
|
|
313
|
+
"""Resolve integer type."""
|
|
314
|
+
return ResolvedType(python_type="int", is_optional=not required)
|
|
315
|
+
|
|
316
|
+
def _resolve_number(self, context: TypeContext, required: bool) -> ResolvedType:
|
|
317
|
+
"""Resolve number type."""
|
|
318
|
+
return ResolvedType(python_type="float", is_optional=not required)
|
|
319
|
+
|
|
320
|
+
def _resolve_boolean(self, schema: IRSchema, context: TypeContext, required: bool) -> ResolvedType:
|
|
321
|
+
"""Resolve boolean type, handling enums with Literal types.
|
|
322
|
+
|
|
323
|
+
Boolean enums are resolved to:
|
|
324
|
+
- Literal[False] for enum: [false]
|
|
325
|
+
- Literal[True] for enum: [true]
|
|
326
|
+
- bool for enum: [true, false] (covers all boolean values)
|
|
327
|
+
"""
|
|
328
|
+
# Check if this is a boolean enum
|
|
329
|
+
if hasattr(schema, "enum") and schema.enum:
|
|
330
|
+
enum_values = schema.enum
|
|
331
|
+
# Filter out None values which might be present for nullable enums
|
|
332
|
+
bool_values = [v for v in enum_values if v is not None]
|
|
333
|
+
|
|
334
|
+
if len(bool_values) == 1:
|
|
335
|
+
# Single value enum - use Literal
|
|
336
|
+
value = bool_values[0]
|
|
337
|
+
context.add_import("typing", "Literal")
|
|
338
|
+
literal_value = "True" if value else "False"
|
|
339
|
+
return ResolvedType(python_type=f"Literal[{literal_value}]", is_optional=not required)
|
|
340
|
+
elif len(bool_values) == 2 and set(bool_values) == {True, False}:
|
|
341
|
+
# Both True and False - just use bool
|
|
342
|
+
return ResolvedType(python_type="bool", is_optional=not required)
|
|
343
|
+
# Otherwise fall through to regular bool
|
|
344
|
+
|
|
345
|
+
return ResolvedType(python_type="bool", is_optional=not required)
|
|
346
|
+
|
|
347
|
+
def _resolve_null(self, context: TypeContext, required: bool) -> ResolvedType:
|
|
348
|
+
"""Resolve null type.
|
|
349
|
+
|
|
350
|
+
When a schema has type=null or no type defined (None), we map it to Python's Any type
|
|
351
|
+
because null schemas represent unknown/arbitrary data rather than the None value.
|
|
352
|
+
"""
|
|
353
|
+
context.add_import("typing", "Any")
|
|
354
|
+
return ResolvedType(python_type="Any", is_optional=not required)
|
|
355
|
+
|
|
356
|
+
def _resolve_array(
|
|
357
|
+
self, schema: IRSchema, context: TypeContext, required: bool, resolve_underlying: bool = False
|
|
358
|
+
) -> ResolvedType:
|
|
359
|
+
"""Resolve array type."""
|
|
360
|
+
items_schema = getattr(schema, "items", None)
|
|
361
|
+
if not items_schema:
|
|
362
|
+
schema_name = getattr(schema, "name", "unnamed")
|
|
363
|
+
logger.warning(f"Array schema '{schema_name}' missing 'items' definition.")
|
|
364
|
+
logger.info(" Arrays in OpenAPI must define the type of items they contain.")
|
|
365
|
+
logger.info(' Example: { "type": "array", "items": { "type": "string" } }')
|
|
366
|
+
logger.info(" This will be mapped to List[Any] - consider fixing the OpenAPI spec.")
|
|
367
|
+
context.add_import("typing", "List")
|
|
368
|
+
context.add_import("typing", "Any")
|
|
369
|
+
return ResolvedType(python_type="List[Any]", is_optional=not required)
|
|
370
|
+
|
|
371
|
+
# For array items, we generally want to preserve model references unless the item itself is a primitive alias
|
|
372
|
+
# Only resolve underlying for primitive type aliases, not for actual models that should be generated
|
|
373
|
+
items_resolve_underlying = (
|
|
374
|
+
resolve_underlying
|
|
375
|
+
and getattr(items_schema, "name", None)
|
|
376
|
+
and not getattr(items_schema, "properties", None)
|
|
377
|
+
and getattr(items_schema, "type", None) in ("string", "integer", "number", "boolean")
|
|
378
|
+
)
|
|
379
|
+
item_type = self.resolve_schema(
|
|
380
|
+
items_schema, context, required=True, resolve_underlying=bool(items_resolve_underlying)
|
|
381
|
+
)
|
|
382
|
+
context.add_import("typing", "List")
|
|
383
|
+
|
|
384
|
+
# Format the item type properly, handling forward references
|
|
385
|
+
item_type_str = item_type.python_type
|
|
386
|
+
if item_type.is_forward_ref and not item_type_str.startswith('"'):
|
|
387
|
+
item_type_str = f'"{item_type_str}"'
|
|
388
|
+
|
|
389
|
+
return ResolvedType(python_type=f"List[{item_type_str}]", is_optional=not required)
|
|
390
|
+
|
|
391
|
+
def _resolve_object(self, schema: IRSchema, context: TypeContext, required: bool) -> ResolvedType:
|
|
392
|
+
"""Resolve object type."""
|
|
393
|
+
properties = getattr(schema, "properties", None)
|
|
394
|
+
|
|
395
|
+
if not properties:
|
|
396
|
+
# Generic object
|
|
397
|
+
context.add_import("typing", "Dict")
|
|
398
|
+
context.add_import("typing", "Any")
|
|
399
|
+
return ResolvedType(python_type="dict[str, Any]", is_optional=not required)
|
|
400
|
+
|
|
401
|
+
# Object with properties - should be promoted to named type
|
|
402
|
+
logger.warning("Found object with properties - should be promoted to named type")
|
|
403
|
+
context.add_import("typing", "Dict")
|
|
404
|
+
context.add_import("typing", "Any")
|
|
405
|
+
return ResolvedType(python_type="dict[str, Any]", is_optional=not required)
|
|
406
|
+
|
|
407
|
+
def _resolve_any(self, context: TypeContext, required: bool = True) -> ResolvedType:
|
|
408
|
+
"""Resolve to Any type."""
|
|
409
|
+
context.add_import("typing", "Any")
|
|
410
|
+
return ResolvedType(python_type="Any", is_optional=not required)
|
|
411
|
+
|
|
412
|
+
def _resolve_any_of(
|
|
413
|
+
self, schema: IRSchema, context: TypeContext, required: bool, resolve_underlying: bool = False
|
|
414
|
+
) -> ResolvedType:
|
|
415
|
+
"""Resolve anyOf composition to Union type."""
|
|
416
|
+
if not schema.any_of:
|
|
417
|
+
return self._resolve_any(context, required)
|
|
418
|
+
|
|
419
|
+
resolved_types = []
|
|
420
|
+
for sub_schema in schema.any_of:
|
|
421
|
+
# For union components, preserve model references unless it's a primitive type alias
|
|
422
|
+
sub_resolve_underlying = (
|
|
423
|
+
resolve_underlying
|
|
424
|
+
and getattr(sub_schema, "name", None)
|
|
425
|
+
and not getattr(sub_schema, "properties", None)
|
|
426
|
+
and getattr(sub_schema, "type", None) in ("string", "integer", "number", "boolean")
|
|
427
|
+
)
|
|
428
|
+
sub_resolved = self.resolve_schema(
|
|
429
|
+
sub_schema, context, required=True, resolve_underlying=bool(sub_resolve_underlying)
|
|
430
|
+
)
|
|
431
|
+
# Format sub-types properly, handling forward references
|
|
432
|
+
sub_type_str = sub_resolved.python_type
|
|
433
|
+
if sub_resolved.is_forward_ref and not sub_type_str.startswith('"'):
|
|
434
|
+
sub_type_str = f'"{sub_type_str}"'
|
|
435
|
+
resolved_types.append(sub_type_str)
|
|
436
|
+
|
|
437
|
+
if len(resolved_types) == 1:
|
|
438
|
+
return ResolvedType(python_type=resolved_types[0], is_optional=not required)
|
|
439
|
+
|
|
440
|
+
# Sort types for consistent ordering
|
|
441
|
+
resolved_types.sort()
|
|
442
|
+
context.add_import("typing", "Union")
|
|
443
|
+
union_type = f"Union[{', '.join(resolved_types)}]"
|
|
444
|
+
|
|
445
|
+
return ResolvedType(python_type=union_type, is_optional=not required)
|
|
446
|
+
|
|
447
|
+
def _resolve_all_of(
|
|
448
|
+
self, schema: IRSchema, context: TypeContext, required: bool, resolve_underlying: bool = False
|
|
449
|
+
) -> ResolvedType:
|
|
450
|
+
"""Resolve allOf composition."""
|
|
451
|
+
# For allOf, we typically need to merge the schemas
|
|
452
|
+
# For now, we'll use the first schema if it has a clear type
|
|
453
|
+
if not schema.all_of:
|
|
454
|
+
return self._resolve_any(context, required)
|
|
455
|
+
|
|
456
|
+
# Simple implementation: use the first concrete schema
|
|
457
|
+
for sub_schema in schema.all_of:
|
|
458
|
+
if hasattr(sub_schema, "type") and sub_schema.type:
|
|
459
|
+
return self.resolve_schema(sub_schema, context, required, resolve_underlying)
|
|
460
|
+
|
|
461
|
+
# Fallback - if no schema has a concrete type, return Any
|
|
462
|
+
# Don't recurse into schemas with no type as that causes warnings
|
|
463
|
+
return self._resolve_any(context, required)
|
|
464
|
+
|
|
465
|
+
def _resolve_one_of(
|
|
466
|
+
self, schema: IRSchema, context: TypeContext, required: bool, resolve_underlying: bool = False
|
|
467
|
+
) -> ResolvedType:
|
|
468
|
+
"""Resolve oneOf composition to Union type."""
|
|
469
|
+
if not schema.one_of:
|
|
470
|
+
return self._resolve_any(context, required)
|
|
471
|
+
|
|
472
|
+
resolved_types = []
|
|
473
|
+
for sub_schema in schema.one_of:
|
|
474
|
+
# For union components, preserve model references unless it's a primitive type alias
|
|
475
|
+
sub_resolve_underlying = (
|
|
476
|
+
resolve_underlying
|
|
477
|
+
and getattr(sub_schema, "name", None)
|
|
478
|
+
and not getattr(sub_schema, "properties", None)
|
|
479
|
+
and getattr(sub_schema, "type", None) in ("string", "integer", "number", "boolean")
|
|
480
|
+
)
|
|
481
|
+
sub_resolved = self.resolve_schema(
|
|
482
|
+
sub_schema, context, required=True, resolve_underlying=bool(sub_resolve_underlying)
|
|
483
|
+
)
|
|
484
|
+
# Format sub-types properly, handling forward references
|
|
485
|
+
sub_type_str = sub_resolved.python_type
|
|
486
|
+
if sub_resolved.is_forward_ref and not sub_type_str.startswith('"'):
|
|
487
|
+
sub_type_str = f'"{sub_type_str}"'
|
|
488
|
+
resolved_types.append(sub_type_str)
|
|
489
|
+
|
|
490
|
+
if len(resolved_types) == 1:
|
|
491
|
+
return ResolvedType(python_type=resolved_types[0], is_optional=not required)
|
|
492
|
+
|
|
493
|
+
# Sort types for consistent ordering
|
|
494
|
+
resolved_types.sort()
|
|
495
|
+
context.add_import("typing", "Union")
|
|
496
|
+
union_type = f"Union[{', '.join(resolved_types)}]"
|
|
497
|
+
|
|
498
|
+
return ResolvedType(python_type=union_type, is_optional=not required)
|