Samyak000 commited on
Commit
28b8fd9
·
verified ·
1 Parent(s): 61f9f3b

Create main.py

Browse files
Files changed (1) hide show
  1. main.py +472 -0
main.py ADDED
@@ -0,0 +1,472 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Codesage Backend API
3
+ FastAPI server for GitHub App integration
4
+ Connects desktop app to GitHub securely
5
+ """
6
+
7
+ from fastapi import FastAPI, HTTPException, Request
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from pydantic import BaseModel
10
+ from typing import Optional, List, Dict
11
+ import json
12
+ import os
13
+ from pathlib import Path
14
+ from dotenv import load_dotenv
15
+
16
+ from github_api import GitHubAppAuth, GitHubInsights, TokenManager
17
+
18
+ # Load environment variables from .env file
19
+ load_dotenv()
20
+
21
+
22
+ # Initialize FastAPI app
23
+ app = FastAPI(
24
+ title="Codesage Backend API",
25
+ description="Enterprise-grade GitHub insights for development teams",
26
+ version="1.0.0"
27
+ )
28
+
29
+ # CORS configuration (allow desktop app to connect)
30
+ app.add_middleware(
31
+ CORSMiddleware,
32
+ allow_origins=["*"], # In production, specify your desktop app origins
33
+ allow_credentials=True,
34
+ allow_methods=["*"],
35
+ allow_headers=["*"],
36
+ )
37
+
38
+ # Load environment variables
39
+ APP_ID = os.getenv("GITHUB_APP_ID")
40
+ PRIVATE_KEY_PATH = os.getenv("GITHUB_PRIVATE_KEY_PATH", "private-key.pem")
41
+
42
+ # Initialize GitHub App authentication
43
+ if APP_ID and os.path.exists(PRIVATE_KEY_PATH):
44
+ with open(PRIVATE_KEY_PATH, "r") as f:
45
+ private_key = f.read()
46
+
47
+ github_auth = GitHubAppAuth(APP_ID, private_key)
48
+ token_manager = TokenManager(github_auth)
49
+ else:
50
+ github_auth = None
51
+ token_manager = None
52
+ print("⚠️ WARNING: GitHub App credentials not configured")
53
+
54
+ # Simple JSON storage for installations
55
+ INSTALLATIONS_FILE = "installations.json"
56
+
57
+
58
+ # ============================================================================
59
+ # DATA MODELS
60
+ # ============================================================================
61
+
62
+ class InstallationCallback(BaseModel):
63
+ """Data received from GitHub installation callback"""
64
+ installation_id: int
65
+ org: str
66
+ setup_action: Optional[str] = None
67
+
68
+
69
+ class InsightsRequest(BaseModel):
70
+ """Request for repository insights from desktop app"""
71
+ org: str
72
+ repo: str
73
+
74
+
75
+ class TokenRefreshRequest(BaseModel):
76
+ """Request to refresh installation token"""
77
+ installation_id: int
78
+
79
+
80
+ # ============================================================================
81
+ # UTILITY FUNCTIONS
82
+ # ============================================================================
83
+
84
+ def load_installations() -> Dict:
85
+ """Load installations from JSON file"""
86
+ if os.path.exists(INSTALLATIONS_FILE):
87
+ with open(INSTALLATIONS_FILE, "r") as f:
88
+ return json.load(f)
89
+ return {}
90
+
91
+
92
+ def save_installation(org: str, installation_id: int):
93
+ """Save installation to JSON file"""
94
+ installations = load_installations()
95
+ installations[org] = {
96
+ "installation_id": installation_id,
97
+ "installed_at": None # Could add timestamp
98
+ }
99
+
100
+ with open(INSTALLATIONS_FILE, "w") as f:
101
+ json.dump(installations, f, indent=2)
102
+
103
+
104
+ def get_installation_id(org: str) -> Optional[int]:
105
+ """Get installation ID for an organization"""
106
+ installations = load_installations()
107
+ org_data = installations.get(org)
108
+
109
+ if org_data:
110
+ return org_data.get("installation_id")
111
+ return None
112
+
113
+
114
+ # ============================================================================
115
+ # API ENDPOINTS
116
+ # ============================================================================
117
+
118
+ @app.get("/")
119
+ def root():
120
+ """Health check endpoint"""
121
+ return {
122
+ "service": "Codesage Backend API",
123
+ "status": "operational",
124
+ "configured": github_auth is not None
125
+ }
126
+
127
+
128
+ @app.get("/health")
129
+ def health_check():
130
+ """Detailed health check"""
131
+ return {
132
+ "status": "healthy",
133
+ "github_app_configured": github_auth is not None,
134
+ "installations_count": len(load_installations())
135
+ }
136
+
137
+
138
+ @app.get("/github/callback")
139
+ async def github_callback(request: Request):
140
+ """
141
+ Handle GitHub App installation callback
142
+
143
+ After org installs Codesage, GitHub redirects here with installation_id
144
+ This is the critical "make our product a member" step
145
+ """
146
+ # Get query parameters
147
+ query_params = dict(request.query_params)
148
+
149
+ installation_id = query_params.get("installation_id")
150
+ setup_action = query_params.get("setup_action")
151
+
152
+ if not installation_id:
153
+ return {
154
+ "success": False,
155
+ "message": "Missing installation_id parameter",
156
+ "received_params": query_params
157
+ }
158
+
159
+ try:
160
+ installation_id = int(installation_id)
161
+ except ValueError:
162
+ return {
163
+ "success": False,
164
+ "message": "Invalid installation_id format",
165
+ "installation_id": installation_id
166
+ }
167
+
168
+ # Try to get org name if credentials are configured
169
+ org_name = None
170
+
171
+ if github_auth:
172
+ try:
173
+ # Get installation token to verify and fetch org info
174
+ token_data = github_auth.get_installation_token(installation_id)
175
+ token = token_data["token"]
176
+
177
+ # Get installation repositories to extract org name
178
+ repos = github_auth.get_installation_repositories(token)
179
+
180
+ if repos:
181
+ org_name = repos[0]["owner"]["login"]
182
+
183
+ except Exception as e:
184
+ print(f"⚠️ Could not authenticate with GitHub: {str(e)}")
185
+ print(f" Installation ID {installation_id} captured but org name unknown")
186
+
187
+ # Save with org name if we got it, otherwise use placeholder
188
+ if org_name:
189
+ save_installation(org_name, installation_id)
190
+ return {
191
+ "success": True,
192
+ "message": f"✅ Successfully installed Codesage for {org_name}",
193
+ "org": org_name,
194
+ "installation_id": installation_id
195
+ }
196
+ else:
197
+ # Save with placeholder - user can update via /github/install
198
+ placeholder = f"installation_{installation_id}"
199
+ save_installation(placeholder, installation_id)
200
+ return {
201
+ "success": True,
202
+ "message": "✅ Installation ID captured! Use /github/install to register with org name",
203
+ "installation_id": installation_id,
204
+ "note": f"Saved as '{placeholder}'. Update your .env file with correct private key path and use POST /github/install to register with org name"
205
+ }
206
+
207
+
208
+ @app.post("/github/install")
209
+ def register_installation(data: InstallationCallback):
210
+ """
211
+ Manually register an installation (alternative to callback)
212
+
213
+ Use this during development or for manual setup
214
+ """
215
+ save_installation(data.org, data.installation_id)
216
+
217
+ return {
218
+ "success": True,
219
+ "message": f"Installation registered for {data.org}",
220
+ "installation_id": data.installation_id
221
+ }
222
+
223
+
224
+ @app.get("/installations")
225
+ def list_installations():
226
+ """
227
+ List all registered installations
228
+ Shows which orgs have installed Codesage
229
+ """
230
+ installations = load_installations()
231
+
232
+ return {
233
+ "count": len(installations),
234
+ "installations": installations
235
+ }
236
+
237
+
238
+ @app.get("/installations/{org}")
239
+ def get_org_installation(org: str):
240
+ """Get installation details for a specific org"""
241
+ installation_id = get_installation_id(org)
242
+
243
+ if not installation_id:
244
+ raise HTTPException(status_code=404, detail=f"No installation found for {org}")
245
+
246
+ return {
247
+ "org": org,
248
+ "installation_id": installation_id
249
+ }
250
+
251
+
252
+ @app.get("/repos/{org}")
253
+ def list_org_repositories(org: str):
254
+ """
255
+ List all repositories accessible for an org
256
+ This is what the desktop app calls first
257
+ """
258
+ if not github_auth:
259
+ raise HTTPException(status_code=500, detail="GitHub App not configured")
260
+
261
+ installation_id = get_installation_id(org)
262
+ if not installation_id:
263
+ raise HTTPException(status_code=404, detail=f"No installation found for {org}")
264
+
265
+ try:
266
+ # Get fresh token
267
+ token = token_manager.get_token(installation_id)
268
+
269
+ # Fetch repositories
270
+ repos = github_auth.get_installation_repositories(token)
271
+
272
+ # Return simplified list
273
+ return {
274
+ "org": org,
275
+ "count": len(repos),
276
+ "repositories": [
277
+ {
278
+ "name": repo["name"],
279
+ "full_name": repo["full_name"],
280
+ "private": repo["private"],
281
+ "default_branch": repo.get("default_branch"),
282
+ "language": repo.get("language"),
283
+ "updated_at": repo.get("updated_at")
284
+ }
285
+ for repo in repos
286
+ ]
287
+ }
288
+ except Exception as e:
289
+ raise HTTPException(status_code=500, detail=f"Failed to fetch repositories: {str(e)}")
290
+
291
+
292
+ @app.post("/insights/commits")
293
+ def get_commits(data: InsightsRequest):
294
+ """
295
+ Get commit history for a repository
296
+ Desktop app → Backend → GitHub
297
+ """
298
+ if not github_auth:
299
+ raise HTTPException(status_code=500, detail="GitHub App not configured")
300
+
301
+ installation_id = get_installation_id(data.org)
302
+ if not installation_id:
303
+ raise HTTPException(status_code=404, detail=f"No installation found for {data.org}")
304
+
305
+ try:
306
+ token = token_manager.get_token(installation_id)
307
+ insights = GitHubInsights(token)
308
+ commits = insights.get_commits(data.org, data.repo)
309
+
310
+ return {
311
+ "org": data.org,
312
+ "repo": data.repo,
313
+ "count": len(commits),
314
+ "commits": commits
315
+ }
316
+ except Exception as e:
317
+ raise HTTPException(status_code=500, detail=f"Failed to fetch commits: {str(e)}")
318
+
319
+
320
+ @app.post("/insights/pull-requests")
321
+ def get_pull_requests(data: InsightsRequest):
322
+ """Get pull requests for a repository"""
323
+ if not github_auth:
324
+ raise HTTPException(status_code=500, detail="GitHub App not configured")
325
+
326
+ installation_id = get_installation_id(data.org)
327
+ if not installation_id:
328
+ raise HTTPException(status_code=404, detail=f"No installation found for {data.org}")
329
+
330
+ try:
331
+ token = token_manager.get_token(installation_id)
332
+ insights = GitHubInsights(token)
333
+ prs = insights.get_pull_requests(data.org, data.repo)
334
+
335
+ return {
336
+ "org": data.org,
337
+ "repo": data.repo,
338
+ "count": len(prs),
339
+ "pull_requests": prs
340
+ }
341
+ except Exception as e:
342
+ raise HTTPException(status_code=500, detail=f"Failed to fetch pull requests: {str(e)}")
343
+
344
+
345
+ @app.post("/insights/issues")
346
+ def get_issues(data: InsightsRequest):
347
+ """Get issues for a repository"""
348
+ if not github_auth:
349
+ raise HTTPException(status_code=500, detail="GitHub App not configured")
350
+
351
+ installation_id = get_installation_id(data.org)
352
+ if not installation_id:
353
+ raise HTTPException(status_code=404, detail=f"No installation found for {data.org}")
354
+
355
+ try:
356
+ token = token_manager.get_token(installation_id)
357
+ insights = GitHubInsights(token)
358
+ issues = insights.get_issues(data.org, data.repo)
359
+
360
+ return {
361
+ "org": data.org,
362
+ "repo": data.repo,
363
+ "count": len(issues),
364
+ "issues": issues
365
+ }
366
+ except Exception as e:
367
+ raise HTTPException(status_code=500, detail=f"Failed to fetch issues: {str(e)}")
368
+
369
+
370
+ @app.post("/insights/contributors")
371
+ def get_contributors(data: InsightsRequest):
372
+ """Get contributors and their stats"""
373
+ if not github_auth:
374
+ raise HTTPException(status_code=500, detail="GitHub App not configured")
375
+
376
+ installation_id = get_installation_id(data.org)
377
+ if not installation_id:
378
+ raise HTTPException(status_code=404, detail=f"No installation found for {data.org}")
379
+
380
+ try:
381
+ token = token_manager.get_token(installation_id)
382
+ insights = GitHubInsights(token)
383
+ contributors = insights.get_contributors(data.org, data.repo)
384
+
385
+ return {
386
+ "org": data.org,
387
+ "repo": data.repo,
388
+ "count": len(contributors),
389
+ "contributors": contributors
390
+ }
391
+ except Exception as e:
392
+ raise HTTPException(status_code=500, detail=f"Failed to fetch contributors: {str(e)}")
393
+
394
+
395
+ @app.post("/insights/overview")
396
+ def get_repository_overview(data: InsightsRequest):
397
+ """
398
+ Get comprehensive repository overview
399
+ Combines multiple insights into one powerful response
400
+ """
401
+ if not github_auth:
402
+ raise HTTPException(status_code=500, detail="GitHub App not configured")
403
+
404
+ installation_id = get_installation_id(data.org)
405
+ if not installation_id:
406
+ raise HTTPException(status_code=404, detail=f"No installation found for {data.org}")
407
+
408
+ try:
409
+ token = token_manager.get_token(installation_id)
410
+ insights = GitHubInsights(token)
411
+
412
+ # Fetch multiple insights
413
+ repo_info = insights.get_repository_info(data.org, data.repo)
414
+ languages = insights.get_languages(data.org, data.repo)
415
+ contributors = insights.get_contributors(data.org, data.repo)
416
+ recent_commits = insights.get_commits(data.org, data.repo, per_page=10)
417
+
418
+ return {
419
+ "org": data.org,
420
+ "repo": data.repo,
421
+ "overview": {
422
+ "name": repo_info.get("name"),
423
+ "description": repo_info.get("description"),
424
+ "stars": repo_info.get("stargazers_count"),
425
+ "forks": repo_info.get("forks_count"),
426
+ "open_issues": repo_info.get("open_issues_count"),
427
+ "default_branch": repo_info.get("default_branch"),
428
+ "created_at": repo_info.get("created_at"),
429
+ "updated_at": repo_info.get("updated_at"),
430
+ "size": repo_info.get("size"),
431
+ "languages": languages,
432
+ "contributors_count": len(contributors),
433
+ "recent_commits_count": len(recent_commits)
434
+ }
435
+ }
436
+ except Exception as e:
437
+ raise HTTPException(status_code=500, detail=f"Failed to fetch overview: {str(e)}")
438
+
439
+
440
+ @app.post("/insights/activity")
441
+ def get_repository_activity(data: InsightsRequest):
442
+ """Get repository activity stats (code frequency, commit activity)"""
443
+ if not github_auth:
444
+ raise HTTPException(status_code=500, detail="GitHub App not configured")
445
+
446
+ installation_id = get_installation_id(data.org)
447
+ if not installation_id:
448
+ raise HTTPException(status_code=404, detail=f"No installation found for {data.org}")
449
+
450
+ try:
451
+ token = token_manager.get_token(installation_id)
452
+ insights = GitHubInsights(token)
453
+
454
+ code_frequency = insights.get_code_frequency(data.org, data.repo)
455
+ commit_activity = insights.get_commit_activity(data.org, data.repo)
456
+
457
+ return {
458
+ "org": data.org,
459
+ "repo": data.repo,
460
+ "code_frequency": code_frequency,
461
+ "commit_activity": commit_activity
462
+ }
463
+ except Exception as e:
464
+ raise HTTPException(status_code=500, detail=f"Failed to fetch activity: {str(e)}")
465
+
466
+
467
+ # ============================================================================
468
+ # RUN SERVER
469
+ # ============================================================================
470
+
471
+
472
+