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.
- pyopenapi_gen/__init__.py +224 -0
- pyopenapi_gen/__main__.py +6 -0
- pyopenapi_gen/cli.py +62 -0
- pyopenapi_gen/context/CLAUDE.md +284 -0
- pyopenapi_gen/context/file_manager.py +52 -0
- pyopenapi_gen/context/import_collector.py +382 -0
- pyopenapi_gen/context/render_context.py +726 -0
- pyopenapi_gen/core/CLAUDE.md +224 -0
- pyopenapi_gen/core/__init__.py +0 -0
- pyopenapi_gen/core/auth/base.py +22 -0
- pyopenapi_gen/core/auth/plugins.py +89 -0
- pyopenapi_gen/core/cattrs_converter.py +810 -0
- pyopenapi_gen/core/exceptions.py +20 -0
- pyopenapi_gen/core/http_status_codes.py +218 -0
- pyopenapi_gen/core/http_transport.py +222 -0
- pyopenapi_gen/core/loader/__init__.py +12 -0
- pyopenapi_gen/core/loader/loader.py +174 -0
- pyopenapi_gen/core/loader/operations/__init__.py +12 -0
- pyopenapi_gen/core/loader/operations/parser.py +161 -0
- pyopenapi_gen/core/loader/operations/post_processor.py +62 -0
- pyopenapi_gen/core/loader/operations/request_body.py +90 -0
- pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
- pyopenapi_gen/core/loader/parameters/parser.py +186 -0
- pyopenapi_gen/core/loader/responses/__init__.py +10 -0
- pyopenapi_gen/core/loader/responses/parser.py +111 -0
- pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
- pyopenapi_gen/core/loader/schemas/extractor.py +275 -0
- pyopenapi_gen/core/pagination.py +64 -0
- pyopenapi_gen/core/parsing/__init__.py +13 -0
- pyopenapi_gen/core/parsing/common/__init__.py +1 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/__init__.py +9 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/__init__.py +0 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/cyclic_properties.py +66 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/direct_cycle.py +33 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/existing_schema.py +22 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +54 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +52 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +50 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +51 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +86 -0
- pyopenapi_gen/core/parsing/common/type_parser.py +73 -0
- pyopenapi_gen/core/parsing/context.py +187 -0
- pyopenapi_gen/core/parsing/cycle_helpers.py +126 -0
- pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
- pyopenapi_gen/core/parsing/keywords/all_of_parser.py +81 -0
- pyopenapi_gen/core/parsing/keywords/any_of_parser.py +84 -0
- pyopenapi_gen/core/parsing/keywords/array_items_parser.py +72 -0
- pyopenapi_gen/core/parsing/keywords/one_of_parser.py +77 -0
- pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
- pyopenapi_gen/core/parsing/schema_finalizer.py +169 -0
- pyopenapi_gen/core/parsing/schema_parser.py +804 -0
- pyopenapi_gen/core/parsing/transformers/__init__.py +0 -0
- pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +285 -0
- pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +120 -0
- pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
- pyopenapi_gen/core/postprocess_manager.py +260 -0
- pyopenapi_gen/core/spec_fetcher.py +148 -0
- pyopenapi_gen/core/streaming_helpers.py +84 -0
- pyopenapi_gen/core/telemetry.py +69 -0
- pyopenapi_gen/core/utils.py +456 -0
- pyopenapi_gen/core/warning_collector.py +83 -0
- pyopenapi_gen/core/writers/code_writer.py +135 -0
- pyopenapi_gen/core/writers/documentation_writer.py +222 -0
- pyopenapi_gen/core/writers/line_writer.py +217 -0
- pyopenapi_gen/core/writers/python_construct_renderer.py +321 -0
- pyopenapi_gen/core_package_template/README.md +21 -0
- pyopenapi_gen/emit/models_emitter.py +143 -0
- pyopenapi_gen/emitters/CLAUDE.md +286 -0
- pyopenapi_gen/emitters/client_emitter.py +51 -0
- pyopenapi_gen/emitters/core_emitter.py +181 -0
- pyopenapi_gen/emitters/docs_emitter.py +44 -0
- pyopenapi_gen/emitters/endpoints_emitter.py +247 -0
- pyopenapi_gen/emitters/exceptions_emitter.py +187 -0
- pyopenapi_gen/emitters/mocks_emitter.py +185 -0
- pyopenapi_gen/emitters/models_emitter.py +426 -0
- pyopenapi_gen/generator/CLAUDE.md +352 -0
- pyopenapi_gen/generator/client_generator.py +567 -0
- pyopenapi_gen/generator/exceptions.py +7 -0
- pyopenapi_gen/helpers/CLAUDE.md +325 -0
- pyopenapi_gen/helpers/__init__.py +1 -0
- pyopenapi_gen/helpers/endpoint_utils.py +532 -0
- pyopenapi_gen/helpers/type_cleaner.py +334 -0
- pyopenapi_gen/helpers/type_helper.py +112 -0
- pyopenapi_gen/helpers/type_resolution/__init__.py +1 -0
- pyopenapi_gen/helpers/type_resolution/array_resolver.py +57 -0
- pyopenapi_gen/helpers/type_resolution/composition_resolver.py +79 -0
- pyopenapi_gen/helpers/type_resolution/finalizer.py +105 -0
- pyopenapi_gen/helpers/type_resolution/named_resolver.py +172 -0
- pyopenapi_gen/helpers/type_resolution/object_resolver.py +216 -0
- pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +109 -0
- pyopenapi_gen/helpers/type_resolution/resolver.py +47 -0
- pyopenapi_gen/helpers/url_utils.py +14 -0
- pyopenapi_gen/http_types.py +20 -0
- pyopenapi_gen/ir.py +165 -0
- pyopenapi_gen/py.typed +1 -0
- pyopenapi_gen/types/CLAUDE.md +140 -0
- pyopenapi_gen/types/__init__.py +11 -0
- pyopenapi_gen/types/contracts/__init__.py +13 -0
- pyopenapi_gen/types/contracts/protocols.py +106 -0
- pyopenapi_gen/types/contracts/types.py +28 -0
- pyopenapi_gen/types/resolvers/__init__.py +7 -0
- pyopenapi_gen/types/resolvers/reference_resolver.py +71 -0
- pyopenapi_gen/types/resolvers/response_resolver.py +177 -0
- pyopenapi_gen/types/resolvers/schema_resolver.py +498 -0
- pyopenapi_gen/types/services/__init__.py +5 -0
- pyopenapi_gen/types/services/type_service.py +165 -0
- pyopenapi_gen/types/strategies/__init__.py +5 -0
- pyopenapi_gen/types/strategies/response_strategy.py +310 -0
- pyopenapi_gen/visit/CLAUDE.md +272 -0
- pyopenapi_gen/visit/client_visitor.py +477 -0
- pyopenapi_gen/visit/docs_visitor.py +38 -0
- pyopenapi_gen/visit/endpoint/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/endpoint_visitor.py +292 -0
- pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +123 -0
- pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +222 -0
- pyopenapi_gen/visit/endpoint/generators/mock_generator.py +140 -0
- pyopenapi_gen/visit/endpoint/generators/overload_generator.py +252 -0
- pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
- pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +705 -0
- pyopenapi_gen/visit/endpoint/generators/signature_generator.py +83 -0
- pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +207 -0
- pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +78 -0
- pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
- pyopenapi_gen/visit/exception_visitor.py +90 -0
- pyopenapi_gen/visit/model/__init__.py +0 -0
- pyopenapi_gen/visit/model/alias_generator.py +93 -0
- pyopenapi_gen/visit/model/dataclass_generator.py +553 -0
- pyopenapi_gen/visit/model/enum_generator.py +212 -0
- pyopenapi_gen/visit/model/model_visitor.py +198 -0
- pyopenapi_gen/visit/visitor.py +97 -0
- pyopenapi_gen-2.7.2.dist-info/METADATA +1169 -0
- pyopenapi_gen-2.7.2.dist-info/RECORD +137 -0
- pyopenapi_gen-2.7.2.dist-info/WHEEL +4 -0
- pyopenapi_gen-2.7.2.dist-info/entry_points.txt +2 -0
- 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()
|