Download mathly2/cleanup.py from wzender/mathly: direct link, hf CLI and curl.
- Browser
- Download file 3.65 kB
-
https://huggingface.co/spaces/wzender/mathly/resolve/main/mathly2/cleanup.py
- Command line
-
hf download hf://spaces/wzender/mathly/mathly2/cleanup.py
-
curl -L -o cleanup.py https://huggingface.co/spaces/wzender/mathly/resolve/main/mathly2/cleanup.py
3.65 kB
| """The hourly cleanup of pictures, and deleting a student's pictures on request. `python -m mathly2.cleanup` runs it once; the server runs it | |
| every hour when it is connected to Supabase.""" | |
| from __future__ import annotations | |
| import logging | |
| import threading | |
| from datetime import datetime | |
| from . import clock, pictures, retention | |
| log = logging.getLogger("mathly.cleanup") | |
| def run(store, backend, now: datetime | None = None) -> dict: | |
| """Remove pictures nothing refers to (after a day), answer pictures past their school year, and the pages of removed assignments. | |
| Returns how many of each were removed. A picture whose bytes cannot be deleted now is left for the next run.""" | |
| now = now or clock.now() | |
| today = clock.school_day(now) | |
| removed = {"orphans": 0, "expired": 0, "removed_assignments": 0} | |
| for pic in store.pictures(): | |
| refs = store.picture_refs(pic["id"]) | |
| referenced = any(refs.values()) | |
| if pic["kind"] in ("answer", "work") and retention.is_expired(clock.school_day(pic["created_at"]), today): | |
| reason = "expired" | |
| elif not referenced and now - pic["created_at"] > retention.ORPHAN_AGE: | |
| reason = "orphans" | |
| elif pic["kind"] == "page" and refs["assignments"] and all((store.get_assignment(a) or {}).get("removed_at") for a in refs["assignments"]): | |
| reason = "removed_assignments" | |
| else: | |
| continue | |
| try: | |
| pictures.remove_picture(store, backend, pic, now) | |
| removed[reason] += 1 | |
| except pictures.PictureError as exc: | |
| log.warning("could not delete a picture (%s); it stays for the next run", exc.code) | |
| return removed | |
| def delete_answer_pictures(store, backend, assignment_ids, user_id: str, now: datetime | None = None) -> int: | |
| """Delete a student's answer pictures for these assignments now (a teacher of the class, a manager or a developer asked). The submissions | |
| keep their grade and note. Returns how many pictures were deleted.""" | |
| count = 0 | |
| for sub in store.submissions_of_assignments(assignment_ids): | |
| if sub["user_id"] != user_id: | |
| continue | |
| for pid in sub["pictures"]: | |
| pic = store.get_picture(pid) | |
| if pic and pic["kind"] == "answer" and pic["owner_id"] == user_id and not pic["deleted_at"]: | |
| pictures.remove_picture(store, backend, pic, now) | |
| count += 1 | |
| return count | |
| def start_hourly(store, backend, every: float = 3600.0) -> threading.Event: | |
| """Run the cleanup now and then every `every` seconds in a background thread. Returns an event that stops it when set.""" | |
| stop = threading.Event() | |
| def loop() -> None: | |
| while True: | |
| try: | |
| log.info("cleanup removed %s", run(store, backend)) | |
| except Exception: # a failed run must not stop the next one | |
| log.exception("the picture cleanup failed") | |
| if stop.wait(every): | |
| return | |
| threading.Thread(target=loop, name="picture-cleanup", daemon=True).start() | |
| return stop | |
| def main() -> None: | |
| from .config import load_settings | |
| from .store import SupabaseStore | |
| settings = load_settings() | |
| if not settings.supabase_on: | |
| raise SystemExit("Set the Supabase settings: the cleanup works on the real database.") | |
| store = SupabaseStore(settings.supabase_url, settings.supabase_secret) | |
| backend = pictures.SupabasePictures(settings.supabase_url, settings.supabase_secret, settings.answer_bucket) | |
| print("Removed", run(store, backend)) | |
| if __name__ == "__main__": | |
| main() | |