357 lines
12 KiB
Python
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)
|