taskcheck 0.3.2.post1__tar.gz → 0.3.3__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.3.2.post1
3
+ Version: 0.3.3
4
4
  Summary: Automatic scheduler for taskwarrior
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -144,3 +144,7 @@ additional_attributes_unplanned = ["due", "urgency"] # additional attributes to
144
144
  -i, --install: install taskcheck configuration
145
145
  -r, --report: show tasks planned until a certain time
146
146
  ```
147
+
148
+ ## TODO
149
+
150
+ - [ ] Consider due dates are nearer while planning in the future
@@ -131,3 +131,7 @@ additional_attributes_unplanned = ["due", "urgency"] # additional attributes to
131
131
  -i, --install: install taskcheck configuration
132
132
  -r, --report: show tasks planned until a certain time
133
133
  ```
134
+
135
+ ## TODO
136
+
137
+ - [ ] Consider due dates are nearer while planning in the future
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "taskcheck"
3
- version = "0.3.2-1"
3
+ version = "0.3.3"
4
4
  description = "Automatic scheduler for taskwarrior"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -20,6 +20,9 @@ class UrgencyCoefficients:
20
20
  estimated: dict
21
21
  inherit: bool
22
22
  active: float
23
+ age_max: int
24
+ urgency_due: int
25
+ urgency_age: int
23
26
 
24
27
 
25
28
  def get_urgency_coefficients():
@@ -31,16 +34,22 @@ def get_urgency_coefficients():
31
34
  result = subprocess.run(["task", "_show"], capture_output=True, text=True)
32
35
  inherit_urgency = False
33
36
  active_task_coefficient = 0
34
- coefficients = {}
37
+ est_coeffs = {}
38
+ urgency_age_max = 0
39
+ urgency_age = 0
40
+ urgency_due = 0
35
41
  pattern1 = re.compile(r"^urgency\.uda\.estimated\.(.+)\.coefficient=(.+)$")
36
42
  pattern2 = re.compile(r"^urgency\.inherit=(.+)$")
37
- pattern3 = re.compile(r"^urgency\.active.coefficient=(.+)$")
43
+ pattern3 = re.compile(r"^urgency\.active\.coefficient=(.+)$")
44
+ pattern4 = re.compile(r"^urgency\.age\.max=(.+)$")
45
+ pattern5 = re.compile(r"^urgency\.due\.coefficient=(.+)$")
46
+ pattern6 = re.compile(r"^urgency\.age\.coefficient=(.+)$")
38
47
  for line in result.stdout.splitlines():
39
48
  match = pattern1.match(line)
40
49
  if match:
41
50
  estimated_value = match.group(1)
42
51
  coefficient = float(match.group(2))
43
- coefficients[estimated_value] = coefficient
52
+ est_coeffs[estimated_value] = coefficient
44
53
 
45
54
  match = pattern2.match(line)
46
55
  if match:
@@ -51,20 +60,26 @@ def get_urgency_coefficients():
51
60
  active_coefficient = float(match.group(1))
52
61
  active_task_coefficient = active_coefficient
53
62
 
54
- return UrgencyCoefficients(coefficients, inherit_urgency, active_task_coefficient)
63
+ match = pattern4.match(line)
64
+ if match:
65
+ urgency_age_max = int(match.group(1))
55
66
 
67
+ match = pattern5.match(line)
68
+ if match:
69
+ urgency_due = int(match.group(1))
56
70
 
