z230
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user