Repository navigation
Expand file tree
/
Copy pathgenerate_static.py
More file actions
107 lines (87 loc) · 3.88 KB
/
Copy pathgenerate_static.py
File metadata and controls
107 lines (87 loc) · 3.88 KB
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
#!/usr/bin/env python3
"""
AndroidGuides.com — Crawlable-HTML generator (P2)
Reads devices.json and writes devices-static.html: one plain-English,
plain-HTML sentence per device. This fragment is what AI crawlers and
non-JavaScript visitors read on /devices (they don't run the JS table,
so without this they'd see an empty page). Runs monthly in the same
GitHub Actions workflow as update_devices.py; the fragment is embedded
in the /devices WordPress page (auto-push arrives with P3).
Usage: python generate_static.py
"""
import json
from datetime import date, datetime
from pathlib import Path
from support_status import support_sentence, support_state
INPUT = Path(__file__).parent / "devices.json"
OUTPUT = Path(__file__).parent / "devices-static.html"
MONTHS = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"]
def human(iso: str) -> str:
d = datetime.fromisoformat(iso).date()
return f"{MONTHS[d.month - 1]} {d.day}, {d.year}"
def esc(s: str) -> str:
return (s.replace("&", "&").replace("<", "<").replace(">", ">")
.replace('"', """).replace("'", "'"))
def is_ended(eol: date, as_of: date) -> bool:
"""The EOL date is the final supported calendar day."""
return eol < as_of
def sentence(d: dict, today: date) -> str:
plain_name = f"{d['brand']} {d['model']}"
clause = esc(support_sentence(d, today))
device_link = f'<a href="/device/{esc(d["id"])}/">{esc(plain_name)}</a>'
clause = clause.replace(esc(plain_name), f"<b>{device_link}</b>", 1)
qualifier = (
(d.get("support_window") or {}).get("provenance") or {}
).get("list_qualifier")
qualifier_html = (
f' <span class="source-scope">{esc(qualifier)}</span>' if qualifier else ""
)
return f"<li>{clause}{qualifier_html} (Released {human(d['released'])}.)</li>"
def main() -> int:
data = json.loads(INPUT.read_text())
devices = sorted(data["devices"], key=lambda x: x["released"], reverse=True)
today = date.fromisoformat(data["generated"])
n = len(devices)
brands = {}
counts = {
"supported": 0,
"ending": 0,
"guarantee_elapsed": 0,
"ended": 0,
"unknown": 0,
}
for d in devices:
brands[d["brand"]] = brands.get(d["brand"], 0) + 1
state = support_state(d, today)
if state not in counts:
raise ValueError(f"unhandled support state: {state}")
counts[state] += 1
brand_txt = ", ".join(f"{v} {k}" for k, v in sorted(brands.items()))
items = "\n".join(sentence(d, today) for d in devices)
html = f"""<!-- devices-static fragment · generated {data['generated']} · {n} devices · auto-generated by generate_static.py — do not hand-edit -->
<section id="agd-static">
<h2>Android security update end dates — full list</h2>
<p>As of {human(data['generated'])}, AndroidGuides.com tracks {n} Android devices ({brand_txt}):
{counts['supported']} have a stated support window beyond 12 months, {counts['ending']} have a stated support window ending within 12 months,
{counts['guarantee_elapsed']} have passed their stated guarantee period without a confirmed stop, {counts['ended']} are confirmed no longer supported,
and {counts['unknown']} have insufficient evidence for a current status.
Data refreshes monthly from manufacturer commitments and endoflife.date.</p>
<details>
<summary>Show all {n} devices as a plain text list</summary>
<ul>
{items}
</ul>
</details>
</section>
"""
OUTPUT.write_text(html, encoding="utf-8")
print(
f"[OK] wrote {OUTPUT} ({n} sentences; "
f"{counts['supported']} supported / {counts['ending']} ending / "
f"{counts['guarantee_elapsed']} guarantee elapsed / {counts['ended']} ended / "
f"{counts['unknown']} unknown)"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())