from __future__ import annotations import logging import threading from typing import Generic, TypeVar import diskcache logger = logging.getLogger(__name__) T = TypeVar("T") class SingleObjectDataCache(Generic[T]): """Cache for status and solar conditions. This uses DiskCache, but unlike the standard DiskCache users like activity ref and callsign lookup handlers, status and solar conditions are persisted as a single object. If we just load the object from DiskCache and modify it, DiskCache doesn't know that it's been updated and needs re-caching, so we provide a store() method that any functions updating the object can call afterwards.""" def __init__(self, cache_dir: str, object_if_empty: T) -> None: """Initialize a SingleObjectDataCache. Provide the directory to load the cache from and save it to. If the cache is empty, the provided object_if_empty parameter will be used to initialise it.""" self._lock = threading.Lock() self._cache = diskcache.Cache(cache_dir) # This cache stores a single object, doesn't matter what it's called so "object" will do if "object" not in self._cache: self._cache.add("object", object_if_empty) try: self._obj: T = self._cache.get("object") except Exception: # noqa: BLE001 (If any exceptions we treat the data as junk and start from scratch, it's probably due to a version upgrade having incompatible data structures, fine to not log the exception in this case) logger.warning(f"Failed to load cache from {cache_dir}, clearing it.") self._cache.clear() self._cache.add("object", object_if_empty) self._obj = object_if_empty def get(self) -> T: """Get the data object. This can then be manipulated as necessary across multiple threads. Any function modifying the object must remember to call store() afterwards.""" return self._obj def store(self) -> None: """Store the updated object in the cache. Any function modifying the object must remember to call this afterwards.""" with self._lock: self._cache.set("object", self._obj) def close(self) -> None: self.store() self._cache.close()