taskcheck 0.1.3__tar.gz → 0.2.0__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.2.0/PKG-INFO +133 -0
- taskcheck-0.2.0/README.md +122 -0
- {taskcheck-0.1.3 → taskcheck-0.2.0}/pyproject.toml +1 -1
- taskcheck-0.2.0/taskcheck/__main__.py +48 -0
- taskcheck-0.2.0/taskcheck/common.py +184 -0
- taskcheck-0.2.0/taskcheck/install.py +143 -0
- taskcheck-0.2.0/taskcheck/parallel.py +268 -0
- taskcheck-0.2.0/taskcheck/sequential.py +149 -0
- taskcheck-0.2.0/taskcheck.egg-info/PKG-INFO +133 -0
- {taskcheck-0.1.3 → taskcheck-0.2.0}/taskcheck.egg-info/SOURCES.txt +4 -0
- taskcheck-0.1.3/PKG-INFO +0 -160
- taskcheck-0.1.3/README.md +0 -149
- taskcheck-0.1.3/taskcheck/__main__.py +0 -341
- taskcheck-0.1.3/taskcheck.egg-info/PKG-INFO +0 -160
- {taskcheck-0.1.3 → taskcheck-0.2.0}/LICENSE +0 -0
- {taskcheck-0.1.3 → taskcheck-0.2.0}/setup.cfg +0 -0
- {taskcheck-0.1.3 → taskcheck-0.2.0}/taskcheck/ical.py +0 -0
- {taskcheck-0.1.3 → taskcheck-0.2.0}/taskcheck.egg-info/dependency_links.txt +0 -0
- {taskcheck-0.1.3 → taskcheck-0.2.0}/taskcheck.egg-info/entry_points.txt +0 -0
- {taskcheck-0.1.3 → taskcheck-0.2.0}/taskcheck.egg-info/requires.txt +0 -0
- {taskcheck-0.1.3 → taskcheck-0.2.0}/taskcheck.egg-info/top_level.txt +0 -0
taskcheck-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: taskcheck
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Automatic scheduler for taskwarrior
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: appdirs>=1.4.4
|
|
9
|
+
Requires-Dist: icalendar>=6.0.1
|
|
10
|
+
Requires-Dist: requests>=2.32.3
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
> _A non-AI automatic scheduler for taskwarrior (i.e. alternative to skedpal/timehero/flowsavvy/reclaim/trevor/motion)_
|
|
14
|
+
|
|
15
|
+
This is a taskwarrior extension checks if tasks can be completed on time, considering estimated time and working hours.
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- [x] Use arbitrarily complex time maps
|
|
20
|
+
- [x] Use ical to block time from scheduling (e.g. for meetings, vacations, etc.)
|
|
21
|
+
- [x] Implement scheduling algorithm for parallely working on multiple tasks
|
|
22
|
+
- [ ] Use Google/Microsoft/Apple API to access calendars
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
1. `pipx install taskcheck`
|
|
27
|
+
2. `taskcheck --install`
|
|
28
|
+
|
|
29
|
+
## How does it work
|
|
30
|
+
|
|
31
|
+
This extension parses your pending and waiting tasks sorted decreasingly by urgency and tries to schedule them in the future.
|
|
32
|
+
It considers their estimated time to schedule all tasks starting from the most urgent one.
|
|
33
|
+
|
|
34
|
+
You will need to add the `estimated` and `time_map` UDAs to your tasks. The `estimated` attribute is
|
|
35
|
+
the expected time to complete the task in hours. The `time_map` is a comma-separated list of strings
|
|
36
|
+
that indicates the hours per day in which you will work on a task (e.g. `work`, `weekend`, etc.).
|
|
37
|
+
The exact correspondence between the `time_map` and the hours of the day is defined in the configuration
|
|
38
|
+
file of taskcheck.
|
|
39
|
+
|
|
40
|
+
`taskcheck` will modify the Taskwarrior tasks by adding the `completion_date` attribute with the expected
|
|
41
|
+
date of completion and the `scheduled` attribute with the date in which the task is expected to
|
|
42
|
+
start.
|
|
43
|
+
|
|
44
|
+
It will also print a red line for every task whose `completion_date` is after its `due_date`.
|
|
45
|
+
|
|
46
|
+
You can exclude a task from being scheduled by removing the `time_map` or `estimated` attributes.
|
|
47
|
+
|
|
48
|
+
You can see tasks that you can execute now with the `task ready` report.
|
|
49
|
+
|
|
50
|
+
You can see the schedule for a task in the `scheduling` UDA, e.g. `task scheduling:$(task calc
|
|
51
|
+
tomorrow)` will show you the tasks that include a scheduling for tomorrow.
|
|
52
|
+
|
|
53
|
+
In general, it is recommended to run taskcheck rather frequently and at least once at the beginning
|
|
54
|
+
of your working day.
|
|
55
|
+
|
|
56
|
+
### Algorithms
|
|
57
|
+
|
|
58
|
+
Taskcheck provides two scheduling algorithms: `parallel` and `sequential`.
|
|
59
|
+
|
|
60
|
+
#### Sequential
|
|
61
|
+
|
|
62
|
+
The `sequential` algorithm schedules tasks one after the other. It is the simplest algorithm.
|
|
63
|
+
It looks at your tasks one by one, computes the time slots available for that task, and fills them
|
|
64
|
+
as soon as possible.
|
|
65
|
+
|
|
66
|
+
While it sounds logical, it has a problem.
|
|
67
|
+
While you're working on a task, its remaining estimated time is decreasing, and if your urgency
|
|
68
|
+
depends on the estimated time left, the urgency of the task will decrease as well.
|
|
69
|
+
This is especially problematic for long tasks: imagine if you block your whole week for a single
|
|
70
|
+
task and all the other small tasks, even if urgent, don't get scheduled. After a few days, the
|
|
71
|
+
urgency of the first long task should have decreased.
|
|
72
|
+
|
|
73
|
+
#### Parallel
|
|
74
|
+
|
|
75
|
+
The `parallel` algorithm tries to solve the problem of the `sequential` algorithm.
|
|
76
|
+
|
|
77
|
+
Instead of allocating all the available time for a task until the task is finished, the `parallel`
|
|
78
|
+
algorithm allocates a fixed amount of time for each task (e.g. 1 hour) and then recomputes the
|
|
79
|
+
urgency and restart the allocation procedure.
|
|
80
|
+
|
|
81
|
+
If after 1 hour the long task has decreased urgency, that will be noticed and the newer most urgent
|
|
82
|
+
task will get scheduled in its place.
|
|
83
|
+
|
|
84
|
+
The minimum time for a task is by default 2 hours, but you can change it by tuning the `min_block`
|
|
85
|
+
taskwarrior UDA.
|
|
86
|
+
|
|
87
|
+
The `parallel` algorithm is the default algorithm.
|
|
88
|
+
|
|
89
|
+
## Configuration
|
|
90
|
+
|
|
91
|
+
`taskcheck --install` allows you to create required and recommended configurations for
|
|
92
|
+
taskwarrior. It will also generate a default configuration file for taskcheck.
|
|
93
|
+
|
|
94
|
+
Below is an example of taskcheck configuration file:
|
|
95
|
+
|
|
96
|
+
```toml
|
|
97
|
+
[time_maps]
|
|
98
|
+
# in which hours you will work in each day (in 24h format, if you use e.g. 25.67 you will likely
|
|
99
|
+
# get an error)
|
|
100
|
+
[time_maps.work]
|
|
101
|
+
monday = [[9, 12.30], [14, 17]]
|
|
102
|
+
tuesday = [[9, 12.30], [14, 17]]
|
|
103
|
+
wednesday = [[9, 12.30], [14, 17]]
|
|
104
|
+
thursday = [[9, 12.30], [14, 17]]
|
|
105
|
+
friday = [[9, 12.30], [14, 17]]
|
|
106
|
+
|
|
107
|
+
[time_maps.weekend]
|
|
108
|
+
saturday = [[9, 12.30], ]
|
|
109
|
+
sunday = [[9, 12.30], ]
|
|
110
|
+
|
|
111
|
+
[scheduler]
|
|
112
|
+
algorithm = "parallel" # or "sequential", see above
|
|
113
|
+
days_ahead = 1000 # how far go with the schedule (lower values make the computation faster)
|
|
114
|
+
|
|
115
|
+
[calendars]
|
|
116
|
+
# ical calendars can be used to block your time and make the scheduling more precise
|
|
117
|
+
[calendars.1]
|
|
118
|
+
url = "https://your/url/to/calendar.ics"
|
|
119
|
+
expiration = 0.08 # in hours (0.08 hours =~ 5 minutes)
|
|
120
|
+
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)
|
|
121
|
+
|
|
122
|
+
[calendars.holidays]
|
|
123
|
+
url = "https://www.officeholidays.com/ics-clean/italy/milan"
|
|
124
|
+
event_all_day_is_blocking = true
|
|
125
|
+
expiration = 720 # in hours (720 hours = 30 days)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## CLI Options
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
-v, --verbose: increase output verbosity
|
|
132
|
+
-i, --install: install taskcheck configuration
|
|
133
|
+
```
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
|
|
2
|
+
> _A non-AI automatic scheduler for taskwarrior (i.e. alternative to skedpal/timehero/flowsavvy/reclaim/trevor/motion)_
|
|
3
|
+
|
|
4
|
+
This is a taskwarrior extension checks if tasks can be completed on time, considering estimated time and working hours.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
- [x] Use arbitrarily complex time maps
|
|
9
|
+
- [x] Use ical to block time from scheduling (e.g. for meetings, vacations, etc.)
|
|
10
|
+
- [x] Implement scheduling algorithm for parallely working on multiple tasks
|
|
11
|
+
- [ ] Use Google/Microsoft/Apple API to access calendars
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
1. `pipx install taskcheck`
|
|
16
|
+
2. `taskcheck --install`
|
|
17
|
+
|
|
18
|
+
## How does it work
|
|
19
|
+
|
|
20
|
+
This extension parses your pending and waiting tasks sorted decreasingly by urgency and tries to schedule them in the future.
|
|
21
|
+
It considers their estimated time to schedule all tasks starting from the most urgent one.
|
|
22
|
+
|
|
23
|
+
You will need to add the `estimated` and `time_map` UDAs to your tasks. The `estimated` attribute is
|
|
24
|
+
the expected time to complete the task in hours. The `time_map` is a comma-separated list of strings
|
|
25
|
+
that indicates the hours per day in which you will work on a task (e.g. `work`, `weekend`, etc.).
|
|
26
|
+
The exact correspondence between the `time_map` and the hours of the day is defined in the configuration
|
|
27
|
+
file of taskcheck.
|
|
28
|
+
|
|
29
|
+
`taskcheck` will modify the Taskwarrior tasks by adding the `completion_date` attribute with the expected
|
|
30
|
+
date of completion and the `scheduled` attribute with the date in which the task is expected to
|
|
31
|
+
start.
|
|
32
|
+
|
|
33
|
+
It will also print a red line for every task whose `completion_date` is after its `due_date`.
|
|
34
|
+
|
|
35
|
+
You can exclude a task from being scheduled by removing the `time_map` or `estimated` attributes.
|
|
36
|
+
|
|
37
|
+
You can see tasks that you can execute now with the `task ready` report.
|
|
38
|
+
|
|
39
|
+
You can see the schedule for a task in the `scheduling` UDA, e.g. `task scheduling:$(task calc
|
|
40
|
+
tomorrow)` will show you the tasks that include a scheduling for tomorrow.
|
|
41
|
+
|
|
42
|
+
In general, it is recommended to run taskcheck rather frequently and at least once at the beginning
|
|
43
|
+
of your working day.
|
|
44
|
+
|
|
45
|
+
### Algorithms
|
|
46
|
+
|
|
47
|
+
Taskcheck provides two scheduling algorithms: `parallel` and `sequential`.
|
|
48
|
+
|
|
49
|
+
#### Sequential
|
|
50
|
+
|
|
51
|
+
The `sequential` algorithm schedules tasks one after the other. It is the simplest algorithm.
|
|
52
|
+
It looks at your tasks one by one, computes the time slots available for that task, and fills them
|
|
53
|
+
as soon as possible.
|
|
54
|
+
|
|
55
|
+
While it sounds logical, it has a problem.
|
|
56
|
+
While you're working on a task, its remaining estimated time is decreasing, and if your urgency
|
|
57
|
+
depends on the estimated time left, the urgency of the task will decrease as well.
|
|
58
|
+
This is especially problematic for long tasks: imagine if you block your whole week for a single
|
|
59
|
+
task and all the other small tasks, even if urgent, don't get scheduled. After a few days, the
|
|
60
|
+
urgency of the first long task should have decreased.
|
|
61
|
+
|
|
62
|
+
#### Parallel
|
|
63
|
+
|
|
64
|
+
The `parallel` algorithm tries to solve the problem of the `sequential` algorithm.
|
|
65
|
+
|
|
66
|
+
Instead of allocating all the available time for a task until the task is finished, the `parallel`
|
|
67
|
+
algorithm allocates a fixed amount of time for each task (e.g. 1 hour) and then recomputes the
|
|
68
|
+
urgency and restart the allocation procedure.
|
|
69
|
+
|
|
70
|
+
If after 1 hour the long task has decreased urgency, that will be noticed and the newer most urgent
|
|
71
|
+
task will get scheduled in its place.
|
|
72
|
+
|
|
73
|
+
The minimum time for a task is by default 2 hours, but you can change it by tuning the `min_block`
|
|
74
|
+
taskwarrior UDA.
|
|
75
|
+
|
|
76
|
+
The `parallel` algorithm is the default algorithm.
|
|
77
|
+
|
|
78
|
+
## Configuration
|
|
79
|
+
|
|
80
|
+
`taskcheck --install` allows you to create required and recommended configurations for
|
|
81
|
+
taskwarrior. It will also generate a default configuration file for taskcheck.
|
|
82
|
+
|
|
83
|
+
Below is an example of taskcheck configuration file:
|
|
84
|
+
|
|
85
|
+
```toml
|
|
86
|
+
[time_maps]
|
|
87
|
+
# in which hours you will work in each day (in 24h format, if you use e.g. 25.67 you will likely
|
|
88
|
+
# get an error)
|
|
89
|
+
[time_maps.work]
|
|
90
|
+
monday = [[9, 12.30], [14, 17]]
|
|
91
|
+
tuesday = [[9, 12.30], [14, 17]]
|
|
92
|
+
wednesday = [[9, 12.30], [14, 17]]
|
|
93
|
+
thursday = [[9, 12.30], [14, 17]]
|
|
94
|
+
friday = [[9, 12.30], [14, 17]]
|
|
95
|
+
|
|
96
|
+
[time_maps.weekend]
|
|
97
|
+
saturday = [[9, 12.30], ]
|
|
98
|
+
sunday = [[9, 12.30], ]
|
|
99
|
+
|
|
100
|
+
[scheduler]
|
|
101
|
+
algorithm = "parallel" # or "sequential", see above
|
|
102
|
+
days_ahead = 1000 # how far go with the schedule (lower values make the computation faster)
|
|
103
|
+
|
|
104
|
+
[calendars]
|
|
105
|
+
# ical calendars can be used to block your time and make the scheduling more precise
|
|
106
|
+
[calendars.1]
|
|
107
|
+
url = "https://your/url/to/calendar.ics"
|
|
108
|
+
expiration = 0.08 # in hours (0.08 hours =~ 5 minutes)
|
|
109
|
+
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)
|
|
110
|
+
|
|
111
|
+
[calendars.holidays]
|
|
112
|
+
url = "https://www.officeholidays.com/ics-clean/italy/milan"
|
|
113
|
+
event_all_day_is_blocking = true
|
|
114
|
+
expiration = 720 # in hours (720 hours = 30 days)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## CLI Options
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
-v, --verbose: increase output verbosity
|
|
121
|
+
-i, --install: install taskcheck configuration
|
|
122
|
+
```
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import tomllib
|
|
2
|
+
import argparse
|
|
3
|
+
|
|
4
|
+
from taskcheck.parallel import check_tasks_parallel
|
|
5
|
+
from taskcheck.sequential import check_tasks_sequentially
|
|
6
|
+
from taskcheck.common import config_dir
|
|
7
|
+
|
|
8
|
+
arg_parser = argparse.ArgumentParser()
|
|
9
|
+
arg_parser.add_argument(
|
|
10
|
+
"-v", "--verbose", action="store_true", help="increase output verbosity"
|
|
11
|
+
)
|
|
12
|
+
arg_parser.add_argument(
|
|
13
|
+
"-i",
|
|
14
|
+
"--install",
|
|
15
|
+
action="store_true",
|
|
16
|
+
help="install the UDAs, required settings, and default config file",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
args = arg_parser.parse_args()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Load working hours and exceptions from TOML file
|
|
23
|
+
def load_config():
|
|
24
|
+
with open(config_dir / "taskcheck.toml", "rb") as f:
|
|
25
|
+
config = tomllib.load(f)
|
|
26
|
+
return config
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main():
|
|
30
|
+
# Load data and check tasks
|
|
31
|
+
if args.install:
|
|
32
|
+
from taskcheck.install import install
|
|
33
|
+
|
|
34
|
+
install()
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
config = load_config()
|
|
38
|
+
algo = config["scheduler"].get("algorithm", "parallel")
|
|
39
|
+
if algo == "parallel":
|
|
40
|
+
check_tasks_parallel(config, verbose=args.verbose)
|
|
41
|
+
elif algo == "sequential":
|
|
42
|
+
check_tasks_sequentially(config, verbose=args.verbose)
|
|
43
|
+
else:
|
|
44
|
+
raise ValueError(f"Unknown algorithm: {algo}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
main()
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import json
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import appdirs
|
|
6
|
+
|
|
7
|
+
from taskcheck.ical import ical_to_dict
|
|
8
|
+
|
|
9
|
+
config_dir = Path(appdirs.user_config_dir("task"))
|
|
10
|
+
|
|
11
|
+
# Taskwarrior status to avoid
|
|
12
|
+
AVOID_STATUS = ["completed", "deleted", "recurring"]
|
|
13
|
+
|
|
14
|
+
long_range_time_map = {}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Get tasks from Taskwarrior and sort by urgency
|
|
18
|
+
def get_tasks():
|
|
19
|
+
result = subprocess.run(["task", "export"], capture_output=True, text=True)
|
|
20
|
+
tasks = json.loads(result.stdout)
|
|
21
|
+
return sorted(
|
|
22
|
+
(task for task in tasks if "estimated" in task),
|
|
23
|
+
key=lambda t: -t.get("urgency", 0),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def hours_to_decimal(hour):
|
|
28
|
+
return int(hour) + (hour - int(hour)) * 100 / 60
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def hours_to_time(hour):
|
|
32
|
+
hours = int(hour)
|
|
33
|
+
minutes = int((hour - hours) * 100)
|
|
34
|
+
return datetime.strptime(f"{hours}:{minutes}", "%H:%M").time()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def time_to_decimal(time):
|
|
38
|
+
# round to 2 digits after the point
|
|
39
|
+
return time.hour + time.minute / 60
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_available_hours(time_map, date, calendars, verbose=False):
|
|
43
|
+
day_of_week = date.strftime("%A").lower()
|
|
44
|
+
schedule = time_map.get(day_of_week, [])
|
|
45
|
+
available_hours = sum(
|
|
46
|
+
hours_to_decimal(end) - hours_to_decimal(start) for start, end in schedule
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
blocked_hours = 0
|
|
50
|
+
for schedule_start, schedule_end in schedule:
|
|
51
|
+
# schedule_start and schedule_end are numbers, actually
|
|
52
|
+
# so let's convert them to datetime.time objects
|
|
53
|
+
schedule_start = hours_to_time(schedule_start)
|
|
54
|
+
schedule_end = hours_to_time(schedule_end)
|
|
55
|
+
schedule_blocked_hours = 0
|
|
56
|
+
for calendar in calendars:
|
|
57
|
+
for event in calendar:
|
|
58
|
+
# we use str to make object serializable as jsons
|
|
59
|
+
if isinstance(event["start"], str):
|
|
60
|
+
event["start"] = datetime.fromisoformat(event["start"])
|
|
61
|
+
if isinstance(event["end"], str):
|
|
62
|
+
event["end"] = datetime.fromisoformat(event["end"])
|
|
63
|
+
|
|
64
|
+
if event["start"].date() > date:
|
|
65
|
+
break
|
|
66
|
+
elif event["end"].date() < date:
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
# check if the event overlaps with one of the working hours
|
|
70
|
+
event_start = event["start"].time()
|
|
71
|
+
event_end = event["end"].time()
|
|
72
|
+
if event["start"].date() < date:
|
|
73
|
+
event_start = datetime(date.year, date.month, date.day, 0, 0).time()
|
|
74
|
+
if event["end"].date() > date:
|
|
75
|
+
event_end = datetime(date.year, date.month, date.day, 23, 59).time()
|
|
76
|
+
|
|
77
|
+
if event_start < schedule_end and event_end > schedule_start:
|
|
78
|
+
schedule_blocked_hours += time_to_decimal(
|
|
79
|
+
min(schedule_end, event_end)
|
|
80
|
+
) - time_to_decimal(max(schedule_start, event_start))
|
|
81
|
+
if verbose and schedule_blocked_hours > 0:
|
|
82
|
+
print(
|
|
83
|
+
f"Blocked hours on {date} between {schedule_start} and {schedule_end}: {schedule_blocked_hours}"
|
|
84
|
+
)
|
|
85
|
+
blocked_hours += schedule_blocked_hours
|
|
86
|
+
available_hours -= blocked_hours
|
|
87
|
+
return available_hours
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def pdth_to_hours(duration_str):
|
|
91
|
+
# string format is P#DT#H
|
|
92
|
+
# with D and H optional
|
|
93
|
+
duration_str = duration_str[1:] # Remove leading "P"
|
|
94
|
+
days, hours = 0, 0
|
|
95
|
+
if "D" in duration_str:
|
|
96
|
+
days, duration_str = duration_str.split("D")
|
|
97
|
+
days = int(days)
|
|
98
|
+
if "H" in duration_str:
|
|
99
|
+
hours = int(duration_str.split("T")[1].split("H")[0])
|
|
100
|
+
return days * 24 + hours
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def hours_to_pdth(hours):
|
|
104
|
+
days = hours // 24
|
|
105
|
+
hours = hours % 24
|
|
106
|
+
return f"P{days}DT{hours}H"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def get_long_range_time_map(
|
|
110
|
+
time_maps, time_map_names, days_ahead, calendars, verbose=False
|
|
111
|
+
):
|
|
112
|
+
key = ",".join(time_map_names)
|
|
113
|
+
if key in long_range_time_map:
|
|
114
|
+
task_time_map = long_range_time_map[key]
|
|
115
|
+
else:
|
|
116
|
+
if verbose:
|
|
117
|
+
print(f"Calculating long range time map for {key}")
|
|
118
|
+
task_time_map = []
|
|
119
|
+
for d in range(days_ahead):
|
|
120
|
+
date = datetime.today().date() + timedelta(days=d)
|
|
121
|
+
daily_hours = 0
|
|
122
|
+
for time_map_name in time_map_names:
|
|
123
|
+
if time_map_name not in time_maps:
|
|
124
|
+
raise ValueError(f"Time map '{time_map_name}' does not exist.")
|
|
125
|
+
time_map = time_maps[time_map_name]
|
|
126
|
+
daily_hours += get_available_hours(time_map, date, calendars)
|
|
127
|
+
task_time_map.append(daily_hours)
|
|
128
|
+
long_range_time_map[key] = task_time_map
|
|
129
|
+
|
|
130
|
+
today_time = time_to_decimal(datetime.now().time())
|
|
131
|
+
today_weekday = datetime.today().strftime("%A").lower()
|
|
132
|
+
today_used_hours = 0
|
|
133
|
+
# compare with the time_maps of today
|
|
134
|
+
for time_map_name in time_map_names:
|
|
135
|
+
time_map = time_maps[time_map_name].get(today_weekday)
|
|
136
|
+
if time_map:
|
|
137
|
+
for schedule_start, schedule_end in time_map:
|
|
138
|
+
if schedule_start <= today_time <= schedule_end:
|
|
139
|
+
today_used_hours += today_time - schedule_start
|
|
140
|
+
break
|
|
141
|
+
elif today_time > schedule_end:
|
|
142
|
+
today_used_hours += schedule_end - schedule_start
|
|
143
|
+
return task_time_map, today_used_hours
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def mark_end_date(
|
|
147
|
+
due_date, end_date, start_date, scheduling_note, id, description=None
|
|
148
|
+
):
|
|
149
|
+
start_end_fields = [f"scheduled:{start_date}", f"completion_date:{end_date}"]
|
|
150
|
+
|
|
151
|
+
subprocess.run(
|
|
152
|
+
[
|
|
153
|
+
"task",
|
|
154
|
+
str(id),
|
|
155
|
+
"modify",
|
|
156
|
+
*start_end_fields,
|
|
157
|
+
f'scheduling:"{scheduling_note}"',
|
|
158
|
+
],
|
|
159
|
+
stdout=subprocess.DEVNULL,
|
|
160
|
+
stderr=subprocess.DEVNULL,
|
|
161
|
+
)
|
|
162
|
+
if due_date is not None and end_date > due_date:
|
|
163
|
+
# print in bold red using ANSI escape codes
|
|
164
|
+
description = "('" + description + "')" if description is not None else ""
|
|
165
|
+
print(f"\033[1;31mTask {id} {description} may not be completed on time\033[0m")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def get_calendars(config, verbose=False):
|
|
169
|
+
calendars = []
|
|
170
|
+
for calname in config["calendars"]:
|
|
171
|
+
calendar = config["calendars"][calname]
|
|
172
|
+
calendar = ical_to_dict(
|
|
173
|
+
calendar["url"],
|
|
174
|
+
config["scheduler"]["days_ahead"],
|
|
175
|
+
all_day=calendar["event_all_day_is_blocking"],
|
|
176
|
+
expiration=calendar["expiration"],
|
|
177
|
+
verbose=verbose,
|
|
178
|
+
tz_name=calendar.get("timezone"),
|
|
179
|
+
)
|
|
180
|
+
calendar.sort(key=lambda e: e["start"])
|
|
181
|
+
calendars.append(calendar)
|
|
182
|
+
if verbose:
|
|
183
|
+
print(f"Loaded {len(calendars)} calendars")
|
|
184
|
+
return calendars
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from taskcheck.common import config_dir
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
default_config = """
|
|
6
|
+
[time_maps]
|
|
7
|
+
[time_maps.work]
|
|
8
|
+
monday = [[9, 12.30], [14, 17]]
|
|
9
|
+
tuesday = [[9, 12.30], [14, 17]]
|
|
10
|
+
wednesday = [[9, 12.30], [14, 17]]
|
|
11
|
+
thursday = [[9, 12.30], [14, 17]]
|
|
12
|
+
friday = [[9, 12.30], [14, 17]]
|
|
13
|
+
|
|
14
|
+
[scheduler]
|
|
15
|
+
algorithm = "parallel"
|
|
16
|
+
days_ahead = 365
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def apply_config(key, value):
|
|
21
|
+
subprocess.run(["task", "config", key, value], stdout=subprocess.DEVNULL)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def install():
|
|
25
|
+
"""
|
|
26
|
+
Adds the required and optional settings explaining them to the user.
|
|
27
|
+
Also initialize the config file with some sane default.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
# Define the required configurations
|
|
31
|
+
required_configs = [
|
|
32
|
+
("uda.time_map.type", "string"),
|
|
33
|
+
("uda.time_map.label", "Time Map"),
|
|
34
|
+
("uda.time_map.default", "work"),
|
|
35
|
+
("uda.estimated.type", "duration"),
|
|
36
|
+
("uda.estimated.label", "Estimated Time"),
|
|
37
|
+
("uda.completion_date.type", "date"),
|
|
38
|
+
("uda.completion_date.label", "Expected Completion Date"),
|
|
39
|
+
("uda.scheduling.type", "string"),
|
|
40
|
+
("uda.scheduling.label", "Scheduling"),
|
|
41
|
+
("uda.min_block.type", "numeric"),
|
|
42
|
+
("uda.min_block.label", "Minimum Time Block"),
|
|
43
|
+
("uda.min_block.default", "2"),
|
|
44
|
+
("recurrence.confirmation", "no"),
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
# Optional configurations
|
|
48
|
+
optional_configs = [
|
|
49
|
+
("urgency.uda.estimated.PT1H.coefficient", "1"),
|
|
50
|
+
("urgency.uda.estimated.PT2H.coefficient", "2.32"),
|
|
51
|
+
("urgency.uda.estimated.PT3H.coefficient", "3.67"),
|
|
52
|
+
("urgency.uda.estimated.PT4H.coefficient", "4.64"),
|
|
53
|
+
("urgency.uda.estimated.PT5H.coefficient", "5.39"),
|
|
54
|
+
("urgency.uda.estimated.PT6H.coefficient", "6.02"),
|
|
55
|
+
("urgency.uda.estimated.PT7H.coefficient", "6.56"),
|
|
56
|
+
("urgency.uda.estimated.PT8H.coefficient", "7.03"),
|
|
57
|
+
("urgency.uda.estimated.PT9H.coefficient", "7.45"),
|
|
58
|
+
("urgency.uda.estimated.PT10H.coefficient", "7.82"),
|
|
59
|
+
("urgency.uda.estimated.PT11H.coefficient", "8.16"),
|
|
60
|
+
("urgency.uda.estimated.PT12H.coefficient", "8.47"),
|
|
61
|
+
("urgency.uda.estimated.PT13H.coefficient", "8.75"),
|
|
62
|
+
("urgency.uda.estimated.PT14H.coefficient", "9.01"),
|
|
63
|
+
("urgency.uda.estimated.PT15H.coefficient", "9.25"),
|
|
64
|
+
("urgency.uda.estimated.PT16H.coefficient", "9.47"),
|
|
65
|
+
("urgency.uda.estimated.PT17H.coefficient", "9.68"),
|
|
66
|
+
("urgency.uda.estimated.PT18H.coefficient", "9.87"),
|
|
67
|
+
("urgency.uda.estimated.PT19H.coefficient", "10.05"),
|
|
68
|
+
("urgency.uda.estimated.PT20H.coefficient", "10.22"),
|
|
69
|
+
("urgency.uda.estimated.PT21H.coefficient", "10.38"),
|
|
70
|
+
("urgency.uda.estimated.PT22H.coefficient", "10.53"),
|
|
71
|
+
("urgency.uda.estimated.PT23H.coefficient", "10.67"),
|
|
72
|
+
("urgency.uda.estimated.PT24H.coefficient", "10.80"),
|
|
73
|
+
("urgency.uda.estimated.PT25H.coefficient", "10.93"),
|
|
74
|
+
("urgency.uda.estimated.PT26H.coefficient", "11.05"),
|
|
75
|
+
("urgency.uda.estimated.PT27H.coefficient", "11.16"),
|
|
76
|
+
("urgency.uda.estimated.PT28H.coefficient", "11.27"),
|
|
77
|
+
("urgency.uda.estimated.PT29H.coefficient", "11.37"),
|
|
78
|
+
("urgency.uda.estimated.PT30H.coefficient", "11.47"),
|
|
79
|
+
("urgency.uda.estimated.PT31H.coefficient", "11.56"),
|
|
80
|
+
("urgency.uda.estimated.PT32H.coefficient", "11.65"),
|
|
81
|
+
("urgency.uda.estimated.PT33H.coefficient", "11.73"),
|
|
82
|
+
("urgency.uda.estimated.PT34H.coefficient", "11.81"),
|
|
83
|
+
("urgency.uda.estimated.PT35H.coefficient", "11.89"),
|
|
84
|
+
("urgency.uda.estimated.PT36H.coefficient", "11.96"),
|
|
85
|
+
(
|
|
86
|
+
"report.ready.columns",
|
|
87
|
+
"id,start.age,entry.age,depends.indicator,priority,project,tags,recur.indicator,scheduled.relative,due.relative,until.remaining,description,urgency",
|
|
88
|
+
),
|
|
89
|
+
("journal.info", "0"),
|
|
90
|
+
("urgency.inherit", "1"),
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
answer = input(
|
|
94
|
+
"""Required configuration for taskcheck includes:
|
|
95
|
+
- 3 UDA fields
|
|
96
|
+
- turning off confirmation for recurring tasks
|
|
97
|
+
|
|
98
|
+
Do you want to continue? [y/N]"""
|
|
99
|
+
)
|
|
100
|
+
if answer.lower() not in ["y", "yes"]:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
answer = input(
|
|
104
|
+
"""Optional configuration for taskcheck includes:
|
|
105
|
+
- urgency coefficients for estimated time (up to 36 hour duration)
|
|
106
|
+
- better `ready` report columns
|
|
107
|
+
- turning off printing journal logs in the task details
|
|
108
|
+
- turning on urgency inheritance from dependant tasks
|
|
109
|
+
|
|
110
|
+
Do you want to apply optional configurations? [y/N]"""
|
|
111
|
+
)
|
|
112
|
+
if answer.lower() not in ["y", "yes"]:
|
|
113
|
+
configs = required_configs
|
|
114
|
+
else:
|
|
115
|
+
configs = required_configs + optional_configs
|
|
116
|
+
|
|
117
|
+
# Apply configurations using apply_config
|
|
118
|
+
try:
|
|
119
|
+
for key, value in configs:
|
|
120
|
+
apply_config(key, value)
|
|
121
|
+
print("Taskwarrior configurations have been applied successfully.")
|
|
122
|
+
except Exception as e:
|
|
123
|
+
print(f"Failed to apply configurations: {e}")
|
|
124
|
+
|
|
125
|
+
answer = input(
|
|
126
|
+
"""Do you want to create a default config file for taskcheck? [y/N]"""
|
|
127
|
+
)
|
|
128
|
+
if answer.lower() not in ["y", "yes"]:
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
132
|
+
config_file = config_dir / "taskcheck.toml"
|
|
133
|
+
if config_file.exists():
|
|
134
|
+
print(f"Config file already exists at {config_file}")
|
|
135
|
+
return
|
|
136
|
+
with open(config_file, "w") as f:
|
|
137
|
+
f.write(default_config)
|
|
138
|
+
|
|
139
|
+
print(f"Default config file created at {config_file}")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
if __name__ == "__main__":
|
|
143
|
+
install()
|