Guides

Common tasks, real-world patterns, copy-paste friendly.

1. Managing multiple job descriptions

A JobDescription is an ordinary Python object. You can keep a dictionary of them, one per open role, and run any resume against any JD.

roles = {
    "senior_python": reveilio.JobDescription.from_file("roles/senior_python.pdf"),
    "ml_engineer":  reveilio.JobDescription.from_file("roles/ml_engineer.pdf"),
    "devrel":       reveilio.JobDescription.from_text(pasted_devrel_text),
}

# Score Alice against every open role to find her best fit
alice = reveilio.Resume.from_file("resumes/alice.pdf")
fits = {name: reveilio.analyze_resume(alice, jd) for name, jd in roles.items()}
best = max(fits.items(), key=lambda kv: kv[1].overall_score)
print("Alice best fits", best[0], "at", best[1].overall_score)
Tip: the structured JD parse is cached on the JobDescription instance. It is only computed on first access to jd.parsed. Reusing the same object across many resumes means one LLM call for the JD instead of N.

2. Free-text JDs and resumes

Sometimes the source of truth is not a file. It is a row in your ATS or a message in Slack. Both JobDescription and Resume accept raw strings:

jd = reveilio.JobDescription.from_text(row["job_description"])
resume = reveilio.Resume.from_text(row["resume_text"], filename=row["candidate_id"])
result = reveilio.analyze_resume(resume, jd)

3. Web upload handlers (bytes in, result out)

When wiring reveilio into a Flask, FastAPI, or Django view, you typically receive bytes from an uploaded file. Use from_bytes:

@app.post("/analyze")
def analyze(jd_file, resume_file):
    jd = reveilio.JobDescription.from_bytes(jd_file.read(), filename=jd_file.filename)
    rs = reveilio.Resume.from_bytes(resume_file.read(), filename=resume_file.filename)
    return reveilio.analyze_resume(rs, jd).model_dump()

4. Folder analysis: recursion, filters, and parallelism

results = reveilio.analyze_folder(
    "./resumes",
    jd,
    extensions=(".pdf", ".docx"),  # limit to just these two types
    max_workers=8,                    # LLM calls are I/O-bound, so threads help
    recursive=True,                  # walk into subfolders
)

Results are sorted by overall_score descending, with rank assigned from 1 to N. Ties preserve the LLM-returned order.

Rate limits: hosted LLMs will return HTTP 429 if you set max_workers too aggressively. Start with 4 and increase only if your provider quota is generous.

5. Downloading PDF reports

Two exporters ship with the package. Both write a file to disk and return the resulting pathlib.Path:

# Per-candidate detailed report
reveilio.save_report_pdf(result, "reports/alice.pdf")

# Batch ranking summary
reveilio.save_batch_report_pdf(results, "reports/ranking.pdf")

For the raw bytes (for example, to stream a PDF in an HTTP response), bypass the convenience wrapper and call the underlying builder:

from reveilio.reports.pdf import generate_candidate_report

buf = generate_candidate_report(result.model_dump())
return Response(buf.getvalue(), media_type="application/pdf")

6. Inspecting the result

An AnalysisResult is a pydantic model. Every field is documented and ready to use:

result.overall_score            # float, 0-100
result.confidence_level         # 'High' / 'Medium' / 'Low'
result.recommendation           # 'Shortlist' / 'Needs Review' / 'Not Suitable'
result.detailed_scores          # dict: dimension -> {score, reasoning}
result.strengths                # list[str]
result.weaknesses               # list[str]
result.career_flags             # list[str]
result.ai_summary               # executive critique (about 120 words)
result.suggested_questions      # populated only when recommendation == 'Shortlist'
result.candidate_data.name      # extracted structured fields
result.candidate_data.skills
result.candidate_data.experience   # list[ExperienceItem]
result.rank                     # only set by analyze_folder()

7. Filtering and ranking in pure Python

# Only shortlisted candidates, sorted by experience
shortlist = [r for r in results if r.recommendation == "Shortlist"]
shortlist.sort(key=lambda r: r.candidate_data.total_experience, reverse=True)

# Candidates with a specific skill
python_devs = [r for r in results
               if "Python" in (r.candidate_data.skills or [])]

8. Handling provider errors

The scorer wraps its LLM call in a try / except and returns a safe fallback result rather than raising, so a single bad resume will not crash a 500-resume batch. You can detect the error case by checking the summary:

if "Error" in (result.ai_summary or ""):
    print("LLM call failed for", result.candidate_data.filename)

For the parsing and configuration layers (missing API key, unsupported file extension), reveilio does raise. Those are programmer errors worth stopping for.

Continue to the API reference.