Initial commit
This commit is contained in:
commit
b6ce1cbe64
120
build.py
Executable file
120
build.py
Executable file
|
@ -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()
|
56
template.html
Normal file
56
template.html
Normal file
|
@ -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>
|
Loading…
Reference in a new issue