tasks.py 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. """
  2. Task data access layer.
  3. Single source of truth for loading and iterating task directories.
  4. Replaces scattered task.json parsing across 9+ files.
  5. Provides:
  6. load_task — Load a single task by directory path
  7. iter_active_tasks — Iterate all non-archived tasks (sorted)
  8. get_all_statuses — Get {dir_name: status} map for children progress
  9. """
  10. from __future__ import annotations
  11. from collections.abc import Iterator
  12. from pathlib import Path
  13. from .io import read_json
  14. from .paths import FILE_TASK_JSON
  15. from .types import TaskInfo
  16. def load_task(task_dir: Path) -> TaskInfo | None:
  17. """Load task from a directory containing task.json.
  18. Args:
  19. task_dir: Absolute path to the task directory.
  20. Returns:
  21. TaskInfo if task.json exists and is valid, None otherwise.
  22. """
  23. task_json = task_dir / FILE_TASK_JSON
  24. if not task_json.is_file():
  25. return None
  26. data = read_json(task_json)
  27. if not data:
  28. return None
  29. return TaskInfo(
  30. dir_name=task_dir.name,
  31. directory=task_dir,
  32. title=data.get("title") or data.get("name") or "unknown",
  33. status=data.get("status", "unknown"),
  34. assignee=data.get("assignee", ""),
  35. priority=data.get("priority", "P2"),
  36. children=tuple(data.get("children", [])),
  37. parent=data.get("parent"),
  38. package=data.get("package"),
  39. raw=data,
  40. )
  41. def iter_active_tasks(tasks_dir: Path) -> Iterator[TaskInfo]:
  42. """Iterate all active (non-archived) tasks, sorted by directory name.
  43. Skips the "archive" directory and directories without valid task.json.
  44. Args:
  45. tasks_dir: Path to the tasks directory.
  46. Yields:
  47. TaskInfo for each valid task.
  48. """
  49. if not tasks_dir.is_dir():
  50. return
  51. for d in sorted(tasks_dir.iterdir()):
  52. if not d.is_dir() or d.name == "archive":
  53. continue
  54. info = load_task(d)
  55. if info is not None:
  56. yield info
  57. def get_all_statuses(tasks_dir: Path) -> dict[str, str]:
  58. """Get a {dir_name: status} mapping for all active tasks.
  59. Useful for computing children progress without loading full TaskInfo.
  60. Args:
  61. tasks_dir: Path to the tasks directory.
  62. Returns:
  63. Dict mapping directory names to status strings.
  64. """
  65. return {t.dir_name: t.status for t in iter_active_tasks(tasks_dir)}
  66. def children_progress(
  67. children: tuple[str, ...] | list[str],
  68. all_statuses: dict[str, str],
  69. ) -> str:
  70. """Format children progress string like " [2/3 done]".
  71. Args:
  72. children: List of child directory names.
  73. all_statuses: Status map from get_all_statuses().
  74. Returns:
  75. Formatted string, or "" if no children.
  76. """
  77. if not children:
  78. return ""
  79. # A child missing from active statuses has been archived (cmd_archive
  80. # sets status=completed before moving the dir). Count it as done so
  81. # parent progress doesn't regress when children are archived.
  82. done = sum(
  83. 1 for c in children
  84. if c not in all_statuses or all_statuses.get(c) in ("completed", "done")
  85. )
  86. return f" [{done}/{len(children)} done]"