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 Gitea Language Stats
Calculates the percentage of every programming language used Calculates the percentage of every programming language used
in all repositories of a given account and prints a markdown in all repositories of a given account and updates the README.md
table to stdout, delimited by the in the .profile repository between the markers
markers <!--LANG_STATS_START--> <!--LANG_STATS_END-->. <!--LANG_STATS_START--> and <!--LANG_STATS_END-->.
When run as a systemd job, it checks for new commits since the last run 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. 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 Required environment variables
-------------------------------- --------------------------------
GITEA_BASE_URL e.g. https://gitea.example.com 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 GITEA_USERNAME the account to analyse
Optional environment variables Optional environment variables
@@ -20,8 +20,10 @@ Optional environment variables
STATE_FILE path to store last-run state (default: ~/.local/state/language_stats.json) STATE_FILE path to store last-run state (default: ~/.local/state/language_stats.json)
""" """
import argparse import argparse
import base64
import json import json
import os import os
import re
import sys import sys
from collections import defaultdict from collections import defaultdict
from pathlib import Path from pathlib import Path
@@ -38,6 +40,14 @@ USERNAME = os.getenv("GITEA_USERNAME")
DEFAULT_STATE_DIR = Path.home() / ".local" / "state" DEFAULT_STATE_DIR = Path.home() / ".local" / "state"
STATE_FILE = Path(os.getenv("STATE_FILE", DEFAULT_STATE_DIR / "language_stats.json")) 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. # List of files whose byte-counts should be excluded from the totals.
# Each tuple: (repository_name, file_path, language_name) # Each tuple: (repository_name, file_path, language_name)
EXCLUDE_FILES: list[tuple[str, str, str]] = [ 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]: def get_repo_timestamps(repos: list[dict]) -> dict[str, str]:
"""Extract repository names and their updated_at timestamps.""" """Extract repository names and their updated_at timestamps.
return {repo["name"]: repo.get("updated_at", "") for repo in repos}
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: def load_state() -> dict:
@@ -151,11 +169,11 @@ def subtract_excluded_files(session: requests.Session, totals: dict[str, int]) -
return totals 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.""" """Generate markdown output for language statistics."""
total_bytes = sum(totals.values()) total_bytes = sum(totals.values())
if total_bytes == 0: if total_bytes == 0:
return [] return ""
pairs = sorted(totals.items(), key=lambda x: x[1], reverse=True) 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 bar = "" * int(pct / 2) # up to 50 chars
lines.append(f"{bar} {lang}{pct:5.1f}%\\") 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: def main() -> int:
@@ -213,9 +275,14 @@ def main() -> int:
if sum(totals.values()) == 0: if sum(totals.values()) == 0:
sys.exit("No language data returned from API.") sys.exit("No language data returned from API.")
lines = generate_markdown(totals) markdown = generate_markdown(totals)
for line in lines:
print(line) # 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 # Save current state for next run
new_state = {"repo_timestamps": current_timestamps} new_state = {"repo_timestamps": current_timestamps}