pyopenapi-gen 0.8.3__py3-none-any.whl → 0.8.5__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 (36) hide show
  1. pyopenapi_gen/cli.py +5 -22
  2. pyopenapi_gen/context/import_collector.py +8 -8
  3. pyopenapi_gen/core/loader/operations/parser.py +1 -1
  4. pyopenapi_gen/core/parsing/context.py +2 -1
  5. pyopenapi_gen/core/parsing/cycle_helpers.py +1 -1
  6. pyopenapi_gen/core/parsing/keywords/properties_parser.py +4 -4
  7. pyopenapi_gen/core/parsing/schema_parser.py +4 -4
  8. pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +1 -1
  9. pyopenapi_gen/core/postprocess_manager.py +39 -13
  10. pyopenapi_gen/core/schemas.py +101 -16
  11. pyopenapi_gen/core/writers/python_construct_renderer.py +57 -9
  12. pyopenapi_gen/emitters/endpoints_emitter.py +1 -1
  13. pyopenapi_gen/helpers/endpoint_utils.py +4 -22
  14. pyopenapi_gen/helpers/type_cleaner.py +1 -1
  15. pyopenapi_gen/helpers/type_resolution/composition_resolver.py +1 -1
  16. pyopenapi_gen/helpers/type_resolution/finalizer.py +1 -1
  17. pyopenapi_gen/types/contracts/types.py +0 -1
  18. pyopenapi_gen/types/resolvers/response_resolver.py +5 -33
  19. pyopenapi_gen/types/resolvers/schema_resolver.py +2 -2
  20. pyopenapi_gen/types/services/type_service.py +0 -18
  21. pyopenapi_gen/types/strategies/__init__.py +5 -0
  22. pyopenapi_gen/types/strategies/response_strategy.py +187 -0
  23. pyopenapi_gen/visit/endpoint/endpoint_visitor.py +1 -20
  24. pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +5 -3
  25. pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +12 -6
  26. pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +352 -343
  27. pyopenapi_gen/visit/endpoint/generators/signature_generator.py +7 -4
  28. pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +4 -2
  29. pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +1 -1
  30. pyopenapi_gen/visit/model/dataclass_generator.py +32 -1
  31. pyopenapi_gen-0.8.5.dist-info/METADATA +383 -0
  32. {pyopenapi_gen-0.8.3.dist-info → pyopenapi_gen-0.8.5.dist-info}/RECORD +35 -33
  33. pyopenapi_gen-0.8.3.dist-info/METADATA +0 -224
  34. {pyopenapi_gen-0.8.3.dist-info → pyopenapi_gen-0.8.5.dist-info}/WHEEL +0 -0
  35. {pyopenapi_gen-0.8.3.dist-info → pyopenapi_gen-0.8.5.dist-info}/entry_points.txt +0 -0
  36. {pyopenapi_gen-0.8.3.dist-info → pyopenapi_gen-0.8.5.dist-info}/licenses/LICENSE +0 -0
@@ -58,28 +58,21 @@ class OpenAPIResponseResolver(ResponseTypeResolver):
58
58
  if hasattr(response, "ref") and response.ref:
59
59
  return self._resolve_response_reference(response.ref, context)
60
60
 
61
+ # Handle streaming responses (check before content validation)
62
+ if hasattr(response, "stream") and response.stream:
63
+ return self._resolve_streaming_response(response, context)
64
+
61
65
  # Handle responses without content (e.g., 204)
62
66
  if not hasattr(response, "content") or not response.content:
63
67
  return ResolvedType(python_type="None")
64
68
 
65
- # Handle streaming responses
66
- if hasattr(response, "stream") and response.stream:
67
- return self._resolve_streaming_response(response, context)
68
-
69
69
  # Get the content schema
70
70
  schema = self._get_response_schema(response)
71
71
  if not schema:
72
72
  return ResolvedType(python_type="None")
73
73
 
74
- # Resolve the schema
74
+ # Resolve the schema directly (no unwrapping)
75
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
76
  return resolved
