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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: defistream
3
- Version: 1.3.0
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.3.0"
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"]
@@ -36,7 +36,7 @@ from .query import (
36
36
  QueryBuilder,
37
37
  )
38
38
 
39
- __version__ = "1.3.0"
39
+ __version__ = "1.4.1"
40
40
 
41
41
  __all__ = [
42
42
  # Clients
@@ -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()