"""
GitHub Projects Lister (made with clanker)
======================
Fetches all your GitHub repositories and lists them with
descriptions and languages/technologies used.
SETUP:
1. Set your GitHub username below (required)
2. Optionally set a Personal Access Token for private repos
→ Create one at: https://github.com/settings/tokens
(needs the 'repo' scope for private repos, or 'public_repo' for public only)
3. Choose your output format
USAGE:
pip install requests
python github_projects.py
"""
import sys
import requests
import json
import csv
import os
from datetime import datetime
# ─────────────────────────────────────────────
# CONFIGURATION — edit these values
# ─────────────────────────────────────────────
GITHUB_USERNAME = "your_username_here" # ← required
GITHUB_TOKEN = "" # ← optional (leave empty for public repos only)
OUTPUT_FORMAT = "markdown" # options: "terminal", "markdown", "json", "csv"
OUTPUT_FILE = "github_projects" # filename (without extension)
INCLUDE_FORKS = False # set True to include forked repos
INCLUDE_ARCHIVED = False # set True to include archived repos
SORT_BY = "updated" # options: "updated", "created", "pushed", "full_name"
# ─────────────────────────────────────────────
def fetch_all_repos(username: str, token: str = "") -> list[dict]:
"""Fetch all repos for a user, handling pagination."""
headers = {"Accept": "application/vnd.github+json"}
if token:
headers["Authorization"] = f"Bearer {token}"
repos = []
page = 1
while True:
url = f"https://api.github.com/users/{username}/repos"
params = {
"per_page": 100,
"page": page,
"sort": SORT_BY,
"direction": "desc",
"type": "owner", # only repos the user owns (not starred etc.)
}
response = requests.get(url, headers=headers, params=params)
if response.status_code == 401:
raise ValueError("❌ Invalid or expired GitHub token.")
elif response.status_code == 404:
raise ValueError(f"❌ GitHub user '{username}' not found.")
elif response.status_code != 200:
raise ValueError(f"❌ GitHub API error {response.status_code}: {response.text}")
page_repos = response.json()
if not page_repos:
break
repos.extend(page_repos)
page += 1
# If less than 100 results, we've hit the last page
if len(page_repos) < 100:
break
return repos
def fetch_repo_languages(repo: dict, headers: dict) -> list[str]:
"""Fetch the language breakdown for a single repo."""
lang_url = repo.get("languages_url", "")
if not lang_url:
return []
resp = requests.get(lang_url, headers=headers)
if resp.status_code == 200:
langs = resp.json()
# Return languages sorted by bytes of code (most used first)
return sorted(langs, key=langs.get, reverse=True)
return []
def build_project_list(repos: list[dict], token: str = "") -> list[dict]:
"""Filter repos and enrich them with language data."""
headers = {"Accept": "application/vnd.github+json"}
if token:
headers["Authorization"] = f"Bearer {token}"
projects = []
total = len(repos)
for i, repo in enumerate(repos, 1):
# Apply filters
if not INCLUDE_FORKS and repo.get("fork"):
continue
if not INCLUDE_ARCHIVED and repo.get("archived"):
continue
print(f" Fetching languages... ({i}/{total}) {repo['name']}", end="\r")
languages = fetch_repo_languages(repo, headers)
primary_lang = repo.get("language") or (languages[0] if languages else "—")
projects.append({
"name": repo["name"],
"description": repo.get("description") or "No description provided.",
"url": repo["html_url"],
"primary_language": primary_lang,
"all_languages": languages if languages else ([primary_lang] if primary_lang != "—" else []),
"topics": repo.get("topics", []),
"stars": repo.get("stargazers_count", 0),
"forks": repo.get("forks_count", 0),
"is_private": repo.get("private", False),
"updated_at": repo.get("updated_at", "")[:10], # YYYY-MM-DD
"created_at": repo.get("created_at", "")[:10],
})
print() # newline after progress indicator
return projects
# ─────────────────────────────────────────────
# OUTPUT FUNCTIONS
# ─────────────────────────────────────────────
def format_languages(project: dict) -> str:
langs = project["all_languages"]
if not langs:
return "—"
return ", ".join(langs)
def output_terminal(projects: list[dict]):
print(f"\n{'─'*70}")
print(f" GitHub Projects for @{GITHUB_USERNAME} ({len(projects)} repos)")
print(f"{'─'*70}\n")
for p in projects:
visibility = "🔒" if p["is_private"] else "🌐"
stars = f"⭐ {p['stars']}" if p["stars"] else ""
print(f"{visibility} {p['name']} {stars}")
print(f" {p['description']}")
print(f" 🛠 {format_languages(p)}")
if p["topics"]:
print(f" 🏷 {', '.join(p['topics'])}")
print(f" 🔗 {p['url']} │ Updated: {p['updated_at']}")
print()
def output_markdown(projects: list[dict], filepath: str):
lines = [
f"# GitHub Projects — @{GITHUB_USERNAME}",
f"\n> Generated on {datetime.now().strftime('%Y-%m-%d %H:%M')} • {len(projects)} repositories\n",
"---\n",
]
for p in projects:
visibility = "🔒 Private" if p["is_private"] else "🌐 Public"
stars = f" · ⭐ {p['stars']}" if p["stars"] else ""
lines.append(f"## [{p['name']}]({p['url']})")
lines.append(f"*{visibility}{stars} · Updated {p['updated_at']}*\n")
lines.append(f"{p['description']}\n")
lines.append(f"**Languages / Technologies:** {format_languages(p)}")
if p["topics"]:
lines.append(f"\n**Topics:** {', '.join(p['topics'])}")
lines.append("\n---\n")
with open(filepath, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
print(f"✅ Markdown saved → {filepath}")
def output_json(projects: list[dict], filepath: str):
with open(filepath, "w", encoding="utf-8") as f:
json.dump(projects, f, indent=2, ensure_ascii=False)
print(f"✅ JSON saved → {filepath}")
def output_csv(projects: list[dict], filepath: str):
fieldnames = ["name", "description", "primary_language", "all_languages",
"topics", "stars", "forks", "is_private", "updated_at", "url"]
with open(filepath, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
for p in projects:
row = {**p}
row["all_languages"] = ", ".join(p["all_languages"])
row["topics"] = ", ".join(p["topics"])
writer.writerow({k: row[k] for k in fieldnames})
print(f"✅ CSV saved → {filepath}")
# ─────────────────────────────────────────────
# MAIN
# ─────────────────────────────────────────────
def main():
if len(sys.argv) > 1:
GITHUB_USERNAME = sys.argv[1]
if GITHUB_USERNAME == "your_username_here":
print("⚠️ Please pass your GITHUB_USERNAME as argument.")
return
print(f"\n🔍 Fetching repos for @{GITHUB_USERNAME}...")
repos = fetch_all_repos(GITHUB_USERNAME, GITHUB_TOKEN)
print(f" Found {len(repos)} repo(s). Enriching with language data...")
projects = build_project_list(repos, GITHUB_TOKEN)
print(f" {len(projects)} project(s) after filtering.\n")
if not projects:
print("No projects to display after applying filters.")
return
fmt = OUTPUT_FORMAT.lower()
if fmt == "terminal":
output_terminal(projects)
elif fmt == "markdown":
path = f"{OUTPUT_FILE}.md"
output_terminal(projects) # also print to terminal
output_markdown(projects, path)
elif fmt == "json":
path = f"{OUTPUT_FILE}.json"
output_json(projects, path)
elif fmt == "csv":
path = f"{OUTPUT_FILE}.csv"
output_csv(projects, path)
else:
print(f"❌ Unknown OUTPUT_FORMAT '{fmt}'. Choose: terminal, markdown, json, csv")
if __name__ == "__main__":
main()