taskcheck 0.2.0__tar.gz → 0.3.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/taskcheck.egg-info → taskcheck-0.3.0}/PKG-INFO +36 -12
- {taskcheck-0.2.0 → taskcheck-0.3.0}/README.md +34 -11
- {taskcheck-0.2.0 → taskcheck-0.3.0}/pyproject.toml +7 -2
- {taskcheck-0.2.0 → taskcheck-0.3.0}/taskcheck/__main__.py +15 -2
- {taskcheck-0.2.0 → taskcheck-0.3.0}/taskcheck/common.py +22 -5
- {taskcheck-0.2.0 → taskcheck-0.3.0}/taskcheck/install.py +17 -6
- {taskcheck-0.2.0 → taskcheck-0.3.0}/taskcheck/parallel.py +53 -30
- taskcheck-0.3.0/taskcheck/report.py +122 -0
- {taskcheck-0.2.0 → taskcheck-0.3.0}/taskcheck/sequential.py +3 -2
- {taskcheck-0.2.0 → taskcheck-0.3.0/taskcheck.egg-info}/PKG-INFO +36 -12
- {taskcheck-0.2.0 → taskcheck-0.3.0}/taskcheck.egg-info/SOURCES.txt +1 -0
- {taskcheck-0.2.0 → taskcheck-0.3.0}/taskcheck.egg-info/requires.txt +1 -0
- {taskcheck-0.2.0 → taskcheck-0.3.0}/LICENSE +0 -0
- {taskcheck-0.2.0 → taskcheck-0.3.0}/setup.cfg +0 -0
- {taskcheck-0.2.0 → taskcheck-0.3.0}/taskcheck/ical.py +0 -0
- {taskcheck-0.2.0 → taskcheck-0.3.0}/taskcheck.egg-info/dependency_links.txt +0 -0
- {taskcheck-0.2.0 → taskcheck-0.3.0}/taskcheck.egg-info/entry_points.txt +0 -0
- {taskcheck-0.2.0 → taskcheck-0.3.0}/taskcheck.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: taskcheck
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Automatic scheduler for taskwarrior
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -8,6 +8,7 @@ License-File: LICENSE
|
|
|
8
8
|
Requires-Dist: appdirs>=1.4.4
|
|
9
9
|
Requires-Dist: icalendar>=6.0.1
|
|
10
10
|
Requires-Dist: requests>=2.32.3
|
|
11
|
+
Requires-Dist: rich>=13.9.4
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
> _A non-AI automatic scheduler for taskwarrior (i.e. alternative to skedpal/timehero/flowsavvy/reclaim/trevor/motion)_
|
|
@@ -19,7 +20,8 @@ This is a taskwarrior extension checks if tasks can be completed on time, consid
|
|
|
19
20
|
- [x] Use arbitrarily complex time maps
|
|
20
21
|
- [x] Use ical to block time from scheduling (e.g. for meetings, vacations, etc.)
|
|
21
22
|
- [x] Implement scheduling algorithm for parallely working on multiple tasks
|
|
22
|
-
- [ ] Use Google
|
|
23
|
+
- [ ] Use Google API to access calendars
|
|
24
|
+
- [ ] Export tasks to iCal calendar and API calendars
|
|
23
25
|
|
|
24
26
|
## Install
|
|
25
27
|
|
|
@@ -31,29 +33,45 @@ This is a taskwarrior extension checks if tasks can be completed on time, consid
|
|
|
31
33
|
This extension parses your pending and waiting tasks sorted decreasingly by urgency and tries to schedule them in the future.
|
|
32
34
|
It considers their estimated time to schedule all tasks starting from the most urgent one.
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
#### UDAs
|
|
37
|
+
|
|
38
|
+
Taskcheck leverages to UDAs, `estimated` and `time_map`. The `estimated` attribute is
|
|
35
39
|
the expected time to complete the task in hours. The `time_map` is a comma-separated list of strings
|
|
36
40
|
that indicates the hours per day in which you will work on a task (e.g. `work`, `weekend`, etc.).
|
|
37
41
|
The exact correspondence between the `time_map` and the hours of the day is defined in the configuration
|
|
38
|
-
file of taskcheck.
|
|
42
|
+
file of taskcheck. For instance:
|
|
43
|
+
|
|
44
|
+
```toml
|
|
45
|
+
[time_maps]
|
|
46
|
+
# get an error)
|
|
47
|
+
[time_maps.work]
|
|
48
|
+
monday = [[9, 12.30], [14, 17]]
|
|
49
|
+
tuesday = [[9, 12.30], [14, 17]]
|
|
50
|
+
# ...
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
#### They say it's an "AI"
|
|
39
54
|
|
|
40
|
-
|
|
55
|
+
Taskcheck will also parse online iCal calendars (Google, Apple, etc.) and will match them with your time maps.
|
|
56
|
+
It will then modify the Taskwarrior tasks by adding the `completion_date` attribute with the expected
|
|
41
57
|
date of completion and the `scheduled` attribute with the date in which the task is expected to
|
|
42
58
|
start.
|
|
43
59
|
|
|
44
60
|
It will also print a red line for every task whose `completion_date` is after its `due_date`.
|
|
45
61
|
|
|
46
|
-
|
|
62
|
+
In general, it is recommended to run taskcheck rather frequently and at least once at the beginning
|
|
63
|
+
of your working day.
|
|
47
64
|
|
|
48
|
-
|
|
65
|
+
#### Reports
|
|
49
66
|
|
|
50
|
-
You can
|
|
51
|
-
|
|
67
|
+
You can also print simple reports that exploit the `scheduling` UDA filled by Taskcheck to grasp
|
|
68
|
+
how much time you have to work on which task in which day. For
|
|
69
|
+
instance:
|
|
52
70
|
|
|
53
|
-
|
|
54
|
-
|
|
71
|
+
- `taskcheck -r today` will show the tasks planned for today
|
|
72
|
+
- `taskcheck -r 1w` will show the tasks planned for the next week,
|
|
55
73
|
|
|
56
|
-
|
|
74
|
+
## Algorithms
|
|
57
75
|
|
|
58
76
|
Taskcheck provides two scheduling algorithms: `parallel` and `sequential`.
|
|
59
77
|
|
|
@@ -125,9 +143,15 @@ event_all_day_is_blocking = true
|
|
|
125
143
|
expiration = 720 # in hours (720 hours = 30 days)
|
|
126
144
|
```
|
|
127
145
|
|
|
146
|
+
## Tips and Tricks
|
|
147
|
+
|
|
148
|
+
- You can exclude a task from being scheduled by removing the `time_map` or `estimated` attributes.
|
|
149
|
+
- You can see tasks that you can execute now with the `task ready` report.
|
|
150
|
+
|
|
128
151
|
## CLI Options
|
|
129
152
|
|
|
130
153
|
```
|
|
131
154
|
-v, --verbose: increase output verbosity
|
|
132
155
|
-i, --install: install taskcheck configuration
|
|
156
|
+
-r, --report: show tasks planned until a certain time
|
|
133
157
|
```
|
|
@@ -8,7 +8,8 @@ This is a taskwarrior extension checks if tasks can be completed on time, consid
|
|
|
8
8
|
- [x] Use arbitrarily complex time maps
|
|
9
9
|
- [x] Use ical to block time from scheduling (e.g. for meetings, vacations, etc.)
|
|
10
10
|
- [x] Implement scheduling algorithm for parallely working on multiple tasks
|
|
11
|
-
- [ ] Use Google
|
|
11
|
+
- [ ] Use Google API to access calendars
|
|
12
|
+
- [ ] Export tasks to iCal calendar and API calendars
|
|
12
13
|
|
|
13
14
|
## Install
|
|
14
15
|
|
|
@@ -20,29 +21,45 @@ This is a taskwarrior extension checks if tasks can be completed on time, consid
|
|
|
20
21
|
This extension parses your pending and waiting tasks sorted decreasingly by urgency and tries to schedule them in the future.
|
|
21
22
|
It considers their estimated time to schedule all tasks starting from the most urgent one.
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
#### UDAs
|
|
25
|
+
|
|
26
|
+
Taskcheck leverages to UDAs, `estimated` and `time_map`. The `estimated` attribute is
|
|
24
27
|
the expected time to complete the task in hours. The `time_map` is a comma-separated list of strings
|
|
25
28
|
that indicates the hours per day in which you will work on a task (e.g. `work`, `weekend`, etc.).
|
|
26
29
|
The exact correspondence between the `time_map` and the hours of the day is defined in the configuration
|
|
27
|
-
file of taskcheck.
|
|
30
|
+
file of taskcheck. For instance:
|
|
31
|
+
|
|
32
|
+
```toml
|
|
33
|
+
[time_maps]
|
|
34
|
+
# get an error)
|
|
35
|
+
[time_maps.work]
|
|
36
|
+
monday = [[9, 12.30], [14, 17]]
|
|
37
|
+
tuesday = [[9, 12.30], [14, 17]]
|
|
38
|
+
# ...
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
#### They say it's an "AI"
|
|
28
42
|
|
|
29
|
-
|
|
43
|
+
Taskcheck will also parse online iCal calendars (Google, Apple, etc.) and will match them with your time maps.
|
|
44
|
+
It will then modify the Taskwarrior tasks by adding the `completion_date` attribute with the expected
|
|
30
45
|
date of completion and the `scheduled` attribute with the date in which the task is expected to
|
|
31
46
|
start.
|
|
32
47
|
|
|
33
48
|
It will also print a red line for every task whose `completion_date` is after its `due_date`.
|
|
34
49
|
|
|
35
|
-
|
|
50
|
+
In general, it is recommended to run taskcheck rather frequently and at least once at the beginning
|
|
51
|
+
of your working day.
|
|
36
52
|
|
|
37
|
-
|
|
53
|
+
#### Reports
|
|
38
54
|
|
|
39
|
-
You can
|
|
40
|
-
|
|
55
|
+
You can also print simple reports that exploit the `scheduling` UDA filled by Taskcheck to grasp
|
|
56
|
+
how much time you have to work on which task in which day. For
|
|
57
|
+
instance:
|
|
41
58
|
|
|
42
|
-
|
|
43
|
-
|
|
59
|
+
- `taskcheck -r today` will show the tasks planned for today
|
|
60
|
+
- `taskcheck -r 1w` will show the tasks planned for the next week,
|
|
44
61
|
|
|
45
|
-
|
|
62
|
+
## Algorithms
|
|
46
63
|
|
|
47
64
|
Taskcheck provides two scheduling algorithms: `parallel` and `sequential`.
|
|
48
65
|
|
|
@@ -114,9 +131,15 @@ event_all_day_is_blocking = true
|
|
|
114
131
|
expiration = 720 # in hours (720 hours = 30 days)
|
|
115
132
|
```
|
|
116
133
|
|
|
134
|
+
## Tips and Tricks
|
|
135
|
+
|
|
136
|
+
- You can exclude a task from being scheduled by removing the `time_map` or `estimated` attributes.
|
|
137
|
+
- You can see tasks that you can execute now with the `task ready` report.
|
|
138
|
+
|
|
117
139
|
## CLI Options
|
|
118
140
|
|
|
119
141
|
```
|
|
120
142
|
-v, --verbose: increase output verbosity
|
|
121
143
|
-i, --install: install taskcheck configuration
|
|
144
|
+
-r, --report: show tasks planned until a certain time
|
|
122
145
|
```
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "taskcheck"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Automatic scheduler for taskwarrior"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
7
|
-
dependencies = [
|
|
7
|
+
dependencies = [
|
|
8
|
+
"appdirs>=1.4.4",
|
|
9
|
+
"icalendar>=6.0.1",
|
|
10
|
+
"requests>=2.32.3",
|
|
11
|
+
"rich>=13.9.4",
|
|
12
|
+
]
|
|
8
13
|
|
|
9
14
|
[project.scripts]
|
|
10
15
|
taskcheck = "taskcheck.__main__:main"
|
|
@@ -7,13 +7,19 @@ from taskcheck.common import config_dir
|
|
|
7
7
|
|
|
8
8
|
arg_parser = argparse.ArgumentParser()
|
|
9
9
|
arg_parser.add_argument(
|
|
10
|
-
"-v", "--verbose", action="store_true", help="increase output verbosity"
|
|
10
|
+
"-v", "--verbose", action="store_true", help="increase output verbosity."
|
|
11
11
|
)
|
|
12
12
|
arg_parser.add_argument(
|
|
13
13
|
"-i",
|
|
14
14
|
"--install",
|
|
15
15
|
action="store_true",
|
|
16
|
-
help="install the UDAs, required settings, and default config file",
|
|
16
|
+
help="install the UDAs, required settings, and default config file.",
|
|
17
|
+
)
|
|
18
|
+
arg_parser.add_argument(
|
|
19
|
+
"-r",
|
|
20
|
+
"--report",
|
|
21
|
+
action="store",
|
|
22
|
+
help="generate a report of the tasks based on the scheduling; can be any Taskwarrior datetime specification (e.g. today, tomorrow, eom, som, 1st, 2nd, etc.). It is considered as `by`, meaning that the report will be generated for all the days until the specified date and including it.",
|
|
17
23
|
)
|
|
18
24
|
|
|
19
25
|
args = arg_parser.parse_args()
|
|
@@ -34,6 +40,13 @@ def main():
|
|
|
34
40
|
install()
|
|
35
41
|
return
|
|
36
42
|
|
|
43
|
+
if args.report:
|
|
44
|
+
from taskcheck.report import generate_report
|
|
45
|
+
|
|
46
|
+
config = load_config()
|
|
47
|
+
generate_report(config, args.report, args.verbose)
|
|
48
|
+
return
|
|
49
|
+
|
|
37
50
|
config = load_config()
|
|
38
51
|
algo = config["scheduler"].get("algorithm", "parallel")
|
|
39
52
|
if algo == "parallel":
|
|
@@ -91,19 +91,36 @@ def pdth_to_hours(duration_str):
|
|
|
91
91
|
# string format is P#DT#H
|
|
92
92
|
# with D and H optional
|
|
93
93
|
duration_str = duration_str[1:] # Remove leading "P"
|
|
94
|
-
days, hours = 0, 0
|
|
94
|
+
days, hours, minutes = 0, 0, 0
|
|
95
95
|
if "D" in duration_str:
|
|
96
96
|
days, duration_str = duration_str.split("D")
|
|
97
97
|
days = int(days)
|
|
98
98
|
if "H" in duration_str:
|
|
99
99
|
hours = int(duration_str.split("T")[1].split("H")[0])
|
|
100
|
-
|
|
100
|
+
if "M" in duration_str:
|
|
101
|
+
minutes = int(duration_str.split("H")[1].split("M")[0])
|
|
102
|
+
return days * 24 + hours + minutes / 60
|
|
101
103
|
|
|
102
104
|
|
|
103
105
|
def hours_to_pdth(hours):
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
days_ = hours / 24
|
|
107
|
+
hours_ = hours % 24
|
|
108
|
+
minutes_ = int(round((hours_ - int(hours_)) * 60))
|
|
109
|
+
days_ = int(days_)
|
|
110
|
+
hours_ = int(hours_)
|
|
111
|
+
minutes_ = int(minutes_)
|
|
112
|
+
ret = "P"
|
|
113
|
+
if days_ == 0:
|
|
114
|
+
ret += "T"
|
|
115
|
+
else:
|
|
116
|
+
ret += f"{days_}DT"
|
|
117
|
+
if hours_ > 0:
|
|
118
|
+
ret += f"{hours_}H"
|
|
119
|
+
if minutes_ > 0:
|
|
120
|
+
ret += f"{minutes_}M"
|
|
121
|
+
if ret == "PT":
|
|
122
|
+
print("Warning: hours_to_pdth() returned 'PT' for hours = ", hours)
|
|
123
|
+
return ret
|
|
107
124
|
|
|
108
125
|
|
|
109
126
|
def get_long_range_time_map(
|
|
@@ -88,26 +88,35 @@ def install():
|
|
|
88
88
|
),
|
|
89
89
|
("journal.info", "0"),
|
|
90
90
|
("urgency.inherit", "1"),
|
|
91
|
+
("urgency.blocked.coefficient", "0"),
|
|
92
|
+
("urgency.blocking.coefficient", "0"),
|
|
93
|
+
("urgency.waiting.coefficient", "0"),
|
|
94
|
+
("urgency.scheduled.coefficient", "0"),
|
|
91
95
|
]
|
|
92
96
|
|
|
93
97
|
answer = input(
|
|
94
|
-
"""
|
|
98
|
+
"""
|
|
99
|
+
_________________________________________________________________________________________
|
|
100
|
+
Required configuration for taskcheck includes:
|
|
95
101
|
- 3 UDA fields
|
|
96
102
|
- turning off confirmation for recurring tasks
|
|
97
103
|
|
|
98
|
-
Do you want to continue? [y/N]"""
|
|
104
|
+
Do you want to continue? [y/N] """
|
|
99
105
|
)
|
|
100
106
|
if answer.lower() not in ["y", "yes"]:
|
|
101
107
|
return
|
|
102
108
|
|
|
103
109
|
answer = input(
|
|
104
|
-
"""
|
|
110
|
+
"""
|
|
111
|
+
_________________________________________________________________________________________
|
|
112
|
+
Optional configuration for taskcheck includes:
|
|
105
113
|
- urgency coefficients for estimated time (up to 36 hour duration)
|
|
106
114
|
- better `ready` report columns
|
|
107
115
|
- turning off printing journal logs in the task details
|
|
108
|
-
- turning on urgency inheritance from dependant tasks
|
|
116
|
+
- turning on urgency inheritance from dependant tasks and disabling blocked and blocking urgency coefficients (as recommended by Taskwarrior documentation)
|
|
117
|
+
- turning off urgency for waiting and scheduled tasks (taskcheck already take them into account)
|
|
109
118
|
|
|
110
|
-
Do you want to apply optional configurations? [y/N]"""
|
|
119
|
+
Do you want to apply optional configurations? [y/N] """
|
|
111
120
|
)
|
|
112
121
|
if answer.lower() not in ["y", "yes"]:
|
|
113
122
|
configs = required_configs
|
|
@@ -123,7 +132,9 @@ Do you want to apply optional configurations? [y/N]"""
|
|
|
123
132
|
print(f"Failed to apply configurations: {e}")
|
|
124
133
|
|
|
125
134
|
answer = input(
|
|
126
|
-
"""
|
|
135
|
+
"""
|
|
136
|
+
_________________________________________________________________________________________
|
|
137
|
+
Do you want to create a default config file for taskcheck? [y/N] """
|
|
127
138
|
)
|
|
128
139
|
if answer.lower() not in ["y", "yes"]:
|
|
129
140
|
return
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import subprocess
|
|
2
2
|
import re
|
|
3
|
+
from dataclasses import dataclass
|
|
3
4
|
|
|
4
5
|
from datetime import datetime, timedelta
|
|
5
6
|
from taskcheck.common import (
|
|
@@ -13,6 +14,13 @@ from taskcheck.common import (
|
|
|
13
14
|
)
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
@dataclass
|
|
18
|
+
class UrgencyCoefficients:
|
|
19
|
+
estimated: dict
|
|
20
|
+
inherit: bool
|
|
21
|
+
active: float
|
|
22
|
+
|
|
23
|
+
|
|
16
24
|
def get_urgency_coefficients():
|
|
17
25
|
"""
|
|
18
26
|
Retrieves urgency coefficients from Taskwarrior configurations.
|
|
@@ -21,34 +29,40 @@ def get_urgency_coefficients():
|
|
|
21
29
|
"""
|
|
22
30
|
result = subprocess.run(["task", "_show"], capture_output=True, text=True)
|
|
23
31
|
inherit_urgency = False
|
|
32
|
+
active_task_coefficient = 0
|
|
24
33
|
coefficients = {}
|
|
25
34
|
pattern1 = re.compile(r"^urgency\.uda\.estimated\.(.+)\.coefficient=(.+)$")
|
|
26
35
|
pattern2 = re.compile(r"^urgency\.inherit=(.+)$")
|
|
36
|
+
pattern3 = re.compile(r"^urgency\.active.coefficient=(.+)$")
|
|
27
37
|
for line in result.stdout.splitlines():
|
|
28
38
|
match = pattern1.match(line)
|
|
29
39
|
if match:
|
|
30
40
|
estimated_value = match.group(1)
|
|
31
41
|
coefficient = float(match.group(2))
|
|
32
42
|
coefficients[estimated_value] = coefficient
|
|
43
|
+
|
|
33
44
|
match = pattern2.match(line)
|
|
34
45
|
if match:
|
|
35
46
|
inherit_urgency = match.group(1) == "1"
|
|
36
47
|
|
|
37
|
-
|
|
48
|
+
match = pattern3.match(line)
|
|
49
|
+
if match:
|
|
50
|
+
active_coefficient = float(match.group(1))
|
|
51
|
+
active_task_coefficient = active_coefficient
|
|
52
|
+
|
|
53
|
+
return UrgencyCoefficients(coefficients, inherit_urgency, active_task_coefficient)
|
|
38
54
|
|
|
39
55
|
|
|
40
|
-
def compute_estimated_urgency(remaining_hours,
|
|
56
|
+
def compute_estimated_urgency(remaining_hours, urgency_coefficients):
|
|
41
57
|
"""
|
|
42
58
|
Computes the estimated urgency for the given remaining hours using the coefficients.
|
|
43
59
|
"""
|
|
44
|
-
# Convert remaining hours to PDTH format used in the coefficients
|
|
45
|
-
pdth_value = hours_to_pdth(remaining_hours)
|
|
46
60
|
# Find the closest match (e.g., if '2h' is not available, use '1h' or '3h')
|
|
47
61
|
closest_match = min(
|
|
48
|
-
|
|
49
|
-
key=lambda x: abs(
|
|
62
|
+
urgency_coefficients.estimated.keys(),
|
|
63
|
+
key=lambda x: abs(pdth_to_hours(x) - remaining_hours),
|
|
50
64
|
)
|
|
51
|
-
coefficient =
|
|
65
|
+
coefficient = urgency_coefficients.estimated[closest_match]
|
|
52
66
|
# Compute the urgency
|
|
53
67
|
estimated_urgency = coefficient * remaining_hours
|
|
54
68
|
return estimated_urgency
|
|
@@ -60,7 +74,7 @@ def check_tasks_parallel(config, verbose=False):
|
|
|
60
74
|
days_ahead = config["scheduler"]["days_ahead"]
|
|
61
75
|
calendars = get_calendars(config)
|
|
62
76
|
today = datetime.today().date()
|
|
63
|
-
urgency_coefficients
|
|
77
|
+
urgency_coefficients = get_urgency_coefficients()
|
|
64
78
|
|
|
65
79
|
task_info = initialize_task_info(
|
|
66
80
|
tasks, time_maps, days_ahead, urgency_coefficients, calendars
|
|
@@ -69,7 +83,7 @@ def check_tasks_parallel(config, verbose=False):
|
|
|
69
83
|
for day_offset in range(days_ahead):
|
|
70
84
|
date = today + timedelta(days=day_offset)
|
|
71
85
|
allocate_time_for_day(
|
|
72
|
-
task_info, day_offset, date, urgency_coefficients,
|
|
86
|
+
task_info, day_offset, date, urgency_coefficients, verbose
|
|
73
87
|
)
|
|
74
88
|
|
|
75
89
|
update_tasks_with_scheduling_info(task_info, verbose)
|
|
@@ -100,13 +114,12 @@ def initialize_task_info(tasks, time_maps, days_ahead, urgency_coefficients, cal
|
|
|
100
114
|
"scheduling": {},
|
|
101
115
|
"urgency": initial_urgency,
|
|
102
116
|
"estimated_urgency": estimated_urgency,
|
|
117
|
+
"started": False,
|
|
103
118
|
}
|
|
104
119
|
return task_info
|
|
105
120
|
|
|
106
121
|
|
|
107
|
-
def allocate_time_for_day(
|
|
108
|
-
task_info, day_offset, date, urgency_coefficients, inherit_urgency, verbose
|
|
109
|
-
):
|
|
122
|
+
def allocate_time_for_day(task_info, day_offset, date, urgency_coefficients, verbose):
|
|
110
123
|
total_available_hours = compute_total_available_hours(task_info, day_offset)
|
|
111
124
|
if verbose:
|
|
112
125
|
print(f"Day {date}, total available hours: {total_available_hours:.2f}")
|
|
@@ -117,7 +130,7 @@ def allocate_time_for_day(
|
|
|
117
130
|
tasks_remaining = prepare_tasks_remaining(task_info, day_offset)
|
|
118
131
|
|
|
119
132
|
while day_remaining_hours > 0 and tasks_remaining:
|
|
120
|
-
recompute_urgencies(tasks_remaining, urgency_coefficients
|
|
133
|
+
recompute_urgencies(tasks_remaining, urgency_coefficients)
|
|
121
134
|
sorted_task_ids = sorted(
|
|
122
135
|
tasks_remaining.keys(),
|
|
123
136
|
key=lambda x: (tasks_remaining[x]["urgency"], x),
|
|
@@ -125,10 +138,14 @@ def allocate_time_for_day(
|
|
|
125
138
|
)
|
|
126
139
|
|
|
127
140
|
allocated = False
|
|
128
|
-
for
|
|
129
|
-
if
|
|
141
|
+
for uuid in sorted_task_ids:
|
|
142
|
+
if uuid not in tasks_remaining:
|
|
143
|
+
# already completed
|
|
144
|
+
continue
|
|
145
|
+
info = tasks_remaining[uuid]
|
|
146
|
+
if any(d in tasks_remaining for d in info["task"].get("depends", [])):
|
|
147
|
+
# cannot execute this task until all its dependencies are completed
|
|
130
148
|
continue
|
|
131
|
-
info = tasks_remaining[id]
|
|
132
149
|
allocation = allocate_time_to_task(info, day_offset, day_remaining_hours)
|
|
133
150
|
if allocation > 0:
|
|
134
151
|
day_remaining_hours -= allocation
|
|
@@ -143,7 +160,7 @@ def allocate_time_for_day(
|
|
|
143
160
|
info["remaining_hours"] <= 0
|
|
144
161
|
or info["task_time_map"][day_offset] <= 0
|
|
145
162
|
):
|
|
146
|
-
del tasks_remaining[
|
|
163
|
+
del tasks_remaining[uuid]
|
|
147
164
|
# if day_remaining_hours <= 0:
|
|
148
165
|
break
|
|
149
166
|
if not allocated:
|
|
@@ -168,13 +185,13 @@ def compute_total_available_hours(task_info, day_offset):
|
|
|
168
185
|
|
|
169
186
|
def prepare_tasks_remaining(task_info, day_offset):
|
|
170
187
|
return {
|
|
171
|
-
info["task"]["
|
|
188
|
+
info["task"]["uuid"]: info
|
|
172
189
|
for info in task_info.values()
|
|
173
190
|
if info["remaining_hours"] > 0 and info["task_time_map"][day_offset] > 0
|
|
174
191
|
}
|
|
175
192
|
|
|
176
193
|
|
|
177
|
-
def recompute_urgencies(tasks_remaining, urgency_coefficients
|
|
194
|
+
def recompute_urgencies(tasks_remaining, urgency_coefficients):
|
|
178
195
|
# Recompute estimated urgencies as before
|
|
179
196
|
for info in tasks_remaining.values():
|
|
180
197
|
remaining_hours = info["remaining_hours"]
|
|
@@ -184,23 +201,28 @@ def recompute_urgencies(tasks_remaining, urgency_coefficients, inherit_urgency):
|
|
|
184
201
|
_old_estimated_urgency = info["estimated_urgency"]
|
|
185
202
|
info["estimated_urgency"] = estimated_urgency
|
|
186
203
|
info["urgency"] = info["urgency"] - _old_estimated_urgency + estimated_urgency
|
|
204
|
+
started_by_user = info["task"].get("start", "") != ""
|
|
205
|
+
started_by_scheduler = info["started"]
|
|
206
|
+
if started_by_scheduler and not started_by_user:
|
|
207
|
+
# If the task was started by the scheduler, apply the active task coefficient
|
|
208
|
+
info["urgency"] += urgency_coefficients.active
|
|
187
209
|
|
|
188
|
-
if
|
|
210
|
+
if urgency_coefficients.inherit:
|
|
189
211
|
# Define a recursive function to compute the maximum urgency
|
|
190
212
|
def get_max_urgency(info, visited):
|
|
191
|
-
|
|
192
|
-
if
|
|
193
|
-
return visited[
|
|
213
|
+
task_uuid = info["task"]["uuid"]
|
|
214
|
+
if task_uuid in visited:
|
|
215
|
+
return visited[task_uuid] # Return cached value to avoid cycles
|
|
194
216
|
# Start with the current task's urgency
|
|
195
217
|
urgency = info["urgency"]
|
|
196
|
-
visited[
|
|
218
|
+
visited[task_uuid] = urgency # Mark as visited
|
|
197
219
|
# Recursively compute urgencies of dependents
|
|
198
|
-
for
|
|
199
|
-
if
|
|
200
|
-
dep_info = tasks_remaining[
|
|
220
|
+
for dep_uuid in info["task"].get("depends", []):
|
|
221
|
+
if dep_uuid in tasks_remaining:
|
|
222
|
+
dep_info = tasks_remaining[dep_uuid]
|
|
201
223
|
dep_urgency = get_max_urgency(dep_info, visited)
|
|
202
224
|
urgency = max(urgency, dep_urgency)
|
|
203
|
-
visited[
|
|
225
|
+
visited[task_uuid] = urgency # Update with the maximum urgency found
|
|
204
226
|
return urgency
|
|
205
227
|
|
|
206
228
|
# Update urgencies based on dependents
|
|
@@ -222,11 +244,12 @@ def allocate_time_to_task(info, day_offset, day_remaining_hours):
|
|
|
222
244
|
hours_to_decimal(info["task"].get("min_block", 2)),
|
|
223
245
|
)
|
|
224
246
|
|
|
225
|
-
if allocation <= 0:
|
|
247
|
+
if allocation <= 0.05:
|
|
226
248
|
return 0
|
|
227
249
|
|
|
228
250
|
info["remaining_hours"] -= allocation
|
|
229
251
|
info["task_time_map"][day_offset] -= allocation
|
|
252
|
+
info["started"] = True
|
|
230
253
|
|
|
231
254
|
return allocation
|
|
232
255
|
|
|
@@ -248,7 +271,7 @@ def update_tasks_with_scheduling_info(task_info, verbose):
|
|
|
248
271
|
end_date = scheduled_dates[-1]
|
|
249
272
|
for date_str in scheduled_dates:
|
|
250
273
|
hours = info["scheduling"][date_str]
|
|
251
|
-
scheduling_note += f"{date_str}
|
|
274
|
+
scheduling_note += f"{date_str} - {hours_to_pdth(hours)}\n"
|
|
252
275
|
|
|
253
276
|
subprocess.run(
|
|
254
277
|
[
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from datetime import timedelta
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
from rich import print
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
from rich.text import Text
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_tasks(tasks, year, month, day):
|
|
17
|
+
regex = re.compile(rf"{year}-{month}-{day} - ([PDTHM0-9]+)")
|
|
18
|
+
valid_tasks = []
|
|
19
|
+
for task in tasks:
|
|
20
|
+
for line in task["scheduling"].split("\n"):
|
|
21
|
+
m = regex.match(line)
|
|
22
|
+
if m:
|
|
23
|
+
due = task.get("due", "")
|
|
24
|
+
if due:
|
|
25
|
+
due = datetime.strptime(due, "%Y%m%dT%H%M%SZ")
|
|
26
|
+
due = due - datetime.today()
|
|
27
|
+
valid_tasks.append(
|
|
28
|
+
{
|
|
29
|
+
"id": task["id"],
|
|
30
|
+
"project": task.get("project", ""),
|
|
31
|
+
"description": task["description"],
|
|
32
|
+
"scheduling_day": f"{year}-{month}-{day}",
|
|
33
|
+
"scheduling_hours": m.group(1),
|
|
34
|
+
"urgency": task["urgency"],
|
|
35
|
+
"due": due,
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
valid_tasks = sorted(valid_tasks, key=lambda x: x["urgency"], reverse=True)
|
|
39
|
+
return valid_tasks
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_taskwarrior_date(date, _retry=True):
|
|
43
|
+
date = subprocess.run(
|
|
44
|
+
["task", "calc", date],
|
|
45
|
+
capture_output=True,
|
|
46
|
+
text=True,
|
|
47
|
+
)
|
|
48
|
+
date = date.stdout.strip()
|
|
49
|
+
try:
|
|
50
|
+
date = datetime.strptime(date, "%Y-%m-%dT%H:%M:%S")
|
|
51
|
+
except Exception as _:
|
|
52
|
+
if _retry:
|
|
53
|
+
return get_taskwarrior_date("today+" + date, False)
|
|
54
|
+
else:
|
|
55
|
+
print(
|
|
56
|
+
"Please provide a valid date. Check with `task calc` that your date is in the format YYYY-MM-DDTHH:MM:SS",
|
|
57
|
+
file=sys.stderr,
|
|
58
|
+
)
|
|
59
|
+
exit(2)
|
|
60
|
+
return date
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_days_in_constraint(constraint):
|
|
64
|
+
current_date = datetime.today().replace(hour=0, minute=0, second=0, microsecond=0)
|
|
65
|
+
constraint = get_taskwarrior_date(constraint)
|
|
66
|
+
while current_date <= constraint:
|
|
67
|
+
yield current_date.year, current_date.month, current_date.day
|
|
68
|
+
current_date += timedelta(days=1)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def generate_report(config, constraint, verbose=False):
|
|
72
|
+
console = Console()
|
|
73
|
+
tasks = subprocess.run(
|
|
74
|
+
["task", "scheduling~.", "export"], capture_output=True, text=True
|
|
75
|
+
)
|
|
76
|
+
tasks = json.loads(tasks.stdout)
|
|
77
|
+
|
|
78
|
+
for year, month, day in get_days_in_constraint(constraint):
|
|
79
|
+
this_day_tasks = get_tasks(tasks, year, month, day)
|
|
80
|
+
|
|
81
|
+
# Create a colorful date header with emojis and a panel
|
|
82
|
+
date_str = f":calendar: [bold cyan]{year}-{month}-{day}[/bold cyan]"
|
|
83
|
+
console.print(Panel(date_str, style="bold magenta", expand=False))
|
|
84
|
+
|
|
85
|
+
if this_day_tasks:
|
|
86
|
+
table = Table(show_header=True, header_style="bold blue")
|
|
87
|
+
table.add_column("Task", style="dim", width=12)
|
|
88
|
+
table.add_column("Project", style="dim", width=12)
|
|
89
|
+
table.add_column("Description")
|
|
90
|
+
table.add_column("Time", justify="right")
|
|
91
|
+
table.add_column("Urgency")
|
|
92
|
+
table.add_column("Due date")
|
|
93
|
+
|
|
94
|
+
for task in this_day_tasks:
|
|
95
|
+
task_id = f"[bold green]#{task['id']}[/bold green]"
|
|
96
|
+
project = task["project"]
|
|
97
|
+
description = Text(task["description"], style="white")
|
|
98
|
+
hours = f"[yellow]{task['scheduling_hours']}h[/yellow]"
|
|
99
|
+
urgency = f"[bold]{task['urgency']}[/bold]"
|
|
100
|
+
due = task["due"]
|
|
101
|
+
if due != "" and due < timedelta(days=5):
|
|
102
|
+
due = f"[red]{due.days}d[/red]"
|
|
103
|
+
elif due != "":
|
|
104
|
+
due = f"[green]{due.days}d[/green]"
|
|
105
|
+
|
|
106
|
+
# Add an emoji based on a keyword in the description
|
|
107
|
+
if "meeting" in task["description"].lower():
|
|
108
|
+
emoji = ":busts_in_silhouette:"
|
|
109
|
+
elif "review" in task["description"].lower():
|
|
110
|
+
emoji = ":mag_right:"
|
|
111
|
+
elif "write" in task["description"].lower():
|
|
112
|
+
emoji = ":pencil:"
|
|
113
|
+
else:
|
|
114
|
+
emoji = ":pushpin:"
|
|
115
|
+
|
|
116
|
+
table.add_row(
|
|
117
|
+
f"{emoji} {task_id}", project, description, hours, urgency, due
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
console.print(table)
|
|
121
|
+
else:
|
|
122
|
+
console.print("[italic dim]No tasks scheduled for this day.[/italic dim]")
|
|
@@ -5,6 +5,7 @@ from taskcheck.common import (
|
|
|
5
5
|
get_tasks,
|
|
6
6
|
mark_end_date,
|
|
7
7
|
pdth_to_hours,
|
|
8
|
+
hours_to_pdth,
|
|
8
9
|
AVOID_STATUS,
|
|
9
10
|
)
|
|
10
11
|
|
|
@@ -43,7 +44,7 @@ def schedule_task_on_day(
|
|
|
43
44
|
# consume all the remaining task's hours
|
|
44
45
|
if scheduling_note != "":
|
|
45
46
|
scheduling_note += "\n"
|
|
46
|
-
scheduling_note += f"{current_date}
|
|
47
|
+
scheduling_note += f"{current_date} - {hours_to_pdth(task_remaining_hours)}\n"
|
|
47
48
|
used_hours[day_offset] += task_remaining_hours
|
|
48
49
|
task_remaining_hours = 0
|
|
49
50
|
end_date = current_date
|
|
@@ -54,7 +55,7 @@ def schedule_task_on_day(
|
|
|
54
55
|
# consume all the available hours on this task
|
|
55
56
|
if scheduling_note != "":
|
|
56
57
|
scheduling_note += "\n"
|
|
57
|
-
scheduling_note += f"{current_date}
|
|
58
|
+
scheduling_note += f"{current_date} - {hours_to_pdth(employable_hours)}\n"
|
|
58
59
|
task_remaining_hours -= employable_hours
|
|
59
60
|
used_hours[day_offset] += employable_hours
|
|
60
61
|
if verbose:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: taskcheck
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Automatic scheduler for taskwarrior
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -8,6 +8,7 @@ License-File: LICENSE
|
|
|
8
8
|
Requires-Dist: appdirs>=1.4.4
|
|
9
9
|
Requires-Dist: icalendar>=6.0.1
|
|
10
10
|
Requires-Dist: requests>=2.32.3
|
|
11
|
+
Requires-Dist: rich>=13.9.4
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
> _A non-AI automatic scheduler for taskwarrior (i.e. alternative to skedpal/timehero/flowsavvy/reclaim/trevor/motion)_
|
|
@@ -19,7 +20,8 @@ This is a taskwarrior extension checks if tasks can be completed on time, consid
|
|
|
19
20
|
- [x] Use arbitrarily complex time maps
|
|
20
21
|
- [x] Use ical to block time from scheduling (e.g. for meetings, vacations, etc.)
|
|
21
22
|
- [x] Implement scheduling algorithm for parallely working on multiple tasks
|
|
22
|
-
- [ ] Use Google
|
|
23
|
+
- [ ] Use Google API to access calendars
|
|
24
|
+
- [ ] Export tasks to iCal calendar and API calendars
|
|
23
25
|
|
|
24
26
|
## Install
|
|
25
27
|
|
|
@@ -31,29 +33,45 @@ This is a taskwarrior extension checks if tasks can be completed on time, consid
|
|
|
31
33
|
This extension parses your pending and waiting tasks sorted decreasingly by urgency and tries to schedule them in the future.
|
|
32
34
|
It considers their estimated time to schedule all tasks starting from the most urgent one.
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
#### UDAs
|
|
37
|
+
|
|
38
|
+
Taskcheck leverages to UDAs, `estimated` and `time_map`. The `estimated` attribute is
|
|
35
39
|
the expected time to complete the task in hours. The `time_map` is a comma-separated list of strings
|
|
36
40
|
that indicates the hours per day in which you will work on a task (e.g. `work`, `weekend`, etc.).
|
|
37
41
|
The exact correspondence between the `time_map` and the hours of the day is defined in the configuration
|
|
38
|
-
file of taskcheck.
|
|
42
|
+
file of taskcheck. For instance:
|
|
43
|
+
|
|
44
|
+
```toml
|
|
45
|
+
[time_maps]
|
|
46
|
+
# get an error)
|
|
47
|
+
[time_maps.work]
|
|
48
|
+
monday = [[9, 12.30], [14, 17]]
|
|
49
|
+
tuesday = [[9, 12.30], [14, 17]]
|
|
50
|
+
# ...
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
#### They say it's an "AI"
|
|
39
54
|
|
|
40
|
-
|
|
55
|
+
Taskcheck will also parse online iCal calendars (Google, Apple, etc.) and will match them with your time maps.
|
|
56
|
+
It will then modify the Taskwarrior tasks by adding the `completion_date` attribute with the expected
|
|
41
57
|
date of completion and the `scheduled` attribute with the date in which the task is expected to
|
|
42
58
|
start.
|
|
43
59
|
|
|
44
60
|
It will also print a red line for every task whose `completion_date` is after its `due_date`.
|
|
45
61
|
|
|
46
|
-
|
|
62
|
+
In general, it is recommended to run taskcheck rather frequently and at least once at the beginning
|
|
63
|
+
of your working day.
|
|
47
64
|
|
|
48
|
-
|
|
65
|
+
#### Reports
|
|
49
66
|
|
|
50
|
-
You can
|
|
51
|
-
|
|
67
|
+
You can also print simple reports that exploit the `scheduling` UDA filled by Taskcheck to grasp
|
|
68
|
+
how much time you have to work on which task in which day. For
|
|
69
|
+
instance:
|
|
52
70
|
|
|
53
|
-
|
|
54
|
-
|
|
71
|
+
- `taskcheck -r today` will show the tasks planned for today
|
|
72
|
+
- `taskcheck -r 1w` will show the tasks planned for the next week,
|
|
55
73
|
|
|
56
|
-
|
|
74
|
+
## Algorithms
|
|
57
75
|
|
|
58
76
|
Taskcheck provides two scheduling algorithms: `parallel` and `sequential`.
|
|
59
77
|
|
|
@@ -125,9 +143,15 @@ event_all_day_is_blocking = true
|
|
|
125
143
|
expiration = 720 # in hours (720 hours = 30 days)
|
|
126
144
|
```
|
|
127
145
|
|
|
146
|
+
## Tips and Tricks
|
|
147
|
+
|
|
148
|
+
- You can exclude a task from being scheduled by removing the `time_map` or `estimated` attributes.
|
|
149
|
+
- You can see tasks that you can execute now with the `task ready` report.
|
|
150
|
+
|
|
128
151
|
## CLI Options
|
|
129
152
|
|
|
130
153
|
```
|
|
131
154
|
-v, --verbose: increase output verbosity
|
|
132
155
|
-i, --install: install taskcheck configuration
|
|
156
|
+
-r, --report: show tasks planned until a certain time
|
|
133
157
|
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|