From 46b7227b18532a49c9930adcfdca80493e26e8a5 Mon Sep 17 00:00:00 2001 From: "vladimir.buzalka" Date: Wed, 13 May 2026 13:27:15 +0200 Subject: [PATCH] =?UTF-8?q?P=C5=99id=C3=A1n=20sync=5Fagenda=5Fto=5Fmysql.p?= =?UTF-8?q?y=20=E2=80=94=20historick=C3=BD=20a=20inkrement=C3=A1ln=C3=AD?= =?UTF-8?q?=20sync=20agendy=20do=20MySQL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nový skript pro ukládání Medevio rezervací do tabulky medevio_agenda - Režim FULL: dávkový načet od 2025-01-01 po měsících - Režim INCREMENTAL: jeden dotaz, -7 dní až +10 let dopředu - Tabulka medevio_agenda se vytvoří automaticky (CREATE TABLE IF NOT EXISTS) - UPSERT logika — bezpečné opakované spouštění Co-Authored-By: Claude Sonnet 4.6 --- .../sync_agenda_to_mysql.py | 227 ++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 Medevio/40 agenda a požadavky/sync_agenda_to_mysql.py diff --git a/Medevio/40 agenda a požadavky/sync_agenda_to_mysql.py b/Medevio/40 agenda a požadavky/sync_agenda_to_mysql.py new file mode 100644 index 0000000..4587dd2 --- /dev/null +++ b/Medevio/40 agenda a požadavky/sync_agenda_to_mysql.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Sync Medevio agenda reservations to MySQL table medevio_agenda. + +Modes (set MODE below): + FULL Load all reservations from 2025-01-01 to today (monthly batches) + INCREMENTAL Last 7 days to +30 days ahead +""" + +import sys +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") + +import json +import time +import pymysql +import requests +from pathlib import Path +from datetime import datetime, date +from dateutil import parser as dtparser, tz +from dateutil.relativedelta import relativedelta + +# ==================== CONFIG ==================== +GRAPHQL_URL = "https://api.medevio.cz/graphql" +CALENDAR_ID = "144c4e12-347c-49ca-9ec0-8ca965a4470d" +CLINIC_SLUG = "mudr-buzalkova" + +TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt" + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3306, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +PRAGUE_TZ = tz.gettz("Europe/Prague") +FULL_SINCE = date(2025, 1, 1) + +# ==================== MODE ==================== +MODE = "FULL" # "FULL" nebo "INCREMENTAL" + +# ==================== TABLE ==================== +CREATE_TABLE_SQL = """ +CREATE TABLE IF NOT EXISTS medevio_agenda ( + reservation_id VARCHAR(36) NOT NULL PRIMARY KEY, + start_dt DATETIME, + end_dt DATETIME, + date DATE, + time_interval VARCHAR(20), + note TEXT, + done TINYINT(1), + color VARCHAR(50), + request_id VARCHAR(36), + request_title VARCHAR(500), + patient_name VARCHAR(200), + patient_surname VARCHAR(200), + patient_dob VARCHAR(50), + insurance_short VARCHAR(50), + synced_at DATETIME NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +""" + +# ==================== UPSERT ==================== +UPSERT_SQL = """ +INSERT INTO medevio_agenda ( + reservation_id, start_dt, end_dt, date, time_interval, + note, done, color, + request_id, request_title, + patient_name, patient_surname, patient_dob, insurance_short, + synced_at +) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) +ON DUPLICATE KEY UPDATE + start_dt = VALUES(start_dt), + end_dt = VALUES(end_dt), + date = VALUES(date), + time_interval = VALUES(time_interval), + note = VALUES(note), + done = VALUES(done), + color = VALUES(color), + request_id = VALUES(request_id), + request_title = VALUES(request_title), + patient_name = VALUES(patient_name), + patient_surname = VALUES(patient_surname), + patient_dob = VALUES(patient_dob), + insurance_short = VALUES(insurance_short), + synced_at = VALUES(synced_at) +""" + + +def parse_dt(iso_str): + """Parse ISO datetime, convert to Europe/Prague, return naive datetime for MySQL.""" + if not iso_str: + return None + try: + return dtparser.isoparse(iso_str).astimezone(PRAGUE_TZ).replace(tzinfo=None) + except Exception: + return None + + +def fetch_reservations(headers, since: datetime, until: datetime): + payload = { + "operationName": "ClinicAgenda_ListClinicReservations", + "variables": { + "calendarIds": [CALENDAR_ID], + "clinicSlug": CLINIC_SLUG, + "since": since.strftime("%Y-%m-%dT%H:%M:%S") + "Z", + "until": until.strftime("%Y-%m-%dT%H:%M:%S") + "Z", + "locale": "cs", + "emptyCalendarIds": False, + }, + "query": """query ClinicAgenda_ListClinicReservations( + $calendarIds: [UUID!], $clinicSlug: String!, + $locale: Locale!, $since: DateTime!, $until: DateTime!, + $emptyCalendarIds: Boolean! + ) { + reservations: listClinicReservations( + clinicSlug: $clinicSlug, calendarIds: $calendarIds, + since: $since, until: $until + ) @skip(if: $emptyCalendarIds) { + id start end note done color + request { + id displayTitle(locale: $locale) + extendedPatient { + name surname dob insuranceCompanyObject { shortName } + } + } + } + }""", + } + response = requests.post(GRAPHQL_URL, headers=headers, json=payload, timeout=30) + response.raise_for_status() + resp = response.json() + if "errors" in resp or "data" not in resp: + print(f"❌ API error: {resp}") + return [] + return resp["data"]["reservations"] or [] + + +def upsert_reservations(conn, reservations): + now = datetime.now().replace(microsecond=0) + rows = [] + for r in reservations: + req = r.get("request") or {} + patient = req.get("extendedPatient") or {} + insurance = patient.get("insuranceCompanyObject") or {} + start_dt = parse_dt(r.get("start")) + end_dt = parse_dt(r.get("end")) + rows.append(( + r["id"], + start_dt, + end_dt, + start_dt.date() if start_dt else None, + f"{start_dt.strftime('%H:%M')}-{end_dt.strftime('%H:%M')}" if start_dt and end_dt else None, + r.get("note") or None, + 1 if r.get("done") else 0, + r.get("color") or None, + req.get("id") or None, + req.get("displayTitle") or None, + patient.get("name") or None, + patient.get("surname") or None, + patient.get("dob") or None, + insurance.get("shortName") or None, + now, + )) + with conn.cursor() as cur: + cur.executemany(UPSERT_SQL, rows) + conn.commit() + return len(rows) + + +def main(): + token = TOKEN_PATH.read_text(encoding="utf-8").strip() + if token.startswith("Bearer "): + token = token.split(" ", 1)[1] + + headers = { + "content-type": "application/json", + "authorization": f"Bearer {token}", + "origin": "https://my.medevio.cz", + "referer": "https://my.medevio.cz/", + } + + conn = pymysql.connect(**DB_CONFIG) + with conn.cursor() as cur: + cur.execute(CREATE_TABLE_SQL) + conn.commit() + print("✅ Tabulka medevio_agenda připravena.") + + today = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) + + if MODE == "FULL": + batch_start = datetime(FULL_SINCE.year, FULL_SINCE.month, FULL_SINCE.day) + print(f"📥 Režim FULL: {FULL_SINCE} → {today.date()}") + total = 0 + while batch_start < today: + batch_end = min(batch_start + relativedelta(months=1), today) + reservations = fetch_reservations(headers, batch_start, batch_end) + count = upsert_reservations(conn, reservations) + total += count + print(f" {batch_start.date()} → {batch_end.date()}: {count} rezervací") + batch_start = batch_end + time.sleep(0.5) + print(f"\n✅ FULL hotovo — celkem {total} rezervací uloženo.") + else: + since = today - relativedelta(days=7) + until = today + relativedelta(years=10) + print(f"🔄 Režim INCREMENTAL: {since.date()} → {until.date()}") + reservations = fetch_reservations(headers, since, until) + count = upsert_reservations(conn, reservations) + print(f"✅ INCREMENTAL hotovo — {count} rezervací upsertováno.") + + conn.close() + + +if __name__ == "__main__": + main()