research-link-ai / src /researchlink /services /github_client.py
MHamdan's picture
Deploy Research-Link-AI (Docker Space, offline demo)
a753e74 verified
Raw
History Blame Contribute Delete
3.93 kB
"""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],
}