This commit is contained in:
2026-06-12 15:29:57 +02:00
parent 39e578af2d
commit 35e6310dac
79 changed files with 27428 additions and 0 deletions
@@ -0,0 +1,302 @@
# Název: janssenpc_file_send.py
# Verze: 2.4
# Datum: 2026-06-05
# 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).
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"
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 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:
try:
encrypted = _FERNET.encrypt(f.read_bytes())
enc_name = f.name + ".enc"
resp = requests.post(
UPLOAD_URL,
headers={"Authorization": f"Bearer {TOKEN}"},
files={"file": (enc_name, encrypted, "application/octet-stream")},
timeout=120,
)
resp.raise_for_status()
status = resp.json().get('status', '?').upper()
log(f" {status:10} | {f.name} (zašifrováno)")
move_to_trash(f)
log(f" PŘESUNUTO | {f.name} -> Trash")
except Exception as e:
log(f" CHYBA | {f.name} | {e}")
log("=== Hotovo ===")
@@ -0,0 +1,76 @@
# janssenpc_file_send v2.5
Odesílací skript na JNJ počítači: přejmenuje soubory ve `##JNJPrenos`, zašifruje
(Fernet/AES-128) a odešle na `https://msgs.buzalka.cz/upload-file`, pak přesune
do `Trash`. Detekci nových souborů zajišťuje `janssenpc_file_watch.py`.
Stahování ze serveru řeší samostatný `janssenpc_file_receive` (ručně) — sem nepatří.
## Spuštění
Spouští se automaticky přes `janssenpc_file_watch.py` (watchdog). Ručně:
```
C:\Users\vbuzalka\OneDrive - JNJ\##JNJPrenos\Python\python.exe "...\janssenpc_file_send.py"
```
## Co je nového v v2.5 — diagnostika JNJ filtru
Důvod: v logu z 2026-06-12 byl poslední soubor označen jako `UPLOADED` + `PŘESUNUTO`,
ale **doma fakticky nepřišel**. Podezření na korporátní filtr (Zscaler/SiteMinder),
který už dříve sabotoval downloady (`403 + ?_sm_nck=1`). v2.4 logoval jen
`resp.json()['status']`, takže podvrženou/přesměrovanou odpověď nebylo poznat.
### 1. Funkce `upload()` — podrobný log každého POSTu
Loguje:
- přesné jméno v multipartu `repr(enc_name)` (odhalí problémové znaky/diakritiku),
- velikost zašifrovaného payloadu i původního souboru,
- **HTTP kód** a dobu trvání,
- **finální URL** po redirectech (`resp.url`),
- celý **řetěz redirectů** (`resp.history` + `Location`),
- hlavičky `Content-Type`, `Server`, `Content-Length`,
- **tělo odpovědi** (oříznuté na 1500 B).
### 2. Detekce stop filtru → `⚠ PODEZŘENÍ NA FILTR`
Zaloguje varování, pokud:
- `_sm_nck` ve finální URL (SiteMinder/Zscaler replay),
- finální URL je na cizím hostu (přesměrováno),
- `Content-Type` není `application/json`,
- tělo vypadá jako HTML (block page),
- hlavička `Server` není `uvicorn` ani `nginx`.
> **Pozn.:** Před uvicornem běží SWAG reverse proxy, takže `Server: nginx` je
> NORMÁLNÍ a poplach se na něj nehlásí. Spolehlivý důkaz pravé odpovědi serveru je
> přítomnost `dropbox_path` v JSON těle (to generuje jen `app.py`).
## Ověřeno v provozu (2026-06-12)
První ostrý běh v2.5 prokázal, že přenosový řetězec klient → server → Dropbox
funguje korektně: `HTTP 200`, finální URL bez redirectu/`_sm_nck`, pravé JSON tělo
s `dropbox_path`, soubor fyzicky dorazil do `U:\Dropbox\!!!Days\Downloads Z230\`
(velikost bajt po bajtu sedí). Dřívější podezření na JNJ filtr se NEPOTVRDILO —
šlo o zpožděný Dropbox sync na domácí straně.
### 3. Bezpečné mazání — Trash JEN při ověřeném úspěchu
Soubor se přesune do `Trash` **pouze** když:
`HTTP 200` **a** `Content-Type: application/json` **a** pole `status`
`{OK, UPLOADED, SAVED, RECEIVED}`.
Jinak se loguje `PONECHÁNO` a soubor zůstává ve složce — aby falešný „úspěch"
podvržený filtrem nesmazal nepřenesený soubor. (Pozn.: `OK_STATUSES` případně sladit
s tím, co skutečně vrací `app.py` na `/upload-file` — ověřit z těla v logu.)
## Jak diagnostikovat
1. Nasadit v2.5 na JNJ stroj místo v2.4 (jako `janssenpc_file_send.py`).
2. Spustit přenos, otevřít `file_send.log` u sekce `>> UPLOAD start`.
3. Porovnat: pokud je tam `⚠ PODEZŘENÍ NA FILTR` nebo `status` mimo množinu →
chyba je na cestě (filtr / endpoint), ne v serveru. Pokud je odpověď čistá JSON
`uvicorn` 200 a soubor přesto doma chybí → problém je až na serveru
(`/upload-file` dešifrování / zápis do Dropboxu) — řešit v `app.py`.
## Vazby
- Watcher: `janssenpc_file_watch.py` (v1.1).
- Server: `app.py` ≥ v2.3, endpoint `POST /upload-file` (dešifruje `.enc` → Dropbox
`/!!!Days/Downloads Z230/`).
- Protějšek pro download: `janssenpc_file_receive_v1.2`.
## Historie
- v2.5 (2026-06-12): podrobný logging uploadu + detekce filtru + Trash jen při ověřeném úspěchu
- v2.4 (2026-06-05): Fernet šifrování uploadu, endpoint `/upload-file`
- v2.2 (2026-06-02): odesílání bez šifrování přes `/upload-dropbox`
@@ -0,0 +1,398 @@
# 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 ===")
+59
View File
@@ -0,0 +1,59 @@
# janssenpc_file_send v2.6
Odesílací skript na JNJ počítači: přejmenuje soubory ve `##JNJPrenos`, zašifruje
(Fernet/AES-128) a odešle na `https://msgs.buzalka.cz/upload-file`, pak přesune
do `Trash`. Detekci nových souborů zajišťuje `janssenpc_file_watch.py` (v1.2).
Stahování ze serveru řeší samostatný `janssenpc_file_receive` (ručně) — sem nepatří.
## Spuštění
Spouští se automaticky přes `janssenpc_file_watch.py` (watchdog). Ručně:
```
C:\Users\vbuzalka\OneDrive - JNJ\##JNJPrenos\Python\python.exe "...\janssenpc_file_send.py"
```
## Co je nového v v2.6 — ošetření nedotaženého stahování z Chrome
Problém: Chrome stahuje report do dočasného souboru `*.crdownload` a teprve po
dokončení ho přejmenuje na finální jméno; u některých typů zapisuje rovnou do
finálního jména. Watchdog vystřelí hned na `on_created`, takže dřívější verze
začaly zpracovávat / odesílat i napůl stažený soubor (riziko: upload rozdělaného
`.crdownload`, nebo načtení neúplného CSV/XLSX).
### `is_ready(f)` — gate před zpracováním i odesláním
Soubor projde, jen když:
1. **nemá dočasnou příponu** (`.crdownload` / `.tmp` / `.part`),
2. **velikost je stabilní**`STABILITY_REQUIRED` (3) kontrol po sobě stejná a > 0,
poll po `STABILITY_INTERVAL` (1 s), max `STABILITY_TIMEOUT` (120 s),
3. **jde otevřít na zápis** (`r+b`) — zamčený soubor (Chrome ještě zapisuje) hodí
`PermissionError` → přeskočí se.
Nepřipravený soubor se v daném běhu jen přeskočí (zaloguje důvod) a zpracuje ho
příští běh / další událost watcheru.
Volá se na DVOU místech: na začátku `prejmenuj()` (ať pandas nečte neúplný soubor)
a znovu v odesílací fázi (pojistka před uploadem).
## Spolupráce s watcherem (v1.2)
- Watcher **ignoruje** dočasné přípony (nespouští send na `.crdownload`).
- Watcher **debouncuje** sérii událostí (vytvoření + přejmenování + zápis) do jednoho
spuštění (`DEBOUNCE_SECONDS = 5`).
- Vlastní „dotažení" řeší až `is_ready` v file_send — watcher jen omezuje zbytečná spuštění.
## Zděděno z v2.5 — podrobný logging uploadu
Funkce `upload()` loguje HTTP kód, finální URL, redirecty, hlavičky a tělo odpovědi;
detekuje stopy korporátního filtru (`_sm_nck`, cizí host, ne-JSON, HTML, neznámý Server).
`Server: nginx` je OČEKÁVANÉ (SWAG reverse proxy). Soubor se přesune do `Trash` jen
při ověřeném úspěchu (`HTTP 200` + `application/json` + `status`
`{OK, UPLOADED, SAVED, RECEIVED}`); jinak `PONECHÁNO`.
## Vazby
- Watcher: `janssenpc_file_watch_v1.2.py`.
- Server: `app.py` ≥ v2.3, endpoint `POST /upload-file` (vrací
`{"status":"uploaded","file":...,"dropbox_path":...}`).
- Protějšek pro download: `janssenpc_file_receive_v1.2`.
## Historie
- v2.6 (2026-06-12): is_ready gate (stabilní velikost + zámek + ignor .crdownload) proti nedotaženému stahování
- v2.5 (2026-06-12): podrobný logging uploadu + detekce filtru + Trash jen při ověřeném úspěchu (ověřeno: přenos OK, filtr nevinen)
- v2.4 (2026-06-05): Fernet šifrování uploadu, endpoint `/upload-file`
- v2.2 (2026-06-02): odesílání bez šifrování přes `/upload-dropbox`
+467
View File
@@ -0,0 +1,467 @@
# Název: janssenpc_file_send.py
# Verze: 2.6
# 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.6 (NEDOTAŽENÉ STAHOVÁNÍ Z CHROME):
# Chrome stahuje do dočasného souboru *.crdownload a teprve po dokončení
# ho přejmenuje na finální jméno; u některých typů zapisuje rovnou do
# finálního jména. Dřív watch+send začaly zpracovávat/odesílat i napůl
# stažený soubor. Nově:
# - IGNORE_SUFFIXES: dočasné přípony (.crdownload/.tmp/.part) se přeskakují.
# - is_ready(): před přejmenováním i před odesláním se čeká, až je velikost
# STABILNÍ (nemění se po STABILITY_REQUIRED kontrol) a soubor jde otevřít
# na zápis (r+b → zamčený soubor hodí PermissionError). Nestabilní/zamčený
# soubor se v tomto běhu přeskočí (zpracuje ho další běh / další událost).
#
# 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); 'Server: nginx'
# je OČEKÁVANÉ (SWAG reverse proxy), nehlásí se.
# - 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ě).
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
# --- Readiness (nedotažené stahování) ---
IGNORE_SUFFIXES = {".crdownload", ".tmp", ".part"} # dočasné soubory prohlížeče/OS
STABILITY_INTERVAL = 1.0 # vteřin mezi kontrolami velikosti
STABILITY_REQUIRED = 3 # kolikrát po sobě musí být velikost stejná
STABILITY_TIMEOUT = 120 # max vteřin čekání na stabilizaci
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 is_ready(f: Path) -> bool:
"""Je soubor dotažený a připravený ke zpracování?
- Dočasné přípony (.crdownload/.tmp/.part) → přeskočit.
- Velikost musí být STABILNÍ (STABILITY_REQUIRED kontrol po sobě) a > 0.
- Soubor musí jít otevřít na zápis (r+b) — jinak ho ještě někdo drží (Chrome zapisuje).
Vrací False, pokud soubor není připravený (zpracuje ho příští běh / událost)."""
suffix = f.suffix.lower()
if suffix in IGNORE_SUFFIXES:
log(f" Přeskočeno (probíhá stahování): {f.name}")
return False
last_size = -1
stable = 0
waited = 0.0
while waited <= STABILITY_TIMEOUT:
try:
size = f.stat().st_size
except FileNotFoundError:
# soubor mezitím zmizel (Chrome přejmenoval .crdownload → finální)
log(f" Přeskočeno (soubor zmizel během kontroly): {f.name}")
return False
if size > 0 and size == last_size:
stable += 1
if stable >= STABILITY_REQUIRED:
break
else:
stable = 0
last_size = size
time.sleep(STABILITY_INTERVAL)
waited += STABILITY_INTERVAL
else:
log(f" Přeskočeno (velikost se za {STABILITY_TIMEOUT}s neustálila): {f.name}")
return False
# Zámek: zapisuje-li do souboru ještě jiný proces, r+b selže (Windows).
try:
with f.open("r+b"):
pass
except PermissionError:
log(f" Přeskočeno (soubor je zamčený jiným procesem): {f.name}")
return False
except FileNotFoundError:
log(f" Přeskočeno (soubor zmizel během kontroly zámku): {f.name}")
return False
return True
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:
# Přeskoč nedotažené / zamčené soubory (Chrome ještě stahuje)
if not is_ready(f):
continue
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:
# Pojistka: neodesílej nedotažený / zamčený / dočasný soubor
if not is_ready(f):
continue
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 ===")
+89
View File
@@ -0,0 +1,89 @@
# Název: janssenpc_file_watch.py
# Verze: 1.2
# Datum: 2026-06-12
# Popis: Démon hlídající složku ##JNJPrenos (watchdog). Při objevení nového souboru
# spustí janssenpc_file_send.py, který zajistí přejmenování, upload a přesun do Trash.
#
# Změna v1.2 (NEDOTAŽENÉ STAHOVÁNÍ Z CHROME):
# - Ignoruje dočasné přípony (.crdownload/.tmp/.part) — Chrome stahuje do
# *.crdownload a teprve po dokončení přejmenuje na finální jméno. Spouštět
# send na dočasný soubor nemá smysl (vyřeší to až událost přejmenování).
# - Debounce: shlukne rychlou sérii událostí (vytvoření + přejmenování + zápis)
# do JEDNOHO spuštění send. Mezi spuštěními drží DEBOUNCE_SECONDS odstup.
# Vlastní kontrolu „dotažení" (stabilní velikost + zámek) dělá až file_send
# (funkce is_ready) — watch jen omezuje zbytečná spuštění.
import subprocess
import sys
import threading
import time
from pathlib import Path
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
SOURCE_DIR = Path(r"C:\Users\vbuzalka\OneDrive - JNJ\##JNJPrenos")
SEND_SCRIPT = Path(__file__).parent / "janssenpc_file_send.py"
IGNORE_SUFFIXES = {".crdownload", ".tmp", ".part"} # probíhající stahování
DEBOUNCE_SECONDS = 5.0 # série událostí v tomto okně spustí send jen jednou
def run_send():
subprocess.run([sys.executable, str(SEND_SCRIPT)], check=False)
class DebouncedRunner:
"""Shlukne rychlou sérii událostí do jednoho spuštění send."""
def __init__(self, delay: float):
self.delay = delay
self._timer = None
self._lock = threading.Lock()
def trigger(self):
with self._lock:
if self._timer is not None:
self._timer.cancel()
self._timer = threading.Timer(self.delay, run_send)
self._timer.daemon = True
self._timer.start()
_runner = DebouncedRunner(DEBOUNCE_SECONDS)
def _is_temp(path: str) -> bool:
return Path(path).suffix.lower() in IGNORE_SUFFIXES
class NewFileHandler(FileSystemEventHandler):
def on_created(self, event):
if event.is_directory or _is_temp(event.src_path):
return
_runner.trigger()
def on_moved(self, event):
if event.is_directory:
return
# Cíl přejmenování je rozhodující (Chrome: *.crdownload -> finální jméno)
dest = getattr(event, "dest_path", "") or event.src_path
if _is_temp(dest):
return
_runner.trigger()
if __name__ == "__main__":
# Při startu zpracuj soubory, které už tam jsou (kromě dočasných)
if any(f for f in SOURCE_DIR.iterdir() if f.is_file() and f.suffix.lower() not in IGNORE_SUFFIXES):
run_send()
observer = Observer()
observer.schedule(NewFileHandler(), str(SOURCE_DIR), recursive=False)
observer.start()
print(f"Hlídám: {SOURCE_DIR}")
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()