"""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_UPSERT = 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() ) ON CONFLICT (ioda_libelle) WHERE ioda_libelle IS NOT NULL DO UPDATE SET nom_complet = EXCLUDED.nom_complet, description = COALESCE(EXCLUDED.description, applications.description), editeur = COALESCE(EXCLUDED.editeur, applications.editeur), ioda_lib_court = EXCLUDED.ioda_lib_court, ioda_code_pos = EXCLUDED.ioda_code_pos, ioda_type = EXCLUDED.ioda_type, ioda_statut = EXCLUDED.ioda_statut, ioda_alias = EXCLUDED.ioda_alias, ioda_perimetre = EXCLUDED.ioda_perimetre, ioda_dept_domaine = EXCLUDED.ioda_dept_domaine, ioda_resp_metier = EXCLUDED.ioda_resp_metier, ioda_resp_dsi = EXCLUDED.ioda_resp_dsi, ioda_nb_components = EXCLUDED.ioda_nb_components, ioda_commentaire = EXCLUDED.ioda_commentaire, ioda_imported_at = now(), updated_at = 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 = 0 with engine.begin() as conn: for svc in services: # nom_court max 50 chars: prefere lib_court, sinon truncate libelle nom_court = (svc["lib_court"] or svc["libelle"])[:50] params = { **svc, "nom_court": nom_court, "nom_complet": svc["libelle"][:200], } r = conn.execute(SQL_UPSERT, params) # Discriminer insert vs update via xmin trick? Plus simple: compter rowcount inserted += 1 # upsert ne distingue pas, on log la totale print(f"[OK] Upsert termine — {inserted} services traites") print(f"[INFO] Verif: SELECT COUNT(*) FROM applications WHERE ioda_libelle IS NOT NULL;") if __name__ == "__main__": main()