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.
Files changed (48) hide show
  1. ducklake_client-0.1.0/.github/workflows/release.yml +74 -0
  2. ducklake_client-0.1.0/.gitignore +13 -0
  3. ducklake_client-0.1.0/.python-version +1 -0
  4. ducklake_client-0.1.0/PKG-INFO +135 -0
  5. ducklake_client-0.1.0/README.md +126 -0
  6. ducklake_client-0.1.0/demo.py +31 -0
  7. ducklake_client-0.1.0/pyproject.toml +21 -0
  8. ducklake_client-0.1.0/src/ducklake_client/__init__.py +63 -0
  9. ducklake_client-0.1.0/src/ducklake_client/_attach.py +34 -0
  10. ducklake_client-0.1.0/src/ducklake_client/_connection.py +95 -0
  11. ducklake_client-0.1.0/src/ducklake_client/client.py +100 -0
  12. ducklake_client-0.1.0/src/ducklake_client/config.py +217 -0
  13. ducklake_client-0.1.0/src/ducklake_client/exceptions.py +17 -0
  14. ducklake_client-0.1.0/src/ducklake_client/modules/__init__.py +11 -0
  15. ducklake_client-0.1.0/src/ducklake_client/modules/base.py +30 -0
  16. ducklake_client-0.1.0/src/ducklake_client/modules/schema.py +23 -0
  17. ducklake_client-0.1.0/src/ducklake_client/modules/table.py +91 -0
  18. ducklake_client-0.1.0/src/ducklake_client/modules/view.py +18 -0
  19. ducklake_client-0.1.0/src/ducklake_client/operations/__init__.py +18 -0
  20. ducklake_client-0.1.0/src/ducklake_client/operations/base.py +57 -0
  21. ducklake_client-0.1.0/src/ducklake_client/operations/schema_create.py +30 -0
  22. ducklake_client-0.1.0/src/ducklake_client/operations/table_comment.py +44 -0
  23. ducklake_client-0.1.0/src/ducklake_client/operations/table_create.py +61 -0
  24. ducklake_client-0.1.0/src/ducklake_client/operations/table_info.py +291 -0
  25. ducklake_client-0.1.0/src/ducklake_client/operations/table_list.py +46 -0
  26. ducklake_client-0.1.0/src/ducklake_client/operations/table_names.py +28 -0
  27. ducklake_client-0.1.0/src/ducklake_client/operations/view_list.py +46 -0
  28. ducklake_client-0.1.0/src/ducklake_client/py.typed +1 -0
  29. ducklake_client-0.1.0/src/ducklake_client/schema.py +187 -0
  30. ducklake_client-0.1.0/src/ducklake_client/templates/__init__.py +1 -0
  31. ducklake_client-0.1.0/src/ducklake_client/templates/schema_create.sql +1 -0
  32. ducklake_client-0.1.0/src/ducklake_client/templates/table_column_comment.sql +1 -0
  33. ducklake_client-0.1.0/src/ducklake_client/templates/table_comment.sql +1 -0
  34. ducklake_client-0.1.0/src/ducklake_client/templates/table_create.sql +3 -0
  35. ducklake_client-0.1.0/src/ducklake_client/templates/table_create_from_csv.sql +2 -0
  36. ducklake_client-0.1.0/src/ducklake_client/templates/table_info_columns.sql +6 -0
  37. ducklake_client-0.1.0/src/ducklake_client/templates/table_info_duckdb_columns.sql +5 -0
  38. ducklake_client-0.1.0/src/ducklake_client/templates/table_info_duckdb_table.sql +5 -0
  39. ducklake_client-0.1.0/src/ducklake_client/templates/table_info_ducklake_metadata.sql +19 -0
  40. ducklake_client-0.1.0/src/ducklake_client/templates/table_info_partition_specs.sql +20 -0
  41. ducklake_client-0.1.0/src/ducklake_client/templates/table_info_row_count.sql +2 -0
  42. ducklake_client-0.1.0/src/ducklake_client/templates/table_info_snapshots.sql +3 -0
  43. ducklake_client-0.1.0/src/ducklake_client/templates/table_info_sort_specs.sql +17 -0
  44. ducklake_client-0.1.0/src/ducklake_client/templates/table_info_summary.sql +1 -0
  45. ducklake_client-0.1.0/src/ducklake_client/templates/table_info_table.sql +5 -0
  46. ducklake_client-0.1.0/src/ducklake_client/templates/table_list.sql +6 -0
  47. ducklake_client-0.1.0/src/ducklake_client/templates/view_list.sql +6 -0
  48. 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,13 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ demo/
10
+ demo.ducklake/
11
+
12
+ # Virtual environments
13
+ .venv
@@ -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()