-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathmain.py
133 lines (112 loc) · 4.91 KB
/
main.py
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
127
128
129
130
131
132
133
from typing import Optional, Literal
import datetime
from datetime import timedelta
from enum import Enum
import typer
import pytz
from tasklib import TaskWarrior, Task
from icalendar import Calendar, Event
class CalendarType(str, Enum):
due = "due"
plan = "plan"
def main(calendar: CalendarType):
tw = TaskWarrior()
tasks = tw.tasks.filter("-COMPLETED -DELETED")
due_tasks = tasks.filter("due.any:")
scheduled_tasks = tasks.filter("plan.any:")
cal = Calendar(version="2.0", prodid="-//Allgreed//tw-ical-feed//")
if calendar == CalendarType.due:
calendar_name = "Tasks due"
for t in due_tasks:
cal.add_component(mk_event(t, False))
elif calendar == CalendarType.plan:
calendar_name = "Tasks planned"
for t in scheduled_tasks:
cal.add_component(mk_event(t, True))
else:
assert 0, "unreachable"
cal.add("summary", calendar_name)
cal.add("X-WR-CALNAME", calendar_name)
print(cal.to_ical().decode("utf-8"))
# note: iphone calendar works instantly after manual refresh
# note: gmail takes ~24-36 hours and doesn't react to modifications
def mk_event(t: Task, plan: bool) -> Event:
# TODO: unify the hanlding between due and planned!!!
# !!!!!!!!!!!
if not plan:
uuid, description, due_date, entry, modified = t["uuid"], t["description"], t["due"].date(), t["entry"], t["modified"]
event = Event(
summary=description,
uid=uuid,
# this is the reason why there has to be 2 streams for the planned and due - otherwise I'd have to maitain
# some kind of state of use ugly hack like plan being due uuid +1
)
# huh, cannot specify "last-modified" as a constructor parameter? :c
# also: it doesn't fire conversion when passed as constructor parameters - that's why the dtstart date
# conversion hasn't kicked in
# TODO: maybe address this?
event.add("dtstart", due_date)
event.add("dtend", due_date + timedelta(days=1))
event.add("dtstamp", entry)
event.add("last-modified", modified)
event.add("description", uuid)
event.add("location", t["geo"])
return event
if t["plan"]:
# TODO: this is copy-paste of the due handling, maybe something can be abstracted?
uuid, description, _planned_time, entry, modified = t["uuid"], t["description"], t["plan"], t["entry"], t["modified"]
event = Event(
summary=description,
uid=uuid,
)
planned_time = datetime.datetime.strptime(_planned_time, "%Y%m%dT%H%M%SZ").replace(tzinfo=pytz.utc)
now = datetime.datetime.now()
utc_offset = now.astimezone().utcoffset()
assert utc_offset
midnight_reference = (now.replace(hour=0,minute=0,second=0,microsecond=0) - utc_offset).time()
assert 1, "Tasks *were* created in the same timezone as this programme is run"
# TODO: ^ this obviously doesn't hold during time change / aka dailght saving switch, even for the happy path,
# since Taskwarrior doesn't store the creation timezone I don't necessarily have a better idea on how to do it
# also: no idea how to enforce the assert programmatically
if planned_time.time() == midnight_reference:
dtime = timedelta(days=1)
planned_time = (planned_time + utc_offset).date()
else:
dtime = parse_UDA_duration(t["estimate"]) or timedelta(minutes=30)
# TODO: when merging, if merging - with intraday due dates the event should start 15 minutes *before* the due date and end on the due date exactly
# ^ wat?
event.add("dtstart", planned_time)
event.add("dtend", planned_time + dtime)
event.add("dtstamp", entry)
event.add("last-modified", modified)
event.add("description", uuid)
event.add("location", t["geo"])
return event
# TODO: upstream parsing uda by type to tasklib? -> https://github.com/GothenburgBitFactory/tasklib/issues/131
def parse_UDA_duration(maybe_uda_duration: Optional[str]) -> Optional[timedelta]:
# PT30M is 30 minutes
# PT5H is 5h
# Guess what PT5H30M is 5h 30 minutes
# TODO: add proper tests
# TODO: handle more values
if not maybe_uda_duration:
return None
# TODO: unfuck implementation
def fuj():
try:
return datetime.datetime.strptime(maybe_uda_duration, "PT%HH")
except ValueError:
try:
return datetime.datetime.strptime(maybe_uda_duration, "PT%MM")
except ValueError:
return datetime.datetime.strptime(maybe_uda_duration, "PT%HH%MM")
try:
dt = fuj()
except ValueError as e:
# TODO: singal error to stderr
return None
else:
base_offset = datetime.datetime(year=1900, day=1, month=1)
return dt - base_offset
if __name__ == "__main__":
typer.run(main)