#!/usr/bin/env python3 """Gleicht Fristen, Aufräumregeln und Datenschutzerklärung ab. python3 tools/check-retention.py Drei Fragen, die sich sonst nur von Hand beantworten lassen: 1. Wird jede Frist aus der Verwaltung auch tatsächlich irgendwo angewandt? Eine Einstellung, die nichts bewirkt, ist schlimmer als keine - sie behauptet eine Wirkung. 2. Wird jede Tabelle mit einem Ablauf- oder Löschzeitpunkt vom Aufräumlauf erfasst? Sonst wächst sie unbegrenzt, und die Datenschutzerklärung sagt die Unwahrheit. 3. Werden die Fristen im Aufräumlauf aus der Laufzeitkonfiguration gelesen - oder stehen dort noch feste Zahlen? """ import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent APP = ROOT / "backend" / "app" RUNTIME = APP / "runtime_settings.py" MAINTENANCE = APP / "maintenance.py" MODELS = APP / "models.py" # Tabellen mit Zeitfeldern, die BEWUSST nicht aufgeräumt werden. NO_CLEANUP = { # Preise sind der Zweck der Sammlung und ohne Personenbezug. "price_point": "unbefristet, kein Personenbezug", # Wird beim Zustellversuch aufgeräumt, nicht zeitgesteuert. "push_subscription": "Aufräumen bei ungültigem Endpunkt", # Haengt an der Mitgliedschaft, verschwindet per CASCADE. "notify_state": "CASCADE mit der Liste", # Laufzeitkonfiguration, kein Ablauf. "setting": "Konfiguration", # Kernbestand. "user": "Kontenbereinigung in _retire_users", "shopping_list": "Archivbereinigung in _archive_lists", "list_member": "CASCADE", "market": "Soft Delete, siehe cleanup_deleted_days", "category": "Soft Delete, siehe cleanup_deleted_days", "article": "Soft Delete, siehe cleanup_deleted_days", "article_attribute": "CASCADE", "article_market": "CASCADE", "list_item": "Soft Delete, siehe cleanup_deleted_days", "user_session": "über expires_at aufgeräumt", } def duration_keys() -> list[str]: source = RUNTIME.read_text(encoding="utf-8") return re.findall(r'^\s*"(\w+)",\s*"', source, re.M) def main() -> int: problems: list[str] = [] notes: list[str] = [] keys = duration_keys() if not keys: print("Keine Fristen in runtime_settings.py gefunden.", file=sys.stderr) return 2 # --- 1. Wird jede Frist irgendwo angewandt? --- sources = { path: path.read_text(encoding="utf-8") for path in APP.rglob("*.py") if path.name not in {"runtime_settings.py", "config.py"} } for key in keys: users = [ path.relative_to(ROOT) for path, text in sources.items() if f'"{key}"' in text ] if not users: problems.append( f"Frist '{key}' ist einstellbar, wird aber nirgends verwendet" ) else: notes.append(f" {key:26} -> {', '.join(str(u) for u in users)}") # --- 2. Erfasst der Aufräumlauf alle Tabellen mit Ablauf? --- models = MODELS.read_text(encoding="utf-8") maintenance = MAINTENANCE.read_text(encoding="utf-8") for match in re.finditer(r'__tablename__ = "(\w+)"', models): table = match.group(1) block = models[match.start():] stop = block.find("\nclass ", 1) if stop > 0: block = block[:stop] has_expiry = bool( re.search(r"^\s+(expires_at|deleted_at|fetched_at|created_at|window_start)", block, re.M) ) if not has_expiry: continue # Klassenname aus der Tabelle ableiten class_match = re.search( r"class (\w+)\(Base\):(?:(?!\nclass ).)*?__tablename__ = \"" + table + '"', models, re.S, ) class_name = class_match.group(1) if class_match else None covered = class_name and class_name in maintenance if not covered and table not in NO_CLEANUP: problems.append( f"Tabelle '{table}' hat ein Zeitfeld, wird aber vom " "Aufräumlauf nicht erfasst" ) # --- 3. Feste Zahlen im Aufräumlauf? --- for line_no, line in enumerate(maintenance.splitlines(), 1): if "timedelta(days=" not in line: continue # Erlaubt: aus Variablen berechnet, oder die fest dokumentierten # Sonderfälle (7 Tage Puffer für Mail-Token, 1 Tag Rate Limit). literal = re.search(r"timedelta\(days=(\d+)\)", line) if literal and literal.group(1) not in {"1", "7"}: problems.append( f"maintenance.py Zeile {line_no}: feste Frist " f"{literal.group(1)} Tage - gehört in runtime_settings.py" ) print("Fristen und ihre Verwendung:") print("\n".join(notes)) if problems: print(f"\n{len(problems)} Problem(e):\n") for entry in problems: print(f" - {entry}") return 1 print(f"\n{len(keys)} Fristen, alle angewandt; Aufräumlauf vollständig.") return 0 if __name__ == "__main__": raise SystemExit(main())