taskcheck 0.1.1__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.1/taskcheck.egg-info → taskcheck-0.1.2}/PKG-INFO +14 -5
- {taskcheck-0.1.1 → taskcheck-0.1.2}/README.md +13 -3
- {taskcheck-0.1.1 → taskcheck-0.1.2}/pyproject.toml +2 -7
- {taskcheck-0.1.1 → taskcheck-0.1.2}/taskcheck/__main__.py +17 -4
- {taskcheck-0.1.1 → taskcheck-0.1.2}/taskcheck/ical.py +51 -30
- {taskcheck-0.1.1 → taskcheck-0.1.2/taskcheck.egg-info}/PKG-INFO +14 -5
- {taskcheck-0.1.1 → taskcheck-0.1.2}/taskcheck.egg-info/requires.txt +0 -1
- {taskcheck-0.1.1 → taskcheck-0.1.2}/LICENSE +0 -0
- {taskcheck-0.1.1 → taskcheck-0.1.2}/setup.cfg +0 -0
- {taskcheck-0.1.1 → taskcheck-0.1.2}/taskcheck.egg-info/SOURCES.txt +0 -0
- {taskcheck-0.1.1 → taskcheck-0.1.2}/taskcheck.egg-info/dependency_links.txt +0 -0
- {taskcheck-0.1.1 → taskcheck-0.1.2}/taskcheck.egg-info/entry_points.txt +0 -0
- {taskcheck-0.1.1 → 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,6 +75,7 @@ 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:
|
|
80
81
|
# we use str to make object serializable as jsons
|
|
@@ -87,13 +88,24 @@ def get_available_hours(time_map, date, calendars):
|
|
|
87
88
|
break
|
|
88
89
|
elif event["end"].date() < date:
|
|
89
90
|
continue
|
|
91
|
+
|
|
90
92
|
# check if the event overlaps with one of the working hours
|
|
91
93
|
event_start = event["start"].time()
|
|
92
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
|
+
|
|
93
100
|
if event_start < schedule_end and event_end > schedule_start:
|
|
94
|
-
|
|
101
|
+
schedule_blocked_hours += _time_to_decimal(
|
|
95
102
|
min(schedule_end, event_end)
|
|
96
|
-
) -
|
|
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
|
|
97
109
|
available_hours -= blocked_hours
|
|
98
110
|
return available_hours
|
|
99
111
|
|
|
@@ -204,6 +216,7 @@ def get_calendars(config):
|
|
|
204
216
|
all_day=calendar["event_all_day_is_blocking"],
|
|
205
217
|
expiration=calendar["expiration"],
|
|
206
218
|
verbose=args.verbose,
|
|
219
|
+
tz_name=calendar.get("timezone"),
|
|
207
220
|
)
|
|
208
221
|
calendar.sort(key=lambda e: e["start"])
|
|
209
222
|
calendars.append(calendar)
|
|
@@ -3,38 +3,29 @@ import json
|
|
|
3
3
|
from icalendar import Calendar
|
|
4
4
|
from datetime import datetime, timedelta
|
|
5
5
|
from dateutil.rrule import rruleset, rrulestr
|
|
6
|
-
|
|
6
|
+
import requests
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
import appdirs
|
|
9
|
+
import zoneinfo
|
|
9
10
|
|
|
10
11
|
import time
|
|
11
12
|
|
|
12
13
|
CACHE = Path(appdirs.user_cache_dir("taskcheck"))
|
|
13
14
|
|
|
14
15
|
|
|
15
|
-
def fetch_ical_data(url
|
|
16
|
-
|
|
17
|
-
"taskcheck",
|
|
18
|
-
use_cache_dir=True, # Save files in the default user cache dir
|
|
19
|
-
cache_control=True, # Use Cache-Control response headers for expiration, if available
|
|
20
|
-
urls_expire_after={url: expiration}, # Set expiration time
|
|
21
|
-
allowable_codes=[
|
|
22
|
-
200,
|
|
23
|
-
],
|
|
24
|
-
allowable_methods=["GET", "POST"], # Cache whatever HTTP methods you want
|
|
25
|
-
stale_if_error=True, # In case of request errors, use stale cache data if possible
|
|
26
|
-
)
|
|
27
|
-
response = session.get(url)
|
|
16
|
+
def fetch_ical_data(url):
|
|
17
|
+
response = requests.get(url)
|
|
28
18
|
response.raise_for_status()
|
|
29
19
|
return response.text
|
|
30
20
|
|
|
31
21
|
|
|
32
|
-
def parse_ical_events(ical_text, days_ahead, all_day):
|
|
22
|
+
def parse_ical_events(ical_text, days_ahead, all_day, tz_name=None, verbose=False):
|
|
33
23
|
cal = Calendar.from_ical(ical_text)
|
|
34
24
|
today = datetime.now().date()
|
|
35
25
|
end_date = today + timedelta(days=days_ahead)
|
|
36
26
|
|
|
37
27
|
events = []
|
|
28
|
+
exceptions = set()
|
|
38
29
|
|
|
39
30
|
for component in cal.walk():
|
|
40
31
|
if component.name == "VEVENT":
|
|
@@ -61,9 +52,12 @@ def parse_ical_events(ical_text, days_ahead, all_day):
|
|
|
61
52
|
)
|
|
62
53
|
event_end = event_start + timedelta(days=1) - timedelta(seconds=1)
|
|
63
54
|
if recurrence_id:
|
|
64
|
-
|
|
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())
|
|
65
59
|
|
|
66
|
-
|
|
60
|
+
occurrences = rruleset()
|
|
67
61
|
if recurrence_rule:
|
|
68
62
|
# avoid handling rules that are already ended
|
|
69
63
|
# and don't create events beyond end_date
|
|
@@ -72,26 +66,49 @@ def parse_ical_events(ical_text, days_ahead, all_day):
|
|
|
72
66
|
until = until[0].date()
|
|
73
67
|
if until < today:
|
|
74
68
|
continue
|
|
75
|
-
elif until > end_date:
|
|
76
|
-
recurrence_rule["UNTIL"] = end_date
|
|
77
69
|
rrule = rrulestr(
|
|
78
70
|
str(recurrence_rule.to_ical(), "utf-8"),
|
|
79
71
|
dtstart=event_start,
|
|
80
72
|
)
|
|
81
|
-
|
|
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())
|
|
82
85
|
else:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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:
|
|
87
95
|
events.append(
|
|
88
96
|
{
|
|
89
|
-
"summary": component.get("summary"),
|
|
90
|
-
"start":
|
|
91
|
-
"end":
|
|
97
|
+
# "summary": component.get("summary"),
|
|
98
|
+
"start": occurrence.isoformat(),
|
|
99
|
+
"end": end.isoformat(),
|
|
92
100
|
}
|
|
93
101
|
)
|
|
102
|
+
elif end.date() > end_date:
|
|
103
|
+
break
|
|
94
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"])
|
|
95
112
|
return events
|
|
96
113
|
|
|
97
114
|
|
|
@@ -100,7 +117,9 @@ def get_cache_filename(url):
|
|
|
100
117
|
return CACHE / f"{hash_object.hexdigest()}.json"
|
|
101
118
|
|
|
102
119
|
|
|
103
|
-
def ical_to_dict(
|
|
120
|
+
def ical_to_dict(
|
|
121
|
+
url, days_ahead=7, all_day=False, expiration=0.25, verbose=False, tz_name=None
|
|
122
|
+
):
|
|
104
123
|
CACHE.mkdir(exist_ok=True, parents=True)
|
|
105
124
|
cache_file = CACHE / get_cache_filename(url)
|
|
106
125
|
current_time = time.time()
|
|
@@ -117,11 +136,13 @@ def ical_to_dict(url, days_ahead=7, all_day=False, expiration=0.25, verbose=Fals
|
|
|
117
136
|
|
|
118
137
|
# Fetch and parse iCal data
|
|
119
138
|
ttt = time.time()
|
|
120
|
-
ical_text = fetch_ical_data(url
|
|
139
|
+
ical_text = fetch_ical_data(url)
|
|
121
140
|
if verbose:
|
|
122
141
|
print("Time taken to fetch ical data: ", time.time() - ttt)
|
|
123
142
|
ttt = time.time()
|
|
124
|
-
events = parse_ical_events(
|
|
143
|
+
events = parse_ical_events(
|
|
144
|
+
ical_text, days_ahead, all_day, tz_name=tz_name, verbose=verbose
|
|
145
|
+
)
|
|
125
146
|
if verbose:
|
|
126
147
|
print("Time taken to parse ical data: ", time.time() - ttt)
|
|
127
148
|
|
|
@@ -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
|
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|