57
- def compute_estimated_urgency(remaining_hours, urgency_coefficients):
58
- """
59
- Computes the estimated urgency for the given remaining hours using the coefficients.
60
- """
61
- # Find the closest match (e.g., if '2h' is not available, use '1h' or '3h')
62
- closest_match = min(
63
- urgency_coefficients.estimated.keys(),
64
- key=lambda x: abs(pdth_to_hours(x) - remaining_hours),
71
+ match = pattern6.match(line)
72
+ if match:
73
+ urgency_age = int(match.group(1))
74
+
75
+ return UrgencyCoefficients(
76
+ est_coeffs,
77
+ inherit_urgency,
78
+ active_task_coefficient,
79
+ urgency_age_max,
80
+ urgency_due,
81
+ urgency_age,
65
82
  )
66
- coefficient = urgency_coefficients.estimated[closest_match]
67
- return coefficient
68
83
 
69
84
 
70
85
  def check_tasks_parallel(config, verbose=False):
@@ -72,7 +87,6 @@ def check_tasks_parallel(config, verbose=False):
72
87
  time_maps = config["time_maps"]
73
88
  days_ahead = config["scheduler"]["days_ahead"]
74
89
  calendars = get_calendars(config)
75
- today = datetime.today().date()
76
90
  urgency_coefficients = get_urgency_coefficients()
77
91
 
78
92
  task_info = initialize_task_info(
@@ -80,16 +94,14 @@ def check_tasks_parallel(config, verbose=False):
80
94
  )
81
95
 
82
96
  for day_offset in range(days_ahead):
83
- date = today + timedelta(days=day_offset)
84
- allocate_time_for_day(
85
- task_info, day_offset, date, urgency_coefficients, verbose
86
- )
97
+ allocate_time_for_day(task_info, day_offset, urgency_coefficients, verbose)
87
98
 
88
99
  update_tasks_with_scheduling_info(task_info, verbose)
89
100
 
90
101
 
91
102
  def initialize_task_info(tasks, time_maps, days_ahead, urgency_coefficients, calendars):
92
103
  task_info = {}
104
+ today = datetime.today().date()
93
105
  for task in tasks:
94
106
  if task.get("status") in AVOID_STATUS:
95
107
  continue
@@ -102,9 +114,11 @@ def initialize_task_info(tasks, time_maps, days_ahead, urgency_coefficients, cal
102
114
  )
103
115
  task_uuid = task["uuid"]
104
116
  initial_urgency = float(task.get("urgency", 0))
105
- estimated_urgency = compute_estimated_urgency(
106
- estimated_hours, urgency_coefficients
117
+ estimated_urgency = urgency_estimated(
118
+ {"remaining_hours": estimated_hours}, None, urgency_coefficients
107
119
  )
120
+ due_urgency = urgency_due({"task": task}, today, urgency_coefficients)
121
+ age_urgency = urgency_age({"task": task}, today, urgency_coefficients)
108
122
  task_info[task_uuid] = {
109
123
  "task": task,
110
124
  "remaining_hours": estimated_hours,
@@ -113,12 +127,15 @@ def initialize_task_info(tasks, time_maps, days_ahead, urgency_coefficients, cal
113
127
  "scheduling": {},
114
128
  "urgency": initial_urgency,
115
129
  "estimated_urgency": estimated_urgency,
130
+ "due_urgency": due_urgency,
131
+ "age_urgency": age_urgency,
116
132
  "started": False,
117
133
  }
118
134
  return task_info
119
135
 
120
136
 
121
- def allocate_time_for_day(task_info, day_offset, date, urgency_coefficients, verbose):
137
+ def allocate_time_for_day(task_info, day_offset, urgency_coefficients, verbose):
138
+ date = datetime.today().date() + timedelta(days=day_offset)
122
139
  total_available_hours = compute_total_available_hours(task_info, day_offset)
123
140
  # if verbose:
124
141
  # print(f"Day {date}, total available hours: {total_available_hours:.2f}")
@@ -129,7 +146,7 @@ def allocate_time_for_day(task_info, day_offset, date, urgency_coefficients, ver
129
146
  tasks_remaining = prepare_tasks_remaining(task_info, day_offset)
130
147
 
131
148
  while day_remaining_hours > 0 and tasks_remaining:
132
- recompute_urgencies(tasks_remaining, urgency_coefficients)
149
+ recompute_urgencies(tasks_remaining, urgency_coefficients, date)
133
150
  sorted_task_ids = sorted(
134
151
  tasks_remaining.keys(),
135
152
  key=lambda x: tasks_remaining[x]["urgency"],
@@ -156,6 +173,12 @@ def allocate_time_for_day(task_info, day_offset, date, urgency_coefficients, ver
156
173
  ],
157
174
  )
158
175
  continue
176
+
177
+ wait = info["task"].get("wait")
178
+ if wait and date <= datetime.strptime(wait, "%Y%m%dT%H%M%SZ").date():
179
+ if verbose:
180
+ print(f"Skipping task {info['task']['id']} due to wait date {wait}")
181
+ continue
159
182
  allocation = allocate_time_to_task(info, day_offset, day_remaining_hours)
160
183
  if allocation > 0:
161
184
  day_remaining_hours -= allocation
@@ -201,23 +224,88 @@ def prepare_tasks_remaining(task_info, day_offset):
201
224
  }
202
225
 
203
226
 
204
- def recompute_urgencies(tasks_remaining, urgency_coefficients):
227
+ def urgency_due(info, date, urgency_coefficients):
228
+ lfs = 0
229
+ task = info["task"]
230
+ if "due" in task:
231
+ due = datetime.strptime(task["due"], "%Y%m%dT%H%M%SZ").date()
232
+ # Map a range of 21 days to the value 0.2 - 1.0
233
+ days_overdue = (date - due).days
234
+ if days_overdue >= 7.0:
235
+ lfs = 1.0 # < 1 wk ago
236
+ elif days_overdue >= -14.0:
237
+ lfs = ((days_overdue + 14.0) * 0.8 / 21.0) + 0.2
238
+ else:
239
+ lfs = 0.2 # 2 wks
240
+ return lfs * urgency_coefficients.urgency_due
241
+
242
+
243
+ def urgency_age(info, date, urgency_coefficients):
244
+ urgency_age_max = urgency_coefficients.age_max
245
+ lfs = 1.0
246
+ task = info["task"]
247
+ if "entry" not in task:
248
+ return 1.0
249
+ entry = datetime.strptime(task["entry"], "%Y%m%dT%H%M%SZ").date()
250
+ age = (date - entry).days # in days
251
+ if urgency_age_max == 0 or age >= urgency_age_max:
252
+ lfs = 1.0
253
+ return lfs * age / urgency_age_max * urgency_coefficients.urgency_age
254
+
255
+
256
+ def urgency_estimated(info, date, urgency_coefficients):
257
+ """
258
+ Computes the estimated urgency for the given remaining hours using the coefficients.
259
+ """
260
+ # Find the closest match (e.g., if '2h' is not available, use '1h' or '3h')
261
+ closest_match = min(
262
+ urgency_coefficients.estimated.keys(),
263
+ key=lambda x: abs(pdth_to_hours(x) - info["remaining_hours"]),
264
+ )
265
+ coefficient = urgency_coefficients.estimated[closest_match]
266
+ return coefficient
267
+
268
+
269
+ def update_urgency(info, urgency_key, urgency_compute_fn, urgency_coefficients, date):
270
+ urgency_value = urgency_compute_fn(info, date, urgency_coefficients)
271
+ old_urgency = info[urgency_key]
272
+ if old_urgency != urgency_value:
273
+ print(
274
+ f"new urgency value for task {info['task']['id']}: {urgency_key}: {urgency_value}"
275
+ )
276
+ info[urgency_key] = urgency_value
277
+ info["urgency"] = info["urgency"] - old_urgency + urgency_value
278
+
279
+
280
+ def recompute_urgencies(tasks_remaining, urgency_coefficients, date):
281
+ """Recompute urgency simulating that today is `date`"""
205
282
  # Recompute estimated urgencies as before
206
283
  for info in tasks_remaining.values():
207
- remaining_hours = info["remaining_hours"]
208
- estimated_urgency = compute_estimated_urgency(
209
- remaining_hours, urgency_coefficients
284
+ # Update estimated urgency
285
+ update_urgency(
286
+ info, "estimated_urgency", urgency_estimated, urgency_coefficients, date
210
287
  )
211
- _old_estimated_urgency = info["estimated_urgency"]
212
- info["estimated_urgency"] = estimated_urgency
213
- info["urgency"] = info["urgency"] - _old_estimated_urgency + estimated_urgency
288
+ # Update due partial urgency
289
+ update_urgency(info, "due_urgency", urgency_due, urgency_coefficients, date)
290
+ # Update age partial urgency
291
+ update_urgency(info, "age_urgency", urgency_age, urgency_coefficients, date)
292
+
214
293
  started_by_user = info["task"].get("start", "") != ""
215
294
  started_by_scheduler = info["started"]
216
295
  if started_by_scheduler and not started_by_user:
217
296
  # If the task was started by the scheduler, apply the active task coefficient
218
297
  info["urgency"] += urgency_coefficients.active
298
+ info["started"] = False
219
299
 
220
300
  if urgency_coefficients.inherit:
301
+ # Build reverse dependencies mapping
302
+ reverse_deps = {} # Map from task_uuid to list of tasks that depend on it
303
+ for task_uuid, info in tasks_remaining.items():
304
+ for dep_uuid in info["task"].get("depends", []):
305
+ if dep_uuid not in reverse_deps:
306
+ reverse_deps[dep_uuid] = []
307
+ reverse_deps[dep_uuid].append(task_uuid)
308
+
221
309
  # Define a recursive function to compute the maximum urgency
222
310
  def get_max_urgency(info, visited):
223
311
  task_uuid = info["task"]["uuid"]
@@ -226,8 +314,8 @@ def recompute_urgencies(tasks_remaining, urgency_coefficients):
226
314
  # Start with the current task's urgency
227
315
  urgency = info["urgency"]
228
316
  visited[task_uuid] = urgency # Mark as visited
229
- # Recursively compute urgencies of dependents
230
- for dep_uuid in info["task"].get("depends", []):
317
+ # Recursively compute urgencies of tasks that depend on this task
318
+ for dep_uuid in reverse_deps.get(task_uuid, []):
231
319
  if dep_uuid in tasks_remaining:
232
320
  dep_info = tasks_remaining[dep_uuid]
233
321
  dep_urgency = get_max_urgency(dep_info, visited)
@@ -235,7 +323,7 @@ def recompute_urgencies(tasks_remaining, urgency_coefficients):
235
323
  visited[task_uuid] = urgency # Update with the maximum urgency found
236
324
  return urgency
237
325
 
238
- # Update urgencies based on dependents
326
+ # Update urgencies based on tasks that depend on them
239
327
  for info in tasks_remaining.values():
240
328
  visited = {} # Reset visited dictionary for each task
241
329
  max_urgency = get_max_urgency(info, visited)
@@ -257,9 +345,10 @@ def allocate_time_to_task(info, day_offset, day_remaining_hours):
257
345
  if allocation <= 0.05:
258
346
  return 0
259
347
 
348
+ if info["remaining_hours"] == info["task"]["estimated"]:
349
+ info["started"] = True
260
350
  info["remaining_hours"] -= allocation
261
351
  info["task_time_map"][day_offset] -= allocation
262
- info["started"] = True
263
352
 
264
353
  return allocation
265
354
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: taskcheck
3
- Version: 0.3.2.post1
3
+ Version: 0.3.3
4
4
  Summary: Automatic scheduler for taskwarrior
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -144,3 +144,7 @@ additional_attributes_unplanned = ["due", "urgency"] # additional attributes to
144
144
  -i, --install: install taskcheck configuration
145
145
  -r, --report: show tasks planned until a certain time
146
146
  ```
147
+
148
+ ## TODO
149
+
150
+ - [ ] Consider due dates are nearer while planning in the future
File without changes
File without changes