diff --git a/.claude/worktrees/heuristic-lichterman b/.claude/worktrees/heuristic-lichterman new file mode 160000 index 0000000..e345e47 --- /dev/null +++ b/.claude/worktrees/heuristic-lichterman @@ -0,0 +1 @@ +Subproject commit e345e477d36987ffb10056f822f1b4794e015870 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..26397e5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Virtual environment +.venv/ + +# Python +__pycache__/ +*.pyc + +# PyCharm / IDE +.idea/ + +# Claude worktrees +.claude/worktrees/ + +# OS +.DS_Store +Thumbs.db diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/Medevio.iml b/.idea/Medevio.iml deleted file mode 100644 index b51fc52..0000000 --- a/.idea/Medevio.iml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 105ce2d..0000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 35161d5..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 6e0f634..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/10ReadPozadavky/PRAVIDELNE_3_StahniKomunikaciDELTA.py b/10ReadPozadavky/PRAVIDELNE_3_StahniKomunikaciDELTA.py new file mode 100644 index 0000000..b3f5d60 --- /dev/null +++ b/10ReadPozadavky/PRAVIDELNE_3_StahniKomunikaciDELTA.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Delta sync Medevio communication. +Stáhne pouze zprávy změněné po messagesProcessed pro každý požadavek. +""" + +import json +import requests +import pymysql +from pathlib import Path +from datetime import datetime +import time +import sys + +# ============================== +# UTF-8 SAFE OUTPUT +# ============================== +try: + sys.stdout.reconfigure(encoding='utf-8') + sys.stderr.reconfigure(encoding='utf-8') +except AttributeError: + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + + +def safe_print(text: str): + enc = sys.stdout.encoding or "" + if not enc.lower().startswith("utf"): + text = ''.join(ch for ch in text if ord(ch) < 65536) + try: + print(text) + except UnicodeEncodeError: + text = ''.join(ch for ch in text if ord(ch) < 128) + print(text) + + +# ============================== +# CONFIG +# ============================== +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 + createdAt + updatedAt + } + } +} +""" + + +# ============================== +# HELPERS +# ============================== +def parse_dt(s): + if not s: + return None + try: + return datetime.fromisoformat(s.replace("Z", "+00:00")) + except Exception: + return None + + +def read_token(path: Path) -> str: + tok = path.read_text(encoding="utf-8").strip() + return tok.replace("Bearer ", "") + + +# ============================== +# FETCH MESSAGES (DELTA) +# ============================== +def fetch_messages(headers, request_id, updated_since): + payload = { + "operationName": "UseMessages_ListMessages", + "query": GRAPHQL_QUERY_MESSAGES, + "variables": { + "requestId": request_id, + "updatedSince": updated_since, + }, + } + + r = requests.post( + "https://api.medevio.cz/graphql", + json=payload, + headers=headers, + timeout=30 + ) + + if r.status_code != 200: + safe_print(f"❌ HTTP {r.status_code} for request {request_id}") + return [] + + j = r.json() + if "errors" in j: + safe_print(f"❌ GraphQL error for {request_id}: {j['errors']}") + return [] + + return j.get("data", {}).get("messages", []) or [] + + +# ============================== +# INSERT 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 + + mr = msg.get("medicalRecord") or {} + + 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) + """ + + 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") + )) + + +# ============================== +# INSERT ATTACHMENT (DEDUP) +# ============================== +def insert_download(cur, req_id, msg, existing_ids): + mr = msg.get("medicalRecord") or {} + attachment_id = mr.get("id") + if not attachment_id or attachment_id in existing_ids: + return + + 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: + safe_print(f"⚠️ Attachment download failed: {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) + + # existing attachments + with conn.cursor() as cur: + cur.execute("SELECT attachment_id FROM medevio_downloads") + existing_ids = {r["attachment_id"] for r in cur.fetchall()} + + # select requests needing sync + with conn.cursor() as cur: + cur.execute(""" + SELECT id, messagesProcessed + FROM pozadavky + WHERE messagesProcessed IS NULL + OR messagesProcessed < updatedAt + """) + rows = cur.fetchall() + + safe_print(f"📋 Found {len(rows)} requests for message delta-sync\n") + + for i, row in enumerate(rows, 1): + req_id = row["id"] + updated_since = row["messagesProcessed"] + if updated_since: + updated_since = updated_since.replace(microsecond=0).isoformat() + "Z" + + safe_print(f"[{i}/{len(rows)}] {req_id}") + + messages = fetch_messages(headers, req_id, updated_since) + if not messages: + safe_print(" ⏭ No new messages") + else: + 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() + safe_print(f" ✅ {len(messages)} new/updated messages") + + with conn.cursor() as cur: + cur.execute( + "UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s", + (req_id,) + ) + conn.commit() + + time.sleep(0.25) + + conn.close() + safe_print("\n🎉 Delta message sync DONE") + + +# ============================== +if __name__ == "__main__": + main() diff --git a/12 Readallinbatches/10 Readallpozadavkyinbatches.py b/12 Readallinbatches/10 Readallpozadavkyinbatches.py new file mode 100644 index 0000000..f6387f6 --- /dev/null +++ b/12 Readallinbatches/10 Readallpozadavkyinbatches.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import pymysql +import requests +from pathlib import Path +from datetime import datetime +from dateutil import parser +import time +import sys + +# ================================ +# UTF-8 SAFE OUTPUT (Windows friendly) +# ================================ +try: + sys.stdout.reconfigure(encoding='utf-8') + sys.stderr.reconfigure(encoding='utf-8') +except AttributeError: + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + + +def safe_print(text: str): + enc = sys.stdout.encoding or "" + if not enc.lower().startswith("utf"): + text = ''.join(ch for ch in text if ord(ch) < 65536) + try: + print(text) + except UnicodeEncodeError: + text = ''.join(ch for ch in text if ord(ch) < 128) + print(text) + + +# ================================ +# 🔧 CONFIG +# ================================ +TOKEN_PATH = Path("token.txt") +CLINIC_SLUG = "mudr-buzalkova" + +BATCH_SIZE = 500 +STATES = ["ACTIVE", "DONE"] # explicitně – jinak API vrací jen ACTIVE + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +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 + } + } + } +} +""" + + +# ================================ +# 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 + + +# ================================ +# DATETIME PARSER +# ================================ +def to_mysql_dt(iso_str): + if not iso_str: + return None + try: + dt = parser.isoparse(iso_str) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=datetime.now().astimezone().tzinfo) + return dt.astimezone().strftime("%Y-%m-%d %H:%M:%S") + except Exception: + return None + + +# ================================ +# UPSERT +# ================================ +def upsert(conn, r): + p = r.get("extendedPatient") or {} + + api_updated = to_mysql_dt(r.get("updatedAt")) + msg_updated = to_mysql_dt((r.get("lastMessage") or {}).get("createdAt")) + + final_updated = max(filter(None, [api_updated, msg_updated]), default=None) + + 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(r.get("createdAt")), + final_updated, + to_mysql_dt(r.get("doneAt")), + to_mysql_dt(r.get("removedAt")), + p.get("name"), + p.get("surname"), + p.get("identificationNumber"), + ) + + with conn.cursor() as cur: + cur.execute(sql, vals) + conn.commit() + + +# ================================ +# FETCH PAGE (per state) +# ================================ +def fetch_state(headers, state, offset): + variables = { + "clinicSlug": CLINIC_SLUG, + "queueId": None, + "queueAssignment": "ANY", + "state": state, + "pageInfo": {"first": BATCH_SIZE, "offset": offset}, + "locale": "cs", + } + + 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()["data"]["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) + + safe_print(f"\n=== FULL Medevio READ-ALL sync @ {datetime.now():%Y-%m-%d %H:%M:%S} ===") + + grand_total = 0 + + for state in STATES: + safe_print(f"\n🔁 STATE = {state}") + offset = 0 + total = None + processed = 0 + + while True: + batch, count = fetch_state(headers, state, offset) + + if total is None: + total = count + safe_print(f"📡 {state}: celkem {total}") + + if not batch: + break + + for r in batch: + upsert(conn, r) + + processed += len(batch) + safe_print(f" • {processed}/{total}") + + offset += BATCH_SIZE + if offset >= count: + break + + time.sleep(0.4) + + grand_total += processed + + conn.close() + safe_print(f"\n✅ HOTOVO – celkem zpracováno {grand_total} požadavků\n") + + +# ================================ +if __name__ == "__main__": + main() diff --git a/10ReadPozadavky/PRAVIDELNE_3_StahniKomunikacifull.py b/12 Readallinbatches/PRAVIDELNE_3_StahniKomunikacifull.py similarity index 99% rename from 10ReadPozadavky/PRAVIDELNE_3_StahniKomunikacifull.py rename to 12 Readallinbatches/PRAVIDELNE_3_StahniKomunikacifull.py index 675e4a9..34550c0 100644 --- a/10ReadPozadavky/PRAVIDELNE_3_StahniKomunikacifull.py +++ b/12 Readallinbatches/PRAVIDELNE_3_StahniKomunikacifull.py @@ -21,7 +21,7 @@ import argparse # ============================== # 🔧 CONFIGURATION # ============================== -TOKEN_PATH = Path("token.txt") +TOKEN_PATH = Path("../10ReadPozadavky/token.txt") DB_CONFIG = { "host": "192.168.1.76", diff --git a/12 Readallinbatches/medevio_storage.json b/12 Readallinbatches/medevio_storage.json new file mode 100644 index 0000000..a0dc794 --- /dev/null +++ b/12 Readallinbatches/medevio_storage.json @@ -0,0 +1 @@ +{"cookies": [{"name": "gateway-access-token", "value": "YwBgkf8McREDKs7vCZj0EZD2fJsuV8RyDPtYx7WiDoz0nFJ9kxId8kcNEPBLFSwM+Tiz80+SOdFwo+oj", "domain": "my.medevio.cz", "path": "/", "expires": 1763372319, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "aws-waf-token", "value": "b6a1d4eb-4350-40e5-8e52-1f5f9600fbb8:CgoAr9pC8c6zAAAA:OYwXLY5OyitSQPl5v2oIlS+hIxsrb5LxV4VjCyE2gJCFFE5PQu+0Zbxse2ZIofrNv5QKs0TYUDTmxPhZyTr9Qtjnq2gsVQxWHXzrbebv3Z7RbzB63u6Ymn3Fo8IbDev3CfCNcNuxCKltFEXLqSCjI2vqNY+7HZkgQBIqy2wMgzli3aSLq0w8lWYtZzyyot7q8RPXWMGTfaBUo2reY0SOSffm9rAivE9PszNfPid71CvNrGAAoxRbwb25eVujlyIcDVWe5vZ9Iw==", "domain": ".my.medevio.cz", "path": "/", "expires": 1761125920, "httpOnly": false, "secure": true, "sameSite": "Lax"}], "origins": [{"origin": "https://my.medevio.cz", "localStorage": [{"name": "awswaf_token_refresh_timestamp", "value": "1760780309860"}, {"name": "awswaf_session_storage", "value": "b6a1d4eb-4350-40e5-8e52-1f5f9600fbb8:CgoAr9pC8c+zAAAA:+vw//1NzmePjPpbGCJzUB+orCRivtJd098DbDX4AnABiGRw/+ql6ShqvFY4YdCY7w2tegb5mEPBdAmc4sNi22kNR9BuEoAgCUiMhkU1AZWfzM51zPfTh7SveCrREZ7xdvxcqKPMmfVLRYX5E4+UWh22z/LKQ7+d9VERp3J+wWCUW3dFFirkezy3N7b2FVjTlY/RxsZwhejQziTG/L3CkIFFP3mOReNgBvDpj7aKoM1knY4IL4TZ8E7zNv3nTsvzACLYvnUutVOUcofN1TfOzwZshSKsEXsMzrQn8PzLccX1jM5VSzce7gfEzl0zSPsT8NB3Sna+rhMIttDNYgvbW1HsfG2LIeKMR27Zf8hkslDRVVkcU/Kp2jLOEdhhrBKGjKY2o9/uX3NExdzh5MEKQSSRtmue01BpWYILPH23rMsz4YSmF+Ough5OeQoC95rkcYwVXMhwvUN9Zfp9UZ4xCNfFUex5dOrg9aJntYRnaceeocGUttNI5AdT0i3+osV6XHXzKxeqO8zLCS9BIsCzxaHfdqqem5DorMceuGKz+QqksatIQAA=="}, {"name": "Application.Intl.locale", "value": "cs"}, {"name": "Password.prefill", "value": "{\"username\":\"vladimir.buzalka@buzalka.cz\",\"type\":\"email\"}"}]}]} \ No newline at end of file diff --git a/12 Readallinbatches/token.txt b/12 Readallinbatches/token.txt new file mode 100644 index 0000000..d31188b --- /dev/null +++ b/12 Readallinbatches/token.txt @@ -0,0 +1 @@ +nYvrvgflIKcDiQg8Hhpud+qG8iGZ8eH8su4nyT/Mgcm7XQp65ygY9s39+O01wIpk/7sKd6fBHkiKvsqH \ No newline at end of file diff --git a/12 Tower1/10 ReadAllActive.py b/12 Tower1/10 ReadAllActive.py new file mode 100644 index 0000000..483581e --- /dev/null +++ b/12 Tower1/10 ReadAllActive.py @@ -0,0 +1,214 @@ +#!/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 +import sys + +# Force UTF-8 output +try: + sys.stdout.reconfigure(encoding='utf-8') + sys.stderr.reconfigure(encoding='utf-8') +except AttributeError: + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + +# ================================ +# 🔧 CONFIGURATION +# ================================ +TOKEN_PATH = Path("token.txt") +CLINIC_SLUG = "mudr-buzalkova" +BATCH_SIZE = 100 + +DB_CONFIG = { + "host": "192.168.1.50", + "port": 3306, + "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): + if not iso_str: + return None + try: + dt = parser.isoparse(iso_str) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + 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 +# ================================ +def upsert(conn, r): + p = r.get("extendedPatient") or {} + 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")) + + 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() \ No newline at end of file diff --git a/12 Tower1/12 ReadAllinBatches.py b/12 Tower1/12 ReadAllinBatches.py new file mode 100644 index 0000000..b88ebdc --- /dev/null +++ b/12 Tower1/12 ReadAllinBatches.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import pymysql +import requests +from pathlib import Path +from datetime import datetime +from dateutil import parser +import time +import sys + +# ================================ +# UTF-8 SAFE OUTPUT (Windows friendly) +# ================================ +try: + sys.stdout.reconfigure(encoding='utf-8') + sys.stderr.reconfigure(encoding='utf-8') +except AttributeError: + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + + +def safe_print(text: str): + enc = sys.stdout.encoding or "" + if not enc.lower().startswith("utf"): + text = ''.join(ch for ch in text if ord(ch) < 65536) + try: + print(text) + except UnicodeEncodeError: + text = ''.join(ch for ch in text if ord(ch) < 128) + print(text) + + +# ================================ +# 🔧 CONFIG +# ================================ +TOKEN_PATH = Path("token.txt") +CLINIC_SLUG = "mudr-buzalkova" + +BATCH_SIZE = 500 +STATES = ["ACTIVE", "DONE"] # explicitně – jinak API vrací jen ACTIVE + +DB_CONFIG = { + "host": "192.168.1.50", + "port": 3306, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +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 + } + } + } +} +""" + + +# ================================ +# 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 + + +# ================================ +# DATETIME PARSER +# ================================ +def to_mysql_dt(iso_str): + if not iso_str: + return None + try: + dt = parser.isoparse(iso_str) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=datetime.now().astimezone().tzinfo) + return dt.astimezone().strftime("%Y-%m-%d %H:%M:%S") + except Exception: + return None + + +# ================================ +# UPSERT +# ================================ +def upsert(conn, r): + p = r.get("extendedPatient") or {} + + api_updated = to_mysql_dt(r.get("updatedAt")) + msg_updated = to_mysql_dt((r.get("lastMessage") or {}).get("createdAt")) + + final_updated = max(filter(None, [api_updated, msg_updated]), default=None) + + 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(r.get("createdAt")), + final_updated, + to_mysql_dt(r.get("doneAt")), + to_mysql_dt(r.get("removedAt")), + p.get("name"), + p.get("surname"), + p.get("identificationNumber"), + ) + + with conn.cursor() as cur: + cur.execute(sql, vals) + conn.commit() + + +# ================================ +# FETCH PAGE (per state) +# ================================ +def fetch_state(headers, state, offset): + variables = { + "clinicSlug": CLINIC_SLUG, + "queueId": None, + "queueAssignment": "ANY", + "state": state, + "pageInfo": {"first": BATCH_SIZE, "offset": offset}, + "locale": "cs", + } + + 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()["data"]["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) + + safe_print(f"\n=== FULL Medevio READ-ALL sync @ {datetime.now():%Y-%m-%d %H:%M:%S} ===") + + grand_total = 0 + + for state in STATES: + safe_print(f"\n🔁 STATE = {state}") + offset = 0 + total = None + processed = 0 + + while True: + batch, count = fetch_state(headers, state, offset) + + if total is None: + total = count + safe_print(f"📡 {state}: celkem {total}") + + if not batch: + break + + for r in batch: + upsert(conn, r) + + processed += len(batch) + safe_print(f" • {processed}/{total}") + + offset += BATCH_SIZE + if offset >= count: + break + + time.sleep(0.4) + + grand_total += processed + + conn.close() + safe_print(f"\n✅ HOTOVO – celkem zpracováno {grand_total} požadavků\n") + + +# ================================ +if __name__ == "__main__": + main() diff --git a/12 Tower1/20 ReadPoznamky.py b/12 Tower1/20 ReadPoznamky.py new file mode 100644 index 0000000..63ba4b8 --- /dev/null +++ b/12 Tower1/20 ReadPoznamky.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Download and store Medevio questionnaires (userNote + eCRF) for all patient requests. +Uses the verified working query "GetPatientRequest2". +""" + +import json +import requests +import pymysql +from datetime import datetime +from pathlib import Path +import time +import sys + +# Force UTF-8 output even under Windows Task Scheduler +try: + sys.stdout.reconfigure(encoding='utf-8') + sys.stderr.reconfigure(encoding='utf-8') +except AttributeError: + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + + +# ============================== +# 🛡 SAFE PRINT FOR CP1250 / EMOJI +# ============================== +def safe_print(text: str): + enc = sys.stdout.encoding or "" + if not enc.lower().startswith("utf"): + text = ''.join(ch for ch in text if ord(ch) < 65536) + try: + print(text) + except UnicodeEncodeError: + text = ''.join(ch for ch in text if ord(ch) < 128) + print(text) + + +# ============================== +# 🔧 CONFIGURATION (UPDATED TO 192.168.1.50) +# ============================== +TOKEN_PATH = Path("token.txt") +CLINIC_SLUG = "mudr-buzalkova" +GRAPHQL_URL = "https://api.medevio.cz/graphql" + +DB_CONFIG = { + "host": "192.168.1.50", + "port": 3306, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + + +# ============================== +# 🕒 DATETIME FIXER +# ============================== +def fix_datetime(dt_str): + if not dt_str: + return None + try: + return datetime.fromisoformat(dt_str.replace("Z", "").replace("+00:00", "")) + except Exception: + return None + + +# Optional filter +CREATED_AFTER = "2025-01-01" + + +# ============================== +# 🧮 HELPERS +# ============================== +def read_token(p: Path) -> str: + tok = p.read_text(encoding="utf-8").strip() + if tok.startswith("Bearer "): + return tok.split(" ", 1)[1] + return tok + + +GRAPHQL_QUERY = r""" +query GetPatientRequest2($requestId: UUID!, $clinicSlug: String!, $locale: Locale!) { + request: getPatientRequest2(patientRequestId: $requestId, clinicSlug: $clinicSlug) { + id + displayTitle(locale: $locale) + createdAt + updatedAt + userNote + eventType + extendedPatient(clinicSlug: $clinicSlug) { + name + surname + identificationNumber + } + ecrfFilledData(locale: $locale) { + name + groups { + label + fields { + name + label + type + value + } + } + } + } +} +""" + + +def fetch_questionnaire(headers, request_id, clinic_slug): + payload = { + "operationName": "GetPatientRequest2", + "query": GRAPHQL_QUERY, + "variables": { + "requestId": request_id, + "clinicSlug": clinic_slug, + "locale": "cs", + }, + } + r = requests.post(GRAPHQL_URL, json=payload, headers=headers, timeout=40) + if r.status_code != 200: + safe_print(f"❌ HTTP {r.status_code} for {request_id}: {r.text}") + return None + return r.json().get("data", {}).get("request") + + +def insert_questionnaire(cur, req): + if not req: + return + + patient = req.get("extendedPatient") or {} + ecrf_data = req.get("ecrfFilledData") + created_at = fix_datetime(req.get("createdAt")) + updated_at = fix_datetime(req.get("updatedAt")) + + cur.execute(""" + INSERT INTO medevio_questionnaires ( + request_id, created_at, updated_at, user_note, ecrf_json + ) + VALUES (%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + updated_at = VALUES(updated_at), + user_note = VALUES(user_note), + ecrf_json = VALUES(ecrf_json), + updated_local = NOW() + """, ( + req.get("id"), + created_at, + updated_at, + req.get("userNote"), + json.dumps(ecrf_data, ensure_ascii=False), + )) + + safe_print(f" 💾 Stored questionnaire for {patient.get('surname','')} {patient.get('name','')}") + + +# ============================== +# 🧠 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 list of requests from the table we just filled + with conn.cursor() as cur: + sql = """ + SELECT id, pacient_jmeno, pacient_prijmeni, createdAt, updatedAt, questionnaireprocessed + FROM pozadavky + WHERE (questionnaireprocessed IS NULL OR questionnaireprocessed < updatedAt) + """ + if CREATED_AFTER: + sql += " AND createdAt >= %s" + cur.execute(sql, (CREATED_AFTER,)) + else: + cur.execute(sql) + + rows = cur.fetchall() + + safe_print(f"📋 Found {len(rows)} requests needing questionnaire check.") + + for i, row in enumerate(rows, 1): + req_id = row["id"] + safe_print(f"\n[{i}/{len(rows)}] 🔍 Fetching questionnaire for {req_id} ...") + + req = fetch_questionnaire(headers, req_id, CLINIC_SLUG) + if not req: + safe_print(" ⚠️ No questionnaire data found.") + continue + + with conn.cursor() as cur: + insert_questionnaire(cur, req) + cur.execute( + "UPDATE pozadavky SET questionnaireprocessed = NOW() WHERE id = %s", + (req_id,) + ) + conn.commit() + + time.sleep(0.6) + + conn.close() + safe_print("\n✅ Done! All questionnaires stored in MySQL table `medevio_questionnaires`.") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/12 Tower1/30 ReadConversation.py b/12 Tower1/30 ReadConversation.py new file mode 100644 index 0000000..d1e4371 --- /dev/null +++ b/12 Tower1/30 ReadConversation.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import json +import requests +import pymysql +from pathlib import Path +from datetime import datetime +import time +import sys + +# UTF-8 SAFE OUTPUT +try: + sys.stdout.reconfigure(encoding='utf-8') + sys.stderr.reconfigure(encoding='utf-8') +except AttributeError: + pass + +# ============================== +# CONFIG (.50) +# ============================== +TOKEN_PATH = Path("token.txt") + +DB_CONFIG = { + "host": "192.168.1.50", + "port": 3306, + "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 createdAt updatedAt } + } +} +""" + +def parse_dt(s): + if not s: return None + try: return datetime.fromisoformat(s.replace("Z", "+00:00")) + except: return None + +def read_token(path: Path) -> str: + return path.read_text(encoding="utf-8").strip().replace("Bearer ", "") + +def main(): + token = read_token(TOKEN_PATH) + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + conn = pymysql.connect(**DB_CONFIG) + + # 1. Seznam již stažených příloh (prevence duplicit) + with conn.cursor() as cur: + cur.execute("SELECT attachment_id FROM medevio_downloads") + existing_ids = {r["attachment_id"] for r in cur.fetchall()} + + # 2. Seznam požadavků k synchronizaci + with conn.cursor() as cur: + cur.execute(""" + SELECT id, messagesProcessed FROM pozadavky + WHERE messagesProcessed IS NULL OR messagesProcessed < updatedAt + """) + rows = cur.fetchall() + + print(f"📋 Počet požadavků k synchronizaci zpráv: {len(rows)}") + + for i, row in enumerate(rows, 1): + req_id = row["id"] + updated_since = row["messagesProcessed"] + if updated_since: + updated_since = updated_since.replace(microsecond=0).isoformat() + "Z" + + print(f"[{i}/{len(rows)}] Synchronizuji: {req_id}") + + payload = { + "operationName": "UseMessages_ListMessages", + "query": GRAPHQL_QUERY_MESSAGES, + "variables": {"requestId": req_id, "updatedSince": updated_since} + } + + try: + r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30) + messages = r.json().get("data", {}).get("messages", []) or [] + + if messages: + with conn.cursor() as cur: + for msg in messages: + # Uložení zprávy + sender = msg.get("sender") or {} + sender_name = " ".join(filter(None, [sender.get("name"), sender.get("surname")])) + mr = msg.get("medicalRecord") or {} + + cur.execute(""" + 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 + text = VALUES(text), updated_at = VALUES(updated_at), read_at = VALUES(read_at) + """, ( + 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") + )) + + # Uložení přílohy (pokud existuje a nemáme ji) + attachment_id = mr.get("id") + if attachment_id and attachment_id not in existing_ids: + url = mr.get("downloadUrl") or mr.get("url") + if url: + att_r = requests.get(url, timeout=30) + if att_r.status_code == 200: + 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) + """, ( + req_id, attachment_id, "MESSAGE_ATTACHMENT", + url.split("/")[-1].split("?")[0], mr.get("contentType"), + len(att_r.content), parse_dt(msg.get("createdAt")), att_r.content + )) + existing_ids.add(attachment_id) + + cur.execute("UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s", (req_id,)) + conn.commit() + else: + with conn.cursor() as cur: + cur.execute("UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s", (req_id,)) + conn.commit() + + time.sleep(0.3) + except Exception as e: + print(f" ❌ Chyba u {req_id}: {e}") + + conn.close() + print("\n🎉 Delta sync zpráv a příloh DOKONČEN") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/12 Tower1/40 DownloadPrilohy.py b/12 Tower1/40 DownloadPrilohy.py new file mode 100644 index 0000000..2507099 --- /dev/null +++ b/12 Tower1/40 DownloadPrilohy.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Download all attachments for pozadavky where attachmentsProcessed IS NULL +Store them in MySQL table `medevio_downloads` on 192.168.1.50. +""" + +import zlib +import json +import requests +import pymysql +from pathlib import Path +from datetime import datetime +import time +import sys + +# Force UTF-8 output +try: + sys.stdout.reconfigure(encoding='utf-8') + sys.stderr.reconfigure(encoding='utf-8') +except AttributeError: + pass + + +# ============================== +# 🛡 SAFE PRINT +# ============================== +def safe_print(text: str): + enc = sys.stdout.encoding or "" + if not enc or not enc.lower().startswith("utf"): + text = ''.join(ch for ch in text if ord(ch) < 65536) + try: + print(text) + except UnicodeEncodeError: + text = ''.join(ch for ch in text if ord(ch) < 128) + print(text) + + +# ============================== +# 🔧 CONFIGURATION (.50) +# ============================== +TOKEN_PATH = Path("token.txt") +CLINIC_SLUG = "mudr-buzalkova" + +DB_CONFIG = { + "host": "192.168.1.50", + "port": 3306, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +CREATED_AFTER = "2024-12-01" + +GRAPHQL_QUERY = r""" +query ClinicRequestDetail_GetPatientRequest2($requestId: UUID!) { + patientRequestMedicalRecords: listMedicalRecordsForPatientRequest( + attachmentTypes: [ECRF_FILL_ATTACHMENT, MESSAGE_ATTACHMENT, PATIENT_REQUEST_ATTACHMENT] + patientRequestId: $requestId + pageInfo: {first: 100, offset: 0} + ) { + attachmentType + id + medicalRecord { + contentType + description + downloadUrl + id + url + visibleToPatient + } + } +} +""" + + +def extract_filename_from_url(url: str) -> str: + try: + return url.split("/")[-1].split("?")[0] + except: + return "unknown_filename" + + +def read_token(p: Path) -> str: + tok = p.read_text(encoding="utf-8").strip() + return tok.split(" ", 1)[1] if tok.startswith("Bearer ") else tok + + +def main(): + token = read_token(TOKEN_PATH) + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + conn = pymysql.connect(**DB_CONFIG) + + # 1. Načtení ID již stažených příloh + with conn.cursor() as cur: + cur.execute("SELECT attachment_id FROM medevio_downloads") + existing_ids = {row["attachment_id"] for row in cur.fetchall()} + + safe_print(f"✅ V databázi již máme {len(existing_ids)} příloh.") + + # 2. Výběr požadavků ke zpracování + sql = "SELECT id, pacient_prijmeni, pacient_jmeno, createdAt FROM pozadavky WHERE attachmentsProcessed IS NULL" + params = [] + if CREATED_AFTER: + sql += " AND createdAt >= %s" + params.append(CREATED_AFTER) + + with conn.cursor() as cur: + cur.execute(sql, params) + req_rows = cur.fetchall() + + safe_print(f"📋 Počet požadavků ke stažení příloh: {len(req_rows)}") + + for i, row in enumerate(req_rows, 1): + req_id = row["id"] + prijmeni = row.get("pacient_prijmeni") or "Neznamy" + created_date = row.get("createdAt") or datetime.now() + + safe_print(f"\n[{i}/{len(req_rows)}] 🧾 {prijmeni} ({req_id})") + + payload = { + "operationName": "ClinicRequestDetail_GetPatientRequest2", + "query": GRAPHQL_QUERY, + "variables": {"requestId": req_id}, + } + + try: + r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30) + attachments = r.json().get("data", {}).get("patientRequestMedicalRecords", []) + + if attachments: + with conn.cursor() as cur: + for a in attachments: + m = a.get("medicalRecord") or {} + att_id = a.get("id") + + if att_id in existing_ids: + continue + + url = m.get("downloadUrl") + if url: + att_r = requests.get(url, timeout=30) + if att_r.status_code == 200: + content = att_r.content + filename = extract_filename_from_url(url) + + 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) + """, (req_id, att_id, a.get("attachmentType"), filename, + m.get("contentType"), len(content), created_date, content)) + existing_ids.add(att_id) + safe_print(f" 💾 Uloženo: {filename} ({len(content) / 1024:.1f} kB)") + + conn.commit() + + # Označíme jako zpracované i když nebyly nalezeny žádné přílohy + with conn.cursor() as cur: + cur.execute("UPDATE pozadavky SET attachmentsProcessed = NOW() WHERE id = %s", (req_id,)) + conn.commit() + + time.sleep(0.3) + except Exception as e: + print(f" ❌ Chyba u {req_id}: {e}") + + conn.close() + safe_print("\n🎯 Všechny přílohy byly zpracovány.") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/12 Tower1/50 SaveToFileSystem incremental.py b/12 Tower1/50 SaveToFileSystem incremental.py new file mode 100644 index 0000000..831ceb9 --- /dev/null +++ b/12 Tower1/50 SaveToFileSystem incremental.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import shutil +import pymysql +import re +from pathlib import Path +from datetime import datetime +import time +import sys + +# Force UTF-8 output even under Windows Task Scheduler +import sys +try: + sys.stdout.reconfigure(encoding='utf-8') + sys.stderr.reconfigure(encoding='utf-8') +except AttributeError: + # Python < 3.7 fallback (not needed for you, but safe) + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + +# ============================== +# 🛡 SAFE PRINT FOR CP1250 / EMOJI +# ============================== +def safe_print(text: str = ""): + enc = sys.stdout.encoding or "" + if not enc.lower().startswith("utf"): + # Strip emoji and characters outside BMP for Task Scheduler + text = ''.join(ch for ch in text if ord(ch) < 65536) + try: + print(text) + except UnicodeEncodeError: + # ASCII fallback + text = ''.join(ch for ch in text if ord(ch) < 128) + print(text) + + +# ============================== +# ⚙️ CONFIGURATION +# ============================== +DB_CONFIG = { + "host": "192.168.1.50", + "port": 3306, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", +} + +BASE_DIR = Path(r"u:\Dropbox\Ordinace\Dokumentace_ke_zpracování\MP") +BASE_DIR.mkdir(parents=True, exist_ok=True) + + +def sanitize_name(name: str) -> str: + """Replace invalid filename characters with underscore.""" + return re.sub(r'[<>:"/\\|?*\x00-\x1F]', "_", name).strip() + + +def make_abbrev(title: str) -> str: + if not title: + return "" + words = re.findall(r"[A-Za-zÁ-Žá-ž0-9]+", title) + abbr = "" + for w in words: + if w.isdigit(): + abbr += w + else: + abbr += w[0] + return abbr.upper() + + +# ============================== +# 🧹 DELETE UNEXPECTED FILES +# ============================== +def clean_folder(folder: Path, valid_files: set): + if not folder.exists(): + return + + for f in folder.iterdir(): + if f.is_file(): + if f.name.startswith("▲"): + continue + sanitized = sanitize_name(f.name) + if sanitized not in valid_files: + safe_print(f"🗑️ Removing unexpected file: {f.name}") + try: + f.unlink() + except Exception as e: + safe_print(f"⚠️ Could not delete {f}: {e}") + + +# ============================== +# 📦 DB CONNECTION +# ============================== +conn = pymysql.connect(**DB_CONFIG) + +cur_meta = conn.cursor(pymysql.cursors.DictCursor) +cur_blob = conn.cursor() + +safe_print("🔍 Loading metadata from DB (FAST)…") + +cur_meta.execute(""" + SELECT d.id AS download_id, + d.request_id, + d.filename, + d.created_at, + p.updatedAt AS req_updated_at, + p.pacient_jmeno AS jmeno, + p.pacient_prijmeni AS prijmeni, + p.displayTitle + FROM medevio_downloads d + JOIN pozadavky p ON d.request_id = p.id + ORDER BY p.updatedAt DESC +""") + +rows = cur_meta.fetchall() +safe_print(f"📋 Found {len(rows)} attachment records.\n") + +# ============================== +# 🧠 MAIN LOOP WITH PROGRESS +# ============================== + +unique_request_ids = [] +seen = set() +for r in rows: + req_id = r["request_id"] + if req_id not in seen: + unique_request_ids.append(req_id) + seen.add(req_id) + +total_requests = len(unique_request_ids) +safe_print(f"🔄 Processing {total_requests} unique requests...\n") + +processed_requests = set() +current_index = 0 + +for r in rows: + req_id = r["request_id"] + + if req_id in processed_requests: + continue + processed_requests.add(req_id) + + current_index += 1 + percent = (current_index / total_requests) * 100 + + safe_print(f"\n[ {percent:5.1f}% ] Processing request {current_index} / {total_requests} → {req_id}") + + # ========== FETCH VALID FILENAMES ========== + cur_meta.execute( + "SELECT filename FROM medevio_downloads WHERE request_id=%s", + (req_id,) + ) + valid_files = {sanitize_name(row["filename"]) for row in cur_meta.fetchall()} + + # ========== BUILD FOLDER NAME ========== + updated_at = r["req_updated_at"] or datetime.now() + date_str = updated_at.strftime("%Y-%m-%d") + + prijmeni = sanitize_name(r["prijmeni"] or "Unknown") + jmeno = sanitize_name(r["jmeno"] or "") + title = r.get("displayTitle") or "" + abbr = make_abbrev(title) + + clean_folder_name = sanitize_name( + f"{date_str} {prijmeni}, {jmeno} [{abbr}] {req_id}" + ) + + # ========== DETECT EXISTING FOLDER ========== + existing_folder = None + + for f in BASE_DIR.iterdir(): + if f.is_dir() and req_id in f.name: + existing_folder = f + break + + main_folder = existing_folder if existing_folder else BASE_DIR / clean_folder_name + + # ========== MERGE DUPLICATES ========== + possible_dups = [ + f for f in BASE_DIR.iterdir() + if f.is_dir() and req_id in f.name and f != main_folder + ] + + for dup in possible_dups: + safe_print(f"♻️ Merging duplicate folder: {dup.name}") + + clean_folder(dup, valid_files) + main_folder.mkdir(parents=True, exist_ok=True) + + for f in dup.iterdir(): + if f.is_file(): + target = main_folder / f.name + if not target.exists(): + f.rename(target) + + shutil.rmtree(dup, ignore_errors=True) + + # ========== CLEAN MAIN FOLDER ========== + clean_folder(main_folder, valid_files) + + # ========== DOWNLOAD MISSING FILES ========== + added_new_file = False + main_folder.mkdir(parents=True, exist_ok=True) + + for filename in valid_files: + dest_plain = main_folder / filename + dest_marked = main_folder / ("▲" + filename) + + if dest_plain.exists() or dest_marked.exists(): + continue + + added_new_file = True + + cur_blob.execute( + "SELECT file_content FROM medevio_downloads " + "WHERE request_id=%s AND filename=%s", + (req_id, filename) + ) + row = cur_blob.fetchone() + if not row: + continue + + content = row[0] + if not content: + continue + + with open(dest_plain, "wb") as f: + f.write(content) + + safe_print(f"💾 Wrote: {dest_plain.relative_to(BASE_DIR)}") + + # ========== REMOVE ▲ FLAG IF NEW FILES ADDED ========== + if added_new_file and "▲" in main_folder.name: + new_name = main_folder.name.replace("▲", "").strip() + new_path = main_folder.parent / new_name + + if new_path != main_folder: + try: + main_folder.rename(new_path) + safe_print(f"🔄 Folder flag ▲ removed → {new_name}") + main_folder = new_path + except Exception as e: + safe_print(f"⚠️ Could not rename folder: {e}") + +safe_print("\n🎯 Export complete.\n") + +cur_blob.close() +cur_meta.close() +conn.close() diff --git a/12 Tower1/50 SaveToFileSystem single step.py b/12 Tower1/50 SaveToFileSystem single step.py new file mode 100644 index 0000000..65bfd4f --- /dev/null +++ b/12 Tower1/50 SaveToFileSystem single step.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import shutil +import pymysql +import re +from pathlib import Path +from datetime import datetime + +# ============================== +# ⚙️ CONFIGURATION +# ============================== +DB_CONFIG = { + "host": "192.168.1.50", + "port": 3306, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", +} + +BASE_DIR = Path(r"u:\Dropbox\Ordinace\Dokumentace_ke_zpracování\MP") +BASE_DIR.mkdir(parents=True, exist_ok=True) + + +def sanitize_name(name: str) -> str: + return re.sub(r'[<>:"/\\|?*\x00-\x1F]', "_", name).strip() + + +def clean_folder(folder: Path, valid_files: set): + """Remove files that do NOT exist in MySQL for this request.""" + if not folder.exists(): + return + + for f in folder.iterdir(): + if f.is_file() and sanitize_name(f.name) not in valid_files: + print(f"🗑️ Removing unexpected file: {f.name}") + try: + f.unlink() + except Exception as e: + print(f"⚠️ Cannot delete {f}: {e}") + + +# ============================== +# 📥 LOAD EVERYTHING IN ONE QUERY +# ============================== +conn = pymysql.connect(**DB_CONFIG) +cur = conn.cursor(pymysql.cursors.DictCursor) + +print("📥 Loading ALL metadata + BLOBs with ONE MySQL query…") + +cur.execute(""" + SELECT + d.id AS download_id, + d.request_id, + d.filename, + d.file_content, + p.updatedAt AS req_updated_at, + p.pacient_jmeno AS jmeno, + p.pacient_prijmeni AS prijmeni + FROM medevio_downloads d + JOIN pozadavky p ON d.request_id = p.id + ORDER BY p.updatedAt DESC, d.created_at ASC +""") + +rows = cur.fetchall() +print(f"📦 Loaded {len(rows)} total file rows.\n") + +conn.close() + +# ============================== +# 🔄 ORGANIZE ROWS PER REQUEST +# ============================== +requests = {} # req_id → list of file dicts + +for r in rows: + req_id = r["request_id"] + if req_id not in requests: + requests[req_id] = [] + requests[req_id].append(r) + +print(f"📌 Unique requests: {len(requests)}\n") + +# ============================== +# 🧠 MAIN LOOP – SAME LOGIC AS BEFORE +# ============================== +for req_id, filelist in requests.items(): + + # ========== GET UPDATEDAT (same logic) ========== + any_row = filelist[0] + updated_at = any_row["req_updated_at"] or datetime.now() + date_str = updated_at.strftime("%Y-%m-%d") + + prijmeni = sanitize_name(any_row["prijmeni"] or "Unknown") + jmeno = sanitize_name(any_row["jmeno"] or "") + + folder_name = sanitize_name(f"{date_str} {prijmeni}, {jmeno} {req_id}") + main_folder = BASE_DIR / folder_name + + # ========== VALID FILES ========== + valid_files = {sanitize_name(r["filename"]) for r in filelist} + + # ========== FIND OLD FOLDERS ========== + possible_dups = [ + f for f in BASE_DIR.iterdir() + if f.is_dir() and req_id in f.name and f != main_folder + ] + + # ========== MERGE OLD FOLDERS ========== + for dup in possible_dups: + print(f"♻️ Merging folder: {dup.name}") + + clean_folder(dup, valid_files) + main_folder.mkdir(parents=True, exist_ok=True) + + for f in dup.iterdir(): + if f.is_file(): + target = main_folder / f.name + if not target.exists(): + f.rename(target) + + shutil.rmtree(dup, ignore_errors=True) + + # ========== CLEAN MAIN FOLDER ========== + main_folder.mkdir(parents=True, exist_ok=True) + clean_folder(main_folder, valid_files) + + # ========== SAVE FILES (fast now) ========== + for r in filelist: + filename = sanitize_name(r["filename"]) + dest = main_folder / filename + + if dest.exists(): + continue + + content = r["file_content"] + if not content: + continue + + with open(dest, "wb") as f: + f.write(content) + + print(f"💾 Saved: {dest.relative_to(BASE_DIR)}") + +print("\n🎯 Export complete.\n") diff --git a/12 Tower1/token.txt b/12 Tower1/token.txt new file mode 100644 index 0000000..d31188b --- /dev/null +++ b/12 Tower1/token.txt @@ -0,0 +1 @@ +nYvrvgflIKcDiQg8Hhpud+qG8iGZ8eH8su4nyT/Mgcm7XQp65ygY9s39+O01wIpk/7sKd6fBHkiKvsqH \ No newline at end of file diff --git a/30 ManipulacePoznámek/102 Zapiš ID request do nejnovějšího otevřeného požadavku.py b/30 ManipulacePoznámek/102 Zapiš ID request do nejnovějšího otevřeného požadavku.py index ec3254e..66db1fb 100644 --- a/30 ManipulacePoznámek/102 Zapiš ID request do nejnovějšího otevřeného požadavku.py +++ b/30 ManipulacePoznámek/102 Zapiš ID request do nejnovějšího otevřeného požadavku.py @@ -5,6 +5,7 @@ import requests import mysql.connector from pathlib import Path import sys +from datetime import datetime # UTF-8 handling try: @@ -18,6 +19,11 @@ except: TOKEN_PATH = Path("token.txt") GRAPHQL_URL = "https://api.medevio.cz/graphql" +# --- ZPRACOVÁNÍ --- +# Zadejte počet požadavků ke zpracování. +# 0 znamená zpracovat VŠECHNY nesynchronizované požadavky. +PROCESS_LIMIT = 10 # <-- Používáme PROCESS_LIMIT + # --- MySQL DB --- DB_CONFIG = { "host": "192.168.1.76", @@ -40,51 +46,86 @@ def read_token(p: Path) -> str: # === DB Funkce === -def get_latest_open_request_id_from_db(): +def get_requests_to_process_from_db(limit): """ - Získá ID, Titul, Jméno a Příjmení nejnovějšího otevřeného požadavku z MySQL. + Získá seznam požadavků (ID, Titul, Jméno, Příjmení) k synchronizaci z MySQL. + Použije LIMIT, pokud limit > 0. """ - print("🔍 Připojuji se k MySQL a hledám ID nejnovějšího otevřeného požadavku...") + if limit == 0: + print("🔍 Připojuji se k MySQL a hledám **VŠECHNY** nesynchronizované požadavky...") + else: + print(f"🔍 Připojuji se k MySQL a hledám **{limit}** nesynchronizovaných požadavků...") + + requests_list = [] + conn = None try: conn = mysql.connector.connect(**DB_CONFIG) cursor = conn.cursor() - # SQL dotaz: Nyní vybíráme navíc jméno a příjmení pacienta + # Základní SQL dotaz query = """ SELECT id, displayTitle, pacient_jmeno, pacient_prijmeni FROM pozadavky WHERE doneAt IS NULL + AND noteSyncedAt IS NULL ORDER BY updatedAt DESC - LIMIT 1; """ + # Podmíněné přidání LIMIT klauzule + if limit > 0: + query += f"LIMIT {limit};" + else: + query += ";" + cursor.execute(query) - result = cursor.fetchone() + results = cursor.fetchall() - cursor.close() - conn.close() - - if result: + for result in results: request_id, display_title, jmeno, prijmeni = result - print(f"✅ Nalezen požadavek ID: {request_id} (Titul: {display_title})") - print(f" Pacient: **{prijmeni} {jmeno}**") # Vypíšeme pro snadnou kontrolu - return { + requests_list.append({ "id": request_id, "displayTitle": display_title, "jmeno": jmeno, "prijmeni": prijmeni - } + }) - print("❌ Nebyl nalezen žádný otevřený požadavek v DB.") - return None + cursor.close() + + if requests_list: + print(f"✅ Nalezeno {len(requests_list)} požadavků ke zpracování.") + else: + print("❌ Nebyl nalezen žádný nesynchronizovaný otevřený požadavek v DB.") + + return requests_list except mysql.connector.Error as err: print(f"❌ Chyba při připojení/dotazu MySQL: {err}") - return None + return [] + finally: + if conn and conn.is_connected(): + conn.close() -# === GraphQL Operace === -# Tyto GraphQL dotazy jsou beze změny +def update_db_sync_time(request_id, conn): + """Aktualizuje sloupec noteSyncedAt v tabulce pozadavky. Používá existující připojení.""" + cursor = conn.cursor() + + current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + update_query = """ + UPDATE pozadavky + SET noteSyncedAt = %s + WHERE id = %s; + """ + + cursor.execute(update_query, (current_time, request_id)) + conn.commit() + + cursor.close() + print(f" (DB: Čas synchronizace pro {request_id} uložen)") + + +# === GraphQL Operace (Beze Změny) === QUERY_GET_NOTE = r""" query ClinicRequestNotes_Get($patientRequestId: String!) { @@ -103,6 +144,14 @@ mutation ClinicRequestNotes_Update($noteInput: UpdateClinicPatientRequestNoteInp } """ +MUTATION_CREATE_NOTE = r""" +mutation ClinicRequestNotes_Create($noteInput: CreateClinicPatientRequestNoteInput!) { + createClinicPatientRequestNote(noteInput: $noteInput) { + id + } +} +""" + def gql(query, variables, token): """Obecná funkce pro volání GraphQL endpointu.""" @@ -120,63 +169,92 @@ def gql(query, variables, token): def get_internal_note(request_id, token): """Získá jedinou interní poznámku (obsah a ID) pro daný požadavek.""" - print(f"🔍 Načítám poznámku z Medevia k požadavku {request_id}...") data = gql(QUERY_GET_NOTE, {"patientRequestId": request_id}, token) notes = data.get("data", {}).get("notes", []) - - if notes: - print("✅ Interní poznámka nalezena.") - return notes[0] - - print(f"⚠️ Interní poznámka pro požadavek {request_id} neexistuje.") - return None + return notes[0] if notes else None def update_internal_note(note_id, new_content, token): """Aktualizuje obsah poznámky v Medeviu.""" variables = {"noteInput": {"id": note_id, "content": new_content}} - print(f"📝 Odesílám aktualizaci poznámky {note_id}...") return gql(MUTATION_UPDATE_NOTE, variables, token) +def create_internal_note(request_id, content, token): + """Vytvoří novou interní poznámku k požadavku v Medeviu.""" + variables = {"noteInput": {"requestId": request_id, "content": content}} + return gql(MUTATION_CREATE_NOTE, variables, token) + + # === MAIN === def main(): token = read_token(TOKEN_PATH) - # 1. Zjistit ID a jméno pacienta z TVÉ DB - latest_request = get_latest_open_request_id_from_db() - if not latest_request: + # 1. Získat seznam ID požadavků ke zpracování (používáme PROCESS_LIMIT) + requests_to_process = get_requests_to_process_from_db(PROCESS_LIMIT) + + if not requests_to_process: return - request_id = latest_request["id"] + # Pro update DB time otevřeme připojení jednou a použijeme ho v cyklu + conn = mysql.connector.connect(**DB_CONFIG) - # 2. Získat existující interní poznámku z Medevia - note = get_internal_note(request_id, token) - if not note: - return + print("\n=============================================") + print(f"START ZPRACOVÁNÍ {len(requests_to_process)} POŽADAVKŮ") + print("=============================================\n") - note_id = note["id"] - old_content = note["content"] or "" + for idx, request in enumerate(requests_to_process, 1): + request_id = request["id"] - # 3. Vytvořit nový obsah (ID požadavku jako první řádek) - # Text, který vložíme na začátek - prepend_text = f"ID DB Synchronizace: {request_id}\n" - new_content = prepend_text + old_content + print( + f"[{idx}/{len(requests_to_process)}] Zpracovávám požadavek: {request['prijmeni']} {request['jmeno']} (ID: {request_id})") - print("--- Nový obsah který odešlu (začátek) ---") - print(f"-> {prepend_text.strip()}") - print("------------------------------------------") + # 2. Vytvořit text, který chceme přidat/vytvořit + prepend_text = f"ID: {request_id}\n" - # 4. Aktualizovat poznámku v Medeviu - try: - update_internal_note(note_id, new_content, token) - print(f"\n✅ Úspěch! Poznámka {note_id} k požadavku {request_id} byla aktualizována v Medeviu.") - print(f" **Zkontroluj požadavek pacienta: {latest_request['prijmeni']} {latest_request['jmeno']}**") - except requests.exceptions.HTTPError as e: - print(f"\n❌ Chyba při aktualizaci Medevio API: {e}") - except Exception as e: - print(f"\n❌ Neočekávaná chyba: {e}") + # 3. Pokusit se získat existující interní poznámku z Medevia + note = get_internal_note(request_id, token) + + medevio_update_success = False + + if note: + # A) POZNÁMKA EXISTUJE -> AKTUALIZOVAT + note_id = note["id"] + old_content = note["content"] or "" + new_content = prepend_text + old_content + + try: + # Odeslání aktualizace + update_internal_note(note_id, new_content, token) + print(f" (Medevio: Poznámka {note_id} **aktualizována**.)") + medevio_update_success = True + except requests.exceptions.HTTPError as e: + print(f" ❌ Chyba při aktualizaci Medevio API: {e}") + + else: + # B) POZNÁMKA NEEXISTUJE -> VYTVOŘIT + new_content = prepend_text.strip() + + try: + # Odeslání vytvoření + result = create_internal_note(request_id, new_content, token) + new_note_id = result.get("data", {}).get("createClinicPatientRequestNote", {}).get("id", "N/A") + print(f" (Medevio: Nová poznámka {new_note_id} **vytvořena**.)") + medevio_update_success = True + except requests.exceptions.HTTPError as e: + print(f" ❌ Chyba při vytváření Medevio API: {e}") + + # 4. AKTUALIZACE ČASOVÉHO RAZÍTKA V DB + if medevio_update_success: + update_db_sync_time(request_id, conn) + + print("---------------------------------------------") + + # Uzavřeme připojení k DB po dokončení cyklu + if conn and conn.is_connected(): + conn.close() + print("\n✅ Všechny požadavky zpracovány. Připojení k DB uzavřeno.") if __name__ == "__main__": diff --git a/40 agenda a požadavky/871 test.py b/40 agenda a požadavky/Report_AgendaPozadavky.py similarity index 86% rename from 40 agenda a požadavky/871 test.py rename to 40 agenda a požadavky/Report_AgendaPozadavky.py index c3c4bf8..dab86c2 100644 --- a/40 agenda a požadavky/871 test.py +++ b/40 agenda a požadavky/Report_AgendaPozadavky.py @@ -30,7 +30,7 @@ CLINIC_SLUG = "mudr-buzalkova" DB_CONFIG = { "host": "192.168.1.76", - "port": 3307, + "port": 3306, "user": "root", "password": "Vlado9674+", "database": "medevio", @@ -38,28 +38,24 @@ DB_CONFIG = { "cursorclass": pymysql.cursors.DictCursor, } -EXPORT_DIR = Path(r"z:\Dropbox\Ordinace\Reporty") +EXPORT_DIR = Path(r"u:\Dropbox\Ordinace\Reporty") EXPORT_DIR.mkdir(exist_ok=True, parents=True) -timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") -xlsx_path = EXPORT_DIR / f"{timestamp} Agenda + Pozadavky (Merged).xlsx" + +# Delete previous reports +for old in EXPORT_DIR.glob("* Agenda + Požadavky.xlsx"): + old.unlink() + print(f"🗑️ Deleted old report: {old.name}") + +timestamp = datetime.now().strftime("%Y-%m-%d %H-%M-%S") +xlsx_path = EXPORT_DIR / f"{timestamp} Agenda + Požadavky.xlsx" # ==================== LOAD TOKEN ==================== -def load_gateway_token(storage_path="medevio_storage.json"): - path = Path(storage_path) - if not path.exists(): - raise SystemExit(f"❌ Storage file not found: {path}") - with path.open("r", encoding="utf-8") as f: - state = json.load(f) - token = next( - (c["value"] for c in state["cookies"] if c["name"] == "gateway-access-token"), - None, - ) - if not token: - raise SystemExit("❌ gateway-access-token not found in storage file.") - return token - - -gateway_token = load_gateway_token() +TOKEN_PATH = Path("token.txt") +if not TOKEN_PATH.exists(): + TOKEN_PATH = Path(__file__).parent / "token.txt" +if not TOKEN_PATH.exists(): + raise SystemExit(f"❌ token.txt not found") +gateway_token = TOKEN_PATH.read_text(encoding="utf-8").strip() headers = { "content-type": "application/json", @@ -80,8 +76,18 @@ thin_border = Border( ) +REQUEST_URL_TEMPLATE = "https://my.medevio.cz/mudr-buzalkova/klinika/pozadavky?pozadavek={}" +link_font = Font(color="0563C1", underline="single") + + def format_ws(ws, df): """Apply unified formatting to a worksheet.""" + # Find Request_ID column index (1-based) + req_id_col = None + columns = list(df.columns) + if "Request_ID" in columns: + req_id_col = columns.index("Request_ID") + 1 + for col_idx in range(1, len(df.columns) + 1): col_letter = get_column_letter(col_idx) cell = ws.cell(row=1, column=col_idx) @@ -96,6 +102,10 @@ def format_ws(ws, df): cell.border = thin_border if r_idx % 2 == 0: cell.fill = alt_fill + # Add hyperlink to Request_ID cells + if req_id_col and cell.column == req_id_col and cell.value: + cell.hyperlink = REQUEST_URL_TEMPLATE.format(cell.value) + cell.font = link_font ws.freeze_panes = "A2" @@ -138,7 +148,12 @@ payload = { r = requests.post(GRAPHQL_URL, headers=headers, data=json.dumps(payload)) r.raise_for_status() -reservations = r.json()["data"]["reservations"] +resp = r.json() +if "errors" in resp or "data" not in resp: + print("❌ API response:") + print(json.dumps(resp, indent=2, ensure_ascii=False)) + raise SystemExit("API call failed - check token or query.") +reservations = resp["data"]["reservations"] rows = [] for r in reservations: diff --git a/40 agenda a požadavky/check_mysql.py b/40 agenda a požadavky/check_mysql.py new file mode 100644 index 0000000..df344ad --- /dev/null +++ b/40 agenda a požadavky/check_mysql.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Check one request in MySQL.""" + +import pymysql +import json + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3306, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +REQUEST_ID = "6b46b5a8-b080-4821-86b0-39adabeec86b" + +conn = pymysql.connect(**DB_CONFIG) +with conn.cursor() as cur: + cur.execute("SELECT * FROM pozadavky WHERE id = %s", (REQUEST_ID,)) + row = cur.fetchone() +conn.close() + +if row: + # Convert datetime objects to strings for JSON serialization + for k, v in row.items(): + if hasattr(v, 'isoformat'): + row[k] = v.isoformat() + print(json.dumps(row, indent=2, ensure_ascii=False, default=str)) +else: + print(f"Not found: {REQUEST_ID}") diff --git a/40 agenda a požadavky/check_request.py b/40 agenda a požadavky/check_request.py new file mode 100644 index 0000000..4acbed4 --- /dev/null +++ b/40 agenda a požadavky/check_request.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Quick check: fetch one request from Medevio API and print all fields.""" + +import json +import requests +from pathlib import Path + +TOKEN_PATH = Path(__file__).parent / "token.txt" +GRAPHQL_URL = "https://api.medevio.cz/graphql" +CLINIC_SLUG = "mudr-buzalkova" +REQUEST_ID = "6b46b5a8-b080-4821-86b0-39adabeec86b" + +token = TOKEN_PATH.read_text(encoding="utf-8").strip() +headers = { + "content-type": "application/json", + "authorization": f"Bearer {token}", + "origin": "https://my.medevio.cz", + "referer": "https://my.medevio.cz/", +} + +# Query with as many fields as possible +QUERY = """ +query GetPatientRequest2($requestId: UUID!, $clinicSlug: String!, $locale: Locale!) { + request: getPatientRequest2(patientRequestId: $requestId, clinicSlug: $clinicSlug) { + id + displayTitle(locale: $locale) + createdAt + updatedAt + doneAt + removedAt + userNote + eventType + extendedPatient(clinicSlug: $clinicSlug) { + name + surname + dob + identificationNumber + insuranceCompanyObject { shortName } + } + ecrfFilledData(locale: $locale) { + name + groups { + label + fields { name label type value } + } + } + } +} +""" + +payload = { + "operationName": "GetPatientRequest2", + "query": QUERY, + "variables": { + "requestId": REQUEST_ID, + "clinicSlug": CLINIC_SLUG, + "locale": "cs", + }, +} + +r = requests.post(GRAPHQL_URL, json=payload, headers=headers, timeout=30) +print(json.dumps(r.json(), indent=2, ensure_ascii=False)) diff --git a/40 agenda a požadavky/medevio_storage.json b/40 agenda a požadavky/medevio_storage.json new file mode 100644 index 0000000..a0dc794 --- /dev/null +++ b/40 agenda a požadavky/medevio_storage.json @@ -0,0 +1 @@ +{"cookies": [{"name": "gateway-access-token", "value": "YwBgkf8McREDKs7vCZj0EZD2fJsuV8RyDPtYx7WiDoz0nFJ9kxId8kcNEPBLFSwM+Tiz80+SOdFwo+oj", "domain": "my.medevio.cz", "path": "/", "expires": 1763372319, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "aws-waf-token", "value": "b6a1d4eb-4350-40e5-8e52-1f5f9600fbb8:CgoAr9pC8c6zAAAA:OYwXLY5OyitSQPl5v2oIlS+hIxsrb5LxV4VjCyE2gJCFFE5PQu+0Zbxse2ZIofrNv5QKs0TYUDTmxPhZyTr9Qtjnq2gsVQxWHXzrbebv3Z7RbzB63u6Ymn3Fo8IbDev3CfCNcNuxCKltFEXLqSCjI2vqNY+7HZkgQBIqy2wMgzli3aSLq0w8lWYtZzyyot7q8RPXWMGTfaBUo2reY0SOSffm9rAivE9PszNfPid71CvNrGAAoxRbwb25eVujlyIcDVWe5vZ9Iw==", "domain": ".my.medevio.cz", "path": "/", "expires": 1761125920, "httpOnly": false, "secure": true, "sameSite": "Lax"}], "origins": [{"origin": "https://my.medevio.cz", "localStorage": [{"name": "awswaf_token_refresh_timestamp", "value": "1760780309860"}, {"name": "awswaf_session_storage", "value": "b6a1d4eb-4350-40e5-8e52-1f5f9600fbb8:CgoAr9pC8c+zAAAA:+vw//1NzmePjPpbGCJzUB+orCRivtJd098DbDX4AnABiGRw/+ql6ShqvFY4YdCY7w2tegb5mEPBdAmc4sNi22kNR9BuEoAgCUiMhkU1AZWfzM51zPfTh7SveCrREZ7xdvxcqKPMmfVLRYX5E4+UWh22z/LKQ7+d9VERp3J+wWCUW3dFFirkezy3N7b2FVjTlY/RxsZwhejQziTG/L3CkIFFP3mOReNgBvDpj7aKoM1knY4IL4TZ8E7zNv3nTsvzACLYvnUutVOUcofN1TfOzwZshSKsEXsMzrQn8PzLccX1jM5VSzce7gfEzl0zSPsT8NB3Sna+rhMIttDNYgvbW1HsfG2LIeKMR27Zf8hkslDRVVkcU/Kp2jLOEdhhrBKGjKY2o9/uX3NExdzh5MEKQSSRtmue01BpWYILPH23rMsz4YSmF+Ough5OeQoC95rkcYwVXMhwvUN9Zfp9UZ4xCNfFUex5dOrg9aJntYRnaceeocGUttNI5AdT0i3+osV6XHXzKxeqO8zLCS9BIsCzxaHfdqqem5DorMceuGKz+QqksatIQAA=="}, {"name": "Application.Intl.locale", "value": "cs"}, {"name": "Password.prefill", "value": "{\"username\":\"vladimir.buzalka@buzalka.cz\",\"type\":\"email\"}"}]}]} \ No newline at end of file diff --git a/40 agenda a požadavky/sync_open_requests.py b/40 agenda a požadavky/sync_open_requests.py new file mode 100644 index 0000000..2c063f3 --- /dev/null +++ b/40 agenda a požadavky/sync_open_requests.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Sync open requests: checks each request marked as open in MySQL (doneAt IS NULL +AND removedAt IS NULL) against the Medevio API. If the API shows the request is +closed (doneAt) or removed (removedAt), updates MySQL accordingly. +""" + +import json +import sys +import time +import requests +import pymysql +from pathlib import Path +from datetime import datetime + +# ============================== +# UTF-8 output (Windows friendly) +# ============================== +try: + sys.stdout.reconfigure(encoding="utf-8") + sys.stderr.reconfigure(encoding="utf-8") +except AttributeError: + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8") + +# ============================== +# DRY RUN - set to True to only print what would be updated, False to actually update +# ============================== +DRY_RUN = False + +# ============================== +# CONFIG +# ============================== +GRAPHQL_URL = "https://api.medevio.cz/graphql" +CLINIC_SLUG = "mudr-buzalkova" + +TOKEN_PATH = Path(__file__).parent / "token.txt" +if not TOKEN_PATH.exists(): + raise SystemExit("❌ token.txt not found") + +gateway_token = TOKEN_PATH.read_text(encoding="utf-8").strip() +headers = { + "content-type": "application/json", + "authorization": f"Bearer {gateway_token}", + "origin": "https://my.medevio.cz", + "referer": "https://my.medevio.cz/", +} + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3306, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +GRAPHQL_QUERY = """ +query GetPatientRequest2($requestId: UUID!, $clinicSlug: String!) { + request: getPatientRequest2(patientRequestId: $requestId, clinicSlug: $clinicSlug) { + id + doneAt + removedAt + updatedAt + } +} +""" + + +def fix_datetime(dt_str): + if not dt_str: + return None + try: + return datetime.fromisoformat(dt_str.replace("Z", "+00:00")) + except Exception: + return None + + +def fetch_request(request_id): + payload = { + "operationName": "GetPatientRequest2", + "query": GRAPHQL_QUERY, + "variables": { + "requestId": request_id, + "clinicSlug": CLINIC_SLUG, + }, + } + for attempt in range(3): + try: + r = requests.post(GRAPHQL_URL, json=payload, headers=headers, timeout=30) + break + except (requests.ConnectionError, requests.Timeout, requests.exceptions.RequestException) as e: + print(f" ⚠️ Attempt {attempt+1}/3 failed: {e}") + time.sleep(2) + else: + print(f" ❌ Connection failed after 3 attempts for {request_id}") + return None + if r.status_code != 200: + print(f" ❌ HTTP {r.status_code} for {request_id}") + return None + data = r.json() + if "errors" in data: + print(f" ❌ API error for {request_id}: {data['errors']}") + return None + return data.get("data", {}).get("request") + + +# ============================== +# MAIN +# ============================== +conn = pymysql.connect(**DB_CONFIG) + +# 1) Read all open requests from MySQL +with conn.cursor() as cur: + cur.execute( + "SELECT id, displayTitle, pacient_prijmeni, pacient_jmeno " + "FROM pozadavky WHERE doneAt IS NULL AND removedAt IS NULL" + ) + open_requests = cur.fetchall() + +mode = "DRY RUN" if DRY_RUN else "LIVE" +print(f"🔧 Mode: {mode}") +print(f"📋 Found {len(open_requests)} open requests in MySQL.\n") + +updated = 0 +errors = 0 + +for i, req in enumerate(open_requests, 1): + rid = req["id"] + name = f"{req.get('pacient_prijmeni', '')} {req.get('pacient_jmeno', '')}".strip() + title = req.get("displayTitle", "") + print(f"[{i}/{len(open_requests)}] {name} – {title} ({rid})") + + api_data = fetch_request(rid) + if api_data is None: + errors += 1 + continue + + api_done = api_data.get("doneAt") + api_removed = api_data.get("removedAt") + api_updated = api_data.get("updatedAt") + + if api_done or api_removed: + done_dt = fix_datetime(api_done) + removed_dt = fix_datetime(api_removed) + updated_dt = fix_datetime(api_updated) + + status = "DONE" if api_done else "REMOVED" + + if DRY_RUN: + print(f" 🔍 Would update → {status} (doneAt={api_done}, removedAt={api_removed})") + else: + with conn.cursor() as cur: + cur.execute( + "UPDATE pozadavky SET doneAt = %s, removedAt = %s, updatedAt = %s WHERE id = %s", + (done_dt, removed_dt, updated_dt, rid), + ) + conn.commit() + print(f" ✅ Updated → {status}") + + updated += 1 + else: + print(f" ⏳ Still open") + + # Be gentle with the API + time.sleep(1) + +conn.close() + +print(f"\n{'='*50}") +print(f"📊 Total open in MySQL: {len(open_requests)}") +print(f"✅ Updated (closed/removed): {updated}") +print(f"⏳ Still open: {len(open_requests) - updated - errors}") +print(f"❌ Errors: {errors}") diff --git a/50 Různé testy/10 RenameFoldersTriangles.py b/50 Různé testy/10 RenameFoldersTriangles.py new file mode 100644 index 0000000..8c1c8cb --- /dev/null +++ b/50 Různé testy/10 RenameFoldersTriangles.py @@ -0,0 +1,46 @@ +import os +from pathlib import Path + +# Define the target directory +target_path = Path(r"U:\Dropbox\Ordinace\Dokumentace_ke_zpracování\MP") + + +def rename_folders(): + # Ensure the path exists + if not target_path.exists(): + print(f"Error: The path {target_path} does not exist.") + return + + # Iterate through items in the directory + for folder in target_path.iterdir(): + # Only process directories + if folder.is_dir(): + original_name = folder.name + + # Check if name starts with the triangle + if original_name.startswith("▲"): + # 1. Remove the triangle from the start + name_without_tri = original_name[1:] + + # 2. Prepare the name to be at least 10 chars long + # (so the triangle can sit at index 10 / position 11) + clean_name = name_without_tri.ljust(10) + + # 3. Construct new name: first 10 chars + triangle + the rest + new_name = clean_name[:10] + "▲" + clean_name[10:] + + # Remove trailing spaces if the original name was short + # but you don't want extra spaces at the very end + new_name = new_name.rstrip() + + new_folder_path = folder.parent / new_name + + try: + print(f"Renaming: '{original_name}' -> '{new_name}'") + folder.rename(new_folder_path) + except Exception as e: + print(f"Could not rename {original_name}: {e}") + + +if __name__ == "__main__": + rename_folders() \ No newline at end of file diff --git a/dddddd.py b/dddddd.py new file mode 100644 index 0000000..f5f717e --- /dev/null +++ b/dddddd.py @@ -0,0 +1,315 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +""" +FAST FILE HASH INDEXER – UNRAID (BLAKE3 ONLY, ALL SHARES) +- HARDCODED SINGLE SHARE MODE +- SQL OPTIMIZATION +- STRICT MODE (NO TOLERANCE) - Updates DB on any mismatch +""" + +import os +import pymysql +import socket +import platform +from blake3 import blake3 + +# ============================== +# ENV / HOST +# ============================== + +HOSTNAME = socket.gethostname() +OS_NAME = platform.system() + +# ZDE JE TO NATVRDO PRO TESTOVÁNÍ: +# SCAN_ONLY_THIS = None #"#Fotky" +SCAN_ONLY_THIS = '#Library' # "#Fotky" + +# ============================== +# CONFIG +# ============================== + +EXCLUDED_SHARES = {"domains", "appdata", "system", "isos"} + +# --- File size limits (bytes) --- +FILE_MIN_SIZE = 0 +FILE_MAX_SIZE = 1024 * 1024 * 1024 * 1024 # 50MB + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "torrents", + "charset": "utf8mb4", + "autocommit": True, +} + +CHUNK_SIZE = 4 * 1024 * 1024 # 4 MB +PRINT_SKIPPED = False + + +# ============================== +# HASH +# ============================== + +def compute_blake3(path: str) -> bytes: + h = blake3() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(CHUNK_SIZE), b""): + h.update(chunk) + return h.digest() + + +# ============================== +# SHARE / PATH HELPERS +# ============================== + +def get_user_shares(): + if SCAN_ONLY_THIS: + path = f"/mnt/user/{SCAN_ONLY_THIS}" + if os.path.isdir(path): + print(f"🎯 SINGLE SHARE MODE ACTIVE: Scanning only '{SCAN_ONLY_THIS}'") + return [SCAN_ONLY_THIS] + else: + print(f"⚠️ ERROR: Requested share '{SCAN_ONLY_THIS}' not found in /mnt/user!") + return [] + + shares = [] + if not os.path.exists("/mnt/user"): + return [] + + for name in os.listdir("/mnt/user"): + if name.startswith("."): + continue + if name in EXCLUDED_SHARES: + continue + path = f"/mnt/user/{name}" + if os.path.isdir(path): + shares.append(name) + return sorted(shares) + + +def find_physical_roots(shares): + roots = [] + if not os.path.exists("/mnt"): + return [] + for disk in os.listdir("/mnt"): + if not disk.startswith("disk"): + continue + for share in shares: + path = f"/mnt/{disk}/{share}" + if os.path.isdir(path): + roots.append((share, path)) + return sorted(roots) + + +def logical_path_from_disk_path(disk_path: str) -> str: + if not disk_path.startswith("/mnt/disk"): + raise ValueError(f"Unexpected disk path: {disk_path}") + parts = disk_path.split("/", 3) + return f"/mnt/user/{parts[3]}" + + +def size_allowed(size: int) -> bool: + if FILE_MIN_SIZE is not None and size < FILE_MIN_SIZE: + return False + if FILE_MAX_SIZE is not None and size > FILE_MAX_SIZE: + return False + return True + + +# ============================== +# MAIN +# ============================== + +def main(): + print("🚀 BLAKE3 indexer starting", flush=True) + print(f"🖥 Host: {HOSTNAME} | OS: {OS_NAME}", flush=True) + + if FILE_MIN_SIZE or FILE_MAX_SIZE: + print(f"📏 File size limits: min={FILE_MIN_SIZE} max={FILE_MAX_SIZE}", flush=True) + + shares = get_user_shares() + if not shares: + print("❌ No user shares to index!", flush=True) + return + + print("📦 User shares to index:", flush=True) + for s in shares: + print(f" - {s}", flush=True) + + scan_roots = find_physical_roots(shares) + if not scan_roots: + print("❌ No physical disk roots found!", flush=True) + return + + print("📂 Physical scan roots:", flush=True) + for _, path in scan_roots: + print(f" - {path}", flush=True) + + try: + db = pymysql.connect(**DB_CONFIG) + cur = db.cursor() + # === TOTO JE TEN PŘÍKAZ "NEPŘEMÝŠLEJ" === + # Nastaví relaci na UTC. MySQL přestane posouvat časy o hodinu sem a tam. + # cur.execute("SET time_zone = '+00:00'") + # ========================================= + except Exception as e: + print(f"❌ Database connection failed: {e}") + return + + print("📥 Loading already indexed files into memory...", flush=True) + + # === OPTIMALIZACE SQL === + if SCAN_ONLY_THIS: + search_pattern = f"/mnt/user/{SCAN_ONLY_THIS}%" + print(f"⚡ OPTIMIZATION: Fetching only DB records for '{search_pattern}'", flush=True) + cur.execute(""" + SELECT full_path, file_size, UNIX_TIMESTAMP(mtime) + FROM file_md5_index + WHERE host_name = %s AND full_path LIKE %s + """, (HOSTNAME, search_pattern)) + else: + cur.execute(""" + SELECT full_path, file_size, UNIX_TIMESTAMP(mtime) + FROM file_md5_index + WHERE host_name = %s + """, (HOSTNAME,)) + + # Načteme do slovníku pro rychlé vyhledávání + # Formát: { "cesta": (velikost, mtime) } + indexed_map = {row[0]: (row[1], row[2]) for row in cur.fetchall()} + print(f"✅ Loaded {len(indexed_map):,} indexed entries", flush=True) + print("======================================", flush=True) + + new_files = 0 + skipped = 0 + filtered = 0 + seen_paths = set() + + # --- SCAN --- + for share, scan_root in scan_roots: + for root, _, files in os.walk(scan_root): + for fname in files: + disk_path = os.path.join(root, fname) + + try: + stat = os.stat(disk_path) + except OSError: + continue + + size = stat.st_size + if not size_allowed(size): + filtered += 1 + continue + + logical_path = logical_path_from_disk_path(disk_path) + + if logical_path in seen_paths: + continue + seen_paths.add(logical_path) + + mtime = int(stat.st_mtime) + + # === PŘÍSNÁ KONTROLA (ŽÁDNÁ TOLERANCE) === + # Pokud soubor v DB existuje a přesně sedí velikost i čas, přeskočíme ho. + # Vše ostatní (včetně posunu času o 1s) se považuje za změnu a aktualizuje se. + + is_match = False + if logical_path in indexed_map: + db_size, db_mtime = indexed_map[logical_path] + if size == db_size and mtime == db_mtime: + is_match = True + + if is_match: + skipped += 1 + if PRINT_SKIPPED: + print(f"⏭ SKIP {logical_path}", flush=True) + continue + # ============================================ + + print("➕ NEW / UPDATED", flush=True) + print(f" File: {logical_path}", flush=True) + print(f" Size: {size:,} B", flush=True) + + try: + b3 = compute_blake3(disk_path) + except Exception as e: + print(f"❌ BLAKE3 failed: {e}", flush=True) + continue + + # Zde proběhne UPDATE mtime na hodnotu z disku + cur.execute(""" + INSERT INTO file_md5_index + (os_name, host_name, full_path, file_name, directory, + file_size, mtime, blake3) + VALUES (%s, %s, %s, %s, %s, %s, FROM_UNIXTIME(%s), %s) + ON DUPLICATE KEY UPDATE + file_size = VALUES(file_size), + mtime = VALUES(mtime), + blake3 = VALUES(blake3), + updated_at = CURRENT_TIMESTAMP + """, ( + OS_NAME, + HOSTNAME, + logical_path, + fname, + os.path.dirname(logical_path), + size, + mtime, + b3, + )) + + new_files += 1 + print(f" B3 : {b3.hex()}", flush=True) + print("--------------------------------------", flush=True) + + print("======================================", flush=True) + print(f"✅ New / updated : {new_files}", flush=True) + print(f"⏭ Skipped : {skipped}", flush=True) + print(f"🚫 Size filtered: {filtered}", flush=True) + print("🏁 Script finished", flush=True) + + + # ============================== + # DB CLEANUP – REMOVE DELETED FILES + # ============================== + + print("🧹 Checking for deleted files in DB...", flush=True) + + db_paths = set(indexed_map.keys()) + deleted_paths = db_paths - seen_paths + + # Omezíme jen na aktuální share (pokud je aktivní) + if SCAN_ONLY_THIS: + prefix = f"/mnt/user/{SCAN_ONLY_THIS}/" + deleted_paths = {p for p in deleted_paths if p.startswith(prefix)} + + if deleted_paths: + print(f"🗑 Removing {len(deleted_paths):,} deleted files from DB", flush=True) + + BATCH_SIZE = 1000 + deleted_paths = list(deleted_paths) + + for i in range(0, len(deleted_paths), BATCH_SIZE): + batch = deleted_paths[i:i + BATCH_SIZE] + placeholders = ",".join(["%s"] * len(batch)) + + sql = f""" + DELETE FROM file_md5_index + WHERE host_name = %s + AND full_path IN ({placeholders}) + """ + + cur.execute(sql, (HOSTNAME, *batch)) + + print("✅ DB cleanup completed", flush=True) + else: + print("✅ No deleted files found in DB", flush=True) + + cur.close() + db.close() + +if __name__ == "__main__": + main() \ No newline at end of file