kundencenter/docs/lizenzsystem-erweiterung/apply_erweiterung.py
2026-09-27 00:51:32 +02:00

82 lines
5.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 "<PRAEFIX>-<uuid>" (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-<zeit>)
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 "<PRAEFIX>-<uuid>". 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())