Compare commits

..

No commits in common. "main" and "v0.1.0" have entirely different histories.
main ... v0.1.0

4 changed files with 14 additions and 30 deletions

2
.gitignore vendored
View File

@ -1,5 +1,5 @@
# claude
.claude/
CLAUDE.md
# python
__pycache__/

View File

@ -12,19 +12,17 @@ you `delete` or `clear` them.
`requirements.txt`:
```
aiokv @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiokv.git@v0.1.1
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.1"
pip install "aiokv @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiokv.git@v0.1.0"
```
Requires `aiofiles` (pulled transitively).
Drop the `@v0.1.1` suffix from the line above to install the latest unpinned.
## Usage
```python
@ -62,11 +60,8 @@ 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
`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.
@ -82,12 +77,10 @@ are consistent and no update is lost. All blocking filesystem calls run via
## 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.
- `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
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.
Tagged `vX.Y.Z`. Pin the tag in `requirements.txt`.

View File

@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "aiokv"
version = "0.1.1"
version = "0.1.0"
description = "Async file-backed key-value store for single-process local state — atomic writes, no TTL, config-free, installable."
requires-python = ">=3.10"
dependencies = [

View File

@ -82,17 +82,11 @@ class AioKV:
return False
async def clear(self) -> bool:
"""remove the backing file entirely; returns True on success, False on error
a file already absent (or removed concurrently between the check and the remove)
is success the goal state, no file, is reached.
"""
"""remove the backing file entirely; returns True on success, False on error"""
try:
async with self.lock:
try:
if await asyncio.to_thread(os.path.exists, self.file):
await asyncio.to_thread(os.remove, self.file)
except FileNotFoundError:
pass
return True
except Exception:
log.exception("aiokv.clear() failed")
@ -113,7 +107,7 @@ class AioKV:
"""
if not await asyncio.to_thread(os.path.exists, self.file):
return {}
async with aiofiles.open(self.file, mode="r", encoding="utf-8") as f:
async with aiofiles.open(self.file, mode="r") as f:
data = await f.read()
if not data:
return {}
@ -126,10 +120,7 @@ class AioKV:
"""write the store atomically: temp file in the same dir, then os.replace
os.replace is atomic on POSIX, so a reader never sees a partial file and a
process crash mid-write leaves the previous good file intact. note this is
process-crash safety, NOT power-loss durability there is no fsync of the temp
file or the directory, so an OS/power failure could still lose the most recent
write (acceptable here: this is reconstructible single-process state, not a db).
crash mid-write leaves the previous good file intact.
"""
directory = os.path.dirname(self.file) or "."
await asyncio.to_thread(os.makedirs, directory, exist_ok=True)
@ -137,7 +128,7 @@ class AioKV:
payload = json.dumps(cache)
tmp = f"{self.file}.{os.getpid()}.tmp"
try:
async with aiofiles.open(tmp, mode="w", encoding="utf-8") as f:
async with aiofiles.open(tmp, mode="w") as f:
await f.write(payload)
await asyncio.to_thread(os.replace, tmp, self.file)
except Exception: