Initial commit — clean history (removed large test files, browser profiles, Medidata/Clario downloads)

This commit is contained in:
2026-06-01 15:36:31 +02:00
commit bb604e593e
1304 changed files with 116480 additions and 0 deletions
@@ -0,0 +1,238 @@
# download_test_results_v1.0.py — dokumentace
**Verze:** 1.0 · **Datum:** 2026-05-29
**Umístění:** `U:\PythonProject\Janssen\Covance_UCO3001\download_test_results_v1.0.py`
---
## 1. Účel
Automatické stažení reportu **Standard Test Results** z portálu Labcorp Sponsor
Portal (`xsp.labcorp.com`) pro studii **77242113UCO3001** (interní study ID
**36940**). Skript projde **všech 12 center** studie, u každého vyexportuje grid
do CSV a uloží ho timestampovaný do adresáře `Source/`.
> Pozn.: Stahuje se záložka **Standard** (ne Microbiology). URL končí
> `/standard-test-results`.
---
## 2. Spuštění
```bat
U:\PythonProject\Janssen\.venv\Scripts\python.exe ^
U:\PythonProject\Janssen\Covance_UCO3001\download_test_results_v1.0.py
```
- Prohlížeč běží **viditelně** (`headless=False`), maximalizovaný.
- Používá **persistent profile** (`browser_profile/` vedle skriptu) — session
(přihlášení) přežívá mezi spuštěními.
---
## 3. Konfigurace (konstanty v hlavičce skriptu)
| Konstanta | Hodnota / význam |
|---|---|
| `EMAIL` | `vbuzalka@its.jnj.com` (login přes iMedidata/OKTA na xsp.covance.com) |
| `PASSWORD` | heslo k účtu (uložené přímo v kódu) |
| `LOGIN_URL` | `https://xsp.covance.com/` — po přihlášení redirect na xsp.labcorp.com |
| `OUT_DIR` | `U:\PythonProject\Janssen\Covance_UCO3001\Source` |
| `PROFILE_DIR` | `browser_profile/` vedle skriptu (persistent Chromium profil) |
| `STUDY` | `36940` (interní study ID pro 77242113UCO3001) |
| `SITE_IDS` | seznam 12 interních čísel center — viz níže |
### Interní čísla center (SITE_IDS)
```
930551, 930556, 930525, 930549, 930543, 930547,
930555, 930557, 930539, 930536, 930553, 930531
```
> **Zdroj:** převzato z `download_equeries_report_v1.1.py` (proměnná `SITES`).
> Jsou to **interní ID** Labcorpu, **nikoli** čísla center typu CZ10001.
> V URL test-results se používá právě toto interní ID:
> `…/test-results/{SITE_ID}/standard-test-results`.
### Generování REPORTS (DRY)
`REPORTS` se sestaví automaticky z `SITE_IDS` — URL i název souboru mají vzor
napsaný jen jednou. **Přidání/odebrání centra = úprava seznamu `SITE_IDS`.**
---
## 4. Výstupní soubory
Formát názvu (timestamp + popisný název):
```
{YYYY-MM-DD_HHMMSS} sponsor-study-36940-test-results-{SITE_ID}-standard.csv
```
Příklad:
```
2026-05-29_125710 sponsor-study-36940-test-results-930557-standard.csv
```
- Timestamp se generuje pro **každý** report zvlášť (v okamžiku exportu).
- Staré soubory se **nikdy nemažou** (verzování přes timestamp).
- Browser dočasný název se na disk neukládá — `expect_download` zachytí download
event a `save_as()` ho uloží pod naším názvem.
---
## 5. Průběh skriptu (kroky + logging)
Každý krok se loguje s časem (`[HH:MM:SS]`, `flush=True` → vypisuje průběžně).
| Fáze | Co dělá |
|---|---|
| START | spustí prohlížeč |
| LOGIN | otevře login; **pokud je session aktivní, přihlášení přeskočí** |
| Pro každé centrum: | |
| KROK 1/5 | navigace na report URL |
| KROK 2/5 | čeká na řádky gridu `.ag-row` **nebo** prázdný grid; pak stabilizace počtu |
| KROK 3/5 | klik na viditelné tři tečky (`more_horiz`) → otevře menu |
| KROK 4/5 | klik na viditelné „Export to CSV" → zachytí download |
| KROK 5/5 | uloží soubor do `OUT_DIR` |
| KONEC | souhrn `hotovo X/12` + seznam selhaných center |
### Příklad výstupu
```
[12:56:35] START: prohlizec spusten.
[12:56:35] LOGIN: otviram login stranku...
[12:56:49] LOGIN: prihlaseni OK (...)
[12:56:49] === Centrum 930557 (studie 36940) ===
[12:56:59] KROK 1/5: stranka nactena (...)
[12:57:05] KROK 2/5: radky se objevily, cekam na stabilizaci poctu...
[12:57:06] ...kontrola #1: 153 radku
[12:57:08] ...kontrola #2: 153 radku
[12:57:10] KROK 2/5: data stabilni (153 radku v gridu).
[12:57:10] KROK 3/5: menu otevreno.
[12:57:11] KROK 4/5: stahovani zachyceno, ukladam soubor...
[12:57:11] KROK 5/5: HOTOVO -> ...\2026-05-29_125710 sponsor-study-36940-test-results-930557-standard.csv
```
---
## 6. Klíčové technické poznatky (PROČ to tak je) — ověřeno přes Chrome DevTools
Stránka test-results je **Angular SPA** s knihovnou **MDL** a tabulkou **AG Grid**.
Tyto detaily byly zjištěny živou inspekcí DOM (Claude in Chrome MCP), ne hádáním:
### 6.1 Čekání na data = řádky AG Gridu
- Grid je `<covance-ag-grid>``<ag-grid-angular>` (AG Grid).
- Data jsou načtena, jakmile se objeví řádky **`div.ag-row`** (count jde z 0 → N;
pro 930557 to bylo 153 řádků).
- **Řádky jsou `position-absolute`** (virtuální render AG Gridu) → Playwright je
**nepovažuje za „visible"**. Proto:
-`wait_for_selector("div.ag-row")` (default `state="visible"`) **timeoutuje**
i když řádky existují.
- ✅ čekat na **přítomnost v DOM**:
`wait_for_function("() => document.querySelectorAll('div.ag-row').length > 0")`.
- Stabilizace: počet řádků se čte opakovaně co 2 s, dokud se 2× po sobě neshodne.
### 6.2 „Fetching Data" / spinner — POZOR, NEPLATÍ pro tuto stránku
- Na test-results stránce **NENÍ** text „Fetching Data" (ten je na *samples*
reportu, jiná stránka!).
- Je tu element `<loading-bar>`, ale ten jen krátce problikne `<mdl-progress>`
při route-loadingu, **ne** během načítání dat gridu → nelze na něj spoléhat.
- Spolehlivý signál je výhradně **objevení `.ag-row`**.
### 6.3 Tři tečky (export) — na stránce jsou DVA `<ag-export>`
- Na stránce existují **2× `<ag-export>`**: jeden **skrytý**, jeden viditelný.
- Existují **3× ikona `more_horiz`**: 1 mimo export (toolbar), 1 ve skrytém
ag-export, 1 ve viditelném.
- Proto je nutný filtr na viditelnost:
`page.locator("ag-export button:visible", has_text="more_horiz").first.click()`
### 6.4 „Export to CSV" — v DOM jsou DVĚ položky
- Po otevření menu existují **2× `mdl-menu-item` „Export to CSV"** (jedna skrytá
z neviditelného ag-export, jedna viditelná). Stejně tak 2× „Export to Excel".
- Proto:
`page.locator("mdl-menu-item:visible", has_text="Export to CSV").first.click()`
-`get_by_text("Export to CSV")`**strict mode violation** (2 elementy).
### 6.5 Prázdné centrum (ověřeno přes Chrome MCP na centru 930551)
- AG Grid při 0 záznamech zobrazí no-rows overlay s textem **„No Data"**.
- Struktura: `.ag-overlay``.ag-overlay-panel`
**`.ag-overlay-no-rows-wrapper`** → `<span>No Data</span>`.
- ⚠️ Třída **NENÍ** `.ag-overlay-no-rows-center` (to byl chybný předpoklad,
na který detekce nikdy nezabrala) — správně je **`.ag-overlay-no-rows-wrapper`**.
- ⚠️ Text „No Data" **NENÍ** v `.ag-body-viewport` (ten je prázdný,
`height: 1px`) — je v samostatném overlay sourozenci.
- ⚠️ Na stránce jsou **2 overlaye** (jeden skrytý, jeden viditelný) — stejně
jako u `ag-export`. Proto kontrola **viditelnosti** `offsetParent !== null`.
- Detekce (KROK 2):
```js
() => {
if (document.querySelectorAll('div.ag-row').length > 0) return false;
return [...document.querySelectorAll('.ag-overlay-no-rows-wrapper')]
.some(e => e.offsetParent !== null);
}
```
- KROK 2 čeká na `.ag-row` **NEBO** tuto detekci → centrum bez dat
necheká zbytečně 120 s a export se přeskočí.
---
## 7. Login logika (sdílená napříč Covance skripty)
```python
page.goto(LOGIN_URL)
page.wait_for_load_state("networkidle")
if not page.get_by_label("Email").is_visible():
return # session aktivní → login přeskočit
# jinak: Email → Next → Password → Verify → wait redirect (code= zmizí z URL)
```
- **Proč `is_visible()` a ne kontrola URL:** po `goto(LOGIN_URL)` zůstane URL
`xsp.covance.com` i po redirectu na dashboard, takže URL test je nespolehlivý.
Spolehlivé je, zda **existuje pole Email** (login formulář) nebo ne.
- `is_visible()` neháže výjimku — když pole není, vrátí `False`.
---
## 8. Chrome flagy proti „Restore pages" / broken session
V `args` launchu:
```
--disable-restore-session-state # neobnovovat předchozí session
--disable-session-crashed-bubble # potlačit "Chromium didn't shut down correctly"
```
Důvod: při natvrdo ukončeném skriptu (kill) Chromium jinak při dalším startu
nabídne „Restore pages?" dialog, který rozbil interakci.
---
## 9. Robustnost smyčky
- Každé centrum je v `try/except` → **chyba u jednoho nezastaví zbytek**.
- Na konci souhrn: `hotovo X/12` + seznam `SELHALA centra: …`.
- Selhané centrum v logu = snadno se dohledá a vyřadí/opraví ID.
---
## 10. Možná budoucí rozšíření
- **Microbiology záložka**: existuje i `…/test-results/{SITE}/microbiology…`
(analogická stránka, pravděpodobně stejná AG Grid logika).
- **Druhá studie** (MDD3003, study 35472): přidat další `STUDY` + `SITE_IDS`
a obalit do vnější smyčky (vzor viz `download_samples_report` se seznamem STUDIES).
- **Ověření počtu**: počet `.ag-row` se loguje — dá se porovnat s počtem řádků
ve staženém CSV jako sanity check.
---
## 11. Příbuzné skripty (stejná složka / portál)
| Skript | Co stahuje |
|---|---|
| `download_samples_report_v1.1.py` | All Samples (sampletracking, čeká na „Fetching Data") |
| `download_kit_inventory_v2.1.py` | Kit inventory (on-hand expiration) |
| `download_equeries_report_v1.1.py` | eQuery reporty (zdroj SITE_IDS) |
| `download_test_results_v1.0.py` | **tento** — Standard Test Results |
Všechny sdílejí: persistent profile, login logiku s `is_visible()` checkem,
`ag-export` + „Export to CSV" pattern (equeries/test-results).
@@ -0,0 +1,173 @@
# =============================================================================
# Název: download_test_results_v1.0.py
# Verze: 1.0
# Datum: 2026-05-29
# Popis: Stahuje Standard Test Results ze xsp.labcorp.com pro studii 36940.
# Čeká na načtení AG Grid řádků (.ag-row) před exportem.
# Výstup: timestampované CSV do adresáře Source/.
# =============================================================================
from playwright.sync_api import sync_playwright
from datetime import datetime
import os
def log(msg):
print(f"[{datetime.now().strftime('%H:%M:%S')}] {msg}", flush=True)
EMAIL = "vbuzalka@its.jnj.com"
PASSWORD = "%zT3Wqfc9)cWua5"
LOGIN_URL = "https://xsp.covance.com/"
OUT_DIR = r"U:\PythonProject\Janssen\Covance_UCO3001\Source"
PROFILE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "browser_profile")
# Studie + jejich interni cisla center.
# 36940 = 77242113UCO3001 (zdroj center: download_equeries_report SITES)
# 35472 = druha studie
STUDIES = [
{
"study": "36940",
"sites": [
"930551", "930556", "930525", "930549", "930543", "930547",
"930555", "930557", "930539", "930536", "930553", "930531",
],
},
{
"study": "35472",
"sites": [
"898745", "898739", "898733", "898744", "898727",
],
},
]
# Typy reportu: zalozka v URL + suffix v nazvu souboru.
REPORT_TYPES = [
{"slug": "standard-test-results", "suffix": "standard"},
{"slug": "microbiology", "suffix": "microbiology"},
]
REPORTS = [
{
"site": sid,
"study": st["study"],
"type": rt["suffix"],
"url": f"https://xsp.labcorp.com/sponsor/study/{st['study']}/test-results/{sid}/{rt['slug']}",
"filename": f"sponsor-study-{st['study']}-test-results-{sid}-{rt['suffix']}.csv",
}
for st in STUDIES
for sid in st["sites"]
for rt in REPORT_TYPES
]
def login(page):
log("LOGIN: otviram login stranku...")
page.goto(LOGIN_URL)
page.wait_for_load_state("networkidle")
if not page.get_by_label("Email").is_visible():
log(f"LOGIN: session uz aktivni, prihlaseni preskoceno ({page.url})")
return
log("LOGIN: zadavam email...")
page.get_by_label("Email").fill(EMAIL)
page.get_by_role("button", name="Next").click()
page.wait_for_load_state("networkidle")
log("LOGIN: zadavam heslo...")
page.get_by_label("Password").fill(PASSWORD)
page.get_by_role("button", name="Verify").click()
log("LOGIN: cekam na presmerovani po prihlaseni...")
page.wait_for_url(lambda url: "code=" not in url, timeout=60000)
page.wait_for_load_state("networkidle", timeout=60000)
page.wait_for_timeout(2000)
log(f"LOGIN: prihlaseni OK ({page.url})")
def download_report(page, report):
log(f"=== Centrum {report['site']} / {report['type']} (studie {report['study']}) ===")
log(f"KROK 1/5: navigace na report URL...")
page.goto(report["url"])
log(f"KROK 1/5: stranka nactena ({page.url})")
# Grid je AG Grid uvnitř <covance-ag-grid>. Data jsou nactena, jakmile
# se v gridu objevi radky (.ag-row jde z 0 -> N). Pockej na prvni radek
# a pak na stabilizaci poctu (proti castecnemu renderu).
log("KROK 2/5: cekam na radky gridu (.ag-row) nebo prazdny grid ('No Data')...")
# AG Grid radky jsou position-absolute (virtualni render), takze nejsou
# "visible" dle Playwrightu -> cekej na pritomnost v DOM, ne na viditelnost.
# Prazdne centrum: AG Grid vykresli no-rows overlay s textem "No Data" ve
# wrapperu .ag-overlay-no-rows-wrapper. POZOR: trida NENI -no-rows-center;
# navic jsou na strance 2 overlaye (jeden skryty) -> kontroluj viditelny
# (offsetParent != null). Detekuj, aby to u centra bez dat necekalo 120 s.
EMPTY_GRID_JS = """() => {
if (document.querySelectorAll('div.ag-row').length > 0) return false;
return [...document.querySelectorAll('.ag-overlay-no-rows-wrapper')]
.some(e => e.offsetParent !== null);
}"""
page.wait_for_function(
f"""() => document.querySelectorAll('div.ag-row').length > 0
|| ({EMPTY_GRID_JS})()""",
timeout=120000,
)
if page.evaluate(EMPTY_GRID_JS):
log("KROK 2/5: centrum bez dat ('No Data' overlay) — preskakuji export.")
return
log("KROK 2/5: radky se objevily, cekam na stabilizaci poctu...")
prev = -1
for i in range(20): # max ~40 s stabilizace
cnt = page.locator("div.ag-row").count()
log(f" ...kontrola #{i+1}: {cnt} radku")
if cnt == prev and cnt > 0:
break
prev = cnt
page.wait_for_timeout(2000)
page.wait_for_timeout(2000) # buffer
log(f"KROK 2/5: data stabilni ({prev} radku v gridu).")
# Tri tecky: na strance jsou 2x <ag-export> (jeden skryty), klikni na
# VIDITELNY more_horiz button.
log("KROK 3/5: klikam na viditelne tri tecky (more_horiz)...")
page.locator("ag-export button:visible", has_text="more_horiz").first.click()
log("KROK 3/5: menu otevreno.")
timestamp = datetime.now().strftime("%Y-%m-%d_%H%M%S")
dest = os.path.join(OUT_DIR, f"{timestamp} {report['filename']}")
log("KROK 4/5: klikam na 'Export to CSV' a cekam na stahovani...")
with page.expect_download(timeout=60000) as dl:
# 2x "Export to CSV" v DOM (jeden skryty) -> klikni na VIDITELNY
page.locator("mdl-menu-item:visible", has_text="Export to CSV").first.click()
log("KROK 4/5: stahovani zachyceno, ukladam soubor...")
dl.value.save_as(dest)
log(f"KROK 5/5: HOTOVO -> {dest}")
if __name__ == "__main__":
with sync_playwright() as p:
context = p.chromium.launch_persistent_context(
user_data_dir=PROFILE_DIR,
headless=False,
args=[
"--disable-blink-features=AutomationControlled",
"--start-maximized",
"--disable-restore-session-state",
"--disable-session-crashed-bubble",
],
no_viewport=True,
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
accept_downloads=True,
)
context.add_init_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})")
page = context.new_page()
log("START: prohlizec spusten.")
login(page)
ok, failed = 0, []
for idx, report in enumerate(REPORTS, 1):
log(f">>> Report {idx}/{len(REPORTS)}")
try:
download_report(page, report)
ok += 1
except Exception as e:
failed.append(f"{report['site']}/{report['type']}")
log(f"CHYBA u centra {report['site']}/{report['type']}: {e!r} — pokracuji dalsim.")
log(f"KONEC: hotovo {ok}/{len(REPORTS)} reportu.")
if failed:
log(f"KONEC: SELHALA centra: {', '.join(failed)}")
context.close()
@@ -0,0 +1,231 @@
# download_test_results_v1.1.py — dokumentace
**Verze:** 1.1 · **Datum:** 2026-05-29
**Umístění:** `U:\PythonProject\Janssen\Covance_UCO3001\download_test_results_v1.1.py`
> **Změny v1.1 oproti 1.0:** přidána **druhá studie 35472 (MDD)** a druhý typ
> reportu **Microbiology**. Dříve jen 36940 + Standard.
---
## 1. Účel
Automatické stažení reportů **Test Results** z portálu Labcorp Sponsor Portal
(`xsp.labcorp.com`). Skript projde **2 studie × jejich centra × 2 typy reportu**,
u každého vyexportuje grid do CSV a uloží ho timestampovaný do `Source/`.
| Studie | Interní ID | Význam | Počet center |
|---|---|---|---|
| UC | `36940` | 77242113UCO3001 | 12 |
| MDD | `35472` | druhá studie | 5 |
Typy reportu (záložka v URL): **Standard** (`/standard-test-results`) a
**Microbiology** (`/microbiology`).
**Celkem: (12 + 5) × 2 = 34 reportů** (prázdná centra se přeskakují, viz 6.5).
---
## 2. Spuštění
```bat
U:\PythonProject\Janssen\.venv\Scripts\python.exe ^
U:\PythonProject\Janssen\Covance_UCO3001\download_test_results_v1.1.py
```
- Prohlížeč běží **viditelně** (`headless=False`), maximalizovaný.
- **Persistent profile** (`browser_profile/` vedle skriptu) — session přežívá.
### Dočasný test (1 centrum / studie)
Pro rychlé ověření stačí dočasně zúžit `STUDIES`, např.:
```python
STUDIES = [
{"study": "36940", "sites": ["930557"]}, # UC — centrum s daty
{"study": "35472", "sites": ["898745"]}, # MDD — první centrum
]
```
→ proběhnou 4 reporty (2 centra × 2 typy). Po testu vrátit plný seznam.
---
## 3. Konfigurace (konstanty v hlavičce skriptu)
| Konstanta | Hodnota / význam |
|---|---|
| `EMAIL` | `vbuzalka@its.jnj.com` (login přes iMedidata/OKTA na xsp.covance.com) |
| `PASSWORD` | heslo k účtu (uložené přímo v kódu) |
| `LOGIN_URL` | `https://xsp.covance.com/` — po přihlášení redirect na xsp.labcorp.com |
| `OUT_DIR` | `U:\PythonProject\Janssen\Covance_UCO3001\Source` |
| `PROFILE_DIR` | `browser_profile/` vedle skriptu (persistent Chromium profil) |
| `STUDIES` | seznam studií, každá má `study` (interní ID) + `sites` (interní ID center) |
| `REPORT_TYPES` | `standard-test-results`/`standard` a `microbiology`/`microbiology` |
### Interní čísla center
```
36940 (UC): 930551, 930556, 930525, 930549, 930543, 930547,
930555, 930557, 930539, 930536, 930553, 930531
35472 (MDD): 898745, 898739, 898733, 898744, 898727
```
> **Pozor:** jsou to **interní ID** Labcorpu, **nikoli** čísla center typu
> CZ10001. V URL test-results se používá právě toto interní ID:
> `…/test-results/{SITE_ID}/{standard-test-results|microbiology}`.
> (Pozn.: 36940 zdroj = `download_equeries_report_v1.1.py SITES`;
> 35472 zdroj = ručně dodaná čísla center.)
### Generování REPORTS (DRY)
`REPORTS` se sestaví automaticky jako **kartézský součin**
`STUDIES × sites × REPORT_TYPES`. Přidání/odebrání centra, studie nebo typu
reportu = úprava příslušného seznamu, vzor URL i názvu se píše jen jednou.
---
## 4. Výstupní soubory
Formát názvu (timestamp + popisný název):
```
{YYYY-MM-DD_HHMMSS} sponsor-study-{STUDY}-test-results-{SITE_ID}-{TYP}.csv
```
`{TYP}` = `standard` nebo `microbiology`. Příklady:
```
2026-05-29_131121 sponsor-study-36940-test-results-930557-standard.csv
2026-05-29_131500 sponsor-study-36940-test-results-930557-microbiology.csv
2026-05-29_131800 sponsor-study-35472-test-results-898745-standard.csv
```
- Timestamp se generuje pro **každý** report zvlášť (v okamžiku exportu).
- Staré soubory se **nikdy nemažou** (verzování přes timestamp).
- `expect_download` zachytí download event, `save_as()` uloží pod naším názvem.
---
## 5. Průběh skriptu (kroky + logging)
Každý krok se loguje s časem (`[HH:MM:SS]`, `flush=True`).
| Fáze | Co dělá |
|---|---|
| START | spustí prohlížeč |
| LOGIN | otevře login; **pokud je session aktivní, přihlášení přeskočí** |
| Pro každý report (studie × centrum × typ): | |
| KROK 1/5 | navigace na report URL |
| KROK 2/5 | čeká na řádky gridu `.ag-row` **nebo** prázdný grid („No Data"); pak stabilizace počtu |
| KROK 3/5 | klik na viditelné tři tečky (`more_horiz`) → otevře menu |
| KROK 4/5 | klik na viditelné „Export to CSV" → zachytí download |
| KROK 5/5 | uloží soubor do `OUT_DIR` |
| KONEC | souhrn `hotovo X/34` + seznam selhaných (`site/typ`) |
Log u každého reportu ukazuje i typ: `=== Centrum 930557 / microbiology (studie 36940) ===`.
---
## 6. Klíčové technické poznatky (PROČ to tak je) — ověřeno přes Chrome MCP
Stránka test-results je **Angular SPA** s knihovnou **MDL** a tabulkou **AG Grid**.
Microbiology záložka má **stejnou** strukturu jako Standard → vše níže platí pro oba.
### 6.1 Čekání na data = řádky AG Gridu
- Data jsou načtena, jakmile se objeví řádky **`div.ag-row`** (count 0 → N).
- **Řádky jsou `position-absolute`** (virtuální render) → Playwright je
**nepovažuje za „visible"**. Proto:
-`wait_for_selector("div.ag-row")` (default visible) **timeoutuje**.
-`wait_for_function("() => document.querySelectorAll('div.ag-row').length > 0")`.
- Stabilizace: počet řádků se čte co 2 s, dokud se 2× po sobě neshodne.
- **POZN.: počet `.ag-row` se „zastropuje"** (typicky ~153) kvůli virtuálnímu
renderu — NENÍ to skutečný počet záznamů. Slouží jen jako signál „data jsou".
**Export do CSV ale vyexportuje VŠECHNY řádky** (ověřeno: různé velikosti CSV).
### 6.2 „Fetching Data" / spinner — NEPLATÍ pro tuto stránku
- Na test-results **NENÍ** text „Fetching Data" (ten je na *samples* reportu).
- `<loading-bar>` jen problikne při route-loadu → nespoléhat. Signál = `.ag-row`.
### 6.3 Tři tečky (export) — na stránce jsou DVA `<ag-export>`
- 2× `<ag-export>` (1 skrytý), 3× ikona `more_horiz`. Proto filtr na viditelnost:
`page.locator("ag-export button:visible", has_text="more_horiz").first.click()`
### 6.4 „Export to CSV" — v DOM jsou DVĚ položky
- 2× `mdl-menu-item` „Export to CSV" (1 skrytá). Proto:
`page.locator("mdl-menu-item:visible", has_text="Export to CSV").first.click()`
-`get_by_text("Export to CSV")` → strict mode violation (2 elementy).
### 6.5 Prázdné centrum (ověřeno přes Chrome MCP na centru 930551)
- AG Grid při 0 záznamech zobrazí no-rows overlay s textem **„No Data"**.
- Struktura: `.ag-overlay``.ag-overlay-panel`
**`.ag-overlay-no-rows-wrapper`** → `<span>No Data</span>`.
- ⚠️ Třída **NENÍ** `.ag-overlay-no-rows-center` (chybný předpoklad, nikdy
nezabral) — správně je **`.ag-overlay-no-rows-wrapper`**.
- ⚠️ Text „No Data" **NENÍ** v `.ag-body-viewport` (ten je prázdný, `height: 1px`).
- ⚠️ Na stránce jsou **2 overlaye** (1 skrytý, 1 viditelný) → kontrola
viditelnosti `offsetParent !== null`.
- Detekce (KROK 2):
```js
() => {
if (document.querySelectorAll('div.ag-row').length > 0) return false;
return [...document.querySelectorAll('.ag-overlay-no-rows-wrapper')]
.some(e => e.offsetParent !== null);
}
```
- KROK 2 čeká na `.ag-row` **NEBO** tuto detekci → prázdné centrum necheká
zbytečně 120 s a export se přeskočí.
---
## 7. Login logika (sdílená napříč Covance skripty)
```python
page.goto(LOGIN_URL)
page.wait_for_load_state("networkidle")
if not page.get_by_label("Email").is_visible():
return # session aktivní → login přeskočit
# jinak: Email → Next → Password → Verify → wait redirect (code= zmizí z URL)
```
- **Proč `is_visible()` a ne kontrola URL:** po `goto(LOGIN_URL)` zůstane URL
`xsp.covance.com` i po redirectu, takže URL test je nespolehlivý.
- `is_visible()` neháže výjimku — když pole není, vrátí `False`.
---
## 8. Chrome flagy proti „Restore pages" / broken session
```
--disable-restore-session-state # neobnovovat předchozí session
--disable-session-crashed-bubble # potlačit "Chromium didn't shut down correctly"
```
---
## 9. Robustnost smyčky
- Každý report je v `try/except` → **chyba u jednoho nezastaví zbytek**.
- Na konci souhrn: `hotovo X/34` + seznam `SELHALA centra: site/typ, …`.
---
## 10. Možná budoucí rozšíření
- **Další studie / centra**: přidat položku do `STUDIES`.
- **Další typ reportu**: přidat položku do `REPORT_TYPES` (slug + suffix).
- **Ověření počtu**: počet `.ag-row` se loguje — POZOR, je zastropený virtuálním
renderem, takže ho nelze porovnávat s počtem řádků v CSV (CSV má všechny).
---
## 11. Příbuzné skripty (stejná složka / portál)
| Skript | Co stahuje |
|---|---|
| `download_samples_report_v1.1.py` | All Samples (sampletracking, čeká na „Fetching Data") |
| `download_kit_inventory_v2.1.py` | Kit inventory (on-hand expiration) |
| `download_equeries_report_v1.1.py` | eQuery reporty (zdroj SITE_IDS pro 36940) |
| `download_test_results_v1.1.py` | **tento** — Test Results (Standard + Microbiology, 2 studie) |
Všechny sdílejí: persistent profile, login s `is_visible()` checkem,
`ag-export` + „Export to CSV" pattern.
@@ -0,0 +1,175 @@
# =============================================================================
# Název: download_test_results_v1.1.py
# Verze: 1.1
# Datum: 2026-05-29
# Popis: Stahuje Test Results ze xsp.labcorp.com pro 2 studie (36940, 35472),
# oba typy reportu (Standard + Microbiology), pres vsechna centra.
# Ceka na nacteni AG Grid radku (.ag-row); prazdne centrum ('No Data')
# preskoci. Vystup: timestampovane CSV do adresare Source/.
# Zmeny v1.1: + studie 35472, + report typ microbiology (driv jen 36940/standard).
# =============================================================================
from playwright.sync_api import sync_playwright
from datetime import datetime
import os
def log(msg):
print(f"[{datetime.now().strftime('%H:%M:%S')}] {msg}", flush=True)
EMAIL = "vbuzalka@its.jnj.com"
PASSWORD = "%zT3Wqfc9)cWua5"
LOGIN_URL = "https://xsp.covance.com/"
OUT_DIR = r"U:\PythonProject\Janssen\Covance_UCO3001\Source"
PROFILE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "browser_profile")
# Studie + jejich interni cisla center.
# 36940 = 77242113UCO3001 (UC) — zdroj center: download_equeries_report SITES
# 35472 = druha studie (MDD)
STUDIES = [
{
"study": "36940",
"sites": [
"930551", "930556", "930525", "930549", "930543", "930547",
"930555", "930557", "930539", "930536", "930553", "930531",
],
},
{
"study": "35472",
"sites": [
"898745", "898739", "898733", "898744", "898727",
],
},
]
# Typy reportu: zalozka v URL + suffix v nazvu souboru.
REPORT_TYPES = [
{"slug": "standard-test-results", "suffix": "standard"},
{"slug": "microbiology", "suffix": "microbiology"},
]
REPORTS = [
{
"site": sid,
"study": st["study"],
"type": rt["suffix"],
"url": f"https://xsp.labcorp.com/sponsor/study/{st['study']}/test-results/{sid}/{rt['slug']}",
"filename": f"sponsor-study-{st['study']}-test-results-{sid}-{rt['suffix']}.csv",
}
for st in STUDIES
for sid in st["sites"]
for rt in REPORT_TYPES
]
def login(page):
log("LOGIN: otviram login stranku...")
page.goto(LOGIN_URL)
page.wait_for_load_state("networkidle")
if not page.get_by_label("Email").is_visible():
log(f"LOGIN: session uz aktivni, prihlaseni preskoceno ({page.url})")
return
log("LOGIN: zadavam email...")
page.get_by_label("Email").fill(EMAIL)
page.get_by_role("button", name="Next").click()
page.wait_for_load_state("networkidle")
log("LOGIN: zadavam heslo...")
page.get_by_label("Password").fill(PASSWORD)
page.get_by_role("button", name="Verify").click()
log("LOGIN: cekam na presmerovani po prihlaseni...")
page.wait_for_url(lambda url: "code=" not in url, timeout=60000)
page.wait_for_load_state("networkidle", timeout=60000)
page.wait_for_timeout(2000)
log(f"LOGIN: prihlaseni OK ({page.url})")
def download_report(page, report):
log(f"=== Centrum {report['site']} / {report['type']} (studie {report['study']}) ===")
log(f"KROK 1/5: navigace na report URL...")
page.goto(report["url"])
log(f"KROK 1/5: stranka nactena ({page.url})")
# Grid je AG Grid uvnitř <covance-ag-grid>. Data jsou nactena, jakmile
# se v gridu objevi radky (.ag-row jde z 0 -> N). Pockej na prvni radek
# a pak na stabilizaci poctu (proti castecnemu renderu).
log("KROK 2/5: cekam na radky gridu (.ag-row) nebo prazdny grid ('No Data')...")
# AG Grid radky jsou position-absolute (virtualni render), takze nejsou
# "visible" dle Playwrightu -> cekej na pritomnost v DOM, ne na viditelnost.
# Prazdne centrum: AG Grid vykresli no-rows overlay s textem "No Data" ve
# wrapperu .ag-overlay-no-rows-wrapper. POZOR: trida NENI -no-rows-center;
# navic jsou na strance 2 overlaye (jeden skryty) -> kontroluj viditelny
# (offsetParent != null). Detekuj, aby to u centra bez dat necekalo 120 s.
EMPTY_GRID_JS = """() => {
if (document.querySelectorAll('div.ag-row').length > 0) return false;
return [...document.querySelectorAll('.ag-overlay-no-rows-wrapper')]
.some(e => e.offsetParent !== null);
}"""
page.wait_for_function(
f"""() => document.querySelectorAll('div.ag-row').length > 0
|| ({EMPTY_GRID_JS})()""",
timeout=120000,
)
if page.evaluate(EMPTY_GRID_JS):
log("KROK 2/5: centrum bez dat ('No Data' overlay) — preskakuji export.")
return
log("KROK 2/5: radky se objevily, cekam na stabilizaci poctu...")
prev = -1
for i in range(20): # max ~40 s stabilizace
cnt = page.locator("div.ag-row").count()
log(f" ...kontrola #{i+1}: {cnt} radku")
if cnt == prev and cnt > 0:
break
prev = cnt
page.wait_for_timeout(2000)
page.wait_for_timeout(2000) # buffer
log(f"KROK 2/5: data stabilni ({prev} radku v gridu).")
# Tri tecky: na strance jsou 2x <ag-export> (jeden skryty), klikni na
# VIDITELNY more_horiz button.
log("KROK 3/5: klikam na viditelne tri tecky (more_horiz)...")
page.locator("ag-export button:visible", has_text="more_horiz").first.click()
log("KROK 3/5: menu otevreno.")
timestamp = datetime.now().strftime("%Y-%m-%d_%H%M%S")
dest = os.path.join(OUT_DIR, f"{timestamp} {report['filename']}")
log("KROK 4/5: klikam na 'Export to CSV' a cekam na stahovani...")
with page.expect_download(timeout=60000) as dl:
# 2x "Export to CSV" v DOM (jeden skryty) -> klikni na VIDITELNY
page.locator("mdl-menu-item:visible", has_text="Export to CSV").first.click()
log("KROK 4/5: stahovani zachyceno, ukladam soubor...")
dl.value.save_as(dest)
log(f"KROK 5/5: HOTOVO -> {dest}")
if __name__ == "__main__":
with sync_playwright() as p:
context = p.chromium.launch_persistent_context(
user_data_dir=PROFILE_DIR,
headless=False,
args=[
"--disable-blink-features=AutomationControlled",
"--start-maximized",
"--disable-restore-session-state",
"--disable-session-crashed-bubble",
],
no_viewport=True,
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
accept_downloads=True,
)
context.add_init_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})")
page = context.new_page()
log("START: prohlizec spusten.")
login(page)
ok, failed = 0, []
for idx, report in enumerate(REPORTS, 1):
log(f">>> Report {idx}/{len(REPORTS)}")
try:
download_report(page, report)
ok += 1
except Exception as e:
failed.append(f"{report['site']}/{report['type']}")
log(f"CHYBA u centra {report['site']}/{report['type']}: {e!r} — pokracuji dalsim.")
log(f"KONEC: hotovo {ok}/{len(REPORTS)} reportu.")
if failed:
log(f"KONEC: SELHALA centra: {', '.join(failed)}")
context.close()
@@ -0,0 +1,233 @@
# download_test_results_v1.2.py — dokumentace
**Verze:** 1.2 · **Datum:** 2026-05-29
**Umístění:** `U:\PythonProject\Janssen\Covance_UCO3001\download_test_results_v1.2.py`
> **Změny v1.2 oproti 1.1:** přidán **paralelní běh přes sharding**
> (`--shard N --of M`). Skript lze spustit ve více procesech současně, každý
> zpracuje svůj podíl reportů a používá vlastní profil. Spouští se přes launcher
> `run_test_results_parallel_v1.0.py`.
> **Změny v1.1 oproti 1.0:** druhá studie 35472 (MDD) + report Microbiology.
---
## 1. Účel
Automatické stažení reportů **Test Results** z portálu Labcorp Sponsor Portal
(`xsp.labcorp.com`). Skript projde **2 studie × jejich centra × 2 typy reportu**,
u každého vyexportuje grid do CSV a uloží ho timestampovaný do `Source/`.
| Studie | Interní ID | Význam | Počet center |
|---|---|---|---|
| UC | `36940` | 77242113UCO3001 | 12 |
| MDD | `35472` | druhá studie | 5 |
Typy reportu: **Standard** (`/standard-test-results`) a **Microbiology** (`/microbiology`).
**Celkem: (12 + 5) × 2 = 34 reportů** (prázdná centra se přeskakují, viz 6.5).
---
## 2. Spuštění
### 2a) Paralelně (doporučeno — rychlejší)
Přes launcher, který rozjede 4 procesy (každý ve vlastním okně):
```bat
U:\PythonProject\Janssen\.venv\Scripts\python.exe ^
U:\PythonProject\Janssen\Covance_UCO3001\run_test_results_parallel_v1.0.py
```
### 2b) Serialně (jeden proces, jako dřív)
```bat
U:\PythonProject\Janssen\.venv\Scripts\python.exe ^
U:\PythonProject\Janssen\Covance_UCO3001\download_test_results_v1.2.py
```
### 2c) Jeden konkrétní shard ručně
```bat
…\python.exe download_test_results_v1.2.py --shard 2 --of 4
```
- Prohlížeč běží **viditelně** (`headless=False`), maximalizovaný.
- **Persistent profile** — session přežívá (viz sekce 4).
---
## 3. Paralelní běh (sharding) — jak funguje
### 3.1 Rozdělení práce
Argumenty `--shard N --of M` rozkrojí seznam všech 34 reportů:
```python
REPORTS = ALL_REPORTS[SHARD - 1::OF]
```
Tj. shard 1 z 4 vezme reporty na indexech 0, 4, 8, … ; shard 2 indexy 1, 5, 9, …
Rovnoměrné rozdělení, **žádný report neudělají dva shardy**. Bez argumentů
(`--of 1`) běží serialně přes všech 34.
### 3.2 Proč nestačily 4 taby v jednom procesu
Playwright **sync API je blokující**`wait_for_*` drží jediné vlákno celou
dobu čekání, takže 4 taby v jednom sync skriptu by se stejně střídaly sériově
(čekání by se sčítala, ne překrývala). Skutečná paralelnost vyžaduje buď async
API (přepis logiky), nebo — jak je zvoleno zde — **4 nezávislé procesy**, kde
souběh zajišťuje OS.
### 3.3 Profil per shard (povinné)
Chrome zamyká adresář profilu → **dvě běžící instance nemohou sdílet jeden
profil**. Proto:
| Běh | Profil |
|---|---|
| serialní (`--of 1`) | `browser_profile/` (původní) |
| shard N (`--of M>1`) | `browser_profile_{N}/` |
### 3.4 Login (varianta B)
Session se mezi profily **nesdílí**. Každý shard se proto při prvním spuštění
**přihlásí sám** (login je jen heslem, žádné MFA). Po prvním běhu už session
zůstane uložená v `browser_profile_{N}/`, takže další běhy login přeskočí
(`is_visible()` check). Launcher startuje procesy s **rozestupem** (`STAGGER_S`),
aby se OKTA login nezahltil 4 současnými požadavky.
### 3.5 Launcher `run_test_results_parallel_v1.0.py`
- `N_SHARDS = 4` — počet souběžných procesů (oken).
- `STAGGER_S = 4` — rozestup mezi starty (s).
- Každý proces se spustí v **novém konzolovém okně** (`CREATE_NEW_CONSOLE`) →
logy se neprolínají; navíc každý log má prefix `[S{shard}/{of}]`.
- Launcher počká, až všechny dobíhnou, a vypíše souhrn (které shardy selhaly
dle `returncode`).
### 3.6 Reálné zrychlení
Ne přesně 4× — export běží na serveru a velká centra generují hodně řádků,
takže reálně spíš 23×. Server může souběžné exporty throttlovat.
---
## 4. Výstupní soubory
Formát názvu (timestamp + popisný název):
```
{YYYY-MM-DD_HHMMSS} sponsor-study-{STUDY}-test-results-{SITE_ID}-{TYP}.csv
```
`{TYP}` = `standard` nebo `microbiology`. Všechny shardy ukládají do **stejného**
adresáře `Source/`; timestamp + unikátní názvy (site/typ) zaručují, že nedojde ke
kolizi. Staré soubory se **nikdy nemažou** (verzování přes timestamp).
---
## 5. Průběh jednoho reportu (kroky + logging)
| Fáze | Co dělá |
|---|---|
| START | vypíše shard/of, profil, počet reportů; spustí prohlížeč |
| LOGIN | otevře login; **pokud je session aktivní, přihlášení přeskočí** |
| KROK 1/5 | navigace na report URL |
| KROK 2/5 | čeká na `.ag-row` **nebo** prázdný grid („No Data"); pak stabilizace počtu |
| KROK 3/5 | klik na viditelné tři tečky (`more_horiz`) → menu |
| KROK 4/5 | klik na viditelné „Export to CSV" → zachytí download |
| KROK 5/5 | uloží soubor do `OUT_DIR` |
| KONEC | souhrn `hotovo X/Y (shard N/M)` + seznam selhaných |
Log u každého reportu: `=== Centrum 930557 / microbiology (studie 36940) ===`,
prefixovaný `[S{shard}/{of}]` při paralelním běhu.
---
## 6. Klíčové technické poznatky (PROČ to tak je) — ověřeno přes Chrome MCP
Stránka test-results je **Angular SPA** s **MDL** a tabulkou **AG Grid**.
Microbiology záložka má **stejnou** strukturu jako Standard.
### 6.1 Čekání na data = řádky AG Gridu
- Data načtena, jakmile se objeví `div.ag-row` (count 0 → N).
- Řádky jsou `position-absolute` → Playwright je **nepovažuje za „visible"**:
-`wait_for_selector("div.ag-row")` (default visible) **timeoutuje**.
-`wait_for_function("() => document.querySelectorAll('div.ag-row').length > 0")`.
- Stabilizace: počet se čte co 2 s, dokud se 2× neshodne.
- **Počet `.ag-row` je zastropený** (~153) virtuálním renderem — NENÍ to skutečný
počet záznamů. **Export do CSV ale vyexportuje VŠECHNY řádky** (ověřeno: různé
velikosti CSV).
### 6.2 „Fetching Data" / spinner — NEPLATÍ pro tuto stránku
Na test-results NENÍ text „Fetching Data" (je jen na *samples* reportu).
`<loading-bar>` jen problikne → nespoléhat. Signál = `.ag-row`.
### 6.3 Tři tečky (export) — DVA `<ag-export>`
2× `<ag-export>` (1 skrytý) → filtr na viditelnost:
`page.locator("ag-export button:visible", has_text="more_horiz").first.click()`
### 6.4 „Export to CSV" — DVĚ položky v DOM
2× `mdl-menu-item` (1 skrytá) →
`page.locator("mdl-menu-item:visible", has_text="Export to CSV").first.click()`
(❌ `get_by_text` → strict mode violation.)
### 6.5 Prázdné centrum (ověřeno přes Chrome MCP na centru 930551)
- No-rows overlay s textem **„No Data"** ve `.ag-overlay-no-rows-wrapper`.
- ⚠️ Třída **NENÍ** `.ag-overlay-no-rows-center`.
- ⚠️ Text **NENÍ** v `.ag-body-viewport` (ten je prázdný, `height: 1px`).
- ⚠️ Na stránce **2 overlaye** (1 skrytý) → kontrola `offsetParent !== null`.
- Detekce (KROK 2):
```js
() => {
if (document.querySelectorAll('div.ag-row').length > 0) return false;
return [...document.querySelectorAll('.ag-overlay-no-rows-wrapper')]
.some(e => e.offsetParent !== null);
}
```
- KROK 2 čeká na `.ag-row` **NEBO** tuto detekci → prázdné centrum nečeká 120 s.
---
## 7. Login logika (sdílená napříč Covance skripty)
```python
page.goto(LOGIN_URL); page.wait_for_load_state("networkidle")
if not page.get_by_label("Email").is_visible():
return # session aktivní → login přeskočit
# jinak: Email → Next → Password → Verify → wait redirect (code= zmizí z URL)
```
`is_visible()` (ne kontrola URL — po redirectu zůstává `xsp.covance.com`).
---
## 8. Chrome flagy proti „Restore pages" / broken session
```
--disable-restore-session-state
--disable-session-crashed-bubble
```
---
## 9. Robustnost
- Každý report v `try/except` → chyba u jednoho nezastaví zbytek shardu.
- Souhrn na konci: `hotovo X/Y (shard N/M)` + `SELHALA centra: site/typ, …`.
- Launcher hlídá `returncode` každého shardu.
---
## 10. Možná budoucí rozšíření
- **Více/méně procesů:** změnit `N_SHARDS` v launcheru (profily `_1.._N`).
- **Další studie / centra / typ reportu:** přidat do `STUDIES` / `REPORT_TYPES`.
- **Vyčistit profily:** smazat `browser_profile_*/` (vynutí nový login).
---
## 11. Příbuzné skripty (stejná složka / portál)
| Skript | Co stahuje |
|---|---|
| `download_samples_report_v1.1.py` | All Samples (sampletracking) |
| `download_kit_inventory_v2.1.py` | Kit inventory |
| `download_equeries_report_v1.1.py` | eQuery reporty (zdroj SITE_IDS pro 36940) |
| `download_test_results_v1.2.py` | **tento** — Test Results (Standard + Microbiology, 2 studie, sharding) |
| `run_test_results_parallel_v1.0.py` | launcher 4 paralelních shardů test-results |
@@ -0,0 +1,199 @@
# =============================================================================
# Název: download_test_results_v1.2.py
# Verze: 1.2
# Datum: 2026-05-29
# Popis: Stahuje Test Results ze xsp.labcorp.com pro 2 studie (36940, 35472),
# oba typy reportu (Standard + Microbiology), pres vsechna centra.
# Ceka na nacteni AG Grid radku (.ag-row); prazdne centrum ('No Data')
# preskoci. Vystup: timestampovane CSV do adresare Source/.
# Zmeny v1.2: + paralelni beh pres sharding (--shard N --of M). Kazdy shard
# vezme svuj podil reportu (REPORTS[shard-1::of]) a pouziva vlastni
# profil browser_profile_{shard}, takze 4 procesy mohou bezet
# soucasne (kazdy se prihlasi do sveho profilu sam). Bez argumentu
# = puvodni serialni beh nad profilem browser_profile.
# Zmeny v1.1: + studie 35472, + report typ microbiology (driv jen 36940/standard).
# =============================================================================
from playwright.sync_api import sync_playwright
from datetime import datetime
import argparse
import os
# --- argumenty: sharding pro paralelni beh ----------------------------------
parser = argparse.ArgumentParser(description="Stahovani Test Results (XSP) s podporou shardingu.")
parser.add_argument("--shard", type=int, default=1, help="poradi tohoto shardu (1..of)")
parser.add_argument("--of", type=int, default=1, help="celkovy pocet shardu")
ARGS = parser.parse_args()
SHARD, OF = ARGS.shard, ARGS.of
TAG = f"[S{SHARD}/{OF}]" if OF > 1 else ""
def log(msg):
print(f"[{datetime.now().strftime('%H:%M:%S')}] {TAG} {msg}", flush=True)
EMAIL = "vbuzalka@its.jnj.com"
PASSWORD = "%zT3Wqfc9)cWua5"
LOGIN_URL = "https://xsp.covance.com/"
OUT_DIR = r"U:\PythonProject\Janssen\Covance_UCO3001\Source"
# Pri paralelnim behu MUSI mit kazdy shard vlastni profil (Chrome zamyka adresar
# profilu -> dve bezici instance nemohou sdilet jeden). Serialni beh (of=1)
# pouziva puvodni browser_profile.
_BASE = os.path.dirname(os.path.abspath(__file__))
PROFILE_DIR = os.path.join(_BASE, "browser_profile" if OF == 1 else f"browser_profile_{SHARD}")
# Studie + jejich interni cisla center.
# 36940 = 77242113UCO3001 (UC) — zdroj center: download_equeries_report SITES
# 35472 = druha studie (MDD)
STUDIES = [
{
"study": "36940",
"sites": [
"930551", "930556", "930525", "930549", "930543", "930547",
"930555", "930557", "930539", "930536", "930553", "930531",
],
},
{
"study": "35472",
"sites": [
"898745", "898739", "898733", "898744", "898727",
],
},
]
# Typy reportu: zalozka v URL + suffix v nazvu souboru.
REPORT_TYPES = [
{"slug": "standard-test-results", "suffix": "standard"},
{"slug": "microbiology", "suffix": "microbiology"},
]
ALL_REPORTS = [
{
"site": sid,
"study": st["study"],
"type": rt["suffix"],
"url": f"https://xsp.labcorp.com/sponsor/study/{st['study']}/test-results/{sid}/{rt['slug']}",
"filename": f"sponsor-study-{st['study']}-test-results-{sid}-{rt['suffix']}.csv",
}
for st in STUDIES
for sid in st["sites"]
for rt in REPORT_TYPES
]
# Tento shard vezme kazdy of-ty report od indexu (shard-1). Rovnomerne rozdeleni
# a zadny report neudela dva shardy zaroven.
REPORTS = ALL_REPORTS[SHARD - 1::OF]
def login(page):
log("LOGIN: otviram login stranku...")
page.goto(LOGIN_URL)
page.wait_for_load_state("networkidle")
if not page.get_by_label("Email").is_visible():
log(f"LOGIN: session uz aktivni, prihlaseni preskoceno ({page.url})")
return
log("LOGIN: zadavam email...")
page.get_by_label("Email").fill(EMAIL)
page.get_by_role("button", name="Next").click()
page.wait_for_load_state("networkidle")
log("LOGIN: zadavam heslo...")
page.get_by_label("Password").fill(PASSWORD)
page.get_by_role("button", name="Verify").click()
log("LOGIN: cekam na presmerovani po prihlaseni...")
page.wait_for_url(lambda url: "code=" not in url, timeout=60000)
page.wait_for_load_state("networkidle", timeout=60000)
page.wait_for_timeout(2000)
log(f"LOGIN: prihlaseni OK ({page.url})")
def download_report(page, report):
log(f"=== Centrum {report['site']} / {report['type']} (studie {report['study']}) ===")
log(f"KROK 1/5: navigace na report URL...")
page.goto(report["url"])
log(f"KROK 1/5: stranka nactena ({page.url})")
# Grid je AG Grid uvnitř <covance-ag-grid>. Data jsou nactena, jakmile
# se v gridu objevi radky (.ag-row jde z 0 -> N). Pockej na prvni radek
# a pak na stabilizaci poctu (proti castecnemu renderu).
log("KROK 2/5: cekam na radky gridu (.ag-row) nebo prazdny grid ('No Data')...")
# AG Grid radky jsou position-absolute (virtualni render), takze nejsou
# "visible" dle Playwrightu -> cekej na pritomnost v DOM, ne na viditelnost.
# Prazdne centrum: AG Grid vykresli no-rows overlay s textem "No Data" ve
# wrapperu .ag-overlay-no-rows-wrapper. POZOR: trida NENI -no-rows-center;
# navic jsou na strance 2 overlaye (jeden skryty) -> kontroluj viditelny
# (offsetParent != null). Detekuj, aby to u centra bez dat necekalo 120 s.
EMPTY_GRID_JS = """() => {
if (document.querySelectorAll('div.ag-row').length > 0) return false;
return [...document.querySelectorAll('.ag-overlay-no-rows-wrapper')]
.some(e => e.offsetParent !== null);
}"""
page.wait_for_function(
f"""() => document.querySelectorAll('div.ag-row').length > 0
|| ({EMPTY_GRID_JS})()""",
timeout=120000,
)
if page.evaluate(EMPTY_GRID_JS):
log("KROK 2/5: centrum bez dat ('No Data' overlay) — preskakuji export.")
return
log("KROK 2/5: radky se objevily, cekam na stabilizaci poctu...")
prev = -1
for i in range(20): # max ~40 s stabilizace
cnt = page.locator("div.ag-row").count()
log(f" ...kontrola #{i+1}: {cnt} radku")
if cnt == prev and cnt > 0:
break
prev = cnt
page.wait_for_timeout(2000)
page.wait_for_timeout(2000) # buffer
log(f"KROK 2/5: data stabilni ({prev} radku v gridu).")
# Tri tecky: na strance jsou 2x <ag-export> (jeden skryty), klikni na
# VIDITELNY more_horiz button.
log("KROK 3/5: klikam na viditelne tri tecky (more_horiz)...")
page.locator("ag-export button:visible", has_text="more_horiz").first.click()
log("KROK 3/5: menu otevreno.")
timestamp = datetime.now().strftime("%Y-%m-%d_%H%M%S")
dest = os.path.join(OUT_DIR, f"{timestamp} {report['filename']}")
log("KROK 4/5: klikam na 'Export to CSV' a cekam na stahovani...")
with page.expect_download(timeout=60000) as dl:
# 2x "Export to CSV" v DOM (jeden skryty) -> klikni na VIDITELNY
page.locator("mdl-menu-item:visible", has_text="Export to CSV").first.click()
log("KROK 4/5: stahovani zachyceno, ukladam soubor...")
dl.value.save_as(dest)
log(f"KROK 5/5: HOTOVO -> {dest}")
if __name__ == "__main__":
log(f"START: shard {SHARD}/{OF}, profil '{os.path.basename(PROFILE_DIR)}', "
f"{len(REPORTS)}/{len(ALL_REPORTS)} reportu k zpracovani.")
with sync_playwright() as p:
context = p.chromium.launch_persistent_context(
user_data_dir=PROFILE_DIR,
headless=False,
args=[
"--disable-blink-features=AutomationControlled",
"--start-maximized",
"--disable-restore-session-state",
"--disable-session-crashed-bubble",
],
no_viewport=True,
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
accept_downloads=True,
)
context.add_init_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})")
page = context.new_page()
log("START: prohlizec spusten.")
login(page)
ok, failed = 0, []
for idx, report in enumerate(REPORTS, 1):
log(f">>> Report {idx}/{len(REPORTS)}")
try:
download_report(page, report)
ok += 1
except Exception as e:
failed.append(f"{report['site']}/{report['type']}")
log(f"CHYBA u centra {report['site']}/{report['type']}: {e!r} — pokracuji dalsim.")
log(f"KONEC: hotovo {ok}/{len(REPORTS)} reportu (shard {SHARD}/{OF}).")
if failed:
log(f"KONEC: SELHALA centra: {', '.join(failed)}")
context.close()
@@ -0,0 +1,263 @@
# download_test_results_v1.3.py — dokumentace
**Verze:** 1.3 · **Datum:** 2026-05-29
**Umístění:** `U:\PythonProject\Janssen\Covance_UCO3001\download_test_results_v1.3.py`
> **Změny v1.3 oproti 1.2:** **robustní login + okno se při chybě nezavře.**
> - `login()` už **nečeká na `networkidle`** (login SPA labcorp/OKTA jí nikdy
> nedosáhne → dřív to vedlo k timeoutu a pádu procesu / „zmizelému" oknu).
> Místo toho čeká přímo na pole **Email** a **Password** přes
> `wait_for(state="visible")`.
> - Celý běh je obalen `try/except` a na konci je `input()` → **konzolové okno
> shardu se při chybě nezavře**, takže je vidět chybový log.
> - Launcher povýšen na `run_test_results_parallel_v1.1.py` (cílí na v1.3,
> `STAGGER_S = 8`).
>
> **Změny v1.2 oproti 1.1:** paralelní běh přes sharding (`--shard N --of M`).
> **Změny v1.1 oproti 1.0:** druhá studie 35472 (MDD) + report Microbiology.
---
## 1. Účel
Automatické stažení reportů **Test Results** z portálu Labcorp Sponsor Portal
(`xsp.labcorp.com`). Skript projde **2 studie × jejich centra × 2 typy reportu**,
u každého vyexportuje grid do CSV a uloží ho timestampovaný do `Source/`.
| Studie | Interní ID | Význam | Počet center |
|---|---|---|---|
| UC | `36940` | 77242113UCO3001 | 12 |
| MDD | `35472` | druhá studie | 5 |
Typy reportu: **Standard** (`/standard-test-results`) a **Microbiology** (`/microbiology`).
**Celkem: (12 + 5) × 2 = 34 reportů** (prázdná centra se přeskakují, viz 6.5).
---
## 2. Spuštění
### 2a) Paralelně (doporučeno — rychlejší)
Přes launcher, který rozjede 4 procesy (každý ve vlastním okně):
```bat
U:\PythonProject\Janssen\.venv\Scripts\python.exe ^
U:\PythonProject\Janssen\Covance_UCO3001\run_test_results_parallel_v1.1.py
```
### 2b) Serialně (jeden proces, jako dřív)
```bat
U:\PythonProject\Janssen\.venv\Scripts\python.exe ^
U:\PythonProject\Janssen\Covance_UCO3001\download_test_results_v1.3.py
```
### 2c) Jeden konkrétní shard ručně
```bat
…\python.exe download_test_results_v1.3.py --shard 2 --of 4
```
- Prohlížeč běží **viditelně** (`headless=False`), maximalizovaný.
- **Persistent profile** — session přežívá (viz sekce 4).
- Po dokončení (i po chybě) okno **čeká na Enter** — schválně, ať je vidět log.
---
## 3. Paralelní běh (sharding) — jak funguje
### 3.1 Rozdělení práce
Argumenty `--shard N --of M` rozkrojí seznam všech 34 reportů:
```python
REPORTS = ALL_REPORTS[SHARD - 1::OF]
```
Tj. shard 1 z 4 vezme reporty na indexech 0, 4, 8, … ; shard 2 indexy 1, 5, 9, …
Rovnoměrné rozdělení, **žádný report neudělají dva shardy**. Bez argumentů
(`--of 1`) běží serialně přes všech 34.
### 3.2 Proč nestačily 4 taby v jednom procesu
Playwright **sync API je blokující**`wait_for_*` drží jediné vlákno celou
dobu čekání, takže 4 taby v jednom sync skriptu by se stejně střídaly sériově.
Skutečná paralelnost vyžaduje buď async API, nebo — jak je zvoleno zde —
**4 nezávislé procesy**, kde souběh zajišťuje OS.
### 3.3 Profil per shard (povinné)
Chrome zamyká adresář profilu → **dvě běžící instance nemohou sdílet jeden
profil**. Proto:
| Běh | Profil |
|---|---|
| serialní (`--of 1`) | `browser_profile/` (původní) |
| shard N (`--of M>1`) | `browser_profile_{N}/` |
### 3.4 Login (varianta B) — POZOR na fragilitu
Session se mezi profily **nesdílí**. Každý shard se proto při prvním spuštění
**přihlásí sám** (login je jen heslem, žádné MFA). Po prvním běhu už session
zůstane uložená v `browser_profile_{N}/`, takže další běhy login přeskočí.
**Klíčová oprava ve v1.3:** login stránka labcorp/OKTA je SPA, která
**nikdy nedosáhne stavu `networkidle`** (běží tam analytika/polling). Původní
`page.wait_for_load_state("networkidle")` se proto zasekl až do timeoutu a
shard spadl ještě před vyplněním přihlášení (okno „zmizelo"). Nové `login()`:
```python
page.goto(LOGIN_URL)
try:
page.get_by_label("Email").wait_for(state="visible", timeout=12000)
except Exception:
return # Email se neobjevil -> session aktivni
page.get_by_label("Email").fill(EMAIL)
page.get_by_role("button", name="Next").click()
page.get_by_label("Password").wait_for(state="visible", timeout=30000)
page.get_by_label("Password").fill(PASSWORD)
page.get_by_role("button", name="Verify").click()
page.wait_for_url(lambda url: "code=" not in url or "xsp." in url, timeout=60000)
```
Launcher startuje procesy s rozestupem (`STAGGER_S = 8 s`), aby se OKTA login
nezahltil souběžnými požadavky.
### 3.5 Okno se při chybě nezavře
`__main__` je obalen `try/except` + `finally: input(...)`. Když shard spadne,
vypíše `FATAL: ...` + traceback a **čeká na Enter** místo okamžitého zavření
okna. Tím je vždy vidět, co se pokazilo. (Důsledek: launcher reportuje
„HOTOVO" až po zavření všech oken Enterem.)
### 3.6 Launcher `run_test_results_parallel_v1.1.py`
- `N_SHARDS = 4` — počet souběžných procesů (oken).
- `STAGGER_S = 8` — rozestup mezi starty (s).
- Každý proces v **novém konzolovém okně** (`CREATE_NEW_CONSOLE`) → logy se
neprolínají; navíc každý log má prefix `[S{shard}/{of}]`.
- Launcher počká na všechny a vypíše souhrn (které shardy selhaly dle `returncode`).
### 3.7 Reálné zrychlení
Ne přesně 4× — export běží na serveru, velká centra generují hodně řádků →
reálně spíš 23×. Server může souběžné exporty throttlovat.
---
## 4. Výstupní soubory
```
{YYYY-MM-DD_HHMMSS} sponsor-study-{STUDY}-test-results-{SITE_ID}-{TYP}.csv
```
`{TYP}` = `standard` nebo `microbiology`. Všechny shardy ukládají do **stejného**
adresáře `Source/`; timestamp + unikátní názvy (site/typ) zaručí, že nedojde ke
kolizi. Staré soubory se **nikdy nemažou**.
---
## 5. Průběh jednoho reportu (kroky + logging)
| Fáze | Co dělá |
|---|---|
| START | vypíše shard/of, profil, počet reportů; spustí prohlížeč |
| LOGIN | čeká na pole Email; pokud není → session aktivní, login přeskočí |
| KROK 1/5 | navigace na report URL |
| KROK 2/5 | čeká na `.ag-row` **nebo** prázdný grid („No Data"); pak stabilizace počtu |
| KROK 3/5 | klik na viditelné tři tečky (`more_horiz`) → menu |
| KROK 4/5 | klik na viditelné „Export to CSV" → zachytí download |
| KROK 5/5 | uloží soubor do `OUT_DIR` |
| KONEC | souhrn `hotovo X/Y (shard N/M)` + seznam selhaných; pak čeká na Enter |
---
## 6. Klíčové technické poznatky (PROČ to tak je) — ověřeno přes Chrome MCP
Stránka test-results je **Angular SPA** s **MDL** a tabulkou **AG Grid**.
Microbiology záložka má **stejnou** strukturu jako Standard.
### 6.1 Čekání na data = řádky AG Gridu
- Data načtena, jakmile se objeví `div.ag-row` (count 0 → N).
- Řádky jsou `position-absolute` → Playwright je **nepovažuje za „visible"**:
-`wait_for_selector("div.ag-row")` (default visible) **timeoutuje**.
-`wait_for_function("() => document.querySelectorAll('div.ag-row').length > 0")`.
- Stabilizace: počet se čte co 2 s, dokud se 2× neshodne.
- **Počet `.ag-row` je zastropený** (~153) virtuálním renderem — NENÍ to skutečný
počet záznamů. **Export do CSV ale vyexportuje VŠECHNY řádky.**
### 6.2 „Fetching Data" / spinner — NEPLATÍ pro tuto stránku
Na test-results NENÍ text „Fetching Data" (je jen na *samples* reportu).
`<loading-bar>` jen problikne → nespoléhat. Signál = `.ag-row`.
### 6.3 Tři tečky (export) — DVA `<ag-export>`
2× `<ag-export>` (1 skrytý) → filtr na viditelnost:
`page.locator("ag-export button:visible", has_text="more_horiz").first.click()`
### 6.4 „Export to CSV" — DVĚ položky v DOM
2× `mdl-menu-item` (1 skrytá) →
`page.locator("mdl-menu-item:visible", has_text="Export to CSV").first.click()`
### 6.5 Prázdné centrum (ověřeno přes Chrome MCP na centru 930551)
- No-rows overlay s textem **„No Data"** ve `.ag-overlay-no-rows-wrapper`.
- ⚠️ Třída **NENÍ** `.ag-overlay-no-rows-center`.
- ⚠️ Text **NENÍ** v `.ag-body-viewport` (ten je prázdný, `height: 1px`).
- ⚠️ Na stránce **2 overlaye** (1 skrytý) → kontrola `offsetParent !== null`.
- Detekce (KROK 2):
```js
() => {
if (document.querySelectorAll('div.ag-row').length > 0) return false;
return [...document.querySelectorAll('.ag-overlay-no-rows-wrapper')]
.some(e => e.offsetParent !== null);
}
```
- KROK 2 čeká na `.ag-row` **NEBO** tuto detekci → prázdné centrum nečeká 120 s.
---
## 7. Login logika (sdílená napříč Covance skripty) — v1.3
```python
page.goto(LOGIN_URL)
# NEcekat na networkidle (login SPA ji nikdy nedosahne)
try:
page.get_by_label("Email").wait_for(state="visible", timeout=12000)
except Exception:
return # session aktivni → login přeskočit
# jinak: Email → Next → cekej na Password → Password → Verify → wait redirect
```
`wait_for(state="visible")` na pole (NE `networkidle`, NE kontrola URL — po
redirectu zůstává `xsp.covance.com`).
---
## 8. Chrome flagy proti „Restore pages" / broken session
```
--disable-restore-session-state
--disable-session-crashed-bubble
```
---
## 9. Robustnost
- Každý report v `try/except` → chyba u jednoho nezastaví zbytek shardu.
- Celý běh shardu v `try/except` + `finally: input()` → okno se při pádu nezavře.
- Souhrn na konci: `hotovo X/Y (shard N/M)` + `SELHALA centra: …`.
- Launcher hlídá `returncode` každého shardu.
---
## 10. Možná budoucí rozšíření
- **Více/méně procesů:** změnit `N_SHARDS` v launcheru (profily `_1.._N`).
- **Další studie / centra / typ reportu:** přidat do `STUDIES` / `REPORT_TYPES`.
- **Vyčistit profily:** smazat `browser_profile_*/` (vynutí nový login).
---
## 11. Příbuzné skripty (stejná složka / portál)
| Skript | Co stahuje |
|---|---|
| `download_samples_report_v1.1.py` | All Samples (sampletracking) |
| `download_kit_inventory_v2.1.py` | Kit inventory |
| `download_equeries_report_v1.1.py` | eQuery reporty (zdroj SITE_IDS pro 36940) |
| `download_test_results_v1.3.py` | **tento** — Test Results (Standard + Microbiology, 2 studie, sharding) |
| `run_test_results_parallel_v1.1.py` | launcher 4 paralelních shardů test-results |
@@ -0,0 +1,233 @@
# =============================================================================
# Název: download_test_results_v1.3.py
# Verze: 1.3
# Datum: 2026-05-29
# Popis: Stahuje Test Results ze xsp.labcorp.com pro 2 studie (36940, 35472),
# oba typy reportu (Standard + Microbiology), pres vsechna centra.
# Ceka na nacteni AG Grid radku (.ag-row); prazdne centrum ('No Data')
# preskoci. Vystup: timestampovane CSV do adresare Source/.
# Zmeny v1.3: ROBUSTNI LOGIN — login() uz NEceka na 'networkidle' (login SPA
# labcorp/OKTA ji nikdy nedosahne -> drive timeout/pad procesu).
# Misto toho ceka primo na pole Email/Password pres wait_for(visible).
# Cely beh obalen try/except + na konci input() -> okno se pri chybe
# NEZAVRE, takze je videt chybovy log (driv 'zmizla' okna shardu).
# Zmeny v1.2: + paralelni beh pres sharding (--shard N --of M). Kazdy shard
# vezme svuj podil reportu (REPORTS[shard-1::of]) a pouziva vlastni
# profil browser_profile_{shard}. Bez argumentu = serialni beh.
# Zmeny v1.1: + studie 35472, + report typ microbiology (driv jen 36940/standard).
# =============================================================================
from playwright.sync_api import sync_playwright
from datetime import datetime
import argparse
import traceback
import os
# --- argumenty: sharding pro paralelni beh ----------------------------------
parser = argparse.ArgumentParser(description="Stahovani Test Results (XSP) s podporou shardingu.")
parser.add_argument("--shard", type=int, default=1, help="poradi tohoto shardu (1..of)")
parser.add_argument("--of", type=int, default=1, help="celkovy pocet shardu")
ARGS = parser.parse_args()
SHARD, OF = ARGS.shard, ARGS.of
TAG = f"[S{SHARD}/{OF}]" if OF > 1 else ""
def log(msg):
print(f"[{datetime.now().strftime('%H:%M:%S')}] {TAG} {msg}", flush=True)
EMAIL = "vbuzalka@its.jnj.com"
PASSWORD = "%zT3Wqfc9)cWua5"
LOGIN_URL = "https://xsp.covance.com/"
OUT_DIR = r"U:\PythonProject\Janssen\Covance_UCO3001\Source"
# Pri paralelnim behu MUSI mit kazdy shard vlastni profil (Chrome zamyka adresar
# profilu -> dve bezici instance nemohou sdilet jeden). Serialni beh (of=1)
# pouziva puvodni browser_profile.
_BASE = os.path.dirname(os.path.abspath(__file__))
PROFILE_DIR = os.path.join(_BASE, "browser_profile" if OF == 1 else f"browser_profile_{SHARD}")
# Studie + jejich interni cisla center.
# 36940 = 77242113UCO3001 (UC) — zdroj center: download_equeries_report SITES
# 35472 = druha studie (MDD)
STUDIES = [
{
"study": "36940",
"sites": [
"930551", "930556", "930525", "930549", "930543", "930547",
"930555", "930557", "930539", "930536", "930553", "930531",
],
},
{
"study": "35472",
"sites": [
"898745", "898739", "898733", "898744", "898727",
],
},
]
# Typy reportu: zalozka v URL + suffix v nazvu souboru.
REPORT_TYPES = [
{"slug": "standard-test-results", "suffix": "standard"},
{"slug": "microbiology", "suffix": "microbiology"},
]
ALL_REPORTS = [
{
"site": sid,
"study": st["study"],
"type": rt["suffix"],
"url": f"https://xsp.labcorp.com/sponsor/study/{st['study']}/test-results/{sid}/{rt['slug']}",
"filename": f"sponsor-study-{st['study']}-test-results-{sid}-{rt['suffix']}.csv",
}
for st in STUDIES
for sid in st["sites"]
for rt in REPORT_TYPES
]
# Tento shard vezme kazdy of-ty report od indexu (shard-1). Rovnomerne rozdeleni
# a zadny report neudela dva shardy zaroven.
REPORTS = ALL_REPORTS[SHARD - 1::OF]
def login(page):
log("LOGIN: otviram login stranku...")
page.goto(LOGIN_URL)
# NEcekat na 'networkidle' — login SPA (labcorp/OKTA) ji nikdy nedosahne
# (analytika/polling bezi porad) -> drive to vedlo k timeoutu a padu procesu.
# Misto toho cekame primo na pole Email. Pokud se do 12 s neobjevi,
# povazujeme session za aktivni (uz prihlaseno).
try:
page.get_by_label("Email").wait_for(state="visible", timeout=12000)
except Exception:
log(f"LOGIN: Email pole se neobjevilo -> session aktivni, login preskocen ({page.url})")
return
log("LOGIN: zadavam email...")
page.get_by_label("Email").fill(EMAIL)
page.get_by_role("button", name="Next").click()
log("LOGIN: cekam na pole pro heslo...")
page.get_by_label("Password").wait_for(state="visible", timeout=30000)
log("LOGIN: zadavam heslo...")
page.get_by_label("Password").fill(PASSWORD)
page.get_by_role("button", name="Verify").click()
log("LOGIN: cekam na presmerovani po prihlaseni...")
try:
# Po prihlaseni se v URL objevi 'code=' (OAuth redirect) a pak zmizi
# az dorazime do aplikace. Bereme i pripad, ze uz jsme na xsp domene.
page.wait_for_url(
lambda url: "code=" not in url or "xsp." in url,
timeout=60000,
)
except Exception:
log("LOGIN: wait_for_url vyprsel, pokracuji (overim pristup pri 1. reportu).")
page.wait_for_timeout(3000)
log(f"LOGIN: prihlaseni hotovo ({page.url})")
def download_report(page, report):
log(f"=== Centrum {report['site']} / {report['type']} (studie {report['study']}) ===")
log(f"KROK 1/5: navigace na report URL...")
page.goto(report["url"])
log(f"KROK 1/5: stranka nactena ({page.url})")
# Grid je AG Grid uvnitř <covance-ag-grid>. Data jsou nactena, jakmile
# se v gridu objevi radky (.ag-row jde z 0 -> N). Pockej na prvni radek
# a pak na stabilizaci poctu (proti castecnemu renderu).
log("KROK 2/5: cekam na radky gridu (.ag-row) nebo prazdny grid ('No Data')...")
# AG Grid radky jsou position-absolute (virtualni render), takze nejsou
# "visible" dle Playwrightu -> cekej na pritomnost v DOM, ne na viditelnost.
# Prazdne centrum: AG Grid vykresli no-rows overlay s textem "No Data" ve
# wrapperu .ag-overlay-no-rows-wrapper. POZOR: trida NENI -no-rows-center;
# navic jsou na strance 2 overlaye (jeden skryty) -> kontroluj viditelny
# (offsetParent != null). Detekuj, aby to u centra bez dat necekalo 120 s.
EMPTY_GRID_JS = """() => {
if (document.querySelectorAll('div.ag-row').length > 0) return false;
return [...document.querySelectorAll('.ag-overlay-no-rows-wrapper')]
.some(e => e.offsetParent !== null);
}"""
page.wait_for_function(
f"""() => document.querySelectorAll('div.ag-row').length > 0
|| ({EMPTY_GRID_JS})()""",
timeout=120000,
)
if page.evaluate(EMPTY_GRID_JS):
log("KROK 2/5: centrum bez dat ('No Data' overlay) — preskakuji export.")
return
log("KROK 2/5: radky se objevily, cekam na stabilizaci poctu...")
prev = -1
for i in range(20): # max ~40 s stabilizace
cnt = page.locator("div.ag-row").count()
log(f" ...kontrola #{i+1}: {cnt} radku")
if cnt == prev and cnt > 0:
break
prev = cnt
page.wait_for_timeout(2000)
page.wait_for_timeout(2000) # buffer
log(f"KROK 2/5: data stabilni ({prev} radku v gridu).")
# Tri tecky: na strance jsou 2x <ag-export> (jeden skryty), klikni na
# VIDITELNY more_horiz button.
log("KROK 3/5: klikam na viditelne tri tecky (more_horiz)...")
page.locator("ag-export button:visible", has_text="more_horiz").first.click()
log("KROK 3/5: menu otevreno.")
timestamp = datetime.now().strftime("%Y-%m-%d_%H%M%S")
dest = os.path.join(OUT_DIR, f"{timestamp} {report['filename']}")
log("KROK 4/5: klikam na 'Export to CSV' a cekam na stahovani...")
with page.expect_download(timeout=60000) as dl:
# 2x "Export to CSV" v DOM (jeden skryty) -> klikni na VIDITELNY
page.locator("mdl-menu-item:visible", has_text="Export to CSV").first.click()
log("KROK 4/5: stahovani zachyceno, ukladam soubor...")
dl.value.save_as(dest)
log(f"KROK 5/5: HOTOVO -> {dest}")
def main():
log(f"START: shard {SHARD}/{OF}, profil '{os.path.basename(PROFILE_DIR)}', "
f"{len(REPORTS)}/{len(ALL_REPORTS)} reportu k zpracovani.")
with sync_playwright() as p:
context = p.chromium.launch_persistent_context(
user_data_dir=PROFILE_DIR,
headless=False,
args=[
"--disable-blink-features=AutomationControlled",
"--start-maximized",
"--disable-restore-session-state",
"--disable-session-crashed-bubble",
],
no_viewport=True,
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
accept_downloads=True,
)
context.add_init_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})")
page = context.new_page()
log("START: prohlizec spusten.")
login(page)
ok, failed = 0, []
for idx, report in enumerate(REPORTS, 1):
log(f">>> Report {idx}/{len(REPORTS)}")
try:
download_report(page, report)
ok += 1
except Exception as e:
failed.append(f"{report['site']}/{report['type']}")
log(f"CHYBA u centra {report['site']}/{report['type']}: {e!r} — pokracuji dalsim.")
log(f"KONEC: hotovo {ok}/{len(REPORTS)} reportu (shard {SHARD}/{OF}).")
if failed:
log(f"KONEC: SELHALA centra: {', '.join(failed)}")
context.close()
if __name__ == "__main__":
try:
main()
except Exception as e:
log(f"FATAL: beh shardu spadl: {e!r}")
traceback.print_exc()
finally:
# Nech okno otevrene, at je videt log/chyba (driv okno pri padu zmizelo).
try:
input("\n[Enter] pro zavreni tohoto okna...")
except EOFError:
pass
@@ -0,0 +1,54 @@
# =============================================================================
# Název: run_test_results_parallel_v1.0.py
# Verze: 1.0
# Datum: 2026-05-29
# Popis: Launcher pro paralelni stahovani Test Results. Spusti N procesu
# download_test_results_v1.2.py, kazdy s --shard i --of N, kazdy
# ve vlastnim konzolovem okne (CREATE_NEW_CONSOLE) a s vlastnim
# profilem browser_profile_{i}. Pocka, az vsechny dobehnou.
# Pri variante B se kazdy proces pri prvnim startu prihlasi sam
# (login jen heslem) a session si ulozi do sveho profilu.
# =============================================================================
import subprocess
import sys
import os
import time
N_SHARDS = 4 # pocet soubeznych procesu (oken)
STAGGER_S = 4 # rozestup mezi starty (s) — at se OKTA login
# nezahlti 4 soucasnymi pozadavky najednou
BASE = os.path.dirname(os.path.abspath(__file__))
SCRIPT = os.path.join(BASE, "download_test_results_v1.2.py")
PYEXE = sys.executable # stejny interpreter (.venv), kterym byl spusten launcher
# Vlastni konzolove okno pro kazdy proces (jen Windows) -> logy se neprolinaji.
CREATE_NEW_CONSOLE = getattr(subprocess, "CREATE_NEW_CONSOLE", 0)
def main():
print(f"Launcher: spoustim {N_SHARDS} shardu skriptu {os.path.basename(SCRIPT)}")
procs = []
for shard in range(1, N_SHARDS + 1):
cmd = [PYEXE, SCRIPT, "--shard", str(shard), "--of", str(N_SHARDS)]
print(f" -> shard {shard}/{N_SHARDS}: {' '.join(cmd)}")
p = subprocess.Popen(cmd, cwd=BASE, creationflags=CREATE_NEW_CONSOLE)
procs.append((shard, p))
if shard < N_SHARDS and STAGGER_S:
time.sleep(STAGGER_S) # rozestup startu (login)
print("Launcher: vsechny shardy spusteny, cekam na dokonceni...")
rc = {}
for shard, p in procs:
p.wait()
rc[shard] = p.returncode
print(f"Launcher: shard {shard} skoncil (returncode={p.returncode}).")
failed = [s for s, code in rc.items() if code != 0]
if failed:
print(f"Launcher: HOTOVO, ale shardy {failed} skoncily s chybou (returncode != 0).")
else:
print("Launcher: HOTOVO — vsechny shardy uspesne dokonceny.")
if __name__ == "__main__":
main()
@@ -0,0 +1,57 @@
# =============================================================================
# Název: run_test_results_parallel_v1.1.py
# Verze: 1.1
# Datum: 2026-05-29
# Popis: Launcher pro paralelni stahovani Test Results. Spusti N procesu
# download_test_results_v1.3.py, kazdy s --shard i --of N, kazdy
# ve vlastnim konzolovem okne (CREATE_NEW_CONSOLE) a s vlastnim
# profilem browser_profile_{i}. Pocka, az vsechny dobehnou.
# Pri variante B se kazdy proces pri prvnim startu prihlasi sam
# (login jen heslem) a session si ulozi do sveho profilu.
# Zmeny v1.1: cili na download_test_results_v1.3.py (robustni login + okno se
# pri chybe nezavre). STAGGER_S zvysen na 8 s, aby se 4 soubezne
# OKTA loginy nezahltily.
# =============================================================================
import subprocess
import sys
import os
import time
N_SHARDS = 4 # pocet soubeznych procesu (oken)
STAGGER_S = 8 # rozestup mezi starty (s) — at se OKTA login
# nezahlti soucasnymi pozadavky najednou
BASE = os.path.dirname(os.path.abspath(__file__))
SCRIPT = os.path.join(BASE, "download_test_results_v1.3.py")
PYEXE = sys.executable # stejny interpreter (.venv), kterym byl spusten launcher
# Vlastni konzolove okno pro kazdy proces (jen Windows) -> logy se neprolinaji.
CREATE_NEW_CONSOLE = getattr(subprocess, "CREATE_NEW_CONSOLE", 0)
def main():
print(f"Launcher: spoustim {N_SHARDS} shardu skriptu {os.path.basename(SCRIPT)}")
procs = []
for shard in range(1, N_SHARDS + 1):
cmd = [PYEXE, SCRIPT, "--shard", str(shard), "--of", str(N_SHARDS)]
print(f" -> shard {shard}/{N_SHARDS}: {' '.join(cmd)}")
p = subprocess.Popen(cmd, cwd=BASE, creationflags=CREATE_NEW_CONSOLE)
procs.append((shard, p))
if shard < N_SHARDS and STAGGER_S:
time.sleep(STAGGER_S) # rozestup startu (login)
print("Launcher: vsechny shardy spusteny, cekam na dokonceni...")
rc = {}
for shard, p in procs:
p.wait()
rc[shard] = p.returncode
print(f"Launcher: shard {shard} skoncil (returncode={p.returncode}).")
failed = [s for s, code in rc.items() if code != 0]
if failed:
print(f"Launcher: HOTOVO, ale shardy {failed} skoncily s chybou (returncode != 0).")
else:
print("Launcher: HOTOVO — vsechny shardy uspesne dokonceny.")
if __name__ == "__main__":
main()