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,6 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .env
4
+ data/
5
+ *.duckdb
6
+ *.duckdb.wal
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: chain-analysis
3
+ Version: 0.1.0
4
+ Summary: Python client for the Horatio Chain Analysis service
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: httpx>=0.25
7
+ Requires-Dist: polars>=1.0
@@ -0,0 +1,3 @@
1
+ from .client import ChainAnalysisClient
2
+
3
+ __all__ = ["ChainAnalysisClient"]
@@ -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"