Skip to content

Commit d74d92f

Browse files
Merge pull request #20 from datalogics-cgreen/pdfcloud-5464-zip
PDFCLOUD-5555 Add zip and unzip client methods
2 parents 5fc736f + b68d8c3 commit d74d92f

6 files changed

Lines changed: 916 additions & 2 deletions

File tree

src/pdfrest/client.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,9 @@
113113
SummarizePdfTextPayload,
114114
TiffPdfRestPayload,
115115
TranslatePdfTextPayload,
116+
UnzipPayload,
116117
UploadURLs,
118+
ZipPayload,
117119
)
118120
from .types import (
119121
ALL_PDF_INFO_QUERIES,
@@ -2676,6 +2678,58 @@ def merge_pdfs(
26762678
timeout=timeout,
26772679
)
26782680

2681+
def zip_files(
2682+
self,
2683+
files: PdfRestFile | Sequence[PdfRestFile],
2684+
*,
2685+
output: str | None = None,
2686+
extra_query: Query | None = None,
2687+
extra_headers: AnyMapping | None = None,
2688+
extra_body: Body | None = None,
2689+
timeout: TimeoutTypes | None = None,
2690+
) -> PdfRestFileBasedResponse:
2691+
"""Compress one or more files into a zip archive."""
2692+
2693+
payload: dict[str, Any] = {"files": files}
2694+
if output is not None:
2695+
payload["output"] = output
2696+
2697+
return self._post_file_operation(
2698+
endpoint="/zip",
2699+
payload=payload,
2700+
payload_model=ZipPayload,
2701+
extra_query=extra_query,
2702+
extra_headers=extra_headers,
2703+
extra_body=extra_body,
2704+
timeout=timeout,
2705+
)
2706+
2707+
def unzip_file(
2708+
self,
2709+
file: PdfRestFile | Sequence[PdfRestFile],
2710+
*,
2711+
password: str | None = None,
2712+
extra_query: Query | None = None,
2713+
extra_headers: AnyMapping | None = None,
2714+
extra_body: Body | None = None,
2715+
timeout: TimeoutTypes | None = None,
2716+
) -> PdfRestFileBasedResponse:
2717+
"""Extract files from a zip archive."""
2718+
2719+
payload: dict[str, Any] = {"files": file}
2720+
if password is not None:
2721+
payload["password"] = password
2722+
2723+
return self._post_file_operation(
2724+
endpoint="/unzip",
2725+
payload=payload,
2726+
payload_model=UnzipPayload,
2727+
extra_query=extra_query,
2728+
extra_headers=extra_headers,
2729+
extra_body=extra_body,
2730+
timeout=timeout,
2731+
)
2732+
26792733
def convert_to_excel(
26802734
self,
26812735
file: PdfRestFile | Sequence[PdfRestFile],
@@ -4115,6 +4169,58 @@ async def merge_pdfs(
41154169
timeout=timeout,
41164170
)
41174171

4172+
async def zip_files(
4173+
self,
4174+
files: PdfRestFile | Sequence[PdfRestFile],
4175+
*,
4176+
output: str | None = None,
4177+
extra_query: Query | None = None,
4178+
extra_headers: AnyMapping | None = None,
4179+
extra_body: Body | None = None,
4180+
timeout: TimeoutTypes | None = None,
4181+
) -> PdfRestFileBasedResponse:
4182+
"""Asynchronously compress one or more files into a zip archive."""
4183+
4184+
payload: dict[str, Any] = {"files": files}
4185+
if output is not None:
4186+
payload["output"] = output
4187+
4188+
return await self._post_file_operation(
4189+
endpoint="/zip",
4190+
payload=payload,
4191+
payload_model=ZipPayload,
4192+
extra_query=extra_query,
4193+
extra_headers=extra_headers,
4194+
extra_body=extra_body,
4195+
timeout=timeout,
4196+
)
4197+
4198+
async def unzip_file(
4199+
self,
4200+
file: PdfRestFile | Sequence[PdfRestFile],
4201+
*,
4202+
password: str | None = None,
4203+
extra_query: Query | None = None,
4204+
extra_headers: AnyMapping | None = None,
4205+
extra_body: Body | None = None,
4206+
timeout: TimeoutTypes | None = None,
4207+
) -> PdfRestFileBasedResponse:
4208+
"""Asynchronously extract files from a zip archive."""
4209+
4210+
payload: dict[str, Any] = {"files": file}
4211+
if password is not None:
4212+
payload["password"] = password
4213+
4214+
return await self._post_file_operation(
4215+
endpoint="/unzip",
4216+
payload=payload,
4217+
payload_model=UnzipPayload,
4218+
extra_query=extra_query,
4219+
extra_headers=extra_headers,
4220+
extra_body=extra_body,
4221+
timeout=timeout,
4222+
)
4223+
41184224
async def convert_to_excel(
41194225
self,
41204226
file: PdfRestFile | Sequence[PdfRestFile],

src/pdfrest/models/_internal.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ def _serialize_file_ids(value: list[PdfRestFile]) -> str:
124124
return ",".join(str(file.id) for file in value)
125125

126126

127+
def _serialize_file_id_list(value: list[PdfRestFile]) -> list[str]:
128+
return [str(file.id) for file in value]
129+
130+
127131
def _bool_to_on_off(value: Any) -> Any:
128132
if isinstance(value, bool):
129133
return "on" if value else "off"
@@ -263,6 +267,49 @@ class DeletePayload(BaseModel):
263267
]
264268

265269

270+
class ZipPayload(BaseModel):
271+
"""Adapt caller options into a pdfRest-ready zip request payload."""
272+
273+
files: Annotated[
274+
list[PdfRestFile],
275+
Field(
276+
min_length=1,
277+
validation_alias=AliasChoices("file", "files"),
278+
serialization_alias="id",
279+
),
280+
BeforeValidator(_ensure_list),
281+
PlainSerializer(_serialize_file_id_list),
282+
]
283+
output: Annotated[
284+
str | None,
285+
Field(serialization_alias="output", min_length=1, default=None),
286+
AfterValidator(_validate_output_prefix),
287+
] = None
288+
289+
290+
class UnzipPayload(BaseModel):
291+
"""Adapt caller options into a pdfRest-ready unzip request payload."""
292+
293+
files: Annotated[
294+
list[PdfRestFile],
295+
Field(
296+
min_length=1,
297+
max_length=1,
298+
validation_alias=AliasChoices("file", "files"),
299+
serialization_alias="id",
300+
),
301+
BeforeValidator(_ensure_list),
302+
AfterValidator(
303+
_allowed_mime_types("application/zip", error_msg="Must be a ZIP file")
304+
),
305+
PlainSerializer(_serialize_as_first_file_id),
306+
]
307+
password: Annotated[
308+
str | None,
309+
Field(default=None, min_length=1),
310+
] = None
311+
312+
266313
PageNumber = Annotated[int, Field(ge=1), PlainSerializer(lambda x: str(x))]
267314

268315

@@ -1673,7 +1720,7 @@ class PdfRestRawUploadedFile(BaseModel):
16731720
name: Annotated[str, Field(description="The name of the file")]
16741721
id: Annotated[PdfRestFileID, Field(description="The id of the file")]
16751722
output_url: Annotated[
1676-
str | None,
1723+
list[HttpUrl] | HttpUrl | None,
16771724
Field(description="The url of the unzipped file", alias="outputUrl"),
16781725
BeforeValidator(_ensure_list),
16791726
] = None
@@ -1696,7 +1743,7 @@ class PdfRestRawFileResponse(BaseModel):
16961743
BeforeValidator(_ensure_list),
16971744
]
16981745
output_urls: Annotated[
1699-
list[HttpUrl] | None,
1746+
list[HttpUrl] | HttpUrl | None,
17001747
Field(alias="outputUrl", description="The url of the file"),
17011748
BeforeValidator(_ensure_list),
17021749
] = None

tests/live/test_live_unzip.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from __future__ import annotations
2+
3+
import zipfile
4+
from pathlib import Path
5+
6+
import pytest
7+
8+
from pdfrest import AsyncPdfRestClient, PdfRestApiError, PdfRestClient
9+
from pdfrest.models import PdfRestFile
10+
11+
from ..resources import get_test_resource_path
12+
13+
14+
def _build_zip_payload(tmp_path: Path) -> Path:
15+
zip_path = tmp_path / "sample.zip"
16+
with zipfile.ZipFile(zip_path, "w") as bundle:
17+
report_path = get_test_resource_path("report.pdf")
18+
bundle.write(report_path, arcname="report.pdf")
19+
return zip_path
20+
21+
22+
@pytest.fixture(scope="module")
23+
def uploaded_zip_file(
24+
pdfrest_api_key: str,
25+
pdfrest_live_base_url: str,
26+
tmp_path_factory: pytest.TempPathFactory,
27+
) -> PdfRestFile:
28+
zip_path = _build_zip_payload(tmp_path_factory.mktemp("zip-input"))
29+
with PdfRestClient(
30+
api_key=pdfrest_api_key,
31+
base_url=pdfrest_live_base_url,
32+
) as client:
33+
return client.files.create_from_paths([zip_path])[0]
34+
35+
36+
def test_live_unzip_file(
37+
pdfrest_api_key: str,
38+
pdfrest_live_base_url: str,
39+
uploaded_zip_file: PdfRestFile,
40+
) -> None:
41+
with PdfRestClient(
42+
api_key=pdfrest_api_key,
43+
base_url=pdfrest_live_base_url,
44+
) as client:
45+
response = client.unzip_file(uploaded_zip_file)
46+
47+
assert response.output_files
48+
assert all(file.size > 0 for file in response.output_files)
49+
assert all(file.name for file in response.output_files)
50+
assert str(response.input_id) == str(uploaded_zip_file.id)
51+
52+
53+
def test_live_unzip_invalid_override(
54+
pdfrest_api_key: str,
55+
pdfrest_live_base_url: str,
56+
uploaded_zip_file: PdfRestFile,
57+
) -> None:
58+
with (
59+
PdfRestClient(
60+
api_key=pdfrest_api_key,
61+
base_url=pdfrest_live_base_url,
62+
) as client,
63+
pytest.raises(PdfRestApiError, match="id"),
64+
):
65+
client.unzip_file(
66+
uploaded_zip_file,
67+
extra_body={"id": "not-a-uuid"},
68+
)
69+
70+
71+
@pytest.mark.asyncio
72+
async def test_live_unzip_file_async(
73+
pdfrest_api_key: str,
74+
pdfrest_live_base_url: str,
75+
uploaded_zip_file: PdfRestFile,
76+
) -> None:
77+
async with AsyncPdfRestClient(
78+
api_key=pdfrest_api_key,
79+
base_url=pdfrest_live_base_url,
80+
) as client:
81+
response = await client.unzip_file(
82+
uploaded_zip_file,
83+
extra_query={"trace": "async"},
84+
)
85+
86+
assert response.output_files
87+
assert all(file.size > 0 for file in response.output_files)
88+
assert all(file.name for file in response.output_files)
89+
assert str(response.input_id) == str(uploaded_zip_file.id)

tests/live/test_live_zip.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
from pdfrest import AsyncPdfRestClient, PdfRestApiError, PdfRestClient
6+
from pdfrest.models import PdfRestFile
7+
8+
from ..resources import get_test_resource_path
9+
10+
11+
@pytest.fixture(scope="module")
12+
def uploaded_zip_inputs(
13+
pdfrest_api_key: str,
14+
pdfrest_live_base_url: str,
15+
) -> list[PdfRestFile]:
16+
paths = [
17+
get_test_resource_path("report.pdf"),
18+
get_test_resource_path("report.docx"),
19+
]
20+
with PdfRestClient(
21+
api_key=pdfrest_api_key,
22+
base_url=pdfrest_live_base_url,
23+
) as client:
24+
return client.files.create_from_paths(paths)
25+
26+
27+
def test_live_zip_files(
28+
pdfrest_api_key: str,
29+
pdfrest_live_base_url: str,
30+
uploaded_zip_inputs: list[PdfRestFile],
31+
) -> None:
32+
with PdfRestClient(
33+
api_key=pdfrest_api_key,
34+
base_url=pdfrest_live_base_url,
35+
) as client:
36+
response = client.zip_files(
37+
uploaded_zip_inputs,
38+
output="live-zip",
39+
)
40+
41+
assert response.output_file.name.startswith("live-zip")
42+
assert response.output_file.name.endswith(".zip")
43+
assert response.output_file.type == "application/zip"
44+
assert response.output_file.size > 0
45+
assert {str(file.id) for file in uploaded_zip_inputs} == {
46+
str(file_id) for file_id in response.input_ids
47+
}
48+
49+
50+
def test_live_zip_files_invalid_id_override(
51+
pdfrest_api_key: str,
52+
pdfrest_live_base_url: str,
53+
uploaded_zip_inputs: list[PdfRestFile],
54+
) -> None:
55+
with (
56+
PdfRestClient(
57+
api_key=pdfrest_api_key,
58+
base_url=pdfrest_live_base_url,
59+
) as client,
60+
pytest.raises(PdfRestApiError, match="id"),
61+
):
62+
client.zip_files(
63+
uploaded_zip_inputs,
64+
extra_body={"id": "not-a-uuid"},
65+
)
66+
67+
68+
@pytest.mark.asyncio
69+
async def test_live_zip_files_async(
70+
pdfrest_api_key: str,
71+
pdfrest_live_base_url: str,
72+
uploaded_zip_inputs: list[PdfRestFile],
73+
) -> None:
74+
async with AsyncPdfRestClient(
75+
api_key=pdfrest_api_key,
76+
base_url=pdfrest_live_base_url,
77+
) as client:
78+
response = await client.zip_files(
79+
uploaded_zip_inputs,
80+
output="live-zip-async",
81+
extra_query={"trace": "async"},
82+
)
83+
84+
assert response.output_file.name.startswith("live-zip-async")
85+
assert response.output_file.name.endswith(".zip")
86+
assert response.output_file.type == "application/zip"
87+
assert response.output_file.size > 0
88+
assert {str(file.id) for file in uploaded_zip_inputs} == {
89+
str(file_id) for file_id in response.input_ids
90+
}

0 commit comments

Comments
 (0)