pyopenapi-gen 0.8.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyopenapi_gen/__init__.py +114 -0
- pyopenapi_gen/__main__.py +6 -0
- pyopenapi_gen/cli.py +86 -0
- pyopenapi_gen/context/file_manager.py +52 -0
- pyopenapi_gen/context/import_collector.py +382 -0
- pyopenapi_gen/context/render_context.py +630 -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/exceptions.py +25 -0
- pyopenapi_gen/core/http_transport.py +219 -0
- pyopenapi_gen/core/loader/__init__.py +12 -0
- pyopenapi_gen/core/loader/loader.py +158 -0
- pyopenapi_gen/core/loader/operations/__init__.py +12 -0
- pyopenapi_gen/core/loader/operations/parser.py +155 -0
- pyopenapi_gen/core/loader/operations/post_processor.py +60 -0
- pyopenapi_gen/core/loader/operations/request_body.py +85 -0
- pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
- pyopenapi_gen/core/loader/parameters/parser.py +121 -0
- pyopenapi_gen/core/loader/responses/__init__.py +10 -0
- pyopenapi_gen/core/loader/responses/parser.py +104 -0
- pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
- pyopenapi_gen/core/loader/schemas/extractor.py +184 -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 +74 -0
- pyopenapi_gen/core/parsing/context.py +184 -0
- pyopenapi_gen/core/parsing/cycle_helpers.py +123 -0
- pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
- pyopenapi_gen/core/parsing/keywords/all_of_parser.py +77 -0
- pyopenapi_gen/core/parsing/keywords/any_of_parser.py +79 -0
- pyopenapi_gen/core/parsing/keywords/array_items_parser.py +69 -0
- pyopenapi_gen/core/parsing/keywords/one_of_parser.py +72 -0
- pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
- pyopenapi_gen/core/parsing/schema_finalizer.py +166 -0
- pyopenapi_gen/core/parsing/schema_parser.py +610 -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 +117 -0
- pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
- pyopenapi_gen/core/postprocess_manager.py +161 -0
- pyopenapi_gen/core/schemas.py +40 -0
- pyopenapi_gen/core/streaming_helpers.py +86 -0
- pyopenapi_gen/core/telemetry.py +67 -0
- pyopenapi_gen/core/utils.py +409 -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 +274 -0
- pyopenapi_gen/core_package_template/README.md +21 -0
- pyopenapi_gen/emit/models_emitter.py +143 -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 +223 -0
- pyopenapi_gen/emitters/exceptions_emitter.py +52 -0
- pyopenapi_gen/emitters/models_emitter.py +428 -0
- pyopenapi_gen/generator/client_generator.py +562 -0
- pyopenapi_gen/helpers/__init__.py +1 -0
- pyopenapi_gen/helpers/endpoint_utils.py +552 -0
- pyopenapi_gen/helpers/type_cleaner.py +341 -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 +89 -0
- pyopenapi_gen/helpers/type_resolution/named_resolver.py +174 -0
- pyopenapi_gen/helpers/type_resolution/object_resolver.py +212 -0
- pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +57 -0
- pyopenapi_gen/helpers/type_resolution/resolver.py +48 -0
- pyopenapi_gen/helpers/url_utils.py +14 -0
- pyopenapi_gen/http_types.py +20 -0
- pyopenapi_gen/ir.py +167 -0
- pyopenapi_gen/py.typed +1 -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 +30 -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 +203 -0
- pyopenapi_gen/types/resolvers/schema_resolver.py +367 -0
- pyopenapi_gen/types/services/__init__.py +5 -0
- pyopenapi_gen/types/services/type_service.py +133 -0
- pyopenapi_gen/visit/client_visitor.py +228 -0
- pyopenapi_gen/visit/docs_visitor.py +38 -0
- pyopenapi_gen/visit/endpoint/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/endpoint_visitor.py +103 -0
- pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +121 -0
- pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +87 -0
- pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
- pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +497 -0
- pyopenapi_gen/visit/endpoint/generators/signature_generator.py +88 -0
- pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +183 -0
- pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +76 -0
- pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
- pyopenapi_gen/visit/exception_visitor.py +52 -0
- pyopenapi_gen/visit/model/__init__.py +0 -0
- pyopenapi_gen/visit/model/alias_generator.py +89 -0
- pyopenapi_gen/visit/model/dataclass_generator.py +197 -0
- pyopenapi_gen/visit/model/enum_generator.py +200 -0
- pyopenapi_gen/visit/model/model_visitor.py +197 -0
- pyopenapi_gen/visit/visitor.py +97 -0
- pyopenapi_gen-0.8.3.dist-info/METADATA +224 -0
- pyopenapi_gen-0.8.3.dist-info/RECORD +122 -0
- pyopenapi_gen-0.8.3.dist-info/WHEEL +4 -0
- pyopenapi_gen-0.8.3.dist-info/entry_points.txt +2 -0
- pyopenapi_gen-0.8.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,228 @@
|
|
1
|
+
import logging # Added for logging
|
2
|
+
import re
|
3
|
+
import textwrap
|
4
|
+
from typing import TYPE_CHECKING, cast
|
5
|
+
|
6
|
+
from pyopenapi_gen import IRSpec
|
7
|
+
|
8
|
+
from ..context.render_context import RenderContext
|
9
|
+
from ..core.utils import NameSanitizer
|
10
|
+
from ..core.writers.code_writer import CodeWriter
|
11
|
+
from ..core.writers.documentation_writer import DocumentationBlock, DocumentationWriter
|
12
|
+
|
13
|
+
if TYPE_CHECKING:
|
14
|
+
# To prevent circular imports if any type from core itself is needed for hints
|
15
|
+
pass
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__) # Added for logging
|
18
|
+
|
19
|
+
|
20
|
+
class ClientVisitor:
|
21
|
+
"""Visitor for rendering the Python API client from IRSpec."""
|
22
|
+
|
23
|
+
def __init__(self) -> None:
|
24
|
+
pass
|
25
|
+
|
26
|
+
def visit(self, spec: IRSpec, context: RenderContext) -> str:
|
27
|
+
tag_candidates: dict[str, list[str]] = {}
|
28
|
+
for op in spec.operations:
|
29
|
+
# Use DEFAULT_TAG consistent with EndpointsEmitter
|
30
|
+
tags = op.tags or ["default"] # Use literal "default" here
|
31
|
+
# Loop through the determined tags (original or default)
|
32
|
+
for tag in tags:
|
33
|
+
key = NameSanitizer.normalize_tag_key(tag)
|
34
|
+
if key not in tag_candidates:
|
35
|
+
tag_candidates[key] = []
|
36
|
+
tag_candidates[key].append(tag)
|
37
|
+
# Ensure the old logic is removed (idempotent if already gone)
|
38
|
+
# if op.tags:
|
39
|
+
# ...
|
40
|
+
# else:
|
41
|
+
# ...
|
42
|
+
|
43
|
+
def tag_score(t: str) -> tuple[bool, int, int, str]:
|
44
|
+
is_pascal = bool(re.search(r"[a-z][A-Z]", t)) or bool(re.search(r"[A-Z]{2,}", t))
|
45
|
+
words = re.findall(r"[A-Z]?[a-z]+|[A-Z]+(?![a-z])|[0-9]+", t)
|
46
|
+
words += re.split(r"[_-]+", t)
|
47
|
+
word_count = len([w for w in words if w])
|
48
|
+
upper = sum(1 for c in t if c.isupper())
|
49
|
+
return (is_pascal, word_count, upper, t)
|
50
|
+
|
51
|
+
tag_map = {}
|
52
|
+
for key, candidates in tag_candidates.items():
|
53
|
+
best = max(candidates, key=tag_score)
|
54
|
+
tag_map[key] = best
|
55
|
+
tag_tuples = [
|
56
|
+
(
|
57
|
+
tag_map[key],
|
58
|
+
NameSanitizer.sanitize_class_name(tag_map[key]) + "Client",
|
59
|
+
NameSanitizer.sanitize_module_name(tag_map[key]),
|
60
|
+
)
|
61
|
+
for key in sorted(tag_map)
|
62
|
+
]
|
63
|
+
writer = CodeWriter()
|
64
|
+
# Register all endpoint client imports using relative imports (endpoints are within the same package)
|
65
|
+
for _, class_name, module_name in tag_tuples:
|
66
|
+
# Use relative imports for endpoints since they're part of the same generated package
|
67
|
+
context.import_collector.add_relative_import(f".endpoints.{module_name}", class_name)
|
68
|
+
|
69
|
+
# Register core/config/typing imports for class signature
|
70
|
+
# Use LOGICAL import path for core components
|
71
|
+
|
72
|
+
# Use the core_package name from the context to form the base of the import path
|
73
|
+
# RenderContext.add_import will handle making it relative correctly based on the current file.
|
74
|
+
context.add_import(f"{context.core_package_name}.http_transport", "HttpTransport")
|
75
|
+
context.add_import(f"{context.core_package_name}.http_transport", "HttpxTransport")
|
76
|
+
context.add_import(f"{context.core_package_name}.config", "ClientConfig")
|
77
|
+
# If security schemes are present and an auth plugin like ApiKeyAuth is used by the client itself,
|
78
|
+
# it would also be registered here using context.core_package.
|
79
|
+
# For now, the client_py_content check in the test looks for this for ApiKeyAuth specifically:
|
80
|
+
context.add_import(f"{context.core_package_name}.auth.plugins", "ApiKeyAuth")
|
81
|
+
|
82
|
+
context.add_typing_imports_for_type("Optional[HttpTransport]")
|
83
|
+
context.add_typing_imports_for_type("Any")
|
84
|
+
context.add_typing_imports_for_type("Dict")
|
85
|
+
# Class definition
|
86
|
+
writer.write_line("class APIClient:")
|
87
|
+
writer.indent()
|
88
|
+
# Build docstring for APIClient
|
89
|
+
docstring_lines = []
|
90
|
+
# Add API title and version
|
91
|
+
docstring_lines.append(f"{spec.title} (version {spec.version})")
|
92
|
+
# Add API description if present
|
93
|
+
if getattr(spec, "description", None):
|
94
|
+
desc = spec.description
|
95
|
+
if desc is not None:
|
96
|
+
# Remove triple quotes, escape backslashes, and dedent
|
97
|
+
desc_clean = desc.replace('"""', "'").replace("'''", "'").replace("\\", "\\\\").strip()
|
98
|
+
desc_clean = textwrap.dedent(desc_clean)
|
99
|
+
docstring_lines.append("")
|
100
|
+
docstring_lines.append(desc_clean)
|
101
|
+
# Add a blank line before the generated summary/args
|
102
|
+
docstring_lines.append("")
|
103
|
+
summary = "Async API client with pluggable transport, tag-specific clients, and client-level headers."
|
104
|
+
args: list[tuple[str, str, str]] = [
|
105
|
+
("config", "ClientConfig", "Client configuration object."),
|
106
|
+
("transport", "Optional[HttpTransport]", "Custom HTTP transport (optional)."),
|
107
|
+
]
|
108
|
+
for tag, class_name, module_name in tag_tuples:
|
109
|
+
args.append((module_name, class_name, f"Client for '{tag}' endpoints."))
|
110
|
+
doc_block = DocumentationBlock(
|
111
|
+
summary=summary,
|
112
|
+
args=cast(list[tuple[str, str, str] | tuple[str, str]], args),
|
113
|
+
)
|
114
|
+
docstring = DocumentationWriter(width=88).render_docstring(doc_block, indent=0)
|
115
|
+
docstring_lines.extend([line for line in docstring.splitlines()])
|
116
|
+
# Write only one docstring, no extra triple quotes after
|
117
|
+
writer.write_line('"""') # At class indent (1)
|
118
|
+
writer.dedent() # Go to indent 0 for docstring content
|
119
|
+
for line in docstring_lines:
|
120
|
+
writer.write_line(line.rstrip('"'))
|
121
|
+
writer.indent() # Back to class indent (1)
|
122
|
+
writer.write_line('"""')
|
123
|
+
# __init__
|
124
|
+
writer.write_line(
|
125
|
+
"def __init__(self, config: ClientConfig, transport: Optional[HttpTransport] = None) -> None:"
|
126
|
+
)
|
127
|
+
writer.indent()
|
128
|
+
writer.write_line("self.config = config")
|
129
|
+
writer.write_line(
|
130
|
+
"self.transport = transport if transport is not None else "
|
131
|
+
"HttpxTransport(str(config.base_url), config.timeout)"
|
132
|
+
)
|
133
|
+
writer.write_line("self._base_url: str = str(self.config.base_url)")
|
134
|
+
# Initialize private fields for each tag client
|
135
|
+
for tag, class_name, module_name in tag_tuples:
|
136
|
+
context.add_typing_imports_for_type(f"Optional[{class_name}]")
|
137
|
+
writer.write_line(f"self._{module_name}: Optional[{class_name}] = None")
|
138
|
+
writer.dedent()
|
139
|
+
writer.write_line("")
|
140
|
+
# @property for each tag client
|
141
|
+
for tag, class_name, module_name in tag_tuples:
|
142
|
+
writer.write_line(f"@property")
|
143
|
+
# Use context.add_import here too
|
144
|
+
current_gen_pkg_name_prop = context.get_current_package_name_for_generated_code()
|
145
|
+
if not current_gen_pkg_name_prop:
|
146
|
+
logger.error(
|
147
|
+
f"[ClientVisitor Property] Could not determine generated package name from context. "
|
148
|
+
f"Cannot form fully qualified import for endpoints.{module_name}.{class_name}"
|
149
|
+
)
|
150
|
+
# Fallback or raise error
|
151
|
+
context.add_import(f"endpoints.{module_name}", class_name)
|
152
|
+
else:
|
153
|
+
logical_module_for_add_import_prop = f"{current_gen_pkg_name_prop}.endpoints.{module_name}"
|
154
|
+
context.add_import(logical_module_for_add_import_prop, class_name)
|
155
|
+
|
156
|
+
writer.write_line(f"def {module_name}(self) -> {class_name}:")
|
157
|
+
writer.indent()
|
158
|
+
writer.write_line(f'"""Client for \'{tag}\' endpoints."""')
|
159
|
+
writer.write_line(f"if self._{module_name} is None:")
|
160
|
+
writer.indent()
|
161
|
+
writer.write_line(f"self._{module_name} = {class_name}(self.transport, self._base_url)")
|
162
|
+
writer.dedent()
|
163
|
+
writer.write_line(f"return self._{module_name}")
|
164
|
+
writer.dedent()
|
165
|
+
writer.write_line("")
|
166
|
+
# request method
|
167
|
+
context.add_typing_imports_for_type("Any")
|
168
|
+
writer.write_line("async def request(self, method: str, url: str, **kwargs: Any) -> Any:")
|
169
|
+
writer.indent()
|
170
|
+
writer.write_line('"""Send an HTTP request via the transport."""')
|
171
|
+
writer.write_line("return await self.transport.request(method, url, **kwargs)")
|
172
|
+
writer.dedent()
|
173
|
+
writer.write_line("")
|
174
|
+
# close method
|
175
|
+
context.add_typing_imports_for_type("None")
|
176
|
+
writer.write_line("async def close(self) -> None:")
|
177
|
+
writer.indent()
|
178
|
+
writer.write_line('"""Close the underlying transport if supported."""')
|
179
|
+
writer.write_line("if hasattr(self.transport, 'close'):")
|
180
|
+
writer.indent()
|
181
|
+
writer.write_line("await self.transport.close()")
|
182
|
+
writer.dedent()
|
183
|
+
writer.write_line("else:")
|
184
|
+
writer.indent()
|
185
|
+
writer.write_line("pass # Or log a warning if close is expected but not found")
|
186
|
+
writer.dedent()
|
187
|
+
writer.dedent()
|
188
|
+
writer.write_line("")
|
189
|
+
# __aenter__ for async context management (dedented)
|
190
|
+
writer.write_line("async def __aenter__(self) -> 'APIClient':")
|
191
|
+
writer.indent()
|
192
|
+
writer.write_line('"""Enter the async context manager. Returns self."""')
|
193
|
+
writer.write_line("if hasattr(self.transport, '__aenter__'):")
|
194
|
+
writer.indent()
|
195
|
+
writer.write_line("await self.transport.__aenter__()")
|
196
|
+
writer.dedent()
|
197
|
+
writer.write_line("return self")
|
198
|
+
writer.dedent()
|
199
|
+
writer.write_line("")
|
200
|
+
# __aexit__ for async context management (dedented)
|
201
|
+
context.add_typing_imports_for_type("type[BaseException] | None")
|
202
|
+
context.add_typing_imports_for_type("BaseException | None")
|
203
|
+
context.add_typing_imports_for_type("object | None")
|
204
|
+
writer.write_line(
|
205
|
+
"async def __aexit__(self, exc_type: type[BaseException] | None, "
|
206
|
+
"exc_val: BaseException | None, exc_tb: object | None) -> None:"
|
207
|
+
)
|
208
|
+
writer.indent()
|
209
|
+
writer.write_line('"""Exit the async context manager, ensuring transport is closed."""')
|
210
|
+
# Close internal transport if it supports __aexit__
|
211
|
+
writer.write_line("if hasattr(self.transport, '__aexit__'):")
|
212
|
+
writer.indent()
|
213
|
+
writer.write_line("await self.transport.__aexit__(exc_type, exc_val, exc_tb)")
|
214
|
+
writer.dedent()
|
215
|
+
writer.write_line("else:") # Fallback if transport doesn't have __aexit__ but has close()
|
216
|
+
writer.indent()
|
217
|
+
writer.write_line("await self.close()")
|
218
|
+
writer.dedent()
|
219
|
+
writer.dedent()
|
220
|
+
writer.write_line("")
|
221
|
+
|
222
|
+
# Ensure crucial core imports are present before final rendering
|
223
|
+
# This is a fallback / re-emphasis due to potential issues with ImportCollector
|
224
|
+
context.add_import(f"{context.core_package_name}.http_transport", "HttpTransport")
|
225
|
+
context.add_import(f"{context.core_package_name}.http_transport", "HttpxTransport")
|
226
|
+
context.add_import(f"{context.core_package_name}.config", "ClientConfig")
|
227
|
+
|
228
|
+
return writer.get_code()
|
@@ -0,0 +1,38 @@
|
|
1
|
+
from pyopenapi_gen import IRSpec
|
2
|
+
|
3
|
+
from ..context.render_context import RenderContext
|
4
|
+
from ..core.utils import NameSanitizer
|
5
|
+
from ..core.writers.code_writer import CodeWriter
|
6
|
+
|
7
|
+
|
8
|
+
class DocsVisitor:
|
9
|
+
"""Visitor for rendering markdown documentation from IRSpec."""
|
10
|
+
|
11
|
+
def visit(self, spec: IRSpec, context: RenderContext) -> dict[str, str]:
|
12
|
+
# List tags
|
13
|
+
tags = sorted({t for op in spec.operations for t in op.tags})
|
14
|
+
# Generate index.md with sanitized links
|
15
|
+
writer = CodeWriter()
|
16
|
+
writer.write_line("# API Documentation\n")
|
17
|
+
writer.write_line("Generated documentation for the API.\n")
|
18
|
+
writer.write_line("## Tags")
|
19
|
+
for tag in tags:
|
20
|
+
writer.write_line(f"- [{tag}]({NameSanitizer.sanitize_module_name(tag)}.md)")
|
21
|
+
index_content = writer.get_code()
|
22
|
+
result = {"index.md": index_content}
|
23
|
+
# Generate docs per tag
|
24
|
+
for tag in tags:
|
25
|
+
ops = [op for op in spec.operations if tag in op.tags]
|
26
|
+
tag_writer = CodeWriter()
|
27
|
+
tag_writer.write_line(f"# {tag.capitalize()} Operations\n")
|
28
|
+
for op in ops:
|
29
|
+
tag_writer.write_line(f"### {op.operation_id}\n")
|
30
|
+
tag_writer.write_line(f"**Method:** `{op.method.value}` ")
|
31
|
+
tag_writer.write_line(f"**Path:** `{op.path}` \n")
|
32
|
+
desc = op.description or ""
|
33
|
+
if desc:
|
34
|
+
tag_writer.write_line(desc)
|
35
|
+
tag_writer.write_line("")
|
36
|
+
filename = NameSanitizer.sanitize_module_name(tag) + ".md"
|
37
|
+
result[filename] = tag_writer.get_code()
|
38
|
+
return result
|
@@ -0,0 +1 @@
|
|
1
|
+
# This directory will contain helper classes for EndpointMethodGenerator
|
@@ -0,0 +1,103 @@
|
|
1
|
+
import logging
|
2
|
+
from typing import Any
|
3
|
+
|
4
|
+
from pyopenapi_gen import IROperation
|
5
|
+
from pyopenapi_gen.helpers.endpoint_utils import (
|
6
|
+
get_return_type_unified,
|
7
|
+
)
|
8
|
+
|
9
|
+
from ...context.render_context import RenderContext
|
10
|
+
from ...core.utils import NameSanitizer
|
11
|
+
from ...core.writers.code_writer import CodeWriter
|
12
|
+
from ..visitor import Visitor
|
13
|
+
from .generators.endpoint_method_generator import EndpointMethodGenerator
|
14
|
+
|
15
|
+
# Get logger instance
|
16
|
+
logger = logging.getLogger(__name__)
|
17
|
+
|
18
|
+
|
19
|
+
class EndpointVisitor(Visitor[IROperation, str]):
|
20
|
+
"""
|
21
|
+
Visitor for rendering a Python endpoint client method/class from an IROperation.
|
22
|
+
The method generation part is delegated to EndpointMethodGenerator.
|
23
|
+
This class remains responsible for assembling methods into a class (emit_endpoint_client_class).
|
24
|
+
Returns the rendered code as a string (does not write files).
|
25
|
+
"""
|
26
|
+
|
27
|
+
def __init__(self, schemas: dict[str, Any] | None = None) -> None:
|
28
|
+
self.schemas = schemas or {}
|
29
|
+
# Formatter is likely not needed here anymore if all formatting happens in EndpointMethodGenerator
|
30
|
+
# self.formatter = Formatter()
|
31
|
+
|
32
|
+
def visit_IROperation(self, op: IROperation, context: RenderContext) -> str:
|
33
|
+
"""
|
34
|
+
Generate a fully functional async endpoint method for the given operation
|
35
|
+
by delegating to EndpointMethodGenerator.
|
36
|
+
Returns the method code as a string.
|
37
|
+
"""
|
38
|
+
# Instantiate the new generator
|
39
|
+
method_generator = EndpointMethodGenerator(schemas=self.schemas)
|
40
|
+
return method_generator.generate(op, context)
|
41
|
+
|
42
|
+
def emit_endpoint_client_class(
|
43
|
+
self,
|
44
|
+
tag: str,
|
45
|
+
method_codes: list[str],
|
46
|
+
context: RenderContext,
|
47
|
+
) -> str:
|
48
|
+
"""
|
49
|
+
Emit the endpoint client class for a tag, aggregating all endpoint methods.
|
50
|
+
The generated class is fully type-annotated and uses HttpTransport for HTTP communication.
|
51
|
+
Args:
|
52
|
+
tag: The tag name for the endpoint group.
|
53
|
+
method_codes: List of method code blocks as strings.
|
54
|
+
context: The RenderContext for import tracking.
|
55
|
+
"""
|
56
|
+
context.add_import("typing", "cast")
|
57
|
+
# Import core transport and streaming helpers
|
58
|
+
context.add_import(f"{context.core_package_name}.http_transport", "HttpTransport")
|
59
|
+
context.add_import(f"{context.core_package_name}.streaming_helpers", "iter_bytes")
|
60
|
+
context.add_import("typing", "Callable")
|
61
|
+
context.add_import("typing", "Optional")
|
62
|
+
writer = CodeWriter()
|
63
|
+
class_name = NameSanitizer.sanitize_class_name(tag) + "Client"
|
64
|
+
writer.write_line(f"class {class_name}:")
|
65
|
+
writer.indent()
|
66
|
+
writer.write_line(f'"""Client for {tag} endpoints. Uses HttpTransport for all HTTP and header management."""')
|
67
|
+
writer.write_line("")
|
68
|
+
|
69
|
+
writer.write_line("def __init__(self, transport: HttpTransport, base_url: str) -> None:")
|
70
|
+
writer.indent()
|
71
|
+
writer.write_line("self._transport = transport")
|
72
|
+
writer.write_line("self.base_url: str = base_url")
|
73
|
+
writer.dedent()
|
74
|
+
writer.write_line("")
|
75
|
+
|
76
|
+
# Write methods
|
77
|
+
for i, method_code in enumerate(method_codes):
|
78
|
+
# Revert to write_block, as it handles indentation correctly
|
79
|
+
writer.write_block(method_code)
|
80
|
+
|
81
|
+
if i < len(method_codes) - 1:
|
82
|
+
writer.write_line("") # First blank line
|
83
|
+
writer.write_line("") # Second blank line (for testing separation)
|
84
|
+
|
85
|
+
writer.dedent() # Dedent to close the class block
|
86
|
+
return writer.get_code()
|
87
|
+
|
88
|
+
def _get_response_return_type_details(self, context: RenderContext, op: IROperation) -> tuple[str, bool, bool, str]:
|
89
|
+
"""Gets type details for the endpoint response."""
|
90
|
+
# Check if this is a streaming response (either at op level or in schema)
|
91
|
+
is_streaming = any(getattr(resp, "stream", False) for resp in op.responses if resp.status_code.startswith("2"))
|
92
|
+
|
93
|
+
# Get the primary Python type for the operation's success response using unified service
|
94
|
+
return_type = get_return_type_unified(op, context, self.schemas)
|
95
|
+
should_unwrap = False # Unified service handles unwrapping internally
|
96
|
+
|
97
|
+
# Determine the summary description (for docstring)
|
98
|
+
success_resp = next((r for r in op.responses if r.status_code.startswith("2")), None)
|
99
|
+
return_description = (
|
100
|
+
success_resp.description if success_resp and success_resp.description else "Successful operation"
|
101
|
+
)
|
102
|
+
|
103
|
+
return return_type, should_unwrap, is_streaming, return_description
|
@@ -0,0 +1 @@
|
|
1
|
+
# पैसाले सुख दिदैन।
|
@@ -0,0 +1,121 @@
|
|
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, Dict, Optional
|
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, get_return_type_unified
|
14
|
+
|
15
|
+
if TYPE_CHECKING:
|
16
|
+
from pyopenapi_gen import IROperation
|
17
|
+
from pyopenapi_gen.context.render_context import RenderContext
|
18
|
+
|
19
|
+
logger = logging.getLogger(__name__)
|
20
|
+
|
21
|
+
|
22
|
+
class EndpointDocstringGenerator:
|
23
|
+
"""Generates the Python docstring for an endpoint operation."""
|
24
|
+
|
25
|
+
def __init__(self, schemas: Optional[Dict[str, Any]] = None) -> None:
|
26
|
+
self.schemas: Dict[str, Any] = schemas or {}
|
27
|
+
self.doc_writer = DocumentationWriter(width=88)
|
28
|
+
|
29
|
+
def _wrap_docstring(self, prefix: str, text: str, width: int = 88) -> str:
|
30
|
+
"""Internal helper to wrap text for docstrings."""
|
31
|
+
# This was a staticmethod in EndpointMethodGenerator, can be a helper here.
|
32
|
+
if not text:
|
33
|
+
return prefix.rstrip()
|
34
|
+
initial_indent = prefix
|
35
|
+
subsequent_indent = " " * len(prefix)
|
36
|
+
wrapped = textwrap.wrap(text, width=width, initial_indent=initial_indent, subsequent_indent=subsequent_indent)
|
37
|
+
# The original had "\n ".join(wrapped), which might be too specific if prefix changes.
|
38
|
+
# Let's ensure it joins with newline and respects the subsequent_indent for multi-lines.
|
39
|
+
if not wrapped:
|
40
|
+
return prefix.rstrip()
|
41
|
+
# For single line, no complex join needed, just the wrapped line.
|
42
|
+
if len(wrapped) == 1:
|
43
|
+
return wrapped[0]
|
44
|
+
# For multi-line, ensure proper joining. textwrap handles indent per line.
|
45
|
+
return "\n".join(wrapped)
|
46
|
+
|
47
|
+
def generate_docstring(
|
48
|
+
self,
|
49
|
+
writer: CodeWriter,
|
50
|
+
op: IROperation,
|
51
|
+
context: RenderContext,
|
52
|
+
primary_content_type: Optional[str],
|
53
|
+
) -> None:
|
54
|
+
"""Writes the method docstring to the provided CodeWriter."""
|
55
|
+
summary = op.summary or None
|
56
|
+
description = op.description or None
|
57
|
+
args: list[tuple[str, str, str] | tuple[str, str]] = []
|
58
|
+
|
59
|
+
for param in op.parameters:
|
60
|
+
param_type = get_param_type(param, context, self.schemas)
|
61
|
+
desc = param.description or ""
|
62
|
+
args.append((param.name, param_type, desc))
|
63
|
+
|
64
|
+
if op.request_body and primary_content_type:
|
65
|
+
body_desc = op.request_body.description or "Request body."
|
66
|
+
# Standardized body parameter names based on content type
|
67
|
+
if primary_content_type == "multipart/form-data":
|
68
|
+
args.append(("files", "Dict[str, IO[Any]]", body_desc + " (multipart/form-data)"))
|
69
|
+
elif primary_content_type == "application/x-www-form-urlencoded":
|
70
|
+
# The type here could be more specific if schema is available, but Dict[str, Any] is a safe default.
|
71
|
+
args.append(("form_data", "Dict[str, Any]", body_desc + " (x-www-form-urlencoded)"))
|
72
|
+
elif primary_content_type == "application/json":
|
73
|
+
body_type = get_request_body_type(op.request_body, context, self.schemas)
|
74
|
+
args.append(("body", body_type, body_desc + " (json)"))
|
75
|
+
else: # Fallback for other types like application/octet-stream
|
76
|
+
args.append(("bytes_content", "bytes", body_desc + f" ({primary_content_type})"))
|
77
|
+
|
78
|
+
return_type = get_return_type_unified(op, context, self.schemas)
|
79
|
+
response_desc = None
|
80
|
+
# Prioritize 2xx success codes for the main response description
|
81
|
+
for code in ("200", "201", "202", "default"): # Include default as it might be the success response
|
82
|
+
resp = next((r for r in op.responses if r.status_code == code), None)
|
83
|
+
if resp and resp.description:
|
84
|
+
response_desc = resp.description.strip()
|
85
|
+
break
|
86
|
+
if not response_desc: # Fallback to any response description if no 2xx/default found
|
87
|
+
for resp in op.responses:
|
88
|
+
if resp.description:
|
89
|
+
response_desc = resp.description.strip()
|
90
|
+
break
|
91
|
+
|
92
|
+
returns = (return_type, response_desc or "Response object.") if return_type and return_type != "None" else None
|
93
|
+
|
94
|
+
error_codes = [r for r in op.responses if r.status_code.isdigit() and int(r.status_code) >= 400]
|
95
|
+
raises = []
|
96
|
+
if error_codes:
|
97
|
+
for resp in error_codes:
|
98
|
+
# Using a generic HTTPError, specific error classes could be mapped later
|
99
|
+
code_to_raise = "HTTPError"
|
100
|
+
desc = f"{resp.status_code}: {resp.description.strip() if resp.description else 'HTTP error.'}"
|
101
|
+
raises.append((code_to_raise, desc))
|
102
|
+
else:
|
103
|
+
raises.append(("HTTPError", "If the server returns a non-2xx HTTP response."))
|
104
|
+
|
105
|
+
doc_block = DocumentationBlock(
|
106
|
+
summary=summary,
|
107
|
+
description=description,
|
108
|
+
args=args,
|
109
|
+
returns=returns,
|
110
|
+
raises=raises,
|
111
|
+
)
|
112
|
+
|
113
|
+
# The DocumentationWriter handles the actual formatting and wrapping.
|
114
|
+
# The _wrap_docstring helper is not directly used here if DocumentationWriter handles it all.
|
115
|
+
# However, DocumentationWriter.render_docstring itself might need indentation control.
|
116
|
+
# Original called writer.write_line(line) for each line of docstring.
|
117
|
+
docstring_str = self.doc_writer.render_docstring(
|
118
|
+
doc_block, indent=0
|
119
|
+
) # indent=0 as CodeWriter handles method indent
|
120
|
+
for line in docstring_str.splitlines():
|
121
|
+
writer.write_line(line)
|
@@ -0,0 +1,87 @@
|
|
1
|
+
import logging
|
2
|
+
from typing import Any
|
3
|
+
|
4
|
+
from pyopenapi_gen import IROperation
|
5
|
+
|
6
|
+
from ....context.render_context import RenderContext
|
7
|
+
from ....core.utils import Formatter
|
8
|
+
from ....core.writers.code_writer import CodeWriter
|
9
|
+
from ..processors.import_analyzer import EndpointImportAnalyzer
|
10
|
+
from ..processors.parameter_processor import EndpointParameterProcessor
|
11
|
+
from .docstring_generator import EndpointDocstringGenerator
|
12
|
+
from .request_generator import EndpointRequestGenerator
|
13
|
+
from .response_handler_generator import EndpointResponseHandlerGenerator
|
14
|
+
from .signature_generator import EndpointMethodSignatureGenerator
|
15
|
+
from .url_args_generator import EndpointUrlArgsGenerator
|
16
|
+
|
17
|
+
# Get logger instance
|
18
|
+
logger = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
|
21
|
+
class EndpointMethodGenerator:
|
22
|
+
"""
|
23
|
+
Generates the Python code for a single endpoint method.
|
24
|
+
"""
|
25
|
+
|
26
|
+
def __init__(self, schemas: dict[str, Any] | None = None) -> None:
|
27
|
+
self.schemas = schemas or {}
|
28
|
+
self.formatter = Formatter()
|
29
|
+
self.parameter_processor = EndpointParameterProcessor(self.schemas)
|
30
|
+
self.import_analyzer = EndpointImportAnalyzer(self.schemas)
|
31
|
+
self.signature_generator = EndpointMethodSignatureGenerator(self.schemas)
|
32
|
+
self.docstring_generator = EndpointDocstringGenerator(self.schemas)
|
33
|
+
self.url_args_generator = EndpointUrlArgsGenerator(self.schemas)
|
34
|
+
self.request_generator = EndpointRequestGenerator(self.schemas)
|
35
|
+
self.response_handler_generator = EndpointResponseHandlerGenerator(self.schemas)
|
36
|
+
|
37
|
+
def generate(self, op: IROperation, context: RenderContext) -> str:
|
38
|
+
"""
|
39
|
+
Generate a fully functional async endpoint method for the given operation.
|
40
|
+
Returns the method code as a string.
|
41
|
+
"""
|
42
|
+
writer = CodeWriter()
|
43
|
+
context.add_import(f"{context.core_package_name}.http_transport", "HttpTransport")
|
44
|
+
context.add_import(f"{context.core_package_name}.exceptions", "HTTPError")
|
45
|
+
|
46
|
+
# Special case for updateAgentDataSource was removed.
|
47
|
+
|
48
|
+
self.import_analyzer.analyze_and_register_imports(op, context)
|
49
|
+
|
50
|
+
ordered_params, primary_content_type, resolved_body_type = self.parameter_processor.process_parameters(
|
51
|
+
op, context
|
52
|
+
)
|
53
|
+
self.signature_generator.generate_signature(writer, op, context, ordered_params)
|
54
|
+
|
55
|
+
self.docstring_generator.generate_docstring(writer, op, context, primary_content_type)
|
56
|
+
|
57
|
+
# Snapshot of code *before* main body parts are written
|
58
|
+
# This includes signature and docstring.
|
59
|
+
code_snapshot_before_body_parts = writer.get_code()
|
60
|
+
|
61
|
+
has_header_params = self.url_args_generator.generate_url_and_args(
|
62
|
+
writer, op, context, ordered_params, primary_content_type, resolved_body_type
|
63
|
+
)
|
64
|
+
self.request_generator.generate_request_call(writer, op, context, has_header_params, primary_content_type)
|
65
|
+
|
66
|
+
# Call the new response handler generator
|
67
|
+
self.response_handler_generator.generate_response_handling(writer, op, context)
|
68
|
+
|
69
|
+
# Check if any actual statements were added for the body
|
70
|
+
current_full_code = writer.get_code()
|
71
|
+
# The part of the code added by the body-writing methods
|
72
|
+
body_part_actually_written = current_full_code[len(code_snapshot_before_body_parts) :]
|
73
|
+
|
74
|
+
body_is_effectively_empty = True
|
75
|
+
# Check if the written body part contains any non-comment, non-whitespace lines
|
76
|
+
if body_part_actually_written.strip(): # Check if non-whitespace exists at all
|
77
|
+
if any(
|
78
|
+
line.strip() and not line.strip().startswith("#") for line in body_part_actually_written.splitlines()
|
79
|
+
):
|
80
|
+
body_is_effectively_empty = False
|
81
|
+
|
82
|
+
if body_is_effectively_empty:
|
83
|
+
writer.write_line("pass")
|
84
|
+
|
85
|
+
writer.dedent() # This matches the indent() from _write_method_signature
|
86
|
+
|
87
|
+
return writer.get_code().strip()
|