LibBee / src /services /runtime_store.py
nikeshn's picture
Upload 8 files
a2416df verified
Raw History Blame Contribute Delete
2.04 kB
"""Small atomic JSON store for a single-process LibBee deployment."""
import copy
import json
import os
import tempfile
import time
from pathlib import Path
from typing import Any, Dict, Optional
class JsonRuntimeStore:
def __init__(self, path: Path, default: Optional[Dict[str, Any]] = None):
self.path = Path(path)
self.path.parent.mkdir(parents=True, exist_ok=True)
self.default = copy.deepcopy(default or {})
def load(self) -> Dict[str, Any]:
data = copy.deepcopy(self.default)
try:
loaded = json.loads(self.path.read_text(encoding="utf-8"))
if isinstance(loaded, dict):
data.update(loaded)
except (OSError, UnicodeError, json.JSONDecodeError):
pass
return data
def save(self, data: Dict[str, Any]) -> None:
"""Replace a complete object with a unique, owner-only temporary file.
A shared fixed `.tmp` name allowed independent writers to overwrite or
unlink each other's temporary file. Unique files prevent that race;
callers still need a database for multi-process read/modify/write use.
"""
if not isinstance(data, dict):
raise TypeError("runtime data must be a JSON object")
tmp = None
try:
with tempfile.NamedTemporaryFile(
mode="w", encoding="utf-8", dir=self.path.parent,
prefix=f".{self.path.name}.", suffix=".tmp", delete=False,
) as handle:
tmp = Path(handle.name)
json.dump(data, handle, ensure_ascii=False, indent=2, allow_nan=False)
handle.flush()
os.fsync(handle.fileno())
os.replace(tmp, self.path)
finally:
if tmp is not None:
tmp.unlink(missing_ok=True)
def update(self, **kwargs: Any) -> Dict[str, Any]:
data = self.load()
data.update(kwargs)
data["updated_at"] = time.time()
self.save(data)
return data