post to .profile

This commit is contained in:
Mark Eaton
2026-01-10 21:23:00 -05:00
parent 47f1d9e3ec
commit c40df69f9e
+79 -12
View File
@@ -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 <!--LANG_STATS_START--> <!--LANG_STATS_END-->.
in all repositories of a given account and updates the README.md
in the .profile repository between the markers
<!--LANG_STATS_START--> and <!--LANG_STATS_END-->.
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 = "<!--LANG_STATS_START-->"
MARKER_END = "<!--LANG_STATS_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}