Skip to content

Commit 9b7ff4e

Browse files
client: Add extract_pdf_text method for text extraction
- Introduced `extract_pdf_text` to extract text content from PDFs with options like `full_text`, `preserve_line_breaks`, `word_style`, and `word_coordinates`. - Added both sync and async implementations for the `extract_pdf_text` method. - Validated payloads using `ExtractTextPayload` and model validation. - Returns structured `ExtractedTextDocument` with extracted text and metadata. tests: Add unit and live tests for extract_pdf_text - Added comprehensive unit tests to validate different payload combinations. - Introduced live tests to ensure end-to-end functionality of API integration. - Verified error handling for invalid pages, payloads, and server responses. Assisted-by: Codex
1 parent 726c4f4 commit 9b7ff4e

3 files changed

Lines changed: 494 additions & 0 deletions

File tree

src/pdfrest/client.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
translate_httpx_error,
6161
)
6262
from .models import (
63+
ExtractedTextDocument,
6364
PdfRestDeletionResponse,
6465
PdfRestErrorResponse,
6566
PdfRestFile,
@@ -2388,6 +2389,48 @@ def extract_images(
23882389
timeout=timeout,
23892390
)
23902391

2392+
def extract_pdf_text(
2393+
self,
2394+
file: PdfRestFile | Sequence[PdfRestFile],
2395+
*,
2396+
pages: PdfPageSelection | None = None,
2397+
full_text: Literal["off", "by_page", "document"] = "document",
2398+
preserve_line_breaks: bool = False,
2399+
word_style: bool = False,
2400+
word_coordinates: bool = False,
2401+
extra_query: Query | None = None,
2402+
extra_headers: AnyMapping | None = None,
2403+
extra_body: Body | None = None,
2404+
timeout: TimeoutTypes | None = None,
2405+
) -> ExtractedTextDocument:
2406+
"""Extract text content from a PDF and return parsed JSON results."""
2407+
2408+
payload: dict[str, Any] = {
2409+
"files": file,
2410+
"full_text": full_text,
2411+
"preserve_line_breaks": preserve_line_breaks,
2412+
"word_style": word_style,
2413+
"word_coordinates": word_coordinates,
2414+
"output_type": "json",
2415+
}
2416+
if pages is not None:
2417+
payload["pages"] = pages
2418+
2419+
validated_payload = ExtractTextPayload.model_validate(payload)
2420+
request = self.prepare_request(
2421+
"POST",
2422+
"/extracted-text",
2423+
json_body=validated_payload.model_dump(
2424+
mode="json", by_alias=True, exclude_none=True, exclude_unset=True
2425+
),
2426+
extra_query=extra_query,
2427+
extra_headers=extra_headers,
2428+
extra_body=extra_body,
2429+
timeout=timeout,
2430+
)
2431+
raw_payload = self._send_request(request)
2432+
return ExtractedTextDocument.model_validate(raw_payload)
2433+
23912434
def extract_pdf_text_to_file(
23922435
self,
23932436
file: PdfRestFile | Sequence[PdfRestFile],
@@ -3398,6 +3441,48 @@ async def extract_images(
33983441
timeout=timeout,
33993442
)
34003443

3444+
async def extract_pdf_text(
3445+
self,
3446+
file: PdfRestFile | Sequence[PdfRestFile],
3447+
*,
3448+
pages: PdfPageSelection | None = None,
3449+
full_text: Literal["off", "by_page", "document"] = "document",
3450+
preserve_line_breaks: bool = False,
3451+
word_style: bool = False,
3452+
word_coordinates: bool = False,
3453+
extra_query: Query | None = None,
3454+
extra_headers: AnyMapping | None = None,
3455+
extra_body: Body | None = None,
3456+
timeout: TimeoutTypes | None = None,
3457+
) -> ExtractedTextDocument:
3458+
"""Extract text content from a PDF and return parsed JSON results."""
3459+
3460+
payload: dict[str, Any] = {
3461+
"files": file,
3462+
"full_text": full_text,
3463+
"preserve_line_breaks": preserve_line_breaks,
3464+
"word_style": word_style,
3465+
"word_coordinates": word_coordinates,
3466+
"output_type": "json",
3467+
}
3468+
if pages is not None:
3469+
payload["pages"] = pages
3470+
3471+
validated_payload = ExtractTextPayload.model_validate(payload)
3472+
request = self.prepare_request(
3473+
"POST",
3474+
"/extracted-text",
3475+
json_body=validated_payload.model_dump(
3476+
mode="json", by_alias=True, exclude_none=True, exclude_unset=True
3477+
),
3478+
extra_query=extra_query,
3479+
extra_headers=extra_headers,
3480+
extra_body=extra_body,
3481+
timeout=timeout,
3482+
)
3483+
raw_payload = await self._send_request(request)
3484+
return ExtractedTextDocument.model_validate(raw_payload)
3485+
34013486
async def extract_pdf_text_to_file(
34023487
self,
34033488
file: PdfRestFile | Sequence[PdfRestFile],
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
from __future__ import annotations
2+
3+
from itertools import product
4+
5+
import pytest
6+
7+
from pdfrest import AsyncPdfRestClient, PdfRestApiError, PdfRestClient
8+
from pdfrest.models import ExtractedTextDocument
9+
10+
from ..resources import get_test_resource_path
11+
12+
FULL_TEXT_OPTIONS = ("off", "by_page", "document")
13+
BOOL_OPTION_SETS = list(product([False, True], repeat=3))
14+
15+
LIVE_OPTION_SETS = [
16+
pytest.param(
17+
{
18+
"full_text": full_text,
19+
"preserve_line_breaks": preserve,
20+
"word_style": word_style,
21+
"word_coordinates": word_coordinates,
22+
},
23+
id=f"{full_text}-plb-{int(preserve)}-ws-{int(word_style)}-wc-{int(word_coordinates)}",
24+
)
25+
for full_text in FULL_TEXT_OPTIONS
26+
for preserve, word_style, word_coordinates in BOOL_OPTION_SETS
27+
]
28+
29+
30+
def _assert_live_full_text(
31+
response: ExtractedTextDocument,
32+
*,
33+
full_text_mode: str,
34+
) -> None:
35+
if full_text_mode == "off":
36+
assert response.full_text is None
37+
elif full_text_mode == "document":
38+
assert response.full_text is not None
39+
assert response.full_text.document_text is not None
40+
else:
41+
assert response.full_text is not None
42+
assert response.full_text.pages is not None
43+
44+
45+
@pytest.mark.parametrize("options", LIVE_OPTION_SETS)
46+
def test_live_extract_pdf_text_success(
47+
options: dict[str, bool | str],
48+
pdfrest_api_key: str,
49+
pdfrest_live_base_url: str,
50+
) -> None:
51+
resource = get_test_resource_path("report.pdf")
52+
with PdfRestClient(
53+
api_key=pdfrest_api_key,
54+
base_url=pdfrest_live_base_url,
55+
) as client:
56+
uploaded = client.files.create_from_paths([resource])[0]
57+
response = client.extract_pdf_text(uploaded, **options)
58+
59+
assert isinstance(response, ExtractedTextDocument)
60+
assert response.input_id == uploaded.id
61+
_assert_live_full_text(response, full_text_mode=options["full_text"])
62+
if options["word_style"] == "on" or options["word_coordinates"] == "on":
63+
assert response.words is not None
64+
assert response.words
65+
66+
67+
@pytest.mark.asyncio
68+
@pytest.mark.parametrize("options", LIVE_OPTION_SETS)
69+
async def test_live_async_extract_pdf_text_success(
70+
options: dict[str, bool | str],
71+
pdfrest_api_key: str,
72+
pdfrest_live_base_url: str,
73+
) -> None:
74+
resource = get_test_resource_path("report.pdf")
75+
async with AsyncPdfRestClient(
76+
api_key=pdfrest_api_key,
77+
base_url=pdfrest_live_base_url,
78+
) as client:
79+
uploaded = (await client.files.create_from_paths([resource]))[0]
80+
response = await client.extract_pdf_text(uploaded, **options)
81+
82+
assert isinstance(response, ExtractedTextDocument)
83+
assert response.input_id == uploaded.id
84+
_assert_live_full_text(response, full_text_mode=options["full_text"])
85+
if options["word_style"] == "on" or options["word_coordinates"] == "on":
86+
assert response.words is not None
87+
assert response.words
88+
89+
90+
def test_live_extract_pdf_text_invalid_pages(
91+
pdfrest_api_key: str,
92+
pdfrest_live_base_url: str,
93+
) -> None:
94+
resource = get_test_resource_path("report.pdf")
95+
with PdfRestClient(
96+
api_key=pdfrest_api_key,
97+
base_url=pdfrest_live_base_url,
98+
) as client:
99+
uploaded = client.files.create_from_paths([resource])[0]
100+
with pytest.raises(PdfRestApiError, match=r"(?i)page"):
101+
client.extract_pdf_text(
102+
uploaded,
103+
extra_body={"pages": "last-1"},
104+
)
105+
106+
107+
@pytest.mark.asyncio
108+
async def test_live_async_extract_pdf_text_invalid_pages(
109+
pdfrest_api_key: str,
110+
pdfrest_live_base_url: str,
111+
) -> None:
112+
resource = get_test_resource_path("report.pdf")
113+
async with AsyncPdfRestClient(
114+
api_key=pdfrest_api_key,
115+
base_url=pdfrest_live_base_url,
116+
) as client:
117+
uploaded = (await client.files.create_from_paths([resource]))[0]
118+
with pytest.raises(PdfRestApiError, match=r"(?i)page"):
119+
await client.extract_pdf_text(
120+
uploaded,
121+
extra_body={"pages": "last-1"},
122+
)

0 commit comments

Comments
 (0)