horatio-data-provider 0.1.0__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.
@@ -0,0 +1,9 @@
1
+ .env
2
+ .venv/
3
+ __pycache__/
4
+ *.pyc
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ data/*.duckdb*
9
+ *.parquet
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: horatio-data-provider
3
+ Version: 0.1.0
4
+ Summary: Python client for the Horatio data provider service
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: httpx>=0.25
7
+ Requires-Dist: pandas>=2.0
8
+ Requires-Dist: pyarrow>=14.0
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
11
+ Requires-Dist: pytest>=8.0; extra == 'dev'
12
+ Requires-Dist: respx>=0.21; extra == 'dev'
@@ -0,0 +1,4 @@
1
+ from ._client import DataProviderClient
2
+ from .exceptions import DataProviderError, DataProviderHTTPError
3
+
4
+ __all__ = ["DataProviderClient", "DataProviderError", "DataProviderHTTPError"]
@@ -0,0 +1,42 @@
1
+ import httpx
2
+
3
+ from .erc20 import ERC20Namespace
4
+ from .binance import BinanceNamespace
5
+ from .protocols import (
6
+ AaveNamespace,
7
+ UniswapNamespace,
8
+ LidoNamespace,
9
+ StaderNamespace,
10
+ ThresholdNamespace,
11
+ NativeNamespace,
12
+ CacheNamespace,
13
+ )
14
+
15
+
16
+ class DataProviderClient:
17
+ def __init__(self, url: str):
18
+ self._url = url.rstrip("/")
19
+ self._session = httpx.AsyncClient(timeout=3600)
20
+ self.erc20 = ERC20Namespace(self._session, self._url)
21
+ self.binance = BinanceNamespace(self._session, self._url)
22
+ self.aave = AaveNamespace(self._session, self._url)
23
+ self.uniswap = UniswapNamespace(self._session, self._url)
24
+ self.lido = LidoNamespace(self._session, self._url)
25
+ self.stader = StaderNamespace(self._session, self._url)
26
+ self.threshold = ThresholdNamespace(self._session, self._url)
27
+ self.native = NativeNamespace(self._session, self._url)
28
+ self.cache = CacheNamespace(self._session, self._url)
29
+
30
+ async def health(self) -> bool:
31
+ response = await self._session.get(self._url + "/health")
32
+ response.raise_for_status()
33
+ return True
34
+
35
+ async def close(self) -> None:
36
+ await self._session.aclose()
37
+
38
+ async def __aenter__(self) -> "DataProviderClient":
39
+ return self
40
+
41
+ async def __aexit__(self, *_) -> None:
42
+ await self.close()
@@ -0,0 +1,17 @@
1
+ import io
2
+
3
+ import httpx
4
+ import pandas as pd
5
+ import pyarrow.parquet as pq
6
+
7
+ from .exceptions import DataProviderHTTPError
8
+
9
+
10
+ async def fetch_parquet(session: httpx.AsyncClient, url: str, body: dict) -> pd.DataFrame:
11
+ response = await session.post(url, json=body)
12
+ content_type = response.headers.get("content-type", "")
13
+ if "application/json" in content_type:
14
+ data = response.json()
15
+ raise DataProviderHTTPError(response.status_code, data.get("error", str(data)))
16
+ response.raise_for_status()
17
+ return pq.read_table(io.BytesIO(response.content)).to_pandas()
@@ -0,0 +1,36 @@
1
+ import httpx
2
+ import pandas as pd
3
+
4
+ from ._http import fetch_parquet
5
+
6
+
7
+ class BaseQuery:
8
+ def __init__(self, session: httpx.AsyncClient, base_url: str, body: dict):
9
+ self._session = session
10
+ self._base_url = base_url
11
+ self._body = body
12
+
13
+ def network(self, n: str) -> "BaseQuery":
14
+ self._body["network"] = n
15
+ return self
16
+
17
+ def time_range(self, since: str, until: str) -> "BaseQuery":
18
+ self._body["since"] = since
19
+ self._body["until"] = until
20
+ return self
21
+
22
+
23
+ class CacheableQuery(BaseQuery):
24
+ def cache(self, cache_type: str = "append") -> "CacheableQuery":
25
+ self._body["cache"] = True
26
+ self._body["cache_type"] = cache_type
27
+ return self
28
+
29
+
30
+ class EventQuery(BaseQuery):
31
+ def __init__(self, session: httpx.AsyncClient, base_url: str, path: str, body: dict):
32
+ super().__init__(session, base_url, body)
33
+ self._path = path
34
+
35
+ async def fetch(self) -> pd.DataFrame:
36
+ return await fetch_parquet(self._session, self._base_url + self._path, self._body)
@@ -0,0 +1,39 @@
1
+ import httpx
2
+ import pandas as pd
3
+
4
+ from ._http import fetch_parquet
5
+ from ._query import BaseQuery, CacheableQuery
6
+
7
+
8
+ class RawTradesQuery(CacheableQuery):
9
+ def __init__(self, session: httpx.AsyncClient, base_url: str, token: str):
10
+ super().__init__(session, base_url, {"token": token})
11
+
12
+ async def fetch(self) -> pd.DataFrame:
13
+ return await fetch_parquet(self._session, self._base_url + "/binance/raw_trades/read", self._body)
14
+
15
+
16
+ class OHLCVQuery(BaseQuery):
17
+ def __init__(self, session: httpx.AsyncClient, base_url: str, token: str, window: str):
18
+ super().__init__(session, base_url, {"token": token, "window": window})
19
+
20
+ async def fetch(self) -> pd.DataFrame:
21
+ return await fetch_parquet(self._session, self._base_url + "/binance/ohlcv/read", self._body)
22
+
23
+
24
+ class BinanceNamespace:
25
+ def __init__(self, session: httpx.AsyncClient, base_url: str):
26
+ self._session = session
27
+ self._base_url = base_url
28
+
29
+ def raw_trades(self, token: str) -> RawTradesQuery:
30
+ return RawTradesQuery(self._session, self._base_url, token)
31
+
32
+ def ohlcv(self, token: str, window: str) -> OHLCVQuery:
33
+ return OHLCVQuery(self._session, self._base_url, token, window)
34
+
35
+ async def flush_raw_trades(self, token: str | None = None) -> None:
36
+ body = {}
37
+ if token is not None:
38
+ body["token"] = token
39
+ await self._session.post(self._base_url + "/binance/raw_trades/flush", json=body)
@@ -0,0 +1,40 @@
1
+ import httpx
2
+ import pandas as pd
3
+
4
+ from ._http import fetch_parquet
5
+ from ._query import CacheableQuery
6
+
7
+
8
+ class ERC20TransfersQuery(CacheableQuery):
9
+ def __init__(self, session: httpx.AsyncClient, base_url: str, tokens: list[str]):
10
+ super().__init__(session, base_url, {"tokens": tokens})
11
+ self._min_amount = None
12
+
13
+ def min_amount(self, amount: float) -> "ERC20TransfersQuery":
14
+ self._min_amount = amount
15
+ return self
16
+
17
+ async def fetch(self) -> pd.DataFrame:
18
+ if self._min_amount is not None:
19
+ self._body["min_amount"] = self._min_amount
20
+ path = "/erc20_transfers/read/min"
21
+ else:
22
+ path = "/erc20_transfers/read"
23
+ return await fetch_parquet(self._session, self._base_url + path, self._body)
24
+
25
+
26
+ class ERC20Namespace:
27
+ def __init__(self, session: httpx.AsyncClient, base_url: str):
28
+ self._session = session
29
+ self._base_url = base_url
30
+
31
+ def transfers(self, tokens: list[str]) -> ERC20TransfersQuery:
32
+ return ERC20TransfersQuery(self._session, self._base_url, tokens)
33
+
34
+ async def flush(self, network: str | None = None, token: str | None = None) -> None:
35
+ body = {}
36
+ if network is not None:
37
+ body["network"] = network
38
+ if token is not None:
39
+ body["token"] = token
40
+ await self._session.post(self._base_url + "/erc20_transfers/flush", json=body)
@@ -0,0 +1,8 @@
1
+ class DataProviderError(Exception):
2
+ pass
3
+
4
+
5
+ class DataProviderHTTPError(DataProviderError):
6
+ def __init__(self, status_code: int, message: str):
7
+ self.status_code = status_code
8
+ super().__init__(f"HTTP {status_code}: {message}")
@@ -0,0 +1,151 @@
1
+ import httpx
2
+ import pandas as pd
3
+
4
+ from ._http import fetch_parquet
5
+ from ._query import BaseQuery, EventQuery
6
+
7
+
8
+ class AaveNamespace:
9
+ def __init__(self, session: httpx.AsyncClient, base_url: str):
10
+ self._session = session
11
+ self._base_url = base_url
12
+
13
+ def _q(self, event: str) -> EventQuery:
14
+ return EventQuery(self._session, self._base_url, "/aave/read", {"event": event})
15
+
16
+ def deposits(self) -> EventQuery:
17
+ return self._q("deposit")
18
+
19
+ def withdrawals(self) -> EventQuery:
20
+ return self._q("withdraw")
21
+
22
+ def borrows(self) -> EventQuery:
23
+ return self._q("borrow")
24
+
25
+ def repays(self) -> EventQuery:
26
+ return self._q("repay")
27
+
28
+ def flashloans(self) -> EventQuery:
29
+ return self._q("flashloan")
30
+
31
+ def liquidations(self) -> EventQuery:
32
+ return self._q("liquidation")
33
+
34
+
35
+ class UniswapNamespace:
36
+ def __init__(self, session: httpx.AsyncClient, base_url: str):
37
+ self._session = session
38
+ self._base_url = base_url
39
+
40
+ def _q(self, event: str, symbol0: str, symbol1: str, fee: int) -> EventQuery:
41
+ return EventQuery(
42
+ self._session,
43
+ self._base_url,
44
+ "/uniswap/read",
45
+ {"event": event, "symbol0": symbol0, "symbol1": symbol1, "fee": fee},
46
+ )
47
+
48
+ def swaps(self, symbol0: str, symbol1: str, fee: int) -> EventQuery:
49
+ return self._q("swap", symbol0, symbol1, fee)
50
+
51
+ def deposits(self, symbol0: str, symbol1: str, fee: int) -> EventQuery:
52
+ return self._q("deposit", symbol0, symbol1, fee)
53
+
54
+ def withdrawals(self, symbol0: str, symbol1: str, fee: int) -> EventQuery:
55
+ return self._q("withdraw", symbol0, symbol1, fee)
56
+
57
+ def collects(self, symbol0: str, symbol1: str, fee: int) -> EventQuery:
58
+ return self._q("collect", symbol0, symbol1, fee)
59
+
60
+
61
+ class LidoNamespace:
62
+ def __init__(self, session: httpx.AsyncClient, base_url: str):
63
+ self._session = session
64
+ self._base_url = base_url
65
+
66
+ def _q(self, event: str) -> EventQuery:
67
+ return EventQuery(self._session, self._base_url, "/lido/read", {"event": event})
68
+
69
+ def deposits(self) -> EventQuery:
70
+ return self._q("deposit")
71
+
72
+ def withdrawal_requests(self) -> EventQuery:
73
+ return self._q("withdrawal_request")
74
+
75
+ def withdrawals_claimed(self) -> EventQuery:
76
+ return self._q("withdrawal_claimed")
77
+
78
+
79
+ class StaderNamespace:
80
+ def __init__(self, session: httpx.AsyncClient, base_url: str):
81
+ self._session = session
82
+ self._base_url = base_url
83
+
84
+ def _q(self, event: str) -> EventQuery:
85
+ return EventQuery(self._session, self._base_url, "/stader/read", {"event": event})
86
+
87
+ def deposits(self) -> EventQuery:
88
+ return self._q("deposit")
89
+
90
+ def withdrawal_requests(self) -> EventQuery:
91
+ return self._q("withdrawal_request")
92
+
93
+ def withdrawals(self) -> EventQuery:
94
+ return self._q("withdrawal")
95
+
96
+
97
+ class ThresholdNamespace:
98
+ def __init__(self, session: httpx.AsyncClient, base_url: str):
99
+ self._session = session
100
+ self._base_url = base_url
101
+
102
+ def _q(self, event: str) -> EventQuery:
103
+ return EventQuery(self._session, self._base_url, "/threshold/read", {"event": event})
104
+
105
+ def deposits(self) -> EventQuery:
106
+ return self._q("deposit")
107
+
108
+ def deposit_requests(self) -> EventQuery:
109
+ return self._q("deposit_request")
110
+
111
+ def withdrawals(self) -> EventQuery:
112
+ return self._q("withdrawal")
113
+
114
+ def withdrawal_requests(self) -> EventQuery:
115
+ return self._q("withdrawal_request")
116
+
117
+
118
+ class NativeTransfersQuery(BaseQuery):
119
+ def __init__(self, session: httpx.AsyncClient, base_url: str):
120
+ super().__init__(session, base_url, {})
121
+ self._min_amount = None
122
+
123
+ def min_amount(self, amount: float) -> "NativeTransfersQuery":
124
+ self._min_amount = amount
125
+ return self
126
+
127
+ async def fetch(self) -> pd.DataFrame:
128
+ if self._min_amount is not None:
129
+ self._body["min_amount"] = self._min_amount
130
+ path = "/native_transfers/read/min"
131
+ else:
132
+ path = "/native_transfers/read"
133
+ return await fetch_parquet(self._session, self._base_url + path, self._body)
134
+
135
+
136
+ class NativeNamespace:
137
+ def __init__(self, session: httpx.AsyncClient, base_url: str):
138
+ self._session = session
139
+ self._base_url = base_url
140
+
141
+ def transfers(self) -> NativeTransfersQuery:
142
+ return NativeTransfersQuery(self._session, self._base_url)
143
+
144
+
145
+ class CacheNamespace:
146
+ def __init__(self, session: httpx.AsyncClient, base_url: str):
147
+ self._session = session
148
+ self._base_url = base_url
149
+
150
+ async def flush(self) -> None:
151
+ await self._session.post(self._base_url + "/cache/flush", json={})
@@ -0,0 +1,20 @@
1
+ [project]
2
+ name = "horatio-data-provider"
3
+ version = "0.1.0"
4
+ description = "Python client for the Horatio data provider service"
5
+ requires-python = ">=3.10"
6
+ dependencies = [
7
+ "httpx>=0.25",
8
+ "pyarrow>=14.0",
9
+ "pandas>=2.0",
10
+ ]
11
+
12
+ [project.optional-dependencies]
13
+ dev = ["pytest>=8.0", "pytest-asyncio>=0.23", "respx>=0.21"]
14
+
15
+ [tool.pytest.ini_options]
16
+ asyncio_mode = "auto"
17
+
18
+ [build-system]
19
+ requires = ["hatchling"]
20
+ build-backend = "hatchling.build"
File without changes
@@ -0,0 +1,23 @@
1
+ import io
2
+
3
+ import pandas as pd
4
+ import pyarrow as pa
5
+ import pyarrow.parquet as pq
6
+ import pytest
7
+
8
+
9
+ def make_parquet_bytes(df: pd.DataFrame) -> bytes:
10
+ table = pa.Table.from_pandas(df)
11
+ buf = io.BytesIO()
12
+ pq.write_table(table, buf)
13
+ return buf.getvalue()
14
+
15
+
16
+ @pytest.fixture
17
+ def sample_df():
18
+ return pd.DataFrame({"amount": [1.0, 2.0], "token": ["USDT", "USDC"]})
19
+
20
+
21
+ @pytest.fixture
22
+ def parquet_bytes(sample_df):
23
+ return make_parquet_bytes(sample_df)
@@ -0,0 +1,66 @@
1
+ import json
2
+
3
+ import httpx
4
+ import pytest
5
+ import respx
6
+
7
+ from horatio_data_provider import DataProviderClient
8
+
9
+ BASE = "http://test"
10
+
11
+
12
+ @pytest.fixture
13
+ def client():
14
+ return DataProviderClient(BASE)
15
+
16
+
17
+ @respx.mock
18
+ @pytest.mark.asyncio
19
+ async def test_raw_trades_url_and_body(client, parquet_bytes):
20
+ route = respx.post(f"{BASE}/binance/raw_trades/read").mock(
21
+ return_value=httpx.Response(200, content=parquet_bytes, headers={"content-type": "application/octet-stream"})
22
+ )
23
+ await client.binance.raw_trades("BTC").time_range("2025-01-01", "2025-01-02").fetch()
24
+ assert route.called
25
+ body = json.loads(route.calls[0].request.content)
26
+ assert body["token"] == "BTC"
27
+ assert body["since"] == "2025-01-01"
28
+ assert body["until"] == "2025-01-02"
29
+
30
+
31
+ @respx.mock
32
+ @pytest.mark.asyncio
33
+ async def test_ohlcv_url_and_window(client, parquet_bytes):
34
+ route = respx.post(f"{BASE}/binance/ohlcv/read").mock(
35
+ return_value=httpx.Response(200, content=parquet_bytes, headers={"content-type": "application/octet-stream"})
36
+ )
37
+ await client.binance.ohlcv("BTC", "1h").time_range("2025-01-01", "2025-01-02").fetch()
38
+ assert route.called
39
+ body = json.loads(route.calls[0].request.content)
40
+ assert body["token"] == "BTC"
41
+ assert body["window"] == "1h"
42
+
43
+
44
+ @respx.mock
45
+ @pytest.mark.asyncio
46
+ async def test_raw_trades_cache(client, parquet_bytes):
47
+ route = respx.post(f"{BASE}/binance/raw_trades/read").mock(
48
+ return_value=httpx.Response(200, content=parquet_bytes, headers={"content-type": "application/octet-stream"})
49
+ )
50
+ await client.binance.raw_trades("BTC").time_range("2025-01-01", "2025-01-02").cache("force_replace").fetch()
51
+ assert route.called
52
+ body = json.loads(route.calls[0].request.content)
53
+ assert body["cache"] is True
54
+ assert body["cache_type"] == "force_replace"
55
+
56
+
57
+ @respx.mock
58
+ @pytest.mark.asyncio
59
+ async def test_flush_raw_trades(client):
60
+ route = respx.post(f"{BASE}/binance/raw_trades/flush").mock(
61
+ return_value=httpx.Response(200, content=b"{}", headers={"content-type": "application/json"})
62
+ )
63
+ await client.binance.flush_raw_trades(token="BTC")
64
+ assert route.called
65
+ body = json.loads(route.calls[0].request.content)
66
+ assert body["token"] == "BTC"
@@ -0,0 +1,86 @@
1
+ import json
2
+
3
+ import httpx
4
+ import pytest
5
+ import respx
6
+
7
+ from horatio_data_provider import DataProviderClient, DataProviderHTTPError
8
+ from .conftest import make_parquet_bytes
9
+ import pandas as pd
10
+
11
+ BASE = "http://test"
12
+
13
+
14
+ @pytest.fixture
15
+ def client():
16
+ return DataProviderClient(BASE)
17
+
18
+
19
+ @respx.mock
20
+ @pytest.mark.asyncio
21
+ async def test_transfers_correct_url_and_body(client, parquet_bytes):
22
+ route = respx.post(f"{BASE}/erc20_transfers/read").mock(
23
+ return_value=httpx.Response(200, content=parquet_bytes, headers={"content-type": "application/octet-stream"})
24
+ )
25
+ df = await client.erc20.transfers(["USDT", "USDC"]).network("ETH").time_range("2025-01-01", "2025-01-02").fetch()
26
+ assert route.called
27
+ body = json.loads(route.calls[0].request.content)
28
+ assert body["tokens"] == ["USDT", "USDC"]
29
+ assert body["network"] == "ETH"
30
+ assert body["since"] == "2025-01-01"
31
+ assert body["until"] == "2025-01-02"
32
+ assert isinstance(df, pd.DataFrame)
33
+
34
+
35
+ @respx.mock
36
+ @pytest.mark.asyncio
37
+ async def test_min_amount_routes_to_min_endpoint(client, parquet_bytes):
38
+ route = respx.post(f"{BASE}/erc20_transfers/read/min").mock(
39
+ return_value=httpx.Response(200, content=parquet_bytes, headers={"content-type": "application/octet-stream"})
40
+ )
41
+ await client.erc20.transfers(["USDT"]).network("ETH").time_range("2025-01-01", "2025-01-02").min_amount(1000).fetch()
42
+ assert route.called
43
+ body = json.loads(route.calls[0].request.content)
44
+ assert body["min_amount"] == 1000
45
+
46
+
47
+ @respx.mock
48
+ @pytest.mark.asyncio
49
+ async def test_cache_sets_body_fields(client, parquet_bytes):
50
+ route = respx.post(f"{BASE}/erc20_transfers/read").mock(
51
+ return_value=httpx.Response(200, content=parquet_bytes, headers={"content-type": "application/octet-stream"})
52
+ )
53
+ await client.erc20.transfers(["USDT"]).network("ETH").time_range("2025-01-01", "2025-01-02").cache("append").fetch()
54
+ assert route.called
55
+ body = json.loads(route.calls[0].request.content)
56
+ assert body["cache"] is True
57
+ assert body["cache_type"] == "append"
58
+
59
+
60
+ @respx.mock
61
+ @pytest.mark.asyncio
62
+ async def test_flush(client):
63
+ route = respx.post(f"{BASE}/erc20_transfers/flush").mock(
64
+ return_value=httpx.Response(200, content=b"{}", headers={"content-type": "application/json"})
65
+ )
66
+ await client.erc20.flush(network="ETH", token="USDT")
67
+ assert route.called
68
+ body = json.loads(route.calls[0].request.content)
69
+ assert body["network"] == "ETH"
70
+ assert body["token"] == "USDT"
71
+
72
+
73
+ @respx.mock
74
+ @pytest.mark.asyncio
75
+ async def test_http_error_propagation(client):
76
+ respx.post(f"{BASE}/erc20_transfers/read").mock(
77
+ return_value=httpx.Response(
78
+ 400,
79
+ json={"error": "bad request"},
80
+ headers={"content-type": "application/json"},
81
+ )
82
+ )
83
+ with pytest.raises(DataProviderHTTPError) as exc_info:
84
+ await client.erc20.transfers(["USDT"]).network("ETH").time_range("2025-01-01", "2025-01-02").fetch()
85
+ assert exc_info.value.status_code == 400
86
+ assert "bad request" in str(exc_info.value)
@@ -0,0 +1,147 @@
1
+ import json
2
+
3
+ import httpx
4
+ import pytest
5
+ import respx
6
+
7
+ from horatio_data_provider import DataProviderClient
8
+
9
+ BASE = "http://test"
10
+
11
+
12
+ @pytest.fixture
13
+ def client():
14
+ return DataProviderClient(BASE)
15
+
16
+
17
+ # --- AAVE ---
18
+
19
+ @respx.mock
20
+ @pytest.mark.asyncio
21
+ async def test_aave_deposit_event_routing(client, parquet_bytes):
22
+ route = respx.post(f"{BASE}/aave/read").mock(
23
+ return_value=httpx.Response(200, content=parquet_bytes, headers={"content-type": "application/octet-stream"})
24
+ )
25
+ await client.aave.deposits().network("ETH").time_range("2025-01-01", "2025-01-02").fetch()
26
+ assert route.called
27
+ body = json.loads(route.calls[0].request.content)
28
+ assert body["event"] == "deposit"
29
+
30
+
31
+ @respx.mock
32
+ @pytest.mark.asyncio
33
+ async def test_aave_borrow_event_routing(client, parquet_bytes):
34
+ route = respx.post(f"{BASE}/aave/read").mock(
35
+ return_value=httpx.Response(200, content=parquet_bytes, headers={"content-type": "application/octet-stream"})
36
+ )
37
+ await client.aave.borrows().network("ETH").time_range("2025-01-01", "2025-01-02").fetch()
38
+ assert route.called
39
+ body = json.loads(route.calls[0].request.content)
40
+ assert body["event"] == "borrow"
41
+
42
+
43
+ # --- Uniswap ---
44
+
45
+ @respx.mock
46
+ @pytest.mark.asyncio
47
+ async def test_uniswap_swaps_body(client, parquet_bytes):
48
+ route = respx.post(f"{BASE}/uniswap/read").mock(
49
+ return_value=httpx.Response(200, content=parquet_bytes, headers={"content-type": "application/octet-stream"})
50
+ )
51
+ await client.uniswap.swaps("WETH", "USDC", fee=500).network("ETH").time_range("2025-01-01", "2025-01-02").fetch()
52
+ assert route.called
53
+ body = json.loads(route.calls[0].request.content)
54
+ assert body["event"] == "swap"
55
+ assert body["symbol0"] == "WETH"
56
+ assert body["symbol1"] == "USDC"
57
+ assert body["fee"] == 500
58
+
59
+
60
+ # --- Lido ---
61
+
62
+ @respx.mock
63
+ @pytest.mark.asyncio
64
+ async def test_lido_deposit_event_routing(client, parquet_bytes):
65
+ route = respx.post(f"{BASE}/lido/read").mock(
66
+ return_value=httpx.Response(200, content=parquet_bytes, headers={"content-type": "application/octet-stream"})
67
+ )
68
+ await client.lido.deposits().network("ETH").time_range("2025-01-01", "2025-01-02").fetch()
69
+ assert route.called
70
+ body = json.loads(route.calls[0].request.content)
71
+ assert body["event"] == "deposit"
72
+
73
+
74
+ @respx.mock
75
+ @pytest.mark.asyncio
76
+ async def test_lido_withdrawal_request_event_routing(client, parquet_bytes):
77
+ route = respx.post(f"{BASE}/lido/read").mock(
78
+ return_value=httpx.Response(200, content=parquet_bytes, headers={"content-type": "application/octet-stream"})
79
+ )
80
+ await client.lido.withdrawal_requests().network("ETH").time_range("2025-01-01", "2025-01-02").fetch()
81
+ assert route.called
82
+ body = json.loads(route.calls[0].request.content)
83
+ assert body["event"] == "withdrawal_request"
84
+
85
+
86
+ # --- Stader ---
87
+
88
+ @respx.mock
89
+ @pytest.mark.asyncio
90
+ async def test_stader_deposit_event_routing(client, parquet_bytes):
91
+ route = respx.post(f"{BASE}/stader/read").mock(
92
+ return_value=httpx.Response(200, content=parquet_bytes, headers={"content-type": "application/octet-stream"})
93
+ )
94
+ await client.stader.deposits().network("ETH").time_range("2025-01-01", "2025-01-02").fetch()
95
+ assert route.called
96
+ body = json.loads(route.calls[0].request.content)
97
+ assert body["event"] == "deposit"
98
+
99
+
100
+ # --- Threshold ---
101
+
102
+ @respx.mock
103
+ @pytest.mark.asyncio
104
+ async def test_threshold_deposit_request_event_routing(client, parquet_bytes):
105
+ route = respx.post(f"{BASE}/threshold/read").mock(
106
+ return_value=httpx.Response(200, content=parquet_bytes, headers={"content-type": "application/octet-stream"})
107
+ )
108
+ await client.threshold.deposit_requests().network("ETH").time_range("2025-01-01", "2025-01-02").fetch()
109
+ assert route.called
110
+ body = json.loads(route.calls[0].request.content)
111
+ assert body["event"] == "deposit_request"
112
+
113
+
114
+ # --- Native ---
115
+
116
+ @respx.mock
117
+ @pytest.mark.asyncio
118
+ async def test_native_transfers_standard_url(client, parquet_bytes):
119
+ route = respx.post(f"{BASE}/native_transfers/read").mock(
120
+ return_value=httpx.Response(200, content=parquet_bytes, headers={"content-type": "application/octet-stream"})
121
+ )
122
+ await client.native.transfers().network("ETH").time_range("2025-01-01", "2025-01-02").fetch()
123
+ assert route.called
124
+
125
+
126
+ @respx.mock
127
+ @pytest.mark.asyncio
128
+ async def test_native_transfers_min_amount_url(client, parquet_bytes):
129
+ route = respx.post(f"{BASE}/native_transfers/read/min").mock(
130
+ return_value=httpx.Response(200, content=parquet_bytes, headers={"content-type": "application/octet-stream"})
131
+ )
132
+ await client.native.transfers().network("ETH").time_range("2025-01-01", "2025-01-02").min_amount(0.5).fetch()
133
+ assert route.called
134
+ body = json.loads(route.calls[0].request.content)
135
+ assert body["min_amount"] == 0.5
136
+
137
+
138
+ # --- Cache ---
139
+
140
+ @respx.mock
141
+ @pytest.mark.asyncio
142
+ async def test_cache_flush(client):
143
+ route = respx.post(f"{BASE}/cache/flush").mock(
144
+ return_value=httpx.Response(200, content=b"{}", headers={"content-type": "application/json"})
145
+ )
146
+ await client.cache.flush()
147
+ assert route.called