Skip to content

Commit f139a7f

Browse files
Merge pull request #42 from pdfrest/pdfcloud-5839-tests
PDFCLOUD-5839 Re-align tests with API
2 parents 1e395c8 + a05964a commit f139a7f

8 files changed

Lines changed: 313 additions & 14 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "pdfrest"
3-
version = "1.0.3"
3+
version = "1.0.4"
44
description = "Python client library for interacting with the pdfRest API"
55
readme = {file = "README.md", content-type = "text/markdown"}
66
authors = [

src/pdfrest/models/_internal.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import re
44
from collections.abc import Callable, Mapping, Sequence
55
from pathlib import PurePath
6-
from typing import Annotated, Any, Generic, Literal, TypeVar, cast
6+
from typing import Annotated, Any, Generic, Literal, TypeVar, cast, get_args
77

88
from langcodes import tag_is_valid
99
from pydantic import (
@@ -49,6 +49,10 @@
4949
from .public import PdfRestFile, PdfRestFileID
5050

5151
PdfConvertColorProfile = PdfPresetColorProfile | Literal["custom"]
52+
PDFA_OUTPUT_TYPES: tuple[PdfAType, ...] = cast(tuple[PdfAType, ...], get_args(PdfAType))
53+
PDFA_OUTPUT_TYPE_MAP: dict[str, PdfAType] = {
54+
output_type.casefold(): output_type for output_type in PDFA_OUTPUT_TYPES
55+
}
5256

5357

5458
def _ensure_list(value: Any) -> Any:
@@ -188,6 +192,12 @@ def _bool_to_true_false(value: Any) -> Any:
188192
return value
189193

190194

195+
def _normalize_pdfa_output_type(value: Any) -> Any:
196+
if not isinstance(value, str):
197+
return value
198+
return PDFA_OUTPUT_TYPE_MAP.get(value.casefold(), value)
199+
200+
191201
def _serialize_page_ranges(value: list[str | int | tuple[str | int, ...]]) -> str:
192202
def join_tuple(value: str | int | tuple[str | int, ...]) -> str:
193203
if isinstance(value, tuple):
@@ -1011,7 +1021,7 @@ class _PdfSignatureDisplayModel(BaseModel):
10111021
class _PdfSignatureConfigurationModel(BaseModel):
10121022
type: Literal["new", "existing"]
10131023
name: str | None = None
1014-
logo_opacity: Annotated[float | None, Field(gt=0, le=1, default=None)] = None
1024+
logo_opacity: Annotated[float | None, Field(ge=0, le=1, default=None)] = None
10151025
location: _PdfSignatureLocationModel | None = None
10161026
display: _PdfSignatureDisplayModel | None = None
10171027

@@ -1344,7 +1354,11 @@ class PdfToPdfaPayload(BaseModel):
13441354
),
13451355
PlainSerializer(_serialize_as_first_file_id),
13461356
]
1347-
output_type: Annotated[PdfAType, Field(serialization_alias="output_type")]
1357+
output_type: Annotated[
1358+
PdfAType,
1359+
Field(serialization_alias="output_type"),
1360+
BeforeValidator(_normalize_pdfa_output_type),
1361+
]
13481362
output: Annotated[
13491363
str | None,
13501364
Field(serialization_alias="output", min_length=1, default=None),

src/pdfrest/types/public.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ class PdfNewSignatureConfiguration(TypedDict, total=False):
257257
type: Must be ``"new"``.
258258
location: Placement rectangle and page as [PdfSignatureLocation][pdfrest.types.PdfSignatureLocation].
259259
name: Optional name for the signature field.
260-
logo_opacity: Optional logo opacity in the range ``(0, 1]``.
260+
logo_opacity: Optional logo opacity in the range ``[0, 1]``.
261261
display: Optional visible-signature settings as [PdfSignatureDisplay][pdfrest.types.PdfSignatureDisplay].
262262
"""
263263

@@ -275,7 +275,7 @@ class PdfExistingSignatureConfiguration(TypedDict, total=False):
275275
type: Must be ``"existing"``.
276276
location: Optional placement override as [PdfSignatureLocation][pdfrest.types.PdfSignatureLocation].
277277
name: Optional existing signature field name.
278-
logo_opacity: Optional logo opacity in the range ``(0, 1]``.
278+
logo_opacity: Optional logo opacity in the range ``[0, 1]``.
279279
display: Optional visible-signature settings as [PdfSignatureDisplay][pdfrest.types.PdfSignatureDisplay].
280280
"""
281281

@@ -323,7 +323,9 @@ class PdfPemCredentials(TypedDict):
323323
#: [AsyncPdfRestClient.sign_pdf][pdfrest.AsyncPdfRestClient.sign_pdf].
324324
PdfSignatureCredentials = PdfPfxCredentials | PdfPemCredentials
325325

326-
#: PDF/A conformance targets accepted by ``convert_to_pdfa``.
326+
#: Canonical PDF/A conformance targets accepted by ``convert_to_pdfa``.
327+
#: Payload validation accepts case-insensitive string input and normalizes it
328+
#: to one of these literals before serialization.
327329
PdfAType = Literal["PDF/A-1b", "PDF/A-2b", "PDF/A-2u", "PDF/A-3b", "PDF/A-3u"]
328330
#: PDF/X conformance targets accepted by ``convert_to_pdfx``.
329331
PdfXType = Literal["PDF/X-1a", "PDF/X-3", "PDF/X-4", "PDF/X-6"]

tests/live/test_live_convert_to_pdfa.py

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from typing import cast, get_args
3+
from typing import Any, cast, get_args
44

55
import pytest
66

@@ -101,6 +101,28 @@ def test_live_convert_to_pdfa_with_rasterize_option(
101101
assert str(response.input_id) == str(uploaded_pdf_for_pdfa.id)
102102

103103

104+
def test_live_convert_to_pdfa_accepts_lowercase_output_type(
105+
pdfrest_api_key: str,
106+
pdfrest_live_base_url: str,
107+
uploaded_pdf_for_pdfa: PdfRestFile,
108+
) -> None:
109+
with PdfRestClient(
110+
api_key=pdfrest_api_key,
111+
base_url=pdfrest_live_base_url,
112+
) as client:
113+
response = client.convert_to_pdfa(
114+
uploaded_pdf_for_pdfa,
115+
output_type=cast(Any, "pdf/a-2b"),
116+
output="pdfa-lowercase",
117+
)
118+
119+
assert response.output_files
120+
output_file = response.output_file
121+
assert output_file.name.startswith("pdfa-lowercase")
122+
assert output_file.type == "application/pdf"
123+
assert str(response.input_id) == str(uploaded_pdf_for_pdfa.id)
124+
125+
104126
@pytest.mark.asyncio
105127
async def test_live_async_convert_to_pdfa_with_rasterize_option(
106128
pdfrest_api_key: str,
@@ -125,12 +147,34 @@ async def test_live_async_convert_to_pdfa_with_rasterize_option(
125147
assert str(response.input_id) == str(uploaded_pdf_for_pdfa.id)
126148

127149

150+
@pytest.mark.asyncio
151+
async def test_live_async_convert_to_pdfa_accepts_lowercase_output_type(
152+
pdfrest_api_key: str,
153+
pdfrest_live_base_url: str,
154+
uploaded_pdf_for_pdfa: PdfRestFile,
155+
) -> None:
156+
async with AsyncPdfRestClient(
157+
api_key=pdfrest_api_key,
158+
base_url=pdfrest_live_base_url,
159+
) as client:
160+
response = await client.convert_to_pdfa(
161+
uploaded_pdf_for_pdfa,
162+
output_type=cast(Any, "pdf/a-2b"),
163+
output="async-pdfa-lowercase",
164+
)
165+
166+
assert response.output_files
167+
output_file = response.output_file
168+
assert output_file.name.startswith("async-pdfa-lowercase")
169+
assert output_file.type == "application/pdf"
170+
assert str(response.input_id) == str(uploaded_pdf_for_pdfa.id)
171+
172+
128173
@pytest.mark.parametrize(
129174
"invalid_output_type",
130175
[
131176
pytest.param("PDF/A-0", id="pdfa-0"),
132177
pytest.param("PDF/A-99", id="pdfa-99"),
133-
pytest.param("pdf/a-2b", id="lowercase"),
134178
],
135179
)
136180
def test_live_convert_to_pdfa_invalid_output_type(
@@ -159,7 +203,6 @@ def test_live_convert_to_pdfa_invalid_output_type(
159203
[
160204
pytest.param("PDF/A-0", id="pdfa-0"),
161205
pytest.param("PDF/A-99", id="pdfa-99"),
162-
pytest.param("pdf/a-2b", id="lowercase"),
163206
],
164207
)
165208
async def test_live_async_convert_to_pdfa_invalid_output_type(

tests/live/test_live_sign_pdf.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
)
2020

2121
INVALID_LOGO_OPACITY_VALUES = (
22-
pytest.param(0.0, id="zero"),
22+
pytest.param(-0.1, id="below-min"),
2323
pytest.param(1.1, id="above-max"),
2424
)
2525

@@ -501,6 +501,7 @@ def test_live_sign_pdf_invalid_logo_opacity(
501501
"signature_configuration": _to_json_string(
502502
{
503503
"type": "new",
504+
"name": "live-invalid-logo-opacity",
504505
"location": make_signature_location(),
505506
"logo_opacity": invalid_logo_opacity,
506507
}
@@ -509,6 +510,37 @@ def test_live_sign_pdf_invalid_logo_opacity(
509510
)
510511

511512

513+
def test_live_sign_pdf_logo_opacity_zero_is_allowed(
514+
pdfrest_api_key: str,
515+
pdfrest_live_base_url: str,
516+
uploaded_pdf_for_signing: PdfRestFile,
517+
uploaded_pfx_credential: PdfRestFile,
518+
uploaded_passphrase: PdfRestFile,
519+
) -> None:
520+
with PdfRestClient(
521+
api_key=pdfrest_api_key,
522+
base_url=pdfrest_live_base_url,
523+
) as client:
524+
response = client.sign_pdf(
525+
uploaded_pdf_for_signing,
526+
signature_configuration={
527+
"type": "new",
528+
"name": "live-logo-opacity-zero",
529+
"location": make_signature_location(),
530+
"logo_opacity": 0.0,
531+
},
532+
credentials={
533+
"pfx": uploaded_pfx_credential,
534+
"passphrase": uploaded_passphrase,
535+
},
536+
output="live-logo-opacity-zero",
537+
)
538+
539+
assert response.output_file.type == "application/pdf"
540+
assert response.output_file.name == "live-logo-opacity-zero.pdf"
541+
assert str(uploaded_pdf_for_signing.id) in response.input_ids
542+
543+
512544
@pytest.mark.asyncio
513545
async def test_live_async_sign_pdf_invalid_signature_configuration(
514546
pdfrest_api_key: str,
@@ -601,9 +633,42 @@ async def test_live_async_sign_pdf_invalid_logo_opacity(
601633
"signature_configuration": _to_json_string(
602634
{
603635
"type": "new",
636+
"name": "live-async-invalid-logo-opacity",
604637
"location": make_signature_location(),
605638
"logo_opacity": invalid_logo_opacity,
606639
}
607640
)
608641
},
609642
)
643+
644+
645+
@pytest.mark.asyncio
646+
async def test_live_async_sign_pdf_logo_opacity_zero_is_allowed(
647+
pdfrest_api_key: str,
648+
pdfrest_live_base_url: str,
649+
uploaded_pdf_for_signing: PdfRestFile,
650+
uploaded_pfx_credential: PdfRestFile,
651+
uploaded_passphrase: PdfRestFile,
652+
) -> None:
653+
async with AsyncPdfRestClient(
654+
api_key=pdfrest_api_key,
655+
base_url=pdfrest_live_base_url,
656+
) as client:
657+
response = await client.sign_pdf(
658+
uploaded_pdf_for_signing,
659+
signature_configuration={
660+
"type": "new",
661+
"name": "live-async-logo-opacity-zero",
662+
"location": make_signature_location(),
663+
"logo_opacity": 0.0,
664+
},
665+
credentials={
666+
"pfx": uploaded_pfx_credential,
667+
"passphrase": uploaded_passphrase,
668+
},
669+
output="live-async-logo-opacity-zero",
670+
)
671+
672+
assert response.output_file.type == "application/pdf"
673+
assert response.output_file.name == "live-async-logo-opacity-zero.pdf"
674+
assert str(uploaded_pdf_for_signing.id) in response.input_ids

tests/test_convert_to_pdfa.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import json
4+
from typing import Any, cast
45

56
import httpx
67
import pytest
@@ -210,6 +211,44 @@ def handler(request: httpx.Request) -> httpx.Response:
210211
assert timeout_value == pytest.approx(0.33)
211212

212213

214+
def test_convert_to_pdfa_normalizes_lowercase_output_type(
215+
monkeypatch: pytest.MonkeyPatch,
216+
) -> None:
217+
monkeypatch.delenv("PDFREST_API_KEY", raising=False)
218+
input_file = make_pdf_file(PdfRestFileID.generate(1))
219+
output_id = str(PdfRestFileID.generate())
220+
221+
def handler(request: httpx.Request) -> httpx.Response:
222+
if request.method == "POST" and request.url.path == "/pdfa":
223+
payload = json.loads(request.content.decode("utf-8"))
224+
assert payload["output_type"] == "PDF/A-2b"
225+
assert payload["id"] == str(input_file.id)
226+
return httpx.Response(
227+
200,
228+
json={"inputId": [input_file.id], "outputId": [output_id]},
229+
)
230+
if request.method == "GET" and request.url.path == f"/resource/{output_id}":
231+
return httpx.Response(
232+
200,
233+
json=build_file_info_payload(
234+
output_id, "lowercase.pdf", "application/pdf"
235+
),
236+
)
237+
msg = f"Unexpected request {request.method} {request.url}"
238+
raise AssertionError(msg)
239+
240+
transport = httpx.MockTransport(handler)
241+
with PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client:
242+
response = client.convert_to_pdfa(
243+
input_file,
244+
output_type=cast(Any, "pdf/a-2b"),
245+
)
246+
247+
assert isinstance(response, PdfRestFileBasedResponse)
248+
assert response.output_file.name == "lowercase.pdf"
249+
assert str(response.input_id) == str(input_file.id)
250+
251+
213252
@pytest.mark.asyncio
214253
async def test_async_convert_to_pdfa_request_customization(
215254
monkeypatch: pytest.MonkeyPatch,
@@ -272,6 +311,45 @@ def handler(request: httpx.Request) -> httpx.Response:
272311
assert timeout_value == pytest.approx(0.72)
273312

274313

314+
@pytest.mark.asyncio
315+
async def test_async_convert_to_pdfa_normalizes_lowercase_output_type(
316+
monkeypatch: pytest.MonkeyPatch,
317+
) -> None:
318+
monkeypatch.delenv("PDFREST_API_KEY", raising=False)
319+
input_file = make_pdf_file(PdfRestFileID.generate(2))
320+
output_id = str(PdfRestFileID.generate())
321+
322+
def handler(request: httpx.Request) -> httpx.Response:
323+
if request.method == "POST" and request.url.path == "/pdfa":
324+
payload = json.loads(request.content.decode("utf-8"))
325+
assert payload["output_type"] == "PDF/A-2b"
326+
assert payload["id"] == str(input_file.id)
327+
return httpx.Response(
328+
200,
329+
json={"inputId": [input_file.id], "outputId": [output_id]},
330+
)
331+
if request.method == "GET" and request.url.path == f"/resource/{output_id}":
332+
return httpx.Response(
333+
200,
334+
json=build_file_info_payload(
335+
output_id, "async-lowercase.pdf", "application/pdf"
336+
),
337+
)
338+
msg = f"Unexpected request {request.method} {request.url}"
339+
raise AssertionError(msg)
340+
341+
transport = httpx.MockTransport(handler)
342+
async with AsyncPdfRestClient(api_key=ASYNC_API_KEY, transport=transport) as client:
343+
response = await client.convert_to_pdfa(
344+
input_file,
345+
output_type=cast(Any, "pdf/a-2b"),
346+
)
347+
348+
assert isinstance(response, PdfRestFileBasedResponse)
349+
assert response.output_file.name == "async-lowercase.pdf"
350+
assert str(response.input_id) == str(input_file.id)
351+
352+
275353
def test_convert_to_pdfa_validation(monkeypatch: pytest.MonkeyPatch) -> None:
276354
monkeypatch.delenv("PDFREST_API_KEY", raising=False)
277355
pdf_file = make_pdf_file(PdfRestFileID.generate(1))

0 commit comments

Comments
 (0)