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
otterapi/codegen/endpoints.py
CHANGED
|
@@ -1,25 +1,1708 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Endpoint generation module for creating sync and async HTTP request functions.
|
|
3
|
+
|
|
4
|
+
This module provides utilities for generating Python AST nodes that represent
|
|
5
|
+
HTTP endpoint functions. It supports both synchronous and asynchronous request
|
|
6
|
+
patterns with automatic parameter handling, response validation, and type hints.
|
|
7
|
+
|
|
8
|
+
Key Features:
|
|
9
|
+
- Generates sync and async base request functions with response validation
|
|
10
|
+
- Creates endpoint-specific functions with proper parameter handling
|
|
11
|
+
- Supports query, path, header, and body parameters
|
|
12
|
+
- Automatic TypeAdapter validation and RootModel unwrapping
|
|
13
|
+
- Configurable BASE_URL handling
|
|
14
|
+
- File upload support (multipart/form-data)
|
|
15
|
+
- Form data support (application/x-www-form-urlencoded)
|
|
16
|
+
- Binary response handling with streaming
|
|
17
|
+
- Request timeout configuration
|
|
18
|
+
- Unified EndpointFunctionFactory for consistent endpoint generation
|
|
19
|
+
"""
|
|
20
|
+
|
|
1
21
|
import ast
|
|
22
|
+
import re
|
|
2
23
|
import textwrap
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from enum import Enum
|
|
26
|
+
from typing import TYPE_CHECKING, Literal, Self
|
|
27
|
+
|
|
28
|
+
from otterapi.codegen.ast_utils import (
|
|
29
|
+
_argument,
|
|
30
|
+
_assign,
|
|
31
|
+
_async_func,
|
|
32
|
+
_attr,
|
|
33
|
+
_call,
|
|
34
|
+
_func,
|
|
35
|
+
_name,
|
|
36
|
+
_subscript,
|
|
37
|
+
_union_expr,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from otterapi.codegen.types import Parameter, RequestBodyInfo, ResponseInfo, Type
|
|
42
|
+
|
|
43
|
+
# Type alias for import dictionaries used throughout this module
|
|
44
|
+
ImportDict = dict[str, set[str]]
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
# Enums and dataclasses
|
|
48
|
+
'EndpointMode',
|
|
49
|
+
'DataFrameLibrary',
|
|
50
|
+
'PaginationStyle',
|
|
51
|
+
'EndpointFunctionConfig',
|
|
52
|
+
'FunctionSignature',
|
|
53
|
+
# Factory class
|
|
54
|
+
'EndpointFunctionFactory',
|
|
55
|
+
# Builder classes
|
|
56
|
+
'ParameterASTBuilder',
|
|
57
|
+
'FunctionSignatureBuilder',
|
|
58
|
+
# Base request functions
|
|
59
|
+
'base_request_fn',
|
|
60
|
+
'base_async_request_fn',
|
|
61
|
+
'request_fn',
|
|
62
|
+
'async_request_fn',
|
|
63
|
+
# Helper functions
|
|
64
|
+
'clean_docstring',
|
|
65
|
+
'get_parameters',
|
|
66
|
+
'get_base_call_keywords',
|
|
67
|
+
'build_header_params',
|
|
68
|
+
'build_query_params',
|
|
69
|
+
'build_path_params',
|
|
70
|
+
'build_body_params',
|
|
71
|
+
'prepare_call_from_parameters',
|
|
72
|
+
'build_default_client_code',
|
|
73
|
+
# Convenience functions
|
|
74
|
+
'build_standalone_endpoint_fn',
|
|
75
|
+
'build_delegating_endpoint_fn',
|
|
76
|
+
'build_standalone_dataframe_fn',
|
|
77
|
+
'build_delegating_dataframe_fn',
|
|
78
|
+
'build_standalone_paginated_fn',
|
|
79
|
+
'build_standalone_paginated_iter_fn',
|
|
80
|
+
'build_standalone_paginated_dataframe_fn',
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# =============================================================================
|
|
85
|
+
# Enums and Configuration
|
|
86
|
+
# =============================================================================
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class EndpointMode(Enum):
|
|
90
|
+
"""Mode of endpoint function generation."""
|
|
91
|
+
|
|
92
|
+
STANDALONE = 'standalone'
|
|
93
|
+
"""Standalone function that uses a Client instance internally."""
|
|
94
|
+
|
|
95
|
+
DELEGATING = 'delegating'
|
|
96
|
+
"""Delegating function that calls _get_client().method_name()."""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class DataFrameLibrary(Enum):
|
|
100
|
+
"""DataFrame library for DataFrame endpoint functions."""
|
|
101
|
+
|
|
102
|
+
PANDAS = 'pandas'
|
|
103
|
+
POLARS = 'polars'
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class PaginationStyle(Enum):
|
|
107
|
+
"""Pagination style for paginated endpoint functions."""
|
|
108
|
+
|
|
109
|
+
OFFSET = 'offset'
|
|
110
|
+
CURSOR = 'cursor'
|
|
111
|
+
PAGE = 'page'
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def clean_docstring(docstring: str) -> str:
|
|
115
|
+
"""Clean and normalize a docstring by removing excess indentation.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
docstring: The raw docstring to clean.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
A cleaned docstring with normalized indentation.
|
|
122
|
+
"""
|
|
123
|
+
return textwrap.dedent(f'\n{docstring}\n').strip()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# =============================================================================
|
|
127
|
+
# Function Signature Building
|
|
128
|
+
# =============================================================================
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass
|
|
132
|
+
class FunctionSignature:
|
|
133
|
+
"""Represents a function signature with args, kwargs, and annotations.
|
|
134
|
+
|
|
135
|
+
This dataclass holds all the components needed to build the arguments
|
|
136
|
+
portion of a function definition AST node.
|
|
137
|
+
|
|
138
|
+
Attributes:
|
|
139
|
+
args: Required positional arguments.
|
|
140
|
+
kwonlyargs: Optional keyword-only arguments.
|
|
141
|
+
kw_defaults: Default values for keyword-only arguments.
|
|
142
|
+
imports: Required imports for type annotations.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
args: list[ast.arg] = field(default_factory=list)
|
|
146
|
+
kwonlyargs: list[ast.arg] = field(default_factory=list)
|
|
147
|
+
kw_defaults: list[ast.expr] = field(default_factory=list)
|
|
148
|
+
imports: ImportDict = field(default_factory=dict)
|
|
149
|
+
|
|
150
|
+
def to_ast_arguments(
|
|
151
|
+
self,
|
|
152
|
+
kwargs: ast.arg | None = None,
|
|
153
|
+
) -> ast.arguments:
|
|
154
|
+
"""Convert to an ast.arguments node.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
kwargs: Optional **kwargs argument to include.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
An ast.arguments node ready for use in a FunctionDef.
|
|
161
|
+
"""
|
|
162
|
+
return ast.arguments(
|
|
163
|
+
posonlyargs=[],
|
|
164
|
+
args=self.args,
|
|
165
|
+
kwonlyargs=self.kwonlyargs,
|
|
166
|
+
kw_defaults=self.kw_defaults,
|
|
167
|
+
kwarg=kwargs,
|
|
168
|
+
defaults=[],
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class FunctionSignatureBuilder:
|
|
173
|
+
"""Builder for function signatures from endpoint parameters.
|
|
174
|
+
|
|
175
|
+
This builder provides a fluent interface for constructing function
|
|
176
|
+
signatures, handling the common patterns of:
|
|
177
|
+
- Converting parameters to function arguments
|
|
178
|
+
- Separating required and optional parameters
|
|
179
|
+
- Adding body parameters
|
|
180
|
+
- Adding optional client/path parameters
|
|
181
|
+
- Collecting type annotation imports
|
|
182
|
+
|
|
183
|
+
Example:
|
|
184
|
+
>>> builder = FunctionSignatureBuilder()
|
|
185
|
+
>>> signature = (
|
|
186
|
+
... builder
|
|
187
|
+
... .add_parameters(endpoint.parameters)
|
|
188
|
+
... .add_request_body(endpoint.request_body)
|
|
189
|
+
... .add_client_parameter()
|
|
190
|
+
... .build()
|
|
191
|
+
... )
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
def __init__(self):
|
|
195
|
+
"""Initialize an empty signature builder."""
|
|
196
|
+
self._args: list[ast.arg] = []
|
|
197
|
+
self._kwonlyargs: list[ast.arg] = []
|
|
198
|
+
self._kw_defaults: list[ast.expr] = []
|
|
199
|
+
self._imports: ImportDict = {}
|
|
200
|
+
|
|
201
|
+
def _add_import(self, module: str, name: str) -> None:
|
|
202
|
+
"""Add a single import to the collection."""
|
|
203
|
+
if module not in self._imports:
|
|
204
|
+
self._imports[module] = set()
|
|
205
|
+
self._imports[module].add(name)
|
|
206
|
+
|
|
207
|
+
def _merge_imports(self, imports: ImportDict) -> None:
|
|
208
|
+
"""Merge imports from another ImportDict."""
|
|
209
|
+
for module, names in imports.items():
|
|
210
|
+
if module not in self._imports:
|
|
211
|
+
self._imports[module] = set()
|
|
212
|
+
self._imports[module].update(names)
|
|
213
|
+
|
|
214
|
+
def add_parameters(self, parameters: list['Parameter'] | None) -> Self:
|
|
215
|
+
"""Add endpoint parameters to the signature.
|
|
216
|
+
|
|
217
|
+
Separates required parameters into positional args and optional
|
|
218
|
+
parameters into keyword-only args with None defaults.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
parameters: List of Parameter objects, or None.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Self for method chaining.
|
|
225
|
+
"""
|
|
226
|
+
if not parameters:
|
|
227
|
+
return self
|
|
228
|
+
|
|
229
|
+
for param in parameters:
|
|
230
|
+
if param.type:
|
|
231
|
+
annotation = param.type.annotation_ast
|
|
232
|
+
self._merge_imports(param.type.annotation_imports)
|
|
233
|
+
else:
|
|
234
|
+
annotation = _name('Any')
|
|
235
|
+
self._add_import('typing', 'Any')
|
|
236
|
+
|
|
237
|
+
arg = _argument(param.name_sanitized, annotation)
|
|
238
|
+
|
|
239
|
+
if param.required:
|
|
240
|
+
self._args.append(arg)
|
|
241
|
+
else:
|
|
242
|
+
self._kwonlyargs.append(arg)
|
|
243
|
+
self._kw_defaults.append(ast.Constant(value=None))
|
|
244
|
+
|
|
245
|
+
return self
|
|
246
|
+
|
|
247
|
+
def add_request_body(
|
|
248
|
+
self,
|
|
249
|
+
body: 'RequestBodyInfo | None',
|
|
250
|
+
) -> Self:
|
|
251
|
+
"""Add request body parameter to the signature.
|
|
252
|
+
|
|
253
|
+
Adds a 'body' parameter with the appropriate type annotation.
|
|
254
|
+
Required bodies become positional args, optional become keyword-only.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
body: The RequestBodyInfo object, or None.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Self for method chaining.
|
|
261
|
+
"""
|
|
262
|
+
if not body:
|
|
263
|
+
return self
|
|
264
|
+
|
|
265
|
+
if body.type:
|
|
266
|
+
body_annotation = body.type.annotation_ast
|
|
267
|
+
self._merge_imports(body.type.annotation_imports)
|
|
268
|
+
else:
|
|
269
|
+
body_annotation = _name('Any')
|
|
270
|
+
self._add_import('typing', 'Any')
|
|
271
|
+
|
|
272
|
+
body_arg = _argument('body', body_annotation)
|
|
273
|
+
|
|
274
|
+
if body.required:
|
|
275
|
+
self._args.append(body_arg)
|
|
276
|
+
else:
|
|
277
|
+
self._kwonlyargs.append(body_arg)
|
|
278
|
+
self._kw_defaults.append(ast.Constant(value=None))
|
|
279
|
+
|
|
280
|
+
return self
|
|
281
|
+
|
|
282
|
+
def add_client_parameter(self, client_type: str = 'Client') -> Self:
|
|
283
|
+
"""Add an optional client parameter to the signature.
|
|
284
|
+
|
|
285
|
+
Adds a keyword-only 'client' parameter with type `Client | None`
|
|
286
|
+
and default value None.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
client_type: The name of the client class (default: 'Client').
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Self for method chaining.
|
|
293
|
+
"""
|
|
294
|
+
self._kwonlyargs.append(
|
|
295
|
+
_argument(
|
|
296
|
+
'client',
|
|
297
|
+
_union_expr([_name(client_type), ast.Constant(value=None)]),
|
|
298
|
+
)
|
|
299
|
+
)
|
|
300
|
+
self._kw_defaults.append(ast.Constant(value=None))
|
|
301
|
+
|
|
302
|
+
return self
|
|
303
|
+
|
|
304
|
+
def add_path_parameter(self, default: str | None = None) -> Self:
|
|
305
|
+
"""Add an optional path parameter for DataFrame methods.
|
|
306
|
+
|
|
307
|
+
Adds a keyword-only 'path' parameter with type `str | None`
|
|
308
|
+
and the specified default value.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
default: Default value for the path parameter.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Self for method chaining.
|
|
315
|
+
"""
|
|
316
|
+
self._kwonlyargs.append(
|
|
317
|
+
_argument(
|
|
318
|
+
'path',
|
|
319
|
+
_union_expr([_name('str'), ast.Constant(value=None)]),
|
|
320
|
+
)
|
|
321
|
+
)
|
|
322
|
+
self._kw_defaults.append(ast.Constant(value=default))
|
|
323
|
+
|
|
324
|
+
return self
|
|
325
|
+
|
|
326
|
+
def add_self_parameter(self) -> Self:
|
|
327
|
+
"""Add 'self' as the first positional argument.
|
|
328
|
+
|
|
329
|
+
Used when building method signatures for classes.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
Self for method chaining.
|
|
333
|
+
"""
|
|
334
|
+
self._args.insert(0, _argument('self'))
|
|
335
|
+
return self
|
|
336
|
+
|
|
337
|
+
def add_custom_kwarg(
|
|
338
|
+
self,
|
|
339
|
+
name: str,
|
|
340
|
+
annotation: ast.expr,
|
|
341
|
+
default: ast.expr,
|
|
342
|
+
imports: ImportDict | None = None,
|
|
343
|
+
) -> Self:
|
|
344
|
+
"""Add a custom keyword-only argument.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
name: The argument name.
|
|
348
|
+
annotation: The type annotation AST node.
|
|
349
|
+
default: The default value AST node.
|
|
350
|
+
imports: Optional imports required for the annotation.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
Self for method chaining.
|
|
354
|
+
"""
|
|
355
|
+
self._kwonlyargs.append(_argument(name, annotation))
|
|
356
|
+
self._kw_defaults.append(default)
|
|
357
|
+
|
|
358
|
+
if imports:
|
|
359
|
+
self._merge_imports(imports)
|
|
360
|
+
|
|
361
|
+
return self
|
|
362
|
+
|
|
363
|
+
def build(self) -> FunctionSignature:
|
|
364
|
+
"""Build and return the function signature.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
A FunctionSignature containing all accumulated arguments and imports.
|
|
368
|
+
"""
|
|
369
|
+
return FunctionSignature(
|
|
370
|
+
args=self._args.copy(),
|
|
371
|
+
kwonlyargs=self._kwonlyargs.copy(),
|
|
372
|
+
kw_defaults=self._kw_defaults.copy(),
|
|
373
|
+
imports=self._imports.copy(),
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
def reset(self) -> Self:
|
|
377
|
+
"""Reset the builder to empty state.
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
Self for method chaining.
|
|
381
|
+
"""
|
|
382
|
+
self._args = []
|
|
383
|
+
self._kwonlyargs = []
|
|
384
|
+
self._kw_defaults = []
|
|
385
|
+
self._imports = {}
|
|
386
|
+
return self
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
# =============================================================================
|
|
390
|
+
# Parameter AST Building
|
|
391
|
+
# =============================================================================
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class ParameterASTBuilder:
|
|
395
|
+
"""Unified builder for parameter-related AST nodes.
|
|
396
|
+
|
|
397
|
+
This class provides static methods for building AST nodes that represent
|
|
398
|
+
various types of request parameters.
|
|
399
|
+
|
|
400
|
+
Example:
|
|
401
|
+
>>> query_dict = ParameterASTBuilder.build_query_params(parameters)
|
|
402
|
+
>>> path_expr = ParameterASTBuilder.build_path_expr('/pet/{petId}', parameters)
|
|
403
|
+
"""
|
|
404
|
+
|
|
405
|
+
@staticmethod
|
|
406
|
+
def build_query_params(parameters: list['Parameter']) -> ast.Dict | None:
|
|
407
|
+
"""Build a dictionary AST node for query parameters.
|
|
408
|
+
|
|
409
|
+
Creates an AST Dict node mapping query parameter names to their values.
|
|
410
|
+
Only includes parameters where location == 'query'.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
parameters: List of Parameter objects to filter for query params.
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
An AST Dict node for query parameters, or None if no query params.
|
|
417
|
+
"""
|
|
418
|
+
query_params = [p for p in parameters if p.location == 'query']
|
|
419
|
+
if not query_params:
|
|
420
|
+
return None
|
|
421
|
+
|
|
422
|
+
return ast.Dict(
|
|
423
|
+
keys=[ast.Constant(value=param.name) for param in query_params],
|
|
424
|
+
values=[_name(param.name_sanitized) for param in query_params],
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
@staticmethod
|
|
428
|
+
def build_header_params(parameters: list['Parameter']) -> ast.Dict | None:
|
|
429
|
+
"""Build a dictionary AST node for header parameters.
|
|
430
|
+
|
|
431
|
+
Creates an AST Dict node mapping header parameter names to their values.
|
|
432
|
+
Only includes parameters where location == 'header'.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
parameters: List of Parameter objects to filter for headers.
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
An AST Dict node for header parameters, or None if no header params.
|
|
439
|
+
"""
|
|
440
|
+
header_params = [p for p in parameters if p.location == 'header']
|
|
441
|
+
if not header_params:
|
|
442
|
+
return None
|
|
443
|
+
|
|
444
|
+
return ast.Dict(
|
|
445
|
+
keys=[ast.Constant(value=param.name) for param in header_params],
|
|
446
|
+
values=[_name(param.name_sanitized) for param in header_params],
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
@staticmethod
|
|
450
|
+
def build_path_expr(path: str, parameters: list['Parameter']) -> ast.expr:
|
|
451
|
+
"""Build an f-string or constant for the request path.
|
|
452
|
+
|
|
453
|
+
Replaces OpenAPI path parameters like {petId} with Python f-string
|
|
454
|
+
expressions using the sanitized parameter names.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
path: The OpenAPI path string with placeholders (e.g., '/pet/{petId}').
|
|
458
|
+
parameters: List of Parameter objects to map placeholders to variables.
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
An AST expression:
|
|
462
|
+
- ast.Constant for static paths without parameters
|
|
463
|
+
- ast.JoinedStr (f-string) for paths with parameter substitution
|
|
464
|
+
"""
|
|
465
|
+
path_params = {
|
|
466
|
+
p.name: p.name_sanitized for p in parameters if p.location == 'path'
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if not path_params:
|
|
470
|
+
return ast.Constant(value=path)
|
|
471
|
+
|
|
472
|
+
pattern = r'\{([^}]+)\}'
|
|
473
|
+
parts = re.split(pattern, path)
|
|
474
|
+
values: list[ast.expr] = []
|
|
475
|
+
|
|
476
|
+
for i, part in enumerate(parts):
|
|
477
|
+
if i % 2 == 0:
|
|
478
|
+
if part:
|
|
479
|
+
values.append(ast.Constant(value=part))
|
|
480
|
+
else:
|
|
481
|
+
sanitized = path_params.get(part, part)
|
|
482
|
+
values.append(ast.FormattedValue(value=_name(sanitized), conversion=-1))
|
|
483
|
+
|
|
484
|
+
if len(values) == 1 and isinstance(values[0], ast.Constant):
|
|
485
|
+
return values[0]
|
|
486
|
+
|
|
487
|
+
return ast.JoinedStr(values=values)
|
|
488
|
+
|
|
489
|
+
@staticmethod
|
|
490
|
+
def build_body_expr(
|
|
491
|
+
body: 'RequestBodyInfo | None',
|
|
492
|
+
) -> tuple[ast.expr | None, str | None]:
|
|
493
|
+
"""Build an AST expression for the request body parameter.
|
|
494
|
+
|
|
495
|
+
Handles different content types and determines the appropriate
|
|
496
|
+
httpx parameter name to use.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
body: The RequestBodyInfo object, or None if no body.
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
A tuple of (body_expr, httpx_param_name).
|
|
503
|
+
"""
|
|
504
|
+
if not body:
|
|
505
|
+
return None, None
|
|
506
|
+
|
|
507
|
+
body_name = 'body'
|
|
508
|
+
|
|
509
|
+
if body.is_json and body.type and body.type.type in ('model', 'root'):
|
|
510
|
+
body_expr = _call(
|
|
511
|
+
func=_attr(_name(body_name), 'model_dump'),
|
|
512
|
+
args=[],
|
|
513
|
+
)
|
|
514
|
+
elif body.is_multipart:
|
|
515
|
+
body_expr = _name(body_name)
|
|
516
|
+
elif body.is_form:
|
|
517
|
+
if body.type and body.type.type in ('model', 'root'):
|
|
518
|
+
body_expr = _call(
|
|
519
|
+
func=_attr(_name(body_name), 'model_dump'),
|
|
520
|
+
args=[],
|
|
521
|
+
)
|
|
522
|
+
else:
|
|
523
|
+
body_expr = _name(body_name)
|
|
524
|
+
else:
|
|
525
|
+
body_expr = _name(body_name)
|
|
526
|
+
|
|
527
|
+
return body_expr, body.httpx_param_name
|
|
528
|
+
|
|
529
|
+
@staticmethod
|
|
530
|
+
def prepare_all_params(
|
|
531
|
+
parameters: list['Parameter'] | None,
|
|
532
|
+
path: str,
|
|
533
|
+
request_body_info: 'RequestBodyInfo | None' = None,
|
|
534
|
+
) -> tuple[ast.expr | None, ast.expr | None, ast.expr | None, str | None, ast.expr]:
|
|
535
|
+
"""Prepare all parameter AST nodes for a request function call.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
parameters: List of Parameter objects, or None.
|
|
539
|
+
path: The API path with optional placeholders.
|
|
540
|
+
request_body_info: Request body info, or None.
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
A tuple of (query_params, header_params, body_expr, body_param_name, path_expr).
|
|
544
|
+
"""
|
|
545
|
+
if not parameters:
|
|
546
|
+
parameters = []
|
|
547
|
+
|
|
548
|
+
query_params = ParameterASTBuilder.build_query_params(parameters)
|
|
549
|
+
header_params = ParameterASTBuilder.build_header_params(parameters)
|
|
550
|
+
body_expr, body_param_name = ParameterASTBuilder.build_body_expr(
|
|
551
|
+
request_body_info
|
|
552
|
+
)
|
|
553
|
+
path_expr = ParameterASTBuilder.build_path_expr(path, parameters)
|
|
554
|
+
|
|
555
|
+
return query_params, header_params, body_expr, body_param_name, path_expr
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
# =============================================================================
|
|
559
|
+
# Endpoint Function Configuration
|
|
560
|
+
# =============================================================================
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
@dataclass
|
|
564
|
+
class EndpointFunctionConfig:
|
|
565
|
+
"""Configuration for endpoint function generation.
|
|
566
|
+
|
|
567
|
+
This dataclass holds all the parameters needed to generate an endpoint
|
|
568
|
+
function. By using a configuration object, we can easily create different
|
|
569
|
+
types of endpoint functions through a single factory.
|
|
570
|
+
|
|
571
|
+
Attributes:
|
|
572
|
+
fn_name: The name of the function to generate.
|
|
573
|
+
method: HTTP method (GET, POST, etc.).
|
|
574
|
+
path: The API path with optional placeholders.
|
|
575
|
+
parameters: List of Parameter objects for the endpoint.
|
|
576
|
+
request_body_info: RequestBodyInfo object for the request body, or None.
|
|
577
|
+
response_type: The response Type for the return annotation.
|
|
578
|
+
response_infos: List of ResponseInfo objects for status code handling.
|
|
579
|
+
docs: Optional docstring for the generated function.
|
|
580
|
+
is_async: Whether to generate an async function.
|
|
581
|
+
mode: The generation mode (standalone or delegating).
|
|
582
|
+
client_method_name: For delegating mode, the client method to call.
|
|
583
|
+
dataframe_library: For DataFrame functions, which library to use.
|
|
584
|
+
dataframe_path: For DataFrame functions, the default path parameter.
|
|
585
|
+
"""
|
|
586
|
+
|
|
587
|
+
fn_name: str
|
|
588
|
+
method: str
|
|
589
|
+
path: str
|
|
590
|
+
parameters: list['Parameter'] | None = None
|
|
591
|
+
request_body_info: 'RequestBodyInfo | None' = None
|
|
592
|
+
response_type: 'Type | None' = None
|
|
593
|
+
response_infos: list['ResponseInfo'] | None = None
|
|
594
|
+
docs: str | None = None
|
|
595
|
+
is_async: bool = False
|
|
596
|
+
mode: EndpointMode = EndpointMode.STANDALONE
|
|
597
|
+
client_method_name: str | None = None
|
|
598
|
+
dataframe_library: DataFrameLibrary | None = None
|
|
599
|
+
dataframe_path: str | None = None
|
|
600
|
+
# Pagination config
|
|
601
|
+
pagination_style: PaginationStyle | None = None
|
|
602
|
+
pagination_config: dict | None = None # Contains offset_param, limit_param, etc.
|
|
603
|
+
is_iterator: bool = False # If True, generates _iter function
|
|
604
|
+
is_paginated_dataframe: bool = (
|
|
605
|
+
False # If True, generates DataFrame from paginated results
|
|
606
|
+
)
|
|
607
|
+
item_type_ast: ast.expr | None = None # The type of items in the list
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
# =============================================================================
|
|
611
|
+
# Endpoint Function Factory
|
|
612
|
+
# =============================================================================
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
class EndpointFunctionFactory:
|
|
616
|
+
"""Factory for generating endpoint functions.
|
|
617
|
+
|
|
618
|
+
This factory consolidates the logic for generating various types of
|
|
619
|
+
endpoint functions, reducing code duplication and improving maintainability.
|
|
620
|
+
|
|
621
|
+
Example:
|
|
622
|
+
>>> config = EndpointFunctionConfig(
|
|
623
|
+
... fn_name='get_pet_by_id',
|
|
624
|
+
... method='get',
|
|
625
|
+
... path='/pet/{petId}',
|
|
626
|
+
... parameters=[...],
|
|
627
|
+
... mode=EndpointMode.STANDALONE,
|
|
628
|
+
... )
|
|
629
|
+
>>> factory = EndpointFunctionFactory(config)
|
|
630
|
+
>>> func_ast, imports = factory.build()
|
|
631
|
+
"""
|
|
632
|
+
|
|
633
|
+
def __init__(self, config: EndpointFunctionConfig):
|
|
634
|
+
"""Initialize the factory with configuration.
|
|
635
|
+
|
|
636
|
+
Args:
|
|
637
|
+
config: The endpoint function configuration.
|
|
638
|
+
"""
|
|
639
|
+
self.config = config
|
|
640
|
+
self._imports: ImportDict = {}
|
|
641
|
+
|
|
642
|
+
def _add_import(self, module: str, name: str) -> None:
|
|
643
|
+
"""Add a single import to the collection."""
|
|
644
|
+
if module not in self._imports:
|
|
645
|
+
self._imports[module] = set()
|
|
646
|
+
self._imports[module].add(name)
|
|
647
|
+
|
|
648
|
+
def _merge_imports(self, imports: ImportDict) -> None:
|
|
649
|
+
"""Merge imports from another ImportDict."""
|
|
650
|
+
for module, names in imports.items():
|
|
651
|
+
if module not in self._imports:
|
|
652
|
+
self._imports[module] = set()
|
|
653
|
+
self._imports[module].update(names)
|
|
654
|
+
|
|
655
|
+
def build(self) -> tuple[ast.FunctionDef | ast.AsyncFunctionDef, ImportDict]:
|
|
656
|
+
"""Build the endpoint function based on configuration.
|
|
657
|
+
|
|
658
|
+
Returns:
|
|
659
|
+
A tuple of (function_ast, imports).
|
|
660
|
+
"""
|
|
661
|
+
self._imports = {}
|
|
662
|
+
|
|
663
|
+
signature = self._build_signature()
|
|
664
|
+
|
|
665
|
+
if self.config.pagination_style:
|
|
666
|
+
if self.config.is_iterator:
|
|
667
|
+
body = self._build_paginated_iter_body()
|
|
668
|
+
elif self.config.is_paginated_dataframe:
|
|
669
|
+
body = self._build_paginated_dataframe_body()
|
|
670
|
+
else:
|
|
671
|
+
body = self._build_paginated_body()
|
|
672
|
+
elif self.config.dataframe_library:
|
|
673
|
+
body = self._build_dataframe_body()
|
|
674
|
+
elif self.config.mode == EndpointMode.STANDALONE:
|
|
675
|
+
body = self._build_standalone_body()
|
|
676
|
+
else:
|
|
677
|
+
body = self._build_delegating_body()
|
|
678
|
+
|
|
679
|
+
returns = self._build_return_type()
|
|
680
|
+
|
|
681
|
+
func_builder = _async_func if self.config.is_async else _func
|
|
682
|
+
|
|
683
|
+
func_ast = func_builder(
|
|
684
|
+
name=self.config.fn_name,
|
|
685
|
+
args=signature.args,
|
|
686
|
+
body=body,
|
|
687
|
+
kwargs=_argument('kwargs', _name('dict')),
|
|
688
|
+
kwonlyargs=signature.kwonlyargs,
|
|
689
|
+
kw_defaults=signature.kw_defaults,
|
|
690
|
+
returns=returns,
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
self._merge_imports(signature.imports)
|
|
694
|
+
|
|
695
|
+
return func_ast, self._imports
|
|
696
|
+
|
|
697
|
+
def _build_signature(self) -> FunctionSignature:
|
|
698
|
+
"""Build the function signature using FunctionSignatureBuilder."""
|
|
699
|
+
builder = FunctionSignatureBuilder()
|
|
700
|
+
|
|
701
|
+
# For pagination, we need to filter out the pagination parameters
|
|
702
|
+
# since we'll add our own pagination-specific parameters
|
|
703
|
+
if self.config.pagination_style and self.config.pagination_config:
|
|
704
|
+
pag_config = self.config.pagination_config
|
|
705
|
+
skip_params = set()
|
|
706
|
+
if self.config.pagination_style == PaginationStyle.OFFSET:
|
|
707
|
+
skip_params = {
|
|
708
|
+
pag_config.get('offset_param'),
|
|
709
|
+
pag_config.get('limit_param'),
|
|
710
|
+
}
|
|
711
|
+
elif self.config.pagination_style == PaginationStyle.CURSOR:
|
|
712
|
+
skip_params = {
|
|
713
|
+
pag_config.get('cursor_param'),
|
|
714
|
+
pag_config.get('limit_param'),
|
|
715
|
+
}
|
|
716
|
+
elif self.config.pagination_style == PaginationStyle.PAGE:
|
|
717
|
+
skip_params = {
|
|
718
|
+
pag_config.get('page_param'),
|
|
719
|
+
pag_config.get('per_page_param'),
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
filtered_params = [
|
|
723
|
+
p for p in (self.config.parameters or []) if p.name not in skip_params
|
|
724
|
+
]
|
|
725
|
+
builder.add_parameters(filtered_params)
|
|
726
|
+
|
|
727
|
+
# Add pagination-specific parameters
|
|
728
|
+
self._add_pagination_parameters(builder)
|
|
729
|
+
else:
|
|
730
|
+
builder.add_parameters(self.config.parameters)
|
|
731
|
+
|
|
732
|
+
builder.add_request_body(self.config.request_body_info)
|
|
733
|
+
|
|
734
|
+
if self.config.dataframe_library and not self.config.is_paginated_dataframe:
|
|
735
|
+
# Only add path parameter for non-paginated DataFrame functions
|
|
736
|
+
# Paginated DataFrame functions don't need it - data is already extracted
|
|
737
|
+
builder.add_path_parameter(default=self.config.dataframe_path)
|
|
738
|
+
|
|
739
|
+
if self.config.mode == EndpointMode.STANDALONE:
|
|
740
|
+
builder.add_client_parameter()
|
|
741
|
+
|
|
742
|
+
return builder.build()
|
|
743
|
+
|
|
744
|
+
def _add_pagination_parameters(self, builder: FunctionSignatureBuilder) -> None:
|
|
745
|
+
"""Add pagination-specific parameters to the signature builder."""
|
|
746
|
+
pag_config = self.config.pagination_config or {}
|
|
747
|
+
default_page_size = pag_config.get('default_page_size', 100)
|
|
748
|
+
|
|
749
|
+
if self.config.pagination_style == PaginationStyle.OFFSET:
|
|
750
|
+
# offset: int | None = None
|
|
751
|
+
builder.add_custom_kwarg(
|
|
752
|
+
name='offset',
|
|
753
|
+
annotation=_union_expr([_name('int'), ast.Constant(value=None)]),
|
|
754
|
+
default=ast.Constant(value=None),
|
|
755
|
+
)
|
|
756
|
+
elif self.config.pagination_style == PaginationStyle.CURSOR:
|
|
757
|
+
# cursor: str | None = None
|
|
758
|
+
builder.add_custom_kwarg(
|
|
759
|
+
name='cursor',
|
|
760
|
+
annotation=_union_expr([_name('str'), ast.Constant(value=None)]),
|
|
761
|
+
default=ast.Constant(value=None),
|
|
762
|
+
)
|
|
763
|
+
elif self.config.pagination_style == PaginationStyle.PAGE:
|
|
764
|
+
# page: int | None = None
|
|
765
|
+
builder.add_custom_kwarg(
|
|
766
|
+
name='page',
|
|
767
|
+
annotation=_union_expr([_name('int'), ast.Constant(value=None)]),
|
|
768
|
+
default=ast.Constant(value=None),
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
# page_size: int = default_page_size
|
|
772
|
+
builder.add_custom_kwarg(
|
|
773
|
+
name='page_size',
|
|
774
|
+
annotation=_name('int'),
|
|
775
|
+
default=ast.Constant(value=default_page_size),
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
# max_items: int | None = None
|
|
779
|
+
builder.add_custom_kwarg(
|
|
780
|
+
name='max_items',
|
|
781
|
+
annotation=_union_expr([_name('int'), ast.Constant(value=None)]),
|
|
782
|
+
default=ast.Constant(value=None),
|
|
783
|
+
)
|
|
784
|
+
|
|
785
|
+
def _build_return_type(self) -> ast.expr:
|
|
786
|
+
"""Build the return type annotation."""
|
|
787
|
+
if self.config.dataframe_library:
|
|
788
|
+
if self.config.dataframe_library == DataFrameLibrary.PANDAS:
|
|
789
|
+
return ast.Constant(value='pd.DataFrame')
|
|
790
|
+
else:
|
|
791
|
+
return ast.Constant(value='pl.DataFrame')
|
|
792
|
+
|
|
793
|
+
# Pagination return types
|
|
794
|
+
if self.config.pagination_style:
|
|
795
|
+
if self.config.item_type_ast:
|
|
796
|
+
if self.config.is_iterator:
|
|
797
|
+
# Iterator[ItemType] or AsyncIterator[ItemType]
|
|
798
|
+
self._add_import('collections.abc', 'Iterator')
|
|
799
|
+
self._add_import('collections.abc', 'AsyncIterator')
|
|
800
|
+
iter_type = 'AsyncIterator' if self.config.is_async else 'Iterator'
|
|
801
|
+
return _subscript(iter_type, self.config.item_type_ast)
|
|
802
|
+
else:
|
|
803
|
+
# list[ItemType]
|
|
804
|
+
return _subscript('list', self.config.item_type_ast)
|
|
805
|
+
else:
|
|
806
|
+
# Fallback to list[Any]
|
|
807
|
+
self._add_import('typing', 'Any')
|
|
808
|
+
if self.config.is_iterator:
|
|
809
|
+
self._add_import('collections.abc', 'Iterator')
|
|
810
|
+
self._add_import('collections.abc', 'AsyncIterator')
|
|
811
|
+
iter_type = 'AsyncIterator' if self.config.is_async else 'Iterator'
|
|
812
|
+
return _subscript(iter_type, _name('Any'))
|
|
813
|
+
else:
|
|
814
|
+
return _subscript('list', _name('Any'))
|
|
815
|
+
|
|
816
|
+
if self.config.response_type:
|
|
817
|
+
self._merge_imports(self.config.response_type.annotation_imports)
|
|
818
|
+
return self.config.response_type.annotation_ast
|
|
819
|
+
|
|
820
|
+
self._add_import('httpx', 'Response')
|
|
821
|
+
return _name('Response')
|
|
822
|
+
|
|
823
|
+
def _build_standalone_body(self) -> list[ast.stmt]:
|
|
824
|
+
"""Build the body for a standalone endpoint function."""
|
|
825
|
+
body: list[ast.stmt] = []
|
|
826
|
+
|
|
827
|
+
if self.config.docs:
|
|
828
|
+
body.append(
|
|
829
|
+
ast.Expr(value=ast.Constant(value=clean_docstring(self.config.docs)))
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
# c = client or Client()
|
|
833
|
+
body.append(
|
|
834
|
+
_assign(
|
|
835
|
+
_name('c'),
|
|
836
|
+
ast.BoolOp(
|
|
837
|
+
op=ast.Or(),
|
|
838
|
+
values=[_name('client'), _call(_name('Client'))],
|
|
839
|
+
),
|
|
840
|
+
)
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
request_keywords = self._build_request_keywords()
|
|
844
|
+
|
|
845
|
+
request_method = '_request_async' if self.config.is_async else '_request'
|
|
846
|
+
request_call = _call(
|
|
847
|
+
func=_attr('c', request_method),
|
|
848
|
+
keywords=request_keywords,
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
if self.config.is_async:
|
|
852
|
+
request_call = ast.Await(value=request_call)
|
|
853
|
+
|
|
854
|
+
if self.config.response_type:
|
|
855
|
+
self._merge_imports(self.config.response_type.annotation_imports)
|
|
856
|
+
|
|
857
|
+
# Check if response type is a raw type (Response, bytes, str) that doesn't need parsing
|
|
858
|
+
is_raw_response = self._is_raw_response_type(self.config.response_type)
|
|
859
|
+
|
|
860
|
+
if is_raw_response:
|
|
861
|
+
# For non-JSON responses, just return the response directly
|
|
862
|
+
body.append(ast.Return(value=request_call))
|
|
863
|
+
else:
|
|
864
|
+
# For JSON responses, parse and validate with Pydantic
|
|
865
|
+
body.append(_assign(_name('response'), request_call))
|
|
866
|
+
|
|
867
|
+
parse_method = (
|
|
868
|
+
'_parse_response_async'
|
|
869
|
+
if self.config.is_async
|
|
870
|
+
else '_parse_response'
|
|
871
|
+
)
|
|
872
|
+
parse_call = _call(
|
|
873
|
+
func=_attr('c', parse_method),
|
|
874
|
+
args=[_name('response'), self.config.response_type.annotation_ast],
|
|
875
|
+
)
|
|
876
|
+
if self.config.is_async:
|
|
877
|
+
parse_call = ast.Await(value=parse_call)
|
|
878
|
+
body.append(ast.Return(value=parse_call))
|
|
879
|
+
else:
|
|
880
|
+
body.append(ast.Return(value=request_call))
|
|
881
|
+
|
|
882
|
+
return body
|
|
883
|
+
|
|
884
|
+
def _is_raw_response_type(self, response_type: 'Type') -> bool:
|
|
885
|
+
"""Check if the response type is a raw type that doesn't need JSON parsing.
|
|
886
|
+
|
|
887
|
+
Raw types include: Response, bytes, str (for non-JSON content types).
|
|
888
|
+
"""
|
|
889
|
+
if response_type is None:
|
|
890
|
+
return True
|
|
891
|
+
|
|
892
|
+
ann = response_type.annotation_ast
|
|
893
|
+
if isinstance(ann, ast.Name):
|
|
894
|
+
# Simple type like Response, bytes, str
|
|
895
|
+
return ann.id in ('Response', 'bytes', 'str')
|
|
896
|
+
|
|
897
|
+
# For Union types, check if ALL types are raw types
|
|
898
|
+
if isinstance(ann, ast.Subscript):
|
|
899
|
+
if isinstance(ann.value, ast.Name) and ann.value.id == 'Union':
|
|
900
|
+
if isinstance(ann.slice, ast.Tuple):
|
|
901
|
+
for elt in ann.slice.elts:
|
|
902
|
+
if isinstance(elt, ast.Name):
|
|
903
|
+
if elt.id not in ('Response', 'bytes', 'str', 'None'):
|
|
904
|
+
return False
|
|
905
|
+
elif isinstance(elt, ast.Constant) and elt.value is None:
|
|
906
|
+
continue # None is OK
|
|
907
|
+
else:
|
|
908
|
+
return False # Complex type, not raw
|
|
909
|
+
return True
|
|
910
|
+
|
|
911
|
+
return False
|
|
912
|
+
|
|
913
|
+
def _build_delegating_body(self) -> list[ast.stmt]:
|
|
914
|
+
"""Build the body for a delegating endpoint function."""
|
|
915
|
+
body: list[ast.stmt] = []
|
|
916
|
+
|
|
917
|
+
if self.config.docs:
|
|
918
|
+
body.append(
|
|
919
|
+
ast.Expr(value=ast.Constant(value=clean_docstring(self.config.docs)))
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
call_args, call_keywords = self._build_delegating_call_args()
|
|
923
|
+
|
|
924
|
+
method_name = self.config.client_method_name or self.config.fn_name
|
|
925
|
+
client_call = _call(
|
|
926
|
+
func=_attr(_call(_name('Client')), method_name),
|
|
927
|
+
args=call_args,
|
|
928
|
+
keywords=call_keywords,
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
if self.config.is_async:
|
|
932
|
+
client_call = ast.Await(value=client_call)
|
|
933
|
+
|
|
934
|
+
body.append(ast.Return(value=client_call))
|
|
935
|
+
|
|
936
|
+
return body
|
|
937
|
+
|
|
938
|
+
def _build_paginated_dataframe_body(self) -> list[ast.stmt]:
|
|
939
|
+
"""Build the body for a paginated DataFrame endpoint function.
|
|
940
|
+
|
|
941
|
+
This combines pagination (to fetch all items) with DataFrame conversion.
|
|
942
|
+
"""
|
|
943
|
+
body: list[ast.stmt] = []
|
|
944
|
+
pag_config = self.config.pagination_config or {}
|
|
945
|
+
|
|
946
|
+
library = self.config.dataframe_library
|
|
947
|
+
return_type_str = (
|
|
948
|
+
'pd.DataFrame' if library == DataFrameLibrary.PANDAS else 'pl.DataFrame'
|
|
949
|
+
)
|
|
950
|
+
|
|
951
|
+
# Add docstring
|
|
952
|
+
doc_content = self.config.docs or ''
|
|
953
|
+
doc_suffix = f'\n\nReturns:\n {return_type_str}'
|
|
954
|
+
body.append(
|
|
955
|
+
ast.Expr(
|
|
956
|
+
value=ast.Constant(value=clean_docstring(doc_content + doc_suffix))
|
|
957
|
+
)
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
# c = client or Client()
|
|
961
|
+
body.append(
|
|
962
|
+
_assign(
|
|
963
|
+
_name('c'),
|
|
964
|
+
ast.BoolOp(
|
|
965
|
+
op=ast.Or(),
|
|
966
|
+
values=[_name('client'), _call(_name('Client'))],
|
|
967
|
+
),
|
|
968
|
+
)
|
|
969
|
+
)
|
|
970
|
+
|
|
971
|
+
# Build the fetch_page inner function
|
|
972
|
+
fetch_page_fn = self._build_fetch_page_function()
|
|
973
|
+
body.append(fetch_page_fn)
|
|
974
|
+
|
|
975
|
+
# Build extract_items lambda
|
|
976
|
+
data_path = pag_config.get('data_path')
|
|
977
|
+
extract_items = self._build_extract_lambda(data_path)
|
|
978
|
+
|
|
979
|
+
# Build pagination call to get all items
|
|
980
|
+
if self.config.pagination_style == PaginationStyle.OFFSET:
|
|
981
|
+
paginate_fn = (
|
|
982
|
+
'paginate_offset_async' if self.config.is_async else 'paginate_offset'
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
total_path = pag_config.get('total_path')
|
|
986
|
+
get_total = self._build_extract_lambda(total_path) if total_path else None
|
|
987
|
+
|
|
988
|
+
paginate_keywords = [
|
|
989
|
+
ast.keyword(arg='fetch_page', value=_name('fetch_page')),
|
|
990
|
+
ast.keyword(arg='extract_items', value=extract_items),
|
|
991
|
+
]
|
|
992
|
+
if get_total:
|
|
993
|
+
paginate_keywords.append(ast.keyword(arg='get_total', value=get_total))
|
|
994
|
+
paginate_keywords.extend(
|
|
995
|
+
[
|
|
996
|
+
ast.keyword(
|
|
997
|
+
arg='start_offset',
|
|
998
|
+
value=ast.BoolOp(
|
|
999
|
+
op=ast.Or(), values=[_name('offset'), ast.Constant(value=0)]
|
|
1000
|
+
),
|
|
1001
|
+
),
|
|
1002
|
+
ast.keyword(arg='page_size', value=_name('page_size')),
|
|
1003
|
+
ast.keyword(arg='max_items', value=_name('max_items')),
|
|
1004
|
+
]
|
|
1005
|
+
)
|
|
1006
|
+
|
|
1007
|
+
elif self.config.pagination_style == PaginationStyle.CURSOR:
|
|
1008
|
+
paginate_fn = (
|
|
1009
|
+
'paginate_cursor_async' if self.config.is_async else 'paginate_cursor'
|
|
1010
|
+
)
|
|
1011
|
+
|
|
1012
|
+
next_cursor_path = pag_config.get('next_cursor_path')
|
|
1013
|
+
get_next_cursor = (
|
|
1014
|
+
self._build_extract_lambda(next_cursor_path)
|
|
1015
|
+
if next_cursor_path
|
|
1016
|
+
else ast.Lambda(
|
|
1017
|
+
args=ast.arguments(
|
|
1018
|
+
posonlyargs=[],
|
|
1019
|
+
args=[ast.arg(arg='page')],
|
|
1020
|
+
kwonlyargs=[],
|
|
1021
|
+
kw_defaults=[],
|
|
1022
|
+
defaults=[],
|
|
1023
|
+
),
|
|
1024
|
+
body=ast.Constant(value=None),
|
|
1025
|
+
)
|
|
1026
|
+
)
|
|
1027
|
+
|
|
1028
|
+
paginate_keywords = [
|
|
1029
|
+
ast.keyword(arg='fetch_page', value=_name('fetch_page')),
|
|
1030
|
+
ast.keyword(arg='extract_items', value=extract_items),
|
|
1031
|
+
ast.keyword(arg='get_next_cursor', value=get_next_cursor),
|
|
1032
|
+
ast.keyword(arg='start_cursor', value=_name('cursor')),
|
|
1033
|
+
ast.keyword(arg='page_size', value=_name('page_size')),
|
|
1034
|
+
ast.keyword(arg='max_items', value=_name('max_items')),
|
|
1035
|
+
]
|
|
1036
|
+
|
|
1037
|
+
elif self.config.pagination_style == PaginationStyle.PAGE:
|
|
1038
|
+
paginate_fn = (
|
|
1039
|
+
'paginate_page_async' if self.config.is_async else 'paginate_page'
|
|
1040
|
+
)
|
|
1041
|
+
|
|
1042
|
+
total_pages_path = pag_config.get('total_pages_path')
|
|
1043
|
+
get_total_pages = (
|
|
1044
|
+
self._build_extract_lambda(total_pages_path)
|
|
1045
|
+
if total_pages_path
|
|
1046
|
+
else None
|
|
1047
|
+
)
|
|
1048
|
+
|
|
1049
|
+
paginate_keywords = [
|
|
1050
|
+
ast.keyword(arg='fetch_page', value=_name('fetch_page')),
|
|
1051
|
+
ast.keyword(arg='extract_items', value=extract_items),
|
|
1052
|
+
]
|
|
1053
|
+
if get_total_pages:
|
|
1054
|
+
paginate_keywords.append(
|
|
1055
|
+
ast.keyword(arg='get_total_pages', value=get_total_pages)
|
|
1056
|
+
)
|
|
1057
|
+
paginate_keywords.extend(
|
|
1058
|
+
[
|
|
1059
|
+
ast.keyword(
|
|
1060
|
+
arg='start_page',
|
|
1061
|
+
value=ast.BoolOp(
|
|
1062
|
+
op=ast.Or(), values=[_name('page'), ast.Constant(value=1)]
|
|
1063
|
+
),
|
|
1064
|
+
),
|
|
1065
|
+
ast.keyword(arg='page_size', value=_name('page_size')),
|
|
1066
|
+
ast.keyword(arg='max_items', value=_name('max_items')),
|
|
1067
|
+
]
|
|
1068
|
+
)
|
|
1069
|
+
else:
|
|
1070
|
+
raise ValueError(
|
|
1071
|
+
f'Unsupported pagination style: {self.config.pagination_style}'
|
|
1072
|
+
)
|
|
1073
|
+
|
|
1074
|
+
paginate_call = _call(
|
|
1075
|
+
func=_name(paginate_fn),
|
|
1076
|
+
keywords=paginate_keywords,
|
|
1077
|
+
)
|
|
1078
|
+
|
|
1079
|
+
if self.config.is_async:
|
|
1080
|
+
paginate_call = ast.Await(value=paginate_call)
|
|
1081
|
+
|
|
1082
|
+
# items = paginate_offset(...)
|
|
1083
|
+
body.append(_assign(_name('items'), paginate_call))
|
|
1084
|
+
|
|
1085
|
+
# Convert to DataFrame
|
|
1086
|
+
helper_fn = 'to_pandas' if library == DataFrameLibrary.PANDAS else 'to_polars'
|
|
1087
|
+
|
|
1088
|
+
# return to_pandas(items) or to_polars(items)
|
|
1089
|
+
body.append(
|
|
1090
|
+
ast.Return(
|
|
1091
|
+
value=_call(
|
|
1092
|
+
func=_name(helper_fn),
|
|
1093
|
+
args=[_name('items')],
|
|
1094
|
+
)
|
|
1095
|
+
)
|
|
1096
|
+
)
|
|
1097
|
+
|
|
1098
|
+
return body
|
|
1099
|
+
|
|
1100
|
+
def _build_dataframe_body(self) -> list[ast.stmt]:
|
|
1101
|
+
"""Build the body for a DataFrame endpoint function."""
|
|
1102
|
+
body: list[ast.stmt] = []
|
|
1103
|
+
|
|
1104
|
+
library = self.config.dataframe_library
|
|
1105
|
+
return_type_str = (
|
|
1106
|
+
'pd.DataFrame' if library == DataFrameLibrary.PANDAS else 'pl.DataFrame'
|
|
1107
|
+
)
|
|
1108
|
+
|
|
1109
|
+
doc_content = self.config.docs or ''
|
|
1110
|
+
doc_suffix = f'\n\nReturns:\n {return_type_str}'
|
|
1111
|
+
body.append(
|
|
1112
|
+
ast.Expr(
|
|
1113
|
+
value=ast.Constant(value=clean_docstring(doc_content + doc_suffix))
|
|
1114
|
+
)
|
|
1115
|
+
)
|
|
1116
|
+
|
|
1117
|
+
if self.config.mode == EndpointMode.STANDALONE:
|
|
1118
|
+
body.append(
|
|
1119
|
+
_assign(
|
|
1120
|
+
_name('c'),
|
|
1121
|
+
ast.BoolOp(
|
|
1122
|
+
op=ast.Or(),
|
|
1123
|
+
values=[_name('client'), _call(_name('Client'))],
|
|
1124
|
+
),
|
|
1125
|
+
)
|
|
1126
|
+
)
|
|
1127
|
+
|
|
1128
|
+
request_keywords = self._build_request_keywords()
|
|
1129
|
+
|
|
1130
|
+
request_json_method = (
|
|
1131
|
+
'_request_json_async' if self.config.is_async else '_request_json'
|
|
1132
|
+
)
|
|
1133
|
+
request_call = _call(
|
|
1134
|
+
func=_attr('c', request_json_method),
|
|
1135
|
+
keywords=request_keywords,
|
|
1136
|
+
)
|
|
1137
|
+
|
|
1138
|
+
if self.config.is_async:
|
|
1139
|
+
request_call = ast.Await(value=request_call)
|
|
1140
|
+
|
|
1141
|
+
body.append(_assign(_name('data'), request_call))
|
|
1142
|
+
else:
|
|
1143
|
+
call_args, call_keywords = self._build_delegating_call_args()
|
|
1144
|
+
method_name = self.config.client_method_name or self.config.fn_name
|
|
1145
|
+
client_call = _call(
|
|
1146
|
+
func=_attr(_call(_name('Client')), method_name),
|
|
1147
|
+
args=call_args,
|
|
1148
|
+
keywords=call_keywords,
|
|
1149
|
+
)
|
|
1150
|
+
|
|
1151
|
+
if self.config.is_async:
|
|
1152
|
+
client_call = ast.Await(value=client_call)
|
|
1153
|
+
|
|
1154
|
+
body.append(ast.Return(value=client_call))
|
|
1155
|
+
return body
|
|
1156
|
+
|
|
1157
|
+
helper_fn = 'to_pandas' if library == DataFrameLibrary.PANDAS else 'to_polars'
|
|
1158
|
+
|
|
1159
|
+
body.append(
|
|
1160
|
+
ast.Return(
|
|
1161
|
+
value=_call(
|
|
1162
|
+
func=_name(helper_fn),
|
|
1163
|
+
args=[_name('data')],
|
|
1164
|
+
keywords=[ast.keyword(arg='path', value=_name('path'))],
|
|
1165
|
+
)
|
|
1166
|
+
)
|
|
1167
|
+
)
|
|
1168
|
+
|
|
1169
|
+
return body
|
|
1170
|
+
|
|
1171
|
+
def _build_paginated_body(self) -> list[ast.stmt]:
|
|
1172
|
+
"""Build the body for a paginated endpoint function."""
|
|
1173
|
+
body: list[ast.stmt] = []
|
|
1174
|
+
pag_config = self.config.pagination_config or {}
|
|
1175
|
+
|
|
1176
|
+
# Add docstring
|
|
1177
|
+
if self.config.docs:
|
|
1178
|
+
body.append(
|
|
1179
|
+
ast.Expr(value=ast.Constant(value=clean_docstring(self.config.docs)))
|
|
1180
|
+
)
|
|
1181
|
+
|
|
1182
|
+
# c = client or Client()
|
|
1183
|
+
body.append(
|
|
1184
|
+
_assign(
|
|
1185
|
+
_name('c'),
|
|
1186
|
+
ast.BoolOp(
|
|
1187
|
+
op=ast.Or(),
|
|
1188
|
+
values=[_name('client'), _call(_name('Client'))],
|
|
1189
|
+
),
|
|
1190
|
+
)
|
|
1191
|
+
)
|
|
1192
|
+
|
|
1193
|
+
# Build the fetch_page inner function
|
|
1194
|
+
fetch_page_fn = self._build_fetch_page_function()
|
|
1195
|
+
body.append(fetch_page_fn)
|
|
1196
|
+
|
|
1197
|
+
# Build extract_items lambda
|
|
1198
|
+
data_path = pag_config.get('data_path')
|
|
1199
|
+
extract_items = self._build_extract_lambda(data_path)
|
|
1200
|
+
|
|
1201
|
+
# Build pagination call
|
|
1202
|
+
if self.config.pagination_style == PaginationStyle.OFFSET:
|
|
1203
|
+
paginate_fn = (
|
|
1204
|
+
'paginate_offset_async' if self.config.is_async else 'paginate_offset'
|
|
1205
|
+
)
|
|
1206
|
+
|
|
1207
|
+
# Build get_total lambda if total_path is specified
|
|
1208
|
+
total_path = pag_config.get('total_path')
|
|
1209
|
+
get_total = self._build_extract_lambda(total_path) if total_path else None
|
|
1210
|
+
|
|
1211
|
+
paginate_keywords = [
|
|
1212
|
+
ast.keyword(arg='fetch_page', value=_name('fetch_page')),
|
|
1213
|
+
ast.keyword(arg='extract_items', value=extract_items),
|
|
1214
|
+
]
|
|
1215
|
+
if get_total:
|
|
1216
|
+
paginate_keywords.append(ast.keyword(arg='get_total', value=get_total))
|
|
1217
|
+
paginate_keywords.extend(
|
|
1218
|
+
[
|
|
1219
|
+
ast.keyword(
|
|
1220
|
+
arg='start_offset',
|
|
1221
|
+
value=ast.BoolOp(
|
|
1222
|
+
op=ast.Or(), values=[_name('offset'), ast.Constant(value=0)]
|
|
1223
|
+
),
|
|
1224
|
+
),
|
|
1225
|
+
ast.keyword(arg='page_size', value=_name('page_size')),
|
|
1226
|
+
ast.keyword(arg='max_items', value=_name('max_items')),
|
|
1227
|
+
]
|
|
1228
|
+
)
|
|
1229
|
+
|
|
1230
|
+
elif self.config.pagination_style == PaginationStyle.CURSOR:
|
|
1231
|
+
paginate_fn = (
|
|
1232
|
+
'paginate_cursor_async' if self.config.is_async else 'paginate_cursor'
|
|
1233
|
+
)
|
|
1234
|
+
|
|
1235
|
+
next_cursor_path = pag_config.get('next_cursor_path')
|
|
1236
|
+
get_next_cursor = (
|
|
1237
|
+
self._build_extract_lambda(next_cursor_path)
|
|
1238
|
+
if next_cursor_path
|
|
1239
|
+
else ast.Lambda(
|
|
1240
|
+
args=ast.arguments(
|
|
1241
|
+
posonlyargs=[],
|
|
1242
|
+
args=[ast.arg(arg='page')],
|
|
1243
|
+
kwonlyargs=[],
|
|
1244
|
+
kw_defaults=[],
|
|
1245
|
+
defaults=[],
|
|
1246
|
+
),
|
|
1247
|
+
body=ast.Constant(value=None),
|
|
1248
|
+
)
|
|
1249
|
+
)
|
|
1250
|
+
|
|
1251
|
+
paginate_keywords = [
|
|
1252
|
+
ast.keyword(arg='fetch_page', value=_name('fetch_page')),
|
|
1253
|
+
ast.keyword(arg='extract_items', value=extract_items),
|
|
1254
|
+
ast.keyword(arg='get_next_cursor', value=get_next_cursor),
|
|
1255
|
+
ast.keyword(arg='start_cursor', value=_name('cursor')),
|
|
1256
|
+
ast.keyword(arg='page_size', value=_name('page_size')),
|
|
1257
|
+
ast.keyword(arg='max_items', value=_name('max_items')),
|
|
1258
|
+
]
|
|
1259
|
+
|
|
1260
|
+
elif self.config.pagination_style == PaginationStyle.PAGE:
|
|
1261
|
+
paginate_fn = (
|
|
1262
|
+
'paginate_page_async' if self.config.is_async else 'paginate_page'
|
|
1263
|
+
)
|
|
1264
|
+
|
|
1265
|
+
total_pages_path = pag_config.get('total_pages_path')
|
|
1266
|
+
get_total_pages = (
|
|
1267
|
+
self._build_extract_lambda(total_pages_path)
|
|
1268
|
+
if total_pages_path
|
|
1269
|
+
else None
|
|
1270
|
+
)
|
|
1271
|
+
|
|
1272
|
+
paginate_keywords = [
|
|
1273
|
+
ast.keyword(arg='fetch_page', value=_name('fetch_page')),
|
|
1274
|
+
ast.keyword(arg='extract_items', value=extract_items),
|
|
1275
|
+
]
|
|
1276
|
+
if get_total_pages:
|
|
1277
|
+
paginate_keywords.append(
|
|
1278
|
+
ast.keyword(arg='get_total_pages', value=get_total_pages)
|
|
1279
|
+
)
|
|
1280
|
+
paginate_keywords.extend(
|
|
1281
|
+
[
|
|
1282
|
+
ast.keyword(
|
|
1283
|
+
arg='start_page',
|
|
1284
|
+
value=ast.BoolOp(
|
|
1285
|
+
op=ast.Or(), values=[_name('page'), ast.Constant(value=1)]
|
|
1286
|
+
),
|
|
1287
|
+
),
|
|
1288
|
+
ast.keyword(arg='page_size', value=_name('page_size')),
|
|
1289
|
+
ast.keyword(arg='max_items', value=_name('max_items')),
|
|
1290
|
+
]
|
|
1291
|
+
)
|
|
1292
|
+
else:
|
|
1293
|
+
raise ValueError(
|
|
1294
|
+
f'Unsupported pagination style: {self.config.pagination_style}'
|
|
1295
|
+
)
|
|
1296
|
+
|
|
1297
|
+
paginate_call = _call(
|
|
1298
|
+
func=_name(paginate_fn),
|
|
1299
|
+
keywords=paginate_keywords,
|
|
1300
|
+
)
|
|
1301
|
+
|
|
1302
|
+
if self.config.is_async:
|
|
1303
|
+
paginate_call = ast.Await(value=paginate_call)
|
|
1304
|
+
|
|
1305
|
+
body.append(ast.Return(value=paginate_call))
|
|
1306
|
+
|
|
1307
|
+
return body
|
|
1308
|
+
|
|
1309
|
+
def _build_paginated_iter_body(self) -> list[ast.stmt]:
|
|
1310
|
+
"""Build the body for a paginated iterator endpoint function."""
|
|
1311
|
+
body: list[ast.stmt] = []
|
|
1312
|
+
pag_config = self.config.pagination_config or {}
|
|
1313
|
+
|
|
1314
|
+
# Add docstring
|
|
1315
|
+
if self.config.docs:
|
|
1316
|
+
body.append(
|
|
1317
|
+
ast.Expr(value=ast.Constant(value=clean_docstring(self.config.docs)))
|
|
1318
|
+
)
|
|
1319
|
+
|
|
1320
|
+
# c = client or Client()
|
|
1321
|
+
body.append(
|
|
1322
|
+
_assign(
|
|
1323
|
+
_name('c'),
|
|
1324
|
+
ast.BoolOp(
|
|
1325
|
+
op=ast.Or(),
|
|
1326
|
+
values=[_name('client'), _call(_name('Client'))],
|
|
1327
|
+
),
|
|
1328
|
+
)
|
|
1329
|
+
)
|
|
1330
|
+
|
|
1331
|
+
# Build the fetch_page inner function
|
|
1332
|
+
fetch_page_fn = self._build_fetch_page_function()
|
|
1333
|
+
body.append(fetch_page_fn)
|
|
1334
|
+
|
|
1335
|
+
# Build extract_items lambda
|
|
1336
|
+
data_path = pag_config.get('data_path')
|
|
1337
|
+
extract_items = self._build_extract_lambda(data_path)
|
|
1338
|
+
|
|
1339
|
+
# Build iterate call
|
|
1340
|
+
if self.config.pagination_style == PaginationStyle.OFFSET:
|
|
1341
|
+
iterate_fn = (
|
|
1342
|
+
'iterate_offset_async' if self.config.is_async else 'iterate_offset'
|
|
1343
|
+
)
|
|
1344
|
+
|
|
1345
|
+
total_path = pag_config.get('total_path')
|
|
1346
|
+
get_total = self._build_extract_lambda(total_path) if total_path else None
|
|
1347
|
+
|
|
1348
|
+
iterate_keywords = [
|
|
1349
|
+
ast.keyword(arg='fetch_page', value=_name('fetch_page')),
|
|
1350
|
+
ast.keyword(arg='extract_items', value=extract_items),
|
|
1351
|
+
]
|
|
1352
|
+
if get_total:
|
|
1353
|
+
iterate_keywords.append(ast.keyword(arg='get_total', value=get_total))
|
|
1354
|
+
iterate_keywords.extend(
|
|
1355
|
+
[
|
|
1356
|
+
ast.keyword(
|
|
1357
|
+
arg='start_offset',
|
|
1358
|
+
value=ast.BoolOp(
|
|
1359
|
+
op=ast.Or(), values=[_name('offset'), ast.Constant(value=0)]
|
|
1360
|
+
),
|
|
1361
|
+
),
|
|
1362
|
+
ast.keyword(arg='page_size', value=_name('page_size')),
|
|
1363
|
+
ast.keyword(arg='max_items', value=_name('max_items')),
|
|
1364
|
+
]
|
|
1365
|
+
)
|
|
1366
|
+
|
|
1367
|
+
elif self.config.pagination_style == PaginationStyle.CURSOR:
|
|
1368
|
+
iterate_fn = (
|
|
1369
|
+
'iterate_cursor_async' if self.config.is_async else 'iterate_cursor'
|
|
1370
|
+
)
|
|
1371
|
+
|
|
1372
|
+
next_cursor_path = pag_config.get('next_cursor_path')
|
|
1373
|
+
get_next_cursor = (
|
|
1374
|
+
self._build_extract_lambda(next_cursor_path)
|
|
1375
|
+
if next_cursor_path
|
|
1376
|
+
else ast.Lambda(
|
|
1377
|
+
args=ast.arguments(
|
|
1378
|
+
posonlyargs=[],
|
|
1379
|
+
args=[ast.arg(arg='page')],
|
|
1380
|
+
kwonlyargs=[],
|
|
1381
|
+
kw_defaults=[],
|
|
1382
|
+
defaults=[],
|
|
1383
|
+
),
|
|
1384
|
+
body=ast.Constant(value=None),
|
|
1385
|
+
)
|
|
1386
|
+
)
|
|
1387
|
+
|
|
1388
|
+
iterate_keywords = [
|
|
1389
|
+
ast.keyword(arg='fetch_page', value=_name('fetch_page')),
|
|
1390
|
+
ast.keyword(arg='extract_items', value=extract_items),
|
|
1391
|
+
ast.keyword(arg='get_next_cursor', value=get_next_cursor),
|
|
1392
|
+
ast.keyword(arg='start_cursor', value=_name('cursor')),
|
|
1393
|
+
ast.keyword(arg='page_size', value=_name('page_size')),
|
|
1394
|
+
ast.keyword(arg='max_items', value=_name('max_items')),
|
|
1395
|
+
]
|
|
1396
|
+
|
|
1397
|
+
elif self.config.pagination_style == PaginationStyle.PAGE:
|
|
1398
|
+
# Page-based pagination iterator
|
|
1399
|
+
iterate_fn = (
|
|
1400
|
+
'iterate_page_async' if self.config.is_async else 'iterate_page'
|
|
1401
|
+
)
|
|
1402
|
+
|
|
1403
|
+
total_pages_path = pag_config.get('total_pages_path')
|
|
1404
|
+
get_total_pages = (
|
|
1405
|
+
self._build_extract_lambda(total_pages_path)
|
|
1406
|
+
if total_pages_path
|
|
1407
|
+
else None
|
|
1408
|
+
)
|
|
1409
|
+
|
|
1410
|
+
iterate_keywords = [
|
|
1411
|
+
ast.keyword(arg='fetch_page', value=_name('fetch_page')),
|
|
1412
|
+
ast.keyword(arg='extract_items', value=extract_items),
|
|
1413
|
+
]
|
|
1414
|
+
if get_total_pages:
|
|
1415
|
+
iterate_keywords.append(
|
|
1416
|
+
ast.keyword(arg='get_total_pages', value=get_total_pages)
|
|
1417
|
+
)
|
|
1418
|
+
iterate_keywords.extend(
|
|
1419
|
+
[
|
|
1420
|
+
ast.keyword(
|
|
1421
|
+
arg='start_page',
|
|
1422
|
+
value=ast.BoolOp(
|
|
1423
|
+
op=ast.Or(), values=[_name('page'), ast.Constant(value=1)]
|
|
1424
|
+
),
|
|
1425
|
+
),
|
|
1426
|
+
ast.keyword(arg='page_size', value=_name('page_size')),
|
|
1427
|
+
ast.keyword(arg='max_items', value=_name('max_items')),
|
|
1428
|
+
]
|
|
1429
|
+
)
|
|
1430
|
+
else:
|
|
1431
|
+
raise ValueError(
|
|
1432
|
+
f'Unsupported pagination style: {self.config.pagination_style}'
|
|
1433
|
+
)
|
|
1434
|
+
|
|
1435
|
+
iterate_call = _call(
|
|
1436
|
+
func=_name(iterate_fn),
|
|
1437
|
+
keywords=iterate_keywords,
|
|
1438
|
+
)
|
|
1439
|
+
|
|
1440
|
+
if self.config.is_async:
|
|
1441
|
+
# async for item in iterate_..._async(...): yield item
|
|
1442
|
+
body.append(
|
|
1443
|
+
ast.AsyncFor(
|
|
1444
|
+
target=ast.Name(id='item', ctx=ast.Store()),
|
|
1445
|
+
iter=iterate_call,
|
|
1446
|
+
body=[ast.Expr(value=ast.Yield(value=_name('item')))],
|
|
1447
|
+
orelse=[],
|
|
1448
|
+
)
|
|
1449
|
+
)
|
|
1450
|
+
else:
|
|
1451
|
+
# yield from iterate_...()
|
|
1452
|
+
body.append(ast.Expr(value=ast.YieldFrom(value=iterate_call)))
|
|
1453
|
+
|
|
1454
|
+
return body
|
|
1455
|
+
|
|
1456
|
+
def _build_fetch_page_function(self) -> ast.FunctionDef | ast.AsyncFunctionDef:
|
|
1457
|
+
"""Build the inner fetch_page function for pagination."""
|
|
1458
|
+
pag_config = self.config.pagination_config or {}
|
|
1459
|
+
|
|
1460
|
+
# Determine parameter names based on pagination style
|
|
1461
|
+
if self.config.pagination_style == PaginationStyle.OFFSET:
|
|
1462
|
+
param1_name = 'off'
|
|
1463
|
+
param2_name = 'limit'
|
|
1464
|
+
param1_api_name = pag_config.get('offset_param', 'offset')
|
|
1465
|
+
param2_api_name = pag_config.get('limit_param', 'limit')
|
|
1466
|
+
elif self.config.pagination_style == PaginationStyle.CURSOR:
|
|
1467
|
+
param1_name = 'cur'
|
|
1468
|
+
param2_name = 'limit'
|
|
1469
|
+
param1_api_name = pag_config.get('cursor_param', 'cursor')
|
|
1470
|
+
param2_api_name = pag_config.get('limit_param', 'limit')
|
|
1471
|
+
elif self.config.pagination_style == PaginationStyle.PAGE:
|
|
1472
|
+
param1_name = 'pg'
|
|
1473
|
+
param2_name = 'per_page'
|
|
1474
|
+
param1_api_name = pag_config.get('page_param', 'page')
|
|
1475
|
+
param2_api_name = pag_config.get('per_page_param', 'per_page')
|
|
1476
|
+
else:
|
|
1477
|
+
param1_name = 'param1'
|
|
1478
|
+
param2_name = 'param2'
|
|
1479
|
+
param1_api_name = 'param1'
|
|
1480
|
+
param2_api_name = 'param2'
|
|
1481
|
+
|
|
1482
|
+
# Build the params dict for the request
|
|
1483
|
+
# Start with static params from original endpoint parameters
|
|
1484
|
+
param_keys = []
|
|
1485
|
+
param_values = []
|
|
1486
|
+
|
|
1487
|
+
# Add pagination params
|
|
1488
|
+
param_keys.append(ast.Constant(value=param1_api_name))
|
|
1489
|
+
param_values.append(_name(param1_name))
|
|
1490
|
+
param_keys.append(ast.Constant(value=param2_api_name))
|
|
1491
|
+
param_values.append(_name(param2_name))
|
|
1492
|
+
|
|
1493
|
+
# Add any other query parameters from the original endpoint
|
|
1494
|
+
if self.config.parameters:
|
|
1495
|
+
for param in self.config.parameters:
|
|
1496
|
+
if param.location == 'query':
|
|
1497
|
+
# Skip pagination params we're handling
|
|
1498
|
+
skip_params = {
|
|
1499
|
+
pag_config.get('offset_param'),
|
|
1500
|
+
pag_config.get('limit_param'),
|
|
1501
|
+
pag_config.get('cursor_param'),
|
|
1502
|
+
pag_config.get('page_param'),
|
|
1503
|
+
pag_config.get('per_page_param'),
|
|
1504
|
+
}
|
|
1505
|
+
if param.name not in skip_params:
|
|
1506
|
+
param_keys.append(ast.Constant(value=param.name))
|
|
1507
|
+
param_values.append(_name(param.name_sanitized))
|
|
1508
|
+
|
|
1509
|
+
params_dict = ast.Dict(keys=param_keys, values=param_values)
|
|
1510
|
+
|
|
1511
|
+
# Build request call
|
|
1512
|
+
request_method = '_request_async' if self.config.is_async else '_request'
|
|
1513
|
+
|
|
1514
|
+
# Build path expression (handle path parameters)
|
|
1515
|
+
path_expr = ParameterASTBuilder.build_path_expr(
|
|
1516
|
+
self.config.path, self.config.parameters or []
|
|
1517
|
+
)
|
|
1518
|
+
|
|
1519
|
+
request_keywords = [
|
|
1520
|
+
ast.keyword(
|
|
1521
|
+
arg='method', value=ast.Constant(value=self.config.method.lower())
|
|
1522
|
+
),
|
|
1523
|
+
ast.keyword(arg='path', value=path_expr),
|
|
1524
|
+
ast.keyword(arg='params', value=params_dict),
|
|
1525
|
+
ast.keyword(arg=None, value=_name('kwargs')),
|
|
1526
|
+
]
|
|
1527
|
+
|
|
1528
|
+
request_call = _call(
|
|
1529
|
+
func=_attr('c', request_method),
|
|
1530
|
+
keywords=request_keywords,
|
|
1531
|
+
)
|
|
1532
|
+
|
|
1533
|
+
if self.config.is_async:
|
|
1534
|
+
request_call = ast.Await(value=request_call)
|
|
1535
|
+
|
|
1536
|
+
# Build parse response call
|
|
1537
|
+
if self.config.response_type:
|
|
1538
|
+
parse_method = (
|
|
1539
|
+
'_parse_response_async' if self.config.is_async else '_parse_response'
|
|
1540
|
+
)
|
|
1541
|
+
parse_call = _call(
|
|
1542
|
+
func=_attr('c', parse_method),
|
|
1543
|
+
args=[_name('response'), self.config.response_type.annotation_ast],
|
|
1544
|
+
)
|
|
1545
|
+
if self.config.is_async:
|
|
1546
|
+
parse_call = ast.Await(value=parse_call)
|
|
1547
|
+
|
|
1548
|
+
fetch_body = [
|
|
1549
|
+
_assign(_name('response'), request_call),
|
|
1550
|
+
ast.Return(value=parse_call),
|
|
1551
|
+
]
|
|
1552
|
+
else:
|
|
1553
|
+
fetch_body = [
|
|
1554
|
+
ast.Return(value=request_call),
|
|
1555
|
+
]
|
|
1556
|
+
|
|
1557
|
+
# Determine param1 annotation
|
|
1558
|
+
if self.config.pagination_style == PaginationStyle.CURSOR:
|
|
1559
|
+
param1_annotation = _union_expr([_name('str'), ast.Constant(value=None)])
|
|
1560
|
+
else:
|
|
1561
|
+
param1_annotation = _name('int')
|
|
1562
|
+
|
|
1563
|
+
fetch_args = [
|
|
1564
|
+
_argument(param1_name, param1_annotation),
|
|
1565
|
+
_argument(param2_name, _name('int')),
|
|
1566
|
+
]
|
|
1567
|
+
|
|
1568
|
+
if self.config.is_async:
|
|
1569
|
+
return ast.AsyncFunctionDef(
|
|
1570
|
+
name='fetch_page',
|
|
1571
|
+
args=ast.arguments(
|
|
1572
|
+
posonlyargs=[],
|
|
1573
|
+
args=fetch_args,
|
|
1574
|
+
kwonlyargs=[],
|
|
1575
|
+
kw_defaults=[],
|
|
1576
|
+
kwarg=None,
|
|
1577
|
+
defaults=[],
|
|
1578
|
+
),
|
|
1579
|
+
body=fetch_body,
|
|
1580
|
+
decorator_list=[],
|
|
1581
|
+
returns=None,
|
|
1582
|
+
)
|
|
1583
|
+
else:
|
|
1584
|
+
return ast.FunctionDef(
|
|
1585
|
+
name='fetch_page',
|
|
1586
|
+
args=ast.arguments(
|
|
1587
|
+
posonlyargs=[],
|
|
1588
|
+
args=fetch_args,
|
|
1589
|
+
kwonlyargs=[],
|
|
1590
|
+
kw_defaults=[],
|
|
1591
|
+
kwarg=None,
|
|
1592
|
+
defaults=[],
|
|
1593
|
+
),
|
|
1594
|
+
body=fetch_body,
|
|
1595
|
+
decorator_list=[],
|
|
1596
|
+
returns=None,
|
|
1597
|
+
)
|
|
1598
|
+
|
|
1599
|
+
def _build_extract_lambda(self, path: str | None) -> ast.expr:
|
|
1600
|
+
"""Build a lambda to extract data from a response using a path."""
|
|
1601
|
+
page_arg = ast.arg(arg='page', annotation=None)
|
|
1602
|
+
|
|
1603
|
+
if path:
|
|
1604
|
+
# Check if path looks like a simple attribute access (no dots)
|
|
1605
|
+
if '.' not in path:
|
|
1606
|
+
# lambda page: page.attr
|
|
1607
|
+
body = ast.Attribute(
|
|
1608
|
+
value=ast.Name(id='page', ctx=ast.Load()),
|
|
1609
|
+
attr=path,
|
|
1610
|
+
ctx=ast.Load(),
|
|
1611
|
+
)
|
|
1612
|
+
else:
|
|
1613
|
+
# lambda page: extract_path(page, "path")
|
|
1614
|
+
body = ast.Call(
|
|
1615
|
+
func=_name('extract_path'),
|
|
1616
|
+
args=[
|
|
1617
|
+
ast.Name(id='page', ctx=ast.Load()),
|
|
1618
|
+
ast.Constant(value=path),
|
|
1619
|
+
],
|
|
1620
|
+
keywords=[],
|
|
1621
|
+
)
|
|
1622
|
+
else:
|
|
1623
|
+
# lambda page: page
|
|
1624
|
+
body = ast.Name(id='page', ctx=ast.Load())
|
|
1625
|
+
|
|
1626
|
+
return ast.Lambda(
|
|
1627
|
+
args=ast.arguments(
|
|
1628
|
+
posonlyargs=[],
|
|
1629
|
+
args=[page_arg],
|
|
1630
|
+
kwonlyargs=[],
|
|
1631
|
+
kw_defaults=[],
|
|
1632
|
+
defaults=[],
|
|
1633
|
+
),
|
|
1634
|
+
body=body,
|
|
1635
|
+
)
|
|
1636
|
+
|
|
1637
|
+
def _build_request_keywords(self) -> list[ast.keyword]:
|
|
1638
|
+
"""Build the keywords for a request call."""
|
|
1639
|
+
parameters = self.config.parameters or []
|
|
1640
|
+
|
|
1641
|
+
path_expr = ParameterASTBuilder.build_path_expr(self.config.path, parameters)
|
|
1642
|
+
query_params = ParameterASTBuilder.build_query_params(parameters)
|
|
1643
|
+
header_params = ParameterASTBuilder.build_header_params(parameters)
|
|
1644
|
+
body_expr, body_param_name = ParameterASTBuilder.build_body_expr(
|
|
1645
|
+
self.config.request_body_info
|
|
1646
|
+
)
|
|
1647
|
+
|
|
1648
|
+
request_keywords: list[ast.keyword] = [
|
|
1649
|
+
ast.keyword(
|
|
1650
|
+
arg='method', value=ast.Constant(value=self.config.method.lower())
|
|
1651
|
+
),
|
|
1652
|
+
ast.keyword(arg='path', value=path_expr),
|
|
1653
|
+
]
|
|
1654
|
+
|
|
1655
|
+
if query_params:
|
|
1656
|
+
request_keywords.append(ast.keyword(arg='params', value=query_params))
|
|
1657
|
+
|
|
1658
|
+
if header_params:
|
|
1659
|
+
request_keywords.append(ast.keyword(arg='headers', value=header_params))
|
|
1660
|
+
|
|
1661
|
+
if body_expr and body_param_name:
|
|
1662
|
+
request_keywords.append(ast.keyword(arg=body_param_name, value=body_expr))
|
|
1663
|
+
|
|
1664
|
+
request_keywords.append(ast.keyword(arg=None, value=_name('kwargs')))
|
|
1665
|
+
|
|
1666
|
+
return request_keywords
|
|
1667
|
+
|
|
1668
|
+
def _build_delegating_call_args(
|
|
1669
|
+
self,
|
|
1670
|
+
) -> tuple[list[ast.expr], list[ast.keyword]]:
|
|
1671
|
+
"""Build the arguments for a delegating client method call."""
|
|
1672
|
+
call_args: list[ast.expr] = []
|
|
1673
|
+
call_keywords: list[ast.keyword] = []
|
|
1674
|
+
|
|
1675
|
+
builder = FunctionSignatureBuilder()
|
|
1676
|
+
builder.add_parameters(self.config.parameters)
|
|
1677
|
+
builder.add_request_body(self.config.request_body_info)
|
|
1678
|
+
|
|
1679
|
+
if self.config.dataframe_library:
|
|
1680
|
+
builder.add_path_parameter(default=self.config.dataframe_path)
|
|
1681
|
+
|
|
1682
|
+
signature = builder.build()
|
|
3
1683
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
_assign,
|
|
7
|
-
_async_func,
|
|
8
|
-
_attr,
|
|
9
|
-
_call,
|
|
10
|
-
_func,
|
|
11
|
-
_name,
|
|
12
|
-
_subscript,
|
|
13
|
-
_union_expr,
|
|
14
|
-
)
|
|
15
|
-
from otterapi.codegen.type_generator import Parameter, Type
|
|
1684
|
+
for arg in signature.args:
|
|
1685
|
+
call_args.append(_name(arg.arg))
|
|
16
1686
|
|
|
1687
|
+
for kwarg in signature.kwonlyargs:
|
|
1688
|
+
call_keywords.append(ast.keyword(arg=kwarg.arg, value=_name(kwarg.arg)))
|
|
17
1689
|
|
|
18
|
-
|
|
19
|
-
|
|
1690
|
+
call_keywords.append(ast.keyword(arg=None, value=_name('kwargs')))
|
|
1691
|
+
|
|
1692
|
+
return call_args, call_keywords
|
|
1693
|
+
|
|
1694
|
+
|
|
1695
|
+
# =============================================================================
|
|
1696
|
+
# Base Request Function Generation
|
|
1697
|
+
# =============================================================================
|
|
20
1698
|
|
|
21
1699
|
|
|
22
|
-
def
|
|
1700
|
+
def _get_base_request_arguments() -> tuple[list[ast.arg], list[ast.arg], ast.arg]:
|
|
1701
|
+
"""Build the argument signature for base request functions.
|
|
1702
|
+
|
|
1703
|
+
Returns:
|
|
1704
|
+
A tuple of (args, kwonlyargs, kwargs).
|
|
1705
|
+
"""
|
|
23
1706
|
args = [
|
|
24
1707
|
_argument('method', _name('str')),
|
|
25
1708
|
_argument('path', _name('str')),
|
|
@@ -30,36 +1713,47 @@ def get_base_request_arguments():
|
|
|
30
1713
|
_union_expr([_subscript('Type', _name('T')), ast.Constant(value=None)]),
|
|
31
1714
|
),
|
|
32
1715
|
_argument('supported_status_codes', _subscript('list', _name('int'))),
|
|
1716
|
+
_argument('timeout', _union_expr([_name('float'), ast.Constant(value=None)])),
|
|
1717
|
+
_argument('stream', _name('bool')),
|
|
33
1718
|
]
|
|
34
1719
|
kwargs = _argument('kwargs', _name('dict'))
|
|
35
1720
|
|
|
36
1721
|
return args, kwonlyargs, kwargs
|
|
37
1722
|
|
|
38
1723
|
|
|
39
|
-
def
|
|
40
|
-
|
|
1724
|
+
def _build_url_fstring(base_url_var: str = 'BASE_URL') -> ast.JoinedStr:
|
|
1725
|
+
"""Build an f-string AST node for URL construction.
|
|
41
1726
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
1727
|
+
Args:
|
|
1728
|
+
base_url_var: The variable name containing the base URL.
|
|
1729
|
+
|
|
1730
|
+
Returns:
|
|
1731
|
+
An AST JoinedStr node representing f"{base_url_var}{path}".
|
|
1732
|
+
"""
|
|
1733
|
+
return ast.JoinedStr(
|
|
1734
|
+
values=[
|
|
1735
|
+
ast.FormattedValue(value=_name(base_url_var), conversion=-1),
|
|
1736
|
+
ast.FormattedValue(value=_name('path'), conversion=-1),
|
|
1737
|
+
]
|
|
1738
|
+
)
|
|
1739
|
+
|
|
1740
|
+
|
|
1741
|
+
def _build_response_validation_body(is_async: bool = False) -> list[ast.stmt]:
|
|
1742
|
+
"""Build the shared response validation AST statements.
|
|
1743
|
+
|
|
1744
|
+
Args:
|
|
1745
|
+
is_async: Whether this is for async functions.
|
|
1746
|
+
|
|
1747
|
+
Returns:
|
|
1748
|
+
List of AST statements for response validation.
|
|
1749
|
+
"""
|
|
1750
|
+
stream_check = ast.If(
|
|
1751
|
+
test=_name('stream'),
|
|
1752
|
+
body=[ast.Return(value=_name('response'))],
|
|
1753
|
+
orelse=[],
|
|
1754
|
+
)
|
|
1755
|
+
|
|
1756
|
+
return [
|
|
63
1757
|
ast.If(
|
|
64
1758
|
test=ast.BoolOp(
|
|
65
1759
|
op=ast.Or(),
|
|
@@ -72,129 +1766,36 @@ def base_request_fn():
|
|
|
72
1766
|
),
|
|
73
1767
|
],
|
|
74
1768
|
),
|
|
75
|
-
body=[
|
|
76
|
-
ast.Expr(
|
|
77
|
-
value=_call(
|
|
78
|
-
func=_attr('response', 'raise_for_status'),
|
|
79
|
-
)
|
|
80
|
-
)
|
|
81
|
-
],
|
|
1769
|
+
body=[ast.Expr(value=_call(func=_attr('response', 'raise_for_status')))],
|
|
82
1770
|
orelse=[],
|
|
83
1771
|
),
|
|
1772
|
+
stream_check,
|
|
84
1773
|
_assign(
|
|
85
|
-
target=_name('
|
|
1774
|
+
target=_name('content_type'),
|
|
86
1775
|
value=_call(
|
|
87
|
-
func=_attr('response', '
|
|
1776
|
+
func=_attr(_attr('response', 'headers'), 'get'),
|
|
1777
|
+
args=[ast.Constant(value='content-type'), ast.Constant(value='')],
|
|
88
1778
|
),
|
|
89
1779
|
),
|
|
90
1780
|
ast.If(
|
|
91
|
-
test=ast.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
1781
|
+
test=ast.BoolOp(
|
|
1782
|
+
op=ast.Or(),
|
|
1783
|
+
values=[
|
|
1784
|
+
ast.Compare(
|
|
1785
|
+
left=ast.Constant(value='application/json'),
|
|
1786
|
+
ops=[ast.In()],
|
|
1787
|
+
comparators=[_name('content_type')],
|
|
1788
|
+
),
|
|
99
1789
|
_call(
|
|
100
|
-
func=_name('
|
|
101
|
-
args=[
|
|
1790
|
+
func=_attr(_name('content_type'), 'endswith'),
|
|
1791
|
+
args=[ast.Constant(value='+json')],
|
|
102
1792
|
),
|
|
103
|
-
|
|
104
|
-
),
|
|
105
|
-
args=[_name('data')],
|
|
106
|
-
),
|
|
107
|
-
),
|
|
108
|
-
ast.If(
|
|
109
|
-
test=_call(
|
|
110
|
-
func=_name('isinstance'),
|
|
111
|
-
args=[_name('validated_data'), _name('RootModel')],
|
|
1793
|
+
],
|
|
112
1794
|
),
|
|
113
|
-
body=[ast.Return(value=_attr('validated_data', 'root'))],
|
|
114
|
-
orelse=[],
|
|
115
|
-
),
|
|
116
|
-
ast.Return(value=_name('validated_data')),
|
|
117
|
-
]
|
|
118
|
-
|
|
119
|
-
return _func(
|
|
120
|
-
name='request_sync',
|
|
121
|
-
args=args,
|
|
122
|
-
body=body,
|
|
123
|
-
kwargs=kwargs,
|
|
124
|
-
kwonlyargs=kwonlyargs,
|
|
125
|
-
kw_defaults=[_name('Json'), ast.Constant(value=None)],
|
|
126
|
-
returns=_name('T'),
|
|
127
|
-
), {
|
|
128
|
-
'httpx': ['request'],
|
|
129
|
-
'pydantic': ['TypeAdapter', 'Json', 'RootModel'],
|
|
130
|
-
'typing': ['Type', 'TypeVar', 'Union'],
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def base_async_request_fn():
|
|
135
|
-
args, kwonlyargs, kwargs = get_base_request_arguments()
|
|
136
|
-
|
|
137
|
-
body = [
|
|
138
|
-
ast.AsyncWith(
|
|
139
|
-
items=[
|
|
140
|
-
ast.withitem(
|
|
141
|
-
context_expr=_call(func=_name('AsyncClient')),
|
|
142
|
-
optional_vars=_name('client'),
|
|
143
|
-
)
|
|
144
|
-
],
|
|
145
1795
|
body=[
|
|
146
|
-
_assign(
|
|
147
|
-
target=_name('response'),
|
|
148
|
-
value=ast.Await(
|
|
149
|
-
value=_call(
|
|
150
|
-
func=_attr('client', 'request'),
|
|
151
|
-
args=[
|
|
152
|
-
_name('method'),
|
|
153
|
-
ast.Expr(
|
|
154
|
-
value=ast.JoinedStr(
|
|
155
|
-
values=[
|
|
156
|
-
ast.FormattedValue(
|
|
157
|
-
value=_name('BASE_URL'), conversion=-1
|
|
158
|
-
),
|
|
159
|
-
ast.FormattedValue(
|
|
160
|
-
value=_name('path'), conversion=-1
|
|
161
|
-
),
|
|
162
|
-
]
|
|
163
|
-
)
|
|
164
|
-
),
|
|
165
|
-
],
|
|
166
|
-
keywords=[ast.keyword(arg=None, value=_name('kwargs'))],
|
|
167
|
-
)
|
|
168
|
-
),
|
|
169
|
-
),
|
|
170
|
-
ast.If(
|
|
171
|
-
test=ast.BoolOp(
|
|
172
|
-
op=ast.Or(),
|
|
173
|
-
values=[
|
|
174
|
-
ast.UnaryOp(
|
|
175
|
-
op=ast.Not(), operand=_name('supported_status_codes')
|
|
176
|
-
),
|
|
177
|
-
ast.Compare(
|
|
178
|
-
left=_attr('response', 'status_code'),
|
|
179
|
-
ops=[ast.NotIn()],
|
|
180
|
-
comparators=[_name('supported_status_codes')],
|
|
181
|
-
),
|
|
182
|
-
],
|
|
183
|
-
),
|
|
184
|
-
body=[
|
|
185
|
-
ast.Expr(
|
|
186
|
-
value=_call(
|
|
187
|
-
func=_attr('response', 'raise_for_status'),
|
|
188
|
-
)
|
|
189
|
-
)
|
|
190
|
-
],
|
|
191
|
-
orelse=[],
|
|
192
|
-
),
|
|
193
1796
|
_assign(
|
|
194
1797
|
target=_name('data'),
|
|
195
|
-
value=_call(
|
|
196
|
-
func=_attr('response', 'json'),
|
|
197
|
-
),
|
|
1798
|
+
value=_call(func=_attr('response', 'json')),
|
|
198
1799
|
),
|
|
199
1800
|
ast.If(
|
|
200
1801
|
test=ast.UnaryOp(op=ast.Not(), operand=_name('response_model')),
|
|
@@ -224,278 +1825,931 @@ def base_async_request_fn():
|
|
|
224
1825
|
),
|
|
225
1826
|
ast.Return(value=_name('validated_data')),
|
|
226
1827
|
],
|
|
227
|
-
|
|
1828
|
+
orelse=[
|
|
1829
|
+
ast.If(
|
|
1830
|
+
test=_call(
|
|
1831
|
+
func=_attr(_name('content_type'), 'startswith'),
|
|
1832
|
+
args=[ast.Constant(value='text/')],
|
|
1833
|
+
),
|
|
1834
|
+
body=[
|
|
1835
|
+
ast.Return(value=_attr('response', 'text')),
|
|
1836
|
+
],
|
|
1837
|
+
orelse=[
|
|
1838
|
+
ast.If(
|
|
1839
|
+
test=ast.BoolOp(
|
|
1840
|
+
op=ast.Or(),
|
|
1841
|
+
values=[
|
|
1842
|
+
ast.Compare(
|
|
1843
|
+
left=ast.Constant(
|
|
1844
|
+
value='application/octet-stream'
|
|
1845
|
+
),
|
|
1846
|
+
ops=[ast.In()],
|
|
1847
|
+
comparators=[_name('content_type')],
|
|
1848
|
+
),
|
|
1849
|
+
_call(
|
|
1850
|
+
func=_attr(_name('content_type'), 'startswith'),
|
|
1851
|
+
args=[ast.Constant(value='image/')],
|
|
1852
|
+
),
|
|
1853
|
+
_call(
|
|
1854
|
+
func=_attr(_name('content_type'), 'startswith'),
|
|
1855
|
+
args=[ast.Constant(value='audio/')],
|
|
1856
|
+
),
|
|
1857
|
+
_call(
|
|
1858
|
+
func=_attr(_name('content_type'), 'startswith'),
|
|
1859
|
+
args=[ast.Constant(value='video/')],
|
|
1860
|
+
),
|
|
1861
|
+
_call(
|
|
1862
|
+
func=_attr(_name('content_type'), 'startswith'),
|
|
1863
|
+
args=[ast.Constant(value='application/pdf')],
|
|
1864
|
+
),
|
|
1865
|
+
],
|
|
1866
|
+
),
|
|
1867
|
+
body=[
|
|
1868
|
+
ast.Return(value=_attr('response', 'content')),
|
|
1869
|
+
],
|
|
1870
|
+
orelse=[
|
|
1871
|
+
ast.Return(value=_name('response')),
|
|
1872
|
+
],
|
|
1873
|
+
),
|
|
1874
|
+
],
|
|
1875
|
+
),
|
|
1876
|
+
],
|
|
1877
|
+
),
|
|
1878
|
+
]
|
|
1879
|
+
|
|
1880
|
+
|
|
1881
|
+
def _build_base_request_fn(
|
|
1882
|
+
is_async: bool,
|
|
1883
|
+
base_url_var: str = 'BASE_URL',
|
|
1884
|
+
) -> tuple[ast.FunctionDef | ast.AsyncFunctionDef, ImportDict]:
|
|
1885
|
+
"""Build a base request function (sync or async).
|
|
1886
|
+
|
|
1887
|
+
Args:
|
|
1888
|
+
is_async: If True, generates an async function; otherwise sync.
|
|
1889
|
+
base_url_var: The variable name containing the base URL configuration.
|
|
1890
|
+
|
|
1891
|
+
Returns:
|
|
1892
|
+
A tuple of (function_ast, imports).
|
|
1893
|
+
"""
|
|
1894
|
+
args, kwonlyargs, kwargs = _get_base_request_arguments()
|
|
1895
|
+
url_fstring = _build_url_fstring(base_url_var)
|
|
1896
|
+
validation_body = _build_response_validation_body(is_async)
|
|
1897
|
+
|
|
1898
|
+
request_keywords = [
|
|
1899
|
+
ast.keyword(arg=None, value=_name('kwargs')),
|
|
1900
|
+
ast.keyword(
|
|
1901
|
+
arg='timeout',
|
|
1902
|
+
value=_name('timeout'),
|
|
1903
|
+
),
|
|
228
1904
|
]
|
|
229
1905
|
|
|
230
|
-
|
|
231
|
-
|
|
1906
|
+
if is_async:
|
|
1907
|
+
request_call = ast.Await(
|
|
1908
|
+
value=_call(
|
|
1909
|
+
func=_attr('client', 'request'),
|
|
1910
|
+
args=[_name('method'), ast.Expr(value=url_fstring)],
|
|
1911
|
+
keywords=request_keywords,
|
|
1912
|
+
)
|
|
1913
|
+
)
|
|
1914
|
+
inner_body = [
|
|
1915
|
+
_assign(target=_name('response'), value=request_call),
|
|
1916
|
+
*validation_body,
|
|
1917
|
+
]
|
|
1918
|
+
body = [
|
|
1919
|
+
ast.AsyncWith(
|
|
1920
|
+
items=[
|
|
1921
|
+
ast.withitem(
|
|
1922
|
+
context_expr=_call(func=_name('AsyncClient')),
|
|
1923
|
+
optional_vars=_name('client'),
|
|
1924
|
+
)
|
|
1925
|
+
],
|
|
1926
|
+
body=inner_body,
|
|
1927
|
+
)
|
|
1928
|
+
]
|
|
1929
|
+
func_builder = _async_func
|
|
1930
|
+
func_name = 'request_async'
|
|
1931
|
+
http_imports = {'httpx': {'AsyncClient'}}
|
|
1932
|
+
else:
|
|
1933
|
+
request_call = _call(
|
|
1934
|
+
func=_name('request'),
|
|
1935
|
+
args=[_name('method'), ast.Expr(value=url_fstring)],
|
|
1936
|
+
keywords=request_keywords,
|
|
1937
|
+
)
|
|
1938
|
+
body = [
|
|
1939
|
+
_assign(target=_name('response'), value=request_call),
|
|
1940
|
+
*validation_body,
|
|
1941
|
+
]
|
|
1942
|
+
func_builder = _func
|
|
1943
|
+
func_name = 'request_sync'
|
|
1944
|
+
http_imports = {'httpx': {'request'}}
|
|
1945
|
+
|
|
1946
|
+
func_ast = func_builder(
|
|
1947
|
+
name=func_name,
|
|
232
1948
|
args=args,
|
|
233
1949
|
body=body,
|
|
234
1950
|
kwargs=kwargs,
|
|
235
1951
|
kwonlyargs=kwonlyargs,
|
|
236
|
-
kw_defaults=[
|
|
1952
|
+
kw_defaults=[
|
|
1953
|
+
_name('Json'),
|
|
1954
|
+
ast.Constant(value=None),
|
|
1955
|
+
ast.Constant(value=None),
|
|
1956
|
+
ast.Constant(value=False),
|
|
1957
|
+
],
|
|
237
1958
|
returns=_name('T'),
|
|
238
|
-
)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
1959
|
+
)
|
|
1960
|
+
|
|
1961
|
+
imports: ImportDict = {
|
|
1962
|
+
**http_imports,
|
|
1963
|
+
'pydantic': {'TypeAdapter', 'Json', 'RootModel'},
|
|
1964
|
+
'typing': {'Type', 'TypeVar'},
|
|
242
1965
|
}
|
|
243
1966
|
|
|
1967
|
+
return func_ast, imports
|
|
1968
|
+
|
|
1969
|
+
|
|
1970
|
+
def base_request_fn(
|
|
1971
|
+
base_url_var: str = 'BASE_URL',
|
|
1972
|
+
) -> tuple[ast.FunctionDef, ImportDict]:
|
|
1973
|
+
"""Generate a synchronous base request function.
|
|
1974
|
+
|
|
1975
|
+
Creates a `request_sync` function that handles HTTP requests with response
|
|
1976
|
+
validation and model deserialization.
|
|
1977
|
+
|
|
1978
|
+
Args:
|
|
1979
|
+
base_url_var: The variable name containing the base URL configuration.
|
|
1980
|
+
|
|
1981
|
+
Returns:
|
|
1982
|
+
A tuple of (function_ast, imports) for the sync request function.
|
|
1983
|
+
"""
|
|
1984
|
+
return _build_base_request_fn(is_async=False, base_url_var=base_url_var)
|
|
1985
|
+
|
|
1986
|
+
|
|
1987
|
+
def base_async_request_fn(
|
|
1988
|
+
base_url_var: str = 'BASE_URL',
|
|
1989
|
+
) -> tuple[ast.AsyncFunctionDef, ImportDict]:
|
|
1990
|
+
"""Generate an asynchronous base request function.
|
|
1991
|
+
|
|
1992
|
+
Creates a `request_async` function that handles HTTP requests with response
|
|
1993
|
+
validation and model deserialization using httpx.AsyncClient.
|
|
1994
|
+
|
|
1995
|
+
Args:
|
|
1996
|
+
base_url_var: The variable name containing the base URL configuration.
|
|
1997
|
+
|
|
1998
|
+
Returns:
|
|
1999
|
+
A tuple of (function_ast, imports) for the async request function.
|
|
2000
|
+
"""
|
|
2001
|
+
return _build_base_request_fn(is_async=True, base_url_var=base_url_var)
|
|
2002
|
+
|
|
2003
|
+
|
|
2004
|
+
# =============================================================================
|
|
2005
|
+
# Parameter and Call Building Helpers
|
|
2006
|
+
# =============================================================================
|
|
2007
|
+
|
|
244
2008
|
|
|
245
2009
|
def get_parameters(
|
|
246
|
-
parameters: list[Parameter],
|
|
247
|
-
) -> tuple[list[ast.arg], list[ast.arg], list[ast.expr],
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
2010
|
+
parameters: list['Parameter'],
|
|
2011
|
+
) -> tuple[list[ast.arg], list[ast.arg], list[ast.expr], ImportDict]:
|
|
2012
|
+
"""Extract function arguments from OpenAPI parameters.
|
|
2013
|
+
|
|
2014
|
+
Separates required and optional parameters and generates appropriate
|
|
2015
|
+
AST argument nodes for each.
|
|
2016
|
+
|
|
2017
|
+
Args:
|
|
2018
|
+
parameters: List of Parameter objects from the OpenAPI spec.
|
|
2019
|
+
|
|
2020
|
+
Returns:
|
|
2021
|
+
A tuple of (args, kwonlyargs, kw_defaults, imports).
|
|
2022
|
+
"""
|
|
2023
|
+
args: list[ast.arg] = []
|
|
2024
|
+
kwonlyargs: list[ast.arg] = []
|
|
2025
|
+
kw_defaults: list[ast.expr] = []
|
|
2026
|
+
imports: ImportDict = {}
|
|
252
2027
|
|
|
253
2028
|
for param in parameters:
|
|
254
|
-
if param.
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if param_type is None:
|
|
261
|
-
param_type = _name('Any')
|
|
2029
|
+
if param.type:
|
|
2030
|
+
annotation = param.type.annotation_ast
|
|
2031
|
+
imports.update(param.type.annotation_imports)
|
|
2032
|
+
else:
|
|
2033
|
+
annotation = _name('Any')
|
|
262
2034
|
imports.setdefault('typing', set()).add('Any')
|
|
263
2035
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
2036
|
+
arg = _argument(param.name_sanitized, annotation)
|
|
2037
|
+
|
|
2038
|
+
if param.required:
|
|
2039
|
+
args.append(arg)
|
|
267
2040
|
else:
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
kw_defaults.append(ast.Constant(None))
|
|
2041
|
+
kwonlyargs.append(arg)
|
|
2042
|
+
kw_defaults.append(ast.Constant(value=None))
|
|
271
2043
|
|
|
272
2044
|
return args, kwonlyargs, kw_defaults, imports
|
|
273
2045
|
|
|
274
2046
|
|
|
275
2047
|
def get_base_call_keywords(
|
|
276
|
-
|
|
2048
|
+
query_params: ast.expr | None,
|
|
2049
|
+
header_params: ast.expr | None,
|
|
2050
|
+
response_model_ast: ast.expr | None,
|
|
2051
|
+
supported_status_codes: list[int] | None,
|
|
2052
|
+
body_expr: ast.expr | None = None,
|
|
2053
|
+
body_param_name: str | None = None,
|
|
2054
|
+
timeout: float | None = None,
|
|
2055
|
+
stream: bool = False,
|
|
277
2056
|
) -> list[ast.keyword]:
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
2057
|
+
"""Build the keyword arguments for a request function call.
|
|
2058
|
+
|
|
2059
|
+
Args:
|
|
2060
|
+
query_params: AST expression for query parameters dictionary.
|
|
2061
|
+
header_params: AST expression for header parameters dictionary.
|
|
2062
|
+
response_model_ast: AST expression for the response model type.
|
|
2063
|
+
supported_status_codes: List of valid HTTP status codes.
|
|
2064
|
+
body_expr: AST expression for the request body.
|
|
2065
|
+
body_param_name: The httpx parameter name for the body.
|
|
2066
|
+
timeout: Optional request timeout in seconds.
|
|
2067
|
+
stream: Whether to stream the response.
|
|
2068
|
+
|
|
2069
|
+
Returns:
|
|
2070
|
+
List of AST keyword nodes for the function call.
|
|
2071
|
+
"""
|
|
2072
|
+
keywords: list[ast.keyword] = []
|
|
2073
|
+
|
|
2074
|
+
keywords.append(
|
|
287
2075
|
ast.keyword(
|
|
288
|
-
arg='
|
|
289
|
-
)
|
|
290
|
-
|
|
2076
|
+
arg='response_model', value=response_model_ast or ast.Constant(value=None)
|
|
2077
|
+
)
|
|
2078
|
+
)
|
|
2079
|
+
|
|
2080
|
+
if supported_status_codes:
|
|
2081
|
+
keywords.append(
|
|
2082
|
+
ast.keyword(
|
|
2083
|
+
arg='supported_status_codes',
|
|
2084
|
+
value=ast.List(
|
|
2085
|
+
elts=[ast.Constant(value=code) for code in supported_status_codes]
|
|
2086
|
+
),
|
|
2087
|
+
)
|
|
2088
|
+
)
|
|
2089
|
+
else:
|
|
2090
|
+
keywords.append(
|
|
2091
|
+
ast.keyword(arg='supported_status_codes', value=ast.Constant(value=None))
|
|
2092
|
+
)
|
|
291
2093
|
|
|
2094
|
+
if timeout is not None:
|
|
2095
|
+
keywords.append(ast.keyword(arg='timeout', value=ast.Constant(value=timeout)))
|
|
292
2096
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
return None
|
|
2097
|
+
if stream:
|
|
2098
|
+
keywords.append(ast.keyword(arg='stream', value=ast.Constant(value=True)))
|
|
296
2099
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
values=[_name(header.name_sanitized) for header in headers],
|
|
300
|
-
)
|
|
2100
|
+
if query_params:
|
|
2101
|
+
keywords.append(ast.keyword(arg='params', value=query_params))
|
|
301
2102
|
|
|
2103
|
+
if header_params:
|
|
2104
|
+
keywords.append(ast.keyword(arg='headers', value=header_params))
|
|
302
2105
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
return None
|
|
2106
|
+
if body_expr and body_param_name:
|
|
2107
|
+
keywords.append(ast.keyword(arg=body_param_name, value=body_expr))
|
|
306
2108
|
|
|
307
|
-
return
|
|
308
|
-
keys=[ast.Constant(value=query.name) for query in queries],
|
|
309
|
-
values=[_name(query.name_sanitized) for query in queries],
|
|
310
|
-
)
|
|
2109
|
+
return keywords
|
|
311
2110
|
|
|
312
2111
|
|
|
313
|
-
def
|
|
314
|
-
|
|
315
|
-
) -> ast.
|
|
316
|
-
"""Build
|
|
317
|
-
if not paths:
|
|
318
|
-
return ast.Constant(value=path)
|
|
319
|
-
|
|
320
|
-
# Split the path into parts and build the f-string
|
|
321
|
-
values = []
|
|
322
|
-
current_pos = 0
|
|
323
|
-
|
|
324
|
-
for path_param in paths:
|
|
325
|
-
param_placeholder = f'{{{path_param.name}}}'
|
|
326
|
-
param_pos = path.find(param_placeholder, current_pos)
|
|
327
|
-
|
|
328
|
-
if param_pos != -1:
|
|
329
|
-
# Add any literal text before the parameter
|
|
330
|
-
if param_pos > current_pos:
|
|
331
|
-
literal_text = path[current_pos:param_pos]
|
|
332
|
-
values.append(ast.Constant(value=literal_text))
|
|
333
|
-
|
|
334
|
-
# Add the formatted value for the parameter
|
|
335
|
-
values.append(
|
|
336
|
-
ast.FormattedValue(
|
|
337
|
-
value=_name(path_param.name_sanitized),
|
|
338
|
-
conversion=-1, # No conversion (default)
|
|
339
|
-
)
|
|
340
|
-
)
|
|
2112
|
+
def build_header_params(
|
|
2113
|
+
parameters: list['Parameter'],
|
|
2114
|
+
) -> ast.Dict | None:
|
|
2115
|
+
"""Build a dictionary AST node for header parameters.
|
|
341
2116
|
|
|
342
|
-
|
|
2117
|
+
Args:
|
|
2118
|
+
parameters: List of Parameter objects to filter for headers.
|
|
343
2119
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
2120
|
+
Returns:
|
|
2121
|
+
An AST Dict node for header parameters, or None if no header params.
|
|
2122
|
+
"""
|
|
2123
|
+
return ParameterASTBuilder.build_header_params(parameters)
|
|
348
2124
|
|
|
349
|
-
return ast.JoinedStr(values=values)
|
|
350
2125
|
|
|
2126
|
+
def build_query_params(
|
|
2127
|
+
parameters: list['Parameter'],
|
|
2128
|
+
) -> ast.Dict | None:
|
|
2129
|
+
"""Build a dictionary AST node for query parameters.
|
|
351
2130
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
return None
|
|
2131
|
+
Args:
|
|
2132
|
+
parameters: List of Parameter objects to filter for query params.
|
|
355
2133
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
2134
|
+
Returns:
|
|
2135
|
+
An AST Dict node for query parameters, or None if no query params.
|
|
2136
|
+
"""
|
|
2137
|
+
return ParameterASTBuilder.build_query_params(parameters)
|
|
2138
|
+
|
|
2139
|
+
|
|
2140
|
+
def build_path_params(
|
|
2141
|
+
path: str,
|
|
2142
|
+
parameters: list['Parameter'],
|
|
2143
|
+
) -> ast.expr:
|
|
2144
|
+
"""Build an f-string or constant for the request path.
|
|
2145
|
+
|
|
2146
|
+
Args:
|
|
2147
|
+
path: The OpenAPI path string with placeholders.
|
|
2148
|
+
parameters: List of Parameter objects to map placeholders to variables.
|
|
2149
|
+
|
|
2150
|
+
Returns:
|
|
2151
|
+
An AST expression (JoinedStr for f-strings, Constant for static paths).
|
|
2152
|
+
"""
|
|
2153
|
+
return ParameterASTBuilder.build_path_expr(path, parameters)
|
|
361
2154
|
|
|
362
|
-
|
|
2155
|
+
|
|
2156
|
+
def build_body_params(
|
|
2157
|
+
body: 'RequestBodyInfo | None',
|
|
2158
|
+
) -> tuple[ast.expr | None, str | None]:
|
|
2159
|
+
"""Build an AST expression for the request body parameter.
|
|
2160
|
+
|
|
2161
|
+
Args:
|
|
2162
|
+
body: The RequestBodyInfo object, or None if no body.
|
|
2163
|
+
|
|
2164
|
+
Returns:
|
|
2165
|
+
A tuple of (body_expr, httpx_param_name).
|
|
2166
|
+
"""
|
|
2167
|
+
return ParameterASTBuilder.build_body_expr(body)
|
|
363
2168
|
|
|
364
2169
|
|
|
365
2170
|
def prepare_call_from_parameters(
|
|
366
|
-
parameters: list[Parameter] | None,
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
query_params = [p for p in parameters if p.location == 'query']
|
|
372
|
-
path_params = [p for p in parameters if p.location == 'path']
|
|
373
|
-
header_params = [p for p in parameters if p.location == 'header']
|
|
374
|
-
body_params = [p for p in parameters if p.location == 'body']
|
|
375
|
-
|
|
376
|
-
if len(body_params) > 1:
|
|
377
|
-
raise ValueError('Multiple body parameters are not supported.')
|
|
378
|
-
|
|
379
|
-
return (
|
|
380
|
-
build_query_params(query_params),
|
|
381
|
-
build_header_params(header_params),
|
|
382
|
-
build_body_params(body_params[0] if len(body_params) == 1 else None),
|
|
383
|
-
build_path_params(path_params, path),
|
|
384
|
-
)
|
|
2171
|
+
parameters: list['Parameter'] | None,
|
|
2172
|
+
path: str,
|
|
2173
|
+
request_body_info: 'RequestBodyInfo | None' = None,
|
|
2174
|
+
) -> tuple[ast.expr | None, ast.expr | None, ast.expr | None, str | None, ast.expr]:
|
|
2175
|
+
"""Prepare all parameter AST nodes for a request function call.
|
|
385
2176
|
|
|
2177
|
+
Args:
|
|
2178
|
+
parameters: List of Parameter objects, or None.
|
|
2179
|
+
path: The API path with optional placeholders.
|
|
2180
|
+
request_body_info: Request body info, or None.
|
|
386
2181
|
|
|
387
|
-
|
|
2182
|
+
Returns:
|
|
2183
|
+
A tuple of (query_params, header_params, body_expr, body_param_name, path_expr).
|
|
2184
|
+
"""
|
|
2185
|
+
return ParameterASTBuilder.prepare_all_params(parameters, path, request_body_info)
|
|
2186
|
+
|
|
2187
|
+
|
|
2188
|
+
# =============================================================================
|
|
2189
|
+
# Endpoint Function Building
|
|
2190
|
+
# =============================================================================
|
|
2191
|
+
|
|
2192
|
+
|
|
2193
|
+
def _build_endpoint_fn(
|
|
388
2194
|
name: str,
|
|
389
2195
|
method: str,
|
|
390
2196
|
path: str,
|
|
391
|
-
response_model: Type,
|
|
2197
|
+
response_model: 'Type | None',
|
|
2198
|
+
is_async: bool,
|
|
392
2199
|
docs: str | None = None,
|
|
393
|
-
parameters: list | None = None,
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
2200
|
+
parameters: list['Parameter'] | None = None,
|
|
2201
|
+
response_infos: list['ResponseInfo'] | None = None,
|
|
2202
|
+
request_body_info: 'RequestBodyInfo | None' = None,
|
|
2203
|
+
) -> tuple[ast.FunctionDef | ast.AsyncFunctionDef, ImportDict]:
|
|
2204
|
+
"""Build an endpoint function (sync or async).
|
|
2205
|
+
|
|
2206
|
+
Args:
|
|
2207
|
+
name: The function name.
|
|
2208
|
+
method: HTTP method (GET, POST, etc.).
|
|
2209
|
+
path: The API path with optional placeholders.
|
|
2210
|
+
response_model: The response Type for validation, or None.
|
|
2211
|
+
is_async: Whether to generate an async function.
|
|
2212
|
+
docs: Optional docstring.
|
|
2213
|
+
parameters: List of Parameter objects.
|
|
2214
|
+
response_infos: List of ResponseInfo objects.
|
|
2215
|
+
request_body_info: RequestBodyInfo object for the body, or None.
|
|
2216
|
+
|
|
2217
|
+
Returns:
|
|
2218
|
+
A tuple of (function_ast, imports).
|
|
2219
|
+
"""
|
|
2220
|
+
imports: ImportDict = {}
|
|
2221
|
+
func_builder = _async_func if is_async else _func
|
|
2222
|
+
base_fn_name = 'request_async' if is_async else 'request_sync'
|
|
2223
|
+
|
|
2224
|
+
if parameters:
|
|
2225
|
+
args, kwonlyargs, kw_defaults, param_imports = get_parameters(parameters)
|
|
2226
|
+
imports.update(param_imports)
|
|
2227
|
+
else:
|
|
2228
|
+
args, kwonlyargs, kw_defaults = [], [], []
|
|
2229
|
+
|
|
2230
|
+
if request_body_info:
|
|
2231
|
+
body_annotation = (
|
|
2232
|
+
request_body_info.type.annotation_ast
|
|
2233
|
+
if request_body_info.type
|
|
2234
|
+
else _name('Any')
|
|
2235
|
+
)
|
|
2236
|
+
if request_body_info.type:
|
|
2237
|
+
imports.update(request_body_info.type.annotation_imports)
|
|
2238
|
+
else:
|
|
2239
|
+
imports.setdefault('typing', set()).add('Any')
|
|
2240
|
+
|
|
2241
|
+
body_arg = _argument('body', body_annotation)
|
|
2242
|
+
if request_body_info.required:
|
|
2243
|
+
args.append(body_arg)
|
|
2244
|
+
else:
|
|
2245
|
+
kwonlyargs.append(body_arg)
|
|
2246
|
+
kw_defaults.append(ast.Constant(value=None))
|
|
397
2247
|
|
|
398
|
-
query_params, header_params,
|
|
399
|
-
prepare_call_from_parameters(parameters, path)
|
|
2248
|
+
query_params, header_params, body_expr, body_param_name, path_expr = (
|
|
2249
|
+
prepare_call_from_parameters(parameters, path, request_body_info)
|
|
400
2250
|
)
|
|
2251
|
+
|
|
2252
|
+
if response_model:
|
|
2253
|
+
response_model_ast = response_model.annotation_ast
|
|
2254
|
+
imports.update(response_model.annotation_imports)
|
|
2255
|
+
else:
|
|
2256
|
+
response_model_ast = ast.Constant(value=None)
|
|
2257
|
+
|
|
2258
|
+
supported_status_codes = None
|
|
2259
|
+
if response_infos:
|
|
2260
|
+
supported_status_codes = [
|
|
2261
|
+
r.status_code for r in response_infos if r.status_code
|
|
2262
|
+
]
|
|
2263
|
+
|
|
401
2264
|
call_keywords = get_base_call_keywords(
|
|
402
|
-
|
|
2265
|
+
query_params=query_params,
|
|
2266
|
+
header_params=header_params,
|
|
2267
|
+
response_model_ast=response_model_ast,
|
|
2268
|
+
supported_status_codes=supported_status_codes,
|
|
2269
|
+
body_expr=body_expr,
|
|
2270
|
+
body_param_name=body_param_name,
|
|
2271
|
+
stream=False,
|
|
403
2272
|
)
|
|
404
2273
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
call_keywords
|
|
409
|
-
|
|
410
|
-
|
|
2274
|
+
call_args = [
|
|
2275
|
+
ast.keyword(arg='method', value=ast.Constant(value=method.lower())),
|
|
2276
|
+
ast.keyword(arg='path', value=path_expr),
|
|
2277
|
+
*call_keywords,
|
|
2278
|
+
ast.keyword(arg=None, value=_name('kwargs')),
|
|
2279
|
+
]
|
|
411
2280
|
|
|
412
|
-
|
|
2281
|
+
request_call = _call(
|
|
2282
|
+
func=_name(base_fn_name),
|
|
2283
|
+
keywords=call_args,
|
|
2284
|
+
)
|
|
413
2285
|
|
|
414
|
-
|
|
415
|
-
ast.
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
args=[],
|
|
419
|
-
keywords=call_keywords,
|
|
420
|
-
)
|
|
421
|
-
)
|
|
422
|
-
]
|
|
2286
|
+
if is_async:
|
|
2287
|
+
request_call = ast.Await(value=request_call)
|
|
2288
|
+
|
|
2289
|
+
body: list[ast.stmt] = []
|
|
423
2290
|
|
|
424
2291
|
if docs:
|
|
425
|
-
|
|
426
|
-
|
|
2292
|
+
body.append(ast.Expr(value=ast.Constant(value=clean_docstring(docs))))
|
|
2293
|
+
|
|
2294
|
+
body.append(ast.Return(value=request_call))
|
|
427
2295
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
2296
|
+
if response_model:
|
|
2297
|
+
returns = response_model.annotation_ast
|
|
2298
|
+
else:
|
|
2299
|
+
returns = _name('Response')
|
|
2300
|
+
imports.setdefault('httpx', set()).add('Response')
|
|
432
2301
|
|
|
433
|
-
|
|
2302
|
+
func_ast = func_builder(
|
|
434
2303
|
name=name,
|
|
435
2304
|
args=args,
|
|
436
2305
|
body=body,
|
|
437
2306
|
kwargs=_argument('kwargs', _name('dict')),
|
|
438
2307
|
kwonlyargs=kwonlyargs,
|
|
439
2308
|
kw_defaults=kw_defaults,
|
|
440
|
-
returns=
|
|
441
|
-
)
|
|
2309
|
+
returns=returns,
|
|
2310
|
+
)
|
|
2311
|
+
|
|
2312
|
+
return func_ast, imports
|
|
2313
|
+
|
|
2314
|
+
|
|
2315
|
+
def request_fn(
|
|
2316
|
+
name: str,
|
|
2317
|
+
method: str,
|
|
2318
|
+
path: str,
|
|
2319
|
+
response_model: 'Type | None',
|
|
2320
|
+
docs: str | None = None,
|
|
2321
|
+
parameters: list['Parameter'] | None = None,
|
|
2322
|
+
response_infos: list['ResponseInfo'] | None = None,
|
|
2323
|
+
request_body_info: 'RequestBodyInfo | None' = None,
|
|
2324
|
+
) -> tuple[ast.FunctionDef, ImportDict]:
|
|
2325
|
+
"""Generate a synchronous endpoint function.
|
|
2326
|
+
|
|
2327
|
+
Args:
|
|
2328
|
+
name: The function name to generate.
|
|
2329
|
+
method: HTTP method (GET, POST, etc.).
|
|
2330
|
+
path: The API path with optional placeholders.
|
|
2331
|
+
response_model: The response type for validation, or None.
|
|
2332
|
+
docs: Optional docstring for the generated function.
|
|
2333
|
+
parameters: List of Parameter objects for the endpoint.
|
|
2334
|
+
response_infos: List of ResponseInfo objects describing response content types.
|
|
2335
|
+
request_body_info: RequestBodyInfo object for the request body, or None.
|
|
2336
|
+
|
|
2337
|
+
Returns:
|
|
2338
|
+
A tuple of (function_ast, imports) for the sync endpoint function.
|
|
2339
|
+
"""
|
|
2340
|
+
return _build_endpoint_fn(
|
|
2341
|
+
name=name,
|
|
2342
|
+
method=method,
|
|
2343
|
+
path=path,
|
|
2344
|
+
response_model=response_model,
|
|
2345
|
+
is_async=False,
|
|
2346
|
+
docs=docs,
|
|
2347
|
+
parameters=parameters,
|
|
2348
|
+
response_infos=response_infos,
|
|
2349
|
+
request_body_info=request_body_info,
|
|
2350
|
+
)
|
|
442
2351
|
|
|
443
2352
|
|
|
444
2353
|
def async_request_fn(
|
|
445
2354
|
name: str,
|
|
446
2355
|
method: str,
|
|
447
2356
|
path: str,
|
|
448
|
-
response_model: Type,
|
|
2357
|
+
response_model: 'Type | None',
|
|
449
2358
|
docs: str | None = None,
|
|
450
|
-
parameters: list | None = None,
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
2359
|
+
parameters: list['Parameter'] | None = None,
|
|
2360
|
+
response_infos: list['ResponseInfo'] | None = None,
|
|
2361
|
+
request_body_info: 'RequestBodyInfo | None' = None,
|
|
2362
|
+
) -> tuple[ast.AsyncFunctionDef, ImportDict]:
|
|
2363
|
+
"""Generate an asynchronous endpoint function.
|
|
2364
|
+
|
|
2365
|
+
Args:
|
|
2366
|
+
name: The function name to generate.
|
|
2367
|
+
method: HTTP method (GET, POST, etc.).
|
|
2368
|
+
path: The API path with optional placeholders.
|
|
2369
|
+
response_model: The response type for validation, or None.
|
|
2370
|
+
docs: Optional docstring for the generated function.
|
|
2371
|
+
parameters: List of Parameter objects for the endpoint.
|
|
2372
|
+
response_infos: List of ResponseInfo objects describing response content types.
|
|
2373
|
+
request_body_info: RequestBodyInfo object for the request body, or None.
|
|
2374
|
+
|
|
2375
|
+
Returns:
|
|
2376
|
+
A tuple of (function_ast, imports) for the async endpoint function.
|
|
2377
|
+
"""
|
|
2378
|
+
return _build_endpoint_fn(
|
|
2379
|
+
name=name,
|
|
2380
|
+
method=method,
|
|
2381
|
+
path=path,
|
|
2382
|
+
response_model=response_model,
|
|
2383
|
+
is_async=True,
|
|
2384
|
+
docs=docs,
|
|
2385
|
+
parameters=parameters,
|
|
2386
|
+
response_infos=response_infos,
|
|
2387
|
+
request_body_info=request_body_info,
|
|
2388
|
+
)
|
|
2389
|
+
|
|
2390
|
+
|
|
2391
|
+
# =============================================================================
|
|
2392
|
+
# Default Client Code Generation
|
|
2393
|
+
# =============================================================================
|
|
2394
|
+
|
|
2395
|
+
|
|
2396
|
+
def build_default_client_code() -> tuple[list[ast.stmt], ImportDict]:
|
|
2397
|
+
"""Build any module-level client code needed.
|
|
2398
|
+
|
|
2399
|
+
Previously this created a global singleton pattern. Now it returns
|
|
2400
|
+
an empty list since each endpoint creates its own Client instance
|
|
2401
|
+
when none is provided.
|
|
2402
|
+
|
|
2403
|
+
Returns:
|
|
2404
|
+
A tuple of (statements, imports) - currently empty.
|
|
2405
|
+
"""
|
|
2406
|
+
imports: ImportDict = {}
|
|
2407
|
+
imports.setdefault('typing', set()).add('Union')
|
|
2408
|
+
|
|
2409
|
+
# No global state needed - endpoints use `client or Client()`
|
|
2410
|
+
return [], imports
|
|
2411
|
+
|
|
2412
|
+
|
|
2413
|
+
# =============================================================================
|
|
2414
|
+
# Convenience Functions (Factory Wrappers)
|
|
2415
|
+
# =============================================================================
|
|
454
2416
|
|
|
455
|
-
|
|
456
|
-
|
|
2417
|
+
|
|
2418
|
+
def build_standalone_endpoint_fn(
|
|
2419
|
+
fn_name: str,
|
|
2420
|
+
method: str,
|
|
2421
|
+
path: str,
|
|
2422
|
+
parameters: list['Parameter'] | None,
|
|
2423
|
+
request_body_info: 'RequestBodyInfo | None',
|
|
2424
|
+
response_type: 'Type | None',
|
|
2425
|
+
response_infos: list['ResponseInfo'] | None = None,
|
|
2426
|
+
docs: str | None = None,
|
|
2427
|
+
is_async: bool = False,
|
|
2428
|
+
) -> tuple[ast.FunctionDef | ast.AsyncFunctionDef, ImportDict]:
|
|
2429
|
+
"""Build a standalone endpoint function with full implementation.
|
|
2430
|
+
|
|
2431
|
+
This is a convenience wrapper around EndpointFunctionFactory.
|
|
2432
|
+
|
|
2433
|
+
Args:
|
|
2434
|
+
fn_name: The function name to generate.
|
|
2435
|
+
method: HTTP method (GET, POST, etc.).
|
|
2436
|
+
path: The API path with optional placeholders.
|
|
2437
|
+
parameters: List of Parameter objects for the endpoint.
|
|
2438
|
+
request_body_info: RequestBodyInfo object for the request body, or None.
|
|
2439
|
+
response_type: The response Type for the return annotation.
|
|
2440
|
+
response_infos: List of ResponseInfo objects for status code handling.
|
|
2441
|
+
docs: Optional docstring for the generated function.
|
|
2442
|
+
is_async: Whether to generate an async function.
|
|
2443
|
+
|
|
2444
|
+
Returns:
|
|
2445
|
+
A tuple of (function_ast, imports).
|
|
2446
|
+
"""
|
|
2447
|
+
config = EndpointFunctionConfig(
|
|
2448
|
+
fn_name=fn_name,
|
|
2449
|
+
method=method,
|
|
2450
|
+
path=path,
|
|
2451
|
+
parameters=parameters,
|
|
2452
|
+
request_body_info=request_body_info,
|
|
2453
|
+
response_type=response_type,
|
|
2454
|
+
response_infos=response_infos,
|
|
2455
|
+
docs=docs,
|
|
2456
|
+
is_async=is_async,
|
|
2457
|
+
mode=EndpointMode.STANDALONE,
|
|
457
2458
|
)
|
|
458
|
-
|
|
459
|
-
|
|
2459
|
+
return EndpointFunctionFactory(config).build()
|
|
2460
|
+
|
|
2461
|
+
|
|
2462
|
+
def build_delegating_endpoint_fn(
|
|
2463
|
+
fn_name: str,
|
|
2464
|
+
client_method_name: str,
|
|
2465
|
+
parameters: list['Parameter'] | None,
|
|
2466
|
+
request_body_info: 'RequestBodyInfo | None',
|
|
2467
|
+
response_type: 'Type | None',
|
|
2468
|
+
docs: str | None = None,
|
|
2469
|
+
is_async: bool = False,
|
|
2470
|
+
) -> tuple[ast.FunctionDef | ast.AsyncFunctionDef, ImportDict]:
|
|
2471
|
+
"""Build an endpoint function that delegates to a client method.
|
|
2472
|
+
|
|
2473
|
+
This is a convenience wrapper around EndpointFunctionFactory.
|
|
2474
|
+
|
|
2475
|
+
Args:
|
|
2476
|
+
fn_name: The function name to generate.
|
|
2477
|
+
client_method_name: The client method to delegate to.
|
|
2478
|
+
parameters: List of Parameter objects for the endpoint.
|
|
2479
|
+
request_body_info: RequestBodyInfo object for the request body, or None.
|
|
2480
|
+
response_type: The response Type for the return annotation.
|
|
2481
|
+
docs: Optional docstring for the generated function.
|
|
2482
|
+
is_async: Whether to generate an async function.
|
|
2483
|
+
|
|
2484
|
+
Returns:
|
|
2485
|
+
A tuple of (function_ast, imports).
|
|
2486
|
+
"""
|
|
2487
|
+
config = EndpointFunctionConfig(
|
|
2488
|
+
fn_name=fn_name,
|
|
2489
|
+
method='',
|
|
2490
|
+
path='',
|
|
2491
|
+
parameters=parameters,
|
|
2492
|
+
request_body_info=request_body_info,
|
|
2493
|
+
response_type=response_type,
|
|
2494
|
+
docs=docs,
|
|
2495
|
+
is_async=is_async,
|
|
2496
|
+
mode=EndpointMode.DELEGATING,
|
|
2497
|
+
client_method_name=client_method_name,
|
|
460
2498
|
)
|
|
2499
|
+
return EndpointFunctionFactory(config).build()
|
|
461
2500
|
|
|
462
|
-
if query_params:
|
|
463
|
-
call_keywords.append(ast.keyword(arg='params', value=query_params))
|
|
464
|
-
if header_params:
|
|
465
|
-
call_keywords.append(ast.keyword(arg='headers', value=header_params))
|
|
466
|
-
if body_params:
|
|
467
|
-
call_keywords.append(ast.keyword(arg='json', value=body_params))
|
|
468
2501
|
|
|
469
|
-
|
|
470
|
-
|
|
2502
|
+
def build_standalone_dataframe_fn(
|
|
2503
|
+
fn_name: str,
|
|
2504
|
+
method: str,
|
|
2505
|
+
path: str,
|
|
2506
|
+
parameters: list['Parameter'] | None,
|
|
2507
|
+
request_body_info: 'RequestBodyInfo | None',
|
|
2508
|
+
library: Literal['pandas', 'polars'],
|
|
2509
|
+
default_path: str | None = None,
|
|
2510
|
+
docs: str | None = None,
|
|
2511
|
+
is_async: bool = False,
|
|
2512
|
+
) -> tuple[ast.FunctionDef | ast.AsyncFunctionDef, ImportDict]:
|
|
2513
|
+
"""Build a standalone DataFrame endpoint function.
|
|
2514
|
+
|
|
2515
|
+
This is a convenience wrapper around EndpointFunctionFactory.
|
|
2516
|
+
|
|
2517
|
+
Args:
|
|
2518
|
+
fn_name: The function name to generate.
|
|
2519
|
+
method: HTTP method (GET, POST, etc.).
|
|
2520
|
+
path: The API path with optional placeholders.
|
|
2521
|
+
parameters: List of Parameter objects for the endpoint.
|
|
2522
|
+
request_body_info: RequestBodyInfo object for the request body, or None.
|
|
2523
|
+
library: The DataFrame library ('pandas' or 'polars').
|
|
2524
|
+
default_path: Default path value for the path parameter.
|
|
2525
|
+
docs: Optional docstring for the generated function.
|
|
2526
|
+
is_async: Whether to generate an async function.
|
|
2527
|
+
|
|
2528
|
+
Returns:
|
|
2529
|
+
A tuple of (function_ast, imports).
|
|
2530
|
+
"""
|
|
2531
|
+
df_library = (
|
|
2532
|
+
DataFrameLibrary.PANDAS if library == 'pandas' else DataFrameLibrary.POLARS
|
|
2533
|
+
)
|
|
2534
|
+
config = EndpointFunctionConfig(
|
|
2535
|
+
fn_name=fn_name,
|
|
2536
|
+
method=method,
|
|
2537
|
+
path=path,
|
|
2538
|
+
parameters=parameters,
|
|
2539
|
+
request_body_info=request_body_info,
|
|
2540
|
+
response_type=None,
|
|
2541
|
+
docs=docs,
|
|
2542
|
+
is_async=is_async,
|
|
2543
|
+
mode=EndpointMode.STANDALONE,
|
|
2544
|
+
dataframe_library=df_library,
|
|
2545
|
+
dataframe_path=default_path,
|
|
2546
|
+
)
|
|
2547
|
+
return EndpointFunctionFactory(config).build()
|
|
471
2548
|
|
|
472
|
-
body = [
|
|
473
|
-
ast.Return(
|
|
474
|
-
value=ast.Await(
|
|
475
|
-
value=_call(
|
|
476
|
-
func=_name('request_async'),
|
|
477
|
-
args=[],
|
|
478
|
-
keywords=call_keywords,
|
|
479
|
-
)
|
|
480
|
-
)
|
|
481
|
-
)
|
|
482
|
-
]
|
|
483
2549
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
2550
|
+
def build_standalone_paginated_dataframe_fn(
|
|
2551
|
+
fn_name: str,
|
|
2552
|
+
method: str,
|
|
2553
|
+
path: str,
|
|
2554
|
+
parameters: list['Parameter'] | None,
|
|
2555
|
+
request_body_info: 'RequestBodyInfo | None',
|
|
2556
|
+
response_type: 'Type | None',
|
|
2557
|
+
pagination_style: str,
|
|
2558
|
+
pagination_config: dict,
|
|
2559
|
+
library: Literal['pandas', 'polars'],
|
|
2560
|
+
item_type_ast: ast.expr | None = None,
|
|
2561
|
+
docs: str | None = None,
|
|
2562
|
+
is_async: bool = False,
|
|
2563
|
+
) -> tuple[ast.FunctionDef | ast.AsyncFunctionDef, ImportDict]:
|
|
2564
|
+
"""Build a standalone paginated DataFrame endpoint function.
|
|
2565
|
+
|
|
2566
|
+
This function fetches all paginated items and returns them as a DataFrame.
|
|
2567
|
+
|
|
2568
|
+
Args:
|
|
2569
|
+
fn_name: The function name to generate.
|
|
2570
|
+
method: HTTP method (GET, POST, etc.).
|
|
2571
|
+
path: The API path with optional placeholders.
|
|
2572
|
+
parameters: List of Parameter objects for the endpoint.
|
|
2573
|
+
request_body_info: RequestBodyInfo object for the request body, or None.
|
|
2574
|
+
response_type: The response Type for parsing.
|
|
2575
|
+
pagination_style: The pagination style ('offset', 'cursor', 'page').
|
|
2576
|
+
pagination_config: Dict with pagination parameter names and paths.
|
|
2577
|
+
library: The DataFrame library ('pandas' or 'polars').
|
|
2578
|
+
item_type_ast: AST for the item type in the list.
|
|
2579
|
+
docs: Optional docstring for the generated function.
|
|
2580
|
+
is_async: Whether to generate an async function.
|
|
2581
|
+
|
|
2582
|
+
Returns:
|
|
2583
|
+
A tuple of (function_ast, imports).
|
|
2584
|
+
"""
|
|
2585
|
+
pag_style = PaginationStyle(pagination_style)
|
|
2586
|
+
df_library = (
|
|
2587
|
+
DataFrameLibrary.PANDAS if library == 'pandas' else DataFrameLibrary.POLARS
|
|
2588
|
+
)
|
|
2589
|
+
config = EndpointFunctionConfig(
|
|
2590
|
+
fn_name=fn_name,
|
|
2591
|
+
method=method,
|
|
2592
|
+
path=path,
|
|
2593
|
+
parameters=parameters,
|
|
2594
|
+
request_body_info=request_body_info,
|
|
2595
|
+
response_type=response_type,
|
|
2596
|
+
docs=docs,
|
|
2597
|
+
is_async=is_async,
|
|
2598
|
+
mode=EndpointMode.STANDALONE,
|
|
2599
|
+
pagination_style=pag_style,
|
|
2600
|
+
pagination_config=pagination_config,
|
|
2601
|
+
is_paginated_dataframe=True,
|
|
2602
|
+
dataframe_library=df_library,
|
|
2603
|
+
item_type_ast=item_type_ast,
|
|
2604
|
+
)
|
|
2605
|
+
return EndpointFunctionFactory(config).build()
|
|
487
2606
|
|
|
488
|
-
response_model_ast = response_model.annotation_ast if response_model else None
|
|
489
|
-
if not response_model_ast:
|
|
490
|
-
response_model_ast = _name('Any')
|
|
491
|
-
imports.setdefault('typing', set()).add('Any')
|
|
492
2607
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
2608
|
+
def build_delegating_dataframe_fn(
|
|
2609
|
+
fn_name: str,
|
|
2610
|
+
client_method_name: str,
|
|
2611
|
+
parameters: list['Parameter'] | None,
|
|
2612
|
+
request_body_info: 'RequestBodyInfo | None',
|
|
2613
|
+
library: Literal['pandas', 'polars'],
|
|
2614
|
+
default_path: str | None = None,
|
|
2615
|
+
docs: str | None = None,
|
|
2616
|
+
is_async: bool = False,
|
|
2617
|
+
) -> tuple[ast.FunctionDef | ast.AsyncFunctionDef, ImportDict]:
|
|
2618
|
+
"""Build a DataFrame endpoint function that delegates to a client method.
|
|
2619
|
+
|
|
2620
|
+
This is a convenience wrapper around EndpointFunctionFactory.
|
|
2621
|
+
|
|
2622
|
+
Args:
|
|
2623
|
+
fn_name: The function name to generate.
|
|
2624
|
+
client_method_name: The client method to delegate to.
|
|
2625
|
+
parameters: List of Parameter objects for the endpoint.
|
|
2626
|
+
request_body_info: RequestBodyInfo object for the request body, or None.
|
|
2627
|
+
library: The DataFrame library ('pandas' or 'polars').
|
|
2628
|
+
default_path: Default path value for the path parameter.
|
|
2629
|
+
docs: Optional docstring for the generated function.
|
|
2630
|
+
is_async: Whether to generate an async function.
|
|
2631
|
+
|
|
2632
|
+
Returns:
|
|
2633
|
+
A tuple of (function_ast, imports).
|
|
2634
|
+
"""
|
|
2635
|
+
df_library = (
|
|
2636
|
+
DataFrameLibrary.PANDAS if library == 'pandas' else DataFrameLibrary.POLARS
|
|
2637
|
+
)
|
|
2638
|
+
config = EndpointFunctionConfig(
|
|
2639
|
+
fn_name=fn_name,
|
|
2640
|
+
method='',
|
|
2641
|
+
path='',
|
|
2642
|
+
parameters=parameters,
|
|
2643
|
+
request_body_info=request_body_info,
|
|
2644
|
+
response_type=None,
|
|
2645
|
+
docs=docs,
|
|
2646
|
+
is_async=is_async,
|
|
2647
|
+
mode=EndpointMode.DELEGATING,
|
|
2648
|
+
client_method_name=client_method_name,
|
|
2649
|
+
dataframe_library=df_library,
|
|
2650
|
+
dataframe_path=default_path,
|
|
2651
|
+
)
|
|
2652
|
+
return EndpointFunctionFactory(config).build()
|
|
2653
|
+
|
|
2654
|
+
|
|
2655
|
+
def build_standalone_paginated_fn(
|
|
2656
|
+
fn_name: str,
|
|
2657
|
+
method: str,
|
|
2658
|
+
path: str,
|
|
2659
|
+
parameters: list['Parameter'] | None,
|
|
2660
|
+
request_body_info: 'RequestBodyInfo | None',
|
|
2661
|
+
response_type: 'Type | None',
|
|
2662
|
+
pagination_style: str,
|
|
2663
|
+
pagination_config: dict,
|
|
2664
|
+
item_type_ast: ast.expr | None = None,
|
|
2665
|
+
docs: str | None = None,
|
|
2666
|
+
is_async: bool = False,
|
|
2667
|
+
) -> tuple[ast.FunctionDef | ast.AsyncFunctionDef, ImportDict]:
|
|
2668
|
+
"""Build a standalone paginated endpoint function.
|
|
2669
|
+
|
|
2670
|
+
This function returns all items by automatically handling pagination.
|
|
2671
|
+
|
|
2672
|
+
Args:
|
|
2673
|
+
fn_name: The function name to generate.
|
|
2674
|
+
method: HTTP method (GET, POST, etc.).
|
|
2675
|
+
path: The API path with optional placeholders.
|
|
2676
|
+
parameters: List of Parameter objects for the endpoint.
|
|
2677
|
+
request_body_info: RequestBodyInfo object for the request body, or None.
|
|
2678
|
+
response_type: The response Type for parsing.
|
|
2679
|
+
pagination_style: The pagination style ('offset', 'cursor', 'page').
|
|
2680
|
+
pagination_config: Dict with pagination parameter names and paths.
|
|
2681
|
+
item_type_ast: AST for the item type in the list.
|
|
2682
|
+
docs: Optional docstring for the generated function.
|
|
2683
|
+
is_async: Whether to generate an async function.
|
|
2684
|
+
|
|
2685
|
+
Returns:
|
|
2686
|
+
A tuple of (function_ast, imports).
|
|
2687
|
+
"""
|
|
2688
|
+
pag_style = PaginationStyle(pagination_style)
|
|
2689
|
+
config = EndpointFunctionConfig(
|
|
2690
|
+
fn_name=fn_name,
|
|
2691
|
+
method=method,
|
|
2692
|
+
path=path,
|
|
2693
|
+
parameters=parameters,
|
|
2694
|
+
request_body_info=request_body_info,
|
|
2695
|
+
response_type=response_type,
|
|
2696
|
+
docs=docs,
|
|
2697
|
+
is_async=is_async,
|
|
2698
|
+
mode=EndpointMode.STANDALONE,
|
|
2699
|
+
pagination_style=pag_style,
|
|
2700
|
+
pagination_config=pagination_config,
|
|
2701
|
+
item_type_ast=item_type_ast,
|
|
2702
|
+
)
|
|
2703
|
+
return EndpointFunctionFactory(config).build()
|
|
2704
|
+
|
|
2705
|
+
|
|
2706
|
+
def build_standalone_paginated_iter_fn(
|
|
2707
|
+
fn_name: str,
|
|
2708
|
+
method: str,
|
|
2709
|
+
path: str,
|
|
2710
|
+
parameters: list['Parameter'] | None,
|
|
2711
|
+
request_body_info: 'RequestBodyInfo | None',
|
|
2712
|
+
response_type: 'Type | None',
|
|
2713
|
+
pagination_style: str,
|
|
2714
|
+
pagination_config: dict,
|
|
2715
|
+
item_type_ast: ast.expr | None = None,
|
|
2716
|
+
docs: str | None = None,
|
|
2717
|
+
is_async: bool = False,
|
|
2718
|
+
) -> tuple[ast.FunctionDef | ast.AsyncFunctionDef, ImportDict]:
|
|
2719
|
+
"""Build a standalone paginated iterator endpoint function.
|
|
2720
|
+
|
|
2721
|
+
This function yields items one at a time for memory-efficient streaming.
|
|
2722
|
+
|
|
2723
|
+
Args:
|
|
2724
|
+
fn_name: The function name to generate.
|
|
2725
|
+
method: HTTP method (GET, POST, etc.).
|
|
2726
|
+
path: The API path with optional placeholders.
|
|
2727
|
+
parameters: List of Parameter objects for the endpoint.
|
|
2728
|
+
request_body_info: RequestBodyInfo object for the request body, or None.
|
|
2729
|
+
response_type: The response Type for parsing.
|
|
2730
|
+
pagination_style: The pagination style ('offset', 'cursor', 'page').
|
|
2731
|
+
pagination_config: Dict with pagination parameter names and paths.
|
|
2732
|
+
item_type_ast: AST for the item type in the list.
|
|
2733
|
+
docs: Optional docstring for the generated function.
|
|
2734
|
+
is_async: Whether to generate an async function.
|
|
2735
|
+
|
|
2736
|
+
Returns:
|
|
2737
|
+
A tuple of (function_ast, imports).
|
|
2738
|
+
"""
|
|
2739
|
+
pag_style = PaginationStyle(pagination_style)
|
|
2740
|
+
config = EndpointFunctionConfig(
|
|
2741
|
+
fn_name=fn_name,
|
|
2742
|
+
method=method,
|
|
2743
|
+
path=path,
|
|
2744
|
+
parameters=parameters,
|
|
2745
|
+
request_body_info=request_body_info,
|
|
2746
|
+
response_type=response_type,
|
|
2747
|
+
docs=docs,
|
|
2748
|
+
is_async=is_async,
|
|
2749
|
+
mode=EndpointMode.STANDALONE,
|
|
2750
|
+
pagination_style=pag_style,
|
|
2751
|
+
pagination_config=pagination_config,
|
|
2752
|
+
is_iterator=True,
|
|
2753
|
+
item_type_ast=item_type_ast,
|
|
2754
|
+
)
|
|
2755
|
+
return EndpointFunctionFactory(config).build()
|