Skip to content

Commit 193dda3

Browse files
client: 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 Assisted-by: Codex
1 parent 3d1c459 commit 193dda3

7 files changed

Lines changed: 565 additions & 4 deletions

File tree

src/pdfrest/client.py

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
GifPdfRestPayload,
8383
JpegPdfRestPayload,
8484
OcrPdfPayload,
85+
PdfBlankPayload,
8586
PdfCompressPayload,
8687
PdfDecryptPayload,
8788
PdfEncryptPayload,
@@ -123,7 +124,9 @@
123124
PdfAType,
124125
PdfInfoQuery,
125126
PdfMergeInput,
127+
PdfPageOrientation,
126128
PdfPageSelection,
129+
PdfPageSize,
127130
PdfRedactionInstruction,
128131
PdfRestriction,
129132
PdfRGBColor,
@@ -1035,8 +1038,9 @@ def _post_file_operation(
10351038
for file_id in output_ids
10361039
]
10371040

1041+
input_ids = raw_response.input_id or (raw_response.ids or [])
10381042
response_payload: dict[str, Any] = {
1039-
"input_id": [str(file_id) for file_id in raw_response.input_id],
1043+
"input_id": [str(file_id) for file_id in input_ids],
10401044
"output_file": [
10411045
file.model_dump(mode="json", by_alias=True) for file in output_files
10421046
],
@@ -1310,8 +1314,9 @@ async def throttled_fetch_file_info(file_id: str) -> PdfRestFile:
13101314
)
13111315
)
13121316

