This commit is contained in:
2026-05-29 13:37:26 +02:00
parent d290be0f6b
commit f02d6dcb8a
80 changed files with 107 additions and 1090 deletions
-402
View File
@@ -1,402 +0,0 @@
"""
create_report.py
Verze: 1.2
Datum: 2026-05-27
Generuje Excel report (.xlsm) pro studii 77242113UCO3001 z MongoDB databáze Clario.
Výstup: U:/Dropbox/!!!Days/Downloads Z230/YYYY-MM-DD 77242113UCO3001 Clario Reports.xlsm
Listy:
MayoScore — jeden řádek = pacient × visit; řádky I-0 s Modified Mayo < 5 červeně tučně
MayoDiary — jeden řádek = denní záznam deníku pacienta
EligibleDays — jeden řádek = jeden eligible day z MayoScore obohacený o data z MayoDiary;
included/excluded flag, excluded dny šedě na žlutém pozadí
VBA makro (Worksheet_SelectionChange na listu MayoScore):
Klik na řádek → automaticky přepne na EligibleDays a vyfiltruje záznamy
pro daného pacienta a visit. Vyžaduje povolení maker při otevření souboru.
"""
from datetime import datetime
from pathlib import Path
from pymongo import MongoClient
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
import xlwings as xw
# ---------------------------------------------------------------------------
# Konfigurace
# ---------------------------------------------------------------------------
MONGO_URI = "mongodb://192.168.1.76:27017"
DB_NAME = "Clario"
OUTPUT_DIR = Path(r"U:\Dropbox\!!!Days\Downloads Z230")
VISIT_ORDER = ["I-0", "I-2", "I-4", "I-8", "I-12"]
COLUMNS_SCORE = [
("Site", lambda d: d.get("site", {}).get("name", "")),
("Subject ID", lambda d: d.get("subject", {}).get("id", "")),
("Visit", lambda d: d["fields"].get("Visit", "")),
("Visit Date", lambda d: d["fields"].get("Visit Date", "")),
("Baseline Stool Frequency", lambda d: _num(d["fields"].get("Baseline Stool Frequency", ""))),
("Central Endoscopy Score", lambda d: _num(d["fields"].get("Central Endoscopy Score", ""))),
("PGA Score", lambda d: _num(d["fields"].get("PGA Score", ""))),
("Stool Frequency Sub-score", lambda d: _num(d["fields"].get("Stool Frequency Sub-score", ""))),
("Rectal Bleeding Sub-score", lambda d: _num(d["fields"].get("Rectal Bleeding Sub-score", ""))),
("Partial Mayo Score", lambda d: _num(d["fields"].get("Partial Mayo Score", ""))),
("Modified Mayo Score", lambda d: _num(d["fields"].get("Modified Mayo Score", ""))),
("Full Mayo Score", lambda d: _num(d["fields"].get("Full Mayo Score", ""))),
]
COLUMNS_DIARY = [
("Subject ID", lambda d: d.get("subject", {}).get("id", "")),
("Report Date", lambda d: d["fields"].get("Report Date", "")),
("Baseline Stool Count", lambda d: _num(d["fields"].get("Baseline Stool Count", ""))),
("Stool Frequency", lambda d: _num(d["fields"].get("Stool Frequency", ""))),
("MAYO050", lambda d: d["fields"].get("MAYO050", "")),
("Not Applicable", lambda d: d["fields"].get("Not Applicable", "")),
("Constipation", lambda d: d["fields"].get("Constipation", "")),
("Diarrhea", lambda d: d["fields"].get("Diarrhea", "")),
("Irregularity", lambda d: d["fields"].get("Irregularity", "")),
]
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _num(value):
"""Převede číselný string na int, jinak vrátí původní hodnotu nebo None."""
if value == "" or value is None:
return None
try:
return int(value)
except (ValueError, TypeError):
try:
return float(value)
except (ValueError, TypeError):
return value
def _visit_sort_key(doc):
visit = doc["fields"].get("Visit", "")
try:
idx = VISIT_ORDER.index(visit)
except ValueError:
idx = len(VISIT_ORDER)
return (doc.get("site", {}).get("name", ""), doc.get("subject", {}).get("id", ""), idx, visit)
def _iso_to_date(value):
"""ISO string → Python date pro Excel."""
if not isinstance(value, str):
return value
try:
return datetime.fromisoformat(value).date()
except ValueError:
return value
# ---------------------------------------------------------------------------
# Styly
# ---------------------------------------------------------------------------
HEADER_FILL = PatternFill("solid", fgColor="1F497D")
HEADER_FONT = Font(bold=True, color="FFFFFF", size=10)
CELL_FONT = Font(size=10)
ALIGN_CTR = Alignment(horizontal="center", vertical="center", wrap_text=False)
ALIGN_LEFT = Alignment(horizontal="left", vertical="center")
THIN = Side(style="thin", color="BFBFBF")
BORDER = Border(left=THIN, right=THIN, top=THIN, bottom=THIN)
# zebra
FILL_ODD = PatternFill("solid", fgColor="FFFFFF")
FILL_EVEN = PatternFill("solid", fgColor="EBF1DE")
SCORE_COLS = {"Partial Mayo Score", "Modified Mayo Score", "Full Mayo Score"}
SCORE_FILL = PatternFill("solid", fgColor="FFC7CE") # červená pro skóre ≥ 5 (placeholder — nepoužíváme podmíněné formátování)
# ---------------------------------------------------------------------------
# Sestavení sheetu
# ---------------------------------------------------------------------------
def _build_sheet(ws, docs, columns, date_cols, center_cols, col_widths, row_font_fn=None):
headers = [c[0] for c in columns]
for col_idx, header in enumerate(headers, 1):
cell = ws.cell(row=1, column=col_idx, value=header)
cell.font = HEADER_FONT
cell.fill = HEADER_FILL
cell.alignment = ALIGN_CTR
cell.border = BORDER
ws.row_dimensions[1].height = 28
for row_idx, doc in enumerate(docs, 2):
fill = FILL_EVEN if row_idx % 2 == 0 else FILL_ODD
font = row_font_fn(doc) if row_font_fn else CELL_FONT
for col_idx, (col_name, getter) in enumerate(columns, 1):
value = getter(doc)
if col_name in date_cols and isinstance(value, str):
value = _iso_to_date(value)
cell = ws.cell(row=row_idx, column=col_idx, value=value)
cell.font = font
cell.fill = fill
cell.border = BORDER
cell.alignment = ALIGN_CTR if col_name in center_cols else ALIGN_LEFT
for col_idx, (col_name, _) in enumerate(columns, 1):
ws.column_dimensions[get_column_letter(col_idx)].width = col_widths.get(col_name, 14)
for col_name in date_cols:
if col_name in headers:
letter = get_column_letter(headers.index(col_name) + 1)
for row_idx in range(2, len(docs) + 2):
ws[f"{letter}{row_idx}"].number_format = "DD-MMM-YYYY"
ws.freeze_panes = "A2"
ws.auto_filter.ref = f"A1:{get_column_letter(len(headers))}1"
def _score_row_font(doc):
visit = doc["fields"].get("Visit", "")
try:
mod_mayo = int(doc["fields"].get("Modified Mayo Score", ""))
except (ValueError, TypeError):
mod_mayo = None
if visit == "I-0" and mod_mayo is not None and mod_mayo < 5:
return Font(size=10, bold=True, color="FF0000")
return CELL_FONT
def build_mayo_score_sheet(ws, docs):
_build_sheet(
ws, docs, COLUMNS_SCORE,
date_cols={"Visit Date"},
center_cols={"Visit", "Central Endoscopy Score", "PGA Score",
"Stool Frequency Sub-score", "Rectal Bleeding Sub-score",
"Partial Mayo Score", "Modified Mayo Score", "Full Mayo Score",
"Baseline Stool Frequency"},
col_widths={
"Site": 18, "Subject ID": 16, "Visit": 12, "Visit Date": 14,
"Baseline Stool Frequency": 14, "Central Endoscopy Score": 14,
"PGA Score": 10, "Stool Frequency Sub-score": 14,
"Rectal Bleeding Sub-score": 14, "Partial Mayo Score": 14,
"Modified Mayo Score": 14, "Full Mayo Score": 13,
},
row_font_fn=_score_row_font,
)
def build_mayo_diary_sheet(ws, docs):
_build_sheet(
ws, docs, COLUMNS_DIARY,
date_cols={"Report Date"},
center_cols={"Baseline Stool Count", "Stool Frequency", "Not Applicable",
"Constipation", "Diarrhea", "Irregularity"},
col_widths={
"Subject ID": 16, "Report Date": 14, "Baseline Stool Count": 14,
"Stool Frequency": 14, "MAYO050": 48, "Not Applicable": 14,
"Constipation": 14, "Diarrhea": 12, "Irregularity": 14,
},
)
def build_eligible_days_sheet(ws, score_docs, diary_docs):
# Lookup diary records by (subject_id, date_part YYYY-MM-DD)
diary_lookup: dict[tuple, dict] = {}
for d in diary_docs:
subj = d.get("subject", {}).get("id", "")
date_iso = d["fields"].get("Report Date", "")
date_part = date_iso[:10] if date_iso else ""
if subj and date_part:
diary_lookup[(subj, date_part)] = d
headers = [
"Included", "Subject ID", "Visit", "Visit Date", "Day",
"Report Date", "Baseline Stool Count", "Stool Frequency",
"MAYO050", "Not Applicable", "Constipation", "Diarrhea", "Irregularity",
]
col_widths = {
"Included": 10, "Subject ID": 16, "Visit": 10, "Visit Date": 14, "Day": 8,
"Report Date": 14, "Baseline Stool Count": 14, "Stool Frequency": 14,
"MAYO050": 48, "Not Applicable": 14, "Constipation": 14,
"Diarrhea": 12, "Irregularity": 14,
}
center_cols = {"Included", "Visit", "Day", "Baseline Stool Count", "Stool Frequency",
"Not Applicable", "Constipation", "Diarrhea", "Irregularity"}
date_cols = {"Visit Date", "Report Date"}
no_fill = PatternFill("solid", fgColor="FFF2CC") # žlutá pro excluded dny
for col_idx, header in enumerate(headers, 1):
cell = ws.cell(row=1, column=col_idx, value=header)
cell.font = HEADER_FONT
cell.fill = HEADER_FILL
cell.alignment = ALIGN_CTR
cell.border = BORDER
ws.row_dimensions[1].height = 28
row_idx = 2
for score_doc in score_docs:
subj = score_doc.get("subject", {}).get("id", "")
visit = score_doc["fields"].get("Visit", "")
visit_date = score_doc["fields"].get("Visit Date", "")
for n in range(1, 11):
day_date_iso = score_doc["fields"].get(f"Eligible Day (-{n})")
if not day_date_iso or day_date_iso == "-":
continue
date_part = day_date_iso[:10]
excl_reason = score_doc["fields"].get(f"Day (-{n}) Excluded Reason(s)", "")
included = "No" if excl_reason and excl_reason != "-" else "Yes"
diary = diary_lookup.get((subj, date_part), {})
df = diary.get("fields", {})
fill = no_fill if included == "No" else (FILL_EVEN if row_idx % 2 == 0 else FILL_ODD)
font = Font(size=10, color="808080") if included == "No" else CELL_FONT
values = [
included,
subj,
visit,
_iso_to_date(visit_date) if isinstance(visit_date, str) else visit_date,
f"-{n}",
_iso_to_date(day_date_iso),
_num(df.get("Baseline Stool Count", "")),
_num(df.get("Stool Frequency", "")),
df.get("MAYO050", ""),
df.get("Not Applicable", ""),
df.get("Constipation", ""),
df.get("Diarrhea", ""),
df.get("Irregularity", ""),
]
for col_idx, (header, value) in enumerate(zip(headers, values), 1):
cell = ws.cell(row=row_idx, column=col_idx, value=value)
cell.font = font
cell.fill = fill
cell.border = BORDER
if header in date_cols:
cell.number_format = "DD-MMM-YYYY"
cell.alignment = ALIGN_CTR if header in center_cols else ALIGN_LEFT
row_idx += 1
for col_idx, header in enumerate(headers, 1):
ws.column_dimensions[get_column_letter(col_idx)].width = col_widths.get(header, 14)
ws.freeze_panes = "A2"
ws.auto_filter.ref = f"A1:{get_column_letter(len(headers))}1"
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=5000)
client.admin.command("ping")
db = client[DB_NAME]
score_docs = list(db["Clario.MayoScore"].find({}))
diary_docs = list(db["Clario.MayoDiary"].find({}))
client.close()
score_docs.sort(key=_visit_sort_key)
diary_docs.sort(key=lambda d: (
d.get("subject", {}).get("id", ""),
d["fields"].get("Report Date", ""),
))
wb = Workbook()
ws_score = wb.active
ws_score.title = "MayoScore"
build_mayo_score_sheet(ws_score, score_docs)
ws_diary = wb.create_sheet("MayoDiary")
build_mayo_diary_sheet(ws_diary, diary_docs)
ws_days = wb.create_sheet("EligibleDays")
build_eligible_days_sheet(ws_days, score_docs, diary_docs)
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
today = datetime.now().strftime("%Y-%m-%d")
filename = f"{today} 77242113UCO3001 Clario Reports.xlsx"
output_path = OUTPUT_DIR / filename
# Uložit jako .xlsx nejdřív, pak přepsat na .xlsm přes xlwings + injektovat VBA
xlsx_path = output_path.with_suffix(".xlsx")
xlsm_path = output_path.with_suffix(".xlsm")
wb.save(str(xlsx_path))
inject_vba(xlsx_path, xlsm_path)
xlsx_path.unlink(missing_ok=True)
print(f"Uloženo: {xlsm_path}")
print(f"MayoScore: {len(score_docs)} záznamů")
print(f"MayoDiary: {len(diary_docs)} záznamů")
print(f"EligibleDays: generováno z {len(score_docs)} score záznamů")
def inject_vba(xlsx_path: Path, xlsm_path: Path) -> None:
vba_code = '''\
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
If Target.Row < 2 Or Target.Column < 1 Then Exit Sub
If Target.Rows.Count > 1 Then Exit Sub
Dim subjectId As String
Dim visit As String
subjectId = CStr(Me.Cells(Target.Row, 2).Value)
visit = CStr(Me.Cells(Target.Row, 3).Value)
If subjectId = "" Or visit = "" Then Exit Sub
Dim ws As Worksheet
On Error Resume Next
Set ws = ThisWorkbook.Sheets("EligibleDays")
On Error GoTo 0
If ws Is Nothing Then Exit Sub
Application.ScreenUpdating = False
ws.AutoFilterMode = False
ws.Range("A1").AutoFilter
ws.Range("A1").AutoFilter Field:=2, Criteria1:=subjectId
ws.Range("A1").AutoFilter Field:=3, Criteria1:=visit
ws.Activate
ws.Range("A2").Select
Application.ScreenUpdating = True
End Sub
'''
app = xw.App(visible=False)
try:
wb = app.books.open(str(xlsx_path))
# Najdi VBComponent odpovídající listu "MayoScore" podle tab názvu
vb_comp = None
for comp in wb.api.VBProject.VBComponents:
if comp.Type == 100: # xlSheet
try:
if comp.Properties("Name").Value == "MayoScore":
vb_comp = comp
break
except Exception:
pass
if vb_comp is None:
# fallback: první sheet (Sheet1)
vb_comp = wb.api.VBProject.VBComponents("Sheet1")
vb_comp.CodeModule.AddFromString(vba_code)
wb.api.SaveAs(str(xlsm_path), FileFormat=52) # 52 = xlOpenXMLWorkbookMacroEnabled
wb.close()
finally:
app.quit()
if __name__ == "__main__":
main()
Binary file not shown.
@@ -1,3 +1,3 @@
2026/05/28-09:34:01.521 26f4 Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Extension State/MANIFEST-000001
2026/05/28-09:34:01.522 26f4 Recovering log #3
2026/05/28-09:34:01.522 26f4 Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Extension State/000003.log
2026/05/29-13:24:16.539 6b34 Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Extension State/MANIFEST-000001
2026/05/29-13:24:16.540 6b34 Recovering log #3
2026/05/29-13:24:16.541 6b34 Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Extension State/000003.log
@@ -1,3 +1,3 @@
2026/05/28-09:20:54.095 55ec Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Extension State/MANIFEST-000001
2026/05/28-09:20:54.096 55ec Recovering log #3
2026/05/28-09:20:54.096 55ec Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Extension State/000003.log
2026/05/29-13:17:37.742 5acc Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Extension State/MANIFEST-000001
2026/05/29-13:17:37.742 5acc Recovering log #3
2026/05/29-13:17:37.743 5acc Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Extension State/000003.log
Binary file not shown.
@@ -1,3 +1,3 @@
2026/05/28-09:34:11.834 4160 Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\GCM Store/MANIFEST-000001
2026/05/28-09:34:11.835 4160 Recovering log #3
2026/05/28-09:34:11.836 4160 Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\GCM Store/000003.log
2026/05/29-13:24:32.087 1144 Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\GCM Store/MANIFEST-000001
2026/05/29-13:24:32.087 1144 Recovering log #3
2026/05/29-13:24:32.087 1144 Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\GCM Store/000003.log
@@ -1,3 +1,3 @@
2026/05/28-09:21:04.862 2184 Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\GCM Store/MANIFEST-000001
2026/05/28-09:21:04.864 2184 Recovering log #3
2026/05/28-09:21:04.865 2184 Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\GCM Store/000003.log
2026/05/29-13:17:47.687 5428 Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\GCM Store/MANIFEST-000001
2026/05/29-13:17:47.696 5428 Recovering log #3
2026/05/29-13:17:47.696 5428 Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\GCM Store/000003.log
Binary file not shown.
@@ -1,3 +1,3 @@
2026/05/28-08:18:49.515 3070 Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\IndexedDB\https_xsp.covance.com_0.indexeddb.leveldb/MANIFEST-000001
2026/05/28-08:18:49.515 3070 Recovering log #3
2026/05/28-08:18:49.515 3070 Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\IndexedDB\https_xsp.covance.com_0.indexeddb.leveldb/000003.log
2026/05/29-11:20:05.334 264 Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\IndexedDB\https_xsp.covance.com_0.indexeddb.leveldb/MANIFEST-000001
2026/05/29-11:20:05.335 264 Recovering log #3
2026/05/29-11:20:05.335 264 Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\IndexedDB\https_xsp.covance.com_0.indexeddb.leveldb/000003.log
@@ -1,3 +1,3 @@
2026/05/28-08:03:00.480 474c Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\IndexedDB\https_xsp.covance.com_0.indexeddb.leveldb/MANIFEST-000001
2026/05/28-08:03:00.481 474c Recovering log #3
2026/05/28-08:03:00.481 474c Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\IndexedDB\https_xsp.covance.com_0.indexeddb.leveldb/000003.log
2026/05/29-11:17:21.700 5a0c Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\IndexedDB\https_xsp.covance.com_0.indexeddb.leveldb/MANIFEST-000001
2026/05/29-11:17:21.700 5a0c Recovering log #3
2026/05/29-11:17:21.700 5a0c Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\IndexedDB\https_xsp.covance.com_0.indexeddb.leveldb/000003.log
@@ -1,6 +1,6 @@
2026/05/28-09:34:01.216 1690 Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Local Storage\leveldb/MANIFEST-000001
2026/05/28-09:34:01.223 1690 Recovering log #106
2026/05/28-09:34:01.225 1690 Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Local Storage\leveldb/000106.log
2026/05/28-09:34:15.864 8d0 Level-0 table #110: started
2026/05/28-09:34:15.951 8d0 Level-0 table #110: 24294 bytes OK
2026/05/28-09:34:16.039 8d0 Delete type=0 #106
2026/05/29-13:24:16.084 988 Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Local Storage\leveldb/MANIFEST-000001
2026/05/29-13:24:16.091 988 Recovering log #160
2026/05/29-13:24:16.093 988 Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Local Storage\leveldb/000160.log
2026/05/29-13:24:33.012 6ac4 Level-0 table #165: started
2026/05/29-13:24:33.073 6ac4 Level-0 table #165: 20511 bytes OK
2026/05/29-13:24:33.150 6ac4 Delete type=0 #160
@@ -1,3 +1,15 @@
2026/05/28-09:20:53.814 57f0 Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Local Storage\leveldb/MANIFEST-000001
2026/05/28-09:20:53.820 57f0 Recovering log #106
2026/05/28-09:20:53.822 57f0 Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Local Storage\leveldb/000106.log
2026/05/29-13:17:37.366 6a60 Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Local Storage\leveldb/MANIFEST-000001
2026/05/29-13:17:37.373 6a60 Recovering log #157
2026/05/29-13:17:37.375 6a60 Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Local Storage\leveldb/000157.log
2026/05/29-13:17:51.674 6264 Level-0 table #161: started
2026/05/29-13:17:51.748 6264 Level-0 table #161: 19877 bytes OK
2026/05/29-13:17:51.815 6264 Delete type=0 #157
2026/05/29-13:17:51.816 6264 Compacting 4@0 + 1@1 files
2026/05/29-13:17:51.881 6264 Generated table #162@0: 24 keys, 30021 bytes
2026/05/29-13:17:51.882 6264 Compacted 4@0 + 1@1 files => 30021 bytes
2026/05/29-13:17:51.914 6264 compacted to: files[ 0 1 1 0 0 0 0 ]
2026/05/29-13:17:51.915 6264 Delete type=2 #149
2026/05/29-13:17:51.915 6264 Delete type=2 #152
2026/05/29-13:17:51.915 6264 Delete type=2 #155
2026/05/29-13:17:51.915 6264 Delete type=2 #158
2026/05/29-13:17:51.915 6264 Delete type=2 #161
@@ -1 +1 @@
{"net":{"http_server_properties":{"servers":[{"anonymization":["GAAAABMAAABodHRwczovL2NvdmFuY2UuY29tAA==",false,0],"server":"https://login.labcorp.com","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2NvdmFuY2UuY29tAA==",false,0],"server":"https://cdn.pendo.io","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2NvdmFuY2UuY29tAA==",false,0],"server":"https://data.pendo.io","supports_spdy":true},{"alternative_service":[{"advertised_alpns":["h3"],"expiration":"13427014730141327","port":443,"protocol_str":"quic"}],"anonymization":["GAAAABMAAABodHRwczovL2NvdmFuY2UuY29tAA==",false,0],"network_stats":{"srtt":7242},"server":"https://content-autofill.googleapis.com","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2NvdmFuY2UuY29tAA==",true,0],"server":"https://login.labcorp.com","supports_spdy":true},{"alternative_service":[{"advertised_alpns":["h3"],"expiration":"13427019241807994","port":443,"protocol_str":"quic"}],"anonymization":["GAAAABIAAABodHRwczovL2dvb2dsZS5jb20AAA==",false,0],"network_stats":{"srtt":14394},"server":"https://accounts.google.com","supports_spdy":true},{"alternative_service":[{"advertised_alpns":["h3"],"expiration":"13424513642283952","port":443,"protocol_str":"quic"}],"anonymization":["GAAAABMAAABodHRwczovL2NvdmFuY2UuY29tAA==",false,0],"network_stats":{"srtt":3782},"server":"https://unpkg.com"},{"alternative_service":[{"advertised_alpns":["h3"],"expiration":"13427019251909434","port":443,"protocol_str":"quic"}],"anonymization":["GAAAABIAAABodHRwczovL2dvb2dsZS5jb20AAA==",false,0],"network_stats":{"srtt":8718},"server":"https://android.clients.google.com","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2xhYmNvcnAuY29tAA==",false,0],"network_stats":{"srtt":3267},"server":"https://fonts.gstatic.com"},{"alternative_service":[{"advertised_alpns":["h3"],"expiration":"13427012816734610","port":443,"protocol_str":"quic"}],"anonymization":["GAAAABMAAABodHRwczovL2xhYmNvcnAuY29tAA==",false,0],"network_stats":{"srtt":6914},"server":"https://fonts.googleapis.com","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2xhYmNvcnAuY29tAA==",false,0],"server":"https://login.labcorp.com","supports_spdy":true},{"alternative_service":[{"advertised_alpns":["h3"],"expiration":"13427019284329367","port":443,"protocol_str":"quic"}],"anonymization":["GAAAABMAAABodHRwczovL2xhYmNvcnAuY29tAA==",false,0],"network_stats":{"srtt":7280},"server":"https://content-autofill.googleapis.com","supports_spdy":true},{"alternative_service":[{"advertised_alpns":["h3"],"expiration":"13427019266698306","port":443,"protocol_str":"quic"}],"anonymization":["MAAAACwAAABodHRwczovL3Bhc3N3b3Jkc2xlYWtjaGVjay1wYS5nb29nbGVhcGlzLmNvbQ==",false,0],"network_stats":{"srtt":3684},"server":"https://passwordsleakcheck-pa.googleapis.com","supports_spdy":true},{"alternative_service":[{"advertised_alpns":["h3"],"expiration":"13424513657366598","port":443,"protocol_str":"quic"}],"anonymization":["GAAAABMAAABodHRwczovL2xhYmNvcnAuY29tAA==",false,0],"network_stats":{"srtt":6122},"server":"https://unpkg.com"},{"alternative_service":[{"advertised_alpns":["h3"],"expiration":"13427019241781650","port":443,"protocol_str":"quic"}],"anonymization":["GAAAABIAAABodHRwczovL2dvb2dsZS5jb20AAA==",false,0],"network_stats":{"srtt":5175},"server":"https://www.google.com","supports_spdy":true}],"supports_quic":{"address":"192.168.1.87","used_quic":true},"version":5},"network_qualities":{"CAESABiAgICA+P////8B":"4G"}}}
{"net":{"http_server_properties":{"servers":[{"anonymization":["GAAAABMAAABodHRwczovL2NvdmFuY2UuY29tAA==",false,0],"server":"https://login.labcorp.com","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2NvdmFuY2UuY29tAA==",false,0],"server":"https://cdn.pendo.io","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2NvdmFuY2UuY29tAA==",false,0],"server":"https://data.pendo.io","supports_spdy":true},{"alternative_service":[{"advertised_alpns":["h3"],"expiration":"13427112006055382","port":443,"protocol_str":"quic"}],"anonymization":["GAAAABMAAABodHRwczovL2NvdmFuY2UuY29tAA==",false,0],"network_stats":{"srtt":6621},"server":"https://content-autofill.googleapis.com","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2NvdmFuY2UuY29tAA==",true,0],"server":"https://login.labcorp.com","supports_spdy":true},{"alternative_service":[{"advertised_alpns":["h3"],"expiration":"13427119456814562","port":443,"protocol_str":"quic"}],"anonymization":["GAAAABIAAABodHRwczovL2dvb2dsZS5jb20AAA==",false,0],"network_stats":{"srtt":10524},"server":"https://accounts.google.com","supports_spdy":true},{"alternative_service":[{"advertised_alpns":["h3"],"expiration":"13424613857546976","port":443,"protocol_str":"quic"}],"anonymization":["GAAAABMAAABodHRwczovL2NvdmFuY2UuY29tAA==",false,0],"network_stats":{"srtt":5917},"server":"https://unpkg.com","supports_spdy":true},{"alternative_service":[{"advertised_alpns":["h3"],"expiration":"13427107435975955","port":443,"protocol_str":"quic"}],"anonymization":["GAAAABMAAABodHRwczovL2xhYmNvcnAuY29tAA==",false,0],"network_stats":{"srtt":6306},"server":"https://fonts.googleapis.com","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2xhYmNvcnAuY29tAA==",false,0],"network_stats":{"srtt":6502},"server":"https://fonts.gstatic.com"},{"alternative_service":[{"advertised_alpns":["h3"],"expiration":"13427119484319015","port":443,"protocol_str":"quic"}],"anonymization":["MAAAACwAAABodHRwczovL3Bhc3N3b3Jkc2xlYWtjaGVjay1wYS5nb29nbGVhcGlzLmNvbQ==",false,0],"network_stats":{"srtt":4832},"server":"https://passwordsleakcheck-pa.googleapis.com","supports_spdy":true},{"alternative_service":[{"advertised_alpns":["h3"],"expiration":"13427119496814314","port":443,"protocol_str":"quic"}],"anonymization":["GAAAABIAAABodHRwczovL2dvb2dsZS5jb20AAA==",false,0],"network_stats":{"srtt":7750},"server":"https://android.clients.google.com","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2xhYmNvcnAuY29tAA==",false,0],"server":"https://login.labcorp.com","supports_spdy":true},{"alternative_service":[{"advertised_alpns":["h3"],"expiration":"13427119978225009","port":443,"protocol_str":"quic"}],"anonymization":["GAAAABMAAABodHRwczovL2xhYmNvcnAuY29tAA==",false,0],"network_stats":{"srtt":6055},"server":"https://content-autofill.googleapis.com","supports_spdy":true},{"alternative_service":[{"advertised_alpns":["h3"],"expiration":"13424614330059042","port":443,"protocol_str":"quic"}],"anonymization":["GAAAABMAAABodHRwczovL2xhYmNvcnAuY29tAA==",false,0],"network_stats":{"srtt":6720},"server":"https://unpkg.com"},{"alternative_service":[{"advertised_alpns":["h3"],"expiration":"13427119456718085","port":443,"protocol_str":"quic"}],"anonymization":["GAAAABIAAABodHRwczovL2dvb2dsZS5jb20AAA==",false,0],"network_stats":{"srtt":6576},"server":"https://www.google.com","supports_spdy":true}],"supports_quic":{"address":"192.168.1.87","used_quic":true},"version":5},"network_qualities":{"CAESABiAgICA+P////8B":"4G"}}}
@@ -1 +1 @@
{"sts":[{"expiry":1811489682.936012,"host":"AMsYuZ7IFgNWVHHQOkqKt9MmwK65kaqBH0lsKFOUFH8=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1779953682.936014},{"expiry":1811489656.619357,"host":"CSChKrlj3luqm4YaUcREYcNSGviDSaiJLYYv+tJJLQY=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1779953656.619369},{"expiry":1811485129.395782,"host":"Cz1hgA9AsWBjlwLNwma1z9V2NS3WBGpE+hAisJYAzJg=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1779949129.395788},{"expiry":1811485129.780754,"host":"bwzZzlAVpSjLBr+9/WzKm/QotRxOYnqfzWRcWGuF4/M=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1779949129.780757},{"expiry":1811489684.552903,"host":"eS7TxKKR6KM2rUe5fGp4j6+eHcckwVadZAWT/TaU2RQ=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1779953684.552906},{"expiry":1811489657.426032,"host":"e3SziuwfuO2UvuBno+qkR1ObHAzZmSUoJhrc7dbP1Uo=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1779953657.426039},{"expiry":1811483216.73492,"host":"nAuqgR4iEWti7SOdT3UHPl6rmZU/DeaIm38P2O2OkgA=","mode":"force-https","sts_include_subdomains":false,"sts_observed":1779947216.734924},{"expiry":1810638866.791394,"host":"rBPMDlZ3oSoS5nWA5b3qh68smMftsPTTcEHPHa/8Asc=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1779102866.791399},{"expiry":1811489641.782165,"host":"5EdUoB7YUY9zZV+2DkgVXgho8WUvp+D+6KpeUOhNQIM=","mode":"force-https","sts_include_subdomains":false,"sts_observed":1779953641.782169},{"expiry":1811489641.808069,"host":"8/RrMmQlCD2Gsp14wUCE1P8r7B2C5+yE0+g79IPyRsc=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1779953641.808072}],"version":2}
{"sts":[{"expiry":1811590376.982667,"host":"AMsYuZ7IFgNWVHHQOkqKt9MmwK65kaqBH0lsKFOUFH8=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1780054376.982671},{"expiry":1811589873.806784,"host":"CSChKrlj3luqm4YaUcREYcNSGviDSaiJLYYv+tJJLQY=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1780053873.80679},{"expiry":1811581653.541007,"host":"Cz1hgA9AsWBjlwLNwma1z9V2NS3WBGpE+hAisJYAzJg=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1780045653.54101},{"expiry":1811582405.562701,"host":"bwzZzlAVpSjLBr+9/WzKm/QotRxOYnqfzWRcWGuF4/M=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1780046405.562703},{"expiry":1811590379.118582,"host":"eS7TxKKR6KM2rUe5fGp4j6+eHcckwVadZAWT/TaU2RQ=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1780054379.118586},{"expiry":1811590330.059279,"host":"e3SziuwfuO2UvuBno+qkR1ObHAzZmSUoJhrc7dbP1Uo=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1780054330.059282},{"expiry":1811577835.976147,"host":"nAuqgR4iEWti7SOdT3UHPl6rmZU/DeaIm38P2O2OkgA=","mode":"force-https","sts_include_subdomains":false,"sts_observed":1780041835.97615},{"expiry":1810638866.791394,"host":"rBPMDlZ3oSoS5nWA5b3qh68smMftsPTTcEHPHa/8Asc=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1779102866.791399},{"expiry":1811589856.718672,"host":"5EdUoB7YUY9zZV+2DkgVXgho8WUvp+D+6KpeUOhNQIM=","mode":"force-https","sts_include_subdomains":false,"sts_observed":1780053856.718678},{"expiry":1811589856.814671,"host":"8/RrMmQlCD2Gsp14wUCE1P8r7B2C5+yE0+g79IPyRsc=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1780053856.814676}],"version":2}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,3 +1,3 @@
2026/05/28-09:34:01.390 1690 Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Session Storage/MANIFEST-000001
2026/05/28-09:34:01.393 1690 Recovering log #4
2026/05/28-09:34:01.411 1690 Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Session Storage/000004.log
2026/05/29-13:24:16.313 988 Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Session Storage/MANIFEST-000001
2026/05/29-13:24:16.314 988 Recovering log #7
2026/05/29-13:24:16.318 988 Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Session Storage/000007.log
@@ -1,3 +1,3 @@
2026/05/28-09:20:53.976 57f0 Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Session Storage/MANIFEST-000001
2026/05/28-09:20:53.978 57f0 Recovering log #4
2026/05/28-09:20:53.995 57f0 Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Session Storage/000004.log
2026/05/29-13:17:37.578 6a60 Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Session Storage/MANIFEST-000001
2026/05/29-13:17:37.580 6a60 Recovering log #7
2026/05/29-13:17:37.611 6a60 Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Session Storage/000007.log
@@ -1,3 +1,3 @@
2026/05/28-09:34:01.159 3804 Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Site Characteristics Database/MANIFEST-000001
2026/05/28-09:34:01.161 3804 Recovering log #3
2026/05/28-09:34:01.161 3804 Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Site Characteristics Database/000003.log
2026/05/29-13:24:16.027 3a94 Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Site Characteristics Database/MANIFEST-000001
2026/05/29-13:24:16.029 3a94 Recovering log #3
2026/05/29-13:24:16.030 3a94 Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Site Characteristics Database/000003.log
@@ -1,3 +1,3 @@
2026/05/28-09:20:53.754 56d0 Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Site Characteristics Database/MANIFEST-000001
2026/05/28-09:20:53.757 56d0 Recovering log #3
2026/05/28-09:20:53.758 56d0 Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Site Characteristics Database/000003.log
2026/05/29-13:17:37.311 662c Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Site Characteristics Database/MANIFEST-000001
2026/05/29-13:17:37.316 662c Recovering log #3
2026/05/29-13:17:37.316 662c Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Site Characteristics Database/000003.log
@@ -1,3 +1,3 @@
2026/05/28-09:34:01.147 5b28 Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Sync Data\LevelDB/MANIFEST-000001
2026/05/28-09:34:01.153 5b28 Recovering log #3
2026/05/28-09:34:01.153 5b28 Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Sync Data\LevelDB/000003.log
2026/05/29-13:24:16.017 6b88 Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Sync Data\LevelDB/MANIFEST-000001
2026/05/29-13:24:16.020 6b88 Recovering log #3
2026/05/29-13:24:16.021 6b88 Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Sync Data\LevelDB/000003.log
@@ -1,3 +1,3 @@
2026/05/28-09:20:53.750 28cc Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Sync Data\LevelDB/MANIFEST-000001
2026/05/28-09:20:53.756 28cc Recovering log #3
2026/05/28-09:20:53.757 28cc Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Sync Data\LevelDB/000003.log
2026/05/29-13:17:37.299 6564 Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Sync Data\LevelDB/MANIFEST-000001
2026/05/29-13:17:37.306 6564 Recovering log #3
2026/05/29-13:17:37.306 6564 Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\Sync Data\LevelDB/000003.log
Binary file not shown.
Binary file not shown.
@@ -1 +1 @@
v10*κE0Iω4ΗΎη4"7>ΑXιzE@κ‰Λ[ρ­σίΟ‡!,σΥ³ξxΉ#PΎ“ρτΟΣπ
v10Œ%4Â{}x|P„Ì1dlðá[5²ilpÒ(÷ZON~ éøBîžl\Û´²ÂD”°²%¥Ðɺê»
@@ -1,3 +1,3 @@
2026/05/28-09:34:01.405 26f4 Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\shared_proto_db/MANIFEST-000001
2026/05/28-09:34:01.406 26f4 Recovering log #3
2026/05/28-09:34:01.408 26f4 Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\shared_proto_db/000003.log
2026/05/29-13:24:16.339 6b88 Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\shared_proto_db/MANIFEST-000001
2026/05/29-13:24:16.339 6b88 Recovering log #3
2026/05/29-13:24:16.341 6b88 Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\shared_proto_db/000003.log
@@ -1,3 +1,3 @@
2026/05/28-09:20:53.995 28cc Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\shared_proto_db/MANIFEST-000001
2026/05/28-09:20:53.996 28cc Recovering log #3
2026/05/28-09:20:53.997 28cc Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\shared_proto_db/000003.log
2026/05/29-13:17:37.597 5d84 Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\shared_proto_db/MANIFEST-000001
2026/05/29-13:17:37.598 5d84 Recovering log #3
2026/05/29-13:17:37.600 5d84 Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\shared_proto_db/000003.log
@@ -1,3 +1,3 @@
2026/05/28-09:34:01.398 26f4 Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\shared_proto_db\metadata/MANIFEST-000001
2026/05/28-09:34:01.399 26f4 Recovering log #3
2026/05/28-09:34:01.399 26f4 Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\shared_proto_db\metadata/000003.log
2026/05/29-13:24:16.334 6b88 Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\shared_proto_db\metadata/MANIFEST-000001
2026/05/29-13:24:16.334 6b88 Recovering log #3
2026/05/29-13:24:16.335 6b88 Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\shared_proto_db\metadata/000003.log
@@ -1,3 +1,3 @@
2026/05/28-09:20:53.987 28cc Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\shared_proto_db\metadata/MANIFEST-000001
2026/05/28-09:20:53.988 28cc Recovering log #3
2026/05/28-09:20:53.988 28cc Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\shared_proto_db\metadata/000003.log
2026/05/29-13:17:37.591 5d84 Reusing MANIFEST U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\shared_proto_db\metadata/MANIFEST-000001
2026/05/29-13:17:37.592 5d84 Recovering log #3
2026/05/29-13:17:37.592 5d84 Reusing old log U:\PythonProject\Janssen\Covance_UCO3001\browser_profile\Default\shared_proto_db\metadata/000003.log
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
{"autofill":{"ablation_seed":"Pts3KboF9Gg="},"breadcrumbs":{"enabled":false,"enabled_time":"13423576449583774"},"browser":{"shortcut_migration_version":"145.0.7632.6","whats_new":{"enabled_order":["ReadAnythingReadAloud","SideBySide","PdfInk2"]}},"chrome_labs_activation_threshold":89,"chrome_labs_new_badge_dict":{},"hardware_acceleration_mode_previous":true,"legacy":{"profile":{"name":{"migrated":true}}},"local":{"password_hash_data_list":[]},"management":{"platform":{"azure_active_directory":8,"enterprise_mdm_win":0}},"network_time":{"network_time_mapping":{"local":1.77994815531412e+12,"network":1.779948155236e+12,"ticks":4838234648.0,"uncertainty":10134021.0}},"os_crypt":{"audit_enabled":true,"encrypted_key":"RFBBUEkBAAAA0Iyd3wEV0RGMegDAT8KX6wEAAADkezkwH4wQT6pFnkj5e+nkEAAAADQAAABHAG8AbwBnAGwAZQAgAEMAaAByAG8AbQBlACAAZgBvAHIAIABUAGUAcwB0AGkAbgBnAAAAA2YAAMAAAAAQAAAAD8GdBnyhPqKO+3clLuoubgAAAAAEgAAAoAAAABAAAADMD85LpYqkKeWfmsz19kEEKAAAADmxXqsmFE4qCIFVf7fbf6Lkktqe+GmWXWIQTJb2EHCeDDclpUygwFUUAAAAxudMuaV2vqrqS22DRgyN4Bw1/aI="},"performance_intervention":{"last_daily_sample":"13424420803777120"},"policy":{"last_statistics_update":"13424420803455233"},"profile":{"info_cache":{"Default":{"active_time":1779952853.947476,"avatar_icon":"chrome://theme/IDR_PROFILE_AVATAR_26","background_apps":false,"default_avatar_fill_color":-2890755,"default_avatar_stroke_color":-16166200,"enterprise_label":"","force_signin_profile_locked":false,"gaia_given_name":"","gaia_id":"","gaia_name":"","hosted_domain":"","is_consented_primary_account":false,"is_ephemeral":false,"is_glic_eligible":false,"is_managed":0,"is_using_default_avatar":true,"is_using_default_name":true,"managed_user_id":"","metrics_bucket_index":1,"name":"Your Chromium","profile_color_seed":-16033840,"profile_highlight_color":-2890755,"signin.with_credential_provider":false,"user_name":""}},"last_active_profiles":["Default"],"metrics":{"next_bucket_index":2},"profile_counts_reported":"13424420803460889","profiles_order":["Default"]},"profile_network_context_service":{"http_cache_finch_experiment_groups":"None None None None"},"session_id_generator_last_value":"1074085646","signin":{"active_accounts_last_emitted":"13424420803387923"},"subresource_filter":{"ruleset_version":{"checksum":0,"content":"","format":0}},"tab_stats":{"discards_external":0,"discards_frozen":0,"discards_proactive":0,"discards_suggested":0,"discards_urgent":0,"last_daily_sample":"13424420803441717","max_tabs_per_window":2,"reloads_external":0,"reloads_frozen":0,"reloads_proactive":0,"reloads_suggested":0,"reloads_urgent":0,"total_tab_count_max":2,"window_count_max":1},"toast":{"non_milestone_update_toast_version":"145.0.7632.6"},"ukm":{"persisted_logs":[]},"uninstall_metrics":{"installation_date2":"1779102849"},"user_experience_metrics":{"client_id2":"43a4d29b-dfa2-4f8b-9a46-fe2351fed503","client_id_timestamp":"1779102849","limited_entropy_randomization_source":"795E664817D8383D9992183FB246CB96","log_record_id":29,"low_entropy_source3":4488,"machine_id":6709466,"pseudo_low_entropy_source":1702,"session_id":28,"stability":{"browser_last_live_timestamp":"13424427286217033","exited_cleanly":true,"saved_system_profile":"CLzkm8sGEhUxNDUuMC43NjMyLjYtNjQtZGV2ZWwYsOKr0AYiBWVuLVVTKhgKCldpbmRvd3MgTlQSCjEwLjAuMTkwNDQyfgoGeDg2XzY0EMb9ARiAgNT7sP8fIhlIUCBaMjMwIFRvd2VyIFdvcmtzdGF0aW9uKAMwgBQ4oAtCCggAEAAaADIAOgBNiU3PQlXibM9CZQAAwD9qGAoMR2VudWluZUludGVsEMONDBgIIAEoAIIBAIoBAKoBBng4Nl82NLABAUoKDW0jOl4V0IbiWUoKDZK3V7MV3xdKP0oKDQUO8PQVgI19ylAAaggIABAAOABAAIABsOKr0AaYAQD4AYgjgAL///////////8BiAIAkgIkNDNhNGQyOWItZGZhMi00ZjhiLTlhNDYtZmUyMzUxZmVkNTAzqAKmDbICoAFkHjnSnstQPTfirX7FiAAqfsiwbNubM6kLj70v2MAtsf5bz4dGdDQ67motfyp+LMUZDNsJTLRUE0n9H/9UH43yOKroA8plfhtjdx59eUuqaR4MRo3j+04u52w1YviiiC9v2UFh7INtJFaBCRbIQET+tRCH0tbGCqYb9QXxOQvdoidEg0ktvAubY+tt3VOfvs9ZduhzQuDj64SLuP9M8w1d8QKiJMcGz0wjig==","saved_system_profile_hash":"D84CCF82A85C1BE2C9DA241E7E58399ECE4C5019","stats_buildtime":"1768354364","stats_version":"145.0.7632.6-64-devel","system_crash_count":0}},"variations_google_groups":{"Default":[]},"was":{"restarted":false}}
{"autofill":{"ablation_seed":"Pts3KboF9Gg="},"breadcrumbs":{"enabled":false,"enabled_time":"13423576449583774"},"browser":{"shortcut_migration_version":"145.0.7632.6","whats_new":{"enabled_order":["ReadAnythingReadAloud","SideBySide","PdfInk2"]}},"chrome_labs_activation_threshold":89,"chrome_labs_new_badge_dict":{},"hardware_acceleration_mode_previous":true,"legacy":{"profile":{"name":{"migrated":true}}},"local":{"password_hash_data_list":[]},"management":{"platform":{"azure_active_directory":8,"enterprise_mdm_win":0}},"network_time":{"network_time_mapping":{"local":1.780042333499841e+12,"network":1.780042333448e+12,"ticks":8161390296.0,"uncertainty":10134152.0}},"os_crypt":{"audit_enabled":true,"encrypted_key":"RFBBUEkBAAAA0Iyd3wEV0RGMegDAT8KX6wEAAADkezkwH4wQT6pFnkj5e+nkEAAAADQAAABHAG8AbwBnAGwAZQAgAEMAaAByAG8AbQBlACAAZgBvAHIAIABUAGUAcwB0AGkAbgBnAAAAA2YAAMAAAAAQAAAAD8GdBnyhPqKO+3clLuoubgAAAAAEgAAAoAAAABAAAADMD85LpYqkKeWfmsz19kEEKAAAADmxXqsmFE4qCIFVf7fbf6Lkktqe+GmWXWIQTJb2EHCeDDclpUygwFUUAAAAxudMuaV2vqrqS22DRgyN4Bw1/aI="},"performance_intervention":{"last_daily_sample":"13424515424338664"},"policy":{"last_statistics_update":"13424515424054832"},"profile":{"info_cache":{"Default":{"active_time":1780050927.615976,"avatar_icon":"chrome://theme/IDR_PROFILE_AVATAR_26","background_apps":false,"default_avatar_fill_color":-2890755,"default_avatar_stroke_color":-16166200,"enterprise_label":"","force_signin_profile_locked":false,"gaia_given_name":"","gaia_id":"","gaia_name":"","hosted_domain":"","is_consented_primary_account":false,"is_ephemeral":false,"is_glic_eligible":false,"is_managed":0,"is_using_default_avatar":true,"is_using_default_name":true,"managed_user_id":"","metrics_bucket_index":1,"name":"Your Chromium","profile_color_seed":-16033840,"profile_highlight_color":-2890755,"signin.with_credential_provider":false,"user_name":""}},"last_active_profiles":[],"metrics":{"next_bucket_index":2},"profile_counts_reported":"13424515424059085","profiles_order":["Default"]},"profile_network_context_service":{"http_cache_finch_experiment_groups":"None None None None"},"session_id_generator_last_value":"1074088497","signin":{"active_accounts_last_emitted":"13424515423988667"},"subresource_filter":{"ruleset_version":{"checksum":0,"content":"","format":0}},"tab_stats":{"discards_external":0,"discards_frozen":0,"discards_proactive":0,"discards_suggested":0,"discards_urgent":0,"last_daily_sample":"13424515424040457","max_tabs_per_window":2,"reloads_external":0,"reloads_frozen":0,"reloads_proactive":0,"reloads_suggested":0,"reloads_urgent":0,"total_tab_count_max":2,"window_count_max":1},"toast":{"non_milestone_update_toast_version":"145.0.7632.6"},"ukm":{"persisted_logs":[]},"uninstall_metrics":{"installation_date2":"1779102849"},"user_experience_metrics":{"client_id2":"43a4d29b-dfa2-4f8b-9a46-fe2351fed503","client_id_timestamp":"1779102849","limited_entropy_randomization_source":"795E664817D8383D9992183FB246CB96","log_record_id":50,"low_entropy_source3":4488,"machine_id":6709466,"pseudo_low_entropy_source":1702,"session_id":49,"stability":{"browser_last_live_timestamp":"13424527980612017","exited_cleanly":true,"saved_system_profile":"CLzkm8sGEhUxNDUuMC43NjMyLjYtNjQtZGV2ZWwYsOKr0AYiBWVuLVVTKhgKCldpbmRvd3MgTlQSCjEwLjAuMTkwNDQyfgoGeDg2XzY0EMb9ARiAgJiqpv8fIhlIUCBaMjMwIFRvd2VyIFdvcmtzdGF0aW9uKAMwgBQ4oAtCCggAEAAaADIAOgBNiU3PQlXibM9CZQAAwD9qGAoMR2VudWluZUludGVsEMONDBgIIAEoAIIBAIoBAKoBBng4Nl82NLABAUoKDW0jOl4V0IbiWUoKDZK3V7MV3xdKP0oKDQUO8PQVgI19ylAAaggIABAAOABAAIABsOKr0AaYAQD4AYgjgAL///////////8BiAIAkgIkNDNhNGQyOWItZGZhMi00ZjhiLTlhNDYtZmUyMzUxZmVkNTAzqAKmDbICqAFkHjnSnstQPTfirX7FiAAqfsiwbNubM6kLj70v2MAtsf5bz4dGdDQ67motfyp+LMUZDNsJTLRUE0n9H/9UH43yOKroA8plfhtjdx59eUuqaR4MRo2A+Qiq4/tOLsZ/zmTnbDVi+KKIL2/ZQWHsg20kVoEJFshARP61EIfS1sYKphv1BfE5C92iJ0SDSS28C5tj623dU5++z1l26HNC4OPrhIu4/0zzDV3xAlCbVVVSD3qO","saved_system_profile_hash":"9DC8C008783143F7F046BE60F9A2BA31B59BCA3C","stats_buildtime":"1768354364","stats_version":"145.0.7632.6-64-devel","system_crash_count":0}},"variations_google_groups":{"Default":[]},"was":{"restarted":false}}
Binary file not shown.
Binary file not shown.
-525
View File
@@ -1,525 +0,0 @@
import glob
import os
import shutil
import pandas as pd
from openpyxl import load_workbook
from openpyxl.styles import Font, PatternFill, Border, Side, Alignment
from openpyxl.utils import get_column_letter
from datetime import date, datetime
# Paths
src_dir = os.path.dirname(os.path.abspath(__file__)) + "/"
out_dir = "U:/Dropbox/!!!Days/Downloads Z230/"
# Find source files
src_files = glob.glob(src_dir + "Protocol 77242113UCO3001 - All Samples*.xlsx")
assert src_files, "Source file not found!"
src_file = src_files[0]
print(f"Source xlsx: {src_file}")
csv_files = glob.glob(src_dir + "_EDCStdRpt-DataListing.csv")
assert csv_files, "CSV file not found!"
csv_file = csv_files[0]
print(f"Source csv: {csv_file}")
kit_csv_files = glob.glob(src_dir + "sponsor-study-36940-kit-inventory-on-hand-expiration.csv")
assert kit_csv_files, "Kit inventory CSV not found!"
kit_csv_file = kit_csv_files[0]
print(f"Kit csv: {kit_csv_file}")
eq_csv_files = glob.glob(src_dir + "sponsor-study-36940-activity-reports-documents-equery.csv")
assert eq_csv_files, "eQuery CSV not found!"
eq_csv_file = eq_csv_files[0]
print(f"eQuery csv: {eq_csv_file}")
timestamp = datetime.now().strftime("%Y-%m-%d_%H%M%S")
out_filename = f"{timestamp} 77242113UCO3001 CZE Labcorp samples and kit inventory report.xlsx"
out_path = out_dir + out_filename
# Copy source file to output — preserves all formatting perfectly
shutil.copy2(src_file, out_path)
# Load data with pandas for analysis
df = pd.read_excel(src_file, sheet_name=0, header=0)
# All unique patients
all_patients = sorted(df['Patient No.'].dropna().unique())
# BXSCR and DNA rows
bxscr = df[df['Protocol Visit Code'] == 'BXSCR']
dna = df[df['Protocol Visit Code'] == 'DNA']
# Parse date value to datetime object
def fmt_date(val):
if pd.isna(val):
return None
if isinstance(val, str):
return datetime.strptime(val, '%d-%b-%Y')
return pd.to_datetime(val).to_pydatetime()
OK_STATUSES = {'Received', 'In Inventory', 'Shipped'}
def get_specimen_info(visit_df, patient, specimen_type=None):
rows = visit_df[visit_df['Patient No.'] == patient]
if specimen_type:
rows = rows[rows['Specimen Type'] == specimen_type]
rows = rows[rows['Sample Status'].isin(OK_STATUSES)]
if rows.empty:
return '', None
row = rows.iloc[0]
return fmt_date(row['Container Receipt Date']), rows.index[0] + 2
def get_label_info(patient, label_code, visit_code):
rows = df[(df['Patient No.'] == patient) &
(df['Protocol Visit Code'] == visit_code) &
(df['Container Label Line 1'] == label_code)]
rows = rows[rows['Sample Status'].isin(OK_STATUSES)]
if rows.empty:
return '', None
row = rows.iloc[0]
return fmt_date(row['Container Receipt Date']), rows.index[0] + 2
# Open copied workbook and add analysis sheet
out_wb = load_workbook(out_path)
# Rename and autofit first sheet
src_ws = out_wb.active
src_ws.title = "Zdroj"
for col in src_ws.columns:
max_len = max((len(str(cell.value)) if cell.value is not None else 0) for cell in col)
src_ws.column_dimensions[get_column_letter(col[0].column)].width = min(max_len + 2, 50)
# ── Styly ────────────────────────────────────────────────────────────────────
thin = Side(style='thin')
border = Border(left=thin, right=thin, top=thin, bottom=thin)
header_fill = PatternFill("solid", fgColor="4472C4")
header_font = Font(name='Calibri', bold=True, size=11, color="FFFFFF")
data_font = Font(name='Calibri', size=11)
date_font_link = Font(name='Calibri', size=11, color="000000", underline='single')
yes_fill = PatternFill("solid", fgColor="E2EFDA")
no_fill = PatternFill("solid", fgColor="FFE7E7")
sum_header_font = Font(name='Calibri', bold=True, size=11, color="000000")
sum_total_font = Font(name='Calibri', bold=True, size=11)
zero_font = Font(name='Calibri', size=11, color="BFBFBF")
zero_red_font = Font(name='Calibri', size=11, color="C00000")
dark_blue_fill = PatternFill("solid", fgColor="203764")
orange_fill = PatternFill("solid", fgColor="FFF2CC")
green_fill = PatternFill("solid", fgColor="E2EFDA")
total_fill = PatternFill("solid", fgColor="D9E1F2")
exp_fill = PatternFill("solid", fgColor="FFE7E7")
ok_fill = PatternFill("solid", fgColor="E2EFDA")
# ── List: Přehled vzorků ──────────────────────────────────────────────────────
analysis_ws = out_wb.create_sheet("Přehled vzorků")
columns = [
("Investigator Name", 24),
("Číslo pacienta", 20),
("Máme biopsii SM11", 20),
("Máme RNA", 16),
("Máme Cryostor", 16),
("DNA", 14),
("PLASMPK I-0 TROUGH", 18),
("PLASMA PK I-0 PEAK", 18),
("SERUM ADA I-0 PRE", 18),
("SM06/SERUM BIOM", 16),
("SM07/WB RNA", 14),
("SM10/FECAL", 14),
("PLASMPK I-2 TROUGH", 18),
("PLASMA PK I-2 PEAK", 18),
("SERUM ADA I-2 PRE", 18),
("STOOL I-2", 12),
("PLASMPK I-4 TROUGH", 18),
("PLASMA PK I-4 PEAK", 18),
("SERUM ADA I-4 PRE", 18),
("SM06/SERUM BIOM", 16),
("SM07/WB RNA", 14),
("STOOL I-4", 12),
]
group_font = Font(name='Calibri', bold=True, size=11)
group_fill = PatternFill("solid", fgColor="FFFFFF")
group_border = Border(left=thin, right=thin, top=thin, bottom=thin)
groups = [
(3, 5, "SCREENING"),
(7, 12, "RANDOMIZACE I-0"),
(13, 16, "I-2"),
(17, 22, "I-4"),
]
for start_col, end_col, label in groups:
analysis_ws.merge_cells(start_row=1, start_column=start_col, end_row=1, end_column=end_col)
cell = analysis_ws.cell(row=1, column=start_col, value=label)
cell.font = group_font
cell.fill = group_fill
cell.alignment = Alignment(horizontal='center', vertical='center')
cell.border = group_border
for c in range(start_col, end_col + 1):
analysis_ws.cell(row=1, column=c).border = group_border
analysis_ws.row_dimensions[1].height = 20
for col_idx, (hdr, width) in enumerate(columns, 1):
cell = analysis_ws.cell(row=2, column=col_idx, value=hdr)
cell.font = header_font
cell.fill = header_fill
cell.border = border
cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
analysis_ws.column_dimensions[get_column_letter(col_idx)].width = width
analysis_ws.row_dimensions[2].height = 30
analysis_ws.freeze_panes = "C3"
src_sheet_name = out_wb.sheetnames[0]
pat_sheet_name = "Seznam pacientů"
_csv_df_pre = pd.read_csv(csv_file, encoding='utf-8')
_pat_pre = _csv_df_pre[['SiteNumber', 'Subject', 'Field4Value']].copy()
_pat_pre['Field4Value'] = _pat_pre['Field4Value'].apply(lambda v: datetime.strptime(str(v).strip(), '%d %b %Y') if pd.notna(v) else None)
_pat_pre = _pat_pre.sort_values(['SiteNumber', 'Subject', 'Field4Value']).reset_index(drop=True)
patient_row_map = {}
for i, row in _pat_pre.iterrows():
pat = row['Subject']
if pat not in patient_row_map:
patient_row_map[pat] = i + 2
bxscr_patients = sorted(bxscr['Patient No.'].dropna().unique())
for row_idx, patient in enumerate(bxscr_patients, 3):
investigator = bxscr[bxscr['Patient No.'] == patient].iloc[0]['Investigator Name']
sm11, sm11_row = get_specimen_info(bxscr, patient, 'Tissue , Paraffin Block')
rna, rna_row = get_specimen_info(bxscr, patient, 'Biopsy RNA Later')
cryo, cryo_row = get_specimen_info(bxscr, patient, 'Biopsy, Frozen Tissue')
dna_date, dna_row = get_specimen_info(dna, patient)
trough, trough_row = get_label_info(patient, 'PLASMPK I-0 TROUGH', 'I-0')
peak, peak_row = get_label_info(patient, 'PLASMA PK I-0 PEAK', 'I-0')
ada, ada_row = get_label_info(patient, 'SERUM ADA I-0 PRE', 'I-0')
sm06, sm06_row = get_label_info(patient, 'SM06/SERUM BIOM', 'I-0')
sm07, sm07_row = get_label_info(patient, 'SM07/WB RNA', 'I-0')
sm10, sm10_row = get_label_info(patient, 'SM10/FECAL', 'I-0')
trough2, trough2_row = get_label_info(patient, 'PLASMPK I-2 TROUGH', 'I-2')
peak2, peak2_row = get_label_info(patient, 'PLASMA PK I-2 PEAK', 'I-2')
ada2, ada2_row = get_label_info(patient, 'SERUM ADA I-2 PRE', 'I-2')
stool2, stool2_row = get_label_info(patient, 'STOOL I-2', 'I-2')
trough4, trough4_row = get_label_info(patient, 'PLASMPK I-4 TROUGH', 'I-4')
peak4, peak4_row = get_label_info(patient, 'PLASMA PK I-4 PEAK', 'I-4')
ada4, ada4_row = get_label_info(patient, 'SERUM ADA I-4 PRE', 'I-4')
sm064, sm064_row = get_label_info(patient, 'SM06/SERUM BIOM', 'I-4')
sm074, sm074_row = get_label_info(patient, 'SM07/WB RNA', 'I-4')
stool4, stool4_row = get_label_info(patient, 'STOOL I-4', 'I-4')
row_data = [investigator, patient,
(sm11, sm11_row), (rna, rna_row), (cryo, cryo_row), (dna_date, dna_row),
(trough, trough_row), (peak, peak_row), (ada, ada_row),
(sm06, sm06_row), (sm07, sm07_row), (sm10, sm10_row),
(trough2, trough2_row), (peak2, peak2_row), (ada2, ada2_row), (stool2, stool2_row),
(trough4, trough4_row), (peak4, peak4_row), (ada4, ada4_row),
(sm064, sm064_row), (sm074, sm074_row), (stool4, stool4_row)]
for col_idx, value in enumerate(row_data, 1):
if col_idx <= 2:
cell = analysis_ws.cell(row=row_idx, column=col_idx, value=value)
if col_idx == 2 and patient in patient_row_map:
cell.hyperlink = f"#'{pat_sheet_name}'!B{patient_row_map[patient]}"
cell.font = Font(name='Calibri', size=11, underline='single')
else:
cell.font = data_font
else:
dt, excel_row = value
cell = analysis_ws.cell(row=row_idx, column=col_idx, value=dt)
if dt and excel_row is not None:
cell.hyperlink = f"#'{src_sheet_name}'!A{excel_row}"
cell.font = date_font_link
cell.fill = yes_fill
cell.number_format = 'DD-MMM-YYYY'
else:
cell.font = Font(name='Calibri', size=11, color="C00000")
cell.fill = no_fill
cell.border = border
cell.alignment = Alignment(horizontal='center', vertical='center')
# ── List: Seznam pacientů ─────────────────────────────────────────────────────
csv_df = pd.read_csv(csv_file, encoding='utf-8')
patients_ws = out_wb.create_sheet("Seznam pacientů")
pat_columns = [
("Číslo centra", 20),
("Číslo pacienta", 20),
("Kód návštěvy", 20),
("Datum návštěvy", 16),
("Typ návštěvy", 16),
]
for col_idx, (col_name, width) in enumerate(pat_columns, 1):
cell = patients_ws.cell(row=1, column=col_idx, value=col_name)
cell.font = header_font
cell.fill = header_fill
cell.border = border
cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
patients_ws.column_dimensions[get_column_letter(col_idx)].width = width
patients_ws.row_dimensions[1].height = 30
patients_ws.freeze_panes = "A2"
def parse_date_edcstd(val):
if pd.isna(val) or str(val).strip() == '':
return None
try:
return datetime.strptime(str(val).strip(), '%d %b %Y')
except:
return None
pat_df = csv_df[['SiteNumber', 'Subject', 'InstanceName', 'Field4Value', 'Field5Value']].copy()
pat_df['Field4Value'] = pat_df['Field4Value'].apply(parse_date_edcstd)
pat_df = pat_df.sort_values(['SiteNumber', 'Subject', 'Field4Value']).reset_index(drop=True)
for row_idx, row in enumerate(pat_df.itertuples(index=False), 2):
for col_idx, value in enumerate(row, 1):
cell = patients_ws.cell(row=row_idx, column=col_idx, value=value)
cell.font = data_font
cell.border = border
cell.alignment = Alignment(horizontal='center', vertical='center')
if col_idx == 4 and value is not None:
cell.number_format = 'DD-MMM-YYYY'
# ── Kit inventory — načtení a příprava dat ────────────────────────────────────
kit_df_raw = pd.read_csv(kit_csv_file, encoding="utf-8")
cze = kit_df_raw[kit_df_raw["Country"] == "CZE"].copy()
def parse_kit_date(val):
if pd.isna(val):
return None
try:
return datetime.strptime(str(val).strip(), "%b %d, %Y")
except:
return None
cze["Shipped Date"] = cze["Shipped Date"].apply(parse_kit_date)
cze["Expiration Date"] = cze["Expiration Date"].apply(parse_kit_date)
cze = cze.sort_values(["Site", "Kit Type", "Expiration Date"]).reset_index(drop=True)
today_dt = datetime.combine(date.today(), datetime.min.time())
def bucket(exp_date):
if exp_date is None:
return None
return "soon" if (exp_date - today_dt).days <= 30 else "ok"
cze["_bucket"] = cze["Expiration Date"].apply(bucket)
kit_order = sorted(cze["Kit Type"].unique(), key=lambda x: (str(x).lstrip("T-").zfill(5), str(x)))
kit_desc = cze.drop_duplicates("Kit Type").set_index("Kit Type")["Description"].to_dict()
kit_sites = sorted(cze["Site"].unique())
# ── Pomocná funkce pro souhrnné tabulky ───────────────────────────────────────
def write_summary_table(ws, current_row, title, rows_data, col_a_header):
for c in range(1, 5):
cell = ws.cell(row=current_row, column=c)
cell.fill = dark_blue_fill
cell.border = border
ws.cell(row=current_row, column=1, value=title).font = Font(name='Calibri', bold=True, size=12, color="FFFFFF")
ws.cell(row=current_row, column=1).alignment = Alignment(horizontal="left", vertical="center")
ws.merge_cells(start_row=current_row, start_column=1, end_row=current_row, end_column=4)
ws.row_dimensions[current_row].height = 22
current_row += 1
for col_idx, (h, f) in enumerate(zip(
[col_a_header, "Description", "Expiruje do 30 dní", "Expiruje později"],
[header_fill, header_fill, orange_fill, green_fill]
), 1):
cell = ws.cell(row=current_row, column=col_idx, value=h)
cell.font = sum_header_font
cell.fill = f
cell.border = border
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
ws.row_dimensions[current_row].height = 28
current_row += 1
totals = [0, 0]
for col_a, col_b, n_soon, n_ok in rows_data:
totals[0] += n_soon
totals[1] += n_ok
all_zero = (n_soon == 0 and n_ok == 0)
row_vals = [col_a, col_b, n_soon, n_ok]
row_fills = [None, None,
orange_fill if n_soon > 0 else None,
green_fill if n_ok > 0 else None]
for col_idx, (val, rfill) in enumerate(zip(row_vals, row_fills), 1):
cell = ws.cell(row=current_row, column=col_idx, value=val)
if col_idx >= 3 and val == 0:
cell.font = zero_red_font if all_zero else zero_font
else:
cell.font = data_font
cell.border = border
cell.alignment = Alignment(horizontal="center" if col_idx >= 2 else "left", vertical="center")
if rfill:
cell.fill = rfill
current_row += 1
for col_idx, val in enumerate(["CELKEM", "", totals[0], totals[1]], 1):
cell = ws.cell(row=current_row, column=col_idx, value=val)
cell.font = sum_total_font
cell.fill = total_fill
cell.border = border
cell.alignment = Alignment(horizontal="center" if col_idx >= 2 else "left", vertical="center")
current_row += 2
return current_row
# ── List: Kit Inventory CZE ───────────────────────────────────────────────────
kit_ws = out_wb.create_sheet("Kit Inventory CZE")
listing_columns = [
("Project No.", 14),
("Region", 10),
("Country", 10),
("Site", 38),
("Kit Type", 12),
("Description", 22),
("Accession", 18),
("Shipped Date", 16),
("Expiration Date", 16),
("Days to Expiration", 20),
]
for col_idx, (hdr, width) in enumerate(listing_columns, 1):
cell = kit_ws.cell(row=1, column=col_idx, value=hdr)
cell.font = header_font
cell.fill = header_fill
cell.border = border
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
kit_ws.column_dimensions[get_column_letter(col_idx)].width = width
kit_ws.row_dimensions[1].height = 30
kit_ws.freeze_panes = "A2"
for row_idx, row in enumerate(cze.itertuples(index=False), 2):
days = row[9]
for col_idx, (col_name, _) in enumerate(listing_columns, 1):
value = row[col_idx - 1]
cell = kit_ws.cell(row=row_idx, column=col_idx, value=value)
cell.font = data_font
cell.border = border
cell.alignment = Alignment(horizontal="center", vertical="center")
if col_name in ("Shipped Date", "Expiration Date") and value is not None:
cell.number_format = "DD-MMM-YYYY"
if col_name == "Days to Expiration":
cell.fill = exp_fill if (pd.notna(days) and days <= 60) else ok_fill
kit_ws.auto_filter.ref = f"A1:{get_column_letter(len(listing_columns))}1"
# ── List: Přehled po centrech ─────────────────────────────────────────────────
ctr_ws = out_wb.create_sheet("Přehled po centrech")
ctr_ws.column_dimensions["A"].width = 22
ctr_ws.column_dimensions["B"].width = 24
ctr_ws.column_dimensions["C"].width = 22
ctr_ws.column_dimensions["D"].width = 20
current_row = 1
for site in kit_sites:
site_df = cze[cze["Site"] == site]
rows_data = []
for kit in kit_order:
desc = kit_desc.get(kit, "")
kit_site_df = site_df[site_df["Kit Type"] == kit]
n_soon = (kit_site_df["_bucket"] == "soon").sum()
n_ok = (kit_site_df["_bucket"] == "ok").sum()
rows_data.append((f"{kit}{desc}", desc, n_soon, n_ok))
current_row = write_summary_table(ctr_ws, current_row, site, rows_data, "Kit Type")
# ── List: Přehled po typech kitů ──────────────────────────────────────────────
sum_ws = out_wb.create_sheet("Přehled po typech")
sum_ws.column_dimensions["A"].width = 38
sum_ws.column_dimensions["B"].width = 22
sum_ws.column_dimensions["C"].width = 22
sum_ws.column_dimensions["D"].width = 20
current_row = 1
for kit in kit_order:
desc = kit_desc.get(kit, "")
kit_df = cze[cze["Kit Type"] == kit]
rows_data = []
for site in sorted(kit_df["Site"].unique()):
site_df = kit_df[kit_df["Site"] == site]
n_soon = (site_df["_bucket"] == "soon").sum()
n_ok = (site_df["_bucket"] == "ok").sum()
rows_data.append((site, desc, n_soon, n_ok))
current_row = write_summary_table(sum_ws, current_row, f"Kit Type {kit}{desc}", rows_data, "Centrum")
# ── List: eQueries ───────────────────────────────────────────────────────────
eq_df = pd.read_csv(eq_csv_file, encoding="utf-8")
eq_cze = eq_df[eq_df["Country"] == "CZECH REPUBLIC"].copy()
status_order = {"Open": 0, "Response Received": 1, "Closed": 2}
eq_cze["_status_order"] = eq_cze["Status"].map(status_order).fillna(99)
eq_cze = eq_cze.sort_values(["_status_order", "Site"]).reset_index(drop=True)
def parse_eq_date(val):
if pd.isna(val):
return None
for fmt in ("%b %d, %Y %I:%M %p", "%b %d, %Y %I:%M %p"):
try:
return datetime.strptime(str(val).strip(), fmt)
except:
pass
try:
return datetime.strptime(str(val).strip().split(" 12:00")[0], "%b %d, %Y")
except:
return None
eq_ws = out_wb.create_sheet("eQueries")
eq_columns = [
("Site", 36),
("Subject", 14),
("Visit", 20),
("Visit Collection Date", 20),
("Accession", 16),
("eQueryId", 12),
("Issue Type", 30),
("Status", 18),
("Create Date", 20),
("Response Date Time", 20),
("Time Before Response", 20),
("User Name", 20),
]
status_fills = {
"Open": PatternFill("solid", fgColor="FFE7E7"),
"Response Received": PatternFill("solid", fgColor="FFF2CC"),
"Closed": PatternFill("solid", fgColor="E2EFDA"),
}
for col_idx, (hdr, width) in enumerate(eq_columns, 1):
cell = eq_ws.cell(row=1, column=col_idx, value=hdr)
cell.font = header_font
cell.fill = header_fill
cell.border = border
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
eq_ws.column_dimensions[get_column_letter(col_idx)].width = width
eq_ws.row_dimensions[1].height = 30
eq_ws.freeze_panes = "A2"
for row_idx, row in enumerate(eq_cze.itertuples(index=False), 2):
status = row[eq_cze.columns.get_loc("Status")]
rfill = status_fills.get(status)
for col_idx, (col_name, _) in enumerate(eq_columns, 1):
value = row[eq_cze.columns.get_loc(col_name)]
if col_name in ("Visit Collection Date", "Create Date", "Response Date Time"):
value = parse_eq_date(value)
cell = eq_ws.cell(row=row_idx, column=col_idx, value=value)
cell.font = data_font
cell.border = border
cell.alignment = Alignment(horizontal="center" if col_idx > 1 else "left", vertical="center")
if col_name in ("Visit Collection Date", "Create Date", "Response Date Time") and value:
cell.number_format = "DD-MMM-YYYY HH:MM"
if rfill:
cell.fill = rfill
eq_ws.auto_filter.ref = f"A1:{get_column_letter(len(eq_columns))}1"
out_wb.save(out_path)
print(f"Saved: {out_path}")
print(f"Patients with BXSCR: {len(bxscr_patients)}, All unique patients: {len(all_patients)}")
print(f"CZE kit rows: {len(cze)}, Kit types: {len(kit_order)}, Sites: {len(kit_sites)}")
print(f"CZE eQueries: {len(eq_cze)} (Open: {(eq_cze['Status']=='Open').sum()}, Response Received: {(eq_cze['Status']=='Response Received').sum()}, Closed: {(eq_cze['Status']=='Closed').sum()})")
@@ -23,6 +23,9 @@ PROFILE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "browser_
def login(page):
page.goto(LOGIN_URL)
page.wait_for_load_state("networkidle")
if not page.get_by_label("Email").is_visible():
print(f"Session aktivni, prihlasen: {page.url}")
return
page.get_by_label("Email").fill(EMAIL)
page.get_by_role("button", name="Next").click()
page.wait_for_load_state("networkidle")
@@ -45,6 +45,9 @@ REPORTS = [
def login(page):
page.goto(LOGIN_URL)
page.wait_for_load_state("networkidle")
if not page.get_by_label("Email").is_visible():
print(f"Session aktivni, prihlasen: {page.url}")
return
page.get_by_label("Email").fill(EMAIL)
page.get_by_role("button", name="Next").click()
page.wait_for_load_state("networkidle")
+20 -17
View File
@@ -1,11 +1,11 @@
# =============================================================================
# Název: download_samples_report_v1.1.py
# Verze: 1.1
# Datum: 2026-05-28
# Datum: 2026-05-29
# Popis: Automatické stažení CSV reportu All Samples ze xsp.labcorp.com pro
# studie 77242113UCO3001 (study 36940) a 42847922MDD3003 (study 35472).
# Aplikuje country filtr CZ, date range od FROM_DATE, čeká na stabil
# Record Count. Výstup do příslušné složky Source/ každé studie.
# Aplikuje country filtr CZ, date range od FROM_DATE, čeká na zmize
# "Fetching Data" před exportem. Výstup do Source/ každé studie.
# =============================================================================
from playwright.sync_api import sync_playwright
from datetime import datetime
@@ -31,6 +31,9 @@ TILE_SUFFIX = "allSamples"
def login(page):
page.goto(LOGIN_URL)
page.wait_for_load_state("networkidle", timeout=120000)
if not page.get_by_label("Email").is_visible():
print(f"Session aktivni, prihlasen: {page.url}")
return
page.get_by_label("Email").fill(EMAIL)
page.get_by_role("button", name="Next").click()
page.wait_for_load_state("networkidle", timeout=120000)
@@ -83,19 +86,14 @@ def export_tile(page, tile_label, file_suffix, timestamp, study_id, out_dir):
page.wait_for_load_state("networkidle", timeout=120000)
page.wait_for_timeout(3000)
# Čekej dokud:
# 1. se neobjeví "No Data Available" (= record count je 0), nebo
# 2. record count není nenulový
page.wait_for_function("""() => {
const noData = document.querySelector('div.table-row.no-data');
if (noData) return true;
const countEl = document.querySelector('div.grid-count span');
if (countEl) {
const n = parseInt(countEl.innerText.trim().replace(/,/g, ''), 10);
return !isNaN(n) && n > 0;
}
return false;
}""", timeout=30000)
# Čekej až zmizí "Fetching Data": po filtru 5s, pak opakuj kontrolu každých 5s
page.wait_for_timeout(5000)
for _ in range(24): # max 2 minuty
if not page.get_by_text("Fetching Data").is_visible():
break
print(" Fetching Data... cekam 5s")
page.wait_for_timeout(5000)
page.wait_for_timeout(5000) # extra buffer po zmizení
if page.locator("div.table-row.no-data").is_visible():
print(f" Record Count: 0 — preskakuji.")
@@ -128,7 +126,12 @@ if __name__ == "__main__":
context = p.chromium.launch_persistent_context(
user_data_dir=PROFILE_DIR,
headless=False,
args=["--disable-blink-features=AutomationControlled", "--start-maximized"],
args=[
"--disable-blink-features=AutomationControlled",
"--start-maximized",
"--disable-restore-session-state",
"--disable-session-crashed-bubble",
],
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,
no_viewport=True,
@@ -1,83 +0,0 @@
# Název: download_test_results_v1.0.py
# Verze: 1.0
# Datum: 2026-05-28
# Popis: Stahuje Standard Test Results ze Covance/Labcorp XSP
# pro studii 77242113UCO3001 (study 36940, report 930556).
# Login přes xsp.covance.com → Okta, pak přímá URL na report.
# Výstup: timestampované CSV do adresáře Source/.
from playwright.sync_api import sync_playwright
from datetime import datetime
import os
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")
REPORTS = [
{
"site": "CZ10003",
"url": "https://xsp.labcorp.com/sponsor/study/36940/test-results/930556/standard-test-results",
"filename": "sponsor-study-36940-test-results-930556-standard-test-results.csv",
},
]
def login(page):
page.goto(LOGIN_URL)
page.wait_for_load_state("networkidle")
page.get_by_label("Email").fill(EMAIL)
page.get_by_role("button", name="Next").click()
page.wait_for_load_state("networkidle")
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, timeout=60000)
page.wait_for_load_state("networkidle", timeout=60000)
page.wait_for_timeout(2000)
print(f"Prihlaseni OK: {page.url}")
def download_report(page, report):
print(f"\n--- {report['site']} ---")
page.goto(report["url"])
page.wait_for_load_state("networkidle", timeout=60000)
page.wait_for_timeout(3000)
print(f"Report nacteny: {page.url}")
# Kliknout 3 tečky (more_horiz) vpravo od filtrovacího řádku
page.locator("button").filter(has_text="more_horiz").last.click()
timestamp = datetime.now().strftime("%Y-%m-%d_%H%M%S")
dest = os.path.join(OUT_DIR, f"{timestamp} {report['filename']}")
with page.expect_download(timeout=60000) as dl:
page.get_by_text("Export to CSV").click()
dl.value.save_as(dest)
print(f"Stazeno: {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"],
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()
login(page)
for report in REPORTS:
download_report(page, report)
context.close()
+6
View File
@@ -1,3 +1,7 @@
# app.py | v1.0 | 2026-05-29
# FastAPI server pro příjem .msg a .db souborů a upload do Dropboxu.
# Endpointy: /upload (.msg → /msgs), /upload-db (.db → /msgs/db), /upload-dropbox (→ Dropbox /!!!Days/Downloads Z230).
from fastapi import FastAPI, UploadFile, File, Header, HTTPException
import shutil
from pathlib import Path
@@ -47,6 +51,8 @@ async def upload_db(
raise HTTPException(status_code=401, detail="Unauthorized")
if not file.filename.endswith(".db"):
raise HTTPException(status_code=400, detail="Only .db files accepted")
for old in DB_DIR.glob("*.db"):
old.unlink()
dest = DB_DIR / file.filename
with dest.open("wb") as f:
shutil.copyfileobj(file.file, f)