Skip to content

Commit 9050466

Browse files
convert_to_pdfa: Normalize PDF/A output types case-insensitively
- Normalize PDF/A `output_type` values case-insensitively during payload validation before enforcing the canonical `PdfAType` literals. - Derive the accepted canonical values from `get_args(PdfAType)` instead of maintaining a separate hard-coded list. - Update unit and live PDF/A tests to cover lowercase inputs. Assisted-by: Codex
1 parent 1e395c8 commit 9050466

3 files changed

Lines changed: 65 additions & 6 deletions

File tree

src/pdfrest/models/_internal.py

Lines changed: 16 additions & 2 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):
@@ -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: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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(

0 commit comments

Comments
 (0)