1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
|
#!/usr/bin/env python3
#
# Converts output of remind to ICS format:
#
# Usage example: rem -ppp12 | rem2ics
#
# https://tools.ietf.org/html/rfc5545
import json
import re
import sys
from datetime import datetime, timedelta, timezone
from os.path import basename, splitext
def escape_text(text: str) -> str:
# https://tools.ietf.org/html/rfc5545#section-3.3.11
return re.sub(r"[\\;:,]", r"\\\g<0>", text)
class Event:
def __init__(self, uid: str, event_json: dict) -> None:
self.event_json = event_json
self.uid = uid
def summary(self) -> str:
event = self.event_json
summary = event.get("body", "")
match = re.search(r'%"(.+)%"', summary)
if match:
summary = match.group(1)
else:
summary = self.__remove_spec(event, summary)
return escape_text(summary)
def description(self) -> str:
event = self.event_json
summary = event.get("body", "")
if '%"' not in summary:
return summary
summary = summary.replace(r'%"', "")
summary = self.__remove_spec(event, summary)
return escape_text(summary)
def categories(self) -> str:
filename = basename(self.event_json["filename"].upper())
return splitext(filename)[0]
def dtstart(self) -> str:
event = self.event_json
if "eventstart" in event:
event_datetime = datetime.strptime(event["eventstart"], "%Y-%m-%dT%H:%M")
return self.__datetime_format(event_datetime)
return event["date"].replace("-", "")
def dtend(self) -> str:
event = self.event_json
if is_multiple_days_event(event):
event_datetime = datetime.strptime(event["until"], "%Y-%m-%d")
return (event_datetime + timedelta(days=1)).strftime("%Y%m%d")
if "eventduration" in event and "eventstart" in event:
event_datetime = datetime.strptime(event["eventstart"], "%Y-%m-%dT%H:%M")
return self.__datetime_format(
event_datetime + timedelta(minutes=int(event["eventduration"]))
)
return self.dtstart()
@staticmethod
def __datetime_format(event_datetime: datetime) -> str:
return event_datetime.astimezone(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
def __remove_spec(self, event: dict, summary: str) -> str:
if "time" in event:
summary = summary.split(" ", 1)[1]
if "passthru" in event and "COLOR" in event["passthru"]:
summary = " ".join(summary.split(" ")[3:])
return summary
def create_uid(event: dict) -> str:
if is_multiple_days_event(event):
date = event["until"]
else:
date = event["date"]
filename = event["filename"]
lineno = event["lineno"]
return f"{basename(filename)}:{lineno}:{date}"
def is_multiple_days_event(event: dict) -> bool:
repeat = "rep" in event
has_until_date = "until" in event
has_time = "time" in event
return repeat and has_until_date and not has_time
def print_ics(events: list[Event]) -> None:
print("BEGIN:VCALENDAR")
print("VERSION:2.0")
for event in events:
print("BEGIN:VEVENT")
print(f"UID:{event.uid}")
print(f"DTSTART:{event.dtstart()}")
print(f"DTEND:{event.dtend()}")
print(f"SUMMARY:{event.summary()}")
print(f"DESCRIPTION:{event.description()}")
print(f"CATEGORIES:{event.categories()}")
print("END:VEVENT")
print("END:VCALENDAR")
def rem2ics() -> None:
added = set()
events = []
data = json.load(sys.stdin)
for month in data:
for event_json in month["entries"]:
uid = create_uid(event_json)
if uid in added:
continue
added.add(uid)
events.append(Event(uid, event_json))
print_ics(events)
if __name__ == "__main__":
rem2ics()
|