Docker-JEE / app.py
Tummala Pavan
Speed up question ZIP extraction
bd7602d
Raw
History Blame Contribute Delete
4.71 kB
import json
import os
import shutil
import subprocess
import tempfile
import zipfile
from pathlib import Path
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, JSONResponse
from starlette.background import BackgroundTask
ROOT = Path(__file__).parent
ALLOWED_ORIGINS = [origin.strip() for origin in os.getenv("ALLOWED_ORIGINS", "*").split(",") if origin.strip()]
EXTRACTION_TIMEOUT = int(os.getenv("EXTRACTION_TIMEOUT_SECONDS", "900"))
app = FastAPI(title="JEE PDF Extraction API", version="1.0.0")
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=False,
allow_methods=["GET", "POST", "OPTIONS"],
allow_headers=["*"],
)
@app.get("/health")
def health():
return {"ok": True, "service": "jee-pdf-extractor"}
@app.post("/extract/questions")
async def extract_questions(pdf: UploadFile = File(...), settings_json: str = Form("{}")):
if not pdf.filename.lower().endswith(".pdf"):
raise HTTPException(status_code=400, detail="Upload a PDF file.")
with tempfile.TemporaryDirectory() as temp_dir_name:
temp_dir = Path(temp_dir_name)
pdf_path = temp_dir / "paper.pdf"
output_dir = temp_dir / "questions"
zip_path = temp_dir / "questions.zip"
await save_upload(pdf, pdf_path)
result = run_extractor([
"questions",
"--pdf",
str(pdf_path),
"--settings-json",
settings_json,
"--output-dir",
str(output_dir),
])
questions = []
for question in result.get("questions", []):
image_path = Path(question["imagePath"])
image_filename = f"q-{question['questionNo']}.png"
archive_path = output_dir / image_filename
question.pop("imagePath", None)
question["imageFilename"] = image_filename
if image_path.resolve() != archive_path.resolve():
shutil.copyfile(image_path, archive_path)
questions.append(question)
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_STORED) as archive:
archive.writestr("metadata.json", json.dumps({"questions": questions}))
for image_path in sorted(output_dir.glob("q-*.png")):
archive.write(image_path, image_path.name)
final_fd, final_name = tempfile.mkstemp(suffix=".zip")
os.close(final_fd)
final_zip = Path(final_name)
shutil.copyfile(zip_path, final_zip)
return FileResponse(
final_zip,
media_type="application/zip",
filename="questions.zip",
background=BackgroundTask(final_zip.unlink, missing_ok=True),
)
@app.post("/extract/key")
async def extract_key(pdf: UploadFile = File(...), questions_json: str = Form("[]")):
if not pdf.filename.lower().endswith(".pdf"):
raise HTTPException(status_code=400, detail="Upload a PDF file.")
with tempfile.TemporaryDirectory() as temp_dir_name:
temp_dir = Path(temp_dir_name)
pdf_path = temp_dir / "answer-key.pdf"
await save_upload(pdf, pdf_path)
result = run_extractor([
"key",
"--pdf",
str(pdf_path),
"--questions-json",
questions_json,
])
return JSONResponse(result)
async def save_upload(upload: UploadFile, destination: Path):
with destination.open("wb") as handle:
while chunk := await upload.read(1024 * 1024):
handle.write(chunk)
if destination.stat().st_size <= 0:
raise HTTPException(status_code=400, detail="Uploaded PDF was empty.")
with destination.open("rb") as handle:
if handle.read(5) != b"%PDF-":
raise HTTPException(status_code=400, detail="Uploaded file is not a valid PDF.")
def run_extractor(args):
command = ["python3", str(ROOT / "server_extract.py"), *args]
try:
completed = subprocess.run(command, check=False, capture_output=True, text=True, timeout=EXTRACTION_TIMEOUT)
except subprocess.TimeoutExpired as error:
raise HTTPException(status_code=504, detail=f"Extraction timed out after {EXTRACTION_TIMEOUT}s") from error
try:
parsed = json.loads(completed.stdout or "{}")
except json.JSONDecodeError as error:
raise HTTPException(status_code=500, detail=completed.stderr or "Extractor returned invalid JSON") from error
if completed.returncode != 0 or parsed.get("error"):
raise HTTPException(status_code=422, detail=parsed.get("error") or completed.stderr or "Extractor failed")
return parsed