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,44 @@
|
|
1
|
+
import os
|
2
|
+
|
3
|
+
from pyopenapi_gen import IRSpec
|
4
|
+
from pyopenapi_gen.context.render_context import RenderContext
|
5
|
+
|
6
|
+
from ..visit.docs_visitor import DocsVisitor
|
7
|
+
|
8
|
+
"""Simple documentation emitter using markdown with Python str.format placeholders."""
|
9
|
+
DOCS_INDEX_TEMPLATE = """# API Documentation
|
10
|
+
|
11
|
+
Generated documentation for the API.
|
12
|
+
|
13
|
+
## Tags
|
14
|
+
{tags_list}
|
15
|
+
"""
|
16
|
+
|
17
|
+
DOCS_TAG_TEMPLATE = """# {tag} Operations
|
18
|
+
|
19
|
+
{operations_list}
|
20
|
+
"""
|
21
|
+
|
22
|
+
DOCS_OPERATION_TEMPLATE = """### {operation_id}
|
23
|
+
|
24
|
+
**Method:** `{method}`
|
25
|
+
**Path:** `{path}`
|
26
|
+
|
27
|
+
{description}
|
28
|
+
"""
|
29
|
+
|
30
|
+
|
31
|
+
class DocsEmitter:
|
32
|
+
"""Generates markdown documentation per tag from IRSpec using visitor/context."""
|
33
|
+
|
34
|
+
def __init__(self) -> None:
|
35
|
+
self.visitor = DocsVisitor()
|
36
|
+
|
37
|
+
def emit(self, spec: IRSpec, output_dir: str) -> None:
|
38
|
+
"""Render docs into <output_dir> as markdown files."""
|
39
|
+
docs_dir = os.path.join(output_dir)
|
40
|
+
context = RenderContext()
|
41
|
+
context.file_manager.ensure_dir(docs_dir)
|
42
|
+
docs = self.visitor.visit(spec, context)
|
43
|
+
for filename, content in docs.items():
|
44
|
+
context.file_manager.write_file(os.path.join(docs_dir, filename), content)
|
@@ -0,0 +1,223 @@
|
|
1
|
+
import logging
|
2
|
+
from pathlib import Path
|
3
|
+
from typing import Dict, List, Optional, Tuple
|
4
|
+
|
5
|
+
from pyopenapi_gen import IROperation, IRParameter, IRRequestBody
|
6
|
+
from pyopenapi_gen.context.render_context import RenderContext
|
7
|
+
from pyopenapi_gen.visit.endpoint.endpoint_visitor import EndpointVisitor
|
8
|
+
|
9
|
+
from ..core.utils import Formatter, NameSanitizer
|
10
|
+
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
# Basic OpenAPI schema to Python type mapping for parameters
|
14
|
+
PARAM_TYPE_MAPPING = {
|
15
|
+
"integer": "int",
|
16
|
+
"number": "float",
|
17
|
+
"boolean": "bool",
|
18
|
+
"string": "str",
|
19
|
+
"array": "List",
|
20
|
+
"object": "Dict[str, Any]",
|
21
|
+
}
|
22
|
+
# Format-specific overrides
|
23
|
+
PARAM_FORMAT_MAPPING = {
|
24
|
+
"int32": "int",
|
25
|
+
"int64": "int",
|
26
|
+
"float": "float",
|
27
|
+
"double": "float",
|
28
|
+
"byte": "str",
|
29
|
+
"binary": "bytes",
|
30
|
+
"date": "date",
|
31
|
+
"date-time": "datetime",
|
32
|
+
}
|
33
|
+
|
34
|
+
# Default tag for untagged operations
|
35
|
+
DEFAULT_TAG = "default"
|
36
|
+
|
37
|
+
|
38
|
+
def schema_to_type(schema: IRParameter) -> str:
|
39
|
+
"""Convert an IRParameter's schema to a Python type string."""
|
40
|
+
s = schema.schema # s is an IRSchema instance
|
41
|
+
py_type: str = "Any" # Default base type
|
42
|
+
|
43
|
+
# 1. Determine base type (without Optional wrapper yet)
|
44
|
+
# Format-specific override has highest precedence for base type determination
|
45
|
+
if s.format and s.format in PARAM_FORMAT_MAPPING:
|
46
|
+
py_type = PARAM_FORMAT_MAPPING[s.format]
|
47
|
+
# Array handling
|
48
|
+
elif s.type == "array" and s.items:
|
49
|
+
# For array items, we recursively call schema_to_type.
|
50
|
+
# The nullability of the item_type itself (e.g. List[Optional[int]])
|
51
|
+
# will be handled by the recursive call based on s.items.is_nullable.
|
52
|
+
item_schema_as_param = IRParameter(name="_item", param_in="_internal", required=False, schema=s.items)
|
53
|
+
item_type_str = schema_to_type(item_schema_as_param)
|
54
|
+
py_type = f"List[{item_type_str}]"
|
55
|
+
# Default mapping based on s.type (primary type)
|
56
|
+
elif s.type and s.type in PARAM_TYPE_MAPPING:
|
57
|
+
py_type = PARAM_TYPE_MAPPING[s.type]
|
58
|
+
# Fallback if type is None or not in mappings (and not format override/array)
|
59
|
+
# If s.type is None and there was no format override, it defaults to "Any".
|
60
|
+
# If s.type is something not recognized, it also defaults to "Any".
|
61
|
+
elif not s.type and not s.format: # Type is None, no format override
|
62
|
+
py_type = "Any"
|
63
|
+
elif s.type: # Type is some string not in PARAM_TYPE_MAPPING and not an array handled above
|
64
|
+
# This could be a reference to a model. For now, schema_to_type is simple and returns Any.
|
65
|
+
# A more sophisticated version would return the schema name for model visitor to handle.
|
66
|
+
# However, based on existing PARAM_TYPE_MAPPING, unknown types become "Any".
|
67
|
+
py_type = "Any"
|
68
|
+
# If py_type is still "Any" here, it means none of the above conditions strongly set a type.
|
69
|
+
|
70
|
+
# 2. Apply nullability based on IRSchema's is_nullable field
|
71
|
+
# This s.is_nullable should be the source of truth from the IR after parsing.
|
72
|
+
if s.is_nullable:
|
73
|
+
# Ensure "Any" also gets wrapped, e.g. Optional[Any]
|
74
|
+
py_type = f"Optional[{py_type}]"
|
75
|
+
|
76
|
+
return py_type
|
77
|
+
|
78
|
+
|
79
|
+
def _get_request_body_type(body: IRRequestBody) -> str:
|
80
|
+
"""Determine the Python type for a request body schema."""
|
81
|
+
for mt, sch in body.content.items():
|
82
|
+
if "json" in mt.lower():
|
83
|
+
return schema_to_type(IRParameter(name="body", param_in="body", required=body.required, schema=sch))
|
84
|
+
# Fallback to generic dict
|
85
|
+
return "Dict[str, Any]"
|
86
|
+
|
87
|
+
|
88
|
+
def _deduplicate_tag_clients(client_classes: List[Tuple[str, str]]) -> List[Tuple[str, str]]:
|
89
|
+
"""
|
90
|
+
Deduplicate client class/module pairs by canonical module/class name.
|
91
|
+
Returns a list of unique (class_name, module_name) pairs.
|
92
|
+
"""
|
93
|
+
seen = set()
|
94
|
+
unique = []
|
95
|
+
for cls, mod in client_classes:
|
96
|
+
key = (cls.lower(), mod.lower())
|
97
|
+
if key not in seen:
|
98
|
+
seen.add(key)
|
99
|
+
unique.append((cls, mod))
|
100
|
+
return unique
|
101
|
+
|
102
|
+
|
103
|
+
class EndpointsEmitter:
|
104
|
+
"""Generates endpoint modules organized by tag from IRSpec using the visitor/context architecture."""
|
105
|
+
|
106
|
+
def __init__(self, context: RenderContext) -> None:
|
107
|
+
self.context = context
|
108
|
+
self.formatter = Formatter()
|
109
|
+
self.visitor: Optional[EndpointVisitor] = None
|
110
|
+
|
111
|
+
def _deduplicate_operation_ids(self, operations: List[IROperation]) -> None:
|
112
|
+
"""
|
113
|
+
Ensures all operations have unique method names within a tag.
|
114
|
+
|
115
|
+
Args:
|
116
|
+
operations: List of operations for a single tag.
|
117
|
+
"""
|
118
|
+
seen_methods: Dict[str, int] = {}
|
119
|
+
for op in operations:
|
120
|
+
method_name = NameSanitizer.sanitize_method_name(op.operation_id)
|
121
|
+
if method_name in seen_methods:
|
122
|
+
seen_methods[method_name] += 1
|
123
|
+
new_op_id = f"{op.operation_id}_{seen_methods[method_name]}"
|
124
|
+
op.operation_id = new_op_id
|
125
|
+
else:
|
126
|
+
seen_methods[method_name] = 1
|
127
|
+
|
128
|
+
def emit(self, operations: List[IROperation], output_dir_str: str) -> List[str]:
|
129
|
+
"""Render endpoint client files per tag under <output_dir>/endpoints.
|
130
|
+
Returns a list of generated file paths."""
|
131
|
+
output_dir = Path(output_dir_str)
|
132
|
+
endpoints_dir = output_dir / "endpoints"
|
133
|
+
|
134
|
+
self.context.file_manager.ensure_dir(str(endpoints_dir))
|
135
|
+
|
136
|
+
# Manage __init__.py and py.typed files
|
137
|
+
common_files_to_ensure = [
|
138
|
+
(endpoints_dir / "__init__.py", ""),
|
139
|
+
(output_dir / "__init__.py", ""), # Ensure root client package __init__.py
|
140
|
+
(endpoints_dir / "py.typed", ""),
|
141
|
+
]
|
142
|
+
for file_path, content in common_files_to_ensure:
|
143
|
+
if not file_path.exists():
|
144
|
+
self.context.file_manager.write_file(str(file_path), content)
|
145
|
+
|
146
|
+
# Ensure parsed_schemas is at least an empty dict if None,
|
147
|
+
# as EndpointVisitor expects Dict[str, IRSchema]
|
148
|
+
current_parsed_schemas = self.context.parsed_schemas
|
149
|
+
if current_parsed_schemas is None:
|
150
|
+
logger.warning(
|
151
|
+
"[EndpointsEmitter] RenderContext.parsed_schemas was None. "
|
152
|
+
"Defaulting to empty dict for EndpointVisitor."
|
153
|
+
)
|
154
|
+
current_parsed_schemas = {} # Default to empty dict if None
|
155
|
+
|
156
|
+
if self.visitor is None:
|
157
|
+
self.visitor = EndpointVisitor(current_parsed_schemas) # Pass the (potentially defaulted) dict
|
158
|
+
|
159
|
+
tag_key_to_ops: Dict[str, List[IROperation]] = {}
|
160
|
+
tag_key_to_candidates: Dict[str, List[str]] = {}
|
161
|
+
for op in operations:
|
162
|
+
tags = op.tags or [DEFAULT_TAG]
|
163
|
+
for tag in tags:
|
164
|
+
key = NameSanitizer.normalize_tag_key(tag)
|
165
|
+
tag_key_to_ops.setdefault(key, []).append(op)
|
166
|
+
tag_key_to_candidates.setdefault(key, []).append(tag)
|
167
|
+
|
168
|
+
def tag_score(t: str) -> tuple[bool, int, int, str]:
|
169
|
+
import re
|
170
|
+
|
171
|
+
is_pascal = bool(re.search(r"[a-z][A-Z]", t)) or bool(re.search(r"[A-Z]{2,}", t))
|
172
|
+
words = re.findall(r"[A-Z]?[a-z]+|[A-Z]+(?![a-z])|[0-9]+", t)
|
173
|
+
words += re.split(r"[_-]+", t)
|
174
|
+
word_count = len([w for w in words if w])
|
175
|
+
upper = sum(1 for c in t if c.isupper())
|
176
|
+
return (is_pascal, word_count, upper, t)
|
177
|
+
|
178
|
+
tag_map: Dict[str, str] = {}
|
179
|
+
for key, candidates in tag_key_to_candidates.items():
|
180
|
+
best_tag_for_key = DEFAULT_TAG # Default if no candidates somehow
|
181
|
+
if candidates:
|
182
|
+
best_tag_for_key = max(candidates, key=tag_score)
|
183
|
+
tag_map[key] = best_tag_for_key
|
184
|
+
|
185
|
+
generated_files: List[str] = []
|
186
|
+
client_classes: List[Tuple[str, str]] = []
|
187
|
+
|
188
|
+
for key, ops_for_tag in tag_key_to_ops.items():
|
189
|
+
canonical_tag_name = tag_map[key]
|
190
|
+
module_name = NameSanitizer.sanitize_module_name(canonical_tag_name)
|
191
|
+
class_name = NameSanitizer.sanitize_class_name(canonical_tag_name) + "Client"
|
192
|
+
file_path = endpoints_dir / f"{module_name}.py"
|
193
|
+
|
194
|
+
# This will set current_file and reset+reinit import_collector's context
|
195
|
+
self.context.set_current_file(str(file_path))
|
196
|
+
|
197
|
+
self._deduplicate_operation_ids(ops_for_tag)
|
198
|
+
|
199
|
+
# EndpointVisitor must exist here due to check above
|
200
|
+
assert self.visitor is not None, "EndpointVisitor not initialized"
|
201
|
+
methods = [self.visitor.visit(op, self.context) for op in ops_for_tag]
|
202
|
+
class_content = self.visitor.emit_endpoint_client_class(canonical_tag_name, methods, self.context)
|
203
|
+
|
204
|
+
imports = self.context.render_imports()
|
205
|
+
file_content = imports + "\n\n" + class_content
|
206
|
+
self.context.file_manager.write_file(str(file_path), file_content)
|
207
|
+
client_classes.append((class_name, module_name))
|
208
|
+
generated_files.append(str(file_path))
|
209
|
+
|
210
|
+
unique_clients = _deduplicate_tag_clients(client_classes)
|
211
|
+
init_lines = []
|
212
|
+
if unique_clients:
|
213
|
+
all_list_items = sorted([f'"{cls}"' for cls, _ in unique_clients])
|
214
|
+
init_lines.append(f"__all__ = [{', '.join(all_list_items)}]")
|
215
|
+
for cls, mod in sorted(unique_clients):
|
216
|
+
init_lines.append(f"from .{mod} import {cls}")
|
217
|
+
|
218
|
+
endpoints_init_path = endpoints_dir / "__init__.py"
|
219
|
+
self.context.file_manager.write_file(str(endpoints_init_path), "\n".join(init_lines) + "\n")
|
220
|
+
if str(endpoints_init_path) not in generated_files:
|
221
|
+
generated_files.append(str(endpoints_init_path))
|
222
|
+
|
223
|
+
return generated_files
|
@@ -0,0 +1,52 @@
|
|
1
|
+
import os
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
from pyopenapi_gen import IRSpec
|
5
|
+
from pyopenapi_gen.context.render_context import RenderContext
|
6
|
+
|
7
|
+
from ..visit.exception_visitor import ExceptionVisitor
|
8
|
+
|
9
|
+
# Template for spec-specific exception aliases
|
10
|
+
EXCEPTIONS_ALIASES_TEMPLATE = '''
|
11
|
+
from .exceptions import HTTPError, ClientError, ServerError
|
12
|
+
|
13
|
+
# Generated exception aliases for specific status codes
|
14
|
+
{% for code in codes %}
|
15
|
+
class Error{{ code }}({% if code < 500 %}ClientError{% else %}ServerError{% endif %}):
|
16
|
+
"""Exception alias for HTTP {{ code }} responses."""
|
17
|
+
pass
|
18
|
+
{% endfor %}
|
19
|
+
'''
|
20
|
+
|
21
|
+
|
22
|
+
class ExceptionsEmitter:
|
23
|
+
"""Generates spec-specific exception aliases in exceptions.py using visitor/context."""
|
24
|
+
|
25
|
+
def __init__(self, core_package_name: str = "core", overall_project_root: Optional[str] = None) -> None:
|
26
|
+
self.visitor = ExceptionVisitor()
|
27
|
+
self.core_package_name = core_package_name
|
28
|
+
self.overall_project_root = overall_project_root
|
29
|
+
|
30
|
+
def emit(self, spec: IRSpec, output_dir: str) -> tuple[list[str], list[str]]:
|
31
|
+
file_path = os.path.join(output_dir, "exception_aliases.py")
|
32
|
+
|
33
|
+
context = RenderContext(
|
34
|
+
package_root_for_generated_code=output_dir,
|
35
|
+
core_package_name=self.core_package_name,
|
36
|
+
overall_project_root=self.overall_project_root,
|
37
|
+
)
|
38
|
+
context.set_current_file(file_path)
|
39
|
+
|
40
|
+
generated_code, alias_names = self.visitor.visit(spec, context)
|
41
|
+
generated_imports = context.render_imports()
|
42
|
+
|
43
|
+
# Add __all__ list
|
44
|
+
if alias_names:
|
45
|
+
all_list_str = ", ".join([f'"{name}"' for name in alias_names])
|
46
|
+
all_assignment = f"\n\n__all__ = [{all_list_str}]\n"
|
47
|
+
generated_code += all_assignment
|
48
|
+
|
49
|
+
full_content = f"{generated_imports}\n\n{generated_code}"
|
50
|
+
with open(file_path, "w") as f:
|
51
|
+
f.write(full_content)
|
52
|
+
return [file_path], alias_names
|