From 585e38284bd14af5d352afca7eb481620f9f971e Mon Sep 17 00:00:00 2001 From: Vladimir Buzalka Date: Sun, 16 Nov 2025 07:53:29 +0100 Subject: [PATCH] notebook --- .idea/Medevio.iml | 2 +- .idea/misc.xml | 2 +- .../PRAVIDELNE_0_READ_ALL_ACTIVE_POZADAVKY.py | 91 ++++-- .../PRAVIDELNE_3_StahniKomunikaci.py | 205 ++++++-------- Testy/13 testy report pacienti.py | 226 +++++++++++++++ Testy/14 Testy updateat.py | 227 +++++++++++++++ Testy/15 test.py | 136 +++++++++ Testy/16 test.py | 228 +++++++++++++++ Testy/17 test.py | 259 ++++++++++++++++++ 9 files changed, 1223 insertions(+), 153 deletions(-) create mode 100644 Testy/13 testy report pacienti.py create mode 100644 Testy/14 Testy updateat.py create mode 100644 Testy/15 test.py create mode 100644 Testy/16 test.py create mode 100644 Testy/17 test.py diff --git a/.idea/Medevio.iml b/.idea/Medevio.iml index 78fd998..3cd7809 100644 --- a/.idea/Medevio.iml +++ b/.idea/Medevio.iml @@ -4,7 +4,7 @@ - + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 7b70d2f..7a3c570 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,5 +3,5 @@ - + \ No newline at end of file diff --git a/10ReadPozadavky/PRAVIDELNE_0_READ_ALL_ACTIVE_POZADAVKY.py b/10ReadPozadavky/PRAVIDELNE_0_READ_ALL_ACTIVE_POZADAVKY.py index 43c709a..cbabdcf 100644 --- a/10ReadPozadavky/PRAVIDELNE_0_READ_ALL_ACTIVE_POZADAVKY.py +++ b/10ReadPozadavky/PRAVIDELNE_0_READ_ALL_ACTIVE_POZADAVKY.py @@ -4,8 +4,9 @@ import pymysql import requests from pathlib import Path -from datetime import datetime +from datetime import datetime, timezone import time +from dateutil import parser # ================================ # 🔧 CONFIGURATION @@ -24,21 +25,22 @@ DB_CONFIG = { "cursorclass": pymysql.cursors.DictCursor, } +# ⭐ NOVÝ TESTOVANÝ DOTAZ – obsahuje lastMessage.createdAt GRAPHQL_QUERY = r""" -query ClinicRequestGrid_ListPatientRequestsForClinic2( +query ClinicRequestList2( $clinicSlug: String!, $queueId: String, $queueAssignment: QueueAssignmentFilter!, + $state: PatientRequestState, $pageInfo: PageInfo!, - $locale: Locale!, - $state: PatientRequestState + $locale: Locale! ) { requestsResponse: listPatientRequestsForClinic2( clinicSlug: $clinicSlug, queueId: $queueId, queueAssignment: $queueAssignment, - pageInfo: $pageInfo, - state: $state + state: $state, + pageInfo: $pageInfo ) { count patientRequests { @@ -53,40 +55,71 @@ query ClinicRequestGrid_ListPatientRequestsForClinic2( surname identificationNumber } + lastMessage { + createdAt + } } } } """ + +# ================================ +# 🧿 SAFE DATETIME PARSER (ALWAYS UTC → LOCAL) +# ================================ +def to_mysql_dt_utc(iso_str): + """ + Parse Medevio timestamps safely. + Treat timestamps WITHOUT timezone as UTC. + Convert to local time before saving to MySQL. + """ + if not iso_str: + return None + try: + dt = parser.isoparse(iso_str) + + # If tz is missing → assume UTC + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + + # Convert to local timezone + dt_local = dt.astimezone() + + return dt_local.strftime("%Y-%m-%d %H:%M:%S") + except: + return None + + # ================================ # 🔑 TOKEN # ================================ def read_token(path: Path) -> str: tok = path.read_text(encoding="utf-8").strip() if tok.startswith("Bearer "): - tok = tok.split(" ", 1)[1] + return tok.split(" ", 1)[1] return tok # ================================ -# 🕒 DATETIME FORMAT -# ================================ -def to_mysql_dt(iso_str): - if not iso_str: - return None - try: - dt = datetime.fromisoformat(iso_str.replace("Z", "+00:00")) - return dt.strftime("%Y-%m-%d %H:%M:%S") - except: - return None - - -# ================================ -# 💾 UPSERT +# 💾 UPSERT (včetně správného updatedAt) # ================================ def upsert(conn, r): p = r.get("extendedPatient") or {} + # raw timestamps z API – nyní přes nový parser + api_updated = to_mysql_dt_utc(r.get("updatedAt")) + + last_msg = r.get("lastMessage") or {} + msg_updated = to_mysql_dt_utc(last_msg.get("createdAt")) + + # nejnovější změna + def max_dt(a, b): + if a and b: + return max(a, b) + return a or b + + final_updated = max_dt(api_updated, msg_updated) + sql = """ INSERT INTO pozadavky ( id, displayTitle, createdAt, updatedAt, doneAt, removedAt, @@ -105,10 +138,10 @@ def upsert(conn, r): vals = ( r.get("id"), r.get("displayTitle"), - to_mysql_dt(r.get("createdAt")), - to_mysql_dt(r.get("updatedAt")), - to_mysql_dt(r.get("doneAt")), - to_mysql_dt(r.get("removedAt")), + to_mysql_dt_utc(r.get("createdAt")), + final_updated, + to_mysql_dt_utc(r.get("doneAt")), + to_mysql_dt_utc(r.get("removedAt")), p.get("name"), p.get("surname"), p.get("identificationNumber"), @@ -133,15 +166,15 @@ def fetch_active(headers, offset): } payload = { - "operationName": "ClinicRequestGrid_ListPatientRequestsForClinic2", + "operationName": "ClinicRequestList2", "query": GRAPHQL_QUERY, "variables": variables, } r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers) r.raise_for_status() - data = r.json().get("data", {}).get("requestsResponse", {}) + data = r.json().get("data", {}).get("requestsResponse", {}) return data.get("patientRequests", []), data.get("count", 0) @@ -160,9 +193,6 @@ def main(): print(f"\n=== Sync ACTIVE požadavků @ {datetime.now():%Y-%m-%d %H:%M:%S} ===") - # ------------------------------- - # 🚀 FETCH ALL ACTIVE REQUESTS - # ------------------------------- offset = 0 total_processed = 0 total_count = None @@ -193,5 +223,6 @@ def main(): print("\n✅ ACTIVE sync hotovo!\n") +# ================================ if __name__ == "__main__": main() diff --git a/10ReadPozadavky/PRAVIDELNE_3_StahniKomunikaci.py b/10ReadPozadavky/PRAVIDELNE_3_StahniKomunikaci.py index ac6c4ae..2b05def 100644 --- a/10ReadPozadavky/PRAVIDELNE_3_StahniKomunikaci.py +++ b/10ReadPozadavky/PRAVIDELNE_3_StahniKomunikaci.py @@ -2,11 +2,10 @@ # -*- coding: utf-8 -*- """ -Read conversation messages for pozadavky where messagesProcessed IS NULL -(Optionally filtered by createdAt), insert them into `medevio_conversation`, -and if a message has an attachment (medicalRecord), download it and save into -`medevio_downloads` (same logic as your attachments script). -Finally, mark pozadavky.messagesProcessed = NOW(). +Stáhne konverzaci pro požadavky, kde: +messagesProcessed IS NULL OR messagesProcessed < updatedAt. + +Vloží do medevio_conversation a přílohy do medevio_downloads. """ import zlib @@ -21,6 +20,7 @@ import time # 🔧 CONFIGURATION # ============================== TOKEN_PATH = Path("token.txt") + DB_CONFIG = { "host": "192.168.1.76", "port": 3307, @@ -31,9 +31,6 @@ DB_CONFIG = { "cursorclass": pymysql.cursors.DictCursor, } -# ✅ Optional: Only process requests created after this date ("" = no limit) -CREATED_AFTER = "2024-01-01" - GRAPHQL_QUERY_MESSAGES = r""" query UseMessages_ListMessages($requestId: String!, $updatedSince: DateTime) { messages: listMessages(patientRequestId: $requestId, updatedSince: $updatedSince) { @@ -64,75 +61,58 @@ query UseMessages_ListMessages($requestId: String!, $updatedSince: DateTime) { """ # ============================== -# 🧮 HELPERS +# ⏱ DATETIME PARSER # ============================== -def short_crc8(uuid_str: str) -> str: - return f"{zlib.crc32(uuid_str.encode('utf-8')) & 0xffffffff:08x}" - -def extract_filename_from_url(url: str) -> str: - try: - return url.split("/")[-1].split("?")[0] - except Exception: - return "unknown_filename" - -def read_token(p: Path) -> str: - tok = p.read_text(encoding="utf-8").strip() - if tok.startswith("Bearer "): - tok = tok.split(" ", 1)[1] - return tok - def parse_dt(s): if not s: return None - # handle both "YYYY-mm-ddTHH:MM:SS" and "YYYY-mm-dd HH:MM:SS" - s = s.replace("T", " ") - fmts = ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M") - for f in fmts: - try: - return datetime.strptime(s[:19], f) - except Exception: - pass - return None + try: + return datetime.fromisoformat(s.replace("Z", "+00:00")) + except: + pass + try: + return datetime.strptime(s[:19], "%Y-%m-%dT%H:%M:%S") + except: + return None + +# ============================== +# 🔐 TOKEN +# ============================== +def read_token(path: Path) -> str: + tok = path.read_text(encoding="utf-8").strip() + return tok.replace("Bearer ", "") # ============================== # 📡 FETCH MESSAGES # ============================== def fetch_messages(headers, request_id): - variables = {"requestId": request_id, "updatedSince": None} payload = { "operationName": "UseMessages_ListMessages", "query": GRAPHQL_QUERY_MESSAGES, - "variables": variables, + "variables": {"requestId": request_id, "updatedSince": None}, } + r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30) if r.status_code != 200: - print(f"❌ HTTP {r.status_code} for messages of request {request_id}") + print("❌ HTTP", r.status_code, "for request", request_id) return [] - data = r.json().get("data", {}).get("messages", []) - return data or [] + return r.json().get("data", {}).get("messages", []) or [] + # ============================== -# 💾 SAVE: conversation row +# 💾 SAVE MESSAGE # ============================== def insert_message(cur, req_id, msg): + sender = msg.get("sender") or {} - sender_name = " ".join(x for x in [sender.get("name"), sender.get("surname")] if x).strip() or None - sender_id = sender.get("id") - sender_clinic_id = sender.get("clinicId") - - text = msg.get("text") - created_at = parse_dt(msg.get("createdAt")) - read_at = parse_dt(msg.get("readAt")) - updated_at = parse_dt(msg.get("updatedAt")) - - mr = msg.get("medicalRecord") or {} - attachment_url = mr.get("downloadUrl") or mr.get("url") - attachment_description = mr.get("description") - attachment_content_type = mr.get("contentType") + sender_name = " ".join( + x for x in [sender.get("name"), sender.get("surname")] if x + ) or None sql = """ INSERT INTO medevio_conversation ( - id, request_id, sender_name, sender_id, sender_clinic_id, + id, request_id, + sender_name, sender_id, sender_clinic_id, text, created_at, read_at, updated_at, attachment_url, attachment_description, attachment_content_type ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) @@ -148,57 +128,57 @@ def insert_message(cur, req_id, msg): attachment_description = VALUES(attachment_description), attachment_content_type = VALUES(attachment_content_type) """ + + mr = msg.get("medicalRecord") or {} + cur.execute(sql, ( msg.get("id"), req_id, sender_name, - sender_id, - sender_clinic_id, - text, - created_at, - read_at, - updated_at, - attachment_url, - attachment_description, - attachment_content_type + sender.get("id"), + sender.get("clinicId"), + msg.get("text"), + parse_dt(msg.get("createdAt")), + parse_dt(msg.get("readAt")), + parse_dt(msg.get("updatedAt")), + mr.get("downloadUrl") or mr.get("url"), + mr.get("description"), + mr.get("contentType") )) + # ============================== -# 💾 SAVE: download attachment (from message) +# 💾 DOWNLOAD MESSAGE ATTACHMENT # ============================== -def insert_download_from_message(cur, req_id, msg, existing_ids): +def insert_download(cur, req_id, msg, existing_ids): + mr = msg.get("medicalRecord") or {} attachment_id = mr.get("id") if not attachment_id: - return False + return + if attachment_id in existing_ids: - print(f" ⏭️ Skipping already downloaded message-attachment {attachment_id}") - return False + return # skip duplicates url = mr.get("downloadUrl") or mr.get("url") if not url: - return False + return try: r = requests.get(url, timeout=30) r.raise_for_status() - content = r.content + data = r.content except Exception as e: - print(f" ⚠️ Failed to download message attachment {attachment_id}: {e}") - return False + print("⚠️ Failed to download:", e) + return - filename = extract_filename_from_url(url) - content_type = mr.get("contentType") - file_size = len(content) - created_date = parse_dt(msg.get("createdAt")) + filename = url.split("/")[-1].split("?")[0] - # We don't have patient names on the message level here; keep NULLs. cur.execute(""" INSERT INTO medevio_downloads ( - request_id, attachment_id, attachment_type, filename, - content_type, file_size, pacient_jmeno, pacient_prijmeni, - created_at, file_content - ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) + request_id, attachment_id, attachment_type, + filename, content_type, file_size, created_at, file_content + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s) ON DUPLICATE KEY UPDATE file_content = VALUES(file_content), file_size = VALUES(file_size), @@ -208,21 +188,20 @@ def insert_download_from_message(cur, req_id, msg, existing_ids): attachment_id, "MESSAGE_ATTACHMENT", filename, - content_type, - file_size, - None, - None, - created_date, - content + mr.get("contentType"), + len(data), + parse_dt(msg.get("createdAt")), + data )) + existing_ids.add(attachment_id) - print(f" 💾 Saved msg attachment {filename} ({file_size/1024:.1f} kB)") - return True + # ============================== # 🧠 MAIN # ============================== def main(): + token = read_token(TOKEN_PATH) headers = { "Authorization": f"Bearer {token}", @@ -232,65 +211,49 @@ def main(): conn = pymysql.connect(**DB_CONFIG) - # Load existing download IDs to skip duplicates (same logic as your script) - print("📦 Loading list of already downloaded attachments...") + # ---- Load existing attachments with conn.cursor() as cur: cur.execute("SELECT attachment_id FROM medevio_downloads") existing_ids = {row["attachment_id"] for row in cur.fetchall()} - print(f"✅ Found {len(existing_ids)} attachments already saved.") - # Pull pozadavky where messagesProcessed IS NULL (optionally by createdAt) + print(f"📦 Already downloaded attachments: {len(existing_ids)}\n") + + # ---- Select pozadavky needing message sync sql = """ - SELECT id, displayTitle, pacient_prijmeni, pacient_jmeno, createdAt + SELECT id FROM pozadavky WHERE messagesProcessed IS NULL + OR messagesProcessed < updatedAt """ - params = [] - if CREATED_AFTER: - sql += " AND createdAt >= %s" - params.append(CREATED_AFTER) - with conn.cursor() as cur: - cur.execute(sql, params) - rows = cur.fetchall() + cur.execute(sql) + requests_to_process = cur.fetchall() - print(f"📋 Found {len(rows)} pozadavky to process (messagesProcessed IS NULL" - + (f", created >= {CREATED_AFTER}" if CREATED_AFTER else "") + ")") + print(f"📋 Found {len(requests_to_process)} pozadavků requiring message sync.\n") - for i, row in enumerate(rows, 1): + # ---- Process each pozadavek + for idx, row in enumerate(requests_to_process, 1): req_id = row["id"] - prijmeni = row.get("pacient_prijmeni") or "Neznamy" - jmeno = row.get("pacient_jmeno") or "" - print(f"\n[{i}/{len(rows)}] 💬 {prijmeni}, {jmeno} ({req_id})") + print(f"[{idx}/{len(requests_to_process)}] Processing {req_id} …") messages = fetch_messages(headers, req_id) - if not messages: - print(" ⚠️ No messages found") - with conn.cursor() as cur: - cur.execute("UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s", (req_id,)) - conn.commit() - continue - inserted = 0 with conn.cursor() as cur: for msg in messages: insert_message(cur, req_id, msg) - # also pull any message attachments into downloads table - insert_download_from_message(cur, req_id, msg, existing_ids) - inserted += 1 + insert_download(cur, req_id, msg, existing_ids) conn.commit() - # mark processed with conn.cursor() as cur: cur.execute("UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s", (req_id,)) conn.commit() - print(f" ✅ {inserted} messages processed for {prijmeni}, {jmeno}") - time.sleep(0.3) # polite API delay + print(f" ✅ {len(messages)} messages saved\n") + time.sleep(0.25) conn.close() - print("\n✅ Done! All new conversations processed and pozadavky updated.") + print("🎉 Done!") + -# ============================== if __name__ == "__main__": main() diff --git a/Testy/13 testy report pacienti.py b/Testy/13 testy report pacienti.py new file mode 100644 index 0000000..ce99d91 --- /dev/null +++ b/Testy/13 testy report pacienti.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import pymysql +import openpyxl +from openpyxl.styles import PatternFill, Font +from openpyxl.utils import get_column_letter + +from datetime import datetime + +timestamp = datetime.now().strftime("%Y-%m-%d %H-%M-%S") +OUTPUT_PATH = fr"U:\Dropbox\!!!Days\Downloads Z230\{timestamp} medevio_patients_report.xlsx" + +# ============================ +# CONFIGURATION +# ============================ +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +# ============================ +# FUNCTIONS +# ============================ + +from openpyxl.styles import Border, Side + +thin_border = Border( + left=Side(style='thin'), + right=Side(style='thin'), + top=Side(style='thin'), + bottom=Side(style='thin') +) + +def apply_thin_borders(ws): + """Apply thin borders to all cells in the worksheet.""" + for row in ws.iter_rows(): + for cell in row: + cell.border = thin_border + +def autofit_columns(ws): + """Auto-adjust column widths based on longest cell content.""" + for col in ws.columns: + max_length = 0 + col_letter = get_column_letter(col[0].column) + for cell in col: + try: + if cell.value: + max_length = max(max_length, len(str(cell.value))) + except: + pass + ws.column_dimensions[col_letter].width = max_length + 2 + +def apply_header_style(ws): + """Make header BRIGHT YELLOW and bold.""" + fill = PatternFill(start_color="FFFF00", end_color="FFFF00", fill_type="solid") + font = Font(bold=True) + + for cell in ws[1]: + cell.fill = fill + cell.font = font + +def create_compact_row(row): + """Produce compact record with merged pojistovna, with user_relationship after prijmeni.""" + + # insurance merged + code = row.get("pojistovna_code") or "" + naz = row.get("pojistovna_nazev") or "" + + if code and naz: + poj = f"{code} ({naz})" + elif code: + poj = code + elif naz: + poj = naz + else: + poj = "" + + return { + "id": row["id"], + "jmeno": row["jmeno"], + "prijmeni": row["prijmeni"], + + # 🔹 inserted here + "user_relationship": row.get("user_relationship"), + + "rodne_cislo": row["rodne_cislo"], + "dob": row["dob"], + "telefon": row["telefon"], + "email": row["email"], + "pojistovna": poj, + "status": row["status"], + "has_mobile_app": row["has_mobile_app"], + "registration_time": row["registration_time"], + "last_update": row["last_update"], + } + +def create_pozadavky_rows(rows): + """Convert raw pozadavky SQL rows into rows for the Excel sheet.""" + output = [] + for r in rows: + output.append({ + # 🔹 First the ID + "id": r["id"], + + # 🔹 Your 3 patient columns immediately after ID + "pacient_jmeno": r["pacient_jmeno"], + "pacient_prijmeni": r["pacient_prijmeni"], + "pacient_rodnecislo": r["pacient_rodnecislo"], + + # 🔹 Then all other fields in any order you prefer + "displayTitle": r["displayTitle"], + "createdAt": r["createdAt"], + "updatedAt": r["updatedAt"], + "doneAt": r["doneAt"], + "removedAt": r["removedAt"], + "attachmentsProcessed": r["attachmentsProcessed"], + "messagesProcessed": r["messagesProcessed"], + "communicationprocessed": r["communicationprocessed"], + "questionnaireprocessed": r["questionnaireprocessed"], + "lastSync": r["lastSync"], + }) + return output + + +# ============================ +# MAIN +# ============================ + +def main(): + print("📥 Connecting to MySQL...") + conn = pymysql.connect(**DB_CONFIG) + + with conn: + with conn.cursor() as cur: + cur.execute("SELECT * FROM medevio_pacienti ORDER BY prijmeni, jmeno") + patients = cur.fetchall() + + print(f"📊 Loaded {len(patients)} patients.") + + # Load pozadavky + + with conn.cursor() as cur: + cur.execute("SELECT * FROM pozadavky ORDER BY createdAt DESC") + pozadavky_rows = cur.fetchall() + + print(f"📄 Loaded {len(pozadavky_rows)} pozadavky.") + + wb = openpyxl.Workbook() + + # --------------------------------- + # 1) FULL SHEET + # --------------------------------- + ws_full = wb.active + ws_full.title = "Patients FULL" + + if patients: + headers = list(patients[0].keys()) + ws_full.append(headers) + + for row in patients: + ws_full.append([row.get(h) for h in headers]) + + apply_header_style(ws_full) + ws_full.freeze_panes = "A2" + ws_full.auto_filter.ref = ws_full.dimensions + autofit_columns(ws_full) + apply_thin_borders(ws_full) + + # --------------------------------- + # 2) COMPACT SHEET + # --------------------------------- + ws_compact = wb.create_sheet("Patients COMPACT") + + compact_rows = [create_compact_row(r) for r in patients] + compact_headers = list(compact_rows[0].keys()) + + ws_compact.append(compact_headers) + for row in compact_rows: + ws_compact.append([row.get(h) for h in compact_headers]) + + apply_header_style(ws_compact) + ws_compact.freeze_panes = "A2" + ws_compact.auto_filter.ref = ws_compact.dimensions + autofit_columns(ws_compact) + + # >>> ADD THIS <<< + ur_col_index = compact_headers.index("user_relationship") + 1 + col_letter = get_column_letter(ur_col_index) + ws_compact.column_dimensions[col_letter].width = 7.14 + apply_thin_borders(ws_compact) + + # --------------------------------- + # 3) POZADAVKY SHEET + # --------------------------------- + ws_p = wb.create_sheet("Pozadavky") + + poz_list = create_pozadavky_rows(pozadavky_rows) + headers_p = list(poz_list[0].keys()) if poz_list else [] + + if headers_p: + ws_p.append(headers_p) + for row in poz_list: + ws_p.append([row.get(h) for h in headers_p]) + + apply_header_style(ws_p) + ws_p.freeze_panes = "A2" + ws_p.auto_filter.ref = ws_p.dimensions + autofit_columns(ws_p) + apply_thin_borders(ws_p) + + + # --------------------------------- + # SAVE + # --------------------------------- + + wb.save(OUTPUT_PATH) + print(f"✅ Excel report saved to:\n{OUTPUT_PATH}") + +if __name__ == "__main__": + main() diff --git a/Testy/14 Testy updateat.py b/Testy/14 Testy updateat.py new file mode 100644 index 0000000..d215ca2 --- /dev/null +++ b/Testy/14 Testy updateat.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import requests +from pathlib import Path +import json + +TOKEN_PATH = Path("token.txt") +CLINIC_SLUG = "mudr-buzalkova" +BATCH_SIZE = 100 + +# přesně tvůj původní dotaz, beze změn +# GRAPHQL_QUERY = r""" +# query ClinicRequestGrid_ListPatientRequestsForClinic2( +# $clinicSlug: String!, +# $queueId: String, +# $queueAssignment: QueueAssignmentFilter!, +# $pageInfo: PageInfo!, +# $locale: Locale!, +# $state: PatientRequestState +# ) { +# requestsResponse: listPatientRequestsForClinic2( +# clinicSlug: $clinicSlug, +# queueId: $queueId, +# queueAssignment: $queueAssignment, +# pageInfo: $pageInfo, +# state: $state +# ) { +# count +# patientRequests { +# id +# displayTitle(locale: $locale) +# createdAt +# updatedAt +# doneAt +# removedAt +# extendedPatient { +# name +# surname +# identificationNumber +# } +# } +# } +# } +# """ +GRAPHQL_QUERY = r""" +query ClinicRequestGrid_ListPatientRequestsForClinic2( + $clinicSlug: String!, + $queueId: String, + $queueAssignment: QueueAssignmentFilter!, + $state: PatientRequestState, + $pageInfo: PageInfo!, + $locale: Locale! +) { + requestsResponse: listPatientRequestsForClinic2( + clinicSlug: $clinicSlug + queueId: $queueId + queueAssignment: $queueAssignment + state: $state + pageInfo: $pageInfo + ) { + count + patientRequests { + id + displayTitle(locale: $locale) + + ### TIME FIELDS ADDED + createdAt + updatedAt + doneAt + removedAt + + extendedPatient { + id + identificationNumber + name + surname + kind + key + type + user { + id + name + surname + } + owner { + name + surname + } + dob + premiumPlanPatient { + id + premiumPlan { + id + } + } + status2 + tags(onlyImportant: true) { + id + } + isUnknownPatient + } + + invoice { + id + status + amount + currency + dueAmount + isOverdue + refundedAmount + settledAmount + } + + lastMessage { + createdAt + id + readAt + sender { + id + name + surname + clinicId + } + text + } + + priority + + queue { + id + name + clinicPatientRequestQueueUsers { + accountable { + id + name + surname + } + id + } + } + + reservations { + calendar { + id + internalName + name + } + id + canceledAt + done + start + } + + tags(onlyImportant: true) { + id + } + + userECRF(locale: $locale) { + id + sid + icon { + color + id + urlSvg + } + ecrfSet { + id + name + } + } + + priceWhenCreated + currencyWhenCreated + createdByDoctor + eventType + clinicNotes { + id + } + clinicMedicalRecord + } + } +} +""" + +def read_token(path: Path) -> str: + tok = path.read_text(encoding="utf-8").strip() + if tok.startswith("Bearer "): + tok = tok.split(" ", 1)[1] + return tok + + +def main(): + token = read_token(TOKEN_PATH) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + variables = { + "clinicSlug": CLINIC_SLUG, + "queueId": None, + "queueAssignment": "ANY", + "pageInfo": {"first": BATCH_SIZE, "offset": 0}, + "locale": "cs", + "state": "ACTIVE", + } + + payload = { + "operationName": "ClinicRequestGrid_ListPatientRequestsForClinic2", + "query": GRAPHQL_QUERY, + "variables": variables, + } + + print("\n===== ČISTÁ ODPOVĚĎ SERVERU =====\n") + + r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30) + + print(f"HTTP {r.status_code}\n") + print(r.text) # <-- TISK NEUPRAVENÉHO JSONU + + print("\n===== KONEC ČISTÉ ODPOVĚDI =====\n") + + +if __name__ == "__main__": + main() diff --git a/Testy/15 test.py b/Testy/15 test.py new file mode 100644 index 0000000..d71905e --- /dev/null +++ b/Testy/15 test.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import requests +from pathlib import Path + +TOKEN_PATH = Path("token.txt") +CLINIC_SLUG = "mudr-buzalkova" +BATCH_SIZE = 100 +TARGET_ID = "cbf6000d-a6ca-4059-88b7-dfdc27220762" # ← sem tvoje ID + +# ⭐ Updated GraphQL with lastMessage included +GRAPHQL_QUERY = r""" +query ClinicRequestGrid_ListPatientRequestsForClinic2( + $clinicSlug: String!, + $queueId: String, + $queueAssignment: QueueAssignmentFilter!, + $pageInfo: PageInfo!, + $locale: Locale!, + $state: PatientRequestState +) { + requestsResponse: listPatientRequestsForClinic2( + clinicSlug: $clinicSlug, + queueId: $queueId, + queueAssignment: $queueAssignment, + pageInfo: $pageInfo, + state: $state + ) { + count + patientRequests { + id + displayTitle(locale: $locale) + createdAt + updatedAt + doneAt + removedAt + lastMessage { + id + createdAt + updatedAt + } + extendedPatient { + name + surname + identificationNumber + } + } + } +} +""" + +def read_token(path: Path) -> str: + tok = path.read_text(encoding="utf-8").strip() + if tok.startswith("Bearer "): + tok = tok.split(" ", 1)[1] + return tok + +def fetch_active(headers, offset): + variables = { + "clinicSlug": CLINIC_SLUG, + "queueId": None, + "queueAssignment": "ANY", + "pageInfo": {"first": BATCH_SIZE, "offset": offset}, + "locale": "cs", + "state": "ACTIVE", + } + + payload = { + "operationName": "ClinicRequestGrid_ListPatientRequestsForClinic2", + "query": GRAPHQL_QUERY, + "variables": variables, + } + + r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30) + + if r.status_code != 200: + print("HTTP status:", r.status_code) + print(r.text) + r.raise_for_status() + + data = r.json().get("data", {}).get("requestsResponse", {}) + return data.get("patientRequests", []), data.get("count", 0) + + +def main(): + token = read_token(TOKEN_PATH) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json" + } + + print(f"=== Hledám updatedAt a lastMessage pro pozadavek {TARGET_ID} ===\n") + + offset = 0 + total_count = None + found = False + + while True: + batch, count = fetch_active(headers, offset) + + if total_count is None: + total_count = count + + if not batch: + break + + for r in batch: + if r["id"] == TARGET_ID: + + print("Nalezeno!\n") + print(f"id: {r['id']}") + print(f"updatedAt: {r['updatedAt']}") + + lm = r.get("lastMessage") or {} + print(f"lastMessage.createdAt: {lm.get('createdAt')}") + print(f"lastMessage.updatedAt: {lm.get('updatedAt')}") + + found = True + break + + if found: + break + + if offset + BATCH_SIZE >= count: + break + + offset += BATCH_SIZE + + if not found: + print("❌ Požadavek nebyl nalezen mezi ACTIVE.") + + print("\n=== HOTOVO ===") + + +if __name__ == "__main__": + main() diff --git a/Testy/16 test.py b/Testy/16 test.py new file mode 100644 index 0000000..cbabdcf --- /dev/null +++ b/Testy/16 test.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import pymysql +import requests +from pathlib import Path +from datetime import datetime, timezone +import time +from dateutil import parser + +# ================================ +# 🔧 CONFIGURATION +# ================================ +TOKEN_PATH = Path("token.txt") +CLINIC_SLUG = "mudr-buzalkova" +BATCH_SIZE = 100 + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +# ⭐ NOVÝ TESTOVANÝ DOTAZ – obsahuje lastMessage.createdAt +GRAPHQL_QUERY = r""" +query ClinicRequestList2( + $clinicSlug: String!, + $queueId: String, + $queueAssignment: QueueAssignmentFilter!, + $state: PatientRequestState, + $pageInfo: PageInfo!, + $locale: Locale! +) { + requestsResponse: listPatientRequestsForClinic2( + clinicSlug: $clinicSlug, + queueId: $queueId, + queueAssignment: $queueAssignment, + state: $state, + pageInfo: $pageInfo + ) { + count + patientRequests { + id + displayTitle(locale: $locale) + createdAt + updatedAt + doneAt + removedAt + extendedPatient { + name + surname + identificationNumber + } + lastMessage { + createdAt + } + } + } +} +""" + + +# ================================ +# 🧿 SAFE DATETIME PARSER (ALWAYS UTC → LOCAL) +# ================================ +def to_mysql_dt_utc(iso_str): + """ + Parse Medevio timestamps safely. + Treat timestamps WITHOUT timezone as UTC. + Convert to local time before saving to MySQL. + """ + if not iso_str: + return None + try: + dt = parser.isoparse(iso_str) + + # If tz is missing → assume UTC + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + + # Convert to local timezone + dt_local = dt.astimezone() + + return dt_local.strftime("%Y-%m-%d %H:%M:%S") + except: + return None + + +# ================================ +# 🔑 TOKEN +# ================================ +def read_token(path: Path) -> str: + tok = path.read_text(encoding="utf-8").strip() + if tok.startswith("Bearer "): + return tok.split(" ", 1)[1] + return tok + + +# ================================ +# 💾 UPSERT (včetně správného updatedAt) +# ================================ +def upsert(conn, r): + p = r.get("extendedPatient") or {} + + # raw timestamps z API – nyní přes nový parser + api_updated = to_mysql_dt_utc(r.get("updatedAt")) + + last_msg = r.get("lastMessage") or {} + msg_updated = to_mysql_dt_utc(last_msg.get("createdAt")) + + # nejnovější změna + def max_dt(a, b): + if a and b: + return max(a, b) + return a or b + + final_updated = max_dt(api_updated, msg_updated) + + sql = """ + INSERT INTO pozadavky ( + id, displayTitle, createdAt, updatedAt, doneAt, removedAt, + pacient_jmeno, pacient_prijmeni, pacient_rodnecislo + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + displayTitle=VALUES(displayTitle), + updatedAt=VALUES(updatedAt), + doneAt=VALUES(doneAt), + removedAt=VALUES(removedAt), + pacient_jmeno=VALUES(pacient_jmeno), + pacient_prijmeni=VALUES(pacient_prijmeni), + pacient_rodnecislo=VALUES(pacient_rodnecislo) + """ + + vals = ( + r.get("id"), + r.get("displayTitle"), + to_mysql_dt_utc(r.get("createdAt")), + final_updated, + to_mysql_dt_utc(r.get("doneAt")), + to_mysql_dt_utc(r.get("removedAt")), + p.get("name"), + p.get("surname"), + p.get("identificationNumber"), + ) + + with conn.cursor() as cur: + cur.execute(sql, vals) + conn.commit() + + +# ================================ +# 📡 FETCH ACTIVE PAGE +# ================================ +def fetch_active(headers, offset): + variables = { + "clinicSlug": CLINIC_SLUG, + "queueId": None, + "queueAssignment": "ANY", + "pageInfo": {"first": BATCH_SIZE, "offset": offset}, + "locale": "cs", + "state": "ACTIVE", + } + + payload = { + "operationName": "ClinicRequestList2", + "query": GRAPHQL_QUERY, + "variables": variables, + } + + r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers) + r.raise_for_status() + + data = r.json().get("data", {}).get("requestsResponse", {}) + return data.get("patientRequests", []), data.get("count", 0) + + +# ================================ +# 🧠 MAIN +# ================================ +def main(): + token = read_token(TOKEN_PATH) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + conn = pymysql.connect(**DB_CONFIG) + + print(f"\n=== Sync ACTIVE požadavků @ {datetime.now():%Y-%m-%d %H:%M:%S} ===") + + offset = 0 + total_processed = 0 + total_count = None + + while True: + batch, count = fetch_active(headers, offset) + + if total_count is None: + total_count = count + print(f"📡 Celkem ACTIVE v Medevio: {count}") + + if not batch: + break + + for r in batch: + upsert(conn, r) + + total_processed += len(batch) + print(f" • {total_processed}/{total_count} ACTIVE processed") + + if offset + BATCH_SIZE >= count: + break + + offset += BATCH_SIZE + time.sleep(0.4) + + conn.close() + print("\n✅ ACTIVE sync hotovo!\n") + + +# ================================ +if __name__ == "__main__": + main() diff --git a/Testy/17 test.py b/Testy/17 test.py new file mode 100644 index 0000000..2b05def --- /dev/null +++ b/Testy/17 test.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Stáhne konverzaci pro požadavky, kde: +messagesProcessed IS NULL OR messagesProcessed < updatedAt. + +Vloží do medevio_conversation a přílohy do medevio_downloads. +""" + +import zlib +import json +import requests +import pymysql +from pathlib import Path +from datetime import datetime +import time + +# ============================== +# 🔧 CONFIGURATION +# ============================== +TOKEN_PATH = Path("token.txt") + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +GRAPHQL_QUERY_MESSAGES = r""" +query UseMessages_ListMessages($requestId: String!, $updatedSince: DateTime) { + messages: listMessages(patientRequestId: $requestId, updatedSince: $updatedSince) { + id + createdAt + updatedAt + readAt + text + type + sender { + id + name + surname + clinicId + } + medicalRecord { + id + description + contentType + url + downloadUrl + token + createdAt + updatedAt + } + } +} +""" + +# ============================== +# ⏱ DATETIME PARSER +# ============================== +def parse_dt(s): + if not s: + return None + try: + return datetime.fromisoformat(s.replace("Z", "+00:00")) + except: + pass + try: + return datetime.strptime(s[:19], "%Y-%m-%dT%H:%M:%S") + except: + return None + +# ============================== +# 🔐 TOKEN +# ============================== +def read_token(path: Path) -> str: + tok = path.read_text(encoding="utf-8").strip() + return tok.replace("Bearer ", "") + +# ============================== +# 📡 FETCH MESSAGES +# ============================== +def fetch_messages(headers, request_id): + payload = { + "operationName": "UseMessages_ListMessages", + "query": GRAPHQL_QUERY_MESSAGES, + "variables": {"requestId": request_id, "updatedSince": None}, + } + + r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30) + if r.status_code != 200: + print("❌ HTTP", r.status_code, "for request", request_id) + return [] + return r.json().get("data", {}).get("messages", []) or [] + + +# ============================== +# 💾 SAVE MESSAGE +# ============================== +def insert_message(cur, req_id, msg): + + sender = msg.get("sender") or {} + sender_name = " ".join( + x for x in [sender.get("name"), sender.get("surname")] if x + ) or None + + sql = """ + INSERT INTO medevio_conversation ( + id, request_id, + sender_name, sender_id, sender_clinic_id, + text, created_at, read_at, updated_at, + attachment_url, attachment_description, attachment_content_type + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + sender_name = VALUES(sender_name), + sender_id = VALUES(sender_id), + sender_clinic_id = VALUES(sender_clinic_id), + text = VALUES(text), + created_at = VALUES(created_at), + read_at = VALUES(read_at), + updated_at = VALUES(updated_at), + attachment_url = VALUES(attachment_url), + attachment_description = VALUES(attachment_description), + attachment_content_type = VALUES(attachment_content_type) + """ + + mr = msg.get("medicalRecord") or {} + + cur.execute(sql, ( + msg.get("id"), + req_id, + sender_name, + sender.get("id"), + sender.get("clinicId"), + msg.get("text"), + parse_dt(msg.get("createdAt")), + parse_dt(msg.get("readAt")), + parse_dt(msg.get("updatedAt")), + mr.get("downloadUrl") or mr.get("url"), + mr.get("description"), + mr.get("contentType") + )) + + +# ============================== +# 💾 DOWNLOAD MESSAGE ATTACHMENT +# ============================== +def insert_download(cur, req_id, msg, existing_ids): + + mr = msg.get("medicalRecord") or {} + attachment_id = mr.get("id") + if not attachment_id: + return + + if attachment_id in existing_ids: + return # skip duplicates + + url = mr.get("downloadUrl") or mr.get("url") + if not url: + return + + try: + r = requests.get(url, timeout=30) + r.raise_for_status() + data = r.content + except Exception as e: + print("⚠️ Failed to download:", e) + return + + filename = url.split("/")[-1].split("?")[0] + + cur.execute(""" + INSERT INTO medevio_downloads ( + request_id, attachment_id, attachment_type, + filename, content_type, file_size, created_at, file_content + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + file_content = VALUES(file_content), + file_size = VALUES(file_size), + downloaded_at = NOW() + """, ( + req_id, + attachment_id, + "MESSAGE_ATTACHMENT", + filename, + mr.get("contentType"), + len(data), + parse_dt(msg.get("createdAt")), + data + )) + + existing_ids.add(attachment_id) + + +# ============================== +# 🧠 MAIN +# ============================== +def main(): + + token = read_token(TOKEN_PATH) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + conn = pymysql.connect(**DB_CONFIG) + + # ---- Load existing attachments + with conn.cursor() as cur: + cur.execute("SELECT attachment_id FROM medevio_downloads") + existing_ids = {row["attachment_id"] for row in cur.fetchall()} + + print(f"📦 Already downloaded attachments: {len(existing_ids)}\n") + + # ---- Select pozadavky needing message sync + sql = """ + SELECT id + FROM pozadavky + WHERE messagesProcessed IS NULL + OR messagesProcessed < updatedAt + """ + with conn.cursor() as cur: + cur.execute(sql) + requests_to_process = cur.fetchall() + + print(f"📋 Found {len(requests_to_process)} pozadavků requiring message sync.\n") + + # ---- Process each pozadavek + for idx, row in enumerate(requests_to_process, 1): + req_id = row["id"] + print(f"[{idx}/{len(requests_to_process)}] Processing {req_id} …") + + messages = fetch_messages(headers, req_id) + + with conn.cursor() as cur: + for msg in messages: + insert_message(cur, req_id, msg) + insert_download(cur, req_id, msg, existing_ids) + conn.commit() + + with conn.cursor() as cur: + cur.execute("UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s", (req_id,)) + conn.commit() + + print(f" ✅ {len(messages)} messages saved\n") + time.sleep(0.25) + + conn.close() + print("🎉 Done!") + + +if __name__ == "__main__": + main()