taskcheck 1.6.2__tar.gz → 1.7.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.
Files changed (36) hide show
  1. {taskcheck-1.6.2/taskcheck.egg-info → taskcheck-1.7.0}/PKG-INFO +6 -1
  2. {taskcheck-1.6.2 → taskcheck-1.7.0}/README.md +5 -0
  3. {taskcheck-1.6.2 → taskcheck-1.7.0}/pyproject.toml +1 -1
  4. {taskcheck-1.6.2 → taskcheck-1.7.0}/taskcheck/__main__.py +2 -2
  5. {taskcheck-1.6.2 → taskcheck-1.7.0}/taskcheck/dry_run.py +16 -1
  6. {taskcheck-1.6.2 → taskcheck-1.7.0}/taskcheck/install.py +2 -2
  7. {taskcheck-1.6.2 → taskcheck-1.7.0}/taskcheck/parallel.py +1 -7
  8. {taskcheck-1.6.2 → taskcheck-1.7.0}/taskcheck/report.py +50 -56
  9. {taskcheck-1.6.2 → taskcheck-1.7.0/taskcheck.egg-info}/PKG-INFO +6 -1
  10. {taskcheck-1.6.2 → taskcheck-1.7.0}/taskcheck.egg-info/SOURCES.txt +2 -0
  11. taskcheck-1.7.0/tests/test_dry_run.py +21 -0
  12. {taskcheck-1.6.2 → taskcheck-1.7.0}/tests/test_report.py +2 -3
  13. taskcheck-1.7.0/tests/test_report_columns.py +29 -0
  14. {taskcheck-1.6.2 → taskcheck-1.7.0}/LICENSE +0 -0
  15. {taskcheck-1.6.2 → taskcheck-1.7.0}/setup.cfg +0 -0
  16. {taskcheck-1.6.2 → taskcheck-1.7.0}/taskcheck/agent_blocks.py +0 -0
  17. {taskcheck-1.6.2 → taskcheck-1.7.0}/taskcheck/common.py +0 -0
  18. {taskcheck-1.6.2 → taskcheck-1.7.0}/taskcheck/google_calendar.py +0 -0
  19. {taskcheck-1.6.2 → taskcheck-1.7.0}/taskcheck/ical.py +0 -0
  20. {taskcheck-1.6.2 → taskcheck-1.7.0}/taskcheck/theme.py +0 -0
  21. {taskcheck-1.6.2 → taskcheck-1.7.0}/taskcheck/timeline.py +0 -0
  22. {taskcheck-1.6.2 → taskcheck-1.7.0}/taskcheck/undo.py +0 -0
  23. {taskcheck-1.6.2 → taskcheck-1.7.0}/taskcheck.egg-info/dependency_links.txt +0 -0
  24. {taskcheck-1.6.2 → taskcheck-1.7.0}/taskcheck.egg-info/entry_points.txt +0 -0
  25. {taskcheck-1.6.2 → taskcheck-1.7.0}/taskcheck.egg-info/requires.txt +0 -0
  26. {taskcheck-1.6.2 → taskcheck-1.7.0}/taskcheck.egg-info/top_level.txt +0 -0
  27. {taskcheck-1.6.2 → taskcheck-1.7.0}/tests/test_agent_blocks.py +0 -0
  28. {taskcheck-1.6.2 → taskcheck-1.7.0}/tests/test_agent_scheduler_integration.py +0 -0
  29. {taskcheck-1.6.2 → taskcheck-1.7.0}/tests/test_common.py +0 -0
  30. {taskcheck-1.6.2 → taskcheck-1.7.0}/tests/test_google_calendar.py +0 -0
  31. {taskcheck-1.6.2 → taskcheck-1.7.0}/tests/test_ical.py +0 -0
  32. {taskcheck-1.6.2 → taskcheck-1.7.0}/tests/test_install.py +0 -0
  33. {taskcheck-1.6.2 → taskcheck-1.7.0}/tests/test_main.py +0 -0
  34. {taskcheck-1.6.2 → taskcheck-1.7.0}/tests/test_parallel.py +0 -0
  35. {taskcheck-1.6.2 → taskcheck-1.7.0}/tests/test_timeline.py +0 -0
  36. {taskcheck-1.6.2 → taskcheck-1.7.0}/tests/test_undo.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: taskcheck
