# ============================================================ # download_vault_v1.4.py # Verze: 1.4 # Datum: 2026-06-12 # Popis: Stažení source-file dokumentu z Veeva Vault (J&J V-TMF). # Perzistentní Chromium profil (session přežije běhy), # login přes J&J SSO formulář, ruční potvrzení 2FA na # telefonu, stažení Source File a přejmenování podle # VTMF čísla do U:\Dropbox\!!!Days\Downloads Z230\. # # v1.1: VAULT_USER / VAULT_PASS se čtou ze souboru .env v rootu # projektu Janssen; env proměnné mají přednost. # v1.2: když údaje chybí (nový počítač — .env se přes Giteu # nepřenáší), skript .env založí / doplní v něm chybějící # řádky VAULT_USER=/VAULT_PASS=, vyzve k doplnění a skončí. # v1.3: robustnější odeslání login formuláře (Sign On / Login / # submit / Enter fallbacky). # v1.4: maintenance okno je jQuery UI dialog (.vv_login_msg_dialog); # jeho "OK" v patičce je nefunkční — jediné funkční zavření je # křížek .ui-dialog-titlebar-close. Zavírá se ve smyčce (dialogy # umí být ve frontě), s opakovanou kontrolou kvůli zpožděnému # zobrazení; záloha klávesa Escape. # Heslo se NIKDY nedává natvrdo do skriptu. # ============================================================ import os import re import sys from pathlib import Path from playwright.sync_api import sync_playwright, TimeoutError as PWTimeout # --- Konfigurace ------------------------------------------------------- LOGIN_URL = ("https://fedlogin.jnj.com/idp/eyJ2c2lkIjoiam5qX3ZlZXZhIn0/" "startSSO.ping?PartnerSpId=janssenetmf.veevavault.com" "&IdpAdapterId=CompIWALDAPEXTFORM" "&TargetResource=https%3A%2F%2Fvtmf.veevavault.com%2F") VAULT_UI_PATTERN = "**vtmf.veevavault.com/ui**" # úspěšný vstup do Vaultu SCRIPT_DIR = Path(__file__).resolve().parent PROFILE_DIR = SCRIPT_DIR / "vault_profile" # perzistentní session ENV_FILE = SCRIPT_DIR.parent / ".env" # root projektu Janssen DOWNLOAD_DIR = Path(r"U:\Dropbox\!!!Days\Downloads Z230") # Zatím jeden dokument; struktura připravená na pozdější seznam # (ve fázi 2 se naplní z Excelu). DOCUMENTS = [ ("https://vtmf.veevavault.com/ui/#doc_info/31824736/1/0", "VTMF-25690359"), ] def log(msg): print(msg, flush=True) def load_env_file(path): """Načte KEY=VALUE řádky z .env do os.environ. Už nastavené env proměnné mají přednost, .env je nepřepisuje.""" if not path.exists(): log(f"[!] .env nenalezen: {path}") return for line in path.read_text(encoding="utf-8").splitlines(): line = line.strip() if not line or line.startswith("#") or "=" not in line: continue key, _, value = line.partition("=") key, value = key.strip(), value.strip().strip('"').strip("'") if value and key not in os.environ: os.environ[key] = value ENV_SECTION_HEADER = "# --- Veeva Vault (J&J V-TMF) — VTMFDownloadFiles/download_vault ---" ENV_KEYS = ("VAULT_USER", "VAULT_PASS") def ensure_credentials(): """Načte .env; pokud VAULT_USER/VAULT_PASS chybí, založí/doplní v .env šablonu, vyzve uživatele k doplnění a ukončí skript.""" load_env_file(ENV_FILE) if all(os.environ.get(k) for k in ENV_KEYS): return existing = ENV_FILE.read_text(encoding="utf-8") if ENV_FILE.exists() else "" # doplnit jen řádky, které v souboru úplně chybí (prázdné hodnoty nechat) missing_lines = [f"{k}=" for k in ENV_KEYS if not re.search(rf"^\s*{k}\s*=", existing, re.M)] if not ENV_FILE.exists(): ENV_FILE.write_text( "# .env — lokální přihlašovací údaje (NEVERZOVAT, je v .gitignore)\n\n" + ENV_SECTION_HEADER + "\n" + "\n".join(missing_lines) + "\n", encoding="utf-8") log(f"[i] Založil jsem nový .env: {ENV_FILE}") elif missing_lines: with open(ENV_FILE, "a", encoding="utf-8") as f: f.write("\n" + ENV_SECTION_HEADER + "\n" + "\n".join(missing_lines) + "\n") log(f"[i] Doplnil jsem chybějící řádky do .env: {ENV_FILE}") print("\n" + "=" * 60) print(" CHYBÍ PŘIHLAŠOVACÍ ÚDAJE.") print(f" Doplň VAULT_USER a VAULT_PASS do souboru:") print(f" {ENV_FILE}") print(" a spusť skript znovu.") print("=" * 60) sys.exit(1) # --- Přihlášení -------------------------------------------------------- def submit_login_form(page, password_box): """Odešle login formulář. Zkouší postupně tlačítka Sign On / Login / OK / submit input; když žádné nenajde, stiskne Enter v poli hesla.""" candidates = [ page.get_by_role("button", name=re.compile("sign\\s*on", re.I)), page.get_by_role("button", name=re.compile("log\\s*in|sign\\s*in", re.I)), page.locator("input[type='submit']"), page.locator("button[type='submit']"), page.get_by_role("button", name=re.compile("^ok$", re.I)), ] for loc in candidates: try: if loc.count() and loc.first.is_visible(): label = (loc.first.inner_text() or loc.first.get_attribute("value") or "submit").strip() log(f"[i] Odesílám formulář tlačítkem '{label}'...") loc.first.click() return except Exception: continue log("[i] Tlačítko nenalezeno, odesílám Enterem v poli hesla...") password_box.press("Enter") def login_if_needed(page): """Otevře login URL, vyplní jméno+heslo, detekuje 2FA a počká na ruční potvrzení. Pokud perzistentní session žije, login přeskočí.""" log(f"[i] Otevírám přihlašovací URL...") page.goto(LOGIN_URL, wait_until="domcontentloaded") # Perzistentní session může rovnou přesměrovat do Vaultu if "vtmf.veevavault.com/ui" in page.url: log("[i] Už přihlášen (perzistentní session).") return # SSO redirecty chvíli trvají — čekáme buď na login formulář, # nebo na samovolný vstup do Vaultu. user_box = page.locator("input[type='text']").first try: user_box.wait_for(timeout=8000) except PWTimeout: if "vtmf.veevavault.com/ui" in page.url: log("[i] Přihlášen bez formuláře (session redirect).") return raise RuntimeError( f"Nenašel jsem login formulář ani Vault. Aktuální URL: {page.url}") # údaje z .env v rootu projektu (případně env proměnné); # jejich existenci už ověřil ensure_credentials() na startu username = os.environ["VAULT_USER"] password = os.environ["VAULT_PASS"] log("[i] Vyplňuji přihlašovací údaje...") user_box.fill(username) password_box = page.locator("input[type='password']").first password_box.fill(password) submit_login_form(page, password_box) # --- Detekce 2FA / čekání na vstup do Vaultu --- log("[i] Odeslán login, čekám na výsledek...") try: page.wait_for_url(VAULT_UI_PATTERN, timeout=15000) log("[ok] Přihlášen rovnou (bez 2FA).") return except PWTimeout: pass # nejsme ve Vaultu -> pravděpodobně 2FA výzva # Pojistka: pokud formulář hlásí chybu (špatné heslo), nečekat na 2FA err = page.locator("text=/invalid|incorrect|failed/i") try: if err.count() and err.first.is_visible(): raise RuntimeError(f"Login selhal: {err.first.inner_text().strip()}") except PWTimeout: pass print("\n" + "=" * 60) print(" VYŽADOVÁNO OVĚŘENÍ NA TELEFONU (2FA).") print(" Potvrď přihlášení v mobilní aplikaci.") print("=" * 60) input(" Až to potvrdíš, stiskni ENTER pro pokračování... ") page.wait_for_url(VAULT_UI_PATTERN, timeout=120000) log("[ok] Přihlášení dokončeno.") def verify_inside(page): """Ověří, že jsme uvnitř Vaultu (URL na /ui).""" page.wait_for_url(VAULT_UI_PATTERN, timeout=30000) log(f"[ok] Uvnitř Vaultu: {page.url}") def dismiss_maintenance_popup(page, attempts=3): """Zavře Veeva login/maintenance jQuery-UI dialog(y), pokud jsou zobrazené. Modré 'OK' v patičce dialogu je nefunkční — jediné funkční zavření je křížek .ui-dialog-titlebar-close v titulku. Dialogy umí být ve frontě (zavírá se ve smyčce) a objevují se i se zpožděním po načtení stránky (attempts × 1 s). Bezpečné volat vždy — když okno není, tiše projde.""" closed = 0 for attempt in range(attempts): # zavírej křížkem, dokud nějaký dialog existuje (max 5 ve frontě) for _ in range(5): btn = page.locator(".ui-dialog-titlebar-close") try: if btn.count() and btn.first.is_visible(): btn.first.click() page.wait_for_timeout(300) closed += 1 log("[i] Zavřel jsem maintenance/login dialog (křížkem).") continue except Exception: pass # dialog mezitím zmizel — v pořádku break if closed: return True if attempt < attempts - 1: page.wait_for_timeout(1000) # okno se může teprve objevit # záloha: jQuery UI dialog s closeOnEscape reaguje na Escape dialog = page.locator(".ui-dialog:visible") try: if dialog.count(): page.keyboard.press("Escape") page.wait_for_timeout(300) log("[i] Zkusil jsem dialog zavřít klávesou Escape.") except Exception: pass return bool(closed) # --- Stažení ----------------------------------------------------------- def build_target_name(suggested, vtmf): """Původní název + ' [VTMF-XXXXXX]' před příponou.""" if "." in suggested: base, ext = suggested.rsplit(".", 1) return f"{base} [{vtmf}].{ext}" return f"{suggested} [{vtmf}]" def find_source_file_button(page): """Najde ikonu Source File (list papíru se šipkou dolů, vpravo nahoře). Více fallback selektorů — DOM se může lišit podle typu dokumentu.""" candidates = [ "[title='Source File']", "[aria-label='Source File']", ] for sel in candidates: loc = page.locator(sel) if loc.count(): return loc.first # fallback: tlačítko/role s názvem Source File loc = page.get_by_role("button", name=re.compile("Source File", re.I)) if loc.count(): return loc.first return None def download_source_file(page, doc_url, vtmf): log(f"[i] Otevírám dokument {vtmf} ...") page.goto(doc_url, wait_until="domcontentloaded") try: page.wait_for_load_state("networkidle", timeout=30000) except PWTimeout: log("[!] networkidle nenastal do 30 s, zkouším pokračovat...") dismiss_maintenance_popup(page) target = find_source_file_button(page) if target is None: raise RuntimeError( f"Nenašel jsem ikonu 'Source File' na stránce dokumentu {vtmf}.") log("[i] Klikám na Source File a čekám na download...") with page.expect_download(timeout=60000) as dl_info: target.click() # Varianta s dropdownem (Source File + Viewable Rendition): # pokud klik otevřel menu místo downloadu, klikni na položku menu. try: item = page.get_by_role("menuitem", name=re.compile("Source File", re.I)) if item.count() and item.first.is_visible(): log("[i] Otevřel se dropdown, vybírám 'Source File'...") item.first.click() except Exception: pass download = dl_info.value new_name = build_target_name(download.suggested_filename, vtmf) DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True) dest = DOWNLOAD_DIR / new_name download.save_as(str(dest)) log(f"[ok] Uloženo: {dest}") return dest # --- Main -------------------------------------------------------------- def main(): ensure_credentials() DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True) with sync_playwright() as p: ctx = p.chromium.launch_persistent_context( user_data_dir=str(PROFILE_DIR), headless=False, accept_downloads=True, no_viewport=True, # okno se chová nativně args=["--start-maximized"], ) page = ctx.pages[0] if ctx.pages else ctx.new_page() ok, failed = [], [] try: login_if_needed(page) verify_inside(page) dismiss_maintenance_popup(page) for doc_url, vtmf in DOCUMENTS: try: ok.append(download_source_file(page, doc_url, vtmf)) except Exception as e: log(f"[CHYBA] {vtmf}: {e}") failed.append(vtmf) finally: log(f"\n[i] Hotovo: {len(ok)} staženo, {len(failed)} chyb" + (f" ({', '.join(failed)})" if failed else "")) input("ENTER pro zavření prohlížeče...") ctx.close() sys.exit(1 if failed else 0) if __name__ == "__main__": main()