#!/usr/bin/python3 """Grab list of bus from Forgejo and use it to make a line graph.""" import configparser import json import os import sys import typing from datetime import datetime, timezone import requests config = configparser.ConfigParser() config_file_path = os.path.expanduser( os.path.join(os.getenv("XDG_CONFIG_HOME", "~/.config"), "bug-chart", "config") ) assert os.path.exists(config_file_path) config.read(os.path.expanduser(config_file_path)) Bug = dict[str, typing.Any] CallParams = dict[str, str | int] hostname = config.get("forgejo", "hostname") token = config.get("forgejo", "token") url = f"https://{hostname}/api/v1/repos/issues/search" def download_bugs(state: str) -> list[Bug]: """Get all bugs from forgejo.""" all_bugs: list[Bug] = [] page = 0 headers = {"Authorization": "token " + token} while bugs := requests.get( url, headers=headers, params=typing.cast(CallParams, {"state": state, "page": page}), ).json(): if isinstance(bugs, dict): print(bugs) sys.exit(1) assert isinstance(bugs, list) all_bugs += bugs page += 1 return all_bugs def get_all_bugs(refresh: bool = False) -> list[Bug]: """Get all bugs.""" filename = config.get("output", "cache") if not refresh and os.path.exists(filename): with open(filename) as fh: return typing.cast(list[Bug], json.load(fh)) all_bugs = download_bugs("open") + download_bugs("closed") with open(filename, "w") as out: json.dump(all_bugs, out, indent=2) return all_bugs def parse_date(date_str: str | None) -> datetime | None: """Parse a date string with timezone information.""" if date_str is None: return None fmt = "%Y-%m-%dT%H:%M:%SZ" if date_str.endswith("Z") else "%Y-%m-%dT%H:%M:%S%z" return datetime.strptime(date_str, fmt).astimezone(timezone.utc) def count_open_bugs(bug_reports: list[Bug]) -> list[tuple[str, int]]: """Count the number of open bugs for each date based on a list of bug reports.""" dates: list[tuple[datetime, int]] = [] # Process each bug report seen: set[int] = set() for report in bug_reports: if report["id"] in seen: continue seen.add(report["id"]) open_date = parse_date(report["created_at"]) assert open_date dates.append((open_date, 1)) close_date = parse_date(report["closed_at"]) if close_date: dates.append((close_date, -1)) dates.sort() bug_count = 0 open_bugs_over_time = [] for dt, delta in dates: bug_count += delta iso_date = dt.isoformat() open_bugs_over_time.append((iso_date, bug_count)) return open_bugs_over_time def main() -> None: """Grab bug reports and generate chart.""" refresh = len(sys.argv) > 1 and sys.argv[1] == "--refresh" bug_reports = get_all_bugs(refresh) open_bugs_over_time = count_open_bugs(bug_reports) json_data = json.dumps(open_bugs_over_time) script_dir = os.path.dirname(os.path.abspath(__file__)) template_path = os.path.join(script_dir, "template.html") with open(template_path, "r") as fh: template_html = fh.read() dest = config.get("output", "dest") with open(dest, "w") as out: out.write(template_html.replace("jsonData = []", "jsonData = " + json_data)) if __name__ == "__main__": main()