Files
anonymisation/profile_defaults.py

357 lines
12 KiB
Python

#!/usr/bin/env python3
"""
Helpers partagés pour les profils métier.
"""
from __future__ import annotations
from copy import deepcopy
from pathlib import Path
from typing import Any, Dict
try:
import yaml
except Exception:
yaml = None
from config_defaults import CONFIG_DIR, deep_merge_dict
DEFAULT_PROFILES_CONFIG_PATH = CONFIG_DIR / "profiles.default.yml"
RUNTIME_PROFILES_CONFIG_PATH = CONFIG_DIR / "profiles.yml"
_RUNTIME_PROFILES_OVERLAY_TEXT = """# Surcharge locale des profils métier.
# Source de vérité : config/profiles.default.yml
# Ne mettez ici que les écarts spécifiques à votre environnement.
#
# Exemples :
# default_profile: chcb_strict
# profiles:
# mon_profil:
# label: Mon profil
# description: Surcharge locale
# require_manual_mask: true
# force_disable_vlm: true
# preferred_manual_mask_template: chcb/formulaire.yml
# param_lists:
# whitelist_phrases:
# - Document validé DIM
# dictionaries_overlay:
# blacklist:
# force_mask_terms:
# - MON_ETAB
{}
"""
_FALLBACK_DEFAULT_PROFILES_TEXT = """version: 1
default_profile: standard_local
profiles:
standard_local:
label: Standard local
description: Profil par défaut pour les traitements internes.
require_manual_mask: false
force_disable_vlm: false
dictionaries_overlay: {}
chcb_strict:
label: CHCB strict
description: Profil conservateur pour le CHCB, orienté diffusion prudente.
require_manual_mask: false
force_disable_vlm: true
dictionaries_overlay:
blacklist:
force_mask_terms:
- CHCB
- Centre Hospitalier de la Côte Basque
- CENTRE HOSPITALIER DE LA COTE BASQUE
partage_recherche:
label: Partage recherche
description: Profil externe strict. Le masque manuel est recommandé pour les formulaires répétitifs.
require_manual_mask: true
force_disable_vlm: true
dictionaries_overlay:
blacklist:
force_mask_terms:
- CHCB
- Centre Hospitalier de la Côte Basque
- CENTRE HOSPITALIER DE LA COTE BASQUE
dossier_audit:
label: Dossier audit
description: Profil orienté traçabilité et reproductibilité.
require_manual_mask: false
force_disable_vlm: true
dictionaries_overlay: {}
demo:
label: Démo
description: Profil léger pour démonstration interne sur poste bureautique.
require_manual_mask: false
force_disable_vlm: true
dictionaries_overlay: {}
"""
_FALLBACK_DEFAULT_PROFILES_DICT: Dict[str, Any] = {
"version": 1,
"default_profile": "standard_local",
"profiles": {
"standard_local": {
"label": "Standard local",
"description": "Profil par défaut pour les traitements internes.",
"require_manual_mask": False,
"force_disable_vlm": False,
"dictionaries_overlay": {},
},
"chcb_strict": {
"label": "CHCB strict",
"description": "Profil conservateur pour le CHCB, orienté diffusion prudente.",
"require_manual_mask": False,
"force_disable_vlm": True,
"dictionaries_overlay": {
"blacklist": {
"force_mask_terms": [
"CHCB",
"Centre Hospitalier de la Côte Basque",
"CENTRE HOSPITALIER DE LA COTE BASQUE",
],
},
},
},
"partage_recherche": {
"label": "Partage recherche",
"description": (
"Profil externe strict. Le masque manuel est recommandé "
"pour les formulaires répétitifs."
),
"require_manual_mask": True,
"force_disable_vlm": True,
"dictionaries_overlay": {
"blacklist": {
"force_mask_terms": [
"CHCB",
"Centre Hospitalier de la Côte Basque",
"CENTRE HOSPITALIER DE LA COTE BASQUE",
],
},
},
},
"dossier_audit": {
"label": "Dossier audit",
"description": "Profil orienté traçabilité et reproductibilité.",
"require_manual_mask": False,
"force_disable_vlm": True,
"dictionaries_overlay": {},
},
"demo": {
"label": "Démo",
"description": "Profil léger pour démonstration interne sur poste bureautique.",
"require_manual_mask": False,
"force_disable_vlm": True,
"dictionaries_overlay": {},
},
},
}
def read_default_profiles_text() -> str:
try:
return DEFAULT_PROFILES_CONFIG_PATH.read_text(encoding="utf-8")
except Exception:
return _FALLBACK_DEFAULT_PROFILES_TEXT
def read_runtime_profiles_overlay_text() -> str:
return _RUNTIME_PROFILES_OVERLAY_TEXT
def load_default_profiles_dict() -> Dict[str, Any]:
text = read_default_profiles_text()
if yaml is not None:
try:
loaded = yaml.safe_load(text) or {}
if isinstance(loaded, dict):
return loaded
except Exception:
pass
return deepcopy(_FALLBACK_DEFAULT_PROFILES_DICT)
def list_default_profile_keys() -> set[str]:
data = load_default_profiles_dict()
profiles = data.get("profiles", {}) or {}
if not isinstance(profiles, dict):
return set()
return {str(key) for key in profiles}
def load_runtime_profiles_overlay_dict(path: Path | None = None) -> Dict[str, Any]:
target = Path(path) if path is not None else RUNTIME_PROFILES_CONFIG_PATH
if not target.exists() or yaml is None:
return {}
try:
loaded = yaml.safe_load(target.read_text(encoding="utf-8")) or {}
if isinstance(loaded, dict):
return loaded
except Exception:
pass
return {}
def load_effective_profiles_dict(path: Path | None = None) -> Dict[str, Any]:
return deep_merge_dict(
load_default_profiles_dict(),
load_runtime_profiles_overlay_dict(path),
)
def _normalize_string_list(values: Any) -> list[str]:
if not isinstance(values, list):
return []
normalized: list[str] = []
for value in values:
text = str(value).strip()
if text:
normalized.append(text)
return normalized
def _normalize_param_lists(value: Any) -> Dict[str, list[str]]:
if not isinstance(value, dict):
return {}
return {
"whitelist_phrases": _normalize_string_list(value.get("whitelist_phrases", [])),
"blacklist_force_mask_terms": _normalize_string_list(
value.get("blacklist_force_mask_terms", [])
),
"additional_stopwords": _normalize_string_list(value.get("additional_stopwords", [])),
}
def _write_runtime_profiles_overlay_dict(path: Path, data: Dict[str, Any]) -> Path:
if yaml is None:
raise RuntimeError("PyYAML indisponible")
body = yaml.safe_dump(
data or {},
allow_unicode=True,
default_flow_style=False,
sort_keys=False,
)
header = (
"# Surcharge locale des profils métier.\n"
"# Source de vérité : config/profiles.default.yml\n"
"# Les profils créés depuis la GUI sont enregistrés ici.\n"
)
path.write_text(header + "\n" + body, encoding="utf-8")
return path
def ensure_runtime_profiles_config(path: Path | None = None) -> Path:
target = Path(path) if path is not None else RUNTIME_PROFILES_CONFIG_PATH
if not target.exists():
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(read_runtime_profiles_overlay_text(), encoding="utf-8")
return target
def list_effective_profiles(path: Path | None = None) -> Dict[str, Dict[str, Any]]:
data = load_effective_profiles_dict(path)
profiles = data.get("profiles", {}) or {}
if not isinstance(profiles, dict):
return {}
normalized: Dict[str, Dict[str, Any]] = {}
for key, value in profiles.items():
if not isinstance(value, dict):
continue
raw_param_lists = value.get("param_lists")
has_param_lists = isinstance(raw_param_lists, dict)
preferred_manual_mask_template = str(value.get("preferred_manual_mask_template") or "").strip()
normalized[str(key)] = {
"label": str(value.get("label") or key),
"description": str(value.get("description") or ""),
"require_manual_mask": bool(value.get("require_manual_mask", False)),
"force_disable_vlm": bool(value.get("force_disable_vlm", False)),
"dictionaries_overlay": deepcopy(value.get("dictionaries_overlay") or {}),
"param_lists": _normalize_param_lists(raw_param_lists),
"has_param_lists": has_param_lists,
"preferred_manual_mask_template": preferred_manual_mask_template,
"has_preferred_manual_mask_template": "preferred_manual_mask_template" in value,
}
return normalized
def get_default_profile_key(path: Path | None = None) -> str:
data = load_effective_profiles_dict(path)
key = str(data.get("default_profile") or "").strip()
profiles = list_effective_profiles(path)
if key and key in profiles:
return key
if profiles:
return next(iter(profiles))
return "standard_local"
def save_runtime_profile(
profile_key: str,
profile_spec: Dict[str, Any],
path: Path | None = None,
*,
set_default: bool = False,
) -> Path:
target = ensure_runtime_profiles_config(path)
data = load_runtime_profiles_overlay_dict(target)
if not isinstance(data, dict):
data = {}
profiles = data.get("profiles")
if not isinstance(profiles, dict):
profiles = {}
data["profiles"] = profiles
normalized_spec: Dict[str, Any] = {
"label": str(profile_spec.get("label") or profile_key),
"description": str(profile_spec.get("description") or ""),
"require_manual_mask": bool(profile_spec.get("require_manual_mask", False)),
"force_disable_vlm": bool(profile_spec.get("force_disable_vlm", False)),
"dictionaries_overlay": deepcopy(profile_spec.get("dictionaries_overlay") or {}),
}
if profile_spec.get("has_param_lists") or "param_lists" in profile_spec:
normalized_spec["param_lists"] = _normalize_param_lists(profile_spec.get("param_lists"))
if (
profile_spec.get("has_preferred_manual_mask_template")
or "preferred_manual_mask_template" in profile_spec
):
normalized_spec["preferred_manual_mask_template"] = str(
profile_spec.get("preferred_manual_mask_template") or ""
).strip()
profiles[str(profile_key)] = normalized_spec
if set_default:
data["default_profile"] = str(profile_key)
return _write_runtime_profiles_overlay_dict(target, data)
def set_runtime_default_profile(profile_key: str, path: Path | None = None) -> Path:
target = ensure_runtime_profiles_config(path)
data = load_runtime_profiles_overlay_dict(target)
if not isinstance(data, dict):
data = {}
data["default_profile"] = str(profile_key)
return _write_runtime_profiles_overlay_dict(target, data)
def delete_runtime_profile(profile_key: str, path: Path | None = None) -> Path:
target = ensure_runtime_profiles_config(path)
data = load_runtime_profiles_overlay_dict(target)
if not isinstance(data, dict):
data = {}
profiles = data.get("profiles")
if isinstance(profiles, dict):
profiles.pop(str(profile_key), None)
if not profiles:
data.pop("profiles", None)
if str(data.get("default_profile") or "").strip() == str(profile_key):
data["default_profile"] = "standard_local"
return _write_runtime_profiles_overlay_dict(target, data)