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>
This commit is contained in:
84
gui_v6/license_store.py
Normal file
84
gui_v6/license_store.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user