Skip to content

Commit 26798b4

Browse files
Merge pull request #40 from SharkyBytes/feature/backend-fastapi-github-stats
(SR NO - 7) Added FastAPI Backend for GitHub Repository Stats Fetching with Modular Structure and CORS Support
2 parents 6a4ab72 + bda1756 commit 26798b4

File tree

6 files changed

+210
-3
lines changed

6 files changed

+210
-3
lines changed

backend/README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Setting Up the .env File for FastAPI
2+
3+
This guide provides step-by-step instructions to configure your `.env` file and securely use a GitHub Personal Access Token in your FastAPI project.
4+
5+
## 1. Create the `.env` File
6+
7+
1. Navigate to the `backend` directory where your project is set up.
8+
2. Create a new file named `.env` (if it doesn’t already exist).
9+
10+
## 2. Add the GitHub Access Token
11+
12+
1. Open the `.env` file and add the following line:
13+
14+
```text
15+
GITHUB_TOKEN=your_personal_access_token_here
16+
```
17+
18+
2. Replace `your_personal_access_token_here` with your actual GitHub Personal Access Token.
19+
20+
## 3. Generate a GitHub Personal Access Token
21+
22+
1. Go to [GitHub Developer Settings](https://github.com/settings/tokens).
23+
2. Click on **Tokens (classic)** and then **Generate new token** and then generate the classic token.
24+
3. Select the required scopes for your application:
25+
- **For public repositories**: Check `repo`.
26+
- **For additional access**: Check `read:org`, `read:user`, etc., as needed.
27+
4. Copy the generated token (it will only be displayed once).
28+
29+
30+
## 5. Load Environment Variables in FastAPI
31+
32+
Ensure you have these things installed in your project:
33+
34+
```bash
35+
pip install python-dotenv requests fastapi uvicorn
36+
37+
```
38+
39+
### Test API Requests
40+
Run your FastAPI server:
41+
42+
```bash
43+
uvicorn backend.main:app --reload
44+
```
45+
46+
Use `curl` or **Postman** to test the `/api/repo-stats` endpoint:
47+
48+
```bash
49+
curl -X POST http://localhost:8000/api/repo-stats \
50+
-H "Content-Type: application/json" \
51+
-d '{"repo_url": "https://github.com/AOSSIE-Org/Devr.AI/"}'
52+
```
53+
54+
If all the things correctly got setup then you will see the JSON repsponse
55+
56+
---
57+
58+
59+
Now, your FastAPI application is securely set up to use GitHub API credentials from a `.env` file!

backend/app/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from pydantic import BaseModel
2+
3+
class RepoRequest(BaseModel):
4+
repo_url: str

backend/app/routes.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from fastapi import APIRouter, HTTPException
2+
from app.models import RepoRequest
3+
from app.utils.github_api import get_repo_stats
4+
5+
router = APIRouter()
6+
7+
@router.post("/repo-stats")
8+
async def repo_stats_endpoint(repo: RepoRequest):
9+
try:
10+
return await get_repo_stats(repo.repo_url)
11+
except Exception as e:
12+
raise HTTPException(status_code=500, detail=str(e))

backend/app/utils/__init__.py

Whitespace-only changes.

backend/app/utils/github_api.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
from fastapi import FastAPI, HTTPException
2+
from pydantic import BaseModel
3+
import requests
4+
import os
5+
from urllib.parse import urlparse
6+
from dotenv import load_dotenv
7+
8+
load_dotenv() # Load environment variables
9+
10+
app = FastAPI()
11+
12+
class RepoRequest(BaseModel):
13+
repo_url: str
14+
15+
def parse_github_url(url: str) -> tuple:
16+
"""Extract owner/repo from GitHub URL"""
17+
parsed = urlparse(url)
18+
path = parsed.path.strip('/').split('/')
19+
if len(path) < 2:
20+
raise ValueError("Invalid GitHub URL")
21+
return path[0], path[1]
22+
23+
def github_api_request(endpoint: str) -> dict:
24+
"""Make authenticated GitHub API request"""
25+
headers = {
26+
"Authorization": f"token {os.getenv('GITHUB_TOKEN')}",
27+
"Accept": "application/vnd.github.v3+json"
28+
}
29+
response = requests.get(f"https://api.github.com{endpoint}", headers=headers)
30+
response.raise_for_status()
31+
return response.json()
32+
33+
34+
@app.post("/repo-stats")
35+
async def get_repo_stats(repo_url: str):
36+
try:
37+
owner, repo_name = parse_github_url(repo_url)
38+
# Rest of your function...
39+
# Get basic repo info
40+
repo_info = github_api_request(f"/repos/{owner}/{repo_name}")
41+
42+
# Get contributors
43+
contributors = github_api_request(f"/repos/{owner}/{repo_name}/contributors")
44+
45+
# Get pull requests
46+
prs = github_api_request(f"/repos/{owner}/{repo_name}/pulls?state=all")
47+
48+
# Get issues
49+
issues = github_api_request(f"/repos/{owner}/{repo_name}/issues?state=all")
50+
51+
community_profile = github_api_request(f"/repos/{owner}/{repo_name}/community/profile")
52+
53+
# Recent commits (last 5)
54+
commits = github_api_request(f"/repos/{owner}/{repo_name}/commits?per_page=5")
55+
56+
code_frequency = github_api_request(f"/repos/{owner}/{repo_name}/stats/code_frequency")
57+
58+
return {
59+
"name": repo_info["full_name"],
60+
"stars": repo_info["stargazers_count"],
61+
"forks": repo_info["forks_count"],
62+
"watchers": repo_info["subscribers_count"],
63+
"created_at": repo_info["created_at"],
64+
"updated_at": repo_info["updated_at"],
65+
# Licensing and topics
66+
# "license": repo_info.get("license", {}).get("spdx_id", "No License"),
67+
68+
"topics": repo_info.get("topics", []),
69+
70+
"contributors": [{
71+
"login": c["login"],
72+
"contributions": c["contributions"],
73+
"avatar_url": c["avatar_url"]
74+
} for c in contributors],
75+
"recent_commits": [{
76+
"sha": commit["sha"][:7],
77+
"author": commit["commit"]["author"]["name"],
78+
"message": commit["commit"]["message"],
79+
"date": commit["commit"]["author"]["date"]
80+
} for commit in commits],
81+
82+
83+
"community": {
84+
"health_percentage": community_profile["health_percentage"],
85+
"code_of_conduct": community_profile.get("files", {}).get("code_of_conduct") is not None,
86+
"license": community_profile.get("files", {}).get("license") is not None,
87+
"readme": community_profile.get("files", {}).get("readme") is not None
88+
},
89+
# Issues
90+
"issues": {
91+
"total": len(issues),
92+
"open": sum(1 for issue in issues if issue["state"] == "open"),
93+
"closed": sum(1 for issue in issues if issue["state"] == "closed"),
94+
"labels": list({label["name"] for issue in issues for label in issue["labels"]})
95+
},
96+
97+
# Code statistics
98+
"code_activity": {
99+
"weekly_commits": len(code_frequency) if isinstance(code_frequency, list) else 0,
100+
"total_additions": sum(week[1] for week in code_frequency) if isinstance(code_frequency, list) else 0,
101+
"total_deletions": sum(abs(week[2]) for week in code_frequency) if isinstance(code_frequency, list) else 0
102+
},
103+
104+
# Pull Requests
105+
"pull_requests": {
106+
"total": len(prs),
107+
"merged": sum(1 for pr in prs if pr["merged_at"]),
108+
"draft": sum(1 for pr in prs if pr["draft"]),
109+
"by_state": {
110+
"open": sum(1 for pr in prs if pr["state"] == "open"),
111+
"closed": sum(1 for pr in prs if pr["state"] == "closed")
112+
}
113+
},
114+
}
115+
116+
except requests.HTTPError as e:
117+
raise HTTPException(status_code=e.response.status_code,
118+
detail="GitHub API error")
119+
except ValueError:
120+
raise HTTPException(status_code=400,
121+
detail="Invalid GitHub URL format")

backend/main.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
from fastapi import FastAPI
2-
from routes import router
2+
from fastapi.middleware.cors import CORSMiddleware
3+
from app.routes import router
34
import uvicorn
45

56
app = FastAPI()
6-
# Include GitHub webhook routes
7-
app.include_router(router)
7+
8+
app.add_middleware(
9+
CORSMiddleware,
10+
allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
11+
allow_credentials=True,
12+
allow_methods=["*"],
13+
allow_headers=["*"],
14+
expose_headers=["*"],
15+
)
16+
17+
18+
app.include_router(router, prefix="/api")
819

920
if __name__ == "__main__":
1021
uvicorn.run(app, host="127.0.0.1", port=8000)

0 commit comments

Comments
 (0)