otterapi 0.0.5__py3-none-any.whl → 0.0.6__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.
- README.md +581 -8
- otterapi/__init__.py +73 -0
- otterapi/cli.py +327 -29
- otterapi/codegen/__init__.py +115 -0
- otterapi/codegen/ast_utils.py +134 -5
- otterapi/codegen/client.py +1271 -0
- otterapi/codegen/codegen.py +1736 -0
- otterapi/codegen/dataframes.py +392 -0
- otterapi/codegen/emitter.py +473 -0
- otterapi/codegen/endpoints.py +2597 -343
- otterapi/codegen/pagination.py +1026 -0
- otterapi/codegen/schema.py +593 -0
- otterapi/codegen/splitting.py +1397 -0
- otterapi/codegen/types.py +1345 -0
- otterapi/codegen/utils.py +180 -1
- otterapi/config.py +1017 -24
- otterapi/exceptions.py +231 -0
- otterapi/openapi/__init__.py +46 -0
- otterapi/openapi/v2/__init__.py +86 -0
- otterapi/openapi/v2/spec.json +1607 -0
- otterapi/openapi/v2/v2.py +1776 -0
- otterapi/openapi/v3/__init__.py +131 -0
- otterapi/openapi/v3/spec.json +1651 -0
- otterapi/openapi/v3/v3.py +1557 -0
- otterapi/openapi/v3_1/__init__.py +133 -0
- otterapi/openapi/v3_1/spec.json +1411 -0
- otterapi/openapi/v3_1/v3_1.py +798 -0
- otterapi/openapi/v3_2/__init__.py +133 -0
- otterapi/openapi/v3_2/spec.json +1666 -0
- otterapi/openapi/v3_2/v3_2.py +777 -0
- otterapi/tests/__init__.py +3 -0
- otterapi/tests/fixtures/__init__.py +455 -0
- otterapi/tests/test_ast_utils.py +680 -0
- otterapi/tests/test_codegen.py +610 -0
- otterapi/tests/test_dataframe.py +1038 -0
- otterapi/tests/test_exceptions.py +493 -0
- otterapi/tests/test_openapi_support.py +616 -0
- otterapi/tests/test_openapi_upgrade.py +215 -0
- otterapi/tests/test_pagination.py +1101 -0
- otterapi/tests/test_splitting_config.py +319 -0
- otterapi/tests/test_splitting_integration.py +427 -0
- otterapi/tests/test_splitting_resolver.py +512 -0
- otterapi/tests/test_splitting_tree.py +525 -0
- otterapi-0.0.6.dist-info/METADATA +627 -0
- otterapi-0.0.6.dist-info/RECORD +48 -0
- {otterapi-0.0.5.dist-info → otterapi-0.0.6.dist-info}/WHEEL +1 -1
- otterapi/codegen/generator.py +0 -358
- otterapi/codegen/openapi_processor.py +0 -27
- otterapi/codegen/type_generator.py +0 -559
- otterapi-0.0.5.dist-info/METADATA +0 -54
- otterapi-0.0.5.dist-info/RECORD +0 -16
- {otterapi-0.0.5.dist-info → otterapi-0.0.6.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,1271 @@
|
|
|
1
|
+
"""Client class generation module for OtterAPI.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for generating a client class that wraps
|
|
4
|
+
all API endpoints with configurable base URL, timeout, headers, and
|
|
5
|
+
HTTP client injection support.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import ast
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from otterapi.codegen.ast_utils import (
|
|
13
|
+
_argument,
|
|
14
|
+
_assign,
|
|
15
|
+
_attr,
|
|
16
|
+
_call,
|
|
17
|
+
_name,
|
|
18
|
+
_subscript,
|
|
19
|
+
_union_expr,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Re-export DataFrameMethodConfig from dataframes for backward compatibility
|
|
23
|
+
from otterapi.codegen.dataframes import DataFrameMethodConfig
|
|
24
|
+
from otterapi.codegen.endpoints import ParameterASTBuilder
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from otterapi.codegen.types import Parameter, RequestBodyInfo, ResponseInfo, Type
|
|
28
|
+
|
|
29
|
+
# Type alias for import dictionaries
|
|
30
|
+
ImportDict = dict[str, set[str]]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class EndpointInfo:
|
|
35
|
+
"""Information about an endpoint for client method generation."""
|
|
36
|
+
|
|
37
|
+
name: str
|
|
38
|
+
async_name: str
|
|
39
|
+
method: str
|
|
40
|
+
path: str
|
|
41
|
+
parameters: list['Parameter'] | None
|
|
42
|
+
request_body: 'RequestBodyInfo | None'
|
|
43
|
+
response_type: 'Type | None'
|
|
44
|
+
response_infos: list['ResponseInfo'] | None
|
|
45
|
+
description: str | None
|
|
46
|
+
dataframe_config: DataFrameMethodConfig = field(
|
|
47
|
+
default_factory=DataFrameMethodConfig
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def generate_api_error_class() -> ast.ClassDef:
|
|
52
|
+
"""Generate the APIError exception class for detailed error handling.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
AST ClassDef for the APIError class.
|
|
56
|
+
"""
|
|
57
|
+
# Build __init__ method
|
|
58
|
+
init_body = [
|
|
59
|
+
# self.status_code = status_code
|
|
60
|
+
_assign(_attr('self', 'status_code'), _name('status_code')),
|
|
61
|
+
# self.response = response
|
|
62
|
+
_assign(_attr('self', 'response'), _name('response')),
|
|
63
|
+
# self.detail = detail
|
|
64
|
+
_assign(_attr('self', 'detail'), _name('detail')),
|
|
65
|
+
# self.body = body
|
|
66
|
+
_assign(_attr('self', 'body'), _name('body')),
|
|
67
|
+
# super().__init__(message)
|
|
68
|
+
ast.Expr(
|
|
69
|
+
value=_call(
|
|
70
|
+
_attr(_call(_name('super')), '__init__'),
|
|
71
|
+
args=[_name('message')],
|
|
72
|
+
)
|
|
73
|
+
),
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
init_method = ast.FunctionDef(
|
|
77
|
+
name='__init__',
|
|
78
|
+
args=ast.arguments(
|
|
79
|
+
posonlyargs=[],
|
|
80
|
+
args=[
|
|
81
|
+
_argument('self'),
|
|
82
|
+
_argument('message', _name('str')),
|
|
83
|
+
],
|
|
84
|
+
kwonlyargs=[
|
|
85
|
+
_argument('status_code', _name('int')),
|
|
86
|
+
_argument('response', _name('Response')),
|
|
87
|
+
_argument(
|
|
88
|
+
'detail', _union_expr([_name('Any'), ast.Constant(value=None)])
|
|
89
|
+
),
|
|
90
|
+
_argument('body', _name('str')),
|
|
91
|
+
],
|
|
92
|
+
kw_defaults=[
|
|
93
|
+
None, # status_code - required, no default
|
|
94
|
+
None, # response - required, no default
|
|
95
|
+
ast.Constant(value=None), # detail - default None
|
|
96
|
+
ast.Constant(value=''), # body - default ''
|
|
97
|
+
],
|
|
98
|
+
kwarg=None,
|
|
99
|
+
defaults=[],
|
|
100
|
+
),
|
|
101
|
+
body=init_body,
|
|
102
|
+
decorator_list=[],
|
|
103
|
+
returns=ast.Constant(value=None),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Build from_response classmethod
|
|
107
|
+
from_response_body = [
|
|
108
|
+
# status_code = response.status_code
|
|
109
|
+
_assign(_name('status_code'), _attr('response', 'status_code')),
|
|
110
|
+
# body = response.text
|
|
111
|
+
_assign(_name('body'), _attr('response', 'text')),
|
|
112
|
+
# detail = None
|
|
113
|
+
_assign(_name('detail'), ast.Constant(value=None)),
|
|
114
|
+
# try: ... except: ...
|
|
115
|
+
ast.Try(
|
|
116
|
+
body=[
|
|
117
|
+
# json_body = response.json()
|
|
118
|
+
_assign(_name('json_body'), _call(_attr('response', 'json'))),
|
|
119
|
+
# if isinstance(json_body, dict):
|
|
120
|
+
ast.If(
|
|
121
|
+
test=_call(
|
|
122
|
+
_name('isinstance'), args=[_name('json_body'), _name('dict')]
|
|
123
|
+
),
|
|
124
|
+
body=[
|
|
125
|
+
# detail = json_body.get('detail', json_body)
|
|
126
|
+
_assign(
|
|
127
|
+
_name('detail'),
|
|
128
|
+
_call(
|
|
129
|
+
_attr('json_body', 'get'),
|
|
130
|
+
args=[ast.Constant(value='detail'), _name('json_body')],
|
|
131
|
+
),
|
|
132
|
+
),
|
|
133
|
+
],
|
|
134
|
+
orelse=[
|
|
135
|
+
# detail = json_body
|
|
136
|
+
_assign(_name('detail'), _name('json_body')),
|
|
137
|
+
],
|
|
138
|
+
),
|
|
139
|
+
],
|
|
140
|
+
handlers=[
|
|
141
|
+
ast.ExceptHandler(
|
|
142
|
+
type=_name('Exception'),
|
|
143
|
+
name=None,
|
|
144
|
+
body=[
|
|
145
|
+
# detail = body if body else None
|
|
146
|
+
_assign(
|
|
147
|
+
_name('detail'),
|
|
148
|
+
ast.IfExp(
|
|
149
|
+
test=_name('body'),
|
|
150
|
+
body=_name('body'),
|
|
151
|
+
orelse=ast.Constant(value=None),
|
|
152
|
+
),
|
|
153
|
+
),
|
|
154
|
+
],
|
|
155
|
+
),
|
|
156
|
+
],
|
|
157
|
+
orelse=[],
|
|
158
|
+
finalbody=[],
|
|
159
|
+
),
|
|
160
|
+
# message = f'HTTP {status_code} Error'
|
|
161
|
+
_assign(
|
|
162
|
+
_name('message'),
|
|
163
|
+
ast.JoinedStr(
|
|
164
|
+
values=[
|
|
165
|
+
ast.Constant(value='HTTP '),
|
|
166
|
+
ast.FormattedValue(value=_name('status_code'), conversion=-1),
|
|
167
|
+
ast.Constant(value=' Error'),
|
|
168
|
+
]
|
|
169
|
+
),
|
|
170
|
+
),
|
|
171
|
+
# if detail:
|
|
172
|
+
ast.If(
|
|
173
|
+
test=_name('detail'),
|
|
174
|
+
body=[
|
|
175
|
+
# if isinstance(detail, list):
|
|
176
|
+
ast.If(
|
|
177
|
+
test=_call(
|
|
178
|
+
_name('isinstance'), args=[_name('detail'), _name('list')]
|
|
179
|
+
),
|
|
180
|
+
body=[
|
|
181
|
+
# error_msgs = []
|
|
182
|
+
_assign(_name('error_msgs'), ast.List(elts=[], ctx=ast.Load())),
|
|
183
|
+
# for err in detail:
|
|
184
|
+
ast.For(
|
|
185
|
+
target=ast.Name(id='err', ctx=ast.Store()),
|
|
186
|
+
iter=_name('detail'),
|
|
187
|
+
body=[
|
|
188
|
+
ast.If(
|
|
189
|
+
test=_call(
|
|
190
|
+
_name('isinstance'),
|
|
191
|
+
args=[_name('err'), _name('dict')],
|
|
192
|
+
),
|
|
193
|
+
body=[
|
|
194
|
+
_assign(
|
|
195
|
+
_name('loc'),
|
|
196
|
+
_call(
|
|
197
|
+
_attr('err', 'get'),
|
|
198
|
+
args=[
|
|
199
|
+
ast.Constant(value='loc'),
|
|
200
|
+
ast.List(elts=[], ctx=ast.Load()),
|
|
201
|
+
],
|
|
202
|
+
),
|
|
203
|
+
),
|
|
204
|
+
_assign(
|
|
205
|
+
_name('msg'),
|
|
206
|
+
_call(
|
|
207
|
+
_attr('err', 'get'),
|
|
208
|
+
args=[
|
|
209
|
+
ast.Constant(value='msg'),
|
|
210
|
+
_call(
|
|
211
|
+
_name('str'),
|
|
212
|
+
args=[_name('err')],
|
|
213
|
+
),
|
|
214
|
+
],
|
|
215
|
+
),
|
|
216
|
+
),
|
|
217
|
+
_assign(
|
|
218
|
+
_name('loc_str'),
|
|
219
|
+
ast.IfExp(
|
|
220
|
+
test=_name('loc'),
|
|
221
|
+
body=_call(
|
|
222
|
+
_attr(
|
|
223
|
+
ast.Constant(value=' -> '),
|
|
224
|
+
'join',
|
|
225
|
+
),
|
|
226
|
+
args=[
|
|
227
|
+
ast.GeneratorExp(
|
|
228
|
+
elt=_call(
|
|
229
|
+
_name('str'),
|
|
230
|
+
args=[_name('x')],
|
|
231
|
+
),
|
|
232
|
+
generators=[
|
|
233
|
+
ast.comprehension(
|
|
234
|
+
target=ast.Name(
|
|
235
|
+
id='x',
|
|
236
|
+
ctx=ast.Store(),
|
|
237
|
+
),
|
|
238
|
+
iter=_name('loc'),
|
|
239
|
+
ifs=[],
|
|
240
|
+
is_async=0,
|
|
241
|
+
)
|
|
242
|
+
],
|
|
243
|
+
)
|
|
244
|
+
],
|
|
245
|
+
),
|
|
246
|
+
orelse=ast.Constant(value='unknown'),
|
|
247
|
+
),
|
|
248
|
+
),
|
|
249
|
+
ast.Expr(
|
|
250
|
+
value=_call(
|
|
251
|
+
_attr('error_msgs', 'append'),
|
|
252
|
+
args=[
|
|
253
|
+
ast.JoinedStr(
|
|
254
|
+
values=[
|
|
255
|
+
ast.Constant(value=' - '),
|
|
256
|
+
ast.FormattedValue(
|
|
257
|
+
value=_name('loc_str'),
|
|
258
|
+
conversion=-1,
|
|
259
|
+
),
|
|
260
|
+
ast.Constant(value=': '),
|
|
261
|
+
ast.FormattedValue(
|
|
262
|
+
value=_name('msg'),
|
|
263
|
+
conversion=-1,
|
|
264
|
+
),
|
|
265
|
+
]
|
|
266
|
+
)
|
|
267
|
+
],
|
|
268
|
+
)
|
|
269
|
+
),
|
|
270
|
+
],
|
|
271
|
+
orelse=[
|
|
272
|
+
ast.Expr(
|
|
273
|
+
value=_call(
|
|
274
|
+
_attr('error_msgs', 'append'),
|
|
275
|
+
args=[
|
|
276
|
+
ast.JoinedStr(
|
|
277
|
+
values=[
|
|
278
|
+
ast.Constant(value=' - '),
|
|
279
|
+
ast.FormattedValue(
|
|
280
|
+
value=_name('err'),
|
|
281
|
+
conversion=-1,
|
|
282
|
+
),
|
|
283
|
+
]
|
|
284
|
+
)
|
|
285
|
+
],
|
|
286
|
+
)
|
|
287
|
+
),
|
|
288
|
+
],
|
|
289
|
+
),
|
|
290
|
+
],
|
|
291
|
+
orelse=[],
|
|
292
|
+
),
|
|
293
|
+
# if error_msgs:
|
|
294
|
+
ast.If(
|
|
295
|
+
test=_name('error_msgs'),
|
|
296
|
+
body=[
|
|
297
|
+
_assign(
|
|
298
|
+
_name('message'),
|
|
299
|
+
ast.BinOp(
|
|
300
|
+
left=ast.JoinedStr(
|
|
301
|
+
values=[
|
|
302
|
+
ast.Constant(value='HTTP '),
|
|
303
|
+
ast.FormattedValue(
|
|
304
|
+
value=_name('status_code'),
|
|
305
|
+
conversion=-1,
|
|
306
|
+
),
|
|
307
|
+
ast.Constant(
|
|
308
|
+
value=' Validation Error:\n'
|
|
309
|
+
),
|
|
310
|
+
]
|
|
311
|
+
),
|
|
312
|
+
op=ast.Add(),
|
|
313
|
+
right=_call(
|
|
314
|
+
_attr(ast.Constant(value='\n'), 'join'),
|
|
315
|
+
args=[_name('error_msgs')],
|
|
316
|
+
),
|
|
317
|
+
),
|
|
318
|
+
),
|
|
319
|
+
],
|
|
320
|
+
orelse=[],
|
|
321
|
+
),
|
|
322
|
+
],
|
|
323
|
+
orelse=[
|
|
324
|
+
# elif isinstance(detail, str):
|
|
325
|
+
ast.If(
|
|
326
|
+
test=_call(
|
|
327
|
+
_name('isinstance'),
|
|
328
|
+
args=[_name('detail'), _name('str')],
|
|
329
|
+
),
|
|
330
|
+
body=[
|
|
331
|
+
_assign(
|
|
332
|
+
_name('message'),
|
|
333
|
+
ast.JoinedStr(
|
|
334
|
+
values=[
|
|
335
|
+
ast.Constant(value='HTTP '),
|
|
336
|
+
ast.FormattedValue(
|
|
337
|
+
value=_name('status_code'),
|
|
338
|
+
conversion=-1,
|
|
339
|
+
),
|
|
340
|
+
ast.Constant(value=' Error: '),
|
|
341
|
+
ast.FormattedValue(
|
|
342
|
+
value=_name('detail'), conversion=-1
|
|
343
|
+
),
|
|
344
|
+
]
|
|
345
|
+
),
|
|
346
|
+
),
|
|
347
|
+
],
|
|
348
|
+
orelse=[
|
|
349
|
+
_assign(
|
|
350
|
+
_name('message'),
|
|
351
|
+
ast.JoinedStr(
|
|
352
|
+
values=[
|
|
353
|
+
ast.Constant(value='HTTP '),
|
|
354
|
+
ast.FormattedValue(
|
|
355
|
+
value=_name('status_code'),
|
|
356
|
+
conversion=-1,
|
|
357
|
+
),
|
|
358
|
+
ast.Constant(value=' Error: '),
|
|
359
|
+
ast.FormattedValue(
|
|
360
|
+
value=_name('detail'), conversion=-1
|
|
361
|
+
),
|
|
362
|
+
]
|
|
363
|
+
),
|
|
364
|
+
),
|
|
365
|
+
],
|
|
366
|
+
),
|
|
367
|
+
],
|
|
368
|
+
),
|
|
369
|
+
],
|
|
370
|
+
orelse=[],
|
|
371
|
+
),
|
|
372
|
+
# return cls(message, status_code=status_code, response=response, detail=detail, body=body)
|
|
373
|
+
ast.Return(
|
|
374
|
+
value=_call(
|
|
375
|
+
_name('cls'),
|
|
376
|
+
args=[_name('message')],
|
|
377
|
+
keywords=[
|
|
378
|
+
ast.keyword(arg='status_code', value=_name('status_code')),
|
|
379
|
+
ast.keyword(arg='response', value=_name('response')),
|
|
380
|
+
ast.keyword(arg='detail', value=_name('detail')),
|
|
381
|
+
ast.keyword(arg='body', value=_name('body')),
|
|
382
|
+
],
|
|
383
|
+
)
|
|
384
|
+
),
|
|
385
|
+
]
|
|
386
|
+
|
|
387
|
+
from_response_method = ast.FunctionDef(
|
|
388
|
+
name='from_response',
|
|
389
|
+
args=ast.arguments(
|
|
390
|
+
posonlyargs=[],
|
|
391
|
+
args=[
|
|
392
|
+
_argument('cls'),
|
|
393
|
+
_argument('response', _name('Response')),
|
|
394
|
+
],
|
|
395
|
+
kwonlyargs=[],
|
|
396
|
+
kw_defaults=[],
|
|
397
|
+
kwarg=None,
|
|
398
|
+
defaults=[],
|
|
399
|
+
),
|
|
400
|
+
body=from_response_body,
|
|
401
|
+
decorator_list=[_name('classmethod')],
|
|
402
|
+
returns=ast.Constant(value='APIError'),
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
# Build __str__ method
|
|
406
|
+
str_method = ast.FunctionDef(
|
|
407
|
+
name='__str__',
|
|
408
|
+
args=ast.arguments(
|
|
409
|
+
posonlyargs=[],
|
|
410
|
+
args=[_argument('self')],
|
|
411
|
+
kwonlyargs=[],
|
|
412
|
+
kw_defaults=[],
|
|
413
|
+
kwarg=None,
|
|
414
|
+
defaults=[],
|
|
415
|
+
),
|
|
416
|
+
body=[
|
|
417
|
+
ast.Return(
|
|
418
|
+
value=ast.Subscript(
|
|
419
|
+
value=_attr('self', 'args'),
|
|
420
|
+
slice=ast.Constant(value=0),
|
|
421
|
+
ctx=ast.Load(),
|
|
422
|
+
)
|
|
423
|
+
),
|
|
424
|
+
],
|
|
425
|
+
decorator_list=[],
|
|
426
|
+
returns=_name('str'),
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
# Build __repr__ method
|
|
430
|
+
repr_method = ast.FunctionDef(
|
|
431
|
+
name='__repr__',
|
|
432
|
+
args=ast.arguments(
|
|
433
|
+
posonlyargs=[],
|
|
434
|
+
args=[_argument('self')],
|
|
435
|
+
kwonlyargs=[],
|
|
436
|
+
kw_defaults=[],
|
|
437
|
+
kwarg=None,
|
|
438
|
+
defaults=[],
|
|
439
|
+
),
|
|
440
|
+
body=[
|
|
441
|
+
ast.Return(
|
|
442
|
+
value=ast.JoinedStr(
|
|
443
|
+
values=[
|
|
444
|
+
ast.Constant(value='APIError(status_code='),
|
|
445
|
+
ast.FormattedValue(
|
|
446
|
+
value=_attr('self', 'status_code'), conversion=-1
|
|
447
|
+
),
|
|
448
|
+
ast.Constant(value=', detail='),
|
|
449
|
+
ast.FormattedValue(
|
|
450
|
+
value=_attr('self', 'detail'), conversion=114
|
|
451
|
+
), # 114 = 'r' for repr
|
|
452
|
+
ast.Constant(value=')'),
|
|
453
|
+
]
|
|
454
|
+
)
|
|
455
|
+
),
|
|
456
|
+
],
|
|
457
|
+
decorator_list=[],
|
|
458
|
+
returns=_name('str'),
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
# Build class docstring
|
|
462
|
+
docstring = ast.Expr(
|
|
463
|
+
value=ast.Constant(
|
|
464
|
+
value="""Exception raised when an API request fails with an error response.
|
|
465
|
+
|
|
466
|
+
This exception provides detailed error information from the API response,
|
|
467
|
+
including the HTTP status code, error message, and full response body.
|
|
468
|
+
|
|
469
|
+
Attributes:
|
|
470
|
+
status_code: The HTTP status code of the response.
|
|
471
|
+
response: The httpx Response object.
|
|
472
|
+
detail: Parsed error detail from the response body (if available).
|
|
473
|
+
body: Raw response body text.
|
|
474
|
+
"""
|
|
475
|
+
)
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
class_def = ast.ClassDef(
|
|
479
|
+
name='APIError',
|
|
480
|
+
bases=[_name('Exception')],
|
|
481
|
+
keywords=[],
|
|
482
|
+
body=[docstring, init_method, from_response_method, str_method, repr_method],
|
|
483
|
+
decorator_list=[],
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
return class_def
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def generate_base_client_class(
|
|
490
|
+
class_name: str,
|
|
491
|
+
default_base_url: str,
|
|
492
|
+
default_timeout: float = 30.0,
|
|
493
|
+
) -> tuple[ast.ClassDef, ImportDict]:
|
|
494
|
+
"""Generate a BaseClient class with only request infrastructure.
|
|
495
|
+
|
|
496
|
+
This class contains only the HTTP request plumbing (__init__, _request,
|
|
497
|
+
_request_async). Endpoint implementations live in the module files.
|
|
498
|
+
|
|
499
|
+
Args:
|
|
500
|
+
class_name: Name for the generated class (e.g., 'BasePetStoreClient').
|
|
501
|
+
default_base_url: Default base URL from the OpenAPI spec.
|
|
502
|
+
default_timeout: Default request timeout in seconds.
|
|
503
|
+
|
|
504
|
+
Returns:
|
|
505
|
+
Tuple of (class AST node, required imports).
|
|
506
|
+
"""
|
|
507
|
+
imports: ImportDict = {
|
|
508
|
+
'httpx': {'Client', 'AsyncClient', 'Response'},
|
|
509
|
+
'typing': {'Any', 'Type', 'TypeVar'},
|
|
510
|
+
'pydantic': {'TypeAdapter', 'RootModel'},
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
# Build __init__ method
|
|
514
|
+
init_method = _build_init_method(default_base_url, default_timeout)
|
|
515
|
+
|
|
516
|
+
# Build _request method (sync)
|
|
517
|
+
request_method = _build_request_method(is_async=False)
|
|
518
|
+
|
|
519
|
+
# Build _request_async method (async)
|
|
520
|
+
async_request_method = _build_request_method(is_async=True)
|
|
521
|
+
|
|
522
|
+
# Build _request_json method (sync) - request + json parsing
|
|
523
|
+
request_json_method = _build_request_json_method(is_async=False)
|
|
524
|
+
|
|
525
|
+
# Build _request_json_async method (async) - request + json parsing
|
|
526
|
+
async_request_json_method = _build_request_json_method(is_async=True)
|
|
527
|
+
|
|
528
|
+
# Build _parse_response method (sync)
|
|
529
|
+
parse_response_method = _build_parse_response_method(is_async=False)
|
|
530
|
+
|
|
531
|
+
# Build _parse_response_async method (async)
|
|
532
|
+
async_parse_response_method = _build_parse_response_method(is_async=True)
|
|
533
|
+
|
|
534
|
+
# Build class body
|
|
535
|
+
class_body: list[ast.stmt] = [
|
|
536
|
+
ast.Expr(
|
|
537
|
+
value=ast.Constant(
|
|
538
|
+
value=f"""Base HTTP client with request infrastructure.
|
|
539
|
+
|
|
540
|
+
This class is regenerated on each code generation run.
|
|
541
|
+
To customize, subclass this in client.py.
|
|
542
|
+
|
|
543
|
+
Endpoint implementations are in the module files (e.g., pet.py, store.py).
|
|
544
|
+
|
|
545
|
+
Args:
|
|
546
|
+
base_url: Base URL for API requests. Default: {default_base_url}
|
|
547
|
+
timeout: Request timeout in seconds. Default: {default_timeout}
|
|
548
|
+
headers: Default headers to include in all requests.
|
|
549
|
+
http_client: Custom httpx.Client for sync requests.
|
|
550
|
+
async_http_client: Custom httpx.AsyncClient for async requests.
|
|
551
|
+
"""
|
|
552
|
+
)
|
|
553
|
+
),
|
|
554
|
+
init_method,
|
|
555
|
+
request_method,
|
|
556
|
+
async_request_method,
|
|
557
|
+
request_json_method,
|
|
558
|
+
async_request_json_method,
|
|
559
|
+
parse_response_method,
|
|
560
|
+
async_parse_response_method,
|
|
561
|
+
]
|
|
562
|
+
|
|
563
|
+
class_def = ast.ClassDef(
|
|
564
|
+
name=class_name,
|
|
565
|
+
bases=[],
|
|
566
|
+
keywords=[],
|
|
567
|
+
body=class_body,
|
|
568
|
+
decorator_list=[],
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
return class_def, imports
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _build_init_method(
|
|
575
|
+
default_base_url: str, default_timeout: float
|
|
576
|
+
) -> ast.FunctionDef:
|
|
577
|
+
"""Build the __init__ method for the client class."""
|
|
578
|
+
init_body: list[ast.stmt] = [
|
|
579
|
+
# self.base_url = base_url.rstrip('/')
|
|
580
|
+
_assign(
|
|
581
|
+
_attr('self', 'base_url'),
|
|
582
|
+
_call(_attr(_name('base_url'), 'rstrip'), [ast.Constant(value='/')]),
|
|
583
|
+
),
|
|
584
|
+
# self.timeout = timeout
|
|
585
|
+
_assign(
|
|
586
|
+
_attr('self', 'timeout'),
|
|
587
|
+
_name('timeout'),
|
|
588
|
+
),
|
|
589
|
+
# self.headers = headers or {}
|
|
590
|
+
_assign(
|
|
591
|
+
_attr('self', 'headers'),
|
|
592
|
+
ast.BoolOp(
|
|
593
|
+
op=ast.Or(),
|
|
594
|
+
values=[_name('headers'), ast.Dict(keys=[], values=[])],
|
|
595
|
+
),
|
|
596
|
+
),
|
|
597
|
+
# self._client = http_client
|
|
598
|
+
_assign(
|
|
599
|
+
_attr('self', '_client'),
|
|
600
|
+
_name('http_client'),
|
|
601
|
+
),
|
|
602
|
+
# self._async_client = async_http_client
|
|
603
|
+
_assign(
|
|
604
|
+
_attr('self', '_async_client'),
|
|
605
|
+
_name('async_http_client'),
|
|
606
|
+
),
|
|
607
|
+
]
|
|
608
|
+
|
|
609
|
+
init_method = ast.FunctionDef(
|
|
610
|
+
name='__init__',
|
|
611
|
+
args=ast.arguments(
|
|
612
|
+
posonlyargs=[],
|
|
613
|
+
args=[
|
|
614
|
+
_argument('self'),
|
|
615
|
+
_argument('base_url', _name('str')),
|
|
616
|
+
_argument('timeout', _name('float')),
|
|
617
|
+
_argument(
|
|
618
|
+
'headers',
|
|
619
|
+
_union_expr(
|
|
620
|
+
[
|
|
621
|
+
_subscript(
|
|
622
|
+
'dict', ast.Tuple(elts=[_name('str'), _name('str')])
|
|
623
|
+
),
|
|
624
|
+
ast.Constant(value=None),
|
|
625
|
+
]
|
|
626
|
+
),
|
|
627
|
+
),
|
|
628
|
+
_argument(
|
|
629
|
+
'http_client',
|
|
630
|
+
_union_expr([_name('Client'), ast.Constant(value=None)]),
|
|
631
|
+
),
|
|
632
|
+
_argument(
|
|
633
|
+
'async_http_client',
|
|
634
|
+
_union_expr([_name('AsyncClient'), ast.Constant(value=None)]),
|
|
635
|
+
),
|
|
636
|
+
],
|
|
637
|
+
kwonlyargs=[],
|
|
638
|
+
kw_defaults=[],
|
|
639
|
+
kwarg=None,
|
|
640
|
+
defaults=[
|
|
641
|
+
ast.Constant(value=default_base_url),
|
|
642
|
+
ast.Constant(value=default_timeout),
|
|
643
|
+
ast.Constant(value=None),
|
|
644
|
+
ast.Constant(value=None),
|
|
645
|
+
ast.Constant(value=None),
|
|
646
|
+
],
|
|
647
|
+
),
|
|
648
|
+
body=init_body,
|
|
649
|
+
decorator_list=[],
|
|
650
|
+
returns=ast.Constant(value=None),
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
return init_method
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _build_parse_response_method(
|
|
657
|
+
is_async: bool,
|
|
658
|
+
) -> ast.FunctionDef | ast.AsyncFunctionDef:
|
|
659
|
+
"""Build the _parse_response or _parse_response_async method.
|
|
660
|
+
|
|
661
|
+
This method handles JSON parsing and Pydantic validation of responses.
|
|
662
|
+
"""
|
|
663
|
+
method_name = '_parse_response_async' if is_async else '_parse_response'
|
|
664
|
+
|
|
665
|
+
# T = TypeVar('T') is module-level, we reference it here
|
|
666
|
+
args = ast.arguments(
|
|
667
|
+
posonlyargs=[],
|
|
668
|
+
args=[
|
|
669
|
+
_argument('self'),
|
|
670
|
+
_argument('response', _name('Response')),
|
|
671
|
+
_argument('response_type', _subscript('Type', _name('T'))),
|
|
672
|
+
],
|
|
673
|
+
kwonlyargs=[],
|
|
674
|
+
kw_defaults=[],
|
|
675
|
+
kwarg=None,
|
|
676
|
+
defaults=[],
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
# Build the method body:
|
|
680
|
+
# data = response.json()
|
|
681
|
+
# validated = TypeAdapter(response_type).validate_python(data)
|
|
682
|
+
# if isinstance(validated, RootModel):
|
|
683
|
+
# return validated.root
|
|
684
|
+
# return validated
|
|
685
|
+
body: list[ast.stmt] = [
|
|
686
|
+
# data = response.json()
|
|
687
|
+
_assign(
|
|
688
|
+
_name('data'),
|
|
689
|
+
_call(func=_attr('response', 'json')),
|
|
690
|
+
),
|
|
691
|
+
# validated = TypeAdapter(response_type).validate_python(data)
|
|
692
|
+
_assign(
|
|
693
|
+
_name('validated'),
|
|
694
|
+
_call(
|
|
695
|
+
func=_attr(
|
|
696
|
+
_call(
|
|
697
|
+
func=_name('TypeAdapter'),
|
|
698
|
+
args=[_name('response_type')],
|
|
699
|
+
),
|
|
700
|
+
'validate_python',
|
|
701
|
+
),
|
|
702
|
+
args=[_name('data')],
|
|
703
|
+
),
|
|
704
|
+
),
|
|
705
|
+
# if isinstance(validated, RootModel): return validated.root
|
|
706
|
+
ast.If(
|
|
707
|
+
test=_call(
|
|
708
|
+
func=_name('isinstance'),
|
|
709
|
+
args=[_name('validated'), _name('RootModel')],
|
|
710
|
+
),
|
|
711
|
+
body=[ast.Return(value=_attr('validated', 'root'))],
|
|
712
|
+
orelse=[],
|
|
713
|
+
),
|
|
714
|
+
# return validated
|
|
715
|
+
ast.Return(value=_name('validated')),
|
|
716
|
+
]
|
|
717
|
+
|
|
718
|
+
if is_async:
|
|
719
|
+
return ast.AsyncFunctionDef(
|
|
720
|
+
name=method_name,
|
|
721
|
+
args=args,
|
|
722
|
+
body=body,
|
|
723
|
+
decorator_list=[],
|
|
724
|
+
returns=_name('T'),
|
|
725
|
+
)
|
|
726
|
+
else:
|
|
727
|
+
return ast.FunctionDef(
|
|
728
|
+
name=method_name,
|
|
729
|
+
args=args,
|
|
730
|
+
body=body,
|
|
731
|
+
decorator_list=[],
|
|
732
|
+
returns=_name('T'),
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def _build_request_json_method(
|
|
737
|
+
is_async: bool,
|
|
738
|
+
) -> ast.FunctionDef | ast.AsyncFunctionDef:
|
|
739
|
+
"""Build the _request_json or _request_json_async method.
|
|
740
|
+
|
|
741
|
+
This method combines _request and .json() parsing for convenience.
|
|
742
|
+
"""
|
|
743
|
+
method_name = '_request_json_async' if is_async else '_request_json'
|
|
744
|
+
request_method = '_request_async' if is_async else '_request'
|
|
745
|
+
|
|
746
|
+
args = ast.arguments(
|
|
747
|
+
posonlyargs=[],
|
|
748
|
+
args=[
|
|
749
|
+
_argument('self'),
|
|
750
|
+
_argument('method', _name('str')),
|
|
751
|
+
_argument('path', _name('str')),
|
|
752
|
+
],
|
|
753
|
+
kwonlyargs=[
|
|
754
|
+
_argument('params', _union_expr([_name('dict'), ast.Constant(value=None)])),
|
|
755
|
+
_argument(
|
|
756
|
+
'headers', _union_expr([_name('dict'), ast.Constant(value=None)])
|
|
757
|
+
),
|
|
758
|
+
_argument('json', _union_expr([_name('Any'), ast.Constant(value=None)])),
|
|
759
|
+
_argument('data', _union_expr([_name('Any'), ast.Constant(value=None)])),
|
|
760
|
+
_argument('files', _union_expr([_name('Any'), ast.Constant(value=None)])),
|
|
761
|
+
_argument('content', _union_expr([_name('Any'), ast.Constant(value=None)])),
|
|
762
|
+
_argument(
|
|
763
|
+
'timeout', _union_expr([_name('float'), ast.Constant(value=None)])
|
|
764
|
+
),
|
|
765
|
+
],
|
|
766
|
+
kw_defaults=[
|
|
767
|
+
ast.Constant(value=None), # params
|
|
768
|
+
ast.Constant(value=None), # headers
|
|
769
|
+
ast.Constant(value=None), # json
|
|
770
|
+
ast.Constant(value=None), # data
|
|
771
|
+
ast.Constant(value=None), # files
|
|
772
|
+
ast.Constant(value=None), # content
|
|
773
|
+
ast.Constant(value=None), # timeout
|
|
774
|
+
],
|
|
775
|
+
kwarg=None,
|
|
776
|
+
defaults=[],
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
# Build the request call with all parameters
|
|
780
|
+
request_call = _call(
|
|
781
|
+
func=_attr('self', request_method),
|
|
782
|
+
args=[_name('method'), _name('path')],
|
|
783
|
+
keywords=[
|
|
784
|
+
ast.keyword(arg='params', value=_name('params')),
|
|
785
|
+
ast.keyword(arg='headers', value=_name('headers')),
|
|
786
|
+
ast.keyword(arg='json', value=_name('json')),
|
|
787
|
+
ast.keyword(arg='data', value=_name('data')),
|
|
788
|
+
ast.keyword(arg='files', value=_name('files')),
|
|
789
|
+
ast.keyword(arg='content', value=_name('content')),
|
|
790
|
+
ast.keyword(arg='timeout', value=_name('timeout')),
|
|
791
|
+
],
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
if is_async:
|
|
795
|
+
request_call = ast.Await(value=request_call)
|
|
796
|
+
|
|
797
|
+
# response = self._request(...) or await self._request_async(...)
|
|
798
|
+
# return response.json()
|
|
799
|
+
body: list[ast.stmt] = [
|
|
800
|
+
_assign(_name('response'), request_call),
|
|
801
|
+
ast.Return(value=_call(func=_attr('response', 'json'))),
|
|
802
|
+
]
|
|
803
|
+
|
|
804
|
+
if is_async:
|
|
805
|
+
return ast.AsyncFunctionDef(
|
|
806
|
+
name=method_name,
|
|
807
|
+
args=args,
|
|
808
|
+
body=body,
|
|
809
|
+
decorator_list=[],
|
|
810
|
+
returns=_name('Any'),
|
|
811
|
+
)
|
|
812
|
+
else:
|
|
813
|
+
return ast.FunctionDef(
|
|
814
|
+
name=method_name,
|
|
815
|
+
args=args,
|
|
816
|
+
body=body,
|
|
817
|
+
decorator_list=[],
|
|
818
|
+
returns=_name('Any'),
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
def _build_request_method(is_async: bool) -> ast.FunctionDef | ast.AsyncFunctionDef:
|
|
823
|
+
"""Build the internal _request or _request_async method."""
|
|
824
|
+
method_name = '_request_async' if is_async else '_request'
|
|
825
|
+
|
|
826
|
+
args = ast.arguments(
|
|
827
|
+
posonlyargs=[],
|
|
828
|
+
args=[
|
|
829
|
+
_argument('self'),
|
|
830
|
+
_argument('method', _name('str')),
|
|
831
|
+
_argument('path', _name('str')),
|
|
832
|
+
],
|
|
833
|
+
kwonlyargs=[
|
|
834
|
+
_argument('params', _union_expr([_name('dict'), ast.Constant(value=None)])),
|
|
835
|
+
_argument(
|
|
836
|
+
'headers', _union_expr([_name('dict'), ast.Constant(value=None)])
|
|
837
|
+
),
|
|
838
|
+
_argument('json', _union_expr([_name('Any'), ast.Constant(value=None)])),
|
|
839
|
+
_argument('data', _union_expr([_name('Any'), ast.Constant(value=None)])),
|
|
840
|
+
_argument('files', _union_expr([_name('Any'), ast.Constant(value=None)])),
|
|
841
|
+
_argument('content', _union_expr([_name('Any'), ast.Constant(value=None)])),
|
|
842
|
+
_argument(
|
|
843
|
+
'timeout', _union_expr([_name('float'), ast.Constant(value=None)])
|
|
844
|
+
),
|
|
845
|
+
],
|
|
846
|
+
kw_defaults=[
|
|
847
|
+
ast.Constant(value=None), # params
|
|
848
|
+
ast.Constant(value=None), # headers
|
|
849
|
+
ast.Constant(value=None), # json
|
|
850
|
+
ast.Constant(value=None), # data
|
|
851
|
+
ast.Constant(value=None), # files
|
|
852
|
+
ast.Constant(value=None), # content
|
|
853
|
+
ast.Constant(value=None), # timeout
|
|
854
|
+
],
|
|
855
|
+
kwarg=None,
|
|
856
|
+
defaults=[],
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
# Build URL: f"{self.base_url}{path}"
|
|
860
|
+
url_expr = ast.JoinedStr(
|
|
861
|
+
values=[
|
|
862
|
+
ast.FormattedValue(value=_attr('self', 'base_url'), conversion=-1),
|
|
863
|
+
ast.FormattedValue(value=_name('path'), conversion=-1),
|
|
864
|
+
]
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
# merged_headers = {**self.headers, **(headers or {})}
|
|
868
|
+
merged_headers = ast.Dict(
|
|
869
|
+
keys=[None, None],
|
|
870
|
+
values=[
|
|
871
|
+
_attr('self', 'headers'),
|
|
872
|
+
ast.BoolOp(
|
|
873
|
+
op=ast.Or(),
|
|
874
|
+
values=[_name('headers'), ast.Dict(keys=[], values=[])],
|
|
875
|
+
),
|
|
876
|
+
],
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
# actual_timeout = timeout if timeout is not None else self.timeout
|
|
880
|
+
timeout_expr = ast.IfExp(
|
|
881
|
+
test=ast.Compare(
|
|
882
|
+
left=_name('timeout'),
|
|
883
|
+
ops=[ast.IsNot()],
|
|
884
|
+
comparators=[ast.Constant(value=None)],
|
|
885
|
+
),
|
|
886
|
+
body=_name('timeout'),
|
|
887
|
+
orelse=_attr('self', 'timeout'),
|
|
888
|
+
)
|
|
889
|
+
|
|
890
|
+
if is_async:
|
|
891
|
+
body = _build_async_request_body(url_expr, merged_headers, timeout_expr)
|
|
892
|
+
return ast.AsyncFunctionDef(
|
|
893
|
+
name=method_name,
|
|
894
|
+
args=args,
|
|
895
|
+
body=body,
|
|
896
|
+
decorator_list=[],
|
|
897
|
+
returns=_name('Response'),
|
|
898
|
+
)
|
|
899
|
+
else:
|
|
900
|
+
body = _build_sync_request_body(url_expr, merged_headers, timeout_expr)
|
|
901
|
+
return ast.FunctionDef(
|
|
902
|
+
name=method_name,
|
|
903
|
+
args=args,
|
|
904
|
+
body=body,
|
|
905
|
+
decorator_list=[],
|
|
906
|
+
returns=_name('Response'),
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def _build_filtered_params_expr() -> ast.expr:
|
|
911
|
+
"""Build expression to filter None values from params dict.
|
|
912
|
+
|
|
913
|
+
Generates: {k: v for k, v in params.items() if v is not None} if params else None
|
|
914
|
+
"""
|
|
915
|
+
# Build the dict comprehension: {k: v for k, v in params.items() if v is not None}
|
|
916
|
+
dict_comp = ast.DictComp(
|
|
917
|
+
key=_name('k'),
|
|
918
|
+
value=_name('v'),
|
|
919
|
+
generators=[
|
|
920
|
+
ast.comprehension(
|
|
921
|
+
target=ast.Tuple(
|
|
922
|
+
elts=[
|
|
923
|
+
ast.Name(id='k', ctx=ast.Store()),
|
|
924
|
+
ast.Name(id='v', ctx=ast.Store()),
|
|
925
|
+
],
|
|
926
|
+
ctx=ast.Store(),
|
|
927
|
+
),
|
|
928
|
+
iter=_call(_attr('params', 'items')),
|
|
929
|
+
ifs=[
|
|
930
|
+
ast.Compare(
|
|
931
|
+
left=_name('v'),
|
|
932
|
+
ops=[ast.IsNot()],
|
|
933
|
+
comparators=[ast.Constant(value=None)],
|
|
934
|
+
)
|
|
935
|
+
],
|
|
936
|
+
is_async=0,
|
|
937
|
+
)
|
|
938
|
+
],
|
|
939
|
+
)
|
|
940
|
+
|
|
941
|
+
# Build the conditional: dict_comp if params else None
|
|
942
|
+
return ast.IfExp(
|
|
943
|
+
test=_name('params'),
|
|
944
|
+
body=dict_comp,
|
|
945
|
+
orelse=ast.Constant(value=None),
|
|
946
|
+
)
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
def _build_sync_request_body(
|
|
950
|
+
url_expr: ast.expr, merged_headers: ast.expr, timeout_expr: ast.expr
|
|
951
|
+
) -> list[ast.stmt]:
|
|
952
|
+
"""Build the body for sync _request method."""
|
|
953
|
+
# Build filtered_params assignment
|
|
954
|
+
filtered_params_stmt = _assign(
|
|
955
|
+
_name('filtered_params'),
|
|
956
|
+
_build_filtered_params_expr(),
|
|
957
|
+
)
|
|
958
|
+
|
|
959
|
+
request_call = _call(
|
|
960
|
+
_attr('client', 'request'),
|
|
961
|
+
args=[_name('method'), url_expr],
|
|
962
|
+
keywords=[
|
|
963
|
+
ast.keyword(arg='params', value=_name('filtered_params')),
|
|
964
|
+
ast.keyword(arg='headers', value=merged_headers),
|
|
965
|
+
ast.keyword(arg='json', value=_name('json')),
|
|
966
|
+
ast.keyword(arg='data', value=_name('data')),
|
|
967
|
+
ast.keyword(arg='files', value=_name('files')),
|
|
968
|
+
ast.keyword(arg='content', value=_name('content')),
|
|
969
|
+
ast.keyword(arg='timeout', value=timeout_expr),
|
|
970
|
+
],
|
|
971
|
+
)
|
|
972
|
+
|
|
973
|
+
return [
|
|
974
|
+
filtered_params_stmt,
|
|
975
|
+
ast.If(
|
|
976
|
+
test=_attr('self', '_client'),
|
|
977
|
+
body=[
|
|
978
|
+
_assign(
|
|
979
|
+
_name('response'),
|
|
980
|
+
_call(
|
|
981
|
+
_attr(_attr('self', '_client'), 'request'),
|
|
982
|
+
args=[_name('method'), url_expr],
|
|
983
|
+
keywords=[
|
|
984
|
+
ast.keyword(arg='params', value=_name('filtered_params')),
|
|
985
|
+
ast.keyword(arg='headers', value=merged_headers),
|
|
986
|
+
ast.keyword(arg='json', value=_name('json')),
|
|
987
|
+
ast.keyword(arg='data', value=_name('data')),
|
|
988
|
+
ast.keyword(arg='files', value=_name('files')),
|
|
989
|
+
ast.keyword(arg='content', value=_name('content')),
|
|
990
|
+
ast.keyword(arg='timeout', value=timeout_expr),
|
|
991
|
+
],
|
|
992
|
+
),
|
|
993
|
+
),
|
|
994
|
+
],
|
|
995
|
+
orelse=[
|
|
996
|
+
ast.With(
|
|
997
|
+
items=[
|
|
998
|
+
ast.withitem(
|
|
999
|
+
context_expr=_call(_name('Client')),
|
|
1000
|
+
optional_vars=_name('client'),
|
|
1001
|
+
)
|
|
1002
|
+
],
|
|
1003
|
+
body=[
|
|
1004
|
+
_assign(_name('response'), request_call),
|
|
1005
|
+
],
|
|
1006
|
+
),
|
|
1007
|
+
],
|
|
1008
|
+
),
|
|
1009
|
+
# if response.is_error: raise APIError.from_response(response)
|
|
1010
|
+
ast.If(
|
|
1011
|
+
test=_attr('response', 'is_error'),
|
|
1012
|
+
body=[
|
|
1013
|
+
ast.Raise(
|
|
1014
|
+
exc=_call(
|
|
1015
|
+
_attr(_name('APIError'), 'from_response'),
|
|
1016
|
+
args=[_name('response')],
|
|
1017
|
+
)
|
|
1018
|
+
),
|
|
1019
|
+
],
|
|
1020
|
+
orelse=[],
|
|
1021
|
+
),
|
|
1022
|
+
ast.Return(value=_name('response')),
|
|
1023
|
+
]
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
def _build_async_request_body(
|
|
1027
|
+
url_expr: ast.expr, merged_headers: ast.expr, timeout_expr: ast.expr
|
|
1028
|
+
) -> list[ast.stmt]:
|
|
1029
|
+
"""Build the body for async _request_async method."""
|
|
1030
|
+
# Build filtered_params assignment
|
|
1031
|
+
filtered_params_stmt = _assign(
|
|
1032
|
+
_name('filtered_params'),
|
|
1033
|
+
_build_filtered_params_expr(),
|
|
1034
|
+
)
|
|
1035
|
+
|
|
1036
|
+
request_call = ast.Await(
|
|
1037
|
+
value=_call(
|
|
1038
|
+
_attr('client', 'request'),
|
|
1039
|
+
args=[_name('method'), url_expr],
|
|
1040
|
+
keywords=[
|
|
1041
|
+
ast.keyword(arg='params', value=_name('filtered_params')),
|
|
1042
|
+
ast.keyword(arg='headers', value=merged_headers),
|
|
1043
|
+
ast.keyword(arg='json', value=_name('json')),
|
|
1044
|
+
ast.keyword(arg='data', value=_name('data')),
|
|
1045
|
+
ast.keyword(arg='files', value=_name('files')),
|
|
1046
|
+
ast.keyword(arg='content', value=_name('content')),
|
|
1047
|
+
ast.keyword(arg='timeout', value=timeout_expr),
|
|
1048
|
+
],
|
|
1049
|
+
)
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
return [
|
|
1053
|
+
filtered_params_stmt,
|
|
1054
|
+
ast.If(
|
|
1055
|
+
test=_attr('self', '_async_client'),
|
|
1056
|
+
body=[
|
|
1057
|
+
_assign(
|
|
1058
|
+
_name('response'),
|
|
1059
|
+
ast.Await(
|
|
1060
|
+
value=_call(
|
|
1061
|
+
_attr(_attr('self', '_async_client'), 'request'),
|
|
1062
|
+
args=[_name('method'), url_expr],
|
|
1063
|
+
keywords=[
|
|
1064
|
+
ast.keyword(
|
|
1065
|
+
arg='params', value=_name('filtered_params')
|
|
1066
|
+
),
|
|
1067
|
+
ast.keyword(arg='headers', value=merged_headers),
|
|
1068
|
+
ast.keyword(arg='json', value=_name('json')),
|
|
1069
|
+
ast.keyword(arg='data', value=_name('data')),
|
|
1070
|
+
ast.keyword(arg='files', value=_name('files')),
|
|
1071
|
+
ast.keyword(arg='content', value=_name('content')),
|
|
1072
|
+
ast.keyword(arg='timeout', value=timeout_expr),
|
|
1073
|
+
],
|
|
1074
|
+
)
|
|
1075
|
+
),
|
|
1076
|
+
),
|
|
1077
|
+
],
|
|
1078
|
+
orelse=[
|
|
1079
|
+
ast.AsyncWith(
|
|
1080
|
+
items=[
|
|
1081
|
+
ast.withitem(
|
|
1082
|
+
context_expr=_call(_name('AsyncClient')),
|
|
1083
|
+
optional_vars=_name('client'),
|
|
1084
|
+
)
|
|
1085
|
+
],
|
|
1086
|
+
body=[
|
|
1087
|
+
_assign(_name('response'), request_call),
|
|
1088
|
+
],
|
|
1089
|
+
),
|
|
1090
|
+
],
|
|
1091
|
+
),
|
|
1092
|
+
# if response.is_error: raise APIError.from_response(response)
|
|
1093
|
+
ast.If(
|
|
1094
|
+
test=_attr('response', 'is_error'),
|
|
1095
|
+
body=[
|
|
1096
|
+
ast.Raise(
|
|
1097
|
+
exc=_call(
|
|
1098
|
+
_attr(_name('APIError'), 'from_response'),
|
|
1099
|
+
args=[_name('response')],
|
|
1100
|
+
)
|
|
1101
|
+
),
|
|
1102
|
+
],
|
|
1103
|
+
orelse=[],
|
|
1104
|
+
),
|
|
1105
|
+
ast.Return(value=_name('response')),
|
|
1106
|
+
]
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
def _build_path_expr(path: str, parameters: list['Parameter'] | None) -> ast.expr:
|
|
1110
|
+
"""Build the path expression with parameter substitution.
|
|
1111
|
+
|
|
1112
|
+
Note:
|
|
1113
|
+
This function delegates to ParameterASTBuilder.build_path_expr().
|
|
1114
|
+
"""
|
|
1115
|
+
return ParameterASTBuilder.build_path_expr(path, parameters or [])
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
def _build_query_params(parameters: list['Parameter'] | None) -> ast.Dict | None:
|
|
1119
|
+
"""Build query parameters dict.
|
|
1120
|
+
|
|
1121
|
+
Note:
|
|
1122
|
+
This function delegates to ParameterASTBuilder.build_query_params().
|
|
1123
|
+
"""
|
|
1124
|
+
if not parameters:
|
|
1125
|
+
return None
|
|
1126
|
+
return ParameterASTBuilder.build_query_params(parameters)
|
|
1127
|
+
|
|
1128
|
+
|
|
1129
|
+
def _build_header_params(parameters: list['Parameter'] | None) -> ast.Dict | None:
|
|
1130
|
+
"""Build header parameters dict.
|
|
1131
|
+
|
|
1132
|
+
Note:
|
|
1133
|
+
This function delegates to ParameterASTBuilder.build_header_params().
|
|
1134
|
+
"""
|
|
1135
|
+
if not parameters:
|
|
1136
|
+
return None
|
|
1137
|
+
return ParameterASTBuilder.build_header_params(parameters)
|
|
1138
|
+
|
|
1139
|
+
|
|
1140
|
+
def _build_body_expr(
|
|
1141
|
+
request_body: 'RequestBodyInfo',
|
|
1142
|
+
) -> tuple[ast.expr | None, str | None]:
|
|
1143
|
+
"""Build the body expression for the request.
|
|
1144
|
+
|
|
1145
|
+
Note:
|
|
1146
|
+
This function delegates to ParameterASTBuilder.build_body_expr().
|
|
1147
|
+
"""
|
|
1148
|
+
return ParameterASTBuilder.build_body_expr(request_body)
|
|
1149
|
+
|
|
1150
|
+
|
|
1151
|
+
def _build_response_processing(
|
|
1152
|
+
response_type: 'Type', response_infos: list['ResponseInfo'] | None
|
|
1153
|
+
) -> list[ast.stmt]:
|
|
1154
|
+
"""Build statements for processing the response."""
|
|
1155
|
+
stmts = []
|
|
1156
|
+
|
|
1157
|
+
# data = response.json()
|
|
1158
|
+
stmts.append(_assign(_name('data'), _call(_attr('response', 'json'))))
|
|
1159
|
+
|
|
1160
|
+
# validated = TypeAdapter(response_type).validate_python(data)
|
|
1161
|
+
stmts.append(
|
|
1162
|
+
_assign(
|
|
1163
|
+
_name('validated'),
|
|
1164
|
+
_call(
|
|
1165
|
+
_attr(
|
|
1166
|
+
_call(_name('TypeAdapter'), [response_type.annotation_ast]),
|
|
1167
|
+
'validate_python',
|
|
1168
|
+
),
|
|
1169
|
+
[_name('data')],
|
|
1170
|
+
),
|
|
1171
|
+
)
|
|
1172
|
+
)
|
|
1173
|
+
|
|
1174
|
+
# if isinstance(validated, RootModel): return validated.root
|
|
1175
|
+
stmts.append(
|
|
1176
|
+
ast.If(
|
|
1177
|
+
test=_call(_name('isinstance'), [_name('validated'), _name('RootModel')]),
|
|
1178
|
+
body=[ast.Return(value=_attr('validated', 'root'))],
|
|
1179
|
+
orelse=[],
|
|
1180
|
+
)
|
|
1181
|
+
)
|
|
1182
|
+
|
|
1183
|
+
# return validated
|
|
1184
|
+
stmts.append(ast.Return(value=_name('validated')))
|
|
1185
|
+
|
|
1186
|
+
return stmts
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
def _merge_imports(target: ImportDict, source: ImportDict) -> None:
|
|
1190
|
+
"""Merge source imports into target imports."""
|
|
1191
|
+
for module, names in source.items():
|
|
1192
|
+
if module not in target:
|
|
1193
|
+
target[module] = set()
|
|
1194
|
+
target[module].update(names)
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
def generate_client_stub(
|
|
1198
|
+
class_name: str,
|
|
1199
|
+
base_class_name: str,
|
|
1200
|
+
module_name: str = '_client',
|
|
1201
|
+
) -> str:
|
|
1202
|
+
"""Generate the user-customizable client.py stub content.
|
|
1203
|
+
|
|
1204
|
+
Args:
|
|
1205
|
+
class_name: Name for the client class (e.g., 'PetStoreClient').
|
|
1206
|
+
base_class_name: Name of the base class to inherit from.
|
|
1207
|
+
module_name: Module name where base class is defined.
|
|
1208
|
+
|
|
1209
|
+
Returns:
|
|
1210
|
+
String content for client.py file.
|
|
1211
|
+
"""
|
|
1212
|
+
return f'''"""API Client.
|
|
1213
|
+
|
|
1214
|
+
This file is generated once and will NOT be overwritten on regeneration.
|
|
1215
|
+
You can safely customize this file to add authentication, logging,
|
|
1216
|
+
error handling, or other client-specific functionality.
|
|
1217
|
+
"""
|
|
1218
|
+
|
|
1219
|
+
from .{module_name} import {base_class_name}
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
class {class_name}({base_class_name}):
|
|
1223
|
+
"""API client with customizable configuration.
|
|
1224
|
+
|
|
1225
|
+
This class inherits from the generated {base_class_name} and can be
|
|
1226
|
+
customized without being overwritten on code regeneration.
|
|
1227
|
+
|
|
1228
|
+
Example:
|
|
1229
|
+
>>> client = {class_name}()
|
|
1230
|
+
>>> # Use default base URL from OpenAPI spec
|
|
1231
|
+
|
|
1232
|
+
>>> client = {class_name}(base_url="https://staging.api.example.com")
|
|
1233
|
+
>>> # Override base URL
|
|
1234
|
+
|
|
1235
|
+
>>> client = {class_name}(timeout=60.0, headers={{"Authorization": "Bearer token"}})
|
|
1236
|
+
>>> # Custom timeout and headers
|
|
1237
|
+
|
|
1238
|
+
>>> import httpx
|
|
1239
|
+
>>> with httpx.Client() as http_client:
|
|
1240
|
+
... client = {class_name}(http_client=http_client)
|
|
1241
|
+
... # Use custom HTTP client (useful for testing/mocking)
|
|
1242
|
+
"""
|
|
1243
|
+
|
|
1244
|
+
pass
|
|
1245
|
+
|
|
1246
|
+
# Add custom methods or override base class methods below.
|
|
1247
|
+
#
|
|
1248
|
+
# Example - adding authentication:
|
|
1249
|
+
#
|
|
1250
|
+
# def __init__(self, api_key: str | None = None, **kwargs):
|
|
1251
|
+
# super().__init__(**kwargs)
|
|
1252
|
+
# if api_key:
|
|
1253
|
+
# self.headers["Authorization"] = f"Bearer {{api_key}}"
|
|
1254
|
+
#
|
|
1255
|
+
# Example - overriding a method:
|
|
1256
|
+
#
|
|
1257
|
+
# def get_pet_by_id(self, pet_id: int, **kwargs):
|
|
1258
|
+
# \"\"\"Get pet with custom error handling.\"\"\"
|
|
1259
|
+
# try:
|
|
1260
|
+
# return super().get_pet_by_id(pet_id, **kwargs)
|
|
1261
|
+
# except httpx.HTTPStatusError as e:
|
|
1262
|
+
# if e.response.status_code == 404:
|
|
1263
|
+
# return None
|
|
1264
|
+
# raise
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
# Convenience alias for shorter imports
|
|
1268
|
+
Client = {class_name}
|
|
1269
|
+
|
|
1270
|
+
__all__ = ["{class_name}", "Client"]
|
|
1271
|
+
'''
|