1317+
input_ids = raw_response.input_id or (raw_response.ids or [])
13131318
response_payload: dict[str, Any] = {
1314-
"input_id": [str(file_id) for file_id in raw_response.input_id],
1319+
"input_id": [str(file_id) for file_id in input_ids],
13151320
"output_file": [
13161321
file.model_dump(mode="json", by_alias=True) for file in output_files
13171322
],
@@ -2973,6 +2978,45 @@ def compress_pdf(
29732978
timeout=timeout,
29742979
)
29752980

2981+
def blank_pdf(
2982+
self,
2983+
*,
2984+
page_size: PdfPageSize,
2985+
page_count: int,
2986+
page_orientation: PdfPageOrientation | None = None,
2987+
custom_height: float | None = None,
2988+
custom_width: float | None = None,
2989+
output: str | None = None,
2990+
extra_query: Query | None = None,
2991+
extra_headers: AnyMapping | None = None,
2992+
extra_body: Body | None = None,
2993+
timeout: TimeoutTypes | None = None,
2994+
) -> PdfRestFileBasedResponse:
2995+
"""Create a blank PDF with the specified size, count, and orientation."""
2996+
2997+
payload: dict[str, Any] = {
2998+
"page_size": page_size,
2999+
"page_count": page_count,
3000+
}
3001+
if page_orientation is not None:
3002+
payload["page_orientation"] = page_orientation
3003+
if custom_height is not None:
3004+
payload["custom_height"] = custom_height
3005+
if custom_width is not None:
3006+
payload["custom_width"] = custom_width
3007+
if output is not None:
3008+
payload["output"] = output
3009+
3010+
return self._post_file_operation(
3011+
endpoint="/blank-pdf",
3012+
payload=payload,
3013+
payload_model=PdfBlankPayload,
3014+
extra_query=extra_query,
3015+
extra_headers=extra_headers,
3016+
extra_body=extra_body,
3017+
timeout=timeout,
3018+
)
3019+
29763020
def flatten_transparencies(
29773021
self,
29783022
file: PdfRestFile | Sequence[PdfRestFile],
@@ -4260,6 +4304,45 @@ async def compress_pdf(
42604304
timeout=timeout,
42614305
)
42624306

4307+
async def blank_pdf(
4308+
self,
4309+
*,
4310+
page_size: PdfPageSize,
4311+
page_count: int,
4312+
page_orientation: PdfPageOrientation | None = None,
4313+
custom_height: float | None = None,
4314+
custom_width: float | None = None,
4315+
output: str | None = None,
4316+
extra_query: Query | None = None,
4317+
extra_headers: AnyMapping | None = None,
4318+
extra_body: Body | None = None,
4319+
timeout: TimeoutTypes | None = None,
4320+
) -> PdfRestFileBasedResponse:
4321+
"""Asynchronously create a blank PDF with the specified size."""
4322+
4323+
payload: dict[str, Any] = {
4324+
"page_size": page_size,
4325+
"page_count": page_count,
4326+
}
4327+
if page_orientation is not None:
4328+
payload["page_orientation"] = page_orientation
4329+
if custom_height is not None:
4330+
payload["custom_height"] = custom_height
4331+
if custom_width is not None:
4332+
payload["custom_width"] = custom_width
4333+
if output is not None:
4334+
payload["output"] = output
4335+
4336+
return await self._post_file_operation(
4337+
endpoint="/blank-pdf",
4338+
payload=payload,
4339+
payload_model=PdfBlankPayload,
4340+
extra_query=extra_query,
4341+
extra_headers=extra_headers,
4342+
extra_body=extra_body,
4343+
timeout=timeout,
4344+
)
4345+
42634346
async def flatten_transparencies(
42644347
self,
42654348
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
OcrLanguage,
2727
PdfAType,
2828
PdfInfoQuery,
29+
PdfPageOrientation,
30+
PdfPageSize,
2931
PdfRestriction,
3032
PdfXType,
3133
SummaryFormat,
@@ -1142,6 +1144,57 @@ class PdfFlattenAnnotationsPayload(BaseModel):
11421144
] = None
11431145

11441146

1147+
class PdfBlankPayload(BaseModel):
1148+
"""Adapt caller options into a pdfRest-ready blank PDF request payload."""
1149+
1150+
page_size: Annotated[
1151+
PdfPageSize,
1152+
Field(serialization_alias="page_size"),
1153+
]
1154+
page_count: Annotated[
1155+
int,
1156+
Field(serialization_alias="page_count", ge=1, le=1000),
1157+
]
1158+
page_orientation: Annotated[
1159+
PdfPageOrientation | None,
1160+
Field(serialization_alias="page_orientation", default=None),
1161+
] = None
1162+
custom_height: Annotated[
1163+
float | None,
1164+
Field(serialization_alias="custom_height", gt=0, default=None),
1165+
] = None
1166+
custom_width: Annotated[
1167+
float | None,
1168+
Field(serialization_alias="custom_width", gt=0, default=None),
1169+
] = None
1170+
output: Annotated[
1171+
str | None,
1172+
Field(serialization_alias="output", min_length=1, default=None),
1173+
AfterValidator(_validate_output_prefix),
1174+
] = None
1175+
1176+
@model_validator(mode="after")
1177+
def _validate_page_configuration(self) -> PdfBlankPayload:
1178+
is_custom = self.page_size == "custom"
1179+
has_custom_height = self.custom_height is not None
1180+
has_custom_width = self.custom_width is not None
1181+
if is_custom:
1182+
if not (has_custom_height and has_custom_width):
1183+
msg = "custom_height and custom_width are required when page_size is 'custom'."
1184+
raise ValueError(msg)
1185+
if self.page_orientation is not None:
1186+
msg = "page_orientation must be omitted when page_size is 'custom'."
1187+
raise ValueError(msg)
1188+
else:
1189+
if self.page_orientation is None:
1190+
msg = "page_orientation is required when page_size is not 'custom'."
1191+
raise ValueError(msg)
1192+
if has_custom_height or has_custom_width:
1193+
msg = "custom_height and custom_width can only be provided when page_size is 'custom'."
1194+
raise ValueError(msg)
1195+
return self
1196+
1197+
11451198
class PdfRestrictPayload(BaseModel):
11461199
"""Adapt caller options into a pdfRest-ready restrict-PDF request payload."""
11471200

@@ -1380,7 +1433,11 @@ class PdfRestRawFileResponse(BaseModel):
13801433

13811434
input_id: Annotated[
13821435
list[PdfRestFileID],
1383-
Field(alias="inputId", description="The id of the input file"),
1436+
Field(
1437+
alias="inputId",
1438+
description="The id of the input file",
1439+
default_factory=list,
1440+
),
13841441
BeforeValidator(_ensure_list),
13851442
]
13861443
output_urls: Annotated[

src/pdfrest/models/public.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,6 @@ class PdfRestFileBasedResponse(BaseModel):
269269
list[PdfRestFileID],
270270
Field(
271271
description="The ids of the files that were input to the pdfRest operation",
272-
min_length=1,
273272
validation_alias=AliasChoices("input_id", "inputId"),
274273
),
275274
]

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,
@@ -47,7 +49,9 @@
4749
"PdfInfoQuery",
4850
"PdfMergeInput",
4951
"PdfMergeSource",
52+
"PdfPageOrientation",
5053
"PdfPageSelection",
54+
"PdfPageSize",
5155
"PdfRGBColor",
5256
"PdfRedactionInstruction",
5357
"PdfRedactionPreset",

src/pdfrest/types/public.py

Lines changed: 4 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",
@@ -177,3 +179,5 @@ class PdfMergeSource(TypedDict, total=False):
177179
ALL_PDF_RESTRICTIONS: tuple[PdfRestriction, ...] = cast(
178180
tuple[PdfRestriction, ...], get_args(PdfRestriction)
179181
)
182+
PdfPageSize = Literal["letter", "legal", "ledger", "A3", "A4", "A5", "custom"]
183+
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)