Add import_patch_history_xlsx: lit sheets hebdo S02..S52, detecte lignes vertes
Pour chaque serveur avec fond vert col A: insere patch_history avec date (col N), heure (col O si presente), note 'Semaine XX'. Fallback sur le lundi de la semaine ISO si pas de date.
This commit is contained in:
parent
90b03ec20b
commit
38756fbfd6
182
tools/import_patch_history_xlsx.py
Normal file
182
tools/import_patch_history_xlsx.py
Normal file
@ -0,0 +1,182 @@
|
||||
"""Import historique patching depuis Planning Patching 2026.xlsx.
|
||||
|
||||
Regle detection serveur patche : cellule hostname (col A) avec fond VERT.
|
||||
|
||||
Pour chaque sheet hebdo S02..S52 :
|
||||
- Extrait num semaine depuis le nom de sheet ('S16' -> 16)
|
||||
- Col A: hostname
|
||||
- Col N (index 13): date (datetime ou string)
|
||||
- Col O (index 14) si present: heure
|
||||
- Ajoute patch_history(server_id, date_patch, status='ok', notes='Semaine X')
|
||||
|
||||
Usage:
|
||||
python tools/import_patch_history_xlsx.py <xlsx> [--dry-run] [--sheets S16,S17]
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import argparse
|
||||
from datetime import datetime, time, timedelta
|
||||
from sqlalchemy import create_engine, text
|
||||
|
||||
try:
|
||||
import openpyxl
|
||||
except ImportError:
|
||||
print("[ERR] pip install openpyxl")
|
||||
raise
|
||||
|
||||
DATABASE_URL = os.getenv("DATABASE_URL_DEMO") or os.getenv("DATABASE_URL") \
|
||||
or "postgresql://patchcenter:PatchCenter2026!@localhost:5432/patchcenter_demo"
|
||||
|
||||
# Codes couleur Excel "vert" (Office / standard)
|
||||
GREEN_HEX_PREFIXES = ("00FF", "92D0", "C6EF", "A9D0", "B7E1", "55B5", "70AD",
|
||||
"00B0", "00A0", "008", "00B5", "00C8")
|
||||
|
||||
|
||||
def is_green(cell):
|
||||
"""Detecte si la cellule a un fond vert."""
|
||||
if cell.fill is None or cell.fill.fgColor is None:
|
||||
return False
|
||||
fc = cell.fill.fgColor
|
||||
rgb = None
|
||||
if fc.type == "rgb" and fc.rgb:
|
||||
rgb = fc.rgb.upper()
|
||||
elif fc.type == "theme":
|
||||
# Themes Office : 9 = green1, 6 = accent2, etc. (approximatif)
|
||||
if fc.theme in (9, 6, 4):
|
||||
return True
|
||||
if rgb and len(rgb) >= 6:
|
||||
# AARRGGBB -> prendre le RRGGBB
|
||||
rr = rgb[-6:-4]; gg = rgb[-4:-2]; bb = rgb[-2:]
|
||||
try:
|
||||
r, g, b = int(rr, 16), int(gg, 16), int(bb, 16)
|
||||
# Vert dominant (G > R, G > B, G > 120)
|
||||
return g > 120 and g > r + 30 and g > b + 30
|
||||
except ValueError:
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
def parse_week_num(sheet_name):
|
||||
m = re.search(r"[Ss](\d{1,2})", sheet_name)
|
||||
return int(m.group(1)) if m else None
|
||||
|
||||
|
||||
def week_to_date(year, week):
|
||||
"""Retourne date du lundi de la semaine ISO."""
|
||||
d = datetime.strptime(f"{year}-W{week:02d}-1", "%Y-W%W-%w")
|
||||
return d.date()
|
||||
|
||||
|
||||
def parse_hour(val):
|
||||
"""Parse une cellule heure: '14:00', '14h', '14H30', datetime.time..."""
|
||||
if val is None:
|
||||
return None
|
||||
if isinstance(val, time):
|
||||
return val
|
||||
if isinstance(val, datetime):
|
||||
return val.time()
|
||||
s = str(val).strip().lower().replace("h", ":")
|
||||
m = re.match(r"(\d{1,2})(?::(\d{2}))?", s)
|
||||
if not m:
|
||||
return None
|
||||
hh = int(m.group(1))
|
||||
mm = int(m.group(2) or 0)
|
||||
if 0 <= hh < 24 and 0 <= mm < 60:
|
||||
return time(hh, mm)
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("xlsx_path")
|
||||
parser.add_argument("--year", type=int, default=2026)
|
||||
parser.add_argument("--sheets", default="", help="Liste de sheets filtrees, ex: S15,S16")
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
engine = create_engine(DATABASE_URL)
|
||||
conn = engine.connect().execution_options(isolation_level="AUTOCOMMIT")
|
||||
|
||||
wb = openpyxl.load_workbook(args.xlsx_path, data_only=True)
|
||||
target_sheets = set(s.strip().upper() for s in args.sheets.split(",") if s.strip()) if args.sheets else None
|
||||
|
||||
# Cache servers: hostname -> id
|
||||
hosts = {}
|
||||
for r in conn.execute(text("SELECT id, hostname FROM servers")).fetchall():
|
||||
hosts[r.hostname.lower()] = r.id
|
||||
|
||||
stats = {"sheets": 0, "patched": 0, "inserted": 0, "no_server": 0, "skipped": 0}
|
||||
|
||||
for sname in wb.sheetnames:
|
||||
wk = parse_week_num(sname)
|
||||
if wk is None or not (1 <= wk <= 53):
|
||||
continue
|
||||
if target_sheets and sname.upper() not in target_sheets:
|
||||
continue
|
||||
ws = wb[sname]
|
||||
stats["sheets"] += 1
|
||||
default_date = week_to_date(args.year, wk)
|
||||
|
||||
for row_idx in range(2, ws.max_row + 1):
|
||||
hn_cell = ws.cell(row=row_idx, column=1)
|
||||
hn = hn_cell.value
|
||||
if not hn:
|
||||
continue
|
||||
hn = str(hn).strip().split(".")[0].lower()
|
||||
if not any(c.isalpha() for c in hn):
|
||||
continue
|
||||
if not is_green(hn_cell):
|
||||
continue
|
||||
stats["patched"] += 1
|
||||
|
||||
sid = hosts.get(hn)
|
||||
if not sid:
|
||||
stats["no_server"] += 1
|
||||
continue
|
||||
|
||||
# Date col N (14), heure col O (15) si present
|
||||
date_val = ws.cell(row=row_idx, column=14).value
|
||||
hour_val = ws.cell(row=row_idx, column=15).value
|
||||
dt_base = None
|
||||
if isinstance(date_val, datetime):
|
||||
dt_base = date_val
|
||||
elif date_val:
|
||||
try:
|
||||
dt_base = datetime.strptime(str(date_val).split()[0], "%d/%m/%Y")
|
||||
except Exception:
|
||||
dt_base = None
|
||||
if not dt_base:
|
||||
dt_base = datetime.combine(default_date, time(0, 0))
|
||||
hr = parse_hour(hour_val)
|
||||
if hr:
|
||||
dt_base = datetime.combine(dt_base.date(), hr)
|
||||
|
||||
note = f"Semaine {wk:02d}"
|
||||
if args.dry_run:
|
||||
print(f" DRY [{sname}] {hn:25s} -> {dt_base.isoformat()} ({note})")
|
||||
else:
|
||||
try:
|
||||
# Evite doublons exacts (server+date)
|
||||
existing = conn.execute(text(
|
||||
"SELECT id FROM patch_history WHERE server_id=:sid AND date_patch=:dt"
|
||||
), {"sid": sid, "dt": dt_base}).fetchone()
|
||||
if existing:
|
||||
stats["skipped"] += 1
|
||||
continue
|
||||
conn.execute(text("""
|
||||
INSERT INTO patch_history (server_id, date_patch, status, notes)
|
||||
VALUES (:sid, :dt, 'ok', :note)
|
||||
"""), {"sid": sid, "dt": dt_base, "note": note})
|
||||
stats["inserted"] += 1
|
||||
except Exception as e:
|
||||
print(f" [ERR] {hn}: {str(e)[:120]}")
|
||||
stats["skipped"] += 1
|
||||
|
||||
conn.close()
|
||||
print(f"\n[DONE] Sheets: {stats['sheets']} | Patches detectes: {stats['patched']} "
|
||||
f"| Inserts: {stats['inserted']} | Sans serveur: {stats['no_server']} "
|
||||
f"| Skip: {stats['skipped']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in New Issue
Block a user