"""GitHub repository inspection utilities.""" from __future__ import annotations import re from pathlib import Path import httpx GITHUB_API = "https://api.github.com" HEADERS = { "Accept": "application/vnd.github.v3+json", "User-Agent": "ResearchLink-AI/0.1", } def parse_github_url(url: str) -> tuple[str | None, str | None]: """Extract (owner, repo) from a GitHub URL.""" match = re.search(r"github\.com[/:]([^/]+)/([^/\s.]+?)(?:\.git)?(?:/|$)", url) if match: return match.group(1), match.group(2) return None, None def fetch_repo_metadata(owner: str, repo: str) -> dict: """Fetch basic repo metadata via GitHub API (unauthenticated, rate-limited).""" url = f"{GITHUB_API}/repos/{owner}/{repo}" try: resp = httpx.get(url, headers=HEADERS, timeout=15.0) if resp.status_code == 200: return resp.json() return {"error": f"HTTP {resp.status_code}"} except Exception as e: return {"error": str(e)} def fetch_readme(owner: str, repo: str) -> str | None: """Fetch the raw README content via GitHub API.""" url = f"{GITHUB_API}/repos/{owner}/{repo}/readme" try: resp = httpx.get(url, headers={**HEADERS, "Accept": "application/vnd.github.raw"}, timeout=15.0) if resp.status_code == 200: return resp.text return None except Exception: return None def fetch_tree(owner: str, repo: str) -> list[dict]: """Fetch the top-level tree (non-recursive) via GitHub API.""" url = f"{GITHUB_API}/repos/{owner}/{repo}/git/trees/HEAD" try: resp = httpx.get(url, headers=HEADERS, timeout=15.0) if resp.status_code == 200: data = resp.json() return data.get("tree", []) return [] except Exception: return [] def detect_language_and_frameworks(tree_items: list[dict], readme: str | None) -> dict: """Heuristically detect language and frameworks from repo tree.""" files = [item["path"] for item in tree_items if item.get("type") == "blob"] folders = [item["path"] for item in tree_items if item.get("type") == "tree"] languages: list[str] = [] frameworks: list[str] = [] setup_commands: list[str] = [] has_tests = False test_paths: list[str] = [] ext_map = {".py": "Python", ".js": "JavaScript", ".ts": "TypeScript", ".go": "Go", ".rs": "Rust", ".cpp": "C++", ".c": "C", ".java": "Java", ".r": "R"} for f in files: ext = Path(f).suffix.lower() if ext in ext_map and ext_map[ext] not in languages: languages.append(ext_map[ext]) if "requirements.txt" in files: setup_commands.append("pip install -r requirements.txt") if "setup.py" in files or "pyproject.toml" in files: setup_commands.append("pip install -e .") if "environment.yml" in files: setup_commands.append("conda env create -f environment.yml") if "Makefile" in files: setup_commands.append("make install") for f in files + folders: if "test" in f.lower() or "spec" in f.lower(): has_tests = True test_paths.append(f) text_to_check = " ".join(files + folders + ([readme] if readme else [])) fw_hints = { "PyTorch": ["torch", "pytorch"], "TensorFlow": ["tensorflow", "tf"], "JAX": ["jax", "flax"], "Gymnasium": ["gymnasium", "gym"], "RLlib": ["rllib"], "Stable-Baselines3": ["stable_baselines", "sb3"], "FastAPI": ["fastapi"], "Flask": ["flask"], "NumPy": ["numpy"], } for fw, hints in fw_hints.items(): if any(h in text_to_check.lower() for h in hints): frameworks.append(fw) return { "languages": languages, "frameworks": frameworks, "setup_commands": setup_commands, "has_tests": has_tests, "test_paths": test_paths[:10], }