defistream 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- defistream/__init__.py +81 -0
- defistream/client.py +380 -0
- defistream/exceptions.py +65 -0
- defistream/models.py +174 -0
- defistream/protocols.py +364 -0
- defistream/py.typed +0 -0
- defistream/query.py +477 -0
- defistream-1.0.0.dist-info/METADATA +490 -0
- defistream-1.0.0.dist-info/RECORD +11 -0
- defistream-1.0.0.dist-info/WHEEL +4 -0
- defistream-1.0.0.dist-info/licenses/LICENSE +21 -0
defistream/query.py
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
"""Query builder for DeFiStream API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .client import BaseClient
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class QueryBuilder:
|
|
14
|
+
"""
|
|
15
|
+
Builder for constructing and executing DeFiStream API queries.
|
|
16
|
+
|
|
17
|
+
Query is only executed when a terminal method is called:
|
|
18
|
+
- as_dict() - returns list of dictionaries
|
|
19
|
+
- as_df() - returns pandas DataFrame (default) or polars DataFrame
|
|
20
|
+
- as_file() - saves to CSV, Parquet, or JSON file
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
query = client.erc20.transfers("USDT").network("ETH").start_block(24000000).end_block(24100000)
|
|
24
|
+
query = query.min_amount(1000).sender("0x...")
|
|
25
|
+
df = query.as_df() # pandas DataFrame
|
|
26
|
+
df = query.as_df("polars") # polars DataFrame
|
|
27
|
+
query.as_file("transfers.csv") # save to CSV
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
client: "BaseClient",
|
|
33
|
+
endpoint: str,
|
|
34
|
+
initial_params: dict[str, Any] | None = None,
|
|
35
|
+
):
|
|
36
|
+
self._client = client
|
|
37
|
+
self._endpoint = endpoint
|
|
38
|
+
self._params: dict[str, Any] = initial_params or {}
|
|
39
|
+
self._verbose = False
|
|
40
|
+
|
|
41
|
+
def _copy_with(self, **updates: Any) -> "QueryBuilder":
|
|
42
|
+
"""Create a copy with updated parameters."""
|
|
43
|
+
new_builder = QueryBuilder(self._client, self._endpoint, self._params.copy())
|
|
44
|
+
new_builder._verbose = self._verbose
|
|
45
|
+
for key, value in updates.items():
|
|
46
|
+
if key == "verbose":
|
|
47
|
+
new_builder._verbose = value
|
|
48
|
+
elif value is not None:
|
|
49
|
+
new_builder._params[key] = value
|
|
50
|
+
return new_builder
|
|
51
|
+
|
|
52
|
+
# Network and block range
|
|
53
|
+
def network(self, network: str) -> "QueryBuilder":
|
|
54
|
+
"""Set the network (ETH, ARB, BASE, OP, etc.)."""
|
|
55
|
+
return self._copy_with(network=network)
|
|
56
|
+
|
|
57
|
+
def start_block(self, block: int) -> "QueryBuilder":
|
|
58
|
+
"""Set the starting block number."""
|
|
59
|
+
return self._copy_with(block_start=block)
|
|
60
|
+
|
|
61
|
+
def end_block(self, block: int) -> "QueryBuilder":
|
|
62
|
+
"""Set the ending block number."""
|
|
63
|
+
return self._copy_with(block_end=block)
|
|
64
|
+
|
|
65
|
+
def block_range(self, start: int, end: int) -> "QueryBuilder":
|
|
66
|
+
"""Set both start and end block numbers."""
|
|
67
|
+
return self._copy_with(block_start=start, block_end=end)
|
|
68
|
+
|
|
69
|
+
# Time range
|
|
70
|
+
def start_time(self, timestamp: str) -> "QueryBuilder":
|
|
71
|
+
"""Set the starting time (ISO format or Unix timestamp)."""
|
|
72
|
+
return self._copy_with(since=timestamp)
|
|
73
|
+
|
|
74
|
+
def end_time(self, timestamp: str) -> "QueryBuilder":
|
|
75
|
+
"""Set the ending time (ISO format or Unix timestamp)."""
|
|
76
|
+
return self._copy_with(until=timestamp)
|
|
77
|
+
|
|
78
|
+
def time_range(self, start: str, end: str) -> "QueryBuilder":
|
|
79
|
+
"""Set both start and end times."""
|
|
80
|
+
return self._copy_with(since=start, until=end)
|
|
81
|
+
|
|
82
|
+
# Common filters
|
|
83
|
+
def sender(self, address: str) -> "QueryBuilder":
|
|
84
|
+
"""Filter by sender address."""
|
|
85
|
+
return self._copy_with(sender=address)
|
|
86
|
+
|
|
87
|
+
def receiver(self, address: str) -> "QueryBuilder":
|
|
88
|
+
"""Filter by receiver address."""
|
|
89
|
+
return self._copy_with(receiver=address)
|
|
90
|
+
|
|
91
|
+
def from_address(self, address: str) -> "QueryBuilder":
|
|
92
|
+
"""Filter by sender address (alias for sender)."""
|
|
93
|
+
return self.sender(address)
|
|
94
|
+
|
|
95
|
+
def to_address(self, address: str) -> "QueryBuilder":
|
|
96
|
+
"""Filter by receiver address (alias for receiver)."""
|
|
97
|
+
return self.receiver(address)
|
|
98
|
+
|
|
99
|
+
def min_amount(self, amount: float) -> "QueryBuilder":
|
|
100
|
+
"""Filter by minimum amount."""
|
|
101
|
+
return self._copy_with(min_amount=amount)
|
|
102
|
+
|
|
103
|
+
# ERC20 specific
|
|
104
|
+
def token(self, symbol: str) -> "QueryBuilder":
|
|
105
|
+
"""Set token symbol (for ERC20)."""
|
|
106
|
+
return self._copy_with(token=symbol)
|
|
107
|
+
|
|
108
|
+
def owner(self, address: str) -> "QueryBuilder":
|
|
109
|
+
"""Filter by owner address (for approvals)."""
|
|
110
|
+
return self._copy_with(owner=address)
|
|
111
|
+
|
|
112
|
+
def spender(self, address: str) -> "QueryBuilder":
|
|
113
|
+
"""Filter by spender address (for approvals)."""
|
|
114
|
+
return self._copy_with(spender=address)
|
|
115
|
+
|
|
116
|
+
# AAVE specific
|
|
117
|
+
def user(self, address: str) -> "QueryBuilder":
|
|
118
|
+
"""Filter by user address (for AAVE)."""
|
|
119
|
+
return self._copy_with(user=address)
|
|
120
|
+
|
|
121
|
+
def reserve(self, address: str) -> "QueryBuilder":
|
|
122
|
+
"""Filter by reserve address (for AAVE)."""
|
|
123
|
+
return self._copy_with(reserve=address)
|
|
124
|
+
|
|
125
|
+
def liquidator(self, address: str) -> "QueryBuilder":
|
|
126
|
+
"""Filter by liquidator address (for AAVE liquidations)."""
|
|
127
|
+
return self._copy_with(liquidator=address)
|
|
128
|
+
|
|
129
|
+
# Uniswap specific
|
|
130
|
+
def symbol0(self, symbol: str) -> "QueryBuilder":
|
|
131
|
+
"""Set first token symbol (for Uniswap)."""
|
|
132
|
+
return self._copy_with(symbol0=symbol)
|
|
133
|
+
|
|
134
|
+
def symbol1(self, symbol: str) -> "QueryBuilder":
|
|
135
|
+
"""Set second token symbol (for Uniswap)."""
|
|
136
|
+
return self._copy_with(symbol1=symbol)
|
|
137
|
+
|
|
138
|
+
def fee(self, fee_tier: int) -> "QueryBuilder":
|
|
139
|
+
"""Set fee tier (for Uniswap): 100, 500, 3000, 10000."""
|
|
140
|
+
return self._copy_with(fee=fee_tier)
|
|
141
|
+
|
|
142
|
+
def pool(self, address: str) -> "QueryBuilder":
|
|
143
|
+
"""Set pool address (for Uniswap)."""
|
|
144
|
+
return self._copy_with(pool=address)
|
|
145
|
+
|
|
146
|
+
# Verbose mode
|
|
147
|
+
def verbose(self, enabled: bool = True) -> "QueryBuilder":
|
|
148
|
+
"""Include all metadata fields (tx_hash, tx_id, log_index, network, name)."""
|
|
149
|
+
return self._copy_with(verbose=enabled)
|
|
150
|
+
|
|
151
|
+
# Build final params
|
|
152
|
+
def _build_params(self) -> dict[str, Any]:
|
|
153
|
+
"""Build the final query parameters."""
|
|
154
|
+
params = self._params.copy()
|
|
155
|
+
if self._verbose:
|
|
156
|
+
params["verbose"] = "true"
|
|
157
|
+
return params
|
|
158
|
+
|
|
159
|
+
# Terminal methods - execute the query
|
|
160
|
+
def as_dict(self) -> list[dict[str, Any]]:
|
|
161
|
+
"""
|
|
162
|
+
Execute query and return results as list of dictionaries.
|
|
163
|
+
|
|
164
|
+
Uses JSON format from API.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
List of event dictionaries
|
|
168
|
+
"""
|
|
169
|
+
params = self._build_params()
|
|
170
|
+
return self._client._request("GET", self._endpoint, params=params)
|
|
171
|
+
|
|
172
|
+
def as_df(self, library: str = "pandas") -> Any:
|
|
173
|
+
"""
|
|
174
|
+
Execute query and return results as DataFrame.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
library: DataFrame library to use - "pandas" (default) or "polars"
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
pandas.DataFrame or polars.DataFrame
|
|
181
|
+
|
|
182
|
+
Example:
|
|
183
|
+
df = query.as_df() # pandas DataFrame
|
|
184
|
+
df = query.as_df("polars") # polars DataFrame
|
|
185
|
+
"""
|
|
186
|
+
if library not in ("pandas", "polars"):
|
|
187
|
+
raise ValueError(f"library must be 'pandas' or 'polars', got '{library}'")
|
|
188
|
+
params = self._build_params()
|
|
189
|
+
params["format"] = "parquet"
|
|
190
|
+
return self._client._request(
|
|
191
|
+
"GET", self._endpoint, params=params, as_dataframe=library
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def as_file(self, path: str, format: str | None = None) -> None:
|
|
195
|
+
"""
|
|
196
|
+
Execute query and save results to file.
|
|
197
|
+
|
|
198
|
+
Format is automatically determined by file extension, or can be
|
|
199
|
+
explicitly specified.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
path: File path to save to
|
|
203
|
+
format: File format - "csv", "parquet", or "json".
|
|
204
|
+
If None, determined from file extension.
|
|
205
|
+
|
|
206
|
+
Example:
|
|
207
|
+
query.as_file("transfers.csv") # CSV format
|
|
208
|
+
query.as_file("transfers.parquet") # Parquet format
|
|
209
|
+
query.as_file("transfers.json") # JSON format
|
|
210
|
+
query.as_file("transfers", format="csv") # Explicit format
|
|
211
|
+
"""
|
|
212
|
+
# Determine format from extension or explicit parameter
|
|
213
|
+
if format is None:
|
|
214
|
+
if path.endswith(".csv"):
|
|
215
|
+
format = "csv"
|
|
216
|
+
elif path.endswith(".parquet"):
|
|
217
|
+
format = "parquet"
|
|
218
|
+
elif path.endswith(".json"):
|
|
219
|
+
format = "json"
|
|
220
|
+
else:
|
|
221
|
+
raise ValueError(
|
|
222
|
+
f"Cannot determine format from path '{path}'. "
|
|
223
|
+
"Use a file extension (.csv, .parquet, .json) or specify format explicitly."
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if format not in ("csv", "parquet", "json"):
|
|
227
|
+
raise ValueError(f"format must be 'csv', 'parquet', or 'json', got '{format}'")
|
|
228
|
+
|
|
229
|
+
params = self._build_params()
|
|
230
|
+
|
|
231
|
+
if format == "json":
|
|
232
|
+
# For JSON, fetch as dict and write manually
|
|
233
|
+
import json
|
|
234
|
+
results = self.as_dict()
|
|
235
|
+
with open(path, "w") as f:
|
|
236
|
+
json.dump(results, f, indent=2)
|
|
237
|
+
else:
|
|
238
|
+
# For CSV and Parquet, use API format parameter
|
|
239
|
+
params["format"] = format
|
|
240
|
+
self._client._request("GET", self._endpoint, params=params, output_file=path)
|
|
241
|
+
|
|
242
|
+
def __repr__(self) -> str:
|
|
243
|
+
return f"QueryBuilder(endpoint={self._endpoint!r}, params={self._params!r}, verbose={self._verbose})"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class AsyncQueryBuilder:
|
|
247
|
+
"""
|
|
248
|
+
Async builder for constructing and executing DeFiStream API queries.
|
|
249
|
+
|
|
250
|
+
Query is only executed when a terminal method is called:
|
|
251
|
+
- as_dict() - returns list of dictionaries
|
|
252
|
+
- as_df() - returns pandas DataFrame (default) or polars DataFrame
|
|
253
|
+
- as_file() - saves to CSV, Parquet, or JSON file
|
|
254
|
+
|
|
255
|
+
Example:
|
|
256
|
+
query = client.erc20.transfers("USDT").network("ETH").start_block(24000000).end_block(24100000)
|
|
257
|
+
df = await query.as_df() # pandas DataFrame
|
|
258
|
+
df = await query.as_df("polars") # polars DataFrame
|
|
259
|
+
await query.as_file("transfers.csv") # save to CSV
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
def __init__(
|
|
263
|
+
self,
|
|
264
|
+
client: "BaseClient",
|
|
265
|
+
endpoint: str,
|
|
266
|
+
initial_params: dict[str, Any] | None = None,
|
|
267
|
+
):
|
|
268
|
+
self._client = client
|
|
269
|
+
self._endpoint = endpoint
|
|
270
|
+
self._params: dict[str, Any] = initial_params or {}
|
|
271
|
+
self._verbose = False
|
|
272
|
+
|
|
273
|
+
def _copy_with(self, **updates: Any) -> "AsyncQueryBuilder":
|
|
274
|
+
"""Create a copy with updated parameters."""
|
|
275
|
+
new_builder = AsyncQueryBuilder(self._client, self._endpoint, self._params.copy())
|
|
276
|
+
new_builder._verbose = self._verbose
|
|
277
|
+
for key, value in updates.items():
|
|
278
|
+
if key == "verbose":
|
|
279
|
+
new_builder._verbose = value
|
|
280
|
+
elif value is not None:
|
|
281
|
+
new_builder._params[key] = value
|
|
282
|
+
return new_builder
|
|
283
|
+
|
|
284
|
+
# Network and block range
|
|
285
|
+
def network(self, network: str) -> "AsyncQueryBuilder":
|
|
286
|
+
"""Set the network (ETH, ARB, BASE, OP, etc.)."""
|
|
287
|
+
return self._copy_with(network=network)
|
|
288
|
+
|
|
289
|
+
def start_block(self, block: int) -> "AsyncQueryBuilder":
|
|
290
|
+
"""Set the starting block number."""
|
|
291
|
+
return self._copy_with(block_start=block)
|
|
292
|
+
|
|
293
|
+
def end_block(self, block: int) -> "AsyncQueryBuilder":
|
|
294
|
+
"""Set the ending block number."""
|
|
295
|
+
return self._copy_with(block_end=block)
|
|
296
|
+
|
|
297
|
+
def block_range(self, start: int, end: int) -> "AsyncQueryBuilder":
|
|
298
|
+
"""Set both start and end block numbers."""
|
|
299
|
+
return self._copy_with(block_start=start, block_end=end)
|
|
300
|
+
|
|
301
|
+
# Time range
|
|
302
|
+
def start_time(self, timestamp: str) -> "AsyncQueryBuilder":
|
|
303
|
+
"""Set the starting time (ISO format or Unix timestamp)."""
|
|
304
|
+
return self._copy_with(since=timestamp)
|
|
305
|
+
|
|
306
|
+
def end_time(self, timestamp: str) -> "AsyncQueryBuilder":
|
|
307
|
+
"""Set the ending time (ISO format or Unix timestamp)."""
|
|
308
|
+
return self._copy_with(until=timestamp)
|
|
309
|
+
|
|
310
|
+
def time_range(self, start: str, end: str) -> "AsyncQueryBuilder":
|
|
311
|
+
"""Set both start and end times."""
|
|
312
|
+
return self._copy_with(since=start, until=end)
|
|
313
|
+
|
|
314
|
+
# Common filters
|
|
315
|
+
def sender(self, address: str) -> "AsyncQueryBuilder":
|
|
316
|
+
"""Filter by sender address."""
|
|
317
|
+
return self._copy_with(sender=address)
|
|
318
|
+
|
|
319
|
+
def receiver(self, address: str) -> "AsyncQueryBuilder":
|
|
320
|
+
"""Filter by receiver address."""
|
|
321
|
+
return self._copy_with(receiver=address)
|
|
322
|
+
|
|
323
|
+
def from_address(self, address: str) -> "AsyncQueryBuilder":
|
|
324
|
+
"""Filter by sender address (alias for sender)."""
|
|
325
|
+
return self.sender(address)
|
|
326
|
+
|
|
327
|
+
def to_address(self, address: str) -> "AsyncQueryBuilder":
|
|
328
|
+
"""Filter by receiver address (alias for receiver)."""
|
|
329
|
+
return self.receiver(address)
|
|
330
|
+
|
|
331
|
+
def min_amount(self, amount: float) -> "AsyncQueryBuilder":
|
|
332
|
+
"""Filter by minimum amount."""
|
|
333
|
+
return self._copy_with(min_amount=amount)
|
|
334
|
+
|
|
335
|
+
# ERC20 specific
|
|
336
|
+
def token(self, symbol: str) -> "AsyncQueryBuilder":
|
|
337
|
+
"""Set token symbol (for ERC20)."""
|
|
338
|
+
return self._copy_with(token=symbol)
|
|
339
|
+
|
|
340
|
+
def owner(self, address: str) -> "AsyncQueryBuilder":
|
|
341
|
+
"""Filter by owner address (for approvals)."""
|
|
342
|
+
return self._copy_with(owner=address)
|
|
343
|
+
|
|
344
|
+
def spender(self, address: str) -> "AsyncQueryBuilder":
|
|
345
|
+
"""Filter by spender address (for approvals)."""
|
|
346
|
+
return self._copy_with(spender=address)
|
|
347
|
+
|
|
348
|
+
# AAVE specific
|
|
349
|
+
def user(self, address: str) -> "AsyncQueryBuilder":
|
|
350
|
+
"""Filter by user address (for AAVE)."""
|
|
351
|
+
return self._copy_with(user=address)
|
|
352
|
+
|
|
353
|
+
def reserve(self, address: str) -> "AsyncQueryBuilder":
|
|
354
|
+
"""Filter by reserve address (for AAVE)."""
|
|
355
|
+
return self._copy_with(reserve=address)
|
|
356
|
+
|
|
357
|
+
def liquidator(self, address: str) -> "AsyncQueryBuilder":
|
|
358
|
+
"""Filter by liquidator address (for AAVE liquidations)."""
|
|
359
|
+
return self._copy_with(liquidator=address)
|
|
360
|
+
|
|
361
|
+
# Uniswap specific
|
|
362
|
+
def symbol0(self, symbol: str) -> "AsyncQueryBuilder":
|
|
363
|
+
"""Set first token symbol (for Uniswap)."""
|
|
364
|
+
return self._copy_with(symbol0=symbol)
|
|
365
|
+
|
|
366
|
+
def symbol1(self, symbol: str) -> "AsyncQueryBuilder":
|
|
367
|
+
"""Set second token symbol (for Uniswap)."""
|
|
368
|
+
return self._copy_with(symbol1=symbol)
|
|
369
|
+
|
|
370
|
+
def fee(self, fee_tier: int) -> "AsyncQueryBuilder":
|
|
371
|
+
"""Set fee tier (for Uniswap): 100, 500, 3000, 10000."""
|
|
372
|
+
return self._copy_with(fee=fee_tier)
|
|
373
|
+
|
|
374
|
+
def pool(self, address: str) -> "AsyncQueryBuilder":
|
|
375
|
+
"""Set pool address (for Uniswap)."""
|
|
376
|
+
return self._copy_with(pool=address)
|
|
377
|
+
|
|
378
|
+
# Verbose mode
|
|
379
|
+
def verbose(self, enabled: bool = True) -> "AsyncQueryBuilder":
|
|
380
|
+
"""Include all metadata fields (tx_hash, tx_id, log_index, network, name)."""
|
|
381
|
+
return self._copy_with(verbose=enabled)
|
|
382
|
+
|
|
383
|
+
# Build final params
|
|
384
|
+
def _build_params(self) -> dict[str, Any]:
|
|
385
|
+
"""Build the final query parameters."""
|
|
386
|
+
params = self._params.copy()
|
|
387
|
+
if self._verbose:
|
|
388
|
+
params["verbose"] = "true"
|
|
389
|
+
return params
|
|
390
|
+
|
|
391
|
+
# Terminal methods - execute the query (async)
|
|
392
|
+
async def as_dict(self) -> list[dict[str, Any]]:
|
|
393
|
+
"""
|
|
394
|
+
Execute query and return results as list of dictionaries.
|
|
395
|
+
|
|
396
|
+
Uses JSON format from API.
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
List of event dictionaries
|
|
400
|
+
"""
|
|
401
|
+
params = self._build_params()
|
|
402
|
+
return await self._client._request("GET", self._endpoint, params=params)
|
|
403
|
+
|
|
404
|
+
async def as_df(self, library: str = "pandas") -> Any:
|
|
405
|
+
"""
|
|
406
|
+
Execute query and return results as DataFrame.
|
|
407
|
+
|
|
408
|
+
Uses Parquet format from API for efficiency, then converts to DataFrame.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
library: DataFrame library to use - "pandas" (default) or "polars"
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
pandas.DataFrame or polars.DataFrame
|
|
415
|
+
|
|
416
|
+
Example:
|
|
417
|
+
df = await query.as_df() # pandas DataFrame
|
|
418
|
+
df = await query.as_df("polars") # polars DataFrame
|
|
419
|
+
"""
|
|
420
|
+
if library not in ("pandas", "polars"):
|
|
421
|
+
raise ValueError(f"library must be 'pandas' or 'polars', got '{library}'")
|
|
422
|
+
params = self._build_params()
|
|
423
|
+
params["format"] = "parquet"
|
|
424
|
+
return await self._client._request(
|
|
425
|
+
"GET", self._endpoint, params=params, as_dataframe=library
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
async def as_file(self, path: str, format: str | None = None) -> None:
|
|
429
|
+
"""
|
|
430
|
+
Execute query and save results to file.
|
|
431
|
+
|
|
432
|
+
Format is automatically determined by file extension, or can be
|
|
433
|
+
explicitly specified.
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
path: File path to save to
|
|
437
|
+
format: File format - "csv", "parquet", or "json".
|
|
438
|
+
If None, determined from file extension.
|
|
439
|
+
|
|
440
|
+
Example:
|
|
441
|
+
await query.as_file("transfers.csv") # CSV format
|
|
442
|
+
await query.as_file("transfers.parquet") # Parquet format
|
|
443
|
+
await query.as_file("transfers.json") # JSON format
|
|
444
|
+
await query.as_file("transfers", format="csv") # Explicit format
|
|
445
|
+
"""
|
|
446
|
+
# Determine format from extension or explicit parameter
|
|
447
|
+
if format is None:
|
|
448
|
+
if path.endswith(".csv"):
|
|
449
|
+
format = "csv"
|
|
450
|
+
elif path.endswith(".parquet"):
|
|
451
|
+
format = "parquet"
|
|
452
|
+
elif path.endswith(".json"):
|
|
453
|
+
format = "json"
|
|
454
|
+
else:
|
|
455
|
+
raise ValueError(
|
|
456
|
+
f"Cannot determine format from path '{path}'. "
|
|
457
|
+
"Use a file extension (.csv, .parquet, .json) or specify format explicitly."
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
if format not in ("csv", "parquet", "json"):
|
|
461
|
+
raise ValueError(f"format must be 'csv', 'parquet', or 'json', got '{format}'")
|
|
462
|
+
|
|
463
|
+
params = self._build_params()
|
|
464
|
+
|
|
465
|
+
if format == "json":
|
|
466
|
+
# For JSON, fetch as dict and write manually
|
|
467
|
+
import json
|
|
468
|
+
results = await self.as_dict()
|
|
469
|
+
with open(path, "w") as f:
|
|
470
|
+
json.dump(results, f, indent=2)
|
|
471
|
+
else:
|
|
472
|
+
# For CSV and Parquet, use API format parameter
|
|
473
|
+
params["format"] = format
|
|
474
|
+
await self._client._request("GET", self._endpoint, params=params, output_file=path)
|
|
475
|
+
|
|
476
|
+
def __repr__(self) -> str:
|
|
477
|
+
return f"AsyncQueryBuilder(endpoint={self._endpoint!r}, params={self._params!r}, verbose={self._verbose})"
|