thunderduck-sqlalchemy 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.
- thunderduck_sqlalchemy-0.1.0/PKG-INFO +153 -0
- thunderduck_sqlalchemy-0.1.0/README.md +131 -0
- thunderduck_sqlalchemy-0.1.0/pyproject.toml +70 -0
- thunderduck_sqlalchemy-0.1.0/pyproject.toml.orig +62 -0
- thunderduck_sqlalchemy-0.1.0/src/thunderduck_sqlalchemy/__init__.py +13 -0
- thunderduck_sqlalchemy-0.1.0/src/thunderduck_sqlalchemy/catalog.py +76 -0
- thunderduck_sqlalchemy-0.1.0/src/thunderduck_sqlalchemy/client.py +216 -0
- thunderduck_sqlalchemy-0.1.0/src/thunderduck_sqlalchemy/dbapi.py +306 -0
- thunderduck_sqlalchemy-0.1.0/src/thunderduck_sqlalchemy/dialect.py +242 -0
- thunderduck_sqlalchemy-0.1.0/src/thunderduck_sqlalchemy/exceptions.py +48 -0
- thunderduck_sqlalchemy-0.1.0/src/thunderduck_sqlalchemy/py.typed +0 -0
- thunderduck_sqlalchemy-0.1.0/src/thunderduck_sqlalchemy/superset_spec.py +124 -0
- thunderduck_sqlalchemy-0.1.0/src/thunderduck_sqlalchemy/types.py +232 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: thunderduck-sqlalchemy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SQLAlchemy dialect and DBAPI for thunderduck.io — query your lakehouse from Superset, Airflow, pandas and friends.
|
|
5
|
+
Keywords: sqlalchemy,duckdb,iceberg,lakehouse,thunderduck,superset
|
|
6
|
+
Author: YILDIRIM ADIGUZEL
|
|
7
|
+
Author-email: YILDIRIM ADIGUZEL <yadiguzel@gmail.com>
|
|
8
|
+
License: Apache-2.0
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Database
|
|
15
|
+
Classifier: Topic :: Database :: Front-Ends
|
|
16
|
+
Requires-Dist: httpx>=0.27.0
|
|
17
|
+
Requires-Dist: sqlalchemy>=1.4
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Project-URL: Homepage, https://thunderduck.io
|
|
20
|
+
Project-URL: Source, https://github.com/212data/thunderduck
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# thunderduck-sqlalchemy
|
|
24
|
+
|
|
25
|
+
A SQLAlchemy dialect and PEP 249 DBAPI for [thunderduck](https://thunderduck.io).
|
|
26
|
+
Query your lakehouse from anything that speaks SQLAlchemy — Superset, Airflow,
|
|
27
|
+
Dagster, Redash, pandas, Jupyter, Streamlit.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install thunderduck-sqlalchemy
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Works with SQLAlchemy 1.4 and 2.x, on Python 3.10+ — so it installs cleanly
|
|
34
|
+
into Apache Superset (which pins SQLAlchemy 1.4) as well as modern Airflow,
|
|
35
|
+
Dagster and pandas environments.
|
|
36
|
+
|
|
37
|
+
## Connect
|
|
38
|
+
|
|
39
|
+
Create an API token in the thunderduck console (**Settings → API Tokens**; it
|
|
40
|
+
starts with `tdk_` and is shown once), then:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from sqlalchemy import create_engine, text
|
|
44
|
+
from sqlalchemy.pool import NullPool
|
|
45
|
+
|
|
46
|
+
engine = create_engine(
|
|
47
|
+
"thunderduck://:tdk_your_token@api.thunderduck.io/",
|
|
48
|
+
poolclass=NullPool,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
with engine.connect() as conn:
|
|
52
|
+
for row in conn.execute(text('SELECT * FROM "lego"."public"."lego_sets" LIMIT 10')):
|
|
53
|
+
print(row)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
pandas works directly:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
import pandas as pd
|
|
60
|
+
|
|
61
|
+
df = pd.read_sql('SELECT * FROM "thunder_duck_demo"."iris"', engine)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### URL options
|
|
65
|
+
|
|
66
|
+
| Option | Default | Meaning |
|
|
67
|
+
| --- | --- | --- |
|
|
68
|
+
| password / `?token=` | — | Required. Your `tdk_…` API token. |
|
|
69
|
+
| `?ssl=false` | `true` | Use plain HTTP. For in-cluster use against the console-api Service. |
|
|
70
|
+
| `?poll_ms=` | `1000` | How often to poll for completion. |
|
|
71
|
+
| `?timeout_ms=` | `300000` | How long to wait for a query. |
|
|
72
|
+
| `?arraysize=` | `1000` | Rows fetched per page. |
|
|
73
|
+
|
|
74
|
+
## Table names
|
|
75
|
+
|
|
76
|
+
thunderduck names are `catalog.schema.table` (flat-file catalogs) or
|
|
77
|
+
`catalog.table` (Iceberg/Nessie catalogs). SQLAlchemy models two levels, so
|
|
78
|
+
**the schema is the whole dotted prefix**:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from sqlalchemy import MetaData, Table
|
|
82
|
+
|
|
83
|
+
md = MetaData()
|
|
84
|
+
sets = Table("lego_sets", md, schema="lego.public", autoload_with=engine) # 3-level
|
|
85
|
+
iris = Table("iris", md, schema="thunder_duck_demo", autoload_with=engine) # 2-level
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Reflection (`inspect(engine).get_schema_names()` etc.) reads thunderduck's
|
|
89
|
+
catalog API rather than issuing SQL, so browsing schemas is fast and free.
|
|
90
|
+
|
|
91
|
+
## Apache Superset
|
|
92
|
+
|
|
93
|
+
Installing the package is all that is needed — it registers a Superset DB
|
|
94
|
+
engine spec automatically. Add a database of type **Other** with:
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
thunderduck://:tdk_your_token@api.thunderduck.io/
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Superset's stop button genuinely cancels the running query.
|
|
101
|
+
|
|
102
|
+
## How it works, and what follows from that
|
|
103
|
+
|
|
104
|
+
Every statement is submitted to thunderduck's REST API, which runs it as an
|
|
105
|
+
isolated Kubernetes Job; results are then paged back over HTTP. That model has
|
|
106
|
+
consequences worth knowing up front:
|
|
107
|
+
|
|
108
|
+
- **Latency.** Expect seconds, not milliseconds, before the first row. Built
|
|
109
|
+
for analytics and BI, not for tight interactive loops.
|
|
110
|
+
- **Read-only.** No DML, no DDL, no transactions. `commit()` and `rollback()`
|
|
111
|
+
are no-ops.
|
|
112
|
+
- **Use `NullPool`.** A connection holds no server-side state, so pooling buys
|
|
113
|
+
nothing. The driver also never issues a `SELECT 1` health check, because
|
|
114
|
+
that would start a whole Job.
|
|
115
|
+
- **Parameters are rendered client-side.** thunderduck's API accepts SQL only,
|
|
116
|
+
so this driver renders bound parameters into the statement, escaping them
|
|
117
|
+
strictly and refusing types it does not recognise. Always pass untrusted
|
|
118
|
+
values as parameters — never build SQL by string concatenation. The
|
|
119
|
+
paramstyle is `pyformat`, so — as with psycopg2 — a literal percent sign in
|
|
120
|
+
hand-written SQL must be escaped as `%%` when you pass parameters.
|
|
121
|
+
- **Row ceiling.** The server caps how many rows one page returns and how many
|
|
122
|
+
a result set stores. Very large result sets are truncated; aggregate in SQL
|
|
123
|
+
rather than pulling raw rows.
|
|
124
|
+
|
|
125
|
+
## Also available
|
|
126
|
+
|
|
127
|
+
For JVM tools (DBeaver, DataGrip) there is a JDBC driver — see the
|
|
128
|
+
`jdbc-driver/` directory of the thunderduck repository.
|
|
129
|
+
|
|
130
|
+
## Development
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
uv sync --dev
|
|
134
|
+
uv run pytest
|
|
135
|
+
uv run ruff check . && uv run ruff format --check .
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
One gotcha: the committed `uv.lock` resolves SQLAlchemy 2.x, so a plain
|
|
139
|
+
`uv run pytest` only exercises **one** of the two supported majors. Apache
|
|
140
|
+
Superset pins `sqlalchemy<2`, so check that combination too before releasing:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
uv venv --python 3.10 .sa14
|
|
144
|
+
uv pip install --python .sa14 -e . "sqlalchemy<2" pytest
|
|
145
|
+
.sa14/bin/python -c "import sqlalchemy; print('SQLALCHEMY', sqlalchemy.__version__)"
|
|
146
|
+
.sa14/bin/pytest -q
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
CI runs both legs on every push.
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
Apache-2.0
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# thunderduck-sqlalchemy
|
|
2
|
+
|
|
3
|
+
A SQLAlchemy dialect and PEP 249 DBAPI for [thunderduck](https://thunderduck.io).
|
|
4
|
+
Query your lakehouse from anything that speaks SQLAlchemy — Superset, Airflow,
|
|
5
|
+
Dagster, Redash, pandas, Jupyter, Streamlit.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install thunderduck-sqlalchemy
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Works with SQLAlchemy 1.4 and 2.x, on Python 3.10+ — so it installs cleanly
|
|
12
|
+
into Apache Superset (which pins SQLAlchemy 1.4) as well as modern Airflow,
|
|
13
|
+
Dagster and pandas environments.
|
|
14
|
+
|
|
15
|
+
## Connect
|
|
16
|
+
|
|
17
|
+
Create an API token in the thunderduck console (**Settings → API Tokens**; it
|
|
18
|
+
starts with `tdk_` and is shown once), then:
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from sqlalchemy import create_engine, text
|
|
22
|
+
from sqlalchemy.pool import NullPool
|
|
23
|
+
|
|
24
|
+
engine = create_engine(
|
|
25
|
+
"thunderduck://:tdk_your_token@api.thunderduck.io/",
|
|
26
|
+
poolclass=NullPool,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
with engine.connect() as conn:
|
|
30
|
+
for row in conn.execute(text('SELECT * FROM "lego"."public"."lego_sets" LIMIT 10')):
|
|
31
|
+
print(row)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
pandas works directly:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import pandas as pd
|
|
38
|
+
|
|
39
|
+
df = pd.read_sql('SELECT * FROM "thunder_duck_demo"."iris"', engine)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### URL options
|
|
43
|
+
|
|
44
|
+
| Option | Default | Meaning |
|
|
45
|
+
| --- | --- | --- |
|
|
46
|
+
| password / `?token=` | — | Required. Your `tdk_…` API token. |
|
|
47
|
+
| `?ssl=false` | `true` | Use plain HTTP. For in-cluster use against the console-api Service. |
|
|
48
|
+
| `?poll_ms=` | `1000` | How often to poll for completion. |
|
|
49
|
+
| `?timeout_ms=` | `300000` | How long to wait for a query. |
|
|
50
|
+
| `?arraysize=` | `1000` | Rows fetched per page. |
|
|
51
|
+
|
|
52
|
+
## Table names
|
|
53
|
+
|
|
54
|
+
thunderduck names are `catalog.schema.table` (flat-file catalogs) or
|
|
55
|
+
`catalog.table` (Iceberg/Nessie catalogs). SQLAlchemy models two levels, so
|
|
56
|
+
**the schema is the whole dotted prefix**:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from sqlalchemy import MetaData, Table
|
|
60
|
+
|
|
61
|
+
md = MetaData()
|
|
62
|
+
sets = Table("lego_sets", md, schema="lego.public", autoload_with=engine) # 3-level
|
|
63
|
+
iris = Table("iris", md, schema="thunder_duck_demo", autoload_with=engine) # 2-level
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Reflection (`inspect(engine).get_schema_names()` etc.) reads thunderduck's
|
|
67
|
+
catalog API rather than issuing SQL, so browsing schemas is fast and free.
|
|
68
|
+
|
|
69
|
+
## Apache Superset
|
|
70
|
+
|
|
71
|
+
Installing the package is all that is needed — it registers a Superset DB
|
|
72
|
+
engine spec automatically. Add a database of type **Other** with:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
thunderduck://:tdk_your_token@api.thunderduck.io/
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Superset's stop button genuinely cancels the running query.
|
|
79
|
+
|
|
80
|
+
## How it works, and what follows from that
|
|
81
|
+
|
|
82
|
+
Every statement is submitted to thunderduck's REST API, which runs it as an
|
|
83
|
+
isolated Kubernetes Job; results are then paged back over HTTP. That model has
|
|
84
|
+
consequences worth knowing up front:
|
|
85
|
+
|
|
86
|
+
- **Latency.** Expect seconds, not milliseconds, before the first row. Built
|
|
87
|
+
for analytics and BI, not for tight interactive loops.
|
|
88
|
+
- **Read-only.** No DML, no DDL, no transactions. `commit()` and `rollback()`
|
|
89
|
+
are no-ops.
|
|
90
|
+
- **Use `NullPool`.** A connection holds no server-side state, so pooling buys
|
|
91
|
+
nothing. The driver also never issues a `SELECT 1` health check, because
|
|
92
|
+
that would start a whole Job.
|
|
93
|
+
- **Parameters are rendered client-side.** thunderduck's API accepts SQL only,
|
|
94
|
+
so this driver renders bound parameters into the statement, escaping them
|
|
95
|
+
strictly and refusing types it does not recognise. Always pass untrusted
|
|
96
|
+
values as parameters — never build SQL by string concatenation. The
|
|
97
|
+
paramstyle is `pyformat`, so — as with psycopg2 — a literal percent sign in
|
|
98
|
+
hand-written SQL must be escaped as `%%` when you pass parameters.
|
|
99
|
+
- **Row ceiling.** The server caps how many rows one page returns and how many
|
|
100
|
+
a result set stores. Very large result sets are truncated; aggregate in SQL
|
|
101
|
+
rather than pulling raw rows.
|
|
102
|
+
|
|
103
|
+
## Also available
|
|
104
|
+
|
|
105
|
+
For JVM tools (DBeaver, DataGrip) there is a JDBC driver — see the
|
|
106
|
+
`jdbc-driver/` directory of the thunderduck repository.
|
|
107
|
+
|
|
108
|
+
## Development
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
uv sync --dev
|
|
112
|
+
uv run pytest
|
|
113
|
+
uv run ruff check . && uv run ruff format --check .
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
One gotcha: the committed `uv.lock` resolves SQLAlchemy 2.x, so a plain
|
|
117
|
+
`uv run pytest` only exercises **one** of the two supported majors. Apache
|
|
118
|
+
Superset pins `sqlalchemy<2`, so check that combination too before releasing:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
uv venv --python 3.10 .sa14
|
|
122
|
+
uv pip install --python .sa14 -e . "sqlalchemy<2" pytest
|
|
123
|
+
.sa14/bin/python -c "import sqlalchemy; print('SQLALCHEMY', sqlalchemy.__version__)"
|
|
124
|
+
.sa14/bin/pytest -q
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
CI runs both legs on every push.
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
Apache-2.0
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "thunderduck-sqlalchemy"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "SQLAlchemy dialect and DBAPI for thunderduck.io — query your lakehouse from Superset, Airflow, pandas and friends."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
keywords = [
|
|
8
|
+
"sqlalchemy",
|
|
9
|
+
"duckdb",
|
|
10
|
+
"iceberg",
|
|
11
|
+
"lakehouse",
|
|
12
|
+
"thunderduck",
|
|
13
|
+
"superset",
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Topic :: Database",
|
|
22
|
+
"Topic :: Database :: Front-Ends",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"httpx>=0.27.0",
|
|
26
|
+
"sqlalchemy>=1.4",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[[project.authors]]
|
|
30
|
+
name = "YILDIRIM ADIGUZEL"
|
|
31
|
+
email = "yadiguzel@gmail.com"
|
|
32
|
+
|
|
33
|
+
[project.license]
|
|
34
|
+
text = "Apache-2.0"
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://thunderduck.io"
|
|
38
|
+
Source = "https://github.com/212data/thunderduck"
|
|
39
|
+
|
|
40
|
+
[project.entry-points."sqlalchemy.dialects"]
|
|
41
|
+
thunderduck = "thunderduck_sqlalchemy.dialect:ThunderduckDialect"
|
|
42
|
+
"thunderduck.rest" = "thunderduck_sqlalchemy.dialect:ThunderduckDialect"
|
|
43
|
+
|
|
44
|
+
[project.entry-points."superset.db_engine_specs"]
|
|
45
|
+
thunderduck = "thunderduck_sqlalchemy.superset_spec:ThunderduckEngineSpec"
|
|
46
|
+
|
|
47
|
+
[dependency-groups]
|
|
48
|
+
dev = [
|
|
49
|
+
"pytest>=8.0.0",
|
|
50
|
+
"ruff>=0.8.0",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[build-system]
|
|
54
|
+
requires = ["uv_build>=0.12.1,<0.13.0"]
|
|
55
|
+
build-backend = "uv_build"
|
|
56
|
+
|
|
57
|
+
[tool.pytest.ini_options]
|
|
58
|
+
testpaths = ["tests"]
|
|
59
|
+
|
|
60
|
+
[tool.ruff]
|
|
61
|
+
line-length = 100
|
|
62
|
+
target-version = "py310"
|
|
63
|
+
|
|
64
|
+
[tool.ruff.lint]
|
|
65
|
+
select = [
|
|
66
|
+
"E",
|
|
67
|
+
"F",
|
|
68
|
+
"I",
|
|
69
|
+
"UP",
|
|
70
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "thunderduck-sqlalchemy"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "SQLAlchemy dialect and DBAPI for thunderduck.io — query your lakehouse from Superset, Airflow, pandas and friends."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "YILDIRIM ADIGUZEL", email = "yadiguzel@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
license = { text = "Apache-2.0" }
|
|
11
|
+
keywords = ["sqlalchemy", "duckdb", "iceberg", "lakehouse", "thunderduck", "superset"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Programming Language :: Python :: 3.10",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Topic :: Database",
|
|
19
|
+
"Topic :: Database :: Front-Ends",
|
|
20
|
+
]
|
|
21
|
+
# sqlalchemy>=1.4, NOT >=2.0: apache-superset 6.1.0 pins sqlalchemy<2, so a 2.0
|
|
22
|
+
# floor would make this package uninstallable in the Superset image. See the
|
|
23
|
+
# plan's Global Constraints.
|
|
24
|
+
dependencies = [
|
|
25
|
+
"httpx>=0.27.0",
|
|
26
|
+
"sqlalchemy>=1.4",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
# Deliberately NO [project.optional-dependencies]: an `apache-superset` extra
|
|
30
|
+
# cannot be resolved alongside this package (its sqlalchemy<2 pin conflicts),
|
|
31
|
+
# and Superset supplies itself at runtime anyway.
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://thunderduck.io"
|
|
35
|
+
Source = "https://github.com/212data/thunderduck"
|
|
36
|
+
|
|
37
|
+
[project.entry-points."sqlalchemy.dialects"]
|
|
38
|
+
thunderduck = "thunderduck_sqlalchemy.dialect:ThunderduckDialect"
|
|
39
|
+
"thunderduck.rest" = "thunderduck_sqlalchemy.dialect:ThunderduckDialect"
|
|
40
|
+
|
|
41
|
+
[project.entry-points."superset.db_engine_specs"]
|
|
42
|
+
thunderduck = "thunderduck_sqlalchemy.superset_spec:ThunderduckEngineSpec"
|
|
43
|
+
|
|
44
|
+
[dependency-groups]
|
|
45
|
+
dev = [
|
|
46
|
+
"pytest>=8.0.0",
|
|
47
|
+
"ruff>=0.8.0",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[build-system]
|
|
51
|
+
requires = ["uv_build>=0.12.1,<0.13.0"]
|
|
52
|
+
build-backend = "uv_build"
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
testpaths = ["tests"]
|
|
56
|
+
|
|
57
|
+
[tool.ruff]
|
|
58
|
+
line-length = 100
|
|
59
|
+
target-version = "py310"
|
|
60
|
+
|
|
61
|
+
[tool.ruff.lint]
|
|
62
|
+
select = ["E", "F", "I", "UP"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""SQLAlchemy dialect and DBAPI for thunderduck.io.
|
|
2
|
+
|
|
3
|
+
Read-only by design: every statement is submitted to console-api, which runs
|
|
4
|
+
it as a Kubernetes Job, and results are paged back over HTTP. See README.md
|
|
5
|
+
for the connection URL format and the caveats that follow from that model.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .dbapi import connect
|
|
9
|
+
from .dialect import ThunderduckDialect
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
|
|
13
|
+
__all__ = ["__version__", "connect", "ThunderduckDialect"]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Flatten console-api's `/catalog/schema` tree into `(schema, table)` pairs.
|
|
2
|
+
|
|
3
|
+
## Two catalog shapes, one rule
|
|
4
|
+
|
|
5
|
+
thunderduck names are not uniformly three-part. Nessie catalogs are 2-level
|
|
6
|
+
(`catalog.table`) and put their tables directly under the catalog; flat-file
|
|
7
|
+
catalogs are 3-level (`catalog.schema.table`) and put them under named
|
|
8
|
+
namespaces. SQLAlchemy and Superset both model exactly two levels.
|
|
9
|
+
|
|
10
|
+
The rule that reconciles them: **the composite `schema` is the whole dotted
|
|
11
|
+
prefix and the `table` is the leaf.** So `iris` in the nessie catalog
|
|
12
|
+
`thunder_duck_demo` has schema `"thunder_duck_demo"`, while `lego_sets` has
|
|
13
|
+
schema `"lego.public"`. The dialect's identifier preparer later splits that
|
|
14
|
+
prefix back apart and quotes each segment, so both compile correctly.
|
|
15
|
+
|
|
16
|
+
A catalog carrying an `error` (console-api could not introspect it) is skipped
|
|
17
|
+
rather than raised on: one unreachable catalog must not break reflection of
|
|
18
|
+
every other one.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class TableRef:
|
|
29
|
+
schema: str
|
|
30
|
+
table: str
|
|
31
|
+
kind: str = "table"
|
|
32
|
+
columns: list[dict[str, str]] = field(default_factory=list)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def flatten(tree: dict[str, Any] | None) -> list[TableRef]:
|
|
36
|
+
refs: list[TableRef] = []
|
|
37
|
+
for catalog in (tree or {}).get("catalogs") or []:
|
|
38
|
+
if catalog.get("error"):
|
|
39
|
+
continue
|
|
40
|
+
catalog_name = catalog.get("name")
|
|
41
|
+
if not catalog_name:
|
|
42
|
+
continue
|
|
43
|
+
for table in catalog.get("tables") or []:
|
|
44
|
+
ref = _make_ref(catalog_name, table)
|
|
45
|
+
if ref:
|
|
46
|
+
refs.append(ref)
|
|
47
|
+
for namespace in catalog.get("namespaces") or []:
|
|
48
|
+
namespace_name = namespace.get("name")
|
|
49
|
+
if not namespace_name:
|
|
50
|
+
continue
|
|
51
|
+
schema = f"{catalog_name}.{namespace_name}"
|
|
52
|
+
for table in namespace.get("tables") or []:
|
|
53
|
+
ref = _make_ref(schema, table)
|
|
54
|
+
if ref:
|
|
55
|
+
refs.append(ref)
|
|
56
|
+
return refs
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _make_ref(schema: str, table: dict[str, Any]) -> TableRef | None:
|
|
60
|
+
name = table.get("name")
|
|
61
|
+
if not name:
|
|
62
|
+
return None
|
|
63
|
+
kind = str(table.get("kind") or "table").lower()
|
|
64
|
+
columns = [
|
|
65
|
+
{"name": str(c.get("name")), "type": str(c.get("type") or "")}
|
|
66
|
+
for c in (table.get("columns") or [])
|
|
67
|
+
if c.get("name")
|
|
68
|
+
]
|
|
69
|
+
return TableRef(schema=schema, table=str(name), kind=kind, columns=columns)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def split_schema(schema: str) -> list[str]:
|
|
73
|
+
"""Split a composite schema into its segments (`"lego.public"` -> 2)."""
|
|
74
|
+
if not schema:
|
|
75
|
+
return []
|
|
76
|
+
return schema.split(".")
|