diff --git a/src/pdfrest/client.py b/src/pdfrest/client.py index 505f1288..7fd45ac6 100644 --- a/src/pdfrest/client.py +++ b/src/pdfrest/client.py @@ -95,10 +95,12 @@ PdfCompressPayload, PdfDecryptPayload, PdfEncryptPayload, + PdfExportFormDataPayload, PdfFlattenAnnotationsPayload, PdfFlattenFormsPayload, PdfFlattenLayersPayload, PdfFlattenTransparenciesPayload, + PdfImportFormDataPayload, PdfInfoPayload, PdfLinearizePayload, PdfMergePayload, @@ -127,6 +129,7 @@ ALL_PDF_INFO_QUERIES, BmpColorModel, CompressionLevel, + ExportDataFormat, ExtractTextGranularity, FlattenQuality, GifColorModel, @@ -2846,6 +2849,68 @@ def convert_to_word( timeout=timeout, ) + def import_form_data( + self, + file: PdfRestFile | Sequence[PdfRestFile], + data_file: PdfRestFile | Sequence[PdfRestFile], + *, + output: str | None = None, + extra_query: Query | None = None, + extra_headers: AnyMapping | None = None, + extra_body: Body | None = None, + timeout: TimeoutTypes | None = None, + ) -> PdfRestFileBasedResponse: + """Import form data from a data file into an existing PDF with form fields.""" + + payload: dict[str, Any] = {"files": file, "data_file": data_file} + if output is not None: + payload["output"] = output + + return self._post_file_operation( + endpoint="/pdf-with-imported-form-data", + payload=payload, + payload_model=PdfImportFormDataPayload, + extra_query=extra_query, + extra_headers=extra_headers, + extra_body=extra_body, + timeout=timeout, + ) + + def export_form_data( + self, + file: PdfRestFile | Sequence[PdfRestFile], + *, + data_format: ExportDataFormat, + output: str | None = None, + extra_query: Query | None = None, + extra_headers: AnyMapping | None = None, + extra_body: Body | None = None, + timeout: TimeoutTypes | None = None, + ) -> PdfRestFileBasedResponse: + """Export form data from a PDF into an external data file. + + `data_format` support depends on detected form type: + - AcroForm PDFs: `xfdf`, `fdf`, `xml` + - XFA PDFs: `xfd`, `xdp`, `xml` + """ + + payload: dict[str, Any] = { + "files": file, + "data_format": data_format, + } + if output is not None: + payload["output"] = output + + return self._post_file_operation( + endpoint="/exported-form-data", + payload=payload, + payload_model=PdfExportFormDataPayload, + extra_query=extra_query, + extra_headers=extra_headers, + extra_body=extra_body, + timeout=timeout, + ) + def flatten_pdf_forms( self, file: PdfRestFile | Sequence[PdfRestFile], @@ -4549,6 +4614,68 @@ async def convert_to_word( timeout=timeout, ) + async def import_form_data( + self, + file: PdfRestFile | Sequence[PdfRestFile], + data_file: PdfRestFile | Sequence[PdfRestFile], + *, + output: str | None = None, + extra_query: Query | None = None, + extra_headers: AnyMapping | None = None, + extra_body: Body | None = None, + timeout: TimeoutTypes | None = None, + ) -> PdfRestFileBasedResponse: + """Asynchronously import form data from a data file into a PDF.""" + + payload: dict[str, Any] = {"files": file, "data_file": data_file} + if output is not None: + payload["output"] = output + + return await self._post_file_operation( + endpoint="/pdf-with-imported-form-data", + payload=payload, + payload_model=PdfImportFormDataPayload, + extra_query=extra_query, + extra_headers=extra_headers, + extra_body=extra_body, + timeout=timeout, + ) + + async def export_form_data( + self, + file: PdfRestFile | Sequence[PdfRestFile], + *, + data_format: ExportDataFormat, + output: str | None = None, + extra_query: Query | None = None, + extra_headers: AnyMapping | None = None, + extra_body: Body | None = None, + timeout: TimeoutTypes | None = None, + ) -> PdfRestFileBasedResponse: + """Asynchronously export form data from a PDF into a data file. + + `data_format` support depends on detected form type: + - AcroForm PDFs: `xfdf`, `fdf`, `xml` + - XFA PDFs: `xfd`, `xdp`, `xml` + """ + + payload: dict[str, Any] = { + "files": file, + "data_format": data_format, + } + if output is not None: + payload["output"] = output + + return await self._post_file_operation( + endpoint="/exported-form-data", + payload=payload, + payload_model=PdfExportFormDataPayload, + extra_query=extra_query, + extra_headers=extra_headers, + extra_body=extra_body, + timeout=timeout, + ) + async def flatten_pdf_forms( self, file: PdfRestFile | Sequence[PdfRestFile], diff --git a/src/pdfrest/models/_internal.py b/src/pdfrest/models/_internal.py index 65f65197..8e31dba5 100644 --- a/src/pdfrest/models/_internal.py +++ b/src/pdfrest/models/_internal.py @@ -23,6 +23,7 @@ from pdfrest.types.public import PdfRedactionPreset from ..types import ( + ExportDataFormat, HtmlPageOrientation, HtmlPageSize, HtmlWebLayout, @@ -1318,6 +1319,80 @@ class PdfFlattenFormsPayload(BaseModel): ] = None +class PdfImportFormDataPayload(BaseModel): + """Adapt caller options into a pdfRest-ready import-form-data request payload.""" + + files: Annotated[ + list[PdfRestFile], + Field( + min_length=1, + max_length=1, + validation_alias=AliasChoices("file", "files"), + serialization_alias="id", + ), + BeforeValidator(_ensure_list), + AfterValidator( + _allowed_mime_types("application/pdf", error_msg="Must be a PDF file") + ), + PlainSerializer(_serialize_as_first_file_id), + ] + data_file: Annotated[ + list[PdfRestFile], + Field( + min_length=1, + max_length=1, + validation_alias=AliasChoices("data_file", "data_files"), + serialization_alias="data_file_id", + ), + BeforeValidator(_ensure_list), + AfterValidator( + _allowed_mime_types( + "application/xml", + "text/xml", + "application/vnd.fdf", + "application/vnd.adobe.xfdf", + "application/vnd.adobe.xdp+xml", + "application/vnd.adobe.xfd+xml", + error_msg="Data file must be an XFDF, XDP, XFD, FDF, or XML file", + ) + ), + PlainSerializer(_serialize_as_first_file_id), + ] + output: Annotated[ + str | None, + Field(serialization_alias="output", min_length=1, default=None), + AfterValidator(_validate_output_prefix), + ] = None + + +class PdfExportFormDataPayload(BaseModel): + """Adapt caller options into a pdfRest-ready export-form-data request payload.""" + + files: Annotated[ + list[PdfRestFile], + Field( + min_length=1, + max_length=1, + validation_alias=AliasChoices("file", "files"), + serialization_alias="id", + ), + BeforeValidator(_ensure_list), + AfterValidator( + _allowed_mime_types("application/pdf", error_msg="Must be a PDF file") + ), + PlainSerializer(_serialize_as_first_file_id), + ] + data_format: Annotated[ + ExportDataFormat, + Field(serialization_alias="data_format"), + ] + output: Annotated[ + str | None, + Field(serialization_alias="output", min_length=1, default=None), + AfterValidator(_validate_output_prefix), + ] = None + + class PdfCompressPayload(BaseModel): """Adapt caller options into a pdfRest-ready compress request payload.""" diff --git a/src/pdfrest/types/__init__.py b/src/pdfrest/types/__init__.py index b7db11ae..61360937 100644 --- a/src/pdfrest/types/__init__.py +++ b/src/pdfrest/types/__init__.py @@ -6,6 +6,7 @@ ALL_PDF_RESTRICTIONS, BmpColorModel, CompressionLevel, + ExportDataFormat, ExtractTextGranularity, FlattenQuality, GifColorModel, @@ -48,6 +49,7 @@ "ALL_PDF_RESTRICTIONS", "BmpColorModel", "CompressionLevel", + "ExportDataFormat", "ExtractTextGranularity", "FlattenQuality", "GifColorModel", diff --git a/src/pdfrest/types/public.py b/src/pdfrest/types/public.py index 20c73d97..55497645 100644 --- a/src/pdfrest/types/public.py +++ b/src/pdfrest/types/public.py @@ -18,6 +18,7 @@ "ALL_PDF_RESTRICTIONS", "BmpColorModel", "CompressionLevel", + "ExportDataFormat", "ExtractTextGranularity", "FlattenQuality", "GifColorModel", @@ -167,6 +168,9 @@ class PdfMergeSource(TypedDict, total=False): JpegColorModel = Literal["rgb", "cmyk", "gray"] TiffColorModel = Literal["rgb", "rgba", "cmyk", "lab", "gray"] GraphicSmoothing = Literal["none", "all", "text", "line", "image"] +# Server accepts all values here, but enforces form-type subsets at runtime: +# AcroForm -> xfdf/fdf/xml, XFA -> xfd/xdp/xml. +ExportDataFormat = Literal["fdf", "xfdf", "xml", "xdp", "xfd"] SummaryFormat = Literal[ "overview", diff --git a/tests/live/test_live_export_form_data.py b/tests/live/test_live_export_form_data.py new file mode 100644 index 00000000..881c2771 --- /dev/null +++ b/tests/live/test_live_export_form_data.py @@ -0,0 +1,275 @@ +from __future__ import annotations + +import pytest + +from pdfrest import AsyncPdfRestClient, PdfRestApiError, PdfRestClient +from pdfrest.models import PdfRestFile +from pdfrest.types import ExportDataFormat + +from ..resources import get_test_resource_path + + +@pytest.fixture(scope="module") +def uploaded_pdf_with_forms( + pdfrest_api_key: str, + pdfrest_live_base_url: str, +) -> PdfRestFile: + resource = get_test_resource_path("form_with_data.pdf") + with PdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client: + return client.files.create_from_paths([resource])[0] + + +@pytest.mark.parametrize( + "data_format", + [ + pytest.param("fdf", id="fdf"), + pytest.param("xfdf", id="xfdf"), + pytest.param("xml", id="xml"), + ], +) +def test_live_export_form_data_acroform( + pdfrest_api_key: str, + pdfrest_live_base_url: str, + uploaded_pdf_with_forms: PdfRestFile, + data_format: ExportDataFormat, +) -> None: + with PdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client: + response = client.export_form_data( + uploaded_pdf_with_forms, + data_format=data_format, + output=f"exported-{data_format}", + ) + + assert response.output_files + output_file = response.output_file + assert output_file.name.startswith(f"exported-{data_format}") + assert output_file.type + assert output_file.size > 0 + assert response.warning is None + assert str(uploaded_pdf_with_forms.id) in { + str(file_id) for file_id in response.input_ids + } + + +@pytest.fixture(scope="module") +def uploaded_xfa_pdf( + pdfrest_api_key: str, + pdfrest_live_base_url: str, +) -> PdfRestFile: + resource = get_test_resource_path("xfa.pdf") + with PdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client: + return client.files.create_from_paths([resource])[0] + + +@pytest.mark.parametrize( + "data_format", + [ + pytest.param("xfd", id="xfd"), + pytest.param("xdp", id="xdp"), + pytest.param("xml", id="xml"), + ], +) +def test_live_export_form_data_xfa( + pdfrest_api_key: str, + pdfrest_live_base_url: str, + uploaded_xfa_pdf: PdfRestFile, + data_format: ExportDataFormat, +) -> None: + with PdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client: + response = client.export_form_data( + uploaded_xfa_pdf, + data_format=data_format, + output=f"exported-xfa-{data_format}", + ) + + assert response.output_files + output_file = response.output_file + assert output_file.name.startswith(f"exported-xfa-{data_format}") + assert output_file.type + assert output_file.size > 0 + assert response.warning is None + assert str(uploaded_xfa_pdf.id) in {str(file_id) for file_id in response.input_ids} + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "data_format", + [ + pytest.param("fdf", id="fdf"), + pytest.param("xfdf", id="xfdf"), + pytest.param("xml", id="xml"), + ], +) +async def test_live_async_export_form_data_acroform( + pdfrest_api_key: str, + pdfrest_live_base_url: str, + uploaded_pdf_with_forms: PdfRestFile, + data_format: ExportDataFormat, +) -> None: + async with AsyncPdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client: + response = await client.export_form_data( + uploaded_pdf_with_forms, + data_format=data_format, + output=f"async-acro-{data_format}", + ) + + assert response.output_files + output_file = response.output_file + assert output_file.name.startswith(f"async-acro-{data_format}") + assert output_file.type + assert output_file.size > 0 + assert response.warning is None + assert str(uploaded_pdf_with_forms.id) in { + str(file_id) for file_id in response.input_ids + } + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "data_format", + [ + pytest.param("xfd", id="xfd"), + pytest.param("xdp", id="xdp"), + pytest.param("xml", id="xml"), + ], +) +async def test_live_async_export_form_data_xfa( + pdfrest_api_key: str, + pdfrest_live_base_url: str, + uploaded_xfa_pdf: PdfRestFile, + data_format: ExportDataFormat, +) -> None: + async with AsyncPdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client: + response = await client.export_form_data( + uploaded_xfa_pdf, + data_format=data_format, + output=f"async-xfa-{data_format}", + ) + + assert response.output_files + output_file = response.output_file + assert output_file.name.startswith(f"async-xfa-{data_format}") + assert output_file.type + assert output_file.size > 0 + assert response.warning is None + assert str(uploaded_xfa_pdf.id) in {str(file_id) for file_id in response.input_ids} + + +@pytest.mark.parametrize( + "invalid_format", + [ + pytest.param("xdp", id="xdp"), + pytest.param("xfd", id="xfd"), + ], +) +def test_live_export_form_data_invalid_format_for_pdf_type( + pdfrest_api_key: str, + pdfrest_live_base_url: str, + uploaded_pdf_with_forms: PdfRestFile, + invalid_format: ExportDataFormat, +) -> None: + with ( + PdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client, + pytest.raises(PdfRestApiError, match=r"(?i)(acroform|data_format)"), + ): + client.export_form_data( + uploaded_pdf_with_forms, + data_format=invalid_format, + ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "invalid_format", + [ + pytest.param("xdp", id="xdp"), + pytest.param("xfd", id="xfd"), + ], +) +async def test_live_async_export_form_data_invalid_format_for_pdf_type( + pdfrest_api_key: str, + pdfrest_live_base_url: str, + uploaded_pdf_with_forms: PdfRestFile, + invalid_format: ExportDataFormat, +) -> None: + async with AsyncPdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client: + with pytest.raises(PdfRestApiError, match=r"(?i)(acroform|data_format)"): + await client.export_form_data( + uploaded_pdf_with_forms, + data_format=invalid_format, + ) + + +@pytest.mark.parametrize( + "invalid_format", + [ + pytest.param("xfdf", id="xfdf"), + pytest.param("fdf", id="fdf"), + ], +) +def test_live_export_form_data_invalid_format_for_xfa( + pdfrest_api_key: str, + pdfrest_live_base_url: str, + uploaded_xfa_pdf: PdfRestFile, + invalid_format: ExportDataFormat, +) -> None: + with ( + PdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client, + pytest.raises(PdfRestApiError, match=r"(?i)(xfa|data_format)"), + ): + client.export_form_data( + uploaded_xfa_pdf, + data_format=invalid_format, + ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "invalid_format", + [ + pytest.param("xfdf", id="xfdf"), + pytest.param("fdf", id="fdf"), + ], +) +async def test_live_async_export_form_data_invalid_format_for_xfa( + pdfrest_api_key: str, + pdfrest_live_base_url: str, + uploaded_xfa_pdf: PdfRestFile, + invalid_format: ExportDataFormat, +) -> None: + async with AsyncPdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client: + with pytest.raises(PdfRestApiError, match=r"(?i)(xfa|data_format)"): + await client.export_form_data( + uploaded_xfa_pdf, + data_format=invalid_format, + ) diff --git a/tests/live/test_live_import_form_data.py b/tests/live/test_live_import_form_data.py new file mode 100644 index 00000000..f20ca1b2 --- /dev/null +++ b/tests/live/test_live_import_form_data.py @@ -0,0 +1,260 @@ +from __future__ import annotations + +import pytest + +from pdfrest import AsyncPdfRestClient, PdfRestApiError, PdfRestClient +from pdfrest.models import PdfRestFile + +from ..resources import get_test_resource_path + +IMPORT_FORM_DATA_SUCCESS_CASES = ( + pytest.param( + ("acroform.pdf", "test-data-acro.xml", None), + id="acro-xml", + ), + pytest.param( + ("acroform.pdf", "test-data-acro.xfdf", None), + id="acro-xfdf", + ), + pytest.param( + ("xfa.pdf", "test-data-xfa.xml", None), + id="xfa-xml", + ), + pytest.param( + ("xfa.pdf", "test-data-xfa.xdp", None), + id="xfa-xdp", + ), + pytest.param( + ( + "xfa.pdf", + "test-data-xfa.xfd", + "application/vnd.adobe.xfd+xml", + ), + id="xfa-xfd", + ), +) + + +@pytest.fixture(scope="module", params=IMPORT_FORM_DATA_SUCCESS_CASES) +def uploaded_success_import_form_data_case( + pdfrest_api_key: str, + pdfrest_live_base_url: str, + request: pytest.FixtureRequest, +) -> tuple[PdfRestFile, PdfRestFile]: + pdf_resource_name, data_resource_name, forced_data_mime = request.param + pdf_resource = get_test_resource_path(pdf_resource_name) + data_resource = get_test_resource_path(data_resource_name) + + with PdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client: + input_file = client.files.create_from_paths([pdf_resource])[0] + data_file = client.files.create_from_paths([data_resource])[0] + + # pdfRest currently reports .xfd uploads as application/octet-stream. + # Override the local MIME metadata so the request can exercise xfd imports. + if forced_data_mime is not None: + data_file = data_file.model_copy(update={"type": forced_data_mime}) + + return input_file, data_file + + +@pytest.fixture(scope="module") +def uploaded_acro_import_pair( + pdfrest_api_key: str, + pdfrest_live_base_url: str, +) -> tuple[PdfRestFile, PdfRestFile]: + acro_pdf = get_test_resource_path("acroform.pdf") + acro_data = get_test_resource_path("test-data-acro.xml") + + with PdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client: + input_file = client.files.create_from_paths([acro_pdf])[0] + data_file = client.files.create_from_paths([acro_data])[0] + + return input_file, data_file + + +@pytest.fixture(scope="module") +def uploaded_acro_fdf_import_pair( + pdfrest_api_key: str, + pdfrest_live_base_url: str, +) -> tuple[PdfRestFile, PdfRestFile]: + acro_pdf = get_test_resource_path("acroform.pdf") + acro_fdf = get_test_resource_path("test-data-acro.fdf") + + with PdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client: + input_file = client.files.create_from_paths([acro_pdf])[0] + data_file = client.files.create_from_paths([acro_fdf])[0] + + return input_file, data_file + + +@pytest.mark.parametrize( + "output_name", + [ + pytest.param(None, id="default-output"), + pytest.param("imported-form", id="custom-output"), + ], +) +def test_live_import_form_data( + pdfrest_api_key: str, + pdfrest_live_base_url: str, + uploaded_success_import_form_data_case: tuple[PdfRestFile, PdfRestFile], + output_name: str | None, +) -> None: + kwargs: dict[str, str] = {} + if output_name is not None: + kwargs["output"] = output_name + + uploaded_pdf_with_forms, uploaded_form_data_file = ( + uploaded_success_import_form_data_case + ) + + with PdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client: + response = client.import_form_data( + uploaded_pdf_with_forms, + uploaded_form_data_file, + **kwargs, + ) + + assert response.output_files + output_file = response.output_file + assert output_file.type == "application/pdf" + assert output_file.size > 0 + assert response.warning is None + assert str(uploaded_pdf_with_forms.id) in { + str(file_id) for file_id in response.input_ids + } + assert str(uploaded_form_data_file.id) in { + str(file_id) for file_id in response.input_ids + } + if output_name is not None: + assert output_file.name.startswith(output_name) + else: + assert output_file.name.endswith(".pdf") + + +@pytest.mark.asyncio +async def test_live_async_import_form_data_success( + pdfrest_api_key: str, + pdfrest_live_base_url: str, + uploaded_success_import_form_data_case: tuple[PdfRestFile, PdfRestFile], +) -> None: + uploaded_pdf_with_forms, uploaded_form_data_file = ( + uploaded_success_import_form_data_case + ) + + async with AsyncPdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client: + response = await client.import_form_data( + uploaded_pdf_with_forms, + uploaded_form_data_file, + output="async-imported", + ) + + assert response.output_files + output_file = response.output_file + assert output_file.name.startswith("async-imported") + assert output_file.type == "application/pdf" + assert output_file.size > 0 + assert response.warning is None + assert str(uploaded_pdf_with_forms.id) in { + str(file_id) for file_id in response.input_ids + } + assert str(uploaded_form_data_file.id) in { + str(file_id) for file_id in response.input_ids + } + + +def test_live_import_form_data_fdf_server_error( + pdfrest_api_key: str, + pdfrest_live_base_url: str, + uploaded_acro_fdf_import_pair: tuple[PdfRestFile, PdfRestFile], +) -> None: + uploaded_pdf_with_forms, uploaded_form_data_file = uploaded_acro_fdf_import_pair + + with ( + PdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client, + pytest.raises( + PdfRestApiError, + match=r"(?i)(issue processing|filled correctly|corrupted)", + ), + ): + client.import_form_data(uploaded_pdf_with_forms, uploaded_form_data_file) + + +@pytest.mark.asyncio +async def test_live_async_import_form_data_fdf_server_error( + pdfrest_api_key: str, + pdfrest_live_base_url: str, + uploaded_acro_fdf_import_pair: tuple[PdfRestFile, PdfRestFile], +) -> None: + uploaded_pdf_with_forms, uploaded_form_data_file = uploaded_acro_fdf_import_pair + + async with AsyncPdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client: + with pytest.raises( + PdfRestApiError, + match=r"(?i)(issue processing|filled correctly|corrupted)", + ): + await client.import_form_data( + uploaded_pdf_with_forms, uploaded_form_data_file + ) + + +def test_live_import_form_data_invalid_data_file_id( + pdfrest_api_key: str, + pdfrest_live_base_url: str, + uploaded_acro_import_pair: tuple[PdfRestFile, PdfRestFile], +) -> None: + uploaded_pdf_with_forms, uploaded_form_data_file = uploaded_acro_import_pair + + with ( + PdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client, + pytest.raises(PdfRestApiError, match=r"(?i)(data|id)"), + ): + client.import_form_data( + uploaded_pdf_with_forms, + uploaded_form_data_file, + extra_body={"data_file_id": "ffffffff-ffff-ffff-ffff-ffffffffffff"}, + ) + + +@pytest.mark.asyncio +async def test_live_async_import_form_data_invalid_data_file_id( + pdfrest_api_key: str, + pdfrest_live_base_url: str, + uploaded_acro_import_pair: tuple[PdfRestFile, PdfRestFile], +) -> None: + uploaded_pdf_with_forms, uploaded_form_data_file = uploaded_acro_import_pair + + async with AsyncPdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client: + with pytest.raises(PdfRestApiError, match=r"(?i)(data|id)"): + await client.import_form_data( + uploaded_pdf_with_forms, + uploaded_form_data_file, + extra_body={"data_file_id": "00000000-0000-0000-0000-000000000000"}, + ) diff --git a/tests/resources/acroform.pdf b/tests/resources/acroform.pdf new file mode 100644 index 00000000..f8a37c6b Binary files /dev/null and b/tests/resources/acroform.pdf differ diff --git a/tests/resources/form_data.xml b/tests/resources/form_data.xml new file mode 100644 index 00000000..9aa268dd --- /dev/null +++ b/tests/resources/form_data.xml @@ -0,0 +1,14 @@ + + + + + Jamie + + + Appleseed + + + Green + + + diff --git a/tests/resources/test-data-acro.fdf b/tests/resources/test-data-acro.fdf new file mode 100644 index 00000000..68a80087 --- /dev/null +++ b/tests/resources/test-data-acro.fdf @@ -0,0 +1,19 @@ +%FDF-1.2 +%âãÏÓ +1 0 obj +<< +/F (minimal_contact_form_acroform.pdf) +/Fields [ + << /T (Name) /V (Alice Example) >> + << /T (Email) /V (alice@example.com) >> + << /T (Phone) /V (+1 312 555 0100) >> + << /T (Message) /V (Hello!\nThis is a sample message populated via FDF.) >> + << /T (Subscribe) /V /Yes >> +] +>> +endobj +trailer +<< +/Root 1 0 R +>> +%%EOF diff --git a/tests/resources/test-data-acro.xfdf b/tests/resources/test-data-acro.xfdf new file mode 100644 index 00000000..8d2a7d76 --- /dev/null +++ b/tests/resources/test-data-acro.xfdf @@ -0,0 +1,11 @@ + + + + Alice Example + alice@example.com + +1 312 555 0100 + Hello! +This is a sample message populated via XFDF. + Yes + + diff --git a/tests/resources/test-data-acro.xml b/tests/resources/test-data-acro.xml new file mode 100644 index 00000000..32ebb856 --- /dev/null +++ b/tests/resources/test-data-acro.xml @@ -0,0 +1,9 @@ + + + Alice Example + alice@example.com + +1 312 555 0100 + Hello! +This is a sample message populated via XML (convert to FDF/XFDF as needed). + true + diff --git a/tests/resources/test-data-xfa.xdp b/tests/resources/test-data-xfa.xdp new file mode 100644 index 00000000..39dcfbcb --- /dev/null +++ b/tests/resources/test-data-xfa.xdp @@ -0,0 +1,292 @@ + + + +JohnDoexxxxxxxxxJaneDoexxxxxxxxx
123 Oak StAnytown, USA 00000
HoH
+
diff --git a/tests/resources/test-data-xfa.xfd b/tests/resources/test-data-xfa.xfd new file mode 100644 index 00000000..5a3f984f --- /dev/null +++ b/tests/resources/test-data-xfa.xfd @@ -0,0 +1,300 @@ + + +JohnDoexxxxxxxxxJaneDoexxxxxxxxx
123 Oak StAnytown, USA 00000
HoH
diff --git a/tests/resources/test-data-xfa.xml b/tests/resources/test-data-xfa.xml new file mode 100644 index 00000000..3319f551 --- /dev/null +++ b/tests/resources/test-data-xfa.xml @@ -0,0 +1,284 @@ + +JohnDoexxxxxxxxxJaneDoexxxxxxxxx
123 Oak StAnytown, USA 00000
HoH
diff --git a/tests/test_export_form_data.py b/tests/test_export_form_data.py new file mode 100644 index 00000000..f590e504 --- /dev/null +++ b/tests/test_export_form_data.py @@ -0,0 +1,379 @@ +from __future__ import annotations + +import json + +import httpx +import pytest +from pydantic import ValidationError + +from pdfrest import AsyncPdfRestClient, PdfRestClient +from pdfrest.models import PdfRestFile, PdfRestFileBasedResponse, PdfRestFileID +from pdfrest.models._internal import PdfExportFormDataPayload +from pdfrest.types import ExportDataFormat + +from .graphics_test_helpers import ( + ASYNC_API_KEY, + VALID_API_KEY, + build_file_info_payload, + make_pdf_file, +) + + +@pytest.mark.parametrize( + ("data_format", "output_name", "mime_type"), + [ + pytest.param("fdf", "exported-data.fdf", "application/vnd.fdf", id="fdf"), + pytest.param( + "xfdf", + "exported-data.xfdf", + "application/vnd.adobe.xfdf", + id="xfdf", + ), + pytest.param("xml", "exported-data.xml", "application/xml", id="xml"), + pytest.param( + "xdp", + "exported-data.xdp", + "application/vnd.adobe.xdp+xml", + id="xdp", + ), + pytest.param( + "xfd", + "exported-data.xfd", + "application/vnd.adobe.xfd+xml", + id="xfd", + ), + ], +) +def test_export_form_data_success( + monkeypatch: pytest.MonkeyPatch, + data_format: ExportDataFormat, + output_name: str, + mime_type: str, +) -> None: + monkeypatch.delenv("PDFREST_API_KEY", raising=False) + input_file = make_pdf_file(PdfRestFileID.generate(1)) + output_id = str(PdfRestFileID.generate()) + + payload_dump = PdfExportFormDataPayload.model_validate( + {"files": [input_file], "data_format": data_format, "output": "exported-data"} + ).model_dump(mode="json", by_alias=True, exclude_none=True, exclude_unset=True) + + seen: dict[str, int] = {"post": 0, "get": 0} + + def handler(request: httpx.Request) -> httpx.Response: + if request.method == "POST" and request.url.path == "/exported-form-data": + seen["post"] += 1 + payload = json.loads(request.content.decode("utf-8")) + assert payload == payload_dump + return httpx.Response( + 200, + json={ + "inputId": [input_file.id], + "outputId": [output_id], + }, + ) + if request.method == "GET" and request.url.path == f"/resource/{output_id}": + seen["get"] += 1 + assert request.url.params["format"] == "info" + return httpx.Response( + 200, + json=build_file_info_payload( + output_id, + output_name, + mime_type, + ), + ) + msg = f"Unexpected request {request.method} {request.url}" + raise AssertionError(msg) + + transport = httpx.MockTransport(handler) + with PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client: + response = client.export_form_data( + input_file, + data_format=data_format, + output="exported-data", + ) + + assert seen == {"post": 1, "get": 1} + assert isinstance(response, PdfRestFileBasedResponse) + assert response.output_file.name == output_name + assert response.output_file.type == mime_type + assert str(response.input_id) == str(input_file.id) + assert response.warning is None + + +def test_export_form_data_request_customization( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("PDFREST_API_KEY", raising=False) + input_file = make_pdf_file(PdfRestFileID.generate(1)) + output_id = str(PdfRestFileID.generate()) + captured_timeout: dict[str, float | dict[str, float] | None] = {} + + def handler(request: httpx.Request) -> httpx.Response: + if request.method == "POST" and request.url.path == "/exported-form-data": + assert request.url.params["trace"] == "true" + assert request.headers["X-Debug"] == "sync" + captured_timeout["value"] = request.extensions.get("timeout") + payload = json.loads(request.content.decode("utf-8")) + assert payload["id"] == str(input_file.id) + assert payload["data_format"] == "fdf" + assert payload["output"] == "custom-data" + assert payload["flag"] == "yes" + return httpx.Response( + 200, + json={ + "inputId": [input_file.id], + "outputId": [output_id], + }, + ) + if request.method == "GET" and request.url.path == f"/resource/{output_id}": + assert request.url.params["format"] == "info" + assert request.url.params["trace"] == "true" + assert request.headers["X-Debug"] == "sync" + return httpx.Response( + 200, + json=build_file_info_payload( + output_id, + "custom-data.fdf", + "application/vnd.fdf", + ), + ) + msg = f"Unexpected request {request.method} {request.url}" + raise AssertionError(msg) + + transport = httpx.MockTransport(handler) + with PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client: + response = client.export_form_data( + input_file, + data_format="fdf", + output="custom-data", + extra_query={"trace": "true"}, + extra_headers={"X-Debug": "sync"}, + extra_body={"flag": "yes"}, + timeout=0.37, + ) + + assert isinstance(response, PdfRestFileBasedResponse) + assert response.output_file.name == "custom-data.fdf" + timeout_value = captured_timeout["value"] + assert timeout_value is not None + if isinstance(timeout_value, dict): + assert all( + component == pytest.approx(0.37) for component in timeout_value.values() + ) + else: + assert timeout_value == pytest.approx(0.37) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("data_format", "output_name", "mime_type"), + [ + pytest.param("fdf", "async-data.fdf", "application/vnd.fdf", id="fdf"), + pytest.param( + "xfdf", + "async-data.xfdf", + "application/vnd.adobe.xfdf", + id="xfdf", + ), + pytest.param("xml", "async-data.xml", "application/xml", id="xml"), + pytest.param( + "xdp", + "async-data.xdp", + "application/vnd.adobe.xdp+xml", + id="xdp", + ), + pytest.param( + "xfd", + "async-data.xfd", + "application/vnd.adobe.xfd+xml", + id="xfd", + ), + ], +) +async def test_async_export_form_data_success( + monkeypatch: pytest.MonkeyPatch, + data_format: ExportDataFormat, + output_name: str, + mime_type: str, +) -> None: + monkeypatch.delenv("PDFREST_API_KEY", raising=False) + input_file = make_pdf_file(PdfRestFileID.generate(2)) + output_id = str(PdfRestFileID.generate()) + + payload_dump = PdfExportFormDataPayload.model_validate( + {"files": [input_file], "data_format": data_format} + ).model_dump(mode="json", by_alias=True, exclude_none=True, exclude_unset=True) + + seen: dict[str, int] = {"post": 0, "get": 0} + + def handler(request: httpx.Request) -> httpx.Response: + if request.method == "POST" and request.url.path == "/exported-form-data": + seen["post"] += 1 + payload = json.loads(request.content.decode("utf-8")) + assert payload == payload_dump + return httpx.Response( + 200, + json={ + "inputId": [input_file.id], + "outputId": [output_id], + }, + ) + if request.method == "GET" and request.url.path == f"/resource/{output_id}": + seen["get"] += 1 + assert request.url.params["format"] == "info" + return httpx.Response( + 200, + json=build_file_info_payload( + output_id, + output_name, + mime_type, + ), + ) + msg = f"Unexpected request {request.method} {request.url}" + raise AssertionError(msg) + + transport = httpx.MockTransport(handler) + async with AsyncPdfRestClient(api_key=ASYNC_API_KEY, transport=transport) as client: + response = await client.export_form_data(input_file, data_format=data_format) + + assert seen == {"post": 1, "get": 1} + assert isinstance(response, PdfRestFileBasedResponse) + assert response.output_file.name == output_name + assert response.output_file.type == mime_type + assert str(response.input_id) == str(input_file.id) + + +@pytest.mark.asyncio +async def test_async_export_form_data_request_customization( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("PDFREST_API_KEY", raising=False) + input_file = make_pdf_file(PdfRestFileID.generate(2)) + output_id = str(PdfRestFileID.generate()) + captured_timeout: dict[str, float | dict[str, float] | None] = {} + + def handler(request: httpx.Request) -> httpx.Response: + if request.method == "POST" and request.url.path == "/exported-form-data": + assert request.url.params["trace"] == "async" + assert request.headers["X-Debug"] == "async" + captured_timeout["value"] = request.extensions.get("timeout") + payload = json.loads(request.content.decode("utf-8")) + assert payload["id"] == str(input_file.id) + assert payload["data_format"] == "xml" + assert payload["note"] == "details" + return httpx.Response( + 200, + json={ + "inputId": [input_file.id], + "outputId": [output_id], + }, + ) + if request.method == "GET" and request.url.path == f"/resource/{output_id}": + assert request.url.params["format"] == "info" + assert request.url.params["trace"] == "async" + assert request.headers["X-Debug"] == "async" + return httpx.Response( + 200, + json=build_file_info_payload( + output_id, + "async-custom.xml", + "application/xml", + ), + ) + msg = f"Unexpected request {request.method} {request.url}" + raise AssertionError(msg) + + transport = httpx.MockTransport(handler) + async with AsyncPdfRestClient(api_key=ASYNC_API_KEY, transport=transport) as client: + response = await client.export_form_data( + input_file, + data_format="xml", + extra_query={"trace": "async"}, + extra_headers={"X-Debug": "async"}, + extra_body={"note": "details"}, + timeout=0.62, + ) + + assert isinstance(response, PdfRestFileBasedResponse) + assert response.output_file.name == "async-custom.xml" + timeout_value = captured_timeout["value"] + assert timeout_value is not None + if isinstance(timeout_value, dict): + assert all( + component == pytest.approx(0.62) for component in timeout_value.values() + ) + else: + assert timeout_value == pytest.approx(0.62) + + +def test_export_form_data_validation(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("PDFREST_API_KEY", raising=False) + pdf_file = make_pdf_file(PdfRestFileID.generate(1)) + png_file = PdfRestFile.model_validate( + build_file_info_payload( + PdfRestFileID.generate(), + "example.png", + "image/png", + ) + ) + transport = httpx.MockTransport(lambda request: (_ for _ in ()).throw(RuntimeError)) + + with ( + PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client, + pytest.raises(ValidationError, match="Must be a PDF file"), + ): + client.export_form_data(png_file, data_format="xml") + + with ( + PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client, + pytest.raises(ValidationError, match="Input should be 'fdf'"), + ): + client.export_form_data(pdf_file, data_format="yaml") # type: ignore[arg-type] + + with ( + PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client, + pytest.raises( + ValidationError, match="List should have at most 1 item after validation" + ), + ): + client.export_form_data( + [pdf_file, make_pdf_file(PdfRestFileID.generate())], data_format="xml" + ) + + +@pytest.mark.asyncio +async def test_async_export_form_data_validation( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("PDFREST_API_KEY", raising=False) + pdf_file = make_pdf_file(PdfRestFileID.generate(1)) + png_file = PdfRestFile.model_validate( + build_file_info_payload( + PdfRestFileID.generate(), + "example.png", + "image/png", + ) + ) + transport = httpx.MockTransport(lambda request: (_ for _ in ()).throw(RuntimeError)) + + async with AsyncPdfRestClient(api_key=ASYNC_API_KEY, transport=transport) as client: + with pytest.raises(ValidationError, match="Must be a PDF file"): + await client.export_form_data(png_file, data_format="xml") + + async with AsyncPdfRestClient(api_key=ASYNC_API_KEY, transport=transport) as client: + with pytest.raises(ValidationError, match="Input should be 'fdf'"): + await client.export_form_data( + pdf_file, + data_format="yaml", # type: ignore[arg-type] + ) + + async with AsyncPdfRestClient(api_key=ASYNC_API_KEY, transport=transport) as client: + with pytest.raises( + ValidationError, match="List should have at most 1 item after validation" + ): + await client.export_form_data( + [pdf_file, make_pdf_file(PdfRestFileID.generate())], + data_format="xml", + ) diff --git a/tests/test_import_form_data.py b/tests/test_import_form_data.py new file mode 100644 index 00000000..10519bb3 --- /dev/null +++ b/tests/test_import_form_data.py @@ -0,0 +1,406 @@ +from __future__ import annotations + +import json + +import httpx +import pytest +from pydantic import ValidationError + +from pdfrest import AsyncPdfRestClient, PdfRestClient +from pdfrest.models import PdfRestFile, PdfRestFileBasedResponse, PdfRestFileID +from pdfrest.models._internal import PdfImportFormDataPayload + +from .graphics_test_helpers import ( + ASYNC_API_KEY, + VALID_API_KEY, + build_file_info_payload, + make_pdf_file, +) + +ACCEPTED_IMPORT_DATA_FILE_MIME_TYPES = ( + pytest.param("application/xml", id="application-xml"), + pytest.param("text/xml", id="text-xml"), + pytest.param("application/vnd.fdf", id="application-vnd-fdf"), + pytest.param( + "application/vnd.adobe.xfdf", + id="application-vnd-adobe-xfdf", + ), + pytest.param( + "application/vnd.adobe.xdp+xml", + id="application-vnd-adobe-xdp+xml", + ), + pytest.param( + "application/vnd.adobe.xfd+xml", + id="application-vnd-adobe-xfd+xml", + ), +) + + +def _make_data_file( + file_id: PdfRestFileID, *, mime_type: str = "application/xml" +) -> PdfRestFile: + file_name = "form-data.xml" + if mime_type == "application/vnd.fdf": + file_name = "form-data.fdf" + elif mime_type == "application/vnd.adobe.xfdf": + file_name = "form-data.xfdf" + elif mime_type == "application/vnd.adobe.xdp+xml": + file_name = "form-data.xdp" + elif mime_type == "application/vnd.adobe.xfd+xml": + file_name = "form-data.xfd" + + return PdfRestFile.model_validate( + build_file_info_payload(file_id, file_name, mime_type) + ) + + +@pytest.mark.parametrize("data_file_mime", ACCEPTED_IMPORT_DATA_FILE_MIME_TYPES) +def test_import_form_data_success( + monkeypatch: pytest.MonkeyPatch, + data_file_mime: str, +) -> None: + monkeypatch.delenv("PDFREST_API_KEY", raising=False) + input_file = make_pdf_file(PdfRestFileID.generate(1)) + data_file = _make_data_file(PdfRestFileID.generate(2), mime_type=data_file_mime) + output_id = str(PdfRestFileID.generate()) + + payload_dump = PdfImportFormDataPayload.model_validate( + {"files": [input_file], "data_file": [data_file], "output": "filled-form"} + ).model_dump(mode="json", by_alias=True, exclude_none=True, exclude_unset=True) + + seen: dict[str, int] = {"post": 0, "get": 0} + + def handler(request: httpx.Request) -> httpx.Response: + if ( + request.method == "POST" + and request.url.path == "/pdf-with-imported-form-data" + ): + seen["post"] += 1 + payload = json.loads(request.content.decode("utf-8")) + assert payload == payload_dump + return httpx.Response( + 200, + json={ + "inputId": [input_file.id, data_file.id], + "outputId": [output_id], + }, + ) + if request.method == "GET" and request.url.path == f"/resource/{output_id}": + seen["get"] += 1 + assert request.url.params["format"] == "info" + return httpx.Response( + 200, + json=build_file_info_payload( + output_id, + "filled-form.pdf", + "application/pdf", + ), + ) + msg = f"Unexpected request {request.method} {request.url}" + raise AssertionError(msg) + + transport = httpx.MockTransport(handler) + with PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client: + response = client.import_form_data( + input_file, + data_file, + output="filled-form", + ) + + assert seen == {"post": 1, "get": 1} + assert isinstance(response, PdfRestFileBasedResponse) + assert response.output_file.name == "filled-form.pdf" + assert response.output_file.type == "application/pdf" + assert str(input_file.id) in {str(file_id) for file_id in response.input_ids} + assert str(data_file.id) in {str(file_id) for file_id in response.input_ids} + assert response.warning is None + + +def test_import_form_data_request_customization( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("PDFREST_API_KEY", raising=False) + input_file = make_pdf_file(PdfRestFileID.generate(1)) + data_file = _make_data_file(PdfRestFileID.generate(2)) + output_id = str(PdfRestFileID.generate()) + captured_timeout: dict[str, float | dict[str, float] | None] = {} + + def handler(request: httpx.Request) -> httpx.Response: + if ( + request.method == "POST" + and request.url.path == "/pdf-with-imported-form-data" + ): + assert request.url.params["trace"] == "true" + assert request.headers["X-Debug"] == "sync" + captured_timeout["value"] = request.extensions.get("timeout") + payload = json.loads(request.content.decode("utf-8")) + assert payload["id"] == str(input_file.id) + assert payload["data_file_id"] == str(data_file.id) + assert payload["output"] == "custom-output" + assert payload["debug"] == "flag" + return httpx.Response( + 200, + json={ + "inputId": [input_file.id, data_file.id], + "outputId": [output_id], + }, + ) + if request.method == "GET" and request.url.path == f"/resource/{output_id}": + assert request.url.params["format"] == "info" + assert request.url.params["trace"] == "true" + assert request.headers["X-Debug"] == "sync" + return httpx.Response( + 200, + json=build_file_info_payload( + output_id, + "custom.pdf", + "application/pdf", + ), + ) + msg = f"Unexpected request {request.method} {request.url}" + raise AssertionError(msg) + + transport = httpx.MockTransport(handler) + with PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client: + response = client.import_form_data( + input_file, + data_file, + output="custom-output", + extra_query={"trace": "true"}, + extra_headers={"X-Debug": "sync"}, + extra_body={"debug": "flag"}, + timeout=0.41, + ) + + assert isinstance(response, PdfRestFileBasedResponse) + assert response.output_file.name == "custom.pdf" + timeout_value = captured_timeout["value"] + assert timeout_value is not None + if isinstance(timeout_value, dict): + assert all( + component == pytest.approx(0.41) for component in timeout_value.values() + ) + else: + assert timeout_value == pytest.approx(0.41) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("data_file_mime", ACCEPTED_IMPORT_DATA_FILE_MIME_TYPES) +async def test_async_import_form_data_success( + monkeypatch: pytest.MonkeyPatch, + data_file_mime: str, +) -> None: + monkeypatch.delenv("PDFREST_API_KEY", raising=False) + input_file = make_pdf_file(PdfRestFileID.generate(2)) + data_file = _make_data_file(PdfRestFileID.generate(1), mime_type=data_file_mime) + output_id = str(PdfRestFileID.generate()) + + payload_dump = PdfImportFormDataPayload.model_validate( + {"files": [input_file], "data_file": [data_file]} + ).model_dump(mode="json", by_alias=True, exclude_none=True, exclude_unset=True) + + seen: dict[str, int] = {"post": 0, "get": 0} + + def handler(request: httpx.Request) -> httpx.Response: + if ( + request.method == "POST" + and request.url.path == "/pdf-with-imported-form-data" + ): + seen["post"] += 1 + payload = json.loads(request.content.decode("utf-8")) + assert payload == payload_dump + return httpx.Response( + 200, + json={ + "inputId": [input_file.id, data_file.id], + "outputId": [output_id], + }, + ) + if request.method == "GET" and request.url.path == f"/resource/{output_id}": + seen["get"] += 1 + assert request.url.params["format"] == "info" + return httpx.Response( + 200, + json=build_file_info_payload( + output_id, + "async.pdf", + "application/pdf", + ), + ) + msg = f"Unexpected request {request.method} {request.url}" + raise AssertionError(msg) + + transport = httpx.MockTransport(handler) + async with AsyncPdfRestClient(api_key=ASYNC_API_KEY, transport=transport) as client: + response = await client.import_form_data(input_file, data_file) + + assert seen == {"post": 1, "get": 1} + assert isinstance(response, PdfRestFileBasedResponse) + assert response.output_file.name == "async.pdf" + assert response.output_file.type == "application/pdf" + assert str(input_file.id) in {str(file_id) for file_id in response.input_ids} + assert str(data_file.id) in {str(file_id) for file_id in response.input_ids} + + +@pytest.mark.asyncio +async def test_async_import_form_data_request_customization( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("PDFREST_API_KEY", raising=False) + input_file = make_pdf_file(PdfRestFileID.generate(2)) + data_file = _make_data_file(PdfRestFileID.generate(1)) + output_id = str(PdfRestFileID.generate()) + captured_timeout: dict[str, float | dict[str, float] | None] = {} + + def handler(request: httpx.Request) -> httpx.Response: + if ( + request.method == "POST" + and request.url.path == "/pdf-with-imported-form-data" + ): + assert request.url.params["trace"] == "async" + assert request.headers["X-Debug"] == "async" + captured_timeout["value"] = request.extensions.get("timeout") + payload = json.loads(request.content.decode("utf-8")) + assert payload["id"] == str(input_file.id) + assert payload["data_file_id"] == str(data_file.id) + assert payload["note"] == "details" + return httpx.Response( + 200, + json={ + "inputId": [input_file.id], + "outputId": [output_id], + }, + ) + if request.method == "GET" and request.url.path == f"/resource/{output_id}": + assert request.url.params["format"] == "info" + assert request.url.params["trace"] == "async" + assert request.headers["X-Debug"] == "async" + return httpx.Response( + 200, + json=build_file_info_payload( + output_id, + "async-custom.pdf", + "application/pdf", + ), + ) + msg = f"Unexpected request {request.method} {request.url}" + raise AssertionError(msg) + + transport = httpx.MockTransport(handler) + async with AsyncPdfRestClient(api_key=ASYNC_API_KEY, transport=transport) as client: + response = await client.import_form_data( + input_file, + data_file, + extra_query={"trace": "async"}, + extra_headers={"X-Debug": "async"}, + extra_body={"note": "details"}, + timeout=0.73, + ) + + assert isinstance(response, PdfRestFileBasedResponse) + assert response.output_file.name == "async-custom.pdf" + timeout_value = captured_timeout["value"] + assert timeout_value is not None + if isinstance(timeout_value, dict): + assert all( + component == pytest.approx(0.73) for component in timeout_value.values() + ) + else: + assert timeout_value == pytest.approx(0.73) + + +def test_import_form_data_validation(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("PDFREST_API_KEY", raising=False) + pdf_file = make_pdf_file(PdfRestFileID.generate(1)) + data_file = _make_data_file(PdfRestFileID.generate(2)) + png_file = PdfRestFile.model_validate( + build_file_info_payload( + PdfRestFileID.generate(), + "example.png", + "image/png", + ) + ) + transport = httpx.MockTransport(lambda request: (_ for _ in ()).throw(RuntimeError)) + + with ( + PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client, + pytest.raises(ValidationError, match="Must be a PDF file"), + ): + client.import_form_data(png_file, data_file) + + with ( + PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client, + pytest.raises( + ValidationError, + match="Data file must be an XFDF, XDP, XFD, FDF, or XML file", + ), + ): + client.import_form_data(pdf_file, png_file) + + with ( + PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client, + pytest.raises( + ValidationError, match="List should have at most 1 item after validation" + ), + ): + client.import_form_data( + [pdf_file, make_pdf_file(PdfRestFileID.generate())], + data_file, + ) + + with ( + PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client, + pytest.raises( + ValidationError, match="List should have at most 1 item after validation" + ), + ): + client.import_form_data( + pdf_file, + [data_file, _make_data_file(PdfRestFileID.generate())], + ) + + +@pytest.mark.asyncio +async def test_async_import_form_data_validation( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("PDFREST_API_KEY", raising=False) + pdf_file = make_pdf_file(PdfRestFileID.generate(1)) + data_file = _make_data_file(PdfRestFileID.generate(2)) + png_file = PdfRestFile.model_validate( + build_file_info_payload( + PdfRestFileID.generate(), + "example.png", + "image/png", + ) + ) + transport = httpx.MockTransport(lambda request: (_ for _ in ()).throw(RuntimeError)) + + async with AsyncPdfRestClient(api_key=ASYNC_API_KEY, transport=transport) as client: + with pytest.raises(ValidationError, match="Must be a PDF file"): + await client.import_form_data(png_file, data_file) + + async with AsyncPdfRestClient(api_key=ASYNC_API_KEY, transport=transport) as client: + with pytest.raises( + ValidationError, + match="Data file must be an XFDF, XDP, XFD, FDF, or XML file", + ): + await client.import_form_data(pdf_file, png_file) + + async with AsyncPdfRestClient(api_key=ASYNC_API_KEY, transport=transport) as client: + with pytest.raises( + ValidationError, match="List should have at most 1 item after validation" + ): + await client.import_form_data( + [pdf_file, make_pdf_file(PdfRestFileID.generate())], + data_file, + ) + + async with AsyncPdfRestClient(api_key=ASYNC_API_KEY, transport=transport) as client: + with pytest.raises( + ValidationError, match="List should have at most 1 item after validation" + ): + await client.import_form_data( + pdf_file, + [data_file, _make_data_file(PdfRestFileID.generate())], + )