fastapi-codebase-qa / src /ingest.py
Islam Mamedov
Day 4: 42-question eval harness; fix max file size (routing.py was unindexed)
bbb66cb
Raw
History Blame Contribute Delete
5.68 kB
import argparse
import json
import os
import subprocess
from datetime import datetime, timedelta, timezone
from pathlib import Path
from github import Auth, Github
DATA_DIR = Path("data")
# Directories that add noise, not knowledge. Extend as you inspect results.
SKIP_DIRS = {".git", ".github", "__pycache__", "node_modules", "tests", "test",
"scripts"}
CODE_EXTS = {".py"}
DOC_EXTS = {".md"}
MAX_FILE_BYTES = 400_000 # skip generated/vendored monsters
ISSUE_LOOKBACK_DAYS = 730 # ~2 years
def clone_repo(repo: str) -> Path:
"""Shallow-clone the repo into data/repo/ (we only need current state)."""
dest = DATA_DIR / "repo"
if dest.exists():
print(f"[clone] {dest} already exists, skipping clone")
return dest
url = f"https://github.com/{repo}.git"
print(f"[clone] {url} -> {dest}")
subprocess.run(["git", "clone", "--depth", "1", url, str(dest)], check=True)
return dest
def collect_files(repo_dir: Path) -> list[dict]:
"""Walk the clone and collect code + doc files worth indexing."""
records = []
for path in sorted(repo_dir.rglob("*")):
if not path.is_file():
continue
rel = path.relative_to(repo_dir)
if any(part in SKIP_DIRS for part in rel.parts):
continue
# FastAPI-specific: keep only English docs, skip tutorial snippets
if rel.parts[0] == "docs" and rel.parts[1] != "en":
continue
if rel.parts[0] == "docs_src":
continue
ext = path.suffix.lower()
if ext in CODE_EXTS:
source_type = "code"
elif ext in DOC_EXTS:
source_type = "doc"
else:
continue
size = path.stat().st_size
if size == 0 or size > MAX_FILE_BYTES:
continue
records.append({
"source_type": source_type,
"path": str(rel),
"size_bytes": size,
})
return records
def fetch_issues(repo: str, max_issues: int) -> list[dict]:
"""Pull recent closed issues (not PRs) and save each as JSON.
Resumable: issues already saved to disk are skipped (no API calls),
so a crashed or interrupted run can simply be re-run.
"""
token = os.environ.get("GITHUB_TOKEN")
if not token:
print("[warn] no GITHUB_TOKEN set - unauthenticated limit is 60 req/hr")
gh = Github(auth=Auth.Token(token)) if token else Github()
since = datetime.now(timezone.utc) - timedelta(days=ISSUE_LOOKBACK_DAYS)
issues_dir = DATA_DIR / "issues"
issues_dir.mkdir(parents=True, exist_ok=True)
records = []
# sort="comments" surfaces the most-discussed (usually most useful) issues first
issues = gh.get_repo(repo).get_issues(state="closed", since=since,
sort="comments", direction="desc")
for issue in issues:
if issue.pull_request is not None: # PRs come through the same API; skip
continue
if len(records) >= max_issues:
break
# Resume support: skip issues already saved from a previous run
out_path = issues_dir / f"{issue.number}.json"
if out_path.exists():
records.append({"source_type": "issue",
"path": f"issues/{issue.number}.json",
"url": issue.html_url})
continue
# Keep up to 3 comments, favoring the most 👍-reacted (often the answer)
comments = []
if issue.comments > 0:
all_comments = list(issue.get_comments())
all_comments.sort(key=lambda c: c.reactions.get("+1", 0), reverse=True)
comments = [c.body for c in all_comments[:3] if c.body]
record = {
"number": issue.number,
"title": issue.title,
"body": issue.body or "",
"labels": [label.name for label in issue.labels],
"url": issue.html_url,
"comments": comments,
}
out_path.write_text(
json.dumps(record, ensure_ascii=False, indent=2), encoding="utf-8")
records.append({"source_type": "issue",
"path": f"issues/{issue.number}.json",
"url": issue.html_url})
if len(records) % 50 == 0:
print(f"[issues] fetched {len(records)}...")
return records
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--repo", required=True, help="e.g. fastapi/fastapi")
parser.add_argument("--max-issues", type=int, default=300)
parser.add_argument("--skip-issues", action="store_true",
help="only clone + collect files (useful while iterating)")
args = parser.parse_args()
DATA_DIR.mkdir(exist_ok=True)
repo_dir = clone_repo(args.repo)
file_records = collect_files(repo_dir)
issue_records = [] if args.skip_issues else fetch_issues(args.repo,
args.max_issues)
manifest = {
"repo": args.repo,
"ingested_at": datetime.now(timezone.utc).isoformat(),
"files": file_records,
"issues": issue_records,
}
(DATA_DIR / "manifest.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
n_code = sum(1 for r in file_records if r["source_type"] == "code")
n_docs = sum(1 for r in file_records if r["source_type"] == "doc")
print(f"\n[done] {n_code} code files, {n_docs} docs, {len(issue_records)} issues")
print(f"[done] manifest -> {DATA_DIR / 'manifest.json'}")
if __name__ == "__main__":
main()