-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtaskcal.py
More file actions
143 lines (118 loc) · 4.33 KB
/
taskcal.py
File metadata and controls
143 lines (118 loc) · 4.33 KB
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
134
135
136
137
138
139
140
141
142
143
#!/usr/bin/env python
# TODO
# set custom prodid
# apply tw filter in Taskcal's constructor already
import os
from collections import namedtuple
from pathlib import Path
from typing import DefaultDict, Dict
from icalendar import Calendar, Todo
from tasklib import Task, TaskWarrior
from tasklib.filters import TaskWarriorFilter
icalendar_defaults = {
"version": "2.0",
"prodid": "-//SabreDAV//SabreDAV//EN",
"calscale": "gregorian",
}
# Taskwarrior attributes to icalendar properties
Assoc = namedtuple("Assoc", ["attr", "prop"])
simple_associations = frozenset(
[
Assoc("uuid", "uid"),
Assoc("description", "summary"),
Assoc("tags", "categories"),
]
)
date_associations = frozenset(
[
Assoc("modified", "dtstamp"),
Assoc("end", "completed"),
Assoc("due", "due"),
Assoc("scheduled", "start"),
]
)
direct_associations = simple_associations | date_associations
other_associations = frozenset(
[
Assoc("priority", "priority"),
Assoc("depends", "related-to"),
Assoc("status", "status"),
]
)
priorities = {"H": 0, "M": 5, "L": 9}
class Taskcal:
def __init__(
self,
*,
tw_rc: str = None,
tw_data_dir: str = None,
filter: str = "status.any:",
) -> None:
# When tasklib runs taskwarrior commands, it overrides the data.location
# settings if the user sets it to a custom value in the TaskWarrior
# constructor. It also sets TASKRC to a custom value if the user sets
# that too in the constructor. If only one of the two (or none of the
# two) is set by the user, the taskwarrior command will do its own
# resolution of the data location based on, among others, the env
# variables TASKDATA and TASKRC. To be sure that when I use
# `tw_data_dir` or `tw_rc` in this constructor I will always use the
# intended path, I unset these env variables.
os.environ.pop("TASKDATA", None)
os.environ.pop("TASKRC", None)
if tw_data_dir and tw_rc:
raise TypeError(
"only one of 'tw_data_dir' and 'tw_rc' must be given"
)
elif tw_data_dir:
if not Path(tw_data_dir).exists():
raise FileNotFoundError
if not Path(tw_data_dir).is_dir():
raise NotADirectoryError(f"'{tw_data_dir}' is not a directory")
self.tw = TaskWarrior(data_location=tw_data_dir)
elif tw_rc:
if not Path(tw_rc).exists():
raise FileNotFoundError
if not Path(tw_rc).is_file():
raise FileNotFoundError(f"'{tw_rc}' is not a file")
self.tw = TaskWarrior(taskrc_location=tw_rc)
else:
raise TypeError("no taskwarrior data dir or taskrc path given")
self.filter = TaskWarriorFilter(self.tw)
self.filter.add_filter(filter)
@property
def calendars(self) -> Dict[str, Calendar]:
calendars: DefaultDict[str, Calendar] = DefaultDict(
lambda: Calendar(icalendar_defaults)
)
tasks = self.tw.filter_tasks(self.filter)
if not tasks:
# Initialize the default calendar
calendars["<noname>"]
for task in tasks:
task.refresh()
todo = Todo()
for assoc in direct_associations:
if task[assoc.attr]:
todo.add(assoc.prop, task[assoc.attr])
if task["priority"]:
todo.add("priority", priorities.get(task["priority"]))
todo.add("status", self.tw_status_to_ics_status(task))
for dependency in task["depends"] or []:
todo.add("related-to", dependency["uuid"])
calendars[task["project"] or "<noname>"].add_component(todo)
for calname, calendar in calendars.items():
if calname == "<noname>":
continue
calendar.add("X-WR-CALNAME", calname)
calendar.add("NAME", calname)
return dict(calendars)
@staticmethod
def tw_status_to_ics_status(task: Task) -> str:
if task.active:
return "in-process"
elif task.completed:
return "completed"
elif task.deleted:
return "cancelled"
else: # pending/waiting
return "needs-action"