tigrbl_engine_duckdb 0.1.1.dev2__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.
- tigrbl_engine_duckdb-0.1.1.dev2/.gitignore +38 -0
- tigrbl_engine_duckdb-0.1.1.dev2/LICENSE +9 -0
- tigrbl_engine_duckdb-0.1.1.dev2/PKG-INFO +56 -0
- tigrbl_engine_duckdb-0.1.1.dev2/README.md +28 -0
- tigrbl_engine_duckdb-0.1.1.dev2/distout/.gitignore +1 -0
- tigrbl_engine_duckdb-0.1.1.dev2/pyproject.toml +48 -0
- tigrbl_engine_duckdb-0.1.1.dev2/src/tigrbl_engine_duckdb/__init__.py +12 -0
- tigrbl_engine_duckdb-0.1.1.dev2/src/tigrbl_engine_duckdb/duck_builder.py +56 -0
- tigrbl_engine_duckdb-0.1.1.dev2/src/tigrbl_engine_duckdb/duck_session.py +121 -0
- tigrbl_engine_duckdb-0.1.1.dev2/src/tigrbl_engine_duckdb/plugin.py +10 -0
- tigrbl_engine_duckdb-0.1.1.dev2/tests/test_smoke.py +8 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
|
|
2
|
+
.spyproject
|
|
3
|
+
.spyder
|
|
4
|
+
*.pyc
|
|
5
|
+
build
|
|
6
|
+
dist
|
|
7
|
+
*.egg-info
|
|
8
|
+
*.txt
|
|
9
|
+
env
|
|
10
|
+
.env
|
|
11
|
+
.env.*
|
|
12
|
+
/.vs
|
|
13
|
+
.pyirc
|
|
14
|
+
.venv/
|
|
15
|
+
.ipynb_checkpoints/
|
|
16
|
+
.env
|
|
17
|
+
.DS_STORE
|
|
18
|
+
/combined
|
|
19
|
+
.venv_core*
|
|
20
|
+
*.obj
|
|
21
|
+
pytest_results.json
|
|
22
|
+
pkgs/community/swarmauri_vectorstore_annoy/test_annoy.ann
|
|
23
|
+
/pkgs/standards/ptree_dag/pkgs
|
|
24
|
+
*secrets/*
|
|
25
|
+
peagen_artifacts/
|
|
26
|
+
pkgs/standards/peagen/peagen.zip
|
|
27
|
+
/pkgs/uv.lock
|
|
28
|
+
pkgs/standards/peagen/.pymon
|
|
29
|
+
*.pem
|
|
30
|
+
gateway.db
|
|
31
|
+
kms.db
|
|
32
|
+
*.asc
|
|
33
|
+
*.so
|
|
34
|
+
*.db
|
|
35
|
+
target/
|
|
36
|
+
!.gitkeep # keep the empty dir in repo
|
|
37
|
+
pkgs/experimental/swarmakit/libs/svelte/.vscode/extensions.json
|
|
38
|
+
node_modules/
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tigrbl_engine_duckdb
|
|
3
|
+
Version: 0.1.1.dev2
|
|
4
|
+
Summary: DuckDB engine extension for Tigrbl (optional plugin).
|
|
5
|
+
Project-URL: Homepage, https://github.com/swarmauri/swarmauri-sdk
|
|
6
|
+
Project-URL: Repository, https://github.com/swarmauri/swarmauri-sdk/tree/master/pkgs/experimental/tigrbl_engine_duckdb
|
|
7
|
+
Author-email: Jacob Stewart <jacob@swarmauri.com>
|
|
8
|
+
License: Apache License
|
|
9
|
+
Version 2.0, January 2004
|
|
10
|
+
http://www.apache.org/licenses/
|
|
11
|
+
|
|
12
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
13
|
+
|
|
14
|
+
1. Definitions.
|
|
15
|
+
...
|
|
16
|
+
END OF TERMS AND CONDITIONS
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Keywords: experimental,tigrbl,tigrbl_engine_duckdb
|
|
19
|
+
Classifier: Development Status :: 1 - Planning
|
|
20
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Requires-Python: <3.13,>=3.10
|
|
25
|
+
Requires-Dist: duckdb>=1.0.0
|
|
26
|
+
Requires-Dist: tigrbl>=0.3.0.dev4
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# tigrbl_engine_duckdb
|
|
30
|
+
|
|
31
|
+
DuckDB engine extension for **Tigrbl**. This package registers the `duckdb`
|
|
32
|
+
engine kind with Tigrbl’s engine registry via entry points.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install tigrbl_engine_duckdb
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Use
|
|
41
|
+
|
|
42
|
+
After installing, you can bind DuckDB using `engine_ctx`:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from tigrbl.engine.decorators import engine_ctx
|
|
46
|
+
from tigrbl.session.decorators import session_ctx
|
|
47
|
+
|
|
48
|
+
@engine_ctx({"kind": "duckdb", "path": "./data/app.duckdb",
|
|
49
|
+
"pragmas": {"memory_limit": "2GB"}})
|
|
50
|
+
@session_ctx({"isolation": "repeatable_read"})
|
|
51
|
+
class AnalyticsAPI:
|
|
52
|
+
pass
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
No import of this package is required in your app; Tigrbl auto-loads the
|
|
56
|
+
plugin via entry points on import.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# tigrbl_engine_duckdb
|
|
2
|
+
|
|
3
|
+
DuckDB engine extension for **Tigrbl**. This package registers the `duckdb`
|
|
4
|
+
engine kind with Tigrbl’s engine registry via entry points.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install tigrbl_engine_duckdb
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Use
|
|
13
|
+
|
|
14
|
+
After installing, you can bind DuckDB using `engine_ctx`:
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from tigrbl.engine.decorators import engine_ctx
|
|
18
|
+
from tigrbl.session.decorators import session_ctx
|
|
19
|
+
|
|
20
|
+
@engine_ctx({"kind": "duckdb", "path": "./data/app.duckdb",
|
|
21
|
+
"pragmas": {"memory_limit": "2GB"}})
|
|
22
|
+
@session_ctx({"isolation": "repeatable_read"})
|
|
23
|
+
class AnalyticsAPI:
|
|
24
|
+
pass
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
No import of this package is required in your app; Tigrbl auto-loads the
|
|
28
|
+
plugin via entry points on import.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.18.0"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tigrbl_engine_duckdb"
|
|
7
|
+
version = "0.1.1.dev2"
|
|
8
|
+
description = "DuckDB engine extension for Tigrbl (optional plugin)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
authors = [{ name = "Jacob Stewart", email = "jacob@swarmauri.com" }]
|
|
12
|
+
requires-python = ">=3.10,<3.13"
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 1 - Planning",
|
|
15
|
+
"License :: OSI Approved :: Apache Software License",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
]
|
|
20
|
+
keywords = ["tigrbl_engine_duckdb", "tigrbl", "experimental"]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"duckdb>=1.0.0",
|
|
23
|
+
"tigrbl>=0.3.0.dev4",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/swarmauri/swarmauri-sdk"
|
|
28
|
+
Repository = "https://github.com/swarmauri/swarmauri-sdk/tree/master/pkgs/experimental/tigrbl_engine_duckdb"
|
|
29
|
+
|
|
30
|
+
[project.entry-points."tigrbl.engine"]
|
|
31
|
+
duckdb = "tigrbl_engine_duckdb.plugin:register"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["src/tigrbl_engine_duckdb"]
|
|
35
|
+
|
|
36
|
+
[dependency-groups]
|
|
37
|
+
dev = [
|
|
38
|
+
"pytest>=8.0",
|
|
39
|
+
"pytest-asyncio>=0.24.0",
|
|
40
|
+
"pytest-xdist>=3.6.1",
|
|
41
|
+
"pytest-json-report>=1.5.0",
|
|
42
|
+
"python-dotenv",
|
|
43
|
+
"requests>=2.32.3",
|
|
44
|
+
"flake8>=7.0",
|
|
45
|
+
"pytest-timeout>=2.3.1",
|
|
46
|
+
"ruff>=0.9.9",
|
|
47
|
+
"pytest-benchmark>=4.0.0",
|
|
48
|
+
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .duck_builder import duckdb_engine, duckdb_capabilities
|
|
4
|
+
from .duck_session import DuckDBSession
|
|
5
|
+
from .plugin import register
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"duckdb_engine",
|
|
9
|
+
"duckdb_capabilities",
|
|
10
|
+
"DuckDBSession",
|
|
11
|
+
"register",
|
|
12
|
+
]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any, Callable, Optional, Tuple, Mapping
|
|
3
|
+
|
|
4
|
+
import duckdb
|
|
5
|
+
|
|
6
|
+
from tigrbl.session.spec import SessionSpec
|
|
7
|
+
from .duck_session import DuckDBSession
|
|
8
|
+
|
|
9
|
+
SessionFactory = Callable[[], Any]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def duckdb_engine(
|
|
13
|
+
*,
|
|
14
|
+
path: Optional[str] = None, # None or ":memory:" → in-memory
|
|
15
|
+
read_only: bool = False,
|
|
16
|
+
threads: Optional[int] = None,
|
|
17
|
+
pragmas: Optional[Mapping[str, Any]] = None,
|
|
18
|
+
mapping: Optional[Mapping[str, Any]] = None, # accepted but not required
|
|
19
|
+
spec: Optional[Any] = None, # accepted for signature parity
|
|
20
|
+
dsn: Optional[str] = None, # accepted for signature parity
|
|
21
|
+
) -> Tuple[Any, SessionFactory]:
|
|
22
|
+
"""
|
|
23
|
+
Build a DuckDB 'engine' and a session factory that yields DuckDBSession.
|
|
24
|
+
No SQLAlchemy; pure duckdb bindings.
|
|
25
|
+
Returns:
|
|
26
|
+
(engine_handle, sessionmaker)
|
|
27
|
+
"""
|
|
28
|
+
db_path = path or ":memory:"
|
|
29
|
+
|
|
30
|
+
def mk_session(spec_in: Optional[SessionSpec] = None) -> DuckDBSession:
|
|
31
|
+
conn = duckdb.connect(db_path, read_only=read_only)
|
|
32
|
+
# Pragmas per-session for determinism.
|
|
33
|
+
if threads is not None:
|
|
34
|
+
conn.execute(f"PRAGMA threads={int(threads)}")
|
|
35
|
+
if pragmas:
|
|
36
|
+
for k, v in pragmas.items():
|
|
37
|
+
if isinstance(v, bool):
|
|
38
|
+
v = "true" if v else "false"
|
|
39
|
+
conn.execute(f"PRAGMA {k}={v}")
|
|
40
|
+
return DuckDBSession(conn, spec_in)
|
|
41
|
+
|
|
42
|
+
engine_handle = {"kind": "duckdb", "path": db_path, "read_only": read_only}
|
|
43
|
+
return engine_handle, mk_session
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def duckdb_capabilities() -> dict[str, Any]:
|
|
47
|
+
"""
|
|
48
|
+
Capability advertisement for session_ctx validation.
|
|
49
|
+
DuckDB provides MVCC snapshot-style isolation.
|
|
50
|
+
"""
|
|
51
|
+
return {
|
|
52
|
+
"transactional": True,
|
|
53
|
+
"isolation_levels": {"snapshot", "repeatable_read"},
|
|
54
|
+
"read_only_enforced": False,
|
|
55
|
+
"async_native": False,
|
|
56
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any, Callable, Optional, Sequence
|
|
5
|
+
|
|
6
|
+
import duckdb
|
|
7
|
+
|
|
8
|
+
from tigrbl.session.base import TigrblSessionBase
|
|
9
|
+
from tigrbl.session.spec import SessionSpec
|
|
10
|
+
from tigrbl.core.crud.helpers.model import _single_pk_name, _model_columns
|
|
11
|
+
from tigrbl.core.crud.helpers import NoResultFound
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _ScalarResult:
|
|
15
|
+
"""Minimal result facade for Tigrbl core CRUD."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, items: Sequence[Any]) -> None:
|
|
18
|
+
self._items = list(items)
|
|
19
|
+
|
|
20
|
+
def scalars(self) -> "_ScalarResult":
|
|
21
|
+
return self
|
|
22
|
+
|
|
23
|
+
def all(self) -> list[Any]:
|
|
24
|
+
return list(self._items)
|
|
25
|
+
|
|
26
|
+
def scalar_one(self) -> Any:
|
|
27
|
+
if len(self._items) != 1:
|
|
28
|
+
raise NoResultFound("expected exactly one row")
|
|
29
|
+
return self._items[0]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DuckDBSession(TigrblSessionBase):
|
|
33
|
+
"""Transactional session over a synchronous duckdb.Connection."""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self, conn: duckdb.DuckDBPyConnection, spec: Optional[SessionSpec] = None
|
|
37
|
+
) -> None:
|
|
38
|
+
super().__init__(spec)
|
|
39
|
+
self._c = conn
|
|
40
|
+
|
|
41
|
+
# ---------- async marker ----------
|
|
42
|
+
async def run_sync(self, fn: Callable[[Any], Any]) -> Any:
|
|
43
|
+
return await asyncio.to_thread(fn, self._c)
|
|
44
|
+
|
|
45
|
+
# ---------- TX primitives ----------
|
|
46
|
+
async def _tx_begin_impl(self) -> None:
|
|
47
|
+
await asyncio.to_thread(self._c.execute, "BEGIN")
|
|
48
|
+
|
|
49
|
+
async def _tx_commit_impl(self) -> None:
|
|
50
|
+
await asyncio.to_thread(self._c.execute, "COMMIT")
|
|
51
|
+
|
|
52
|
+
async def _tx_rollback_impl(self) -> None:
|
|
53
|
+
await asyncio.to_thread(self._c.execute, "ROLLBACK")
|
|
54
|
+
|
|
55
|
+
# ---------- CRUD primitives ----------
|
|
56
|
+
def _add_impl(self, obj: Any) -> Any:
|
|
57
|
+
table = getattr(obj.__class__, "__tablename__", obj.__class__.__name__)
|
|
58
|
+
cols = list(_model_columns(obj.__class__).keys())
|
|
59
|
+
placeholders = ", ".join(["?"] * len(cols))
|
|
60
|
+
col_list = ", ".join(cols)
|
|
61
|
+
sql = f'INSERT INTO "{table}" ({col_list}) VALUES ({placeholders})'
|
|
62
|
+
values = [getattr(obj, c, None) for c in cols]
|
|
63
|
+
|
|
64
|
+
def _exec():
|
|
65
|
+
self._c.execute(sql, values)
|
|
66
|
+
|
|
67
|
+
return _exec()
|
|
68
|
+
|
|
69
|
+
async def _delete_impl(self, obj: Any) -> None:
|
|
70
|
+
table = getattr(obj.__class__, "__tablename__", obj.__class__.__name__)
|
|
71
|
+
pk = _single_pk_name(obj.__class__)
|
|
72
|
+
ident = getattr(obj, pk)
|
|
73
|
+
await asyncio.to_thread(
|
|
74
|
+
self._c.execute, f'DELETE FROM "{table}" WHERE "{pk}" = ?', [ident]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
async def _flush_impl(self) -> None:
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
async def _refresh_impl(self, obj: Any) -> None:
|
|
81
|
+
pk = _single_pk_name(obj.__class__)
|
|
82
|
+
ident = getattr(obj, pk)
|
|
83
|
+
fresh = await self._get_impl(obj.__class__, ident)
|
|
84
|
+
if fresh:
|
|
85
|
+
for c in _model_columns(obj.__class__).keys():
|
|
86
|
+
setattr(obj, c, getattr(fresh, c, None))
|
|
87
|
+
|
|
88
|
+
async def _get_impl(self, model: type, ident: Any) -> Any | None:
|
|
89
|
+
table = getattr(model, "__tablename__", model.__name__)
|
|
90
|
+
pk = _single_pk_name(model)
|
|
91
|
+
cols = list(_model_columns(model).keys())
|
|
92
|
+
col_list = ", ".join([f'"{c}"' for c in cols])
|
|
93
|
+
sql = f'SELECT {col_list} FROM "{table}" WHERE "{pk}" = ?'
|
|
94
|
+
cur = await asyncio.to_thread(self._c.execute, sql, [ident])
|
|
95
|
+
row = cur.fetchone()
|
|
96
|
+
if not row:
|
|
97
|
+
return None
|
|
98
|
+
obj = model()
|
|
99
|
+
for i, c in enumerate(cols):
|
|
100
|
+
setattr(obj, c, row[i])
|
|
101
|
+
return obj
|
|
102
|
+
|
|
103
|
+
async def _execute_impl(self, stmt: Any) -> Any:
|
|
104
|
+
if isinstance(stmt, tuple) and len(stmt) == 2 and isinstance(stmt[0], str):
|
|
105
|
+
sql, params = stmt
|
|
106
|
+
cur = await asyncio.to_thread(self._c.execute, sql, params or [])
|
|
107
|
+
rows = cur.fetchall() or []
|
|
108
|
+
return _ScalarResult(rows)
|
|
109
|
+
|
|
110
|
+
if isinstance(stmt, str):
|
|
111
|
+
cur = await asyncio.to_thread(self._c.execute, stmt)
|
|
112
|
+
rows = cur.fetchall() or []
|
|
113
|
+
return _ScalarResult(rows)
|
|
114
|
+
|
|
115
|
+
raise NotImplementedError(f"Unsupported statement type: {type(stmt)}")
|
|
116
|
+
|
|
117
|
+
async def _close_impl(self) -> None:
|
|
118
|
+
try:
|
|
119
|
+
await asyncio.to_thread(self._c.close)
|
|
120
|
+
except Exception:
|
|
121
|
+
pass
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from tigrbl.engine.registry import register_engine
|
|
4
|
+
from .duck_builder import duckdb_engine, duckdb_capabilities
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def register() -> None:
|
|
8
|
+
# Entry point hook, called by Tigrbl's plugin loader.
|
|
9
|
+
# Registers the 'duckdb' engine kind.
|
|
10
|
+
register_engine("duckdb", duckdb_engine, duckdb_capabilities)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_package_assets_present() -> None:
|
|
5
|
+
package_dir = Path(__file__).resolve().parents[1]
|
|
6
|
+
assert (package_dir / "README.md").is_file()
|
|
7
|
+
assert (package_dir / "LICENSE").is_file()
|
|
8
|
+
assert (package_dir / "pyproject.toml").is_file()
|