Skip to content

PDFCLOUD-5556 Add client methods for digital signatures in PDF#21

Closed
datalogics-cgreen wants to merge 3 commits intopdfrest:mainfrom
datalogics-cgreen:pdfcloud-5464-sign
Closed

PDFCLOUD-5556 Add client methods for digital signatures in PDF#21
datalogics-cgreen wants to merge 3 commits intopdfrest:mainfrom
datalogics-cgreen:pdfcloud-5464-sign

Conversation

@datalogics-cgreen
Copy link
Copy Markdown
Contributor

@datalogics-cgreen datalogics-cgreen commented Jan 21, 2026

Adds client support for the Sign PDF endpoint, including new payload models and public types in the pdfrest package.

Adds comprehensive Sign PDF tests (unit + live) and supporting test fixtures/resources for signing credentials and assets.

@datalogics-cgreen datalogics-cgreen changed the title PDFCLOUD-5464 sign PDFCLOUD-5556 Add client methods for digital signatures in PDF Jan 30, 2026
Copy link
Copy Markdown
Contributor

@datalogics-kam datalogics-kam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes

  • Proper use of Pydantic dumping
  • Bool | none code smell
  • Mismatch between payload optionality vs typing of the public interface
  • Validation should be done in Pydantic models, not client.py.

def _serialize_signature_configuration(
value: _PdfSignatureConfigurationModel,
) -> str:
payload = value.model_dump(mode="json", exclude_none=True)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use model_dump_json instead of model_dump followed by json.dumps.

Comment on lines +591 to +592
x: str | int | float
y: str | int | float
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use float.

Comment on lines +125 to +126
x: str | int | float
y: str | int | float
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use float as a type here.

class PdfSignatureLocation(TypedDict):
bottom_left: Required[PdfSignaturePoint]
top_right: Required[PdfSignaturePoint]
page: Required[str | int]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering...can this still be typed...maybe a Pydantic annotation won't help on the int but probably that str is a Literal really.

Comment thread src/pdfrest/client.py Outdated
Comment on lines +4346 to +4365
if "pfx" in credential_map and "certificate" in credential_map:
msg = "Provide either PFX credentials or certificate/private_key, not both."
raise PdfRestConfigurationError(msg)
if "pfx" not in credential_map and "certificate" not in credential_map:
msg = "Either PFX credentials (pfx + passphrase) or certificate/private_key credentials are required."
raise PdfRestConfigurationError(msg)
if "pfx" in credential_map:
payload["pfx_credential"] = credential_map["pfx"]
try:
payload["pfx_passphrase"] = credential_map["passphrase"]
except KeyError as exc:
msg = "PFX credentials require both 'pfx' and 'passphrase' files."
raise PdfRestConfigurationError(msg) from exc
elif "certificate" in credential_map:
payload["certificate"] = credential_map["certificate"]
try:
payload["private_key"] = credential_map["private_key"]
except KeyError as exc:
msg = "Certificate credentials require both 'certificate' and 'private_key' files."
raise PdfRestConfigurationError(msg) from exc
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All this should be on a Payload validator.

Suggested change
if "pfx" in credential_map and "certificate" in credential_map:
msg = "Provide either PFX credentials or certificate/private_key, not both."
raise PdfRestConfigurationError(msg)
if "pfx" not in credential_map and "certificate" not in credential_map:
msg = "Either PFX credentials (pfx + passphrase) or certificate/private_key credentials are required."
raise PdfRestConfigurationError(msg)
if "pfx" in credential_map:
payload["pfx_credential"] = credential_map["pfx"]
try:
payload["pfx_passphrase"] = credential_map["passphrase"]
except KeyError as exc:
msg = "PFX credentials require both 'pfx' and 'passphrase' files."
raise PdfRestConfigurationError(msg) from exc
elif "certificate" in credential_map:
payload["certificate"] = credential_map["certificate"]
try:
payload["private_key"] = credential_map["private_key"]
except KeyError as exc:
msg = "Certificate credentials require both 'certificate' and 'private_key' files."
raise PdfRestConfigurationError(msg) from exc
# Move to payload validator

