tentaclio-databricks 2.1.0__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.
- tentaclio_databricks/__init__.py +16 -0
- tentaclio_databricks/__main__.py +6 -0
- tentaclio_databricks/clients/__init__.py +9 -0
- tentaclio_databricks/clients/databricks_client.py +167 -0
- tentaclio_databricks-2.1.0.dist-info/METADATA +136 -0
- tentaclio_databricks-2.1.0.dist-info/RECORD +9 -0
- tentaclio_databricks-2.1.0.dist-info/WHEEL +5 -0
- tentaclio_databricks-2.1.0.dist-info/licenses/LICENSE.txt +21 -0
- tentaclio_databricks-2.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""This package implements the tentaclio databricks database client"""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError
|
|
4
|
+
from importlib.metadata import version as _version
|
|
5
|
+
|
|
6
|
+
from tentaclio import * # noqa
|
|
7
|
+
|
|
8
|
+
from .clients.databricks_client import DatabricksClient
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
__version__ = _version("tentaclio-databricks")
|
|
12
|
+
except PackageNotFoundError:
|
|
13
|
+
__version__ = "0.0.0"
|
|
14
|
+
|
|
15
|
+
# Add DB registry
|
|
16
|
+
DB_REGISTRY.register("databricks+thrift", DatabricksClient) # type: ignore
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""The client module defines stream and query based clients.
|
|
2
|
+
|
|
3
|
+
Stream clients allow to read and write from stream based sources with ease,
|
|
4
|
+
and in an interchangable manner.
|
|
5
|
+
|
|
6
|
+
Query based clients unify how to access databases leveraging from sqlalchemy.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .databricks_client import * # noqa
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Databricks query client."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from databricks import sql
|
|
8
|
+
from tentaclio import URL
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from polars import DataFrame as PolarsDataFrame # pyright: ignore[reportMissingImports]
|
|
12
|
+
else:
|
|
13
|
+
PolarsDataFrame = Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DatabricksClientException(Exception):
|
|
17
|
+
"""Databricks client specific exception."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DatabricksClient:
|
|
21
|
+
"""Databricks client, backed by an Apache Thrift connection."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
url: URL,
|
|
26
|
+
arraysize: int = 1000000,
|
|
27
|
+
use_arrow: bool = False,
|
|
28
|
+
query_annotations: Optional[Dict[str, str]] = None,
|
|
29
|
+
**kwargs,
|
|
30
|
+
):
|
|
31
|
+
# This is a very common issue reported by the users
|
|
32
|
+
if url.query is None or "HTTPPath" not in url.query:
|
|
33
|
+
raise DatabricksClientException(
|
|
34
|
+
"Missing the HTTPPath element in the http query. \n\n"
|
|
35
|
+
"The url should look like: "
|
|
36
|
+
"databricks+thrift://workspaceurl.databricks.com/?HTTPPath=value\n"
|
|
37
|
+
"Check the connection details of your databricks warehouse"
|
|
38
|
+
)
|
|
39
|
+
if url.username is None or url.username == "":
|
|
40
|
+
raise DatabricksClientException(
|
|
41
|
+
"Missing the token in the url:\n\n"
|
|
42
|
+
"The url should look like: "
|
|
43
|
+
"databricks+thrift://token@workspaceurl.databricks.com/?HTTPPath=value"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
self.server_hostname = url.hostname
|
|
47
|
+
self.http_path = url.query["HTTPPath"]
|
|
48
|
+
self.access_token = url.username
|
|
49
|
+
self.arraysize = arraysize
|
|
50
|
+
self.use_arrow = use_arrow
|
|
51
|
+
self.query_annotations = query_annotations or {}
|
|
52
|
+
|
|
53
|
+
def __enter__(self):
|
|
54
|
+
conn_kwargs = {
|
|
55
|
+
"server_hostname": self.server_hostname,
|
|
56
|
+
"http_path": self.http_path,
|
|
57
|
+
"access_token": self.access_token,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if self.use_arrow:
|
|
61
|
+
conn_kwargs.update(
|
|
62
|
+
{
|
|
63
|
+
"use_arrow_native_complex_types": True,
|
|
64
|
+
"use_arrow_native_decimals": True,
|
|
65
|
+
"use_arrow_native_timestamps": True,
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
self.conn = sql.connect(**conn_kwargs)
|
|
70
|
+
|
|
71
|
+
# Only set arraysize if not using Arrow
|
|
72
|
+
if self.use_arrow:
|
|
73
|
+
self.cursor = self.conn.cursor()
|
|
74
|
+
else:
|
|
75
|
+
self.cursor = self.conn.cursor(arraysize=self.arraysize)
|
|
76
|
+
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
80
|
+
self.cursor.close()
|
|
81
|
+
self.conn.close()
|
|
82
|
+
|
|
83
|
+
def _build_query_comment(self) -> str:
|
|
84
|
+
"""Build a SQL comment from query annotations.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Formatted SQL comment string, or empty string if no annotations.
|
|
88
|
+
"""
|
|
89
|
+
if not self.query_annotations:
|
|
90
|
+
return ""
|
|
91
|
+
|
|
92
|
+
parts = []
|
|
93
|
+
for key, value in self.query_annotations.items():
|
|
94
|
+
# Escape single quotes in values
|
|
95
|
+
escaped_value = value.replace("'", "''")
|
|
96
|
+
parts.append(f"{key}='{escaped_value}'")
|
|
97
|
+
|
|
98
|
+
if parts:
|
|
99
|
+
return f"/* {', '.join(parts)} */\n"
|
|
100
|
+
return ""
|
|
101
|
+
|
|
102
|
+
def _prepend_comment(self, sql_query: str) -> str:
|
|
103
|
+
"""Prepend query comment to SQL query if configured.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
sql_query: The SQL query string.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
SQL query with prepended comment if annotations configured, else original query.
|
|
110
|
+
"""
|
|
111
|
+
comment = self._build_query_comment()
|
|
112
|
+
if comment:
|
|
113
|
+
return f"{comment}{sql_query}"
|
|
114
|
+
return sql_query
|
|
115
|
+
|
|
116
|
+
def query(self, sql_query: str, **kwargs) -> List[Any]:
|
|
117
|
+
"""Execute a SQL query, and return results."""
|
|
118
|
+
sql_query = self._prepend_comment(sql_query)
|
|
119
|
+
self.cursor.execute(sql_query, **kwargs)
|
|
120
|
+
return self.cursor.fetchall()
|
|
121
|
+
|
|
122
|
+
def execute(self, sql_query: str, **kwargs) -> None:
|
|
123
|
+
"""Execute a raw SQL query command."""
|
|
124
|
+
sql_query = self._prepend_comment(sql_query)
|
|
125
|
+
self.cursor.execute(sql_query, **kwargs)
|
|
126
|
+
|
|
127
|
+
def get_df(self, sql_query: str, **kwargs) -> pd.DataFrame:
|
|
128
|
+
"""Run a raw SQL query and return a data frame."""
|
|
129
|
+
sql_query = self._prepend_comment(sql_query)
|
|
130
|
+
self.cursor.execute(sql_query, **kwargs)
|
|
131
|
+
|
|
132
|
+
if self.use_arrow:
|
|
133
|
+
# Default to Arrow for fast data transfer
|
|
134
|
+
try:
|
|
135
|
+
arrow_table = self.cursor.fetchall_arrow()
|
|
136
|
+
return arrow_table.to_pandas()
|
|
137
|
+
except AttributeError:
|
|
138
|
+
data = self.cursor.fetchall()
|
|
139
|
+
else:
|
|
140
|
+
# Original row-by-row method
|
|
141
|
+
data = self.cursor.fetchall()
|
|
142
|
+
|
|
143
|
+
# Build DataFrame from rows (fallback path)
|
|
144
|
+
columns = (
|
|
145
|
+
[col_desc[0] for col_desc in self.cursor.description]
|
|
146
|
+
if self.cursor.description
|
|
147
|
+
else []
|
|
148
|
+
)
|
|
149
|
+
return pd.DataFrame(data, columns=columns)
|
|
150
|
+
|
|
151
|
+
def get_pl(self, sql_query: str, **kwargs) -> PolarsDataFrame:
|
|
152
|
+
"""Run a raw SQL query and return a polars DataFrame."""
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
pl = importlib.import_module("polars")
|
|
156
|
+
except ModuleNotFoundError as exc:
|
|
157
|
+
raise ModuleNotFoundError(
|
|
158
|
+
"Polars is not installed. Install it with "
|
|
159
|
+
"`pip install tentaclio-databricks[polars]` or `pip install polars` to use "
|
|
160
|
+
"the `.get_pl()` method."
|
|
161
|
+
) from exc
|
|
162
|
+
|
|
163
|
+
sql_query = self._prepend_comment(sql_query)
|
|
164
|
+
self.cursor.execute(sql_query, **kwargs)
|
|
165
|
+
arrow_table = self.cursor.fetchall_arrow()
|
|
166
|
+
|
|
167
|
+
return pl.DataFrame(arrow_table)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tentaclio-databricks
|
|
3
|
+
Version: 2.1.0
|
|
4
|
+
Summary: A python project containing all the dependencies for schema databricks for tentaclio.
|
|
5
|
+
Author-email: Octopus Energy <nerds@octopus.energy>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Intended Audience :: Developers
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Programming Language :: Python
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE.txt
|
|
19
|
+
Requires-Dist: tentaclio
|
|
20
|
+
Requires-Dist: databricks-sql-connector
|
|
21
|
+
Requires-Dist: pandas<3
|
|
22
|
+
Requires-Dist: pyarrow
|
|
23
|
+
Provides-Extra: polars
|
|
24
|
+
Requires-Dist: polars>=0.16; extra == "polars"
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-mock; extra == "dev"
|
|
29
|
+
Requires-Dist: black; extra == "dev"
|
|
30
|
+
Requires-Dist: flake8; extra == "dev"
|
|
31
|
+
Requires-Dist: Flake8-pyproject; extra == "dev"
|
|
32
|
+
Requires-Dist: isort>=5.11.2; extra == "dev"
|
|
33
|
+
Requires-Dist: mypy; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# tentaclio-databricks
|
|
37
|
+
|
|
38
|
+
A package containing all the dependencies for the `databricks+thrift` tentaclio schema .
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
This project comes with a `Makefile` which is ready to do basic common tasks
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
$ make help
|
|
46
|
+
install Initalise the virtual env installing deps
|
|
47
|
+
clean Remove all the unwanted clutter
|
|
48
|
+
lock Lock dependencies
|
|
49
|
+
update Update dependencies (whole tree)
|
|
50
|
+
sync Install dependencies as per the lock file
|
|
51
|
+
lint Lint files with flake and mypy
|
|
52
|
+
format Run black and isort
|
|
53
|
+
test Run unit tests
|
|
54
|
+
circleci Validate circleci configuration (needs circleci cli)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Configuring access to Databricks
|
|
58
|
+
|
|
59
|
+
Your connection url should be in the following format:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
databricks+thrift://<token>@<host>?HTTPPath=<http_path>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Example values:
|
|
66
|
+
|
|
67
|
+
- token: dapi1213456789abc
|
|
68
|
+
- host: myhost.databricks.com
|
|
69
|
+
- http_path: /sql/1.0/endpoints/123456789
|
|
70
|
+
|
|
71
|
+
## Query Comments
|
|
72
|
+
|
|
73
|
+
Queries can be annotated with comments for observability using the `query_annotations` parameter.
|
|
74
|
+
|
|
75
|
+
### Example: Basic usage
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import os
|
|
79
|
+
from tentaclio import URL
|
|
80
|
+
from tentaclio_databricks.clients.databricks_client import DatabricksClient
|
|
81
|
+
|
|
82
|
+
url = URL("databricks+thrift://token@host.databricks.com?HTTPPath=/sql/1.0/endpoints/123")
|
|
83
|
+
client = DatabricksClient(
|
|
84
|
+
url,
|
|
85
|
+
query_annotations={
|
|
86
|
+
"app_name": "JupyterHub",
|
|
87
|
+
"user": os.environ.get("USER_NAME", "unknown"),
|
|
88
|
+
"pipeline_id": "456"
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Result
|
|
94
|
+
|
|
95
|
+
All queries executed by the client will have prepended comments:
|
|
96
|
+
|
|
97
|
+
```sql
|
|
98
|
+
/* app_name='JupyterHub', user='john', pipeline_id='456' */
|
|
99
|
+
SELECT * FROM table
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Arrow Usage
|
|
103
|
+
|
|
104
|
+
`DatabricksClient` enables Arrow's usage for faster data transfer and more faithful type handling.
|
|
105
|
+
|
|
106
|
+
### Example: Default row-based fetch
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from tentaclio import URL
|
|
110
|
+
from tentaclio_databricks.clients.databricks_client import DatabricksClient
|
|
111
|
+
|
|
112
|
+
url = URL("databricks+thrift://token@host.databricks.com?HTTPPath=/sql/1.0/endpoints/123")
|
|
113
|
+
with DatabricksClient(url) as client:
|
|
114
|
+
df = client.get_df("SELECT * FROM sample_table")
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
When Arrow is disabled, the client falls back to `fetchall()` and uses `arraysize` for the cursor.
|
|
118
|
+
|
|
119
|
+
### Example: Arrow enabled
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from tentaclio import URL
|
|
123
|
+
from tentaclio_databricks.clients.databricks_client import DatabricksClient
|
|
124
|
+
|
|
125
|
+
url = URL("databricks+thrift://token@host.databricks.com?HTTPPath=/sql/1.0/endpoints/123")
|
|
126
|
+
with DatabricksClient(url, use_arrow=True) as client:
|
|
127
|
+
df = client.get_df("SELECT * FROM sample_table")
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
When Arrow is enabled, the client configures the connection with:
|
|
131
|
+
|
|
132
|
+
- `use_arrow_native_complex_types`
|
|
133
|
+
- `use_arrow_native_decimals`
|
|
134
|
+
- `use_arrow_native_timestamps`
|
|
135
|
+
|
|
136
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
tentaclio_databricks/__init__.py,sha256=Te6JwNyyV3Pjfii-n63bydjLcXLFaypwkKbwwYLxSKA,472
|
|
2
|
+
tentaclio_databricks/__main__.py,sha256=ed3jPmp9bmfc5ekIcjPxVEAPqQ3mXYacga2oUfEJgPI,147
|
|
3
|
+
tentaclio_databricks/clients/__init__.py,sha256=FRSm7eaquCUtXPiaba1oxaT2Lv6Zf4gRfqhcKIwPZKU,296
|
|
4
|
+
tentaclio_databricks/clients/databricks_client.py,sha256=9FWtMCVTa-xCg8rzcur5rE9tyWLCD4T1RtUytuTHncI,5636
|
|
5
|
+
tentaclio_databricks-2.1.0.dist-info/licenses/LICENSE.txt,sha256=Aqv2h5R53YlGaQXl7XgUDMZPUxJK1HftERN2ExXJ_u4,1080
|
|
6
|
+
tentaclio_databricks-2.1.0.dist-info/METADATA,sha256=W0CMq9tGkwHUxTI0TdchW_lBpFLLq-KALh-v7DVE77w,4158
|
|
7
|
+
tentaclio_databricks-2.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
8
|
+
tentaclio_databricks-2.1.0.dist-info/top_level.txt,sha256=ZoGTS3HcLGAiSp9c638A_y5eZ9J4uzl91KQylxs9ptY,21
|
|
9
|
+
tentaclio_databricks-2.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Octopus Energy Limited.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tentaclio_databricks
|