Compare commits
No commits in common. "main" and "v0.1.0" have entirely different histories.
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,5 @@
|
||||
# claude
|
||||
.claude/
|
||||
CLAUDE.md
|
||||
|
||||
# python
|
||||
__pycache__/
|
||||
|
||||
21
README.md
21
README.md
@ -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`.
|
||||
|
||||
@ -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 = [
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user