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.
- {taskcheck-0.3.2.post1/taskcheck.egg-info → taskcheck-0.3.3}/PKG-INFO +5 -1
- {taskcheck-0.3.2.post1 → taskcheck-0.3.3}/README.md +4 -0
- {taskcheck-0.3.2.post1 → taskcheck-0.3.3}/pyproject.toml +1 -1
- {taskcheck-0.3.2.post1 → taskcheck-0.3.3}/taskcheck/parallel.py +123 -34
- {taskcheck-0.3.2.post1 → taskcheck-0.3.3/taskcheck.egg-info}/PKG-INFO +5 -1
- {taskcheck-0.3.2.post1 → taskcheck-0.3.3}/LICENSE +0 -0
- {taskcheck-0.3.2.post1 → taskcheck-0.3.3}/setup.cfg +0 -0
- {taskcheck-0.3.2.post1 → taskcheck-0.3.3}/taskcheck/__main__.py +0 -0
- {taskcheck-0.3.2.post1 → taskcheck-0.3.3}/taskcheck/common.py +0 -0
- {taskcheck-0.3.2.post1 → taskcheck-0.3.3}/taskcheck/ical.py +0 -0
- {taskcheck-0.3.2.post1 → taskcheck-0.3.3}/taskcheck/install.py +0 -0
- {taskcheck-0.3.2.post1 → taskcheck-0.3.3}/taskcheck/report.py +0 -0
- {taskcheck-0.3.2.post1 → taskcheck-0.3.3}/taskcheck.egg-info/SOURCES.txt +0 -0
- {taskcheck-0.3.2.post1 → taskcheck-0.3.3}/taskcheck.egg-info/dependency_links.txt +0 -0
- {taskcheck-0.3.2.post1 → taskcheck-0.3.3}/taskcheck.egg-info/entry_points.txt +0 -0
- {taskcheck-0.3.2.post1 → taskcheck-0.3.3}/taskcheck.egg-info/requires.txt +0 -0
- {taskcheck-0.3.2.post1 → taskcheck-0.3.3}/taskcheck.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: taskcheck
|
|
3
|
-
Version: 0.3.
|
|
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
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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 =
|
|
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,
|
|
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
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
284
|
+
# Update estimated urgency
|
|
285
|
+
update_urgency(
|
|
286
|
+
info, "estimated_urgency", urgency_estimated, urgency_coefficients, date
|
|
210
287
|
)
|
|
211
|
-
|
|
212
|
-
info
|
|
213
|
-
|
|
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
|
|
230
|
-
for dep_uuid in
|
|
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
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|