399 lines
17 KiB
Python
399 lines
17 KiB
Python
# Název: janssenpc_file_send.py
|
|
# Verze: 2.5
|
|
# Datum: 2026-06-12
|
|
# Popis: Přejmenuje soubory ve složce ##JNJPrenos, zašifruje (Fernet/AES-128),
|
|
# odešle na msgs.buzalka.cz a přesune do podsložky Trash.
|
|
# Detekci nových souborů k odeslání zajišťuje FileWatch na JNJ počítači.
|
|
# (Stahování souborů ze serveru řeší samostatný skript janssenpc_file_receive,
|
|
# spouštěný ručně ad hoc — do file_send nepatří.)
|
|
# Loguje průběh do file_send.log vedle skriptu.
|
|
# Podporuje: PANORAMA Site Contacts (xlsx), Panorama Dashboard (xlsx),
|
|
# Site Visit Report (xlsx), Follow-Up Letter (xlsx),
|
|
# Clario MayoScore (csv), Clario MayoDiary (csv),
|
|
# Clario Data Corrections / DCRs (csv).
|
|
#
|
|
# Změna v2.5 (PODROBNÝ LOGGING UPLOADU — diagnostika JNJ filtru):
|
|
# - upload() loguje celý průběh: přesné jméno v multipartu (repr), velikost
|
|
# payloadu, HTTP kód, dobu, FINÁLNÍ URL po redirectech, řetěz redirectů,
|
|
# hlavičky Server/Content-Type/Content-Length, a tělo odpovědi (oříznuté).
|
|
# - Detekuje stopy korporátního filtru (Zscaler/SiteMinder): '_sm_nck' v URL,
|
|
# redirect na cizí host, Content-Type != application/json, HTML tělo,
|
|
# hlavička Server nepatřící uvicornu.
|
|
# - Soubor se přesune do Trash JEN při OVĚŘENÉM úspěchu serveru
|
|
# (HTTP 200 + application/json + status v očekávané množině). Jinak
|
|
# zůstává ve složce a loguje se PODEZŘENÍ — aby se „falešný úspěch"
|
|
# (podvržená odpověď filtru) nemazal.
|
|
|
|
import os
|
|
import time
|
|
import shutil
|
|
import base64
|
|
import hashlib
|
|
import requests
|
|
import pandas as pd
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
from cryptography.fernet import Fernet
|
|
|
|
TOKEN = "13e1bb01-9fd5-44a8-8ce9-4ee27133d340"
|
|
UPLOAD_URL = "https://msgs.buzalka.cz/upload-file"
|
|
_FERNET = Fernet(base64.urlsafe_b64encode(hashlib.sha256(TOKEN.encode()).digest()))
|
|
SOURCE_DIR = Path(r"C:\Users\vbuzalka\OneDrive - JNJ\##JNJPrenos")
|
|
TRASH_DIR = SOURCE_DIR / "Trash"
|
|
LOG_FILE = Path(__file__).parent / "file_send.log"
|
|
|
|
# Hodnoty pole "status", které server vrací při skutečném přijetí souboru.
|
|
OK_STATUSES = {"OK", "UPLOADED", "SAVED", "RECEIVED"}
|
|
# Maximální délka těla odpovědi, kterou zapíšeme do logu.
|
|
BODY_LOG_LIMIT = 1500
|
|
|
|
MAYO_DIARY_COLUMNS = [
|
|
'Protocol', 'Country', 'Site', 'PI Name', 'Subject ID',
|
|
'Report Date', 'Report Start Date/Time', 'Report End Date/Time',
|
|
'Stool Frequency', 'Form Number', 'Role', 'Original Source',
|
|
]
|
|
|
|
MAYO_SCORE_COLUMNS = [
|
|
'Protocol', 'Study Population', 'Country', 'Site', 'Principal Investigator',
|
|
'Participant ID', 'Baseline Stool Frequency', 'Visit', 'Visit Date',
|
|
'Endoscopy Completed?', 'Central Endoscopy Score', 'Local Endoscopy Score',
|
|
'Partial Mayo Score', 'Full Mayo Score',
|
|
]
|
|
|
|
DCR_ECOA_COLUMNS = [
|
|
'Protocol', 'Data Correction ID', 'Description', 'Query History',
|
|
]
|
|
|
|
DCR_ECG_COLUMNS = [
|
|
'Protocol', 'Data Correction ID', 'Site ID', 'PI_NAME', 'Subject Number', 'Query History',
|
|
]
|
|
|
|
PANORAMA_COLUMNS = [
|
|
'Part', 'Source', 'Sector', 'TA', 'Protocol ID', 'Interventional',
|
|
'Region', 'Country Name', 'Institution Name', 'Site City',
|
|
'Site Zip/Postal Code', 'Site Address', 'MSID', 'Site ID',
|
|
'Site Status', 'SM Full Name', 'PI Name', 'St F Subj Enr Act',
|
|
'ID', 'Category', 'Type', 'Priority', 'Severity', 'Description',
|
|
'Brief Description - Subject ID', 'Comments', 'Created By',
|
|
'Create Date', 'Last Modified Date', 'Start Date', 'Due Date',
|
|
'End Date', 'Status', 'Days Outstanding', 'Action Taken',
|
|
'Escalated To', 'Visit Report Status', 'Visit Report Approved',
|
|
'Visit Report Type', 'Visit Report Status End Date', 'Active',
|
|
'Association', 'Deviation', 'Deviation Closed Date', 'Reason For Exclusion'
|
|
]
|
|
|
|
|
|
def log(msg: str):
|
|
ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
line = f"[{ts}] {msg}"
|
|
print(line)
|
|
with LOG_FILE.open("a", encoding="utf-8") as lf:
|
|
lf.write(line + "\n")
|
|
|
|
|
|
def move_to_trash(f: Path):
|
|
TRASH_DIR.mkdir(exist_ok=True)
|
|
dest = TRASH_DIR / f.name
|
|
if dest.exists():
|
|
ts = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
dest = TRASH_DIR / f"{f.stem}_{ts}{f.suffix}"
|
|
shutil.move(str(f), dest)
|
|
|
|
|
|
def get_timestamp(file_path: str) -> str:
|
|
return datetime.fromtimestamp(os.path.getmtime(file_path)).strftime('%Y-%m-%d_%H-%M-%S')
|
|
|
|
|
|
def upload(f: Path) -> bool:
|
|
"""Zašifruje a odešle soubor. Vrací True JEN při ověřeném úspěchu serveru.
|
|
Podrobně loguje odpověď kvůli diagnostice korporátního filtru (Zscaler/SiteMinder)."""
|
|
enc_name = f.name + ".enc"
|
|
try:
|
|
encrypted = _FERNET.encrypt(f.read_bytes())
|
|
except Exception as e:
|
|
log(f" CHYBA (šifrování) | {f.name} | {e}")
|
|
return False
|
|
|
|
log(f" >> UPLOAD start | {f.name}")
|
|
log(f" URL pole 'file' (multipart filename) = {enc_name!r}")
|
|
log(f" payload (zašifrováno) = {len(encrypted)} B | původní soubor = {f.stat().st_size} B")
|
|
log(f" POST {UPLOAD_URL}")
|
|
|
|
try:
|
|
t0 = time.time()
|
|
resp = requests.post(
|
|
UPLOAD_URL,
|
|
headers={"Authorization": f"Bearer {TOKEN}"},
|
|
files={"file": (enc_name, encrypted, "application/octet-stream")},
|
|
timeout=120,
|
|
allow_redirects=True,
|
|
)
|
|
dt = time.time() - t0
|
|
except Exception as e:
|
|
log(f" CHYBA (POST selhal) | {f.name} | {type(e).__name__}: {e}")
|
|
return False
|
|
|
|
# --- Podrobný rozbor odpovědi ---
|
|
final_url = str(resp.url)
|
|
ctype = resp.headers.get("Content-Type", "")
|
|
server_hdr = resp.headers.get("Server", "")
|
|
clen = resp.headers.get("Content-Length", "")
|
|
body = resp.text or ""
|
|
|
|
log(f" <- HTTP {resp.status_code} | {dt:.2f}s")
|
|
log(f" finální URL : {final_url}")
|
|
log(f" Content-Type: {ctype!r}")
|
|
log(f" Server : {server_hdr!r} | Content-Length: {clen!r}")
|
|
|
|
# Řetěz redirectů (filtr typicky přesměrovává)
|
|
if resp.history:
|
|
for h in resp.history:
|
|
loc = h.headers.get("Location", "")
|
|
log(f" REDIRECT {h.status_code} -> {loc}")
|
|
|
|
# Tělo odpovědi (oříznuté)
|
|
body_snip = body[:BODY_LOG_LIMIT].replace("\n", "\\n")
|
|
log(f" tělo[{len(body)}B]: {body_snip}")
|
|
|
|
# --- Detekce stop korporátního filtru ---
|
|
suspicious = []
|
|
if "_sm_nck" in final_url:
|
|
suspicious.append("'_sm_nck' ve finální URL (SiteMinder/Zscaler replay)")
|
|
if "msgs.buzalka.cz" not in final_url:
|
|
suspicious.append("finální URL je na CIZÍM hostu (přesměrováno filtrem)")
|
|
if "application/json" not in ctype.lower():
|
|
suspicious.append(f"Content-Type není JSON ({ctype!r})")
|
|
if "<html" in body.lower() or "<!doctype" in body.lower():
|
|
suspicious.append("tělo vypadá jako HTML (block page filtru)")
|
|
# Pozn.: před uvicornem běží SWAG reverse proxy → 'Server: nginx' je OČEKÁVANÉ,
|
|
# nehlásíme jako podezření. Spolehlivý signál pravosti je dropbox_path v JSON těle.
|
|
if server_hdr and not any(s in server_hdr.lower() for s in ("uvicorn", "nginx")):
|
|
suspicious.append(f"hlavička Server není uvicorn/nginx ({server_hdr!r})")
|
|
if suspicious:
|
|
for s in suspicious:
|
|
log(f" ⚠ PODEZŘENÍ NA FILTR: {s}")
|
|
|
|
# --- Ověření skutečného úspěchu ---
|
|
if resp.status_code != 200:
|
|
log(f" NEÚSPĚCH | {f.name} | HTTP {resp.status_code} — soubor PONECHÁN")
|
|
return False
|
|
|
|
try:
|
|
data = resp.json()
|
|
except Exception as e:
|
|
log(f" NEÚSPĚCH | {f.name} | odpověď není JSON ({e}) — soubor PONECHÁN")
|
|
return False
|
|
|
|
status = str(data.get("status", "?")).upper()
|
|
if status in OK_STATUSES:
|
|
log(f" UPLOADED | {f.name} (zašifrováno, status={status})")
|
|
return True
|
|
else:
|
|
log(f" NEÚSPĚCH | {f.name} | server status={status!r} mimo {OK_STATUSES} — soubor PONECHÁN")
|
|
return False
|
|
|
|
|
|
def prejmenuj(directory: Path) -> None:
|
|
log(f"--- Přejmenování, adresář: {directory} ---")
|
|
files = [f for f in directory.iterdir() if f.is_file()]
|
|
log(f" Nalezeno souborů: {len(files)} — {[f.name for f in files]}")
|
|
|
|
for f in files:
|
|
filename = f.name
|
|
file_path = str(f)
|
|
|
|
# 0a. CLARIO MAYO DIARY (CSV)
|
|
if 'MAYO-DIARY' in filename and filename.endswith('.csv'):
|
|
log(f" Detekován MayoDiary: {filename}")
|
|
try:
|
|
df = pd.read_csv(file_path)
|
|
missing = set(MAYO_DIARY_COLUMNS) - set(df.columns)
|
|
if not missing:
|
|
protocols = df['Protocol'].dropna().unique()
|
|
log(f" Protocol: {list(protocols)}")
|
|
if len(protocols) > 0:
|
|
study = str(protocols[0]).strip()
|
|
new_name = f"{get_timestamp(file_path)} {study} Clario MayoDiary.csv"
|
|
f.rename(directory / new_name)
|
|
log(f" ÚSPĚCH: -> '{new_name}'")
|
|
else:
|
|
log(f" VAROVÁNÍ: Sloupec Protocol je prázdný.")
|
|
else:
|
|
log(f" PŘESKOČENO: Chybí sloupce: {missing}")
|
|
except Exception as e:
|
|
log(f" CHYBA: {e}")
|
|
continue
|
|
|
|
# 0b. CLARIO MAYO SCORE (CSV)
|
|
if 'Custom.MayoScoreReport' in filename and filename.endswith('.csv'):
|
|
log(f" Detekován MayoScore: {filename}")
|
|
try:
|
|
df = pd.read_csv(file_path)
|
|
missing = set(MAYO_SCORE_COLUMNS) - set(df.columns)
|
|
if not missing:
|
|
protocols = df['Protocol'].dropna().unique()
|
|
log(f" Protocol: {list(protocols)}")
|
|
if len(protocols) > 0:
|
|
study = str(protocols[0]).strip()
|
|
new_name = f"{get_timestamp(file_path)} {study} Clario MayoScore.csv"
|
|
f.rename(directory / new_name)
|
|
log(f" ÚSPĚCH: -> '{new_name}'")
|
|
else:
|
|
log(f" VAROVÁNÍ: Sloupec Protocol je prázdný.")
|
|
else:
|
|
log(f" PŘESKOČENO: Chybí sloupce: {missing}")
|
|
except Exception as e:
|
|
log(f" CHYBA: {e}")
|
|
continue
|
|
|
|
# 0c. CLARIO DATA CORRECTIONS (CSV) — ECG nebo eCOA
|
|
if filename.endswith('.csv'):
|
|
try:
|
|
df = pd.read_csv(file_path, nrows=2)
|
|
cols = set(df.columns)
|
|
log(f" CSV sloupce ({filename}): {sorted(cols)}")
|
|
|
|
missing_ecg = set(DCR_ECG_COLUMNS) - cols
|
|
missing_ecoa = set(DCR_ECOA_COLUMNS) - cols
|
|
log(f" Chybí pro ECG: {missing_ecg or '—'}")
|
|
log(f" Chybí pro eCOA: {missing_ecoa or '—'}")
|
|
|
|
if not missing_ecg:
|
|
label = "Clario ECG DCRs"
|
|
elif not missing_ecoa:
|
|
label = "Clario eCOA DCRs"
|
|
else:
|
|
log(f" Neznámý CSV typ — bude odeslán bez přejmenování: {filename}")
|
|
# nepokračujeme continue — soubor projde dál k odeslání
|
|
label = None
|
|
|
|
if label:
|
|
log(f" Detekován {label}: {filename}")
|
|
protocols = df['Protocol'].dropna().unique()
|
|
log(f" Protocol: {list(protocols)}")
|
|
if len(protocols) > 0:
|
|
study = str(protocols[0]).strip()
|
|
new_name = f"{get_timestamp(file_path)} {study} {label}.csv"
|
|
f.rename(directory / new_name)
|
|
log(f" ÚSPĚCH přejmenování: -> '{new_name}'")
|
|
else:
|
|
log(f" VAROVÁNÍ: Sloupec Protocol je prázdný — odesílám pod původním názvem.")
|
|
except Exception as e:
|
|
log(f" CHYBA při zpracování CSV {filename}: {e}")
|
|
continue
|
|
|
|
# Ostatní — jen xlsx
|
|
if not filename.endswith('.xlsx'):
|
|
log(f" Přeskočeno (neznámý typ): {filename}")
|
|
continue
|
|
|
|
# 1a. PANORAMA SITE CONTACTS (XLSX) — soubor pojmenovaný "PANORAMA Dashboard"
|
|
if 'PANORAMA Dashboard' in filename:
|
|
log(f" Detekován PANORAMA Site Contacts: {filename}")
|
|
try:
|
|
with pd.ExcelFile(file_path) as xl:
|
|
sheet_names = xl.sheet_names
|
|
if 'Site Contacts' in sheet_names:
|
|
df_a1 = xl.parse('Site Contacts', nrows=1, header=None)
|
|
a1 = str(df_a1.iloc[0, 0]) if not df_a1.empty else ''
|
|
else:
|
|
a1 = None
|
|
# soubor je nyní zavřen — přejmenování proběhne bez chyby
|
|
if a1 is None:
|
|
log(f" PŘESKOČENO: List 'Site Contacts' nenalezen.")
|
|
elif 'Title: Site Contacts' in a1:
|
|
new_name = f"{get_timestamp(file_path)} PANORAMA Site Contacts.xlsx"
|
|
f.rename(directory / new_name)
|
|
log(f" ÚSPĚCH: -> '{new_name}'")
|
|
else:
|
|
log(f" PŘESKOČENO: A1 neodpovídá vzoru ({a1[:50]})")
|
|
except Exception as e:
|
|
log(f" CHYBA: {e}")
|
|
continue
|
|
|
|
# 1. PANORAMA DASHBOARD (XLSX)
|
|
if 'Panorama Dashboard' in filename:
|
|
log(f" Detekován Panorama: {filename}")
|
|
try:
|
|
df = pd.read_excel(file_path, skiprows=5)
|
|
missing = set(PANORAMA_COLUMNS) - set(df.columns)
|
|
if not missing:
|
|
ids = df['Protocol ID'].dropna().unique()
|
|
log(f" Protocol ID: {list(ids)}")
|
|
if len(ids) > 0:
|
|
study = str(ids[0]).strip()
|
|
new_name = f"{get_timestamp(file_path)} {study} Panorama Deviations and Issues.xlsx"
|
|
f.rename(directory / new_name)
|
|
log(f" ÚSPĚCH: -> '{new_name}'")
|
|
else:
|
|
log(f" VAROVÁNÍ: Protocol ID je prázdný.")
|
|
else:
|
|
log(f" PŘESKOČENO: Chybí sloupce: {missing}")
|
|
except Exception as e:
|
|
log(f" CHYBA: {e}")
|
|
continue
|
|
|
|
# 2. SITE VISIT REPORT A FOLLOW-UP LETTER (XLSX)
|
|
try:
|
|
df_a1 = pd.read_excel(file_path, nrows=1, header=None)
|
|
if not df_a1.empty:
|
|
a1 = str(df_a1.iloc[0, 0])
|
|
log(f" A1: {a1[:80]}")
|
|
is_site_visit = "Title: Site Visit Report Details" in a1
|
|
is_follow_up = "Title: Follow-Up Letter Details" in a1
|
|
|
|
if is_site_visit or is_follow_up:
|
|
suffix = "Site Visit Details.xlsx" if is_site_visit else "FUL details.xlsx"
|
|
log(f" Detekován {'Site Visit' if is_site_visit else 'Follow-Up Letter'}: {filename}")
|
|
df = pd.read_excel(file_path, skiprows=5)
|
|
if 'Protocol ID' in df.columns:
|
|
ids = df['Protocol ID'].dropna().unique()
|
|
log(f" Protocol ID: {list(ids)}")
|
|
if len(ids) > 0:
|
|
study = str(ids[0]).strip()
|
|
new_name = f"{get_timestamp(file_path)} {study} {suffix}"
|
|
f.rename(directory / new_name)
|
|
log(f" ÚSPĚCH: -> '{new_name}'")
|
|
else:
|
|
log(f" VAROVÁNÍ: Protocol ID je prázdný.")
|
|
else:
|
|
log(f" PŘESKOČENO: Chybí sloupec Protocol ID.")
|
|
else:
|
|
log(f" Přeskočeno (neznámý xlsx obsah): {filename}")
|
|
except Exception as e:
|
|
log(f" CHYBA: {e}")
|
|
|
|
log("--- Přejmenování dokončeno ---")
|
|
|
|
|
|
# === HLAVNÍ LOGIKA ===
|
|
|
|
log("=== Spuštění ===")
|
|
log(f"Zdrojový adresář: {SOURCE_DIR} (existuje: {SOURCE_DIR.exists()})")
|
|
|
|
# 1. Přejmenuj
|
|
prejmenuj(SOURCE_DIR)
|
|
|
|
# 2. Počkej 10 vteřin
|
|
log("Čekám 10 vteřin...")
|
|
time.sleep(10)
|
|
|
|
# 3. Odešli soubory
|
|
files = [f for f in SOURCE_DIR.iterdir() if f.is_file()]
|
|
log(f"Souborů k odeslání: {len(files)}")
|
|
for f in files:
|
|
log(f" Nalezen: {f.name}")
|
|
|
|
if not files:
|
|
log("Žádné soubory k odeslání.")
|
|
else:
|
|
for f in files:
|
|
ok = upload(f)
|
|
if ok:
|
|
move_to_trash(f)
|
|
log(f" PŘESUNUTO | {f.name} -> Trash")
|
|
else:
|
|
log(f" PONECHÁNO | {f.name} (NEodesláno ověřeně — zůstává ve složce)")
|
|
|
|
log("=== Hotovo ===")
|