84
77
 
85
78
  def _resolve_response_reference(self, ref: str, context: TypeContext) -> ResolvedType:
@@ -136,27 +129,6 @@ class OpenAPIResponseResolver(ResponseTypeResolver):
136
129
 
137
130
  return response.content.get(content_type)
138
131
 
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
132
  def _resolve_streaming_response(self, response: IRResponse, context: TypeContext) -> ResolvedType:
161
133
  """
162
134
  Resolve a streaming response to an AsyncIterator type.
@@ -307,7 +307,7 @@ class OpenAPISchemaResolver(SchemaTypeResolver):
307
307
  # Sort types for consistent ordering
308
308
  resolved_types.sort()
309
309
  context.add_import("typing", "Union")
310
- union_type = f"Union[{', '.join(resolved_types)}]"
310
+ union_type = f"Union[{", ".join(resolved_types)}]"
311
311
 
312
312
  return ResolvedType(python_type=union_type, is_optional=not required)
313
313
 
@@ -362,6 +362,6 @@ class OpenAPISchemaResolver(SchemaTypeResolver):
362
362
  # Sort types for consistent ordering
363
363
  resolved_types.sort()
364
364
  context.add_import("typing", "Union")
365
- union_type = f"Union[{', '.join(resolved_types)}]"
365
+ union_type = f"Union[{", ".join(resolved_types)}]"
366
366
 
367
367
  return ResolvedType(python_type=union_type, is_optional=not required)
@@ -85,24 +85,6 @@ class UnifiedTypeService:
85
85
  resolved = self.response_resolver.resolve_operation_response(operation, type_context)
86
86
  return self._format_resolved_type(resolved, context)
87
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
88
  def resolve_response_type(self, response: IRResponse, context: RenderContext) -> str:
107
89
  """
108
90
  Resolve a specific response to a Python type string.
