-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwebapp.py
More file actions
152 lines (120 loc) · 4.51 KB
/
Copy pathwebapp.py
File metadata and controls
152 lines (120 loc) · 4.51 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
141
142
143
144
145
146
147
148
149
150
151
152
#!/usr/bin/env python3
"""
CV Screener — Web UI. FastAPI app: upload PDFs -> scored results -> download Excel.
Usage: uv run uvicorn webapp:app --host 0.0.0.0 --port 8080
"""
import os
import sys
import tempfile
import time
from fastapi import FastAPI, UploadFile, File, HTTPException
from fastapi.responses import HTMLResponse, Response, JSONResponse
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
if _SCRIPT_DIR not in sys.path:
sys.path.insert(0, _SCRIPT_DIR)
from cv_screener import extract_text, evaluate_cv, load_criteria, CVEvaluation
import pandas as pd
app = FastAPI(title="CV Screener", version="0.2.0")
_latest_results: list[CVEvaluation] = []
_latest_criteria: dict = {}
_DEFAULT_CRITERIA = os.path.join(_SCRIPT_DIR, "criteria.yaml")
_HTML_PATH = os.path.join(_SCRIPT_DIR, "templates", "index.html")
with open(_HTML_PATH) as _fh:
_INDEX_HTML = _fh.read()
def _load_default_criteria() -> dict:
return load_criteria(_DEFAULT_CRITERIA)
@app.get("/", response_class=HTMLResponse)
async def index():
return HTMLResponse(content=_INDEX_HTML, status_code=200)
@app.post("/upload")
async def upload_files(files: list[UploadFile] = File(...)):
"""
Upload PDF CVs, process them, return results as JSON.
Accepts multiple PDF files. Processes synchronously.
"""
global _latest_results, _latest_criteria
if not files:
raise HTTPException(400, "No files uploaded")
criteria = _load_default_criteria()
pdf_files = [f for f in files if f.filename and f.filename.lower().endswith(".pdf")]
if not pdf_files:
raise HTTPException(400, "No PDF files found in upload. Please upload PDF files only.")
t0 = time.time()
tmpdir = tempfile.mkdtemp(prefix="cv_screener_")
saved_paths = []
try:
for f in pdf_files:
dest = os.path.join(tmpdir, f.filename)
content = await f.read()
with open(dest, "wb") as fh:
fh.write(content)
saved_paths.append(dest)
results: list[CVEvaluation] = []
total = len(saved_paths)
for i, path in enumerate(saved_paths, 1):
ev = CVEvaluation()
try:
text = extract_text(path)
ev = evaluate_cv(text, criteria)
except Exception as e:
ev.notes = f"ERROR: {e}"
ev.file_name = os.path.basename(path)
results.append(ev)
results.sort(key=lambda r: r.overall_score, reverse=True)
_latest_results = results
_latest_criteria = criteria
columns = criteria.get("output_columns", [])
rows = []
for ev in results:
row = {}
for col in columns:
key = col.lower().replace(" ", "_")
val = getattr(ev, key, "")
row[col] = val
rows.append(row)
scores = [r.overall_score for r in results]
elapsed = round(time.time() - t0, 2)
summary = {
"total": len(results),
"mean": round(sum(scores) / len(scores), 1) if scores else 0,
"min": min(scores) if scores else 0,
"max": max(scores) if scores else 0,
"time_s": elapsed,
}
return JSONResponse({"results": rows, "summary": summary})
finally:
for p in saved_paths:
try:
os.remove(p)
except OSError:
pass
try:
os.rmdir(tmpdir)
except OSError:
pass
@app.get("/download")
async def download_results():
global _latest_results, _latest_criteria
if not _latest_results:
raise HTTPException(404, "No results available. Upload CVs first.")
criteria = _latest_criteria
columns = criteria.get("output_columns", [])
rows = [ev.to_row(columns) for ev in _latest_results]
df = pd.DataFrame(rows, columns=columns)
output = tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False)
try:
df.to_excel(output.name, index=False, engine="openpyxl")
output.seek(0)
with open(output.name, "rb") as fh:
data = fh.read()
finally:
os.unlink(output.name)
return Response(
content=data,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": "attachment; filename=cv_screening_results.xlsx"},
)
if __name__ == "__main__":
import uvicorn
port = int(os.environ.get("PORT", "8080"))
uvicorn.run("webapp:app", host="0.0.0.0", port=port, log_level="info")