taskcheck 0.3.0.post1__tar.gz → 0.3.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.3.0.post1/taskcheck.egg-info → taskcheck-0.3.2}/PKG-INFO +15 -26
- {taskcheck-0.3.0.post1 → taskcheck-0.3.2}/README.md +13 -25
- {taskcheck-0.3.0.post1 → taskcheck-0.3.2}/pyproject.toml +2 -1
- {taskcheck-0.3.0.post1 → taskcheck-0.3.2}/taskcheck/__main__.py +1 -7
- {taskcheck-0.3.0.post1 → taskcheck-0.3.2}/taskcheck/common.py +6 -1
- {taskcheck-0.3.0.post1 → taskcheck-0.3.2}/taskcheck/install.py +0 -1
- taskcheck-0.3.2/taskcheck/report.py +397 -0
- {taskcheck-0.3.0.post1 → taskcheck-0.3.2/taskcheck.egg-info}/PKG-INFO +15 -26
- {taskcheck-0.3.0.post1 → taskcheck-0.3.2}/taskcheck.egg-info/SOURCES.txt +0 -1
- {taskcheck-0.3.0.post1 → taskcheck-0.3.2}/taskcheck.egg-info/requires.txt +1 -0
- taskcheck-0.3.0.post1/taskcheck/report.py +0 -122
- taskcheck-0.3.0.post1/taskcheck/sequential.py +0 -150
- {taskcheck-0.3.0.post1 → taskcheck-0.3.2}/LICENSE +0 -0
- {taskcheck-0.3.0.post1 → taskcheck-0.3.2}/setup.cfg +0 -0
- {taskcheck-0.3.0.post1 → taskcheck-0.3.2}/taskcheck/ical.py +0 -0
- {taskcheck-0.3.0.post1 → taskcheck-0.3.2}/taskcheck/parallel.py +0 -0
- {taskcheck-0.3.0.post1 → taskcheck-0.3.2}/taskcheck.egg-info/dependency_links.txt +0 -0
- {taskcheck-0.3.0.post1 → taskcheck-0.3.2}/taskcheck.egg-info/entry_points.txt +0 -0
- {taskcheck-0.3.0.post1 → taskcheck-0.3.2}/taskcheck.egg-info/top_level.txt +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: taskcheck
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.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: random-unicode-emoji>=2.8
|
|
10
11
|
Requires-Dist: requests>=2.32.3
|
|
11
12
|
Requires-Dist: rich>=13.9.4
|
|
12
13
|
|
|
@@ -15,6 +16,10 @@ Requires-Dist: rich>=13.9.4
|
|
|
15
16
|
|
|
16
17
|
This is a taskwarrior extension checks if tasks can be completed on time, considering estimated time and working hours.
|
|
17
18
|
|
|
19
|
+
<p align="center">
|
|
20
|
+
<img src="https://github.com/user-attachments/assets/b9082701-339b-4407-b941-b613a717382c"/>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
18
23
|
## Features
|
|
19
24
|
|
|
20
25
|
- [x] Use arbitrarily complex time maps
|
|
@@ -71,29 +76,9 @@ instance:
|
|
|
71
76
|
- `taskcheck -r today` will show the tasks planned for today
|
|
72
77
|
- `taskcheck -r 1w` will show the tasks planned for the next week,
|
|
73
78
|
|
|
74
|
-
##
|
|
75
|
-
|
|
76
|
-
Taskcheck provides two scheduling algorithms: `parallel` and `sequential`.
|
|
77
|
-
|
|
78
|
-
#### Sequential
|
|
79
|
-
|
|
80
|
-
The `sequential` algorithm schedules tasks one after the other. It is the simplest algorithm.
|
|
81
|
-
It looks at your tasks one by one, computes the time slots available for that task, and fills them
|
|
82
|
-
as soon as possible.
|
|
83
|
-
|
|
84
|
-
While it sounds logical, it has a problem.
|
|
85
|
-
While you're working on a task, its remaining estimated time is decreasing, and if your urgency
|
|
86
|
-
depends on the estimated time left, the urgency of the task will decrease as well.
|
|
87
|
-
This is especially problematic for long tasks: imagine if you block your whole week for a single
|
|
88
|
-
task and all the other small tasks, even if urgent, don't get scheduled. After a few days, the
|
|
89
|
-
urgency of the first long task should have decreased.
|
|
79
|
+
## Algorithm
|
|
90
80
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
The `parallel` algorithm tries to solve the problem of the `sequential` algorithm.
|
|
94
|
-
|
|
95
|
-
Instead of allocating all the available time for a task until the task is finished, the `parallel`
|
|
96
|
-
algorithm allocates a fixed amount of time for each task (e.g. 1 hour) and then recomputes the
|
|
81
|
+
The algorithm allocates a fixed amount of time for each task (e.g. 1 hour) and then recomputes the
|
|
97
82
|
urgency and restart the allocation procedure.
|
|
98
83
|
|
|
99
84
|
If after 1 hour the long task has decreased urgency, that will be noticed and the newer most urgent
|
|
@@ -102,8 +87,6 @@ task will get scheduled in its place.
|
|
|
102
87
|
The minimum time for a task is by default 2 hours, but you can change it by tuning the `min_block`
|
|
103
88
|
taskwarrior UDA.
|
|
104
89
|
|
|
105
|
-
The `parallel` algorithm is the default algorithm.
|
|
106
|
-
|
|
107
90
|
## Configuration
|
|
108
91
|
|
|
109
92
|
`taskcheck --install` allows you to create required and recommended configurations for
|
|
@@ -127,7 +110,6 @@ saturday = [[9, 12.30], ]
|
|
|
127
110
|
sunday = [[9, 12.30], ]
|
|
128
111
|
|
|
129
112
|
[scheduler]
|
|
130
|
-
algorithm = "parallel" # or "sequential", see above
|
|
131
113
|
days_ahead = 1000 # how far go with the schedule (lower values make the computation faster)
|
|
132
114
|
|
|
133
115
|
[calendars]
|
|
@@ -141,6 +123,13 @@ timezone = "Europe/Rome" # if set, force timezone for this calendar; timezone va
|
|
|
141
123
|
url = "https://www.officeholidays.com/ics-clean/italy/milan"
|
|
142
124
|
event_all_day_is_blocking = true
|
|
143
125
|
expiration = 720 # in hours (720 hours = 30 days)
|
|
126
|
+
|
|
127
|
+
[report]
|
|
128
|
+
additional_attributes = ["estimated", "due", "urgency"] # additional attributes to show in the report
|
|
129
|
+
# when these words are matched in the task description, the corresponding emoji is used
|
|
130
|
+
emoji_keywords = {"meet"=":busts_in_silhouette:", "review"=":mag_right:"}
|
|
131
|
+
include_unplanned = true # include unplanned tasks in the report in an ad-hoc section
|
|
132
|
+
additional_attributes_unplanned = ["due", "urgency"] # additional attributes to show in the report for unplanned tasks
|
|
144
133
|
```
|
|
145
134
|
|
|
146
135
|
## Tips and Tricks
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
This is a taskwarrior extension checks if tasks can be completed on time, considering estimated time and working hours.
|
|
5
5
|
|
|
6
|
+
<p align="center">
|
|
7
|
+
<img src="https://github.com/user-attachments/assets/b9082701-339b-4407-b941-b613a717382c"/>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
6
10
|
## Features
|
|
7
11
|
|
|
8
12
|
- [x] Use arbitrarily complex time maps
|
|
@@ -59,29 +63,9 @@ instance:
|
|
|
59
63
|
- `taskcheck -r today` will show the tasks planned for today
|
|
60
64
|
- `taskcheck -r 1w` will show the tasks planned for the next week,
|
|
61
65
|
|
|
62
|
-
##
|
|
63
|
-
|
|
64
|
-
Taskcheck provides two scheduling algorithms: `parallel` and `sequential`.
|
|
65
|
-
|
|
66
|
-
#### Sequential
|
|
67
|
-
|
|
68
|
-
The `sequential` algorithm schedules tasks one after the other. It is the simplest algorithm.
|
|
69
|
-
It looks at your tasks one by one, computes the time slots available for that task, and fills them
|
|
70
|
-
as soon as possible.
|
|
71
|
-
|
|
72
|
-
While it sounds logical, it has a problem.
|
|
73
|
-
While you're working on a task, its remaining estimated time is decreasing, and if your urgency
|
|
74
|
-
depends on the estimated time left, the urgency of the task will decrease as well.
|
|
75
|
-
This is especially problematic for long tasks: imagine if you block your whole week for a single
|
|
76
|
-
task and all the other small tasks, even if urgent, don't get scheduled. After a few days, the
|
|
77
|
-
urgency of the first long task should have decreased.
|
|
66
|
+
## Algorithm
|
|
78
67
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
The `parallel` algorithm tries to solve the problem of the `sequential` algorithm.
|
|
82
|
-
|
|
83
|
-
Instead of allocating all the available time for a task until the task is finished, the `parallel`
|
|
84
|
-
algorithm allocates a fixed amount of time for each task (e.g. 1 hour) and then recomputes the
|
|
68
|
+
The algorithm allocates a fixed amount of time for each task (e.g. 1 hour) and then recomputes the
|
|
85
69
|
urgency and restart the allocation procedure.
|
|
86
70
|
|
|
87
71
|
If after 1 hour the long task has decreased urgency, that will be noticed and the newer most urgent
|
|
@@ -90,8 +74,6 @@ task will get scheduled in its place.
|
|
|
90
74
|
The minimum time for a task is by default 2 hours, but you can change it by tuning the `min_block`
|
|
91
75
|
taskwarrior UDA.
|
|
92
76
|
|
|
93
|
-
The `parallel` algorithm is the default algorithm.
|
|
94
|
-
|
|
95
77
|
## Configuration
|
|
96
78
|
|
|
97
79
|
`taskcheck --install` allows you to create required and recommended configurations for
|
|
@@ -115,7 +97,6 @@ saturday = [[9, 12.30], ]
|
|
|
115
97
|
sunday = [[9, 12.30], ]
|
|
116
98
|
|
|
117
99
|
[scheduler]
|
|
118
|
-
algorithm = "parallel" # or "sequential", see above
|
|
119
100
|
days_ahead = 1000 # how far go with the schedule (lower values make the computation faster)
|
|
120
101
|
|
|
121
102
|
[calendars]
|
|
@@ -129,6 +110,13 @@ timezone = "Europe/Rome" # if set, force timezone for this calendar; timezone va
|
|
|
129
110
|
url = "https://www.officeholidays.com/ics-clean/italy/milan"
|
|
130
111
|
event_all_day_is_blocking = true
|
|
131
112
|
expiration = 720 # in hours (720 hours = 30 days)
|
|
113
|
+
|
|
114
|
+
[report]
|
|
115
|
+
additional_attributes = ["estimated", "due", "urgency"] # additional attributes to show in the report
|
|
116
|
+
# when these words are matched in the task description, the corresponding emoji is used
|
|
117
|
+
emoji_keywords = {"meet"=":busts_in_silhouette:", "review"=":mag_right:"}
|
|
118
|
+
include_unplanned = true # include unplanned tasks in the report in an ad-hoc section
|
|
119
|
+
additional_attributes_unplanned = ["due", "urgency"] # additional attributes to show in the report for unplanned tasks
|
|
132
120
|
```
|
|
133
121
|
|
|
134
122
|
## Tips and Tricks
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "taskcheck"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.2"
|
|
4
4
|
description = "Automatic scheduler for taskwarrior"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
7
7
|
dependencies = [
|
|
8
8
|
"appdirs>=1.4.4",
|
|
9
9
|
"icalendar>=6.0.1",
|
|
10
|
+
"random-unicode-emoji>=2.8",
|
|
10
11
|
"requests>=2.32.3",
|
|
11
12
|
"rich>=13.9.4",
|
|
12
13
|
]
|
|
@@ -48,13 +48,7 @@ def main():
|
|
|
48
48
|
return
|
|
49
49
|
|
|
50
50
|
config = load_config()
|
|
51
|
-
|
|
52
|
-
if algo == "parallel":
|
|
53
|
-
check_tasks_parallel(config, verbose=args.verbose)
|
|
54
|
-
elif algo == "sequential":
|
|
55
|
-
check_tasks_sequentially(config, verbose=args.verbose)
|
|
56
|
-
else:
|
|
57
|
-
raise ValueError(f"Unknown algorithm: {algo}")
|
|
51
|
+
check_tasks_parallel(config, verbose=args.verbose)
|
|
58
52
|
|
|
59
53
|
|
|
60
54
|
if __name__ == "__main__":
|
|
@@ -101,7 +101,10 @@ def pdth_to_hours(duration_str):
|
|
|
101
101
|
if "H" in duration_str:
|
|
102
102
|
hours = int(duration_str.split("T")[1].split("H")[0])
|
|
103
103
|
if "M" in duration_str:
|
|
104
|
-
|
|
104
|
+
if "H" in duration_str:
|
|
105
|
+
minutes = int(duration_str.split("H")[1].split("M")[0])
|
|
106
|
+
else:
|
|
107
|
+
minutes = int(duration_str.split("T")[1].split("M")[0])
|
|
105
108
|
return days * 24 + hours + minutes / 60
|
|
106
109
|
|
|
107
110
|
|
|
@@ -155,6 +158,8 @@ def get_long_range_time_map(
|
|
|
155
158
|
time_map = time_maps[time_map_name].get(today_weekday)
|
|
156
159
|
if time_map:
|
|
157
160
|
for schedule_start, schedule_end in time_map:
|
|
161
|
+
schedule_start = hours_to_decimal(schedule_start)
|
|
162
|
+
schedule_end = hours_to_decimal(schedule_end)
|
|
158
163
|
if schedule_start <= today_time <= schedule_end:
|
|
159
164
|
today_used_hours += today_time - schedule_start
|
|
160
165
|
break
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import sys
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
from rich import print
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
|
|
16
|
+
from random_unicode_emoji import random_emoji
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
DEFAULT_EMOJI_KEYWORDS = {
|
|
20
|
+
"meet": ":busts_in_silhouette:",
|
|
21
|
+
"incontr": ":busts_in_silhouette:",
|
|
22
|
+
"rencontr": ":busts_in_silhouette:",
|
|
23
|
+
"reun": ":busts_in_silhouette:",
|
|
24
|
+
"treff": ":busts_in_silhouette:",
|
|
25
|
+
"review": ":mag_right:",
|
|
26
|
+
"revis": ":mag_right:",
|
|
27
|
+
"révis": ":mag_right:",
|
|
28
|
+
"überprüf": ":mag_right:",
|
|
29
|
+
"write": ":pencil2:",
|
|
30
|
+
"scriv": ":pencil2:",
|
|
31
|
+
"écri": ":pencil2:",
|
|
32
|
+
"escrib": ":pencil2:",
|
|
33
|
+
"schreib": ":pencil2:",
|
|
34
|
+
"read": ":books:",
|
|
35
|
+
"legg": ":books:",
|
|
36
|
+
"lis": ":books:",
|
|
37
|
+
"le": ":books:",
|
|
38
|
+
"les": ":books:",
|
|
39
|
+
"posta": ":email:",
|
|
40
|
+
"courri": ":email:",
|
|
41
|
+
"corre": ":email:",
|
|
42
|
+
"mail": ":email:",
|
|
43
|
+
"call": ":telephone_receiver:",
|
|
44
|
+
"chiam": ":telephone_receiver:",
|
|
45
|
+
"appel": ":telephone_receiver:",
|
|
46
|
+
"llam": ":telephone_receiver:",
|
|
47
|
+
"ruf": ":telephone_receiver:",
|
|
48
|
+
"présent": ":chart_with_upwards_trend:",
|
|
49
|
+
"present": ":chart_with_upwards_trend:",
|
|
50
|
+
"präsent": ":chart_with_upwards_trend:",
|
|
51
|
+
"learn": ":mortar_board:",
|
|
52
|
+
"impar": ":mortar_board:",
|
|
53
|
+
"appren": ":mortar_board:",
|
|
54
|
+
"aprend": ":mortar_board:",
|
|
55
|
+
"lern": ":mortar_board:",
|
|
56
|
+
"search": ":mag:",
|
|
57
|
+
"cerc": ":mag:",
|
|
58
|
+
"cherch": ":mag:",
|
|
59
|
+
"busc": ":mag:",
|
|
60
|
+
"such": ":mag:",
|
|
61
|
+
"idea": ":bulb:",
|
|
62
|
+
"idé": ":bulb:",
|
|
63
|
+
"ide": ":bulb:",
|
|
64
|
+
"break": ":coffee:",
|
|
65
|
+
"paus": ":coffee:",
|
|
66
|
+
"descans": ":coffee:",
|
|
67
|
+
"lunch": ":fork_and_knife:",
|
|
68
|
+
"pranz": ":fork_and_knife:",
|
|
69
|
+
"déjeun": ":fork_and_knife:",
|
|
70
|
+
"almorz": ":fork_and_knife:",
|
|
71
|
+
"mittag": ":fork_and_knife:",
|
|
72
|
+
"test": ":test_tube:",
|
|
73
|
+
"develop": ":hammer_and_wrench:",
|
|
74
|
+
"svilupp": ":hammer_and_wrench:",
|
|
75
|
+
"développ": ":hammer_and_wrench:",
|
|
76
|
+
"desarroll": ":hammer_and_wrench:",
|
|
77
|
+
"entwickl": ":hammer_and_wrench:",
|
|
78
|
+
"disegn": ":art:",
|
|
79
|
+
"diseñ": ":art:",
|
|
80
|
+
"design": ":art:",
|
|
81
|
+
"dafar": ":memo:",
|
|
82
|
+
"tarea": ":memo:",
|
|
83
|
+
"todo": ":memo:",
|
|
84
|
+
"bug": ":beetle:",
|
|
85
|
+
"käfer": ":beetle:",
|
|
86
|
+
"fix": ":wrench:",
|
|
87
|
+
"corregg": ":wrench:",
|
|
88
|
+
"répar": ":wrench:",
|
|
89
|
+
"beheb": ":wrench:",
|
|
90
|
+
"urgent": ":exclamation:",
|
|
91
|
+
"dringend": ":exclamation:",
|
|
92
|
+
"deadlin": ":hourglass_flowing_sand:",
|
|
93
|
+
"scadenz": ":hourglass_flowing_sand:",
|
|
94
|
+
"délais": ":hourglass_flowing_sand:",
|
|
95
|
+
"fechalim": ":hourglass_flowing_sand:",
|
|
96
|
+
"frist": ":hourglass_flowing_sand:",
|
|
97
|
+
"updat": ":arrows_counterclockwise:",
|
|
98
|
+
"aggiorn": ":arrows_counterclockwise:",
|
|
99
|
+
"mett": ":arrows_counterclockwise:",
|
|
100
|
+
"actualiz": ":arrows_counterclockwise:",
|
|
101
|
+
"aktualisier": ":arrows_counterclockwise:",
|
|
102
|
+
"clean": ":broom:",
|
|
103
|
+
"pul": ":broom:",
|
|
104
|
+
"nettoy": ":broom:",
|
|
105
|
+
"limpi": ":broom:",
|
|
106
|
+
"reinig": ":broom:",
|
|
107
|
+
"deploy": ":rocket:",
|
|
108
|
+
"rilasc": ":rocket:",
|
|
109
|
+
"déploy": ":rocket:",
|
|
110
|
+
"despleg": ":rocket:",
|
|
111
|
+
"bereitstell": ":rocket:",
|
|
112
|
+
"festegg": ":tada:",
|
|
113
|
+
"célébr": ":tada:",
|
|
114
|
+
"celebr": ":tada:",
|
|
115
|
+
"feier": ":tada:",
|
|
116
|
+
"research": ":microscope:",
|
|
117
|
+
"ricerc": ":microscope:",
|
|
118
|
+
"recherch": ":microscope:",
|
|
119
|
+
"investig": ":microscope:",
|
|
120
|
+
"forsch": ":microscope:",
|
|
121
|
+
"shop": ":shopping_cart:",
|
|
122
|
+
"achet": ":shopping_cart:",
|
|
123
|
+
"compr": ":shopping_cart:",
|
|
124
|
+
"einkauf": ":shopping_cart:",
|
|
125
|
+
"social": ":handshake:",
|
|
126
|
+
"sozial": ":handshake:",
|
|
127
|
+
"exercis": ":running_shoe:",
|
|
128
|
+
"eserciz": ":running_shoe:",
|
|
129
|
+
"exercic": ":running_shoe:",
|
|
130
|
+
"ejerc": ":running_shoe:",
|
|
131
|
+
"übun": ":running_shoe:",
|
|
132
|
+
"évènement": ":calendar:",
|
|
133
|
+
"event": ":calendar:",
|
|
134
|
+
"movie": ":clapper:",
|
|
135
|
+
"pelicul": ":clapper:",
|
|
136
|
+
"film": ":clapper:",
|
|
137
|
+
"music": ":musical_note:",
|
|
138
|
+
"musik": ":musical_note:",
|
|
139
|
+
"travel": ":airplane:",
|
|
140
|
+
"viagg": ":airplane:",
|
|
141
|
+
"voyag": ":airplane:",
|
|
142
|
+
"viaj": ":airplane:",
|
|
143
|
+
"reis": ":airplane:",
|
|
144
|
+
"home": ":house:",
|
|
145
|
+
"mais": ":house:",
|
|
146
|
+
"cas": ":house:",
|
|
147
|
+
"haus": ":house:",
|
|
148
|
+
"docu": ":notebook:",
|
|
149
|
+
"dokum": ":notebook:",
|
|
150
|
+
"backup": ":floppy_disk:",
|
|
151
|
+
"salvat": ":floppy_disk:",
|
|
152
|
+
"sauveg": ":floppy_disk:",
|
|
153
|
+
"copiaseg": ":floppy_disk:",
|
|
154
|
+
"sicher": ":floppy_disk:",
|
|
155
|
+
"debug": ":bug:",
|
|
156
|
+
"débug": ":bug:",
|
|
157
|
+
"note": ":spiral_notepad:",
|
|
158
|
+
"not": ":spiral_notepad:",
|
|
159
|
+
"focus": ":dart:",
|
|
160
|
+
"concentr": ":dart:",
|
|
161
|
+
"konzentrier": ":dart:",
|
|
162
|
+
"codic": ":computer:",
|
|
163
|
+
"cod": ":computer:",
|
|
164
|
+
"code": ":computer:",
|
|
165
|
+
"serveur": ":cloud:",
|
|
166
|
+
"servidor": ":cloud:",
|
|
167
|
+
"server": ":cloud:",
|
|
168
|
+
"client": ":briefcase:",
|
|
169
|
+
"priorid": ":bangbang:",
|
|
170
|
+
"priorit": ":bangbang:",
|
|
171
|
+
"star": ":star:",
|
|
172
|
+
"stell": ":star:",
|
|
173
|
+
"étoil": ":star:",
|
|
174
|
+
"estrell": ":star:",
|
|
175
|
+
"stern": ":star:",
|
|
176
|
+
"critic": ":fire:",
|
|
177
|
+
"kritisch": ":fire:",
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def get_tasks(config, tasks, year, month, day):
|
|
182
|
+
regex = re.compile(rf"{year}-{month}-{day} - ([PDTHM0-9]+)")
|
|
183
|
+
valid_tasks = []
|
|
184
|
+
for task in tasks:
|
|
185
|
+
for line in task["scheduling"].split("\n"):
|
|
186
|
+
m = regex.match(line)
|
|
187
|
+
if m:
|
|
188
|
+
due = task.get("due", "")
|
|
189
|
+
if due:
|
|
190
|
+
due = datetime.strptime(due, "%Y%m%dT%H%M%SZ")
|
|
191
|
+
due = due - datetime.today()
|
|
192
|
+
valid_tasks.append(
|
|
193
|
+
{
|
|
194
|
+
"id": task["id"],
|
|
195
|
+
"project": task.get("project", ""),
|
|
196
|
+
"description": task["description"],
|
|
197
|
+
"scheduling_day": f"{year}-{month}-{day}",
|
|
198
|
+
"scheduling_hours": m.group(1),
|
|
199
|
+
**{
|
|
200
|
+
attr: task.get(attr, "")
|
|
201
|
+
for attr in config.get("additional_attributes", [])
|
|
202
|
+
},
|
|
203
|
+
}
|
|
204
|
+
)
|
|
205
|
+
valid_tasks = sorted(valid_tasks, key=lambda x: x["urgency"], reverse=True)
|
|
206
|
+
return valid_tasks
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def get_taskwarrior_date(date, _retry=True):
|
|
210
|
+
date = subprocess.run(
|
|
211
|
+
["task", "calc", date],
|
|
212
|
+
capture_output=True,
|
|
213
|
+
text=True,
|
|
214
|
+
)
|
|
215
|
+
date = date.stdout.strip()
|
|
216
|
+
try:
|
|
217
|
+
date = datetime.strptime(date, "%Y-%m-%dT%H:%M:%S")
|
|
218
|
+
except Exception as _:
|
|
219
|
+
if _retry:
|
|
220
|
+
return get_taskwarrior_date("today+" + date, False)
|
|
221
|
+
else:
|
|
222
|
+
print(
|
|
223
|
+
"Please provide a valid date. Check with `task calc` that your date is in the format YYYY-MM-DDTHH:MM:SS",
|
|
224
|
+
file=sys.stderr,
|
|
225
|
+
)
|
|
226
|
+
exit(2)
|
|
227
|
+
return date
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def get_days_in_constraint(constraint):
|
|
231
|
+
current_date = datetime.today().replace(hour=0, minute=0, second=0, microsecond=0)
|
|
232
|
+
constraint = get_taskwarrior_date(constraint)
|
|
233
|
+
while current_date <= constraint:
|
|
234
|
+
yield current_date.year, current_date.month, current_date.day
|
|
235
|
+
current_date += timedelta(days=1)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def tostring(value):
|
|
239
|
+
if isinstance(value, bool):
|
|
240
|
+
return "Yes" if value else "No"
|
|
241
|
+
elif isinstance(value, datetime):
|
|
242
|
+
return value.strftime("%Y-%m-%d %H:%M")
|
|
243
|
+
elif isinstance(value, str):
|
|
244
|
+
try:
|
|
245
|
+
date = datetime.strptime(value, "%Y%m%dT%H%M%SZ")
|
|
246
|
+
return date.strftime("%Y-%m-%d %H:%M")
|
|
247
|
+
except ValueError:
|
|
248
|
+
return value
|
|
249
|
+
else:
|
|
250
|
+
return str(value)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def get_unplanned_tasks(config, tasks):
|
|
254
|
+
tasks = subprocess.run(
|
|
255
|
+
["task", "scheduling:", "status:pending", "export"],
|
|
256
|
+
capture_output=True,
|
|
257
|
+
text=True,
|
|
258
|
+
)
|
|
259
|
+
tasks = json.loads(tasks.stdout)
|
|
260
|
+
return tasks
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def generate_report(config, constraint, verbose=False):
|
|
264
|
+
config = config["report"]
|
|
265
|
+
console = Console()
|
|
266
|
+
tasks = fetch_tasks()
|
|
267
|
+
|
|
268
|
+
for year, month, day in get_days_in_constraint(constraint):
|
|
269
|
+
this_day_tasks = get_tasks(config, tasks, year, month, day)
|
|
270
|
+
|
|
271
|
+
display_date_header(console, year, month, day)
|
|
272
|
+
|
|
273
|
+
display_tasks_table(console, config, this_day_tasks)
|
|
274
|
+
|
|
275
|
+
if config.get("include_unplanned"):
|
|
276
|
+
unplanned_tasks = get_unplanned_tasks(config, tasks)
|
|
277
|
+
display_unplanned_tasks(console, config, unplanned_tasks)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def fetch_tasks():
|
|
281
|
+
"""Fetch tasks from the task manager and return them as a JSON object."""
|
|
282
|
+
tasks = subprocess.run(
|
|
283
|
+
["task", "scheduling~.", "(", "+PENDING", "or", "+WAITING", ")", "export"],
|
|
284
|
+
capture_output=True,
|
|
285
|
+
text=True,
|
|
286
|
+
)
|
|
287
|
+
return json.loads(tasks.stdout)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def display_date_header(console, year, month, day):
|
|
291
|
+
"""Display a date header with a calendar emoji."""
|
|
292
|
+
date_str = f":calendar: [bold cyan]{year}-{month}-{day}[/bold cyan]"
|
|
293
|
+
console.print(Panel(date_str, style="bold blue", expand=False))
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def display_tasks_table(console, config, tasks):
|
|
297
|
+
"""Display a table of tasks for a specific day."""
|
|
298
|
+
if tasks:
|
|
299
|
+
table = build_tasks_table(config, tasks)
|
|
300
|
+
console.print(table)
|
|
301
|
+
else:
|
|
302
|
+
console.print("[italic dim]No tasks scheduled for this day.[/italic dim]")
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def build_tasks_table(config, tasks):
|
|
306
|
+
"""Build a Rich table for displaying tasks."""
|
|
307
|
+
table = Table(show_header=True, header_style="bold blue")
|
|
308
|
+
table.add_column("Task", style="dim", width=12)
|
|
309
|
+
table.add_column("Project", style="dim", width=12)
|
|
310
|
+
table.add_column("Description")
|
|
311
|
+
table.add_column("Time", justify="right")
|
|
312
|
+
for attr in config.get("additional_attributes", []):
|
|
313
|
+
table.add_column(attr.capitalize(), justify="right")
|
|
314
|
+
|
|
315
|
+
for task in tasks:
|
|
316
|
+
task_id = f"[bold green]#{task['id']}[/bold green]"
|
|
317
|
+
project = task.get("project", "")
|
|
318
|
+
description = Text(task["description"], style="white")
|
|
319
|
+
hours = f"[yellow]{task['scheduling_hours']}[/yellow]"
|
|
320
|
+
emoji = get_task_emoji(config, task)
|
|
321
|
+
|
|
322
|
+
table.add_row(
|
|
323
|
+
f"{emoji} {task_id}",
|
|
324
|
+
project,
|
|
325
|
+
description,
|
|
326
|
+
hours,
|
|
327
|
+
*[
|
|
328
|
+
tostring(task.get(attr, ""))
|
|
329
|
+
for attr in config.get("additional_attributes", [])
|
|
330
|
+
],
|
|
331
|
+
)
|
|
332
|
+
return table
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def get_task_emoji(config, task):
|
|
336
|
+
"""Get an emoji based on keywords in the task description."""
|
|
337
|
+
for keyword in config.get("emoji_keywords", []):
|
|
338
|
+
if keyword in task["description"].lower():
|
|
339
|
+
return config["emoji_keywords"][keyword]
|
|
340
|
+
for keyword, emoji in DEFAULT_EMOJI_KEYWORDS.items():
|
|
341
|
+
if keyword in task["description"].lower():
|
|
342
|
+
return emoji
|
|
343
|
+
# sum the unicode numbers of each letter in the description and use it as seed to get a random unicode
|
|
344
|
+
# emoji
|
|
345
|
+
seed = sum(ord(char) for char in task["description"])
|
|
346
|
+
emoji = ""
|
|
347
|
+
# seed the next call to random.choice
|
|
348
|
+
random.seed(seed)
|
|
349
|
+
emoji = random_emoji()[0]
|
|
350
|
+
# some emoji coutn for more than one character and misbehave the tables of rich
|
|
351
|
+
if len(emoji) > 1:
|
|
352
|
+
while len(emoji) != 1:
|
|
353
|
+
emoji = random_emoji()[0]
|
|
354
|
+
return emoji
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def display_unplanned_tasks(console, config, tasks):
|
|
358
|
+
"""Display unplanned tasks if any are found."""
|
|
359
|
+
if tasks:
|
|
360
|
+
table = build_unplanned_tasks_table(config, tasks)
|
|
361
|
+
console.print(Panel("Unplanned Tasks", style="bold blue", expand=False))
|
|
362
|
+
console.print(table)
|
|
363
|
+
else:
|
|
364
|
+
console.print(
|
|
365
|
+
Panel(
|
|
366
|
+
"[italic dim]No unplanned tasks found.[/italic dim]",
|
|
367
|
+
style="bold blue",
|
|
368
|
+
expand=False,
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def build_unplanned_tasks_table(config, tasks):
|
|
374
|
+
"""Build a Rich table for displaying unplanned tasks."""
|
|
375
|
+
table = Table(show_header=True, header_style="bold blue")
|
|
376
|
+
table.add_column("Task", style="dim", width=12)
|
|
377
|
+
table.add_column("Project", style="dim", width=12)
|
|
378
|
+
table.add_column("Description")
|
|
379
|
+
for attr in config.get("additional_attributes_unplanned", []):
|
|
380
|
+
table.add_column(attr.capitalize(), justify="right")
|
|
381
|
+
|
|
382
|
+
for task in tasks:
|
|
383
|
+
task_id = f"[bold green]#{task['id']}[/bold green]"
|
|
384
|
+
project = task.get("project", "")
|
|
385
|
+
description = Text(task["description"], style="white")
|
|
386
|
+
emoji = get_task_emoji(config, task)
|
|
387
|
+
|
|
388
|
+
table.add_row(
|
|
389
|
+
f"{emoji} {task_id}",
|
|
390
|
+
project,
|
|
391
|
+
description,
|
|
392
|
+
*[
|
|
393
|
+
tostring(task.get(attr, ""))
|
|
394
|
+
for attr in config.get("additional_attributes_unplanned", [])
|
|
395
|
+
],
|
|
396
|
+
)
|
|
397
|
+
return table
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: taskcheck
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.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: random-unicode-emoji>=2.8
|
|
10
11
|
Requires-Dist: requests>=2.32.3
|
|
11
12
|
Requires-Dist: rich>=13.9.4
|
|
12
13
|
|
|
@@ -15,6 +16,10 @@ Requires-Dist: rich>=13.9.4
|
|
|
15
16
|
|
|
16
17
|
This is a taskwarrior extension checks if tasks can be completed on time, considering estimated time and working hours.
|
|
17
18
|
|
|
19
|
+
<p align="center">
|
|
20
|
+
<img src="https://github.com/user-attachments/assets/b9082701-339b-4407-b941-b613a717382c"/>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
18
23
|
## Features
|
|
19
24
|
|
|
20
25
|
- [x] Use arbitrarily complex time maps
|
|
@@ -71,29 +76,9 @@ instance:
|
|
|
71
76
|
- `taskcheck -r today` will show the tasks planned for today
|
|
72
77
|
- `taskcheck -r 1w` will show the tasks planned for the next week,
|
|
73
78
|
|
|
74
|
-
##
|
|
75
|
-
|
|
76
|
-
Taskcheck provides two scheduling algorithms: `parallel` and `sequential`.
|
|
77
|
-
|
|
78
|
-
#### Sequential
|
|
79
|
-
|
|
80
|
-
The `sequential` algorithm schedules tasks one after the other. It is the simplest algorithm.
|
|
81
|
-
It looks at your tasks one by one, computes the time slots available for that task, and fills them
|
|
82
|
-
as soon as possible.
|
|
83
|
-
|
|
84
|
-
While it sounds logical, it has a problem.
|
|
85
|
-
While you're working on a task, its remaining estimated time is decreasing, and if your urgency
|
|
86
|
-
depends on the estimated time left, the urgency of the task will decrease as well.
|
|
87
|
-
This is especially problematic for long tasks: imagine if you block your whole week for a single
|
|
88
|
-
task and all the other small tasks, even if urgent, don't get scheduled. After a few days, the
|
|
89
|
-
urgency of the first long task should have decreased.
|
|
79
|
+
## Algorithm
|
|
90
80
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
The `parallel` algorithm tries to solve the problem of the `sequential` algorithm.
|
|
94
|
-
|
|
95
|
-
Instead of allocating all the available time for a task until the task is finished, the `parallel`
|
|
96
|
-
algorithm allocates a fixed amount of time for each task (e.g. 1 hour) and then recomputes the
|
|
81
|
+
The algorithm allocates a fixed amount of time for each task (e.g. 1 hour) and then recomputes the
|
|
97
82
|
urgency and restart the allocation procedure.
|
|
98
83
|
|
|
99
84
|
If after 1 hour the long task has decreased urgency, that will be noticed and the newer most urgent
|
|
@@ -102,8 +87,6 @@ task will get scheduled in its place.
|
|
|
102
87
|
The minimum time for a task is by default 2 hours, but you can change it by tuning the `min_block`
|
|
103
88
|
taskwarrior UDA.
|
|
104
89
|
|
|
105
|
-
The `parallel` algorithm is the default algorithm.
|
|
106
|
-
|
|
107
90
|
## Configuration
|
|
108
91
|
|
|
109
92
|
`taskcheck --install` allows you to create required and recommended configurations for
|
|
@@ -127,7 +110,6 @@ saturday = [[9, 12.30], ]
|
|
|
127
110
|
sunday = [[9, 12.30], ]
|
|
128
111
|
|
|
129
112
|
[scheduler]
|
|
130
|
-
algorithm = "parallel" # or "sequential", see above
|
|
131
113
|
days_ahead = 1000 # how far go with the schedule (lower values make the computation faster)
|
|
132
114
|
|
|
133
115
|
[calendars]
|
|
@@ -141,6 +123,13 @@ timezone = "Europe/Rome" # if set, force timezone for this calendar; timezone va
|
|
|
141
123
|
url = "https://www.officeholidays.com/ics-clean/italy/milan"
|
|
142
124
|
event_all_day_is_blocking = true
|
|
143
125
|
expiration = 720 # in hours (720 hours = 30 days)
|
|
126
|
+
|
|
127
|
+
[report]
|
|
128
|
+
additional_attributes = ["estimated", "due", "urgency"] # additional attributes to show in the report
|
|
129
|
+
# when these words are matched in the task description, the corresponding emoji is used
|
|
130
|
+
emoji_keywords = {"meet"=":busts_in_silhouette:", "review"=":mag_right:"}
|
|
131
|
+
include_unplanned = true # include unplanned tasks in the report in an ad-hoc section
|
|
132
|
+
additional_attributes_unplanned = ["due", "urgency"] # additional attributes to show in the report for unplanned tasks
|
|
144
133
|
```
|
|
145
134
|
|
|
146
135
|
## Tips and Tricks
|
|
@@ -1,122 +0,0 @@
|
|
|
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]")
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
from datetime import datetime, timedelta
|
|
2
|
-
from taskcheck.common import (
|
|
3
|
-
get_calendars,
|
|
4
|
-
get_long_range_time_map,
|
|
5
|
-
get_tasks,
|
|
6
|
-
mark_end_date,
|
|
7
|
-
pdth_to_hours,
|
|
8
|
-
hours_to_pdth,
|
|
9
|
-
AVOID_STATUS,
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def schedule_task_on_day(
|
|
14
|
-
is_starting,
|
|
15
|
-
day_offset,
|
|
16
|
-
start_date,
|
|
17
|
-
end_date,
|
|
18
|
-
task_remaining_hours,
|
|
19
|
-
task_time_map,
|
|
20
|
-
today,
|
|
21
|
-
used_hours,
|
|
22
|
-
wait,
|
|
23
|
-
scheduling_note,
|
|
24
|
-
verbose=False,
|
|
25
|
-
):
|
|
26
|
-
# we can schedule task on this day
|
|
27
|
-
employable_hours = task_time_map[day_offset] - used_hours[day_offset]
|
|
28
|
-
# avoid using too small values
|
|
29
|
-
employable_hours = 0.01 if 0.01 > employable_hours > 0 else employable_hours
|
|
30
|
-
current_date = today + timedelta(days=day_offset)
|
|
31
|
-
if wait and current_date <= wait:
|
|
32
|
-
if verbose:
|
|
33
|
-
print(f"Skipping date {current_date} because of wait date {wait}")
|
|
34
|
-
return start_date, end_date, task_remaining_hours, is_starting, scheduling_note
|
|
35
|
-
|
|
36
|
-
if is_starting:
|
|
37
|
-
if verbose:
|
|
38
|
-
print(f"Starting task on {current_date}")
|
|
39
|
-
is_starting = False
|
|
40
|
-
start_date = current_date
|
|
41
|
-
|
|
42
|
-
# minimum value we admit is 0.01 (1 minute)
|
|
43
|
-
if task_remaining_hours <= employable_hours + 0.01:
|
|
44
|
-
# consume all the remaining task's hours
|
|
45
|
-
if scheduling_note != "":
|
|
46
|
-
scheduling_note += "\n"
|
|
47
|
-
scheduling_note += f"{current_date} - {hours_to_pdth(task_remaining_hours)}\n"
|
|
48
|
-
used_hours[day_offset] += task_remaining_hours
|
|
49
|
-
task_remaining_hours = 0
|
|
50
|
-
end_date = current_date
|
|
51
|
-
if verbose:
|
|
52
|
-
print(f"Task can be completed on {current_date}")
|
|
53
|
-
print(f"Used hours on {current_date}: {used_hours[day_offset]}")
|
|
54
|
-
else:
|
|
55
|
-
# consume all the available hours on this task
|
|
56
|
-
if scheduling_note != "":
|
|
57
|
-
scheduling_note += "\n"
|
|
58
|
-
scheduling_note += f"{current_date} - {hours_to_pdth(employable_hours)}\n"
|
|
59
|
-
task_remaining_hours -= employable_hours
|
|
60
|
-
used_hours[day_offset] += employable_hours
|
|
61
|
-
if verbose:
|
|
62
|
-
print(f"Working for {employable_hours} hours on task on {current_date}")
|
|
63
|
-
return start_date, end_date, task_remaining_hours, is_starting, scheduling_note
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
# Check if tasks can be completed on time sequentially
|
|
67
|
-
def check_tasks_sequentially(config, verbose=False):
|
|
68
|
-
tasks = get_tasks()
|
|
69
|
-
time_maps = config["time_maps"]
|
|
70
|
-
today = datetime.today().date()
|
|
71
|
-
todo = [True if t["status"] not in AVOID_STATUS else False for t in tasks]
|
|
72
|
-
used_hours = [0] * (config["scheduler"]["days_ahead"] + 1)
|
|
73
|
-
calendars = get_calendars(config)
|
|
74
|
-
|
|
75
|
-
while any(todo):
|
|
76
|
-
for i, task in enumerate(tasks):
|
|
77
|
-
if not todo[i]:
|
|
78
|
-
# skipping tasks already completed
|
|
79
|
-
continue
|
|
80
|
-
|
|
81
|
-
due_date = (
|
|
82
|
-
datetime.strptime(task["due"], "%Y%m%dT%H%M%SZ").date()
|
|
83
|
-
if "due" in task
|
|
84
|
-
else None
|
|
85
|
-
)
|
|
86
|
-
wait_date = (
|
|
87
|
-
datetime.strptime(task["wait"], "%Y%m%dT%H%M%SZ").date()
|
|
88
|
-
if "wait" in task
|
|
89
|
-
else None
|
|
90
|
-
)
|
|
91
|
-
estimated_hours = (
|
|
92
|
-
pdth_to_hours(task["estimated"]) if "estimated" in task else None
|
|
93
|
-
) # Remove trailing "PT" and "H"
|
|
94
|
-
time_map_names = (
|
|
95
|
-
task.get("time_map").split(",") if "time_map" in task else None
|
|
96
|
-
)
|
|
97
|
-
if estimated_hours is None or time_map_names is None:
|
|
98
|
-
todo[i] = False
|
|
99
|
-
if verbose:
|
|
100
|
-
print(
|
|
101
|
-
f"Task {task['id']} ('{task['description']}') has no estimated time or time map: {estimated_hours}, {time_map_names}"
|
|
102
|
-
)
|
|
103
|
-
continue
|
|
104
|
-
if verbose:
|
|
105
|
-
print(
|
|
106
|
-
f"Checking task {task['id']} ('{task['description']}') with estimated hours: {estimated_hours} and wait date: {wait_date}"
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
task_remaining_hours = estimated_hours
|
|
110
|
-
task_time_map, today_used_hours = get_long_range_time_map(
|
|
111
|
-
time_maps, time_map_names, config["scheduler"]["days_ahead"], calendars
|
|
112
|
-
)
|
|
113
|
-
used_hours[0] += today_used_hours
|
|
114
|
-
|
|
115
|
-
# Simulate work day-by-day until task is complete or past due
|
|
116
|
-
is_starting = True
|
|
117
|
-
scheduling_note = ""
|
|
118
|
-
start_date = end_date = None
|
|
119
|
-
for offset in range(len(task_time_map)):
|
|
120
|
-
if task_time_map[offset] > used_hours[offset]:
|
|
121
|
-
(
|
|
122
|
-
start_date,
|
|
123
|
-
end_date,
|
|
124
|
-
task_remaining_hours,
|
|
125
|
-
is_starting,
|
|
126
|
-
scheduling_note,
|
|
127
|
-
) = schedule_task_on_day(
|
|
128
|
-
is_starting,
|
|
129
|
-
offset,
|
|
130
|
-
start_date,
|
|
131
|
-
end_date,
|
|
132
|
-
task_remaining_hours,
|
|
133
|
-
task_time_map,
|
|
134
|
-
today,
|
|
135
|
-
used_hours,
|
|
136
|
-
wait_date,
|
|
137
|
-
scheduling_note,
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
if end_date is not None:
|
|
141
|
-
todo[i] = False
|
|
142
|
-
mark_end_date(
|
|
143
|
-
due_date,
|
|
144
|
-
end_date,
|
|
145
|
-
start_date,
|
|
146
|
-
scheduling_note,
|
|
147
|
-
task["id"],
|
|
148
|
-
task["description"],
|
|
149
|
-
)
|
|
150
|
-
break
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|