3
- Version: 1.6.2
3
+ Version: 1.7.0
4
4
  Summary: Automatic scheduler for taskwarrior
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -114,6 +114,11 @@ Create a preview with `taskcheck --schedule --dry-run`. It does not update Taskw
114
114
  saves the generated schedule to the Taskwarrior data directory. Reuse it with `taskcheck --report
115
115
  today --dry-run` or `taskcheck --timeline 1w --dry-run`.
116
116
 
117
+ Report columns use the labels from `uda.<attribute>.label` in the active Taskwarrior configuration.
118
+ Reports show a robot icon beside the task ID for tasks tagged `agent`.
119
+ Dry-run schedules retain custom task attributes, including `agent_weight`.
120
+ When you load an older preview, Taskcheck fills missing task attributes from Taskwarrior and keeps the saved schedule.
121
+
117
122
  Timeline rows follow dotted project levels such as `work.client`. Without `--zoom`, the most
118
123
  detailed view that fits the terminal width is selected: day, then week, then month. The track uses
119
124
  `▶` for a start, `━` for work, `◀` for an end, and `◆` for a deadline. Thin, heavy, and double rules
@@ -99,6 +99,11 @@ Create a preview with `taskcheck --schedule --dry-run`. It does not update Taskw
99
99
  saves the generated schedule to the Taskwarrior data directory. Reuse it with `taskcheck --report
100
100
  today --dry-run` or `taskcheck --timeline 1w --dry-run`.
101
101
 
102
+ Report columns use the labels from `uda.<attribute>.label` in the active Taskwarrior configuration.
103
+ Reports show a robot icon beside the task ID for tasks tagged `agent`.
104
+ Dry-run schedules retain custom task attributes, including `agent_weight`.
105
+ When you load an older preview, Taskcheck fills missing task attributes from Taskwarrior and keeps the saved schedule.
106
+
102
107
  Timeline rows follow dotted project levels such as `work.client`. Without `--zoom`, the most
103
108
  detailed view that fits the terminal width is selected: day, then week, then month. The track uses
104
109
  `▶` for a start, `━` for work, `◀` for an end, and `◆` for a deadline. Thin, heavy, and double rules
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "taskcheck"
3
- version = "1.6.2"
3
+ version = "1.7.0"
4
4
  description = "Automatic scheduler for taskwarrior"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -102,8 +102,8 @@ def load_config():
102
102
  },
103
103
  "report": {
104
104
  "include_unplanned": true,
105
- "additional_attributes": [],
106
- "additional_attributes_unplanned": [],
105
+ "attributes": ["id", "project", "description", "scheduling_hours"],
106
+ "attributes_unplanned": ["id", "project", "description"],
107
107
  "emoji_keywords": {},
108
108
  ...
109
109
  }
@@ -1,7 +1,9 @@
1
1
  import json
2
+ import subprocess
2
3
  from pathlib import Path
3
4
 
4
5
  from taskcheck.undo import get_task_data_dir
6
+ from taskcheck.common import get_task_env
5
7
 
6
8
 
7
9
  def get_dry_run_path(taskrc=None):
@@ -19,4 +21,17 @@ def load_dry_run_results(taskrc=None):
19
21
  path = get_dry_run_path(taskrc)
20
22
  if not path.exists():
21
23
  return None
22
- return json.loads(path.read_text())
24
+ results = json.loads(path.read_text())
25
+ if not results:
26
+ return results
27
+ response = subprocess.run(
28
+ ["task", "export"], capture_output=True, text=True,
29
+ env=get_task_env(taskrc),
30
+ )
31
+ current_tasks = {
32
+ task["uuid"]: task for task in json.loads(response.stdout)
33
+ }
34
+ return [
35
+ {**current_tasks.get(task.get("uuid"), {}), **task}
36
+ for task in results
37
+ ]
@@ -20,8 +20,8 @@ n_undos = 1
20
20
 
21
21
  [report]
22
22
  include_unplanned = true
23
- additional_attributes = []
24
- additional_attributes_unplanned = []
23
+ attributes = ["id", "project", "description", "scheduling_hours"]
24
+ attributes_unplanned = ["id", "project", "description"]
25
25
  # unplanned_max_due = "30d"
