From c40df69f9ef6732ab5b27bbce6b924051dad84f7 Mon Sep 17 00:00:00 2001 From: Mark Eaton Date: Sat, 10 Jan 2026 21:23:00 -0500 Subject: [PATCH] post to .profile --- scripts/language_stats.py | 91 +++++++++++++++++++++++++++++++++------ 1 file changed, 79 insertions(+), 12 deletions(-) diff --git a/scripts/language_stats.py b/scripts/language_stats.py index 7d22df4..ec6efd0 100644 --- a/scripts/language_stats.py +++ b/scripts/language_stats.py @@ -2,9 +2,9 @@ """ Gitea Language Stats Calculates the percentage of every programming language used -in all repositories of a given account and prints a markdown -table to stdout, delimited by the -markers . +in all repositories of a given account and updates the README.md +in the .profile repository between the markers + and . When run as a systemd job, it checks for new commits since the last run and only regenerates if there are changes. Use --force to always regenerate. @@ -12,7 +12,7 @@ and only regenerates if there are changes. Use --force to always regenerate. Required environment variables -------------------------------- GITEA_BASE_URL e.g. https://gitea.example.com -GITEA_TOKEN personal access-token with `repo` scope +GITEA_TOKEN personal access-token with `repo` scope (needs write access) GITEA_USERNAME the account to analyse Optional environment variables @@ -20,8 +20,10 @@ Optional environment variables STATE_FILE path to store last-run state (default: ~/.local/state/language_stats.json) """ import argparse +import base64 import json import os +import re import sys from collections import defaultdict from pathlib import Path @@ -38,6 +40,14 @@ USERNAME = os.getenv("GITEA_USERNAME") DEFAULT_STATE_DIR = Path.home() / ".local" / "state" STATE_FILE = Path(os.getenv("STATE_FILE", DEFAULT_STATE_DIR / "language_stats.json")) +# Profile repository where README will be updated +PROFILE_REPO = ".profile" +README_PATH = "README.md" + +# Markers for content replacement +MARKER_START = "" +MARKER_END = "" + # List of files whose byte-counts should be excluded from the totals. # Each tuple: (repository_name, file_path, language_name) EXCLUDE_FILES: list[tuple[str, str, str]] = [ @@ -80,8 +90,16 @@ def fetch_repos(session: requests.Session) -> list[dict]: def get_repo_timestamps(repos: list[dict]) -> dict[str, str]: - """Extract repository names and their updated_at timestamps.""" - return {repo["name"]: repo.get("updated_at", "") for repo in repos} + """Extract repository names and their updated_at timestamps. + + Excludes the profile repo to avoid infinite loops (updating it + would trigger another update). + """ + return { + repo["name"]: repo.get("updated_at", "") + for repo in repos + if repo["name"] != PROFILE_REPO + } def load_state() -> dict: @@ -151,11 +169,11 @@ def subtract_excluded_files(session: requests.Session, totals: dict[str, int]) - return totals -def generate_markdown(totals: dict[str, int]) -> list[str]: +def generate_markdown(totals: dict[str, int]) -> str: """Generate markdown output for language statistics.""" total_bytes = sum(totals.values()) if total_bytes == 0: - return [] + return "" pairs = sorted(totals.items(), key=lambda x: x[1], reverse=True) @@ -165,7 +183,51 @@ def generate_markdown(totals: dict[str, int]) -> list[str]: bar = "█" * int(pct / 2) # up to 50 chars lines.append(f"{bar} {lang}{pct:5.1f}%\\") - return lines + return "\n".join(lines) + + +def fetch_readme(session: requests.Session) -> tuple[str, str]: + """Fetch the README from the profile repository. + + Returns: + Tuple of (content, sha) where sha is needed for updating. + """ + resp = session.get( + f"{BASE_URL}/api/v1/repos/{USERNAME}/{PROFILE_REPO}/contents/{README_PATH}", + timeout=30, + ) + resp.raise_for_status() + data = resp.json() + content = base64.b64decode(data["content"]).decode("utf-8") + return content, data["sha"] + + +def replace_between_markers(content: str, new_content: str) -> str: + """Replace content between LANG_STATS markers.""" + pattern = re.compile( + rf"({re.escape(MARKER_START)})(.*?)({re.escape(MARKER_END)})", + re.DOTALL, + ) + replacement = rf"\1\n{new_content}\n\3" + new_readme, count = pattern.subn(replacement, content) + if count == 0: + raise ValueError(f"Markers {MARKER_START} and {MARKER_END} not found in README") + return new_readme + + +def update_readme(session: requests.Session, content: str, sha: str) -> None: + """Update the README in the profile repository.""" + encoded_content = base64.b64encode(content.encode("utf-8")).decode("ascii") + resp = session.put( + f"{BASE_URL}/api/v1/repos/{USERNAME}/{PROFILE_REPO}/contents/{README_PATH}", + json={ + "content": encoded_content, + "sha": sha, + "message": "Update language statistics", + }, + timeout=30, + ) + resp.raise_for_status() def main() -> int: @@ -213,9 +275,14 @@ def main() -> int: if sum(totals.values()) == 0: sys.exit("No language data returned from API.") - lines = generate_markdown(totals) - for line in lines: - print(line) + markdown = generate_markdown(totals) + + # Fetch current README, update it, and push + readme_content, readme_sha = fetch_readme(session) + updated_readme = replace_between_markers(readme_content, markdown) + update_readme(session, updated_readme, readme_sha) + + print("README.md updated successfully.", file=sys.stderr) # Save current state for next run new_state = {"repo_timestamps": current_timestamps}