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