120 lines
3.2 KiB
Python
Executable file
120 lines
3.2 KiB
Python
Executable file
#!/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 collections import Counter
|
|
from datetime import date, datetime, timedelta
|
|
|
|
import pytz
|
|
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() -> list[Bug]:
|
|
"""Get all bugs."""
|
|
filename = config.get("output", "cache")
|
|
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) -> 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(pytz.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."""
|
|
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()
|
|
dest = config.get("output", "dest")
|
|
with open(dest, "w") as out:
|
|
out.write(template_html.replace("jsonData = []", "jsonData = " + json_data))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|