This commit is contained in:
2026-05-29 13:37:54 +02:00
parent f02d6dcb8a
commit 3b4af21abf
187 changed files with 15646 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()