Async file-backed key/value store for local state
Go to file
disqualifier 1e364fcfdb fix: clear() treats a concurrent delete as success; explicit utf-8; durability prose
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>
2026-06-29 21:35:23 -04:00
src/aiokv fix: clear() treats a concurrent delete as success; explicit utf-8; durability prose 2026-06-29 21:35:23 -04:00
.gitignore init: async file-backed kv store for single-process local state 2026-06-24 19:54:04 -04:00
pyproject.toml docs: document _load's ValueError-on-non-object-JSON in the error contract (v0.1.1) 2026-06-29 17:57:37 -04:00
README.md fix: clear() treats a concurrent delete as success; explicit utf-8; durability prose 2026-06-29 21:35:23 -04:00

aiokv

Async file-backed key-value store for single-process local state — last-used command, rate-limit timestamps, seen-IDs, simple bot state. Persists forever to a JSON file with atomic writes.

It is a KV store, not a cache: no TTL, no expiry, no eviction. Values live until you delete or clear them.

Install

requirements.txt:

aiokv @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiokv.git@v0.1.1

Direct:

pip install "aiokv @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiokv.git@v0.1.1"

Requires aiofiles (pulled transitively).

Drop the @v0.1.1 suffix from the line above to install the latest unpinned.

Usage

from aiokv import AioKV

kv = AioKV("state.json")

await kv.set("last_command", "ping")
await kv.set("seen_ran_cleanup")             # value omitted -> stores int(time.time())

cmd  = await kv.get("last_command")          # "ping"
when = await kv.get("seen_ran_cleanup")      # the unix timestamp
miss = await kv.get("absent", default=0)     # 0

await kv.delete("last_command")              # True (removed or absent)
everything = await kv.get_all()              # {"seen_ran_cleanup": ...}
await kv.clear()                             # removes the file

The timestamp default (set(key) with no value) is for "mark that I saw/did X at time T" — the common rate-limit / seen-ID pattern.

Back-compat

This lib was originally aiocache. Legacy call sites keep working — aiocache is a re-export alias of AioKV:

from aiokv import aiocache      # alias of AioKV; same API
kv = aiocache("state.json")

Prefer AioKV in new code.

Durability

Writes are atomic: data is written to a temp file in the same directory and os.replace()d over the target (atomic on POSIX). A process crash mid-write leaves the previous good file intact, and a reader never observes a partial file. (This is process-crash safety, not power-loss durability — there's no fsync, so an OS/power failure could still lose the last write; fine for reconstructible single-process state.) A single asyncio.Lock guards every read and write, so concurrent operations on one instance are consistent and no update is lost. All blocking filesystem calls run via asyncio.to_thread, so nothing stalls the event loop.

Scope — read this

  • Single-process, single-instance only. The lock is per-instance. Two AioKV instances — or two processes — pointing at the same file are not safe; they will clobber each other's writes. For shared cross-process / cross-bot state, that is a database's job (e.g. our mongo lib), not aiokv.
  • Not a cache. No TTL/expiry/eviction. If you need entries that age out, this is the wrong tool.

Error contract

  • get / set / get_all raise on unexpected I/O. _load raises JSONDecodeError on a truncated/corrupt file, and ValueError when the file holds valid JSON that isn't an object (a bare list/number/string/null) — so corruption or a wrong-shaped file is visible rather than silently masked.
  • delete / clear log the exception and return False on error, True otherwise.

Versioning

Releases are tagged vX.Y.Z. The install line above pins a release; drop the @vX.Y.Z suffix to install the latest unpinned. Pin deliberately for reproducible installs.