pyopenapi-gen 0.8.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyopenapi_gen/__init__.py +114 -0
- pyopenapi_gen/__main__.py +6 -0
- pyopenapi_gen/cli.py +86 -0
- pyopenapi_gen/context/file_manager.py +52 -0
- pyopenapi_gen/context/import_collector.py +382 -0
- pyopenapi_gen/context/render_context.py +630 -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/exceptions.py +25 -0
- pyopenapi_gen/core/http_transport.py +219 -0
- pyopenapi_gen/core/loader/__init__.py +12 -0
- pyopenapi_gen/core/loader/loader.py +158 -0
- pyopenapi_gen/core/loader/operations/__init__.py +12 -0
- pyopenapi_gen/core/loader/operations/parser.py +155 -0
- pyopenapi_gen/core/loader/operations/post_processor.py +60 -0
- pyopenapi_gen/core/loader/operations/request_body.py +85 -0
- pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
- pyopenapi_gen/core/loader/parameters/parser.py +121 -0
- pyopenapi_gen/core/loader/responses/__init__.py +10 -0
- pyopenapi_gen/core/loader/responses/parser.py +104 -0
- pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
- pyopenapi_gen/core/loader/schemas/extractor.py +184 -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 +74 -0
- pyopenapi_gen/core/parsing/context.py +184 -0
- pyopenapi_gen/core/parsing/cycle_helpers.py +123 -0
- pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
- pyopenapi_gen/core/parsing/keywords/all_of_parser.py +77 -0
- pyopenapi_gen/core/parsing/keywords/any_of_parser.py +79 -0
- pyopenapi_gen/core/parsing/keywords/array_items_parser.py +69 -0
- pyopenapi_gen/core/parsing/keywords/one_of_parser.py +72 -0
- pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
- pyopenapi_gen/core/parsing/schema_finalizer.py +166 -0
- pyopenapi_gen/core/parsing/schema_parser.py +610 -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 +117 -0
- pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
- pyopenapi_gen/core/postprocess_manager.py +161 -0
- pyopenapi_gen/core/schemas.py +40 -0
- pyopenapi_gen/core/streaming_helpers.py +86 -0
- pyopenapi_gen/core/telemetry.py +67 -0
- pyopenapi_gen/core/utils.py +409 -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 +274 -0
- pyopenapi_gen/core_package_template/README.md +21 -0
- pyopenapi_gen/emit/models_emitter.py +143 -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 +223 -0
- pyopenapi_gen/emitters/exceptions_emitter.py +52 -0
- pyopenapi_gen/emitters/models_emitter.py +428 -0
- pyopenapi_gen/generator/client_generator.py +562 -0
- pyopenapi_gen/helpers/__init__.py +1 -0
- pyopenapi_gen/helpers/endpoint_utils.py +552 -0
- pyopenapi_gen/helpers/type_cleaner.py +341 -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 +89 -0
- pyopenapi_gen/helpers/type_resolution/named_resolver.py +174 -0
- pyopenapi_gen/helpers/type_resolution/object_resolver.py +212 -0
- pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +57 -0
- pyopenapi_gen/helpers/type_resolution/resolver.py +48 -0
- pyopenapi_gen/helpers/url_utils.py +14 -0
- pyopenapi_gen/http_types.py +20 -0
- pyopenapi_gen/ir.py +167 -0
- pyopenapi_gen/py.typed +1 -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 +30 -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 +203 -0
- pyopenapi_gen/types/resolvers/schema_resolver.py +367 -0
- pyopenapi_gen/types/services/__init__.py +5 -0
- pyopenapi_gen/types/services/type_service.py +133 -0
- pyopenapi_gen/visit/client_visitor.py +228 -0
- pyopenapi_gen/visit/docs_visitor.py +38 -0
- pyopenapi_gen/visit/endpoint/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/endpoint_visitor.py +103 -0
- pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +121 -0
- pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +87 -0
- pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
- pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +497 -0
- pyopenapi_gen/visit/endpoint/generators/signature_generator.py +88 -0
- pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +183 -0
- pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +76 -0
- pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
- pyopenapi_gen/visit/exception_visitor.py +52 -0
- pyopenapi_gen/visit/model/__init__.py +0 -0
- pyopenapi_gen/visit/model/alias_generator.py +89 -0
- pyopenapi_gen/visit/model/dataclass_generator.py +197 -0
- pyopenapi_gen/visit/model/enum_generator.py +200 -0
- pyopenapi_gen/visit/model/model_visitor.py +197 -0
- pyopenapi_gen/visit/visitor.py +97 -0
- pyopenapi_gen-0.8.3.dist-info/METADATA +224 -0
- pyopenapi_gen-0.8.3.dist-info/RECORD +122 -0
- pyopenapi_gen-0.8.3.dist-info/WHEEL +4 -0
- pyopenapi_gen-0.8.3.dist-info/entry_points.txt +2 -0
- pyopenapi_gen-0.8.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,367 @@
|
|
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 named schemas with generation_name (fully processed schemas)
|
48
|
+
if schema.name and hasattr(schema, "generation_name") and schema.generation_name:
|
49
|
+
# If resolve_underlying is True, skip named schema resolution for type aliases
|
50
|
+
# and resolve the underlying primitive type instead
|
51
|
+
if not resolve_underlying:
|
52
|
+
return self._resolve_named_schema(schema, context, required)
|
53
|
+
|
54
|
+
# Handle composition types (any_of, all_of, one_of)
|
55
|
+
# These are processed for inline compositions or when generating the alias for a named composition
|
56
|
+
if hasattr(schema, "any_of") and schema.any_of:
|
57
|
+
return self._resolve_any_of(schema, context, required, resolve_underlying)
|
58
|
+
elif hasattr(schema, "all_of") and schema.all_of:
|
59
|
+
return self._resolve_all_of(schema, context, required, resolve_underlying)
|
60
|
+
elif hasattr(schema, "one_of") and schema.one_of:
|
61
|
+
return self._resolve_one_of(schema, context, required, resolve_underlying)
|
62
|
+
|
63
|
+
# Handle named schemas without generation_name (fallback for references)
|
64
|
+
if schema.name and schema.name in self.ref_resolver.schemas:
|
65
|
+
target_schema = self.ref_resolver.schemas[schema.name]
|
66
|
+
# Avoid infinite recursion if it's the same object
|
67
|
+
if target_schema is not schema:
|
68
|
+
return self.resolve_schema(target_schema, context, required, resolve_underlying)
|
69
|
+
|
70
|
+
# Handle by type
|
71
|
+
schema_type = getattr(schema, "type", None)
|
72
|
+
|
73
|
+
# Check if schema.type refers to a named schema (backward compatibility)
|
74
|
+
if (
|
75
|
+
schema_type
|
76
|
+
and hasattr(self.ref_resolver, "schemas")
|
77
|
+
and hasattr(self.ref_resolver.schemas, "__contains__")
|
78
|
+
and schema_type in self.ref_resolver.schemas
|
79
|
+
):
|
80
|
+
target_schema = self.ref_resolver.schemas[schema_type]
|
81
|
+
if target_schema and hasattr(target_schema, "name") and target_schema.name: # It's a named schema reference
|
82
|
+
return self.resolve_schema(target_schema, context, required, resolve_underlying)
|
83
|
+
|
84
|
+
if schema_type == "string":
|
85
|
+
return self._resolve_string(schema, context, required)
|
86
|
+
elif schema_type == "integer":
|
87
|
+
return self._resolve_integer(context, required)
|
88
|
+
elif schema_type == "number":
|
89
|
+
return self._resolve_number(context, required)
|
90
|
+
elif schema_type == "boolean":
|
91
|
+
return self._resolve_boolean(context, required)
|
92
|
+
elif schema_type == "array":
|
93
|
+
return self._resolve_array(schema, context, required, resolve_underlying)
|
94
|
+
elif schema_type == "object":
|
95
|
+
return self._resolve_object(schema, context, required)
|
96
|
+
elif schema_type == "null":
|
97
|
+
return self._resolve_null(context, required)
|
98
|
+
else:
|
99
|
+
logger.warning(f"Unknown schema type: {schema_type}")
|
100
|
+
return self._resolve_any(context)
|
101
|
+
|
102
|
+
def _resolve_reference(
|
103
|
+
self, ref: str, context: TypeContext, required: bool, resolve_underlying: bool = False
|
104
|
+
) -> ResolvedType:
|
105
|
+
"""Resolve a $ref to its target schema."""
|
106
|
+
target_schema = self.ref_resolver.resolve_ref(ref)
|
107
|
+
if not target_schema:
|
108
|
+
raise TypeResolutionError(f"Could not resolve reference: {ref}")
|
109
|
+
|
110
|
+
return self.resolve_schema(target_schema, context, required, resolve_underlying)
|
111
|
+
|
112
|
+
def _resolve_named_schema(self, schema: IRSchema, context: TypeContext, required: bool) -> ResolvedType:
|
113
|
+
"""Resolve a named schema (model/enum)."""
|
114
|
+
class_name = schema.generation_name
|
115
|
+
module_stem = getattr(schema, "final_module_stem", None)
|
116
|
+
|
117
|
+
if not module_stem:
|
118
|
+
logger.warning(f"Named schema {schema.name} missing final_module_stem")
|
119
|
+
return ResolvedType(python_type=class_name or "Any", is_optional=not required)
|
120
|
+
|
121
|
+
# Check if we're trying to import from the same module (self-import)
|
122
|
+
# This only applies when using a RenderContextAdapter with a real RenderContext
|
123
|
+
current_file = None
|
124
|
+
try:
|
125
|
+
# Only check for self-import if we have a RenderContextAdapter with real RenderContext
|
126
|
+
if (
|
127
|
+
hasattr(context, "render_context")
|
128
|
+
and hasattr(context.render_context, "current_file")
|
129
|
+
and
|
130
|
+
# Ensure this is not a mock by checking if it has the expected methods
|
131
|
+
hasattr(context.render_context, "add_import")
|
132
|
+
):
|
133
|
+
current_file = context.render_context.current_file
|
134
|
+
except (AttributeError, TypeError):
|
135
|
+
# If anything goes wrong with attribute access, assume not a self-reference
|
136
|
+
current_file = None
|
137
|
+
|
138
|
+
# Only treat as self-reference if we have a real file path that matches
|
139
|
+
if current_file and isinstance(current_file, str) and current_file.endswith(f"{module_stem}.py"):
|
140
|
+
# This is a self-import (importing from the same file), so skip the import
|
141
|
+
# and mark as forward reference to require quoting
|
142
|
+
return ResolvedType(python_type=class_name or "Any", is_optional=not required, is_forward_ref=True)
|
143
|
+
|
144
|
+
# Use the render context's relative path calculation to determine proper import path
|
145
|
+
import_module = f"..models.{module_stem}" # Default fallback
|
146
|
+
|
147
|
+
# If we have access to RenderContext, use its proper relative path calculation
|
148
|
+
if hasattr(context, "render_context") and hasattr(
|
149
|
+
context.render_context, "calculate_relative_path_for_internal_module"
|
150
|
+
):
|
151
|
+
try:
|
152
|
+
# Calculate relative path from current location to the target model module
|
153
|
+
target_module_relative = f"models.{module_stem}"
|
154
|
+
relative_path = context.render_context.calculate_relative_path_for_internal_module(
|
155
|
+
target_module_relative
|
156
|
+
)
|
157
|
+
if relative_path:
|
158
|
+
import_module = relative_path
|
159
|
+
except Exception as e:
|
160
|
+
# If relative path calculation fails, fall back to the default
|
161
|
+
logger.debug(f"Failed to calculate relative path for {module_stem}, using default: {e}")
|
162
|
+
|
163
|
+
context.add_import(import_module, class_name or "Any")
|
164
|
+
|
165
|
+
return ResolvedType(
|
166
|
+
python_type=class_name or "Any",
|
167
|
+
needs_import=True,
|
168
|
+
import_module=import_module,
|
169
|
+
import_name=class_name or "Any",
|
170
|
+
is_optional=not required,
|
171
|
+
)
|
172
|
+
|
173
|
+
def _resolve_string(self, schema: IRSchema, context: TypeContext, required: bool) -> ResolvedType:
|
174
|
+
"""Resolve string type, handling enums and formats."""
|
175
|
+
if hasattr(schema, "enum") and schema.enum:
|
176
|
+
# This is an enum - should be promoted to named type
|
177
|
+
logger.warning("Found inline enum in string schema - should be promoted")
|
178
|
+
return ResolvedType(python_type="str", is_optional=not required)
|
179
|
+
|
180
|
+
# Handle string formats
|
181
|
+
format_type = getattr(schema, "format", None)
|
182
|
+
if format_type:
|
183
|
+
format_mapping = {
|
184
|
+
"date": "date",
|
185
|
+
"date-time": "datetime",
|
186
|
+
"time": "time",
|
187
|
+
"uuid": "UUID",
|
188
|
+
"email": "str",
|
189
|
+
"uri": "str",
|
190
|
+
"hostname": "str",
|
191
|
+
"ipv4": "str",
|
192
|
+
"ipv6": "str",
|
193
|
+
}
|
194
|
+
|
195
|
+
python_type = format_mapping.get(format_type, "str")
|
196
|
+
|
197
|
+
# Add appropriate imports for special types
|
198
|
+
if python_type == "date":
|
199
|
+
context.add_import("datetime", "date")
|
200
|
+
elif python_type == "datetime":
|
201
|
+
context.add_import("datetime", "datetime")
|
202
|
+
elif python_type == "time":
|
203
|
+
context.add_import("datetime", "time")
|
204
|
+
elif python_type == "UUID":
|
205
|
+
context.add_import("uuid", "UUID")
|
206
|
+
|
207
|
+
return ResolvedType(python_type=python_type, is_optional=not required)
|
208
|
+
|
209
|
+
return ResolvedType(python_type="str", is_optional=not required)
|
210
|
+
|
211
|
+
def _resolve_integer(self, context: TypeContext, required: bool) -> ResolvedType:
|
212
|
+
"""Resolve integer type."""
|
213
|
+
return ResolvedType(python_type="int", is_optional=not required)
|
214
|
+
|
215
|
+
def _resolve_number(self, context: TypeContext, required: bool) -> ResolvedType:
|
216
|
+
"""Resolve number type."""
|
217
|
+
return ResolvedType(python_type="float", is_optional=not required)
|
218
|
+
|
219
|
+
def _resolve_boolean(self, context: TypeContext, required: bool) -> ResolvedType:
|
220
|
+
"""Resolve boolean type."""
|
221
|
+
return ResolvedType(python_type="bool", is_optional=not required)
|
222
|
+
|
223
|
+
def _resolve_null(self, context: TypeContext, required: bool) -> ResolvedType:
|
224
|
+
"""Resolve null type."""
|
225
|
+
return ResolvedType(python_type="None", is_optional=not required)
|
226
|
+
|
227
|
+
def _resolve_array(
|
228
|
+
self, schema: IRSchema, context: TypeContext, required: bool, resolve_underlying: bool = False
|
229
|
+
) -> ResolvedType:
|
230
|
+
"""Resolve array type."""
|
231
|
+
items_schema = getattr(schema, "items", None)
|
232
|
+
if not items_schema:
|
233
|
+
logger.warning("Array schema missing items")
|
234
|
+
context.add_import("typing", "List")
|
235
|
+
context.add_import("typing", "Any")
|
236
|
+
return ResolvedType(python_type="List[Any]", is_optional=not required)
|
237
|
+
|
238
|
+
# For array items, we generally want to preserve model references unless the item itself is a primitive alias
|
239
|
+
# Only resolve underlying for primitive type aliases, not for actual models that should be generated
|
240
|
+
items_resolve_underlying = (
|
241
|
+
resolve_underlying
|
242
|
+
and getattr(items_schema, "name", None)
|
243
|
+
and not getattr(items_schema, "properties", None)
|
244
|
+
and getattr(items_schema, "type", None) in ("string", "integer", "number", "boolean")
|
245
|
+
)
|
246
|
+
item_type = self.resolve_schema(
|
247
|
+
items_schema, context, required=True, resolve_underlying=bool(items_resolve_underlying)
|
248
|
+
)
|
249
|
+
context.add_import("typing", "List")
|
250
|
+
|
251
|
+
# Format the item type properly, handling forward references
|
252
|
+
item_type_str = item_type.python_type
|
253
|
+
if item_type.is_forward_ref and not item_type_str.startswith('"'):
|
254
|
+
item_type_str = f'"{item_type_str}"'
|
255
|
+
|
256
|
+
return ResolvedType(python_type=f"List[{item_type_str}]", is_optional=not required)
|
257
|
+
|
258
|
+
def _resolve_object(self, schema: IRSchema, context: TypeContext, required: bool) -> ResolvedType:
|
259
|
+
"""Resolve object type."""
|
260
|
+
properties = getattr(schema, "properties", None)
|
261
|
+
|
262
|
+
if not properties:
|
263
|
+
# Generic object
|
264
|
+
context.add_import("typing", "Dict")
|
265
|
+
context.add_import("typing", "Any")
|
266
|
+
return ResolvedType(python_type="Dict[str, Any]", is_optional=not required)
|
267
|
+
|
268
|
+
# Object with properties - should be promoted to named type
|
269
|
+
logger.warning("Found object with properties - should be promoted to named type")
|
270
|
+
context.add_import("typing", "Dict")
|
271
|
+
context.add_import("typing", "Any")
|
272
|
+
return ResolvedType(python_type="Dict[str, Any]", is_optional=not required)
|
273
|
+
|
274
|
+
def _resolve_any(self, context: TypeContext) -> ResolvedType:
|
275
|
+
"""Resolve to Any type."""
|
276
|
+
context.add_import("typing", "Any")
|
277
|
+
return ResolvedType(python_type="Any")
|
278
|
+
|
279
|
+
def _resolve_any_of(
|
280
|
+
self, schema: IRSchema, context: TypeContext, required: bool, resolve_underlying: bool = False
|
281
|
+
) -> ResolvedType:
|
282
|
+
"""Resolve anyOf composition to Union type."""
|
283
|
+
if not schema.any_of:
|
284
|
+
return self._resolve_any(context)
|
285
|
+
|
286
|
+
resolved_types = []
|
287
|
+
for sub_schema in schema.any_of:
|
288
|
+
# For union components, preserve model references unless it's a primitive type alias
|
289
|
+
sub_resolve_underlying = (
|
290
|
+
resolve_underlying
|
291
|
+
and getattr(sub_schema, "name", None)
|
292
|
+
and not getattr(sub_schema, "properties", None)
|
293
|
+
and getattr(sub_schema, "type", None) in ("string", "integer", "number", "boolean")
|
294
|
+
)
|
295
|
+
sub_resolved = self.resolve_schema(
|
296
|
+
sub_schema, context, required=True, resolve_underlying=bool(sub_resolve_underlying)
|
297
|
+
)
|
298
|
+
# Format sub-types properly, handling forward references
|
299
|
+
sub_type_str = sub_resolved.python_type
|
300
|
+
if sub_resolved.is_forward_ref and not sub_type_str.startswith('"'):
|
301
|
+
sub_type_str = f'"{sub_type_str}"'
|
302
|
+
resolved_types.append(sub_type_str)
|
303
|
+
|
304
|
+
if len(resolved_types) == 1:
|
305
|
+
return ResolvedType(python_type=resolved_types[0], is_optional=not required)
|
306
|
+
|
307
|
+
# Sort types for consistent ordering
|
308
|
+
resolved_types.sort()
|
309
|
+
context.add_import("typing", "Union")
|
310
|
+
union_type = f"Union[{', '.join(resolved_types)}]"
|
311
|
+
|
312
|
+
return ResolvedType(python_type=union_type, is_optional=not required)
|
313
|
+
|
314
|
+
def _resolve_all_of(
|
315
|
+
self, schema: IRSchema, context: TypeContext, required: bool, resolve_underlying: bool = False
|
316
|
+
) -> ResolvedType:
|
317
|
+
"""Resolve allOf composition."""
|
318
|
+
# For allOf, we typically need to merge the schemas
|
319
|
+
# For now, we'll use the first schema if it has a clear type
|
320
|
+
if not schema.all_of:
|
321
|
+
return self._resolve_any(context)
|
322
|
+
|
323
|
+
# Simple implementation: use the first concrete schema
|
324
|
+
for sub_schema in schema.all_of:
|
325
|
+
if hasattr(sub_schema, "type") and sub_schema.type:
|
326
|
+
return self.resolve_schema(sub_schema, context, required, resolve_underlying)
|
327
|
+
|
328
|
+
# Fallback to first schema
|
329
|
+
if schema.all_of:
|
330
|
+
return self.resolve_schema(schema.all_of[0], context, required, resolve_underlying)
|
331
|
+
|
332
|
+
return self._resolve_any(context)
|
333
|
+
|
334
|
+
def _resolve_one_of(
|
335
|
+
self, schema: IRSchema, context: TypeContext, required: bool, resolve_underlying: bool = False
|
336
|
+
) -> ResolvedType:
|
337
|
+
"""Resolve oneOf composition to Union type."""
|
338
|
+
if not schema.one_of:
|
339
|
+
return self._resolve_any(context)
|
340
|
+
|
341
|
+
resolved_types = []
|
342
|
+
for sub_schema in schema.one_of:
|
343
|
+
# For union components, preserve model references unless it's a primitive type alias
|
344
|
+
sub_resolve_underlying = (
|
345
|
+
resolve_underlying
|
346
|
+
and getattr(sub_schema, "name", None)
|
347
|
+
and not getattr(sub_schema, "properties", None)
|
348
|
+
and getattr(sub_schema, "type", None) in ("string", "integer", "number", "boolean")
|
349
|
+
)
|
350
|
+
sub_resolved = self.resolve_schema(
|
351
|
+
sub_schema, context, required=True, resolve_underlying=bool(sub_resolve_underlying)
|
352
|
+
)
|
353
|
+
# Format sub-types properly, handling forward references
|
354
|
+
sub_type_str = sub_resolved.python_type
|
355
|
+
if sub_resolved.is_forward_ref and not sub_type_str.startswith('"'):
|
356
|
+
sub_type_str = f'"{sub_type_str}"'
|
357
|
+
resolved_types.append(sub_type_str)
|
358
|
+
|
359
|
+
if len(resolved_types) == 1:
|
360
|
+
return ResolvedType(python_type=resolved_types[0], is_optional=not required)
|
361
|
+
|
362
|
+
# Sort types for consistent ordering
|
363
|
+
resolved_types.sort()
|
364
|
+
context.add_import("typing", "Union")
|
365
|
+
union_type = f"Union[{', '.join(resolved_types)}]"
|
366
|
+
|
367
|
+
return ResolvedType(python_type=union_type, is_optional=not required)
|
@@ -0,0 +1,133 @@
|
|
1
|
+
"""Unified type resolution service."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
from typing import Dict, Optional
|
5
|
+
|
6
|
+
from pyopenapi_gen import IROperation, IRResponse, IRSchema
|
7
|
+
from pyopenapi_gen.context.render_context import RenderContext
|
8
|
+
|
9
|
+
from ..contracts.types import ResolvedType
|
10
|
+
from ..resolvers import OpenAPIReferenceResolver, OpenAPIResponseResolver, OpenAPISchemaResolver
|
11
|
+
|
12
|
+
logger = logging.getLogger(__name__)
|
13
|
+
|
14
|
+
|
15
|
+
class RenderContextAdapter:
|
16
|
+
"""Adapter to make RenderContext compatible with TypeContext protocol."""
|
17
|
+
|
18
|
+
def __init__(self, render_context: RenderContext):
|
19
|
+
self.render_context = render_context
|
20
|
+
|
21
|
+
def add_import(self, module: str, name: str) -> None:
|
22
|
+
"""Add an import to the context."""
|
23
|
+
self.render_context.add_import(module, name)
|
24
|
+
|
25
|
+
def add_conditional_import(self, condition: str, module: str, name: str) -> None:
|
26
|
+
"""Add a conditional import (e.g., TYPE_CHECKING)."""
|
27
|
+
self.render_context.add_conditional_import(condition, module, name)
|
28
|
+
|
29
|
+
|
30
|
+
class UnifiedTypeService:
|
31
|
+
"""
|
32
|
+
Unified service for all type resolution needs.
|
33
|
+
|
34
|
+
This is the main entry point for converting OpenAPI schemas, responses,
|
35
|
+
and operations to Python type strings.
|
36
|
+
"""
|
37
|
+
|
38
|
+
def __init__(self, schemas: Dict[str, IRSchema], responses: Optional[Dict[str, IRResponse]] = None):
|
39
|
+
"""
|
40
|
+
Initialize the type service.
|
41
|
+
|
42
|
+
Args:
|
43
|
+
schemas: Dictionary of all schemas by name
|
44
|
+
responses: Dictionary of all responses by name (optional)
|
45
|
+
"""
|
46
|
+
self.ref_resolver = OpenAPIReferenceResolver(schemas, responses)
|
47
|
+
self.schema_resolver = OpenAPISchemaResolver(self.ref_resolver)
|
48
|
+
self.response_resolver = OpenAPIResponseResolver(self.ref_resolver, self.schema_resolver)
|
49
|
+
|
50
|
+
def resolve_schema_type(
|
51
|
+
self, schema: IRSchema, context: RenderContext, required: bool = True, resolve_underlying: bool = False
|
52
|
+
) -> str:
|
53
|
+
"""
|
54
|
+
Resolve a schema to a Python type string.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
schema: The schema to resolve
|
58
|
+
context: Render context for imports
|
59
|
+
required: Whether the field is required
|
60
|
+
resolve_underlying: If True, resolve underlying type for aliases instead of schema name
|
61
|
+
|
62
|
+
Returns:
|
63
|
+
Python type string
|
64
|
+
"""
|
65
|
+
# Check if the schema itself is nullable
|
66
|
+
# If schema.is_nullable=True, it should be Optional regardless of required
|
67
|
+
effective_required = required and not getattr(schema, "is_nullable", False)
|
68
|
+
|
69
|
+
type_context = RenderContextAdapter(context)
|
70
|
+
resolved = self.schema_resolver.resolve_schema(schema, type_context, effective_required, resolve_underlying)
|
71
|
+
return self._format_resolved_type(resolved, context)
|
72
|
+
|
73
|
+
def resolve_operation_response_type(self, operation: IROperation, context: RenderContext) -> str:
|
74
|
+
"""
|
75
|
+
Resolve an operation's response to a Python type string.
|
76
|
+
|
77
|
+
Args:
|
78
|
+
operation: The operation to resolve
|
79
|
+
context: Render context for imports
|
80
|
+
|
81
|
+
Returns:
|
82
|
+
Python type string
|
83
|
+
"""
|
84
|
+
type_context = RenderContextAdapter(context)
|
85
|
+
resolved = self.response_resolver.resolve_operation_response(operation, type_context)
|
86
|
+
return self._format_resolved_type(resolved, context)
|
87
|
+
|
88
|
+
def resolve_operation_response_with_unwrap_info(
|
89
|
+
self, operation: IROperation, context: RenderContext
|
90
|
+
) -> tuple[str, bool]:
|
91
|
+
"""
|
92
|
+
Resolve an operation's response to a Python type string with unwrapping information.
|
93
|
+
|
94
|
+
Args:
|
95
|
+
operation: The operation to resolve
|
96
|
+
context: Render context for imports
|
97
|
+
|
98
|
+
Returns:
|
99
|
+
Tuple of (Python type string, was_unwrapped flag)
|
100
|
+
"""
|
101
|
+
type_context = RenderContextAdapter(context)
|
102
|
+
resolved = self.response_resolver.resolve_operation_response(operation, type_context)
|
103
|
+
python_type = self._format_resolved_type(resolved, context)
|
104
|
+
return python_type, resolved.was_unwrapped
|
105
|
+
|
106
|
+
def resolve_response_type(self, response: IRResponse, context: RenderContext) -> str:
|
107
|
+
"""
|
108
|
+
Resolve a specific response to a Python type string.
|
109
|
+
|
110
|
+
Args:
|
111
|
+
response: The response to resolve
|
112
|
+
context: Render context for imports
|
113
|
+
|
114
|
+
Returns:
|
115
|
+
Python type string
|
116
|
+
"""
|
117
|
+
type_context = RenderContextAdapter(context)
|
118
|
+
resolved = self.response_resolver.resolve_specific_response(response, type_context)
|
119
|
+
return self._format_resolved_type(resolved, context)
|
120
|
+
|
121
|
+
def _format_resolved_type(self, resolved: ResolvedType, context: RenderContext | None = None) -> str:
|
122
|
+
"""Format a ResolvedType into a Python type string."""
|
123
|
+
python_type = resolved.python_type
|
124
|
+
|
125
|
+
if resolved.is_optional and not python_type.startswith("Optional["):
|
126
|
+
if context:
|
127
|
+
context.add_import("typing", "Optional")
|
128
|
+
python_type = f"Optional[{python_type}]"
|
129
|
+
|
130
|
+
if resolved.is_forward_ref and not python_type.startswith('"'):
|
131
|
+
python_type = f'"{python_type}"'
|
132
|
+
|
133
|
+
return python_type
|