mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 00:26:26 +00:00
feat(ats): extract PDF text with pypdf before Poppler (#369)
* feat(ats): extract PDF text with pypdf before Poppler Lead the ATS text-layer check with pypdf (BSD, optional pip install). Fall back to pdftotext -layout -enc UTF-8. No cache directory, no installer, no AGPL pymupdf. Windows users without Poppler still get a mechanical parseability check; visual review remains the last resort. * Update verify_pdf.py * Update apply.md * Update verify_pdf.py * Update verify_pdf.py
This commit is contained in:
+40
-10
@@ -4,7 +4,13 @@ import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from tools.verify_pdf import VerificationError, parse_page_count, run_tool, verify_pdf
|
||||
from tools.verify_pdf import (
|
||||
VerificationError,
|
||||
extract_text_layer,
|
||||
parse_page_count,
|
||||
run_tool,
|
||||
verify_pdf,
|
||||
)
|
||||
|
||||
|
||||
class ParsePageCountTests(unittest.TestCase):
|
||||
@@ -25,11 +31,12 @@ class VerifyPdfTests(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
self.temp_dir.cleanup()
|
||||
|
||||
@patch("tools.verify_pdf._extract_pypdf", return_value=None)
|
||||
@patch("tools.verify_pdf.run_tool")
|
||||
def test_accepts_expected_pages_and_text(self, mock_run_tool):
|
||||
def test_accepts_expected_pages_and_text(self, mock_run_tool, _pypdf):
|
||||
mock_run_tool.side_effect = [
|
||||
"Pages: 2\n",
|
||||
"Professional\nExperience [your.email@example.com]\n",
|
||||
"Pages: 2\n",
|
||||
]
|
||||
|
||||
verify_pdf(
|
||||
@@ -39,23 +46,29 @@ class VerifyPdfTests(unittest.TestCase):
|
||||
required_text=("Professional Experience", "[your.email@example.com]"),
|
||||
)
|
||||
|
||||
@patch("tools.verify_pdf._extract_pypdf", return_value=None)
|
||||
@patch("tools.verify_pdf.run_tool")
|
||||
def test_rejects_wrong_page_count(self, mock_run_tool):
|
||||
mock_run_tool.return_value = "Pages: 3\n"
|
||||
def test_rejects_wrong_page_count(self, mock_run_tool, _pypdf):
|
||||
mock_run_tool.side_effect = ["ok", "Pages: 3\n"]
|
||||
|
||||
with self.assertRaisesRegex(VerificationError, "expected 2 page.*found 3"):
|
||||
verify_pdf(self.pdf, expected_pages=2)
|
||||
|
||||
@patch("tools.verify_pdf._extract_pypdf", return_value=None)
|
||||
@patch("tools.verify_pdf.run_tool")
|
||||
def test_rejects_too_little_extractable_text(self, mock_run_tool):
|
||||
mock_run_tool.return_value = "short"
|
||||
def test_rejects_too_little_extractable_text(self, mock_run_tool, _pypdf):
|
||||
mock_run_tool.side_effect = ["short", "Pages: 1\n"]
|
||||
|
||||
with self.assertRaisesRegex(VerificationError, "expected at least 20"):
|
||||
verify_pdf(self.pdf, min_chars=20)
|
||||
|
||||
@patch("tools.verify_pdf._extract_pypdf", return_value=None)
|
||||
@patch("tools.verify_pdf.run_tool")
|
||||
def test_rejects_missing_required_text(self, mock_run_tool):
|
||||
mock_run_tool.return_value = "Readable text, but not the expected section."
|
||||
def test_rejects_missing_required_text(self, mock_run_tool, _pypdf):
|
||||
mock_run_tool.side_effect = [
|
||||
"Readable text, but not the expected section.",
|
||||
"Pages: 1\n",
|
||||
]
|
||||
|
||||
with self.assertRaisesRegex(VerificationError, "Professional Experience"):
|
||||
verify_pdf(self.pdf, required_text=("Professional Experience",))
|
||||
@@ -64,11 +77,28 @@ class VerifyPdfTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(VerificationError, "PDF does not exist"):
|
||||
verify_pdf(Path(self.temp_dir.name) / "missing.pdf")
|
||||
|
||||
@patch("tools.verify_pdf._extract_pypdf", return_value=("Hello ATS body", 1))
|
||||
def test_pypdf_is_preferred_over_poppler(self, _pypdf):
|
||||
text, pages, extractor = extract_text_layer(self.pdf)
|
||||
self.assertEqual(extractor, "pypdf")
|
||||
self.assertEqual(text, "Hello ATS body")
|
||||
self.assertEqual(pages, 1)
|
||||
|
||||
@patch("tools.verify_pdf._extract_pypdf", return_value=None)
|
||||
@patch("tools.verify_pdf.run_tool")
|
||||
def test_falls_back_to_pdftotext(self, mock_run_tool, _pypdf):
|
||||
mock_run_tool.side_effect = ["poppler text", "Pages: 2\n"]
|
||||
text, pages, extractor = extract_text_layer(self.pdf)
|
||||
self.assertEqual(extractor, "pdftotext")
|
||||
self.assertEqual(text, "poppler text")
|
||||
self.assertEqual(pages, 2)
|
||||
self.assertEqual(mock_run_tool.call_args_list[0][0][0][:3], ["pdftotext", "-layout", "-enc"])
|
||||
|
||||
|
||||
class RunToolTests(unittest.TestCase):
|
||||
@patch("tools.verify_pdf.subprocess.run", side_effect=FileNotFoundError)
|
||||
def test_reports_missing_poppler_command(self, _mock_run):
|
||||
with self.assertRaisesRegex(VerificationError, "install poppler-utils"):
|
||||
with self.assertRaisesRegex(VerificationError, "pip install pypdf"):
|
||||
run_tool(["pdftotext", "example.pdf", "-"])
|
||||
|
||||
@patch("tools.verify_pdf.subprocess.run")
|
||||
|
||||
Reference in New Issue
Block a user