defistream 1.3.0__tar.gz → 1.4.1__tar.gz
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-1.3.0 → defistream-1.4.1}/PKG-INFO +31 -1
- {defistream-1.3.0 → defistream-1.4.1}/README.md +30 -0
- {defistream-1.3.0 → defistream-1.4.1}/pyproject.toml +4 -1
- {defistream-1.3.0 → defistream-1.4.1}/src/defistream/__init__.py +1 -1
- {defistream-1.3.0 → defistream-1.4.1}/src/defistream/query.py +30 -0
- defistream-1.3.0/.claude/settings.local.json +0 -34
- defistream-1.3.0/tests/__init__.py +0 -0
- defistream-1.3.0/tests/conftest.py +0 -117
- defistream-1.3.0/tests/test_client.py +0 -1056
- defistream-1.3.0/tests/test_integration.py +0 -1484
- {defistream-1.3.0 → defistream-1.4.1}/.gitignore +0 -0
- {defistream-1.3.0 → defistream-1.4.1}/LICENSE +0 -0
- {defistream-1.3.0 → defistream-1.4.1}/src/defistream/client.py +0 -0
- {defistream-1.3.0 → defistream-1.4.1}/src/defistream/exceptions.py +0 -0
- {defistream-1.3.0 → defistream-1.4.1}/src/defistream/models.py +0 -0
- {defistream-1.3.0 → defistream-1.4.1}/src/defistream/protocols.py +0 -0
- {defistream-1.3.0 → defistream-1.4.1}/src/defistream/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: defistream
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.1
|
|
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
|
|
@@ -337,6 +337,35 @@ df = (
|
|
|
337
337
|
)
|
|
338
338
|
```
|
|
339
339
|
|
|
340
|
+
### Price Enrichment
|
|
341
|
+
|
|
342
|
+
Use `.with_price()` to enrich events with USD price data. This adds `price` (unit price) and `value_usd` (amount × price) columns to individual events. On aggregate endpoints, it produces an `agg_value_usd` (sum) column.
|
|
343
|
+
|
|
344
|
+
Supported protocols: AAVE, Uniswap, Lido, Stader, ERC20, Native Token.
|
|
345
|
+
|
|
346
|
+
```python
|
|
347
|
+
# Individual events with price data
|
|
348
|
+
df = (
|
|
349
|
+
client.aave.deposits()
|
|
350
|
+
.network("ETH")
|
|
351
|
+
.block_range(21000000, 21010000)
|
|
352
|
+
.with_price()
|
|
353
|
+
.as_df()
|
|
354
|
+
)
|
|
355
|
+
# df now includes 'price' and 'value_usd' columns
|
|
356
|
+
|
|
357
|
+
# Aggregate with price — adds agg_value_usd column
|
|
358
|
+
df = (
|
|
359
|
+
client.aave.deposits()
|
|
360
|
+
.network("ETH")
|
|
361
|
+
.block_range(21000000, 21100000)
|
|
362
|
+
.with_price()
|
|
363
|
+
.aggregate(group_by="time", period="2h")
|
|
364
|
+
.as_df()
|
|
365
|
+
)
|
|
366
|
+
# df now includes 'agg_value_usd' column
|
|
367
|
+
```
|
|
368
|
+
|
|
340
369
|
### Return as DataFrame
|
|
341
370
|
|
|
342
371
|
```python
|
|
@@ -541,6 +570,7 @@ print(f"Request cost: {client.last_response.request_cost}")
|
|
|
541
570
|
| `.end_time(ts)` | Set ending time (ISO format or Unix timestamp) |
|
|
542
571
|
| `.time_range(start, end)` | Set both start and end times |
|
|
543
572
|
| `.verbose()` | Include all metadata fields |
|
|
573
|
+
| `.with_price()` | Enrich events with USD price data (`price` and `value_usd` columns) |
|
|
544
574
|
|
|
545
575
|
### Protocol-Specific Parameters
|
|
546
576
|
|
|
@@ -303,6 +303,35 @@ df = (
|
|
|
303
303
|
)
|
|
304
304
|
```
|
|
305
305
|
|
|
306
|
+
### Price Enrichment
|
|
307
|
+
|
|
308
|
+
Use `.with_price()` to enrich events with USD price data. This adds `price` (unit price) and `value_usd` (amount × price) columns to individual events. On aggregate endpoints, it produces an `agg_value_usd` (sum) column.
|
|
309
|
+
|
|
310
|
+
Supported protocols: AAVE, Uniswap, Lido, Stader, ERC20, Native Token.
|
|
311
|
+
|
|
312
|
+
```python
|
|
313
|
+
# Individual events with price data
|
|
314
|
+
df = (
|
|
315
|
+
client.aave.deposits()
|
|
316
|
+
.network("ETH")
|
|
317
|
+
.block_range(21000000, 21010000)
|
|
318
|
+
.with_price()
|
|
319
|
+
.as_df()
|
|
320
|
+
)
|
|
321
|
+
# df now includes 'price' and 'value_usd' columns
|
|
322
|
+
|
|
323
|
+
# Aggregate with price — adds agg_value_usd column
|
|
324
|
+
df = (
|
|
325
|
+
client.aave.deposits()
|
|
326
|
+
.network("ETH")
|
|
327
|
+
.block_range(21000000, 21100000)
|
|
328
|
+
.with_price()
|
|
329
|
+
.aggregate(group_by="time", period="2h")
|
|
330
|
+
.as_df()
|
|
331
|
+
)
|
|
332
|
+
# df now includes 'agg_value_usd' column
|
|
333
|
+
```
|
|
334
|
+
|
|
306
335
|
### Return as DataFrame
|
|
307
336
|
|
|
308
337
|
```python
|
|
@@ -507,6 +536,7 @@ print(f"Request cost: {client.last_response.request_cost}")
|
|
|
507
536
|
| `.end_time(ts)` | Set ending time (ISO format or Unix timestamp) |
|
|
508
537
|
| `.time_range(start, end)` | Set both start and end times |
|
|
509
538
|
| `.verbose()` | Include all metadata fields |
|
|
539
|
+
| `.with_price()` | Enrich events with USD price data (`price` and `value_usd` columns) |
|
|
510
540
|
|
|
511
541
|
### Protocol-Specific Parameters
|
|
512
542
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "defistream"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.4.1"
|
|
8
8
|
description = "Python client for the DeFiStream API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -48,6 +48,9 @@ Repository = "https://github.com/Eren-Nevin/DeFiStream_PythonClient"
|
|
|
48
48
|
[tool.hatch.build.targets.wheel]
|
|
49
49
|
packages = ["src/defistream"]
|
|
50
50
|
|
|
51
|
+
[tool.hatch.build.targets.sdist]
|
|
52
|
+
exclude = [".claude/", "tests/", ".env"]
|
|
53
|
+
|
|
51
54
|
[tool.pytest.ini_options]
|
|
52
55
|
asyncio_mode = "auto"
|
|
53
56
|
testpaths = ["tests"]
|
|
@@ -109,14 +109,18 @@ class QueryBuilder:
|
|
|
109
109
|
self._endpoint = endpoint
|
|
110
110
|
self._params: dict[str, Any] = initial_params or {}
|
|
111
111
|
self._verbose = False
|
|
112
|
+
self._with_price = False
|
|
112
113
|
|
|
113
114
|
def _copy_with(self, **updates: Any) -> "QueryBuilder":
|
|
114
115
|
"""Create a copy with updated parameters."""
|
|
115
116
|
new_builder = QueryBuilder(self._client, self._endpoint, self._params.copy())
|
|
116
117
|
new_builder._verbose = self._verbose
|
|
118
|
+
new_builder._with_price = self._with_price
|
|
117
119
|
for key, value in updates.items():
|
|
118
120
|
if key == "verbose":
|
|
119
121
|
new_builder._verbose = value
|
|
122
|
+
elif key == "with_price":
|
|
123
|
+
new_builder._with_price = value
|
|
120
124
|
elif value is not None:
|
|
121
125
|
new_builder._params[key] = value
|
|
122
126
|
return new_builder
|
|
@@ -232,6 +236,11 @@ class QueryBuilder:
|
|
|
232
236
|
"""Include all metadata fields (tx_hash, tx_id, log_index, network, name)."""
|
|
233
237
|
return self._copy_with(verbose=enabled)
|
|
234
238
|
|
|
239
|
+
# Price enrichment
|
|
240
|
+
def with_price(self, enabled: bool = True) -> "QueryBuilder":
|
|
241
|
+
"""Enrich events with USD price data (adds ``price`` and ``value_usd`` columns)."""
|
|
242
|
+
return self._copy_with(with_price=enabled)
|
|
243
|
+
|
|
235
244
|
# Build final params
|
|
236
245
|
def _build_params(self) -> dict[str, Any]:
|
|
237
246
|
"""Build the final query parameters."""
|
|
@@ -240,6 +249,8 @@ class QueryBuilder:
|
|
|
240
249
|
_validate_mutual_exclusivity(params)
|
|
241
250
|
if self._verbose:
|
|
242
251
|
params["verbose"] = "true"
|
|
252
|
+
if self._with_price:
|
|
253
|
+
params["with_price"] = "true"
|
|
243
254
|
return params
|
|
244
255
|
|
|
245
256
|
# Terminal methods - execute the query
|
|
@@ -345,6 +356,7 @@ class QueryBuilder:
|
|
|
345
356
|
self._params.copy(),
|
|
346
357
|
)
|
|
347
358
|
builder._verbose = self._verbose
|
|
359
|
+
builder._with_price = self._with_price
|
|
348
360
|
builder._params["group_by"] = group_by
|
|
349
361
|
builder._params["period"] = period
|
|
350
362
|
return builder
|
|
@@ -365,9 +377,12 @@ class AggregateQueryBuilder(QueryBuilder):
|
|
|
365
377
|
"""Create a copy preserving aggregate builder type."""
|
|
366
378
|
new_builder = AggregateQueryBuilder(self._client, self._endpoint, self._params.copy())
|
|
367
379
|
new_builder._verbose = self._verbose
|
|
380
|
+
new_builder._with_price = self._with_price
|
|
368
381
|
for key, value in updates.items():
|
|
369
382
|
if key == "verbose":
|
|
370
383
|
new_builder._verbose = value
|
|
384
|
+
elif key == "with_price":
|
|
385
|
+
new_builder._with_price = value
|
|
371
386
|
elif value is not None:
|
|
372
387
|
new_builder._params[key] = value
|
|
373
388
|
return new_builder
|
|
@@ -449,14 +464,18 @@ class AsyncQueryBuilder:
|
|
|
449
464
|
self._endpoint = endpoint
|
|
450
465
|
self._params: dict[str, Any] = initial_params or {}
|
|
451
466
|
self._verbose = False
|
|
467
|
+
self._with_price = False
|
|
452
468
|
|
|
453
469
|
def _copy_with(self, **updates: Any) -> "AsyncQueryBuilder":
|
|
454
470
|
"""Create a copy with updated parameters."""
|
|
455
471
|
new_builder = AsyncQueryBuilder(self._client, self._endpoint, self._params.copy())
|
|
456
472
|
new_builder._verbose = self._verbose
|
|
473
|
+
new_builder._with_price = self._with_price
|
|
457
474
|
for key, value in updates.items():
|
|
458
475
|
if key == "verbose":
|
|
459
476
|
new_builder._verbose = value
|
|
477
|
+
elif key == "with_price":
|
|
478
|
+
new_builder._with_price = value
|
|
460
479
|
elif value is not None:
|
|
461
480
|
new_builder._params[key] = value
|
|
462
481
|
return new_builder
|
|
@@ -572,6 +591,11 @@ class AsyncQueryBuilder:
|
|
|
572
591
|
"""Include all metadata fields (tx_hash, tx_id, log_index, network, name)."""
|
|
573
592
|
return self._copy_with(verbose=enabled)
|
|
574
593
|
|
|
594
|
+
# Price enrichment
|
|
595
|
+
def with_price(self, enabled: bool = True) -> "AsyncQueryBuilder":
|
|
596
|
+
"""Enrich events with USD price data (adds ``price`` and ``value_usd`` columns)."""
|
|
597
|
+
return self._copy_with(with_price=enabled)
|
|
598
|
+
|
|
575
599
|
# Build final params
|
|
576
600
|
def _build_params(self) -> dict[str, Any]:
|
|
577
601
|
"""Build the final query parameters."""
|
|
@@ -580,6 +604,8 @@ class AsyncQueryBuilder:
|
|
|
580
604
|
_validate_mutual_exclusivity(params)
|
|
581
605
|
if self._verbose:
|
|
582
606
|
params["verbose"] = "true"
|
|
607
|
+
if self._with_price:
|
|
608
|
+
params["with_price"] = "true"
|
|
583
609
|
return params
|
|
584
610
|
|
|
585
611
|
# Terminal methods - execute the query (async)
|
|
@@ -685,6 +711,7 @@ class AsyncQueryBuilder:
|
|
|
685
711
|
self._params.copy(),
|
|
686
712
|
)
|
|
687
713
|
builder._verbose = self._verbose
|
|
714
|
+
builder._with_price = self._with_price
|
|
688
715
|
builder._params["group_by"] = group_by
|
|
689
716
|
builder._params["period"] = period
|
|
690
717
|
return builder
|
|
@@ -705,9 +732,12 @@ class AsyncAggregateQueryBuilder(AsyncQueryBuilder):
|
|
|
705
732
|
"""Create a copy preserving async aggregate builder type."""
|
|
706
733
|
new_builder = AsyncAggregateQueryBuilder(self._client, self._endpoint, self._params.copy())
|
|
707
734
|
new_builder._verbose = self._verbose
|
|
735
|
+
new_builder._with_price = self._with_price
|
|
708
736
|
for key, value in updates.items():
|
|
709
737
|
if key == "verbose":
|
|
710
738
|
new_builder._verbose = value
|
|
739
|
+
elif key == "with_price":
|
|
740
|
+
new_builder._with_price = value
|
|
711
741
|
elif value is not None:
|
|
712
742
|
new_builder._params[key] = value
|
|
713
743
|
return new_builder
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(git log:*)",
|
|
5
|
-
"Bash(python -m pytest:*)",
|
|
6
|
-
"Bash(python3 -m pytest:*)",
|
|
7
|
-
"Bash(git add:*)",
|
|
8
|
-
"Bash(git commit -m \"$\\(cat <<''EOF''\nAdd label/category filters and multi-value support to query builders\n\nAdd involving, involving_label, involving_category, sender_label,\nsender_category, receiver_label, receiver_category methods to both\nQueryBuilder and AsyncQueryBuilder. Update sender/receiver/from_address/\nto_address to accept varargs for multi-value filtering. Add SQL safety\nvalidation for label/category params and mutual exclusivity enforcement\nbetween involving* and sender*/receiver* filter slots.\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
|
|
9
|
-
"Bash(python3 -m build:*)",
|
|
10
|
-
"Bash(python3:*)",
|
|
11
|
-
"Bash(pip3 install:*)",
|
|
12
|
-
"Bash(/home/mvp/Running/Horatio_Chain_Scan/DeFiStream/python-client/.venv/bin/pip install:*)",
|
|
13
|
-
"Bash(/home/mvp/Running/Horatio_Chain_Scan/DeFiStream/python-client/.venv/bin/python:*)",
|
|
14
|
-
"Bash(source:*)",
|
|
15
|
-
"Bash(TWINE_USERNAME=__token__ TWINE_PASSWORD=\"$PYPI_PASSWORD\" /home/mvp/Running/Horatio_Chain_Scan/DeFiStream/python-client/.venv/bin/python:*)",
|
|
16
|
-
"Bash(git commit -m \"$\\(cat <<''EOF''\nBump python-client version to 1.1.0\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
|
|
17
|
-
"Bash(git push:*)",
|
|
18
|
-
"Bash(git subtree push:*)",
|
|
19
|
-
"Bash(.venv/bin/python -m pytest:*)",
|
|
20
|
-
"Bash(.venv/bin/python:*)",
|
|
21
|
-
"Bash(git status:*)",
|
|
22
|
-
"Bash(grep:*)",
|
|
23
|
-
"Bash(git commit -m \"$\\(cat <<''EOF''\nAccept case-insensitive network names in events server\n\nNormalize the network query parameter to uppercase at the entry point\nin both _fetch_events_dataframe and _handle_parquet_request so that\neth, Eth, and ETH are all accepted. Add tests for core and API gateway.\nUpdate API docs to note case-insensitivity.\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
|
|
24
|
-
"Bash(git commit:*)",
|
|
25
|
-
"Bash(curl:*)",
|
|
26
|
-
"Bash(tree:*)",
|
|
27
|
-
"Bash(python -m build:*)"
|
|
28
|
-
]
|
|
29
|
-
},
|
|
30
|
-
"enableAllProjectMcpServers": true,
|
|
31
|
-
"enabledMcpjsonServers": [
|
|
32
|
-
"defistream"
|
|
33
|
-
]
|
|
34
|
-
}
|
|
File without changes
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Shared fixtures for DeFiStream python-client integration tests.
|
|
3
|
-
|
|
4
|
-
Mirrors the API test suite conftest at api/tests/api-gateway/decoders/conftest.py,
|
|
5
|
-
adapted for the python-client's builder-pattern interface.
|
|
6
|
-
|
|
7
|
-
Run with:
|
|
8
|
-
python -m pytest tests/test_integration.py -v
|
|
9
|
-
python -m pytest tests/test_integration.py -v --local # local dev gateway
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
import os
|
|
13
|
-
import sys
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
|
|
16
|
-
import pytest
|
|
17
|
-
|
|
18
|
-
from defistream import AsyncDeFiStream, DeFiStream
|
|
19
|
-
|
|
20
|
-
# ---------------------------------------------------------------------------
|
|
21
|
-
# --local flag support
|
|
22
|
-
# ---------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
_local_mode = "--local" in sys.argv
|
|
25
|
-
|
|
26
|
-
_PRODUCTION_URL = "https://api.defistream.dev/v1"
|
|
27
|
-
_LOCAL_URL = "http://localhost:8081/v1"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def pytest_addoption(parser):
|
|
31
|
-
parser.addoption(
|
|
32
|
-
"--local",
|
|
33
|
-
action="store_true",
|
|
34
|
-
default=False,
|
|
35
|
-
help="Test against local dev gateway (http://localhost:8081/v1) instead of production",
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
# ---------------------------------------------------------------------------
|
|
40
|
-
# .env loading
|
|
41
|
-
# ---------------------------------------------------------------------------
|
|
42
|
-
|
|
43
|
-
def _load_env_file(path: Path) -> dict[str, str]:
|
|
44
|
-
if not path.exists():
|
|
45
|
-
return {}
|
|
46
|
-
env: dict[str, str] = {}
|
|
47
|
-
for line in path.read_text(encoding="utf-8").splitlines():
|
|
48
|
-
line = line.strip()
|
|
49
|
-
if not line or line.startswith("#") or "=" not in line:
|
|
50
|
-
continue
|
|
51
|
-
key, value = line.split("=", 1)
|
|
52
|
-
key = key.strip()
|
|
53
|
-
value = value.strip()
|
|
54
|
-
if (value.startswith('"') and value.endswith('"')) or (
|
|
55
|
-
value.startswith("'") and value.endswith("'")
|
|
56
|
-
):
|
|
57
|
-
value = value[1:-1]
|
|
58
|
-
env[key] = value
|
|
59
|
-
return env
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def _get_env() -> dict[str, str]:
|
|
63
|
-
env = dict(os.environ)
|
|
64
|
-
project_root = Path(__file__).resolve().parents[1] # python-client/
|
|
65
|
-
repo_root = project_root.parent # DeFiStream/
|
|
66
|
-
for candidate in [project_root / ".env", repo_root / ".env"]:
|
|
67
|
-
for key, value in _load_env_file(candidate).items():
|
|
68
|
-
if key not in env:
|
|
69
|
-
env[key] = value
|
|
70
|
-
return env
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
_env = _get_env()
|
|
74
|
-
|
|
75
|
-
# ---------------------------------------------------------------------------
|
|
76
|
-
# Resolved configuration
|
|
77
|
-
# ---------------------------------------------------------------------------
|
|
78
|
-
|
|
79
|
-
API_BASE_URL = _LOCAL_URL if _local_mode else _env.get("API_BASE_URL", _PRODUCTION_URL).rstrip("/")
|
|
80
|
-
TEST_API_KEY = _env.get("TEST_API_KEY", "")
|
|
81
|
-
|
|
82
|
-
# ---------------------------------------------------------------------------
|
|
83
|
-
# Auto-skip integration tests when API key is missing
|
|
84
|
-
# ---------------------------------------------------------------------------
|
|
85
|
-
|
|
86
|
-
_skip_no_key = pytest.mark.skipif(
|
|
87
|
-
not TEST_API_KEY,
|
|
88
|
-
reason="TEST_API_KEY not set in environment or .env",
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def pytest_collection_modifyitems(config, items):
|
|
93
|
-
"""Skip integration tests automatically when TEST_API_KEY is absent."""
|
|
94
|
-
for item in items:
|
|
95
|
-
if "test_integration" in str(item.fspath):
|
|
96
|
-
item.add_marker(_skip_no_key)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
# ---------------------------------------------------------------------------
|
|
100
|
-
# Fixtures
|
|
101
|
-
# ---------------------------------------------------------------------------
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
@pytest.fixture(scope="module")
|
|
105
|
-
def client():
|
|
106
|
-
"""Sync DeFiStream client shared across a test module."""
|
|
107
|
-
c = DeFiStream(api_key=TEST_API_KEY, base_url=API_BASE_URL)
|
|
108
|
-
yield c
|
|
109
|
-
c.close()
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
@pytest.fixture
|
|
113
|
-
async def async_client():
|
|
114
|
-
"""Async DeFiStream client — function-scoped for a clean event loop."""
|
|
115
|
-
c = AsyncDeFiStream(api_key=TEST_API_KEY, base_url=API_BASE_URL)
|
|
116
|
-
yield c
|
|
117
|
-
await c.close()
|