Metadata-Version: 2.1
Name: resumereaderapi
Version: 1.0.1
Summary: Python client for the Resume Reader API: parse PDF, DOCX and scanned resumes into 114 structured JSON fields, and normalize job titles, skills and locations.
Home-page: https://www.resumereaderapi.com
Author: Alpha Quantum
Author-email: info@alpha-quantum.com
License: MIT
Project-URL: Homepage, https://www.resumereaderapi.com
Project-URL: Documentation, https://www.resumereaderapi.com/api-v2.php
Project-URL: Source, https://github.com/explainableaixai/resumereaderapi
Project-URL: Mirror, https://gitlab.com/url-classifications/resumereaderapi
Project-URL: Pricing, https://www.resumereaderapi.com/pricing.php
Keywords: resume parser,cv parser,resume parsing api,resume to json,ats,recruiting,hr tech,job title normalization,skill normalization,location normalization,ocr,cv anonymization
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Office/Business
Classifier: Topic :: Text Processing
Requires-Python: >=3.7
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.20.0

# resumereaderapi (Python)

Resume parser client for Python. Hand it a PDF, a DOCX, a scan or raw text and get structured candidate data back as a dictionary.

Needs `requests`. Python 3.7 and later.

```bash
pip install resumereaderapi
```

## Why use a hosted resume parser

Writing regular expressions for CVs is a trap. Layouts vary, languages vary, and half the documents are scans. A hosted resume parser gives you one stable shape no matter what the candidate uploaded.

