#!/usr/bin/env python3 """ Erweiterung des Lizenzsystems um "Edition" (Schlüssel-Präfix) und festes Ablaufdatum – für das Kundencenter. Was der Patch tut (alles abwärtskompatibel, bestehende Clients ändern sich nicht): 1. POST /licenses/ akzeptiert optional key_prefix: "PREMIUM" | "TRIAL" | "LIFETIME" -> Schlüssel lautet dann "-" (das Familytool erkennt daran die Stufe; ohne Präfix = Unlimited). 2. POST /licenses/ beachtet ein ausdrücklich angegebenes expires_at (bisher wurde es ignoriert). 3. GET / meldet "features": ["key_prefix", "explicit_expiry"] (daran erkennt das Kundencenter die Erweiterung). 4. models.License.license_key wird von 36 auf 64 Zeichen erweitert (DB-Änderung: siehe README, VORHER ausführen). Nutzung (Standard = nur prüfen, nichts ändern): python3 apply_erweiterung.py /pfad/zu/licensing-system/backend # Trockenlauf python3 apply_erweiterung.py /pfad/zu/licensing-system/backend --apply # anwenden (mit Sicherungskopien *.bak-) Das Skript ist idempotent: bereits vorhandene Änderungen werden erkannt und übersprungen. """ import re, sys, time, py_compile, pathlib def main() -> int: args = [a for a in sys.argv[1:] if not a.startswith('--')] apply = '--apply' in sys.argv if len(args) != 1: print(__doc__); return 2 root = pathlib.Path(args[0]) / 'app' files = {n: root / p for n, p in {'models': 'models.py', 'schemas': 'schemas.py', 'licenses': 'routers/licenses.py', 'main': 'main.py'}.items()} for n, f in files.items(): if not f.exists(): print(f'FEHLER: {f} nicht gefunden – falscher Pfad? (erwartet: .../licensing-system/backend)'); return 1 src = {n: f.read_text() for n, f in files.items()} new = dict(src); report = [] def step(name, key, old, repl, marker): if marker in new[key]: report.append(f' = {name}: bereits vorhanden'); return True if old not in new[key]: report.append(f' ! {name}: erwartete Stelle NICHT gefunden – bitte manuell prüfen (siehe README)'); return False new[key] = new[key].replace(old, repl, 1); report.append(f' + {name}'); return True ok = True ok &= step('models: license_key 64 Zeichen', 'models', 'license_key = Column(String(36), unique=True, index=True, default=lambda: str(uuid.uuid4()))', 'license_key = Column(String(64), unique=True, index=True, default=lambda: str(uuid.uuid4()))', 'String(64), unique=True') ok &= step('schemas: key_prefix in LicenseCreate', 'schemas', 'class LicenseCreate(LicenseBase):\n pass', 'class LicenseCreate(LicenseBase):\n # Optionales Schlüssel-Präfix (Edition): Schlüssel lautet "-". Ohne Präfix gilt die Lizenz im Familytool als UNLIMITED.\n key_prefix: Optional[Literal["PREMIUM", "TRIAL", "LIFETIME"]] = None', 'key_prefix: Optional[Literal') if 'Literal' not in re.split(r'\nclass ', new['schemas'], maxsplit=1)[0]: m = re.search(r'^from typing import (.+)$', new['schemas'], re.M) new['schemas'] = new['schemas'].replace(m.group(0), f'from typing import Literal, {m.group(1)}', 1) if m else 'from typing import Literal\n' + new['schemas'] report.append(' + schemas: Literal importiert') ok &= step('licenses: festes Ablaufdatum hat Vorrang', 'licenses', ' # Calculate expires_at based on duration_type\n expires_at = None\n if license.duration_type != schemas.LicenseDurationType.UNLIMITED:', ' # Ein ausdrücklich angegebenes Ablaufdatum hat Vorrang; sonst wird es aus duration_type berechnet\n expires_at = license.expires_at\n if expires_at is None and license.duration_type != schemas.LicenseDurationType.UNLIMITED:', 'expires_at = license.expires_at') ok &= step('licenses: Schlüssel mit Präfix', 'licenses', ' db_license = models.License(\n program_id=license.program_id,', ' extra = {"license_key": f"{license.key_prefix}-{uuid.uuid4()}"} if license.key_prefix else {}\n db_license = models.License(\n **extra,\n program_id=license.program_id,', 'f"{license.key_prefix}-') if not re.search(r'^import uuid$', new['licenses'], re.M): new['licenses'] = re.sub(r'^import datetime$', 'import datetime\nimport uuid', new['licenses'], count=1, flags=re.M); report.append(' + licenses: uuid importiert') ok &= step('main: features in GET /', 'main', 'return {"message": "Licensing System Backend is running!"}', 'return {"message": "Licensing System Backend is running!", "features": ["key_prefix", "explicit_expiry"]}', '"features"') print('\n'.join(report)) if not ok: print('\nABBRUCH: Nicht alle Stellen passen (Code weicht ab). Es wurde NICHTS geändert.'); return 1 changed = [n for n in files if new[n] != src[n]] if not changed: print('\nNichts zu tun – die Erweiterung ist bereits vollständig eingespielt.'); return 0 if not apply: print(f'\nTROCKENLAUF: würde {", ".join(files[n].name for n in changed)} ändern. Mit --apply ausführen.'); return 0 ts = time.strftime('%Y%m%d-%H%M%S') for n in changed: files[n].with_name(files[n].name + f'.bak-{ts}').write_text(src[n]) files[n].write_text(new[n]) py_compile.compile(str(files[n]), doraise=True) print(f'\nANGEWENDET: {", ".join(files[n].name for n in changed)} (Sicherungen: *.bak-{ts}). Jetzt Dienst neu starten (siehe README).') return 0 if __name__ == '__main__': sys.exit(main())