taskcheck 1.6.0__tar.gz → 1.6.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-1.6.0 → taskcheck-1.6.2}/PKG-INFO +11 -2
- taskcheck-1.6.0/taskcheck.egg-info/PKG-INFO → taskcheck-1.6.2/README.md +9 -15
- {taskcheck-1.6.0 → taskcheck-1.6.2}/pyproject.toml +2 -2
- {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck/agent_blocks.py +1 -1
- {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck/parallel.py +17 -3
- {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck/report.py +5 -0
- {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck/timeline.py +5 -1
- taskcheck-1.6.0/README.md → taskcheck-1.6.2/taskcheck.egg-info/PKG-INFO +24 -0
- {taskcheck-1.6.0 → taskcheck-1.6.2}/tests/test_parallel.py +39 -0
- {taskcheck-1.6.0 → taskcheck-1.6.2}/tests/test_report.py +9 -0
- {taskcheck-1.6.0 → taskcheck-1.6.2}/tests/test_timeline.py +8 -0
- {taskcheck-1.6.0 → taskcheck-1.6.2}/LICENSE +0 -0
- {taskcheck-1.6.0 → taskcheck-1.6.2}/setup.cfg +0 -0
- {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck/__main__.py +0 -0
- {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck/common.py +0 -0
- {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck/dry_run.py +0 -0
- {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck/google_calendar.py +0 -0
- {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck/ical.py +0 -0
- {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck/install.py +0 -0
- {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck/theme.py +0 -0
- {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck/undo.py +0 -0
- {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck.egg-info/SOURCES.txt +0 -0
- {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck.egg-info/dependency_links.txt +0 -0
- {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck.egg-info/entry_points.txt +0 -0
- {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck.egg-info/requires.txt +0 -0
- {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck.egg-info/top_level.txt +0 -0
- {taskcheck-1.6.0 → taskcheck-1.6.2}/tests/test_agent_blocks.py +0 -0
- {taskcheck-1.6.0 → taskcheck-1.6.2}/tests/test_agent_scheduler_integration.py +0 -0
- {taskcheck-1.6.0 → taskcheck-1.6.2}/tests/test_common.py +0 -0
- {taskcheck-1.6.0 → taskcheck-1.6.2}/tests/test_google_calendar.py +0 -0
- {taskcheck-1.6.0 → taskcheck-1.6.2}/tests/test_ical.py +0 -0
- {taskcheck-1.6.0 → taskcheck-1.6.2}/tests/test_install.py +0 -0
- {taskcheck-1.6.0 → taskcheck-1.6.2}/tests/test_main.py +0 -0
- {taskcheck-1.6.0 → taskcheck-1.6.2}/tests/test_undo.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: taskcheck
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.2
|
|
4
4
|
Summary: Automatic scheduler for taskwarrior
|
|
5
|
-
Requires-Python: >=3.
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
7
|
Requires-Dist: appdirs>=1.4.4
|
|
8
8
|
Requires-Dist: google-api-python-client>=2.198.0
|
|
@@ -60,6 +60,11 @@ It updates Taskwarrior with:
|
|
|
60
60
|
- `completion_date` → when work is expected to finish
|
|
61
61
|
- a warning when a task will miss its due date
|
|
62
62
|
|
|
63
|
+
Taskcheck does not allocate work before a task's `wait` date.
|
|
64
|
+
It can allocate work on that date because it plans whole days, not exact start times.
|
|
65
|
+
Reports and timelines ignore saved work dates before `wait`, even when you change `wait` after scheduling.
|
|
66
|
+
If no work fits within the planning period, the next scheduling run clears the task's old schedule fields.
|
|
67
|
+
|
|
63
68
|
## Quick start
|
|
64
69
|
|
|
65
70
|
1. `pipx install taskcheck`
|
|
@@ -143,6 +148,10 @@ Before writing a schedule, taskcheck backs up the affected tasks' `scheduled`, `
|
|
|
143
148
|
|
|
144
149
|
If a task would finish after its due date, taskcheck retries with a total-urgency override. Each retry moves the task immediately above the task with the smallest greater urgency by adding `urgency_epsilon` (default: `0.1`). At rank 1, it adds epsilon for up to `max_top_urgency_rounds` retries (default: `10`), then tries once at urgency `1000`. If the task is still late, taskcheck stops raising it and optimizes the other overdue tasks.
|
|
145
150
|
|
|
151
|
+
Each task gets at most `N - 1 + max_top_urgency_rounds` priority increases before the final retry at urgency `1000`.
|
|
152
|
+
Here, `N` is the number of tasks in the schedule.
|
|
153
|
+
This limit stops repeated retries when tasks with competing deadlines keep overtaking each other.
|
|
154
|
+
|
|
146
155
|
## Tips and Tricks
|
|
147
156
|
|
|
148
157
|
- You can exclude a task from being scheduled by removing the `time_map` or `estimated` attributes.
|
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: taskcheck
|
|
3
|
-
Version: 1.6.0
|
|
4
|
-
Summary: Automatic scheduler for taskwarrior
|
|
5
|
-
Requires-Python: >=3.11
|
|
6
|
-
Description-Content-Type: text/markdown
|
|
7
|
-
Requires-Dist: appdirs>=1.4.4
|
|
8
|
-
Requires-Dist: google-api-python-client>=2.198.0
|
|
9
|
-
Requires-Dist: google-auth>=2.56.2
|
|
10
|
-
Requires-Dist: google-auth-oauthlib>=1.2.2
|
|
11
|
-
Requires-Dist: icalendar>=6.0.1
|
|
12
|
-
Requires-Dist: random-unicode-emoji>=2.8
|
|
13
|
-
Requires-Dist: requests>=2.32.3
|
|
14
|
-
Requires-Dist: rich>=13.9.4
|
|
15
|
-
|
|
16
1
|
A Taskwarrior scheduler for people who want a realistic plan, not a manual to-do list.
|
|
17
2
|
It turns tasks, working hours, and calendar blocks into an actionable schedule — then keeps due dates visible.
|
|
18
3
|
|
|
@@ -60,6 +45,11 @@ It updates Taskwarrior with:
|
|
|
60
45
|
- `completion_date` → when work is expected to finish
|
|
61
46
|
- a warning when a task will miss its due date
|
|
62
47
|
|
|
48
|
+
Taskcheck does not allocate work before a task's `wait` date.
|
|
49
|
+
It can allocate work on that date because it plans whole days, not exact start times.
|
|
50
|
+
Reports and timelines ignore saved work dates before `wait`, even when you change `wait` after scheduling.
|
|
51
|
+
If no work fits within the planning period, the next scheduling run clears the task's old schedule fields.
|
|
52
|
+
|
|
63
53
|
## Quick start
|
|
64
54
|
|
|
65
55
|
1. `pipx install taskcheck`
|
|
@@ -143,6 +133,10 @@ Before writing a schedule, taskcheck backs up the affected tasks' `scheduled`, `
|
|
|
143
133
|
|
|
144
134
|
If a task would finish after its due date, taskcheck retries with a total-urgency override. Each retry moves the task immediately above the task with the smallest greater urgency by adding `urgency_epsilon` (default: `0.1`). At rank 1, it adds epsilon for up to `max_top_urgency_rounds` retries (default: `10`), then tries once at urgency `1000`. If the task is still late, taskcheck stops raising it and optimizes the other overdue tasks.
|
|
145
135
|
|
|
136
|
+
Each task gets at most `N - 1 + max_top_urgency_rounds` priority increases before the final retry at urgency `1000`.
|
|
137
|
+
Here, `N` is the number of tasks in the schedule.
|
|
138
|
+
This limit stops repeated retries when tasks with competing deadlines keep overtaking each other.
|
|
139
|
+
|
|
146
140
|
## Tips and Tricks
|
|
147
141
|
|
|
148
142
|
- You can exclude a task from being scheduled by removing the `time_map` or `estimated` attributes.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "taskcheck"
|
|
3
|
-
version = "1.6.
|
|
3
|
+
version = "1.6.2"
|
|
4
4
|
description = "Automatic scheduler for taskwarrior"
|
|
5
5
|
readme = "README.md"
|
|
6
|
-
requires-python = ">=3.
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
7
|
dependencies = [
|
|
8
8
|
"appdirs>=1.4.4",
|
|
9
9
|
"google-api-python-client>=2.198.0",
|
|
@@ -39,7 +39,7 @@ def _eligible(info, pending, date):
|
|
|
39
39
|
if any(dep in pending for dep in info["task"].get("depends", [])):
|
|
40
40
|
return False
|
|
41
41
|
wait = info["task"].get("wait")
|
|
42
|
-
if wait and date
|
|
42
|
+
if wait and date < datetime.strptime(wait, "%Y%m%dT%H%M%SZ").date():
|
|
43
43
|
return False
|
|
44
44
|
return True
|
|
45
45
|
|
|
@@ -58,7 +58,7 @@ def advance_urgency_override(
|
|
|
58
58
|
for task_uuid in overdue_uuids:
|
|
59
59
|
state = adjustment_states.setdefault(
|
|
60
60
|
task_uuid,
|
|
61
|
-
{"top_rounds": 0, "fallback_used": False, "exhausted": False},
|
|
61
|
+
{"top_rounds": 0, "rounds": 0, "fallback_used": False, "exhausted": False},
|
|
62
62
|
)
|
|
63
63
|
if state["exhausted"]:
|
|
64
64
|
continue
|
|
@@ -72,7 +72,10 @@ def advance_urgency_override(
|
|
|
72
72
|
for uuid in task_info
|
|
73
73
|
if uuid != task_uuid and effective_urgency(uuid) > current_urgency
|
|
74
74
|
]
|
|
75
|
-
if
|
|
75
|
+
if state["rounds"] >= len(task_info) - 1 + max_top_rounds:
|
|
76
|
+
urgency_overrides[task_uuid] = 1000.0
|
|
77
|
+
state["fallback_used"] = True
|
|
78
|
+
elif greater_urgencies:
|
|
76
79
|
urgency_overrides[task_uuid] = min(greater_urgencies) + epsilon
|
|
77
80
|
elif state["top_rounds"] < max_top_rounds:
|
|
78
81
|
urgency_overrides[task_uuid] = current_urgency + epsilon
|
|
@@ -80,6 +83,7 @@ def advance_urgency_override(
|
|
|
80
83
|
else:
|
|
81
84
|
urgency_overrides[task_uuid] = 1000.0
|
|
82
85
|
state["fallback_used"] = True
|
|
86
|
+
state["rounds"] += 1
|
|
83
87
|
return task_uuid
|
|
84
88
|
return None
|
|
85
89
|
|
|
@@ -416,7 +420,7 @@ def allocate_time_for_day(
|
|
|
416
420
|
continue
|
|
417
421
|
|
|
418
422
|
wait = info["task"].get("wait")
|
|
419
|
-
if wait and date
|
|
423
|
+
if wait and date < datetime.strptime(wait, "%Y%m%dT%H%M%SZ").date():
|
|
420
424
|
if verbose:
|
|
421
425
|
print(f"Skipping task {info['task']['id']} due to wait date {wait}")
|
|
422
426
|
continue
|
|
@@ -647,6 +651,16 @@ def update_tasks_with_scheduling_info(task_info, verbose, taskrc):
|
|
|
647
651
|
scheduling_note = ""
|
|
648
652
|
scheduled_dates = sorted(info["scheduling"].keys())
|
|
649
653
|
if not scheduled_dates:
|
|
654
|
+
if any(task.get(field) for field in ("scheduled", "completion_date", "scheduling")):
|
|
655
|
+
subprocess.run(
|
|
656
|
+
[
|
|
657
|
+
"task", str(task["id"]), "modify",
|
|
658
|
+
"scheduled:", "completion_date:", "scheduling:",
|
|
659
|
+
],
|
|
660
|
+
stdout=subprocess.DEVNULL,
|
|
661
|
+
stderr=subprocess.DEVNULL,
|
|
662
|
+
env=get_task_env(taskrc),
|
|
663
|
+
)
|
|
650
664
|
continue
|
|
651
665
|
start_date = scheduled_dates[0]
|
|
652
666
|
end_date = scheduled_dates[-1]
|
|
@@ -184,6 +184,11 @@ def get_tasks(config, tasks, year, month, day):
|
|
|
184
184
|
regex = re.compile(rf"{year:04d}-{month:02d}-{day:02d} - ([PDTHM0-9]+)")
|
|
185
185
|
valid_tasks = []
|
|
186
186
|
for task in tasks:
|
|
187
|
+
wait = task.get("wait")
|
|
188
|
+
if wait and datetime(year, month, day).date() < datetime.strptime(
|
|
189
|
+
wait, "%Y%m%dT%H%M%SZ"
|
|
190
|
+
).date():
|
|
191
|
+
continue
|
|
187
192
|
for line in task.get("scheduling", "").split("\n"):
|
|
188
193
|
m = regex.match(line)
|
|
189
194
|
if m:
|
|
@@ -37,10 +37,14 @@ def project_color(project, theme):
|
|
|
37
37
|
|
|
38
38
|
def parse_scheduling_dates(task):
|
|
39
39
|
dates = []
|
|
40
|
+
wait = task.get("wait")
|
|
41
|
+
wait_date = datetime.strptime(wait, "%Y%m%dT%H%M%SZ").date() if wait else None
|
|
40
42
|
for line in task.get("scheduling", "").splitlines():
|
|
41
43
|
match = SCHEDULING_DATE.match(line)
|
|
42
44
|
if match:
|
|
43
|
-
|
|
45
|
+
scheduled_date = date.fromisoformat(match.group(1))
|
|
46
|
+
if wait_date is None or scheduled_date >= wait_date:
|
|
47
|
+
dates.append(scheduled_date)
|
|
44
48
|
return sorted(set(dates))
|
|
45
49
|
|
|
46
50
|
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: taskcheck
|
|
3
|
+
Version: 1.6.2
|
|
4
|
+
Summary: Automatic scheduler for taskwarrior
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: appdirs>=1.4.4
|
|
8
|
+
Requires-Dist: google-api-python-client>=2.198.0
|
|
9
|
+
Requires-Dist: google-auth>=2.56.2
|
|
10
|
+
Requires-Dist: google-auth-oauthlib>=1.2.2
|
|
11
|
+
Requires-Dist: icalendar>=6.0.1
|
|
12
|
+
Requires-Dist: random-unicode-emoji>=2.8
|
|
13
|
+
Requires-Dist: requests>=2.32.3
|
|
14
|
+
Requires-Dist: rich>=13.9.4
|
|
15
|
+
|
|
1
16
|
A Taskwarrior scheduler for people who want a realistic plan, not a manual to-do list.
|
|
2
17
|
It turns tasks, working hours, and calendar blocks into an actionable schedule — then keeps due dates visible.
|
|
3
18
|
|
|
@@ -45,6 +60,11 @@ It updates Taskwarrior with:
|
|
|
45
60
|
- `completion_date` → when work is expected to finish
|
|
46
61
|
- a warning when a task will miss its due date
|
|
47
62
|
|
|
63
|
+
Taskcheck does not allocate work before a task's `wait` date.
|
|
64
|
+
It can allocate work on that date because it plans whole days, not exact start times.
|
|
65
|
+
Reports and timelines ignore saved work dates before `wait`, even when you change `wait` after scheduling.
|
|
66
|
+
If no work fits within the planning period, the next scheduling run clears the task's old schedule fields.
|
|
67
|
+
|
|
48
68
|
## Quick start
|
|
49
69
|
|
|
50
70
|
1. `pipx install taskcheck`
|
|
@@ -128,6 +148,10 @@ Before writing a schedule, taskcheck backs up the affected tasks' `scheduled`, `
|
|
|
128
148
|
|
|
129
149
|
If a task would finish after its due date, taskcheck retries with a total-urgency override. Each retry moves the task immediately above the task with the smallest greater urgency by adding `urgency_epsilon` (default: `0.1`). At rank 1, it adds epsilon for up to `max_top_urgency_rounds` retries (default: `10`), then tries once at urgency `1000`. If the task is still late, taskcheck stops raising it and optimizes the other overdue tasks.
|
|
130
150
|
|
|
151
|
+
Each task gets at most `N - 1 + max_top_urgency_rounds` priority increases before the final retry at urgency `1000`.
|
|
152
|
+
Here, `N` is the number of tasks in the schedule.
|
|
153
|
+
This limit stops repeated retries when tasks with competing deadlines keep overtaking each other.
|
|
154
|
+
|
|
131
155
|
## Tips and Tricks
|
|
132
156
|
|
|
133
157
|
- You can exclude a task from being scheduled by removing the `time_map` or `estimated` attributes.
|
|
@@ -105,6 +105,26 @@ class TestTaskInitialization:
|
|
|
105
105
|
|
|
106
106
|
|
|
107
107
|
class TestTimeAllocation:
|
|
108
|
+
@pytest.mark.parametrize("agent", [False, True])
|
|
109
|
+
def test_task_starts_on_wait_date(self, agent):
|
|
110
|
+
from freezegun import freeze_time
|
|
111
|
+
|
|
112
|
+
task = {
|
|
113
|
+
"id": 1, "uuid": "waiting", "estimated": "PT2H", "time_map": "work",
|
|
114
|
+
"wait": "20231206T000000Z", "urgency": 10.0,
|
|
115
|
+
}
|
|
116
|
+
if agent:
|
|
117
|
+
task.update(tags=["agent"], agent_weight=25)
|
|
118
|
+
coeffs = UrgencyCoefficients({"PT2H": 0}, False, 0, 365, 12, 2)
|
|
119
|
+
with freeze_time("2023-12-05"):
|
|
120
|
+
with patch("taskcheck.parallel.get_long_range_time_map", return_value=([8.0] * 3, 0.0)):
|
|
121
|
+
info = initialize_task_info([task], {}, 3, coeffs, [])
|
|
122
|
+
allocate_time_for_day(info, 0, coeffs, False, 1.0)
|
|
123
|
+
assert info["waiting"]["scheduling"] == {}
|
|
124
|
+
allocate_time_for_day(info, 1, coeffs, False, 1.0)
|
|
125
|
+
assert info["waiting"]["scheduling"] == {"2023-12-06": 2.0}
|
|
126
|
+
assert info["waiting"]["remaining_hours"] == 0
|
|
127
|
+
|
|
108
128
|
def test_allocate_time_for_day_single_task(self, sample_config):
|
|
109
129
|
task_info = {
|
|
110
130
|
"task-1": {
|
|
@@ -368,6 +388,25 @@ class TestMainSchedulingFunction:
|
|
|
368
388
|
|
|
369
389
|
|
|
370
390
|
class TestAutoAdjustUrgency:
|
|
391
|
+
def test_competing_overdue_tasks_exhaust_retries(self):
|
|
392
|
+
task_info = {
|
|
393
|
+
uuid: {"task": {"uuid": uuid, "urgency": urgency}, "urgency": urgency}
|
|
394
|
+
for uuid, urgency in (("a", 1.0), ("b", 2.0))
|
|
395
|
+
}
|
|
396
|
+
overrides = {}
|
|
397
|
+
states = {}
|
|
398
|
+
# Resolving one deadline can make the other task late on the next pass.
|
|
399
|
+
for index in range(12):
|
|
400
|
+
overdue = [task_info["a" if index % 2 == 0 else "b"]["task"]]
|
|
401
|
+
advance_urgency_override(
|
|
402
|
+
overdue, task_info, overrides, states, epsilon=0.1, max_top_rounds=2
|
|
403
|
+
)
|
|
404
|
+
assert advance_urgency_override(
|
|
405
|
+
[info["task"] for info in task_info.values()],
|
|
406
|
+
task_info, overrides, states, epsilon=0.1, max_top_rounds=2,
|
|
407
|
+
) is None
|
|
408
|
+
assert overrides == {"a": 1000.0, "b": 1000.0}
|
|
409
|
+
|
|
371
410
|
def test_urgency_override_moves_up_one_rank_then_uses_bounded_top_rounds(self):
|
|
372
411
|
task_info = {
|
|
373
412
|
"t1": {"task": {"uuid": "t1"}, "urgency": 1.0},
|
|
@@ -67,6 +67,15 @@ class TestDateUtilities:
|
|
|
67
67
|
|
|
68
68
|
|
|
69
69
|
class TestTaskFiltering:
|
|
70
|
+
@pytest.mark.parametrize("day, expected", [(5, []), (6, [1]), (7, [1])])
|
|
71
|
+
def test_wait_date_overrides_saved_schedule(self, day, expected):
|
|
72
|
+
task = {
|
|
73
|
+
"id": 1, "description": "Waiting task", "urgency": 10.0,
|
|
74
|
+
"wait": "20231206T000000Z",
|
|
75
|
+
"scheduling": "\n".join(f"2023-12-{d:02d} - PT2H" for d in (5, 6, 7)),
|
|
76
|
+
}
|
|
77
|
+
assert [row["id"] for row in get_tasks({}, [task], 2023, 12, day)] == expected
|
|
78
|
+
|
|
70
79
|
def test_get_tasks_with_scheduling(self, sample_config):
|
|
71
80
|
tasks_with_scheduling = [
|
|
72
81
|
{
|
|
@@ -23,6 +23,14 @@ def test_parse_scheduling_dates_ignores_invalid_lines():
|
|
|
23
23
|
assert parse_scheduling_dates(task) == [date(2024, 1, 2), date(2024, 1, 3)]
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def test_wait_date_overrides_saved_schedule():
|
|
27
|
+
task = {
|
|
28
|
+
"wait": "20240103T000000Z",
|
|
29
|
+
"scheduling": "2024-01-02 - PT2H\n2024-01-03 - PT2H\n2024-01-04 - PT2H",
|
|
30
|
+
}
|
|
31
|
+
assert parse_scheduling_dates(task) == [date(2024, 1, 3), date(2024, 1, 4)]
|
|
32
|
+
|
|
33
|
+
|
|
26
34
|
def test_day_timeline_groups_projects_and_marks_events():
|
|
27
35
|
tasks = [
|
|
28
36
|
{
|
|
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
|
|
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
|