chainalysis 0.0.1__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.
Potentially problematic release.
This version of chainalysis might be problematic. Click here for more details.
- chainalysis-0.0.1/.github/workflow.yml +17 -0
- chainalysis-0.0.1/.gitignore +14 -0
- chainalysis-0.0.1/.pre-commit-config.yaml +39 -0
- chainalysis-0.0.1/.vscode/settings.json +5 -0
- chainalysis-0.0.1/LICENCE +19 -0
- chainalysis-0.0.1/Makefile +4 -0
- chainalysis-0.0.1/PKG-INFO +23 -0
- chainalysis-0.0.1/README.md +10 -0
- chainalysis-0.0.1/__init__.py +0 -0
- chainalysis-0.0.1/playground/play.py +21 -0
- chainalysis-0.0.1/pyproject.toml +30 -0
- chainalysis-0.0.1/requirements.txt +5 -0
- chainalysis-0.0.1/src/__init__.py +0 -0
- chainalysis-0.0.1/src/chainalysis/DataSolutions.py +22 -0
- chainalysis-0.0.1/src/chainalysis/__init__.py +3 -0
- chainalysis-0.0.1/src/chainalysis/constants.py +12 -0
- chainalysis-0.0.1/src/chainalysis/sql/__init__.py +1 -0
- chainalysis-0.0.1/src/chainalysis/sql/analytical.py +234 -0
- chainalysis-0.0.1/src/chainalysis/sql/sql.py +8 -0
- chainalysis-0.0.1/src/chainalysis/sql/transactional.py +145 -0
- chainalysis-0.0.1/src/chainalysis/util_functions/check_list_type.py +18 -0
- chainalysis-0.0.1/src/chainalysis/util_functions/exceptions.py +87 -0
- chainalysis-0.0.1/src/chainalysis/util_functions/requests.py +87 -0
- chainalysis-0.0.1/src/chainalysis/utils/__init__.py +1 -0
- chainalysis-0.0.1/src/chainalysis/utils/stringify.py +79 -0
- chainalysis-0.0.1/src/chainalysis/utils/utils.py +11 -0
- chainalysis-0.0.1/src/test/__init__.py +0 -0
- chainalysis-0.0.1/src/test/analytical/tests.py +180 -0
- chainalysis-0.0.1/src/test/requests/tests.py +158 -0
- chainalysis-0.0.1/src/test/transactional/__init__.py +0 -0
- chainalysis-0.0.1/src/test/transactional/tests.py +150 -0
- chainalysis-0.0.1/src/test/utils/__init__.py +0 -0
- chainalysis-0.0.1/src/test/utils/tests.py +124 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
3
|
+
rev: v2.3.0
|
|
4
|
+
hooks:
|
|
5
|
+
- id: check-yaml
|
|
6
|
+
- id: end-of-file-fixer
|
|
7
|
+
- id: trailing-whitespace
|
|
8
|
+
- repo: https://github.com/pycqa/isort
|
|
9
|
+
rev: 5.12.0
|
|
10
|
+
hooks:
|
|
11
|
+
- id: isort
|
|
12
|
+
args: ["--profile", "black"]
|
|
13
|
+
name: isort (python)
|
|
14
|
+
- repo: https://github.com/psf/black
|
|
15
|
+
rev: 22.3.0
|
|
16
|
+
hooks:
|
|
17
|
+
- id: black
|
|
18
|
+
- repo: local
|
|
19
|
+
hooks:
|
|
20
|
+
- id: autoflake
|
|
21
|
+
name: autoflake
|
|
22
|
+
entry: autoflake
|
|
23
|
+
language: python
|
|
24
|
+
"types": [python]
|
|
25
|
+
require_serial: true
|
|
26
|
+
args:
|
|
27
|
+
- "--in-place"
|
|
28
|
+
- "--expand-star-imports"
|
|
29
|
+
- "--remove-duplicate-keys"
|
|
30
|
+
- "--remove-unused-variables"
|
|
31
|
+
- "--remove-all-unused-imports"
|
|
32
|
+
- repo: local
|
|
33
|
+
hooks:
|
|
34
|
+
- id: run-tests
|
|
35
|
+
name: Run tests
|
|
36
|
+
entry: make test
|
|
37
|
+
language: system
|
|
38
|
+
pass_filenames: false
|
|
39
|
+
stages: [commit]
|
|
@@ -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.
|
|
@@ -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
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from chainalysis.DataSolutions import DataSolutions
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
ds = DataSolutions(os.environ.get("API_KEY"))
|
|
8
|
+
block_numbers = [1, 2, 3]
|
|
9
|
+
analytical_result = ds.sql.analytical(
|
|
10
|
+
"SELECT * from {{chain}}.electrum_observations WHERE block_number in {{block_number}}",
|
|
11
|
+
parameters={
|
|
12
|
+
"chain": "bitcoin",
|
|
13
|
+
"block_number": ds.utils.stringify.lists(block_numbers),
|
|
14
|
+
},
|
|
15
|
+
)
|
|
16
|
+
# This should return a bad request error
|
|
17
|
+
print(analytical_result.json())
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
main()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [
|
|
3
|
+
"hatchling",
|
|
4
|
+
"pandas==2.2.2",
|
|
5
|
+
"requests==2.32.3",
|
|
6
|
+
"tenacity==9.0.0",
|
|
7
|
+
"autoflake==2.3.1"
|
|
8
|
+
]
|
|
9
|
+
build-backend = "hatchling.build"
|
|
10
|
+
|
|
11
|
+
[project]
|
|
12
|
+
name = "chainalysis"
|
|
13
|
+
version = "0.0.1"
|
|
14
|
+
authors = [
|
|
15
|
+
{ name = "Rah Tarar (musa-tarar)", email = "musa.nishat@chainalysis.com" },
|
|
16
|
+
]
|
|
17
|
+
description = "SDK functions for users to query Data Solutions databases."
|
|
18
|
+
readme = "README.md"
|
|
19
|
+
requires-python = ">=3.8"
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/TransposeData/chainalysis-data-solutions-python-sdk"
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
packages = ["src/chainalysis"]
|
|
File without changes
|
|
@@ -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()
|
|
@@ -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
|
|
@@ -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()
|