Spaces:
Sleeping
Sleeping
| 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() | |