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,140 @@
1
+ """
2
+ Generator for creating mock method implementations.
3
+
4
+ This module generates mock methods that raise NotImplementedError,
5
+ allowing users to create test doubles by subclassing and overriding
6
+ only the methods they need.
7
+ """
8
+
9
+ from typing import Any
10
+
11
+ from ....context.render_context import RenderContext
12
+ from ....core.utils import NameSanitizer
13
+ from ....core.writers.code_writer import CodeWriter
14
+ from ....ir import IROperation
15
+ from .endpoint_method_generator import EndpointMethodGenerator
16
+
17
+
18
+ class MockGenerator:
19
+ """
20
+ Generates mock method implementations for testing.
21
+
22
+ Mock methods preserve the exact signature of the real implementation
23
+ but raise NotImplementedError with helpful error messages instead
24
+ of performing actual operations.
25
+ """
26
+
27
+ def __init__(self, schemas: dict[str, Any] | None = None) -> None:
28
+ self.schemas = schemas or {}
29
+ self.method_generator = EndpointMethodGenerator(self.schemas)
30
+
31
+ def generate(self, op: IROperation, context: RenderContext) -> str:
32
+ """
33
+ Generate a mock method that raises NotImplementedError.
34
+
35
+ Args:
36
+ op: The operation to generate a mock for
37
+ context: Render context for import tracking
38
+
39
+ Returns:
40
+ Complete mock method code as string
41
+ """
42
+ # Generate the full method using EndpointMethodGenerator
43
+ full_method = self.method_generator.generate(op, context)
44
+
45
+ # Parse and transform it to a mock implementation
46
+ return self._transform_to_mock(full_method, op)
47
+
48
+ def _transform_to_mock(self, full_method_code: str, op: IROperation) -> str:
49
+ """
50
+ Transform a full method implementation into a mock that raises NotImplementedError.
51
+
52
+ Args:
53
+ full_method_code: Complete method code from EndpointMethodGenerator
54
+ op: The operation (for generating error messages)
55
+
56
+ Returns:
57
+ Mock method code with NotImplementedError body
58
+ """
59
+ lines = full_method_code.split("\n")
60
+ writer = CodeWriter()
61
+
62
+ i = 0
63
+ while i < len(lines):
64
+ line = lines[i]
65
+ stripped = line.strip()
66
+
67
+ # Handle @overload decorator - keep it
68
+ if stripped.startswith("@overload"):
69
+ writer.write_line(stripped)
70
+ i += 1
71
+
72
+ # Copy overload signature until we hit `: ...`
73
+ while i < len(lines):
74
+ sig_line = lines[i]
75
+ sig_stripped = sig_line.strip()
76
+ writer.write_line(sig_stripped)
77
+
78
+ if sig_stripped.endswith(": ..."):
79
+ writer.write_line("") # Blank line after overload
80
+ i += 1
81
+ break
82
+
83
+ i += 1
84
+ continue
85
+
86
+ # Handle method definition (async def or def)
87
+ if (stripped.startswith("async def ") or stripped.startswith("def ")) and "(" in stripped:
88
+ # Determine if this is an async generator
89
+ is_async_generator = False
90
+
91
+ # Collect signature lines to check return type
92
+ signature_lines = []
93
+ temp_i = i
94
+ while temp_i < len(lines):
95
+ sig_stripped = lines[temp_i].strip()
96
+ signature_lines.append(sig_stripped)
97
+ if sig_stripped.endswith(":") and not sig_stripped.endswith(","):
98
+ # Check if AsyncIterator in return type
99
+ full_sig = " ".join(signature_lines)
100
+ is_async_generator = "AsyncIterator" in full_sig
101
+ break
102
+ temp_i += 1
103
+
104
+ # Write signature lines
105
+ for sig in signature_lines:
106
+ writer.write_line(sig)
107
+
108
+ # Write mock body
109
+ writer.indent()
110
+
111
+ # Docstring
112
+ writer.write_line('"""')
113
+ writer.write_line("Mock implementation that raises NotImplementedError.")
114
+ writer.write_line("")
115
+ writer.write_line("Override this method in your test subclass to provide")
116
+ writer.write_line("the behavior needed for your test scenario.")
117
+ writer.write_line('"""')
118
+
119
+ # Error message
120
+ method_name = NameSanitizer.sanitize_method_name(op.operation_id)
121
+ tag = op.tags[0] if op.tags else "Client"
122
+ class_name = f"Mock{NameSanitizer.sanitize_class_name(tag)}Client"
123
+ error_msg = (
124
+ f'"{class_name}.{method_name}() not implemented. ' f'Override this method in your test subclass."'
125
+ )
126
+ writer.write_line(f"raise NotImplementedError({error_msg})")
127
+
128
+ # For async generators, add unreachable yield for type checker
129
+ if is_async_generator:
130
+ writer.write_line("yield # pragma: no cover")
131
+
132
+ writer.dedent()
133
+
134
+ # Skip the rest of this method implementation in the original code
135
+ i = len(lines) # Exit the loop
136
+ break
137
+
138
+ i += 1
139
+
140
+ return writer.get_code()
@@ -0,0 +1,252 @@
1
+ """Generator for @overload signatures when operations have multiple content types."""
2
+
3
+ import logging
4
+ from typing import Any
5
+
6
+ from pyopenapi_gen import IROperation
7
+
8
+ from ....context.render_context import RenderContext
9
+ from ....core.utils import NameSanitizer
10
+ from ....core.writers.code_writer import CodeWriter
11
+ from ..processors.parameter_processor import EndpointParameterProcessor
12
+ from .docstring_generator import EndpointDocstringGenerator
13
+ from .signature_generator import EndpointMethodSignatureGenerator
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class OverloadMethodGenerator:
19
+ """
20
+ Generates @overload signatures for operations with multiple content types.
21
+
22
+ When an operation's request body accepts multiple content types
23
+ (e.g., application/json and multipart/form-data), this generator creates
24
+ type-safe @overload signatures following PEP 484.
25
+ """
26
+
27
+ def __init__(self, schemas: dict[str, Any] | None = None) -> None:
28
+ self.schemas = schemas or {}
29
+ self.parameter_processor = EndpointParameterProcessor(self.schemas)
30
+ self.signature_generator = EndpointMethodSignatureGenerator(self.schemas)
31
+ self.docstring_generator = EndpointDocstringGenerator(self.schemas)
32
+
33
+ def has_multiple_content_types(self, op: IROperation) -> bool:
34
+ """
35
+ Check if operation request body has multiple content types.
36
+
37
+ Args:
38
+ op: The operation to check
39
+
40
+ Returns:
41
+ True if operation has request body with multiple content types
42
+ """
43
+ if not op.request_body:
44
+ return False
45
+ return len(op.request_body.content) > 1
46
+
47
+ def generate_overload_signatures(
48
+ self, op: IROperation, context: RenderContext, response_strategy: Any
49
+ ) -> list[str]:
50
+ """
51
+ Generate @overload signatures for each content type.
52
+
53
+ Args:
54
+ op: The operation with multiple content types
55
+ context: Render context for import tracking
56
+ response_strategy: Response strategy for return type resolution
57
+
58
+ Returns:
59
+ List of @overload signature code strings
60
+ """
61
+ if not self.has_multiple_content_types(op):
62
+ return []
63
+
64
+ # Ensure typing.overload is imported
65
+ context.add_import("typing", "overload")
66
+ context.add_import("typing", "Literal")
67
+ context.add_import("typing", "IO")
68
+ context.add_import("typing", "Any")
69
+
70
+ overload_signatures = []
71
+
72
+ # Type narrowing: request_body is guaranteed to exist by has_multiple_content_types check
73
+ assert (
74
+ op.request_body is not None
75
+ ), "request_body should not be None after has_multiple_content_types check" # nosec B101 - Type narrowing for mypy, validated by has_multiple_content_types
76
+
77
+ for content_type, schema in op.request_body.content.items():
78
+ signature_code = self._generate_single_overload(op, content_type, schema, context, response_strategy)
79
+ overload_signatures.append(signature_code)
80
+
81
+ return overload_signatures
82
+
83
+ def _generate_single_overload(
84
+ self,
85
+ op: IROperation,
86
+ content_type: str,
87
+ schema: Any,
88
+ context: RenderContext,
89
+ response_strategy: Any,
90
+ ) -> str:
91
+ """
92
+ Generate a single @overload signature for one content type.
93
+
94
+ Args:
95
+ op: The operation
96
+ content_type: The media type (e.g., "application/json")
97
+ schema: The schema for this content type
98
+ context: Render context
99
+ response_strategy: Response strategy for return type
100
+
101
+ Returns:
102
+ @overload signature code as string
103
+ """
104
+ writer = CodeWriter()
105
+
106
+ # Write @overload decorator
107
+ writer.write_line("@overload")
108
+
109
+ # Determine parameter name and type based on content type
110
+ param_info = self._get_content_type_param_info(content_type, schema, context)
111
+
112
+ # Build parameter list from operation parameters directly
113
+ param_parts = ["self"]
114
+
115
+ # Add path, query, and header parameters from operation
116
+ if op.parameters:
117
+ from ....types.services.type_service import UnifiedTypeService
118
+
119
+ type_service = UnifiedTypeService(self.schemas)
120
+
121
+ for param in op.parameters:
122
+ if param.param_in in ("path", "query", "header"):
123
+ param_type = type_service.resolve_schema_type(param.schema, context, required=param.required)
124
+ sanitized_name = NameSanitizer.sanitize_method_name(param.name)
125
+ param_parts.append(f"{sanitized_name}: {param_type}")
126
+
127
+ # Add keyword-only separator
128
+ param_parts.append("*")
129
+
130
+ # Add content-type-specific parameter
131
+ param_parts.append(f"{param_info['name']}: {param_info['type']}")
132
+
133
+ # Add content_type parameter with Literal type
134
+ param_parts.append(f'content_type: Literal["{content_type}"] = "{content_type}"')
135
+
136
+ # Get return type from response strategy
137
+ return_type = response_strategy.return_type
138
+
139
+ # Sanitize method name to snake_case
140
+ method_name = NameSanitizer.sanitize_method_name(op.operation_id)
141
+
142
+ # Write signature
143
+ params_str = ",\n ".join(param_parts)
144
+ writer.write_line(f"async def {method_name}(")
145
+ writer.indent()
146
+ writer.write_line(params_str)
147
+ writer.dedent()
148
+ writer.write_line(f") -> {return_type}: ...")
149
+
150
+ return writer.get_code()
151
+
152
+ def _get_content_type_param_info(self, content_type: str, schema: Any, context: RenderContext) -> dict[str, str]:
153
+ """
154
+ Get parameter name and type hint for a content type.
155
+
156
+ Args:
157
+ content_type: Media type string
158
+ schema: Schema for this content type
159
+ context: Render context for type resolution
160
+
161
+ Returns:
162
+ Dictionary with 'name' and 'type' keys
163
+ """
164
+ from ....types.services.type_service import UnifiedTypeService
165
+
166
+ type_service = UnifiedTypeService(self.schemas)
167
+
168
+ # Map content types to parameter names and base types
169
+ if content_type == "application/json":
170
+ # For JSON, resolve the actual schema type
171
+ type_hint = type_service.resolve_schema_type(schema, context, required=True)
172
+ return {"name": "body", "type": type_hint}
173
+
174
+ elif content_type == "multipart/form-data":
175
+ # For multipart, always use files dict
176
+ return {"name": "files", "type": "dict[str, IO[Any]]"}
177
+
178
+ elif content_type == "application/x-www-form-urlencoded":
179
+ # For form data, use dict
180
+ return {"name": "data", "type": "dict[str, str]"}
181
+
182
+ else:
183
+ # Default: use body with Any type
184
+ logger.warning(f"Unknown content type {content_type}, using body: Any")
185
+ return {"name": "body", "type": "Any"}
186
+
187
+ def generate_implementation_signature(self, op: IROperation, context: RenderContext, response_strategy: Any) -> str:
188
+ """
189
+ Generate the actual implementation method signature with optional parameters.
190
+
191
+ This signature accepts all possible content-type parameters as optional,
192
+ and includes runtime dispatch logic.
193
+
194
+ Args:
195
+ op: The operation
196
+ context: Render context
197
+ response_strategy: Response strategy for return type
198
+
199
+ Returns:
200
+ Implementation method signature code
201
+ """
202
+ writer = CodeWriter()
203
+
204
+ # Build parameter list
205
+ param_parts = ["self"]
206
+
207
+ # Add path, query, and header parameters from operation
208
+ if op.parameters:
209
+ from ....types.services.type_service import UnifiedTypeService
210
+
211
+ type_service = UnifiedTypeService(self.schemas)
212
+
213
+ for param in op.parameters:
214
+ if param.param_in in ("path", "query", "header"):
215
+ param_type = type_service.resolve_schema_type(param.schema, context, required=param.required)
216
+ sanitized_name = NameSanitizer.sanitize_method_name(param.name)
217
+ param_parts.append(f"{sanitized_name}: {param_type}")
218
+
219
+ # Add keyword-only separator
220
+ param_parts.append("*")
221
+
222
+ # Add all possible content-type parameters as optional
223
+ if op.request_body:
224
+ param_types_seen = set()
225
+
226
+ for content_type, schema in op.request_body.content.items():
227
+ param_info = self._get_content_type_param_info(content_type, schema, context)
228
+
229
+ # Avoid duplicate parameters (e.g., if multiple JSON variants)
230
+ param_key = param_info["name"]
231
+ if param_key not in param_types_seen:
232
+ param_parts.append(f"{param_info['name']}: {param_info['type']} | None = None")
233
+ param_types_seen.add(param_key)
234
+
235
+ # Add content_type parameter (no Literal, just str)
236
+ param_parts.append('content_type: str = "application/json"')
237
+
238
+ # Get return type
239
+ return_type = response_strategy.return_type
240
+
241
+ # Sanitize method name to snake_case
242
+ method_name = NameSanitizer.sanitize_method_name(op.operation_id)
243
+
244
+ # Write signature
245
+ params_str = ",\n ".join(param_parts)
246
+ writer.write_line(f"async def {method_name}(")
247
+ writer.indent()
248
+ writer.write_line(params_str)
249
+ writer.dedent()
250
+ writer.write_line(f") -> {return_type}:")
251
+
252
+ return writer.get_code()
@@ -0,0 +1,103 @@
1
+ """
2
+ Helper class for generating the HTTP request call for an endpoint method.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ from pyopenapi_gen.core.writers.code_writer import CodeWriter
11
+
12
+ # No specific utils needed yet, but NameSanitizer might be if param names are manipulated here
13
+
14
+ if TYPE_CHECKING:
15
+ from pyopenapi_gen import IROperation
16
+ from pyopenapi_gen.context.render_context import RenderContext # For context.add_import if needed
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class EndpointRequestGenerator:
22
+ """Generates the self._transport.request(...) call for an endpoint method."""
23
+
24
+ def __init__(self, schemas: dict[str, Any] | None = None) -> None:
25
+ self.schemas: dict[str, Any] = schemas or {}
26
+
27
+ def generate_request_call(
28
+ self,
29
+ writer: CodeWriter,
30
+ op: IROperation,
31
+ context: RenderContext, # Pass context for potential import needs
32
+ has_header_params: bool,
33
+ primary_content_type: str | None,
34
+ # resolved_body_type: str | None, # May not be directly needed here if logic relies on var names
35
+ ) -> None:
36
+ """Writes the self._transport.request call to the CodeWriter."""
37
+ # Logic from EndpointMethodGenerator._write_request
38
+ args_list = []
39
+
40
+ # Determine if 'params' argument is needed for query parameters
41
+ # This relies on UrlArgsGenerator having created a 'params' dict if query params exist.
42
+ # A more robust way could be to check op.parameters directly, but this keeps coupling loose.
43
+ if any(p.param_in == "query" for p in op.parameters): # Check IROperation directly for query params
44
+ args_list.append("params=params")
45
+ else:
46
+ args_list.append("params=None")
47
+
48
+ # Determine 'json' and 'data' arguments based on request body
49
+ if op.request_body:
50
+ if primary_content_type == "application/json":
51
+ args_list.append("json=json_body") # Assumes json_body is defined
52
+ # args_list.append("data=None") # Not strictly needed if json is present, httpx handles it
53
+ elif primary_content_type and "multipart/form-data" in primary_content_type:
54
+ # For multipart, httpx uses 'files' or 'data' depending on content.
55
+ # UrlArgsGenerator created 'files_data'. Httpx typically uses 'files=' for file uploads.
56
+ # Let's assume 'files_data' is a dict suitable for 'files=' or 'data='
57
+ # If 'files_data' is specifically for file-like objects, 'files=files_data' is better.
58
+ # If it can also contain plain data, 'data=files_data' might be used by httpx.
59
+ # For simplicity and common use with files:
60
+ args_list.append("files=files_data") # Assumes files_data is defined
61
+ elif primary_content_type == "application/x-www-form-urlencoded":
62
+ args_list.append("data=form_data_body") # Assumes form_data_body is defined
63
+ elif primary_content_type: # Other types, like application/octet-stream
64
+ args_list.append("data=bytes_body") # Assumes bytes_body is defined
65
+ # else: # No specific content type handled, might mean no body or unhandled type
66
+ # args_list.append("json=None")
67
+ # args_list.append("data=None")
68
+ else: # No request body
69
+ args_list.append("json=None")
70
+ args_list.append("data=None")
71
+
72
+ # Determine 'headers' argument
73
+ if has_header_params: # This flag comes from UrlArgsGenerator
74
+ args_list.append("headers=headers") # Assumes headers dict is defined
75
+ else:
76
+ args_list.append("headers=None")
77
+
78
+ positional_args_str = f'"{op.method.upper()}", url' # url variable is assumed to be defined
79
+ keyword_args_str = ", ".join(args_list)
80
+
81
+ # Check length for formatting (120 is a common line length limit)
82
+ # Account for "response = await self._transport.request(" and ")" and surrounding spaces/indentation
83
+ # A rough estimate, effective line length for arguments should be less than ~120 - ~40 = 80
84
+ effective_args_len = len(positional_args_str) + len(", ") + len(keyword_args_str)
85
+
86
+ base_call_len = len("response = await self._transport.request()") + 2 # +2 for (,)
87
+
88
+ if base_call_len + effective_args_len <= 100: # Adjusted for typical black formatting preference
89
+ writer.write_line(f"response = await self._transport.request({positional_args_str}, {keyword_args_str})")
90
+ else:
91
+ writer.write_line(f"response = await self._transport.request(")
92
+ writer.indent()
93
+ writer.write_line(f"{positional_args_str},")
94
+ # Filter out "*=None" for cleaner multi-line calls if they are truly None and not just assigned None
95
+ # This might be overly complex here; httpx handles None correctly.
96
+ # Sticking to original logic for now.
97
+ num_args = len(args_list)
98
+ for i, arg in enumerate(args_list):
99
+ line_end = "," if i < num_args - 1 else ""
100
+ writer.write_line(f"{arg}{line_end}")
101
+ writer.dedent()
102
+ writer.write_line(")")
103
+ writer.write_line("") # Add a blank line for readability after the request call