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.

Files changed (33) hide show
  1. chainalysis-0.0.1/.github/workflow.yml +17 -0
  2. chainalysis-0.0.1/.gitignore +14 -0
  3. chainalysis-0.0.1/.pre-commit-config.yaml +39 -0
  4. chainalysis-0.0.1/.vscode/settings.json +5 -0
  5. chainalysis-0.0.1/LICENCE +19 -0
  6. chainalysis-0.0.1/Makefile +4 -0
  7. chainalysis-0.0.1/PKG-INFO +23 -0
  8. chainalysis-0.0.1/README.md +10 -0
  9. chainalysis-0.0.1/__init__.py +0 -0
  10. chainalysis-0.0.1/playground/play.py +21 -0
  11. chainalysis-0.0.1/pyproject.toml +30 -0
  12. chainalysis-0.0.1/requirements.txt +5 -0
  13. chainalysis-0.0.1/src/__init__.py +0 -0
  14. chainalysis-0.0.1/src/chainalysis/DataSolutions.py +22 -0
  15. chainalysis-0.0.1/src/chainalysis/__init__.py +3 -0
  16. chainalysis-0.0.1/src/chainalysis/constants.py +12 -0
  17. chainalysis-0.0.1/src/chainalysis/sql/__init__.py +1 -0
  18. chainalysis-0.0.1/src/chainalysis/sql/analytical.py +234 -0
  19. chainalysis-0.0.1/src/chainalysis/sql/sql.py +8 -0
  20. chainalysis-0.0.1/src/chainalysis/sql/transactional.py +145 -0
  21. chainalysis-0.0.1/src/chainalysis/util_functions/check_list_type.py +18 -0
  22. chainalysis-0.0.1/src/chainalysis/util_functions/exceptions.py +87 -0
  23. chainalysis-0.0.1/src/chainalysis/util_functions/requests.py +87 -0
  24. chainalysis-0.0.1/src/chainalysis/utils/__init__.py +1 -0
  25. chainalysis-0.0.1/src/chainalysis/utils/stringify.py +79 -0
  26. chainalysis-0.0.1/src/chainalysis/utils/utils.py +11 -0
  27. chainalysis-0.0.1/src/test/__init__.py +0 -0
  28. chainalysis-0.0.1/src/test/analytical/tests.py +180 -0
  29. chainalysis-0.0.1/src/test/requests/tests.py +158 -0
  30. chainalysis-0.0.1/src/test/transactional/__init__.py +0 -0
  31. chainalysis-0.0.1/src/test/transactional/tests.py +150 -0
  32. chainalysis-0.0.1/src/test/utils/__init__.py +0 -0
  33. chainalysis-0.0.1/src/test/utils/tests.py +124 -0
@@ -0,0 +1,17 @@
1
+ name: Do Nothing Workflow
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+ branches:
9
+ - main
10
+
11
+ jobs:
12
+ do-nothing:
13
+ runs-on: ubuntu-latest
14
+
15
+ steps:
16
+ - name: Do nothing step
17
+ run: echo "This workflow does nothing."
@@ -0,0 +1,14 @@
1
+ # python
2
+ venv
3
+ env
4
+ .env
5
+ .venv
6
+ __pycache__
7
+ *.pyc
8
+ *.egg-info
9
+
10
+ # misc
11
+ .DS_Store
12
+
13
+ docs
14
+ dist
@@ -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,5 @@
1
+ {
2
+ "python.analysis.extraPaths": [
3
+ "./src"
4
+ ]
5
+ }
@@ -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,4 @@
1
+ .PHONY: test
2
+
3
+ test:
4
+ python -m unittest discover test/
@@ -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
+ ![Transpose Banner](https://files.readme.io/c019281-Main_Docs_Banners_v1.png)
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,10 @@
1
+ ![Transpose Banner](https://files.readme.io/c019281-Main_Docs_Banners_v1.png)
2
+
3
+ # Welcome to the Transpose Python SDK
4
+ A modern python wrapper for the Transpose API Suite.
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ pip install DataSolutions
10
+ ```
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"]
@@ -0,0 +1,5 @@
1
+ pandas==2.2.2
2
+ requests==2.32.3
3
+ python-dotenv==1.0.1
4
+ tenacity==9.0.0
5
+ autoflake==2.3.1
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,3 @@
1
+ from chainalysis import *
2
+ from sql import *
3
+ from utils import *
@@ -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,8 @@
1
+ from chainalysis.sql.analytical import Analytical
2
+ from chainalysis.sql.transactional import Transactional
3
+
4
+
5
+ class Sql:
6
+ def __init__(self, api_key: str):
7
+ self.transactional = Transactional(api_key)
8
+ self.analytical = Analytical(api_key)
@@ -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()