| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112 |
- """
- Task data access layer.
- Single source of truth for loading and iterating task directories.
- Replaces scattered task.json parsing across 9+ files.
- Provides:
- load_task — Load a single task by directory path
- iter_active_tasks — Iterate all non-archived tasks (sorted)
- get_all_statuses — Get {dir_name: status} map for children progress
- """
- from __future__ import annotations
- from collections.abc import Iterator
- from pathlib import Path
- from .io import read_json
- from .paths import FILE_TASK_JSON
- from .types import TaskInfo
- def load_task(task_dir: Path) -> TaskInfo | None:
- """Load task from a directory containing task.json.
- Args:
- task_dir: Absolute path to the task directory.
- Returns:
- TaskInfo if task.json exists and is valid, None otherwise.
- """
- task_json = task_dir / FILE_TASK_JSON
- if not task_json.is_file():
- return None
- data = read_json(task_json)
- if not data:
- return None
- return TaskInfo(
- dir_name=task_dir.name,
- directory=task_dir,
- title=data.get("title") or data.get("name") or "unknown",
- status=data.get("status", "unknown"),
- assignee=data.get("assignee", ""),
- priority=data.get("priority", "P2"),
- children=tuple(data.get("children", [])),
- parent=data.get("parent"),
- package=data.get("package"),
- raw=data,
- )
- def iter_active_tasks(tasks_dir: Path) -> Iterator[TaskInfo]:
- """Iterate all active (non-archived) tasks, sorted by directory name.
- Skips the "archive" directory and directories without valid task.json.
- Args:
- tasks_dir: Path to the tasks directory.
- Yields:
- TaskInfo for each valid task.
- """
- if not tasks_dir.is_dir():
- return
- for d in sorted(tasks_dir.iterdir()):
- if not d.is_dir() or d.name == "archive":
- continue
- info = load_task(d)
- if info is not None:
- yield info
- def get_all_statuses(tasks_dir: Path) -> dict[str, str]:
- """Get a {dir_name: status} mapping for all active tasks.
- Useful for computing children progress without loading full TaskInfo.
- Args:
- tasks_dir: Path to the tasks directory.
- Returns:
- Dict mapping directory names to status strings.
- """
- return {t.dir_name: t.status for t in iter_active_tasks(tasks_dir)}
- def children_progress(
- children: tuple[str, ...] | list[str],
- all_statuses: dict[str, str],
- ) -> str:
- """Format children progress string like " [2/3 done]".
- Args:
- children: List of child directory names.
- all_statuses: Status map from get_all_statuses().
- Returns:
- Formatted string, or "" if no children.
- """
- if not children:
- return ""
- # A child missing from active statuses has been archived (cmd_archive
- # sets status=completed before moving the dir). Count it as done so
- # parent progress doesn't regress when children are archived.
- done = sum(
- 1 for c in children
- if c not in all_statuses or all_statuses.get(c) in ("completed", "done")
- )
- return f" [{done}/{len(children)} done]"
|