26
26
  emoji_keywords = {}
27
27
 
@@ -293,13 +293,7 @@ def check_tasks_parallel(
293
293
  scheduling_note += f"{date_str} - {hours_to_pdth(hours)}\n"
294
294
 
295
295
  task_result = {
296
- "id": task["id"],
297
- "uuid": task["uuid"],
298
- "description": task["description"],
299
- "project": task.get("project", ""),
300
- "urgency": task.get("urgency", 0),
301
- "estimated": task["estimated"],
302
- "due": task.get("due", ""),
296
+ **task,
303
297
  "scheduled": start_date,
304
298
  "completion_date": end_date,
305
299
  "scheduling": scheduling_note.strip(),
@@ -198,16 +198,10 @@ def get_tasks(config, tasks, year, month, day):
198
198
  due = due - datetime.today()
199
199
  valid_tasks.append(
200
200
  {
201
- "id": task["id"],
202
- "project": task.get("project", ""),
203
- "description": task["description"],
204
- "urgency": task["urgency"],
201
+ **task,
205
202
  "scheduling_day": f"{year}-{month}-{day}",
206
203
  "scheduling_hours": m.group(1),
207
- **{
208
- attr: task.get(attr, "")
209
- for attr in config.get("additional_attributes", [])
210
- },
204
+ "tags": task.get("tags", []),
211
205
  }
212
206
  )
213
207
  valid_tasks = sorted(valid_tasks, key=lambda x: x["urgency"], reverse=True)
@@ -287,7 +281,22 @@ def get_unplanned_tasks(config, tasks, taskrc=None):
287
281
 
288
282
 
289
283
  def generate_report(config, constraint, verbose=False, force_update=False, taskrc=None, scheduling_results=None):
290
- report_config = config["report"]
284
+ from taskcheck.common import get_task_env
285
+
286
+ report_config = dict(config["report"])
287
+ result = subprocess.run(
288
+ ["task", "_show"],
289
+ capture_output=True,
290
+ text=True,
291
+ env=get_task_env(taskrc),
292
+ )
293
+ report_config["attribute_labels"] = {
294
+ key[4:-6]: value
295
+ for line in result.stdout.splitlines()
296
+ if "=" in line
297
+ for key, value in [line.split("=", 1)]
298
+ if key.startswith("uda.") and key.endswith(".label") and value
299
+ }
291
300
  theme = detect_theme(config=config.get("timeline", {}))
292
301
  console = Console()
293
302
 
@@ -347,32 +356,38 @@ def display_tasks_table(console, config, tasks, theme="dark"):
347
356
 
348
357
  def build_tasks_table(config, tasks, theme="dark"):
349
358
  """Build a Rich table for displaying tasks."""
359
+ return _build_report_table(
360
+ config, tasks, config.get("attributes", ["id", "project", "description", "scheduling_hours"]), theme
361
+ )
362
+
363
+
364
+ def _build_report_table(config, tasks, attributes, theme):
350
365
  header_style = "bold cyan" if theme == "dark" else "bold blue"
351
366
  table = Table(show_header=True, header_style=header_style)
352
- table.add_column("Task", style="dim", width=12)
353
- table.add_column("Project", style="dim", width=12)
354
- table.add_column("Description")
355
- table.add_column("Time", justify="right")
356
- for attr in config.get("additional_attributes", []):
357
- table.add_column(attr.capitalize(), justify="right")
358
-
359
- for task in tasks:
360
- task_id = f"[bold green]#{task['id']}[/bold green]"
361
- project = task.get("project", "")
362
- description = Text(task["description"], style="italic")
363
- hours = f"[yellow]{task['scheduling_hours']}[/yellow]"
364
- emoji = get_task_emoji(config, task)
365
-
366
- table.add_row(
367
- f"{emoji} {task_id}",
368
- project,
369
- description,
370
- hours,
371
- *[
372
- tostring(task.get(attr, ""))
373
- for attr in config.get("additional_attributes", [])
374
- ],
367
+ labels = {"id": "Task", "scheduling_hours": "Time"}
368
+ for attr in attributes:
369
+ label = config.get("attribute_labels", {}).get(
370
+ attr, labels.get(attr, attr.capitalize())
375
371
  )
372
+ if attr in ("id", "project"):
373
+ table.add_column(label, style="dim", width=12)
374
+ else:
375
+ table.add_column(label, justify="left" if attr == "description" else "right")
376
+ if not attributes:
377
+ return table
378
+ for task in tasks:
379
+ cells = []
380
+ for attr in attributes:
381
+ if attr == "id":
382
+ icon = " 🤖" if "agent" in task.get("tags", []) else ""
383
+ cells.append(f"{get_task_emoji(config, task)} [bold green]#{task['id']}[/bold green]{icon}")
384
+ elif attr == "description":
385
+ cells.append(Text(task.get(attr, ""), style="italic"))
386
+ elif attr == "scheduling_hours":
387
+ cells.append(f"[yellow]{task.get(attr, '')}[/yellow]")
388
+ else:
389
+ cells.append(tostring(task.get(attr, "")))
390
+ table.add_row(*cells)
376
391
  return table
377
392
 
378
393
 
@@ -417,27 +432,6 @@ def display_unplanned_tasks(console, config, tasks, theme="dark"):
417
432
 
418
433
  def build_unplanned_tasks_table(config, tasks, theme="dark"):
419
434
  """Build a Rich table for displaying unplanned tasks."""
420
- header_style = "bold cyan" if theme == "dark" else "bold blue"
421
- table = Table(show_header=True, header_style=header_style)
422
- table.add_column("Task", style="dim", width=12)
423
- table.add_column("Project", style="dim", width=12)
424
- table.add_column("Description")
425
- for attr in config.get("additional_attributes_unplanned", []):
426
- table.add_column(attr.capitalize(), justify="right")
427
-
428
- for task in tasks:
429
- task_id = f"[bold green]#{task['id']}[/bold green]"
430
- project = task.get("project", "")
431
- description = Text(task["description"], style="italic")
432
- emoji = get_task_emoji(config, task)
433
-
434
- table.add_row(
435
- f"{emoji} {task_id}",
436
- project,
437
- description,
438
- *[
439
- tostring(task.get(attr, ""))
440
- for attr in config.get("additional_attributes_unplanned", [])
441
- ],
442
- )
443
- return table
435
+ return _build_report_table(
436
+ config, tasks, config.get("attributes_unplanned", ["id", "project", "description"]), theme
437
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: taskcheck
3
- Version: 1.6.2
3
+ Version: 1.7.0
4
4
  Summary: Automatic scheduler for taskwarrior
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -114,6 +114,11 @@ Create a preview with `taskcheck --schedule --dry-run`. It does not update Taskw
114
114
  saves the generated schedule to the Taskwarrior data directory. Reuse it with `taskcheck --report
115
115
  today --dry-run` or `taskcheck --timeline 1w --dry-run`.
116
116
 
117
+ Report columns use the labels from `uda.<attribute>.label` in the active Taskwarrior configuration.
118
+ Reports show a robot icon beside the task ID for tasks tagged `agent`.
119
+ Dry-run schedules retain custom task attributes, including `agent_weight`.
120
+ When you load an older preview, Taskcheck fills missing task attributes from Taskwarrior and keeps the saved schedule.
121
+
117
122
  Timeline rows follow dotted project levels such as `work.client`. Without `--zoom`, the most
118
123
  detailed view that fits the terminal width is selected: day, then week, then month. The track uses
119
124
  `▶` for a start, `━` for work, `◀` for an end, and `◆` for a deadline. Thin, heavy, and double rules
@@ -22,11 +22,13 @@ taskcheck.egg-info/top_level.txt
22
22
  tests/test_agent_blocks.py
23
23
  tests/test_agent_scheduler_integration.py
24
24
  tests/test_common.py
25
+ tests/test_dry_run.py
25
26
  tests/test_google_calendar.py
26
27
  tests/test_ical.py
27
28
  tests/test_install.py
28
29
  tests/test_main.py
29
30
  tests/test_parallel.py
30
31
  tests/test_report.py
32
+ tests/test_report_columns.py
31
33
  tests/test_timeline.py
32
34
  tests/test_undo.py
@@ -0,0 +1,21 @@
1
+ import json
2
+ from unittest.mock import patch
3
+
4
+ from taskcheck.dry_run import load_dry_run_results
5
+
6
+
7
+ def test_old_preview_recovers_missing_attributes_without_changing_schedule(tmp_path):
8
+ path = tmp_path / "dry-run.json"
9
+ cached = [{"uuid": "agent", "scheduling": "2026-11-10 - PT2H"}]
10
+ path.write_text(json.dumps(cached))
11
+ current = [{"uuid": "agent", "agent_weight": 25,
12
+ "scheduling": "2026-11-11 - PT1H"}]
13
+ with patch("taskcheck.dry_run.get_dry_run_path", return_value=path), patch(
14
+ "taskcheck.dry_run.subprocess.run"
15
+ ) as export:
16
+ export.return_value.stdout = json.dumps(current)
17
+ result = load_dry_run_results()
18
+ assert result is not None
19
+ assert result[0]["agent_weight"] == 25
20
+ assert result[0]["scheduling"] == "2026-11-10 - PT2H"
21
+ assert json.loads(path.read_text()) == cached
@@ -246,7 +246,7 @@ class TestReportGeneration:
246
246
  @patch('taskcheck.report.fetch_tasks')
247
247
  @patch('taskcheck.report.get_days_in_constraint')
248
248
  @patch('taskcheck.report.get_unplanned_tasks')
249
- def test_generate_report_with_unplanned(self, mock_unplanned, mock_days, mock_fetch, sample_config, test_taskrc):
249
+ def test_generate_report_with_unplanned(self, mock_unplanned, mock_days, mock_fetch, sample_config, test_taskrc, capsys):
250
250
  mock_fetch.return_value = []
251
251
  mock_days.return_value = [(2023, 12, 5)]
252
252
  mock_unplanned.return_value = [
@@ -260,7 +260,7 @@ class TestReportGeneration:
260
260
 
261
261
  generate_report(sample_config, "today", verbose=True, taskrc=test_taskrc)
262
262
 
263
- mock_unplanned.assert_called_once_with(sample_config["report"], [], taskrc=test_taskrc)
263
+ assert "Unplanned task" in capsys.readouterr().out
264
264
 
265
265
  @patch('taskcheck.report.display_unplanned_tasks')
266
266
  @patch('taskcheck.report.get_unplanned_tasks')
@@ -278,5 +278,4 @@ class TestReportGeneration:
278
278
  generate_report(sample_config, "today", verbose=False, taskrc=test_taskrc, scheduling_results=scheduling_results)
279
279
 
280
280
  mock_fetch.assert_called_once_with(test_taskrc)
281
- mock_unplanned.assert_called_once_with(sample_config["report"], mock_fetch.return_value, taskrc=test_taskrc)
282
281
  mock_display.assert_called_once()
@@ -0,0 +1,29 @@
1
+ from io import StringIO
2
+
3
+ from rich.console import Console
4
+
5
+ from taskcheck.report import build_tasks_table, build_unplanned_tasks_table, get_tasks
6
+
7
+
8
+ def test_explicit_columns_replace_defaults_in_order():
9
+ task = {"id": 2, "description": "Agent work", "urgency": 1,
10
+ "tags": ["agent"], "agent_weight": 0.34,
11
+ "scheduling": "2026-11-10 - PT2H"}
12
+ config = {"attributes": ["agent_weight", "description"],
13
+ "attribute_labels": {"agent_weight": "Attention"}}
14
+ rows = get_tasks(config, [task], 2026, 11, 10)
15
+ table = build_tasks_table(config, rows)
16
+ assert [column.header for column in table.columns] == ["Attention", "Description"]
17
+ stream = StringIO()
18
+ Console(file=stream, width=100).print(table)
19
+ assert "0.34" in stream.getvalue()
20
+ assert "Agent work" in stream.getvalue()
21
+ assert "#2" not in stream.getvalue()
22
+
23
+
24
+ def test_unplanned_columns_are_independent_and_empty_means_none():
25
+ config = {"attributes": [], "attributes_unplanned": ["description"]}
26
+ tasks = [{"id": 1, "description": "Unplanned work"}]
27
+ assert build_tasks_table(config, tasks).columns == []
28
+ table = build_unplanned_tasks_table(config, tasks)
29
+ assert [column.header for column in table.columns] == ["Description"]
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