Skip to content

Commit 9d27eb6

Browse files
Merge pull request #5 from datalogics-kam/pdfcloud-5463-three
PDFCLOUD-5463 Add PDF/X, Flatten Forms, and Word, testing update
2 parents 7f175dc + 80a6eea commit 9d27eb6

8 files changed

Lines changed: 664 additions & 8 deletions

AGENTS.md

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,25 @@
3939
- When calling pdfRest, supply the API key via the `Api-Key` header (not
4040
`Authorization: Bearer`); keep tests and client defaults in sync with this
4141
convention.
42+
- Avoid `@field_validator` on payload models. Prefer existing `BeforeValidator`
43+
helpers (e.g., `_allowed_mime_types`) so validation remains declarative and
44+
consistent across schemas.
4245
- Treat `PdfRestClient` and `AsyncPdfRestClient` as context managers in both
4346
production code and tests so transports are disposed deterministically.
4447
- When uploading content, always send the multipart field name `file`; when
4548
uploading by URL, send a JSON payload using the `url` key with a list of
4649
http/https addresses (single values are promoted to lists internally).
50+
- Always upload local assets before invoking an endpoint helper. Public client
51+
APIs must accept `PdfRestFile` objects (or sequences) rather than raw paths or
52+
ids, including optional resources such as compression profiles. Never expose
53+
`PdfRestFileID` in the interface—callers should upload the profile JSON, get
54+
the resulting `PdfRestFile`, then pass that object into helpers like
55+
`compress_pdf`.
56+
- When an endpoint supports both an inline upload parameter and an `*_id`
57+
variant, ignore the upload form and expose only the base parameter (without
58+
`_id`) typed as `PdfRestFile`. Serialize via `_serialize_as_first_file_id`
59+
with `serialization_alias` pointing to the server’s `*_id` field so requests
60+
always reference already-uploaded resources.
4761
- `prepare_request` rejects mixed multipart (`files`) and JSON payloads; only
4862
URL uploads (`create_from_urls`) should combine JSON bodies with the request.
4963
- Replicate server-side safeguards when porting validation logic: the output
@@ -111,30 +125,48 @@
111125

112126
## Testing Guidelines
113127

