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.
Files changed (34) hide show
  1. {taskcheck-1.6.0 → taskcheck-1.6.2}/PKG-INFO +11 -2
  2. taskcheck-1.6.0/taskcheck.egg-info/PKG-INFO → taskcheck-1.6.2/README.md +9 -15
  3. {taskcheck-1.6.0 → taskcheck-1.6.2}/pyproject.toml +2 -2
  4. {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck/agent_blocks.py +1 -1
  5. {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck/parallel.py +17 -3
  6. {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck/report.py +5 -0
  7. {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck/timeline.py +5 -1
  8. taskcheck-1.6.0/README.md → taskcheck-1.6.2/taskcheck.egg-info/PKG-INFO +24 -0
  9. {taskcheck-1.6.0 → taskcheck-1.6.2}/tests/test_parallel.py +39 -0
  10. {taskcheck-1.6.0 → taskcheck-1.6.2}/tests/test_report.py +9 -0
  11. {taskcheck-1.6.0 → taskcheck-1.6.2}/tests/test_timeline.py +8 -0
  12. {taskcheck-1.6.0 → taskcheck-1.6.2}/LICENSE +0 -0
  13. {taskcheck-1.6.0 → taskcheck-1.6.2}/setup.cfg +0 -0
  14. {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck/__main__.py +0 -0
  15. {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck/common.py +0 -0
  16. {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck/dry_run.py +0 -0
  17. {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck/google_calendar.py +0 -0
  18. {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck/ical.py +0 -0
  19. {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck/install.py +0 -0
  20. {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck/theme.py +0 -0
  21. {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck/undo.py +0 -0
  22. {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck.egg-info/SOURCES.txt +0 -0
  23. {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck.egg-info/dependency_links.txt +0 -0
  24. {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck.egg-info/entry_points.txt +0 -0
  25. {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck.egg-info/requires.txt +0 -0
  26. {taskcheck-1.6.0 → taskcheck-1.6.2}/taskcheck.egg-info/top_level.txt +0 -0
  27. {taskcheck-1.6.0 → taskcheck-1.6.2}/tests/test_agent_blocks.py +0 -0
  28. {taskcheck-1.6.0 → taskcheck-1.6.2}/tests/test_agent_scheduler_integration.py +0 -0
  29. {taskcheck-1.6.0 → taskcheck-1.6.2}/tests/test_common.py +0 -0
  30. {taskcheck-1.6.0 → taskcheck-1.6.2}/tests/test_google_calendar.py +0 -0
  31. {taskcheck-1.6.0 → taskcheck-1.6.2}/tests/test_ical.py +0 -0
  32. {taskcheck-1.6.0 → taskcheck-1.6.2}/tests/test_install.py +0 -0
  33. {taskcheck-1.6.0 → taskcheck-1.6.2}/tests/test_main.py +0 -0
  34. {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.0
3
+ Version: 1.6.2
4
4
  Summary: Automatic scheduler for taskwarrior
5
- Requires-Python: >=3.11
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.0"
3
+ version = "1.6.2"
4
4
  description = "Automatic scheduler for taskwarrior"
5
5
  readme = "README.md"
6
- requires-python = ">=3.11"
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 <= datetime.strptime(wait, "%Y%m%dT%H%M%SZ").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 greater_urgencies:
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 <= datetime.strptime(wait, "%Y%m%dT%H%M%SZ").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
- dates.append(date.fromisoformat(match.group(1)))
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