cultcache-py: push_all(soft) skips a readable store, writes an absent one, refuses an unreadable one

benchmark.py's InMemoryBackingStore implements the BackingStore protocol and takes the new keyword.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Libby 2026-11-12 22:30:37 +00:00
commit 4e1ba0eb82
4 changed files with 69 additions and 5 deletions

View file

@ -67,10 +67,12 @@ class BackingStore(Protocol):
def delete(self, type: str, key: str) -> None:
...
def push_all(self, envelopes: list[CultCacheEnvelope]) -> None:
def push_all(self, envelopes: list[CultCacheEnvelope], soft: bool = False) -> None:
existing = {
(envelope.type, envelope.key): envelope for envelope in self.pull_all()
}
if soft and existing:
return
for envelope in envelopes:
existing[(envelope.type, envelope.key)] = envelope
self._replace_all(list(existing.values()))

View file

@ -106,9 +106,11 @@ class JsonLinesBackingStore:
existing[(envelope.type, envelope.key)] = envelope
self._replace_all(list(existing.values()))
def push_all(self, envelopes: list[CultCacheEnvelope]) -> None:
def push_all(self, envelopes: list[CultCacheEnvelope], soft: bool = False) -> None:
with self._lock:
existing = {(item.type, item.key): item for item in self.pull_all()}
if soft and self.path.exists():
return
for envelope in envelopes:
existing[(envelope.type, envelope.key)] = envelope
self._replace_all(list(existing.values()))
@ -163,9 +165,14 @@ class SingleFileMessagePackBackingStore:
with self._write_transaction():
self._replace_all([envelope], self.pull_all())
def push_all(self, envelopes: list[CultCacheEnvelope]) -> None:
def push_all(self, envelopes: list[CultCacheEnvelope], soft: bool = False) -> None:
"""A soft push writes only where nothing is at the path: a store the reader reads is left byte for byte, and a file it
cannot read is refused by the same read, as in TS and Rust."""
with self._write_transaction():
self._replace_all(list(envelopes), self.pull_all())
arrived = self.pull_all()
if soft and self.path.exists():
return
self._replace_all(list(envelopes), arrived)
def delete(self, type: str, key: str) -> None:
with self._write_transaction():

View file

@ -0,0 +1,53 @@
from __future__ import annotations
import shutil
import tempfile
import unittest
from pathlib import Path
from cultcache_py import JsonLinesBackingStore, SingleFileMessagePackBackingStore, StoreUnreadableError
from cultcache_py.backing_store import CultCacheEnvelope
def envelope(key: str) -> CultCacheEnvelope:
return CultCacheEnvelope(key=key, type="t", payload=b"\x01", stored_at="2026-11-06T00:00:00Z")
class SoftPushAllTests(unittest.TestCase):
"""A soft push writes where nothing is, skips a store it reads, and refuses one it cannot read (TS and Rust parity)."""
def setUp(self) -> None:
self.dir = Path(tempfile.mkdtemp())
self.addCleanup(shutil.rmtree, self.dir, ignore_errors=True)
self.path = self.dir / "state.cc"
def test_over_a_readable_store_it_leaves_the_file_byte_for_byte(self) -> None:
for make in (SingleFileMessagePackBackingStore, JsonLinesBackingStore):
with self.subTest(make.__name__):
path = self.dir / make.__name__
store = make(path)
store.push_all([envelope("a")])
before = path.read_bytes()
store.push_all([envelope("b")], soft=True)
self.assertEqual(before, path.read_bytes())
self.assertEqual(["a"], [item.key for item in make(path).pull_all()])
store.push_all([envelope("b")]) # a plain push still lands what the caller holds
self.assertEqual(["a", "b"], sorted(item.key for item in make(path).pull_all()))
def test_over_an_absent_path_it_writes(self) -> None:
for make in (SingleFileMessagePackBackingStore, JsonLinesBackingStore):
with self.subTest(make.__name__):
path = self.dir / make.__name__
make(path).push_all([envelope("a")], soft=True)
self.assertEqual(["a"], [item.key for item in make(path).pull_all()])
def test_over_an_unreadable_file_it_refuses_and_leaves_it(self) -> None:
junk = b"\xc1\x01\x02\x03"
self.path.write_bytes(junk)
with self.assertRaises(StoreUnreadableError):
SingleFileMessagePackBackingStore(self.path).push_all([envelope("a")], soft=True)
self.assertEqual(junk, self.path.read_bytes())
if __name__ == "__main__":
unittest.main()

View file

@ -137,7 +137,9 @@ class InMemoryBackingStore:
def push(self, envelope: CultCacheEnvelope) -> None:
self._records[(envelope.type, envelope.key)] = envelope
def push_all(self, envelopes: list[CultCacheEnvelope]) -> None:
def push_all(self, envelopes: list[CultCacheEnvelope], soft: bool = False) -> None:
if soft and self._records:
return
for envelope in envelopes:
self.push(envelope)