dirsql 0.0.23__tar.gz → 0.0.25__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.
- {dirsql-0.0.23 → dirsql-0.0.25}/Cargo.lock +8 -20
- dirsql-0.0.25/Cargo.toml +18 -0
- {dirsql-0.0.23 → dirsql-0.0.25}/PKG-INFO +1 -1
- {dirsql-0.0.23 → dirsql-0.0.25}/packages/python/Cargo.toml +6 -5
- dirsql-0.0.25/packages/python/src/lib.rs +315 -0
- {dirsql-0.0.23 → dirsql-0.0.25}/packages/python/tests/integration/test_async_dirsql.py +13 -7
- dirsql-0.0.25/packages/python/tests/integration/test_binding.py +266 -0
- dirsql-0.0.25/packages/python/tests/integration/test_docs_gaps.py +374 -0
- {dirsql-0.0.23/packages/core → dirsql-0.0.25/packages/rust}/Cargo.toml +14 -9
- dirsql-0.0.25/packages/rust/README.md +22 -0
- {dirsql-0.0.23/packages/core → dirsql-0.0.25/packages/rust}/benches/db_bench.rs +1 -1
- {dirsql-0.0.23/packages/core → dirsql-0.0.25/packages/rust}/benches/differ_bench.rs +2 -2
- {dirsql-0.0.23/packages/core → dirsql-0.0.25/packages/rust}/benches/matcher_bench.rs +1 -1
- {dirsql-0.0.23/packages/core → dirsql-0.0.25/packages/rust}/benches/scanner_bench.rs +2 -2
- {dirsql-0.0.23/packages/core → dirsql-0.0.25/packages/rust}/src/differ.rs +71 -31
- dirsql-0.0.25/packages/rust/src/lib.rs +713 -0
- dirsql-0.0.25/packages/rust/tests/async_sdk.rs +174 -0
- dirsql-0.0.25/packages/rust/tests/docs_examples.rs +812 -0
- dirsql-0.0.25/packages/rust/tests/docs_gaps.rs +327 -0
- dirsql-0.0.25/packages/rust/tests/from_config.rs +256 -0
- dirsql-0.0.25/packages/rust/tests/sdk.rs +390 -0
- {dirsql-0.0.23 → dirsql-0.0.25}/pyproject.toml +1 -1
- dirsql-0.0.23/Cargo.toml +0 -3
- dirsql-0.0.23/packages/core/README.md +0 -29
- dirsql-0.0.23/packages/core/src/lib.rs +0 -7
- dirsql-0.0.23/packages/python/src/lib.rs +0 -848
- {dirsql-0.0.23 → dirsql-0.0.25}/README.md +0 -0
- {dirsql-0.0.23 → dirsql-0.0.25}/packages/python/README.md +0 -0
- {dirsql-0.0.23 → dirsql-0.0.25}/packages/python/tests/__init__.py +0 -0
- {dirsql-0.0.23 → dirsql-0.0.25}/packages/python/tests/conftest.py +0 -0
- {dirsql-0.0.23 → dirsql-0.0.25}/packages/python/tests/integration/__init__.py +0 -0
- {dirsql-0.0.23 → dirsql-0.0.25}/packages/python/tests/integration/test_dirsql.py +0 -0
- {dirsql-0.0.23 → dirsql-0.0.25}/packages/python/tests/integration/test_docs_examples.py +0 -0
- {dirsql-0.0.23 → dirsql-0.0.25}/packages/python/tests/integration/test_from_config.py +0 -0
- {dirsql-0.0.23/packages/core → dirsql-0.0.25/packages/rust}/src/config.rs +0 -0
- {dirsql-0.0.23/packages/core → dirsql-0.0.25/packages/rust}/src/db.rs +0 -0
- {dirsql-0.0.23/packages/core → dirsql-0.0.25/packages/rust}/src/matcher.rs +0 -0
- {dirsql-0.0.23/packages/core → dirsql-0.0.25/packages/rust}/src/parser.rs +0 -0
- {dirsql-0.0.23/packages/core → dirsql-0.0.25/packages/rust}/src/scanner.rs +0 -0
- {dirsql-0.0.23/packages/core → dirsql-0.0.25/packages/rust}/src/watcher.rs +0 -0
- {dirsql-0.0.23 → dirsql-0.0.25}/python/dirsql/__init__.py +0 -0
- {dirsql-0.0.23 → dirsql-0.0.25}/python/dirsql/_async.py +0 -0
- {dirsql-0.0.23 → dirsql-0.0.25}/python/dirsql/test_async.py +0 -0
|
@@ -253,18 +253,12 @@ checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1"
|
|
|
253
253
|
[[package]]
|
|
254
254
|
name = "dirsql"
|
|
255
255
|
version = "0.1.0"
|
|
256
|
-
dependencies = [
|
|
257
|
-
"dirsql-core",
|
|
258
|
-
"pyo3",
|
|
259
|
-
"regex",
|
|
260
|
-
]
|
|
261
|
-
|
|
262
|
-
[[package]]
|
|
263
|
-
name = "dirsql-core"
|
|
264
|
-
version = "0.1.0"
|
|
265
256
|
dependencies = [
|
|
266
257
|
"criterion",
|
|
267
258
|
"csv",
|
|
259
|
+
"futures-channel",
|
|
260
|
+
"futures-executor",
|
|
261
|
+
"futures-util",
|
|
268
262
|
"globset",
|
|
269
263
|
"notify",
|
|
270
264
|
"regex",
|
|
@@ -274,6 +268,7 @@ dependencies = [
|
|
|
274
268
|
"serde_yaml",
|
|
275
269
|
"tempfile",
|
|
276
270
|
"thiserror",
|
|
271
|
+
"tokio",
|
|
277
272
|
"toml",
|
|
278
273
|
"walkdir",
|
|
279
274
|
]
|
|
@@ -282,7 +277,7 @@ dependencies = [
|
|
|
282
277
|
name = "dirsql-napi"
|
|
283
278
|
version = "0.1.0"
|
|
284
279
|
dependencies = [
|
|
285
|
-
"dirsql
|
|
280
|
+
"dirsql",
|
|
286
281
|
"napi",
|
|
287
282
|
"napi-build",
|
|
288
283
|
"napi-derive",
|
|
@@ -290,18 +285,11 @@ dependencies = [
|
|
|
290
285
|
]
|
|
291
286
|
|
|
292
287
|
[[package]]
|
|
293
|
-
name = "dirsql-
|
|
288
|
+
name = "dirsql-py-ext"
|
|
294
289
|
version = "0.1.0"
|
|
295
290
|
dependencies = [
|
|
296
|
-
"dirsql
|
|
297
|
-
"
|
|
298
|
-
"futures-core",
|
|
299
|
-
"futures-executor",
|
|
300
|
-
"futures-util",
|
|
301
|
-
"serde_json",
|
|
302
|
-
"tempfile",
|
|
303
|
-
"thiserror",
|
|
304
|
-
"tokio",
|
|
291
|
+
"dirsql",
|
|
292
|
+
"pyo3",
|
|
305
293
|
]
|
|
306
294
|
|
|
307
295
|
[[package]]
|
dirsql-0.0.25/Cargo.toml
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[workspace]
|
|
2
|
+
members = ["packages/python", "packages/rust"]
|
|
3
|
+
resolver = "2"
|
|
4
|
+
|
|
5
|
+
[workspace.package]
|
|
6
|
+
version = "0.1.0"
|
|
7
|
+
edition = "2024"
|
|
8
|
+
license = "MIT"
|
|
9
|
+
repository = "https://github.com/thekevinscott/dirsql"
|
|
10
|
+
|
|
11
|
+
[workspace.dependencies]
|
|
12
|
+
dirsql = { path = "packages/rust" }
|
|
13
|
+
serde = { version = "1", features = ["derive"] }
|
|
14
|
+
serde_json = "1"
|
|
15
|
+
regex = "1"
|
|
16
|
+
thiserror = "2"
|
|
17
|
+
tokio = { version = "1", features = ["rt", "sync"] }
|
|
18
|
+
futures-channel = "0.3"
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
[package]
|
|
2
|
-
name = "dirsql"
|
|
3
|
-
version =
|
|
4
|
-
edition =
|
|
2
|
+
name = "dirsql-py-ext"
|
|
3
|
+
version.workspace = true
|
|
4
|
+
edition.workspace = true
|
|
5
|
+
publish = false
|
|
5
6
|
readme = "README.md"
|
|
6
7
|
|
|
7
8
|
[lib]
|
|
9
|
+
name = "dirsql_py_ext"
|
|
8
10
|
crate-type = ["cdylib", "rlib"]
|
|
9
11
|
|
|
10
12
|
[dependencies]
|
|
11
|
-
dirsql
|
|
13
|
+
dirsql.workspace = true
|
|
12
14
|
pyo3 = { version = "0.28", features = ["extension-module"], optional = true }
|
|
13
|
-
regex = "1"
|
|
14
15
|
|
|
15
16
|
[features]
|
|
16
17
|
default = []
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
//! PyO3 binding for `dirsql`. Intentionally thin: all orchestration lives in
|
|
2
|
+
//! `dirsql::DirSQL`. This layer only:
|
|
3
|
+
//!
|
|
4
|
+
//! - wraps a Python `extract` callable in a Rust closure (acquiring the GIL as
|
|
5
|
+
//! needed) so it can be handed to [`dirsql::Table`]
|
|
6
|
+
//! - converts row dicts, values, and events between Python and Rust
|
|
7
|
+
//! - forwards `new` / `from_config` / `query` / `_start_watcher` /
|
|
8
|
+
//! `_poll_events` to the corresponding `DirSQL` methods
|
|
9
|
+
//!
|
|
10
|
+
//! The Python-side async wrapper (`dirsql._async.DirSQL`) drives this binding
|
|
11
|
+
//! via `asyncio.to_thread`.
|
|
12
|
+
|
|
13
|
+
#[cfg(feature = "extension-module")]
|
|
14
|
+
mod python {
|
|
15
|
+
use ::dirsql::{DirSQL, Row, RowEvent, Table, Value};
|
|
16
|
+
use pyo3::exceptions::PyRuntimeError;
|
|
17
|
+
use pyo3::prelude::*;
|
|
18
|
+
use pyo3::types::{PyDict, PyList};
|
|
19
|
+
use std::collections::HashMap;
|
|
20
|
+
use std::time::Duration;
|
|
21
|
+
|
|
22
|
+
// -- Public PyO3 classes ------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/// A table definition. Mirrors `dirsql::Table` but holds a Python
|
|
25
|
+
/// callable for `extract`.
|
|
26
|
+
#[pyclass(name = "Table", frozen)]
|
|
27
|
+
struct PyTable {
|
|
28
|
+
#[pyo3(get)]
|
|
29
|
+
ddl: String,
|
|
30
|
+
#[pyo3(get)]
|
|
31
|
+
glob: String,
|
|
32
|
+
extract: Py<PyAny>,
|
|
33
|
+
#[pyo3(get)]
|
|
34
|
+
strict: bool,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#[pymethods]
|
|
38
|
+
impl PyTable {
|
|
39
|
+
#[new]
|
|
40
|
+
#[pyo3(signature = (*, ddl, glob, extract, strict=false))]
|
|
41
|
+
fn new(ddl: String, glob: String, extract: Py<PyAny>, strict: bool) -> Self {
|
|
42
|
+
PyTable {
|
|
43
|
+
ddl,
|
|
44
|
+
glob,
|
|
45
|
+
extract,
|
|
46
|
+
strict,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// A row event produced by the watch loop.
|
|
52
|
+
#[pyclass(name = "RowEvent", frozen)]
|
|
53
|
+
struct PyRowEvent {
|
|
54
|
+
#[pyo3(get)]
|
|
55
|
+
table: String,
|
|
56
|
+
#[pyo3(get)]
|
|
57
|
+
action: String,
|
|
58
|
+
#[pyo3(get)]
|
|
59
|
+
row: Option<Py<PyDict>>,
|
|
60
|
+
#[pyo3(get)]
|
|
61
|
+
old_row: Option<Py<PyDict>>,
|
|
62
|
+
#[pyo3(get)]
|
|
63
|
+
error: Option<String>,
|
|
64
|
+
#[pyo3(get)]
|
|
65
|
+
file_path: Option<String>,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#[pymethods]
|
|
69
|
+
impl PyRowEvent {
|
|
70
|
+
fn __repr__(&self) -> String {
|
|
71
|
+
format!("RowEvent(table={:?}, action={:?})", self.table, self.action)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Synchronous binding class. `dirsql._async.DirSQL` wraps it with
|
|
76
|
+
/// `asyncio.to_thread` to produce the async public API.
|
|
77
|
+
#[pyclass(name = "DirSQL")]
|
|
78
|
+
struct PyDirSQL {
|
|
79
|
+
inner: DirSQL,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[pymethods]
|
|
83
|
+
impl PyDirSQL {
|
|
84
|
+
#[new]
|
|
85
|
+
#[pyo3(signature = (root, *, tables, ignore=None))]
|
|
86
|
+
fn new(
|
|
87
|
+
py: Python<'_>,
|
|
88
|
+
root: String,
|
|
89
|
+
tables: Vec<PyRef<'_, PyTable>>,
|
|
90
|
+
ignore: Option<Vec<String>>,
|
|
91
|
+
) -> PyResult<Self> {
|
|
92
|
+
let rust_tables: Vec<Table> = tables.iter().map(|t| build_table(py, t)).collect();
|
|
93
|
+
|
|
94
|
+
let inner = py
|
|
95
|
+
.detach(move || match ignore {
|
|
96
|
+
Some(ig) => DirSQL::with_ignore(root, rust_tables, ig),
|
|
97
|
+
None => DirSQL::new(root, rust_tables),
|
|
98
|
+
})
|
|
99
|
+
.map_err(to_py_err)?;
|
|
100
|
+
|
|
101
|
+
Ok(Self { inner })
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#[classmethod]
|
|
105
|
+
fn from_config(
|
|
106
|
+
_cls: &Bound<'_, pyo3::types::PyType>,
|
|
107
|
+
py: Python<'_>,
|
|
108
|
+
path: String,
|
|
109
|
+
) -> PyResult<Self> {
|
|
110
|
+
let inner = py
|
|
111
|
+
.detach(move || DirSQL::from_config_path(&path))
|
|
112
|
+
.map_err(to_py_err)?;
|
|
113
|
+
Ok(Self { inner })
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
fn query(&self, py: Python<'_>, sql: String) -> PyResult<Py<PyList>> {
|
|
117
|
+
let db = self.inner.clone();
|
|
118
|
+
let rows = py.detach(move || db.query(&sql)).map_err(to_py_err)?;
|
|
119
|
+
|
|
120
|
+
let list = PyList::empty(py);
|
|
121
|
+
for row in rows {
|
|
122
|
+
list.append(value_row_to_py_dict(py, &row)?)?;
|
|
123
|
+
}
|
|
124
|
+
Ok(list.unbind())
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
fn _start_watcher(&self, py: Python<'_>) -> PyResult<()> {
|
|
128
|
+
let db = self.inner.clone();
|
|
129
|
+
py.detach(move || db.start_watching()).map_err(to_py_err)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fn _poll_events(&self, py: Python<'_>, timeout_ms: u64) -> PyResult<Vec<PyRowEvent>> {
|
|
133
|
+
let db = self.inner.clone();
|
|
134
|
+
let events = py
|
|
135
|
+
.detach(move || db.poll_events(Duration::from_millis(timeout_ms)))
|
|
136
|
+
.map_err(to_py_err)?;
|
|
137
|
+
|
|
138
|
+
events.iter().map(|e| row_event_to_py(py, e)).collect()
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// -- Helpers ------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
fn build_table(py: Python<'_>, t: &PyTable) -> Table {
|
|
145
|
+
let extract_ref = t.extract.clone_ref(py);
|
|
146
|
+
let mut table = Table::try_new(
|
|
147
|
+
t.ddl.clone(),
|
|
148
|
+
t.glob.clone(),
|
|
149
|
+
make_extract_closure(extract_ref),
|
|
150
|
+
);
|
|
151
|
+
table.strict = t.strict;
|
|
152
|
+
table
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
|
|
156
|
+
|
|
157
|
+
fn make_extract_closure(
|
|
158
|
+
extract: Py<PyAny>,
|
|
159
|
+
) -> impl Fn(&str, &str) -> std::result::Result<Vec<Row>, BoxError> + Send + Sync + 'static
|
|
160
|
+
{
|
|
161
|
+
move |path: &str, content: &str| {
|
|
162
|
+
Python::attach(|py| -> std::result::Result<Vec<Row>, BoxError> {
|
|
163
|
+
let result = extract
|
|
164
|
+
.call1(py, (path, content))
|
|
165
|
+
.map_err(|e| -> BoxError { Box::new(ExtractError(e.to_string())) })?;
|
|
166
|
+
let raw: Vec<HashMap<String, Py<PyAny>>> = result
|
|
167
|
+
.extract(py)
|
|
168
|
+
.map_err(|e: PyErr| -> BoxError { Box::new(ExtractError(e.to_string())) })?;
|
|
169
|
+
|
|
170
|
+
let mut rows = Vec::with_capacity(raw.len());
|
|
171
|
+
for r in &raw {
|
|
172
|
+
rows.push(
|
|
173
|
+
convert_py_row(py, r)
|
|
174
|
+
.map_err(|e| -> BoxError { Box::new(ExtractError(e.to_string())) })?,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
Ok(rows)
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#[derive(Debug)]
|
|
183
|
+
struct ExtractError(String);
|
|
184
|
+
impl std::fmt::Display for ExtractError {
|
|
185
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
186
|
+
f.write_str(&self.0)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
impl std::error::Error for ExtractError {}
|
|
190
|
+
|
|
191
|
+
fn to_py_err<E: std::fmt::Display>(e: E) -> PyErr {
|
|
192
|
+
PyRuntimeError::new_err(e.to_string())
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
fn row_event_to_py(py: Python<'_>, event: &RowEvent) -> PyResult<PyRowEvent> {
|
|
196
|
+
Ok(match event {
|
|
197
|
+
RowEvent::Insert {
|
|
198
|
+
table,
|
|
199
|
+
row,
|
|
200
|
+
file_path,
|
|
201
|
+
} => PyRowEvent {
|
|
202
|
+
table: table.clone(),
|
|
203
|
+
action: "insert".to_string(),
|
|
204
|
+
row: Some(value_row_to_py_dict(py, row)?),
|
|
205
|
+
old_row: None,
|
|
206
|
+
error: None,
|
|
207
|
+
file_path: Some(file_path.clone()),
|
|
208
|
+
},
|
|
209
|
+
RowEvent::Update {
|
|
210
|
+
table,
|
|
211
|
+
old_row,
|
|
212
|
+
new_row,
|
|
213
|
+
file_path,
|
|
214
|
+
} => PyRowEvent {
|
|
215
|
+
table: table.clone(),
|
|
216
|
+
action: "update".to_string(),
|
|
217
|
+
row: Some(value_row_to_py_dict(py, new_row)?),
|
|
218
|
+
old_row: Some(value_row_to_py_dict(py, old_row)?),
|
|
219
|
+
error: None,
|
|
220
|
+
file_path: Some(file_path.clone()),
|
|
221
|
+
},
|
|
222
|
+
RowEvent::Delete {
|
|
223
|
+
table,
|
|
224
|
+
row,
|
|
225
|
+
file_path,
|
|
226
|
+
} => PyRowEvent {
|
|
227
|
+
table: table.clone(),
|
|
228
|
+
action: "delete".to_string(),
|
|
229
|
+
row: Some(value_row_to_py_dict(py, row)?),
|
|
230
|
+
old_row: None,
|
|
231
|
+
error: None,
|
|
232
|
+
file_path: Some(file_path.clone()),
|
|
233
|
+
},
|
|
234
|
+
RowEvent::Error { file_path, error } => PyRowEvent {
|
|
235
|
+
table: String::new(),
|
|
236
|
+
action: "error".to_string(),
|
|
237
|
+
row: None,
|
|
238
|
+
old_row: None,
|
|
239
|
+
error: Some(error.clone()),
|
|
240
|
+
file_path: Some(file_path.to_string_lossy().to_string()),
|
|
241
|
+
},
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
fn value_row_to_py_dict(py: Python<'_>, row: &Row) -> PyResult<Py<PyDict>> {
|
|
246
|
+
let dict = PyDict::new(py);
|
|
247
|
+
for (key, value) in row {
|
|
248
|
+
dict.set_item(key, value_to_py(py, value))?;
|
|
249
|
+
}
|
|
250
|
+
Ok(dict.unbind())
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
fn convert_py_row(
|
|
254
|
+
py: Python<'_>,
|
|
255
|
+
py_row: &HashMap<String, Py<PyAny>>,
|
|
256
|
+
) -> PyResult<HashMap<String, Value>> {
|
|
257
|
+
let mut row = HashMap::new();
|
|
258
|
+
for (key, val) in py_row {
|
|
259
|
+
row.insert(key.clone(), py_to_value(py, val)?);
|
|
260
|
+
}
|
|
261
|
+
Ok(row)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
fn py_to_value(py: Python<'_>, obj: &Py<PyAny>) -> PyResult<Value> {
|
|
265
|
+
let bound = obj.bind(py);
|
|
266
|
+
|
|
267
|
+
if bound.is_none() {
|
|
268
|
+
return Ok(Value::Null);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// bool must precede int (bool is a subclass of int in Python).
|
|
272
|
+
if bound.is_instance_of::<pyo3::types::PyBool>() {
|
|
273
|
+
let b: bool = bound.extract()?;
|
|
274
|
+
return Ok(Value::Integer(if b { 1 } else { 0 }));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if let Ok(i) = bound.extract::<i64>() {
|
|
278
|
+
return Ok(Value::Integer(i));
|
|
279
|
+
}
|
|
280
|
+
if let Ok(f) = bound.extract::<f64>() {
|
|
281
|
+
return Ok(Value::Real(f));
|
|
282
|
+
}
|
|
283
|
+
if let Ok(s) = bound.extract::<String>() {
|
|
284
|
+
return Ok(Value::Text(s));
|
|
285
|
+
}
|
|
286
|
+
if let Ok(b) = bound.extract::<Vec<u8>>() {
|
|
287
|
+
return Ok(Value::Blob(b));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Fall back to the Python repr.
|
|
291
|
+
Ok(Value::Text(bound.str()?.to_string()))
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
fn value_to_py(py: Python<'_>, value: &Value) -> Py<PyAny> {
|
|
295
|
+
match value {
|
|
296
|
+
Value::Null => py.None(),
|
|
297
|
+
Value::Integer(i) => i.into_pyobject(py).unwrap().into_any().unbind(),
|
|
298
|
+
Value::Real(f) => f.into_pyobject(py).unwrap().into_any().unbind(),
|
|
299
|
+
Value::Text(s) => s.into_pyobject(py).unwrap().into_any().unbind(),
|
|
300
|
+
Value::Blob(b) => b.into_pyobject(py).unwrap().unbind(),
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// -- Module registration ------------------------------------------------
|
|
305
|
+
|
|
306
|
+
#[pymodule]
|
|
307
|
+
#[pyo3(name = "_dirsql")]
|
|
308
|
+
fn py_dirsql_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
309
|
+
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
|
|
310
|
+
m.add_class::<PyTable>()?;
|
|
311
|
+
m.add_class::<PyDirSQL>()?;
|
|
312
|
+
m.add_class::<PyRowEvent>()?;
|
|
313
|
+
Ok(())
|
|
314
|
+
}
|
|
315
|
+
}
|
|
@@ -180,7 +180,7 @@ def describe_DirSQL_async():
|
|
|
180
180
|
async def collect_events():
|
|
181
181
|
async for event in db.watch():
|
|
182
182
|
events.append(event)
|
|
183
|
-
if
|
|
183
|
+
if event.action == "insert":
|
|
184
184
|
break
|
|
185
185
|
|
|
186
186
|
task = asyncio.create_task(collect_events())
|
|
@@ -188,9 +188,15 @@ def describe_DirSQL_async():
|
|
|
188
188
|
# Give the watcher time to start
|
|
189
189
|
await asyncio.sleep(0.3)
|
|
190
190
|
|
|
191
|
-
# Create a new file
|
|
192
|
-
|
|
191
|
+
# Create a new file atomically -- write to a sibling tmp path
|
|
192
|
+
# then rename into place. Without this the watcher can fire on
|
|
193
|
+
# the empty file between open() and write, producing a spurious
|
|
194
|
+
# error event ahead of the insert.
|
|
195
|
+
final = os.path.join(tmp_dir, "new_item.json")
|
|
196
|
+
tmp = final + ".tmp"
|
|
197
|
+
with open(tmp, "w") as f:
|
|
193
198
|
json.dump({"name": "apple"}, f)
|
|
199
|
+
os.replace(tmp, final)
|
|
194
200
|
|
|
195
201
|
# Wait for events with timeout
|
|
196
202
|
try:
|
|
@@ -198,10 +204,10 @@ def describe_DirSQL_async():
|
|
|
198
204
|
except asyncio.TimeoutError:
|
|
199
205
|
pytest.fail("Timed out waiting for watch events")
|
|
200
206
|
|
|
201
|
-
|
|
202
|
-
assert
|
|
203
|
-
assert
|
|
204
|
-
assert
|
|
207
|
+
insert = next((e for e in events if e.action == "insert"), None)
|
|
208
|
+
assert insert is not None, f"no insert event in {events!r}"
|
|
209
|
+
assert insert.table == "items"
|
|
210
|
+
assert insert.row["name"] == "apple"
|
|
205
211
|
|
|
206
212
|
@pytest.mark.asyncio
|
|
207
213
|
async def it_emits_delete_events_for_removed_files(tmp_dir):
|