Files
ordinaceprojekt/Medevio/agenda_dne.py
T
Vladimir Buzalka a9ef60212d notebookvb
2026-05-31 07:51:29 +02:00

386 lines
13 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Cteni agendy z Medevio kalendare.
Hlavni funkce: `list_agendu(start, end=None, calendar=None)`
start date | datetime | str 'YYYY-MM-DD' nebo 'YYYY-MM-DD HH:MM'
end to stejne; pokud None, bere se konec dne `start` (23:59:59)
calendar None = oba (vlado + manzelka), nebo "vlado" / "manzelka" / UUID,
nebo list techto hodnot
Vraci list dictu serazenych podle start. Kazdy dict obsahuje pole `calendar`
(jmeno) navic.
CLI:
python agenda_dne.py # interaktivne se zepta
python agenda_dne.py --od 2026-06-03 --do 2026-06-03 --kalendar vlado
python agenda_dne.py --od 2026-06-01 --do 2026-06-07 # tyden, oba
python agenda_dne.py --den 2026-06-03 # zkratka pro jeden den
python agenda_dne.py --den dnes
python agenda_dne.py --den +1 --kalendar manzelka
"""
import sys
import json
import argparse
from pathlib import Path
from datetime import datetime, date, timedelta
import requests
from dateutil import parser as dtparser, tz
try:
sys.stdout.reconfigure(encoding="utf-8")
except AttributeError:
pass
GRAPHQL_URL = "https://api.medevio.cz/graphql"
CLINIC_SLUG = "mudr-buzalkova"
PRAGUE_TZ = tz.gettz("Europe/Prague")
# Pojmenovane kalendare - viz pamet project-medevio-kalendar
CALENDARS = {
"vlado": "b6555c7e-4e95-4657-b441-87c2c9a7b2ca",
"manzelka": "144c4e12-347c-49ca-9ec0-8ca965a4470d",
}
BASE_DIR = Path(__file__).resolve().parent
TOKEN_PATH = BASE_DIR / "token.txt"
DEBUG_DIR = BASE_DIR / "debug"
QUERY = """query Agenda_ListAll(
$calendarIds: [UUID!]!, $clinicSlug: String!,
$locale: Locale!, $since: DateTime!, $until: DateTime!
) {
reservations: listClinicReservations(
clinicSlug: $clinicSlug, calendarIds: $calendarIds,
since: $since, until: $until
) {
id start end note done color canceledAt calendarId
request {
id displayTitle(locale: $locale)
extendedPatient {
id name surname dob phone identificationNumber
insuranceCompanyObject { code shortName }
}
}
}
recurringReservations: listClinicRecurringReservations(
clinicSlug: $clinicSlug, calendarIds: $calendarIds,
since: $since, until: $until
) {
recurringReservation {
id calendarId color note
rrule { frequency interval dtstart tzid byweekday bymonthday byweekno }
}
instances { start end note color }
}
}"""
# ==================== Helpery ====================
def _load_token() -> str:
token = TOKEN_PATH.read_text(encoding="utf-8").strip()
if token.startswith("Bearer "):
token = token.split(" ", 1)[1]
return token
def _headers() -> dict:
return {
"content-type": "application/json",
"authorization": f"Bearer {_load_token()}",
"origin": "https://my.medevio.cz",
"referer": "https://my.medevio.cz/",
}
def _is_uuid(s: str) -> bool:
return isinstance(s, str) and len(s) == 36 and s.count("-") == 4
def _resolve_calendars(calendar) -> list[tuple[str, str]]:
"""Vraci list (jmeno, uuid). None = oba pojmenovane."""
if calendar is None:
return list(CALENDARS.items())
items = calendar if isinstance(calendar, list) else [calendar]
out = []
for c in items:
if c in CALENDARS:
out.append((c, CALENDARS[c]))
elif _is_uuid(c):
# zkusime najit jmeno
name = next((n for n, u in CALENDARS.items() if u == c), c)
out.append((name, c))
else:
raise ValueError(f"Neznamy kalendar: {c!r}")
return out
def _to_dt(value, end_of_day: bool = False) -> datetime:
"""Prevede date/datetime/str na datetime s Europe/Prague timezone."""
if isinstance(value, datetime):
dt = value
elif isinstance(value, date):
dt = datetime.combine(
value,
datetime.max.time().replace(microsecond=0) if end_of_day else datetime.min.time(),
)
elif isinstance(value, str):
s = value.strip().replace("T", " ")
if " " in s:
dt = datetime.strptime(s, "%Y-%m-%d %H:%M")
else:
d = datetime.strptime(s, "%Y-%m-%d").date()
dt = datetime.combine(
d,
datetime.max.time().replace(microsecond=0) if end_of_day else datetime.min.time(),
)
else:
raise TypeError(f"Nelze prevest na datetime: {value!r}")
if dt.tzinfo is None:
dt = dt.replace(tzinfo=PRAGUE_TZ)
return dt
def _to_utc_iso(dt: datetime) -> str:
return dt.astimezone(tz.UTC).strftime("%Y-%m-%dT%H:%M:%S.000Z")
# ==================== Hlavni funkce ====================
def list_agendu(
start,
end=None,
calendar=None,
save_debug: bool = True,
) -> list[dict]:
"""Vraci rezervace v zadanem rozmezi pro vybrane kalendare.
Pokud `end` neni zadan, pouzije se konec stejneho dne jako `start`.
Pokud `calendar` je None, vrati se oba pojmenovane kalendare slouceny.
"""
start_dt = _to_dt(start, end_of_day=False)
if end is None:
end_dt = _to_dt(start_dt.date(), end_of_day=True)
else:
end_dt = _to_dt(end, end_of_day=True)
cals = _resolve_calendars(calendar)
payload = {
"operationName": "Agenda_ListAll",
"variables": {
"calendarIds": [uuid for _, uuid in cals],
"clinicSlug": CLINIC_SLUG,
"since": _to_utc_iso(start_dt),
"until": _to_utc_iso(end_dt),
"locale": "cs",
},
"query": QUERY,
}
r = requests.post(GRAPHQL_URL, headers=_headers(), data=json.dumps(payload), timeout=30)
r.raise_for_status()
data = r.json()
if save_debug:
DEBUG_DIR.mkdir(exist_ok=True)
debug_path = DEBUG_DIR / f"agenda_{start_dt:%Y%m%d}_{end_dt:%Y%m%d}_{datetime.now():%H%M%S}.json"
debug_path.write_text(
json.dumps({"request": payload["variables"], "response": data}, ensure_ascii=False, indent=2),
encoding="utf-8",
)
print(f"[debug] {debug_path}")
if "errors" in data:
raise RuntimeError(f"GraphQL error: {data['errors']}")
uuid_to_name = {uuid: name for name, uuid in cals}
parsed = []
# 1) Jednorazove rezervace
for res in data["data"].get("reservations") or []:
if res.get("canceledAt"):
continue
s = dtparser.isoparse(res["start"]).astimezone(PRAGUE_TZ)
e = dtparser.isoparse(res["end"]).astimezone(PRAGUE_TZ)
req = res.get("request") or {}
pat = req.get("extendedPatient") or {}
ins = pat.get("insuranceCompanyObject") or {}
request_id = req.get("id") or ""
parsed.append({
"typ": "pacient" if request_id else "poznamka",
"calendar": uuid_to_name.get(res.get("calendarId"), res.get("calendarId")),
"calendar_id": res.get("calendarId"),
"start": s,
"end": e,
"title": req.get("displayTitle") or "",
"patient": f"{pat.get('surname','')} {pat.get('name','')}".strip(),
"dob": pat.get("dob") or "",
"rc": pat.get("identificationNumber") or "",
"phone": pat.get("phone") or "",
"insurance": ins.get("shortName") or "",
"insurance_code": ins.get("code") or "",
"note": (res.get("note") or "").strip(),
"done": bool(res.get("done")),
"color": res.get("color") or "",
"reservation_id": res["id"],
"request_id": request_id,
"patient_id": pat.get("id") or "",
"recurring": False,
})
# 2) Opakujici se - jednotlive instance v intervalu
for rr in data["data"].get("recurringReservations") or []:
rule = rr.get("recurringReservation") or {}
cal_id = rule.get("calendarId")
for inst in rr.get("instances") or []:
s = dtparser.isoparse(inst["start"]).astimezone(PRAGUE_TZ)
e = dtparser.isoparse(inst["end"]).astimezone(PRAGUE_TZ)
parsed.append({
"typ": "poznamka", # opakujici se vznikaji vzdy pres "Jina udalost"
"calendar": uuid_to_name.get(cal_id, cal_id),
"calendar_id": cal_id,
"start": s,
"end": e,
"title": "",
"patient": "",
"dob": "", "rc": "", "phone": "",
"insurance": "", "insurance_code": "",
"note": (inst.get("note") or rule.get("note") or "").strip(),
"done": False,
"color": inst.get("color") or rule.get("color") or "",
"reservation_id": rule.get("id"),
"request_id": "",
"patient_id": "",
"recurring": True,
"rrule": rule.get("rrule"),
})
parsed.sort(key=lambda x: (x["start"], x["calendar"]))
return parsed
# Zachovani zpetne kompatibility
def get_agenda(day, calendar=None) -> list[dict]:
"""Vraci agendu jednoho dne. Pro zpetnou kompat se starsim API."""
return list_agendu(day, end=day, calendar=calendar)
# ==================== Vystup ====================
def print_agenda(reservations: list[dict], header: str | None = None) -> None:
cz_dny = ["pondeli", "utery", "streda", "ctvrtek", "patek", "sobota", "nedele"]
if header is None:
if reservations:
dates = sorted({r["start"].date() for r in reservations})
if len(dates) == 1:
header = f"Agenda {dates[0].isoformat()} ({cz_dny[dates[0].weekday()]})"
else:
header = f"Agenda {dates[0].isoformat()} - {dates[-1].isoformat()}"
else:
header = "Agenda"
print(header)
print("=" * len(header))
if not reservations:
print("(zadne rezervace)")
return
last_date = None
for r in reservations:
if r["start"].date() != last_date:
if last_date is not None:
print()
print(f"--- {r['start'].date().isoformat()} ({cz_dny[r['start'].weekday()]}) ---")
last_date = r["start"].date()
time_str = f"{r['start']:%H:%M}-{r['end']:%H:%M}"
cal = f"[{r['calendar']}]" if r["calendar"] else ""
typ = f"<{r.get('typ','?')}>"
flags = []
if r["done"]:
flags.append("HOTOVO")
if r.get("recurring"):
flags.append("OPAKOVANE")
flag_str = f" [{', '.join(flags)}]" if flags else ""
label = r["patient"] if r.get("typ") == "pacient" else (r.get("note") or "(bez popisu)")
line = f"{time_str} {cal} {typ} {label}"
if r["dob"]:
line += f" *{r['dob']}"
if r["insurance"]:
line += f" {r['insurance']}"
line += flag_str
print(line)
if r["title"]:
print(f" {r['title']}")
if r["note"]:
for ln in r["note"].splitlines():
print(f" poznamka: {ln}")
print()
print(f"Celkem: {len(reservations)} rezervaci")
# ==================== CLI ====================
def _parse_day_arg(arg: str | None) -> date:
if not arg or arg in ("dnes", "today"):
return date.today()
if arg in ("zitra", "tomorrow"):
return date.today() + timedelta(days=1)
if arg in ("vcera", "yesterday"):
return date.today() - timedelta(days=1)
if arg.startswith(("+", "-")) and arg[1:].isdigit():
return date.today() + timedelta(days=int(arg))
return datetime.strptime(arg, "%Y-%m-%d").date()
def _prompt_interactive() -> tuple[date, date, str | None]:
cz_dny = ["pondeli", "utery", "streda", "ctvrtek", "patek", "sobota", "nedele"]
today = date.today()
print("Cteni agendy z Medevio")
print(f"Dnes je {today.isoformat()} ({cz_dny[today.weekday()]})")
print()
print("Od (YYYY-MM-DD / +N / -N / dnes / zitra / vcera, Enter = dnes):")
od = _parse_day_arg(input("> ").strip() or None)
print(f"Do (Enter = stejny den jako od = {od.isoformat()}):")
do_raw = input("> ").strip()
do = _parse_day_arg(do_raw) if do_raw else od
print(f"Kalendar (vlado / manzelka / Enter = oba):")
cal = input("> ").strip() or None
return od, do, cal
def main():
ap = argparse.ArgumentParser(description="Cteni agendy z Medevio.")
ap.add_argument("--od", help="Pocatecni datum 'YYYY-MM-DD' / +N / -N / dnes/zitra/vcera")
ap.add_argument("--do", dest="do_", help="Koncove datum (default = stejny jako --od)")
ap.add_argument("--den", help="Zkratka: --od i --do nastaveny na tento den")
ap.add_argument("--kalendar", default=None,
help=f"{'/'.join(CALENDARS)} nebo UUID. Bez parametru = oba.")
args = ap.parse_args()
if args.den:
od = do = _parse_day_arg(args.den)
cal = args.kalendar
elif args.od:
od = _parse_day_arg(args.od)
do = _parse_day_arg(args.do_) if args.do_ else od
cal = args.kalendar
else:
od, do, cal = _prompt_interactive()
reservations = list_agendu(od, do, calendar=cal)
print_agenda(reservations)
if __name__ == "__main__":
main()