-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgithub_client.py
More file actions
140 lines (114 loc) · 4.81 KB
/
Copy pathgithub_client.py
File metadata and controls
140 lines (114 loc) · 4.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
"""
GitHub API client for the git-brief summarizer.
Fetches repository metadata, file structure, and README content
using the GitHub REST API.
"""
import re
import requests
from typing import Optional
GITHUB_API_BASE = "https://api.github.com"
def parse_repo_url(url: str) -> tuple[str, str]:
"""Parse a GitHub repository URL into owner and repo name.
Args:
url: A GitHub repository URL (e.g. https://github.com/owner/repo).
Returns:
A tuple of (owner, repo_name).
Raises:
ValueError: If the URL is not a valid GitHub repository URL.
"""
pattern = r"(?:https?://)?github\.com/([^/]+)/([^/\s]+?)(?:\.git)?$"
match = re.match(pattern, url.strip().rstrip("/"))
if not match:
raise ValueError(
f"Invalid GitHub repository URL: '{url}'\n"
"Expected format: https://github.com/owner/repo"
)
owner, repo = match.group(1), match.group(2)
return owner, repo
def _get_headers(token: Optional[str] = None) -> dict:
"""Build request headers, optionally including a GitHub PAT.
Args:
token: Optional GitHub Personal Access Token for authenticated requests.
Returns:
A dict of HTTP headers.
"""
headers = {"Accept": "application/vnd.github+json"}
if token:
headers["Authorization"] = f"Bearer {token}"
return headers
def fetch_repo_data(url: str, token: Optional[str] = None) -> dict:
"""Fetch comprehensive data about a GitHub repository.
Retrieves the repository description, primary programming language,
full file tree (recursive), and the raw content of the README file.
Args:
url: The GitHub repository URL.
token: Optional GitHub Personal Access Token. Increases API rate
limits from 60 to 5000 requests per hour.
Returns:
A dictionary with the following keys:
- ``owner`` (str): Repository owner/organisation.
- ``repo`` (str): Repository name.
- ``description`` (str | None): Repository description.
- ``language`` (str | None): Primary programming language.
- ``file_tree`` (list[str]): Sorted list of file paths.
- ``readme`` (str | None): Raw text content of README.md.
Raises:
ValueError: If the URL cannot be parsed.
PermissionError: If the repository is private and no valid token
is provided.
FileNotFoundError: If the repository does not exist.
RuntimeError: For any other unexpected API errors.
"""
owner, repo = parse_repo_url(url)
headers = _get_headers(token)
# ── 1. Repository metadata ────────────────────────────────────────────────
meta_url = f"{GITHUB_API_BASE}/repos/{owner}/{repo}"
meta_resp = requests.get(meta_url, headers=headers, timeout=15)
if meta_resp.status_code == 404:
raise FileNotFoundError(
f"Repository '{owner}/{repo}' not found. "
"It may not exist or could be a private repository."
)
if meta_resp.status_code == 403:
raise PermissionError(
f"Access denied for '{owner}/{repo}'. "
"The repository is private. Provide a valid GitHub token (--token)."
)
if not meta_resp.ok:
raise RuntimeError(
f"GitHub API error {meta_resp.status_code}: {meta_resp.json().get('message', 'Unknown error')}"
)
meta = meta_resp.json()
default_branch = meta.get("default_branch", "main")
# ── 2. Full file tree (recursive git tree) ────────────────────────────────
tree_url = (
f"{GITHUB_API_BASE}/repos/{owner}/{repo}/git/trees/{default_branch}"
"?recursive=1"
)
tree_resp = requests.get(tree_url, headers=headers, timeout=15)
file_tree: list[str] = []
if tree_resp.ok:
tree_data = tree_resp.json()
file_tree = sorted(
item["path"]
for item in tree_data.get("tree", [])
if item["type"] == "blob"
)
# ── 3. README content ─────────────────────────────────────────────────────
readme_content: Optional[str] = None
readme_url = f"{GITHUB_API_BASE}/repos/{owner}/{repo}/readme"
readme_resp = requests.get(
readme_url,
headers={**headers, "Accept": "application/vnd.github.raw+json"},
timeout=15,
)
if readme_resp.ok:
readme_content = readme_resp.text
return {
"owner": owner,
"repo": repo,
"description": meta.get("description"),
"language": meta.get("language"),
"file_tree": file_tree,
"readme": readme_content,
}