ducklake-client 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.
- ducklake_client-0.1.0/.github/workflows/release.yml +74 -0
- ducklake_client-0.1.0/.gitignore +13 -0
- ducklake_client-0.1.0/.python-version +1 -0
- ducklake_client-0.1.0/PKG-INFO +135 -0
- ducklake_client-0.1.0/README.md +126 -0
- ducklake_client-0.1.0/demo.py +31 -0
- ducklake_client-0.1.0/pyproject.toml +21 -0
- ducklake_client-0.1.0/src/ducklake_client/__init__.py +63 -0
- ducklake_client-0.1.0/src/ducklake_client/_attach.py +34 -0
- ducklake_client-0.1.0/src/ducklake_client/_connection.py +95 -0
- ducklake_client-0.1.0/src/ducklake_client/client.py +100 -0
- ducklake_client-0.1.0/src/ducklake_client/config.py +217 -0
- ducklake_client-0.1.0/src/ducklake_client/exceptions.py +17 -0
- ducklake_client-0.1.0/src/ducklake_client/modules/__init__.py +11 -0
- ducklake_client-0.1.0/src/ducklake_client/modules/base.py +30 -0
- ducklake_client-0.1.0/src/ducklake_client/modules/schema.py +23 -0
- ducklake_client-0.1.0/src/ducklake_client/modules/table.py +91 -0
- ducklake_client-0.1.0/src/ducklake_client/modules/view.py +18 -0
- ducklake_client-0.1.0/src/ducklake_client/operations/__init__.py +18 -0
- ducklake_client-0.1.0/src/ducklake_client/operations/base.py +57 -0
- ducklake_client-0.1.0/src/ducklake_client/operations/schema_create.py +30 -0
- ducklake_client-0.1.0/src/ducklake_client/operations/table_comment.py +44 -0
- ducklake_client-0.1.0/src/ducklake_client/operations/table_create.py +61 -0
- ducklake_client-0.1.0/src/ducklake_client/operations/table_info.py +291 -0
- ducklake_client-0.1.0/src/ducklake_client/operations/table_list.py +46 -0
- ducklake_client-0.1.0/src/ducklake_client/operations/table_names.py +28 -0
- ducklake_client-0.1.0/src/ducklake_client/operations/view_list.py +46 -0
- ducklake_client-0.1.0/src/ducklake_client/py.typed +1 -0
- ducklake_client-0.1.0/src/ducklake_client/schema.py +187 -0
- ducklake_client-0.1.0/src/ducklake_client/templates/__init__.py +1 -0
- ducklake_client-0.1.0/src/ducklake_client/templates/schema_create.sql +1 -0
- ducklake_client-0.1.0/src/ducklake_client/templates/table_column_comment.sql +1 -0
- ducklake_client-0.1.0/src/ducklake_client/templates/table_comment.sql +1 -0
- ducklake_client-0.1.0/src/ducklake_client/templates/table_create.sql +3 -0
- ducklake_client-0.1.0/src/ducklake_client/templates/table_create_from_csv.sql +2 -0
- ducklake_client-0.1.0/src/ducklake_client/templates/table_info_columns.sql +6 -0
- ducklake_client-0.1.0/src/ducklake_client/templates/table_info_duckdb_columns.sql +5 -0
- ducklake_client-0.1.0/src/ducklake_client/templates/table_info_duckdb_table.sql +5 -0
- ducklake_client-0.1.0/src/ducklake_client/templates/table_info_ducklake_metadata.sql +19 -0
- ducklake_client-0.1.0/src/ducklake_client/templates/table_info_partition_specs.sql +20 -0
- ducklake_client-0.1.0/src/ducklake_client/templates/table_info_row_count.sql +2 -0
- ducklake_client-0.1.0/src/ducklake_client/templates/table_info_snapshots.sql +3 -0
- ducklake_client-0.1.0/src/ducklake_client/templates/table_info_sort_specs.sql +17 -0
- ducklake_client-0.1.0/src/ducklake_client/templates/table_info_summary.sql +1 -0
- ducklake_client-0.1.0/src/ducklake_client/templates/table_info_table.sql +5 -0
- ducklake_client-0.1.0/src/ducklake_client/templates/table_list.sql +6 -0
- ducklake_client-0.1.0/src/ducklake_client/templates/view_list.sql +6 -0
- ducklake_client-0.1.0/uv.lock +29 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
source:
|
|
7
|
+
description: "Source ref to release from"
|
|
8
|
+
required: true
|
|
9
|
+
default: "main"
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: write
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
release:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- name: Checkout source
|
|
20
|
+
uses: actions/checkout@v4
|
|
21
|
+
with:
|
|
22
|
+
ref: ${{ inputs.source }}
|
|
23
|
+
fetch-depth: 0
|
|
24
|
+
|
|
25
|
+
- name: Set up Python
|
|
26
|
+
uses: actions/setup-python@v5
|
|
27
|
+
with:
|
|
28
|
+
python-version: "3.14"
|
|
29
|
+
|
|
30
|
+
- name: Resolve release version
|
|
31
|
+
id: release
|
|
32
|
+
shell: bash
|
|
33
|
+
run: |
|
|
34
|
+
version="$(python - <<'PY'
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
import tomllib
|
|
37
|
+
|
|
38
|
+
pyproject = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))
|
|
39
|
+
print(pyproject["project"]["version"])
|
|
40
|
+
PY
|
|
41
|
+
)"
|
|
42
|
+
branch="release/v${version}"
|
|
43
|
+
echo "version=${version}" >> "${GITHUB_OUTPUT}"
|
|
44
|
+
echo "branch=${branch}" >> "${GITHUB_OUTPUT}"
|
|
45
|
+
echo "Release version: ${version}"
|
|
46
|
+
echo "Release branch: ${branch}"
|
|
47
|
+
|
|
48
|
+
- name: Ensure release branch does not exist
|
|
49
|
+
shell: bash
|
|
50
|
+
run: |
|
|
51
|
+
branch="${{ steps.release.outputs.branch }}"
|
|
52
|
+
if git ls-remote --exit-code --heads origin "${branch}" >/dev/null 2>&1; then
|
|
53
|
+
echo "::error::Release branch ${branch} already exists"
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
- name: Create release branch
|
|
58
|
+
shell: bash
|
|
59
|
+
run: |
|
|
60
|
+
branch="${{ steps.release.outputs.branch }}"
|
|
61
|
+
git switch -c "${branch}"
|
|
62
|
+
git push origin "HEAD:refs/heads/${branch}"
|
|
63
|
+
|
|
64
|
+
- name: Build package
|
|
65
|
+
shell: bash
|
|
66
|
+
run: |
|
|
67
|
+
python -m pip install --upgrade build
|
|
68
|
+
python -m build
|
|
69
|
+
|
|
70
|
+
- name: Publish package to PyPI
|
|
71
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
72
|
+
with:
|
|
73
|
+
user: __token__
|
|
74
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ducklake-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight wrapper over DuckDB with convenience helpers for DuckLake.
|
|
5
|
+
Author-email: Ekku Leivonen <ekku.leivonen@gmail.com>
|
|
6
|
+
Requires-Python: >=3.14
|
|
7
|
+
Requires-Dist: duckdb
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# ducklake-client
|
|
11
|
+
|
|
12
|
+
Lightweight Python helpers for opening DuckLake connections through DuckDB.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install ducklake-client
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Open a DuckLake connection
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from ducklake_client import ColumnDef, DiskStorage, DuckDBCatalog, DuckLake
|
|
24
|
+
|
|
25
|
+
lake = DuckLake(
|
|
26
|
+
catalog=DuckDBCatalog("metadata.ducklake"),
|
|
27
|
+
storage=DiskStorage("data"),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
lake.schema.create("main")
|
|
32
|
+
lake.table.create(
|
|
33
|
+
"items",
|
|
34
|
+
id=ColumnDef("INTEGER", nullable=False),
|
|
35
|
+
name=ColumnDef("VARCHAR"),
|
|
36
|
+
)
|
|
37
|
+
rows = lake.connection.sql("SELECT * FROM lake.main.items").fetchall()
|
|
38
|
+
finally:
|
|
39
|
+
lake.close()
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`DuckLake` opens the underlying DuckDB connection lazily on first use. The client installs and loads the DuckDB `ducklake` and `parquet` extensions, attaches the catalog as `lake`, and exposes the native DuckDB connection through `connection`.
|
|
43
|
+
|
|
44
|
+
## Context manager usage
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from ducklake_client import DiskStorage, DuckDBCatalog, DuckLake
|
|
48
|
+
|
|
49
|
+
with DuckLake(
|
|
50
|
+
catalog=DuckDBCatalog("metadata.ducklake"),
|
|
51
|
+
storage=DiskStorage("data"),
|
|
52
|
+
) as lake:
|
|
53
|
+
lake.connection.execute("CREATE TABLE IF NOT EXISTS lake.main.events (id INTEGER)")
|
|
54
|
+
lake.connection.execute("INSERT INTO lake.main.events VALUES (?)", [1])
|
|
55
|
+
print(lake.connection.sql("SELECT count(*) FROM lake.main.events").fetchone())
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Modules
|
|
59
|
+
|
|
60
|
+
DuckLake-specific helpers are grouped into modules. Native DuckDB behavior stays on `lake.connection`.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from ducklake_client import ColumnDef, DiskStorage, DuckDBCatalog, DuckLake
|
|
64
|
+
|
|
65
|
+
with DuckLake(
|
|
66
|
+
catalog=DuckDBCatalog("metadata.ducklake"),
|
|
67
|
+
storage=DiskStorage("data"),
|
|
68
|
+
) as lake:
|
|
69
|
+
lake.schema.create("main")
|
|
70
|
+
lake.table.create_from_csv(
|
|
71
|
+
"nl_train_stations",
|
|
72
|
+
"https://blobs.duckdb.org/nl_stations.csv",
|
|
73
|
+
)
|
|
74
|
+
lake.table.comment("nl_train_stations", "Dutch railway stations")
|
|
75
|
+
lake.table.comment(
|
|
76
|
+
"nl_train_stations",
|
|
77
|
+
"Full station name",
|
|
78
|
+
column_name="name_long",
|
|
79
|
+
)
|
|
80
|
+
tables = lake.table.list()
|
|
81
|
+
views = lake.view.list()
|
|
82
|
+
info = lake.table.info("nl_train_stations")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Transactions
|
|
86
|
+
|
|
87
|
+
Use `transaction()` to automatically begin, commit, or roll back a block on the native DuckDB connection.
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from ducklake_client import ColumnDef, DiskStorage, DuckDBCatalog, DuckLake
|
|
91
|
+
|
|
92
|
+
with DuckLake(
|
|
93
|
+
catalog=DuckDBCatalog("metadata.ducklake"),
|
|
94
|
+
storage=DiskStorage("data"),
|
|
95
|
+
) as lake:
|
|
96
|
+
with lake.transaction():
|
|
97
|
+
lake.schema.create("main")
|
|
98
|
+
lake.table.create(
|
|
99
|
+
"items",
|
|
100
|
+
id=ColumnDef("INTEGER", nullable=False),
|
|
101
|
+
name=ColumnDef("VARCHAR"),
|
|
102
|
+
)
|
|
103
|
+
lake.connection.execute("INSERT INTO lake.main.items VALUES (?, ?)", [1, "example"])
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Configuration
|
|
107
|
+
|
|
108
|
+
`DuckLake` requires explicit catalog and storage config objects:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from ducklake_client import DiskStorage, DuckDBCatalog, DuckLake
|
|
112
|
+
|
|
113
|
+
lake = DuckLake(
|
|
114
|
+
catalog=DuckDBCatalog("metadata.ducklake"),
|
|
115
|
+
storage=DiskStorage("data"),
|
|
116
|
+
)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
You can pass DuckDB runtime settings with `DuckDBConfig`:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from ducklake_client import DiskStorage, DuckDBConfig, DuckDBCatalog, DuckLake
|
|
123
|
+
|
|
124
|
+
lake = DuckLake(
|
|
125
|
+
catalog=DuckDBCatalog("metadata.ducklake"),
|
|
126
|
+
storage=DiskStorage("data"),
|
|
127
|
+
duckdb=DuckDBConfig(
|
|
128
|
+
database=":memory:",
|
|
129
|
+
threads=4,
|
|
130
|
+
memory_limit="2GB",
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Catalogs can be `DuckDBCatalog`, `SqliteCatalog`, or `PostgresCatalog`. Storage can be `DiskStorage` or `S3Storage`.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# ducklake-client
|
|
2
|
+
|
|
3
|
+
Lightweight Python helpers for opening DuckLake connections through DuckDB.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install ducklake-client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Open a DuckLake connection
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from ducklake_client import ColumnDef, DiskStorage, DuckDBCatalog, DuckLake
|
|
15
|
+
|
|
16
|
+
lake = DuckLake(
|
|
17
|
+
catalog=DuckDBCatalog("metadata.ducklake"),
|
|
18
|
+
storage=DiskStorage("data"),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
lake.schema.create("main")
|
|
23
|
+
lake.table.create(
|
|
24
|
+
"items",
|
|
25
|
+
id=ColumnDef("INTEGER", nullable=False),
|
|
26
|
+
name=ColumnDef("VARCHAR"),
|
|
27
|
+
)
|
|
28
|
+
rows = lake.connection.sql("SELECT * FROM lake.main.items").fetchall()
|
|
29
|
+
finally:
|
|
30
|
+
lake.close()
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`DuckLake` opens the underlying DuckDB connection lazily on first use. The client installs and loads the DuckDB `ducklake` and `parquet` extensions, attaches the catalog as `lake`, and exposes the native DuckDB connection through `connection`.
|
|
34
|
+
|
|
35
|
+
## Context manager usage
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from ducklake_client import DiskStorage, DuckDBCatalog, DuckLake
|
|
39
|
+
|
|
40
|
+
with DuckLake(
|
|
41
|
+
catalog=DuckDBCatalog("metadata.ducklake"),
|
|
42
|
+
storage=DiskStorage("data"),
|
|
43
|
+
) as lake:
|
|
44
|
+
lake.connection.execute("CREATE TABLE IF NOT EXISTS lake.main.events (id INTEGER)")
|
|
45
|
+
lake.connection.execute("INSERT INTO lake.main.events VALUES (?)", [1])
|
|
46
|
+
print(lake.connection.sql("SELECT count(*) FROM lake.main.events").fetchone())
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Modules
|
|
50
|
+
|
|
51
|
+
DuckLake-specific helpers are grouped into modules. Native DuckDB behavior stays on `lake.connection`.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from ducklake_client import ColumnDef, DiskStorage, DuckDBCatalog, DuckLake
|
|
55
|
+
|
|
56
|
+
with DuckLake(
|
|
57
|
+
catalog=DuckDBCatalog("metadata.ducklake"),
|
|
58
|
+
storage=DiskStorage("data"),
|
|
59
|
+
) as lake:
|
|
60
|
+
lake.schema.create("main")
|
|
61
|
+
lake.table.create_from_csv(
|
|
62
|
+
"nl_train_stations",
|
|
63
|
+
"https://blobs.duckdb.org/nl_stations.csv",
|
|
64
|
+
)
|
|
65
|
+
lake.table.comment("nl_train_stations", "Dutch railway stations")
|
|
66
|
+
lake.table.comment(
|
|
67
|
+
"nl_train_stations",
|
|
68
|
+
"Full station name",
|
|
69
|
+
column_name="name_long",
|
|
70
|
+
)
|
|
71
|
+
tables = lake.table.list()
|
|
72
|
+
views = lake.view.list()
|
|
73
|
+
info = lake.table.info("nl_train_stations")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Transactions
|
|
77
|
+
|
|
78
|
+
Use `transaction()` to automatically begin, commit, or roll back a block on the native DuckDB connection.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from ducklake_client import ColumnDef, DiskStorage, DuckDBCatalog, DuckLake
|
|
82
|
+
|
|
83
|
+
with DuckLake(
|
|
84
|
+
catalog=DuckDBCatalog("metadata.ducklake"),
|
|
85
|
+
storage=DiskStorage("data"),
|
|
86
|
+
) as lake:
|
|
87
|
+
with lake.transaction():
|
|
88
|
+
lake.schema.create("main")
|
|
89
|
+
lake.table.create(
|
|
90
|
+
"items",
|
|
91
|
+
id=ColumnDef("INTEGER", nullable=False),
|
|
92
|
+
name=ColumnDef("VARCHAR"),
|
|
93
|
+
)
|
|
94
|
+
lake.connection.execute("INSERT INTO lake.main.items VALUES (?, ?)", [1, "example"])
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Configuration
|
|
98
|
+
|
|
99
|
+
`DuckLake` requires explicit catalog and storage config objects:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from ducklake_client import DiskStorage, DuckDBCatalog, DuckLake
|
|
103
|
+
|
|
104
|
+
lake = DuckLake(
|
|
105
|
+
catalog=DuckDBCatalog("metadata.ducklake"),
|
|
106
|
+
storage=DiskStorage("data"),
|
|
107
|
+
)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
You can pass DuckDB runtime settings with `DuckDBConfig`:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from ducklake_client import DiskStorage, DuckDBConfig, DuckDBCatalog, DuckLake
|
|
114
|
+
|
|
115
|
+
lake = DuckLake(
|
|
116
|
+
catalog=DuckDBCatalog("metadata.ducklake"),
|
|
117
|
+
storage=DiskStorage("data"),
|
|
118
|
+
duckdb=DuckDBConfig(
|
|
119
|
+
database=":memory:",
|
|
120
|
+
threads=4,
|
|
121
|
+
memory_limit="2GB",
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Catalogs can be `DuckDBCatalog`, `SqliteCatalog`, or `PostgresCatalog`. Storage can be `DiskStorage` or `S3Storage`.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Examples for ducklake-client."""
|
|
2
|
+
|
|
3
|
+
import pprint
|
|
4
|
+
from ducklake_client import DiskStorage, DuckDBCatalog, DuckLake
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main() -> None:
|
|
8
|
+
with DuckLake(
|
|
9
|
+
catalog=DuckDBCatalog("demo.ducklake"),
|
|
10
|
+
storage=DiskStorage("demo"),
|
|
11
|
+
) as lake:
|
|
12
|
+
with lake.transaction():
|
|
13
|
+
lake.schema.create("main")
|
|
14
|
+
lake.table.create_from_csv(
|
|
15
|
+
"nl_train_stations",
|
|
16
|
+
"https://blobs.duckdb.org/nl_stations.csv",
|
|
17
|
+
)
|
|
18
|
+
lake.table.comment("nl_train_stations", "Dutch railway stations")
|
|
19
|
+
lake.table.comment(
|
|
20
|
+
"nl_train_stations",
|
|
21
|
+
"Station ID",
|
|
22
|
+
column_name="id",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# rows = lake.connection.sql("SELECT * FROM lake.main.nl_train_stations LIMIT 5").fetchall()
|
|
26
|
+
info = lake.table.info("nl_train_stations")
|
|
27
|
+
pprint.pprint(info.columns[0], indent=2)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if __name__ == "__main__":
|
|
31
|
+
main()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ducklake-client"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Lightweight wrapper over DuckDB with convenience helpers for DuckLake."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"duckdb",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Ekku Leivonen", email = "ekku.leivonen@gmail.com" }
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["hatchling"]
|
|
18
|
+
build-backend = "hatchling.build"
|
|
19
|
+
|
|
20
|
+
[tool.hatch.build.targets.wheel]
|
|
21
|
+
packages = ["src/ducklake_client"]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Lightweight Python helpers for DuckLake connections."""
|
|
2
|
+
|
|
3
|
+
from ducklake_client.config import (
|
|
4
|
+
CatalogConfig,
|
|
5
|
+
DiskStorage,
|
|
6
|
+
DuckDBCatalog,
|
|
7
|
+
DuckDBConfig,
|
|
8
|
+
PostgresCatalog,
|
|
9
|
+
S3Storage,
|
|
10
|
+
SqliteCatalog,
|
|
11
|
+
StorageConfig,
|
|
12
|
+
)
|
|
13
|
+
from ducklake_client.exceptions import (
|
|
14
|
+
DuckLakeConfigError,
|
|
15
|
+
DuckLakeConnectionError,
|
|
16
|
+
DuckLakeError,
|
|
17
|
+
DuckLakeQueryError,
|
|
18
|
+
)
|
|
19
|
+
from ducklake_client.client import DuckLake
|
|
20
|
+
from ducklake_client.modules import SchemaModule, TableModule, ViewModule
|
|
21
|
+
from ducklake_client.schema import (
|
|
22
|
+
ColumnDataType,
|
|
23
|
+
ColumnDef,
|
|
24
|
+
DuckLakeTableMetadata,
|
|
25
|
+
TableColumnSummary,
|
|
26
|
+
TableInfo,
|
|
27
|
+
TableInfoColumn,
|
|
28
|
+
TableListing,
|
|
29
|
+
TablePartitionSpec,
|
|
30
|
+
TableSnapshotInfo,
|
|
31
|
+
TableSortSpec,
|
|
32
|
+
ViewListing,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"CatalogConfig",
|
|
37
|
+
"ColumnDataType",
|
|
38
|
+
"ColumnDef",
|
|
39
|
+
"DuckDBCatalog",
|
|
40
|
+
"DuckDBConfig",
|
|
41
|
+
"DuckLake",
|
|
42
|
+
"DuckLakeConfigError",
|
|
43
|
+
"DuckLakeConnectionError",
|
|
44
|
+
"DuckLakeError",
|
|
45
|
+
"DuckLakeQueryError",
|
|
46
|
+
"DuckLakeTableMetadata",
|
|
47
|
+
"DiskStorage",
|
|
48
|
+
"PostgresCatalog",
|
|
49
|
+
"S3Storage",
|
|
50
|
+
"SchemaModule",
|
|
51
|
+
"SqliteCatalog",
|
|
52
|
+
"StorageConfig",
|
|
53
|
+
"TableColumnSummary",
|
|
54
|
+
"TableInfo",
|
|
55
|
+
"TableInfoColumn",
|
|
56
|
+
"TableListing",
|
|
57
|
+
"TableModule",
|
|
58
|
+
"TablePartitionSpec",
|
|
59
|
+
"TableSnapshotInfo",
|
|
60
|
+
"TableSortSpec",
|
|
61
|
+
"ViewModule",
|
|
62
|
+
"ViewListing",
|
|
63
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Internal helpers for DuckLake ATTACH SQL generation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
|
|
7
|
+
from ducklake_client.config import CatalogConfig, StorageConfig, quote_identifier, quote_literal
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build_attach_sql(
|
|
11
|
+
*,
|
|
12
|
+
catalog: CatalogConfig,
|
|
13
|
+
storage: StorageConfig,
|
|
14
|
+
alias: str,
|
|
15
|
+
attach_options: Mapping[str, object] | None = None,
|
|
16
|
+
) -> str:
|
|
17
|
+
options: dict[str, object] = {"DATA_PATH": storage.data_path()}
|
|
18
|
+
if attach_options:
|
|
19
|
+
options.update(attach_options)
|
|
20
|
+
rendered_options = ", ".join(
|
|
21
|
+
f"{key.upper()} {_format_attach_value(value)}" for key, value in options.items()
|
|
22
|
+
)
|
|
23
|
+
return (
|
|
24
|
+
f"ATTACH {quote_literal(catalog.attach_uri())} "
|
|
25
|
+
f"AS {quote_identifier(alias)} ({rendered_options})"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _format_attach_value(value: object) -> str:
|
|
30
|
+
if isinstance(value, bool):
|
|
31
|
+
return "true" if value else "false"
|
|
32
|
+
if isinstance(value, int | float):
|
|
33
|
+
return str(value)
|
|
34
|
+
return quote_literal(value)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Lazy DuckDB connection management for DuckLake."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import TYPE_CHECKING, cast
|
|
9
|
+
|
|
10
|
+
from ducklake_client._attach import build_attach_sql
|
|
11
|
+
from ducklake_client.config import (
|
|
12
|
+
CatalogConfig,
|
|
13
|
+
DuckDBConfig,
|
|
14
|
+
DuckDBSettingValue,
|
|
15
|
+
StorageConfig,
|
|
16
|
+
quote_literal,
|
|
17
|
+
)
|
|
18
|
+
from ducklake_client.exceptions import DuckLakeConnectionError
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
import duckdb
|
|
22
|
+
|
|
23
|
+
_SETTING_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class ConnectionManager:
|
|
28
|
+
catalog: CatalogConfig
|
|
29
|
+
storage: StorageConfig
|
|
30
|
+
alias: str
|
|
31
|
+
duckdb: DuckDBConfig = field(default_factory=DuckDBConfig)
|
|
32
|
+
attach_options: Mapping[str, object] | None = None
|
|
33
|
+
_connection: duckdb.DuckDBPyConnection | None = field(default=None, init=False, repr=False)
|
|
34
|
+
|
|
35
|
+
def get(self) -> duckdb.DuckDBPyConnection:
|
|
36
|
+
if self._connection is None:
|
|
37
|
+
self._connection = self._connect()
|
|
38
|
+
return self._connection
|
|
39
|
+
|
|
40
|
+
def close(self) -> None:
|
|
41
|
+
if self._connection is not None:
|
|
42
|
+
self._connection.close()
|
|
43
|
+
self._connection = None
|
|
44
|
+
|
|
45
|
+
def _connect(self) -> duckdb.DuckDBPyConnection:
|
|
46
|
+
try:
|
|
47
|
+
import duckdb
|
|
48
|
+
|
|
49
|
+
config = cast(dict[str, str | bool | int | float | list[str]], dict(self.duckdb.config))
|
|
50
|
+
conn = (
|
|
51
|
+
duckdb.connect(str(self.duckdb.database), config=config)
|
|
52
|
+
if config
|
|
53
|
+
else duckdb.connect(str(self.duckdb.database))
|
|
54
|
+
)
|
|
55
|
+
for name, value in self.duckdb.runtime_settings().items():
|
|
56
|
+
conn.execute(_setting_sql(name, value))
|
|
57
|
+
for extension in self._required_extensions():
|
|
58
|
+
if self.duckdb.install_extensions:
|
|
59
|
+
conn.install_extension(extension)
|
|
60
|
+
conn.load_extension(extension)
|
|
61
|
+
for statement in self.storage.setup_statements(secret_name=f"{self.alias}_storage"):
|
|
62
|
+
conn.execute(statement)
|
|
63
|
+
conn.execute(
|
|
64
|
+
build_attach_sql(
|
|
65
|
+
catalog=self.catalog,
|
|
66
|
+
storage=self.storage,
|
|
67
|
+
alias=self.alias,
|
|
68
|
+
attach_options=self.attach_options,
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
return conn
|
|
72
|
+
except Exception as exc:
|
|
73
|
+
raise DuckLakeConnectionError("failed to initialize DuckLake connection") from exc
|
|
74
|
+
|
|
75
|
+
def _required_extensions(self) -> tuple[str, ...]:
|
|
76
|
+
names = [
|
|
77
|
+
"ducklake",
|
|
78
|
+
"parquet",
|
|
79
|
+
*self.catalog.required_extensions(),
|
|
80
|
+
*self.storage.required_extensions(),
|
|
81
|
+
*self.duckdb.extensions,
|
|
82
|
+
]
|
|
83
|
+
return tuple(dict.fromkeys(names))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _setting_sql(name: str, value: DuckDBSettingValue) -> str:
|
|
87
|
+
if not _SETTING_NAME.fullmatch(name):
|
|
88
|
+
raise DuckLakeConnectionError(f"invalid DuckDB setting name: {name!r}")
|
|
89
|
+
if isinstance(value, bool):
|
|
90
|
+
rendered_value = "true" if value else "false"
|
|
91
|
+
elif isinstance(value, int | float):
|
|
92
|
+
rendered_value = str(value)
|
|
93
|
+
else:
|
|
94
|
+
rendered_value = quote_literal(value)
|
|
95
|
+
return f"SET {name} = {rendered_value}"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Public DuckLake client entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator, Mapping
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from functools import cached_property
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from ducklake_client._connection import ConnectionManager
|
|
11
|
+
from ducklake_client.config import (
|
|
12
|
+
CatalogConfig,
|
|
13
|
+
CatalogInput,
|
|
14
|
+
DuckDBConfig,
|
|
15
|
+
StorageConfig,
|
|
16
|
+
StorageInput,
|
|
17
|
+
)
|
|
18
|
+
from ducklake_client.modules.schema import SchemaModule
|
|
19
|
+
from ducklake_client.modules.table import TableModule
|
|
20
|
+
from ducklake_client.modules.view import ViewModule
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
import duckdb
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DuckLake:
|
|
27
|
+
"""A lazy DuckLake connection wrapper."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
*,
|
|
32
|
+
catalog: CatalogInput,
|
|
33
|
+
storage: StorageInput,
|
|
34
|
+
alias: str = "lake",
|
|
35
|
+
duckdb: DuckDBConfig | None = None,
|
|
36
|
+
attach_options: Mapping[str, object] | None = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
if not isinstance(catalog, CatalogConfig):
|
|
39
|
+
raise TypeError("catalog must be a DuckDBCatalog, PostgresCatalog, or SqliteCatalog")
|
|
40
|
+
if not isinstance(storage, StorageConfig):
|
|
41
|
+
raise TypeError("storage must be a DiskStorage or S3Storage")
|
|
42
|
+
|
|
43
|
+
self.alias = alias
|
|
44
|
+
self._manager = ConnectionManager(
|
|
45
|
+
catalog=catalog,
|
|
46
|
+
storage=storage,
|
|
47
|
+
alias=alias,
|
|
48
|
+
duckdb=duckdb or DuckDBConfig(),
|
|
49
|
+
attach_options=attach_options,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def connection(self) -> duckdb.DuckDBPyConnection:
|
|
54
|
+
return self._manager.get()
|
|
55
|
+
|
|
56
|
+
@cached_property
|
|
57
|
+
def schema(self) -> SchemaModule:
|
|
58
|
+
return SchemaModule(self)
|
|
59
|
+
|
|
60
|
+
@cached_property
|
|
61
|
+
def table(self) -> TableModule:
|
|
62
|
+
return TableModule(self)
|
|
63
|
+
|
|
64
|
+
@cached_property
|
|
65
|
+
def view(self) -> ViewModule:
|
|
66
|
+
return ViewModule(self)
|
|
67
|
+
|
|
68
|
+
@contextmanager
|
|
69
|
+
def transaction(self) -> Iterator[DuckLake]:
|
|
70
|
+
"""Run a block inside a DuckDB transaction on this lake's connection."""
|
|
71
|
+
|
|
72
|
+
connection = self.connection
|
|
73
|
+
connection.begin()
|
|
74
|
+
try:
|
|
75
|
+
yield self
|
|
76
|
+
except BaseException:
|
|
77
|
+
try:
|
|
78
|
+
connection.rollback()
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
raise
|
|
82
|
+
else:
|
|
83
|
+
try:
|
|
84
|
+
connection.commit()
|
|
85
|
+
except Exception:
|
|
86
|
+
try:
|
|
87
|
+
connection.rollback()
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
90
|
+
raise
|
|
91
|
+
|
|
92
|
+
def close(self) -> None:
|
|
93
|
+
self._manager.close()
|
|
94
|
+
|
|
95
|
+
def __enter__(self) -> DuckLake:
|
|
96
|
+
self.connection
|
|
97
|
+
return self
|
|
98
|
+
|
|
99
|
+
def __exit__(self, exc_type: object, exc: object, traceback: object) -> None:
|
|
100
|
+
self.close()
|