pyopenapi-gen 0.17.0__py3-none-any.whl → 0.18.0__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.
Potentially problematic release.
This version of pyopenapi-gen might be problematic. Click here for more details.
- pyopenapi_gen/__init__.py +1 -1
- pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +123 -1
- pyopenapi_gen/visit/endpoint/generators/overload_generator.py +243 -0
- {pyopenapi_gen-0.17.0.dist-info → pyopenapi_gen-0.18.0.dist-info}/METADATA +249 -1
- {pyopenapi_gen-0.17.0.dist-info → pyopenapi_gen-0.18.0.dist-info}/RECORD +8 -7
- {pyopenapi_gen-0.17.0.dist-info → pyopenapi_gen-0.18.0.dist-info}/WHEEL +0 -0
- {pyopenapi_gen-0.17.0.dist-info → pyopenapi_gen-0.18.0.dist-info}/entry_points.txt +0 -0
- {pyopenapi_gen-0.17.0.dist-info → pyopenapi_gen-0.18.0.dist-info}/licenses/LICENSE +0 -0
pyopenapi_gen/__init__.py
CHANGED
|
@@ -50,7 +50,7 @@ __all__ = [
|
|
|
50
50
|
]
|
|
51
51
|
|
|
52
52
|
# Semantic version of the generator core – automatically managed by semantic-release.
|
|
53
|
-
__version__: str = "0.
|
|
53
|
+
__version__: str = "0.18.0"
|
|
54
54
|
|
|
55
55
|
# ---------------------------------------------------------------------------
|
|
56
56
|
# Lazy-loading and autocompletion support (This part remains)
|
|
@@ -10,6 +10,7 @@ from ....types.strategies import ResponseStrategyResolver
|
|
|
10
10
|
from ..processors.import_analyzer import EndpointImportAnalyzer
|
|
11
11
|
from ..processors.parameter_processor import EndpointParameterProcessor
|
|
12
12
|
from .docstring_generator import EndpointDocstringGenerator
|
|
13
|
+
from .overload_generator import OverloadMethodGenerator
|
|
13
14
|
from .request_generator import EndpointRequestGenerator
|
|
14
15
|
from .response_handler_generator import EndpointResponseHandlerGenerator
|
|
15
16
|
from .signature_generator import EndpointMethodSignatureGenerator
|
|
@@ -34,13 +35,16 @@ class EndpointMethodGenerator:
|
|
|
34
35
|
self.url_args_generator = EndpointUrlArgsGenerator(self.schemas)
|
|
35
36
|
self.request_generator = EndpointRequestGenerator(self.schemas)
|
|
36
37
|
self.response_handler_generator = EndpointResponseHandlerGenerator(self.schemas)
|
|
38
|
+
self.overload_generator = OverloadMethodGenerator(self.schemas)
|
|
37
39
|
|
|
38
40
|
def generate(self, op: IROperation, context: RenderContext) -> str:
|
|
39
41
|
"""
|
|
40
42
|
Generate a fully functional async endpoint method for the given operation.
|
|
41
43
|
Returns the method code as a string.
|
|
44
|
+
|
|
45
|
+
If the operation has multiple content types, generates @overload signatures
|
|
46
|
+
followed by the implementation method with runtime dispatch.
|
|
42
47
|
"""
|
|
43
|
-
writer = CodeWriter()
|
|
44
48
|
context.add_import(f"{context.core_package_name}.http_transport", "HttpTransport")
|
|
45
49
|
context.add_import(f"{context.core_package_name}.exceptions", "HTTPError")
|
|
46
50
|
|
|
@@ -51,6 +55,16 @@ class EndpointMethodGenerator:
|
|
|
51
55
|
# Pass the response strategy to import analyzer for consistent import resolution
|
|
52
56
|
self.import_analyzer.analyze_and_register_imports(op, context, response_strategy)
|
|
53
57
|
|
|
58
|
+
# Check if operation has multiple content types
|
|
59
|
+
if self.overload_generator.has_multiple_content_types(op):
|
|
60
|
+
return self._generate_overloaded_method(op, context, response_strategy)
|
|
61
|
+
else:
|
|
62
|
+
return self._generate_standard_method(op, context, response_strategy)
|
|
63
|
+
|
|
64
|
+
def _generate_standard_method(self, op: IROperation, context: RenderContext, response_strategy: Any) -> str:
|
|
65
|
+
"""Generate standard method without overloads."""
|
|
66
|
+
writer = CodeWriter()
|
|
67
|
+
|
|
54
68
|
ordered_params, primary_content_type, resolved_body_type = self.parameter_processor.process_parameters(
|
|
55
69
|
op, context
|
|
56
70
|
)
|
|
@@ -91,3 +105,111 @@ class EndpointMethodGenerator:
|
|
|
91
105
|
writer.dedent() # This matches the indent() from _write_method_signature
|
|
92
106
|
|
|
93
107
|
return writer.get_code().strip()
|
|
108
|
+
|
|
109
|
+
def _generate_overloaded_method(self, op: IROperation, context: RenderContext, response_strategy: Any) -> str:
|
|
110
|
+
"""Generate method with @overload signatures for multiple content types."""
|
|
111
|
+
parts = []
|
|
112
|
+
|
|
113
|
+
# Generate overload signatures
|
|
114
|
+
overload_sigs = self.overload_generator.generate_overload_signatures(op, context, response_strategy)
|
|
115
|
+
parts.extend(overload_sigs)
|
|
116
|
+
|
|
117
|
+
# Generate implementation method
|
|
118
|
+
impl_method = self._generate_implementation_method(op, context, response_strategy)
|
|
119
|
+
parts.append(impl_method)
|
|
120
|
+
|
|
121
|
+
# Join with double newlines between overloads and implementation
|
|
122
|
+
return "\n\n".join(parts)
|
|
123
|
+
|
|
124
|
+
def _generate_implementation_method(self, op: IROperation, context: RenderContext, response_strategy: Any) -> str:
|
|
125
|
+
"""Generate the implementation method with runtime dispatch for multiple content types."""
|
|
126
|
+
# Type narrowing: request_body is guaranteed to exist when this method is called
|
|
127
|
+
assert (
|
|
128
|
+
op.request_body is not None
|
|
129
|
+
), "request_body should not be None in _generate_implementation_method" # nosec B101 - Type narrowing for mypy, validated by has_multiple_content_types
|
|
130
|
+
|
|
131
|
+
writer = CodeWriter()
|
|
132
|
+
|
|
133
|
+
# Generate implementation signature (accepts all content-type parameters as optional)
|
|
134
|
+
impl_sig = self.overload_generator.generate_implementation_signature(op, context, response_strategy)
|
|
135
|
+
writer.write_block(impl_sig)
|
|
136
|
+
|
|
137
|
+
# Generate docstring
|
|
138
|
+
ordered_params, primary_content_type, _ = self.parameter_processor.process_parameters(op, context)
|
|
139
|
+
writer.indent()
|
|
140
|
+
writer.write_line('"""')
|
|
141
|
+
writer.write_line(f"{op.summary or op.operation_id}")
|
|
142
|
+
writer.write_line("")
|
|
143
|
+
writer.write_line("Supports multiple content types:")
|
|
144
|
+
for content_type in op.request_body.content.keys():
|
|
145
|
+
writer.write_line(f"- {content_type}")
|
|
146
|
+
writer.write_line('"""')
|
|
147
|
+
|
|
148
|
+
# Generate URL construction
|
|
149
|
+
writer.write_line(f'url = f"{{self.base_url}}{op.path}"')
|
|
150
|
+
writer.write_line("")
|
|
151
|
+
|
|
152
|
+
# Generate runtime dispatch logic
|
|
153
|
+
writer.write_line("# Runtime dispatch based on content type")
|
|
154
|
+
|
|
155
|
+
first_content_type = True
|
|
156
|
+
for content_type in op.request_body.content.keys():
|
|
157
|
+
param_info = self.overload_generator._get_content_type_param_info(
|
|
158
|
+
content_type, op.request_body.content[content_type], context
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if first_content_type:
|
|
162
|
+
writer.write_line(f"if {param_info['name']} is not None:")
|
|
163
|
+
first_content_type = False
|
|
164
|
+
else:
|
|
165
|
+
writer.write_line(f"elif {param_info['name']} is not None:")
|
|
166
|
+
|
|
167
|
+
writer.indent()
|
|
168
|
+
|
|
169
|
+
# Generate request call for this content type
|
|
170
|
+
if content_type == "application/json":
|
|
171
|
+
writer.write_line(f"json_body = {param_info['name']}")
|
|
172
|
+
writer.write_line("response = await self._transport.request(")
|
|
173
|
+
writer.indent()
|
|
174
|
+
writer.write_line(f'"{op.method.value.upper()}", url,')
|
|
175
|
+
writer.write_line("params=None,")
|
|
176
|
+
writer.write_line("json=json_body,")
|
|
177
|
+
writer.write_line("headers=None")
|
|
178
|
+
writer.dedent()
|
|
179
|
+
writer.write_line(")")
|
|
180
|
+
elif content_type == "multipart/form-data":
|
|
181
|
+
writer.write_line(f"files_data = {param_info['name']}")
|
|
182
|
+
writer.write_line("response = await self._transport.request(")
|
|
183
|
+
writer.indent()
|
|
184
|
+
writer.write_line(f'"{op.method.value.upper()}", url,')
|
|
185
|
+
writer.write_line("params=None,")
|
|
186
|
+
writer.write_line("files=files_data,")
|
|
187
|
+
writer.write_line("headers=None")
|
|
188
|
+
writer.dedent()
|
|
189
|
+
writer.write_line(")")
|
|
190
|
+
else:
|
|
191
|
+
writer.write_line(f"data = {param_info['name']}")
|
|
192
|
+
writer.write_line("response = await self._transport.request(")
|
|
193
|
+
writer.indent()
|
|
194
|
+
writer.write_line(f'"{op.method.value.upper()}", url,')
|
|
195
|
+
writer.write_line("params=None,")
|
|
196
|
+
writer.write_line("data=data,")
|
|
197
|
+
writer.write_line("headers=None")
|
|
198
|
+
writer.dedent()
|
|
199
|
+
writer.write_line(")")
|
|
200
|
+
|
|
201
|
+
writer.dedent()
|
|
202
|
+
|
|
203
|
+
# Add else clause for error
|
|
204
|
+
writer.write_line("else:")
|
|
205
|
+
writer.indent()
|
|
206
|
+
writer.write_line('raise ValueError("One of the content-type parameters must be provided")')
|
|
207
|
+
writer.dedent()
|
|
208
|
+
writer.write_line("")
|
|
209
|
+
|
|
210
|
+
# Generate response handling (reuse existing generator)
|
|
211
|
+
self.response_handler_generator.generate_response_handling(writer, op, context, response_strategy)
|
|
212
|
+
|
|
213
|
+
writer.dedent()
|
|
214
|
+
|
|
215
|
+
return writer.get_code().strip()
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Generator for @overload signatures when operations have multiple content types."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pyopenapi_gen import IROperation
|
|
7
|
+
|
|
8
|
+
from ....context.render_context import RenderContext
|
|
9
|
+
from ....core.writers.code_writer import CodeWriter
|
|
10
|
+
from ..processors.parameter_processor import EndpointParameterProcessor
|
|
11
|
+
from .docstring_generator import EndpointDocstringGenerator
|
|
12
|
+
from .signature_generator import EndpointMethodSignatureGenerator
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class OverloadMethodGenerator:
|
|
18
|
+
"""
|
|
19
|
+
Generates @overload signatures for operations with multiple content types.
|
|
20
|
+
|
|
21
|
+
When an operation's request body accepts multiple content types
|
|
22
|
+
(e.g., application/json and multipart/form-data), this generator creates
|
|
23
|
+
type-safe @overload signatures following PEP 484.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, schemas: dict[str, Any] | None = None) -> None:
|
|
27
|
+
self.schemas = schemas or {}
|
|
28
|
+
self.parameter_processor = EndpointParameterProcessor(self.schemas)
|
|
29
|
+
self.signature_generator = EndpointMethodSignatureGenerator(self.schemas)
|
|
30
|
+
self.docstring_generator = EndpointDocstringGenerator(self.schemas)
|
|
31
|
+
|
|
32
|
+
def has_multiple_content_types(self, op: IROperation) -> bool:
|
|
33
|
+
"""
|
|
34
|
+
Check if operation request body has multiple content types.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
op: The operation to check
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
True if operation has request body with multiple content types
|
|
41
|
+
"""
|
|
42
|
+
if not op.request_body:
|
|
43
|
+
return False
|
|
44
|
+
return len(op.request_body.content) > 1
|
|
45
|
+
|
|
46
|
+
def generate_overload_signatures(
|
|
47
|
+
self, op: IROperation, context: RenderContext, response_strategy: Any
|
|
48
|
+
) -> list[str]:
|
|
49
|
+
"""
|
|
50
|
+
Generate @overload signatures for each content type.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
op: The operation with multiple content types
|
|
54
|
+
context: Render context for import tracking
|
|
55
|
+
response_strategy: Response strategy for return type resolution
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
List of @overload signature code strings
|
|
59
|
+
"""
|
|
60
|
+
if not self.has_multiple_content_types(op):
|
|
61
|
+
return []
|
|
62
|
+
|
|
63
|
+
# Ensure typing.overload is imported
|
|
64
|
+
context.add_import("typing", "overload")
|
|
65
|
+
context.add_import("typing", "Literal")
|
|
66
|
+
context.add_import("typing", "IO")
|
|
67
|
+
context.add_import("typing", "Any")
|
|
68
|
+
|
|
69
|
+
overload_signatures = []
|
|
70
|
+
|
|
71
|
+
# Type narrowing: request_body is guaranteed to exist by has_multiple_content_types check
|
|
72
|
+
assert (
|
|
73
|
+
op.request_body is not None
|
|
74
|
+
), "request_body should not be None after has_multiple_content_types check" # nosec B101 - Type narrowing for mypy, validated by has_multiple_content_types
|
|
75
|
+
|
|
76
|
+
for content_type, schema in op.request_body.content.items():
|
|
77
|
+
signature_code = self._generate_single_overload(op, content_type, schema, context, response_strategy)
|
|
78
|
+
overload_signatures.append(signature_code)
|
|
79
|
+
|
|
80
|
+
return overload_signatures
|
|
81
|
+
|
|
82
|
+
def _generate_single_overload(
|
|
83
|
+
self,
|
|
84
|
+
op: IROperation,
|
|
85
|
+
content_type: str,
|
|
86
|
+
schema: Any,
|
|
87
|
+
context: RenderContext,
|
|
88
|
+
response_strategy: Any,
|
|
89
|
+
) -> str:
|
|
90
|
+
"""
|
|
91
|
+
Generate a single @overload signature for one content type.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
op: The operation
|
|
95
|
+
content_type: The media type (e.g., "application/json")
|
|
96
|
+
schema: The schema for this content type
|
|
97
|
+
context: Render context
|
|
98
|
+
response_strategy: Response strategy for return type
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
@overload signature code as string
|
|
102
|
+
"""
|
|
103
|
+
writer = CodeWriter()
|
|
104
|
+
|
|
105
|
+
# Write @overload decorator
|
|
106
|
+
writer.write_line("@overload")
|
|
107
|
+
|
|
108
|
+
# Determine parameter name and type based on content type
|
|
109
|
+
param_info = self._get_content_type_param_info(content_type, schema, context)
|
|
110
|
+
|
|
111
|
+
# Build parameter list from operation parameters directly
|
|
112
|
+
param_parts = ["self"]
|
|
113
|
+
|
|
114
|
+
# Add path, query, and header parameters from operation
|
|
115
|
+
if op.parameters:
|
|
116
|
+
from ....types.services.type_service import UnifiedTypeService
|
|
117
|
+
|
|
118
|
+
type_service = UnifiedTypeService(self.schemas)
|
|
119
|
+
|
|
120
|
+
for param in op.parameters:
|
|
121
|
+
if param.param_in in ("path", "query", "header"):
|
|
122
|
+
param_type = type_service.resolve_schema_type(param.schema, context, required=param.required)
|
|
123
|
+
param_parts.append(f"{param.name}: {param_type}")
|
|
124
|
+
|
|
125
|
+
# Add keyword-only separator
|
|
126
|
+
param_parts.append("*")
|
|
127
|
+
|
|
128
|
+
# Add content-type-specific parameter
|
|
129
|
+
param_parts.append(f"{param_info['name']}: {param_info['type']}")
|
|
130
|
+
|
|
131
|
+
# Add content_type parameter with Literal type
|
|
132
|
+
param_parts.append(f'content_type: Literal["{content_type}"] = "{content_type}"')
|
|
133
|
+
|
|
134
|
+
# Get return type from response strategy
|
|
135
|
+
return_type = response_strategy.return_type
|
|
136
|
+
|
|
137
|
+
# Write signature
|
|
138
|
+
params_str = ",\n ".join(param_parts)
|
|
139
|
+
writer.write_line(f"async def {op.operation_id}(")
|
|
140
|
+
writer.indent()
|
|
141
|
+
writer.write_line(params_str)
|
|
142
|
+
writer.dedent()
|
|
143
|
+
writer.write_line(f") -> {return_type}: ...")
|
|
144
|
+
|
|
145
|
+
return writer.get_code()
|
|
146
|
+
|
|
147
|
+
def _get_content_type_param_info(self, content_type: str, schema: Any, context: RenderContext) -> dict[str, str]:
|
|
148
|
+
"""
|
|
149
|
+
Get parameter name and type hint for a content type.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
content_type: Media type string
|
|
153
|
+
schema: Schema for this content type
|
|
154
|
+
context: Render context for type resolution
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Dictionary with 'name' and 'type' keys
|
|
158
|
+
"""
|
|
159
|
+
from ....types.services.type_service import UnifiedTypeService
|
|
160
|
+
|
|
161
|
+
type_service = UnifiedTypeService(self.schemas)
|
|
162
|
+
|
|
163
|
+
# Map content types to parameter names and base types
|
|
164
|
+
if content_type == "application/json":
|
|
165
|
+
# For JSON, resolve the actual schema type
|
|
166
|
+
type_hint = type_service.resolve_schema_type(schema, context, required=True)
|
|
167
|
+
return {"name": "body", "type": type_hint}
|
|
168
|
+
|
|
169
|
+
elif content_type == "multipart/form-data":
|
|
170
|
+
# For multipart, always use files dict
|
|
171
|
+
return {"name": "files", "type": "dict[str, IO[Any]]"}
|
|
172
|
+
|
|
173
|
+
elif content_type == "application/x-www-form-urlencoded":
|
|
174
|
+
# For form data, use dict
|
|
175
|
+
return {"name": "data", "type": "dict[str, str]"}
|
|
176
|
+
|
|
177
|
+
else:
|
|
178
|
+
# Default: use body with Any type
|
|
179
|
+
logger.warning(f"Unknown content type {content_type}, using body: Any")
|
|
180
|
+
return {"name": "body", "type": "Any"}
|
|
181
|
+
|
|
182
|
+
def generate_implementation_signature(self, op: IROperation, context: RenderContext, response_strategy: Any) -> str:
|
|
183
|
+
"""
|
|
184
|
+
Generate the actual implementation method signature with optional parameters.
|
|
185
|
+
|
|
186
|
+
This signature accepts all possible content-type parameters as optional,
|
|
187
|
+
and includes runtime dispatch logic.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
op: The operation
|
|
191
|
+
context: Render context
|
|
192
|
+
response_strategy: Response strategy for return type
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Implementation method signature code
|
|
196
|
+
"""
|
|
197
|
+
writer = CodeWriter()
|
|
198
|
+
|
|
199
|
+
# Build parameter list
|
|
200
|
+
param_parts = ["self"]
|
|
201
|
+
|
|
202
|
+
# Add path, query, and header parameters from operation
|
|
203
|
+
if op.parameters:
|
|
204
|
+
from ....types.services.type_service import UnifiedTypeService
|
|
205
|
+
|
|
206
|
+
type_service = UnifiedTypeService(self.schemas)
|
|
207
|
+
|
|
208
|
+
for param in op.parameters:
|
|
209
|
+
if param.param_in in ("path", "query", "header"):
|
|
210
|
+
param_type = type_service.resolve_schema_type(param.schema, context, required=param.required)
|
|
211
|
+
param_parts.append(f"{param.name}: {param_type}")
|
|
212
|
+
|
|
213
|
+
# Add keyword-only separator
|
|
214
|
+
param_parts.append("*")
|
|
215
|
+
|
|
216
|
+
# Add all possible content-type parameters as optional
|
|
217
|
+
if op.request_body:
|
|
218
|
+
param_types_seen = set()
|
|
219
|
+
|
|
220
|
+
for content_type, schema in op.request_body.content.items():
|
|
221
|
+
param_info = self._get_content_type_param_info(content_type, schema, context)
|
|
222
|
+
|
|
223
|
+
# Avoid duplicate parameters (e.g., if multiple JSON variants)
|
|
224
|
+
param_key = param_info["name"]
|
|
225
|
+
if param_key not in param_types_seen:
|
|
226
|
+
param_parts.append(f"{param_info['name']}: {param_info['type']} | None = None")
|
|
227
|
+
param_types_seen.add(param_key)
|
|
228
|
+
|
|
229
|
+
# Add content_type parameter (no Literal, just str)
|
|
230
|
+
param_parts.append('content_type: str = "application/json"')
|
|
231
|
+
|
|
232
|
+
# Get return type
|
|
233
|
+
return_type = response_strategy.return_type
|
|
234
|
+
|
|
235
|
+
# Write signature
|
|
236
|
+
params_str = ",\n ".join(param_parts)
|
|
237
|
+
writer.write_line(f"async def {op.operation_id}(")
|
|
238
|
+
writer.indent()
|
|
239
|
+
writer.write_line(params_str)
|
|
240
|
+
writer.dedent()
|
|
241
|
+
writer.write_line(f") -> {return_type}:")
|
|
242
|
+
|
|
243
|
+
return writer.get_code()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyopenapi-gen
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.18.0
|
|
4
4
|
Summary: Modern, async-first Python client generator for OpenAPI specifications with advanced cycle detection and unified type resolution
|
|
5
5
|
Project-URL: Homepage, https://github.com/your-org/pyopenapi-gen
|
|
6
6
|
Project-URL: Documentation, https://github.com/your-org/pyopenapi-gen/blob/main/README.md
|
|
@@ -123,6 +123,254 @@ async def main():
|
|
|
123
123
|
asyncio.run(main())
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
+
## 🐍 Using as a Library (Programmatic API)
|
|
127
|
+
|
|
128
|
+
### Why Programmatic Usage?
|
|
129
|
+
The generator was designed to work both as a CLI tool and as a Python library. Programmatic usage enables integration with build systems, CI/CD pipelines, code generators, and custom tooling. You get the same powerful code generation capabilities with full Python API access.
|
|
130
|
+
|
|
131
|
+
### What Is the Programmatic API?
|
|
132
|
+
A simple, function-based API that wraps the internal `ClientGenerator` class, providing a clean entry point for library usage without requiring knowledge of internal structure.
|
|
133
|
+
|
|
134
|
+
```mermaid
|
|
135
|
+
graph TD
|
|
136
|
+
A[Your Build Script] --> B[generate_client Function]
|
|
137
|
+
B --> C[ClientGenerator]
|
|
138
|
+
C --> D[Load OpenAPI Spec]
|
|
139
|
+
D --> E[Generate Code]
|
|
140
|
+
E --> F[Write Files]
|
|
141
|
+
F --> G[Post-Process]
|
|
142
|
+
G --> H[Return File List]
|
|
143
|
+
|
|
144
|
+
subgraph "Public API"
|
|
145
|
+
B
|
|
146
|
+
I[ClientGenerator Class]
|
|
147
|
+
J[GenerationError Exception]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
subgraph "Advanced API"
|
|
151
|
+
K[load_ir_from_spec]
|
|
152
|
+
L[IR Models]
|
|
153
|
+
M[WarningCollector]
|
|
154
|
+
end
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### How to Use Programmatically
|
|
158
|
+
|
|
159
|
+
#### Basic Usage
|
|
160
|
+
```python
|
|
161
|
+
from pyopenapi_gen import generate_client
|
|
162
|
+
|
|
163
|
+
# Simple client generation
|
|
164
|
+
files = generate_client(
|
|
165
|
+
spec_path="input/openapi.yaml",
|
|
166
|
+
project_root=".",
|
|
167
|
+
output_package="pyapis.my_client"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
print(f"Generated {len(files)} files")
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
#### Advanced Usage with All Options
|
|
174
|
+
```python
|
|
175
|
+
from pyopenapi_gen import generate_client, GenerationError
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
files = generate_client(
|
|
179
|
+
spec_path="input/openapi.yaml",
|
|
180
|
+
project_root=".",
|
|
181
|
+
output_package="pyapis.my_client",
|
|
182
|
+
core_package="pyapis.core", # Optional shared core
|
|
183
|
+
force=True, # Overwrite without diff check
|
|
184
|
+
no_postprocess=False, # Run Black + mypy
|
|
185
|
+
verbose=True # Show progress
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Process generated files
|
|
189
|
+
for file_path in files:
|
|
190
|
+
print(f"Generated: {file_path}")
|
|
191
|
+
|
|
192
|
+
except GenerationError as e:
|
|
193
|
+
print(f"Generation failed: {e}")
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
#### Multi-Client Generation Script
|
|
197
|
+
```python
|
|
198
|
+
from pyopenapi_gen import generate_client
|
|
199
|
+
from pathlib import Path
|
|
200
|
+
|
|
201
|
+
# Configuration for multiple clients
|
|
202
|
+
clients = [
|
|
203
|
+
{"spec": "api_v1.yaml", "package": "pyapis.client_v1"},
|
|
204
|
+
{"spec": "api_v2.yaml", "package": "pyapis.client_v2"},
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
# Shared core package
|
|
208
|
+
core_package = "pyapis.core"
|
|
209
|
+
|
|
210
|
+
# Generate all clients
|
|
211
|
+
for client_config in clients:
|
|
212
|
+
print(f"Generating {client_config['package']}...")
|
|
213
|
+
|
|
214
|
+
generate_client(
|
|
215
|
+
spec_path=client_config["spec"],
|
|
216
|
+
project_root=".",
|
|
217
|
+
output_package=client_config["package"],
|
|
218
|
+
core_package=core_package,
|
|
219
|
+
force=True,
|
|
220
|
+
verbose=True
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
print("All clients generated successfully!")
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
#### Integration with Build Systems
|
|
227
|
+
```python
|
|
228
|
+
# Example: Custom build script
|
|
229
|
+
import sys
|
|
230
|
+
from pathlib import Path
|
|
231
|
+
from pyopenapi_gen import generate_client, GenerationError
|
|
232
|
+
|
|
233
|
+
def build_api_clients():
|
|
234
|
+
"""Generate all API clients as part of build process"""
|
|
235
|
+
|
|
236
|
+
specs_dir = Path("specs")
|
|
237
|
+
|
|
238
|
+
# Find all OpenAPI specs
|
|
239
|
+
spec_files = list(specs_dir.glob("*.yaml")) + list(specs_dir.glob("*.json"))
|
|
240
|
+
|
|
241
|
+
if not spec_files:
|
|
242
|
+
print("No OpenAPI specs found in specs/")
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
# Generate clients
|
|
246
|
+
for spec_file in spec_files:
|
|
247
|
+
client_name = spec_file.stem
|
|
248
|
+
package_name = f"pyapis.{client_name}"
|
|
249
|
+
|
|
250
|
+
print(f"Generating client for {spec_file.name}...")
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
generate_client(
|
|
254
|
+
spec_path=str(spec_file),
|
|
255
|
+
project_root="src",
|
|
256
|
+
output_package=package_name,
|
|
257
|
+
core_package="pyapis.core",
|
|
258
|
+
force=True
|
|
259
|
+
)
|
|
260
|
+
except GenerationError as e:
|
|
261
|
+
print(f"Failed to generate {client_name}: {e}", file=sys.stderr)
|
|
262
|
+
return False
|
|
263
|
+
|
|
264
|
+
return True
|
|
265
|
+
|
|
266
|
+
if __name__ == "__main__":
|
|
267
|
+
success = build_api_clients()
|
|
268
|
+
sys.exit(0 if success else 1)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### API Reference
|
|
272
|
+
|
|
273
|
+
#### `generate_client()` Function
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
def generate_client(
|
|
277
|
+
spec_path: str,
|
|
278
|
+
project_root: str,
|
|
279
|
+
output_package: str,
|
|
280
|
+
core_package: str | None = None,
|
|
281
|
+
force: bool = False,
|
|
282
|
+
no_postprocess: bool = False,
|
|
283
|
+
verbose: bool = False,
|
|
284
|
+
) -> List[Path]
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Parameters**:
|
|
288
|
+
- `spec_path`: Path to OpenAPI spec file (YAML or JSON)
|
|
289
|
+
- `project_root`: Root directory of your Python project
|
|
290
|
+
- `output_package`: Python package name (e.g., `'pyapis.my_client'`)
|
|
291
|
+
- `core_package`: Optional shared core package name (defaults to `{output_package}.core`)
|
|
292
|
+
- `force`: Skip diff check and overwrite existing output
|
|
293
|
+
- `no_postprocess`: Skip Black formatting and mypy type checking
|
|
294
|
+
- `verbose`: Print detailed progress information
|
|
295
|
+
|
|
296
|
+
**Returns**: List of `Path` objects for all generated files
|
|
297
|
+
|
|
298
|
+
**Raises**: `GenerationError` if generation fails
|
|
299
|
+
|
|
300
|
+
#### `ClientGenerator` Class (Advanced)
|
|
301
|
+
|
|
302
|
+
For advanced use cases requiring more control:
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
from pyopenapi_gen import ClientGenerator, GenerationError
|
|
306
|
+
from pathlib import Path
|
|
307
|
+
|
|
308
|
+
# Create generator with custom settings
|
|
309
|
+
generator = ClientGenerator(verbose=True)
|
|
310
|
+
|
|
311
|
+
# Generate with full control
|
|
312
|
+
try:
|
|
313
|
+
files = generator.generate(
|
|
314
|
+
spec_path="openapi.yaml",
|
|
315
|
+
project_root=Path("."),
|
|
316
|
+
output_package="pyapis.my_client",
|
|
317
|
+
core_package="pyapis.core",
|
|
318
|
+
force=False,
|
|
319
|
+
no_postprocess=False
|
|
320
|
+
)
|
|
321
|
+
except GenerationError as e:
|
|
322
|
+
print(f"Generation failed: {e}")
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
#### `GenerationError` Exception
|
|
326
|
+
|
|
327
|
+
Raised when generation fails. Contains contextual information about the failure:
|
|
328
|
+
|
|
329
|
+
```python
|
|
330
|
+
from pyopenapi_gen import generate_client, GenerationError
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
generate_client(
|
|
334
|
+
spec_path="invalid.yaml",
|
|
335
|
+
project_root=".",
|
|
336
|
+
output_package="test"
|
|
337
|
+
)
|
|
338
|
+
except GenerationError as e:
|
|
339
|
+
# Exception message includes context
|
|
340
|
+
print(f"Error: {e}")
|
|
341
|
+
# Typical causes:
|
|
342
|
+
# - Invalid OpenAPI specification
|
|
343
|
+
# - File I/O errors
|
|
344
|
+
# - Type checking failures
|
|
345
|
+
# - Invalid project structure
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Comparison: CLI vs Programmatic API
|
|
349
|
+
|
|
350
|
+
**CLI Usage**:
|
|
351
|
+
```bash
|
|
352
|
+
pyopenapi-gen input/openapi.yaml \
|
|
353
|
+
--project-root . \
|
|
354
|
+
--output-package pyapis.my_client \
|
|
355
|
+
--force \
|
|
356
|
+
--verbose
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
**Equivalent Programmatic Usage**:
|
|
360
|
+
```python
|
|
361
|
+
from pyopenapi_gen import generate_client
|
|
362
|
+
|
|
363
|
+
generate_client(
|
|
364
|
+
spec_path="input/openapi.yaml",
|
|
365
|
+
project_root=".",
|
|
366
|
+
output_package="pyapis.my_client",
|
|
367
|
+
force=True,
|
|
368
|
+
verbose=True
|
|
369
|
+
)
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Both approaches use the same underlying implementation and produce identical results.
|
|
373
|
+
|
|
126
374
|
## 🔧 Configuration Options
|
|
127
375
|
|
|
128
376
|
### Standalone Client (Default)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
pyopenapi_gen/__init__.py,sha256=
|
|
1
|
+
pyopenapi_gen/__init__.py,sha256=PuqDvWUTyOlVU2Et6YBu9am_aSyMIC-O3YrbFuGnEXY,7533
|
|
2
2
|
pyopenapi_gen/__main__.py,sha256=4-SCaCNhBd7rtyRK58uoDbdl93J0KhUeajP_b0CPpLE,110
|
|
3
3
|
pyopenapi_gen/cli.py,sha256=9T_XF3-ih_JlM_BOkmHft-HoMCGOqL5UrnAHBJ0fb5w,2320
|
|
4
4
|
pyopenapi_gen/http_types.py,sha256=EMMYZBt8PNVZKPFu77TQija-JI-nOKyXvpiQP9-VSWE,467
|
|
@@ -112,7 +112,8 @@ pyopenapi_gen/visit/endpoint/__init__.py,sha256=DftIZSWp6Z8jKWoJE2VGKL4G_5cqwFXe
|
|
|
112
112
|
pyopenapi_gen/visit/endpoint/endpoint_visitor.py,sha256=1aS0i2D_Y-979_7aBd0W6IS2UVO6wMujsMMw8Qt8PRE,3574
|
|
113
113
|
pyopenapi_gen/visit/endpoint/generators/__init__.py,sha256=-X-GYnJZ9twiEBr_U0obW8VuSoY6IJmYaxinn-seMzA,50
|
|
114
114
|
pyopenapi_gen/visit/endpoint/generators/docstring_generator.py,sha256=_VpRabZ2g_N42TTWGI6gtPxqzlZD65dOHm8U_YvtWbQ,5891
|
|
115
|
-
pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py,sha256=
|
|
115
|
+
pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py,sha256=X0tdSVmfUTDSOVKmC5cQLO3SfC_hsRCQpt5lPoC6MYI,9998
|
|
116
|
+
pyopenapi_gen/visit/endpoint/generators/overload_generator.py,sha256=1W8pt5iUSDsgxJJUoxfQyRZkxCwFi1WbvVM__SazCe0,9074
|
|
116
117
|
pyopenapi_gen/visit/endpoint/generators/request_generator.py,sha256=MKtZ6Fa050gCgqAGhXeo--p_AzqV9RmDd8e4Zvglgo0,5349
|
|
117
118
|
pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py,sha256=Jb_8hRQY1OfpThupa7UdCNxa712m3WaeNIbLhlYk1bg,24193
|
|
118
119
|
pyopenapi_gen/visit/endpoint/generators/signature_generator.py,sha256=ENi34Rf2x1Ijtvca652ihV9L2UUT1O2SEsG-66Pm_Z8,3873
|
|
@@ -125,8 +126,8 @@ pyopenapi_gen/visit/model/alias_generator.py,sha256=wEMHipPA1_CFxvQ6CS9j4qgXK93s
|
|
|
125
126
|
pyopenapi_gen/visit/model/dataclass_generator.py,sha256=L794s2yyYUO__akGd120NJMV7g2xqiPfQyZo8CduIVM,14426
|
|
126
127
|
pyopenapi_gen/visit/model/enum_generator.py,sha256=AXqKUFuWUUjUF_6_HqBKY8vB5GYu35Pb2C2WPFrOw1k,10061
|
|
127
128
|
pyopenapi_gen/visit/model/model_visitor.py,sha256=TC6pbxpQiy5FWhmQpfllLuXA3ImTYNMcrazkOFZCIyo,9470
|
|
128
|
-
pyopenapi_gen-0.
|
|
129
|
-
pyopenapi_gen-0.
|
|
130
|
-
pyopenapi_gen-0.
|
|
131
|
-
pyopenapi_gen-0.
|
|
132
|
-
pyopenapi_gen-0.
|
|
129
|
+
pyopenapi_gen-0.18.0.dist-info/METADATA,sha256=kLYKgzczN64FbkIQ0y_Tg-jnZ78yU8VrFZVeVL-LKic,20551
|
|
130
|
+
pyopenapi_gen-0.18.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
131
|
+
pyopenapi_gen-0.18.0.dist-info/entry_points.txt,sha256=gxSlNiwom50T3OEZnlocA6qRjGdV0bn6hN_Xr-Ub5wA,56
|
|
132
|
+
pyopenapi_gen-0.18.0.dist-info/licenses/LICENSE,sha256=UFAyTWKa4w10-QerlJaHJeep7G2gcwpf-JmvI2dS2Gc,1088
|
|
133
|
+
pyopenapi_gen-0.18.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|