query-cascade 0.3.5__tar.gz → 0.3.7__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.
- {query_cascade-0.3.5 → query_cascade-0.3.7}/PKG-INFO +12 -1
- {query_cascade-0.3.5 → query_cascade-0.3.7}/README.md +11 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/pyproject.toml +1 -1
- {query_cascade-0.3.5 → query_cascade-0.3.7}/src/cascade/_store.py +33 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/src/cascade/engine.py +43 -1
- {query_cascade-0.3.5 → query_cascade-0.3.7}/src/query_cascade.egg-info/PKG-INFO +12 -1
- {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_engine.py +46 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/setup.cfg +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/src/cascade/__init__.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/src/cascade/_canonical.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/src/cascade/_disk_cache.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/src/cascade/_errors.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/src/cascade/_evaluator.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/src/cascade/_graph_export.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/src/cascade/_persistence.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/src/cascade/_runtime.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/src/cascade/_scheduler.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/src/cascade/_serde.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/src/cascade/_state.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/src/cascade/_synthetic_graph.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/src/query_cascade.egg-info/SOURCES.txt +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/src/query_cascade.egg-info/dependency_links.txt +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/src/query_cascade.egg-info/requires.txt +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/src/query_cascade.egg-info/top_level.txt +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_async.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_async_coverage.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_code_versioning.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_dependency_parallelism.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_disk_cache.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_engine_compute_many.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_engine_concurrency.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_engine_persistence_and_tracing.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_engine_private.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_engine_private2.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_engine_sweep.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_error_caching.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_error_caching_persistence.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_evaluator_and_synthetic_coverage.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_examples.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_fixed_point.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_internal_invariants.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_memoize_false.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_performance.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_repro_touch_memo_keyerror.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_scale_behavior.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_serde.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_stateful_engine_invariants.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_store_coverage.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_subgraph_stats_export.py +0 -0
- {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_sweep_unaccessed.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: query-cascade
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.7
|
|
4
4
|
Summary: Minimal demand-driven query framework for incremental computation.
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -207,6 +207,17 @@ def parse(source: str):
|
|
|
207
207
|
```
|
|
208
208
|
If a query throws an exception, it gets cached just like a regular return value. Subsequent calls instantly re-raise the exception, preserving incremental evaluation speed during error states. Cached exceptions can also be hydrated from the persistent disk cache.
|
|
209
209
|
|
|
210
|
+
### Input Debouncing & Transactions
|
|
211
|
+
When updating multiple inputs, intermediate read states ("flapping") can cause inconsistent evaluations. Use `engine.transaction()` to batch updates so they are committed atomically.
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
with engine.transaction():
|
|
215
|
+
theme.set("dark")
|
|
216
|
+
layout.set("grid")
|
|
217
|
+
|
|
218
|
+
# Queries will only re-evaluate once observing both changes simultaneously.
|
|
219
|
+
```
|
|
220
|
+
|
|
210
221
|
### Code-Aware Caching (Automatic Invalidation)
|
|
211
222
|
Cascade automatically inspects the Python bytecode of your `@engine.query` and `@engine.input` functions. If you edit a function's logic and the module is hot-reloaded (or you restart your script), Cascade compares the new function's bytecode hash against the previously cached logic. If the logic has changed, Cascade immediately invalidates that function's memory and persistent disk caches, avoiding stale results without requiring manual cache wipes. Code formatting and comments do not affect the bytecode hash.
|
|
212
223
|
|
|
@@ -188,6 +188,17 @@ def parse(source: str):
|
|
|
188
188
|
```
|
|
189
189
|
If a query throws an exception, it gets cached just like a regular return value. Subsequent calls instantly re-raise the exception, preserving incremental evaluation speed during error states. Cached exceptions can also be hydrated from the persistent disk cache.
|
|
190
190
|
|
|
191
|
+
### Input Debouncing & Transactions
|
|
192
|
+
When updating multiple inputs, intermediate read states ("flapping") can cause inconsistent evaluations. Use `engine.transaction()` to batch updates so they are committed atomically.
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
with engine.transaction():
|
|
196
|
+
theme.set("dark")
|
|
197
|
+
layout.set("grid")
|
|
198
|
+
|
|
199
|
+
# Queries will only re-evaluate once observing both changes simultaneously.
|
|
200
|
+
```
|
|
201
|
+
|
|
191
202
|
### Code-Aware Caching (Automatic Invalidation)
|
|
192
203
|
Cascade automatically inspects the Python bytecode of your `@engine.query` and `@engine.input` functions. If you edit a function's logic and the module is hot-reloaded (or you restart your script), Cascade compares the new function's bytecode hash against the previously cached logic. If the logic has changed, Cascade immediately invalidates that function's memory and persistent disk caches, avoiding stale results without requiring manual cache wipes. Code formatting and comments do not affect the bytecode hash.
|
|
193
204
|
|
|
@@ -319,6 +319,39 @@ class GraphStore:
|
|
|
319
319
|
self.trace_event("input_set", trace_key, detail=f"changed_at={changed_at}")
|
|
320
320
|
return self.revision
|
|
321
321
|
|
|
322
|
+
def set_inputs(
|
|
323
|
+
self,
|
|
324
|
+
updates: list[tuple[str, tuple[Any, ...], Any]],
|
|
325
|
+
*,
|
|
326
|
+
bump_cancel_epoch: bool = True,
|
|
327
|
+
) -> int:
|
|
328
|
+
with self.lock:
|
|
329
|
+
if not updates:
|
|
330
|
+
return self.revision
|
|
331
|
+
self.revision += 1
|
|
332
|
+
if bump_cancel_epoch:
|
|
333
|
+
self.cancel_epoch += 1
|
|
334
|
+
for input_id, args, value in updates:
|
|
335
|
+
key = (input_id, args)
|
|
336
|
+
versions = self.inputs.setdefault(key, [])
|
|
337
|
+
current = versions[-1] if versions else None
|
|
338
|
+
value_hash = self.stable_hash(value)
|
|
339
|
+
if current is not None and current.value_hash == value_hash:
|
|
340
|
+
changed_at = current.changed_at
|
|
341
|
+
else:
|
|
342
|
+
changed_at = self.revision
|
|
343
|
+
versions.append(
|
|
344
|
+
InputVersion(
|
|
345
|
+
revision=self.revision,
|
|
346
|
+
changed_at=changed_at,
|
|
347
|
+
value_hash=value_hash,
|
|
348
|
+
value=value,
|
|
349
|
+
)
|
|
350
|
+
)
|
|
351
|
+
trace_key = ("input", input_id, args)
|
|
352
|
+
self.trace_event("input_set", trace_key, detail=f"changed_at={changed_at}")
|
|
353
|
+
return self.revision
|
|
354
|
+
|
|
322
355
|
def inspect_graph(self) -> dict[str, Any]:
|
|
323
356
|
with self.lock:
|
|
324
357
|
nodes = [self.key_to_str(k) for k in self.memos.keys()]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import contextvars
|
|
4
5
|
import inspect
|
|
5
6
|
import concurrent.futures
|
|
6
7
|
import os
|
|
@@ -38,6 +39,30 @@ __all__ = [
|
|
|
38
39
|
]
|
|
39
40
|
|
|
40
41
|
|
|
42
|
+
class EngineTransaction:
|
|
43
|
+
def __init__(self, engine: Engine) -> None:
|
|
44
|
+
self.engine = engine
|
|
45
|
+
self.updates: list[tuple[str, tuple[Any, ...], Any]] = []
|
|
46
|
+
self._token: contextvars.Token | None = None
|
|
47
|
+
|
|
48
|
+
def __enter__(self) -> EngineTransaction:
|
|
49
|
+
if _active_transaction.get() is not None:
|
|
50
|
+
raise RuntimeError("Nested transactions are not supported")
|
|
51
|
+
self._token = _active_transaction.set(self)
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
55
|
+
if self._token is not None:
|
|
56
|
+
_active_transaction.reset(self._token)
|
|
57
|
+
if exc_type is None and self.updates:
|
|
58
|
+
self.engine._set_inputs(self.updates)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
_active_transaction: contextvars.ContextVar[EngineTransaction | None] = contextvars.ContextVar(
|
|
62
|
+
"active_transaction", default=None
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
41
66
|
class _InputHandle:
|
|
42
67
|
def __init__(self, engine: Engine, fn: Callable[..., Any]) -> None:
|
|
43
68
|
self._engine = engine
|
|
@@ -61,13 +86,19 @@ class _InputHandle:
|
|
|
61
86
|
|
|
62
87
|
return self._engine._read_input(self._id, self._fn, args, snapshot=snapshot)
|
|
63
88
|
|
|
64
|
-
def set(self, *args: Any, value: Any = _UNSET) -> int:
|
|
89
|
+
def set(self, *args: Any, value: Any = _UNSET) -> int | None:
|
|
65
90
|
if value is _UNSET:
|
|
66
91
|
if not args:
|
|
67
92
|
raise TypeError("set() requires input value")
|
|
68
93
|
*input_args, resolved_value = args
|
|
69
94
|
args = tuple(input_args)
|
|
70
95
|
value = resolved_value
|
|
96
|
+
|
|
97
|
+
tx = _active_transaction.get()
|
|
98
|
+
if tx is not None and tx.engine is self._engine:
|
|
99
|
+
tx.updates.append((self._id, tuple(args), value))
|
|
100
|
+
return None
|
|
101
|
+
|
|
71
102
|
return self._engine._set_input(self._id, tuple(args), value)
|
|
72
103
|
|
|
73
104
|
@property
|
|
@@ -245,6 +276,9 @@ class Engine:
|
|
|
245
276
|
raise PersistentCacheError("engine has no persistent cache; pass cache_dir= to Engine")
|
|
246
277
|
self._disk.clear()
|
|
247
278
|
|
|
279
|
+
def transaction(self) -> EngineTransaction:
|
|
280
|
+
return EngineTransaction(self)
|
|
281
|
+
|
|
248
282
|
def input(self, fn: Callable[..., Any]) -> _InputHandle:
|
|
249
283
|
handle = _InputHandle(self, fn)
|
|
250
284
|
self._store.register_input(handle.id, fn)
|
|
@@ -502,6 +536,14 @@ class Engine:
|
|
|
502
536
|
) -> int:
|
|
503
537
|
return self._store.set_input(input_id, args, value, bump_cancel_epoch=bump_cancel_epoch)
|
|
504
538
|
|
|
539
|
+
def _set_inputs(
|
|
540
|
+
self,
|
|
541
|
+
updates: list[tuple[str, tuple[Any, ...], Any]],
|
|
542
|
+
*,
|
|
543
|
+
bump_cancel_epoch: bool = True,
|
|
544
|
+
) -> int:
|
|
545
|
+
return self._store.set_inputs(updates, bump_cancel_epoch=bump_cancel_epoch)
|
|
546
|
+
|
|
505
547
|
def _read_input(
|
|
506
548
|
self,
|
|
507
549
|
input_id: str,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: query-cascade
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.7
|
|
4
4
|
Summary: Minimal demand-driven query framework for incremental computation.
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -207,6 +207,17 @@ def parse(source: str):
|
|
|
207
207
|
```
|
|
208
208
|
If a query throws an exception, it gets cached just like a regular return value. Subsequent calls instantly re-raise the exception, preserving incremental evaluation speed during error states. Cached exceptions can also be hydrated from the persistent disk cache.
|
|
209
209
|
|
|
210
|
+
### Input Debouncing & Transactions
|
|
211
|
+
When updating multiple inputs, intermediate read states ("flapping") can cause inconsistent evaluations. Use `engine.transaction()` to batch updates so they are committed atomically.
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
with engine.transaction():
|
|
215
|
+
theme.set("dark")
|
|
216
|
+
layout.set("grid")
|
|
217
|
+
|
|
218
|
+
# Queries will only re-evaluate once observing both changes simultaneously.
|
|
219
|
+
```
|
|
220
|
+
|
|
210
221
|
### Code-Aware Caching (Automatic Invalidation)
|
|
211
222
|
Cascade automatically inspects the Python bytecode of your `@engine.query` and `@engine.input` functions. If you edit a function's logic and the module is hot-reloaded (or you restart your script), Cascade compares the new function's bytecode hash against the previously cached logic. If the logic has changed, Cascade immediately invalidates that function's memory and persistent disk caches, avoiding stale results without requiring manual cache wipes. Code formatting and comments do not affect the bytecode hash.
|
|
212
223
|
|
|
@@ -773,3 +773,49 @@ def test_inspect_graph_full_summary_oracle_across_varied_graphs() -> None:
|
|
|
773
773
|
expected_memo_count=1,
|
|
774
774
|
expected_input_count=3,
|
|
775
775
|
)
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def test_engine_transaction() -> None:
|
|
779
|
+
engine = Engine()
|
|
780
|
+
|
|
781
|
+
@engine.input
|
|
782
|
+
def input_a() -> int:
|
|
783
|
+
return 0
|
|
784
|
+
|
|
785
|
+
@engine.input
|
|
786
|
+
def input_b() -> int:
|
|
787
|
+
return 0
|
|
788
|
+
|
|
789
|
+
@engine.query
|
|
790
|
+
def q() -> tuple[int, int]:
|
|
791
|
+
return (input_a(), input_b())
|
|
792
|
+
|
|
793
|
+
assert q() == (0, 0)
|
|
794
|
+
rev_before = engine.revision
|
|
795
|
+
|
|
796
|
+
with engine.transaction():
|
|
797
|
+
input_a.set(1)
|
|
798
|
+
input_b.set(2)
|
|
799
|
+
# uncommitted changes should not be visible
|
|
800
|
+
assert q() == (0, 0)
|
|
801
|
+
|
|
802
|
+
assert q() == (1, 2)
|
|
803
|
+
assert engine.revision == rev_before + 1
|
|
804
|
+
|
|
805
|
+
# Exception in transaction should abort updates
|
|
806
|
+
try:
|
|
807
|
+
with engine.transaction():
|
|
808
|
+
input_a.set(10)
|
|
809
|
+
input_b.set(20)
|
|
810
|
+
raise ValueError("abort")
|
|
811
|
+
except ValueError:
|
|
812
|
+
pass
|
|
813
|
+
|
|
814
|
+
assert q() == (1, 2)
|
|
815
|
+
assert engine.revision == rev_before + 1
|
|
816
|
+
|
|
817
|
+
# Nested transactions raise error
|
|
818
|
+
with engine.transaction():
|
|
819
|
+
with pytest.raises(RuntimeError, match="Nested transactions"):
|
|
820
|
+
with engine.transaction():
|
|
821
|
+
pass
|
|
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
|
|
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
|
|
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
|