Skip to content

Commit 2523806

Browse files
models: Add validators to handle demo redacted values in PDF metadata
- Introduced `BeforeValidator` to process demo redacted values for various fields in `_internal.py` and `public.py`. - Added `demo_value_sanitizers` module to centralize logic for detecting and replacing demo redacted values with appropriate defaults: - For boolean fields: replace with `True` or `False`. - For integer fields: replace with `0`. - For file IDs: replace with a placeholder UUID. - Applied validators to metadata properties, ensuring consistent handling of demo data input. Assisted-by: Codex
1 parent 2487dcf commit 2523806

3 files changed

Lines changed: 118 additions & 1 deletion

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
import re
5+
from typing import Any
6+
7+
from pydantic import ValidationInfo
8+
9+
LOGGER = logging.getLogger("pdfrest.models")
10+
11+
_DEMO_UUID = "00000000-0000-4000-8000-000000000000"
12+
_REDACTED_X_PATTERN = re.compile(r"^[Xx-]{8,}$")
13+
14+
15+
def _field_name(info: ValidationInfo) -> str:
16+
return info.field_name or "<unknown>"
17+
18+
19+
def _looks_like_demo_redaction(value: Any) -> bool:
20+
if not isinstance(value, str):
21+
return False
22+
if _looks_like_generate_redacted_string(value):
23+
return True
24+
return bool(_REDACTED_X_PATTERN.fullmatch(value))
25+
26+
27+
def _looks_like_generate_redacted_string(value: str) -> bool:
28+
"""Detect strings redacted by PDFCloud-API generateRedactedString.
29+
30+
The upstream redactor preserves the first two characters and replaces all
31+
non-whitespace characters after that with '*'.
32+
"""
33+
if len(value) < 3:
34+
return False
35+
tail = value[2:]
36+
if "*" not in tail:
37+
return False
38+
return all(char == "*" or char.isspace() for char in tail)
39+
40+
41+
def _log_replacement(original: Any, replacement: Any, info: ValidationInfo) -> None:
42+
LOGGER.warning(
43+
"Demo value %s detected in %s; replaced with %s",
44+
original,
45+
_field_name(info),
46+
replacement,
47+
)
48+
49+
50+
def _demo_bool_or_passthrough(
51+
value: Any, info: ValidationInfo, *, replacement: bool
52+
) -> Any:
53+
if value is None or isinstance(value, bool):
54+
return value
55+
if _looks_like_demo_redaction(value):
56+
_log_replacement(value, replacement, info)
57+
return replacement
58+
return value
59+
60+
61+
def demo_bool_false_or_passthrough(value: Any, info: ValidationInfo) -> Any:
62+
return _demo_bool_or_passthrough(value, info, replacement=False)
63+
64+
65+
def demo_bool_true_or_passthrough(value: Any, info: ValidationInfo) -> Any:
66+
return _demo_bool_or_passthrough(value, info, replacement=True)
67+
68+
69+
def demo_file_id_or_passthrough(value: Any, info: ValidationInfo) -> Any:
70+
if value is None:
71+
return value
72+
if _looks_like_demo_redaction(value):
73+
replacement = _DEMO_UUID
74+
_log_replacement(value, replacement, info)
75+
return replacement
76+
return value
77+
78+
79+
def demo_int_or_passthrough(value: Any, info: ValidationInfo) -> Any:
80+
if value is None or isinstance(value, int):
81+
return value
82+
if _looks_like_demo_redaction(value):
83+
replacement = 0
84+
_log_replacement(value, replacement, info)
85+
return replacement
86+
return value

src/pdfrest/models/_internal.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
WatermarkHorizontalAlignment,
4646
WatermarkVerticalAlignment,
4747
)
48+
from ._demo_value_sanitizers import demo_file_id_or_passthrough
4849
from .public import PdfRestFile, PdfRestFileID
4950

