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.
Files changed (50) hide show
  1. {query_cascade-0.3.5 → query_cascade-0.3.7}/PKG-INFO +12 -1
  2. {query_cascade-0.3.5 → query_cascade-0.3.7}/README.md +11 -0
  3. {query_cascade-0.3.5 → query_cascade-0.3.7}/pyproject.toml +1 -1
  4. {query_cascade-0.3.5 → query_cascade-0.3.7}/src/cascade/_store.py +33 -0
  5. {query_cascade-0.3.5 → query_cascade-0.3.7}/src/cascade/engine.py +43 -1
  6. {query_cascade-0.3.5 → query_cascade-0.3.7}/src/query_cascade.egg-info/PKG-INFO +12 -1
  7. {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_engine.py +46 -0
  8. {query_cascade-0.3.5 → query_cascade-0.3.7}/setup.cfg +0 -0
  9. {query_cascade-0.3.5 → query_cascade-0.3.7}/src/cascade/__init__.py +0 -0
  10. {query_cascade-0.3.5 → query_cascade-0.3.7}/src/cascade/_canonical.py +0 -0
  11. {query_cascade-0.3.5 → query_cascade-0.3.7}/src/cascade/_disk_cache.py +0 -0
  12. {query_cascade-0.3.5 → query_cascade-0.3.7}/src/cascade/_errors.py +0 -0
  13. {query_cascade-0.3.5 → query_cascade-0.3.7}/src/cascade/_evaluator.py +0 -0
  14. {query_cascade-0.3.5 → query_cascade-0.3.7}/src/cascade/_graph_export.py +0 -0
  15. {query_cascade-0.3.5 → query_cascade-0.3.7}/src/cascade/_persistence.py +0 -0
  16. {query_cascade-0.3.5 → query_cascade-0.3.7}/src/cascade/_runtime.py +0 -0
  17. {query_cascade-0.3.5 → query_cascade-0.3.7}/src/cascade/_scheduler.py +0 -0
  18. {query_cascade-0.3.5 → query_cascade-0.3.7}/src/cascade/_serde.py +0 -0
  19. {query_cascade-0.3.5 → query_cascade-0.3.7}/src/cascade/_state.py +0 -0
  20. {query_cascade-0.3.5 → query_cascade-0.3.7}/src/cascade/_synthetic_graph.py +0 -0
  21. {query_cascade-0.3.5 → query_cascade-0.3.7}/src/query_cascade.egg-info/SOURCES.txt +0 -0
  22. {query_cascade-0.3.5 → query_cascade-0.3.7}/src/query_cascade.egg-info/dependency_links.txt +0 -0
  23. {query_cascade-0.3.5 → query_cascade-0.3.7}/src/query_cascade.egg-info/requires.txt +0 -0
  24. {query_cascade-0.3.5 → query_cascade-0.3.7}/src/query_cascade.egg-info/top_level.txt +0 -0
  25. {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_async.py +0 -0
  26. {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_async_coverage.py +0 -0
  27. {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_code_versioning.py +0 -0
  28. {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_dependency_parallelism.py +0 -0
  29. {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_disk_cache.py +0 -0
  30. {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_engine_compute_many.py +0 -0
  31. {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_engine_concurrency.py +0 -0
  32. {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_engine_persistence_and_tracing.py +0 -0
  33. {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_engine_private.py +0 -0
  34. {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_engine_private2.py +0 -0
  35. {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_engine_sweep.py +0 -0
  36. {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_error_caching.py +0 -0
  37. {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_error_caching_persistence.py +0 -0
  38. {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_evaluator_and_synthetic_coverage.py +0 -0
  39. {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_examples.py +0 -0
  40. {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_fixed_point.py +0 -0
  41. {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_internal_invariants.py +0 -0
  42. {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_memoize_false.py +0 -0
  43. {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_performance.py +0 -0
  44. {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_repro_touch_memo_keyerror.py +0 -0
  45. {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_scale_behavior.py +0 -0
  46. {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_serde.py +0 -0
  47. {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_stateful_engine_invariants.py +0 -0
  48. {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_store_coverage.py +0 -0
  49. {query_cascade-0.3.5 → query_cascade-0.3.7}/tests/test_subgraph_stats_export.py +0 -0
  50. {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.5
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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "query-cascade"
7
- version = "0.3.5"
7
+ version = "0.3.7"
8
8
  description = "Minimal demand-driven query framework for incremental computation."
9
9
  requires-python = ">=3.12"
10
10
  readme = "README.md"
@@ -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.5
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