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