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:
parent
25f21fcc53
commit
4e1ba0eb82
4 changed files with 69 additions and 5 deletions
|
|
@ -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()))
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
53
packages/cultcache-py/tests/test_soft_push_all.py
Normal file
53
packages/cultcache-py/tests/test_soft_push_all.py
Normal 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()
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue