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,477 @@
|
|
|
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
|
+
# Step 1: Process tags and build tag_tuples
|
|
28
|
+
tag_candidates: dict[str, list[str]] = {}
|
|
29
|
+
for op in spec.operations:
|
|
30
|
+
# Use DEFAULT_TAG consistent with EndpointsEmitter
|
|
31
|
+
tags = op.tags or ["default"] # Use literal "default" here
|
|
32
|
+
# Loop through the determined tags (original or default)
|
|
33
|
+
for tag in tags:
|
|
34
|
+
key = NameSanitizer.normalize_tag_key(tag)
|
|
35
|
+
if key not in tag_candidates:
|
|
36
|
+
tag_candidates[key] = []
|
|
37
|
+
tag_candidates[key].append(tag)
|
|
38
|
+
# Ensure the old logic is removed (idempotent if already gone)
|
|
39
|
+
# if op.tags:
|
|
40
|
+
# ...
|
|
41
|
+
# else:
|
|
42
|
+
# ...
|
|
43
|
+
|
|
44
|
+
def tag_score(t: str) -> tuple[bool, int, int, str]:
|
|
45
|
+
is_pascal = bool(re.search(r"[a-z][A-Z]", t)) or bool(re.search(r"[A-Z]{2,}", t))
|
|
46
|
+
words = re.findall(r"[A-Z]?[a-z]+|[A-Z]+(?![a-z])|[0-9]+", t)
|
|
47
|
+
words += re.split(r"[_-]+", t)
|
|
48
|
+
word_count = len([w for w in words if w])
|
|
49
|
+
upper = sum(1 for c in t if c.isupper())
|
|
50
|
+
return (is_pascal, word_count, upper, t)
|
|
51
|
+
|
|
52
|
+
tag_map = {}
|
|
53
|
+
for key, candidates in tag_candidates.items():
|
|
54
|
+
best = max(candidates, key=tag_score)
|
|
55
|
+
tag_map[key] = best
|
|
56
|
+
tag_tuples = [
|
|
57
|
+
(
|
|
58
|
+
tag_map[key],
|
|
59
|
+
NameSanitizer.sanitize_class_name(tag_map[key]) + "Client",
|
|
60
|
+
NameSanitizer.sanitize_module_name(tag_map[key]),
|
|
61
|
+
)
|
|
62
|
+
for key in sorted(tag_map)
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
# Step 2: Generate Protocol definition
|
|
66
|
+
protocol_code = self.generate_client_protocol(spec, context, tag_tuples)
|
|
67
|
+
|
|
68
|
+
# Step 3: Generate implementation class
|
|
69
|
+
impl_code = self._generate_client_implementation(spec, context, tag_tuples)
|
|
70
|
+
|
|
71
|
+
# Step 4: Combine Protocol and implementation
|
|
72
|
+
return f"{protocol_code}\n\n\n{impl_code}"
|
|
73
|
+
|
|
74
|
+
def _generate_client_implementation(
|
|
75
|
+
self, spec: IRSpec, context: RenderContext, tag_tuples: list[tuple[str, str, str]]
|
|
76
|
+
) -> str:
|
|
77
|
+
"""
|
|
78
|
+
Generate the APIClient implementation class.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
spec: The IR specification
|
|
82
|
+
context: Render context for import management
|
|
83
|
+
tag_tuples: List of (tag_name, class_name, module_name) tuples
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Implementation class code as string
|
|
87
|
+
"""
|
|
88
|
+
writer = CodeWriter()
|
|
89
|
+
# Register all endpoint client imports using relative imports (endpoints are within the same package)
|
|
90
|
+
for _, class_name, module_name in tag_tuples:
|
|
91
|
+
# Use relative imports for endpoints since they're part of the same generated package
|
|
92
|
+
# Import both the implementation and the Protocol
|
|
93
|
+
protocol_name = f"{class_name}Protocol"
|
|
94
|
+
context.import_collector.add_relative_import(f".endpoints.{module_name}", class_name)
|
|
95
|
+
context.import_collector.add_relative_import(f".endpoints.{module_name}", protocol_name)
|
|
96
|
+
|
|
97
|
+
# Register core/config/typing imports for class signature
|
|
98
|
+
# Use LOGICAL import path for core components
|
|
99
|
+
|
|
100
|
+
# Use the core_package name from the context to form the base of the import path
|
|
101
|
+
# RenderContext.add_import will handle making it relative correctly based on the current file.
|
|
102
|
+
context.add_import(f"{context.core_package_name}.http_transport", "HttpTransport")
|
|
103
|
+
context.add_import(f"{context.core_package_name}.http_transport", "HttpxTransport")
|
|
104
|
+
context.add_import(f"{context.core_package_name}.config", "ClientConfig")
|
|
105
|
+
# If security schemes are present and an auth plugin like ApiKeyAuth is used by the client itself,
|
|
106
|
+
# it would also be registered here using context.core_package.
|
|
107
|
+
# For now, the client_py_content check in the test looks for this for ApiKeyAuth specifically:
|
|
108
|
+
context.add_import(f"{context.core_package_name}.auth.plugins", "ApiKeyAuth")
|
|
109
|
+
|
|
110
|
+
context.add_typing_imports_for_type("HttpTransport | None")
|
|
111
|
+
context.add_typing_imports_for_type("Any")
|
|
112
|
+
context.add_typing_imports_for_type("Dict")
|
|
113
|
+
# Class definition - implements Protocol
|
|
114
|
+
writer.write_line("class APIClient(APIClientProtocol):")
|
|
115
|
+
writer.indent()
|
|
116
|
+
# Build docstring for APIClient
|
|
117
|
+
docstring_lines = []
|
|
118
|
+
# Add API title and version
|
|
119
|
+
docstring_lines.append(f"{spec.title} (version {spec.version})")
|
|
120
|
+
# Add API description if present
|
|
121
|
+
if getattr(spec, "description", None):
|
|
122
|
+
desc = spec.description
|
|
123
|
+
if desc is not None:
|
|
124
|
+
# Remove triple quotes, escape backslashes, and dedent
|
|
125
|
+
desc_clean = desc.replace('"""', "'").replace("'''", "'").replace("\\", "\\\\").strip()
|
|
126
|
+
desc_clean = textwrap.dedent(desc_clean)
|
|
127
|
+
docstring_lines.append("")
|
|
128
|
+
docstring_lines.append(desc_clean)
|
|
129
|
+
# Add a blank line before the generated summary/args
|
|
130
|
+
docstring_lines.append("")
|
|
131
|
+
summary = "Async API client with pluggable transport, tag-specific clients, and client-level headers."
|
|
132
|
+
args: list[tuple[str, str, str]] = [
|
|
133
|
+
("config", "ClientConfig", "Client configuration object."),
|
|
134
|
+
("transport", "HttpTransport | None", "Custom HTTP transport (optional)."),
|
|
135
|
+
]
|
|
136
|
+
for tag, class_name, module_name in tag_tuples:
|
|
137
|
+
args.append((module_name, class_name, f"Client for '{tag}' endpoints."))
|
|
138
|
+
doc_block = DocumentationBlock(
|
|
139
|
+
summary=summary,
|
|
140
|
+
args=cast(list[tuple[str, str, str] | tuple[str, str]], args),
|
|
141
|
+
)
|
|
142
|
+
docstring = DocumentationWriter(width=88).render_docstring(doc_block, indent=0)
|
|
143
|
+
docstring_lines.extend([line for line in docstring.splitlines()])
|
|
144
|
+
# Write only one docstring, no extra triple quotes after
|
|
145
|
+
writer.write_line('"""') # At class indent (1)
|
|
146
|
+
writer.dedent() # Go to indent 0 for docstring content
|
|
147
|
+
for line in docstring_lines:
|
|
148
|
+
writer.write_line(line.rstrip('"'))
|
|
149
|
+
writer.indent() # Back to class indent (1)
|
|
150
|
+
writer.write_line('"""')
|
|
151
|
+
# __init__
|
|
152
|
+
writer.write_line("def __init__(self, config: ClientConfig, transport: HttpTransport | None = None) -> None:")
|
|
153
|
+
writer.indent()
|
|
154
|
+
writer.write_line("self.config = config")
|
|
155
|
+
writer.write_line(
|
|
156
|
+
"self.transport = transport if transport is not None else "
|
|
157
|
+
"HttpxTransport(str(config.base_url), config.timeout)"
|
|
158
|
+
)
|
|
159
|
+
writer.write_line("self._base_url: str = str(self.config.base_url)")
|
|
160
|
+
# Initialize private fields for each tag client
|
|
161
|
+
for tag, class_name, module_name in tag_tuples:
|
|
162
|
+
context.add_typing_imports_for_type(f"{class_name} | None")
|
|
163
|
+
writer.write_line(f"self._{module_name}: {class_name} | None = None")
|
|
164
|
+
writer.dedent()
|
|
165
|
+
writer.write_line("")
|
|
166
|
+
# @property for each tag client
|
|
167
|
+
for tag, class_name, module_name in tag_tuples:
|
|
168
|
+
writer.write_line(f"@property")
|
|
169
|
+
# Use context.add_import here too
|
|
170
|
+
current_gen_pkg_name_prop = context.get_current_package_name_for_generated_code()
|
|
171
|
+
if not current_gen_pkg_name_prop:
|
|
172
|
+
logger.error(
|
|
173
|
+
f"[ClientVisitor Property] Could not determine generated package name from context. "
|
|
174
|
+
f"Cannot form fully qualified import for endpoints.{module_name}.{class_name}"
|
|
175
|
+
)
|
|
176
|
+
# Fallback or raise error
|
|
177
|
+
context.add_import(f"endpoints.{module_name}", class_name)
|
|
178
|
+
else:
|
|
179
|
+
logical_module_for_add_import_prop = f"{current_gen_pkg_name_prop}.endpoints.{module_name}"
|
|
180
|
+
context.add_import(logical_module_for_add_import_prop, class_name)
|
|
181
|
+
|
|
182
|
+
writer.write_line(f"def {module_name}(self) -> {class_name}:")
|
|
183
|
+
writer.indent()
|
|
184
|
+
writer.write_line(f'"""Client for \'{tag}\' endpoints."""')
|
|
185
|
+
writer.write_line(f"if self._{module_name} is None:")
|
|
186
|
+
writer.indent()
|
|
187
|
+
writer.write_line(f"self._{module_name} = {class_name}(self.transport, self._base_url)")
|
|
188
|
+
writer.dedent()
|
|
189
|
+
writer.write_line(f"return self._{module_name}")
|
|
190
|
+
writer.dedent()
|
|
191
|
+
writer.write_line("")
|
|
192
|
+
# request method
|
|
193
|
+
context.add_typing_imports_for_type("Any")
|
|
194
|
+
writer.write_line("async def request(self, method: str, url: str, **kwargs: Any) -> Any:")
|
|
195
|
+
writer.indent()
|
|
196
|
+
writer.write_line('"""Send an HTTP request via the transport."""')
|
|
197
|
+
writer.write_line("return await self.transport.request(method, url, **kwargs)")
|
|
198
|
+
writer.dedent()
|
|
199
|
+
writer.write_line("")
|
|
200
|
+
# close method
|
|
201
|
+
context.add_typing_imports_for_type("None")
|
|
202
|
+
writer.write_line("async def close(self) -> None:")
|
|
203
|
+
writer.indent()
|
|
204
|
+
writer.write_line('"""Close the underlying transport if supported."""')
|
|
205
|
+
writer.write_line("if hasattr(self.transport, 'close'):")
|
|
206
|
+
writer.indent()
|
|
207
|
+
writer.write_line("await self.transport.close()")
|
|
208
|
+
writer.dedent()
|
|
209
|
+
writer.write_line("else:")
|
|
210
|
+
writer.indent()
|
|
211
|
+
writer.write_line("pass # Or log a warning if close is expected but not found")
|
|
212
|
+
writer.dedent()
|
|
213
|
+
writer.dedent()
|
|
214
|
+
writer.write_line("")
|
|
215
|
+
# __aenter__ for async context management (dedented)
|
|
216
|
+
writer.write_line("async def __aenter__(self) -> 'APIClient':")
|
|
217
|
+
writer.indent()
|
|
218
|
+
writer.write_line('"""Enter the async context manager. Returns self."""')
|
|
219
|
+
writer.write_line("if hasattr(self.transport, '__aenter__'):")
|
|
220
|
+
writer.indent()
|
|
221
|
+
writer.write_line("await self.transport.__aenter__()")
|
|
222
|
+
writer.dedent()
|
|
223
|
+
writer.write_line("return self")
|
|
224
|
+
writer.dedent()
|
|
225
|
+
writer.write_line("")
|
|
226
|
+
# __aexit__ for async context management (dedented)
|
|
227
|
+
context.add_typing_imports_for_type("type[BaseException] | None")
|
|
228
|
+
context.add_typing_imports_for_type("BaseException | None")
|
|
229
|
+
context.add_typing_imports_for_type("object | None")
|
|
230
|
+
writer.write_line(
|
|
231
|
+
"async def __aexit__(self, exc_type: type[BaseException] | None, "
|
|
232
|
+
"exc_val: BaseException | None, exc_tb: object | None) -> None:"
|
|
233
|
+
)
|
|
234
|
+
writer.indent()
|
|
235
|
+
writer.write_line('"""Exit the async context manager, ensuring transport is closed."""')
|
|
236
|
+
# Close internal transport if it supports __aexit__
|
|
237
|
+
writer.write_line("if hasattr(self.transport, '__aexit__'):")
|
|
238
|
+
writer.indent()
|
|
239
|
+
writer.write_line("await self.transport.__aexit__(exc_type, exc_val, exc_tb)")
|
|
240
|
+
writer.dedent()
|
|
241
|
+
writer.write_line("else:") # Fallback if transport doesn't have __aexit__ but has close()
|
|
242
|
+
writer.indent()
|
|
243
|
+
writer.write_line("await self.close()")
|
|
244
|
+
writer.dedent()
|
|
245
|
+
writer.dedent()
|
|
246
|
+
writer.write_line("")
|
|
247
|
+
|
|
248
|
+
# Ensure crucial core imports are present before final rendering
|
|
249
|
+
# This is a fallback / re-emphasis due to potential issues with ImportCollector
|
|
250
|
+
context.add_import(f"{context.core_package_name}.http_transport", "HttpTransport")
|
|
251
|
+
context.add_import(f"{context.core_package_name}.http_transport", "HttpxTransport")
|
|
252
|
+
context.add_import(f"{context.core_package_name}.config", "ClientConfig")
|
|
253
|
+
|
|
254
|
+
return writer.get_code()
|
|
255
|
+
|
|
256
|
+
def generate_client_protocol(
|
|
257
|
+
self, spec: IRSpec, context: RenderContext, tag_tuples: list[tuple[str, str, str]]
|
|
258
|
+
) -> str:
|
|
259
|
+
"""
|
|
260
|
+
Generate APIClientProtocol defining the client interface.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
spec: The IR specification
|
|
264
|
+
context: Render context for import management
|
|
265
|
+
tag_tuples: List of (tag_name, class_name, module_name) tuples
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Protocol class code as string with:
|
|
269
|
+
- Tag-based endpoint properties
|
|
270
|
+
- Standard methods (request, close, __aenter__, __aexit__)
|
|
271
|
+
"""
|
|
272
|
+
# Register Protocol imports
|
|
273
|
+
context.add_typing_imports_for_type("Protocol")
|
|
274
|
+
context.add_import("typing", "runtime_checkable")
|
|
275
|
+
context.add_typing_imports_for_type("Any")
|
|
276
|
+
|
|
277
|
+
writer = CodeWriter()
|
|
278
|
+
|
|
279
|
+
# Protocol class header
|
|
280
|
+
writer.write_line("@runtime_checkable")
|
|
281
|
+
writer.write_line("class APIClientProtocol(Protocol):")
|
|
282
|
+
writer.indent()
|
|
283
|
+
|
|
284
|
+
# Docstring
|
|
285
|
+
writer.write_line('"""Protocol defining the interface of APIClient for dependency injection."""')
|
|
286
|
+
writer.write_line("")
|
|
287
|
+
|
|
288
|
+
# Tag-based endpoint properties
|
|
289
|
+
for tag, class_name, module_name in tag_tuples:
|
|
290
|
+
# Use forward reference for tag client protocol
|
|
291
|
+
protocol_name = f"{class_name}Protocol"
|
|
292
|
+
writer.write_line("@property")
|
|
293
|
+
writer.write_line(f"def {module_name}(self) -> '{protocol_name}':")
|
|
294
|
+
writer.indent()
|
|
295
|
+
writer.write_line("...")
|
|
296
|
+
writer.dedent()
|
|
297
|
+
writer.write_line("")
|
|
298
|
+
|
|
299
|
+
# Standard methods
|
|
300
|
+
# request method
|
|
301
|
+
writer.write_line("async def request(self, method: str, url: str, **kwargs: Any) -> Any:")
|
|
302
|
+
writer.indent()
|
|
303
|
+
writer.write_line("...")
|
|
304
|
+
writer.dedent()
|
|
305
|
+
writer.write_line("")
|
|
306
|
+
|
|
307
|
+
# close method
|
|
308
|
+
writer.write_line("async def close(self) -> None:")
|
|
309
|
+
writer.indent()
|
|
310
|
+
writer.write_line("...")
|
|
311
|
+
writer.dedent()
|
|
312
|
+
writer.write_line("")
|
|
313
|
+
|
|
314
|
+
# __aenter__ method
|
|
315
|
+
writer.write_line("async def __aenter__(self) -> 'APIClientProtocol':")
|
|
316
|
+
writer.indent()
|
|
317
|
+
writer.write_line("...")
|
|
318
|
+
writer.dedent()
|
|
319
|
+
writer.write_line("")
|
|
320
|
+
|
|
321
|
+
# __aexit__ method
|
|
322
|
+
context.add_typing_imports_for_type("type[BaseException] | None")
|
|
323
|
+
context.add_typing_imports_for_type("BaseException | None")
|
|
324
|
+
context.add_typing_imports_for_type("object | None")
|
|
325
|
+
writer.write_line(
|
|
326
|
+
"async def __aexit__(self, exc_type: type[BaseException] | None, "
|
|
327
|
+
"exc_val: BaseException | None, exc_tb: object | None) -> None:"
|
|
328
|
+
)
|
|
329
|
+
writer.indent()
|
|
330
|
+
writer.write_line("...")
|
|
331
|
+
writer.dedent()
|
|
332
|
+
|
|
333
|
+
writer.dedent() # Close class
|
|
334
|
+
return writer.get_code()
|
|
335
|
+
|
|
336
|
+
def generate_client_mock_class(
|
|
337
|
+
self, spec: IRSpec, context: RenderContext, tag_tuples: list[tuple[str, str, str]]
|
|
338
|
+
) -> str:
|
|
339
|
+
"""
|
|
340
|
+
Generate MockAPIClient for testing.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
spec: The IR specification
|
|
344
|
+
context: Render context for import management
|
|
345
|
+
tag_tuples: List of (tag, class_name, module_name) tuples
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
Mock client class code as string
|
|
349
|
+
"""
|
|
350
|
+
# Import TYPE_CHECKING for Protocol imports
|
|
351
|
+
context.add_import("typing", "TYPE_CHECKING")
|
|
352
|
+
context.add_import("typing", "Any")
|
|
353
|
+
|
|
354
|
+
writer = CodeWriter()
|
|
355
|
+
|
|
356
|
+
# TYPE_CHECKING imports
|
|
357
|
+
writer.write_line("if TYPE_CHECKING:")
|
|
358
|
+
writer.indent()
|
|
359
|
+
writer.write_line("from ..client import APIClientProtocol")
|
|
360
|
+
for tag, class_name, module_name in tag_tuples:
|
|
361
|
+
protocol_name = f"{class_name}Protocol"
|
|
362
|
+
writer.write_line(f"from ..endpoints.{module_name} import {protocol_name}")
|
|
363
|
+
writer.dedent()
|
|
364
|
+
writer.write_line("")
|
|
365
|
+
|
|
366
|
+
# Import mock endpoint classes
|
|
367
|
+
for tag, class_name, module_name in tag_tuples:
|
|
368
|
+
mock_class_name = f"Mock{class_name}"
|
|
369
|
+
writer.write_line(f"from .endpoints.mock_{module_name} import {mock_class_name}")
|
|
370
|
+
writer.write_line("")
|
|
371
|
+
|
|
372
|
+
# Class definition
|
|
373
|
+
writer.write_line("class MockAPIClient:")
|
|
374
|
+
writer.indent()
|
|
375
|
+
|
|
376
|
+
# Docstring
|
|
377
|
+
writer.write_line('"""')
|
|
378
|
+
writer.write_line("Mock implementation of APIClient for testing.")
|
|
379
|
+
writer.write_line("")
|
|
380
|
+
writer.write_line("Auto-creates default mock implementations for all tag-based endpoint clients.")
|
|
381
|
+
writer.write_line("You can override specific tag clients by passing them to the constructor.")
|
|
382
|
+
writer.write_line("")
|
|
383
|
+
writer.write_line("Example:")
|
|
384
|
+
writer.write_line(" # Use all defaults")
|
|
385
|
+
writer.write_line(" client = MockAPIClient()")
|
|
386
|
+
writer.write_line("")
|
|
387
|
+
writer.write_line(" # Override specific tag client")
|
|
388
|
+
for tag, class_name, module_name in tag_tuples[:1]: # Show example with first tag
|
|
389
|
+
mock_class_name = f"Mock{class_name}"
|
|
390
|
+
writer.write_line(f" class My{class_name}Mock({mock_class_name}):")
|
|
391
|
+
writer.write_line(" async def method_name(self, ...) -> ReturnType:")
|
|
392
|
+
writer.write_line(" return test_data")
|
|
393
|
+
writer.write_line("")
|
|
394
|
+
writer.write_line(f" client = MockAPIClient({module_name}=My{class_name}Mock())")
|
|
395
|
+
break
|
|
396
|
+
writer.write_line('"""')
|
|
397
|
+
writer.write_line("")
|
|
398
|
+
|
|
399
|
+
# Constructor
|
|
400
|
+
writer.write_line("def __init__(")
|
|
401
|
+
writer.indent()
|
|
402
|
+
writer.write_line("self,")
|
|
403
|
+
for tag, class_name, module_name in tag_tuples:
|
|
404
|
+
protocol_name = f"{class_name}Protocol"
|
|
405
|
+
writer.write_line(f'{module_name}: "{protocol_name} | None" = None,')
|
|
406
|
+
writer.dedent()
|
|
407
|
+
writer.write_line(") -> None:")
|
|
408
|
+
writer.indent()
|
|
409
|
+
|
|
410
|
+
# Initialize tag clients
|
|
411
|
+
for tag, class_name, module_name in tag_tuples:
|
|
412
|
+
mock_class_name = f"Mock{class_name}"
|
|
413
|
+
writer.write_line(
|
|
414
|
+
f"self._{module_name} = {module_name} if {module_name} is not None else {mock_class_name}()"
|
|
415
|
+
)
|
|
416
|
+
writer.dedent()
|
|
417
|
+
writer.write_line("")
|
|
418
|
+
|
|
419
|
+
# Properties for tag clients
|
|
420
|
+
for tag, class_name, module_name in tag_tuples:
|
|
421
|
+
protocol_name = f"{class_name}Protocol"
|
|
422
|
+
writer.write_line("@property")
|
|
423
|
+
writer.write_line(f'def {module_name}(self) -> "{protocol_name}":')
|
|
424
|
+
writer.indent()
|
|
425
|
+
writer.write_line(f"return self._{module_name}")
|
|
426
|
+
writer.dedent()
|
|
427
|
+
writer.write_line("")
|
|
428
|
+
|
|
429
|
+
# request() method
|
|
430
|
+
writer.write_line("async def request(self, method: str, url: str, **kwargs: Any) -> Any:")
|
|
431
|
+
writer.indent()
|
|
432
|
+
writer.write_line('"""')
|
|
433
|
+
writer.write_line("Mock request method - raises NotImplementedError.")
|
|
434
|
+
writer.write_line("")
|
|
435
|
+
writer.write_line("This is a low-level method - consider using tag-specific methods instead.")
|
|
436
|
+
writer.write_line('"""')
|
|
437
|
+
writer.write_line(
|
|
438
|
+
"raise NotImplementedError("
|
|
439
|
+
'"MockAPIClient.request() not implemented. '
|
|
440
|
+
'Use tag-specific methods instead."'
|
|
441
|
+
")"
|
|
442
|
+
)
|
|
443
|
+
writer.dedent()
|
|
444
|
+
writer.write_line("")
|
|
445
|
+
|
|
446
|
+
# close() method
|
|
447
|
+
writer.write_line("async def close(self) -> None:")
|
|
448
|
+
writer.indent()
|
|
449
|
+
writer.write_line('"""Mock close method - no-op for testing."""')
|
|
450
|
+
writer.write_line("pass # No cleanup needed for mocks")
|
|
451
|
+
writer.dedent()
|
|
452
|
+
writer.write_line("")
|
|
453
|
+
|
|
454
|
+
# __aenter__() method
|
|
455
|
+
writer.write_line('async def __aenter__(self) -> "APIClientProtocol":')
|
|
456
|
+
writer.indent()
|
|
457
|
+
writer.write_line('"""Enter async context manager."""')
|
|
458
|
+
writer.write_line("return self")
|
|
459
|
+
writer.dedent()
|
|
460
|
+
writer.write_line("")
|
|
461
|
+
|
|
462
|
+
# __aexit__() method
|
|
463
|
+
writer.write_line(
|
|
464
|
+
"async def __aexit__("
|
|
465
|
+
"self, "
|
|
466
|
+
"exc_type: type[BaseException] | None, "
|
|
467
|
+
"exc_val: BaseException | None, "
|
|
468
|
+
"exc_tb: object | None"
|
|
469
|
+
") -> None:"
|
|
470
|
+
)
|
|
471
|
+
writer.indent()
|
|
472
|
+
writer.write_line('"""Exit async context manager - no-op for mocks."""')
|
|
473
|
+
writer.write_line("pass # No cleanup needed for mocks")
|
|
474
|
+
writer.dedent()
|
|
475
|
+
|
|
476
|
+
writer.dedent() # Close class
|
|
477
|
+
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
|