dirsql 0.0.17__tar.gz → 0.0.18__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 (29) hide show
  1. {dirsql-0.0.17 → dirsql-0.0.18}/PKG-INFO +1 -1
  2. {dirsql-0.0.17 → dirsql-0.0.18}/pyproject.toml +2 -2
  3. {dirsql-0.0.17 → dirsql-0.0.18}/python/dirsql/_async.py +26 -10
  4. {dirsql-0.0.17 → dirsql-0.0.18}/tests/integration/test_async_dirsql.py +60 -25
  5. {dirsql-0.0.17 → dirsql-0.0.18}/.claude/CLAUDE.md +0 -0
  6. {dirsql-0.0.17 → dirsql-0.0.18}/.github/workflows/minor-release.yml +0 -0
  7. {dirsql-0.0.17 → dirsql-0.0.18}/.github/workflows/patch-release.yml +0 -0
  8. {dirsql-0.0.17 → dirsql-0.0.18}/.github/workflows/pr-monitor.yml +0 -0
  9. {dirsql-0.0.17 → dirsql-0.0.18}/.github/workflows/publish.yml +0 -0
  10. {dirsql-0.0.17 → dirsql-0.0.18}/.github/workflows/python-lint.yml +0 -0
  11. {dirsql-0.0.17 → dirsql-0.0.18}/.github/workflows/python-test.yml +0 -0
  12. {dirsql-0.0.17 → dirsql-0.0.18}/.github/workflows/rust-test.yml +0 -0
  13. {dirsql-0.0.17 → dirsql-0.0.18}/.gitignore +0 -0
  14. {dirsql-0.0.17 → dirsql-0.0.18}/.npmignore +0 -0
  15. {dirsql-0.0.17 → dirsql-0.0.18}/Cargo.lock +0 -0
  16. {dirsql-0.0.17 → dirsql-0.0.18}/Cargo.toml +0 -0
  17. {dirsql-0.0.17 → dirsql-0.0.18}/LICENSE +0 -0
  18. {dirsql-0.0.17 → dirsql-0.0.18}/SUMMARY.md +0 -0
  19. {dirsql-0.0.17 → dirsql-0.0.18}/python/dirsql/__init__.py +0 -0
  20. {dirsql-0.0.17 → dirsql-0.0.18}/src/db.rs +0 -0
  21. {dirsql-0.0.17 → dirsql-0.0.18}/src/differ.rs +0 -0
  22. {dirsql-0.0.17 → dirsql-0.0.18}/src/lib.rs +0 -0
  23. {dirsql-0.0.17 → dirsql-0.0.18}/src/matcher.rs +0 -0
  24. {dirsql-0.0.17 → dirsql-0.0.18}/src/scanner.rs +0 -0
  25. {dirsql-0.0.17 → dirsql-0.0.18}/src/watcher.rs +0 -0
  26. {dirsql-0.0.17 → dirsql-0.0.18}/tests/__init__.py +0 -0
  27. {dirsql-0.0.17 → dirsql-0.0.18}/tests/conftest.py +0 -0
  28. {dirsql-0.0.17 → dirsql-0.0.18}/tests/integration/__init__.py +0 -0
  29. {dirsql-0.0.17 → dirsql-0.0.18}/tests/integration/test_dirsql.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirsql
3
- Version: 0.0.17
3
+ Version: 0.0.18
4
4
  Requires-Dist: pytest>=8 ; extra == 'dev'
5
5
  Requires-Dist: pytest-describe>=2 ; extra == 'dev'
6
6
  Requires-Dist: pytest-asyncio>=0.23 ; extra == 'dev'
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "dirsql"
7
- version = "0.0.17"
7
+ version = "0.0.18"
8
8
  description = "Ephemeral SQL index over a local directory"
9
9
  license = "MIT"
10
10
  requires-python = ">=3.12"
@@ -24,7 +24,7 @@ dev = [
24
24
  ]
25
25
 
26
26
  [tool.maturin]
