isaachwf's picture
Sync from GitHub via hub-sync
008947a verified
Raw History Blame Contribute Delete
3.06 kB
"""Atomic, versioned disk caches for geocoding and prepared map data."""
from __future__ import annotations
import hashlib
import json
import os
import shutil
import tempfile
from collections.abc import Iterator
from contextlib import contextmanager
from pathlib import Path
from filelock import FileLock
from .models import MapConfig
from .paths import CACHE_DIR
CACHE_SCHEMA_VERSION = 1
def map_cache_key(config: MapConfig) -> str:
"""Build a stable key from data-affecting map parameters only."""
bbox = config.viewport.bbox
payload = {
"schema": CACHE_SCHEMA_VERSION,
"center": [round(config.viewport.center.latitude, 5), round(config.viewport.center.longitude, 5)],
"bbox": [round(value, 5) for value in bbox.as_tuple()],
"distance_m": round(config.viewport.distance_m or 0, 1),
"network_type": config.network_type,
"layers": sorted(config.data_layers),
}
encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
return hashlib.sha256(encoded).hexdigest()
class DiskCache:
"""Small filesystem cache with per-key locking and atomic directories."""
def __init__(self, root: Path = CACHE_DIR) -> None:
self.root = Path(root)
self.map_root = self.root / "map_data"
self.geocoding_root = self.root / "geocoding"
def map_path(self, key: str) -> Path:
return self.map_root / key
def geocoding_path(self, key: str) -> Path:
return self.geocoding_root / f"{key}.json"
@contextmanager
def lock(self, namespace: str, key: str) -> Iterator[None]:
lock_dir = self.root / "locks" / namespace
lock_dir.mkdir(parents=True, exist_ok=True)
with FileLock(str(lock_dir / f"{key}.lock"), timeout=180):
yield
@contextmanager
def atomic_directory(self, destination: Path) -> Iterator[Path]:
destination.parent.mkdir(parents=True, exist_ok=True)
temporary = Path(tempfile.mkdtemp(prefix=f".{destination.name}-", dir=destination.parent))
try:
yield temporary
if destination.exists():
shutil.rmtree(destination)
os.replace(temporary, destination)
except Exception:
shutil.rmtree(temporary, ignore_errors=True)
raise
def read_json(self, path: Path) -> dict[str, object] | None:
try:
return json.loads(path.read_text(encoding="utf-8"))
except (FileNotFoundError, json.JSONDecodeError, OSError):
return None
def write_json(self, path: Path, payload: dict[str, object]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
handle, name = tempfile.mkstemp(prefix=f".{path.stem}-", suffix=".tmp", dir=path.parent)
try:
with os.fdopen(handle, "w", encoding="utf-8") as stream:
json.dump(payload, stream, ensure_ascii=False, sort_keys=True)
os.replace(name, path)
finally:
if os.path.exists(name):
os.unlink(name)