From b6ce1cbe6480c2997bb62f1d8447446dce338310 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Thu, 21 Dec 2023 18:49:15 +0000 Subject: [PATCH] Initial commit --- build.py | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++ template.html | 56 +++++++++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100755 build.py create mode 100644 template.html diff --git a/build.py b/build.py new file mode 100755 index 0000000..7854a8d --- /dev/null +++ b/build.py @@ -0,0 +1,120 @@ +#!/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 date, datetime, timedelta + +import pytz +import requests + +config = configparser.ConfigParser() +config.read("config") + + +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.""" + # params: dict[str, str | int] = {} + + all_bugs: list[Bug] = [] + page = 0 + while bugs := requests.get( + url, + headers={"Authorization": "token " + token}, + 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() -> list[Bug]: + """Get all bugs.""" + filename = "all_bugs.json" + if 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) -> date | 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(pytz.utc).date() + + +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.""" + open_dates: dict[date, int] = {} + close_dates: dict[date, 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"]) + if open_date: + open_dates[open_date] = open_dates.get(open_date, 0) + 1 + + close_date = parse_date(report["closed_at"]) + if close_date: + close_dates[close_date] = close_dates.get(close_date, 0) + 1 + + # Create a date range from the earliest open date to the latest close date + start_date = min(open_dates.keys()) + end_date = max(close_dates.keys(), default=start_date) + delta = end_date - start_date + + # Count open bugs for each date + open_bug_count = 0 + open_bugs_over_time = [] + for i in range(delta.days + 1): + current_date = start_date + timedelta(days=i) + open_bug_count += open_dates.get(current_date, 0) + open_bug_count -= close_dates.get(current_date, 0) + open_bugs_over_time.append((current_date.isoformat(), open_bug_count)) + + return open_bugs_over_time + + +def main() -> None: + """Grab bug reports and generate chart.""" + bug_reports = get_all_bugs() + + open_bugs_over_time = count_open_bugs(bug_reports) + json_data = json.dumps(open_bugs_over_time) + + template_html = open("template.html").read() + with open("chart.html", "w") as out: + out.write(template_html.replace("jsonData = []", "jsonData = " + json_data)) + + +if __name__ == "__main__": + main() diff --git a/template.html b/template.html new file mode 100644 index 0000000..f605b0d --- /dev/null +++ b/template.html @@ -0,0 +1,56 @@ + + + + + Open bugs over time + + + + + + + + + + +