mathly / mathly2 /cached.py
wzender's picture
Claude
Subjects and worksheets: more subjects, and a worksheet becomes a lesson (#6)
b6f5bc4 unverified
Raw History Blame Contribute Delete
2.84 kB
"""A store that remembers slow-changing reads (classes, members, roles, tests, assignments) for a short time.
Any change made through it to one of those forgets everything remembered at once. Rows come back as copies.
"""
from __future__ import annotations
import copy
import threading
import time
READS = frozenset({
"classes", "classes_of_student", "classes_taught", "members", "get_class", "roles_of",
"tests_of_classes", "get_test", "assignments_of_classes", "get_assignment", "all_students", "accounts", "curriculum",
"subjects",
})
CHANGES = frozenset({
"create_class", "update_class", "delete_class", "add_member", "remove_member", "give_role", "take_role", "link_user",
"create_test", "update_test", "remove_test", "create_assignment", "remove_assignment", "set_lesson_dates",
"create_subject", "update_subject", "delete_subject", "create_lesson", "save_lesson", "delete_lesson",
})
def _key(name: str, args: tuple, kwargs: dict):
def freeze(v):
if isinstance(v, (set, frozenset)):
return tuple(sorted(v, key=repr))
if isinstance(v, (list, tuple)):
return tuple(freeze(x) for x in v)
return v
return name, freeze(args), tuple(sorted((k, freeze(v)) for k, v in kwargs.items()))
class CachedStore:
def __init__(self, store, seconds: float, clock=time.monotonic) -> None:
self._store, self._seconds, self._clock = store, seconds, clock
self._lock = threading.Lock()
self._kept: dict = {} # key -> (expires, value)
self._generation = 0 # bumped by every change, so a read that began before one is not kept
def forget(self) -> None:
with self._lock:
self._kept.clear()
self._generation += 1
def __getattr__(self, name: str):
target = getattr(self._store, name)
if self._seconds <= 0 or not callable(target):
return target
if name in CHANGES:
def change(*args, **kwargs):
try:
return target(*args, **kwargs)
finally:
self.forget()
return change
if name in READS:
def read(*args, **kwargs):
key = _key(name, args, kwargs)
now = self._clock()
with self._lock:
hit = self._kept.get(key)
if hit and hit[0] > now:
return copy.deepcopy(hit[1])
generation = self._generation
value = target(*args, **kwargs)
with self._lock:
if generation == self._generation:
self._kept[key] = (now + self._seconds, copy.deepcopy(value))
return value
return read
return target