Files
anonymisation/gui_v6/license_store.py
Domi31tls d265cd3269 feat(gui): add GUI V6 G1 foundation (license client/store, shell, About tab)
Socle de la refonte GUI V6 (couche présentation uniquement, aucune logique de
détection) :
- license_store: stockage licence hors dépôt (%LOCALAPPDATA%/Aivanov | XDG),
  read/write atomique/delete, ne journalise aucun token
- license_client: LicenseStatus + activate/check/local_status, session HTTP
  injectable, serveur indisponible géré sans crash, aucune clé privée
- theme: 4 thèmes + couleurs de statut licence
- app + tab_about: shell customtkinter minimal (header, bandeau licence,
  3 onglets), onglet À propos étoffé
- Pseudonymisation_Gui_V6.py: point d'entrée + --self-test (exit 0 sans fenêtre)
- requirements.txt: customtkinter==5.2.2

Tests: 20 nouveaux (store sur vrais fichiers, client sur session injectée).
Suite tests/unit: 167 passed, 0 régression. V5/moteur/managers/specs intacts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 18:50:23 +02:00

85 lines
2.9 KiB
Python

"""Stockage local de la licence signée, hors dépôt.
La licence est stockée dans l'espace utilisateur, jamais dans le dépôt Git :
- Windows : ``%LOCALAPPDATA%/Aivanov/Anonymisation/license.json``
- Linux/dev : ``~/.local/share/aivanov/anonymisation/license.json``
Règle de sécurité : ce module ne journalise jamais le contenu de la licence
(le token signé est sensible). Aucune impression, aucun log du payload.
"""
from __future__ import annotations
import json
import os
import sys
from pathlib import Path
from typing import Any, Optional
_APP_VENDOR = "Aivanov"
_APP_NAME = "Anonymisation"
_LICENSE_FILENAME = "license.json"
def default_license_path() -> Path:
"""Retourne le chemin du fichier licence dans l'espace utilisateur.
Le répertoire n'est pas créé ici ; il l'est à la première écriture.
"""
if sys.platform.startswith("win"):
base = os.environ.get("LOCALAPPDATA")
root = Path(base) if base else Path.home() / "AppData" / "Local"
return root / _APP_VENDOR / _APP_NAME / _LICENSE_FILENAME
# Linux / macOS / dev : respecte XDG_DATA_HOME si défini.
base = os.environ.get("XDG_DATA_HOME")
root = Path(base) if base else Path.home() / ".local" / "share"
return root / _APP_VENDOR.lower() / _APP_NAME.lower() / _LICENSE_FILENAME
class LicenseStore:
"""Lit / écrit / supprime le fichier licence local (JSON).
Le chemin est injectable pour les tests (vrai fichier dans un tmp_path) ;
par défaut il pointe vers l'emplacement utilisateur de la plateforme.
"""
def __init__(self, path: Optional[Path] = None) -> None:
self._path = Path(path) if path is not None else default_license_path()
@property
def path(self) -> Path:
return self._path
def exists(self) -> bool:
return self._path.is_file()
def load(self) -> Optional[dict[str, Any]]:
"""Retourne le payload licence, ou ``None`` si absent / illisible."""
if not self._path.is_file():
return None
try:
with self._path.open("r", encoding="utf-8") as handle:
data = json.load(handle)
except (json.JSONDecodeError, OSError, ValueError):
return None
return data if isinstance(data, dict) else None
def save(self, data: dict[str, Any]) -> Path:
"""Écrit le payload licence de façon atomique. Crée le répertoire parent."""
self._path.parent.mkdir(parents=True, exist_ok=True)
tmp = self._path.with_name(self._path.name + ".tmp")
with tmp.open("w", encoding="utf-8") as handle:
json.dump(data, handle, ensure_ascii=False, indent=2)
os.replace(tmp, self._path)
return self._path
def delete(self) -> bool:
"""Supprime le fichier licence. Retourne True s'il existait."""
try:
self._path.unlink()
return True
except FileNotFoundError:
return False