#!/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()