5051
PdfConvertColorProfile = PdfPresetColorProfile | Literal["custom"]
@@ -2584,7 +2585,11 @@ class PdfRestRawUploadedFile(BaseModel):
25842585
"""
25852586

25862587
name: Annotated[str, Field(description="The name of the file")]
2587-
id: Annotated[PdfRestFileID, Field(description="The id of the file")]
2588+
id: Annotated[
2589+
PdfRestFileID,
2590+
BeforeValidator(demo_file_id_or_passthrough),
2591+
Field(description="The id of the file"),
2592+
]
25882593
output_url: Annotated[
25892594
list[HttpUrl] | HttpUrl | None,
25902595
Field(description="The url of the unzipped file", alias="outputUrl"),

src/pdfrest/models/public.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
AliasChoices,
1212
AwareDatetime,
1313
BaseModel,
14+
BeforeValidator,
1415
ConfigDict,
1516
Field,
1617
HttpUrl,
@@ -20,6 +21,12 @@
2021
from pydantic_core import CoreSchema
2122
from typing_extensions import override
2223

24+
from ._demo_value_sanitizers import (
25+
demo_bool_false_or_passthrough,
26+
demo_bool_true_or_passthrough,
27+
demo_int_or_passthrough,
28+
)
29+
2330
__all__ = (
2431
"ExtractTextResponse",
2532
"ExtractedTextDocument",
@@ -851,6 +858,7 @@ class PdfRestInfoResponse(BaseModel):
851858

852859
tagged: Annotated[
853860
bool | None,
861+
BeforeValidator(demo_bool_false_or_passthrough),
854862
Field(
855863
description="Indicates whether structure tags are present in the PDF "
856864
"document. The result is true or false."
@@ -860,6 +868,7 @@ class PdfRestInfoResponse(BaseModel):
860868

861869
image_only: Annotated[
862870
bool | None,
871+
BeforeValidator(demo_bool_false_or_passthrough),
863872
Field(
864873
description=(
865874
"Indicates whether the document is 'image only,' meaning it consists "
@@ -995,6 +1004,7 @@ class PdfRestInfoResponse(BaseModel):
9951004

9961005
contains_annotations: Annotated[
9971006
bool | None,
1007+
BeforeValidator(demo_bool_false_or_passthrough),
9981008
Field(
9991009
description=(
10001010
"Indicates whether the PDF document contains annotations such as "
@@ -1007,6 +1017,7 @@ class PdfRestInfoResponse(BaseModel):
10071017

10081018
contains_signature: Annotated[
10091019
bool | None,
1020+
BeforeValidator(demo_bool_false_or_passthrough),
10101021
Field(
10111022
description="Indicates whether the PDF contains any digital signatures. "
10121023
"The result is true or false."
@@ -1028,6 +1039,7 @@ class PdfRestInfoResponse(BaseModel):
10281039

10291040
file_size: Annotated[
10301041
int | None,
1042+
BeforeValidator(demo_int_or_passthrough),
10311043
Field(
10321044
description="The size of the PDF file in bytes. The result is an integer."
10331045
),
@@ -1042,6 +1054,7 @@ class PdfRestInfoResponse(BaseModel):
10421054

10431055
restrict_permissions_set: Annotated[
10441056
bool | None,
1057+
BeforeValidator(demo_bool_false_or_passthrough),
10451058
Field(
10461059
description=(
10471060
"Indicates whether the PDF file has restricted permissions, such as "
@@ -1054,6 +1067,7 @@ class PdfRestInfoResponse(BaseModel):
10541067

10551068
contains_xfa: Annotated[
10561069
bool | None,
1070+
BeforeValidator(demo_bool_false_or_passthrough),
10571071
Field(
10581072
description="Indicates whether the PDF contains XFA forms. The result is "
10591073
"true or false."
@@ -1063,6 +1077,7 @@ class PdfRestInfoResponse(BaseModel):
10631077

10641078
contains_acroforms: Annotated[
10651079
bool | None,
1080+
BeforeValidator(demo_bool_false_or_passthrough),
10661081
Field(
10671082
description="Indicates whether the PDF contains Acroforms. The result is "
10681083
"true or false."
@@ -1072,6 +1087,7 @@ class PdfRestInfoResponse(BaseModel):
10721087

10731088
contains_javascript: Annotated[
10741089
bool | None,
1090+
BeforeValidator(demo_bool_false_or_passthrough),
10751091
Field(
10761092
description="Indicates whether the PDF contains JavaScript. The result is "
10771093
"true or false."
@@ -1081,6 +1097,7 @@ class PdfRestInfoResponse(BaseModel):
10811097

10821098
contains_transparency: Annotated[
10831099
bool | None,
1100+
BeforeValidator(demo_bool_false_or_passthrough),
10841101
Field(
10851102
description="Indicates whether the PDF contains transparent objects. The "
10861103
"result is true or false."
@@ -1090,6 +1107,7 @@ class PdfRestInfoResponse(BaseModel):
10901107

10911108
contains_embedded_file: Annotated[
10921109
bool | None,
1110+
BeforeValidator(demo_bool_false_or_passthrough),
10931111
Field(
10941112
description="Indicates whether the PDF contains one or more embedded "
10951113
"files. The result is true or false."
@@ -1099,6 +1117,7 @@ class PdfRestInfoResponse(BaseModel):
10991117

11001118
uses_embedded_fonts: Annotated[
11011119
bool | None,
1120+
BeforeValidator(demo_bool_false_or_passthrough),
11021121
Field(
11031122
description="Indicates whether the PDF contains fully embedded fonts. "
11041123
"The result is true or false."
@@ -1108,6 +1127,7 @@ class PdfRestInfoResponse(BaseModel):
11081127

11091128
uses_nonembedded_fonts: Annotated[
11101129
bool | None,
1130+
BeforeValidator(demo_bool_false_or_passthrough),
11111131
Field(
11121132
description="Indicates whether the PDF contains non-embedded fonts. The "
11131133
"result is true or false."
@@ -1117,6 +1137,7 @@ class PdfRestInfoResponse(BaseModel):
11171137

11181138
pdfa: Annotated[
11191139
bool | None,
1140+
BeforeValidator(demo_bool_false_or_passthrough),
11201141
Field(
11211142
description="Indicates whether the document conforms to the PDF/A "
11221143
"standard. The result is true or false."
@@ -1126,6 +1147,7 @@ class PdfRestInfoResponse(BaseModel):
11261147

11271148
pdfua_claim: Annotated[
11281149
bool | None,
1150+
BeforeValidator(demo_bool_false_or_passthrough),
11291151
Field(
11301152
description="Indicates whether the document claims to conform to the "
11311153
"PDF/UA standard. The result is true or false."
@@ -1135,6 +1157,7 @@ class PdfRestInfoResponse(BaseModel):
11351157

11361158
pdfe_claim: Annotated[
11371159
bool | None,
1160+
BeforeValidator(demo_bool_false_or_passthrough),
11381161
Field(
11391162
description="Indicates whether the document claims to conform to the "
11401163
"PDF/E standard. The result is true or false."
@@ -1144,6 +1167,7 @@ class PdfRestInfoResponse(BaseModel):
11441167

11451168
pdfx_claim: Annotated[
11461169
bool | None,
1170+
BeforeValidator(demo_bool_false_or_passthrough),
11471171
Field(
11481172
description="Indicates whether the document claims to conform to the "
11491173
"PDF/X standard. The result is true or false."
@@ -1153,6 +1177,7 @@ class PdfRestInfoResponse(BaseModel):
11531177

11541178
requires_password_to_open: Annotated[
11551179
bool | None,
1180+
BeforeValidator(demo_bool_false_or_passthrough),
11561181
Field(
11571182
description=(
11581183
"Indicates whether the PDF requires a password to open. The result "
@@ -1165,6 +1190,7 @@ class PdfRestInfoResponse(BaseModel):
11651190

11661191
all_queries_processed: Annotated[
11671192
bool,
1193+
BeforeValidator(demo_bool_true_or_passthrough),
11681194
Field(
11691195
validation_alias=AliasChoices(
11701196
"all_queries_processed", "allQueriesProcessed"

0 commit comments

Comments
 (0)