kussssh/IPO-Analyzer
0
1"""2FastAPI Application — IPO Analyzer API3Endpoints: SEBI Scraper → Download PDF → Analyze → Get Results4Supports DRHP, RHP, and Prospectus document types.5"""6import json7import re8import uuid9import shutil10import threading11from pathlib import Path12from typing import Dict, Any13 14from fastapi import FastAPI, UploadFile, File, HTTPException15from fastapi.middleware.cors import CORSMiddleware16from fastapi.staticfiles import StaticFiles17from fastapi.responses import FileResponse, JSONResponse18 19from backend.config import UPLOAD_DIR, OUTPUT_DIR20from backend.pipeline import run_full_analysis21from backend.scraper import (22 scrape_drhp_list,23 scrape_rhp_list,24 scrape_prospectus_list,25 build_ipo_tracker,26 find_pdf_on_detail_page,27 download_pdf,28)29 30app = FastAPI(title="IPO Analyzer API", version="2.0.0")31 32# CORS for frontend33app.add_middleware(34 CORSMiddleware,35 allow_origins=["*"],36 allow_credentials=True,37 allow_methods=["*"],38 allow_headers=["*"],39)40 41# Serve frontend static files42FRONTEND_DIR = Path(__file__).resolve().parent.parent / "frontend"43if FRONTEND_DIR.exists():44 app.mount("/static", StaticFiles(directory=str(FRONTEND_DIR)), name="static")45 46# In-memory job tracker47jobs: Dict[str, Dict[str, Any]] = {}48 49 50@app.get("/")51async def root():52 """Serve frontend or health check."""53 index_path = FRONTEND_DIR / "index.html"54 if index_path.exists():55 return FileResponse(str(index_path))56 return {"status": "ok", "message": "IPO Analyzer API is running"}57 58 59@app.post("/analyze")60async def analyze_drhp(file: UploadFile = File(...)):61 """62 Upload a PDF and start analysis.63 Returns a job_id for polling progress.64 """65 if not file.filename.lower().endswith(".pdf"):66 raise HTTPException(status_code=400, detail="Only PDF files are accepted")67 68 # Save uploaded file69 job_id = str(uuid.uuid4())[:8]70 pdf_filename = f"{job_id}_{file.filename}"71 pdf_path = UPLOAD_DIR / pdf_filename72 73 with open(pdf_path, "wb") as f:74 shutil.copyfileobj(file.file, f)75 76 # Initialize job77 jobs[job_id] = {78 "status": "processing",79 "step": 0,80 "total_steps": 10,81 "message": "Starting analysis...",82 "pdf_filename": file.filename,83 "result": None,84 "error": None,85 }86 87 # Run analysis in background thread88 def _run():89 try:90 def progress_cb(step, total, msg):91 jobs[job_id]["step"] = step92 jobs[job_id]["total_steps"] = total93 jobs[job_id]["message"] = msg94 95 result = run_full_analysis(str(pdf_path), progress_callback=progress_cb)96 jobs[job_id]["status"] = "complete"97 jobs[job_id]["result"] = result98 jobs[job_id]["message"] = "Analysis complete!"99 except Exception as e:100 jobs[job_id]["status"] = "error"101 jobs[job_id]["error"] = str(e)102 jobs[job_id]["message"] = f"Error: {str(e)}"103 import traceback104 traceback.print_exc()105 106 thread = threading.Thread(target=_run, daemon=True)107 thread.start()108 109 return {"job_id": job_id, "message": "Analysis started"}110 111 112@app.get("/status/{job_id}")113async def get_status(job_id: str):114 """Poll analysis progress."""115 if job_id not in jobs:116 raise HTTPException(status_code=404, detail="Job not found")117 job = jobs[job_id]118 return {119 "job_id": job_id,120 "status": job["status"],121 "step": job["step"],122 "total_steps": job["total_steps"],123 "message": job["message"],124 "pdf_filename": job["pdf_filename"],125 }126 127 128@app.get("/result/{job_id}")129async def get_result(job_id: str):130 """Get completed analysis result."""131 if job_id not in jobs:132 raise HTTPException(status_code=404, detail="Job not found")133 job = jobs[job_id]134 if job["status"] == "processing":135 return JSONResponse(status_code=202, content={"message": "Still processing", "step": job["step"]})136 if job["status"] == "error":137 raise HTTPException(status_code=500, detail=job["error"])138 return job["result"]139 140 141@app.get("/results")142async def list_results():143 """List all saved analysis results."""144 results = []145 for f in OUTPUT_DIR.glob("*.json"):146 try:147 with open(f, "r", encoding="utf-8") as fp:148 data = json.load(fp)149 results.append({150 "filename": f.name,151 "company": data.get("company", "Unknown"),152 "verdict": data.get("verdict", "Unknown"),153 "score": data.get("score", {}),154 })155 except Exception:156 continue157 return results158 159 160@app.get("/results/{filename}")161async def get_saved_result(filename: str):162 """Get a previously saved analysis result by filename."""163 filepath = OUTPUT_DIR / filename164 if not filepath.exists():165 raise HTTPException(status_code=404, detail="Result not found")166 with open(filepath, "r", encoding="utf-8") as f:167 return json.load(f)168 169 170# ═══════════════════════════════════════════════════171# SEBI Scraper Endpoints172# ═══════════════════════════════════════════════════173 174@app.get("/sebi/drhp-list")175async def get_sebi_drhp_list():176 """Scrape SEBI website for DRHP filings (filtered)."""177 entries = scrape_drhp_list()178 return {"entries": entries, "count": len(entries)}179 180 181@app.get("/sebi/rhp-list")182async def get_sebi_rhp_list():183 """Scrape SEBI website for RHP filings (filtered)."""184 entries = scrape_rhp_list()185 return {"entries": entries, "count": len(entries)}186 187 188@app.get("/sebi/prospectus-list")189async def get_sebi_prospectus_list():190 """Scrape SEBI website for Prospectus filings (filtered)."""191 entries = scrape_prospectus_list()192 return {"entries": entries, "count": len(entries)}193 194 195@app.get("/sebi/ipo-tracker")196async def get_ipo_tracker():197 """198 Unified IPO tracker — cross-references DRHP, RHP, and Prospectus listings.199 Returns companies with their document availability across all 3 stages.200 """201 tracker = build_ipo_tracker()202 return {"companies": tracker, "count": len(tracker)}203 204 205@app.post("/sebi/analyze")206async def analyze_from_sebi(detail_url: str, name: str, doc_type: str = "drhp"):207 """208 Download a PDF from SEBI and start analysis.209 If already analyzed, returns cached result instantly.210 211 Args:212 detail_url: URL of the SEBI detail page or direct PDF213 name: Company name for display214 doc_type: Document type — 'drhp', 'rhp', or 'prospectus'215 """216 safe_name = re.sub(r'[^\w\-.]', '_', name)[:80]217 218 # ── Check for cached result ──219 for f in OUTPUT_DIR.glob("*.json"):220 if safe_name.lower() in f.name.lower():221 try:222 with open(f, "r", encoding="utf-8") as fp:223 cached = json.load(fp)224 print(f"📦 Found cached result for: {name} → {f.name}")225 job_id = str(uuid.uuid4())[:8]226 jobs[job_id] = {227 "status": "complete",228 "step": 10,229 "total_steps": 10,230 "message": "Loaded from cache!",231 "pdf_filename": name,232 "result": cached,233 "error": None,234 }235 return {"job_id": job_id, "message": "Loaded from cache (already analyzed)"}236 except Exception:237 break238 239 # ── Check if PDF already downloaded ──240 existing_pdf = None241 for f in UPLOAD_DIR.glob("*.pdf"):242 if safe_name.lower() in f.name.lower() and f.stat().st_size > 10000:243 existing_pdf = str(f)244 print(f"📦 Found cached PDF: {f.name}")245 break246 247 job_id = str(uuid.uuid4())[:8]248 249 jobs[job_id] = {250 "status": "processing",251 "step": 0,252 "total_steps": 12,253 "message": "Starting..." if existing_pdf else f"Finding {doc_type.upper()} PDF on SEBI...",254 "pdf_filename": name,255 "result": None,256 "error": None,257 }258 259 def _run():260 try:261 def progress_cb(step, total, msg):262 jobs[job_id]["step"] = step + 2263 jobs[job_id]["total_steps"] = total + 2264 jobs[job_id]["message"] = msg265 266 pdf_path = existing_pdf267 268 if not pdf_path:269 # Step 1: Find PDF URL270 jobs[job_id]["message"] = f"Finding {doc_type.upper()} PDF on SEBI detail page..."271 jobs[job_id]["step"] = 1272 273 if detail_url.lower().endswith(".pdf"):274 pdf_url = detail_url275 else:276 pdf_url = find_pdf_on_detail_page(detail_url)277 278 if not pdf_url:279 jobs[job_id]["status"] = "error"280 jobs[job_id]["error"] = "Could not find PDF on the SEBI detail page."281 jobs[job_id]["message"] = "Error: PDF not found on detail page"282 return283 284 # Step 2: Download PDF285 jobs[job_id]["message"] = f"Downloading {doc_type.upper()} PDF..."286 jobs[job_id]["step"] = 2287 288 pdf_filename = f"{job_id}_{safe_name}_{doc_type}.pdf"289 pdf_path = download_pdf(pdf_url, pdf_filename)290 else:291 jobs[job_id]["message"] = "Using cached PDF..."292 jobs[job_id]["step"] = 2293 294 # Step 3+: Run analysis295 result = run_full_analysis(pdf_path, progress_callback=progress_cb)296 jobs[job_id]["status"] = "complete"297 jobs[job_id]["result"] = result298 jobs[job_id]["message"] = "Analysis complete!"299 except Exception as e:300 jobs[job_id]["status"] = "error"301 jobs[job_id]["error"] = str(e)302 jobs[job_id]["message"] = f"Error: {str(e)}"303 import traceback304 traceback.print_exc()305 306 thread = threading.Thread(target=_run, daemon=True)307 thread.start()308 309 return {"job_id": job_id, "message": f"SEBI {doc_type.upper()} analysis started"}310 311 312from backend.api import app as app313 