Přidán sync_agenda_to_mysql.py — historický a inkrementální sync agendy do MySQL
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||||
Reference in New Issue
Block a user