128+
- **Live Test Requirement (Do Not Skip):** Every new endpoint or service must
129+
ship with a matching live pytest module under `tests/live/` before the work is
130+
considered complete. Mirror the naming/structure used by the graphic
131+
conversion suites: one module per endpoint, parameterized success cases that
132+
enumerate all accepted literals, at least one invalid input that hits the
133+
server, and coverage for any request options surfaced on the client. If an
134+
endpoint cannot be exercised live, call that out explicitly in the PR
135+
description with the reason and the follow-up plan; otherwise reviewers should
136+
block the change. Treat this as a release gate on par with unit tests.
137+
114138
- Write pytest tests: files named `test_*.py`, test functions `test_*`, fixtures
115139
in `conftest.py` where shared.
140+
116141
- Ensure high-value coverage of public functions and edge cases; document intent
117142
in test docstrings when non-obvious.
143+
118144
- Use `uvx nox -s tests` to exercise the full interpreter matrix locally when
119145
validating compatibility.
146+
120147
- When writing live tests for URL uploads, first create the remote resources via
121148
`create_from_paths`, then reuse the returned URLs in `create_from_urls` to
122149
avoid relying on third-party availability.
150+
123151
- For parameterized tests prefer `pytest.param(..., id="short-label")` so test
124152
IDs stay readable; make assertions for every relevant response attribute (name
125153
prefix, MIME type, size, URLs, warnings).
154+
126155
- Avoid manual loops over test parameters; prefer `@pytest.mark.parametrize`
127156
with explicit `id=` values so each combination is visible and reproducible.
157+
128158
- Always couple `pytest.raises` with an explicit `match=` regex that reflects
129159
the intended validation error wording—mirror the human-readable text rather
130160
than relying on default exception formatting.
161+
131162
- Mirror PNG’s request/response scenarios for each graphic conversion endpoint:
132163
maintain per-endpoint test modules (`test_convert_to_png.py`,
133164
`test_convert_to_bmp.py`, etc.) covering success, parameter customization,
134165
validation errors, multi-file guards, and async flows. Keep shared payload
135166
validation (output prefix and page-range cases) in a dedicated suite (e.g.,
136167
`tests/test_graphic_payload_validation.py`) that exercises every payload
137168
model.
169+
138170
- When introducing additional pdfRest endpoints, follow the same pattern used
139171
for graphic conversions: encapsulate shared request validation in a typed
140172
payload model, expose fully named client methods, and create a dedicated test
@@ -143,15 +175,20 @@
143175
checks (e.g., common field requirements, payload serialization) in shared
144176
helper tests so new services inherit consistent coverage with minimal
145177
duplication.
178+
146179
- Prefer `pytest.mark.parametrize` (with `pytest.param(..., id="...")`) over
147-
explicit loops inside tests; nest parametrization for multi-dimensional
148-
coverage so each case appears as an individual test item.
180+
explicit loops or copy/paste blocks—if only the input value or expected error
181+
changes, parameterize it so failures point to the exact case and reviewers
182+
don’t have to diff almost-identical code. Nest parametrization for
183+
multi-dimensional coverage so each combination appears as its own test item.
184+
149185
- Live tests should verify that literal enumerations match pdfRest’s accepted
150186
values. Exercise format-specific options (e.g., each image format’s
151187
`color_model`) individually, and run smoothing enumerations through every
152188
enabled endpoint to confirm consistent server behaviour. Include “wildly”
153189
invalid values (e.g., bogus literals or mixed lists) alongside boundary
154190
failures so the server-side error messaging is exercised.
191+
155192
- Provide live integration tests under `tests/live/` (with an `__init__.py` so
156193
pytest discovers the package) that introspect payload models to enumerate
157194
valid/invalid literal values and numeric boundaries. These tests should vary a
@@ -162,11 +199,13 @@
162199
exception surfaced by the client). When test fixtures produce deterministic
163200
results (e.g., `tests/resources/report.pdf`), assert the concrete values
164201
returned by pdfRest rather than only checking for presence or type.
202+
165203
- Use `tests/resources/20-pages.pdf` for high-page-count scenarios such as split
166204
and merge endpoints so boundary coverage (multi-output splits, staggered page
167205
selections) remains reproducible. Parameterize live split/merge tests to cover
168206
multiple page-group patterns, and pair each success case with an invalid input
169207
that reaches the server by overriding the JSON body via `extra_body`.
208+
170209
- Developers can load a pdfRest API key from `.env` during ad-hoc exploration.
171210
The repo includes `python-dotenv`; call `load_dotenv()` (optionally pointing
172211
to `.env`) in temporary scripts to drive the in-flight client against live
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from __future__ import annotations
2+
3+
from typing import cast, get_args
4+
5+
import pytest
6+
7+
from pdfrest import PdfRestApiError, PdfRestClient
8+
from pdfrest.models import PdfRestFile
9+
from pdfrest.types import PdfXType
10+
11+
from ..resources import get_test_resource_path
12+
13+
PDFX_TYPES: tuple[PdfXType, ...] = cast(tuple[PdfXType, ...], get_args(PdfXType))
14+
15+
16+
@pytest.fixture(scope="module")
17+
def uploaded_pdf_for_pdfx(
18+
pdfrest_api_key: str,
19+
pdfrest_live_base_url: str,
20+
) -> PdfRestFile:
21+
resource = get_test_resource_path("report.pdf")
22+
with PdfRestClient(
23+
api_key=pdfrest_api_key,
24+
base_url=pdfrest_live_base_url,
25+
) as client:
26+
return client.files.create_from_paths([resource])[0]
27+
28+
29+
@pytest.mark.parametrize("output_type", PDFX_TYPES, ids=list(PDFX_TYPES))
30+
def test_live_convert_to_pdfx_success(
31+
pdfrest_api_key: str,
32+
pdfrest_live_base_url: str,
33+
uploaded_pdf_for_pdfx: PdfRestFile,
34+
output_type: PdfXType,
35+
) -> None:
36+
with PdfRestClient(
37+
api_key=pdfrest_api_key,
38+
base_url=pdfrest_live_base_url,
39+
) as client:
40+
response = client.convert_to_pdfx(
41+
uploaded_pdf_for_pdfx,
42+
output_type=output_type,
43+
output="pdfx-live",
44+
)
45+
46+
assert response.output_files
47+
output_file = response.output_file
48+
assert output_file.type == "application/pdf"
49+
assert str(response.input_id) == str(uploaded_pdf_for_pdfx.id)
50+
assert output_file.name.startswith("pdfx-live")
51+
52+
53+
@pytest.mark.parametrize(
54+
"invalid_output_type",
55+
[
56+
pytest.param("PDF/X-0", id="pdfx-0"),
57+
pytest.param("PDF/X-99", id="pdfx-99"),
58+
pytest.param("pdf/x-4", id="lowercase"),
59+
],
60+
)
61+
def test_live_convert_to_pdfx_invalid_output_type(
62+
pdfrest_api_key: str,
63+
pdfrest_live_base_url: str,
64+
uploaded_pdf_for_pdfx: PdfRestFile,
65+
invalid_output_type: str,
66+
) -> None:
67+
with (
68+
PdfRestClient(
69+
api_key=pdfrest_api_key,
70+
base_url=pdfrest_live_base_url,
71+
) as client,
72+
pytest.raises(PdfRestApiError),
73+
):
74+
client.convert_to_pdfx(
75+
uploaded_pdf_for_pdfx,
76+
output_type="PDF/X-1a",
77+
extra_body={"output_type": invalid_output_type},
78+
)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
from pdfrest import 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_pdf_for_word(
13+
pdfrest_api_key: str,
14+
pdfrest_live_base_url: str,
15+
) -> PdfRestFile:
16+
resource = get_test_resource_path("report.pdf")
17+
with PdfRestClient(
18+
api_key=pdfrest_api_key,
19+
base_url=pdfrest_live_base_url,
20+
) as client:
21+
return client.files.create_from_paths([resource])[0]
22+
23+
24+
@pytest.mark.parametrize(
25+
"output_name",
26+
[
27+
pytest.param(None, id="default-output"),
28+
pytest.param("live-word", id="custom-output"),
29+
],
30+
)
31+
def test_live_convert_to_word_success(
32+
pdfrest_api_key: str,
33+
pdfrest_live_base_url: str,
34+
uploaded_pdf_for_word: PdfRestFile,
35+
output_name: str | None,
36+
) -> None:
37+
kwargs: dict[str, str] = {}
38+
if output_name is not None:
39+
kwargs["output"] = output_name
40+
41+
with PdfRestClient(
42+
api_key=pdfrest_api_key,
43+
base_url=pdfrest_live_base_url,
44+
) as client:
45+
response = client.convert_to_word(uploaded_pdf_for_word, **kwargs)
46+
47+
assert response.output_files
48+
output_file = response.output_file
49+
assert (
50+
output_file.type
51+
== "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
52+
)
53+
assert str(response.input_id) == str(uploaded_pdf_for_word.id)
54+
if output_name is not None:
55+
assert output_file.name.startswith(output_name)
56+
else:
57+
assert output_file.name.endswith(".docx")
58+
59+
60+
def test_live_convert_to_word_invalid_file_id(
61+
pdfrest_api_key: str,
62+
pdfrest_live_base_url: str,
63+
uploaded_pdf_for_word: PdfRestFile,
64+
) -> None:
65+
with (
66+
PdfRestClient(
67+
api_key=pdfrest_api_key,
68+
base_url=pdfrest_live_base_url,
69+
) as client,
70+
pytest.raises(PdfRestApiError),
71+
):
72+
client.convert_to_word(
73+
uploaded_pdf_for_word,
74+
extra_body={"id": "00000000-0000-0000-0000-000000000000"},
75+
)
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
from pdfrest import 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_pdf_with_forms(
13+
pdfrest_api_key: str,
14+
pdfrest_live_base_url: str,
15+
) -> PdfRestFile:
16+
resource = get_test_resource_path("form_with_data.pdf")
17+
with PdfRestClient(
18+
api_key=pdfrest_api_key,
19+
base_url=pdfrest_live_base_url,
20+
) as client:
21+
return client.files.create_from_paths([resource])[0]
22+
23+
24+
@pytest.mark.parametrize(
25+
"output_name",
26+
[
27+
pytest.param(None, id="default-output"),
28+
pytest.param("flattened-live", id="custom-output"),
29+
],
30+
)
31+
def test_live_flatten_pdf_forms(
32+
pdfrest_api_key: str,
33+
pdfrest_live_base_url: str,
34+
uploaded_pdf_with_forms: PdfRestFile,
35+
output_name: str | None,
36+
) -> None:
37+
kwargs: dict[str, str] = {}
38+
if output_name is not None:
39+
kwargs["output"] = output_name
40+
41+
with PdfRestClient(
42+
api_key=pdfrest_api_key,
43+
base_url=pdfrest_live_base_url,
44+
) as client:
45+
response = client.flatten_pdf_forms(uploaded_pdf_with_forms, **kwargs)
46+
47+
assert response.output_files
48+
output_file = response.output_file
49+
assert output_file.type == "application/pdf"
50+
assert str(response.input_id) == str(uploaded_pdf_with_forms.id)
51+
if output_name is not None:
52+
assert output_file.name.startswith(output_name)
53+
else:
54+
assert output_file.name.endswith(".pdf")
55+
56+
57+
def test_live_flatten_pdf_forms_invalid_file_id(
58+
pdfrest_api_key: str,
59+
pdfrest_live_base_url: str,
60+
uploaded_pdf_with_forms: PdfRestFile,
61+
) -> None:
62+
with (
63+
PdfRestClient(
64+
api_key=pdfrest_api_key,
65+
base_url=pdfrest_live_base_url,
66+
) as client,
67+
pytest.raises(PdfRestApiError),
68+
):
69+
client.flatten_pdf_forms(
70+
uploaded_pdf_with_forms,
71+
extra_body={"id": "ffffffff-ffff-ffff-ffff-ffffffffffff"},
72+
)

tests/resources/form_with_data.pdf

22.5 KB
Binary file not shown.

0 commit comments

Comments
 (0)