Comment thread src/pdfrest/client.py Outdated
Comment on lines +3001 to +3020
if "pfx" in credential_map and "certificate" in credential_map:
msg = "Provide either PFX credentials or certificate/private_key, not both."
raise PdfRestConfigurationError(msg)
if "pfx" not in credential_map and "certificate" not in credential_map:
msg = "Either PFX credentials (pfx + passphrase) or certificate/private_key credentials are required."
raise PdfRestConfigurationError(msg)
if "pfx" in credential_map:
payload["pfx_credential"] = credential_map["pfx"]
try:
payload["pfx_passphrase"] = credential_map["passphrase"]
except KeyError as exc:
msg = "PFX credentials require both 'pfx' and 'passphrase' files."
raise PdfRestConfigurationError(msg) from exc
elif "certificate" in credential_map:
payload["certificate"] = credential_map["certificate"]
try:
payload["private_key"] = credential_map["private_key"]
except KeyError as exc:
msg = "Certificate credentials require both 'certificate' and 'private_key' files."
raise PdfRestConfigurationError(msg) from exc
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All this should be done by validating the payload, which can have optional fields, and check them either before or after the whole class is validated (I think before is for data mods, after is better for validity)

Not least because it duplicates 21 lines of complicated code.

Suggested change
if "pfx" in credential_map and "certificate" in credential_map:
msg = "Provide either PFX credentials or certificate/private_key, not both."
raise PdfRestConfigurationError(msg)
if "pfx" not in credential_map and "certificate" not in credential_map:
msg = "Either PFX credentials (pfx + passphrase) or certificate/private_key credentials are required."
raise PdfRestConfigurationError(msg)
if "pfx" in credential_map:
payload["pfx_credential"] = credential_map["pfx"]
try:
payload["pfx_passphrase"] = credential_map["passphrase"]
except KeyError as exc:
msg = "PFX credentials require both 'pfx' and 'passphrase' files."
raise PdfRestConfigurationError(msg) from exc
elif "certificate" in credential_map:
payload["certificate"] = credential_map["certificate"]
try:
payload["private_key"] = credential_map["private_key"]
except KeyError as exc:
msg = "Certificate credentials require both 'certificate' and 'private_key' files."
raise PdfRestConfigurationError(msg) from exc
# Move to payload validator


class _PdfSignatureDisplayModel(BaseModel):
include_distinguished_name: bool | None = None
include_datetime: bool | None = None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these bools really optional? Optional bool can be confusing...and the TypedDict doesn't have optional values. This is the part that validates, and makes sure that at runtime, the TypedDict got passed the correct values.

type checking -> IDE time
validation -> runtime

Comment on lines +604 to +607
contact: str | None = None
location: str | None = None
name: str | None = None
reason: str | None = None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the types on PdfSignatureDisplay, these are not optional. They should validate how they're typed on the public interface.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are meant to be optional. If they are not wanted, then they should be omitted from the request.


class _PdfSignatureConfigurationModel(BaseModel):
type: Literal["new"]
name: str | None = None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the public interface says this isn't optional...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's required.

Comment thread src/pdfrest/client.py
timeout=timeout,
)

def sign_pdf(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since I was on the topic of "things we split into separate functions for clarity", pdfAssistant has

pdfassistant_chatbot/functions/signed_pdf.py
255:async def digitally_sign_pdf_with_pfx(
362:async def digitally_sign_pdf_with_non_pfx(

...should that idea be surfaced here? Just trying to keep the interface grounded.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what I have now done with Watermark PDF and what I'm currently working on for /pdf.

@datalogics-kam
Copy link
Copy Markdown
Contributor

Replaced by #26 with some additional fixes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants