From b6ce1cbe6480c2997bb62f1d8447446dce338310 Mon Sep 17 00:00:00 2001
From: Edward Betts <edward@4angle.com>
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 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title>Open bugs over time</title>
+  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
+  <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
+</head>
+
+<body>
+  <canvas id="bugChart" width="800" height="400"></canvas>
+
+<script>
+document.addEventListener('DOMContentLoaded', function () {
+    const jsonData = [];
+
+    var labels = jsonData.map(function(pair) { return pair[0]; });
+    var data = jsonData.map(function(pair) { return pair[1]; });
+
+    var ctx = document.getElementById('bugChart').getContext('2d');
+
+    var chart = new Chart(ctx, {
+        type: 'line',
+        data: {
+            labels: labels,
+            datasets: [{
+                label: 'Open bugs over time',
+                backgroundColor: 'rgba(0, 123, 255, 0.5)',
+                borderColor: 'rgba(0, 123, 255, 1)',
+                borderWidth: 1,
+                data: data
+            }]
+        },
+        options: {
+            scales: {
+                x: {
+                    type: 'time',
+                    time: {
+                        parser: 'yyyy-MM-dd',
+                        unit: 'month',
+                        // displayFormats: {'month': 'MMM yyyy'}
+                    }
+                },
+                y: {
+                    ticks: {
+                        beginAtZero: true
+                    }
+                }
+            }
+        }
+    });
+});
+</script>
+
+</body>
+</html>