Skip to main content
Glama

EDT Unicaen MCP Server

by Infuseting
utils.py13.3 kB
import json import urllib.request import urllib.parse import datetime import re from utils import * from typing import Optional base_url = "https://edt.infuseting.fr/assets/json/" prof_url = base_url + "prof.json" salle_url = base_url + "salle.json" student_url = base_url + "student.json" univ_url = base_url + "univ.json" def load_json(path: str): """Load JSON from local file or URL.""" if path.startswith("http://") or path.startswith("https://"): with urllib.request.urlopen(path) as resp: return json.load(resp) else: with open(path, "r", encoding="utf-8") as f: return json.load(f) return None _prof_data = load_json(prof_url).get("prof", []) _salle_data = load_json(salle_url).get("salle", []) _student_data = load_json(student_url).get("student", []) _univ_data = load_json(univ_url).get("univ", []) def parse_limit_to_datetime(value: Optional[str]) -> Optional[datetime.datetime]: """Try to parse a start/end limit string into a datetime. Accepts: - ISO-like datetime strings (YYYY-MM-DDTHH:MM[:SS] or YYYY-MM-DD HH:MM[:SS]) - Time-only strings like HH:MM or HH:MM:SS (interpreted for today) Returns a naive datetime or None if empty/invalid. """ if not value: return None s = str(value).strip() if not s: return None # Try ISO datetime parse first (also accepts 'YYYY-MM-DD HH:MM' / 'YYYY-MM-DDTHH:MM') try: return datetime.datetime.fromisoformat(s) except Exception: pass # Common date-only formats: YYYY-MM-DD, DD/MM/YYYY, DD-MM-YYYY # Interpret as start of that day (00:00:00) m_date_iso = re.match(r"^(\d{4})-(\d{2})-(\d{2})$", s) if m_date_iso: try: d = datetime.date.fromisoformat(s) return datetime.datetime.combine(d, datetime.time.min) except Exception: pass m_date_eu = re.match(r"^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})$", s) if m_date_eu: try: day = int(m_date_eu.group(1)) month = int(m_date_eu.group(2)) year = int(m_date_eu.group(3)) d = datetime.date(year, month, day) return datetime.datetime.combine(d, datetime.time.min) except Exception: pass # Natural words if s.lower() in ("today", "aujourd'hui", "aujourdhui"): d = datetime.date.today() return datetime.datetime.combine(d, datetime.time.min) if s.lower() in ("tomorrow", "demain"): d = datetime.date.today() + datetime.timedelta(days=1) return datetime.datetime.combine(d, datetime.time.min) # If it's only a time like HH:MM or HH:MM:SS, interpret for today m = re.match(r"^(\d{1,2}):(\d{2})(:(\d{2}))?$", s) if m: hour = int(m.group(1)) minute = int(m.group(2)) second = int(m.group(4)) if m.group(4) else 0 today = datetime.date.today() try: return datetime.datetime.combine(today, datetime.time(hour, minute, second)) except Exception: return None # Unknown format return None def find_entries_by_name(name: str): """Search in prof/student/salle/univ for matching descTT/nameUniv. Returns a list of entries with keys: type, desc, adeUniv, adeResources, adeProjectId """ name_l = name.lower() results = [] def check_list(items, key_desc, typ): for it in items: desc = (it.get(key_desc) or "").strip() if desc and name_l in desc.lower(): results.append({ "type": typ, "desc": desc, "adeUniv": it.get("adeUniv"), "adeResources": it.get("adeResources"), "adeProjectId": int(it.get("adeProjectId")) if it.get("adeProjectId") is not None else None, }) check_list(_prof_data, "descTT", "prof") check_list(_student_data, "descTT", "student") check_list(_salle_data, "descTT", "salle") # univ entries have nameUniv and timetable list for u in _univ_data: if name_l in (u.get("nameUniv","") or "").lower(): results.append({ "type": "univ", "desc": u.get("nameUniv"), "adeUniv": u.get("adeUniv"), "adeResources": None, "adeProjectId": None, "timetable": u.get("timetable", []), }) # also check timetable entries for t in u.get("timetable", []): if name_l in (t.get("descTT","") or "").lower(): results.append({ "type": "univ-timetable", "desc": t.get("descTT"), "adeUniv": u.get("adeUniv"), "adeResources": t.get("adeResources"), "adeProjectId": int(t.get("adeProjectId")) if t.get("adeProjectId") is not None else None, }) return results def build_ade_url(entry, date: datetime.date | None = None): """Construct the ADE/proxy URL according to adeProjectId rules. - adeProjectId == 2024 -> intervenant ICS via proxy (intervenant) - adeProjectId == 2023 -> etudiant ICS via proxy (etudiant) - otherwise -> anonymous_cal.jsp with params (resources, projectId, firstDate, lastDate) """ # New behaviour: single request to edt.infuseting.fr/update with parameters. adeResources = entry.get("adeResources") adeProjectId = entry.get("adeProjectId") if adeProjectId is None or adeResources is None: return None d = date or datetime.date.today() params = { "adeBase": str(adeProjectId), "adeRessources": str(adeResources), "lastUpdate": "0", "date": d.strftime("%Y-%m-%d"), } print("https://edt.infuseting.fr/update/index.php" + "?" + urllib.parse.urlencode(params)) return "https://edt.infuseting.fr/update/index.php" + "?" + urllib.parse.urlencode(params) def fetch_url(url: str, timeout: int = 15) -> str: req = urllib.request.Request(url, headers={"User-Agent": "MCPEdtUnicaen/1.0"}) with urllib.request.urlopen(req, timeout=timeout) as resp: charset = resp.headers.get_content_charset() or "utf-8" return resp.read().decode(charset, errors="ignore") def parse_ics_next_event(ics_text: str): """Very small ICS parser: extract VEVENT DTSTART/SUMMARY and return next event after now. Returns dict or None. """ now = datetime.datetime.now() events = [] parts = re.split(r"BEGIN:VEVENT\r?\n", ics_text) for part in parts[1:]: # limit to until END:VEVENT vevent = part.split("END:VEVENT")[0] m = re.search(r"DTSTART(?:;[^:]+)?:([0-9TZ+-]+)", vevent) if not m: continue dtstr = m.group(1).strip() # ignore all-day events (DATE only) if re.fullmatch(r"\d{8}", dtstr): continue # try parse dt = None try: if dtstr.endswith("Z"): dt = datetime.datetime.strptime(dtstr, "%Y%m%dT%H%M%SZ") else: # try with seconds try: dt = datetime.datetime.strptime(dtstr, "%Y%m%dT%H%M%S") except Exception: dt = datetime.datetime.strptime(dtstr, "%Y%m%dT%H%M") except Exception: # fallback: skip continue # summary s = "" m2 = re.search(r"SUMMARY:(.+)", vevent) if m2: s = m2.group(1).strip() events.append({"start": dt, "summary": s, "raw": vevent}) # pick next future = [e for e in events if e["start"] and e["start"] > now] if not future: return None future.sort(key=lambda e: e["start"]) nxt = future[0] return {"start": nxt["start"].isoformat(), "summary": nxt["summary"]} def parse_ics_events(ics_text: str): """Parse ICS and return list of events with start, end, summary.""" events = [] parts = re.split(r"BEGIN:VEVENT\r?\n", ics_text) for part in parts[1:]: vevent = part.split("END:VEVENT")[0] m = re.search(r"DTSTART(?:;[^:]+)?:([0-9TZ+-]+)", vevent) if not m: continue dtstart = m.group(1).strip() # skip all-day if re.fullmatch(r"\d{8}", dtstart): continue dt1 = None try: if dtstart.endswith("Z"): dt1 = datetime.datetime.strptime(dtstart, "%Y%m%dT%H%M%SZ") else: try: dt1 = datetime.datetime.strptime(dtstart, "%Y%m%dT%H%M%S") except Exception: dt1 = datetime.datetime.strptime(dtstart, "%Y%m%dT%H%M") except Exception: continue # DTEND dt2 = None m2 = re.search(r"DTEND(?:;[^:]+)?:([0-9TZ+-]+)", vevent) if m2: dtend = m2.group(1).strip() try: if dtend.endswith("Z"): dt2 = datetime.datetime.strptime(dtend, "%Y%m%dT%H%M%SZ") else: try: dt2 = datetime.datetime.strptime(dtend, "%Y%m%dT%H%M%S") except Exception: dt2 = datetime.datetime.strptime(dtend, "%Y%m%dT%H%M") except Exception: dt2 = None s = "" m3 = re.search(r"SUMMARY:(.+)", vevent) if m3: s = m3.group(1).strip() events.append({"start": dt1, "end": dt2, "summary": s, "raw": vevent}) return events def parse_update_json_and_next_event(json_text: str): """Parse JSON returned by https://edt.infuseting.fr/update and find next event. Expected structure: { "YYYY-MM-DD": { "content": [ { 'DTSTART': 'YYYYmmddTHHMMSS', 'SUMMARY': '...' }, ... ], 'lastUpdate': ... }, ... } Returns next event dict or None. """ try: data = json.loads(json_text) except Exception: return None now = datetime.datetime.now() events = [] for key, val in data.items(): # skip non-dict if not isinstance(val, dict): continue content = val.get("content") or [] if not isinstance(content, list): continue for ev in content: dtstr = ev.get("DTSTART") or ev.get("start") or None summary = ev.get("SUMMARY") or ev.get("summary") or ev.get("SUMMARY:CONFERENCE") or "" if not dtstr: continue # try parsing with common formats dt = None for fmt in ("%Y%m%dT%H%M%S", "%Y%m%dT%H%M"): try: dt = datetime.datetime.strptime(dtstr, fmt) break except Exception: continue if not dt: # try ISO parse fallback try: dt = datetime.datetime.fromisoformat(dtstr) except Exception: continue events.append({"start": dt, "summary": summary, "raw": ev}) future = [e for e in events if e["start"] and e["start"] > now] if not future: return None future.sort(key=lambda e: e["start"]) nxt = future[0] return {"start": nxt["start"].isoformat(), "summary": nxt["summary"]} def parse_update_json_events(json_text: str, only_date: str | None = None): """Return list of events (with start, end, summary) from update endpoint JSON. If only_date is provided (YYYY-MM-DD), only events for that date are returned. """ try: data = json.loads(json_text) except Exception: return [] events = [] for key, val in data.items(): if only_date and key != only_date: continue if not isinstance(val, dict): continue content = val.get("content") or [] if not isinstance(content, list): continue for ev in content: dtstr = ev.get("DTSTART") or ev.get("start") or None dtendstr = ev.get("DTEND") or ev.get("end") or None summary = ev.get("SUMMARY") or ev.get("summary") or "" if not dtstr: continue dt = None for fmt in ("%Y%m%dT%H%M%S", "%Y%m%dT%H%M"): try: dt = datetime.datetime.strptime(dtstr, fmt) break except Exception: continue if not dt: try: dt = datetime.datetime.fromisoformat(dtstr) except Exception: continue dtend = None if dtendstr: for fmt in ("%Y%m%dT%H%M%S", "%Y%m%dT%H%M"): try: dtend = datetime.datetime.strptime(dtendstr, fmt) break except Exception: continue if not dtend: try: dtend = datetime.datetime.fromisoformat(dtendstr) except Exception: dtend = None events.append({"start": dt, "end": dtend, "summary": summary, "raw": ev}) return events

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Infuseting/MCPEdtUnicaen'

If you have feedback or need assistance with the MCP directory API, please join our Discord server