Strategy SELECT puis INSERT/UPDATE plutot que ON CONFLICT: - Cherche par ioda_libelle d'abord, sinon par nom_court (apps non-IODA) - Si UPDATE existant -> enrichit avec champs IODA - Si INSERT et nom_court deja pris -> suffixe avec -IODA-<POS> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
177 lines
6.5 KiB
Python
177 lines
6.5 KiB
Python
"""Import applications depuis le fichier IODA Sanef.
|
|
|
|
Lit `deploy/ServeursAssociesAuxServicesIODA_*.xlsx` (sheet "Services Metiers")
|
|
et UPSERT dans la table `applications` en utilisant `ioda_libelle` comme cle.
|
|
|
|
Usage:
|
|
python tools/import_applications_ioda.py [chemin_fichier.xlsx]
|
|
|
|
Si chemin omis: cherche dans deploy/ le plus recent
|
|
ServeursAssoci*AuxServicesIODA_*.xlsx.
|
|
"""
|
|
import os
|
|
import sys
|
|
import glob
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
|
|
import openpyxl
|
|
from sqlalchemy import create_engine, text
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
DATABASE_URL = (os.getenv("DATABASE_URL_DEMO")
|
|
or os.getenv("DATABASE_URL")
|
|
or "postgresql://patchcenter:PatchCenter2026!@localhost:5432/patchcenter_db")
|
|
|
|
|
|
def find_ioda_file():
|
|
"""Trouve le fichier IODA le plus recent dans deploy/."""
|
|
pattern = str(ROOT / "deploy" / "ServeursAssoci*AuxServicesIODA_*.xlsx")
|
|
files = sorted(glob.glob(pattern))
|
|
if not files:
|
|
# Fallback: deploy/ sans variantes accent
|
|
files = sorted(glob.glob(str(ROOT / "deploy" / "*IODA*.xlsx")))
|
|
return files[-1] if files else None
|
|
|
|
|
|
def parse_services(xlsx_path):
|
|
"""Renvoie liste de dicts {libelle, lib_court, code_pos, ...} depuis sheet 'Services Metiers'."""
|
|
wb = openpyxl.load_workbook(xlsx_path, data_only=True)
|
|
ws_name = next((s for s in wb.sheetnames if "Services" in s and "Metier" in s), None)
|
|
if not ws_name:
|
|
raise SystemExit(f"[ERR] Sheet 'Services Metiers' introuvable. Sheets: {wb.sheetnames}")
|
|
ws = wb[ws_name]
|
|
|
|
services = []
|
|
for i, row in enumerate(ws.iter_rows(values_only=True)):
|
|
# Header sur 2 lignes (i=0 mega-header, i=1 vrais en-tetes)
|
|
if i < 2:
|
|
continue
|
|
# Cols: 0 Actions, 1 Type, 2 Statut, 3 Code Zone POS, 4 Lib court, 5 Libelle,
|
|
# 6 Alias, 7 Editeur, 8 Description, 9 Commentaire, 10 Nb composants,
|
|
# 11 Perimetre, 12 Dept/Domaine, 13 Resp Metier, 14 Resp DSI
|
|
libelle = row[5]
|
|
if not libelle or not str(libelle).strip():
|
|
continue
|
|
def s(v):
|
|
if v is None: return None
|
|
t = str(v).strip()
|
|
return t or None
|
|
|
|
services.append({
|
|
"libelle": str(libelle).strip(),
|
|
"type": s(row[1]),
|
|
"statut": s(row[2]),
|
|
"code_pos": s(row[3]),
|
|
"lib_court": s(row[4]),
|
|
"alias": s(row[6]),
|
|
"editeur": s(row[7]),
|
|
"description": s(row[8]),
|
|
"commentaire": s(row[9]),
|
|
"nb_components": int(row[10]) if row[10] and str(row[10]).strip().isdigit() else None,
|
|
"perimetre": s(row[11]),
|
|
"dept_domaine": s(row[12]),
|
|
"resp_metier": s(row[13]),
|
|
"resp_dsi": s(row[14]),
|
|
})
|
|
return services
|
|
|
|
|
|
SQL_FIND = text("""
|
|
SELECT id FROM applications
|
|
WHERE ioda_libelle = :libelle
|
|
OR (ioda_libelle IS NULL AND nom_court = :nom_court)
|
|
LIMIT 1
|
|
""")
|
|
|
|
SQL_UPDATE = text("""
|
|
UPDATE applications SET
|
|
nom_complet = :nom_complet,
|
|
description = COALESCE(:description, description),
|
|
editeur = COALESCE(:editeur, editeur),
|
|
ioda_libelle = :libelle,
|
|
ioda_lib_court = :lib_court,
|
|
ioda_code_pos = :code_pos,
|
|
ioda_type = :type,
|
|
ioda_statut = :statut,
|
|
ioda_alias = :alias,
|
|
ioda_perimetre = :perimetre,
|
|
ioda_dept_domaine = :dept_domaine,
|
|
ioda_resp_metier = :resp_metier,
|
|
ioda_resp_dsi = :resp_dsi,
|
|
ioda_nb_components = :nb_components,
|
|
ioda_commentaire = :commentaire,
|
|
ioda_imported_at = now(),
|
|
updated_at = now()
|
|
WHERE id = :id
|
|
""")
|
|
|
|
SQL_INSERT = text("""
|
|
INSERT INTO applications (
|
|
nom_court, nom_complet, description, editeur, criticite,
|
|
ioda_libelle, ioda_lib_court, ioda_code_pos, ioda_type, ioda_statut,
|
|
ioda_alias, ioda_perimetre, ioda_dept_domaine, ioda_resp_metier,
|
|
ioda_resp_dsi, ioda_nb_components, ioda_commentaire, ioda_imported_at,
|
|
created_at, updated_at
|
|
) VALUES (
|
|
:nom_court, :nom_complet, :description, :editeur, 'standard',
|
|
:libelle, :lib_court, :code_pos, :type, :statut,
|
|
:alias, :perimetre, :dept_domaine, :resp_metier,
|
|
:resp_dsi, :nb_components, :commentaire, now(),
|
|
now(), now()
|
|
)
|
|
""")
|
|
|
|
|
|
def main():
|
|
xlsx = sys.argv[1] if len(sys.argv) > 1 else find_ioda_file()
|
|
if not xlsx or not os.path.exists(xlsx):
|
|
print(f"[ERR] Fichier IODA introuvable. Place-le dans deploy/ (ex: deploy/ServeursAssociesAuxServicesIODA_YYYYMMDD.xlsx)")
|
|
sys.exit(1)
|
|
|
|
print(f"[INFO] Fichier: {xlsx}")
|
|
services = parse_services(xlsx)
|
|
print(f"[INFO] Services parses: {len(services)}")
|
|
|
|
engine = create_engine(DATABASE_URL)
|
|
print(f"[INFO] DB: {DATABASE_URL.rsplit('@', 1)[-1]}")
|
|
|
|
inserted = updated = skipped = 0
|
|
with engine.begin() as conn:
|
|
used_nom_court = set()
|
|
for svc in services:
|
|
base_nc = (svc["lib_court"] or svc["libelle"])[:50]
|
|
params = {
|
|
**svc,
|
|
"nom_court": base_nc,
|
|
"nom_complet": svc["libelle"][:200],
|
|
}
|
|
existing = conn.execute(SQL_FIND, params).fetchone()
|
|
if existing:
|
|
conn.execute(SQL_UPDATE, {**params, "id": existing[0]})
|
|
updated += 1
|
|
else:
|
|
# Eviter collision nom_court avec autre app non-IODA en BDD
|
|
# ou avec un autre service IODA deja traite cette session
|
|
nc = base_nc
|
|
if nc in used_nom_court:
|
|
# ajout suffixe POS pour unicite
|
|
suf = svc.get("code_pos") or "X"
|
|
nc = (base_nc[:46] + "-" + suf)[:50]
|
|
used_nom_court.add(nc)
|
|
# Verif collision DB
|
|
row = conn.execute(text("SELECT 1 FROM applications WHERE nom_court=:nc"),
|
|
{"nc": nc}).fetchone()
|
|
if row:
|
|
nc = (base_nc[:42] + "-IODA-" + (svc.get("code_pos") or "X"))[:50]
|
|
params["nom_court"] = nc
|
|
conn.execute(SQL_INSERT, params)
|
|
inserted += 1
|
|
|
|
print(f"[OK] Termine — INSERT: {inserted} | UPDATE: {updated} | SKIP: {skipped}")
|
|
print(f"[INFO] Verif: SELECT COUNT(*) FROM applications WHERE ioda_libelle IS NOT NULL;")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|