Skip to content

Commit 9470658

Browse files
Add Convert to Word
Assisted-by: Codex
1 parent 0e28699 commit 9470658

3 files changed

Lines changed: 229 additions & 0 deletions

File tree

src/pdfrest/client.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
PdfRedactionPreviewPayload,
8080
PdfRestRawFileResponse,
8181
PdfSplitPayload,
82+
PdfToWordPayload,
8283
PngPdfRestPayload,
8384
TiffPdfRestPayload,
8485
UploadURLs,
@@ -2141,6 +2142,32 @@ def merge_pdfs(
21412142
timeout=timeout,
21422143
)
21432144

2145+
def convert_to_word(
2146+
self,
2147+
file: PdfRestFile | Sequence[PdfRestFile],
2148+
*,
2149+
output: str | None = None,
2150+
extra_query: Query | None = None,
2151+
extra_headers: AnyMapping | None = None,
2152+
extra_body: Body | None = None,
2153+
timeout: TimeoutTypes | None = None,
2154+
) -> PdfRestFileBasedResponse:
2155+
"""Convert a PDF to a Word document."""
2156+
2157+
payload: dict[str, Any] = {"files": file}
2158+
if output is not None:
2159+
payload["output"] = output
2160+
2161+
return self._post_file_operation(
2162+
endpoint="/word",
2163+
payload=payload,
2164+
payload_model=PdfToWordPayload,
2165+
extra_query=extra_query,
2166+
extra_headers=extra_headers,
2167+
extra_body=extra_body,
2168+
timeout=timeout,
2169+
)
2170+
21442171
def convert_to_png(
21452172
self,
21462173
files: PdfRestFile | Sequence[PdfRestFile],
@@ -2572,6 +2599,32 @@ async def merge_pdfs(
25722599
timeout=timeout,
25732600
)
25742601

2602+
async def convert_to_word(
2603+
self,
2604+
file: PdfRestFile | Sequence[PdfRestFile],
2605+
*,
2606+
output: str | None = None,
2607+
extra_query: Query | None = None,
2608+
extra_headers: AnyMapping | None = None,
2609+
extra_body: Body | None = None,
2610+
timeout: TimeoutTypes | None = None,
2611+
) -> PdfRestFileBasedResponse:
2612+
"""Asynchronously convert a PDF to a Word document."""
2613+
2614+
payload: dict[str, Any] = {"files": file}
2615+
if output is not None:
2616+
payload["output"] = output
2617+
2618+
return await self._post_file_operation(
2619+
endpoint="/word",
2620+
payload=payload,
2621+
payload_model=PdfToWordPayload,
2622+
extra_query=extra_query,
2623+
extra_headers=extra_headers,
2624+
extra_body=extra_body,
2625+
timeout=timeout,
2626+
)
2627+
25752628
async def convert_to_png(
25762629
self,
25772630
files: PdfRestFile | Sequence[PdfRestFile],

src/pdfrest/models/_internal.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,30 @@ def _serialize_pdf_merge_payload(
476476
return payload
477477

478478

479+
class PdfToWordPayload(BaseModel):
480+
"""Adapt caller options into a pdfRest-ready Word request payload."""
481+
482+
files: Annotated[
483+
list[PdfRestFile],
484+
Field(
485+
min_length=1,
486+
max_length=1,
487+
validation_alias=AliasChoices("file", "files"),
488+
serialization_alias="id",
489+
),
490+
BeforeValidator(_ensure_list),
491+
AfterValidator(
492+
_allowed_mime_types("application/pdf", error_msg="Must be a PDF file")
493+
),
494+
PlainSerializer(_serialize_as_first_file_id),
495+
]
496+
output: Annotated[
497+
str | None,
498+
Field(serialization_alias="output", min_length=1, default=None),
499+
AfterValidator(_validate_output_prefix),
500+
] = None
501+
502+
479503
class BmpPdfRestPayload(BasePdfRestGraphicPayload[Literal["rgb", "gray"]]):
480504
"""Adapt caller options into a pdfRest-ready BMP request payload."""
481505

tests/test_convert_to_word.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
from __future__ import annotations
2+
3+
import json
4+
5+
import httpx
6+
import pytest
7+
from pydantic import ValidationError
8+
9+
from pdfrest import AsyncPdfRestClient, PdfRestClient
10+
from pdfrest.models import PdfRestFile, PdfRestFileBasedResponse, PdfRestFileID
11+
from pdfrest.models._internal import PdfToWordPayload
12+
13+
from .graphics_test_helpers import (
14+
ASYNC_API_KEY,
15+
VALID_API_KEY,
16+
build_file_info_payload,
17+
make_pdf_file,
18+
)
19+
20+
21+
def test_convert_to_word_success(monkeypatch: pytest.MonkeyPatch) -> None:
22+
monkeypatch.delenv("PDFREST_API_KEY", raising=False)
23+
input_file = make_pdf_file(PdfRestFileID.generate(1))
24+
output_id = str(PdfRestFileID.generate())
25+
26+
payload_dump = PdfToWordPayload.model_validate(
27+
{"files": [input_file], "output": "report"}
28+
).model_dump(mode="json", by_alias=True, exclude_none=True, exclude_unset=True)
29+
30+
seen: dict[str, int] = {"post": 0, "get": 0}
31+
32+
def handler(request: httpx.Request) -> httpx.Response:
33+
if request.method == "POST" and request.url.path == "/word":
34+
seen["post"] += 1
35+
payload = json.loads(request.content.decode("utf-8"))
36+
assert payload == payload_dump
37+
return httpx.Response(
38+
200,
39+
json={
40+
"inputId": [input_file.id],
41+
"outputId": [output_id],
42+
},
43+
)
44+
if request.method == "GET" and request.url.path == f"/resource/{output_id}":
45+
seen["get"] += 1
46+
assert request.url.params["format"] == "info"
47+
return httpx.Response(
48+
200,
49+
json=build_file_info_payload(
50+
output_id,
51+
"report.docx",
52+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
53+
),
54+
)
55+
msg = f"Unexpected request {request.method} {request.url}"
56+
raise AssertionError(msg)
57+
58+
transport = httpx.MockTransport(handler)
59+
with PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client:
60+
response = client.convert_to_word(input_file, output="report")
61+
62+
assert seen == {"post": 1, "get": 1}
63+
assert isinstance(response, PdfRestFileBasedResponse)
64+
output_file = response.output_file
65+
assert output_file.name == "report.docx"
66+
assert (
67+
output_file.type
68+
== "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
69+
)
70+
assert response.warning is None
71+
assert str(response.input_id) == str(input_file.id)
72+
73+
74+
@pytest.mark.asyncio
75+
async def test_async_convert_to_word_success(
76+
monkeypatch: pytest.MonkeyPatch,
77+
) -> None:
78+
monkeypatch.delenv("PDFREST_API_KEY", raising=False)
79+
input_file = make_pdf_file(PdfRestFileID.generate(2))
80+
output_id = str(PdfRestFileID.generate())
81+
82+
payload_dump = PdfToWordPayload.model_validate({"files": [input_file]}).model_dump(
83+
mode="json", by_alias=True, exclude_none=True, exclude_unset=True
84+
)
85+
86+
seen: dict[str, int] = {"post": 0, "get": 0}
87+
88+
def handler(request: httpx.Request) -> httpx.Response:
89+
if request.method == "POST" and request.url.path == "/word":
90+
seen["post"] += 1
91+
payload = json.loads(request.content.decode("utf-8"))
92+
assert payload == payload_dump
93+
return httpx.Response(
94+
200,
95+
json={
96+
"inputId": [input_file.id],
97+
"outputId": [output_id],
98+
},
99+
)
100+
if request.method == "GET" and request.url.path == f"/resource/{output_id}":
101+
seen["get"] += 1
102+
assert request.url.params["format"] == "info"
103+
return httpx.Response(
104+
200,
105+
json=build_file_info_payload(
106+
output_id,
107+
"async.docx",
108+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
109+
),
110+
)
111+
msg = f"Unexpected request {request.method} {request.url}"
112+
raise AssertionError(msg)
113+
114+
transport = httpx.MockTransport(handler)
115+
async with AsyncPdfRestClient(api_key=ASYNC_API_KEY, transport=transport) as client:
116+
response = await client.convert_to_word(input_file)
117+
118+
assert seen == {"post": 1, "get": 1}
119+
assert isinstance(response, PdfRestFileBasedResponse)
120+
assert response.output_file.name == "async.docx"
121+
assert (
122+
response.output_file.type
123+
== "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
124+
)
125+
assert str(response.input_id) == str(input_file.id)
126+
127+
128+
def test_convert_to_word_validation(monkeypatch: pytest.MonkeyPatch) -> None:
129+
monkeypatch.delenv("PDFREST_API_KEY", raising=False)
130+
pdf_file = make_pdf_file(PdfRestFileID.generate(1))
131+
png_file = PdfRestFile.model_validate(
132+
build_file_info_payload(
133+
PdfRestFileID.generate(),
134+
"example.png",
135+
"image/png",
136+
)
137+
)
138+
transport = httpx.MockTransport(lambda request: (_ for _ in ()).throw(RuntimeError))
139+
140+
with (
141+
PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client,
142+
pytest.raises(ValidationError, match="Must be a PDF file"),
143+
):
144+
client.convert_to_word(png_file)
145+
146+
with (
147+
PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client,
148+
pytest.raises(
149+
ValidationError, match="List should have at most 1 item after validation"
150+
),
151+
):
152+
client.convert_to_word([pdf_file, make_pdf_file(PdfRestFileID.generate())])

0 commit comments

Comments
 (0)