77import json
88import subprocess
99import sys
10+ import time
1011from collections import Counter
1112from datetime import datetime , timezone
1213from pathlib import Path
@@ -70,8 +71,29 @@ def run_gh(*args: str) -> str:
7071 return subprocess .check_output (["gh" , * args ], text = True )
7172
7273
73- def fetch_board_items (owner : str , project_number : int , limit : int ) -> dict :
74- return json .loads (
74+ def fetch_board_items (
75+ owner : str ,
76+ project_number : int ,
77+ limit : int ,
78+ * ,
79+ cache_file : Path | None = None ,
80+ cache_max_age : float = 120 ,
81+ ) -> dict :
82+ """Fetch project board items, optionally using a file cache.
83+
84+ When *cache_file* is set and the file exists and is younger than
85+ *cache_max_age* seconds, the cached JSON is returned without an API call.
86+ Otherwise the board is fetched from GitHub and written to the cache file.
87+ """
88+ if cache_file is not None :
89+ try :
90+ age = time .time () - cache_file .stat ().st_mtime
91+ if age < cache_max_age :
92+ return json .loads (cache_file .read_text ())
93+ except (FileNotFoundError , json .JSONDecodeError ):
94+ pass
95+
96+ data = json .loads (
7597 run_gh (
7698 "project" ,
7799 "item-list" ,
@@ -85,6 +107,12 @@ def fetch_board_items(owner: str, project_number: int, limit: int) -> dict:
85107 )
86108 )
87109
110+ if cache_file is not None :
111+ cache_file .parent .mkdir (parents = True , exist_ok = True )
112+ cache_file .write_text (json .dumps (data ))
113+
114+ return data
115+
88116
89117def fetch_pr_reviews (repo : str , pr_number : int ) -> list [dict ]:
90118 data = json .loads (run_gh ("api" , f"repos/{ repo } /pulls/{ pr_number } /reviews" ))
@@ -1130,6 +1158,7 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
11301158 next_parser .add_argument ("--limit" , type = int , default = 500 )
11311159 next_parser .add_argument ("--number" , type = int )
11321160 next_parser .add_argument ("--format" , choices = ["text" , "json" ], default = "text" )
1161+ next_parser .add_argument ("--board-cache" , type = Path , default = None )
11331162
11341163 claim_parser = subparsers .add_parser ("claim-next" )
11351164 claim_parser .add_argument ("mode" , choices = ["ready" , "review" ])
@@ -1142,6 +1171,7 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
11421171 claim_parser .add_argument ("--format" , choices = ["text" , "json" ], default = "json" )
11431172 claim_parser .add_argument ("--project-id" , default = PROJECT_ID )
11441173 claim_parser .add_argument ("--field-id" , default = STATUS_FIELD_ID )
1174+ claim_parser .add_argument ("--board-cache" , type = Path , default = None )
11451175
11461176 ack_parser = subparsers .add_parser ("ack" )
11471177 ack_parser .add_argument ("state_file" , type = Path )
@@ -1154,6 +1184,7 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
11541184 list_parser .add_argument ("--project-number" , type = int , default = 8 )
11551185 list_parser .add_argument ("--limit" , type = int , default = 500 )
11561186 list_parser .add_argument ("--format" , choices = ["text" , "json" ], default = "text" )
1187+ list_parser .add_argument ("--board-cache" , type = Path , default = None )
11571188
11581189 move_parser = subparsers .add_parser ("move" )
11591190 move_parser .add_argument ("item_id" )
@@ -1183,7 +1214,7 @@ def main(argv: list[str] | None = None) -> int:
11831214 if args .command == "claim-next" :
11841215 if args .mode == "review" and not args .repo :
11851216 raise SystemExit ("--repo is required in claim-next review mode" )
1186- board_data = fetch_board_items (args .owner , args .project_number , args .limit )
1217+ board_data = fetch_board_items (args .owner , args .project_number , args .limit , cache_file = args . board_cache )
11871218 claim_result = claim_next_entry (
11881219 args .mode ,
11891220 board_data ,
@@ -1205,7 +1236,7 @@ def main(argv: list[str] | None = None) -> int:
12051236 if args .command == "list" :
12061237 if args .mode == "review" and not args .repo :
12071238 raise SystemExit ("--repo is required in list review mode" )
1208- board_data = fetch_board_items (args .owner , args .project_number , args .limit )
1239+ board_data = fetch_board_items (args .owner , args .project_number , args .limit , cache_file = args . board_cache )
12091240 if args .mode == "ready" :
12101241 items = status_items (board_data , STATUS_READY )
12111242 return print_candidate_list (args .mode , items , fmt = args .format )
@@ -1234,7 +1265,7 @@ def main(argv: list[str] | None = None) -> int:
12341265 if args .mode in {"review" , "final-review" } and not args .repo :
12351266 raise SystemExit (f"--repo is required in { args .mode } mode" )
12361267
1237- board_data = fetch_board_items (args .owner , args .project_number , args .limit )
1268+ board_data = fetch_board_items (args .owner , args .project_number , args .limit , cache_file = args . board_cache )
12381269 next_item = select_next_entry (
12391270 args .mode ,
12401271 board_data ,
0 commit comments