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.
- horatio_data_provider-0.1.0/.gitignore +9 -0
- horatio_data_provider-0.1.0/PKG-INFO +12 -0
- horatio_data_provider-0.1.0/horatio_data_provider/__init__.py +4 -0
- horatio_data_provider-0.1.0/horatio_data_provider/_client.py +42 -0
- horatio_data_provider-0.1.0/horatio_data_provider/_http.py +17 -0
- horatio_data_provider-0.1.0/horatio_data_provider/_query.py +36 -0
- horatio_data_provider-0.1.0/horatio_data_provider/binance.py +39 -0
- horatio_data_provider-0.1.0/horatio_data_provider/erc20.py +40 -0
- horatio_data_provider-0.1.0/horatio_data_provider/exceptions.py +8 -0
- horatio_data_provider-0.1.0/horatio_data_provider/protocols.py +151 -0
- horatio_data_provider-0.1.0/pyproject.toml +20 -0
- horatio_data_provider-0.1.0/tests/__init__.py +0 -0
- horatio_data_provider-0.1.0/tests/conftest.py +23 -0
- horatio_data_provider-0.1.0/tests/test_binance.py +66 -0
- horatio_data_provider-0.1.0/tests/test_erc20.py +86 -0
- horatio_data_provider-0.1.0/tests/test_protocols.py +147 -0
|
@@ -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,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,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
|