query-cascade 0.3.1__tar.gz → 0.3.2__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 (40) hide show
  1. {query_cascade-0.3.1 → query_cascade-0.3.2}/PKG-INFO +1 -1
  2. {query_cascade-0.3.1 → query_cascade-0.3.2}/pyproject.toml +1 -1
  3. {query_cascade-0.3.1 → query_cascade-0.3.2}/src/query_cascade.egg-info/PKG-INFO +1 -1
  4. {query_cascade-0.3.1 → query_cascade-0.3.2}/tests/test_disk_cache.py +169 -0
  5. {query_cascade-0.3.1 → query_cascade-0.3.2}/README.md +0 -0
  6. {query_cascade-0.3.1 → query_cascade-0.3.2}/setup.cfg +0 -0
  7. {query_cascade-0.3.1 → query_cascade-0.3.2}/src/cascade/__init__.py +0 -0
  8. {query_cascade-0.3.1 → query_cascade-0.3.2}/src/cascade/_canonical.py +0 -0
  9. {query_cascade-0.3.1 → query_cascade-0.3.2}/src/cascade/_disk_cache.py +0 -0
  10. {query_cascade-0.3.1 → query_cascade-0.3.2}/src/cascade/_errors.py +0 -0
  11. {query_cascade-0.3.1 → query_cascade-0.3.2}/src/cascade/_evaluator.py +0 -0
  12. {query_cascade-0.3.1 → query_cascade-0.3.2}/src/cascade/_graph_export.py +0 -0
  13. {query_cascade-0.3.1 → query_cascade-0.3.2}/src/cascade/_persistence.py +0 -0
  14. {query_cascade-0.3.1 → query_cascade-0.3.2}/src/cascade/_runtime.py +0 -0
  15. {query_cascade-0.3.1 → query_cascade-0.3.2}/src/cascade/_scheduler.py +0 -0
  16. {query_cascade-0.3.1 → query_cascade-0.3.2}/src/cascade/_serde.py +0 -0
  17. {query_cascade-0.3.1 → query_cascade-0.3.2}/src/cascade/_state.py +0 -0
  18. {query_cascade-0.3.1 → query_cascade-0.3.2}/src/cascade/_store.py +0 -0
  19. {query_cascade-0.3.1 → query_cascade-0.3.2}/src/cascade/_synthetic_graph.py +0 -0
  20. {query_cascade-0.3.1 → query_cascade-0.3.2}/src/cascade/engine.py +0 -0
  21. {query_cascade-0.3.1 → query_cascade-0.3.2}/src/query_cascade.egg-info/SOURCES.txt +0 -0
  22. {query_cascade-0.3.1 → query_cascade-0.3.2}/src/query_cascade.egg-info/dependency_links.txt +0 -0
  23. {query_cascade-0.3.1 → query_cascade-0.3.2}/src/query_cascade.egg-info/requires.txt +0 -0
  24. {query_cascade-0.3.1 → query_cascade-0.3.2}/src/query_cascade.egg-info/top_level.txt +0 -0
  25. {query_cascade-0.3.1 → query_cascade-0.3.2}/tests/test_dependency_parallelism.py +0 -0
  26. {query_cascade-0.3.1 → query_cascade-0.3.2}/tests/test_engine.py +0 -0
  27. {query_cascade-0.3.1 → query_cascade-0.3.2}/tests/test_engine_compute_many.py +0 -0
  28. {query_cascade-0.3.1 → query_cascade-0.3.2}/tests/test_engine_concurrency.py +0 -0
  29. {query_cascade-0.3.1 → query_cascade-0.3.2}/tests/test_engine_persistence_and_tracing.py +0 -0
  30. {query_cascade-0.3.1 → query_cascade-0.3.2}/tests/test_engine_private.py +0 -0
  31. {query_cascade-0.3.1 → query_cascade-0.3.2}/tests/test_engine_private2.py +0 -0
  32. {query_cascade-0.3.1 → query_cascade-0.3.2}/tests/test_evaluator_and_synthetic_coverage.py +0 -0
  33. {query_cascade-0.3.1 → query_cascade-0.3.2}/tests/test_examples.py +0 -0
  34. {query_cascade-0.3.1 → query_cascade-0.3.2}/tests/test_internal_invariants.py +0 -0
  35. {query_cascade-0.3.1 → query_cascade-0.3.2}/tests/test_performance.py +0 -0
  36. {query_cascade-0.3.1 → query_cascade-0.3.2}/tests/test_repro_touch_memo_keyerror.py +0 -0
  37. {query_cascade-0.3.1 → query_cascade-0.3.2}/tests/test_scale_behavior.py +0 -0
  38. {query_cascade-0.3.1 → query_cascade-0.3.2}/tests/test_serde.py +0 -0
  39. {query_cascade-0.3.1 → query_cascade-0.3.2}/tests/test_stateful_engine_invariants.py +0 -0
  40. {query_cascade-0.3.1 → query_cascade-0.3.2}/tests/test_subgraph_stats_export.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: query-cascade
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Minimal demand-driven query framework for incremental computation.
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "query-cascade"
7
- version = "0.3.1"
7
+ version = "0.3.2"
8
8
  description = "Minimal demand-driven query framework for incremental computation."
