ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
work_board.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3
4"""Print a kanban board representation of the project items."""
5
6from __future__ import annotations
7
8import json
9import sys
10from datetime import UTC, datetime, timedelta
11from pathlib import Path
12
13sys.path.insert(0, str(Path(__file__).resolve().parent))
14from work_git import ToolMissingError, WorkError, run_process
15from work_tracker import TrackerSchema, tracker_schema
16
17GH_TIMEOUT_S = 30
18LANDED_RETENTION_DAYS = 7
19NO_STATUS = "No Status"
20LANDED_STATUS = "landed"
21
22
23def _load_schema() -> TrackerSchema | None:
24 """Return the tracker authority, reporting a load failure."""
25 try:
26 return tracker_schema()
27 except WorkError as exc:
28 print(f"error loading tracker schema: {exc}", file=sys.stderr)
29 return None
30
31
32def _fetch_items(schema: TrackerSchema) -> tuple[list, int]:
33 """Fetch raw board items, reporting transport and parse failures."""
34 try:
35 proc = run_process(
36 [
37 "gh",
38 "project",
39 "item-list",
40 str(schema.project_number),
41 "--owner",
42 schema.project_owner,
43 "--limit",
44 "2500",
45 "--format",
46 "json",
47 ],
48 timeout=GH_TIMEOUT_S,
49 )
50 except ToolMissingError:
51 print("error: 'gh' CLI is not installed", file=sys.stderr)
52 return [], 1
53 except WorkError as exc:
54 print(f"error fetching board: {exc}", file=sys.stderr)
55 return [], 1
56 if not proc.ok:
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
60 try:
61 data = json.loads(proc.stdout)
62 except json.JSONDecodeError as exc:
63 print(f"error parsing GitHub JSON: {exc}", file=sys.stderr)
64 return [], 1
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)
68 return [], 1
69 return items, 0
70
71
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)
76
77
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:
81 return False
82 updated_at = item.get("updatedAt", "")
83 if not updated_at:
84 return False
85 try:
86 updated = datetime.fromisoformat(updated_at)
87 except ValueError:
88 return False
89 return now - updated > timedelta(days=LANDED_RETENTION_DAYS)
90
91
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", "")
98 else:
99 number = str(item.get("number", "?"))
100 repo = ""
101 return {"title": item.get("title", "Untitled"), "number": number, "repo": repo}
102
103
104def _group_items(
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] = {}
111 for item in items:
112 status = _normalize_status(status_map, item.get("status", NO_STATUS))
113 if _is_stale_landed(status, item, now):
114 continue
115 entry = _entry(item)
116 if status in board:
117 board[status].append(entry)
118 else:
119 others.setdefault(status, []).append(entry)
120 return board, others
121
122
123def _print_entries(entries: list) -> None:
124 """Print one column with per-item number and repository suffixes."""
125 if not entries:
126 print(" (empty)")
127 for item in entries:
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}")
132
133
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)
143 print()
144
145
146def main() -> int:
147 """Print the project board grouped by status."""
148 schema = _load_schema()
149 if schema is None:
150 return 1
151 items, code = _fetch_items(schema)
152 if code != 0:
153 return code
154 board, others = _group_items(schema, items, datetime.now(UTC))
155 _print_board(schema, board, others)
156 return 0
157
158
159if __name__ == "__main__":
160 sys.exit(main())
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298