Ergänzung Datenschutzerklärung und Impressum, Fristenformular in Einstellungen
This commit is contained in:
147
tools/check-retention.py
Normal file
147
tools/check-retention.py
Normal file
@@ -0,0 +1,147 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user