9
9
  requires-python = ">=3.12"
10
10
  readme = "README.md"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: query-cascade
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Minimal demand-driven query framework for incremental computation.
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -674,3 +674,172 @@ def test_disk_cache_prune_vacuum_edge_cases(tmp_path: Path) -> None:
674
674
  engine.prune([("query", "missing:fid", ())], vacuum_disk=True)
675
675
 
676
676
  engine.shutdown()
677
+
678
+
679
+ def test_dynamic_graph_topology_cross_session(tmp_path: Path) -> None:
680
+ data = tmp_path / "cond.txt"
681
+ data.write_text("A")
682
+ cache = tmp_path / "cache"
683
+ runs: dict[str, int] = {}
684
+
685
+ def build() -> tuple[Engine, Any]:
686
+ engine = Engine(cache_dir=cache)
687
+
688
+ @engine.input
689
+ def condition() -> str:
690
+ return data.read_text().strip()
691
+
692
+ @engine.query
693
+ def branch_a() -> str:
694
+ runs["branch_a"] = runs.get("branch_a", 0) + 1
695
+ return "A_out"
696
+
697
+ @engine.query
698
+ def branch_b() -> str:
699
+ runs["branch_b"] = runs.get("branch_b", 0) + 1
700
+ return "B_out"
701
+
702
+ @engine.query
703
+ def root() -> str:
704
+ runs["root"] = runs.get("root", 0) + 1
705
+ if condition() == "A":
706
+ return branch_a()
707
+ else:
708
+ return branch_b()
709
+
710
+ return engine, root
711
+
712
+ # Session 1: cond is A
713
+ engine_1, root_1 = build()
714
+ assert root_1() == "A_out"
715
+ assert runs == {"root": 1, "branch_a": 1}
716
+ engine_1.shutdown()
717
+
718
+ # Session 2: cond is still A, should hit
719
+ runs.clear()
720
+ engine_2, root_2 = build()
721
+ assert root_2() == "A_out"
722
+ assert runs == {}
723
+ engine_2.shutdown()
724
+
725
+ # Session 3: cond is B, must recompute root and branch_b
726
+ data.write_text("B")
727
+ runs.clear()
728
+ engine_3, root_3 = build()
729
+ assert root_3() == "B_out"
730
+ assert runs == {"root": 1, "branch_b": 1}
731
+ engine_3.shutdown()
732
+
733
+ # Session 4: cond is B, should hit
734
+ runs.clear()
735
+ engine_4, root_4 = build()
736
+ assert root_4() == "B_out"
737
+ assert runs == {}
738
+ engine_4.shutdown()
739
+
740
+ # Session 5: cond is A again. root's fingerprint was overwritten by Session 3 (which depended on B),
741
+ # so root must recompute. But branch_a will hit the cache from Session 1!
742
+ data.write_text("A")
743
+ runs.clear()
744
+ engine_5, root_5 = build()
745
+ assert root_5() == "A_out"
746
+ assert runs == {"root": 1}
747
+ engine_5.shutdown()
748
+
749
+
750
+ def test_errors_are_not_cached_across_sessions(tmp_path: Path) -> None:
751
+ data = tmp_path / "state.txt"
752
+ data.write_text("fail")
753
+ cache = tmp_path / "cache"
754
+ runs: dict[str, int] = {}
755
+
756
+ def build() -> tuple[Engine, Any]:
757
+ engine = Engine(cache_dir=cache)
758
+
759
+ @engine.input
760
+ def state() -> str:
761
+ return data.read_text().strip()
762
+
763
+ @engine.query
764
+ def process() -> str:
765
+ runs["process"] = runs.get("process", 0) + 1
766
+ val = state()
767
+ if val == "fail":
768
+ raise ValueError("Oops")
769
+ return "Success"
770
+
771
+ return engine, process
772
+
773
+ # Session 1: fails
774
+ engine_1, process_1 = build()
775
+ with pytest.raises(ValueError, match="Oops"):
776
+ process_1()
777
+ assert runs == {"process": 1}
778
+ engine_1.shutdown()
779
+
780
+ # Session 2: still failing, must re-run because errors aren't cached
781
+ runs.clear()
782
+ engine_2, process_2 = build()
783
+ with pytest.raises(ValueError, match="Oops"):
784
+ process_2()
785
+ assert runs == {"process": 1}
786
+ engine_2.shutdown()
787
+
788
+ # Session 3: state fixed, runs and caches
789
+ data.write_text("ok")
790
+ runs.clear()
791
+ engine_3, process_3 = build()
792
+ assert process_3() == "Success"
793
+ assert runs == {"process": 1}
794
+ engine_3.shutdown()
795
+
796
+ # Session 4: state still ok, hits cache
797
+ runs.clear()
798
+ engine_4, process_4 = build()
799
+ assert process_4() == "Success"
800
+ assert runs == {}
801
+ engine_4.shutdown()
802
+
803
+
804
+ def test_deep_dependency_chain_cross_session(tmp_path: Path) -> None:
805
+ data = tmp_path / "val.txt"
806
+ data.write_text("0")
807
+ cache = tmp_path / "cache"
808
+ runs: dict[str, int] = {}
809
+
810
+ def build() -> tuple[Engine, Any]:
811
+ engine = Engine(cache_dir=cache)
812
+
813
+ @engine.input
814
+ def base() -> int:
815
+ return int(data.read_text().strip())
816
+
817
+ @engine.query
818
+ def chain(idx: int) -> int:
819
+ runs[f"q_{idx}"] = runs.get(f"q_{idx}", 0) + 1
820
+ if idx == 0:
821
+ return base() + 1
822
+ return chain(idx - 1) + 1
823
+
824
+ return engine, chain
825
+
826
+ # Session 1: compute the chain
827
+ engine_1, chain_1 = build()
828
+ assert chain_1(49) == 50
829
+ assert len(runs) == 50
830
+ engine_1.shutdown()
831
+
832
+ # Session 2: hit the chain
833
+ runs.clear()
834
+ engine_2, chain_2 = build()
835
+ assert chain_2(49) == 50
836
+ assert len(runs) == 0
837
+ engine_2.shutdown()
838
+
839
+ # Session 3: update base, recomputes all
840
+ data.write_text("10")
841
+ runs.clear()
842
+ engine_3, chain_3 = build()
843
+ assert chain_3(49) == 60
844
+ assert len(runs) == 50
845
+ engine_3.shutdown()
File without changes
File without changes