Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .agents/skills/pdfrest-client-api/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,15 @@ body to the current API contract.
- Do not change positional meaning, narrow accepted types, alter established
defaults, remove arguments, or change response shapes as part of an API
adaptation.
- Treat every existing public export as part of the compatibility surface, not
only client methods. Preserve public type aliases at runtime and under static
type checking: equality, `typing.get_origin()`, `typing.get_args()`,
annotations, and assignability must remain equivalent to the base branch.
Reusing or composing a public type is acceptable only when those observable
contracts remain unchanged.
- Run the existing tests for unrelated services unchanged. If a new endpoint
makes one fail, fix the compatibility break instead of rewriting that test to
accept the regression. Add focused tests for the new behavior separately.
- If the server requires a breaking SDK change and no faithful compatible
translation/default exists, stop and ask the user for a migration decision
before editing.
Expand Down Expand Up @@ -385,6 +394,10 @@ documentation with its types; do not hand-copy a shape schema into Markdown.
placement and an `Accepted values:` list whose bullets document each literal
spelling and meaning. Inspect generated HTML to confirm the list renders with
the alias rather than only in the source file.
- Make every public literal's rendered documentation self-contained. When an
alias reuses or extends another literal, expand the complete applicable list
at that alias instead of directing readers to combine values from another
entry.
- Public types are re-exported through `pdfrest.types`. Verify that the rendered
reference resolves the re-export to its source union and member types. A
self-reference such as `PdfAddShapeObject = PdfAddShapeObject` is a rendering
Expand All @@ -404,3 +417,7 @@ Run the full pytest suite and `uvx nox -s tests` when practical. For API
reference changes, also run the strict docs build. Run the focused example and
the example matrix as described above. Report the OpenAPI operation inspected,
files changed, checks run, checks skipped, and any live-validation limitation.
Before completion, compare every touched pre-existing public interface with the
base branch at runtime and with the type checker, run its original unrelated
tests without adaptations, and inspect rendered HTML for complete literal value
lists rather than relying on a successful documentation build alone.
9 changes: 9 additions & 0 deletions docs/api-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@ Use this group for password protection, permissions, signatures, and standards.
- Archival/print conformance:
[convert_to_pdfa][pdfrest.PdfRestClient.convert_to_pdfa],
[convert_to_pdfx][pdfrest.PdfRestClient.convert_to_pdfx]
- Electronic invoices:
[convert_invoice_xml_to_zugferd_pdf][pdfrest.PdfRestClient.convert_invoice_xml_to_zugferd_pdf]
generates a PDF/A-3 invoice from CII XML, while
[convert_pdf_to_zugferd_pdf][pdfrest.PdfRestClient.convert_pdf_to_zugferd_pdf]
embeds CII XML into an existing matching PDF. The latter preserves the PDF by
default; set `allow_regeneration=True` only when the service may replace a
mismatched PDF with a newly rendered invoice.

## PDF cleanup and rendering normalization

Expand All @@ -130,6 +137,8 @@ Use this group when the user starts with non-PDF content or needs downstream
formats.

- Into PDF:
[convert_invoice_xml_to_zugferd_pdf][pdfrest.PdfRestClient.convert_invoice_xml_to_zugferd_pdf],
[convert_pdf_to_zugferd_pdf][pdfrest.PdfRestClient.convert_pdf_to_zugferd_pdf],
[convert_office_to_pdf][pdfrest.PdfRestClient.convert_office_to_pdf],
[convert_postscript_to_pdf][pdfrest.PdfRestClient.convert_postscript_to_pdf],
[convert_email_to_pdf][pdfrest.PdfRestClient.convert_email_to_pdf],
Expand Down
5 changes: 5 additions & 0 deletions docs/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,11 @@ to the API guide and API reference for exact method names and options.
- Apply an uploaded `.joboptions` Distiller profile when converting PostScript
or EPS with `convert_postscript_to_pdf`
- Convert to standards formats (`PDF/A`, `PDF/X`)
- Create validated ZUGFeRD or Factur-X PDF/A-3 invoices from uploaded CII XML
with ``convert_invoice_xml_to_zugferd_pdf`` and optional logo/render controls
- Embed CII XML into a matching PDF with ``convert_pdf_to_zugferd_pdf``;
preserve the supplied PDF by default or explicitly permit regeneration when
the PDF and invoice do not match

