chain-analysis 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,206 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
import polars as pl
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ChainAnalysisClient:
|
|
11
|
+
"""Client for the Horatio Chain Analysis service."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, url: str, user: str = "admin", password: str = "admin"):
|
|
14
|
+
self._url = url.rstrip("/")
|
|
15
|
+
self._user = user
|
|
16
|
+
self._password = password
|
|
17
|
+
self._session = httpx.AsyncClient(timeout=600)
|
|
18
|
+
self.wallets = WalletNamespace(self)
|
|
19
|
+
|
|
20
|
+
def _auth(self) -> dict[str, str]:
|
|
21
|
+
import base64
|
|
22
|
+
encoded = base64.b64encode(f"{self._user}:{self._password}".encode()).decode()
|
|
23
|
+
return {"Authorization": f"Basic {encoded}"}
|
|
24
|
+
|
|
25
|
+
async def health(self) -> bool:
|
|
26
|
+
res = await self._session.get(f"{self._url}/health", headers=self._auth())
|
|
27
|
+
res.raise_for_status()
|
|
28
|
+
return True
|
|
29
|
+
|
|
30
|
+
async def close(self) -> None:
|
|
31
|
+
await self._session.aclose()
|
|
32
|
+
|
|
33
|
+
async def __aenter__(self) -> ChainAnalysisClient:
|
|
34
|
+
return self
|
|
35
|
+
|
|
36
|
+
async def __aexit__(self, *_) -> None:
|
|
37
|
+
await self.close()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class WalletNamespace:
|
|
41
|
+
"""Wallet operations."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, client: ChainAnalysisClient):
|
|
44
|
+
self._client = client
|
|
45
|
+
|
|
46
|
+
async def query(
|
|
47
|
+
self,
|
|
48
|
+
*,
|
|
49
|
+
network: Optional[str | list[str]] = None,
|
|
50
|
+
entity: Optional[str | list[str]] = None,
|
|
51
|
+
categories: Optional[list[str]] = None,
|
|
52
|
+
labels: Optional[list[str]] = None,
|
|
53
|
+
) -> pl.DataFrame:
|
|
54
|
+
"""Query wallets by filters. Returns DataFrame with 'address' column.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
network: Filter by network type (e.g. 'EVM', 'TRON'). Single or list.
|
|
58
|
+
entity: Filter by entity name (exact, case-insensitive). Single or list.
|
|
59
|
+
categories: Wallet must have ALL listed categories.
|
|
60
|
+
labels: Wallet must have ALL listed labels.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Polars DataFrame with single 'address' column.
|
|
64
|
+
"""
|
|
65
|
+
body: dict = {"format": "parquet"}
|
|
66
|
+
if network is not None:
|
|
67
|
+
body["network"] = network
|
|
68
|
+
if entity is not None:
|
|
69
|
+
body["entity"] = entity
|
|
70
|
+
if categories is not None:
|
|
71
|
+
body["categories"] = categories
|
|
72
|
+
if labels is not None:
|
|
73
|
+
body["labels"] = labels
|
|
74
|
+
|
|
75
|
+
res = await self._client._session.post(
|
|
76
|
+
f"{self._client._url}/api/wallets/query",
|
|
77
|
+
json=body,
|
|
78
|
+
headers={**self._client._auth(), "Content-Type": "application/json"},
|
|
79
|
+
)
|
|
80
|
+
res.raise_for_status()
|
|
81
|
+
return pl.read_parquet(io.BytesIO(res.content))
|
|
82
|
+
|
|
83
|
+
async def get(self, address: str) -> dict:
|
|
84
|
+
"""Get a single wallet's details.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Dict with keys: wallet, network, entity, categories, labels.
|
|
88
|
+
"""
|
|
89
|
+
res = await self._client._session.get(
|
|
90
|
+
f"{self._client._url}/api/wallets/{address}",
|
|
91
|
+
headers=self._client._auth(),
|
|
92
|
+
)
|
|
93
|
+
res.raise_for_status()
|
|
94
|
+
return res.json()
|
|
95
|
+
|
|
96
|
+
async def create(
|
|
97
|
+
self,
|
|
98
|
+
wallet: str,
|
|
99
|
+
*,
|
|
100
|
+
entity: Optional[str] = None,
|
|
101
|
+
categories: Optional[list[str]] = None,
|
|
102
|
+
labels: Optional[list[str]] = None,
|
|
103
|
+
) -> dict:
|
|
104
|
+
"""Create a new wallet entry.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Dict with the created wallet details.
|
|
108
|
+
"""
|
|
109
|
+
body: dict = {"wallet": wallet}
|
|
110
|
+
if entity is not None:
|
|
111
|
+
body["entity"] = entity
|
|
112
|
+
if categories is not None:
|
|
113
|
+
body["categories"] = categories
|
|
114
|
+
if labels is not None:
|
|
115
|
+
body["labels"] = labels
|
|
116
|
+
res = await self._client._session.post(
|
|
117
|
+
f"{self._client._url}/api/wallets",
|
|
118
|
+
json=body,
|
|
119
|
+
headers={**self._client._auth(), "Content-Type": "application/json"},
|
|
120
|
+
)
|
|
121
|
+
res.raise_for_status()
|
|
122
|
+
return res.json()
|
|
123
|
+
|
|
124
|
+
async def update(
|
|
125
|
+
self,
|
|
126
|
+
address: str,
|
|
127
|
+
*,
|
|
128
|
+
entity: Optional[str] = None,
|
|
129
|
+
categories: Optional[list[str]] = None,
|
|
130
|
+
labels: Optional[list[str]] = None,
|
|
131
|
+
) -> dict:
|
|
132
|
+
"""Update a wallet's entity, categories, or labels.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Dict with the updated wallet details.
|
|
136
|
+
"""
|
|
137
|
+
body: dict = {}
|
|
138
|
+
if entity is not None:
|
|
139
|
+
body["entity"] = entity
|
|
140
|
+
if categories is not None:
|
|
141
|
+
body["categories"] = categories
|
|
142
|
+
if labels is not None:
|
|
143
|
+
body["labels"] = labels
|
|
144
|
+
res = await self._client._session.put(
|
|
145
|
+
f"{self._client._url}/api/wallets/{address}",
|
|
146
|
+
json=body,
|
|
147
|
+
headers={**self._client._auth(), "Content-Type": "application/json"},
|
|
148
|
+
)
|
|
149
|
+
res.raise_for_status()
|
|
150
|
+
return res.json()
|
|
151
|
+
|
|
152
|
+
async def delete(self, address: str) -> dict:
|
|
153
|
+
"""Delete a wallet entry."""
|
|
154
|
+
res = await self._client._session.delete(
|
|
155
|
+
f"{self._client._url}/api/wallets/{address}",
|
|
156
|
+
headers=self._client._auth(),
|
|
157
|
+
)
|
|
158
|
+
res.raise_for_status()
|
|
159
|
+
return res.json()
|
|
160
|
+
|
|
161
|
+
async def bulk_update(
|
|
162
|
+
self,
|
|
163
|
+
addresses: list[str],
|
|
164
|
+
*,
|
|
165
|
+
add_categories: Optional[list[str]] = None,
|
|
166
|
+
remove_categories: Optional[list[str]] = None,
|
|
167
|
+
add_labels: Optional[list[str]] = None,
|
|
168
|
+
remove_labels: Optional[list[str]] = None,
|
|
169
|
+
entity: Optional[str] = None,
|
|
170
|
+
) -> dict:
|
|
171
|
+
"""Bulk add/remove categories or labels for multiple wallets.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Dict with 'updated' count.
|
|
175
|
+
"""
|
|
176
|
+
body: dict = {"addresses": addresses}
|
|
177
|
+
if add_categories:
|
|
178
|
+
body["add_categories"] = add_categories
|
|
179
|
+
if remove_categories:
|
|
180
|
+
body["remove_categories"] = remove_categories
|
|
181
|
+
if add_labels:
|
|
182
|
+
body["add_labels"] = add_labels
|
|
183
|
+
if remove_labels:
|
|
184
|
+
body["remove_labels"] = remove_labels
|
|
185
|
+
if entity is not None:
|
|
186
|
+
body["entity"] = entity
|
|
187
|
+
res = await self._client._session.post(
|
|
188
|
+
f"{self._client._url}/api/cex/bulk-update",
|
|
189
|
+
json=body,
|
|
190
|
+
headers={**self._client._auth(), "Content-Type": "application/json"},
|
|
191
|
+
)
|
|
192
|
+
res.raise_for_status()
|
|
193
|
+
return res.json()
|
|
194
|
+
|
|
195
|
+
async def distinct(self) -> dict:
|
|
196
|
+
"""Get all distinct entities, categories, and labels.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Dict with keys: entities, categories, labels (each a list of str).
|
|
200
|
+
"""
|
|
201
|
+
res = await self._client._session.get(
|
|
202
|
+
f"{self._client._url}/api/wallets/distinct",
|
|
203
|
+
headers=self._client._auth(),
|
|
204
|
+
)
|
|
205
|
+
res.raise_for_status()
|
|
206
|
+
return res.json()
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "chain-analysis"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Python client for the Horatio Chain Analysis service"
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"httpx>=0.25",
|
|
8
|
+
"polars>=1.0",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
[build-system]
|
|
12
|
+
requires = ["hatchling"]
|
|
13
|
+
build-backend = "hatchling.build"
|