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.
Files changed (122) hide show
  1. pyopenapi_gen/__init__.py +114 -0
  2. pyopenapi_gen/__main__.py +6 -0
  3. pyopenapi_gen/cli.py +86 -0
  4. pyopenapi_gen/context/file_manager.py +52 -0
  5. pyopenapi_gen/context/import_collector.py +382 -0
  6. pyopenapi_gen/context/render_context.py +630 -0
  7. pyopenapi_gen/core/__init__.py +0 -0
  8. pyopenapi_gen/core/auth/base.py +22 -0
  9. pyopenapi_gen/core/auth/plugins.py +89 -0
  10. pyopenapi_gen/core/exceptions.py +25 -0
  11. pyopenapi_gen/core/http_transport.py +219 -0
  12. pyopenapi_gen/core/loader/__init__.py +12 -0
  13. pyopenapi_gen/core/loader/loader.py +158 -0
  14. pyopenapi_gen/core/loader/operations/__init__.py +12 -0
  15. pyopenapi_gen/core/loader/operations/parser.py +155 -0
  16. pyopenapi_gen/core/loader/operations/post_processor.py +60 -0
  17. pyopenapi_gen/core/loader/operations/request_body.py +85 -0
  18. pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
  19. pyopenapi_gen/core/loader/parameters/parser.py +121 -0
  20. pyopenapi_gen/core/loader/responses/__init__.py +10 -0
  21. pyopenapi_gen/core/loader/responses/parser.py +104 -0
  22. pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
  23. pyopenapi_gen/core/loader/schemas/extractor.py +184 -0
  24. pyopenapi_gen/core/pagination.py +64 -0
  25. pyopenapi_gen/core/parsing/__init__.py +13 -0
  26. pyopenapi_gen/core/parsing/common/__init__.py +1 -0
  27. pyopenapi_gen/core/parsing/common/ref_resolution/__init__.py +9 -0
  28. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/__init__.py +0 -0
  29. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/cyclic_properties.py +66 -0
  30. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/direct_cycle.py +33 -0
  31. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/existing_schema.py +22 -0
  32. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +54 -0
  33. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +52 -0
  34. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +50 -0
  35. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +51 -0
  36. pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +86 -0
  37. pyopenapi_gen/core/parsing/common/type_parser.py +74 -0
  38. pyopenapi_gen/core/parsing/context.py +184 -0
  39. pyopenapi_gen/core/parsing/cycle_helpers.py +123 -0
  40. pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
  41. pyopenapi_gen/core/parsing/keywords/all_of_parser.py +77 -0
  42. pyopenapi_gen/core/parsing/keywords/any_of_parser.py +79 -0
  43. pyopenapi_gen/core/parsing/keywords/array_items_parser.py +69 -0
  44. pyopenapi_gen/core/parsing/keywords/one_of_parser.py +72 -0
  45. pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
  46. pyopenapi_gen/core/parsing/schema_finalizer.py +166 -0
  47. pyopenapi_gen/core/parsing/schema_parser.py +610 -0
  48. pyopenapi_gen/core/parsing/transformers/__init__.py +0 -0
  49. pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +285 -0
  50. pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +117 -0
  51. pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
  52. pyopenapi_gen/core/postprocess_manager.py +161 -0
  53. pyopenapi_gen/core/schemas.py +40 -0
  54. pyopenapi_gen/core/streaming_helpers.py +86 -0
  55. pyopenapi_gen/core/telemetry.py +67 -0
  56. pyopenapi_gen/core/utils.py +409 -0
  57. pyopenapi_gen/core/warning_collector.py +83 -0
  58. pyopenapi_gen/core/writers/code_writer.py +135 -0
  59. pyopenapi_gen/core/writers/documentation_writer.py +222 -0
  60. pyopenapi_gen/core/writers/line_writer.py +217 -0
  61. pyopenapi_gen/core/writers/python_construct_renderer.py +274 -0
  62. pyopenapi_gen/core_package_template/README.md +21 -0
  63. pyopenapi_gen/emit/models_emitter.py +143 -0
  64. pyopenapi_gen/emitters/client_emitter.py +51 -0
  65. pyopenapi_gen/emitters/core_emitter.py +181 -0
  66. pyopenapi_gen/emitters/docs_emitter.py +44 -0
  67. pyopenapi_gen/emitters/endpoints_emitter.py +223 -0
  68. pyopenapi_gen/emitters/exceptions_emitter.py +52 -0
  69. pyopenapi_gen/emitters/models_emitter.py +428 -0
  70. pyopenapi_gen/generator/client_generator.py +562 -0
  71. pyopenapi_gen/helpers/__init__.py +1 -0
  72. pyopenapi_gen/helpers/endpoint_utils.py +552 -0
  73. pyopenapi_gen/helpers/type_cleaner.py +341 -0
  74. pyopenapi_gen/helpers/type_helper.py +112 -0
  75. pyopenapi_gen/helpers/type_resolution/__init__.py +1 -0
  76. pyopenapi_gen/helpers/type_resolution/array_resolver.py +57 -0
  77. pyopenapi_gen/helpers/type_resolution/composition_resolver.py +79 -0
  78. pyopenapi_gen/helpers/type_resolution/finalizer.py +89 -0
  79. pyopenapi_gen/helpers/type_resolution/named_resolver.py +174 -0
  80. pyopenapi_gen/helpers/type_resolution/object_resolver.py +212 -0
  81. pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +57 -0
  82. pyopenapi_gen/helpers/type_resolution/resolver.py +48 -0
  83. pyopenapi_gen/helpers/url_utils.py +14 -0
  84. pyopenapi_gen/http_types.py +20 -0
  85. pyopenapi_gen/ir.py +167 -0
  86. pyopenapi_gen/py.typed +1 -0
  87. pyopenapi_gen/types/__init__.py +11 -0
  88. pyopenapi_gen/types/contracts/__init__.py +13 -0
  89. pyopenapi_gen/types/contracts/protocols.py +106 -0
  90. pyopenapi_gen/types/contracts/types.py +30 -0
  91. pyopenapi_gen/types/resolvers/__init__.py +7 -0
  92. pyopenapi_gen/types/resolvers/reference_resolver.py +71 -0
  93. pyopenapi_gen/types/resolvers/response_resolver.py +203 -0
  94. pyopenapi_gen/types/resolvers/schema_resolver.py +367 -0
  95. pyopenapi_gen/types/services/__init__.py +5 -0
  96. pyopenapi_gen/types/services/type_service.py +133 -0
  97. pyopenapi_gen/visit/client_visitor.py +228 -0
  98. pyopenapi_gen/visit/docs_visitor.py +38 -0
  99. pyopenapi_gen/visit/endpoint/__init__.py +1 -0
  100. pyopenapi_gen/visit/endpoint/endpoint_visitor.py +103 -0
  101. pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
  102. pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +121 -0
  103. pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +87 -0
  104. pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
  105. pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +497 -0
  106. pyopenapi_gen/visit/endpoint/generators/signature_generator.py +88 -0
  107. pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +183 -0
  108. pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
  109. pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +76 -0
  110. pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
  111. pyopenapi_gen/visit/exception_visitor.py +52 -0
  112. pyopenapi_gen/visit/model/__init__.py +0 -0
  113. pyopenapi_gen/visit/model/alias_generator.py +89 -0
  114. pyopenapi_gen/visit/model/dataclass_generator.py +197 -0
  115. pyopenapi_gen/visit/model/enum_generator.py +200 -0
  116. pyopenapi_gen/visit/model/model_visitor.py +197 -0
  117. pyopenapi_gen/visit/visitor.py +97 -0
  118. pyopenapi_gen-0.8.3.dist-info/METADATA +224 -0
  119. pyopenapi_gen-0.8.3.dist-info/RECORD +122 -0
  120. pyopenapi_gen-0.8.3.dist-info/WHEEL +4 -0
  121. pyopenapi_gen-0.8.3.dist-info/entry_points.txt +2 -0
  122. 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,5 @@
1
+ """High-level type resolution services."""
2
+
3
+ from .type_service import UnifiedTypeService
4
+
5
+ __all__ = ["UnifiedTypeService"]
@@ -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