27
- features = ["pyo3/extension-module"]
27
+ features = ["extension-module"]
28
28
  exclude = [
29
29
  ".github/",
30
30
  ".claude/",
@@ -33,7 +33,8 @@ class AsyncDirSQL:
33
33
  """Async wrapper around DirSQL.
34
34
 
35
35
  Usage:
36
- db = await AsyncDirSQL(root, tables=[...])
36
+ db = AsyncDirSQL(root, tables=[...])
37
+ await db.ready()
37
38
  results = await db.query("SELECT ...")
38
39
  async for event in db.watch():
39
40
  ...
@@ -44,15 +45,30 @@ class AsyncDirSQL:
44
45
  self._tables = tables
45
46
  self._ignore = ignore
46
47
  self._db = None
47
-
48
- def __await__(self):
49
- return self._init().__await__()
50
-
51
- async def _init(self):
52
- self._db = await asyncio.to_thread(
53
- DirSQL, self._root, tables=self._tables, ignore=self._ignore
54
- )
55
- return self
48
+ self._ready_event = asyncio.Event()
49
+ self._init_error = None
50
+ self._task = asyncio.ensure_future(self._init_bg())
51
+
52
+ async def _init_bg(self):
53
+ """Run the scan in the background."""
54
+ try:
55
+ self._db = await asyncio.to_thread(
56
+ DirSQL, self._root, tables=self._tables, ignore=self._ignore
57
+ )
58
+ except Exception as exc:
59
+ self._init_error = exc
60
+ finally:
61
+ self._ready_event.set()
62
+
63
+ async def ready(self):
64
+ """Wait until the initial scan is complete.
65
+
66
+ Raises any exception that occurred during init.
67
+ Can be called multiple times safely.
68
+ """
69
+ await self._ready_event.wait()
70
+ if self._init_error is not None:
71
+ raise self._init_error
56
72
 
57
73
  async def query(self, sql):
58
74
  """Execute a SQL query asynchronously."""
@@ -12,9 +12,9 @@ from dirsql import AsyncDirSQL, Table
12
12
  def describe_AsyncDirSQL():
13
13
  def describe_init():
14
14
  @pytest.mark.asyncio
15
- async def it_creates_instance_with_await(jsonl_dir):
16
- """AsyncDirSQL can be initialized with await."""
17
- db = await AsyncDirSQL(
15
+ async def it_creates_instance_synchronously(jsonl_dir):
16
+ """AsyncDirSQL constructor is sync and returns immediately."""
17
+ db = AsyncDirSQL(
18
18
  jsonl_dir,
19
19
  tables=[
20
20
  Table(
@@ -35,9 +35,9 @@ def describe_AsyncDirSQL():
35
35
  assert db is not None
36
36
 
37
37
  @pytest.mark.asyncio
38
- async def it_indexes_files_on_init(jsonl_dir):
39
- """Async init scans and indexes directory contents."""
40
- db = await AsyncDirSQL(
38
+ async def it_indexes_files_after_ready(jsonl_dir):
39
+ """Data is available after awaiting ready()."""
40
+ db = AsyncDirSQL(
41
41
  jsonl_dir,
42
42
  tables=[
43
43
  Table(
@@ -55,33 +55,61 @@ def describe_AsyncDirSQL():
55
55
  ),
56
56
  ],
57
57
  )
58
+ await db.ready()
58
59
  results = await db.query("SELECT * FROM comments")
59
60
  assert len(results) == 3
60
61
 
61
62
  @pytest.mark.asyncio
62
- async def it_raises_on_extract_error_during_init(tmp_dir):
63
- """Extract lambda errors during init raise exceptions."""
63
+ async def it_raises_on_extract_error_during_ready(tmp_dir):
64
+ """Extract lambda errors during ready() raise exceptions."""
64
65
  os.makedirs(os.path.join(tmp_dir, "data"), exist_ok=True)
65
66
  with open(os.path.join(tmp_dir, "data", "bad.json"), "w") as f:
66
67
  f.write("not valid json")
67
68
 
69
+ db = AsyncDirSQL(
70
+ tmp_dir,
71
+ tables=[
72
+ Table(
73
+ ddl="CREATE TABLE items (name TEXT)",
74
+ glob="data/*.json",
75
+ extract=lambda path, content: [json.loads(content)],
76
+ ),
77
+ ],
78
+ )
68
79
  with pytest.raises(Exception):
69
- await AsyncDirSQL(
70
- tmp_dir,
71
- tables=[
72
- Table(
73
- ddl="CREATE TABLE items (name TEXT)",
74
- glob="data/*.json",
75
- extract=lambda path, content: [json.loads(content)],
76
- ),
77
- ],
78
- )
80
+ await db.ready()
81
+
82
+ @pytest.mark.asyncio
83
+ async def it_allows_multiple_ready_calls(jsonl_dir):
84
+ """Calling ready() multiple times is safe and idempotent."""
85
+ db = AsyncDirSQL(
86
+ jsonl_dir,
87
+ tables=[
88
+ Table(
89
+ ddl="CREATE TABLE comments (id TEXT, body TEXT, author TEXT)",
90
+ glob="comments/**/index.jsonl",
91
+ extract=lambda path, content: [
92
+ {
93
+ "id": os.path.basename(os.path.dirname(path)),
94
+ "body": row["body"],
95
+ "author": row["author"],
96
+ }
97
+ for line in content.splitlines()
98
+ for row in [json.loads(line)]
99
+ ],
100
+ ),
101
+ ],
102
+ )
103
+ await db.ready()
104
+ await db.ready()
105
+ results = await db.query("SELECT * FROM comments")
106
+ assert len(results) == 3
79
107
 
80
108
  def describe_query():
81
109
  @pytest.mark.asyncio
82
110
  async def it_returns_results_as_list_of_dicts(jsonl_dir):
83
111
  """Async query returns list of dicts with column names."""
84
- db = await AsyncDirSQL(
112
+ db = AsyncDirSQL(
85
113
  jsonl_dir,
86
114
  tables=[
87
115
  Table(
@@ -99,6 +127,7 @@ def describe_AsyncDirSQL():
99
127
  ),
100
128
  ],
101
129
  )
130
+ await db.ready()
102
131
  results = await db.query(
103
132
  "SELECT author FROM comments WHERE body = 'first comment'"
104
133
  )
@@ -108,7 +137,7 @@ def describe_AsyncDirSQL():
108
137
  @pytest.mark.asyncio
109
138
  async def it_raises_on_invalid_sql(jsonl_dir):
110
139
  """Invalid SQL raises an exception."""
111
- db = await AsyncDirSQL(
140
+ db = AsyncDirSQL(
112
141
  jsonl_dir,
113
142
  tables=[
114
143
  Table(
@@ -126,6 +155,7 @@ def describe_AsyncDirSQL():
126
155
  ),
127
156
  ],
128
157
  )
158
+ await db.ready()
129
159
  with pytest.raises(Exception):
130
160
  await db.query("NOT VALID SQL")
131
161
 
@@ -133,7 +163,7 @@ def describe_AsyncDirSQL():
133
163
  @pytest.mark.asyncio
134
164
  async def it_emits_insert_events_for_new_files(tmp_dir):
135
165
  """watch() yields insert events when a new file is created."""
136
- db = await AsyncDirSQL(
166
+ db = AsyncDirSQL(
137
167
  tmp_dir,
138
168
  tables=[
139
169
  Table(
@@ -143,6 +173,7 @@ def describe_AsyncDirSQL():
143
173
  ),
144
174
  ],
145
175
  )
176
+ await db.ready()
146
177
 
147
178
  events = []
148
179
 
@@ -179,7 +210,7 @@ def describe_AsyncDirSQL():
179
210
  with open(os.path.join(tmp_dir, "doomed.json"), "w") as f:
180
211
  json.dump({"name": "doomed"}, f)
181
212
 
182
- db = await AsyncDirSQL(
213
+ db = AsyncDirSQL(
183
214
  tmp_dir,
184
215
  tables=[
185
216
  Table(
@@ -189,6 +220,7 @@ def describe_AsyncDirSQL():
189
220
  ),
190
221
  ],
191
222
  )
223
+ await db.ready()
192
224
 
193
225
  # Confirm initial data
194
226
  results = await db.query("SELECT * FROM items")
@@ -228,7 +260,7 @@ def describe_AsyncDirSQL():
228
260
  with open(os.path.join(tmp_dir, "item.json"), "w") as f:
229
261
  json.dump({"name": "draft"}, f)
230
262
 
231
- db = await AsyncDirSQL(
263
+ db = AsyncDirSQL(
232
264
  tmp_dir,
233
265
  tables=[
234
266
  Table(
@@ -238,6 +270,7 @@ def describe_AsyncDirSQL():
238
270
  ),
239
271
  ],
240
272
  )
273
+ await db.ready()
241
274
 
242
275
  events = []
243
276
 
@@ -267,7 +300,7 @@ def describe_AsyncDirSQL():
267
300
  @pytest.mark.asyncio
268
301
  async def it_emits_error_events_for_bad_extract(tmp_dir):
269
302
  """watch() yields error events when extract lambda fails."""
270
- db = await AsyncDirSQL(
303
+ db = AsyncDirSQL(
271
304
  tmp_dir,
272
305
  tables=[
273
306
  Table(
@@ -277,6 +310,7 @@ def describe_AsyncDirSQL():
277
310
  ),
278
311
  ],
279
312
  )
313
+ await db.ready()
280
314
 
281
315
  events = []
282
316
 
@@ -305,7 +339,7 @@ def describe_AsyncDirSQL():
305
339
  @pytest.mark.asyncio
306
340
  async def it_updates_db_on_file_changes(tmp_dir):
307
341
  """The database is kept in sync with file system changes."""
308
- db = await AsyncDirSQL(
342
+ db = AsyncDirSQL(
309
343
  tmp_dir,
310
344
  tables=[
311
345
  Table(
@@ -315,6 +349,7 @@ def describe_AsyncDirSQL():
315
349
  ),
316
350
  ],
317
351
  )
352
+ await db.ready()
318
353
 
319
354
  # Initially empty
320
355
  results = await db.query("SELECT * FROM items")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes