Skip to content

Commit e69b07d

Browse files
client.py: Add compress PDF support with comprehensive validation
- Introduced `compress_pdf` method for both synchronous and asynchronous `PdfRestClient` instances. - Added `PdfCompressPayload` to model validation with constraints for files, compression levels, profiles, and output settings. - Included live and unit tests to validate presets, custom profiles, request customization, and error handling. - Added new live test resources and sample compression profile. Assisted-by: Codex
1 parent 9d27eb6 commit e69b07d

5 files changed

Lines changed: 929 additions & 0 deletions

File tree

src/pdfrest/client.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
BmpPdfRestPayload,
7474
GifPdfRestPayload,
7575
JpegPdfRestPayload,
76+
PdfCompressPayload,
7677
PdfFlattenFormsPayload,
7778
PdfInfoPayload,
7879
PdfMergePayload,
@@ -2197,6 +2198,39 @@ def flatten_pdf_forms(
21972198
timeout=timeout,
21982199
)
21992200

2201+
def compress_pdf(
2202+
self,
2203+
file: PdfRestFile | Sequence[PdfRestFile],
2204+
*,
2205+
compression_level: Literal["low", "medium", "high", "custom"],
2206+
profile: PdfRestFile | Sequence[PdfRestFile] | None = None,
2207+
output: str | None = None,
2208+
extra_query: Query | None = None,
2209+
extra_headers: AnyMapping | None = None,
2210+
extra_body: Body | None = None,
2211+
timeout: TimeoutTypes | None = None,
2212+
) -> PdfRestFileBasedResponse:
2213+
"""Compress a PDF using preset or custom compression profiles."""
2214+
2215+
payload: dict[str, Any] = {
2216+
"files": file,
2217+
"compression_level": compression_level,
2218+
}
2219+
if profile is not None:
2220+
payload["profile"] = profile
2221+
if output is not None:
2222+
payload["output"] = output
2223+
2224+
return self._post_file_operation(
2225+
endpoint="/compressed-pdf",
2226+
payload=payload,
2227+
payload_model=PdfCompressPayload,
2228+
extra_query=extra_query,
2229+
extra_headers=extra_headers,
2230+
extra_body=extra_body,
2231+
timeout=timeout,
2232+
)
2233+
22002234
def convert_to_pdfx(
22012235
self,
22022236
file: PdfRestFile | Sequence[PdfRestFile],
@@ -2707,6 +2741,39 @@ async def flatten_pdf_forms(
27072741
timeout=timeout,
27082742
)
27092743

2744+
async def compress_pdf(
2745+
self,
2746+
file: PdfRestFile | Sequence[PdfRestFile],
2747+
*,
2748+
compression_level: Literal["low", "medium", "high", "custom"],
2749+
profile: PdfRestFile | Sequence[PdfRestFile] | None = None,
2750+
output: str | None = None,
2751+
extra_query: Query | None = None,
2752+
extra_headers: AnyMapping | None = None,
2753+
extra_body: Body | None = None,
2754+
timeout: TimeoutTypes | None = None,
2755+
) -> PdfRestFileBasedResponse:
2756+
"""Asynchronously compress a PDF."""
2757+
2758+
payload: dict[str, Any] = {
2759+
"files": file,
2760+
"compression_level": compression_level,
2761+
}
2762+
if profile is not None:
2763+
payload["profile"] = profile
2764+
if output is not None:
2765+
payload["output"] = output
2766+
2767+
return await self._post_file_operation(
2768+
endpoint="/compressed-pdf",
2769+
payload=payload,
2770+
payload_model=PdfCompressPayload,
2771+
extra_query=extra_query,
2772+
extra_headers=extra_headers,
2773+
extra_body=extra_body,
2774+
timeout=timeout,
2775+
)
2776+
27102777
async def convert_to_pdfx(
27112778
self,
27122779
file: PdfRestFile | Sequence[PdfRestFile],

src/pdfrest/models/_internal.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,64 @@ class PdfFlattenFormsPayload(BaseModel):
549549
] = None
550550

551551

552+
class PdfCompressPayload(BaseModel):
553+
"""Adapt caller options into a pdfRest-ready compress request payload."""
554+
555+
files: Annotated[
556+
list[PdfRestFile],
557+
Field(
558+
min_length=1,
559+
max_length=1,
560+
validation_alias=AliasChoices("file", "files"),
561+
serialization_alias="id",
562+
),
563+
BeforeValidator(_ensure_list),
564+
AfterValidator(
565+
_allowed_mime_types("application/pdf", error_msg="Must be a PDF file")
566+
),
567+
PlainSerializer(_serialize_as_first_file_id),
568+
]
569+
compression_level: Annotated[
570+
Literal["low", "medium", "high", "custom"],
571+
Field(serialization_alias="compression_level"),
572+
]
573+
profile: Annotated[
574+
list[PdfRestFile] | None,
575+
Field(
576+
default=None,
577+
min_length=1,
578+
max_length=1,
579+
validation_alias=AliasChoices("profile", "profiles"),
580+
serialization_alias="profile_id",
581+
),
582+
BeforeValidator(_ensure_list),
583+
BeforeValidator(
584+
_allowed_mime_types(
585+
"application/json",
586+
"text/json",
587+
error_msg="Profile must be a JSON file",
588+
)
589+
),
590+
PlainSerializer(_serialize_as_first_file_id),
591+
] = None
592+
output: Annotated[
593+
str | None,
594+
Field(serialization_alias="output", min_length=1, default=None),
595+
AfterValidator(_validate_output_prefix),
596+
] = None
597+
598+
@model_validator(mode="after")
599+
def _validate_profile_dependency(self) -> PdfCompressPayload:
600+
if self.compression_level == "custom":
601+
if not self.profile:
602+
msg = "compression_level 'custom' requires a profile to be provided."
603+
raise ValueError(msg)
604+
elif self.profile:
605+
msg = "A profile can only be provided when compression_level is 'custom'."
606+
raise ValueError(msg)
607+
return self
608+
609+
552610
class BmpPdfRestPayload(BasePdfRestGraphicPayload[Literal["rgb", "gray"]]):
553611
"""Adapt caller options into a pdfRest-ready BMP request payload."""
554612

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
from __future__ import annotations
2+
3+
from typing import Literal
4+
5+
import pytest
6+
7+
from pdfrest import AsyncPdfRestClient, PdfRestApiError, PdfRestClient
8+
from pdfrest.models import PdfRestFile
9+
10+
from ..resources import get_test_resource_path
11+
12+
13+
@pytest.fixture(scope="module")
14+
def uploaded_pdf_for_compression(
15+
pdfrest_api_key: str,
16+
pdfrest_live_base_url: str,
17+
) -> PdfRestFile:
18+
resource = get_test_resource_path("report.pdf")
19+
with PdfRestClient(
20+
api_key=pdfrest_api_key,
21+
base_url=pdfrest_live_base_url,
22+
) as client:
23+
return client.files.create_from_paths([resource])[0]
24+
25+
26+
@pytest.fixture(scope="module")
27+
def uploaded_compression_profile(
28+
pdfrest_api_key: str,
29+
pdfrest_live_base_url: str,
30+
) -> PdfRestFile:
31+
resource = get_test_resource_path("compression_profile.json")
32+
with PdfRestClient(
33+
api_key=pdfrest_api_key,
34+
base_url=pdfrest_live_base_url,
35+
) as client:
36+
return client.files.create_from_paths([resource])[0]
37+
38+
39+
@pytest.mark.parametrize(
40+
"compression_level",
41+
[
42+
pytest.param("low", id="low"),
43+
pytest.param("medium", id="medium"),
44+
pytest.param("high", id="high"),
45+
],
46+
)
47+
def test_live_compress_pdf_presets(
48+
pdfrest_api_key: str,
49+
pdfrest_live_base_url: str,
50+
uploaded_pdf_for_compression: PdfRestFile,
51+
compression_level: Literal["low", "medium", "high"],
52+
) -> None:
53+
with PdfRestClient(
54+
api_key=pdfrest_api_key,
55+
base_url=pdfrest_live_base_url,
56+
) as client:
57+
response = client.compress_pdf(
58+
uploaded_pdf_for_compression,
59+
compression_level=compression_level,
60+
)
61+
62+
assert response.output_files
63+
output_file = response.output_file
64+
assert output_file.type == "application/pdf"
65+
assert str(response.input_id) == str(uploaded_pdf_for_compression.id)
66+
67+
68+
@pytest.mark.asyncio
69+
@pytest.mark.parametrize(
70+
"compression_level",
71+
[
72+
pytest.param("low", id="low"),
73+
pytest.param("medium", id="medium"),
74+
pytest.param("high", id="high"),
75+
],
76+
)
77+
async def test_live_async_compress_pdf_presets(
78+
pdfrest_api_key: str,
79+
pdfrest_live_base_url: str,
80+
uploaded_pdf_for_compression: PdfRestFile,
81+
compression_level: Literal["low", "medium", "high"],
82+
) -> None:
83+
async with AsyncPdfRestClient(
84+
api_key=pdfrest_api_key,
85+
base_url=pdfrest_live_base_url,
86+
) as client:
87+
response = await client.compress_pdf(
88+
uploaded_pdf_for_compression,
89+
compression_level=compression_level,
90+
)
91+
92+
assert response.output_files
93+
output_file = response.output_file
94+
assert output_file.type == "application/pdf"
95+
assert str(response.input_id) == str(uploaded_pdf_for_compression.id)
96+
97+
98+
def test_live_compress_pdf_custom(
99+
pdfrest_api_key: str,
100+
pdfrest_live_base_url: str,
101+
uploaded_pdf_for_compression: PdfRestFile,
102+
uploaded_compression_profile: PdfRestFile,
103+
) -> None:
104+
with PdfRestClient(
105+
api_key=pdfrest_api_key,
106+
base_url=pdfrest_live_base_url,
107+
) as client:
108+
response = client.compress_pdf(
109+
uploaded_pdf_for_compression,
110+
compression_level="custom",
111+
profile=uploaded_compression_profile,
112+
output="compressed-custom",
113+
)
114+
115+
assert (
116+
response.warning
117+
== "The document could not be made smaller. No output was produced."
118+
)
119+
assert len(response.input_ids) == 2
120+
assert uploaded_pdf_for_compression.id in response.input_ids
121+
assert uploaded_compression_profile.id in response.input_ids
122+
123+
124+
@pytest.mark.asyncio
125+
async def test_live_async_compress_pdf_custom(
126+
pdfrest_api_key: str,
127+
pdfrest_live_base_url: str,
128+
uploaded_pdf_for_compression: PdfRestFile,
129+
uploaded_compression_profile: PdfRestFile,
130+
) -> None:
131+
async with AsyncPdfRestClient(
132+
api_key=pdfrest_api_key,
133+
base_url=pdfrest_live_base_url,
134+
) as client:
135+
response = await client.compress_pdf(
136+
uploaded_pdf_for_compression,
137+
compression_level="custom",
138+
profile=uploaded_compression_profile,
139+
output="compressed-custom",
140+
)
141+
142+
assert (
143+
response.warning
144+
== "The document could not be made smaller. No output was produced."
145+
)
146+
assert len(response.input_ids) == 2
147+
assert uploaded_pdf_for_compression.id in response.input_ids
148+
assert uploaded_compression_profile.id in response.input_ids
149+
150+
151+
def test_live_compress_pdf_invalid_level(
152+
pdfrest_api_key: str,
153+
pdfrest_live_base_url: str,
154+
uploaded_pdf_for_compression: PdfRestFile,
155+
) -> None:
156+
with (
157+
PdfRestClient(
158+
api_key=pdfrest_api_key,
159+
base_url=pdfrest_live_base_url,
160+
) as client,
161+
pytest.raises(PdfRestApiError),
162+
):
163+
client.compress_pdf(
164+
uploaded_pdf_for_compression,
165+
compression_level="low",
166+
extra_body={"compression_level": "extreme"},
167+
)
168+
169+
170+
@pytest.mark.asyncio
171+
async def test_live_async_compress_pdf_invalid_level(
172+
pdfrest_api_key: str,
173+
pdfrest_live_base_url: str,
174+
uploaded_pdf_for_compression: PdfRestFile,
175+
) -> None:
176+
async with AsyncPdfRestClient(
177+
api_key=pdfrest_api_key,
178+
base_url=pdfrest_live_base_url,
179+
) as client:
180+
with pytest.raises(PdfRestApiError):
181+
await client.compress_pdf(
182+
uploaded_pdf_for_compression,
183+
compression_level="low",
184+
extra_body={"compression_level": "extreme"},
185+
)

0 commit comments

Comments
 (0)