The service behind this package is the [resume parser](https://www.resumereaderapi.com/) from Alpha Quantum. It returns 114 fields and marks missing facts as `None` instead of guessing.

## Parse in three lines

```python
from resumereaderapi import ResumeReader

rr = ResumeReader("YOUR_API_KEY")
data = rr.parse_file("cv.pdf")

print(data["resume"]["contact"]["emails"])
print(data["remaining_credits"])
```

## Calls

```python
rr.parse_text(text, field_names=None, exclude_sensitive=False, anonymize=False,
              max_pages=None, sections=None, language=None)
rr.parse_file(path, ...same options...)
rr.parse_url(https_url, ...same options...)

rr.normalize_titles(["Sr. SWE II"])
rr.normalize_skills(["K8s", "JS"])
rr.normalize_locations(["NYC"])
```

Normalizer methods batch in groups of 100 and return `{"results": [...], "credits_used": float, "remaining_credits": float}`.

## Skill normalization in practice

Candidates write the same skill ten ways. `JS`, `Javascript` and `java script` should be one filter value.

```python
raw = ["JS", "Javascript", "Python 3.11", "py", "MS Excel", "n/a"]
out = rr.normalize_skills(raw)
for item in out["results"]:
    print(item["input"], "->", item["normalized_skill"], item["skill_type"])
```

Junk entries such as "n/a" come back with `normalized_skill` set to `None`. See the [skill normalization](https://www.resumereaderapi.com/normalization/skills.php) page for fifty real examples.

## Anonymize before a human sees the CV

```python
data = rr.parse_file(
    "cv.docx",
    anonymize=True,
    exclude_sensitive=True,
)
```

Names, contact values and sensitive fields become placeholders such as `[NAME]` and `[EMAIL]`. This is the base of a blind review process, covered under [CV anonymization](https://www.resumereaderapi.com/use-cases/cv-anonymization.php).

## Handle errors

```python
from resumereaderapi import ResumeReaderError

try:
    rr.parse_file(path)
except ResumeReaderError as exc:
    if exc.status == 402:
        print("buy more credits")
    elif exc.status in (413, 422):
        print("bad document:", exc)
    else:
        raise
```

## Example: into a DataFrame

```python
import pandas as pd
from pathlib import Path

rows = []
for p in Path("cvs").glob("*.pdf"):
    r = rr.parse_file(str(p))["resume"]
    rows.append({
        "name": r["contact"]["full_name"],
        "city": r["contact"]["address"]["city"],
        "title": (r["career"]["current_position"] or {}).get("title"),
        "years": r["career"]["total_experience_years"],
        "skills": ", ".join(r["skills"]["technical"]),
    })

pd.DataFrame(rows).to_csv("candidates.csv", index=False)
```

## Example: Flask upload endpoint

```python
from flask import Flask, request, jsonify
import tempfile, os

app = Flask(__name__)

@app.post("/parse")
def parse():
    f = request.files["cv"]
    with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(f.filename)[1]) as tmp:
        f.save(tmp.name)
    try:
        return jsonify(rr.parse_file(tmp.name)["resume"])
    finally:
        os.unlink(tmp.name)
```

<!--expanded-->
## A Python workflow from folder to table

Most people meet resume parsing as a pile of files in a folder. The job is to turn the pile into a table you can sort and join. Python is well suited to it because the pieces are standard: `pathlib` to find files, a thread pool to parallelize calls, and `pandas` to hold the result.

Start by deciding the columns you care about. A recruiting database usually wants a name, a contact method, a location, a current title, a seniority level, years of experience and a list of skills. The parser returns far more than that, so write a small function that selects and flattens what you need, and keep the complete JSON next to it for later.

```python
from pathlib import Path
import json
import pandas as pd
from concurrent.futures import ThreadPoolExecutor
from resumereaderapi import ResumeReader, ResumeReaderError

rr = ResumeReader("YOUR_API_KEY")

def row(path: Path):
    try:
        out = rr.parse_file(str(path))
    except ResumeReaderError as exc:
        return {"file": path.name, "error": exc.status}
    r = out["resume"]
    pos = r["career"]["current_position"] or {}
    return {
        "file": path.name,
        "name": r["contact"]["full_name"],
        "email": (r["contact"]["emails"] or [None])[0],
        "city": r["contact"]["address"]["city"],
        "title": pos.get("title"),
        "seniority": r["career"]["seniority_level"],
        "years": r["career"]["total_experience_years"],
        "skills": "|".join(r["skills"]["technical"]),
        "raw": json.dumps(r),
    }

files = sorted(Path("inbox").glob("*.pdf"))
with ThreadPoolExecutor(max_workers=3) as pool:
    table = pd.DataFrame(pool.map(row, files))
table.to_parquet("candidates.parquet")
```

Three workers is deliberate. The service allows 30 requests per 60 seconds per IP, so a small pool with a pause is safer than a large one that triggers a 429.

## Choosing options

The options change the shape of what you get, so decide them once and write them down.

- **`field_names="fr"`** renames the keys to French for teams that want French schemas. Values keep the language of the resume.
- **`exclude_sensitive=True`** skips the nine sensitive fields completely.
- **`anonymize=True`** replaces contact values and sensitive values with placeholders such as `[NAME]` and `[EMAIL]`.
- **`sections=[...]`** limits extraction to the sections you list, which is useful when you only need contact details.
- **`max_pages`** controls how many pages are processed.
- **`language`** is a hint for non English documents.

If you plan to compare candidates, anonymize the copy that reviewers see and keep the unmasked copy in a restricted store. That arrangement supports blind review without losing the ability to contact a shortlisted person.

## Normalizing after the fact

Parsing gives you the candidate's own words. Normalization gives you a common vocabulary. Run it on columns, not on single values, to keep the cost low and the batches full. Each item costs a tenth of a credit, and batches of up to 100 are handled in one call.

```python
titles = table["title"].dropna().unique().tolist()
mapped = rr.normalize_titles(titles)["results"]
lookup = {m["input"]: m for m in mapped}

table["canonical_title"] = table["title"].map(lambda t: lookup.get(t, {}).get("normalized_title"))
table["function"] = table["title"].map(lambda t: lookup.get(t, {}).get("job_function"))
```

The same pattern works for skills and locations. Deduplicate before sending. Ten thousand candidates may share only two thousand distinct job titles, and you pay for the distinct values only.

The title normalizer returns a canonical title, a seniority level from intern through owner, and a job function such as engineering, sales or finance. The skill normalizer classifies each skill into one of fourteen types, and the location normalizer returns a city, a region, a country and an ISO country code, plus a metro area and a remote flag when they apply. The [skill normalization page](https://www.resumereaderapi.com/normalization/skills.php) shows the alias handling and the type list in detail.

## Data quality checks

Run a few checks after every batch. They take minutes to write and save hours later.

1. The share of rows with an error status. A rise suggests a change in your inputs, such as a new scanner producing unreadable files.
2. The share with an empty email. A jump suggests that contact blocks are not being found in a new template.
3. The distribution of `seniority`. If it shifts sharply, look at the source of your files before you blame the parser.
4. The number of duplicate emails. Duplicates are usually real people who applied twice, and they are a useful signal for deduplication.

## Related reading

Resume data often meets other datasets. A marketing team may want to understand where to advertise open roles, and the [guide to purchase intent segments](https://www.cookielessaudiences.com/features/purchase-intent-data.php) describes a coded, cookie free way to describe what the readers of a site are likely to want. A corporate development team that evaluates staffing businesses can read how to [map a whole thesis universe](https://www.acquisitionuniverse.com/use-cases/thesis-universe-mapping.php) for a vertical.

<!--extra-->
## A final practical tip

Save the raw JSON of every parse next to your flattened table. Flattening is a decision, and decisions change. With the raw record, adding a new column is a re-read of stored data. Without it, adding a column means paying for the parse again.

<!--further-->
## Further reading and practical notes

The [Python documentation](https://docs.python.org/3/) covers `concurrent.futures`, `pathlib` and `base64`, which the examples above use. For the legal side of handling candidate data in Europe, the [European Commission pages on data protection](https://commission.europa.eu/law/law-topic/data-protection_en) set out the principles that apply to a recruiting database: lawful basis, purpose limitation, minimisation and storage limits.

Those principles translate into code in a few concrete ways. Store only the fields you need for the purpose you stated. Keep the original file and the parsed record under the same retention rule. Record when each record was created, so deletion jobs can find it. And make deletion real, which means removing the parsed JSON from every table and backup that holds it, not only from the main one.

When you design the pipeline, think about the failure that hurts most. It is rarely a crash. It is a silent mismatch, for example a parser that returns the wrong person's email because a template put a reference's contact details at the top. Protect against it by sampling. Every week, pull twenty parsed records at random and read them next to their source files. Twenty records take half an hour, and they catch problems that metrics cannot see.

## Frequently needed tweaks

**I only need emails.** Use `sections=["contact"]` to limit extraction.

**I need to re-parse with different options.** Keep the original files. Parsing is cheap compared with losing the source.

**I need to prove what I stored.** Store the raw JSON and the date. Do not edit it in place.

**I have a mix of languages.** Pass `language` when you know it. The parser also handles mixed content, but a hint helps.

## Notes on reliability

The published test suite for the service runs against the live endpoints before every release, including every documented error path, so the status codes in this README are the ones you will actually see. Treat them as part of the contract and write code against them.

## Questions

**What file types?** PDF, DOCX, TXT, RTF, HTML, ODT, spreadsheets and images including scanned PDFs.

**Is there a limit per call?** 10 MB, 20 pages and about 30,000 tokens.

**Does it work in French?** Pass `field_names="fr"` for French keys. Values keep the language of the resume.

MIT license. Contact: info@alpha-quantum.com


