Skip to content

Commit 18562ca

Browse files
Add Blank PDF methods
- Add `blank_pdf()` sync and async methods - Ensure compatibility with Blank PDF response: - Allow PdfRestRawFileResponse.input_id to default empty so missing inputId doesn’t fail validation - When normalizing file responses, fall back to raw ids (outputId) when inputId is absent for blank-pdf - Document blank-pdf handling to keep response construction working without server-provided input ids
1 parent 83656f8 commit 18562ca

6 files changed

Lines changed: 566 additions & 3 deletions

File tree

src/pdfrest/client.py

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
GifPdfRestPayload,
8282
JpegPdfRestPayload,
8383
OcrPdfPayload,
84+
PdfBlankPayload,
8485
PdfCompressPayload,
8586
PdfConvertColorsPayload,
8687
PdfFlattenAnnotationsPayload,
@@ -121,7 +122,9 @@
121122
PdfColorProfile,
122123
PdfInfoQuery,
123124
PdfMergeInput,
125+
PdfPageOrientation,
124126
PdfPageSelection,
127+
PdfPageSize,
125128
PdfRedactionInstruction,
126129
PdfRGBColor,
127130
PdfXType,
@@ -1032,8 +1035,9 @@ def _post_file_operation(
10321035
for file_id in output_ids
10331036
]
10341037

1038+
input_ids = raw_response.input_id or (raw_response.ids or [])
10351039
response_payload: dict[str, Any] = {
1036-
"input_id": [str(file_id) for file_id in raw_response.input_id],
1040+
"input_id": [str(file_id) for file_id in input_ids],
10371041
"output_file": [
10381042
file.model_dump(mode="json", by_alias=True) for file in output_files
10391043
],
@@ -1307,8 +1311,9 @@ async def throttled_fetch_file_info(file_id: str) -> PdfRestFile:
13071311
)
13081312
)
13091313

