# 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íš 2–3×. 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 |
