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
pyopenapi_gen/ir.py ADDED
@@ -0,0 +1,167 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, Dict, List, Optional, Union
5
+
6
+ # Import NameSanitizer at the top for type hints and __post_init__ usage
7
+ from pyopenapi_gen.core.utils import NameSanitizer
8
+
9
+ # Import HTTPMethod as it's used by IROperation
10
+ from .http_types import HTTPMethod
11
+
12
+ # Forward declaration for IRSchema itself if needed for self-references in type hints
13
+ # class IRSchema:
14
+ # pass
15
+
16
+
17
+ @dataclass
18
+ class IRSchema:
19
+ name: Optional[str] = None
20
+ type: Optional[str] = None # E.g., "object", "array", "string", or a reference to another schema name
21
+ format: Optional[str] = None
22
+ description: Optional[str] = None
23
+ required: List[str] = field(default_factory=list)
24
+ properties: Dict[str, IRSchema] = field(default_factory=dict)
25
+ items: Optional[IRSchema] = None # For type: "array"
26
+ enum: Optional[List[Any]] = None
27
+ default: Optional[Any] = None # Added default value
28
+ example: Optional[Any] = None # Added example value
29
+ additional_properties: Optional[Union[bool, IRSchema]] = None # True, False, or an IRSchema
30
+ is_nullable: bool = False
31
+ any_of: Optional[List[IRSchema]] = None
32
+ one_of: Optional[List[IRSchema]] = None
33
+ all_of: Optional[List[IRSchema]] = None # Store the list of IRSchema objects from allOf
34
+ title: Optional[str] = None # Added title
35
+ is_data_wrapper: bool = False # True if schema is a simple {{ "data": OtherSchema }} wrapper
36
+
37
+ # Internal generator flags/helpers
38
+ _from_unresolved_ref: bool = field(
39
+ default=False, repr=False
40
+ ) # If this IRSchema is a placeholder for an unresolvable $ref
41
+ _refers_to_schema: Optional[IRSchema] = (
42
+ None # If this schema is a reference (e.g. a promoted property), this can link to the actual definition
43
+ )
44
+ _is_circular_ref: bool = field(default=False, repr=False) # If this IRSchema is part of a circular reference chain
45
+ _circular_ref_path: Optional[str] = field(default=None, repr=False) # Path of the circular reference
46
+ _max_depth_exceeded_marker: bool = field(
47
+ default=False, repr=False
48
+ ) # If parsing this schema or its components exceeded max depth
49
+ _is_self_referential_stub: bool = field(default=False, repr=False) # If this is a placeholder for allowed self-ref
50
+ _is_name_derived: bool = field(
51
+ default=False, repr=False
52
+ ) # True if the name was derived (e.g. for promoted inline objects)
53
+ _inline_name_resolution_path: Optional[str] = field(
54
+ default=None, repr=False
55
+ ) # Path used for resolving inline names
56
+
57
+ # Fields for storing final, de-collided names for code generation
58
+ generation_name: Optional[str] = field(default=None, repr=True) # Final class/enum name
59
+ final_module_stem: Optional[str] = field(default=None, repr=True) # Final module filename stem
60
+
61
+ def __post_init__(self) -> None:
62
+ # Ensure name is always a valid Python identifier if set
63
+ # This must happen BEFORE type inference that might use the name (though current logic doesn't)
64
+ if self.name:
65
+ # Store original name if needed for specific logic before sanitization, though not currently used here.
66
+ # original_name = self.name
67
+ self.name = NameSanitizer.sanitize_class_name(self.name)
68
+
69
+ # Ensure that if type is a reference (string not matching basic types),
70
+ # other structural fields like properties/items/enum are usually None or empty.
71
+ basic_types = ["object", "array", "string", "integer", "number", "boolean", "null"]
72
+ if self.type and self.type not in basic_types:
73
+ # This schema acts as a reference by name to another schema.
74
+ # It shouldn't typically define its own structure beyond description/nullability.
75
+ pass
76
+
77
+ # The check for is_valid_python_identifier is somewhat redundant if sanitize_class_name works correctly,
78
+ # but can be kept as a safeguard or for logging if a raw name was problematic *before* sanitization.
79
+ if self.name and not NameSanitizer.is_valid_python_identifier(self.name):
80
+ pass # logger.warning or handle as needed elsewhere
81
+
82
+ # Ensure nested schemas are IRSchema instances
83
+ if isinstance(self.items, dict):
84
+ self.items = IRSchema(**self.items)
85
+
86
+ if isinstance(self.properties, dict):
87
+ new_props = {}
88
+ for k, v in self.properties.items():
89
+ if isinstance(v, dict):
90
+ new_props[k] = IRSchema(**v)
91
+ elif isinstance(v, IRSchema): # Already an IRSchema instance
92
+ new_props[k] = v
93
+ # else: it might be some other unexpected type, raise error or log
94
+ self.properties = new_props
95
+
96
+ if isinstance(self.additional_properties, dict):
97
+ self.additional_properties = IRSchema(**self.additional_properties)
98
+
99
+ for comp_list_attr in ["any_of", "one_of", "all_of"]:
100
+ comp_list = getattr(self, comp_list_attr)
101
+ if isinstance(comp_list, list):
102
+ new_comp_list = []
103
+ for item in comp_list:
104
+ if isinstance(item, dict):
105
+ new_comp_list.append(IRSchema(**item))
106
+ elif isinstance(item, IRSchema):
107
+ new_comp_list.append(item)
108
+ # else: item is some other type, could skip or raise
109
+ setattr(self, comp_list_attr, new_comp_list)
110
+
111
+
112
+ # NameSanitizer is now imported at the top
113
+ # from pyopenapi_gen.core.utils import NameSanitizer
114
+
115
+
116
+ @dataclass(slots=True)
117
+ class IRParameter:
118
+ name: str
119
+ param_in: str # Renamed from 'in' to avoid keyword clash, was in_: str in original __init__.py
120
+ required: bool
121
+ schema: IRSchema
122
+ description: Optional[str] = None
123
+ # example: Optional[Any] = None # This was in my latest ir.py but not __init__.py, keeping it from my version
124
+
125
+
126
+ # Adding other IR classes from the original __init__.py structure
127
+ @dataclass(slots=True)
128
+ class IRResponse:
129
+ status_code: str # can be "default" or specific status like "200"
130
+ description: Optional[str]
131
+ content: Dict[str, IRSchema] # media‑type → schema mapping
132
+ stream: bool = False # Indicates a binary or streaming response
133
+ stream_format: Optional[str] = None # Indicates the stream type
134
+
135
+
136
+ @dataclass(slots=True)
137
+ class IRRequestBody:
138
+ required: bool
139
+ content: Dict[str, IRSchema] # media‑type → schema mapping
140
+ description: Optional[str] = None
141
+
142
+
143
+ @dataclass(slots=True)
144
+ class IROperation:
145
+ operation_id: str
146
+ method: HTTPMethod # Enforced via enum for consistency
147
+ path: str # e.g. "/pets/{petId}"
148
+ summary: Optional[str]
149
+ description: Optional[str]
150
+ parameters: List[IRParameter] = field(default_factory=list)
151
+ request_body: Optional[IRRequestBody] = None
152
+ responses: List[IRResponse] = field(default_factory=list)
153
+ tags: List[str] = field(default_factory=list)
154
+
155
+
156
+ @dataclass(slots=True)
157
+ class IRSpec:
158
+ title: str
159
+ version: str
160
+ description: Optional[str] = None
161
+ schemas: Dict[str, IRSchema] = field(default_factory=dict)
162
+ operations: List[IROperation] = field(default_factory=list)
163
+ servers: List[str] = field(default_factory=list)
164
+
165
+ # self._raw_schema_node = None
166
+
167
+ # def __setattr__(self, name, value):
pyopenapi_gen/py.typed ADDED
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,11 @@
1
+ """
2
+ Unified type resolution service for OpenAPI schema to Python type conversion.
3
+
4
+ This package provides a clean, testable architecture for resolving OpenAPI schemas,
5
+ references, and operation responses to Python type strings.
6
+
7
+ Architecture:
8
+ - contracts/: Interfaces and protocols
9
+ - resolvers/: Core resolution logic
10
+ - services/: High-level orchestration
11
+ """
@@ -0,0 +1,13 @@
1
+ """Type resolution contracts and interfaces."""
2
+
3
+ from .protocols import ReferenceResolver, ResponseTypeResolver, SchemaTypeResolver, TypeContext
4
+ from .types import ResolvedType, TypeResolutionError
5
+
6
+ __all__ = [
7
+ "ReferenceResolver",
8
+ "SchemaTypeResolver",
9
+ "ResponseTypeResolver",
10
+ "TypeContext",
11
+ "ResolvedType",
12
+ "TypeResolutionError",
13
+ ]
@@ -0,0 +1,106 @@
1
+ """Protocols for type resolution components."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Dict, Optional, Protocol, runtime_checkable
5
+
6
+ from pyopenapi_gen import IROperation, IRResponse, IRSchema
7
+
8
+ from .types import ResolvedType
9
+
10
+
11
+ @runtime_checkable
12
+ class TypeContext(Protocol):
13
+ """Context for type resolution operations."""
14
+
15
+ def add_import(self, module: str, name: str) -> None:
16
+ """Add an import to the context."""
17
+ ...
18
+
19
+ def add_conditional_import(self, condition: str, module: str, name: str) -> None:
20
+ """Add a conditional import (e.g., TYPE_CHECKING)."""
21
+ ...
22
+
23
+
24
+ class ReferenceResolver(ABC):
25
+ """Resolves OpenAPI $ref references to target schemas."""
26
+
27
+ # Concrete implementations should have these attributes
28
+ schemas: Dict[str, IRSchema]
29
+ responses: Dict[str, IRResponse]
30
+
31
+ @abstractmethod
32
+ def resolve_ref(self, ref: str) -> Optional[IRSchema]:
33
+ """
34
+ Resolve a $ref string to the target schema.
35
+
36
+ Args:
37
+ ref: Reference string like "#/components/schemas/User"
38
+
39
+ Returns:
40
+ Target schema or None if not found
41
+ """
42
+ pass
43
+
44
+ @abstractmethod
45
+ def resolve_response_ref(self, ref: str) -> Optional[IRResponse]:
46
+ """
47
+ Resolve a response $ref to the target response.
48
+
49
+ Args:
50
+ ref: Reference string like "#/components/responses/UserResponse"
51
+
52
+ Returns:
53
+ Target response or None if not found
54
+ """
55
+ pass
56
+
57
+
58
+ class SchemaTypeResolver(ABC):
59
+ """Resolves IRSchema objects to Python types."""
60
+
61
+ @abstractmethod
62
+ def resolve_schema(self, schema: IRSchema, context: TypeContext, required: bool = True) -> ResolvedType:
63
+ """
64
+ Resolve a schema to a Python type.
65
+
66
+ Args:
67
+ schema: The schema to resolve
68
+ context: Type resolution context
69
+ required: Whether the field is required
70
+
71
+ Returns:
72
+ Resolved Python type information
73
+ """
74
+ pass
75
+
76
+
77
+ class ResponseTypeResolver(ABC):
78
+ """Resolves operation responses to Python types."""
79
+
80
+ @abstractmethod
81
+ def resolve_operation_response(self, operation: IROperation, context: TypeContext) -> ResolvedType:
82
+ """
83
+ Resolve an operation's primary response to a Python type.
84
+
85
+ Args:
86
+ operation: The operation to resolve
87
+ context: Type resolution context
88
+
89
+ Returns:
90
+ Resolved Python type information
91
+ """
92
+ pass
93
+
94
+ @abstractmethod
95
+ def resolve_specific_response(self, response: IRResponse, context: TypeContext) -> ResolvedType:
96
+ """
97
+ Resolve a specific response to a Python type.
98
+
99
+ Args:
100
+ response: The response to resolve
101
+ context: Type resolution context
102
+
103
+ Returns:
104
+ Resolved Python type information
105
+ """
106
+ pass
@@ -0,0 +1,30 @@
1
+ """Core types for type resolution."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional
5
+
6
+
7
+ class TypeResolutionError(Exception):
8
+ """Raised when type resolution fails."""
9
+
10
+ pass
11
+
12
+
13
+ @dataclass
14
+ class ResolvedType:
15
+ """Result of type resolution."""
16
+
17
+ python_type: str
18
+ needs_import: bool = False
19
+ import_module: Optional[str] = None
20
+ import_name: Optional[str] = None
21
+ is_optional: bool = False
22
+ is_forward_ref: bool = False
23
+ was_unwrapped: bool = False
24
+
25
+ def __post_init__(self) -> None:
26
+ """Validate resolved type data."""
27
+ if self.needs_import and not self.import_module:
28
+ raise ValueError("needs_import=True requires import_module")
29
+ if self.needs_import and not self.import_name:
30
+ raise ValueError("needs_import=True requires import_name")
@@ -0,0 +1,7 @@
1
+ """Type resolution implementations."""
2
+
3
+ from .reference_resolver import OpenAPIReferenceResolver
4
+ from .response_resolver import OpenAPIResponseResolver
5
+ from .schema_resolver import OpenAPISchemaResolver
6
+
7
+ __all__ = ["OpenAPIReferenceResolver", "OpenAPISchemaResolver", "OpenAPIResponseResolver"]
@@ -0,0 +1,71 @@
1
+ """Reference resolver implementation."""
2
+
3
+ import logging
4
+ from typing import Dict, Optional
5
+
6
+ from pyopenapi_gen import IRResponse, IRSchema
7
+
8
+ from ..contracts.protocols import ReferenceResolver
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class OpenAPIReferenceResolver(ReferenceResolver):
14
+ """Resolves OpenAPI $ref references."""
15
+
16
+ def __init__(self, schemas: Dict[str, IRSchema], responses: Optional[Dict[str, IRResponse]] = None):
17
+ """
18
+ Initialize reference resolver.
19
+
20
+ Args:
21
+ schemas: Dictionary of schemas by name
22
+ responses: Dictionary of responses by name (optional)
23
+ """
24
+ self.schemas = schemas
25
+ self.responses = responses or {}
26
+
27
+ def resolve_ref(self, ref: str) -> Optional[IRSchema]:
28
+ """
29
+ Resolve a schema $ref to the target schema.
30
+
31
+ Args:
32
+ ref: Reference string like "#/components/schemas/User"
33
+
34
+ Returns:
35
+ Target schema or None if not found
36
+ """
37
+ if not ref.startswith("#/components/schemas/"):
38
+ logger.warning(f"Unsupported schema ref format: {ref}")
39
+ return None
40
+
41
+ schema_name = ref.split("/")[-1]
42
+ schema = self.schemas.get(schema_name)
43
+
44
+ if not schema:
45
+ logger.warning(f"Schema not found for ref: {ref}")
46
+ return None
47
+
48
+ return schema
49
+
50
+ def resolve_response_ref(self, ref: str) -> Optional[IRResponse]:
51
+ """
52
+ Resolve a response $ref to the target response.
53
+
54
+ Args:
55
+ ref: Reference string like "#/components/responses/UserResponse"
56
+
57
+ Returns:
58
+ Target response or None if not found
59
+ """
60
+ if not ref.startswith("#/components/responses/"):
61
+ logger.warning(f"Unsupported response ref format: {ref}")
62
+ return None
63
+
64
+ response_name = ref.split("/")[-1]
65
+ response = self.responses.get(response_name)
66
+
67
+ if not response:
68
+ logger.warning(f"Response not found for ref: {ref}")
69
+ return None
70
+
71
+ return response
@@ -0,0 +1,203 @@
1
+ """Response type resolver implementation."""
2
+
3
+ import logging
4
+ from typing import Optional
5
+
6
+ from pyopenapi_gen import IROperation, IRResponse, IRSchema
7
+
8
+ from ..contracts.protocols import ReferenceResolver, ResponseTypeResolver, SchemaTypeResolver, TypeContext
9
+ from ..contracts.types import ResolvedType
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class OpenAPIResponseResolver(ResponseTypeResolver):
15
+ """Resolves operation responses to Python types."""
16
+
17
+ def __init__(self, ref_resolver: ReferenceResolver, schema_resolver: SchemaTypeResolver):
18
+ """
19
+ Initialize response resolver.
20
+
21
+ Args:
22
+ ref_resolver: Reference resolver for handling $ref
23
+ schema_resolver: Schema resolver for handling schemas
24
+ """
25
+ self.ref_resolver = ref_resolver
26
+ self.schema_resolver = schema_resolver
27
+
28
+ def resolve_operation_response(self, operation: IROperation, context: TypeContext) -> ResolvedType:
29
+ """
30
+ Resolve an operation's primary response to a Python type.
31
+
32
+ Args:
33
+ operation: The operation to resolve
34
+ context: Type resolution context
35
+
36
+ Returns:
37
+ Resolved Python type information
38
+ """
39
+ primary_response = self._get_primary_response(operation)
40
+
41
+ if not primary_response:
42
+ return ResolvedType(python_type="None")
43
+
44
+ return self.resolve_specific_response(primary_response, context)
45
+
46
+ def resolve_specific_response(self, response: IRResponse, context: TypeContext) -> ResolvedType:
47
+ """
48
+ Resolve a specific response to a Python type.
49
+
50
+ Args:
51
+ response: The response to resolve
52
+ context: Type resolution context
53
+
54
+ Returns:
55
+ Resolved Python type information
56
+ """
57
+ # Handle response references
58
+ if hasattr(response, "ref") and response.ref:
59
+ return self._resolve_response_reference(response.ref, context)
60
+
61
+ # Handle responses without content (e.g., 204)
62
+ if not hasattr(response, "content") or not response.content:
63
+ return ResolvedType(python_type="None")
64
+
65
+ # Handle streaming responses
66
+ if hasattr(response, "stream") and response.stream:
67
+ return self._resolve_streaming_response(response, context)
68
+
69
+ # Get the content schema
70
+ schema = self._get_response_schema(response)
71
+ if not schema:
72
+ return ResolvedType(python_type="None")
73
+
74
+ # Resolve the schema
75
+ resolved = self.schema_resolver.resolve_schema(schema, context, required=True)
76
+
77
+ # Check for data unwrapping
78
+ unwrapped = self._try_unwrap_data_property(schema, context)
79
+ if unwrapped:
80
+ unwrapped.was_unwrapped = True
81
+ return unwrapped
82
+
83
+ return resolved
84
+
85
+ def _resolve_response_reference(self, ref: str, context: TypeContext) -> ResolvedType:
86
+ """Resolve a response $ref."""
87
+ target_response = self.ref_resolver.resolve_response_ref(ref)
88
+ if not target_response:
89
+ logger.warning(f"Could not resolve response reference: {ref}")
90
+ return ResolvedType(python_type="None")
91
+
92
+ return self.resolve_specific_response(target_response, context)
93
+
94
+ def _get_primary_response(self, operation: IROperation) -> Optional[IRResponse]:
95
+ """Get the primary success response from an operation."""
96
+ if not operation.responses:
97
+ return None
98
+
99
+ # Priority order: 200, 201, 202, 204, other 2xx, default
100
+ for code in ["200", "201", "202", "204"]:
101
+ for response in operation.responses:
102
+ if response.status_code == code:
103
+ return response
104
+
105
+ # Other 2xx responses
106
+ for response in operation.responses:
107
+ if response.status_code.startswith("2"):
108
+ return response
109
+
110
+ # Default response
111
+ for response in operation.responses:
112
+ if response.status_code == "default":
113
+ return response
114
+
115
+ # First response as fallback
116
+ return operation.responses[0] if operation.responses else None
117
+
118
+ def _get_response_schema(self, response: IRResponse) -> IRSchema | None:
119
+ """Get the schema from a response's content."""
120
+ if not response.content:
121
+ return None
122
+
123
+ # Prefer application/json
124
+ content_types = list(response.content.keys())
125
+ content_type = None
126
+
127
+ if "application/json" in content_types:
128
+ content_type = "application/json"
129
+ elif any("json" in ct for ct in content_types):
130
+ content_type = next(ct for ct in content_types if "json" in ct)
131
+ elif content_types:
132
+ content_type = content_types[0]
133
+
134
+ if not content_type:
135
+ return None
136
+
137
+ return response.content.get(content_type)
138
+
139
+ def _try_unwrap_data_property(self, schema: IRSchema | None, context: TypeContext) -> Optional[ResolvedType]:
140
+ """
141
+ Try to unwrap a 'data' property if the schema is a wrapper.
142
+
143
+ Returns unwrapped type or None if not applicable.
144
+ """
145
+ if not schema or not hasattr(schema, "type") or schema.type != "object":
146
+ return None
147
+
148
+ properties = getattr(schema, "properties", None)
149
+ if not properties or len(properties) != 1:
150
+ return None
151
+
152
+ # Check for 'data' property
153
+ data_property = properties.get("data")
154
+ if not data_property:
155
+ return None
156
+
157
+ logger.info(f"Unwrapping 'data' property from response schema")
158
+ return self.schema_resolver.resolve_schema(data_property, context, required=True)
159
+
160
+ def _resolve_streaming_response(self, response: IRResponse, context: TypeContext) -> ResolvedType:
161
+ """
162
+ Resolve a streaming response to an AsyncIterator type.
163
+
164
+ Args:
165
+ response: The streaming response
166
+ context: Type resolution context
167
+
168
+ Returns:
169
+ ResolvedType with AsyncIterator type
170
+ """
171
+ # Add AsyncIterator import
172
+ context.add_import("typing", "AsyncIterator")
173
+
174
+ # Determine the item type for the stream
175
+ if not response.content:
176
+ # Binary stream with no specific content type
177
+ return ResolvedType(python_type="AsyncIterator[bytes]")
178
+
179
+ # Check for binary content types
180
+ content_types = list(response.content.keys())
181
+ is_binary = any(
182
+ ct in ["application/octet-stream", "application/pdf"] or ct.startswith(("image/", "audio/", "video/"))
183
+ for ct in content_types
184
+ )
185
+
186
+ if is_binary:
187
+ return ResolvedType(python_type="AsyncIterator[bytes]")
188
+
189
+ # For event streams (text/event-stream) or JSON streams
190
+ is_event_stream = any("event-stream" in ct for ct in content_types)
191
+ if is_event_stream:
192
+ context.add_import("typing", "Dict")
193
+ context.add_import("typing", "Any")
194
+ return ResolvedType(python_type="AsyncIterator[Dict[str, Any]]")
195
+
196
+ # For other streaming content, try to resolve the schema
197
+ schema = self._get_response_schema(response)
198
+ if schema:
199
+ resolved = self.schema_resolver.resolve_schema(schema, context, required=True)
200
+ return ResolvedType(python_type=f"AsyncIterator[{resolved.python_type}]")
201
+
202
+ # Default to bytes if we can't determine the type
203
+ return ResolvedType(python_type="AsyncIterator[bytes]")