taskcheck 0.1.0.post1__tar.gz → 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {taskcheck-0.1.0.post1/taskcheck.egg-info → taskcheck-0.1.2}/PKG-INFO +14 -5
- {taskcheck-0.1.0.post1 → taskcheck-0.1.2}/README.md +13 -3
- {taskcheck-0.1.0.post1 → taskcheck-0.1.2}/pyproject.toml +2 -7
- {taskcheck-0.1.0.post1 → taskcheck-0.1.2}/taskcheck/__main__.py +28 -9
- taskcheck-0.1.2/taskcheck/ical.py +153 -0
- {taskcheck-0.1.0.post1 → taskcheck-0.1.2/taskcheck.egg-info}/PKG-INFO +14 -5
- {taskcheck-0.1.0.post1 → taskcheck-0.1.2}/taskcheck.egg-info/requires.txt +0 -1
- taskcheck-0.1.0.post1/taskcheck/ical.py +0 -88
- {taskcheck-0.1.0.post1 → taskcheck-0.1.2}/LICENSE +0 -0
- {taskcheck-0.1.0.post1 → taskcheck-0.1.2}/setup.cfg +0 -0
- {taskcheck-0.1.0.post1 → taskcheck-0.1.2}/taskcheck.egg-info/SOURCES.txt +0 -0
- {taskcheck-0.1.0.post1 → taskcheck-0.1.2}/taskcheck.egg-info/dependency_links.txt +0 -0
- {taskcheck-0.1.0.post1 → taskcheck-0.1.2}/taskcheck.egg-info/entry_points.txt +0 -0
- {taskcheck-0.1.0.post1 → taskcheck-0.1.2}/taskcheck.egg-info/top_level.txt +0 -0
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: taskcheck
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Automatic scheduler for taskwarrior
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
7
|
License-File: LICENSE
|
|
8
8
|
Requires-Dist: appdirs>=1.4.4
|
|
9
9
|
Requires-Dist: icalendar>=6.0.1
|
|
10
|
-
Requires-Dist: requests-cache>=1.2.1
|
|
11
10
|
Requires-Dist: requests>=2.32.3
|
|
12
11
|
|
|
13
12
|
|
|
@@ -72,6 +71,7 @@ days_ahead = 1000 # how far go with the schedule (lower values make the computat
|
|
|
72
71
|
[calendars.1]
|
|
73
72
|
url = "https://your/url/to/calendar.ics"
|
|
74
73
|
expiration = 0.08 # in hours (0.08 hours =~ 5 minutes)
|
|
74
|
+
timezone = "Europe/Rome" # if set, force timezone for this calendar; timezone values are TZ identifiers (https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)
|
|
75
75
|
|
|
76
76
|
[calendars.holidays]
|
|
77
77
|
url = "https://www.officeholidays.com/ics-clean/italy/milan"
|
|
@@ -92,16 +92,21 @@ uda.completion_date.type = date
|
|
|
92
92
|
uda.completion_date.label = Expected Completion Date
|
|
93
93
|
|
|
94
94
|
# Adjust urgency for taskcheck
|
|
95
|
-
|
|
96
|
-
urgency.waiting.coefficient = 0.0
|
|
95
|
+
# you will use `wait:` to avoid scheduling tasks too much sooner
|
|
96
|
+
urgency.waiting.coefficient = 0.0
|
|
97
|
+
# you will use `active:` to prioritize tasks already started even if the estimated amount is decreased
|
|
98
|
+
urgency.active.coefficient = 3.0
|
|
99
|
+
urgency.scheduled.coefficient = 0.0 # tasks that are not scheduled may still be urgent
|
|
97
100
|
|
|
98
101
|
# we need to be able to modify recurrent tasks without prompt
|
|
99
102
|
recurrence.confirmation=no
|
|
100
103
|
|
|
101
104
|
# suggested
|
|
102
|
-
urgency.inherit=1
|
|
105
|
+
urgency.inherit=1 # use dependencies to automatically prioritize tasks
|
|
103
106
|
urgency.blocking.coefficient=2
|
|
104
107
|
|
|
108
|
+
# give urgeny values to the estimated time
|
|
109
|
+
# this is ln(estimation)
|
|
105
110
|
urgency.uda.estimated.PT1H.coefficient = 1
|
|
106
111
|
urgency.uda.estimated.PT2H.coefficient = 2.32
|
|
107
112
|
urgency.uda.estimated.PT3H.coefficient = 3.67
|
|
@@ -138,11 +143,15 @@ urgency.uda.estimated.PT33H.coefficient = 11.73
|
|
|
138
143
|
urgency.uda.estimated.PT34H.coefficient = 11.81
|
|
139
144
|
urgency.uda.estimated.PT35H.coefficient = 11.89
|
|
140
145
|
urgency.uda.estimated.PT36H.coefficient = 11.96
|
|
146
|
+
# maximum duration is 36 hours
|
|
147
|
+
|
|
141
148
|
```
|
|
142
149
|
|
|
143
150
|
## CLI Options
|
|
144
151
|
|
|
145
152
|
```
|
|
153
|
+
|
|
146
154
|
-v, --verbose: increase output verbosity
|
|
147
155
|
-t, --today: how much time has been already used today (default: 0)
|
|
156
|
+
|
|
148
157
|
```
|
|
@@ -60,6 +60,7 @@ days_ahead = 1000 # how far go with the schedule (lower values make the computat
|
|
|
60
60
|
[calendars.1]
|
|
61
61
|
url = "https://your/url/to/calendar.ics"
|
|
62
62
|
expiration = 0.08 # in hours (0.08 hours =~ 5 minutes)
|
|
63
|
+
timezone = "Europe/Rome" # if set, force timezone for this calendar; timezone values are TZ identifiers (https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)
|
|
63
64
|
|
|
64
65
|
[calendars.holidays]
|
|
65
66
|
url = "https://www.officeholidays.com/ics-clean/italy/milan"
|
|
@@ -80,16 +81,21 @@ uda.completion_date.type = date
|
|
|
80
81
|
uda.completion_date.label = Expected Completion Date
|
|
81
82
|
|
|
82
83
|
# Adjust urgency for taskcheck
|
|
83
|
-
|
|
84
|
-
urgency.waiting.coefficient = 0.0
|
|
84
|
+
# you will use `wait:` to avoid scheduling tasks too much sooner
|
|
85
|
+
urgency.waiting.coefficient = 0.0
|
|
86
|
+
# you will use `active:` to prioritize tasks already started even if the estimated amount is decreased
|
|
87
|
+
urgency.active.coefficient = 3.0
|
|
88
|
+
urgency.scheduled.coefficient = 0.0 # tasks that are not scheduled may still be urgent
|
|
85
89
|
|
|
86
90
|
# we need to be able to modify recurrent tasks without prompt
|
|
87
91
|
recurrence.confirmation=no
|
|
88
92
|
|
|
89
93
|
# suggested
|
|
90
|
-
urgency.inherit=1
|
|
94
|
+
urgency.inherit=1 # use dependencies to automatically prioritize tasks
|
|
91
95
|
urgency.blocking.coefficient=2
|
|
92
96
|
|
|
97
|
+
# give urgeny values to the estimated time
|
|
98
|
+
# this is ln(estimation)
|
|
93
99
|
urgency.uda.estimated.PT1H.coefficient = 1
|
|
94
100
|
urgency.uda.estimated.PT2H.coefficient = 2.32
|
|
95
101
|
urgency.uda.estimated.PT3H.coefficient = 3.67
|
|
@@ -126,11 +132,15 @@ urgency.uda.estimated.PT33H.coefficient = 11.73
|
|
|
126
132
|
urgency.uda.estimated.PT34H.coefficient = 11.81
|
|
127
133
|
urgency.uda.estimated.PT35H.coefficient = 11.89
|
|
128
134
|
urgency.uda.estimated.PT36H.coefficient = 11.96
|
|
135
|
+
# maximum duration is 36 hours
|
|
136
|
+
|
|
129
137
|
```
|
|
130
138
|
|
|
131
139
|
## CLI Options
|
|
132
140
|
|
|
133
141
|
```
|
|
142
|
+
|
|
134
143
|
-v, --verbose: increase output verbosity
|
|
135
144
|
-t, --today: how much time has been already used today (default: 0)
|
|
145
|
+
|
|
136
146
|
```
|
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "taskcheck"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.2"
|
|
4
4
|
description = "Automatic scheduler for taskwarrior"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
7
|
-
dependencies = [
|
|
8
|
-
"appdirs>=1.4.4",
|
|
9
|
-
"icalendar>=6.0.1",
|
|
10
|
-
"requests-cache>=1.2.1",
|
|
11
|
-
"requests>=2.32.3",
|
|
12
|
-
]
|
|
7
|
+
dependencies = ["appdirs>=1.4.4", "icalendar>=6.0.1", "requests>=2.32.3"]
|
|
13
8
|
|
|
14
9
|
[project.scripts]
|
|
15
10
|
taskcheck = "taskcheck.__main__:main"
|
|
@@ -54,11 +54,11 @@ def _hours_to_decimal(hour):
|
|
|
54
54
|
|
|
55
55
|
def _hours_to_time(hour):
|
|
56
56
|
hours = int(hour)
|
|
57
|
-
minutes = int((hour - hours) *
|
|
57
|
+
minutes = int((hour - hours) * 100)
|
|
58
58
|
return datetime.strptime(f"{hours}:{minutes}", "%H:%M").time()
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
def
|
|
61
|
+
def _time_to_decimal(time):
|
|
62
62
|
return time.hour + time.minute / 60
|
|
63
63
|
|
|
64
64
|
|
|
@@ -75,19 +75,37 @@ def get_available_hours(time_map, date, calendars):
|
|
|
75
75
|
# so let's convert them to datetime.time objects
|
|
76
76
|
schedule_start = _hours_to_time(schedule_start)
|
|
77
77
|
schedule_end = _hours_to_time(schedule_end)
|
|
78
|
+
schedule_blocked_hours = 0
|
|
78
79
|
for calendar in calendars:
|
|
79
80
|
for event in calendar:
|
|
81
|
+
# we use str to make object serializable as jsons
|
|
82
|
+
if isinstance(event["start"], str):
|
|
83
|
+
event["start"] = datetime.fromisoformat(event["start"])
|
|
84
|
+
if isinstance(event["end"], str):
|
|
85
|
+
event["end"] = datetime.fromisoformat(event["end"])
|
|
86
|
+
|
|
80
87
|
if event["start"].date() > date:
|
|
81
88
|
break
|
|
82
89
|
elif event["end"].date() < date:
|
|
83
90
|
continue
|
|
91
|
+
|
|
84
92
|
# check if the event overlaps with one of the working hours
|
|
85
93
|
event_start = event["start"].time()
|
|
86
94
|
event_end = event["end"].time()
|
|
95
|
+
if event["start"].date() < date:
|
|
96
|
+
event_start = datetime(date.year, date.month, date.day, 0, 0).time()
|
|
97
|
+
if event["end"].date() > date:
|
|
98
|
+
event_end = datetime(date.year, date.month, date.day, 23, 59).time()
|
|
99
|
+
|
|
87
100
|
if event_start < schedule_end and event_end > schedule_start:
|
|
88
|
-
|
|
101
|
+
schedule_blocked_hours += _time_to_decimal(
|
|
89
102
|
min(schedule_end, event_end)
|
|
90
|
-
) -
|
|
103
|
+
) - _time_to_decimal(max(schedule_start, event_start))
|
|
104
|
+
if args.verbose and schedule_blocked_hours > 0:
|
|
105
|
+
print(
|
|
106
|
+
f"Blocked hours on {date} between {schedule_start} and {schedule_end}: {schedule_blocked_hours}"
|
|
107
|
+
)
|
|
108
|
+
blocked_hours += schedule_blocked_hours
|
|
91
109
|
available_hours -= blocked_hours
|
|
92
110
|
return available_hours
|
|
93
111
|
|
|
@@ -141,7 +159,7 @@ def schedule_task_on_day(
|
|
|
141
159
|
# we can schedule task on this day
|
|
142
160
|
employable_hours = task_time_map[day_offset] - used_hours[day_offset]
|
|
143
161
|
current_date = today + timedelta(days=day_offset)
|
|
144
|
-
if wait and current_date
|
|
162
|
+
if wait and current_date <= wait:
|
|
145
163
|
if args.verbose:
|
|
146
164
|
print(f"Skipping date {current_date} because of wait date {wait}")
|
|
147
165
|
return start_date, end_date, task_remaining_hours, is_starting
|
|
@@ -198,6 +216,7 @@ def get_calendars(config):
|
|
|
198
216
|
all_day=calendar["event_all_day_is_blocking"],
|
|
199
217
|
expiration=calendar["expiration"],
|
|
200
218
|
verbose=args.verbose,
|
|
219
|
+
tz_name=calendar.get("timezone"),
|
|
201
220
|
)
|
|
202
221
|
calendar.sort(key=lambda e: e["start"])
|
|
203
222
|
calendars.append(calendar)
|
|
@@ -243,10 +262,10 @@ def check_tasks_sequentially(tasks, config):
|
|
|
243
262
|
f"Task {task['id']} ('{task['description']}') has no estimated time or time map: {estimated_hours}, {time_map_names}"
|
|
244
263
|
)
|
|
245
264
|
continue
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
265
|
+
if args.verbose:
|
|
266
|
+
print(
|
|
267
|
+
f"Checking task {task['id']} ('{task['description']}') with estimated hours: {estimated_hours} and wait date: {wait_date}"
|
|
268
|
+
)
|
|
250
269
|
|
|
251
270
|
task_remaining_hours = estimated_hours
|
|
252
271
|
task_time_map = get_long_range_time_map(
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
from icalendar import Calendar
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
from dateutil.rrule import rruleset, rrulestr
|
|
6
|
+
import requests
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import appdirs
|
|
9
|
+
import zoneinfo
|
|
10
|
+
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
CACHE = Path(appdirs.user_cache_dir("taskcheck"))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def fetch_ical_data(url):
|
|
17
|
+
response = requests.get(url)
|
|
18
|
+
response.raise_for_status()
|
|
19
|
+
return response.text
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def parse_ical_events(ical_text, days_ahead, all_day, tz_name=None, verbose=False):
|
|
23
|
+
cal = Calendar.from_ical(ical_text)
|
|
24
|
+
today = datetime.now().date()
|
|
25
|
+
end_date = today + timedelta(days=days_ahead)
|
|
26
|
+
|
|
27
|
+
events = []
|
|
28
|
+
exceptions = set()
|
|
29
|
+
|
|
30
|
+
for component in cal.walk():
|
|
31
|
+
if component.name == "VEVENT":
|
|
32
|
+
event_start = component.get("dtstart").dt
|
|
33
|
+
event_end = component.get("dtend").dt
|
|
34
|
+
recurrence_rule = component.get("rrule")
|
|
35
|
+
recurrence_id = component.get("recurrence-id")
|
|
36
|
+
|
|
37
|
+
# skip all-day events if not requested
|
|
38
|
+
is_all_day = not hasattr(event_start, "hour") or event_start == event_end
|
|
39
|
+
if is_all_day:
|
|
40
|
+
if not all_day:
|
|
41
|
+
continue
|
|
42
|
+
else:
|
|
43
|
+
# all-day events should block since 00:00 and end at 23:59:59
|
|
44
|
+
event_start = datetime(
|
|
45
|
+
event_start.year,
|
|
46
|
+
event_start.month,
|
|
47
|
+
event_start.day,
|
|
48
|
+
0,
|
|
49
|
+
0,
|
|
50
|
+
0,
|
|
51
|
+
0,
|
|
52
|
+
)
|
|
53
|
+
event_end = event_start + timedelta(days=1) - timedelta(seconds=1)
|
|
54
|
+
if recurrence_id:
|
|
55
|
+
if end_date >= recurrence_id.dt.date() >= today:
|
|
56
|
+
# this was an occurrence of a recurring event
|
|
57
|
+
# but has been moved, let's record the exception that must be removed
|
|
58
|
+
exceptions.add(recurrence_id.dt.isoformat())
|
|
59
|
+
|
|
60
|
+
occurrences = rruleset()
|
|
61
|
+
if recurrence_rule:
|
|
62
|
+
# avoid handling rules that are already ended
|
|
63
|
+
# and don't create events beyond end_date
|
|
64
|
+
until = recurrence_rule.get("UNTIL")
|
|
65
|
+
if until is not None:
|
|
66
|
+
until = until[0].date()
|
|
67
|
+
if until < today:
|
|
68
|
+
continue
|
|
69
|
+
rrule = rrulestr(
|
|
70
|
+
str(recurrence_rule.to_ical(), "utf-8"),
|
|
71
|
+
dtstart=event_start,
|
|
72
|
+
)
|
|
73
|
+
occurrences.rrule(rrule) # type: ignore
|
|
74
|
+
# exceptions to recurring events
|
|
75
|
+
excdates = component.get("exdate")
|
|
76
|
+
if excdates is not None:
|
|
77
|
+
if isinstance(excdates, list):
|
|
78
|
+
# concatenate all the .dts into one list
|
|
79
|
+
excdates = [e for exdate in excdates for e in exdate.dts]
|
|
80
|
+
else:
|
|
81
|
+
excdates = excdates.dts
|
|
82
|
+
for excdate in excdates:
|
|
83
|
+
if end_date >= excdate.dt.date() >= today:
|
|
84
|
+
exceptions.add(excdate.dt.isoformat())
|
|
85
|
+
else:
|
|
86
|
+
# if event is not recurring, add it as a single event
|
|
87
|
+
if event_start.date() >= today and event_end.date() <= end_date:
|
|
88
|
+
occurrences.rdate(event_start)
|
|
89
|
+
|
|
90
|
+
for occurrence in occurrences:
|
|
91
|
+
if tz_name is not None:
|
|
92
|
+
occurrence = occurrence.astimezone(zoneinfo.ZoneInfo(tz_name))
|
|
93
|
+
end = occurrence + (event_end - event_start)
|
|
94
|
+
if occurrence.date() >= today and end.date() <= end_date:
|
|
95
|
+
events.append(
|
|
96
|
+
{
|
|
97
|
+
# "summary": component.get("summary"),
|
|
98
|
+
"start": occurrence.isoformat(),
|
|
99
|
+
"end": end.isoformat(),
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
elif end.date() > end_date:
|
|
103
|
+
break
|
|
104
|
+
|
|
105
|
+
with_exc_len = len(events)
|
|
106
|
+
events = [event for event in events if event["start"] not in exceptions]
|
|
107
|
+
if verbose:
|
|
108
|
+
print(
|
|
109
|
+
f"Removed {with_exc_len - len(events)} events that are exceptions to recurring events"
|
|
110
|
+
)
|
|
111
|
+
events.sort(key=lambda x: x["start"])
|
|
112
|
+
return events
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_cache_filename(url):
|
|
116
|
+
hash_object = hashlib.sha256(url.encode())
|
|
117
|
+
return CACHE / f"{hash_object.hexdigest()}.json"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def ical_to_dict(
|
|
121
|
+
url, days_ahead=7, all_day=False, expiration=0.25, verbose=False, tz_name=None
|
|
122
|
+
):
|
|
123
|
+
CACHE.mkdir(exist_ok=True, parents=True)
|
|
124
|
+
cache_file = CACHE / get_cache_filename(url)
|
|
125
|
+
current_time = time.time()
|
|
126
|
+
|
|
127
|
+
# Check if cache file exists and is not expired
|
|
128
|
+
if cache_file.exists():
|
|
129
|
+
cache_mtime = cache_file.stat().st_mtime
|
|
130
|
+
if current_time - cache_mtime < expiration * 3600:
|
|
131
|
+
with open(cache_file, "r") as f:
|
|
132
|
+
events = json.load(f)
|
|
133
|
+
if verbose:
|
|
134
|
+
print("Loaded events from cache")
|
|
135
|
+
return events
|
|
136
|
+
|
|
137
|
+
# Fetch and parse iCal data
|
|
138
|
+
ttt = time.time()
|
|
139
|
+
ical_text = fetch_ical_data(url)
|
|
140
|
+
if verbose:
|
|
141
|
+
print("Time taken to fetch ical data: ", time.time() - ttt)
|
|
142
|
+
ttt = time.time()
|
|
143
|
+
events = parse_ical_events(
|
|
144
|
+
ical_text, days_ahead, all_day, tz_name=tz_name, verbose=verbose
|
|
145
|
+
)
|
|
146
|
+
if verbose:
|
|
147
|
+
print("Time taken to parse ical data: ", time.time() - ttt)
|
|
148
|
+
|
|
149
|
+
# Save events to cache
|
|
150
|
+
with open(cache_file, "w") as f:
|
|
151
|
+
json.dump(events, f)
|
|
152
|
+
|
|
153
|
+
return events
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: taskcheck
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Automatic scheduler for taskwarrior
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
7
|
License-File: LICENSE
|
|
8
8
|
Requires-Dist: appdirs>=1.4.4
|
|
9
9
|
Requires-Dist: icalendar>=6.0.1
|
|
10
|
-
Requires-Dist: requests-cache>=1.2.1
|
|
11
10
|
Requires-Dist: requests>=2.32.3
|
|
12
11
|
|
|
13
12
|
|
|
@@ -72,6 +71,7 @@ days_ahead = 1000 # how far go with the schedule (lower values make the computat
|
|
|
72
71
|
[calendars.1]
|
|
73
72
|
url = "https://your/url/to/calendar.ics"
|
|
74
73
|
expiration = 0.08 # in hours (0.08 hours =~ 5 minutes)
|
|
74
|
+
timezone = "Europe/Rome" # if set, force timezone for this calendar; timezone values are TZ identifiers (https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)
|
|
75
75
|
|
|
76
76
|
[calendars.holidays]
|
|
77
77
|
url = "https://www.officeholidays.com/ics-clean/italy/milan"
|
|
@@ -92,16 +92,21 @@ uda.completion_date.type = date
|
|
|
92
92
|
uda.completion_date.label = Expected Completion Date
|
|
93
93
|
|
|
94
94
|
# Adjust urgency for taskcheck
|
|
95
|
-
|
|
96
|
-
urgency.waiting.coefficient = 0.0
|
|
95
|
+
# you will use `wait:` to avoid scheduling tasks too much sooner
|
|
96
|
+
urgency.waiting.coefficient = 0.0
|
|
97
|
+
# you will use `active:` to prioritize tasks already started even if the estimated amount is decreased
|
|
98
|
+
urgency.active.coefficient = 3.0
|
|
99
|
+
urgency.scheduled.coefficient = 0.0 # tasks that are not scheduled may still be urgent
|
|
97
100
|
|
|
98
101
|
# we need to be able to modify recurrent tasks without prompt
|
|
99
102
|
recurrence.confirmation=no
|
|
100
103
|
|
|
101
104
|
# suggested
|
|
102
|
-
urgency.inherit=1
|
|
105
|
+
urgency.inherit=1 # use dependencies to automatically prioritize tasks
|
|
103
106
|
urgency.blocking.coefficient=2
|
|
104
107
|
|
|
108
|
+
# give urgeny values to the estimated time
|
|
109
|
+
# this is ln(estimation)
|
|
105
110
|
urgency.uda.estimated.PT1H.coefficient = 1
|
|
106
111
|
urgency.uda.estimated.PT2H.coefficient = 2.32
|
|
107
112
|
urgency.uda.estimated.PT3H.coefficient = 3.67
|
|
@@ -138,11 +143,15 @@ urgency.uda.estimated.PT33H.coefficient = 11.73
|
|
|
138
143
|
urgency.uda.estimated.PT34H.coefficient = 11.81
|
|
139
144
|
urgency.uda.estimated.PT35H.coefficient = 11.89
|
|
140
145
|
urgency.uda.estimated.PT36H.coefficient = 11.96
|
|
146
|
+
# maximum duration is 36 hours
|
|
147
|
+
|
|
141
148
|
```
|
|
142
149
|
|
|
143
150
|
## CLI Options
|
|
144
151
|
|
|
145
152
|
```
|
|
153
|
+
|
|
146
154
|
-v, --verbose: increase output verbosity
|
|
147
155
|
-t, --today: how much time has been already used today (default: 0)
|
|
156
|
+
|
|
148
157
|
```
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
from icalendar import Calendar
|
|
2
|
-
from datetime import datetime, timedelta
|
|
3
|
-
from dateutil.rrule import rruleset, rrulestr
|
|
4
|
-
from requests_cache import CachedSession
|
|
5
|
-
|
|
6
|
-
import time
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def fetch_ical_data(url, expiration, verbose):
|
|
10
|
-
session = CachedSession(
|
|
11
|
-
"taskcheck",
|
|
12
|
-
use_cache_dir=True, # Save files in the default user cache dir
|
|
13
|
-
cache_control=True, # Use Cache-Control response headers for expiration, if available
|
|
14
|
-
urls_expire_after={url: expiration}, # Set expiration time
|
|
15
|
-
allowable_codes=[
|
|
16
|
-
200,
|
|
17
|
-
],
|
|
18
|
-
allowable_methods=["GET", "POST"], # Cache whatever HTTP methods you want
|
|
19
|
-
stale_if_error=True, # In case of request errors, use stale cache data if possible
|
|
20
|
-
)
|
|
21
|
-
ttt = time.time()
|
|
22
|
-
response = session.get(url)
|
|
23
|
-
if verbose:
|
|
24
|
-
print("Time taken to fetch ical data: ", time.time() - ttt)
|
|
25
|
-
response.raise_for_status()
|
|
26
|
-
return response.text
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def parse_ical_events(ical_text, days_ahead, all_day):
|
|
30
|
-
cal = Calendar.from_ical(ical_text)
|
|
31
|
-
today = datetime.now().date()
|
|
32
|
-
end_date = today + timedelta(days=days_ahead)
|
|
33
|
-
|
|
34
|
-
events = []
|
|
35
|
-
|
|
36
|
-
for component in cal.walk():
|
|
37
|
-
if component.name == "VEVENT":
|
|
38
|
-
event_start = component.get("dtstart").dt
|
|
39
|
-
event_end = component.get("dtend").dt
|
|
40
|
-
recurrence_rule = component.get("rrule")
|
|
41
|
-
recurrence_id = component.get("recurrence-id")
|
|
42
|
-
|
|
43
|
-
# skip all-day events if not requested
|
|
44
|
-
is_all_day = not hasattr(event_start, "hour") or event_start == event_end
|
|
45
|
-
if is_all_day:
|
|
46
|
-
if not all_day:
|
|
47
|
-
continue
|
|
48
|
-
else:
|
|
49
|
-
# all-day events should block since 00:00 and end at 23:59:59
|
|
50
|
-
event_start = datetime(
|
|
51
|
-
event_start.year,
|
|
52
|
-
event_start.month,
|
|
53
|
-
event_start.day,
|
|
54
|
-
0,
|
|
55
|
-
0,
|
|
56
|
-
0,
|
|
57
|
-
0,
|
|
58
|
-
)
|
|
59
|
-
event_end = event_start + timedelta(days=1) - timedelta(seconds=1)
|
|
60
|
-
if recurrence_id:
|
|
61
|
-
continue # Ignore recurrence exceptions here for simplicity
|
|
62
|
-
|
|
63
|
-
event_dates = rruleset()
|
|
64
|
-
if recurrence_rule:
|
|
65
|
-
rrule = rrulestr(
|
|
66
|
-
str(recurrence_rule.to_ical(), "utf-8"), dtstart=event_start
|
|
67
|
-
)
|
|
68
|
-
event_dates.rrule(rrule) # type: ignore
|
|
69
|
-
else:
|
|
70
|
-
event_dates.rdate(event_start)
|
|
71
|
-
|
|
72
|
-
for event_date in event_dates:
|
|
73
|
-
if today <= event_date.date() <= end_date:
|
|
74
|
-
events.append(
|
|
75
|
-
{
|
|
76
|
-
"summary": component.get("summary"),
|
|
77
|
-
"start": event_date,
|
|
78
|
-
"end": event_end,
|
|
79
|
-
}
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
return events
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def ical_to_dict(url, days_ahead=7, all_day=False, expiration=0.25, verbose=False):
|
|
86
|
-
ical_text = fetch_ical_data(url, expiration * 3600, verbose)
|
|
87
|
-
events = parse_ical_events(ical_text, days_ahead, all_day)
|
|
88
|
-
return events
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|