-
Notifications
You must be signed in to change notification settings - Fork 2
/
dashboard.py
318 lines (275 loc) · 10.1 KB
/
dashboard.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
import datetime
import glob
import json
import os
import subprocess
from pathlib import Path
from collections import defaultdict
from urllib.parse import quote
import fontbakery
import htmlmin
from packaging.version import parse as parse_version
from shaperglot.languages import Languages
from gflanguages import LoadScripts
import humanize
import tqdm
from gftools.push.servers import GFServers
from jinja2 import Environment, FileSystemLoader, select_autoescape
from googlefonts import GoogleFont
FONTBAKERY_BLACKLISTED = ["handjet", "adobeblank"]
BASE_URL = "https://simoncozens.github.io/gf-dashboard/"
TESTING = False
langs = Languages()
scripts = LoadScripts()
server_data = Path("docs/servers.json")
if not server_data.exists():
print(f"{server_data} not found. Generating file. This may take a while")
servers = GFServers()
else:
servers = GFServers.open(server_data)
servers.update_all()
servers.save(server_data)
gfpath = os.environ["GF_PATH"]
fonts = []
github_repo = os.environ.get("GITHUB_REPOSITORY", "")
repo_url = (
os.environ.get("GITHUB_SERVER_URL", "https://github.com/") + "/" + github_repo
)
if os.path.exists("docs/versionhistory.json"):
versionhistory = json.load(open("docs/versionhistory.json"))
else:
versionhistory = {}
reports_this_session = 100
def fontbakery_needs_update(directory, last_update: datetime.datetime):
global reports_this_session
basedir = os.path.basename(directory)
report_file = Path("docs/fontbakery-reports/" + basedir + "-report.json")
if basedir in FONTBAKERY_BLACKLISTED:
return False
if report_file.exists():
try:
report = json.load(open(report_file))
except json.JSONDecodeError as e:
print(
f"::warning file={report_file},title=Fontbakery report failed to parse::{e}"
)
if "fontbakery_version" in report and parse_version(
report["fontbakery_version"]
) < parse_version(fontbakery.__version__):
return True
if report_file.exists() and report_file.stat().st_mtime > last_update.timestamp():
return False
reports_this_session -= 1
return reports_this_session >= 0
def run_fontbakery(directory):
inputs = glob.glob(directory + "/*")
basedir = os.path.basename(directory)
os.makedirs("docs/fontbakery-reports/" + basedir, exist_ok=True)
json_file = Path(f"docs/fontbakery-reports/{basedir}-report.json")
if json_file.exists():
previous_json_time = json_file.stat().st_mtime
else:
previous_json_time = None
args = [
"fontbakery",
"check-googlefonts",
"-F",
"-l",
"WARN",
"--succinct",
"--configuration",
"fontbakery.yml",
"--badges",
f"docs/fontbakery-reports/{basedir}/",
"--html",
f"docs/fontbakery-reports/{basedir}-report.html",
"--json",
str(json_file),
*inputs,
]
args = " ".join(args)
result = subprocess.run(args, shell=True, capture_output=True)
if result.stderr:
print(
f"::warning file={basedir}, title=Fontbakery error::{result.stderr.decode('utf-8')}"
)
print(result.stderr.decode("utf-8"))
if json_file.exists and (
previous_json_time is None or json_file.stat().st_mtime > previous_json_time
):
try:
report = json.load(open(json_file))
report["fontbakery_version"] = fontbakery.__version__
json.dump(report, open(json_file, "w"))
except json.JSONDecodeError as e:
pass # We will report it later
def fontbakery_fails(basedir):
fb_fails = []
file = f"docs/fontbakery-reports/{basedir}-report.json"
if not os.path.exists(file):
return []
try:
report = json.load(open(file))
except json.JSONDecodeError as e:
print(f"::warning file={file},title=Fontbakery report failed to parse::{e}")
failset = set()
for section in report["sections"]:
for check in section["checks"]:
if check["result"] not in ["ERROR", "FAIL"]:
continue
if tuple(check["key"][0:2]) in failset:
continue
failset.add(tuple(check["key"][0:2]))
fb_fails.append(
{
"description": check["description"],
"result": check["result"],
}
)
return fb_fails
def fontbakery_badges(basedir):
fb_badges = []
for badge in glob.glob(f"docs/fontbakery-reports/{basedir}/*.json"):
if "Shaping" in badge:
continue
url = BASE_URL + "fontbakery-reports/" + basedir + "/" + os.path.basename(badge)
fb_badges.append("https://img.shields.io/endpoint?url=" + quote(url, safe=""))
return fb_badges
def tidy_version(version):
version = version.replace("Version ", "")
version = version.split(";")[0].strip()
return version
def rearrange_history(history):
new_history = []
for server, moves in history.items():
# Ignore the first move
for move in moves:
if "1970-01-01" in move["date"]:
continue
new_history.append(
{
"date": datetime.datetime.fromisoformat(move["date"]),
"version": move["version"],
"server": server,
}
)
return sorted(new_history, key=lambda x: x["date"], reverse=True)
script_langs = defaultdict(set)
for lang in langs.keys():
script_langs[langs[lang]["script"]].add(langs[lang]["name"])
def rearrange_languages(languages):
supported_langs_by_script = defaultdict(set)
report = []
for lang in languages:
lang = langs[lang]
supported_langs_by_script[lang["script"]].add(lang["name"])
for script, thislangs in supported_langs_by_script.items():
expected = script_langs[script]
percent = len(thislangs) / len(expected) * 100
result = "%i%% (%i/%i) of languages using the %s script" % (
percent,
len(thislangs),
len(expected),
scripts[script].name,
)
missing = expected - thislangs
if len(missing) > 0 and len(missing) < 10:
result += f" (Missing {'; '.join(missing)})"
elif percent < 100 and len(thislangs) < 10:
result += f" (Supports {'; '.join(thislangs)})"
report.append(result)
return report
for directory in tqdm.tqdm(glob.glob(gfpath + "/ofl/*")):
try:
gf = GoogleFont(directory, gfpath)
except Exception as e:
print(e)
continue
if fontbakery_needs_update(directory, gf.last_updated):
print("Running fontbakery on " + os.path.basename(directory))
run_fontbakery(directory)
gf.html_id = gf.directory.replace("/", "_")
if gf.metadata.name not in versionhistory:
versionhistory[gf.metadata.name] = {}
for s in servers:
if gf.metadata.name not in s.families:
continue
if s.name not in versionhistory[gf.metadata.name]:
versionhistory[gf.metadata.name][s.name] = []
current_version = s.families[gf.metadata.name].version
versions = [x["version"] for x in versionhistory[gf.metadata.name][s.name]]
if current_version not in versions:
versionhistory[gf.metadata.name][s.name].append(
{
"version": current_version,
"date": datetime.datetime.now().isoformat(),
}
)
json.dump(versionhistory, open("docs/versionhistory.json", "w"), indent=2)
gf.version_history = rearrange_history(versionhistory[gf.metadata.name])
gf.server_versions = {
s.name: s.families[gf.metadata.name].version
for s in servers
if s.families.get(gf.metadata.name)
}
if os.path.exists(
"docs/fontbakery-reports/" + os.path.basename(directory) + "-report.html"
):
gf.fontbakery_report = os.path.basename(directory) + "-report.html"
gf.fb_badges = fontbakery_badges(os.path.basename(directory))
gf.fb_fails = fontbakery_fails(os.path.basename(directory))
gf.version_badges = [
f"https://img.shields.io/badge/google/fonts-{tidy_version(gf.dev_version)}-green"
]
color = "green"
last_version = tidy_version(gf.dev_version)
for s in servers:
if gf.server_versions.get(s.name):
version = tidy_version(gf.server_versions[s.name])
if str(version) < str(last_version).strip():
color = "orange"
gf.version_badges.append(
f"https://img.shields.io/badge/{s.name}-{version}-{color}"
)
last_version = version
gf.build_badges = []
if gf.seems_gfr:
workflows = list(gf.upstream_gh.get_workflows())
for workflow in workflows or []:
runs = list(workflow.get_runs())
if runs and len(runs) > 0:
gf.build_badges.append(
f"https://img.shields.io/badge/{workflow.name}-{runs[0].conclusion}"
)
gf.languages = rearrange_languages(gf.supported_languages)
# Prime the pump
_ = gf.recent_commits
_ = gf.recent_pulls
_ = gf.releases
classes = []
if len(list(gf.open_pulls)):
classes.append("openpr")
if gf.new_releases_since_update:
classes.append("newrelease")
if "production" not in gf.server_versions or (
gf.server_versions["production"] != gf.dev_version
):
classes.append("inpipeline")
gf.classes = " ".join(classes)
# Downstream versions if noto
fonts.append(gf)
if TESTING and len(fonts) > 15:
break
fonts = sorted(fonts, key=lambda x: x.metadata.name)
def ago(dt):
return humanize.naturaldelta(
datetime.datetime.now(datetime.timezone.utc)
- dt.replace(tzinfo=datetime.timezone.utc)
)
env = Environment(loader=FileSystemLoader("templates"), autoescape=select_autoescape())
env.filters["ago"] = ago
template = env.get_template("index.html")
html = template.render(fonts=fonts, BASE_URL=BASE_URL)
html = htmlmin.minify(html)
with open("docs/index.html", "w") as f:
f.write(html)