defistream 1.0.4__py3-none-any.whl → 1.1.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 CHANGED
@@ -29,23 +29,9 @@ from .exceptions import (
29
29
  ServerError,
30
30
  ValidationError,
31
31
  )
32
- from .models import (
33
- AAVEBorrowEvent,
34
- AAVEDepositEvent,
35
- AAVELiquidationEvent,
36
- AAVERepayEvent,
37
- AAVEWithdrawEvent,
38
- ERC20TransferEvent,
39
- EventBase,
40
- LidoDepositEvent,
41
- LidoWithdrawEvent,
42
- NativeTransferEvent,
43
- ResponseMetadata,
44
- UniswapSwapEvent,
45
- )
46
32
  from .query import AsyncQueryBuilder, QueryBuilder
47
33
 
48
- __version__ = "1.0.4"
34
+ __version__ = "1.1.0"
49
35
 
50
36
  __all__ = [
51
37
  # Clients
@@ -62,17 +48,4 @@ __all__ = [
62
48
  "ValidationError",
63
49
  "NotFoundError",
64
50
  "ServerError",
65
- # Models
66
- "EventBase",
67
- "ResponseMetadata",
68
- "ERC20TransferEvent",
69
- "NativeTransferEvent",
70
- "AAVEDepositEvent",
71
- "AAVEWithdrawEvent",
72
- "AAVEBorrowEvent",
73
- "AAVERepayEvent",
74
- "AAVELiquidationEvent",
75
- "UniswapSwapEvent",
76
- "LidoDepositEvent",
77
- "LidoWithdrawEvent",
78
51
  ]
defistream/models.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """Pydantic models for DeFiStream API responses."""
2
2
 
3
3
  from typing import Any, Literal
4
- from pydantic import BaseModel, Field
4
+ from pydantic import BaseModel
5
5
 
6
6
 
7
7
  class ResponseMetadata(BaseModel):
@@ -12,153 +12,6 @@ class ResponseMetadata(BaseModel):
12
12
  request_cost: int | None = None
13
13
 
14
14
 
15
- class EventBase(BaseModel):
16
- """Base model for all events."""
17
-
18
- block_number: int
19
- time: str | None = None
20
- # Verbose fields (only present when verbose=true)
21
- name: str | None = None
22
- network: str | None = None
23
- tx_id: str | None = None
24
- tx_hash: str | None = None
25
- log_index: int | None = None
26
-
27
-
28
- class ERC20TransferEvent(EventBase):
29
- """ERC20 Transfer event."""
30
-
31
- sender: str = Field(alias="from_address", default="")
32
- receiver: str = Field(alias="to_address", default="")
33
- amount: float = 0.0
34
- token_address: str | None = None
35
- token_symbol: str | None = None
36
-
37
- model_config = {"populate_by_name": True}
38
-
39
-
40
- class ERC20ApprovalEvent(EventBase):
41
- """ERC20 Approval event."""
42
-
43
- owner: str = ""
44
- spender: str = ""
45
- amount: float = 0.0
46
- token_address: str | None = None
47
- token_symbol: str | None = None
48
-
49
-
50
- class NativeTransferEvent(EventBase):
51
- """Native token (ETH/MATIC/etc) transfer event."""
52
-
53
- sender: str = ""
54
- receiver: str = ""
55
- amount: float = 0.0
56
-
57
-
58
- class AAVEDepositEvent(EventBase):
59
- """AAVE V3 Supply/Deposit event."""
60
-
61
- user: str = ""
62
- reserve: str = ""
63
- amount: float = 0.0
64
- on_behalf_of: str | None = None
65
-
66
-
67
- class AAVEWithdrawEvent(EventBase):
68
- """AAVE V3 Withdraw event."""
69
-
70
- user: str = ""
71
- reserve: str = ""
72
- amount: float = 0.0
73
- to: str | None = None
74
-
75
-
76
- class AAVEBorrowEvent(EventBase):
77
- """AAVE V3 Borrow event."""
78
-
79
- user: str = ""
80
- reserve: str = ""
81
- amount: float = 0.0
82
- interest_rate_mode: int | None = None
83
- borrow_rate: float | None = None
84
- on_behalf_of: str | None = None
85
-
86
-
87
- class AAVERepayEvent(EventBase):
88
- """AAVE V3 Repay event."""
89
-
90
- user: str = ""
91
- reserve: str = ""
92
- amount: float = 0.0
93
- repayer: str | None = None
94
- use_a_tokens: bool | None = None
95
-
96
-
97
- class AAVELiquidationEvent(EventBase):
98
- """AAVE V3 Liquidation event."""
99
-
100
- liquidator: str = ""
101
- user: str = ""
102
- collateral_asset: str = ""
103
- debt_asset: str = ""
104
- debt_to_cover: float = 0.0
105
- liquidated_collateral_amount: float = 0.0
106
- receive_a_token: bool | None = None
107
-
108
-
109
- class UniswapSwapEvent(EventBase):
110
- """Uniswap V3 Swap event."""
111
-
112
- pool: str = ""
113
- sender: str = ""
114
- recipient: str = ""
115
- amount0: float = 0.0
116
- amount1: float = 0.0
117
- sqrt_price_x96: int | None = None
118
- liquidity: int | None = None
119
- tick: int | None = None
120
-
121
-
122
- class UniswapMintEvent(EventBase):
123
- """Uniswap V3 Mint (add liquidity) event."""
124
-
125
- pool: str = ""
126
- owner: str = ""
127
- tick_lower: int = 0
128
- tick_upper: int = 0
129
- amount: int = 0
130
- amount0: float = 0.0
131
- amount1: float = 0.0
132
-
133
-
134
- class UniswapBurnEvent(EventBase):
135
- """Uniswap V3 Burn (remove liquidity) event."""
136
-
137
- pool: str = ""
138
- owner: str = ""
139
- tick_lower: int = 0
140
- tick_upper: int = 0
141
- amount: int = 0
142
- amount0: float = 0.0
143
- amount1: float = 0.0
144
-
145
-
146
- class LidoDepositEvent(EventBase):
147
- """Lido stETH deposit event."""
148
-
149
- sender: str = ""
150
- amount: float = 0.0
151
- shares: float = 0.0
152
-
153
-
154
- class LidoWithdrawEvent(EventBase):
155
- """Lido stETH withdrawal event."""
156
-
157
- owner: str = ""
158
- request_id: int = 0
159
- amount: float = 0.0
160
-
161
-
162
15
  class EventsResponse(BaseModel):
163
16
  """Standard events API response."""
164
17
 
defistream/query.py CHANGED
@@ -4,10 +4,84 @@ from __future__ import annotations
4
4
 
5
5
  from typing import TYPE_CHECKING, Any
6
6
 
7
+ from .exceptions import ValidationError
8
+
7
9
  if TYPE_CHECKING:
8
10
  from .client import BaseClient
9
11
 
10
12
 
13
+ # Label/category parameter names that need SQL safety checks
14
+ _LABEL_CATEGORY_PARAMS = frozenset({
15
+ "involving_label", "involving_category",
16
+ "sender_label", "sender_category",
17
+ "receiver_label", "receiver_category",
18
+ })
19
+
20
+ # Mutual exclusivity groups: within each slot, only one key is allowed
21
+ _INVOLVING_SLOT = ("involving", "involving_label", "involving_category")
22
+ _SENDER_SLOT = ("sender", "sender_label", "sender_category")
23
+ _RECEIVER_SLOT = ("receiver", "receiver_label", "receiver_category")
24
+
25
+
26
+ def _normalize_multi(*args: str) -> str:
27
+ """Join varargs into a comma-separated string.
28
+
29
+ Accepts one or more strings. If a single pre-joined string is passed
30
+ (e.g. "a,b") it passes through unchanged.
31
+
32
+ Raises:
33
+ ValueError: If no arguments are provided.
34
+ """
35
+ if not args:
36
+ raise ValueError("At least one value is required")
37
+ return ",".join(args)
38
+
39
+
40
+ def _validate_sql_safety(params: dict[str, Any]) -> None:
41
+ """Reject label/category values containing quotes or backslashes.
42
+
43
+ Raises:
44
+ ValidationError: If any label/category value contains ' or \\.
45
+ """
46
+ for key in _LABEL_CATEGORY_PARAMS:
47
+ value = params.get(key)
48
+ if value is not None and isinstance(value, str):
49
+ if "'" in value or "\\" in value:
50
+ raise ValidationError(
51
+ f"Parameter '{key}' contains unsafe characters (single quote or backslash)"
52
+ )
53
+
54
+
55
+ def _validate_mutual_exclusivity(params: dict[str, Any]) -> None:
56
+ """Enforce mutual exclusivity between filter slots.
57
+
58
+ Within each slot (involving, sender, receiver) only one key is allowed.
59
+ Cross-slot: involving* cannot combine with sender* or receiver*.
60
+
61
+ Raises:
62
+ ValidationError: If conflicting parameters are present.
63
+ """
64
+ def _check_slot(slot: tuple[str, ...], slot_name: str) -> str | None:
65
+ present = [k for k in slot if k in params]
66
+ if len(present) > 1:
67
+ raise ValidationError(
68
+ f"Cannot combine {present[0]} with {present[1]} — "
69
+ f"pick one {slot_name} filter"
70
+ )
71
+ return present[0] if present else None
72
+
73
+ involving_key = _check_slot(_INVOLVING_SLOT, "involving")
74
+ sender_key = _check_slot(_SENDER_SLOT, "sender")
75
+ receiver_key = _check_slot(_RECEIVER_SLOT, "receiver")
76
+
77
+ if involving_key and (sender_key or receiver_key):
78
+ other = sender_key or receiver_key
79
+ raise ValidationError(
80
+ f"Cannot combine {involving_key} with {other} — "
81
+ f"use involving* or sender*/receiver*, not both"
82
+ )
83
+
84
+
11
85
  class QueryBuilder:
12
86
  """
13
87
  Builder for constructing and executing DeFiStream API queries.
@@ -78,21 +152,49 @@ class QueryBuilder:
78
152
  return self._copy_with(since=start, until=end)
79
153
 
80
154
  # 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)
155
+ def sender(self, *addresses: str) -> "QueryBuilder":
156
+ """Filter by sender address (ERC20, Native Token). Accepts multiple addresses."""
157
+ return self._copy_with(sender=_normalize_multi(*addresses))
158
+
159
+ def receiver(self, *addresses: str) -> "QueryBuilder":
160
+ """Filter by receiver address (ERC20, Native Token). Accepts multiple addresses."""
161
+ return self._copy_with(receiver=_normalize_multi(*addresses))
162
+
163
+ def from_address(self, *addresses: str) -> "QueryBuilder":
164
+ """Filter by sender address (alias for sender). Accepts multiple addresses."""
165
+ return self.sender(*addresses)
166
+
167
+ def to_address(self, *addresses: str) -> "QueryBuilder":
168
+ """Filter by receiver address (alias for receiver). Accepts multiple addresses."""
169
+ return self.receiver(*addresses)
170
+
171
+ def involving(self, *addresses: str) -> "QueryBuilder":
172
+ """Filter by any involved address (all protocols). Accepts multiple addresses."""
173
+ return self._copy_with(involving=_normalize_multi(*addresses))
174
+
175
+ def involving_label(self, *labels: str) -> "QueryBuilder":
176
+ """Filter by label on any involved address (all protocols)."""
177
+ return self._copy_with(involving_label=_normalize_multi(*labels))
178
+
179
+ def involving_category(self, *categories: str) -> "QueryBuilder":
180
+ """Filter by category on any involved address (all protocols)."""
181
+ return self._copy_with(involving_category=_normalize_multi(*categories))
84
182
 
85
- def receiver(self, address: str) -> "QueryBuilder":
86
- """Filter by receiver address (ERC20, Native Token)."""
87
- return self._copy_with(receiver=address)
183
+ def sender_label(self, *labels: str) -> "QueryBuilder":
184
+ """Filter sender by label (ERC20, Native Token)."""
185
+ return self._copy_with(sender_label=_normalize_multi(*labels))
88
186
 
89
- def from_address(self, address: str) -> "QueryBuilder":
90
- """Filter by sender address (alias for sender)."""
91
- return self.sender(address)
187
+ def sender_category(self, *categories: str) -> "QueryBuilder":
188
+ """Filter sender by category (ERC20, Native Token)."""
189
+ return self._copy_with(sender_category=_normalize_multi(*categories))
92
190
 
93
- def to_address(self, address: str) -> "QueryBuilder":
94
- """Filter by receiver address (alias for receiver)."""
95
- return self.receiver(address)
191
+ def receiver_label(self, *labels: str) -> "QueryBuilder":
192
+ """Filter receiver by label (ERC20, Native Token)."""
193
+ return self._copy_with(receiver_label=_normalize_multi(*labels))
194
+
195
+ def receiver_category(self, *categories: str) -> "QueryBuilder":
196
+ """Filter receiver by category (ERC20, Native Token)."""
197
+ return self._copy_with(receiver_category=_normalize_multi(*categories))
96
198
 
97
199
  def min_amount(self, amount: float) -> "QueryBuilder":
98
200
  """Filter by minimum amount (ERC20, Native Token)."""
@@ -134,6 +236,8 @@ class QueryBuilder:
134
236
  def _build_params(self) -> dict[str, Any]:
135
237
  """Build the final query parameters."""
136
238
  params = self._params.copy()
239
+ _validate_sql_safety(params)
240
+ _validate_mutual_exclusivity(params)
137
241
  if self._verbose:
138
242
  params["verbose"] = "true"
139
243
  return params
@@ -294,21 +398,49 @@ class AsyncQueryBuilder:
294
398
  return self._copy_with(since=start, until=end)
295
399
 
296
400
  # 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)
401
+ def sender(self, *addresses: str) -> "AsyncQueryBuilder":
402
+ """Filter by sender address (ERC20, Native Token). Accepts multiple addresses."""
403
+ return self._copy_with(sender=_normalize_multi(*addresses))
404
+
405
+ def receiver(self, *addresses: str) -> "AsyncQueryBuilder":
406
+ """Filter by receiver address (ERC20, Native Token). Accepts multiple addresses."""
407
+ return self._copy_with(receiver=_normalize_multi(*addresses))
408
+
409
+ def from_address(self, *addresses: str) -> "AsyncQueryBuilder":
410
+ """Filter by sender address (alias for sender). Accepts multiple addresses."""
411
+ return self.sender(*addresses)
412
+
413
+ def to_address(self, *addresses: str) -> "AsyncQueryBuilder":
414
+ """Filter by receiver address (alias for receiver). Accepts multiple addresses."""
415
+ return self.receiver(*addresses)
416
+
417
+ def involving(self, *addresses: str) -> "AsyncQueryBuilder":
418
+ """Filter by any involved address (all protocols). Accepts multiple addresses."""
419
+ return self._copy_with(involving=_normalize_multi(*addresses))
420
+
421
+ def involving_label(self, *labels: str) -> "AsyncQueryBuilder":
422
+ """Filter by label on any involved address (all protocols)."""
423
+ return self._copy_with(involving_label=_normalize_multi(*labels))
424
+
425
+ def involving_category(self, *categories: str) -> "AsyncQueryBuilder":
426
+ """Filter by category on any involved address (all protocols)."""
427
+ return self._copy_with(involving_category=_normalize_multi(*categories))
428
+
429
+ def sender_label(self, *labels: str) -> "AsyncQueryBuilder":
430
+ """Filter sender by label (ERC20, Native Token)."""
431
+ return self._copy_with(sender_label=_normalize_multi(*labels))
300
432
 
301
- def receiver(self, address: str) -> "AsyncQueryBuilder":
302
- """Filter by receiver address (ERC20, Native Token)."""
303
- return self._copy_with(receiver=address)
433
+ def sender_category(self, *categories: str) -> "AsyncQueryBuilder":
434
+ """Filter sender by category (ERC20, Native Token)."""
435
+ return self._copy_with(sender_category=_normalize_multi(*categories))
304
436
 
305
- def from_address(self, address: str) -> "AsyncQueryBuilder":
306
- """Filter by sender address (alias for sender)."""
307
- return self.sender(address)
437
+ def receiver_label(self, *labels: str) -> "AsyncQueryBuilder":
438
+ """Filter receiver by label (ERC20, Native Token)."""
439
+ return self._copy_with(receiver_label=_normalize_multi(*labels))
308
440
 
309
- def to_address(self, address: str) -> "AsyncQueryBuilder":
310
- """Filter by receiver address (alias for receiver)."""
311
- return self.receiver(address)
441
+ def receiver_category(self, *categories: str) -> "AsyncQueryBuilder":
442
+ """Filter receiver by category (ERC20, Native Token)."""
443
+ return self._copy_with(receiver_category=_normalize_multi(*categories))
312
444
 
313
445
  def min_amount(self, amount: float) -> "AsyncQueryBuilder":
314
446
  """Filter by minimum amount (ERC20, Native Token)."""
@@ -350,6 +482,8 @@ class AsyncQueryBuilder:
350
482
  def _build_params(self) -> dict[str, Any]:
351
483
  """Build the final query parameters."""
352
484
  params = self._params.copy()
485
+ _validate_sql_safety(params)
486
+ _validate_mutual_exclusivity(params)
353
487
  if self._verbose:
354
488
  params["verbose"] = "true"
355
489
  return params
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: defistream
3
- Version: 1.0.4
3
+ Version: 1.1.0
4
4
  Summary: Python client for the DeFiStream API
5
5
  Project-URL: Homepage, https://defistream.dev
6
6
  Project-URL: Documentation, https://docs.defistream.dev
@@ -204,6 +204,47 @@ df = (
204
204
  )
205
205
  ```
206
206
 
207
+ ### Label & Category Filters
208
+
209
+ ```python
210
+ # Get USDT transfers involving Binance wallets
211
+ df = (
212
+ client.erc20.transfers("USDT")
213
+ .network("ETH")
214
+ .block_range(21000000, 21010000)
215
+ .involving_label("Binance")
216
+ .as_df()
217
+ )
218
+
219
+ # Get USDT transfers FROM exchanges TO DeFi protocols
220
+ df = (
221
+ client.erc20.transfers("USDT")
222
+ .network("ETH")
223
+ .block_range(21000000, 21010000)
224
+ .sender_category("exchange")
225
+ .receiver_category("defi")
226
+ .as_df()
227
+ )
228
+
229
+ # Get AAVE deposits involving exchange addresses
230
+ df = (
231
+ client.aave.deposits()
232
+ .network("ETH")
233
+ .block_range(21000000, 21010000)
234
+ .involving_category("exchange")
235
+ .as_df()
236
+ )
237
+
238
+ # Get native ETH transfers FROM Binance or Coinbase (multi-value)
239
+ df = (
240
+ client.native_token.transfers()
241
+ .network("ETH")
242
+ .block_range(21000000, 21010000)
243
+ .sender_label("Binance,Coinbase")
244
+ .as_df()
245
+ )
246
+ ```
247
+
207
248
  ### Verbose Mode
208
249
 
209
250
  By default, responses omit metadata fields to reduce payload size. Use `.verbose()` to include all fields:
@@ -423,6 +464,23 @@ print(f"Request cost: {client.last_response.request_cost}")
423
464
  | `.symbol1(sym)` | Uniswap | Second token symbol (required) |
424
465
  | `.fee(tier)` | Uniswap | Fee tier: 100, 500, 3000, 10000 (required) |
425
466
 
467
+ ### Address Label & Category Filters
468
+
469
+ Filter events by entity names or categories using the labels database. Available on all protocols.
470
+
471
+ | Method | Protocols | Description |
472
+ |--------|-----------|-------------|
473
+ | `.involving_label(label)` | All | Filter where any involved address matches a label substring (e.g., "Binance") |
474
+ | `.involving_category(cat)` | All | Filter where any involved address matches a category (e.g., "exchange") |
475
+ | `.sender_label(label)` | ERC20, Native | Filter sender by label substring |
476
+ | `.sender_category(cat)` | ERC20, Native | Filter sender by category |
477
+ | `.receiver_label(label)` | ERC20, Native | Filter receiver by label substring |
478
+ | `.receiver_category(cat)` | ERC20, Native | Filter receiver by category |
479
+
480
+ **Multi-value support:** Comma-separated values are accepted (e.g., `"Binance,Coinbase"`).
481
+
482
+ **Mutual exclusivity:** Within each slot (involving/sender/receiver), only one of address/label/category can be set. `involving*` filters cannot be combined with `sender*`/`receiver*` filters.
483
+
426
484
  ### Terminal Methods
427
485
 
428
486
  | Method | Description |
@@ -0,0 +1,11 @@
1
+ defistream/__init__.py,sha256=h-ca8lWZyPYPDd-rFTvO9J2ptW_p-SvspvgmgAe_wIc,1089
2
+ defistream/client.py,sha256=Ku8ouDbM6Mx4lVmqvBwNvNt-h2FkvqauPMSjKyPkjU4,12717
3
+ defistream/exceptions.py,sha256=_GxZQ18_YvXFtmNHeddWV8fHPIllHgFeP7fP0CmHF1k,1492
4
+ defistream/models.py,sha256=Zw3DHAISxB6pivKybNzyHXR5IcRwvTZl23DHbjfyKwM,622
5
+ defistream/protocols.py,sha256=5_bYd46lDy-mK6LZ8sTGW0Z2IVH4g0cQ5rBbbOXsw0U,15368
6
+ defistream/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ defistream/query.py,sha256=yzrVfeRDZqoTYdQtj7SoYa7yd-NfO8heQ85YxhefU7c,22904
8
+ defistream-1.1.0.dist-info/METADATA,sha256=hO1hUJMZZKkKOyd6B2V-r4sYRkaCmCR32VYfKJ_l3E4,12794
9
+ defistream-1.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
10
+ defistream-1.1.0.dist-info/licenses/LICENSE,sha256=72DWAof8dMePfFQmfaswClW5d-sE6k7p-7VpuSKLmU4,1067
11
+ defistream-1.1.0.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- defistream/__init__.py,sha256=Vme3GaE4v7qXQcFFbpFe3Soaq06a_4I1ognx8DhzN-U,1680
2
- defistream/client.py,sha256=Ku8ouDbM6Mx4lVmqvBwNvNt-h2FkvqauPMSjKyPkjU4,12717
3
- defistream/exceptions.py,sha256=_GxZQ18_YvXFtmNHeddWV8fHPIllHgFeP7fP0CmHF1k,1492
4
- defistream/models.py,sha256=JiG4IUq19HIANz9MHQCkaE6onGNyNzXcbdJmosrHsw0,3812
5
- defistream/protocols.py,sha256=5_bYd46lDy-mK6LZ8sTGW0Z2IVH4g0cQ5rBbbOXsw0U,15368
6
- defistream/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- defistream/query.py,sha256=sNn_0OzNDlsp4N74Grumsb08R5OFPPoppPok3vh8Y6Q,16775
8
- defistream-1.0.4.dist-info/METADATA,sha256=YGb1XcogAIo9FkKyHYSkCoD0Po0exCuHrrz8i2M3J6w,10877
9
- defistream-1.0.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
10
- defistream-1.0.4.dist-info/licenses/LICENSE,sha256=72DWAof8dMePfFQmfaswClW5d-sE6k7p-7VpuSKLmU4,1067
11
- defistream-1.0.4.dist-info/RECORD,,