### Extraction and Analysis

Expand Down
5 changes: 5 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ supported interpreter matrix.
synchronously convert selected PDF pages and verify the PostScript output.
- `examples/convert_pdf_to_postscript/async_convert_pdf_to_postscript_example.py`
– asynchronously convert a PDF page and verify the PostScript output.
- `examples/convert_invoice_xml_to_zugferd_pdf/convert_invoice_xml_to_zugferd_pdf_example.py`
– create a formatted ZUGFeRD PDF/A-3 invoice from uploaded CII XML.
- `examples/convert_pdf_to_zugferd_pdf/convert_pdf_to_zugferd_pdf_example.py` –
combine an uploaded PDF with CII XML and explicitly permit replacement when
the visual PDF does not match the invoice.
- `examples/delete/delete_example.py` – demonstrate file deletion (sync + async
variants).
- `examples/extract_text/extract_pdf_text_example.py` – run `extract_pdf_text`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# /// script
# requires-python = ">=3.10"
# dependencies = ["pdfrest", "python-dotenv"]
# ///
"""Create a branded ZUGFeRD PDF/A-3 invoice from CII XML.

This sample demonstrates how to:

1. Upload the checked-in invoice XML resource.
2. Generate a ZUGFeRD PDF with typed appearance and locale options.
3. Download and verify the generated PDF.
4. Print the result metadata and input resource IDs.

Set ``PDFREST_API_KEY`` and optionally ``PDFREST_BASE_URL`` for a self-hosted
deployment, then run from the repository root with
``uv run examples/convert_invoice_xml_to_zugferd_pdf/convert_invoice_xml_to_zugferd_pdf_example.py``.
The required XML input is included in the repository.
"""

from __future__ import annotations

import os
from pathlib import Path

from dotenv import load_dotenv

from pdfrest import PdfRestClient
from pdfrest.types import PdfZugferdRenderOptions

RESOURCE_DIRECTORY = Path(__file__).resolve().parents[1] / "resources"
INVOICE_PATH = RESOURCE_DIRECTORY / "zugferd-minimum-invoice.xml"


def convert_invoice_xml_to_zugferd_pdf() -> None:
"""Upload invoice XML, create the PDF/A-3 invoice, and verify it."""
load_dotenv()
base_url = os.getenv("PDFREST_BASE_URL")
render_options = PdfZugferdRenderOptions(
font="courier",
bold_font="courierbold",
accent_color=(20, 64, 115),
locale="de-DE",
label_language="en",
currency_display="symbol",
date_format="dd.MM.yyyy",
decimal_precision=2,
footer_message="Thank you for your business.",
)

with PdfRestClient(base_url=base_url) as client:
invoice = client.files.create_from_paths(INVOICE_PATH)[0]
response = client.convert_invoice_xml_to_zugferd_pdf(
invoice,
render_options=render_options,
output="zugferd-invoice",
)
pdf_bytes = client.files.read_bytes(response.output_file)

if not pdf_bytes.startswith(b"%PDF-"):
msg = "The generated resource is not a PDF."
raise RuntimeError(msg)

output = response.output_file
print(f"Created: {output.name}")
print(f"MIME type: {output.type}")
print(f"Size: {output.size} bytes")
print(f"Input IDs: {', '.join(response.input_ids)}")
print(f"Download URL: {output.url}")


if __name__ == "__main__": # pragma: no cover - manual example
convert_invoice_xml_to_zugferd_pdf()
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# /// script
# requires-python = ">=3.10"
# dependencies = ["pdfrest", "python-dotenv"]
# ///
"""Create a ZUGFeRD PDF/A-3 invoice from a visual PDF and CII XML.

This sample demonstrates how to:

1. Upload the checked-in PDF and invoice XML resources.
2. Combine them while explicitly allowing a regenerated visual PDF when the
supplied PDF does not match the invoice data.
3. Download and verify the resulting ZUGFeRD PDF.
4. Print the result metadata and input resource IDs.

Set ``PDFREST_API_KEY`` and optionally ``PDFREST_BASE_URL`` for a self-hosted
deployment, then run from the repository root with
``uv run examples/convert_pdf_to_zugferd_pdf/convert_pdf_to_zugferd_pdf_example.py``.
The required inputs are included in the repository.
"""

