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