taskcheck 0.3.1__tar.gz → 0.3.2.post1__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.
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: taskcheck
3
- Version: 0.3.1
3
+ Version: 0.3.2.post1
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
 
@@ -75,29 +76,9 @@ instance:
75
76
  - `taskcheck -r today` will show the tasks planned for today
76
77
  - `taskcheck -r 1w` will show the tasks planned for the next week,
77
78
 
78
- ## Algorithms
79
+ ## Algorithm
79
80
 
80
- Taskcheck provides two scheduling algorithms: `parallel` and `sequential`.
81
-
82
- #### Sequential
83
-
84
- The `sequential` algorithm schedules tasks one after the other. It is the simplest algorithm.
85
- It looks at your tasks one by one, computes the time slots available for that task, and fills them
86
- as soon as possible.
87
-
88
- While it sounds logical, it has a problem.
89
- While you're working on a task, its remaining estimated time is decreasing, and if your urgency
90
- depends on the estimated time left, the urgency of the task will decrease as well.
91
- This is especially problematic for long tasks: imagine if you block your whole week for a single
92
- task and all the other small tasks, even if urgent, don't get scheduled. After a few days, the
93
- urgency of the first long task should have decreased.
94
-
95
- #### Parallel
96
-
97
- The `parallel` algorithm tries to solve the problem of the `sequential` algorithm.
98
-
99
- Instead of allocating all the available time for a task until the task is finished, the `parallel`
100
- 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
101
82
  urgency and restart the allocation procedure.
102
83
 
103
84
  If after 1 hour the long task has decreased urgency, that will be noticed and the newer most urgent
@@ -106,8 +87,6 @@ task will get scheduled in its place.
106
87
  The minimum time for a task is by default 2 hours, but you can change it by tuning the `min_block`
107
88
  taskwarrior UDA.
108
89
 
109
- The `parallel` algorithm is the default algorithm.
110
-
111
90
  ## Configuration
112
91
 
113
92
  `taskcheck --install` allows you to create required and recommended configurations for
@@ -131,7 +110,6 @@ saturday = [[9, 12.30], ]
131
110
  sunday = [[9, 12.30], ]
132
111
 
133
112
  [scheduler]
134
- algorithm = "parallel" # or "sequential", see above
135
113
  days_ahead = 1000 # how far go with the schedule (lower values make the computation faster)
136
114
 
137
115
  [calendars]
@@ -145,6 +123,13 @@ timezone = "Europe/Rome" # if set, force timezone for this calendar; timezone va
145
123
  url = "https://www.officeholidays.com/ics-clean/italy/milan"
146
124
  event_all_day_is_blocking = true
147
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
148
133
  ```
149
134
 
150
135
  ## Tips and Tricks
@@ -63,29 +63,9 @@ instance:
63
63
  - `taskcheck -r today` will show the tasks planned for today
64
64
  - `taskcheck -r 1w` will show the tasks planned for the next week,
65
65
 
66
- ## Algorithms
66
+ ## Algorithm
67
67
 
68
- Taskcheck provides two scheduling algorithms: `parallel` and `sequential`.
69
-
70
- #### Sequential
71
-
72
- The `sequential` algorithm schedules tasks one after the other. It is the simplest algorithm.
73
- It looks at your tasks one by one, computes the time slots available for that task, and fills them
74
- as soon as possible.
75
-
76
- While it sounds logical, it has a problem.
77
- While you're working on a task, its remaining estimated time is decreasing, and if your urgency
78
- depends on the estimated time left, the urgency of the task will decrease as well.
79
- This is especially problematic for long tasks: imagine if you block your whole week for a single
80
- task and all the other small tasks, even if urgent, don't get scheduled. After a few days, the
81
- urgency of the first long task should have decreased.
82
-
83
- #### Parallel
84
-
85
- The `parallel` algorithm tries to solve the problem of the `sequential` algorithm.
86
-
87
- Instead of allocating all the available time for a task until the task is finished, the `parallel`
88
- 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
89
69
  urgency and restart the allocation procedure.
90
70
 
91
71
  If after 1 hour the long task has decreased urgency, that will be noticed and the newer most urgent
@@ -94,8 +74,6 @@ task will get scheduled in its place.
94
74
  The minimum time for a task is by default 2 hours, but you can change it by tuning the `min_block`
95
75
  taskwarrior UDA.
96
76
 
97
- The `parallel` algorithm is the default algorithm.
98
-
99
77
  ## Configuration
100
78
 
101
79
  `taskcheck --install` allows you to create required and recommended configurations for
@@ -119,7 +97,6 @@ saturday = [[9, 12.30], ]
119
97
  sunday = [[9, 12.30], ]
120
98
 
121
99
  [scheduler]
122
- algorithm = "parallel" # or "sequential", see above
123
100
  days_ahead = 1000 # how far go with the schedule (lower values make the computation faster)
124
101
 
125
102
  [calendars]
@@ -133,6 +110,13 @@ timezone = "Europe/Rome" # if set, force timezone for this calendar; timezone va
133
110
  url = "https://www.officeholidays.com/ics-clean/italy/milan"
134
111
  event_all_day_is_blocking = true
135
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
136
120
  ```