1314+
input_ids = raw_response.input_id or (raw_response.ids or [])
13101315
response_payload: dict[str, Any] = {
1311-
"input_id": [str(file_id) for file_id in raw_response.input_id],
1316+
"input_id": [str(file_id) for file_id in input_ids],
13121317
"output_file": [
13131318
file.model_dump(mode="json", by_alias=True) for file in output_files
13141319
],
@@ -2755,6 +2760,45 @@ def convert_colors(
27552760
timeout=timeout,
27562761
)
27572762

2763+
def blank_pdf(
2764+
self,
2765+
*,
2766+
page_size: PdfPageSize,
2767+
page_count: int,
2768+
page_orientation: PdfPageOrientation | None = None,
2769+
custom_height: float | None = None,
2770+
custom_width: float | None = None,
2771+
output: str | None = None,
2772+
extra_query: Query | None = None,
2773+
extra_headers: AnyMapping | None = None,
2774+
extra_body: Body | None = None,
2775+
timeout: TimeoutTypes | None = None,
2776+
) -> PdfRestFileBasedResponse:
2777+
"""Create a blank PDF with the specified size, count, and orientation."""
2778+
2779+
payload: dict[str, Any] = {
2780+
"page_size": page_size,
2781+
"page_count": page_count,
2782+
}
2783+
if page_orientation is not None:
2784+
payload["page_orientation"] = page_orientation
2785+
if custom_height is not None:
2786+
payload["custom_height"] = custom_height
2787+
if custom_width is not None:
2788+
payload["custom_width"] = custom_width
2789+
if output is not None:
2790+
payload["output"] = output
2791+
2792+
return self._post_file_operation(
2793+
endpoint="/blank-pdf",
2794+
payload=payload,
2795+
payload_model=PdfBlankPayload,
2796+
extra_query=extra_query,
2797+
extra_headers=extra_headers,
2798+
extra_body=extra_body,
2799+
timeout=timeout,
2800+
)
2801+
27582802
def flatten_transparencies(
27592803
self,
27602804
file: PdfRestFile | Sequence[PdfRestFile],
@@ -3858,6 +3902,45 @@ async def convert_colors(
38583902
timeout=timeout,
38593903
)
38603904

3905+
async def blank_pdf(
3906+
self,
3907+
*,
3908+
page_size: PdfPageSize,
3909+
page_count: int,
3910+
page_orientation: PdfPageOrientation | None = None,
3911+
custom_height: float | None = None,
3912+
custom_width: float | None = None,
3913+
output: str | None = None,
3914+
extra_query: Query | None = None,
3915+
extra_headers: AnyMapping | None = None,
3916+
extra_body: Body | None = None,
3917+
timeout: TimeoutTypes | None = None,
3918+
) -> PdfRestFileBasedResponse:
3919+
"""Asynchronously create a blank PDF with the specified size."""
3920+
3921+
payload: dict[str, Any] = {
3922+
"page_size": page_size,
3923+
"page_count": page_count,
3924+
}
3925+
if page_orientation is not None:
3926+
payload["page_orientation"] = page_orientation
3927+
if custom_height is not None:
3928+
payload["custom_height"] = custom_height
3929+
if custom_width is not None:
3930+
payload["custom_width"] = custom_width
3931+
if output is not None:
3932+
payload["output"] = output
3933+
3934+
return await self._post_file_operation(
3935+
endpoint="/blank-pdf",
3936+
payload=payload,
3937+
payload_model=PdfBlankPayload,
3938+
extra_query=extra_query,
3939+
extra_headers=extra_headers,
3940+
extra_body=extra_body,
3941+
timeout=timeout,
3942+
)
3943+
38613944
async def flatten_transparencies(
38623945
self,
38633946
file: PdfRestFile | Sequence[PdfRestFile],

src/pdfrest/models/_internal.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
PdfAType,
2727
PdfColorProfile,
2828
PdfInfoQuery,
29+
PdfPageOrientation,
30+
PdfPageSize,
2931
PdfXType,
3032
SummaryFormat,
3133
SummaryOutputFormat,
@@ -1126,6 +1128,57 @@ class PdfFlattenLayersPayload(BaseModel):
11261128
] = None
11271129

11281130

1131+
class PdfBlankPayload(BaseModel):
1132+
"""Adapt caller options into a pdfRest-ready blank PDF request payload."""
1133+
1134+
page_size: Annotated[
1135+
PdfPageSize,
1136+
Field(serialization_alias="page_size"),
1137+
]
1138+
page_count: Annotated[
1139+
int,
1140+
Field(serialization_alias="page_count", ge=1, le=1000),
1141+
]
1142+
page_orientation: Annotated[
1143+
PdfPageOrientation | None,
1144+
Field(serialization_alias="page_orientation", default=None),
1145+
] = None
1146+
custom_height: Annotated[
1147+
float | None,
1148+
Field(serialization_alias="custom_height", gt=0, default=None),
1149+
] = None
1150+
custom_width: Annotated[
1151+
float | None,
1152+
Field(serialization_alias="custom_width", gt=0, default=None),
1153+
] = None
1154+
output: Annotated[
1155+
str | None,
1156+
Field(serialization_alias="output", min_length=1, default=None),
1157+
AfterValidator(_validate_output_prefix),
1158+
] = None
1159+
1160+
@model_validator(mode="after")
1161+
def _validate_page_configuration(self) -> PdfBlankPayload:
1162+
is_custom = self.page_size == "custom"
1163+
has_custom_height = self.custom_height is not None
1164+
has_custom_width = self.custom_width is not None
1165+
if is_custom:
1166+
if not (has_custom_height and has_custom_width):
1167+
msg = "custom_height and custom_width are required when page_size is 'custom'."
1168+
raise ValueError(msg)
1169+
if self.page_orientation is not None:
1170+
msg = "page_orientation must be omitted when page_size is 'custom'."
1171+
raise ValueError(msg)
1172+
else:
1173+
if self.page_orientation is None:
1174+
msg = "page_orientation is required when page_size is not 'custom'."
1175+
raise ValueError(msg)
1176+
if has_custom_height or has_custom_width:
1177+
msg = "custom_height and custom_width can only be provided when page_size is 'custom'."
1178+
raise ValueError(msg)
1179+
return self
1180+
1181+
11291182
class PdfConvertColorsPayload(BaseModel):
11301183
"""Adapt caller options into a pdfRest-ready convert-colors request payload."""
11311184

@@ -1239,7 +1292,11 @@ class PdfRestRawFileResponse(BaseModel):
12391292

12401293
input_id: Annotated[
12411294
list[PdfRestFileID],
1242-
Field(alias="inputId", description="The id of the input file"),
1295+
Field(
1296+
alias="inputId",
1297+
description="The id of the input file",
1298+
default_factory=list,
1299+
),
12431300
BeforeValidator(_ensure_list),
12441301
]
12451302
output_urls: Annotated[

src/pdfrest/types/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
PdfInfoQuery,
1717
PdfMergeInput,
1818
PdfMergeSource,
19+
PdfPageOrientation,
1920
PdfPageSelection,
21+
PdfPageSize,
2022
PdfRedactionInstruction,
2123
PdfRedactionPreset,
2224
PdfRedactionType,
@@ -46,7 +48,9 @@
4648
"PdfInfoQuery",
4749
"PdfMergeInput",
4850
"PdfMergeSource",
51+
"PdfPageOrientation",
4952
"PdfPageSelection",
53+
"PdfPageSize",
5054
"PdfRGBColor",
5155
"PdfRedactionInstruction",
5256
"PdfRedactionPreset",

src/pdfrest/types/public.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@
2828
"PdfInfoQuery",
2929
"PdfMergeInput",
3030
"PdfMergeSource",
31+
"PdfPageOrientation",
3132
"PdfPageSelection",
33+
"PdfPageSize",
3234
"PdfRGBColor",
3335
"PdfRedactionInstruction",
3436
"PdfRedactionPreset",
@@ -178,3 +180,6 @@ class PdfMergeSource(TypedDict, total=False):
178180
"acrobat9-cmyk",
179181
"custom",
180182
]
183+
184+
PdfPageSize = Literal["letter", "legal", "ledger", "A3", "A4", "A5", "custom"]
185+
PdfPageOrientation = Literal["portrait", "landscape"]

tests/live/test_live_blank_pdf.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
from pdfrest import AsyncPdfRestClient, PdfRestApiError, PdfRestClient
6+
7+
8+
@pytest.mark.parametrize(
9+
"output_name",
10+
[
11+
pytest.param(None, id="default-output"),
12+
pytest.param("blank-doc", id="custom-output"),
13+
],
14+
)
15+
def test_live_blank_pdf_success(
16+
pdfrest_api_key: str,
17+
pdfrest_live_base_url: str,
18+
output_name: str | None,
19+
) -> None:
20+
kwargs: dict[str, str | int] = {
21+
"page_size": "letter",
22+
"page_count": 1,
23+
"page_orientation": "portrait",
24+
}
25+
if output_name is not None:
26+
kwargs["output"] = output_name
27+
28+
with PdfRestClient(
29+
api_key=pdfrest_api_key,
30+
base_url=pdfrest_live_base_url,
31+
) as client:
32+
response = client.blank_pdf(**kwargs)
33+
34+
assert response.output_files
35+
output_file = response.output_file
36+
assert output_file.type == "application/pdf"
37+
assert output_file.size > 0
38+
assert response.warning is None
39+
if output_name is not None:
40+
assert output_file.name.startswith(output_name)
41+
else:
42+
assert output_file.name.endswith(".pdf")
43+
44+
45+
@pytest.mark.asyncio
46+
async def test_live_async_blank_pdf_success(
47+
pdfrest_api_key: str,
48+
pdfrest_live_base_url: str,
49+
) -> None:
50+
async with AsyncPdfRestClient(
51+
api_key=pdfrest_api_key,
52+
base_url=pdfrest_live_base_url,
53+
) as client:
54+
response = await client.blank_pdf(
55+
page_size="A4",
56+
page_count=2,
57+
page_orientation="landscape",
58+
output="async-blank",
59+
)
60+
61+
assert response.output_files
62+
output_file = response.output_file
63+
assert output_file.name.startswith("async-blank")
64+
assert output_file.type == "application/pdf"
65+
assert output_file.size > 0
66+
assert response.warning is None
67+
68+
69+
def test_live_blank_pdf_invalid_request(
70+
pdfrest_api_key: str,
71+
pdfrest_live_base_url: str,
72+
) -> None:
73+
with (
74+
PdfRestClient(
75+
api_key=pdfrest_api_key,
76+
base_url=pdfrest_live_base_url,
77+
) as client,
78+
pytest.raises(PdfRestApiError, match=r"(?i)(page|size)"),
79+
):
80+
client.blank_pdf(
81+
page_size="letter",
82+
page_count=1,
83+
page_orientation="portrait",
84+
extra_body={"page_size": "not-a-size"},
85+
)
86+
87+
88+
@pytest.mark.asyncio
89+
async def test_live_async_blank_pdf_invalid_request(
90+
pdfrest_api_key: str,
91+
pdfrest_live_base_url: str,
92+
) -> None:
93+
async with AsyncPdfRestClient(
94+
api_key=pdfrest_api_key,
95+
base_url=pdfrest_live_base_url,
96+
) as client:
97+
with pytest.raises(PdfRestApiError, match=r"(?i)(page|size)"):
98+
await client.blank_pdf(
99+
page_size="letter",
100+
page_count=1,
101+
page_orientation="portrait",
102+
extra_body={"page_size": "bad-size"},
103+
)

0 commit comments

Comments
 (0)