@@ -0,0 +1,5 @@
1
+ """Response handling strategies."""
2
+
3
+ from .response_strategy import ResponseStrategy, ResponseStrategyResolver
4
+
5
+ __all__ = ["ResponseStrategy", "ResponseStrategyResolver"]
@@ -0,0 +1,187 @@
1
+ """Unified response handling strategy.
2
+
3
+ This module provides a single source of truth for how operation responses should be handled,
4
+ eliminating the scattered responsibility that was causing Data_ vs proper schema name issues.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from dataclasses import dataclass
11
+ from typing import Dict, Optional
12
+
13
+ from pyopenapi_gen import IROperation, IRResponse, IRSchema
14
+ from pyopenapi_gen.context.render_context import RenderContext
15
+ from pyopenapi_gen.types.services.type_service import UnifiedTypeService
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ @dataclass
21
+ class ResponseStrategy:
22
+ """Unified strategy for handling a specific operation's response.
23
+
24
+ This class encapsulates all decisions about how to handle a response:
25
+ - What type to use in method signatures (matches OpenAPI schema exactly)
26
+ - Which schema to use for deserialization
27
+ - How to generate the response handling code
28
+ """
29
+
30
+ return_type: str # The Python type for method signature
31
+ response_schema: Optional[IRSchema] # The response schema as defined in OpenAPI spec
32
+ is_streaming: bool # Whether this is a streaming response
33
+
34
+ # Additional context for code generation
35
+ response_ir: Optional[IRResponse] # The original response IR
36
+
37
+
38
+ class ResponseStrategyResolver:
39
+ """Single source of truth for response handling decisions.
40
+
41
+ This resolver examines an operation and its responses to determine the optimal
42
+ strategy for handling the response. It replaces the scattered logic that was
43
+ previously spread across multiple components.
44
+ """
45
+
46
+ def __init__(self, schemas: Dict[str, IRSchema]):
47
+ self.schemas = schemas
48
+ self.type_service = UnifiedTypeService(schemas)
49
+
50
+ def resolve(self, operation: IROperation, context: RenderContext) -> ResponseStrategy:
51
+ """Determine how to handle this operation's response.
52
+
53
+ Uses the response schema exactly as defined in the OpenAPI spec,
54
+ with no unwrapping logic. What you see in the spec is what you get.
55
+
56
+ Args:
57
+ operation: The operation to analyze
58
+ context: Render context for type resolution
59
+
60
+ Returns:
61
+ A ResponseStrategy that all components should use consistently
62
+ """
63
+ primary_response = self._get_primary_response(operation)
64
+
65
+ if not primary_response:
66
+ return ResponseStrategy(return_type="None", response_schema=None, is_streaming=False, response_ir=None)
67
+
68
+ # Handle responses without content (e.g., 204)
69
+ if not hasattr(primary_response, "content") or not primary_response.content:
70
+ return ResponseStrategy(
71
+ return_type="None", response_schema=None, is_streaming=False, response_ir=primary_response
72
+ )
73
+
74
+ # Handle streaming responses
75
+ if hasattr(primary_response, "stream") and primary_response.stream:
76
+ return self._resolve_streaming_strategy(primary_response, context)
77
+
78
+ # Get the response schema
79
+ response_schema = self._get_response_schema(primary_response)
80
+ if not response_schema:
81
+ return ResponseStrategy(
82
+ return_type="None", response_schema=None, is_streaming=False, response_ir=primary_response
83
+ )
84
+
85
+ # Use the response schema as-is from the OpenAPI spec
86
+ return_type = self.type_service.resolve_schema_type(response_schema, context, required=True)
87
+
88
+ return ResponseStrategy(
89
+ return_type=return_type, response_schema=response_schema, is_streaming=False, response_ir=primary_response
90
+ )
91
+
92
+ def _get_primary_response(self, operation: IROperation) -> Optional[IRResponse]:
93
+ """Get the primary success response from an operation."""
94
+ if not operation.responses:
95
+ return None
96
+
97
+ # Priority order: 200, 201, 202, 204, other 2xx, default
98
+ for code in ["200", "201", "202", "204"]:
99
+ for response in operation.responses:
100
+ if response.status_code == code:
101
+ return response
102
+
103
+ # Other 2xx responses
104
+ for response in operation.responses:
105
+ if response.status_code.startswith("2"):
106
+ return response
107
+
108
+ # Default response
109
+ for response in operation.responses:
110
+ if response.status_code == "default":
111
+ return response
112
+
113
+ # First response as fallback
114
+ return operation.responses[0] if operation.responses else None
115
+
116
+ def _get_response_schema(self, response: IRResponse) -> Optional[IRSchema]:
117
+ """Get the schema from a response's content."""
118
+ if not response.content:
119
+ return None
120
+
121
+ # Prefer application/json
122
+ content_types = list(response.content.keys())
123
+ content_type = None
124
+
125
+ if "application/json" in content_types:
126
+ content_type = "application/json"
127
+ elif any("json" in ct for ct in content_types):
128
+ content_type = next(ct for ct in content_types if "json" in ct)
129
+ elif content_types:
130
+ content_type = content_types[0]
131
+
132
+ if not content_type:
133
+ return None
134
+
135
+ return response.content.get(content_type)
136
+
137
+ def _resolve_streaming_strategy(self, response: IRResponse, context: RenderContext) -> ResponseStrategy:
138
+ """Resolve strategy for streaming responses."""
139
+ # Add AsyncIterator import
140
+ context.add_import("typing", "AsyncIterator")
141
+
142
+ # Determine the item type for the stream
143
+ if not response.content:
144
+ # Binary stream with no specific content type
145
+ return ResponseStrategy(
146
+ return_type="AsyncIterator[bytes]", response_schema=None, is_streaming=True, response_ir=response
147
+ )
148
+
149
+ # Check for binary content types
150
+ content_types = list(response.content.keys())
151
+ is_binary = any(
152
+ ct in ["application/octet-stream", "application/pdf"] or ct.startswith(("image/", "audio/", "video/"))
153
+ for ct in content_types
154
+ )
155
+
156
+ if is_binary:
157
+ return ResponseStrategy(
158
+ return_type="AsyncIterator[bytes]", response_schema=None, is_streaming=True, response_ir=response
159
+ )
160
+
161
+ # For event streams (text/event-stream) or JSON streams
162
+ is_event_stream = any("event-stream" in ct for ct in content_types)
163
+ if is_event_stream:
164
+ context.add_import("typing", "Dict")
165
+ context.add_import("typing", "Any")
166
+ return ResponseStrategy(
167
+ return_type="AsyncIterator[Dict[str, Any]]",
168
+ response_schema=None,
169
+ is_streaming=True,
170
+ response_ir=response,
171
+ )
172
+
173
+ # For other streaming content, try to resolve the schema
174
+ schema = self._get_response_schema(response)
175
+ if schema:
176
+ schema_type = self.type_service.resolve_schema_type(schema, context, required=True)
177
+ return ResponseStrategy(
178
+ return_type=f"AsyncIterator[{schema_type}]",
179
+ response_schema=schema,
180
+ is_streaming=True,
181
+ response_ir=response,
182
+ )
183
+
184
+ # Default to bytes if we can't determine the type
185
+ return ResponseStrategy(
186
+ return_type="AsyncIterator[bytes]", response_schema=None, is_streaming=True, response_ir=response
187
+ )
@@ -2,10 +2,8 @@ import logging
2
2
  from typing import Any
