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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: taskcheck
3
- Version: 0.2.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/Microsoft/Apple API to access calendars
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
- You will need to add the `estimated` and `time_map` UDAs to your tasks. The `estimated` attribute is
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
- `taskcheck` will modify the Taskwarrior tasks by adding the `completion_date` attribute with the expected
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
- You can exclude a task from being scheduled by removing the `time_map` or `estimated` attributes.
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
- You can see tasks that you can execute now with the `task ready` report.
65
+ #### Reports
49
66
 
50
- You can see the schedule for a task in the `scheduling` UDA, e.g. `task scheduling:$(task calc
51
- tomorrow)` will show you the tasks that include a scheduling for tomorrow.
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
- In general, it is recommended to run taskcheck rather frequently and at least once at the beginning
54
- of your working day.
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
- ### Algorithms
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/Microsoft/Apple API to access calendars
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
- You will need to add the `estimated` and `time_map` UDAs to your tasks. The `estimated` attribute is
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
- `taskcheck` will modify the Taskwarrior tasks by adding the `completion_date` attribute with the expected
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
- You can exclude a task from being scheduled by removing the `time_map` or `estimated` attributes.
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
- You can see tasks that you can execute now with the `task ready` report.
53
+ #### Reports
38
54
 
39
- You can see the schedule for a task in the `scheduling` UDA, e.g. `task scheduling:$(task calc
40
- tomorrow)` will show you the tasks that include a scheduling for tomorrow.
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
- In general, it is recommended to run taskcheck rather frequently and at least once at the beginning
43
- of your working day.
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
- ### Algorithms
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.2.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 = ["appdirs>=1.4.4", "icalendar>=6.0.1", "requests>=2.32.3"]
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
- return days * 24 + hours
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
- days = hours // 24
105
- hours = hours % 24
106
- return f"P{days}DT{hours}H"
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
- """Required configuration for taskcheck includes:
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
- """Optional configuration for taskcheck includes:
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
- """Do you want to create a default config file for taskcheck? [y/N]"""
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
- return coefficients, inherit_urgency
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, coefficients):
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
- coefficients.keys(),
49
- key=lambda x: abs(int(pdth_value[1]) - int(pdth_to_hours(x[1]))),
62
+ urgency_coefficients.estimated.keys(),
63
+ key=lambda x: abs(pdth_to_hours(x) - remaining_hours),
50
64
  )
51
- coefficient = coefficients[closest_match]
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, inherit_urgency = get_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, inherit_urgency, verbose
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, inherit_urgency)
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 id in sorted_task_ids:
129
- if id not in tasks_remaining:
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[id]
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"]["id"]: info
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, inherit_urgency):
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 inherit_urgency:
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
- task_id = info["task"]["id"]
192
- if task_id in visited:
193
- return visited[task_id] # Return cached value to avoid cycles
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[task_id] = urgency # Mark as visited
218
+ visited[task_uuid] = urgency # Mark as visited
197
219
  # Recursively compute urgencies of dependents
198
- for dep_id in info["task"].get("depends", []):
199
- if dep_id in tasks_remaining:
200
- dep_info = tasks_remaining[dep_id]
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[task_id] = urgency # Update with the maximum urgency found
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}T00:00:00 - {hours:.2f} hours\n"
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}: {task_remaining_hours:.2f} hours"
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}T00:00:00 - {employable_hours:.2f} hours"
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.2.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/Microsoft/Apple API to access calendars
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
- You will need to add the `estimated` and `time_map` UDAs to your tasks. The `estimated` attribute is
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
- `taskcheck` will modify the Taskwarrior tasks by adding the `completion_date` attribute with the expected
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
- You can exclude a task from being scheduled by removing the `time_map` or `estimated` attributes.
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
- You can see tasks that you can execute now with the `task ready` report.
65
+ #### Reports
49
66
 
50
- You can see the schedule for a task in the `scheduling` UDA, e.g. `task scheduling:$(task calc
51
- tomorrow)` will show you the tasks that include a scheduling for tomorrow.
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
- In general, it is recommended to run taskcheck rather frequently and at least once at the beginning
54
- of your working day.
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
- ### Algorithms
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
  ```
@@ -6,6 +6,7 @@ taskcheck/common.py
6
6
  taskcheck/ical.py
7
7
  taskcheck/install.py
8
8
  taskcheck/parallel.py
9
+ taskcheck/report.py
9
10
  taskcheck/sequential.py
10
11
  taskcheck.egg-info/PKG-INFO
11
12
  taskcheck.egg-info/SOURCES.txt
@@ -1,3 +1,4 @@
1
1
  appdirs>=1.4.4
2
2
  icalendar>=6.0.1
3
3
  requests>=2.32.3
4
+ rich>=13.9.4
File without changes
File without changes
File without changes