diff --git a/README.md b/README.md index 89270bf..aaf119d 100644 --- a/README.md +++ b/README.md @@ -12,18 +12,18 @@ you `delete` or `clear` them. `requirements.txt`: ``` -aiokv @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiokv.git@v0.2.0 +aiokv @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiokv.git@v0.2.1 ``` Direct: ```bash -pip install "aiokv @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiokv.git@v0.2.0" +pip install "aiokv @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiokv.git@v0.2.1" ``` Requires `aiofiles` (pulled transitively). -Drop the `@v0.2.0` suffix from the line above to install the latest unpinned. +Drop the `@v0.2.1` suffix from the line above to install the latest unpinned. ## Usage diff --git a/pyproject.toml b/pyproject.toml index d9a5d51..2a25f82 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "aiokv" -version = "0.2.0" +version = "0.2.1" description = "Async file-backed key-value store for single-process local state — atomic writes, no TTL, config-free, installable." requires-python = ">=3.10" dependencies = [ diff --git a/src/aiokv/__init__.py b/src/aiokv/__init__.py index 67b5f7d..d213915 100644 --- a/src/aiokv/__init__.py +++ b/src/aiokv/__init__.py @@ -1,5 +1,5 @@ from .aiokv import AioKV, aiocache -__version__ = "0.2.0" +__version__ = "0.2.1" __all__ = ["AioKV", "aiocache"] diff --git a/src/aiokv/aiokv.py b/src/aiokv/aiokv.py index 71aeeca..e0192d8 100644 --- a/src/aiokv/aiokv.py +++ b/src/aiokv/aiokv.py @@ -1,35 +1,19 @@ """ async file-backed key-value store for single-process local state -a persist-forever KV store (last-used-command, rate-limit timestamps, seen-ids, -simple bot state) backed by a JSON file. NOT a cache: no TTL, no expiry, no -eviction — values live until you delete or clear them. +a persist-forever KV store backed by a JSON file, not a cache (no TTL/expiry/eviction). +see README for usage and the full API. from aiokv import AioKV kv = AioKV("state.json") await kv.set("last_seen", 12345) - await kv.set("ran_cleanup") # value omitted -> stores int(time.time()) - when = await kv.get("ran_cleanup") - await kv.delete("last_seen") + when = await kv.get("last_seen") -durability: atomic writes (temp file + os.replace, symlink-safe), stale -`..tmp` files from a hard crash are swept on save. this is process-crash -safety, NOT power-loss durability — no fsync, so an OS/power failure can still -lose the last write. a single asyncio.Lock guards every read and write, so -concurrent operations on one instance are consistent. JSON (de)serialization -runs off-loop via asyncio.to_thread so a large store never stalls other tasks. - -scope: SINGLE-PROCESS, single-instance local state only. the lock is per-instance — -two AioKV instances (or two processes) pointing at the same file are NOT safe and -will clobber each other. for shared cross-process/cross-bot state, use a database -(e.g. mongo), not this. - -config-free: the file path is passed at construction; nothing is read from a global -config. keys must be str (ValueError otherwise — JSON object keys are always -strings, so a non-str key would silently never round-trip). errors in delete/clear -are logged and swallowed (returning False); get/set raise on unexpected i/o so a -real failure is visible. +atomic writes are process-crash safe (temp file + os.replace) but NOT power-loss safe +(no fsync). the lock is per-instance only - two AioKV instances or processes on the +same file will clobber each other; this is single-process local state, not shared +cross-process storage. """ import os @@ -106,7 +90,7 @@ class AioKV: @staticmethod def _check_key(key: str) -> None: - """raise ValueError if key is not str (JSON object keys are always strings)""" + """raise ValueError if key is not str""" if not isinstance(key, str): raise ValueError(f"aiokv keys must be str, got {type(key).__name__}") @@ -128,7 +112,7 @@ class AioKV: async def _save(self, cache: Dict[str, Any]) -> None: """write atomically: temp file in the same dir, then os.replace over the realpath'd target (symlink-safe). process-crash safe (a crash mid-write leaves the prior good - file intact), NOT power-loss safe — no fsync, so an OS/power failure can still lose + file intact), NOT power-loss safe - no fsync, so an OS/power failure can still lose the last write (acceptable: reconstructible single-process state, not a db)""" target = await asyncio.to_thread(os.path.realpath, self.file) directory = os.path.dirname(target) or "." @@ -181,6 +165,5 @@ class AioKV: return True -# back-compat: this lib was originally named aiocache; legacy call sites using -# `aiocache(...)` keep working via this alias. prefer AioKV in new code. +# back-compat: originally named aiocache; prefer AioKV in new code. aiocache = AioKV