3
3
 
4
4
  from pyopenapi_gen import IROperation
5
- from pyopenapi_gen.helpers.endpoint_utils import (
6
- get_return_type_unified,
7
- )
8
5
 
6
+ # No longer need endpoint utils helpers - using ResponseStrategy pattern
9
7
  from ...context.render_context import RenderContext
10
8
  from ...core.utils import NameSanitizer
11
9
  from ...core.writers.code_writer import CodeWriter
@@ -84,20 +82,3 @@ class EndpointVisitor(Visitor[IROperation, str]):
84
82
 
85
83
  writer.dedent() # Dedent to close the class block
86
84
  return writer.get_code()
87
-
88
- def _get_response_return_type_details(self, context: RenderContext, op: IROperation) -> tuple[str, bool, bool, str]:
89
- """Gets type details for the endpoint response."""
90
- # Check if this is a streaming response (either at op level or in schema)
91
- is_streaming = any(getattr(resp, "stream", False) for resp in op.responses if resp.status_code.startswith("2"))
92
-
93
- # Get the primary Python type for the operation's success response using unified service
94
- return_type = get_return_type_unified(op, context, self.schemas)
95
- should_unwrap = False # Unified service handles unwrapping internally
96
-
97
- # Determine the summary description (for docstring)
98
- success_resp = next((r for r in op.responses if r.status_code.startswith("2")), None)
99
- return_description = (
100
- success_resp.description if success_resp and success_resp.description else "Successful operation"
101
- )
102
-
103
- return return_type, should_unwrap, is_streaming, return_description
@@ -10,11 +10,12 @@ from typing import TYPE_CHECKING, Any, Dict, Optional
10
10
 
11
11
  from pyopenapi_gen.core.writers.code_writer import CodeWriter
12
12
  from pyopenapi_gen.core.writers.documentation_writer import DocumentationBlock, DocumentationWriter
13
- from pyopenapi_gen.helpers.endpoint_utils import get_param_type, get_request_body_type, get_return_type_unified
13
+ from pyopenapi_gen.helpers.endpoint_utils import get_param_type, get_request_body_type
14
14
 
15
15
  if TYPE_CHECKING:
16
16
  from pyopenapi_gen import IROperation
17
17
  from pyopenapi_gen.context.render_context import RenderContext
18
+ from pyopenapi_gen.types.strategies.response_strategy import ResponseStrategy
18
19
 
19
20
  logger = logging.getLogger(__name__)
20
21
 
@@ -50,6 +51,7 @@ class EndpointDocstringGenerator:
50
51
  op: IROperation,
51
52
  context: RenderContext,