137
121
 
138
122
  ## Tips and Tricks
@@ -1,12 +1,13 @@
1
1
  [project]
2
2
  name = "taskcheck"
3
- version = "0.3.1"
3
+ version = "0.3.2-1"
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
  ]
@@ -2,7 +2,6 @@ import tomllib
2
2
  import argparse
3
3
 
4
4
  from taskcheck.parallel import check_tasks_parallel
5
- from taskcheck.sequential import check_tasks_sequentially
6
5
  from taskcheck.common import config_dir
7
6
 
8
7
  arg_parser = argparse.ArgumentParser()
@@ -48,13 +47,7 @@ def main():
48
47
  return
49
48
 
50
49
  config = load_config()
51
- algo = config["scheduler"].get("algorithm", "parallel")
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}")
50
+ check_tasks_parallel(config, verbose=args.verbose)
58
51
 
59
52
 
60
53
  if __name__ == "__main__":
@@ -12,7 +12,6 @@ thursday = [[9, 12.30], [14, 17]]
12
12
  friday = [[9, 12.30], [14, 17]]
13
13
 
14
14
  [scheduler]
15
- algorithm = "parallel"
16
15
  days_ahead = 365
17
16
  """
18
17
 
@@ -64,9 +64,7 @@ def compute_estimated_urgency(remaining_hours, urgency_coefficients):
64
64
  key=lambda x: abs(pdth_to_hours(x) - remaining_hours),
65
65
  )
66
66
  coefficient = urgency_coefficients.estimated[closest_match]
67
- # Compute the urgency
68
- estimated_urgency = coefficient * remaining_hours
69
- return estimated_urgency
67
+ return coefficient
70
68
 
71
69
 
72
70
  def check_tasks_parallel(config, verbose=False):
@@ -122,8 +120,8 @@ def initialize_task_info(tasks, time_maps, days_ahead, urgency_coefficients, cal
122
120
 
123
121
  def allocate_time_for_day(task_info, day_offset, date, urgency_coefficients, verbose):
124
122
  total_available_hours = compute_total_available_hours(task_info, day_offset)
125
- if verbose:
126
- print(f"Day {date}, total available hours: {total_available_hours:.2f}")
123
+ # if verbose:
124
+ # print(f"Day {date}, total available hours: {total_available_hours:.2f}")
127
125
  if total_available_hours <= 0:
128
126
  return
129
127
 
@@ -134,7 +132,7 @@ def allocate_time_for_day(task_info, day_offset, date, urgency_coefficients, ver
134
132
  recompute_urgencies(tasks_remaining, urgency_coefficients)
135
133
  sorted_task_ids = sorted(
136
134
  tasks_remaining.keys(),
137
- key=lambda x: (tasks_remaining[x]["urgency"], x),
135
+ key=lambda x: tasks_remaining[x]["urgency"],
138
136
  reverse=True,
139
137
  )
140
138
 
@@ -146,6 +144,17 @@ def allocate_time_for_day(task_info, day_offset, date, urgency_coefficients, ver
146
144
  info = tasks_remaining[uuid]
147
145
  if any(d in tasks_remaining for d in info["task"].get("depends", [])):
148
146
  # cannot execute this task until all its dependencies are completed
147
+ if verbose:
148
+ print(
149
+ "Skipping task",
150
+ info["task"]["id"],
151
+ "due to dependency on:",
152
+ [
153
+ tasks_remaining[_d]["task"]["id"]
154
+ for _d in info["task"].get("depends", [])
155
+ if _d in tasks_remaining
156
+ ],
157
+ )
149
158
  continue
150
159
  allocation = allocate_time_to_task(info, day_offset, day_remaining_hours)
151
160
  if allocation > 0:
@@ -155,7 +164,7 @@ def allocate_time_for_day(task_info, day_offset, date, urgency_coefficients, ver
155
164
  update_task_scheduling(info, allocation, date_str)
156
165
  if verbose:
157
166
  print(
158
- f"Allocated {allocation:.2f} hours to task {info['task']['id']} on {date}"
167
+ f"Allocated {allocation:.2f} hours to task {info['task']['id']} on {date} with urgency {info['urgency']:.2f} and estimated-related urgency {info['estimated_urgency']:.2f}"
159
168
  )
160
169
  if (
161
170
  info["remaining_hours"] <= 0
@@ -166,8 +175,8 @@ def allocate_time_for_day(task_info, day_offset, date, urgency_coefficients, ver
166
175
  break
167
176
  if not allocated:
168
177
  break
169
- if verbose and day_remaining_hours > 0:
170
- print(f"Unused time on {date}: {day_remaining_hours:.2f} hours")
178
+ # if verbose and day_remaining_hours > 0:
179
+ # print(f"Unused time on {date}: {day_remaining_hours:.2f} hours")
171
180
 
172
181
 
173
182
  def compute_total_available_hours(task_info, day_offset):
@@ -1,3 +1,4 @@
1
+ import random
1
2
  import sys
2
3
  from datetime import timedelta
3
4
  from datetime import datetime
@@ -12,6 +13,170 @@ from rich.table import Table
12
13
  from rich.text import Text
13
14
  from rich.panel import Panel
14
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
+
15
180
 
16
181
  def get_tasks(config, tasks, year, month, day):
17
182
  regex = re.compile(rf"{year}-{month}-{day} - ([PDTHM0-9]+)")
@@ -172,7 +337,21 @@ def get_task_emoji(config, task):
172
337
  for keyword in config.get("emoji_keywords", []):
173
338
  if keyword in task["description"].lower():
174
339
  return config["emoji_keywords"][keyword]
175
- return ":pushpin:"
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
176
355
 
177
356
 
178
357
  def display_unplanned_tasks(console, config, tasks):
@@ -197,6 +376,8 @@ def build_unplanned_tasks_table(config, tasks):
197
376
  table.add_column("Task", style="dim", width=12)
198
377
  table.add_column("Project", style="dim", width=12)
199
378
  table.add_column("Description")
379
+ for attr in config.get("additional_attributes_unplanned", []):
380
+ table.add_column(attr.capitalize(), justify="right")
200
381
 
201
382
  for task in tasks:
202
383
  task_id = f"[bold green]#{task['id']}[/bold green]"
@@ -208,5 +389,9 @@ def build_unplanned_tasks_table(config, tasks):
208
389
  f"{emoji} {task_id}",
209
390
  project,
210
391
  description,
392
+ *[
393
+ tostring(task.get(attr, ""))
394
+ for attr in config.get("additional_attributes_unplanned", [])
395
+ ],
211
396
  )
212
397
  return table
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: taskcheck
3
- Version: 0.3.1
3
+ Version: 0.3.2.post1
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
 
@@ -75,29 +76,9 @@ instance:
75
76
  - `taskcheck -r today` will show the tasks planned for today
76
77
  - `taskcheck -r 1w` will show the tasks planned for the next week,
77
78
 
78
- ## Algorithms
79
+ ## Algorithm
79
80
 
80
- Taskcheck provides two scheduling algorithms: `parallel` and `sequential`.
81
-
82
- #### Sequential
83
-
84
- The `sequential` algorithm schedules tasks one after the other. It is the simplest algorithm.
85
- It looks at your tasks one by one, computes the time slots available for that task, and fills them
86
- as soon as possible.
87
-
88
- While it sounds logical, it has a problem.
89
- While you're working on a task, its remaining estimated time is decreasing, and if your urgency
90
- depends on the estimated time left, the urgency of the task will decrease as well.
91
- This is especially problematic for long tasks: imagine if you block your whole week for a single
92
- task and all the other small tasks, even if urgent, don't get scheduled. After a few days, the
93
- urgency of the first long task should have decreased.
94
-
95
- #### Parallel
96
-
97
- The `parallel` algorithm tries to solve the problem of the `sequential` algorithm.
98
-
99
- Instead of allocating all the available time for a task until the task is finished, the `parallel`
100
- 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
101
82
  urgency and restart the allocation procedure.
102
83
 
103
84
  If after 1 hour the long task has decreased urgency, that will be noticed and the newer most urgent
@@ -106,8 +87,6 @@ task will get scheduled in its place.
106
87
  The minimum time for a task is by default 2 hours, but you can change it by tuning the `min_block`
107
88
  taskwarrior UDA.
108
89
 
109
- The `parallel` algorithm is the default algorithm.
110
-
111
90
  ## Configuration
112
91
 
113
92
  `taskcheck --install` allows you to create required and recommended configurations for
@@ -131,7 +110,6 @@ saturday = [[9, 12.30], ]
131
110
  sunday = [[9, 12.30], ]
132
111
 
133
112
  [scheduler]
134
- algorithm = "parallel" # or "sequential", see above
135
113
  days_ahead = 1000 # how far go with the schedule (lower values make the computation faster)
136
114
 
137
115
  [calendars]
@@ -145,6 +123,13 @@ timezone = "Europe/Rome" # if set, force timezone for this calendar; timezone va
145
123
  url = "https://www.officeholidays.com/ics-clean/italy/milan"
146
124
  event_all_day_is_blocking = true
147
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
148
133
  ```
149
134
 
150
135
  ## Tips and Tricks
@@ -7,7 +7,6 @@ taskcheck/ical.py
7
7
  taskcheck/install.py
8
8
  taskcheck/parallel.py
9
9
  taskcheck/report.py
10
- taskcheck/sequential.py
11
10
  taskcheck.egg-info/PKG-INFO
12
11
  taskcheck.egg-info/SOURCES.txt
13
12
  taskcheck.egg-info/dependency_links.txt
@@ -1,4 +1,5 @@
1
1
  appdirs>=1.4.4
2
2
  icalendar>=6.0.1
3
+ random-unicode-emoji>=2.8
3
4
  requests>=2.32.3
4
5
  rich>=13.9.4
@@ -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