Skip to content

Resume Screening Desk (Python)

Updated

What it does#

Drop CVs and a job description, extract structured candidate profiles, review the criteria, then rank candidates by skill coverage and experience. Every score explains itself: which required skills matched, which are missing, and where the experience figure came from.

Source: resume-desk in the examples repository.

Ranked candidates with matched and missing skills
Ranked candidates with matched and missing skills

Run it#

Requires Python 3.10+.

cd resume-desk
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python app.py

Open http://127.0.0.1:3102 and choose Explore sample data. With Python, SQL and REST APIs required, three years' minimum experience and an 80% skill weight, one candidate scores 100, one scores 66.67, and one stays unranked because their experience cannot be determined.

Note
The package is docxtract-sdk and the import is docxtract. The unscoped PyPI docxtract is an unrelated DOCX text utility. requirements.txt pins the right one.

The SDK calls#

app.py is a small Flask app. Two of its four routes call DocXtract.

from docxtract import DocXtract, DocXtractError

dx = DocXtract(key, max_retries=2)
models = dx.models()                 # costs no credits

Extraction uses a temporary directory as a context manager, so the uploaded bytes are gone by the time the function returns.

options = {"document_type": settings.get("documentType") or "resume", "store_db": False}
if model.strip():
    options["model"] = model.strip()

with tempfile.TemporaryDirectory(prefix="resume-desk-") as folder:
    path = Path(folder) / ("document" + ext)
    path.write_bytes(content)
    path.chmod(0o600)
    result = dx.extract(str(path), **options)

return jsonify(raw=result.data, meta=result.meta, complete=result.complete)

The app extracts two kinds of document with the same call: CVs, and the job description when it arrives as a PDF or image. Each has its own document type and model setting. A resume-oriented template may not infer requirements from a job advert, so the app always shows the criteria for review before ranking.

The job description#

Three ways in, only one of which costs credits.

  • PDF, PNG or JPG: extracted with the Python SDK. Recognised required_skills or skills and minimum_experience_years or min_years prefill the criteria.
  • A .txt file: parsed locally, no API call. Labelled lines prefill the criteria.
  • Pasted text: kept as is. You enter the skills and years yourself. The app does not invent a skills list from prose.

For the text file, the labelled lines look like this:

Required skills: Python, SQL, REST APIs
Minimum experience: 3 years
Job criteria with required skills, minimum years and skill weight
Job criteria with required skills, minimum years and skill weight

The ranking rules#

score = skill coverage × skill weight + experience coverage × (1 − skill weight)
  • Skill coverage is the share of distinct required skills found in the candidate's structured skills, normalised for case and whitespace. Matching is exact. Java does not match JavaScript, and there are no inferred synonyms.
  • Experience coverage is min(candidate years / required years, 1).
  • Reported total experience is preferred. If absent, complete ISO role dates are merged so overlapping jobs are not double-counted. Ambiguous dates stay unknown.
  • Missing structured skills, unknown experience when a minimum applies, or a partial extraction keep the candidate unranked. Unknown is never scored as zero.
  • Equal scores share a rank. Any correction or criteria change invalidates the previous ranking.

Scores are a review aid. The app does not accept or reject anyone and does not contact anyone. Personal fields in the extraction play no part in the score.

Field mapping#

Normalised keys are name, skills, years and experience. Defaults cover the common template shapes, including full_name, technical_skills, total_experience_years and work_experience. Skills may be a list, comma-separated text, or category objects that contain lists.

{"name":"Candidate.Name","skills":"Candidate.TechnicalSkills","years":"Candidate.TotalYears","experience":"Candidate.Roles"}

Where things are#

FilePurpose
app.pyFlask routes for connect, extract, analyse and rank
includes/screening.pyProfile normalisation, merged experience intervals, explainable ranking
includes/*.phpHTML-only view fragments assembled by Python. PHP is not required
tests/test_screening.pyRanking weights and ties, overlapping employment, ignored personal fields, cleanup
.venv/bin/python -m unittest discover -s tests -v