_save() already resolves target = realpath(self.file) for the write path;
_sweep_stale_tmp then independently recomputed the same realpath to derive its
match basename. _sweep_stale_tmp now takes that resolved basename as a
parameter instead of re-resolving it, cutting one syscall per save. the sweep
itself still runs on every save (unchanged) - gating it to first-save-per-
instance would change when a crash-orphaned tmp from before this process
started actually gets swept, so that part is left alone.
Signed-off-by: disqualifier <dev@disqualifier.me>
clear() removed the raw path, so on a symlinked store it unlinked only the
symlink while the real data file kept every value - clear() returned True
but the data resurfaced if the symlink was recreated. It now realpaths the
target first, mirroring _save()'s symlink-safe writes.
_sweep_stale_tmp globbed {base}.*.tmp, which crosses dots and can match an
unrelated neighbor file (deleting it outside the lib's own artifacts), and
interpolated the store's basename unescaped, so glob metacharacters in the
filename (e.g. state[prod].json) either missed the store's own orphans or
cross-matched a different store's. The sweep now lists the directory and
matches a re.escape'd regex anchored to the exact <base>.<pid>.tmp shape
_save() creates.
Signed-off-by: disqualifier <dev@disqualifier.me>
_load() masked permission/IO errors as an empty store via os.path.exists();
now raises FileNotFoundError only, propagating real failures. Non-str keys
silently never round-tripped through get() since JSON object keys are always
strings; set()/get() now raise ValueError on a non-str key. _save() clobbered
a symlinked store file with os.replace(); now realpaths the target first.
json.dumps() allows NaN/Infinity by default, producing invalid JSON for
strict readers; now allow_nan=False. A whitespace-only file raised while a
zero-byte file returned {}; both now return {}. Orphaned .<pid>.tmp files
from a hard crash of a dead process are swept on save. JSON (de)serialization
now runs via asyncio.to_thread instead of on the event loop.
Signed-off-by: disqualifier <dev@disqualifier.me>
clear() handles FileNotFoundError as success (the goal state — no file — is reached)
instead of returning False. read/write open with explicit encoding='utf-8'. atomic-write
prose scoped to process-crash safety (NOT power-loss durability — no fsync), in module,
README, and CLAUDE.
Signed-off-by: disqualifier <dev@disqualifier.me>
README + CLAUDE.md error contract now note that _load raises ValueError on valid-but-
non-object JSON (bare list/number/string/null) in addition to JSONDecodeError on a
corrupt file, matching the module docstring (L1).
Signed-off-by: disqualifier <dev@disqualifier.me>
a store file holding valid JSON that is not an object (null/number/string/array) returned a non-dict, breaking get/set/delete/get_all. now raises ValueError, matching the documented corrupt-file contract.
Signed-off-by: disqualifier <dev@disqualifier.me>
AioKV: async file-backed key-value store for single-process local state
(persist-forever, not a cache — no TTL/eviction). atomic writes via
temp-file + os.replace, every read and write under one asyncio.Lock,
blocking fs calls off the loop via asyncio.to_thread. set(key) defaults
to int(time.time()) for seen/did-X-at-T. aiocache alias kept for
back-compat. src/ layout, hatchling, aiofiles dep.
Signed-off-by: disqualifier <dev@disqualifier.me>