pyopenapi-gen 0.13.0__py3-none-any.whl → 0.14.1__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/cli.py +3 -3
- pyopenapi_gen/context/import_collector.py +10 -10
- pyopenapi_gen/context/render_context.py +13 -13
- pyopenapi_gen/core/auth/plugins.py +7 -7
- pyopenapi_gen/core/http_status_codes.py +218 -0
- pyopenapi_gen/core/http_transport.py +19 -19
- pyopenapi_gen/core/loader/operations/parser.py +2 -2
- pyopenapi_gen/core/loader/operations/request_body.py +3 -3
- pyopenapi_gen/core/loader/parameters/parser.py +3 -3
- pyopenapi_gen/core/loader/responses/parser.py +2 -2
- pyopenapi_gen/core/loader/schemas/extractor.py +4 -4
- pyopenapi_gen/core/pagination.py +3 -3
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +3 -3
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +2 -2
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +3 -3
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +3 -3
- pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +2 -2
- pyopenapi_gen/core/parsing/common/type_parser.py +2 -3
- pyopenapi_gen/core/parsing/context.py +10 -10
- pyopenapi_gen/core/parsing/cycle_helpers.py +5 -2
- pyopenapi_gen/core/parsing/keywords/all_of_parser.py +5 -5
- pyopenapi_gen/core/parsing/keywords/any_of_parser.py +4 -4
- pyopenapi_gen/core/parsing/keywords/array_items_parser.py +4 -4
- pyopenapi_gen/core/parsing/keywords/one_of_parser.py +4 -4
- pyopenapi_gen/core/parsing/keywords/properties_parser.py +5 -5
- pyopenapi_gen/core/parsing/schema_finalizer.py +15 -15
- pyopenapi_gen/core/parsing/schema_parser.py +44 -25
- pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +4 -4
- pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +7 -4
- pyopenapi_gen/core/parsing/unified_cycle_detection.py +10 -10
- pyopenapi_gen/core/postprocess_manager.py +85 -12
- pyopenapi_gen/core/schemas.py +10 -10
- pyopenapi_gen/core/streaming_helpers.py +5 -7
- pyopenapi_gen/core/telemetry.py +4 -4
- pyopenapi_gen/core/utils.py +7 -7
- pyopenapi_gen/core/writers/code_writer.py +2 -2
- pyopenapi_gen/core/writers/documentation_writer.py +18 -18
- pyopenapi_gen/core/writers/line_writer.py +3 -3
- pyopenapi_gen/core/writers/python_construct_renderer.py +15 -11
- pyopenapi_gen/emit/models_emitter.py +2 -2
- pyopenapi_gen/emitters/core_emitter.py +3 -5
- pyopenapi_gen/emitters/endpoints_emitter.py +12 -12
- pyopenapi_gen/emitters/exceptions_emitter.py +153 -18
- pyopenapi_gen/emitters/models_emitter.py +6 -6
- pyopenapi_gen/generator/client_generator.py +10 -8
- pyopenapi_gen/helpers/endpoint_utils.py +16 -18
- pyopenapi_gen/helpers/type_cleaner.py +66 -53
- pyopenapi_gen/helpers/type_helper.py +7 -7
- pyopenapi_gen/helpers/type_resolution/array_resolver.py +4 -4
- pyopenapi_gen/helpers/type_resolution/composition_resolver.py +5 -5
- pyopenapi_gen/helpers/type_resolution/finalizer.py +38 -22
- pyopenapi_gen/helpers/type_resolution/named_resolver.py +4 -5
- pyopenapi_gen/helpers/type_resolution/object_resolver.py +11 -11
- pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +1 -2
- pyopenapi_gen/helpers/type_resolution/resolver.py +2 -3
- pyopenapi_gen/ir.py +32 -34
- pyopenapi_gen/types/contracts/protocols.py +5 -5
- pyopenapi_gen/types/contracts/types.py +2 -3
- pyopenapi_gen/types/resolvers/reference_resolver.py +4 -4
- pyopenapi_gen/types/resolvers/response_resolver.py +6 -4
- pyopenapi_gen/types/resolvers/schema_resolver.py +32 -16
- pyopenapi_gen/types/services/type_service.py +55 -9
- pyopenapi_gen/types/strategies/response_strategy.py +6 -7
- pyopenapi_gen/visit/client_visitor.py +5 -7
- pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +7 -7
- pyopenapi_gen/visit/endpoint/generators/request_generator.py +5 -5
- pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +41 -19
- pyopenapi_gen/visit/endpoint/generators/signature_generator.py +4 -4
- pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +17 -17
- pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +8 -8
- pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +13 -13
- pyopenapi_gen/visit/exception_visitor.py +54 -16
- pyopenapi_gen/visit/model/alias_generator.py +1 -4
- pyopenapi_gen/visit/model/dataclass_generator.py +139 -10
- pyopenapi_gen/visit/model/model_visitor.py +2 -3
- pyopenapi_gen/visit/visitor.py +3 -3
- {pyopenapi_gen-0.13.0.dist-info → pyopenapi_gen-0.14.1.dist-info}/METADATA +1 -1
- pyopenapi_gen-0.14.1.dist-info/RECORD +132 -0
- pyopenapi_gen-0.13.0.dist-info/RECORD +0 -131
- {pyopenapi_gen-0.13.0.dist-info → pyopenapi_gen-0.14.1.dist-info}/WHEEL +0 -0
- {pyopenapi_gen-0.13.0.dist-info → pyopenapi_gen-0.14.1.dist-info}/entry_points.txt +0 -0
- {pyopenapi_gen-0.13.0.dist-info → pyopenapi_gen-0.14.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
import logging
|
2
2
|
from pathlib import Path
|
3
|
-
from typing import
|
3
|
+
from typing import List, Tuple
|
4
4
|
|
5
5
|
from pyopenapi_gen import IROperation, IRParameter, IRRequestBody
|
6
6
|
from pyopenapi_gen.context.render_context import RenderContext
|
@@ -17,7 +17,7 @@ PARAM_TYPE_MAPPING = {
|
|
17
17
|
"boolean": "bool",
|
18
18
|
"string": "str",
|
19
19
|
"array": "List",
|
20
|
-
"object": "
|
20
|
+
"object": "dict[str, Any]",
|
21
21
|
}
|
22
22
|
# Format-specific overrides
|
23
23
|
PARAM_FORMAT_MAPPING = {
|
@@ -47,7 +47,7 @@ def schema_to_type(schema: IRParameter) -> str:
|
|
47
47
|
# Array handling
|
48
48
|
elif s.type == "array" and s.items:
|
49
49
|
# For array items, we recursively call schema_to_type.
|
50
|
-
# The nullability of the item_type itself (e.g. List[
|
50
|
+
# The nullability of the item_type itself (e.g. List[int | None])
|
51
51
|
# will be handled by the recursive call based on s.items.is_nullable.
|
52
52
|
item_schema_as_param = IRParameter(name="_item", param_in="_internal", required=False, schema=s.items)
|
53
53
|
item_type_str = schema_to_type(item_schema_as_param)
|
@@ -70,8 +70,8 @@ def schema_to_type(schema: IRParameter) -> str:
|
|
70
70
|
# 2. Apply nullability based on IRSchema's is_nullable field
|
71
71
|
# This s.is_nullable should be the source of truth from the IR after parsing.
|
72
72
|
if s.is_nullable:
|
73
|
-
# Ensure "Any" also gets wrapped, e.g.
|
74
|
-
py_type = f"
|
73
|
+
# Ensure "Any" also gets wrapped, e.g. Any | None
|
74
|
+
py_type = f"{py_type} | None"
|
75
75
|
|
76
76
|
return py_type
|
77
77
|
|
@@ -82,7 +82,7 @@ def _get_request_body_type(body: IRRequestBody) -> str:
|
|
82
82
|
if "json" in mt.lower():
|
83
83
|
return schema_to_type(IRParameter(name="body", param_in="body", required=body.required, schema=sch))
|
84
84
|
# Fallback to generic dict
|
85
|
-
return "
|
85
|
+
return "dict[str, Any]"
|
86
86
|
|
87
87
|
|
88
88
|
def _deduplicate_tag_clients(client_classes: List[Tuple[str, str]]) -> List[Tuple[str, str]]:
|
@@ -106,7 +106,7 @@ class EndpointsEmitter:
|
|
106
106
|
def __init__(self, context: RenderContext) -> None:
|
107
107
|
self.context = context
|
108
108
|
self.formatter = Formatter()
|
109
|
-
self.visitor:
|
109
|
+
self.visitor: EndpointVisitor | None = None
|
110
110
|
|
111
111
|
def _deduplicate_operation_ids(self, operations: List[IROperation]) -> None:
|
112
112
|
"""
|
@@ -115,7 +115,7 @@ class EndpointsEmitter:
|
|
115
115
|
Args:
|
116
116
|
operations: List of operations for a single tag.
|
117
117
|
"""
|
118
|
-
seen_methods:
|
118
|
+
seen_methods: dict[str, int] = {}
|
119
119
|
for op in operations:
|
120
120
|
method_name = NameSanitizer.sanitize_method_name(op.operation_id)
|
121
121
|
if method_name in seen_methods:
|
@@ -144,7 +144,7 @@ class EndpointsEmitter:
|
|
144
144
|
self.context.file_manager.write_file(str(file_path), content)
|
145
145
|
|
146
146
|
# Ensure parsed_schemas is at least an empty dict if None,
|
147
|
-
# as EndpointVisitor expects
|
147
|
+
# as EndpointVisitor expects dict[str, IRSchema]
|
148
148
|
current_parsed_schemas = self.context.parsed_schemas
|
149
149
|
if current_parsed_schemas is None:
|
150
150
|
logger.warning(
|
@@ -156,8 +156,8 @@ class EndpointsEmitter:
|
|
156
156
|
if self.visitor is None:
|
157
157
|
self.visitor = EndpointVisitor(current_parsed_schemas) # Pass the (potentially defaulted) dict
|
158
158
|
|
159
|
-
tag_key_to_ops:
|
160
|
-
tag_key_to_candidates:
|
159
|
+
tag_key_to_ops: dict[str, List[IROperation]] = {}
|
160
|
+
tag_key_to_candidates: dict[str, List[str]] = {}
|
161
161
|
for op in operations:
|
162
162
|
tags = op.tags or [DEFAULT_TAG]
|
163
163
|
for tag in tags:
|
@@ -175,7 +175,7 @@ class EndpointsEmitter:
|
|
175
175
|
upper = sum(1 for c in t if c.isupper())
|
176
176
|
return (is_pascal, word_count, upper, t)
|
177
177
|
|
178
|
-
tag_map:
|
178
|
+
tag_map: dict[str, str] = {}
|
179
179
|
for key, candidates in tag_key_to_candidates.items():
|
180
180
|
best_tag_for_key = DEFAULT_TAG # Default if no candidates somehow
|
181
181
|
if candidates:
|
@@ -1,34 +1,46 @@
|
|
1
|
+
import json
|
1
2
|
import os
|
2
|
-
from
|
3
|
+
from pathlib import Path
|
3
4
|
|
4
5
|
from pyopenapi_gen import IRSpec
|
5
6
|
from pyopenapi_gen.context.render_context import RenderContext
|
6
7
|
|
7
8
|
from ..visit.exception_visitor import ExceptionVisitor
|
8
9
|
|
9
|
-
# Template for spec-specific exception aliases
|
10
|
-
EXCEPTIONS_ALIASES_TEMPLATE = '''
|
11
|
-
from .exceptions import HTTPError, ClientError, ServerError
|
12
10
|
|
13
|
-
|
14
|
-
|
15
|
-
class Error{{ code }}({% if code < 500 %}ClientError{% else %}ServerError{% endif %}):
|
16
|
-
"""Exception alias for HTTP {{ code }} responses."""
|
17
|
-
pass
|
18
|
-
{% endfor %}
|
19
|
-
'''
|
11
|
+
class ExceptionsEmitter:
|
12
|
+
"""Generates spec-specific exception aliases with multi-client support.
|
20
13
|
|
14
|
+
This emitter handles two scenarios:
|
15
|
+
1. **Single client**: Generates exception_aliases.py directly in the core package
|
16
|
+
2. **Shared core**: Maintains a registry of all needed exception codes across clients
|
17
|
+
and regenerates the complete exception_aliases.py file
|
21
18
|
|
22
|
-
|
23
|
-
|
19
|
+
The registry file (.exception_registry.json) tracks which status codes are used by
|
20
|
+
which clients, ensuring that when multiple clients share a core package, all required
|
21
|
+
exceptions are available.
|
22
|
+
"""
|
24
23
|
|
25
|
-
def __init__(self, core_package_name: str = "core", overall_project_root:
|
24
|
+
def __init__(self, core_package_name: str = "core", overall_project_root: str | None = None) -> None:
|
26
25
|
self.visitor = ExceptionVisitor()
|
27
26
|
self.core_package_name = core_package_name
|
28
27
|
self.overall_project_root = overall_project_root
|
29
28
|
|
30
|
-
def emit(
|
29
|
+
def emit(
|
30
|
+
self, spec: IRSpec, output_dir: str, client_package_name: str | None = None
|
31
|
+
) -> tuple[list[str], list[str]]:
|
32
|
+
"""Generate exception aliases for the given spec.
|
33
|
+
|
34
|
+
Args:
|
35
|
+
spec: IRSpec containing operations and responses
|
36
|
+
output_dir: Directory where exception_aliases.py will be written
|
37
|
+
client_package_name: Name of the client package (for registry tracking)
|
38
|
+
|
39
|
+
Returns:
|
40
|
+
Tuple of (list of generated file paths, list of exception class names)
|
41
|
+
"""
|
31
42
|
file_path = os.path.join(output_dir, "exception_aliases.py")
|
43
|
+
registry_path = os.path.join(output_dir, ".exception_registry.json")
|
32
44
|
|
33
45
|
context = RenderContext(
|
34
46
|
package_root_for_generated_code=output_dir,
|
@@ -37,16 +49,139 @@ class ExceptionsEmitter:
|
|
37
49
|
)
|
38
50
|
context.set_current_file(file_path)
|
39
51
|
|
40
|
-
|
52
|
+
# Generate exception classes for this spec
|
53
|
+
generated_code, alias_names, status_codes = self.visitor.visit(spec, context)
|
54
|
+
|
55
|
+
# Update registry if we have a client package name (shared core scenario)
|
56
|
+
if client_package_name and self._is_shared_core(output_dir):
|
57
|
+
all_codes = self._update_registry(registry_path, client_package_name, status_codes)
|
58
|
+
# Regenerate with ALL codes from registry
|
59
|
+
generated_code, alias_names = self._generate_for_codes(all_codes, context)
|
60
|
+
|
41
61
|
generated_imports = context.render_imports()
|
42
62
|
|
43
|
-
|
63
|
+
alias_names.sort()
|
64
|
+
|
65
|
+
# Add __all__ list with proper spacing (2 blank lines after last class - Ruff E305)
|
44
66
|
if alias_names:
|
45
67
|
all_list_str = ", ".join([f'"{name}"' for name in alias_names])
|
46
|
-
all_assignment = f"\n\n__all__ = [{all_list_str}]\n"
|
68
|
+
all_assignment = f"\n\n\n__all__ = [{all_list_str}]\n"
|
47
69
|
generated_code += all_assignment
|
48
70
|
|
49
71
|
full_content = f"{generated_imports}\n\n{generated_code}"
|
50
72
|
with open(file_path, "w") as f:
|
51
73
|
f.write(full_content)
|
74
|
+
|
52
75
|
return [file_path], alias_names
|
76
|
+
|
77
|
+
def _is_shared_core(self, core_dir: str) -> bool:
|
78
|
+
"""Check if this core package is shared between multiple clients.
|
79
|
+
|
80
|
+
Args:
|
81
|
+
core_dir: Path to the core package directory
|
82
|
+
|
83
|
+
Returns:
|
84
|
+
True if the core package is outside the immediate client package
|
85
|
+
"""
|
86
|
+
# If overall_project_root is set and different from the core dir's parent,
|
87
|
+
# we're in a shared core scenario
|
88
|
+
if self.overall_project_root:
|
89
|
+
core_path = Path(core_dir).resolve()
|
90
|
+
project_root = Path(self.overall_project_root).resolve()
|
91
|
+
# Check if there are other client directories at the same level
|
92
|
+
parent_dir = core_path.parent
|
93
|
+
return parent_dir == project_root or parent_dir.parent == project_root
|
94
|
+
return False
|
95
|
+
|
96
|
+
def _update_registry(self, registry_path: str, client_name: str, status_codes: list[int]) -> list[int]:
|
97
|
+
"""Update the exception registry with this client's status codes.
|
98
|
+
|
99
|
+
Args:
|
100
|
+
registry_path: Path to the .exception_registry.json file
|
101
|
+
client_name: Name of the client package
|
102
|
+
status_codes: List of status codes used by this client
|
103
|
+
|
104
|
+
Returns:
|
105
|
+
Complete list of all status codes across all clients
|
106
|
+
"""
|
107
|
+
registry = {}
|
108
|
+
if os.path.exists(registry_path):
|
109
|
+
with open(registry_path) as f:
|
110
|
+
registry = json.load(f)
|
111
|
+
|
112
|
+
# Update this client's codes
|
113
|
+
registry[client_name] = sorted(status_codes)
|
114
|
+
|
115
|
+
# Write back to registry
|
116
|
+
with open(registry_path, "w") as f:
|
117
|
+
json.dump(registry, f, indent=2, sort_keys=True)
|
118
|
+
|
119
|
+
# Return union of all codes
|
120
|
+
all_codes = set()
|
121
|
+
for codes in registry.values():
|
122
|
+
all_codes.update(codes)
|
123
|
+
|
124
|
+
return sorted(all_codes)
|
125
|
+
|
126
|
+
def _generate_for_codes(self, status_codes: list[int], context: RenderContext) -> tuple[str, list[str]]:
|
127
|
+
"""Generate exception classes for a specific list of status codes.
|
128
|
+
|
129
|
+
Args:
|
130
|
+
status_codes: List of HTTP status codes to generate exceptions for
|
131
|
+
context: Render context for imports
|
132
|
+
|
133
|
+
Returns:
|
134
|
+
Tuple of (generated_code, exception_class_names)
|
135
|
+
"""
|
136
|
+
from ..core.http_status_codes import (
|
137
|
+
get_exception_class_name,
|
138
|
+
get_status_name,
|
139
|
+
is_client_error,
|
140
|
+
is_server_error,
|
141
|
+
)
|
142
|
+
from ..core.writers.python_construct_renderer import PythonConstructRenderer
|
143
|
+
|
144
|
+
renderer = PythonConstructRenderer()
|
145
|
+
all_exception_code = []
|
146
|
+
generated_alias_names = []
|
147
|
+
|
148
|
+
for code in status_codes:
|
149
|
+
# Determine base class
|
150
|
+
if is_client_error(code):
|
151
|
+
base_class = "ClientError"
|
152
|
+
elif is_server_error(code):
|
153
|
+
base_class = "ServerError"
|
154
|
+
else:
|
155
|
+
continue
|
156
|
+
|
157
|
+
# Get human-readable exception class name (e.g., NotFoundError instead of Error404)
|
158
|
+
class_name = get_exception_class_name(code)
|
159
|
+
generated_alias_names.append(class_name)
|
160
|
+
|
161
|
+
# Get human-readable status name for documentation
|
162
|
+
status_name = get_status_name(code)
|
163
|
+
docstring = f"HTTP {code} {status_name}.\n\nRaised when the server responds with a {code} status code."
|
164
|
+
|
165
|
+
# Define the __init__ method body
|
166
|
+
init_method_body = [
|
167
|
+
"def __init__(self, response: Response) -> None:",
|
168
|
+
f' """Initialise {class_name} with the HTTP response.',
|
169
|
+
"", # Empty line without trailing whitespace (Ruff W293)
|
170
|
+
" Args:",
|
171
|
+
" response: The httpx Response object that triggered this exception",
|
172
|
+
' """',
|
173
|
+
" super().__init__(status_code=response.status_code, message=response.text, response=response)",
|
174
|
+
]
|
175
|
+
|
176
|
+
exception_code = renderer.render_class(
|
177
|
+
class_name=class_name,
|
178
|
+
base_classes=[base_class],
|
179
|
+
docstring=docstring,
|
180
|
+
body_lines=init_method_body,
|
181
|
+
context=context,
|
182
|
+
)
|
183
|
+
all_exception_code.append(exception_code)
|
184
|
+
|
185
|
+
# Join the generated class strings with 2 blank lines between classes (PEP 8 / Ruff E302)
|
186
|
+
final_code = "\n\n\n".join(all_exception_code)
|
187
|
+
return final_code, generated_alias_names
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import logging
|
2
2
|
from pathlib import Path
|
3
|
-
from typing import
|
3
|
+
from typing import List, Set
|
4
4
|
|
5
5
|
from pyopenapi_gen import IRSchema, IRSpec
|
6
6
|
from pyopenapi_gen.context.render_context import RenderContext
|
@@ -22,15 +22,15 @@ class ModelsEmitter:
|
|
22
22
|
Handles creation of __init__.py and py.typed files.
|
23
23
|
"""
|
24
24
|
|
25
|
-
def __init__(self, context: RenderContext, parsed_schemas:
|
25
|
+
def __init__(self, context: RenderContext, parsed_schemas: dict[str, IRSchema]):
|
26
26
|
self.context: RenderContext = context
|
27
27
|
# Store a reference to the schemas that were passed in.
|
28
28
|
# These schemas will have their .generation_name and .final_module_stem updated.
|
29
|
-
self.parsed_schemas:
|
29
|
+
self.parsed_schemas: dict[str, IRSchema] = parsed_schemas
|
30
30
|
self.import_collector = self.context.import_collector
|
31
31
|
self.writer = CodeWriter()
|
32
32
|
|
33
|
-
def _generate_model_file(self, schema_ir: IRSchema, models_dir: Path) ->
|
33
|
+
def _generate_model_file(self, schema_ir: IRSchema, models_dir: Path) -> str | None:
|
34
34
|
"""Generates a single Python file for a given IRSchema."""
|
35
35
|
if not schema_ir.name: # Original name, used for logging/initial identification
|
36
36
|
logger.warning(f"Skipping model generation for schema without an original name: {schema_ir}")
|
@@ -168,7 +168,7 @@ class ModelsEmitter:
|
|
168
168
|
generated_content = init_writer.get_code()
|
169
169
|
return generated_content
|
170
170
|
|
171
|
-
def emit(self, spec: IRSpec, output_root: str) ->
|
171
|
+
def emit(self, spec: IRSpec, output_root: str) -> dict[str, List[str]]:
|
172
172
|
"""Emits all model files derived from IR schemas.
|
173
173
|
|
174
174
|
Contracts:
|
@@ -352,7 +352,7 @@ class ModelsEmitter:
|
|
352
352
|
|
353
353
|
# Fetch the schema_ir object using the key from all_schemas_for_generation
|
354
354
|
# This ensures we are working with the potentially newly created & named schemas.
|
355
|
-
current_schema_ir_obj:
|
355
|
+
current_schema_ir_obj: IRSchema | None = all_schemas_for_generation.get(schema_key)
|
356
356
|
|
357
357
|
if not current_schema_ir_obj:
|
358
358
|
logger.warning(f"Schema key '{schema_key}' from all_schemas_for_generation not found. Skipping.")
|
@@ -9,7 +9,7 @@ import tempfile
|
|
9
9
|
import time
|
10
10
|
from datetime import datetime
|
11
11
|
from pathlib import Path
|
12
|
-
from typing import Any,
|
12
|
+
from typing import Any, List
|
13
13
|
|
14
14
|
from pyopenapi_gen.context.render_context import RenderContext
|
15
15
|
from pyopenapi_gen.core.loader.loader import load_ir_from_spec
|
@@ -47,9 +47,9 @@ class ClientGenerator:
|
|
47
47
|
"""
|
48
48
|
self.verbose = verbose
|
49
49
|
self.start_time = time.time()
|
50
|
-
self.timings:
|
50
|
+
self.timings: dict[str, float] = {}
|
51
51
|
|
52
|
-
def _log_progress(self, message: str, stage:
|
52
|
+
def _log_progress(self, message: str, stage: str | None = None) -> None:
|
53
53
|
"""
|
54
54
|
Log a progress message with timestamp.
|
55
55
|
|
@@ -89,7 +89,7 @@ class ClientGenerator:
|
|
89
89
|
output_package: str,
|
90
90
|
force: bool = False,
|
91
91
|
no_postprocess: bool = False,
|
92
|
-
core_package:
|
92
|
+
core_package: str | None = None,
|
93
93
|
) -> List[Path]:
|
94
94
|
"""
|
95
95
|
Generate the client code from the OpenAPI spec.
|
@@ -99,10 +99,10 @@ class ClientGenerator:
|
|
99
99
|
project_root (Path): Path to the root of the Python project (absolute or relative).
|
100
100
|
output_package (str): Python package path for the generated client (e.g., 'pyapis.my_api_client').
|
101
101
|
force (bool): Overwrite output without diff check.
|
102
|
-
name (
|
102
|
+
name (str | None): Custom client package name (not used).
|
103
103
|
docs (bool): Kept for interface compatibility.
|
104
104
|
telemetry (bool): Kept for interface compatibility.
|
105
|
-
auth (
|
105
|
+
auth (str | None): Kept for interface compatibility.
|
106
106
|
no_postprocess (bool): Skip post-processing (type checking, etc.).
|
107
107
|
core_package (str): Python package path for the core package.
|
108
108
|
|
@@ -203,7 +203,7 @@ class ClientGenerator:
|
|
203
203
|
overall_project_root=str(tmp_project_root_for_diff), # Use temp project root for context
|
204
204
|
)
|
205
205
|
exception_files_list, exception_alias_names = exceptions_emitter.emit(
|
206
|
-
ir, str(tmp_core_dir_for_diff)
|
206
|
+
ir, str(tmp_core_dir_for_diff), client_package_name=output_package
|
207
207
|
) # Emit TO temp core dir
|
208
208
|
exception_files = [Path(p) for p in exception_files_list]
|
209
209
|
temp_generated_files += exception_files
|
@@ -374,7 +374,9 @@ class ClientGenerator:
|
|
374
374
|
core_package_name=resolved_core_package_fqn,
|
375
375
|
overall_project_root=str(project_root),
|
376
376
|
)
|
377
|
-
exception_files_list, exception_alias_names = exceptions_emitter.emit(
|
377
|
+
exception_files_list, exception_alias_names = exceptions_emitter.emit(
|
378
|
+
ir, str(core_dir), client_package_name=output_package
|
379
|
+
)
|
378
380
|
generated_files += [Path(p) for p in exception_files_list]
|
379
381
|
self._log_progress(f"Generated {len(exception_files_list)} exception files", "EMIT_EXCEPTIONS")
|
380
382
|
|
@@ -5,7 +5,7 @@ Used by EndpointVisitor and related emitters.
|
|
5
5
|
|
6
6
|
import logging
|
7
7
|
import re
|
8
|
-
from typing import Any,
|
8
|
+
from typing import Any, List
|
9
9
|
|
10
10
|
from pyopenapi_gen import IROperation, IRParameter, IRRequestBody, IRResponse, IRSchema
|
11
11
|
from pyopenapi_gen.context.render_context import RenderContext
|
@@ -17,7 +17,7 @@ from ..types.services.type_service import UnifiedTypeService
|
|
17
17
|
logger = logging.getLogger(__name__)
|
18
18
|
|
19
19
|
|
20
|
-
def get_params(op: IROperation, context: RenderContext, schemas:
|
20
|
+
def get_params(op: IROperation, context: RenderContext, schemas: dict[str, IRSchema]) -> List[dict[str, Any]]:
|
21
21
|
"""
|
22
22
|
Returns a list of dicts with name, type, default, and required for template rendering.
|
23
23
|
Requires the full schema dictionary for type resolution.
|
@@ -37,7 +37,7 @@ def get_params(op: IROperation, context: RenderContext, schemas: Dict[str, IRSch
|
|
37
37
|
return params
|
38
38
|
|
39
39
|
|
40
|
-
def get_param_type(param: IRParameter, context: RenderContext, schemas:
|
40
|
+
def get_param_type(param: IRParameter, context: RenderContext, schemas: dict[str, IRSchema]) -> str:
|
41
41
|
"""Returns the Python type hint for a parameter, resolving references using the schemas dict."""
|
42
42
|
# Use unified service for type resolution
|
43
43
|
type_service = UnifiedTypeService(schemas)
|
@@ -59,7 +59,7 @@ def get_param_type(param: IRParameter, context: RenderContext, schemas: Dict[str
|
|
59
59
|
return py_type
|
60
60
|
|
61
61
|
|
62
|
-
def get_request_body_type(body: IRRequestBody, context: RenderContext, schemas:
|
62
|
+
def get_request_body_type(body: IRRequestBody, context: RenderContext, schemas: dict[str, IRSchema]) -> str:
|
63
63
|
"""Returns the Python type hint for a request body, resolving references using the schemas dict."""
|
64
64
|
# Prefer application/json schema if available
|
65
65
|
json_schema = body.content.get("application/json")
|
@@ -70,11 +70,11 @@ def get_request_body_type(body: IRRequestBody, context: RenderContext, schemas:
|
|
70
70
|
if py_type.startswith(".") and not py_type.startswith(".."):
|
71
71
|
py_type = "models" + py_type
|
72
72
|
|
73
|
-
# If the resolved type is 'Any' for a JSON body, default to
|
73
|
+
# If the resolved type is 'Any' for a JSON body, default to dict[str, Any]
|
74
74
|
if py_type == "Any":
|
75
75
|
context.add_import("typing", "Dict")
|
76
76
|
# context.add_import("typing", "Any") # Already added by the fallback or TypeHelper
|
77
|
-
return "
|
77
|
+
return "dict[str, Any]"
|
78
78
|
return py_type
|
79
79
|
# Fallback for other content types (e.g., octet-stream)
|
80
80
|
# TODO: Handle other types more specifically if needed
|
@@ -89,7 +89,7 @@ def get_request_body_type(body: IRRequestBody, context: RenderContext, schemas:
|
|
89
89
|
def get_return_type(
|
90
90
|
op: IROperation,
|
91
91
|
context: RenderContext,
|
92
|
-
schemas:
|
92
|
+
schemas: dict[str, IRSchema],
|
93
93
|
) -> tuple[str | None, bool]:
|
94
94
|
"""
|
95
95
|
DEPRECATED: Use get_return_type_unified instead.
|
@@ -136,7 +136,7 @@ def get_return_type(
|
|
136
136
|
return (py_type, False)
|
137
137
|
|
138
138
|
|
139
|
-
def _get_primary_response(op: IROperation) ->
|
139
|
+
def _get_primary_response(op: IROperation) -> IRResponse | None:
|
140
140
|
"""Helper to find the best primary success response."""
|
141
141
|
resp = None
|
142
142
|
# Prioritize 200, 201, 202, 204
|
@@ -158,7 +158,7 @@ def _get_primary_response(op: IROperation) -> Optional[IRResponse]:
|
|
158
158
|
return None
|
159
159
|
|
160
160
|
|
161
|
-
def _get_response_schema_and_content_type(resp: IRResponse) -> tuple[
|
161
|
+
def _get_response_schema_and_content_type(resp: IRResponse) -> tuple[IRSchema | None, str | None]:
|
162
162
|
"""Helper to get the schema and content type from a response."""
|
163
163
|
if not resp.content:
|
164
164
|
return None, None
|
@@ -179,7 +179,7 @@ def _get_response_schema_and_content_type(resp: IRResponse) -> tuple[Optional[IR
|
|
179
179
|
return resp.content.get(mt), mt
|
180
180
|
|
181
181
|
|
182
|
-
def _find_resource_schema(update_schema_name: str, schemas:
|
182
|
+
def _find_resource_schema(update_schema_name: str, schemas: dict[str, IRSchema]) -> IRSchema | None:
|
183
183
|
"""
|
184
184
|
Given an update schema name (e.g. 'TenantUpdate'), try to find the corresponding
|
185
185
|
resource schema (e.g. 'Tenant') in the schemas dictionary.
|
@@ -205,7 +205,7 @@ def _find_resource_schema(update_schema_name: str, schemas: Dict[str, IRSchema])
|
|
205
205
|
return None
|
206
206
|
|
207
207
|
|
208
|
-
def _infer_type_from_path(path: str, schemas:
|
208
|
+
def _infer_type_from_path(path: str, schemas: dict[str, IRSchema]) -> IRSchema | None:
|
209
209
|
"""
|
210
210
|
Infers a response type from a path. This is used when a response schema is not specified.
|
211
211
|
|
@@ -350,8 +350,8 @@ def merge_params_with_model_fields(
|
|
350
350
|
op: IROperation,
|
351
351
|
model_schema: IRSchema,
|
352
352
|
context: RenderContext,
|
353
|
-
schemas:
|
354
|
-
) -> List[
|
353
|
+
schemas: dict[str, IRSchema],
|
354
|
+
) -> List[dict[str, Any]]:
|
355
355
|
"""
|
356
356
|
Merge endpoint parameters with required model fields for function signatures.
|
357
357
|
- Ensures all required model fields are present as parameters (without duplication).
|
@@ -484,7 +484,7 @@ def get_type_for_specific_response(
|
|
484
484
|
ctx.add_import("typing", "AsyncIterator")
|
485
485
|
ctx.add_import("typing", "Dict")
|
486
486
|
ctx.add_import("typing", "Any")
|
487
|
-
return "AsyncIterator[
|
487
|
+
return "AsyncIterator[dict[str, Any]]"
|
488
488
|
|
489
489
|
return final_py_type
|
490
490
|
|
@@ -504,9 +504,7 @@ def _is_binary_stream_content(resp_ir: IRResponse) -> bool:
|
|
504
504
|
)
|
505
505
|
|
506
506
|
|
507
|
-
def _get_item_type_from_schema(
|
508
|
-
resp_ir: IRResponse, all_schemas: dict[str, IRSchema], ctx: RenderContext
|
509
|
-
) -> Optional[str]:
|
507
|
+
def _get_item_type_from_schema(resp_ir: IRResponse, all_schemas: dict[str, IRSchema], ctx: RenderContext) -> str | None:
|
510
508
|
"""Extract item type from schema for streaming responses."""
|
511
509
|
schema, _ = _get_response_schema_and_content_type(resp_ir)
|
512
510
|
if not schema:
|
@@ -528,7 +526,7 @@ def get_python_type_for_response_body(resp_ir: IRResponse, all_schemas: dict[str
|
|
528
526
|
return type_service.resolve_schema_type(schema, ctx, required=True)
|
529
527
|
|
530
528
|
|
531
|
-
def get_schema_from_response(resp_ir: IRResponse, all_schemas: dict[str, IRSchema]) ->
|
529
|
+
def get_schema_from_response(resp_ir: IRResponse, all_schemas: dict[str, IRSchema]) -> IRSchema | None:
|
532
530
|
"""Get the schema from a response object."""
|
533
531
|
schema, _ = _get_response_schema_and_content_type(resp_ir)
|
534
532
|
return schema
|