chainalysis 0.0.1__py3-none-any.whl
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.
Potentially problematic release.
This version of chainalysis might be problematic. Click here for more details.
- chainalysis/DataSolutions.py +22 -0
- chainalysis/__init__.py +3 -0
- chainalysis/constants.py +12 -0
- chainalysis/sql/__init__.py +1 -0
- chainalysis/sql/analytical.py +234 -0
- chainalysis/sql/sql.py +8 -0
- chainalysis/sql/transactional.py +145 -0
- chainalysis/util_functions/check_list_type.py +18 -0
- chainalysis/util_functions/exceptions.py +87 -0
- chainalysis/util_functions/requests.py +87 -0
- chainalysis/utils/__init__.py +1 -0
- chainalysis/utils/stringify.py +79 -0
- chainalysis/utils/utils.py +11 -0
- chainalysis-0.0.1.dist-info/METADATA +23 -0
- chainalysis-0.0.1.dist-info/RECORD +17 -0
- chainalysis-0.0.1.dist-info/WHEEL +4 -0
- chainalysis-0.0.1.dist-info/licenses/LICENCE +19 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from chainalysis.sql.sql import Sql
|
|
2
|
+
from chainalysis.utils.utils import Utils
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class DataSolutionsClient:
|
|
6
|
+
"""
|
|
7
|
+
This class provides SDK functions for users to query
|
|
8
|
+
Data Solutions databases.
|
|
9
|
+
|
|
10
|
+
The Analytical class queries the Data Solutions Databricks table.
|
|
11
|
+
The Transactional class queries the Data Solutions Postgres table.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
api_key: str,
|
|
17
|
+
):
|
|
18
|
+
self.api_key = api_key
|
|
19
|
+
|
|
20
|
+
# Define SDK function classes
|
|
21
|
+
self.sql = Sql(api_key)
|
|
22
|
+
self.utils = Utils()
|
chainalysis/__init__.py
ADDED
chainalysis/constants.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
BASE_URL = {
|
|
2
|
+
"base_url": "https://api.transpose.io",
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
ANALYTICAL_ENDPOINTS = {
|
|
6
|
+
"async_query_execution": "sql/analytical/async",
|
|
7
|
+
"async_query_status": "sql/analytical/status",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
TRANSACTIONAL_ENDPOINTS = {
|
|
11
|
+
"query_execution": "sql/transactional",
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from chainalysis.sql import *
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
from time import sleep
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from chainalysis.constants import ANALYTICAL_ENDPOINTS, BASE_URL
|
|
6
|
+
from chainalysis.util_functions.exceptions import (
|
|
7
|
+
BadRequest,
|
|
8
|
+
DataSolutionsAPIException,
|
|
9
|
+
DataSolutionsSDKException,
|
|
10
|
+
UnhandledException,
|
|
11
|
+
)
|
|
12
|
+
from chainalysis.util_functions.requests import issue_request
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Analytical:
|
|
16
|
+
"""
|
|
17
|
+
This class provides methods to execute SQL queries on Data Solutions
|
|
18
|
+
DataBricks tables. It supports fetching results as JSON or a
|
|
19
|
+
pandas DataFrame, and provides query execution statistics.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, api_key: str):
|
|
23
|
+
"""
|
|
24
|
+
Initialize the Analytical class with the provided API key.
|
|
25
|
+
|
|
26
|
+
:param api_key: The API key for accessing the analytical service.
|
|
27
|
+
:type api_key: str
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
self.api_key = api_key
|
|
31
|
+
self._status_code = 0
|
|
32
|
+
self.results = {}
|
|
33
|
+
self._stats = {}
|
|
34
|
+
self.json_response = {}
|
|
35
|
+
self.dataframe_data = None
|
|
36
|
+
self._status = "error"
|
|
37
|
+
self.next = ""
|
|
38
|
+
self._total_pages = 0
|
|
39
|
+
self.query_id = None
|
|
40
|
+
self.exception = UnhandledException()
|
|
41
|
+
|
|
42
|
+
def __call__(
|
|
43
|
+
self,
|
|
44
|
+
query: str,
|
|
45
|
+
parameters: dict[str] = {},
|
|
46
|
+
polling_interval_sec: int = 5,
|
|
47
|
+
):
|
|
48
|
+
"""
|
|
49
|
+
Execute a SQL query asynchronously using the provided parameters
|
|
50
|
+
and polling interval.
|
|
51
|
+
|
|
52
|
+
:param query: The SQL query to be executed.
|
|
53
|
+
:type query: str
|
|
54
|
+
:param parameters: A dictionary of parameters to be used in the query.
|
|
55
|
+
:type parameters: dict[str], optional
|
|
56
|
+
:param polling_interval_sec: The interval in seconds between status checks.
|
|
57
|
+
:type polling_interval_sec: int, optional
|
|
58
|
+
:return: An instance of the Analytical class with query results.
|
|
59
|
+
:rtype: Analytical
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
query_execution_url = (
|
|
63
|
+
f"{BASE_URL['base_url']}/{ANALYTICAL_ENDPOINTS['async_query_execution']}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
body = {
|
|
67
|
+
"sql": query,
|
|
68
|
+
"parameters": parameters,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async_response = issue_request(
|
|
72
|
+
api_key=self.api_key,
|
|
73
|
+
url=query_execution_url,
|
|
74
|
+
body=body,
|
|
75
|
+
method="POST",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
self.query_id = async_response.get("query_id")
|
|
79
|
+
if not self.query_id:
|
|
80
|
+
raise DataSolutionsAPIException(
|
|
81
|
+
"Unexpected response. Query ID was not returned."
|
|
82
|
+
)
|
|
83
|
+
async_query_status_url = f"{BASE_URL['base_url']}/{ANALYTICAL_ENDPOINTS['async_query_status']}?query_id={self.query_id}"
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
while True:
|
|
87
|
+
self.json_response = issue_request(
|
|
88
|
+
api_key=self.api_key,
|
|
89
|
+
url=async_query_status_url,
|
|
90
|
+
method="GET",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
self._status = self.json_response["status"]
|
|
94
|
+
|
|
95
|
+
if self._status == "running" or self._status == "pending":
|
|
96
|
+
sleep(polling_interval_sec)
|
|
97
|
+
elif self._status == "error":
|
|
98
|
+
self.error_message = self.json_response["message"]
|
|
99
|
+
self.error_details = self.json_response.get("details")
|
|
100
|
+
break
|
|
101
|
+
elif self._status == "success":
|
|
102
|
+
self._status_code = 200
|
|
103
|
+
self._stats = self.json_response["stats"]
|
|
104
|
+
self.results = self.json_response["results"]
|
|
105
|
+
self.next_url = self.json_response["next"]
|
|
106
|
+
self._total_pages = self._stats["total_pages"]
|
|
107
|
+
break
|
|
108
|
+
except DataSolutionsSDKException as e:
|
|
109
|
+
self.exception = e.get_exception()
|
|
110
|
+
self._status_code = e.status_code
|
|
111
|
+
except Exception as e:
|
|
112
|
+
self.exception = UnhandledException(
|
|
113
|
+
details=e,
|
|
114
|
+
)
|
|
115
|
+
return self
|
|
116
|
+
|
|
117
|
+
def next_page(self):
|
|
118
|
+
"""
|
|
119
|
+
Fetch the next page of DataBricks query results.
|
|
120
|
+
|
|
121
|
+
:return: An instance of the Analytical class with the next page of results.
|
|
122
|
+
:rtype: Analytical
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
if self.next_url:
|
|
126
|
+
|
|
127
|
+
self.json_response = issue_request(
|
|
128
|
+
api_key=self.api_key,
|
|
129
|
+
url=self.next_url,
|
|
130
|
+
method="GET",
|
|
131
|
+
)
|
|
132
|
+
self._status = self.json_response["status"]
|
|
133
|
+
|
|
134
|
+
if self._status == "error":
|
|
135
|
+
self.error_message = self.json_response["message"]
|
|
136
|
+
self.error_details = self.json_response.get("details")
|
|
137
|
+
elif self._status == "success":
|
|
138
|
+
self._status = self._status
|
|
139
|
+
self._stats = self.json_response["stats"]
|
|
140
|
+
self.results = self.json_response["results"]
|
|
141
|
+
self.next_url = self.json_response["next"]
|
|
142
|
+
else:
|
|
143
|
+
raise BadRequest(
|
|
144
|
+
"No next page available. Use the method has_next() to check if there is a next page that can be retrieved."
|
|
145
|
+
)
|
|
146
|
+
return self
|
|
147
|
+
|
|
148
|
+
def json(self):
|
|
149
|
+
"""
|
|
150
|
+
Return results as a JSON.
|
|
151
|
+
|
|
152
|
+
:raises Exception: Raises an exception if the query resulted in an error.
|
|
153
|
+
:return: Results of the SQL query as a JSON.
|
|
154
|
+
:rtype: dict
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
if self._status != "error":
|
|
158
|
+
return self.results
|
|
159
|
+
else:
|
|
160
|
+
raise self.exception
|
|
161
|
+
|
|
162
|
+
def df(self):
|
|
163
|
+
"""
|
|
164
|
+
Convert query results into a pandas DataFrame.
|
|
165
|
+
|
|
166
|
+
:raises Exception: Raises an exception if the query resulted in an error.
|
|
167
|
+
:return: DataFrame containing the results of the SQL query.
|
|
168
|
+
:rtype: pd.DataFrame
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
if self._status != "error":
|
|
172
|
+
if self.dataframe_data:
|
|
173
|
+
return self.dataframe_data
|
|
174
|
+
self.dataframe_data = pd.DataFrame(self.results)
|
|
175
|
+
return self.dataframe_data
|
|
176
|
+
else:
|
|
177
|
+
raise self.exception
|
|
178
|
+
|
|
179
|
+
def stats(self):
|
|
180
|
+
"""
|
|
181
|
+
Get the statistics of the executed query.
|
|
182
|
+
|
|
183
|
+
:return: Statistics of the query execution.
|
|
184
|
+
:rtype: dict
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
if self._status != "error":
|
|
188
|
+
return self._stats
|
|
189
|
+
else:
|
|
190
|
+
raise self.exception
|
|
191
|
+
|
|
192
|
+
def status_code(self):
|
|
193
|
+
"""
|
|
194
|
+
Get the HTTP status code of the response.
|
|
195
|
+
|
|
196
|
+
:return: HTTP status code.
|
|
197
|
+
:rtype: int
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
return self._status_code
|
|
201
|
+
|
|
202
|
+
def was_successful(self):
|
|
203
|
+
"""
|
|
204
|
+
Determine if the query executed successfully.
|
|
205
|
+
|
|
206
|
+
:return: True if the query was successful, False otherwise.
|
|
207
|
+
:rtype: bool
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
if self._status != "error":
|
|
211
|
+
return True
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
def total_pages(self) -> int:
|
|
215
|
+
"""
|
|
216
|
+
Return total number of pages.
|
|
217
|
+
|
|
218
|
+
:return: Number of pages.
|
|
219
|
+
:rtype: int
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
return self._total_pages
|
|
223
|
+
|
|
224
|
+
def has_next(self) -> bool:
|
|
225
|
+
"""
|
|
226
|
+
Return if the next page exists.
|
|
227
|
+
|
|
228
|
+
:return: Whether next page exists.
|
|
229
|
+
:rtype: bool
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
if self.next_url:
|
|
233
|
+
return True
|
|
234
|
+
return False
|
chainalysis/sql/sql.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
from chainalysis.constants import BASE_URL, TRANSACTIONAL_ENDPOINTS
|
|
4
|
+
from chainalysis.util_functions.exceptions import (
|
|
5
|
+
DataSolutionsSDKException,
|
|
6
|
+
UnhandledException,
|
|
7
|
+
)
|
|
8
|
+
from chainalysis.util_functions.requests import issue_request
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Transactional:
|
|
12
|
+
"""
|
|
13
|
+
This class provides methods to handle transactional queries.
|
|
14
|
+
It supports fetching results as JSON or pandas DataFrame,
|
|
15
|
+
and provides query execution statistics.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, api_key: str):
|
|
19
|
+
"""
|
|
20
|
+
Initialize the Transactional object with an API key.
|
|
21
|
+
|
|
22
|
+
:param api_key: API key for authenticating requests.
|
|
23
|
+
:type api_key: str
|
|
24
|
+
"""
|
|
25
|
+
self.api_key = api_key
|
|
26
|
+
self._status_code = 0
|
|
27
|
+
self.results = {}
|
|
28
|
+
self._stats = {}
|
|
29
|
+
self.json_response = {}
|
|
30
|
+
self.status = "error"
|
|
31
|
+
self.dataframe_data = None
|
|
32
|
+
|
|
33
|
+
def __call__(
|
|
34
|
+
self, query: str, parameters: dict[str, str] = {}, options: dict[str, str] = {}
|
|
35
|
+
) -> "Transactional":
|
|
36
|
+
"""
|
|
37
|
+
Execute a SQL query using the provided parameters and options.
|
|
38
|
+
|
|
39
|
+
:param query: SQL query template with placeholders for parameters.
|
|
40
|
+
:type query: str
|
|
41
|
+
:param parameters: Parameters to format the SQL query.
|
|
42
|
+
:type parameters: dict[str, str]
|
|
43
|
+
:param options: Additional options for query execution.
|
|
44
|
+
:type options: dict[str, str]
|
|
45
|
+
:return: Returns self for chaining or method chaining.
|
|
46
|
+
:rtype: Transactional
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
query_execution_url = (
|
|
50
|
+
f"{BASE_URL['base_url']}/{TRANSACTIONAL_ENDPOINTS['query_execution']}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
body = {
|
|
54
|
+
"sql": query,
|
|
55
|
+
"parameters": parameters,
|
|
56
|
+
"options": options,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
self.json_response = issue_request(
|
|
61
|
+
api_key=self.api_key,
|
|
62
|
+
url=query_execution_url,
|
|
63
|
+
body=body,
|
|
64
|
+
method="POST",
|
|
65
|
+
)
|
|
66
|
+
self.status = self.json_response["status"]
|
|
67
|
+
if self.status == "error":
|
|
68
|
+
self.error_message = self.json_response["message"]
|
|
69
|
+
self.error_details = self.json_response.get("details")
|
|
70
|
+
self.exception = DataSolutionsSDKException(self.error_message)
|
|
71
|
+
else:
|
|
72
|
+
self._stats = self.json_response["stats"]
|
|
73
|
+
self.results = self.json_response["results"]
|
|
74
|
+
self._status_code = 200
|
|
75
|
+
|
|
76
|
+
except DataSolutionsSDKException as e:
|
|
77
|
+
self.exception = e.get_exception()
|
|
78
|
+
self._status_code = e.status_code
|
|
79
|
+
except Exception as e:
|
|
80
|
+
self.exception = UnhandledException(
|
|
81
|
+
details=e,
|
|
82
|
+
)
|
|
83
|
+
return self
|
|
84
|
+
|
|
85
|
+
def json(self) -> dict:
|
|
86
|
+
"""
|
|
87
|
+
Return the JSON data of the results.
|
|
88
|
+
|
|
89
|
+
:raises Exception: Raises an exception if the query resulted in an error.
|
|
90
|
+
:return: JSON results of the SQL query.
|
|
91
|
+
:rtype: dict
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
if self.status != "error":
|
|
95
|
+
return self.results
|
|
96
|
+
else:
|
|
97
|
+
raise self.exception
|
|
98
|
+
|
|
99
|
+
def df(self) -> pd.DataFrame:
|
|
100
|
+
"""
|
|
101
|
+
Convert query results into a pandas DataFrame.
|
|
102
|
+
|
|
103
|
+
:raises Exception: Raises an exception if the query resulted in an error.
|
|
104
|
+
:return: DataFrame containing the results of the SQL query.
|
|
105
|
+
:rtype: pd.DataFrame
|
|
106
|
+
"""
|
|
107
|
+
if self.status != "error":
|
|
108
|
+
if self.dataframe_data:
|
|
109
|
+
return self.dataframe_data
|
|
110
|
+
self.dataframe_data = pd.DataFrame(self.results)
|
|
111
|
+
return self.dataframe_data
|
|
112
|
+
elif self.exception:
|
|
113
|
+
raise self.exception
|
|
114
|
+
|
|
115
|
+
def stats(self) -> dict:
|
|
116
|
+
"""
|
|
117
|
+
Get the statistics of the executed query.
|
|
118
|
+
|
|
119
|
+
:return: Statistics of the query execution.
|
|
120
|
+
:rtype: dict
|
|
121
|
+
"""
|
|
122
|
+
if self.status != "error":
|
|
123
|
+
return self._stats
|
|
124
|
+
else:
|
|
125
|
+
raise self.exception
|
|
126
|
+
|
|
127
|
+
def was_successful(self) -> bool:
|
|
128
|
+
"""
|
|
129
|
+
Determine if the query executed successfully.
|
|
130
|
+
|
|
131
|
+
:return: True if the query was successful, False otherwise.
|
|
132
|
+
:rtype: bool
|
|
133
|
+
"""
|
|
134
|
+
if self.status != "error":
|
|
135
|
+
return True
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
def status_code(self) -> int:
|
|
139
|
+
"""
|
|
140
|
+
Return the status code of the query.
|
|
141
|
+
|
|
142
|
+
:return: The status code of the query.
|
|
143
|
+
:rtype: int
|
|
144
|
+
"""
|
|
145
|
+
return self._status_code
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from typing import Any, List, Type
|
|
2
|
+
|
|
3
|
+
from chainalysis.util_functions.exceptions import BadRequest
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_list_type(my_list: List[Any]) -> Type:
|
|
7
|
+
"""Return the type of list elements if all are the same type, otherwise raise an error."""
|
|
8
|
+
if not my_list:
|
|
9
|
+
raise ValueError("The list is empty and has no element type.")
|
|
10
|
+
|
|
11
|
+
element_types = {type(item) for item in my_list}
|
|
12
|
+
|
|
13
|
+
if len(element_types) > 1:
|
|
14
|
+
raise BadRequest(
|
|
15
|
+
f"The list contains multiple types: {element_types}. Enter a list with only numbers, strings, or bools."
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
return element_types.pop()
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
class DataSolutionsSDKException(Exception):
|
|
2
|
+
"""Base class for SDK exceptions."""
|
|
3
|
+
|
|
4
|
+
def __init__(
|
|
5
|
+
self,
|
|
6
|
+
message=None,
|
|
7
|
+
status_code=0,
|
|
8
|
+
):
|
|
9
|
+
super().__init__(message)
|
|
10
|
+
self.status_code = status_code
|
|
11
|
+
|
|
12
|
+
def get_exception(self):
|
|
13
|
+
return self
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BadRequest(DataSolutionsSDKException):
|
|
17
|
+
"""Exception for Bad Request."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, message="Bad Request"):
|
|
20
|
+
super().__init__(
|
|
21
|
+
message,
|
|
22
|
+
status_code=400,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Unauthorized(DataSolutionsSDKException):
|
|
27
|
+
"""Exception for 401 Unauthorized."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, message="Unauthorized"):
|
|
30
|
+
super().__init__(
|
|
31
|
+
message,
|
|
32
|
+
status_code=401,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Forbidden(DataSolutionsSDKException):
|
|
37
|
+
"""Exception for 403 Forbidden."""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
message="Forbidden. Contact Data Solutions if you believe you should have access to this endpoint/data.",
|
|
42
|
+
):
|
|
43
|
+
super().__init__(
|
|
44
|
+
message,
|
|
45
|
+
status_code=403,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class NotFound(DataSolutionsSDKException):
|
|
50
|
+
"""Exception for 404 Not Found."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, message="Not Found. Is your query correct?"):
|
|
53
|
+
super().__init__(
|
|
54
|
+
message,
|
|
55
|
+
status_code=404,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class InternalServerException(DataSolutionsSDKException):
|
|
60
|
+
"""Exception for 500 Internal Server Error."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, message="Internal Server Error"):
|
|
63
|
+
super().__init__(
|
|
64
|
+
message,
|
|
65
|
+
status_code=500,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class DataSolutionsAPIException(DataSolutionsSDKException):
|
|
70
|
+
"""Exception for the API returning an unexpected response."""
|
|
71
|
+
|
|
72
|
+
def __init__(self, message="Unexpected response from the API"):
|
|
73
|
+
super().__init__(
|
|
74
|
+
message,
|
|
75
|
+
status_code=0,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class UnhandledException(DataSolutionsSDKException):
|
|
80
|
+
"""Unhandled exception."""
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
message="An unhandled exception occured. Please contact the Data Solutions Team.",
|
|
85
|
+
details="",
|
|
86
|
+
):
|
|
87
|
+
super().__init__((message, details))
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from requests import JSONDecodeError, request
|
|
2
|
+
from tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential
|
|
3
|
+
|
|
4
|
+
from chainalysis.util_functions.exceptions import (
|
|
5
|
+
BadRequest,
|
|
6
|
+
DataSolutionsAPIException,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_headers(api_key: str) -> dict:
|
|
11
|
+
"""
|
|
12
|
+
Generate headers for an HTTP request.
|
|
13
|
+
|
|
14
|
+
This function creates a dictionary of headers required for an HTTP request,
|
|
15
|
+
including the API key for authorization and JSON content-type headers which
|
|
16
|
+
indicate what sort of information will be sent with the request.
|
|
17
|
+
|
|
18
|
+
:param api_key: The API key used for authenticating the request.
|
|
19
|
+
:return: A dictionary containing the headers for the HTTP request.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
"X-API-KEY": api_key,
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
"Accept": "application/json",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def retry_if_not_bad_request(exception):
|
|
30
|
+
if isinstance(exception, BadRequest):
|
|
31
|
+
return False # Do not retry if it's a BadRequestException
|
|
32
|
+
return True # Retry for other exceptions
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@retry(
|
|
36
|
+
wait=wait_exponential(multiplier=1, min=4, max=10),
|
|
37
|
+
retry=retry_if_exception(retry_if_not_bad_request),
|
|
38
|
+
stop=stop_after_attempt(10),
|
|
39
|
+
)
|
|
40
|
+
def issue_request(
|
|
41
|
+
url: str,
|
|
42
|
+
api_key: str,
|
|
43
|
+
params: dict = {},
|
|
44
|
+
body: dict = {},
|
|
45
|
+
method: str = "GET",
|
|
46
|
+
) -> dict:
|
|
47
|
+
"""
|
|
48
|
+
Helper method to issue a request to the Data Solutions API.
|
|
49
|
+
This method will automatically retry the request, and handle
|
|
50
|
+
basic error checking.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
# Encode params and body into a single variable data
|
|
54
|
+
data = {**(params or {}), **(body or {})}
|
|
55
|
+
|
|
56
|
+
response = request(
|
|
57
|
+
method,
|
|
58
|
+
url,
|
|
59
|
+
headers=get_headers(api_key),
|
|
60
|
+
json=data,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if response.status_code >= 300:
|
|
64
|
+
try: # Try to decode the json response of the error
|
|
65
|
+
json_output = response.json()
|
|
66
|
+
error_message = json_output.get("message")
|
|
67
|
+
|
|
68
|
+
except JSONDecodeError:
|
|
69
|
+
raise DataSolutionsAPIException(
|
|
70
|
+
message="Unexpected response from the API - response was not JSON",
|
|
71
|
+
)
|
|
72
|
+
if response.status_code == 400:
|
|
73
|
+
raise BadRequest(
|
|
74
|
+
message=error_message,
|
|
75
|
+
)
|
|
76
|
+
raise DataSolutionsAPIException(
|
|
77
|
+
message=error_message,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
try: # Try to decode the json response of the successful request
|
|
81
|
+
response_json = response.json()
|
|
82
|
+
except JSONDecodeError:
|
|
83
|
+
raise DataSolutionsAPIException(
|
|
84
|
+
message="Unexpected response from the API - response was not JSON",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return response_json
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from chainalysis.utils import *
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import decimal
|
|
3
|
+
|
|
4
|
+
from chainalysis.util_functions.check_list_type import get_list_type
|
|
5
|
+
from chainalysis.util_functions.exceptions import BadRequest
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Stringify:
|
|
9
|
+
"""
|
|
10
|
+
This class contains functions to transform python objects
|
|
11
|
+
into formatted string params so they can be used to query
|
|
12
|
+
Data Solutions' databases without needing to be transformed
|
|
13
|
+
manually by the user.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def lists(self, _list: list) -> str:
|
|
17
|
+
"""
|
|
18
|
+
Convert a list to a formatted string param.
|
|
19
|
+
|
|
20
|
+
:param list: The list to be converted.
|
|
21
|
+
:type list: list
|
|
22
|
+
|
|
23
|
+
:raises Exception: Raises an exception if a list type is not submitted.
|
|
24
|
+
:raises Exception: If lists do not contain the right types.
|
|
25
|
+
:return: The converted list.
|
|
26
|
+
:rtype: str
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
if not isinstance(_list, list):
|
|
30
|
+
raise BadRequest(
|
|
31
|
+
"Incorrect type. Supply a list.",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
type = get_list_type(_list)
|
|
35
|
+
|
|
36
|
+
if type == int or type == bool or type == float or type == decimal:
|
|
37
|
+
return f"({', '.join(map(str, _list))})"
|
|
38
|
+
|
|
39
|
+
if type == str:
|
|
40
|
+
result = "("
|
|
41
|
+
|
|
42
|
+
for i, element in enumerate(_list):
|
|
43
|
+
if i > 0:
|
|
44
|
+
result += ", "
|
|
45
|
+
result += f"'{element}'"
|
|
46
|
+
|
|
47
|
+
result += ")"
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
def columns(self, columns: list) -> str:
|
|
51
|
+
"""
|
|
52
|
+
Convert a column select list to a formatted string param.
|
|
53
|
+
|
|
54
|
+
:param list: The column select list object to be converted.
|
|
55
|
+
:type list: list
|
|
56
|
+
|
|
57
|
+
:return: The converted column select list.
|
|
58
|
+
:rtype: str
|
|
59
|
+
"""
|
|
60
|
+
type = get_list_type(columns)
|
|
61
|
+
if type != str:
|
|
62
|
+
raise BadRequest("Columns must be a list")
|
|
63
|
+
return ", ".join(columns)
|
|
64
|
+
|
|
65
|
+
def datetimes(self, _datetime: datetime.datetime) -> str:
|
|
66
|
+
"""
|
|
67
|
+
Convert a datetime object to a formatted string param.
|
|
68
|
+
|
|
69
|
+
:param list: The datetime object to be converted.
|
|
70
|
+
:type list: list
|
|
71
|
+
|
|
72
|
+
:return: The converted datetime object.
|
|
73
|
+
:rtype: str
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
if not isinstance(_datetime, datetime.datetime):
|
|
77
|
+
raise BadRequest("Incorrect type. Supply a datetime.datetime object.")
|
|
78
|
+
|
|
79
|
+
return f"'{_datetime.strftime('%Y-%m-%dT%H:%M:%SZ')}'"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: chainalysis
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: SDK functions for users to query Data Solutions databases.
|
|
5
|
+
Project-URL: Homepage, https://github.com/TransposeData/chainalysis-data-solutions-python-sdk
|
|
6
|
+
Author-email: "Rah Tarar (musa-tarar)" <musa.nishat@chainalysis.com>
|
|
7
|
+
License-File: LICENCE
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
# Welcome to the Transpose Python SDK
|
|
17
|
+
A modern python wrapper for the Transpose API Suite.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install DataSolutions
|
|
23
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
chainalysis/DataSolutions.py,sha256=qeqKENCyCoJrerfcit7j6yELuY-ePk3gHzT-X6ckDR4,545
|
|
2
|
+
chainalysis/__init__.py,sha256=NnWPLQkHHK52JYDzj5MISZNd0LXVJm0vvop1v-oZGSY,64
|
|
3
|
+
chainalysis/constants.py,sha256=LDSDcwxDaKx0bJC-nXL_3piwMdTynMz2mVOZdVeHgDU,266
|
|
4
|
+
chainalysis/sql/__init__.py,sha256=27BfFf5rFz3kg4ColUnLTOpy0VMEpALJhcZcHh8zu6g,30
|
|
5
|
+
chainalysis/sql/analytical.py,sha256=H8RNXQkl1JMyGsVrDBfp8y9BADWC1XDUECIZL20igmg,7045
|
|
6
|
+
chainalysis/sql/sql.py,sha256=6wpc1_rKqSpF-ULDyVzJ_8h28hb3ncIXps4Pd86ryPQ,255
|
|
7
|
+
chainalysis/sql/transactional.py,sha256=fh-3rgoJAMcSHYj6c2wTvAk5SOgBbaBpHVCxmG_dYYI,4472
|
|
8
|
+
chainalysis/util_functions/check_list_type.py,sha256=_lZmdnEkG0uFQYqPUkZFP8cj1qVXEKaBN-MtzaRw06o,603
|
|
9
|
+
chainalysis/util_functions/exceptions.py,sha256=FQSrVGJu1amQAj1SFYb1p5ZUesRNGUli6JyxdT5VX4U,2120
|
|
10
|
+
chainalysis/util_functions/requests.py,sha256=qDpxtkYbPI0vx5jJBtmik1i7O7J76k1qVe7ExgyoxIc,2589
|
|
11
|
+
chainalysis/utils/__init__.py,sha256=OI-IE3BHkwsChB89cE0wB1TQRL_m4_43Fl09CIdPyaI,32
|
|
12
|
+
chainalysis/utils/stringify.py,sha256=srG1QPj4p3tXvt-yWK7fbOigT0zMrPlFmIhQNFSxgXA,2325
|
|
13
|
+
chainalysis/utils/utils.py,sha256=iVWQ8C-9xOmb83uz8xL9cOHHdigY75qf2ej2nkqKHwE,215
|
|
14
|
+
chainalysis-0.0.1.dist-info/METADATA,sha256=yPKOb1cMRUedkGIT3QHbJ6JUiXGHldRpMG6iPYAW0gc,744
|
|
15
|
+
chainalysis-0.0.1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
16
|
+
chainalysis-0.0.1.dist-info/licenses/LICENCE,sha256=7EI8xVBu6h_7_JlVw-yPhhOZlpY9hP8wal7kHtqKT_E,1074
|
|
17
|
+
chainalysis-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2018 The Python Packaging Authority
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|