Retour Dom : remplacer la page vitrine par un vrai éditeur de profils. - gui_v6/profile_editor.py : couche logique (build_profile_spec, profile_is_editable runtime vs defaut, list_profile_choices, slug_for_copy, save/set_default/delete) au-dessus de profile_defaults — persistance dans config/profiles.yml. - gui_v6/editable_list.py : EditableTermList (tableau scrollable de termes, ajout/suppression, pas de pastilles) — reste lisible à 50+ termes. - tab_config : sous-onglet « 👤 Profils » réintroduit comme éditeur — menu déroulant « Profil à modifier », boutons Nouveau / Dupliquer / Enregistrer / Annuler / Définir par défaut, sections Identité, Masquage (require_manual_mask, template), Moteurs (force_disable_vlm), Mots (à masquer/conserver/ignorer éditables), Règles « à venir ». Profils défaut = lecture seule (dupliquer pour modifier). Confirmation non bloquante (pas de modale). - Réglages : bouton « ✏️ Modifier le profil… » → ouvre Profils sur le profil actif. Pas de pastilles inline. Persiste : label, description, require_manual_mask, force_disable_vlm, preferred_manual_mask_template, param_lists (3 listes). 260 tests unit OK (0 régression), self-test OK, nav 5 sous-onglets + thème OK. Préserve 1bbe70a/d30f7b7. Aucun build/push sans GO Dom. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
121 lines
4.0 KiB
Python
121 lines
4.0 KiB
Python
"""Couche logique de l'éditeur de profils (persistance via profile_defaults).
|
|
|
|
Wrappers testables sans display : assemblage de la spec, détection
|
|
runtime/éditable vs defaut/lecture-seule, sauvegarde dans config/profiles.yml.
|
|
Aucun profil par défaut (profiles.default.yml) n'est jamais modifié ici.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Any, Iterable, Optional
|
|
|
|
from profile_defaults import (
|
|
delete_runtime_profile,
|
|
get_default_profile_key,
|
|
list_effective_profiles,
|
|
load_runtime_profiles_overlay_dict,
|
|
save_runtime_profile,
|
|
set_runtime_default_profile,
|
|
)
|
|
|
|
|
|
def _clean_list(values: Optional[Iterable]) -> list[str]:
|
|
return [str(v).strip() for v in (values or []) if str(v).strip()]
|
|
|
|
|
|
def build_profile_spec(
|
|
*,
|
|
label: str,
|
|
description: str = "",
|
|
require_manual_mask: bool = False,
|
|
force_disable_vlm: bool = False,
|
|
preferred_manual_mask_template: str = "",
|
|
whitelist: Optional[Iterable] = None,
|
|
blacklist: Optional[Iterable] = None,
|
|
stopwords: Optional[Iterable] = None,
|
|
) -> dict[str, Any]:
|
|
"""Assemble une spec de profil persistable (3 listes normalisées)."""
|
|
return {
|
|
"label": str(label or "").strip(),
|
|
"description": str(description or ""),
|
|
"require_manual_mask": bool(require_manual_mask),
|
|
"force_disable_vlm": bool(force_disable_vlm),
|
|
"preferred_manual_mask_template": str(preferred_manual_mask_template or "").strip(),
|
|
"has_preferred_manual_mask_template": True,
|
|
"has_param_lists": True,
|
|
"param_lists": {
|
|
"whitelist_phrases": _clean_list(whitelist),
|
|
"blacklist_force_mask_terms": _clean_list(blacklist),
|
|
"additional_stopwords": _clean_list(stopwords),
|
|
},
|
|
}
|
|
|
|
|
|
def runtime_profile_keys(path: Path | None = None) -> set[str]:
|
|
"""Clés des profils définis dans l'overlay runtime (config/profiles.yml)."""
|
|
try:
|
|
data = load_runtime_profiles_overlay_dict(path) or {}
|
|
except Exception:
|
|
return set()
|
|
profiles = data.get("profiles") if isinstance(data, dict) else None
|
|
return set(profiles) if isinstance(profiles, dict) else set()
|
|
|
|
|
|
def profile_is_editable(key: str, path: Path | None = None) -> bool:
|
|
"""Un profil est éditable s'il est dans l'overlay runtime (pas un defaut pur)."""
|
|
return key in runtime_profile_keys(path)
|
|
|
|
|
|
def _default_key(path: Path | None = None) -> Optional[str]:
|
|
try:
|
|
data = load_runtime_profiles_overlay_dict(path) or {}
|
|
if isinstance(data, dict) and data.get("default_profile"):
|
|
return str(data["default_profile"])
|
|
except Exception:
|
|
pass
|
|
try:
|
|
return get_default_profile_key()
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def list_profile_choices(path: Path | None = None) -> list[dict]:
|
|
"""Liste triée des profils avec méta : ``key``, ``label``, ``editable``, ``is_default``."""
|
|
profiles = list_effective_profiles(path)
|
|
runtime = runtime_profile_keys(path)
|
|
default = _default_key(path)
|
|
return [
|
|
{
|
|
"key": key,
|
|
"label": str(profiles[key].get("label") or key),
|
|
"editable": key in runtime,
|
|
"is_default": key == default,
|
|
}
|
|
for key in sorted(profiles)
|
|
]
|
|
|
|
|
|
def slug_for_copy(key: str, existing: Iterable[str]) -> str:
|
|
"""Clé de copie unique : ``{key}_copie`` puis ``_2``, ``_3``…"""
|
|
existing = set(existing)
|
|
base = f"{key}_copie"
|
|
if base not in existing:
|
|
return base
|
|
index = 2
|
|
while f"{base}_{index}" in existing:
|
|
index += 1
|
|
return f"{base}_{index}"
|
|
|
|
|
|
def save_profile(key: str, spec: dict, path: Path | None = None, *, set_default: bool = False) -> Path:
|
|
return save_runtime_profile(key, spec, path, set_default=set_default)
|
|
|
|
|
|
def set_default_profile(key: str, path: Path | None = None) -> Path:
|
|
return set_runtime_default_profile(key, path)
|
|
|
|
|
|
def delete_profile(key: str, path: Path | None = None) -> Path:
|
|
return delete_runtime_profile(key, path)
|