Skip to content

Commit b7ac5b0

Browse files
datalogics-cgreendatalogics-kam
authored andcommitted
Add Delete tool
Assisted-by: Codex
1 parent 89af601 commit b7ac5b0

6 files changed

Lines changed: 292 additions & 1 deletion

File tree

src/pdfrest/client.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
translate_httpx_error,
5959
)
6060
from .models import (
61+
PdfRestDeletionResponse,
6162
PdfRestErrorResponse,
6263
PdfRestFile,
6364
PdfRestFileBasedResponse,
@@ -71,6 +72,7 @@
7172
from .models._internal import (
7273
BasePdfRestGraphicPayload,
7374
BmpPdfRestPayload,
75+
DeletePayload,
7476
GifPdfRestPayload,
7577
JpegPdfRestPayload,
7678
PdfCompressPayload,
@@ -1543,6 +1545,32 @@ def create_from_urls(
15431545
for file_id in file_ids
15441546
]
15451547

1548+
def delete(
1549+
self,
1550+
files: PdfRestFile | Sequence[PdfRestFile],
1551+
*,
1552+
extra_query: Query | None = None,
1553+
extra_headers: AnyMapping | None = None,
1554+
extra_body: Body | None = None,
1555+
timeout: TimeoutTypes | None = None,
1556+
) -> PdfRestDeletionResponse:
1557+
"""Delete one or more uploaded files by reference."""
1558+
1559+
payload = DeletePayload.model_validate({"files": files})
1560+
request = self._client.prepare_request(
1561+
"POST",
1562+
"/delete",
1563+
json_body=payload.model_dump(
1564+
mode="json", by_alias=True, exclude_none=True, exclude_unset=True
1565+
),
1566+
extra_query=extra_query,
1567+
extra_headers=extra_headers,
1568+
extra_body=extra_body,
1569+
timeout=timeout,
1570+
)
1571+
raw_payload = self._client.send_request(request)
1572+
return PdfRestDeletionResponse.model_validate(raw_payload)
1573+
15461574
def read_bytes(
15471575
self,
15481576
file_ref: PdfRestFile | str,
@@ -1817,6 +1845,32 @@ async def fetch(file_id: str) -> PdfRestFile:
18171845

18181846
return await asyncio.gather(*(fetch(file_id) for file_id in file_ids))
18191847

1848+
async def delete(
1849+
self,
1850+
files: PdfRestFile | Sequence[PdfRestFile],
1851+
*,
1852+
extra_query: Query | None = None,
1853+
extra_headers: AnyMapping | None = None,
1854+
extra_body: Body | None = None,
1855+
timeout: TimeoutTypes | None = None,
1856+
) -> PdfRestDeletionResponse:
1857+
"""Delete one or more uploaded files by reference."""
1858+
1859+
payload = DeletePayload.model_validate({"files": files})
1860+
request = self._client.prepare_request(
1861+
"POST",
1862+
"/delete",
1863+
json_body=payload.model_dump(
1864+
mode="json", by_alias=True, exclude_none=True, exclude_unset=True
1865+
),
1866+
extra_query=extra_query,
1867+
extra_headers=extra_headers,
1868+
extra_body=extra_body,
1869+
timeout=timeout,
1870+
)
1871+
raw_payload = await self._client.send_request(request)
1872+
return PdfRestDeletionResponse.model_validate(raw_payload)
1873+
18201874
async def read_bytes(
18211875
self,
18221876
file_ref: PdfRestFile | str,

src/pdfrest/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .public import (
2+
PdfRestDeletionResponse,
23
PdfRestErrorResponse,
34
PdfRestFile,
45
PdfRestFileBasedResponse,
@@ -8,6 +9,7 @@
89
)
910

1011
__all__ = [
12+
"PdfRestDeletionResponse",
1113
"PdfRestErrorResponse",
1214
"PdfRestFile",
1315
"PdfRestFileBasedResponse",

src/pdfrest/models/_internal.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ def _serialize_as_comma_separated_string(value: list[Any] | None) -> str | None:
108108
return ",".join(str(element) for element in value)
109109

110110

111+
def _serialize_file_ids(value: list[PdfRestFile]) -> str:
112+
return ",".join(str(file.id) for file in value)
113+
114+
111115
def _serialize_page_ranges(value: list[str | int | tuple[str | int, ...]]) -> str:
112116
def join_tuple(value: str | int | tuple[str | int, ...]) -> str:
113117
if isinstance(value, tuple):
@@ -165,6 +169,21 @@ class UploadURLs(BaseModel):
165169
]
166170

167171

172+
class DeletePayload(BaseModel):
173+
"""Adapt caller options into a pdfRest-ready delete request payload."""
174+
175+
files: Annotated[
176+
list[PdfRestFile],
177+
Field(
178+
min_length=1,
179+
validation_alias=AliasChoices("file", "files"),
180+
serialization_alias="ids",
181+
),
182+
BeforeValidator(_ensure_list),
183+
PlainSerializer(_serialize_file_ids),
184+
]
185+
186+
168187
PageNumber = Annotated[int, Field(ge=1), PlainSerializer(lambda x: str(x))]
169188

170189

src/pdfrest/models/public.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,15 @@
1919
from pydantic_core import CoreSchema
2020
from typing_extensions import override
2121

22-
__all__ = ("PdfRestErrorResponse", "PdfRestFile", "PdfRestFileID", "UpResponse")
22+
__all__ = (
23+
"PdfRestDeletionResponse",
24+
"PdfRestErrorResponse",
25+
"PdfRestFile",
26+
"PdfRestFileBasedResponse",
27+
"PdfRestFileID",
28+
"PdfRestInfoResponse",
29+
"UpResponse",
30+
)
2331

2432

2533
class PdfRestFileID(str):
@@ -288,6 +296,22 @@ def output_file(self) -> PdfRestFile:
288296
raise ValueError(msg)
289297

290298

299+
class PdfRestDeletionResponse(BaseModel):
300+
"""Response returned by the delete tool."""
301+
302+
model_config = ConfigDict(extra="allow")
303+
304+
deletion_responses: Annotated[
305+
dict[PdfRestFileID, str],
306+
Field(
307+
alias="deletionResponses",
308+
validation_alias=AliasChoices("deletion_responses", "deletionResponses"),
309+
description="Mapping of file ids to deletion results.",
310+
min_length=1,
311+
),
312+
]
313+
314+
291315
class PdfRestInfoResponse(BaseModel):
292316
"""A response containing the output from the /info route."""
293317

tests/live/test_live_delete.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
from pdfrest import PdfRestApiError, PdfRestClient
6+
from pdfrest.models import PdfRestDeletionResponse
7+
8+
from ..resources import get_test_resource_path
9+
10+
11+
def test_live_delete_files_success(
12+
pdfrest_api_key: str,
13+
pdfrest_live_base_url: str,
14+
) -> None:
15+
resource = get_test_resource_path("report.pdf")
16+
with PdfRestClient(
17+
api_key=pdfrest_api_key,
18+
base_url=pdfrest_live_base_url,
19+
) as client:
20+
uploaded = client.files.create_from_paths([resource])[0]
21+
response = client.files.delete(uploaded)
22+
23+
assert isinstance(response, PdfRestDeletionResponse)
24+
assert response.deletion_responses[str(uploaded.id)] == "Successfully Deleted"
25+
26+
27+
def test_live_delete_files_invalid_id(
28+
pdfrest_api_key: str,
29+
pdfrest_live_base_url: str,
30+
) -> None:
31+
resource = get_test_resource_path("report.pdf")
32+
with PdfRestClient(
33+
api_key=pdfrest_api_key,
34+
base_url=pdfrest_live_base_url,
35+
) as client:
36+
uploaded = client.files.create_from_paths([resource])[0]
37+
with pytest.raises(PdfRestApiError):
38+
client.files.delete(
39+
uploaded,
40+
extra_body={"ids": ""},
41+
)

tests/test_delete_files.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
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 PdfRestDeletionResponse, PdfRestFileID
11+
from pdfrest.models._internal import DeletePayload
12+
13+
from .graphics_test_helpers import ASYNC_API_KEY, VALID_API_KEY, make_pdf_file
14+
15+
16+
def test_delete_payload_serialization() -> None:
17+
first = make_pdf_file(PdfRestFileID.generate(1))
18+
second = make_pdf_file(PdfRestFileID.generate(2))
19+
20+
payload = DeletePayload.model_validate({"files": [first, second]})
21+
payload_dump = payload.model_dump(
22+
mode="json", by_alias=True, exclude_none=True, exclude_unset=True
23+
)
24+
25+
assert payload_dump == {"ids": f"{first.id},{second.id}"}
26+
27+
28+
def test_delete_payload_rejects_empty() -> None:
29+
with pytest.raises(ValidationError):
30+
DeletePayload.model_validate({"files": []})
31+
32+
33+
def test_delete_files_success(monkeypatch: pytest.MonkeyPatch) -> None:
34+
monkeypatch.delenv("PDFREST_API_KEY", raising=False)
35+
file_repr = make_pdf_file(PdfRestFileID.generate(1))
36+
payload_dump = DeletePayload.model_validate({"files": [file_repr]}).model_dump(
37+
mode="json", by_alias=True, exclude_none=True, exclude_unset=True
38+
)
39+
40+
seen: dict[str, int] = {"post": 0}
41+
42+
def handler(request: httpx.Request) -> httpx.Response:
43+
if request.method == "POST" and request.url.path == "/delete":
44+
seen["post"] += 1
45+
payload = json.loads(request.content.decode("utf-8"))
46+
assert payload == payload_dump
47+
return httpx.Response(
48+
200,
49+
json={
50+
"deletionResponses": {
51+
str(file_repr.id): "Successfully Deleted",
52+
}
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.files.delete(file_repr)
61+
62+
assert seen == {"post": 1}
63+
assert isinstance(response, PdfRestDeletionResponse)
64+
assert response.deletion_responses[str(file_repr.id)] == "Successfully Deleted"
65+
66+
67+
def test_delete_files_request_customization(
68+
monkeypatch: pytest.MonkeyPatch,
69+
) -> None:
70+
monkeypatch.delenv("PDFREST_API_KEY", raising=False)
71+
file_repr = make_pdf_file(PdfRestFileID.generate(1))
72+
captured_timeout: dict[str, float | dict[str, float] | None] = {}
73+
74+
def handler(request: httpx.Request) -> httpx.Response:
75+
if request.method == "POST" and request.url.path == "/delete":
76+
assert request.url.params["trace"] == "true"
77+
assert request.headers["X-Debug"] == "sync"
78+
captured_timeout["value"] = request.extensions.get("timeout")
79+
payload = json.loads(request.content.decode("utf-8"))
80+
assert payload["ids"] == str(file_repr.id)
81+
assert payload["debug"] is True
82+
return httpx.Response(
83+
200,
84+
json={
85+
"deletionResponses": {
86+
str(file_repr.id): "Successfully Deleted",
87+
}
88+
},
89+
)
90+
msg = f"Unexpected request {request.method} {request.url}"
91+
raise AssertionError(msg)
92+
93+
transport = httpx.MockTransport(handler)
94+
with PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client:
95+
response = client.files.delete(
96+
file_repr,
97+
extra_query={"trace": "true"},
98+
extra_headers={"X-Debug": "sync"},
99+
extra_body={"debug": True},
100+
timeout=0.3,
101+
)
102+
103+
assert isinstance(response, PdfRestDeletionResponse)
104+
timeout_value = captured_timeout["value"]
105+
assert timeout_value is not None
106+
if isinstance(timeout_value, dict):
107+
assert all(
108+
component == pytest.approx(0.3) for component in timeout_value.values()
109+
)
110+
else:
111+
assert timeout_value == pytest.approx(0.3)
112+
113+
114+
@pytest.mark.asyncio
115+
async def test_async_delete_files_success(
116+
monkeypatch: pytest.MonkeyPatch,
117+
) -> None:
118+
monkeypatch.delenv("PDFREST_API_KEY", raising=False)
119+
file_repr = make_pdf_file(PdfRestFileID.generate(2))
120+
payload_dump = DeletePayload.model_validate({"files": [file_repr]}).model_dump(
121+
mode="json", by_alias=True, exclude_none=True, exclude_unset=True
122+
)
123+
124+
seen: dict[str, int] = {"post": 0}
125+
126+
def handler(request: httpx.Request) -> httpx.Response:
127+
if request.method == "POST" and request.url.path == "/delete":
128+
seen["post"] += 1
129+
payload = json.loads(request.content.decode("utf-8"))
130+
assert payload == payload_dump
131+
return httpx.Response(
132+
200,
133+
json={
134+
"deletionResponses": {
135+
str(file_repr.id): "Successfully Deleted",
136+
}
137+
},
138+
)
139+
msg = f"Unexpected request {request.method} {request.url}"
140+
raise AssertionError(msg)
141+
142+
transport = httpx.MockTransport(handler)
143+
async with AsyncPdfRestClient(
144+
api_key=ASYNC_API_KEY,
145+
transport=transport,
146+
) as client:
147+
response = await client.files.delete(file_repr)
148+
149+
assert seen == {"post": 1}
150+
assert isinstance(response, PdfRestDeletionResponse)
151+
assert response.deletion_responses[str(file_repr.id)] == "Successfully Deleted"

0 commit comments

Comments
 (0)