from __future__ import annotations

import os
from pathlib import Path

from dotenv import load_dotenv

from pdfrest import PdfRestClient

RESOURCE_DIRECTORY = Path(__file__).resolve().parents[1] / "resources"
PDF_PATH = RESOURCE_DIRECTORY / "report.pdf"
INVOICE_PATH = RESOURCE_DIRECTORY / "zugferd-minimum-invoice.xml"


def convert_pdf_to_zugferd_pdf() -> None:
"""Upload PDF/XML inputs, create the PDF/A-3 invoice, and verify it."""
load_dotenv()
base_url = os.getenv("PDFREST_BASE_URL")

with PdfRestClient(base_url=base_url) as client:
pdf, invoice = client.files.create_from_paths([PDF_PATH, INVOICE_PATH])
response = client.convert_pdf_to_zugferd_pdf(
pdf,
invoice=invoice,
allow_regeneration=True,
output="zugferd-from-pdf",
)
pdf_bytes = client.files.read_bytes(response.output_file)

if not pdf_bytes.startswith(b"%PDF-"):
msg = "The generated resource is not a PDF."
raise RuntimeError(msg)

output = response.output_file
print(f"Created: {output.name}")
print(f"MIME type: {output.type}")
print(f"Size: {output.size} bytes")
print(f"Input IDs: {', '.join(response.input_ids)}")
print(f"Download URL: {output.url}")


if __name__ == "__main__": # pragma: no cover - manual example
convert_pdf_to_zugferd_pdf()
51 changes: 51 additions & 0 deletions examples/resources/zugferd-minimum-invoice.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Reduced from the official ZUGFeRD 2.4 MINIMUM_Rechnung.xml example.
This deterministic fixture exercises the smallest supported invoice profile.
-->
<rsm:CrossIndustryInvoice
xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
<rsm:ExchangedDocumentContext>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID>urn:factur-x.eu:1p0:minimum</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<ram:ID>471102</ram:ID>
<ram:TypeCode>380</ram:TypeCode>
<ram:IssueDateTime>
<udt:DateTimeString format="102">20200305</udt:DateTimeString>
</ram:IssueDateTime>
</rsm:ExchangedDocument>
<rsm:SupplyChainTradeTransaction>
<ram:ApplicableHeaderTradeAgreement>
<ram:SellerTradeParty>
<ram:Name>Lieferant GmbH</ram:Name>
<ram:PostalTradeAddress>
<ram:CountryID>DE</ram:CountryID>
</ram:PostalTradeAddress>
<ram:SpecifiedTaxRegistration>
<ram:ID schemeID="FC">201/113/40209</ram:ID>
</ram:SpecifiedTaxRegistration>
<ram:SpecifiedTaxRegistration>
<ram:ID schemeID="VA">DE123456789</ram:ID>
</ram:SpecifiedTaxRegistration>
</ram:SellerTradeParty>
<ram:BuyerTradeParty>
<ram:Name>Kunden AG Frankreich</ram:Name>
</ram:BuyerTradeParty>
</ram:ApplicableHeaderTradeAgreement>
<ram:ApplicableHeaderTradeDelivery/>
<ram:ApplicableHeaderTradeSettlement>
<ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
<ram:TaxBasisTotalAmount>198.00</ram:TaxBasisTotalAmount>
<ram:TaxTotalAmount currencyID="EUR">37.62</ram:TaxTotalAmount>
<ram:GrandTotalAmount>235.62</ram:GrandTotalAmount>
<ram:DuePayableAmount>235.62</ram:DuePayableAmount>
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
</ram:ApplicableHeaderTradeSettlement>
</rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "pdfrest"
version = "1.4.1"
version = "1.5.0"
description = "Python client library for interacting with the pdfRest API"
readme = { file = "README.md", content-type = "text/markdown" }
license = "Apache-2.0"
Expand Down
Loading
Loading