pyopenapi-gen 2.7.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. pyopenapi_gen/__init__.py +224 -0
  2. pyopenapi_gen/__main__.py +6 -0
  3. pyopenapi_gen/cli.py +62 -0
  4. pyopenapi_gen/context/CLAUDE.md +284 -0
  5. pyopenapi_gen/context/file_manager.py +52 -0
  6. pyopenapi_gen/context/import_collector.py +382 -0
  7. pyopenapi_gen/context/render_context.py +726 -0
  8. pyopenapi_gen/core/CLAUDE.md +224 -0
  9. pyopenapi_gen/core/__init__.py +0 -0
  10. pyopenapi_gen/core/auth/base.py +22 -0
  11. pyopenapi_gen/core/auth/plugins.py +89 -0
  12. pyopenapi_gen/core/cattrs_converter.py +810 -0
  13. pyopenapi_gen/core/exceptions.py +20 -0
  14. pyopenapi_gen/core/http_status_codes.py +218 -0
  15. pyopenapi_gen/core/http_transport.py +222 -0
  16. pyopenapi_gen/core/loader/__init__.py +12 -0
  17. pyopenapi_gen/core/loader/loader.py +174 -0
  18. pyopenapi_gen/core/loader/operations/__init__.py +12 -0
  19. pyopenapi_gen/core/loader/operations/parser.py +161 -0
  20. pyopenapi_gen/core/loader/operations/post_processor.py +62 -0
  21. pyopenapi_gen/core/loader/operations/request_body.py +90 -0
  22. pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
  23. pyopenapi_gen/core/loader/parameters/parser.py +186 -0
  24. pyopenapi_gen/core/loader/responses/__init__.py +10 -0
  25. pyopenapi_gen/core/loader/responses/parser.py +111 -0
  26. pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
  27. pyopenapi_gen/core/loader/schemas/extractor.py +275 -0
  28. pyopenapi_gen/core/pagination.py +64 -0
  29. pyopenapi_gen/core/parsing/__init__.py +13 -0
  30. pyopenapi_gen/core/parsing/common/__init__.py +1 -0
  31. pyopenapi_gen/core/parsing/common/ref_resolution/__init__.py +9 -0
  32. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/__init__.py +0 -0
  33. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/cyclic_properties.py +66 -0
  34. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/direct_cycle.py +33 -0
  35. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/existing_schema.py +22 -0
  36. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +54 -0
  37. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +52 -0
  38. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +50 -0
  39. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +51 -0
  40. pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +86 -0
  41. pyopenapi_gen/core/parsing/common/type_parser.py +73 -0
  42. pyopenapi_gen/core/parsing/context.py +187 -0
  43. pyopenapi_gen/core/parsing/cycle_helpers.py +126 -0
  44. pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
  45. pyopenapi_gen/core/parsing/keywords/all_of_parser.py +81 -0
  46. pyopenapi_gen/core/parsing/keywords/any_of_parser.py +84 -0
  47. pyopenapi_gen/core/parsing/keywords/array_items_parser.py +72 -0
  48. pyopenapi_gen/core/parsing/keywords/one_of_parser.py +77 -0
  49. pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
  50. pyopenapi_gen/core/parsing/schema_finalizer.py +169 -0
  51. pyopenapi_gen/core/parsing/schema_parser.py +804 -0
  52. pyopenapi_gen/core/parsing/transformers/__init__.py +0 -0
  53. pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +285 -0
  54. pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +120 -0
  55. pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
  56. pyopenapi_gen/core/postprocess_manager.py +260 -0
  57. pyopenapi_gen/core/spec_fetcher.py +148 -0
  58. pyopenapi_gen/core/streaming_helpers.py +84 -0
  59. pyopenapi_gen/core/telemetry.py +69 -0
  60. pyopenapi_gen/core/utils.py +456 -0
  61. pyopenapi_gen/core/warning_collector.py +83 -0
  62. pyopenapi_gen/core/writers/code_writer.py +135 -0
  63. pyopenapi_gen/core/writers/documentation_writer.py +222 -0
  64. pyopenapi_gen/core/writers/line_writer.py +217 -0
  65. pyopenapi_gen/core/writers/python_construct_renderer.py +321 -0
  66. pyopenapi_gen/core_package_template/README.md +21 -0
  67. pyopenapi_gen/emit/models_emitter.py +143 -0
  68. pyopenapi_gen/emitters/CLAUDE.md +286 -0
  69. pyopenapi_gen/emitters/client_emitter.py +51 -0
  70. pyopenapi_gen/emitters/core_emitter.py +181 -0
  71. pyopenapi_gen/emitters/docs_emitter.py +44 -0
  72. pyopenapi_gen/emitters/endpoints_emitter.py +247 -0
  73. pyopenapi_gen/emitters/exceptions_emitter.py +187 -0
  74. pyopenapi_gen/emitters/mocks_emitter.py +185 -0
  75. pyopenapi_gen/emitters/models_emitter.py +426 -0
  76. pyopenapi_gen/generator/CLAUDE.md +352 -0
  77. pyopenapi_gen/generator/client_generator.py +567 -0
  78. pyopenapi_gen/generator/exceptions.py +7 -0
  79. pyopenapi_gen/helpers/CLAUDE.md +325 -0
  80. pyopenapi_gen/helpers/__init__.py +1 -0
  81. pyopenapi_gen/helpers/endpoint_utils.py +532 -0
  82. pyopenapi_gen/helpers/type_cleaner.py +334 -0
  83. pyopenapi_gen/helpers/type_helper.py +112 -0
  84. pyopenapi_gen/helpers/type_resolution/__init__.py +1 -0
  85. pyopenapi_gen/helpers/type_resolution/array_resolver.py +57 -0
  86. pyopenapi_gen/helpers/type_resolution/composition_resolver.py +79 -0
  87. pyopenapi_gen/helpers/type_resolution/finalizer.py +105 -0
  88. pyopenapi_gen/helpers/type_resolution/named_resolver.py +172 -0
  89. pyopenapi_gen/helpers/type_resolution/object_resolver.py +216 -0
  90. pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +109 -0
  91. pyopenapi_gen/helpers/type_resolution/resolver.py +47 -0
  92. pyopenapi_gen/helpers/url_utils.py +14 -0
  93. pyopenapi_gen/http_types.py +20 -0
  94. pyopenapi_gen/ir.py +165 -0
  95. pyopenapi_gen/py.typed +1 -0
  96. pyopenapi_gen/types/CLAUDE.md +140 -0
  97. pyopenapi_gen/types/__init__.py +11 -0
  98. pyopenapi_gen/types/contracts/__init__.py +13 -0
  99. pyopenapi_gen/types/contracts/protocols.py +106 -0
  100. pyopenapi_gen/types/contracts/types.py +28 -0
  101. pyopenapi_gen/types/resolvers/__init__.py +7 -0
  102. pyopenapi_gen/types/resolvers/reference_resolver.py +71 -0
  103. pyopenapi_gen/types/resolvers/response_resolver.py +177 -0
  104. pyopenapi_gen/types/resolvers/schema_resolver.py +498 -0
  105. pyopenapi_gen/types/services/__init__.py +5 -0
  106. pyopenapi_gen/types/services/type_service.py +165 -0
  107. pyopenapi_gen/types/strategies/__init__.py +5 -0
  108. pyopenapi_gen/types/strategies/response_strategy.py +310 -0
  109. pyopenapi_gen/visit/CLAUDE.md +272 -0
  110. pyopenapi_gen/visit/client_visitor.py +477 -0
  111. pyopenapi_gen/visit/docs_visitor.py +38 -0
  112. pyopenapi_gen/visit/endpoint/__init__.py +1 -0
  113. pyopenapi_gen/visit/endpoint/endpoint_visitor.py +292 -0
  114. pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
  115. pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +123 -0
  116. pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +222 -0
  117. pyopenapi_gen/visit/endpoint/generators/mock_generator.py +140 -0
  118. pyopenapi_gen/visit/endpoint/generators/overload_generator.py +252 -0
  119. pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
  120. pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +705 -0
  121. pyopenapi_gen/visit/endpoint/generators/signature_generator.py +83 -0
  122. pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +207 -0
  123. pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
  124. pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +78 -0
  125. pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
  126. pyopenapi_gen/visit/exception_visitor.py +90 -0
  127. pyopenapi_gen/visit/model/__init__.py +0 -0
  128. pyopenapi_gen/visit/model/alias_generator.py +93 -0
  129. pyopenapi_gen/visit/model/dataclass_generator.py +553 -0
  130. pyopenapi_gen/visit/model/enum_generator.py +212 -0
  131. pyopenapi_gen/visit/model/model_visitor.py +198 -0
  132. pyopenapi_gen/visit/visitor.py +97 -0
  133. pyopenapi_gen-2.7.2.dist-info/METADATA +1169 -0
  134. pyopenapi_gen-2.7.2.dist-info/RECORD +137 -0
  135. pyopenapi_gen-2.7.2.dist-info/WHEEL +4 -0
  136. pyopenapi_gen-2.7.2.dist-info/entry_points.txt +2 -0
  137. pyopenapi_gen-2.7.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,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)
@@ -0,0 +1,5 @@
1
+ """High-level type resolution services."""
2
+
3
+ from .type_service import UnifiedTypeService
4
+
5
+ __all__ = ["UnifiedTypeService"]