MicroFish/scripts/star_history.py

1490 lines
61 KiB
Python
Executable File

#!/usr/bin/env python3
"""Generate and maintain a repository-owned Star History chart.
The one-time ``backfill`` command queries only opaque edge cursors and
``starredAt`` timestamps. Scheduled updates receive one aggregate count from the
standalone fetch-only helper, then record and render without credentials. No
stargazer identity is persisted.
"""
from __future__ import annotations
import argparse
import base64
import hashlib
import html
import json
import math
import os
import re
import stat
import subprocess
import sys
import tempfile
import xml.etree.ElementTree as ET
from collections import Counter
from dataclasses import dataclass
from datetime import date, datetime, time, timedelta, timezone
from pathlib import Path
from typing import Any, Mapping, Protocol, Sequence
REPOSITORY = "666ghj/MiroFish"
REPOSITORY_OWNER, REPOSITORY_NAME = REPOSITORY.split("/", 1)
INTERVAL_DAYS = 13
STATE_RELATIVE = Path(".github/star-history/history.json")
LIGHT_SVG_RELATIVE = Path("static/image/star-history-light.svg")
DARK_SVG_RELATIVE = Path("static/image/star-history-dark.svg")
OUTPUT_RELATIVES = (STATE_RELATIVE, LIGHT_SVG_RELATIVE, DARK_SVG_RELATIVE)
MAX_STATE_BYTES = 5_000_000
MAX_COUNT_FILE_BYTES = 64
MAX_STAR_COUNT = (1 << 63) - 1
PAGE_SIZE = 100
RATE_LIMIT_RESERVE = 20
UTC = timezone.utc
# A reviewed snapshot of the repository owner's public GitHub avatar. The
# bootstrapper injects the bounded PNG/JPEG bytes and their digest into this
# template so scheduled rendering remains offline and the SVG self-contained.
OWNER_AVATAR_BASE64 = "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"
OWNER_AVATAR_MEDIA_TYPE = "image/jpeg"
OWNER_AVATAR_DATA_URI = (
f"data:{OWNER_AVATAR_MEDIA_TYPE};base64,{OWNER_AVATAR_BASE64}"
)
OWNER_AVATAR_SHA256 = "0b469b43ffc2e2dad3ea63970b3483f38c0199162ff46819cfa19a0237bb9072"
OWNER_AVATAR_DIMENSIONS = (64, 64)
MAX_INLINE_AVATAR_BYTES = 64_000
# Star History's reviewed 64x64 RGBA watermark icon, embedded exactly as used
# by the upstream MIT-licensed renderer (Copyright 2025 Star History). The
# corresponding license notice is retained in THIRD_PARTY_NOTICES.md.
WATERMARK_LOGO_BASE64 = (
"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"
"MjY6MTQtMDU6MDDygtFyAAAAAElFTkSuQmCC"
)
WATERMARK_LOGO_DATA_URI = f"data:image/png;base64,{WATERMARK_LOGO_BASE64}"
WATERMARK_LOGO_SHA256 = "02d30436a381b85e3e8beda75b06bd40ab98b14a419f293688ef32931b639bad"
WATERMARK_LOGO_DIMENSIONS = (64, 64)
MAX_INLINE_WATERMARK_BYTES = 8_192
STATE_TIMESTAMP_RE = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$")
GRAPHQL_QUERY = """\
query StarTimes($owner: String!, $name: String!, $after: String) {
repository(owner: $owner, name: $name) {
stargazers(
first: 100
after: $after
orderBy: {field: STARRED_AT, direction: DESC}
) {
totalCount
edges { cursor starredAt }
pageInfo { hasNextPage endCursor }
}
}
rateLimit { cost remaining resetAt }
}
"""
class StarHistoryError(RuntimeError):
"""A safe, user-facing error that never includes secrets."""
@dataclass(frozen=True)
class StargazerEdge:
cursor: str
starred_at: datetime
@dataclass(frozen=True)
class StargazerPage:
total_count: int
edges: tuple[StargazerEdge, ...]
has_next_page: bool
end_cursor: str | None
rate_remaining: int
@dataclass(frozen=True)
class Result:
changed: bool
due: bool | None
message: str
class Clock(Protocol):
def now(self) -> datetime: ...
class GitHubGateway(Protocol):
def fetch_stargazer_page(self, after: str | None) -> StargazerPage: ...
class CommandRunner(Protocol):
def run(self, arguments: Sequence[str]) -> subprocess.CompletedProcess[str]: ...
class SystemClock:
def now(self) -> datetime:
return datetime.now(UTC).replace(microsecond=0)
class SubprocessCommandRunner:
def run(self, arguments: Sequence[str]) -> subprocess.CompletedProcess[str]:
try:
return subprocess.run(
list(arguments),
check=False,
capture_output=True,
text=True,
encoding="utf-8",
timeout=45,
)
except FileNotFoundError as exc:
raise StarHistoryError("GitHub CLI (gh) is required for backfill") from exc
except subprocess.TimeoutExpired as exc:
raise StarHistoryError("GitHub GraphQL request timed out") from exc
class GhGraphQLGateway:
"""Production adapter for the one-time, maintainer-authorized backfill."""
def __init__(self, runner: CommandRunner | None = None) -> None:
self._runner = runner or SubprocessCommandRunner()
def fetch_stargazer_page(self, after: str | None) -> StargazerPage:
arguments = [
"gh",
"api",
"graphql",
"-f",
f"query={GRAPHQL_QUERY}",
"-f",
f"owner={REPOSITORY_OWNER}",
"-f",
f"name={REPOSITORY_NAME}",
]
if after is not None:
if not after or "\n" in after or "\r" in after:
raise StarHistoryError("GitHub returned an invalid pagination cursor")
arguments.extend(("-f", f"after={after}"))
completed = self._runner.run(arguments)
if completed.returncode != 0:
raise StarHistoryError(
f"GitHub GraphQL request failed (exit {completed.returncode})"
)
try:
payload = json.loads(completed.stdout)
except json.JSONDecodeError as exc:
raise StarHistoryError("GitHub GraphQL returned malformed JSON") from exc
if not isinstance(payload, dict) or payload.get("errors"):
raise StarHistoryError("GitHub GraphQL rejected the stargazer request")
try:
data = payload["data"]
repository = data["repository"]
stargazers = repository["stargazers"]
rate_limit = data["rateLimit"]
raw_edges = stargazers["edges"]
page_info = stargazers["pageInfo"]
except (KeyError, TypeError) as exc:
raise StarHistoryError("GitHub GraphQL response had an unexpected shape") from exc
if not all(
isinstance(value, dict)
for value in (data, repository, stargazers, rate_limit, page_info)
):
raise StarHistoryError("GitHub GraphQL response had an unexpected shape")
total_count = _strict_non_negative_int(
stargazers.get("totalCount"), "GraphQL totalCount"
)
rate_remaining = _strict_non_negative_int(
rate_limit.get("remaining"), "GraphQL rate remaining"
)
if not isinstance(raw_edges, list):
raise StarHistoryError("GitHub GraphQL edges were not a list")
edges: list[StargazerEdge] = []
for raw_edge in raw_edges:
if not isinstance(raw_edge, dict):
raise StarHistoryError("GitHub GraphQL returned an invalid edge")
cursor = raw_edge.get("cursor")
starred_at = raw_edge.get("starredAt")
if not isinstance(cursor, str) or not cursor:
raise StarHistoryError("GitHub GraphQL returned an invalid edge cursor")
if not isinstance(starred_at, str):
raise StarHistoryError("GitHub GraphQL returned an invalid star timestamp")
edges.append(StargazerEdge(cursor, _parse_github_timestamp(starred_at)))
has_next_page = page_info.get("hasNextPage")
end_cursor = page_info.get("endCursor")
if type(has_next_page) is not bool:
raise StarHistoryError("GitHub GraphQL returned invalid page information")
if end_cursor is not None and not isinstance(end_cursor, str):
raise StarHistoryError("GitHub GraphQL returned an invalid page cursor")
if has_next_page and not end_cursor:
raise StarHistoryError("GitHub GraphQL omitted the next page cursor")
return StargazerPage(
total_count=total_count,
edges=tuple(edges),
has_next_page=has_next_page,
end_cursor=end_cursor,
rate_remaining=rate_remaining,
)
def _strict_non_negative_int(value: Any, label: str) -> int:
if type(value) is not int or value < 0:
raise StarHistoryError(f"{label} must be a non-negative integer")
return value
def _parse_github_timestamp(value: str) -> datetime:
try:
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError as exc:
raise StarHistoryError("GitHub returned an invalid star timestamp") from exc
if parsed.tzinfo is None or parsed.utcoffset() != timedelta(0):
raise StarHistoryError("GitHub star timestamp was not UTC")
return parsed.astimezone(UTC)
def _parse_state_timestamp(value: Any, label: str) -> datetime:
if not isinstance(value, str) or not STATE_TIMESTAMP_RE.fullmatch(value):
raise StarHistoryError(f"{label} must use YYYY-MM-DDTHH:MM:SSZ")
try:
parsed = datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=UTC)
except ValueError as exc:
raise StarHistoryError(f"{label} is not a valid UTC timestamp") from exc
return parsed
def _format_state_timestamp(value: datetime) -> str:
normalized = _normalize_now(value)
return normalized.strftime("%Y-%m-%dT%H:%M:%SZ")
def _normalize_now(value: datetime) -> datetime:
if value.tzinfo is None or value.utcoffset() != timedelta(0):
raise StarHistoryError("clock must return a UTC datetime")
return value.astimezone(UTC).replace(microsecond=0)
def _expect_keys(value: Mapping[str, Any], expected: set[str], label: str) -> None:
actual = set(value)
if actual != expected:
raise StarHistoryError(f"{label} contains missing or unknown fields")
def validate_state(state: Any) -> None:
if not isinstance(state, dict):
raise StarHistoryError("history state must be a JSON object")
_expect_keys(
state,
{
"schema_version",
"repository",
"timezone",
"ongoing_interval_days",
"reconstruction",
"snapshots",
},
"history state",
)
if state["schema_version"] != 1 or type(state["schema_version"]) is not int:
raise StarHistoryError("unsupported history schema_version")
if state["repository"] != REPOSITORY:
raise StarHistoryError("history repository does not match configured repository")
if state["timezone"] != "UTC":
raise StarHistoryError("history timezone must be UTC")
if (
state["ongoing_interval_days"] != INTERVAL_DAYS
or type(state["ongoing_interval_days"]) is not int
):
raise StarHistoryError(
f"history interval must be exactly {INTERVAL_DAYS} days"
)
reconstruction = state["reconstruction"]
if not isinstance(reconstruction, dict):
raise StarHistoryError("reconstruction must be an object")
_expect_keys(
reconstruction,
{"method", "generated_at", "daily"},
"reconstruction",
)
reconstruction_method = reconstruction["method"]
if reconstruction_method not in {
"current_stargazers_starred_at",
"aggregate_snapshot_only",
}:
raise StarHistoryError("unsupported reconstruction method")
generated_at = _parse_state_timestamp(
reconstruction["generated_at"], "reconstruction.generated_at"
)
daily = reconstruction["daily"]
if not isinstance(daily, list):
raise StarHistoryError("reconstruction.daily must be a list")
if reconstruction_method == "aggregate_snapshot_only" and daily:
raise StarHistoryError("aggregate-only history cannot contain reconstructed dates")
previous_day: date | None = None
previous_stars = 0
for index, raw_point in enumerate(daily):
if not isinstance(raw_point, dict):
raise StarHistoryError("reconstruction point must be an object")
_expect_keys(raw_point, {"date", "stars"}, "reconstruction point")
raw_date = raw_point["date"]
if not isinstance(raw_date, str):
raise StarHistoryError("reconstruction date must be a string")
try:
point_day = date.fromisoformat(raw_date)
except ValueError as exc:
raise StarHistoryError("reconstruction date is invalid") from exc
if point_day.isoformat() != raw_date:
raise StarHistoryError("reconstruction date is not canonical")
stars = _strict_non_negative_int(raw_point["stars"], "reconstruction stars")
if index == 0 and stars <= 0:
raise StarHistoryError("first reconstruction point must have stars")
if previous_day is not None and point_day <= previous_day:
raise StarHistoryError("reconstruction dates must be strictly increasing")
if index > 0 and stars <= previous_stars:
raise StarHistoryError("reconstruction stars must be strictly increasing")
if point_day >= generated_at.date():
raise StarHistoryError("reconstruction must contain only completed UTC dates")
previous_day = point_day
previous_stars = stars
snapshots = state["snapshots"]
if not isinstance(snapshots, list):
raise StarHistoryError("snapshots must be a list")
previous_snapshot: datetime | None = None
first_snapshot: datetime | None = None
for raw_snapshot in snapshots:
if not isinstance(raw_snapshot, dict):
raise StarHistoryError("snapshot must be an object")
_expect_keys(raw_snapshot, {"at", "stars"}, "snapshot")
snapshot_at = _parse_state_timestamp(raw_snapshot["at"], "snapshot.at")
_strict_non_negative_int(raw_snapshot["stars"], "snapshot stars")
if previous_snapshot is not None and snapshot_at <= previous_snapshot:
raise StarHistoryError("snapshot timestamps must be strictly increasing")
if first_snapshot is None:
first_snapshot = snapshot_at
previous_snapshot = snapshot_at
if first_snapshot is not None:
if first_snapshot < generated_at:
raise StarHistoryError("first snapshot cannot predate reconstruction")
if previous_day is not None and previous_day >= first_snapshot.date():
raise StarHistoryError("reconstruction dates must predate snapshots")
def canonical_state_bytes(state: Mapping[str, Any]) -> bytes:
validate_state(state)
return (
json.dumps(state, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
).encode("utf-8")
def _safe_workspace(workspace: Path) -> Path:
try:
root = workspace.resolve(strict=True)
except OSError as exc:
raise StarHistoryError("workspace does not exist") from exc
if not root.is_dir():
raise StarHistoryError("workspace is not a directory")
return root
def _safe_target(workspace: Path, relative: Path, create_parent: bool) -> Path:
root = _safe_workspace(workspace)
if relative.is_absolute() or ".." in relative.parts:
raise StarHistoryError("output path escaped the workspace")
current = root
for part in relative.parts[:-1]:
current = current / part
if current.is_symlink():
raise StarHistoryError("output directory cannot be a symbolic link")
target = root / relative
if target.is_symlink():
raise StarHistoryError("output file cannot be a symbolic link")
if create_parent:
try:
target.parent.mkdir(parents=True, exist_ok=True)
except OSError as exc:
raise StarHistoryError("could not create output directory") from exc
current = root
for part in relative.parts[:-1]:
current = current / part
if current.is_symlink():
raise StarHistoryError("output directory cannot be a symbolic link")
if target.is_symlink():
raise StarHistoryError("output file cannot be a symbolic link")
try:
resolved_parent = target.parent.resolve(strict=False)
resolved_parent.relative_to(root)
except (OSError, ValueError) as exc:
raise StarHistoryError("output path escaped the workspace") from exc
return resolved_parent / target.name
def _read_limited(path: Path, limit: int, label: str) -> bytes:
try:
with path.open("rb") as handle:
payload = handle.read(limit + 1)
except FileNotFoundError as exc:
raise StarHistoryError(f"{label} is missing") from exc
except OSError as exc:
raise StarHistoryError(f"could not read {label}") from exc
if len(payload) > limit:
raise StarHistoryError(f"{label} exceeded the size limit")
return payload
def load_star_count_file(path: Path) -> int:
"""Read a tiny, symlink-safe decimal count produced by the fetch-only step."""
flags = os.O_RDONLY
if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW
try:
descriptor = os.open(path, flags)
except OSError as exc:
raise StarHistoryError("Star count file is missing or unsafe") from exc
try:
metadata = os.fstat(descriptor)
if not stat.S_ISREG(metadata.st_mode):
raise StarHistoryError("Star count file is not a regular file")
payload = os.read(descriptor, MAX_COUNT_FILE_BYTES + 1)
except OSError as exc:
raise StarHistoryError("could not read Star count file") from exc
finally:
os.close(descriptor)
if len(payload) > MAX_COUNT_FILE_BYTES:
raise StarHistoryError("Star count file exceeded the size limit")
if not re.fullmatch(rb"(?:0|[1-9][0-9]*)\n?", payload):
raise StarHistoryError("Star count file must contain one decimal integer")
count = int(payload)
if count > MAX_STAR_COUNT:
raise StarHistoryError("Star count exceeded the supported range")
return count
def load_state(workspace: Path, require_canonical: bool = True) -> dict[str, Any]:
state_path = _safe_target(workspace, STATE_RELATIVE, create_parent=False)
payload = _read_limited(state_path, MAX_STATE_BYTES, "history state")
try:
state = json.loads(payload)
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise StarHistoryError("history state is not valid UTF-8 JSON") from exc
validate_state(state)
if require_canonical and canonical_state_bytes(state) != payload:
raise StarHistoryError("history state is not canonically formatted")
return state
def _snapshot_due(state: Mapping[str, Any], now: datetime) -> bool:
normalized = _normalize_now(now)
generated_at = _parse_state_timestamp(
state["reconstruction"]["generated_at"], "reconstruction.generated_at"
)
if normalized < generated_at:
raise StarHistoryError("clock is earlier than the reconstruction timestamp")
snapshots = state["snapshots"]
if not snapshots:
return True
latest = _parse_state_timestamp(snapshots[-1]["at"], "snapshot.at")
if normalized < latest:
raise StarHistoryError("clock is earlier than the latest snapshot")
return normalized - latest >= timedelta(days=INTERVAL_DAYS)
def _build_backfill_state(github: GitHubGateway, now: datetime) -> dict[str, Any]:
normalized_now = _normalize_now(now)
after: str | None = None
seen_page_cursors: set[str] = set()
seen_edge_cursors: set[str] = set()
daily_increments: Counter[date] = Counter()
initial_total: int | None = None
page_number = 0
while True:
page = github.fetch_stargazer_page(after)
page_number += 1
if initial_total is None:
initial_total = page.total_count
pages_required = math.ceil(initial_total / PAGE_SIZE)
remaining_requests = max(0, pages_required - 1)
if page.rate_remaining < remaining_requests + RATE_LIMIT_RESERVE:
raise StarHistoryError("insufficient GitHub GraphQL rate limit for backfill")
for edge in page.edges:
if edge.cursor in seen_edge_cursors:
raise StarHistoryError("GitHub GraphQL repeated an edge cursor")
seen_edge_cursors.add(edge.cursor)
if edge.starred_at.date() < normalized_now.date():
daily_increments[edge.starred_at.date()] += 1
if page.end_cursor is not None:
if page.end_cursor in seen_page_cursors:
raise StarHistoryError("GitHub GraphQL repeated a page cursor")
seen_page_cursors.add(page.end_cursor)
if not page.has_next_page:
break
if page.end_cursor is None:
raise StarHistoryError("GitHub GraphQL omitted the next page cursor")
after = page.end_cursor
if page_number > 10_000:
raise StarHistoryError("GitHub GraphQL exceeded the page safety limit")
if initial_total is None:
raise StarHistoryError("GitHub GraphQL returned no pages")
if len(seen_edge_cursors) != initial_total:
raise StarHistoryError("stargazer list changed or was incomplete during backfill")
running = 0
daily: list[dict[str, Any]] = []
for point_day in sorted(daily_increments):
running += daily_increments[point_day]
daily.append({"date": point_day.isoformat(), "stars": running})
state: dict[str, Any] = {
"schema_version": 1,
"repository": REPOSITORY,
"timezone": "UTC",
"ongoing_interval_days": INTERVAL_DAYS,
"reconstruction": {
"method": "current_stargazers_starred_at",
"generated_at": _format_state_timestamp(normalized_now),
"daily": daily,
},
"snapshots": [],
}
validate_state(state)
return state
def _build_initial_state(now: datetime, stars: int) -> dict[str, Any]:
"""Start an honest history when timestamp backfill is unavailable."""
normalized_now = _normalize_now(now)
checked_stars = _strict_non_negative_int(stars, "stargazers_count")
if checked_stars > MAX_STAR_COUNT:
raise StarHistoryError("Star count exceeded the supported range")
timestamp = _format_state_timestamp(normalized_now)
state: dict[str, Any] = {
"schema_version": 1,
"repository": REPOSITORY,
"timezone": "UTC",
"ongoing_interval_days": INTERVAL_DAYS,
"reconstruction": {
"method": "aggregate_snapshot_only",
"generated_at": timestamp,
"daily": [],
},
"snapshots": [{"at": timestamp, "stars": checked_stars}],
}
validate_state(state)
return state
def _updated_with_snapshot(
state: Mapping[str, Any], now: datetime, stars: int
) -> dict[str, Any]:
normalized_now = _normalize_now(now)
_strict_non_negative_int(stars, "stargazers_count")
updated = json.loads(json.dumps(state))
snapshots: list[dict[str, Any]] = updated["snapshots"]
new_snapshot = {
"at": _format_state_timestamp(normalized_now),
"stars": stars,
}
if snapshots:
latest_at = _parse_state_timestamp(snapshots[-1]["at"], "snapshot.at")
if normalized_now < latest_at:
raise StarHistoryError("clock is earlier than the latest snapshot")
if normalized_now.date() == latest_at.date():
if snapshots[-1]["stars"] == stars:
return updated
snapshots[-1] = new_snapshot
else:
snapshots.append(new_snapshot)
else:
snapshots.append(new_snapshot)
validate_state(updated)
return updated
@dataclass(frozen=True)
class ChartPoint:
at: datetime
stars: int
source: str
def _chart_points(state: Mapping[str, Any]) -> list[ChartPoint]:
points: list[ChartPoint] = []
for item in state["reconstruction"]["daily"]:
point_day = date.fromisoformat(item["date"])
end_of_day = datetime.combine(point_day + timedelta(days=1), time.min, UTC)
points.append(ChartPoint(end_of_day, item["stars"], "reconstruction"))
for item in state["snapshots"]:
points.append(
ChartPoint(
_parse_state_timestamp(item["at"], "snapshot.at"),
item["stars"],
"snapshot",
)
)
points.sort(key=lambda point: point.at)
return points
def _nice_y_axis(maximum: int) -> tuple[int, int]:
if maximum <= 0:
return 1, 5
raw = maximum / 5
exponent = math.floor(math.log10(raw)) if raw > 0 else 0
base = 10**exponent
fraction = raw / base
if fraction <= 1:
multiplier = 1.0
elif fraction <= 2:
multiplier = 2.0
elif fraction <= 2.5:
multiplier = 2.5
elif fraction <= 5:
multiplier = 5.0
else:
multiplier = 10.0
step = max(1, int(multiplier * base))
top = max(step, math.ceil(maximum / step) * step)
return step, top
def _format_number(value: int) -> str:
if value >= 1_000_000:
return f"{value / 1_000_000:.1f}m".replace(".0m", "m")
if value >= 1_000:
return f"{value / 1_000:.1f}k".replace(".0k", "k")
return str(value)
def _format_float(value: float) -> str:
rendered = f"{value:.2f}".rstrip("0").rstrip(".")
return rendered if rendered != "-0" else "0"
def _x_tick_label(value: datetime, span_days: float) -> str:
months = (
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
)
weekdays = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
if span_days >= 365:
return f"{months[value.month - 1]} {value.year}"
if span_days >= 14:
return f"{value.day:02d} {months[value.month - 1]}"
return f"{weekdays[value.weekday()]} {value.day:02d}"
def _sign(value: float) -> int:
if value < 0:
return -1
if value > 0:
return 1
return 0
def _monotone_x_path(points: Sequence[tuple[float, float]]) -> str:
"""Return a D3 curveMonotoneX-equivalent SVG path.
D3 uses Steffen monotonic interpolation: interior tangents are limited so a
smooth cubic cannot overshoot a monotonic run. This small implementation
keeps the Star History curve shape without adding a JavaScript dependency.
"""
normalized: list[tuple[float, float]] = []
for x, y in points:
if not math.isfinite(x) or not math.isfinite(y):
raise StarHistoryError("chart coordinates must be finite")
if normalized and x < normalized[-1][0]:
raise StarHistoryError("chart coordinates must be ordered")
if normalized and x == normalized[-1][0]:
normalized[-1] = (x, y)
else:
normalized.append((x, y))
if not normalized:
return ""
start_x, start_y = normalized[0]
commands = [f"M{_format_float(start_x)},{_format_float(start_y)}"]
if len(normalized) == 1:
return "".join(commands)
if len(normalized) == 2:
end_x, end_y = normalized[1]
commands.append(f"L{_format_float(end_x)},{_format_float(end_y)}")
return "".join(commands)
secants = [
(normalized[index + 1][1] - normalized[index][1])
/ (normalized[index + 1][0] - normalized[index][0])
for index in range(len(normalized) - 1)
]
tangents = [0.0] * len(normalized)
for index in range(1, len(normalized) - 1):
h0 = normalized[index][0] - normalized[index - 1][0]
h1 = normalized[index + 1][0] - normalized[index][0]
slope0 = secants[index - 1]
slope1 = secants[index]
weighted = (slope0 * h1 + slope1 * h0) / (h0 + h1)
tangents[index] = (_sign(slope0) + _sign(slope1)) * min(
abs(slope0), abs(slope1), 0.5 * abs(weighted)
)
tangents[0] = (3 * secants[0] - tangents[1]) / 2
tangents[-1] = (3 * secants[-1] - tangents[-2]) / 2
for index in range(len(normalized) - 1):
x0, y0 = normalized[index]
x1, y1 = normalized[index + 1]
third = (x1 - x0) / 3
control1_x = x0 + third
control1_y = y0 + third * tangents[index]
control2_x = x1 - third
control2_y = y1 - third * tangents[index + 1]
commands.append(
"C"
f"{_format_float(control1_x)},{_format_float(control1_y)} "
f"{_format_float(control2_x)},{_format_float(control2_y)} "
f"{_format_float(x1)},{_format_float(y1)}"
)
return "".join(commands)
def render_svg(state: Mapping[str, Any], theme: str) -> bytes:
"""Render the dependency-free Star History-compatible SVG.
The visual contract is a clean-room Python reimplementation of the MIT
licensed ``star-history/star-history`` renderer behavior reviewed for this
setup. No narayann7 JavaScript, npm package, or runtime dependency is
vendored or executed here.
"""
validate_state(state)
if theme not in {"light", "dark"}:
raise StarHistoryError("unsupported SVG theme")
width = 800.0
height = 533.333
plot_left = 70.0
plot_top = 60.0
plot_width = 700.0
plot_height = 423.333
plot_bottom = plot_top + plot_height
if theme == "light":
background = "#ffffff"
foreground = "#000000"
legend_background = "#ffffff"
line_color = "#dd4528"
else:
background = "#0d1117"
foreground = "#ffffff"
legend_background = "#0d1117"
line_color = "#ff6b6b"
points = _chart_points(state)
generated_at = _parse_state_timestamp(
state["reconstruction"]["generated_at"], "reconstruction.generated_at"
)
if points:
x_min = points[0].at
x_max = points[-1].at
if x_max <= x_min:
# A one-instant history still needs a visible time domain, but the
# display padding must not become a fabricated zero-Star sample.
try:
x_min = points[0].at - timedelta(days=1)
except OverflowError:
pass
try:
x_max = points[-1].at + timedelta(days=1)
except OverflowError:
pass
maximum = max(point.stars for point in points)
else:
try:
x_min = generated_at - timedelta(days=1)
x_max = generated_at
except OverflowError:
x_min = generated_at
x_max = generated_at + timedelta(days=1)
maximum = 0
y_step, empty_y_top = _nice_y_axis(maximum)
y_domain = maximum if maximum > 0 else empty_y_top
x_span = max(1.0, (x_max - x_min).total_seconds())
def x_coord(value: datetime) -> float:
return plot_left + (
(value - x_min).total_seconds() / x_span
) * plot_width
def y_coord(value: int) -> float:
return plot_bottom - (value / y_domain) * plot_height
line_coordinates = [
(x_coord(point.at), y_coord(point.stars)) for point in points
]
if len({x for x, _ in line_coordinates}) == 1 and line_coordinates:
# SVG does not paint a path containing only a move command. Draw a
# small horizontal mark centred on the sole real sample instead.
x, y = line_coordinates[-1]
line_path = (
f"M{_format_float(x - 4)},{_format_float(y)}"
f"H{_format_float(x + 4)}"
)
else:
line_path = _monotone_x_path(line_coordinates)
y_ticks: list[str] = []
y_tick_limit = maximum if maximum > 0 else 5
for value in range(y_step, y_tick_limit + 1, y_step):
y = y_coord(value)
y_ticks.append(
f'<line x1="69" y1="{_format_float(y)}" x2="70" '
f'y2="{_format_float(y)}" stroke="{foreground}"/>'
)
y_ticks.append(
f'<text x="63" y="{_format_float(y + 5)}" text-anchor="end" '
f'font-size="16" fill="{foreground}">{_format_number(value)}</text>'
)
x_ticks: list[str] = []
seen_x_labels: set[str] = set()
span_days = (x_max - x_min).total_seconds() / 86400
tick_count = min(6, max(2, math.ceil(span_days) + 1))
for index in range(tick_count):
ratio = index / (tick_count - 1)
value = x_min + (x_max - x_min) * ratio
label = _x_tick_label(value, span_days)
if label in seen_x_labels:
continue
seen_x_labels.add(label)
x = x_coord(value)
if index == 0:
anchor = "start"
elif index == tick_count - 1:
anchor = "end"
else:
anchor = "middle"
x_ticks.append(
f'<text x="{_format_float(x)}" y="{_format_float(plot_bottom + 18)}" '
f'text-anchor="{anchor}" font-size="16" fill="{foreground}">'
f'{html.escape(label)}</text>'
)
if state["reconstruction"]["method"] == "current_stargazers_starred_at":
description = (
f"Star history for {REPOSITORY}. Dates reconstructed from starredAt "
"timestamps and later aggregate snapshots are rendered as one continuous "
"series. No individual stargazer identity is stored."
)
else:
description = (
f"Star history for {REPOSITORY}. The series starts from the first locally "
"recorded aggregate snapshot. No individual stargazer identity is stored."
)
font = "xkcd"
legend_width = max(
len(REPOSITORY) * 7.5 + 8 + 21,
len(REPOSITORY) * 7 + 8 + 14 + 6,
)
svg = "".join(
[
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 533.333" '
f'width="800" height="533.333" preserveAspectRatio="xMidYMid meet" '
f'role="img" aria-labelledby="title desc">',
f'<title id="title">{html.escape(REPOSITORY_NAME)} Star History</title>',
f'<desc id="desc">{html.escape(description)}</desc>',
f'<rect width="800" height="533.333" fill="{background}"/>',
'<defs>',
'<filter id="xkcdify" filterUnits="userSpaceOnUse" x="-5" y="-5" '
'width="100%" height="100%">',
'<feTurbulence type="fractalNoise" baseFrequency="0.05" result="noise"/>',
'<feDisplacementMap scale="5" xChannelSelector="R" yChannelSelector="G" '
'in="SourceGraphic" in2="noise"/>',
'</filter>',
'<clipPath id="clip-circle-title">'
'<circle r="11" cx="327" cy="23"/></clipPath>',
'</defs>',
f'<g font-family="{font}">',
f'<image x="316" y="12" width="22" height="22" '
f'href="{OWNER_AVATAR_DATA_URI}" clip-path="url(#clip-circle-title)"/>',
f'<text x="400" y="30" text-anchor="middle" font-size="20" '
f'font-weight="700" fill="{foreground}">Star History</text>',
f'<path d="M{_format_float(plot_left)},{_format_float(plot_bottom)}'
f'H{_format_float(plot_left + plot_width)}" fill="none" '
f'stroke="{foreground}" stroke-width="3" filter="url(#xkcdify)"/>',
f'<path d="M{_format_float(plot_left)},{_format_float(plot_bottom)}'
f'V{_format_float(plot_top)}" fill="none" stroke="{foreground}" '
f'stroke-width="3" filter="url(#xkcdify)"/>',
*y_ticks,
*x_ticks,
(
f'<path class="xkcd-chart-xyline" d="{line_path}" fill="none" '
f'stroke="{line_color}" stroke-width="3" stroke-linecap="round" '
f'stroke-linejoin="round" filter="url(#xkcdify)"/>'
if line_path
else ""
),
f'<rect x="78" y="65" width="{_format_float(legend_width)}" height="32" rx="5" '
f'fill="{legend_background}" fill-opacity="0.92" stroke="{foreground}" '
f'stroke-width="2" filter="url(#xkcdify)"/>',
f'<rect x="85" y="77" width="8" height="8" rx="2" fill="{line_color}" '
f'filter="url(#xkcdify)"/>',
f'<text x="99" y="85" font-size="15" fill="{foreground}">'
f'{html.escape(REPOSITORY)}</text>',
f'<text x="400" y="523.333" text-anchor="middle" font-size="17" '
f'fill="{foreground}">Date</text>',
f'<text x="22" y="272" text-anchor="middle" font-size="17" '
f'fill="{foreground}" transform="rotate(-90 22 272)">GitHub Stars</text>',
f'<image x="635" y="508.333" width="20" height="20" '
f'href="{WATERMARK_LOGO_DATA_URI}"/>',
'<text x="720" y="523.333" text-anchor="middle" font-size="16" '
'fill="#666666">star-history.com</text>',
"</g></svg>\n",
]
)
payload = svg.encode("utf-8")
_validate_svg(payload)
return payload
def _reviewed_avatar_dimensions(payload: bytes) -> tuple[int, int] | None:
if OWNER_AVATAR_MEDIA_TYPE == "image/png":
if (
payload.startswith(b"\x89PNG\r\n\x1a\n")
and len(payload) >= 33
and payload[8:12] == (13).to_bytes(4, "big")
and payload[12:16] == b"IHDR"
and payload.endswith(b"IEND\xaeB`\x82")
):
return (
int.from_bytes(payload[16:20], "big"),
int.from_bytes(payload[20:24], "big"),
)
return None
if OWNER_AVATAR_MEDIA_TYPE == "image/jpeg":
if not payload.startswith(b"\xff\xd8\xff") or not payload.endswith(b"\xff\xd9"):
return None
for marker in (0xC0, 0xC1, 0xC2, 0xC3, 0xC5, 0xC6, 0xC7, 0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF):
offset = payload.find(bytes((0xFF, marker)))
if offset >= 0 and offset + 9 <= len(payload):
return (
int.from_bytes(payload[offset + 7 : offset + 9], "big"),
int.from_bytes(payload[offset + 5 : offset + 7], "big"),
)
return None
return None
def _validate_svg(payload: bytes) -> None:
try:
decoded_payload = payload.decode("utf-8", errors="strict")
except UnicodeDecodeError as exc:
raise StarHistoryError("generated SVG must be strict UTF-8") from exc
if decoded_payload.startswith("\ufeff") or "\x00" in decoded_payload:
raise StarHistoryError("generated SVG must be canonical UTF-8")
upper_payload = decoded_payload.upper()
if (
"<!DOCTYPE" in upper_payload
or "<!ENTITY" in upper_payload
or "<?" in decoded_payload
):
raise StarHistoryError("generated SVG contains forbidden XML directives")
try:
root = ET.fromstring(decoded_payload)
except ET.ParseError as exc:
raise StarHistoryError("generated SVG is not valid XML") from exc
svg_namespace = "{http://www.w3.org/2000/svg}"
if root.tag != f"{svg_namespace}svg":
raise StarHistoryError("generated SVG root is invalid")
allowed_attributes: dict[str, set[str]] = {
"svg": {
"viewBox",
"width",
"height",
"preserveAspectRatio",
"role",
"aria-labelledby",
},
"title": {"id"},
"desc": {"id"},
"rect": {
"x",
"y",
"width",
"height",
"rx",
"fill",
"fill-opacity",
"stroke",
"stroke-width",
"filter",
},
"defs": set(),
"filter": {"id", "filterUnits", "x", "y", "width", "height"},
"feTurbulence": {"type", "baseFrequency", "result"},
"feDisplacementMap": {
"scale",
"xChannelSelector",
"yChannelSelector",
"in",
"in2",
},
"clipPath": {"id"},
"circle": {"r", "cx", "cy"},
"g": {"font-family"},
"image": {"x", "y", "width", "height", "href", "clip-path"},
"line": {"x1", "y1", "x2", "y2", "stroke"},
"path": {
"class",
"d",
"fill",
"stroke",
"stroke-width",
"stroke-linecap",
"stroke-linejoin",
"filter",
},
"text": {
"x",
"y",
"text-anchor",
"font-size",
"font-weight",
"fill",
"transform",
},
}
exact_attribute_values: dict[str, set[str]] = {
"filter": {"url(#xkcdify)"},
"fill": {
"none",
"#ffffff",
"#000000",
"#0d1117",
"#dd4528",
"#ff6b6b",
"#666666",
},
"stroke": {
"#ffffff",
"#000000",
"#dd4528",
"#ff6b6b",
},
"font-family": {"xkcd"},
"class": {"xkcd-chart-xyline"},
"id": {"title", "desc", "xkcdify", "clip-circle-title"},
"role": {"img"},
"aria-labelledby": {"title desc"},
"preserveAspectRatio": {"xMidYMid meet"},
"filterUnits": {"userSpaceOnUse"},
"type": {"fractalNoise"},
"result": {"noise"},
"in": {"SourceGraphic"},
"in2": {"noise"},
"xChannelSelector": {"R"},
"yChannelSelector": {"G"},
"baseFrequency": {"0.05"},
"scale": {"5"},
"fill-opacity": {"0.92"},
"stroke-linecap": {"round"},
"stroke-linejoin": {"round"},
"text-anchor": {"start", "middle", "end"},
"font-size": {"15", "16", "17", "20"},
"font-weight": {"700"},
"transform": {"rotate(-90 22 272)"},
}
avatar_count = 0
watermark_count = 0
for element in root.iter():
if not isinstance(element.tag, str) or not element.tag.startswith(svg_namespace):
raise StarHistoryError("generated SVG contains a foreign namespace")
local_name = element.tag[len(svg_namespace) :]
allowed_for_element = allowed_attributes.get(local_name)
if allowed_for_element is None:
raise StarHistoryError("generated SVG contains a forbidden element")
if local_name == "image":
avatar_attributes = {
"x": "316",
"y": "12",
"width": "22",
"height": "22",
"href": OWNER_AVATAR_DATA_URI,
"clip-path": "url(#clip-circle-title)",
}
watermark_attributes = {
"x": "635",
"y": "508.333",
"width": "20",
"height": "20",
"href": WATERMARK_LOGO_DATA_URI,
}
if element.attrib == avatar_attributes:
avatar_count += 1
elif element.attrib == watermark_attributes:
watermark_count += 1
else:
raise StarHistoryError("generated SVG contains an unreviewed image")
for raw_name, value in element.attrib.items():
if raw_name.startswith("{") or raw_name not in allowed_for_element:
raise StarHistoryError("generated SVG contains a forbidden attribute")
if (
not value.isascii()
or "\\" in value
or "/*" in value
or "*/" in value
or any(ord(character) < 0x20 for character in value)
):
raise StarHistoryError("generated SVG contains an unsafe attribute value")
exact_values = exact_attribute_values.get(raw_name)
if exact_values is not None and value not in exact_values:
raise StarHistoryError("generated SVG contains an unsafe attribute value")
if raw_name == "href":
if local_name != "image" or value not in {
OWNER_AVATAR_DATA_URI,
WATERMARK_LOGO_DATA_URI,
}:
raise StarHistoryError("generated SVG contains an external resource")
lowered = value.lower().replace(" ", "")
if "url(" in lowered and lowered not in {
"url(#xkcdify)",
"url(#clip-circle-title)",
}:
raise StarHistoryError("generated SVG contains an external resource")
if lowered.startswith(("http:", "https:", "//")):
raise StarHistoryError("generated SVG contains an external resource")
if avatar_count != 1 or watermark_count != 1:
raise StarHistoryError("generated SVG must contain both reviewed images")
try:
avatar = base64.b64decode(OWNER_AVATAR_BASE64, validate=True)
except ValueError as exc:
raise StarHistoryError("reviewed avatar data is invalid") from exc
if (
len(avatar) > MAX_INLINE_AVATAR_BYTES
or hashlib.sha256(avatar).hexdigest() != OWNER_AVATAR_SHA256
or _reviewed_avatar_dimensions(avatar) != OWNER_AVATAR_DIMENSIONS
):
raise StarHistoryError("reviewed avatar data is invalid")
try:
watermark = base64.b64decode(WATERMARK_LOGO_BASE64, validate=True)
except ValueError as exc:
raise StarHistoryError("reviewed watermark data is invalid") from exc
png_header = (
watermark.startswith(b"\x89PNG\r\n\x1a\n")
and watermark[8:12] == (13).to_bytes(4, "big")
and watermark[12:16] == b"IHDR"
and len(watermark) >= 33
)
watermark_dimensions = (
(
int.from_bytes(watermark[16:20], "big"),
int.from_bytes(watermark[20:24], "big"),
)
if png_header
else None
)
if (
len(watermark) > MAX_INLINE_WATERMARK_BYTES
or not png_header
or watermark_dimensions != WATERMARK_LOGO_DIMENSIONS
or watermark[24:29] != bytes((8, 6, 0, 0, 0))
or not watermark.endswith(b"IEND\xaeB`\x82")
or hashlib.sha256(watermark).hexdigest() != WATERMARK_LOGO_SHA256
):
raise StarHistoryError("reviewed watermark data is invalid")
def _output_payloads(state: Mapping[str, Any]) -> dict[Path, bytes]:
return {
STATE_RELATIVE: canonical_state_bytes(state),
LIGHT_SVG_RELATIVE: render_svg(state, "light"),
DARK_SVG_RELATIVE: render_svg(state, "dark"),
}
def _write_outputs(workspace: Path, state: Mapping[str, Any]) -> bool:
payloads = _output_payloads(state)
targets = {
relative: _safe_target(workspace, relative, create_parent=True)
for relative in OUTPUT_RELATIVES
}
if all(
target.exists() and target.read_bytes() == payloads[relative]
for relative, target in targets.items()
):
return False
temporary_paths: dict[Path, Path] = {}
try:
for relative in OUTPUT_RELATIVES:
target = targets[relative]
with tempfile.NamedTemporaryFile(
mode="wb",
dir=target.parent,
prefix=f".{target.name}.",
suffix=".tmp",
delete=False,
) as handle:
handle.write(payloads[relative])
handle.flush()
os.fsync(handle.fileno())
temporary_paths[relative] = Path(handle.name)
os.chmod(temporary_paths[relative], 0o644)
_validate_svg(temporary_paths[LIGHT_SVG_RELATIVE].read_bytes())
_validate_svg(temporary_paths[DARK_SVG_RELATIVE].read_bytes())
json.loads(temporary_paths[STATE_RELATIVE].read_bytes())
for relative in OUTPUT_RELATIVES:
checked_target = _safe_target(workspace, relative, create_parent=False)
if checked_target != targets[relative]:
raise StarHistoryError("output path changed during update")
os.replace(temporary_paths[relative], targets[relative])
temporary_paths.pop(relative, None)
except OSError as exc:
raise StarHistoryError("could not atomically replace Star History outputs") from exc
finally:
for temporary in temporary_paths.values():
try:
temporary.unlink(missing_ok=True)
except OSError:
pass
check_workspace(workspace)
return True
def check_workspace(workspace: Path) -> None:
state = load_state(workspace, require_canonical=True)
expected = _output_payloads(state)
for relative in OUTPUT_RELATIVES[1:]:
target = _safe_target(workspace, relative, create_parent=False)
actual = _read_limited(target, MAX_STATE_BYTES, str(relative))
_validate_svg(actual)
if actual != expected[relative]:
raise StarHistoryError(f"{relative} is not synchronized with history.json")
def execute(
command: str,
*,
github: GitHubGateway | None,
clock: Clock,
workspace: Path,
force: bool = False,
star_count: int | None = None,
) -> Result:
root = _safe_workspace(workspace)
now = _normalize_now(clock.now())
if command == "backfill":
state_target = _safe_target(root, STATE_RELATIVE, create_parent=False)
if state_target.exists():
raise StarHistoryError("history state already exists; refusing to overwrite backfill")
if github is None:
raise StarHistoryError("GitHub access is required for backfill")
state = _build_backfill_state(github, now)
changed = _write_outputs(root, state)
return Result(changed, True, "historical Star data was reconstructed")
if command == "initialize":
state_target = _safe_target(root, STATE_RELATIVE, create_parent=False)
if state_target.exists():
raise StarHistoryError("history state already exists; refusing to overwrite it")
if star_count is None:
raise StarHistoryError("a fetched Star count is required for initialization")
state = _build_initial_state(now, star_count)
changed = _write_outputs(root, state)
return Result(changed, True, "aggregate-only Star history was initialized")
if command == "due":
state = load_state(root)
due = _snapshot_due(state, now)
return Result(False, due, "true" if due else "false")
if command == "record":
state = load_state(root)
due = _snapshot_due(state, now)
if not due and not force:
return Result(False, False, "snapshot is not due")
if star_count is None:
raise StarHistoryError("a fetched Star count is required for recording")
checked_count = _strict_non_negative_int(star_count, "stargazers_count")
if checked_count > MAX_STAR_COUNT:
raise StarHistoryError("Star count exceeded the supported range")
updated = _updated_with_snapshot(state, now, checked_count)
if updated == state:
return Result(False, due, "same-day snapshot is unchanged")
changed = _write_outputs(root, updated)
return Result(changed, due, "Star snapshot and charts were updated")
if command == "check":
check_workspace(root)
return Result(False, None, "Star History outputs are valid")
raise StarHistoryError("unknown Star History command")
def _repository_root() -> Path:
return Path(__file__).resolve().parents[1]
def _production_gateway(command: str) -> GitHubGateway | None:
if command == "backfill":
return GhGraphQLGateway()
return None
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
subparsers = parser.add_subparsers(dest="command", required=True)
subparsers.add_parser("backfill", help="reconstruct history using maintainer access")
initialize = subparsers.add_parser(
"initialize", help="start an honest history from one aggregate count"
)
initialize.add_argument(
"--count-file", required=True, type=Path, help="file containing one decimal count"
)
subparsers.add_parser(
"due", help=f"print whether a {INTERVAL_DAYS}-day snapshot is due"
)
record = subparsers.add_parser(
"record", help="apply a fetched aggregate count without GitHub credentials"
)
record.add_argument(
"--count-file", required=True, type=Path, help="file containing one decimal count"
)
record.add_argument(
"--force",
action="store_true",
help=f"record before {INTERVAL_DAYS} days",
)
subparsers.add_parser("check", help="verify state and deterministic SVG files")
return parser
def main(argv: Sequence[str] | None = None) -> int:
arguments = build_parser().parse_args(argv)
try:
star_count = (
load_star_count_file(arguments.count_file)
if arguments.command in {"initialize", "record"}
else None
)
result = execute(
arguments.command,
github=_production_gateway(arguments.command),
clock=SystemClock(),
workspace=_repository_root(),
force=bool(getattr(arguments, "force", False)),
star_count=star_count,
)
except StarHistoryError as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
except Exception as exc: # Defensive: never print exception data that may hold a token.
print(f"error: unexpected internal error ({type(exc).__name__})", file=sys.stderr)
return 1
if arguments.command == "due":
print("true" if result.due else "false")
else:
print(result.message)
return 0
if __name__ == "__main__":
raise SystemExit(main())