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,292 @@
1
+ import logging
2
+ from typing import Any
3
+
4
+ from pyopenapi_gen import IROperation
5
+
6
+ # No longer need endpoint utils helpers - using ResponseStrategy pattern
7
+ from ...context.render_context import RenderContext
8
+ from ...core.utils import NameSanitizer
9
+ from ...core.writers.code_writer import CodeWriter
10
+ from ..visitor import Visitor
11
+ from .generators.endpoint_method_generator import EndpointMethodGenerator
12
+
13
+ # Get logger instance
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class EndpointVisitor(Visitor[IROperation, str]):
18
+ """
19
+ Visitor for rendering a Python endpoint client method/class from an IROperation.
20
+ The method generation part is delegated to EndpointMethodGenerator.
21
+ This class remains responsible for assembling methods into a class (emit_endpoint_client_class).
22
+ Returns the rendered code as a string (does not write files).
23
+ """
24
+
25
+ def __init__(self, schemas: dict[str, Any] | None = None) -> None:
26
+ self.schemas = schemas or {}
27
+ # Formatter is likely not needed here anymore if all formatting happens in EndpointMethodGenerator
28
+ # self.formatter = Formatter()
29
+
30
+ def visit_IROperation(self, op: IROperation, context: RenderContext) -> str:
31
+ """
32
+ Generate a fully functional async endpoint method for the given operation
33
+ by delegating to EndpointMethodGenerator.
34
+ Returns the method code as a string.
35
+ """
36
+ # Instantiate the new generator
37
+ method_generator = EndpointMethodGenerator(schemas=self.schemas)
38
+ return method_generator.generate(op, context)
39
+
40
+ def emit_endpoint_client_class(
41
+ self,
42
+ tag: str,
43
+ method_codes: list[str],
44
+ context: RenderContext,
45
+ operations: list[IROperation] | None = None,
46
+ ) -> str:
47
+ """
48
+ Emit the endpoint client class for a tag, aggregating all endpoint methods.
49
+ The generated class is fully type-annotated and uses HttpTransport for HTTP communication.
50
+ Args:
51
+ tag: The tag name for the endpoint group.
52
+ method_codes: List of method code blocks as strings.
53
+ context: The RenderContext for import tracking.
54
+ operations: List of operations for Protocol generation (optional for backwards compatibility).
55
+ """
56
+ # Generate Protocol if operations provided
57
+ protocol_code = ""
58
+ if operations:
59
+ protocol_code = self.generate_endpoint_protocol(tag, operations, context)
60
+
61
+ # Generate implementation
62
+ impl_code = self._generate_endpoint_implementation(tag, method_codes, context)
63
+
64
+ # Combine Protocol and implementation
65
+ if protocol_code:
66
+ return f"{protocol_code}\n\n\n{impl_code}"
67
+ else:
68
+ return impl_code
69
+
70
+ def generate_endpoint_protocol(self, tag: str, operations: list[IROperation], context: RenderContext) -> str:
71
+ """
72
+ Generate Protocol definition for tag-based endpoint client.
73
+
74
+ Args:
75
+ tag: The tag name for the endpoint group
76
+ operations: List of operations for this tag
77
+ context: Render context for import management
78
+
79
+ Returns:
80
+ Protocol class code as string with all operation method signatures
81
+ """
82
+ # Register Protocol imports
83
+ context.add_import("typing", "Protocol")
84
+ context.add_import("typing", "runtime_checkable")
85
+
86
+ writer = CodeWriter()
87
+ class_name = NameSanitizer.sanitize_class_name(tag) + "Client"
88
+ protocol_name = f"{class_name}Protocol"
89
+
90
+ # Protocol class header
91
+ writer.write_line("@runtime_checkable")
92
+ writer.write_line(f"class {protocol_name}(Protocol):")
93
+ writer.indent()
94
+
95
+ # Docstring
96
+ writer.write_line(f'"""Protocol defining the interface of {class_name} for dependency injection."""')
97
+ writer.write_line("")
98
+
99
+ # Generate method signatures from operations
100
+ # We need to extract complete signatures including multi-line ones and decorators
101
+ # For Protocol, we only include the method signatures with ..., not implementations
102
+ # IMPORTANT: Preserve multi-line formatting for readability
103
+ for op in operations:
104
+ method_generator = EndpointMethodGenerator(schemas=self.schemas)
105
+ full_method_code = method_generator.generate(op, context)
106
+
107
+ # Parse the generated code to extract method signatures
108
+ # We want: @overload stubs (already have ...) and final signature converted to stub
109
+ lines = full_method_code.split("\n")
110
+ i = 0
111
+
112
+ while i < len(lines):
113
+ line = lines[i]
114
+ stripped = line.strip()
115
+
116
+ # Handle @overload decorator
117
+ if stripped.startswith("@overload"):
118
+ # Write decorator
119
+ writer.write_line(stripped)
120
+ i += 1
121
+
122
+ # Now process the signature following the decorator
123
+ # Keep collecting lines until we hit the end of the overload signature
124
+ while i < len(lines):
125
+ sig_line = lines[i]
126
+ sig_stripped = sig_line.strip()
127
+
128
+ # Write each line of the signature
129
+ writer.write_line(sig_stripped)
130
+
131
+ # Check for end of overload signature (ends with `: ...`)
132
+ if sig_stripped.endswith(": ..."):
133
+ writer.write_line("") # Blank line after overload
134
+ i += 1
135
+ break
136
+
137
+ i += 1
138
+ continue
139
+
140
+ # Handle non-overload method signatures (the final implementation signature)
141
+ if stripped.startswith("async def ") and "(" in stripped:
142
+ # This is the start of a method signature
143
+ # We need to collect all lines until we hit the colon
144
+ signature_lines = []
145
+
146
+ # Collect signature lines
147
+ while i < len(lines):
148
+ sig_line = lines[i]
149
+ sig_stripped = sig_line.strip()
150
+
151
+ signature_lines.append(sig_stripped)
152
+
153
+ # Check if this completes the signature (ends with :)
154
+ if sig_stripped.endswith(":") and not sig_stripped.endswith(","):
155
+ # This is the final line of the signature
156
+ # For Protocol, convert to stub format
157
+
158
+ # Check if this is an async generator (returns AsyncIterator)
159
+ # If so, remove 'async' from the first line
160
+ is_async_generator = "AsyncIterator" in sig_stripped
161
+
162
+ # Write all lines except the last
163
+ for idx, sig in enumerate(signature_lines[:-1]):
164
+ # For async generators, remove 'async ' from method definition
165
+ if idx == 0 and is_async_generator and sig.startswith("async def "):
166
+ sig = sig.replace("async def ", "def ", 1)
167
+ writer.write_line(sig)
168
+
169
+ # Write last line with ... instead of :
170
+ last_line = signature_lines[-1]
171
+ if last_line.endswith(":"):
172
+ last_line = last_line[:-1] # Remove trailing :
173
+ writer.write_line(f"{last_line}: ...")
174
+ writer.write_line("") # Blank line after method
175
+
176
+ # For Protocol, we only want the signature stub, not the implementation
177
+ # Skip all remaining lines of this method by jumping to end
178
+ i = len(lines) # This will exit the while loop
179
+ break
180
+
181
+ i += 1
182
+ continue
183
+
184
+ i += 1
185
+
186
+ writer.dedent() # Close class
187
+ return writer.get_code()
188
+
189
+ def _generate_endpoint_implementation(self, tag: str, method_codes: list[str], context: RenderContext) -> str:
190
+ """
191
+ Generate the endpoint client implementation class.
192
+
193
+ Args:
194
+ tag: The tag name for the endpoint group
195
+ method_codes: List of method code blocks as strings
196
+ context: Render context for import management
197
+
198
+ Returns:
199
+ Implementation class code as string
200
+ """
201
+ context.add_import("typing", "cast")
202
+ # Import core transport and streaming helpers
203
+ context.add_import(f"{context.core_package_name}.http_transport", "HttpTransport")
204
+ context.add_import(f"{context.core_package_name}.streaming_helpers", "iter_bytes")
205
+ context.add_import("typing", "Callable")
206
+ context.add_import("typing", "Optional")
207
+ writer = CodeWriter()
208
+ class_name = NameSanitizer.sanitize_class_name(tag) + "Client"
209
+ protocol_name = f"{class_name}Protocol"
210
+
211
+ # Class definition - implements Protocol
212
+ writer.write_line(f"class {class_name}({protocol_name}):")
213
+ writer.indent()
214
+ writer.write_line(f'"""Client for {tag} endpoints. Uses HttpTransport for all HTTP and header management."""')
215
+ writer.write_line("")
216
+
217
+ writer.write_line("def __init__(self, transport: HttpTransport, base_url: str) -> None:")
218
+ writer.indent()
219
+ writer.write_line("self._transport = transport")
220
+ writer.write_line("self.base_url: str = base_url")
221
+ writer.dedent()
222
+ writer.write_line("")
223
+
224
+ # Write methods
225
+ for i, method_code in enumerate(method_codes):
226
+ # Revert to write_block, as it handles indentation correctly
227
+ writer.write_block(method_code)
228
+
229
+ if i < len(method_codes) - 1:
230
+ writer.write_line("") # First blank line
231
+ writer.write_line("") # Second blank line (for testing separation)
232
+
233
+ writer.dedent() # Dedent to close the class block
234
+ return writer.get_code()
235
+
236
+ def generate_endpoint_mock_class(self, tag: str, operations: list[IROperation], context: RenderContext) -> str:
237
+ """
238
+ Generate mock implementation class for tag-based endpoint client.
239
+
240
+ Args:
241
+ tag: The tag name for the endpoint group
242
+ operations: List of operations for this tag
243
+ context: Render context for import management
244
+
245
+ Returns:
246
+ Mock class code as string with all operation method stubs
247
+ """
248
+ from .generators.mock_generator import MockGenerator
249
+
250
+ # Import Protocol for type checking
251
+ context.add_import("typing", "TYPE_CHECKING")
252
+
253
+ writer = CodeWriter()
254
+ class_name = NameSanitizer.sanitize_class_name(tag) + "Client"
255
+ protocol_name = f"{class_name}Protocol"
256
+ mock_class_name = f"Mock{class_name}"
257
+
258
+ # TYPE_CHECKING import for Protocol
259
+ writer.write_line("if TYPE_CHECKING:")
260
+ writer.indent()
261
+ writer.write_line(f"from ...endpoints.{NameSanitizer.sanitize_module_name(tag)} import {protocol_name}")
262
+ writer.dedent()
263
+ writer.write_line("")
264
+
265
+ # Class header with docstring
266
+ writer.write_line(f"class {mock_class_name}:")
267
+ writer.indent()
268
+ writer.write_line('"""')
269
+ writer.write_line(f"Mock implementation of {class_name} for testing.")
270
+ writer.write_line("")
271
+ writer.write_line("Provides default implementations that raise NotImplementedError.")
272
+ writer.write_line("Override methods as needed in your tests.")
273
+ writer.write_line("")
274
+ writer.write_line("Example:")
275
+ writer.write_line(f" class Test{class_name}({mock_class_name}):")
276
+ writer.write_line(" async def method_name(self, ...) -> ReturnType:")
277
+ writer.write_line(" return test_data")
278
+ writer.write_line('"""')
279
+ writer.write_line("")
280
+
281
+ # Generate mock methods
282
+ mock_generator = MockGenerator(schemas=self.schemas)
283
+ for i, op in enumerate(operations):
284
+ mock_method_code = mock_generator.generate(op, context)
285
+ writer.write_block(mock_method_code)
286
+
287
+ if i < len(operations) - 1:
288
+ writer.write_line("") # Blank line between methods
289
+ writer.write_line("") # Second blank line for consistency
290
+
291
+ writer.dedent() # Close class
292
+ return writer.get_code()
@@ -0,0 +1 @@
1
+ # पैसाले सुख दिदैन।
@@ -0,0 +1,123 @@
1
+ """
2
+ Helper class for generating the docstring for an endpoint method.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ import textwrap # For _wrap_docstring logic
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ from pyopenapi_gen.core.writers.code_writer import CodeWriter
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
14
+
15
+ if TYPE_CHECKING:
16
+ from pyopenapi_gen import IROperation
17
+ from pyopenapi_gen.context.render_context import RenderContext
18
+ from pyopenapi_gen.types.strategies.response_strategy import ResponseStrategy
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class EndpointDocstringGenerator:
24
+ """Generates the Python docstring for an endpoint operation."""
25
+
26
+ def __init__(self, schemas: dict[str, Any] | None = None) -> None:
27
+ self.schemas: dict[str, Any] = schemas or {}
28
+ self.doc_writer = DocumentationWriter(width=88)
29
+
30
+ def _wrap_docstring(self, prefix: str, text: str, width: int = 88) -> str:
31
+ """Internal helper to wrap text for docstrings."""
32
+ # This was a staticmethod in EndpointMethodGenerator, can be a helper here.
33
+ if not text:
34
+ return prefix.rstrip()
35
+ initial_indent = prefix
36
+ subsequent_indent = " " * len(prefix)
37
+ wrapped = textwrap.wrap(text, width=width, initial_indent=initial_indent, subsequent_indent=subsequent_indent)
38
+ # The original had "\n ".join(wrapped), which might be too specific if prefix changes.
39
+ # Let's ensure it joins with newline and respects the subsequent_indent for multi-lines.
40
+ if not wrapped:
41
+ return prefix.rstrip()
42
+ # For single line, no complex join needed, just the wrapped line.
43
+ if len(wrapped) == 1:
44
+ return wrapped[0]
45
+ # For multi-line, ensure proper joining. textwrap handles indent per line.
46
+ return "\n".join(wrapped)
47
+
48
+ def generate_docstring(
49
+ self,
50
+ writer: CodeWriter,
51
+ op: IROperation,
52
+ context: RenderContext,
53
+ primary_content_type: str | None,
54
+ response_strategy: ResponseStrategy,
55
+ ) -> None:
56
+ """Writes the method docstring to the provided CodeWriter."""
57
+ summary = op.summary or None
58
+ description = op.description or None
59
+ args: list[tuple[str, str, str] | tuple[str, str]] = []
60
+
61
+ for param in op.parameters:
62
+ param_type = get_param_type(param, context, self.schemas)
63
+ desc = param.description or ""
64
+ args.append((param.name, param_type, desc))
65
+
66
+ if op.request_body and primary_content_type:
67
+ body_desc = op.request_body.description or "Request body."
68
+ # Standardized body parameter names based on content type
69
+ if primary_content_type == "multipart/form-data":
70
+ args.append(("files", "dict[str, IO[Any]]", body_desc + " (multipart/form-data)"))
71
+ elif primary_content_type == "application/x-www-form-urlencoded":
72
+ # The type here could be more specific if schema is available, but dict[str, Any] is a safe default.
73
+ args.append(("form_data", "dict[str, Any]", body_desc + " (x-www-form-urlencoded)"))
74
+ elif primary_content_type == "application/json":
75
+ body_type = get_request_body_type(op.request_body, context, self.schemas)
76
+ args.append(("body", body_type, body_desc + " (json)"))
77
+ else: # Fallback for other types like application/octet-stream
78
+ args.append(("bytes_content", "bytes", body_desc + f" ({primary_content_type})"))
79
+
80
+ return_type = response_strategy.return_type
81
+ response_desc = None
82
+ # Prioritize 2xx success codes for the main response description
83
+ for code in ("200", "201", "202", "default"): # Include default as it might be the success response
84
+ resp = next((r for r in op.responses if r.status_code == code), None)
85
+ if resp and resp.description:
86
+ response_desc = resp.description.strip()
87
+ break
88
+ if not response_desc: # Fallback to any response description if no 2xx/default found
89
+ for resp in op.responses:
90
+ if resp.description:
91
+ response_desc = resp.description.strip()
92
+ break
93
+
94
+ returns = (return_type, response_desc or "Response object.") if return_type and return_type != "None" else None
95
+
96
+ error_codes = [r for r in op.responses if r.status_code.isdigit() and int(r.status_code) >= 400]
97
+ raises = []
98
+ if error_codes:
99
+ for resp in error_codes:
100
+ # Using a generic HTTPError, specific error classes could be mapped later
101
+ code_to_raise = "HTTPError"
102
+ desc = f"{resp.status_code}: {resp.description.strip() if resp.description else 'HTTP error.'}"
103
+ raises.append((code_to_raise, desc))
104
+ else:
105
+ raises.append(("HTTPError", "If the server returns a non-2xx HTTP response."))
106
+
107
+ doc_block = DocumentationBlock(
108
+ summary=summary,
109
+ description=description,
110
+ args=args,
111
+ returns=returns,
112
+ raises=raises,
113
+ )
114
+
115
+ # The DocumentationWriter handles the actual formatting and wrapping.
116
+ # The _wrap_docstring helper is not directly used here if DocumentationWriter handles it all.
117
+ # However, DocumentationWriter.render_docstring itself might need indentation control.
118
+ # Original called writer.write_line(line) for each line of docstring.
119
+ docstring_str = self.doc_writer.render_docstring(
120
+ doc_block, indent=0
121
+ ) # indent=0 as CodeWriter handles method indent
122
+ for line in docstring_str.splitlines():
123
+ writer.write_line(line)
@@ -0,0 +1,222 @@
1
+ import logging
2
+ import re
3
+ from typing import Any
4
+
5
+ from pyopenapi_gen import IROperation
6
+
7
+ from ....context.render_context import RenderContext
8
+ from ....core.utils import Formatter, NameSanitizer
9
+ from ....core.writers.code_writer import CodeWriter
10
+ from ....types.strategies import ResponseStrategyResolver
11
+ from ..processors.import_analyzer import EndpointImportAnalyzer
12
+ from ..processors.parameter_processor import EndpointParameterProcessor
13
+ from .docstring_generator import EndpointDocstringGenerator
14
+ from .overload_generator import OverloadMethodGenerator
15
+ from .request_generator import EndpointRequestGenerator
16
+ from .response_handler_generator import EndpointResponseHandlerGenerator
17
+ from .signature_generator import EndpointMethodSignatureGenerator
18
+ from .url_args_generator import EndpointUrlArgsGenerator
19
+
20
+ # Get logger instance
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class EndpointMethodGenerator:
25
+ """
26
+ Generates the Python code for a single endpoint method.
27
+ """
28
+
29
+ def __init__(self, schemas: dict[str, Any] | None = None) -> None:
30
+ self.schemas = schemas or {}
31
+ self.formatter = Formatter()
32
+ self.parameter_processor = EndpointParameterProcessor(self.schemas)
33
+ self.import_analyzer = EndpointImportAnalyzer(self.schemas)
34
+ self.signature_generator = EndpointMethodSignatureGenerator(self.schemas)
35
+ self.docstring_generator = EndpointDocstringGenerator(self.schemas)
36
+ self.url_args_generator = EndpointUrlArgsGenerator(self.schemas)
37
+ self.request_generator = EndpointRequestGenerator(self.schemas)
38
+ self.response_handler_generator = EndpointResponseHandlerGenerator(self.schemas)
39
+ self.overload_generator = OverloadMethodGenerator(self.schemas)
40
+
41
+ def generate(self, op: IROperation, context: RenderContext) -> str:
42
+ """
43
+ Generate a fully functional async endpoint method for the given operation.
44
+ Returns the method code as a string.
45
+
46
+ If the operation has multiple content types, generates @overload signatures
47
+ followed by the implementation method with runtime dispatch.
48
+ """
49
+ context.add_import(f"{context.core_package_name}.http_transport", "HttpTransport")
50
+ context.add_import(f"{context.core_package_name}.exceptions", "HTTPError")
51
+
52
+ # UNIFIED RESPONSE STRATEGY: Resolve once, use everywhere
53
+ strategy_resolver = ResponseStrategyResolver(self.schemas)
54
+ response_strategy = strategy_resolver.resolve(op, context)
55
+
56
+ # Pass the response strategy to import analyzer for consistent import resolution
57
+ self.import_analyzer.analyze_and_register_imports(op, context, response_strategy)
58
+
59
+ # Check if operation has multiple content types
60
+ if self.overload_generator.has_multiple_content_types(op):
61
+ return self._generate_overloaded_method(op, context, response_strategy)
62
+ else:
63
+ return self._generate_standard_method(op, context, response_strategy)
64
+
65
+ def _generate_standard_method(self, op: IROperation, context: RenderContext, response_strategy: Any) -> str:
66
+ """Generate standard method without overloads."""
67
+ writer = CodeWriter()
68
+
69
+ ordered_params, primary_content_type, resolved_body_type = self.parameter_processor.process_parameters(
70
+ op, context
71
+ )
72
+
73
+ # Pass strategy to generators for consistent behavior
74
+ self.signature_generator.generate_signature(writer, op, context, ordered_params, response_strategy)
75
+
76
+ self.docstring_generator.generate_docstring(writer, op, context, primary_content_type, response_strategy)
77
+
78
+ # Snapshot of code *before* main body parts are written
79
+ # This includes signature and docstring.
80
+ code_snapshot_before_body_parts = writer.get_code()
81
+
82
+ has_header_params = self.url_args_generator.generate_url_and_args(
83
+ writer, op, context, ordered_params, primary_content_type, resolved_body_type
84
+ )
85
+ self.request_generator.generate_request_call(writer, op, context, has_header_params, primary_content_type)
86
+
87
+ # Call the new response handler generator with strategy
88
+ self.response_handler_generator.generate_response_handling(writer, op, context, response_strategy)
89
+
90
+ # Check if any actual statements were added for the body
91
+ current_full_code = writer.get_code()
92
+ # The part of the code added by the body-writing methods
93
+ body_part_actually_written = current_full_code[len(code_snapshot_before_body_parts) :]
94
+
95
+ body_is_effectively_empty = True
96
+ # Check if the written body part contains any non-comment, non-whitespace lines
97
+ if body_part_actually_written.strip(): # Check if non-whitespace exists at all
98
+ if any(
99
+ line.strip() and not line.strip().startswith("#") for line in body_part_actually_written.splitlines()
100
+ ):
101
+ body_is_effectively_empty = False
102
+
103
+ if body_is_effectively_empty:
104
+ writer.write_line("pass")
105
+
106
+ writer.dedent() # This matches the indent() from _write_method_signature
107
+
108
+ return writer.get_code().strip()
109
+
110
+ def _generate_overloaded_method(self, op: IROperation, context: RenderContext, response_strategy: Any) -> str:
111
+ """Generate method with @overload signatures for multiple content types."""
112
+ parts = []
113
+
114
+ # Generate overload signatures
115
+ overload_sigs = self.overload_generator.generate_overload_signatures(op, context, response_strategy)
116
+ parts.extend(overload_sigs)
117
+
118
+ # Generate implementation method
119
+ impl_method = self._generate_implementation_method(op, context, response_strategy)
120
+ parts.append(impl_method)
121
+
122
+ # Join with double newlines between overloads and implementation
123
+ return "\n\n".join(parts)
124
+
125
+ def _generate_implementation_method(self, op: IROperation, context: RenderContext, response_strategy: Any) -> str:
126
+ """Generate the implementation method with runtime dispatch for multiple content types."""
127
+ # Type narrowing: request_body is guaranteed to exist when this method is called
128
+ assert (
129
+ op.request_body is not None
130
+ ), "request_body should not be None in _generate_implementation_method" # nosec B101 - Type narrowing for mypy, validated by has_multiple_content_types
131
+
132
+ writer = CodeWriter()
133
+
134
+ # Import DataclassSerializer for automatic conversion
135
+ context.add_import(f"{context.core_package_name}.utils", "DataclassSerializer")
136
+
137
+ # Generate implementation signature (accepts all content-type parameters as optional)
138
+ impl_sig = self.overload_generator.generate_implementation_signature(op, context, response_strategy)
139
+ writer.write_block(impl_sig)
140
+
141
+ # Generate docstring
142
+ ordered_params, primary_content_type, _ = self.parameter_processor.process_parameters(op, context)
143
+ writer.indent()
144
+ writer.write_line('"""')
145
+ writer.write_line(f"{op.summary or op.operation_id}")
146
+ writer.write_line("")
147
+ writer.write_line("Supports multiple content types:")
148
+ for content_type in op.request_body.content.keys():
149
+ writer.write_line(f"- {content_type}")
150
+ writer.write_line('"""')
151
+
152
+ # Generate URL construction with sanitized path variables
153
+ formatted_path = re.sub(
154
+ r"{([^}]+)}", lambda m: f"{{{NameSanitizer.sanitize_method_name(str(m.group(1)))}}}", op.path
155
+ )
156
+ writer.write_line(f'url = f"{{self.base_url}}{formatted_path}"')
157
+ writer.write_line("")
158
+
159
+ # Generate runtime dispatch logic
160
+ writer.write_line("# Runtime dispatch based on content type")
161
+
162
+ first_content_type = True
163
+ for content_type in op.request_body.content.keys():
164
+ param_info = self.overload_generator._get_content_type_param_info(
165
+ content_type, op.request_body.content[content_type], context
166
+ )
167
+
168
+ if first_content_type:
169
+ writer.write_line(f"if {param_info['name']} is not None:")
170
+ first_content_type = False
171
+ else:
172
+ writer.write_line(f"elif {param_info['name']} is not None:")
173
+
174
+ writer.indent()
175
+
176
+ # Generate request call for this content type
177
+ if content_type == "application/json":
178
+ writer.write_line(f"json_body = DataclassSerializer.serialize({param_info['name']})")
179
+ writer.write_line("response = await self._transport.request(")
180
+ writer.indent()
181
+ writer.write_line(f'"{op.method.value.upper()}", url,')
182
+ writer.write_line("params=None,")
183
+ writer.write_line("json=json_body,")
184
+ writer.write_line("headers=None")
185
+ writer.dedent()
186
+ writer.write_line(")")
187
+ elif content_type == "multipart/form-data":
188
+ # Files dict is already in correct format for httpx - pass directly
189
+ writer.write_line("response = await self._transport.request(")
190
+ writer.indent()
191
+ writer.write_line(f'"{op.method.value.upper()}", url,')
192
+ writer.write_line("params=None,")
193
+ writer.write_line(f"files={param_info['name']},")
194
+ writer.write_line("headers=None")
195
+ writer.dedent()
196
+ writer.write_line(")")
197
+ else:
198
+ writer.write_line(f"data = DataclassSerializer.serialize({param_info['name']})")
199
+ writer.write_line("response = await self._transport.request(")
200
+ writer.indent()
201
+ writer.write_line(f'"{op.method.value.upper()}", url,')
202
+ writer.write_line("params=None,")
203
+ writer.write_line("data=data,")
204
+ writer.write_line("headers=None")
205
+ writer.dedent()
206
+ writer.write_line(")")
207
+
208
+ writer.dedent()
209
+
210
+ # Add else clause for error
211
+ writer.write_line("else:")
212
+ writer.indent()
213
+ writer.write_line('raise ValueError("One of the content-type parameters must be provided")')
214
+ writer.dedent()
215
+ writer.write_line("")
216
+
217
+ # Generate response handling (reuse existing generator)
218
+ self.response_handler_generator.generate_response_handling(writer, op, context, response_strategy)
219
+
220
+ writer.dedent()
221
+
222
+ return writer.get_code().strip()