dirsql 0.3.8__cp314-cp314-win_amd64.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.
- dirsql/__init__.py +9 -0
- dirsql/_async.py +110 -0
- dirsql/_binary/dirsql.exe +0 -0
- dirsql/_cli/__init__.py +6 -0
- dirsql/_cli/binary_path.py +20 -0
- dirsql/_cli/is_windows.py +10 -0
- dirsql/_cli/main.py +27 -0
- dirsql/_dirsql.cp314-win_amd64.pyd +0 -0
- dirsql/test_async.py +95 -0
- dirsql-0.3.8.dist-info/METADATA +216 -0
- dirsql-0.3.8.dist-info/RECORD +14 -0
- dirsql-0.3.8.dist-info/WHEEL +4 -0
- dirsql-0.3.8.dist-info/entry_points.txt +2 -0
- dirsql-0.3.8.dist-info/sboms/dirsql-py-ext.cyclonedx.json +2977 -0
dirsql/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""dirsql - Ephemeral SQL index over a local directory.
|
|
2
|
+
|
|
3
|
+
Also available for Rust (crates.io: ``dirsql``) and TypeScript (npm: ``dirsql``).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dirsql._dirsql import Table, RowEvent, __version__
|
|
7
|
+
from dirsql._async import DirSQL
|
|
8
|
+
|
|
9
|
+
__all__ = ["DirSQL", "Table", "RowEvent", "__version__"]
|
dirsql/_async.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Async-by-default DirSQL wrapper."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from dirsql._dirsql import DirSQL as _RustDirSQL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class _WatchStream:
|
|
9
|
+
"""Async iterator that polls for file events."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, db):
|
|
12
|
+
self._db = db
|
|
13
|
+
self._started = False
|
|
14
|
+
self._buffer = []
|
|
15
|
+
|
|
16
|
+
def __aiter__(self):
|
|
17
|
+
return self
|
|
18
|
+
|
|
19
|
+
async def __anext__(self):
|
|
20
|
+
if not self._started:
|
|
21
|
+
await asyncio.to_thread(self._db._start_watcher)
|
|
22
|
+
self._started = True
|
|
23
|
+
|
|
24
|
+
while True:
|
|
25
|
+
if self._buffer:
|
|
26
|
+
return self._buffer.pop(0)
|
|
27
|
+
events = await asyncio.to_thread(self._db._poll_events, 200)
|
|
28
|
+
if events:
|
|
29
|
+
self._buffer.extend(events)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DirSQL:
|
|
33
|
+
"""Async-by-default wrapper around the Rust DirSQL engine.
|
|
34
|
+
|
|
35
|
+
Usage:
|
|
36
|
+
# Programmatic:
|
|
37
|
+
db = DirSQL(root, tables=[...])
|
|
38
|
+
# From a config file:
|
|
39
|
+
db = DirSQL(config="./my-config.toml")
|
|
40
|
+
|
|
41
|
+
await db.ready()
|
|
42
|
+
results = await db.query("SELECT ...")
|
|
43
|
+
async for event in db.watch():
|
|
44
|
+
...
|
|
45
|
+
|
|
46
|
+
At least one of ``root`` or ``config`` must be supplied. When both are
|
|
47
|
+
set, the explicit ``root`` wins over any ``[dirsql].root`` in the config
|
|
48
|
+
file (a warning is emitted on stderr).
|
|
49
|
+
|
|
50
|
+
Pass ``persist=True`` to keep an on-disk SQLite cache (default location:
|
|
51
|
+
``<root>/.dirsql/cache.db``). Override the location with ``persist_path``.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
root=None,
|
|
57
|
+
*,
|
|
58
|
+
tables=None,
|
|
59
|
+
ignore=None,
|
|
60
|
+
config=None,
|
|
61
|
+
persist=False,
|
|
62
|
+
persist_path=None,
|
|
63
|
+
):
|
|
64
|
+
if root is None and config is None:
|
|
65
|
+
raise TypeError("DirSQL requires either a root directory or a config= path")
|
|
66
|
+
self._root = root
|
|
67
|
+
self._tables = tables
|
|
68
|
+
self._ignore = ignore
|
|
69
|
+
self._config = config
|
|
70
|
+
self._persist = persist
|
|
71
|
+
self._persist_path = persist_path
|
|
72
|
+
self._db = None
|
|
73
|
+
self._ready_event = asyncio.Event()
|
|
74
|
+
self._init_error = None
|
|
75
|
+
self._task = asyncio.ensure_future(self._init_bg())
|
|
76
|
+
|
|
77
|
+
async def _init_bg(self):
|
|
78
|
+
"""Run the scan in the background."""
|
|
79
|
+
try:
|
|
80
|
+
self._db = await asyncio.to_thread(
|
|
81
|
+
_RustDirSQL,
|
|
82
|
+
self._root,
|
|
83
|
+
tables=self._tables,
|
|
84
|
+
ignore=self._ignore,
|
|
85
|
+
config=self._config,
|
|
86
|
+
persist=self._persist,
|
|
87
|
+
persist_path=self._persist_path,
|
|
88
|
+
)
|
|
89
|
+
except Exception as exc:
|
|
90
|
+
self._init_error = exc
|
|
91
|
+
finally:
|
|
92
|
+
self._ready_event.set()
|
|
93
|
+
|
|
94
|
+
async def ready(self):
|
|
95
|
+
"""Wait until the initial scan is complete.
|
|
96
|
+
|
|
97
|
+
Raises any exception that occurred during init.
|
|
98
|
+
Can be called multiple times safely.
|
|
99
|
+
"""
|
|
100
|
+
await self._ready_event.wait()
|
|
101
|
+
if self._init_error is not None:
|
|
102
|
+
raise self._init_error
|
|
103
|
+
|
|
104
|
+
async def query(self, sql):
|
|
105
|
+
"""Execute a SQL query asynchronously."""
|
|
106
|
+
return await asyncio.to_thread(self._db.query, sql)
|
|
107
|
+
|
|
108
|
+
def watch(self):
|
|
109
|
+
"""Start watching for file changes. Returns an async iterable of RowEvent."""
|
|
110
|
+
return _WatchStream(self._db)
|
|
Binary file
|
dirsql/_cli/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Resolve the bundled Rust binary inside the installed wheel."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib.resources import files
|
|
6
|
+
|
|
7
|
+
from dirsql._cli.is_windows import is_windows
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def binary_path() -> str:
|
|
11
|
+
name = "dirsql.exe" if is_windows() else "dirsql"
|
|
12
|
+
path = files("dirsql").joinpath("_binary", name)
|
|
13
|
+
if not path.is_file():
|
|
14
|
+
raise FileNotFoundError(
|
|
15
|
+
f"bundled `{name}` not found at {path}. The dirsql PyPI wheel "
|
|
16
|
+
"no longer ships the CLI binary (release-tooling regression "
|
|
17
|
+
"while putitoutthere wires up bundle_cli). Install the CLI via "
|
|
18
|
+
"`cargo install dirsql --features cli` or `npx dirsql`."
|
|
19
|
+
)
|
|
20
|
+
return str(path)
|
dirsql/_cli/main.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Console-script entry point. Execs the bundled binary on POSIX,
|
|
2
|
+
subprocesses it on Windows."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from dirsql._cli.binary_path import binary_path
|
|
11
|
+
from dirsql._cli.is_windows import is_windows
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main(argv: list[str] | None = None) -> int:
|
|
15
|
+
if argv is None:
|
|
16
|
+
argv = sys.argv[1:]
|
|
17
|
+
try:
|
|
18
|
+
binary = binary_path()
|
|
19
|
+
except FileNotFoundError as exc:
|
|
20
|
+
print(f"dirsql: {exc}", file=sys.stderr)
|
|
21
|
+
return 1
|
|
22
|
+
|
|
23
|
+
if is_windows():
|
|
24
|
+
completed = subprocess.run([binary, *argv])
|
|
25
|
+
return completed.returncode
|
|
26
|
+
os.execv(binary, [binary, *argv])
|
|
27
|
+
return 0 # unreachable
|
|
Binary file
|
dirsql/test_async.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Unit tests for the DirSQL async wrapper."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from dirsql import _async as async_mod
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _FakeRustDirSQL:
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
root=None,
|
|
14
|
+
*,
|
|
15
|
+
tables=None,
|
|
16
|
+
ignore=None,
|
|
17
|
+
config=None,
|
|
18
|
+
persist=False,
|
|
19
|
+
persist_path=None,
|
|
20
|
+
):
|
|
21
|
+
self.root = root
|
|
22
|
+
self.tables = tables
|
|
23
|
+
self.ignore = ignore
|
|
24
|
+
self.config = config
|
|
25
|
+
self.persist = persist
|
|
26
|
+
self.persist_path = persist_path
|
|
27
|
+
self.query_calls = []
|
|
28
|
+
|
|
29
|
+
def query(self, sql):
|
|
30
|
+
self.query_calls.append(sql)
|
|
31
|
+
return [{"sql": sql}]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class _FakeWatcherDb:
|
|
35
|
+
def __init__(self, events):
|
|
36
|
+
self.events = list(events)
|
|
37
|
+
self.started = 0
|
|
38
|
+
self.poll_calls = []
|
|
39
|
+
|
|
40
|
+
def _start_watcher(self):
|
|
41
|
+
self.started += 1
|
|
42
|
+
|
|
43
|
+
def _poll_events(self, timeout_ms):
|
|
44
|
+
self.poll_calls.append(timeout_ms)
|
|
45
|
+
if self.events:
|
|
46
|
+
return self.events.pop(0)
|
|
47
|
+
return []
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def describe_DirSQL_async():
|
|
51
|
+
def describe_ready_and_query():
|
|
52
|
+
@pytest.mark.asyncio
|
|
53
|
+
async def it_uses_the_background_db(monkeypatch):
|
|
54
|
+
monkeypatch.setattr(async_mod, "_RustDirSQL", _FakeRustDirSQL)
|
|
55
|
+
|
|
56
|
+
db = async_mod.DirSQL("/tmp/root", tables=["table-a"], ignore=["**/*.tmp"])
|
|
57
|
+
await db.ready()
|
|
58
|
+
|
|
59
|
+
results = await db.query("SELECT 1")
|
|
60
|
+
|
|
61
|
+
assert db._db.root == "/tmp/root"
|
|
62
|
+
assert db._db.tables == ["table-a"]
|
|
63
|
+
assert db._db.ignore == ["**/*.tmp"]
|
|
64
|
+
assert db._db.query_calls == ["SELECT 1"]
|
|
65
|
+
assert results == [{"sql": "SELECT 1"}]
|
|
66
|
+
|
|
67
|
+
@pytest.mark.asyncio
|
|
68
|
+
async def it_propagates_initialization_errors(monkeypatch):
|
|
69
|
+
class _BoomDirSQL:
|
|
70
|
+
def __init__(self, *args, **kwargs):
|
|
71
|
+
raise RuntimeError("boom")
|
|
72
|
+
|
|
73
|
+
monkeypatch.setattr(async_mod, "_RustDirSQL", _BoomDirSQL)
|
|
74
|
+
|
|
75
|
+
db = async_mod.DirSQL("/tmp/root", tables=["table-a"])
|
|
76
|
+
|
|
77
|
+
with pytest.raises(RuntimeError, match="boom"):
|
|
78
|
+
await db.ready()
|
|
79
|
+
|
|
80
|
+
def describe_watch_stream():
|
|
81
|
+
@pytest.mark.asyncio
|
|
82
|
+
async def it_starts_the_watcher_and_buffers_events():
|
|
83
|
+
stream = async_mod._WatchStream(
|
|
84
|
+
_FakeWatcherDb(events=[["event-a", "event-b"]])
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
assert stream.__aiter__() is stream
|
|
88
|
+
|
|
89
|
+
first = await stream.__anext__()
|
|
90
|
+
second = await stream.__anext__()
|
|
91
|
+
|
|
92
|
+
assert first == "event-a"
|
|
93
|
+
assert second == "event-b"
|
|
94
|
+
assert stream._db.started == 1
|
|
95
|
+
assert stream._db.poll_calls == [200]
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dirsql
|
|
3
|
+
Version: 0.3.8
|
|
4
|
+
Requires-Dist: pytest>=8 ; extra == 'dev'
|
|
5
|
+
Requires-Dist: pytest-describe>=2 ; extra == 'dev'
|
|
6
|
+
Requires-Dist: pytest-asyncio>=0.23 ; extra == 'dev'
|
|
7
|
+
Requires-Dist: pytest-cov>=5 ; extra == 'dev'
|
|
8
|
+
Requires-Dist: ruff>=0.4 ; extra == 'dev'
|
|
9
|
+
Requires-Dist: maturin>=1.0 ; extra == 'dev'
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Summary: Ephemeral SQL index over a local directory
|
|
12
|
+
Keywords: sql,filesystem,directory,sqlite,index
|
|
13
|
+
Author: Kevin Scott
|
|
14
|
+
License-Expression: MIT
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
17
|
+
|
|
18
|
+
# `dirsql` (Python SDK)
|
|
19
|
+
|
|
20
|
+
Ephemeral SQL index over a local directory. Watches a filesystem, ingests structured files into an in-memory SQLite database, and exposes a SQL query interface. The database is purely in-memory -- the filesystem is always the source of truth.
|
|
21
|
+
|
|
22
|
+
[Documentation](https://thekevinscott.github.io/dirsql/?lang=python)
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install dirsql
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Requires Python >= 3.12. Ships as a native extension (Rust via PyO3) -- binary wheels are provided for common platforms.
|
|
31
|
+
|
|
32
|
+
Each wheel also bundles the `dirsql` HTTP-server CLI as a console script, so `pip install dirsql` also gives you a `dirsql` command on `$PATH`. See the [CLI guide](https://github.com/thekevinscott/dirsql/blob/main/docs/guide/cli.md).
|
|
33
|
+
|
|
34
|
+
## Publishing (maintainers)
|
|
35
|
+
|
|
36
|
+
Handled by `.github/workflows/publish.yml` (invoked from `minor-release.yml` / `patch-release.yml`). For each target triple the `build` job `cargo build`s the Rust CLI with `--features cli`, stages the binary into `dirsql/_binary/`, runs `maturin build` (which picks the binary up via the `[tool.maturin] include` rule in `pyproject.toml`), and the wheels + sdist are then trusted-published to PyPI.
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import asyncio
|
|
42
|
+
import json
|
|
43
|
+
import os
|
|
44
|
+
import tempfile
|
|
45
|
+
from dirsql import DirSQL, Table
|
|
46
|
+
|
|
47
|
+
async def main():
|
|
48
|
+
# Create some data files
|
|
49
|
+
root = tempfile.mkdtemp()
|
|
50
|
+
os.makedirs(os.path.join(root, "comments", "abc"), exist_ok=True)
|
|
51
|
+
os.makedirs(os.path.join(root, "comments", "def"), exist_ok=True)
|
|
52
|
+
|
|
53
|
+
with open(os.path.join(root, "comments", "abc", "index.jsonl"), "w") as f:
|
|
54
|
+
f.write(json.dumps({"body": "looks good", "author": "alice"}) + "\n")
|
|
55
|
+
f.write(json.dumps({"body": "needs work", "author": "bob"}) + "\n")
|
|
56
|
+
|
|
57
|
+
with open(os.path.join(root, "comments", "def", "index.jsonl"), "w") as f:
|
|
58
|
+
f.write(json.dumps({"body": "agreed", "author": "carol"}) + "\n")
|
|
59
|
+
|
|
60
|
+
# Define a table: DDL, glob pattern, and an extract function
|
|
61
|
+
db = DirSQL(
|
|
62
|
+
root,
|
|
63
|
+
tables=[
|
|
64
|
+
Table(
|
|
65
|
+
ddl="CREATE TABLE comments (id TEXT, body TEXT, author TEXT)",
|
|
66
|
+
glob="comments/**/index.jsonl",
|
|
67
|
+
extract=lambda path: [
|
|
68
|
+
{
|
|
69
|
+
"id": os.path.basename(os.path.dirname(path)),
|
|
70
|
+
"body": row["body"],
|
|
71
|
+
"author": row["author"],
|
|
72
|
+
}
|
|
73
|
+
for line in open(path, encoding="utf-8").read().splitlines()
|
|
74
|
+
for row in [json.loads(line)]
|
|
75
|
+
],
|
|
76
|
+
),
|
|
77
|
+
],
|
|
78
|
+
)
|
|
79
|
+
await db.ready()
|
|
80
|
+
|
|
81
|
+
# Query with SQL
|
|
82
|
+
results = await db.query("SELECT * FROM comments WHERE author = 'alice'")
|
|
83
|
+
# [{"id": "abc", "body": "looks good", "author": "alice"}]
|
|
84
|
+
|
|
85
|
+
asyncio.run(main())
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Multiple Tables and Joins
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
db = DirSQL(
|
|
92
|
+
root,
|
|
93
|
+
tables=[
|
|
94
|
+
Table(
|
|
95
|
+
ddl="CREATE TABLE posts (title TEXT, author_id TEXT)",
|
|
96
|
+
glob="posts/*.json",
|
|
97
|
+
extract=lambda path: [json.loads(open(path, encoding="utf-8").read())],
|
|
98
|
+
),
|
|
99
|
+
Table(
|
|
100
|
+
ddl="CREATE TABLE authors (id TEXT, name TEXT)",
|
|
101
|
+
glob="authors/*.json",
|
|
102
|
+
extract=lambda path: [json.loads(open(path, encoding="utf-8").read())],
|
|
103
|
+
),
|
|
104
|
+
],
|
|
105
|
+
)
|
|
106
|
+
await db.ready()
|
|
107
|
+
|
|
108
|
+
results = await db.query("""
|
|
109
|
+
SELECT posts.title, authors.name
|
|
110
|
+
FROM posts JOIN authors ON posts.author_id = authors.id
|
|
111
|
+
""")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Ignoring Files
|
|
115
|
+
|
|
116
|
+
Pass `ignore` patterns to skip files during scanning and watching:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
db = DirSQL(
|
|
120
|
+
root,
|
|
121
|
+
ignore=["**/drafts/**", "**/.git/**"],
|
|
122
|
+
tables=[...],
|
|
123
|
+
)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Watching for Changes
|
|
127
|
+
|
|
128
|
+
`DirSQL` is async by default. The `watch()` method returns an async iterator of row-level change events.
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
import asyncio
|
|
132
|
+
import json
|
|
133
|
+
from dirsql import DirSQL, Table
|
|
134
|
+
|
|
135
|
+
async def main():
|
|
136
|
+
db = DirSQL(
|
|
137
|
+
"/path/to/data",
|
|
138
|
+
tables=[
|
|
139
|
+
Table(
|
|
140
|
+
ddl="CREATE TABLE items (name TEXT)",
|
|
141
|
+
glob="**/*.json",
|
|
142
|
+
extract=lambda path: [json.loads(open(path, encoding="utf-8").read())],
|
|
143
|
+
),
|
|
144
|
+
],
|
|
145
|
+
)
|
|
146
|
+
await db.ready()
|
|
147
|
+
|
|
148
|
+
# Query
|
|
149
|
+
results = await db.query("SELECT * FROM items")
|
|
150
|
+
|
|
151
|
+
# Watch for file changes (insert/update/delete/error events)
|
|
152
|
+
async for event in db.watch():
|
|
153
|
+
print(f"{event.action} on {event.table}: {event.row}")
|
|
154
|
+
if event.action == "error":
|
|
155
|
+
print(f" error: {event.error}")
|
|
156
|
+
|
|
157
|
+
asyncio.run(main())
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## API Reference
|
|
161
|
+
|
|
162
|
+
### `Table(*, ddl, glob, extract)`
|
|
163
|
+
|
|
164
|
+
Defines how files map to a SQL table.
|
|
165
|
+
|
|
166
|
+
- **`ddl`** (`str`): A `CREATE TABLE` statement defining the schema.
|
|
167
|
+
- **`glob`** (`str`): A glob pattern matched against file paths relative to root.
|
|
168
|
+
- **`extract`** (`Callable[[str], list[dict]]`): A function receiving the matched file's absolute filesystem path and returning a list of row dicts. dirsql does not read file contents; a callback that needs the file body reads it itself (e.g. `open(path, encoding="utf-8").read()`). Each dict's keys must match the DDL column names.
|
|
169
|
+
|
|
170
|
+
### `DirSQL(root=None, *, tables=None, ignore=None, config=None)`
|
|
171
|
+
|
|
172
|
+
Creates an in-memory SQLite database indexed from the directory at `root`. The constructor is sync and returns immediately; scanning runs in a background thread.
|
|
173
|
+
|
|
174
|
+
At least one of `root` or `config` must be supplied. When both `root` and `config` are passed (or `config` declares `[dirsql].root`), the explicit `root` wins and a warning is emitted on stderr.
|
|
175
|
+
|
|
176
|
+
- **`root`** (`str | None`): Path to the directory to index. Optional when `config` supplies one.
|
|
177
|
+
- **`tables`** (`list[Table] | None`): Programmatic table definitions. Appended to any tables in the config file.
|
|
178
|
+
- **`ignore`** (`list[str] | None`): Glob patterns for paths to skip. Appended to any `[dirsql].ignore` patterns in the config file.
|
|
179
|
+
- **`config`** (`str | None`): Optional path to a `.dirsql.toml` file. Its `[[table]]` entries, `[dirsql].ignore`, and optional `[dirsql].root` are merged into the constructor's inputs.
|
|
180
|
+
|
|
181
|
+
#### `await DirSQL.ready()`
|
|
182
|
+
|
|
183
|
+
Wait for the initial scan to complete. Idempotent -- safe to call multiple times. Raises any exception that occurred during init.
|
|
184
|
+
|
|
185
|
+
#### `await DirSQL.query(sql) -> list[dict]`
|
|
186
|
+
|
|
187
|
+
Execute a SQL query. Returns a list of dicts keyed by column name. Internal tracking columns (`_dirsql_*`) are excluded from results.
|
|
188
|
+
|
|
189
|
+
#### `DirSQL.watch() -> AsyncIterator[RowEvent]`
|
|
190
|
+
|
|
191
|
+
Returns an async iterator that yields `RowEvent` objects as files change on disk. Starts the filesystem watcher on first iteration.
|
|
192
|
+
|
|
193
|
+
### `RowEvent`
|
|
194
|
+
|
|
195
|
+
Emitted by `watch()` when a file change produces row-level diffs.
|
|
196
|
+
|
|
197
|
+
- **`table`** (`str`): The affected table name.
|
|
198
|
+
- **`action`** (`str`): One of `"insert"`, `"update"`, `"delete"`, `"error"`.
|
|
199
|
+
- **`row`** (`dict | None`): The new row (for insert/update) or deleted row (for delete).
|
|
200
|
+
- **`old_row`** (`dict | None`): The previous row (for update only).
|
|
201
|
+
- **`error`** (`str | None`): Error message (for error events).
|
|
202
|
+
- **`file_path`** (`str | None`): The relative file path that triggered the event.
|
|
203
|
+
|
|
204
|
+
## How It Works
|
|
205
|
+
|
|
206
|
+
The Rust core (`rusqlite` + `notify` + `walkdir`) does the heavy lifting:
|
|
207
|
+
|
|
208
|
+
1. **Startup scan**: Walks the directory tree, matches files to tables via glob patterns, calls the user-provided `extract` function for each file, and inserts rows into an in-memory SQLite database.
|
|
209
|
+
2. **File watching**: Uses the `notify` crate (inotify on Linux, FSEvents on macOS) to detect file creates, modifications, and deletions.
|
|
210
|
+
3. **Row diffing**: When a file changes, the new rows are diffed against the previous rows for that file, producing granular insert/update/delete events.
|
|
211
|
+
4. **Python bindings**: PyO3 exposes the Rust core as a native Python extension module. The async layer runs blocking operations in a thread pool via `asyncio.to_thread`.
|
|
212
|
+
|
|
213
|
+
## License
|
|
214
|
+
|
|
215
|
+
MIT
|
|
216
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
dirsql/__init__.py,sha256=R4O-oQ4SvQQJqoBEeovbAGMu82hBii_FVlCvLTxXQHc,300
|
|
2
|
+
dirsql/_async.py,sha256=QHAZ_4y68ApkTlTskbX_pZqxKraFXXPvDHsc19tA2cM,3346
|
|
3
|
+
dirsql/_binary/dirsql.exe,sha256=Uqt7OnRm7UagG-3HqsXdEmtMul4emhBNXsze33Hax_Y,6525440
|
|
4
|
+
dirsql/_cli/__init__.py,sha256=nx8YdJ2hCyUMz5Z840dPK2xaYRKwoQjyHtb_Z3c-Fyw,212
|
|
5
|
+
dirsql/_cli/binary_path.py,sha256=9u34ONnOfxYR_ifNE0vAs5UpUpvlWWlPNRYxDW8wz88,730
|
|
6
|
+
dirsql/_cli/is_windows.py,sha256=Gjue2cpKCd-qvuEDus7haB6riQlVRhQ0FqTLH4A2ht4,253
|
|
7
|
+
dirsql/_cli/main.py,sha256=BEX2NqUdCezftkxjd90WOUn2UgsaQuRgxA3O29rNMyA,713
|
|
8
|
+
dirsql/_dirsql.cp314-win_amd64.pyd,sha256=qUE5p6Fpmjsxhbd7RqQdePzH9bIqzcOgWO11LWhTRis,4731392
|
|
9
|
+
dirsql/test_async.py,sha256=BkTwGXqVcxzBUXY44pNBQ8x6iplxfFtW6mV7MDoHN4U,2784
|
|
10
|
+
dirsql-0.3.8.dist-info/METADATA,sha256=jYOam9dAWX1TspOKpGhWtfh_k3E5UhZ0VI1MA57mEiM,8632
|
|
11
|
+
dirsql-0.3.8.dist-info/WHEEL,sha256=eBGhi3OBjShfkato4dop_2MU-x5MJXrOBEBOPEFTPyw,97
|
|
12
|
+
dirsql-0.3.8.dist-info/entry_points.txt,sha256=JnjTYOopHZyXeheGn31QPXGZ2YhhbwteXdJOL4VB3uQ,47
|
|
13
|
+
dirsql-0.3.8.dist-info/sboms/dirsql-py-ext.cyclonedx.json,sha256=3OflGPGXx25J-L8JtDN0GjBFP4yQdLir0tDe8Drs2qQ,92571
|
|
14
|
+
dirsql-0.3.8.dist-info/RECORD,,
|