Handle French experience connectors, common degree forms, and en/em dash bullets while keeping the extraction deterministic and narrow.
236 lines
6.0 KiB
Python
236 lines
6.0 KiB
Python
from textwrap import dedent
|
||
from pathlib import Path
|
||
|
||
from job_research.profile.cv_extractor import extract_cv_signals
|
||
from job_research.profile.cv_extractor import extract_pdf_text
|
||
|
||
|
||
def test_extract_cv_signals_reads_basic_fields_from_text() -> None:
|
||
text = dedent(
|
||
"""
|
||
Tonio
|
||
Location: France
|
||
Languages: French, English
|
||
Skills: Python, SQL, Terraform, GCP, BigQuery
|
||
Data Engineer at Company A
|
||
Analytics Engineer at Company B
|
||
"""
|
||
).strip()
|
||
|
||
extracted = extract_cv_signals(text)
|
||
|
||
assert extracted["name"] == "Tonio"
|
||
assert extracted["location"] == "France"
|
||
assert extracted["languages"] == ["French", "English"]
|
||
assert extracted["skills"] == ["Python", "SQL", "Terraform", "GCP", "BigQuery"]
|
||
assert extracted["experience_entries"][0]["title"] == "Data Engineer"
|
||
assert len(extracted["experience_entries"]) == 2
|
||
|
||
|
||
def test_extract_cv_signals_allows_single_word_titles() -> None:
|
||
text = dedent(
|
||
"""
|
||
Tonio
|
||
Location: France
|
||
Consultant at Accenture
|
||
"""
|
||
).strip()
|
||
|
||
extracted = extract_cv_signals(text)
|
||
|
||
assert extracted["experience_entries"] == [
|
||
{"title": "Consultant", "company": "Accenture"}
|
||
]
|
||
|
||
|
||
def test_extract_cv_signals_allows_lowercase_company_names() -> None:
|
||
text = dedent(
|
||
"""
|
||
Tonio
|
||
Location: France
|
||
Data Engineer at leboncoin
|
||
"""
|
||
).strip()
|
||
|
||
extracted = extract_cv_signals(text)
|
||
|
||
assert extracted["experience_entries"] == [
|
||
{"title": "Data Engineer", "company": "leboncoin"}
|
||
]
|
||
|
||
|
||
def test_extract_cv_signals_ignores_prose_after_company_name() -> None:
|
||
text = dedent(
|
||
"""
|
||
Tonio
|
||
Location: France
|
||
Senior engineer at Microsoft before moving to Paris.
|
||
"""
|
||
).strip()
|
||
|
||
extracted = extract_cv_signals(text)
|
||
|
||
assert extracted["experience_entries"] == []
|
||
|
||
|
||
def test_extract_cv_signals_normalizes_bullet_prefixed_fields_and_experience() -> None:
|
||
text = dedent(
|
||
"""
|
||
Tonio
|
||
- Location: France
|
||
- Data Engineer at Company A
|
||
"""
|
||
).strip()
|
||
|
||
extracted = extract_cv_signals(text)
|
||
|
||
assert extracted["location"] == "France"
|
||
assert extracted["experience_entries"] == [
|
||
{"title": "Data Engineer", "company": "Company A"}
|
||
]
|
||
|
||
|
||
def test_extract_cv_signals_normalizes_en_dash_bullet_prefixed_fields_and_experience() -> None:
|
||
text = dedent(
|
||
"""
|
||
Tonio
|
||
– Location: France
|
||
— Data Engineer at Company A
|
||
"""
|
||
).strip()
|
||
|
||
extracted = extract_cv_signals(text)
|
||
|
||
assert extracted["location"] == "France"
|
||
assert extracted["experience_entries"] == [
|
||
{"title": "Data Engineer", "company": "Company A"}
|
||
]
|
||
|
||
|
||
def test_extract_cv_signals_parses_french_experience_connectors() -> None:
|
||
text = dedent(
|
||
"""
|
||
Tonio
|
||
Location: France
|
||
Ingénieur chez Dassault Systèmes
|
||
Développeur au CNRS
|
||
"""
|
||
).strip()
|
||
|
||
extracted = extract_cv_signals(text)
|
||
|
||
assert extracted["experience_entries"] == [
|
||
{"title": "Ingénieur", "company": "Dassault Systèmes"},
|
||
{"title": "Développeur", "company": "CNRS"},
|
||
]
|
||
|
||
|
||
def test_extract_cv_signals_ignores_in_paris_prose_tail() -> None:
|
||
text = dedent(
|
||
"""
|
||
Tonio
|
||
Location: France
|
||
Data Engineer at Microsoft in Paris
|
||
"""
|
||
).strip()
|
||
|
||
extracted = extract_cv_signals(text)
|
||
|
||
assert extracted["experience_entries"] == []
|
||
|
||
|
||
def test_extract_cv_signals_extracts_education_entries_after_heading() -> None:
|
||
text = dedent(
|
||
"""
|
||
Tonio
|
||
Location: France
|
||
Education:
|
||
- Master of Science in Data Science at University of Paris
|
||
"""
|
||
).strip()
|
||
|
||
extracted = extract_cv_signals(text)
|
||
|
||
assert extracted["education_entries"] == [
|
||
{
|
||
"credential": "Master of Science in Data Science",
|
||
"institution": "University of Paris",
|
||
}
|
||
]
|
||
|
||
|
||
def test_extract_cv_signals_extracts_common_french_education_entries() -> None:
|
||
text = dedent(
|
||
"""
|
||
Tonio
|
||
Location: France
|
||
Education:
|
||
Diplôme d'ingénieur
|
||
CentraleSupélec
|
||
MSc
|
||
University of Paris
|
||
BSc
|
||
University of Oxford
|
||
Bac+5
|
||
École Polytechnique
|
||
"""
|
||
).strip()
|
||
|
||
extracted = extract_cv_signals(text)
|
||
|
||
assert extracted["education_entries"] == [
|
||
{
|
||
"credential": "Diplôme d'ingénieur",
|
||
"institution": "CentraleSupélec",
|
||
},
|
||
{"credential": "MSc", "institution": "University of Paris"},
|
||
{"credential": "BSc", "institution": "University of Oxford"},
|
||
{"credential": "Bac+5", "institution": "École Polytechnique"},
|
||
]
|
||
|
||
|
||
def test_extract_cv_signals_ignores_before_moving_prose_tail() -> None:
|
||
text = dedent(
|
||
"""
|
||
Tonio
|
||
Location: France
|
||
Senior Engineer at Microsoft Before moving to Paris.
|
||
"""
|
||
).strip()
|
||
|
||
extracted = extract_cv_signals(text)
|
||
|
||
assert extracted["experience_entries"] == []
|
||
|
||
|
||
def test_extract_pdf_text_skips_blank_pages(monkeypatch) -> None:
|
||
class FakePage:
|
||
def __init__(self, text: str | None) -> None:
|
||
self._text = text
|
||
|
||
def extract_text(self) -> str | None:
|
||
return self._text
|
||
|
||
class FakePdfReader:
|
||
def __init__(self, path: str) -> None:
|
||
self.path = path
|
||
self.pages = [
|
||
FakePage(None),
|
||
FakePage("Tonio"),
|
||
FakePage(""),
|
||
FakePage("Data Engineer"),
|
||
]
|
||
|
||
seen_paths: list[str] = []
|
||
|
||
def fake_pdf_reader(path: str) -> FakePdfReader:
|
||
seen_paths.append(path)
|
||
return FakePdfReader(path)
|
||
|
||
monkeypatch.setattr("job_research.profile.cv_extractor.PdfReader", fake_pdf_reader)
|
||
|
||
extracted = extract_pdf_text(Path("cv.pdf"))
|
||
|
||
assert extracted == "Tonio\nData Engineer"
|
||
assert seen_paths == ["cv.pdf"]
|