oracle.py
⎇
Raw
1# /// script
2# dependencies = ["icalendar==7.3.0", "recurring-ical-events==3.8.2"]
3# ///
4"""Expected VEVENT instances for tests/corpus.rs, from recurring-ical-events.
5
6 uv run oracle.py <fixture dir> > oracle.tsv # compact, checked in
7 uv run oracle.py <fixture dir> --full > full.tsv # every start, for review
8
9Conventions shared with corpus.rs: starts in [1970, 2040) as unix seconds,
10floating times and dates in UTC, X-WR-TIMEZONE ignored.
11"""
12import datetime as dt
13import os
14import sys
15from concurrent.futures import ProcessPoolExecutor
16
17UTC = dt.timezone.utc
18LO, HI = dt.datetime(1970, 1, 1, tzinfo=UTC), dt.datetime(2040, 1, 1, tzinfo=UTC)
19
20
21def fnv(data: bytes) -> str:
22 h = 0xCBF29CE484222325
23 for b in data:
24 h = ((h ^ b) * 0x100000001B3) & 0xFFFFFFFFFFFFFFFF
25 return f"{h:016x}"
26
27
28def expand(path):
29 # One process per file: icalendar caches TZIDs globally, so a VTIMEZONE
30 # from one fixture would leak into the next.
31 import icalendar
32 import recurring_ical_events
33
34 raw = open(path, "rb").read()
35 try:
36 txt = raw.decode("utf-8")
37 except UnicodeDecodeError:
38 txt = raw.decode("latin-1")
39 starts = []
40 try:
41 for cal in icalendar.Calendar.from_ical(txt, multiple=True):
42 cal.pop("X-WR-TIMEZONE", None)
43 for ev in recurring_ical_events.of(cal, components=["VEVENT"]).between(LO, HI):
44 s = ev.get("DTSTART")
45 if s is None:
46 continue
47 s = s.dt
48 if not isinstance(s, dt.datetime):
49 s = dt.datetime(s.year, s.month, s.day)
50 if s.tzinfo is None:
51 s = s.replace(tzinfo=UTC)
52 if LO <= s < HI:
53 starts.append(int(s.timestamp()))
54 status = "ok"
55 except Exception as e:
56 status = "error:" + type(e).__name__
57 starts.sort()
58 return fnv(raw), status, starts
59
60
61def main():
62 d, full = sys.argv[1], "--full" in sys.argv
63 names = sorted(n for n in os.listdir(d) if n.endswith(".ics"))
64 with ProcessPoolExecutor(max_tasks_per_child=1) as pool:
65 for name, (file_hash, status, starts) in zip(
66 names, pool.map(expand, [os.path.join(d, n) for n in names])
67 ):
68 text = ",".join(map(str, starts))
69 last = text if full else fnv(text.encode())
70 print(f"{name}\t{file_hash}\t{status}\t{len(starts)}\t{last}", flush=True)
71
72
73if __name__ == "__main__":
74 main()
75