4"""Print a kanban board representation of the project items."""
6from __future__
import annotations
10from datetime
import UTC, datetime, timedelta
11from pathlib
import Path
13sys.path.insert(0, str(Path(__file__).resolve().parent))
14from work_git
import ToolMissingError, WorkError, run_process
15from work_tracker
import TrackerSchema, tracker_schema
18LANDED_RETENTION_DAYS = 7
19NO_STATUS =
"No Status"
20LANDED_STATUS =
"landed"
23def _load_schema() -> TrackerSchema | None:
24 """Return the tracker authority, reporting a load failure."""
26 return tracker_schema()
27 except WorkError
as exc:
28 print(f
"error loading tracker schema: {exc}", file=sys.stderr)
32def _fetch_items(schema: TrackerSchema) -> tuple[list, int]:
33 """Fetch raw board items, reporting transport and parse failures."""
40 str(schema.project_number),
50 except ToolMissingError:
51 print(
"error: 'gh' CLI is not installed", file=sys.stderr)
53 except WorkError
as exc:
54 print(f
"error fetching board: {exc}", file=sys.stderr)
57 message = proc.stderr.strip()
or proc.stdout.strip()
or f
"gh exited {proc.returncode}"
58 print(f
"error fetching board: {message}", file=sys.stderr)
59 return [], proc.returncode
61 data = json.loads(proc.stdout)
62 except json.JSONDecodeError
as exc:
63 print(f
"error parsing GitHub JSON: {exc}", file=sys.stderr)
65 items = data.get(
"items", data)
if isinstance(data, dict)
else data
66 if not isinstance(items, list):
67 print(
"error: unexpected JSON structure from gh", file=sys.stderr)
72def _normalize_status(status_map: dict[str, str], raw: object) -> str:
73 """Match a raw status case-insensitively, falling back to the raw string."""
74 text = raw
if isinstance(raw, str)
and raw
else NO_STATUS
75 return status_map.get(text.lower(), text)
78def _is_stale_landed(status: str, item: dict, now: datetime) -> bool:
79 """Decide whether a landed item aged past the retention window."""
80 if status.lower() != LANDED_STATUS:
82 updated_at = item.get(
"updatedAt",
"")
86 updated = datetime.fromisoformat(updated_at)
89 return now - updated > timedelta(days=LANDED_RETENTION_DAYS)
92def _entry(item: dict) -> dict[str, str]:
93 """Project one raw item onto its title, number, and repository."""
94 content = item.get(
"content")
95 if isinstance(content, dict):
96 number = str(content.get(
"number",
"?"))
97 repo = content.get(
"repository",
"")
99 number = str(item.get(
"number",
"?"))
101 return {
"title": item.get(
"title",
"Untitled"),
"number": number,
"repo": repo}
105 schema: TrackerSchema, items: list, now: datetime
106) -> tuple[dict[str, list], dict[str, list]]:
107 """Split items into schema columns and unexpected-status overflow."""
108 status_map = {status.lower(): status
for status
in schema.statuses}
109 board: dict[str, list] = {status: []
for status
in schema.statuses}
110 others: dict[str, list] = {}
112 status = _normalize_status(status_map, item.get(
"status", NO_STATUS))
113 if _is_stale_landed(status, item, now):
117 board[status].append(entry)
119 others.setdefault(status, []).append(entry)
123def _print_entries(entries: list) ->
None:
124 """Print one column with per-item number and repository suffixes."""
128 repo_part = f
" {item['repo']}" if item[
"repo"]
else ""
129 num_part = f
"#{item['number']}" if item[
"number"] !=
"?" else ""
130 suffix = f
" ({num_part}{repo_part})" if (num_part
or repo_part)
else ""
131 print(f
" - {item['title']}{suffix}")
134def _print_board(schema: TrackerSchema, board: dict[str, list], others: dict[str, list]) ->
None:
135 """Print schema columns in order, then any unexpected statuses."""
136 print(f
"=== Project Board: {schema.project_owner}/{schema.project_number} ===")
137 for status
in schema.statuses:
138 print(f
"\n[ {status.upper()} ] ({len(board[status])})")
139 _print_entries(board[status])
140 for status, entries
in others.items():
141 print(f
"\n[ {status.upper()} ] ({len(entries)})")
142 _print_entries(entries)
147 """Print the project board grouped by status."""
148 schema = _load_schema()
151 items, code = _fetch_items(schema)
154 board, others = _group_items(schema, items, datetime.now(UTC))
155 _print_board(schema, board, others)
159if __name__ ==
"__main__":
void main(void)
The application entry point Reset_Handler hands control to.