From 019955dad3246860392103acfe9d53ad8e16d044 Mon Sep 17 00:00:00 2001 From: disqualifier Date: Wed, 24 Jun 2026 19:54:04 -0400 Subject: [PATCH] init: async file-backed kv store for single-process local state Signed-off-by: disqualifier --- .gitignore | 16 ++++++++++ README.md | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 .gitignore create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6ff830 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# claude +CLAUDE.md + +# python +__pycache__/ +*.py[cod] +*.egg-info/ +dist/ +build/ +.eggs/ + +# env +.venv/ +venv/ +.env +.pytest_cache/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..869d9d2 --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# 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.0 +``` + +Direct: + +```bash +pip install "aiokv @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiokv.git@v0.1.0" +``` + +Requires `aiofiles` (pulled transitively). + +## Usage + +```python +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`: + +```python +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 crash mid-write leaves the +previous good file intact, and a reader never observes a partial file. 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 (and `_load` raises on a + truncated/corrupt file) so a real failure is visible rather than silently masked. +- `delete` / `clear` log the exception and return `False` on error, `True` otherwise. + +## Versioning + +Tagged `vX.Y.Z`. Pin the tag in `requirements.txt`.