52
53
  primary_content_type: Optional[str],
54
+ response_strategy: ResponseStrategy,
53
55
  ) -> None:
54
56
  """Writes the method docstring to the provided CodeWriter."""
55
57
  summary = op.summary or None
@@ -75,7 +77,7 @@ class EndpointDocstringGenerator:
75
77
  else: # Fallback for other types like application/octet-stream
76
78
  args.append(("bytes_content", "bytes", body_desc + f" ({primary_content_type})"))
77
79
 
78
- return_type = get_return_type_unified(op, context, self.schemas)
80
+ return_type = response_strategy.return_type
79
81
  response_desc = None
80
82
  # Prioritize 2xx success codes for the main response description
81
83
  for code in ("200", "201", "202", "default"): # Include default as it might be the success response
@@ -97,7 +99,7 @@ class EndpointDocstringGenerator:
97
99
  for resp in error_codes:
98
100
  # Using a generic HTTPError, specific error classes could be mapped later
99
101
  code_to_raise = "HTTPError"
100
- desc = f"{resp.status_code}: {resp.description.strip() if resp.description else 'HTTP error.'}"
102
+ desc = f"{resp.status_code}: {resp.description.strip() if resp.description else "HTTP error."}"
101
103
  raises.append((code_to_raise, desc))
102
104
  else:
103
105
  raises.append(("HTTPError", "If the server returns a non-2xx HTTP response."))
@@ -6,6 +6,7 @@ from pyopenapi_gen import IROperation
6
6
  from ....context.render_context import RenderContext
7
7
  from ....core.utils import Formatter
8
8
  from ....core.writers.code_writer import CodeWriter
9
+ from ....types.strategies import ResponseStrategyResolver
9
10
  from ..processors.import_analyzer import EndpointImportAnalyzer
10
11
  from ..processors.parameter_processor import EndpointParameterProcessor
11
12
  from .docstring_generator import EndpointDocstringGenerator
@@ -43,16 +44,21 @@ class EndpointMethodGenerator:
43
44
  context.add_import(f"{context.core_package_name}.http_transport", "HttpTransport")
44
45
  context.add_import(f"{context.core_package_name}.exceptions", "HTTPError")
45
46
 
46
- # Special case for updateAgentDataSource was removed.
47
+ # UNIFIED RESPONSE STRATEGY: Resolve once, use everywhere
48
+ strategy_resolver = ResponseStrategyResolver(self.schemas)
49
+ response_strategy = strategy_resolver.resolve(op, context)
47
50
 
48
- self.import_analyzer.analyze_and_register_imports(op, context)
51
+ # Pass the response strategy to import analyzer for consistent import resolution
52
+ self.import_analyzer.analyze_and_register_imports(op, context, response_strategy)
49
53
 
50
54
  ordered_params, primary_content_type, resolved_body_type = self.parameter_processor.process_parameters(
51
55
  op, context
52
56
  )
53
- self.signature_generator.generate_signature(writer, op, context, ordered_params)
54
57
 
55
- self.docstring_generator.generate_docstring(writer, op, context, primary_content_type)
58
+ # Pass strategy to generators for consistent behavior
59
+ self.signature_generator.generate_signature(writer, op, context, ordered_params, response_strategy)
60
+
61
+ self.docstring_generator.generate_docstring(writer, op, context, primary_content_type, response_strategy)
56
62
 
57
63
  # Snapshot of code *before* main body parts are written
58
64
  # This includes signature and docstring.
@@ -63,8 +69,8 @@ class EndpointMethodGenerator:
63
69
  )
64
70
  self.request_generator.generate_request_call(writer, op, context, has_header_params, primary_content_type)
65
71
 
66
- # Call the new response handler generator
67
- self.response_handler_generator.generate_response_handling(writer, op, context)
72
+ # Call the new response handler generator with strategy
73
+ self.response_handler_generator.generate_response_handling(writer, op, context, response_strategy)
68
74
 
69
75
  # Check if any actual statements were added for the body
70
76
  current_full_code = writer.get_code()