Skip to content

Commit 9f0da25

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 25cf8f0 commit 9f0da25

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"]
@@ -2581,7 +2582,11 @@ class PdfRestRawUploadedFile(BaseModel):
25812582
with outputUrl"""
25822583

25832584
name: Annotated[str, Field(description="The name of the file")]
2584-
id: Annotated[PdfRestFileID, Field(description="The id of the file")]
2585+
id: Annotated[
2586+
PdfRestFileID,
2587+
BeforeValidator(demo_file_id_or_passthrough),
2588+
Field(description="The id of the file"),
2589+
]
25852590
output_url: Annotated[
25862591
list[HttpUrl] | HttpUrl | None,
25872592
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",
@@ -730,13 +737,15 @@ class PdfRestInfoResponse(BaseModel):
730737
]
731738
tagged: Annotated[
732739
bool | None,
740+
BeforeValidator(demo_bool_false_or_passthrough),
733741
Field(
734742
description="Indicates whether structure tags are present in the PDF "
735743
"document. The result is true or false."
736744
),
737745
] = None
738746
image_only: Annotated[
739747
bool | None,
748+
BeforeValidator(demo_bool_false_or_passthrough),
740749
Field(
741750
description=(
742751
"Indicates whether the document is 'image only,' meaning it consists "
@@ -848,6 +857,7 @@ class PdfRestInfoResponse(BaseModel):
848857
] = None
849858
contains_annotations: Annotated[
850859
bool | None,
860+
BeforeValidator(demo_bool_false_or_passthrough),
851861
Field(
852862
description=(
853863
"Indicates whether the PDF document contains annotations such as "
@@ -858,6 +868,7 @@ class PdfRestInfoResponse(BaseModel):
858868
] = None
859869
contains_signature: Annotated[
860870
bool | None,
871+
BeforeValidator(demo_bool_false_or_passthrough),
861872
Field(
862873
description="Indicates whether the PDF contains any digital signatures. "
863874
"The result is true or false."
@@ -875,6 +886,7 @@ class PdfRestInfoResponse(BaseModel):
875886
] = None
876887
file_size: Annotated[
877888
int | None,
889+
BeforeValidator(demo_int_or_passthrough),
878890
Field(
879891
description="The size of the PDF file in bytes. The result is an integer."
880892
),
@@ -885,6 +897,7 @@ class PdfRestInfoResponse(BaseModel):
885897
] = None
886898
restrict_permissions_set: Annotated[
887899
bool | None,
900+
BeforeValidator(demo_bool_false_or_passthrough),
888901
Field(
889902
description=(
890903
"Indicates whether the PDF file has restricted permissions, such as "
@@ -895,83 +908,95 @@ class PdfRestInfoResponse(BaseModel):
895908
] = None
896909
contains_xfa: Annotated[
897910
bool | None,
911+
BeforeValidator(demo_bool_false_or_passthrough),
898912
Field(
899913
description="Indicates whether the PDF contains XFA forms. The result is "
900914
"true or false."
901915
),
902916
] = None
903917
contains_acroforms: Annotated[
904918
bool | None,
919+
BeforeValidator(demo_bool_false_or_passthrough),
905920
Field(
906921
description="Indicates whether the PDF contains Acroforms. The result is "
907922
"true or false."
908923
),
909924
] = None
910925
contains_javascript: Annotated[
911926
bool | None,
927+
BeforeValidator(demo_bool_false_or_passthrough),
912928
Field(
913929
description="Indicates whether the PDF contains JavaScript. The result is "
914930
"true or false."
915931
),
916932
] = None
917933
contains_transparency: Annotated[
918934
bool | None,
935+
BeforeValidator(demo_bool_false_or_passthrough),
919936
Field(
920937
description="Indicates whether the PDF contains transparent objects. The "
921938
"result is true or false."
922939
),
923940
] = None
924941
contains_embedded_file: Annotated[
925942
bool | None,
943+
BeforeValidator(demo_bool_false_or_passthrough),
926944
Field(
927945
description="Indicates whether the PDF contains one or more embedded "
928946
"files. The result is true or false."
929947
),
930948
] = None
931949
uses_embedded_fonts: Annotated[
932950
bool | None,
951+
BeforeValidator(demo_bool_false_or_passthrough),
933952
Field(
934953
description="Indicates whether the PDF contains fully embedded fonts. "
935954
"The result is true or false."
936955
),
937956
] = None
938957
uses_nonembedded_fonts: Annotated[
939958
bool | None,
959+
BeforeValidator(demo_bool_false_or_passthrough),
940960
Field(
941961
description="Indicates whether the PDF contains non-embedded fonts. The "
942962
"result is true or false."
943963
),
944964
] = None
945965
pdfa: Annotated[
946966
bool | None,
967+
BeforeValidator(demo_bool_false_or_passthrough),
947968
Field(
948969
description="Indicates whether the document conforms to the PDF/A "
949970
"standard. The result is true or false."
950971
),
951972
] = None
952973
pdfua_claim: Annotated[
953974
bool | None,
975+
BeforeValidator(demo_bool_false_or_passthrough),
954976
Field(
955977
description="Indicates whether the document claims to conform to the "
956978
"PDF/UA standard. The result is true or false."
957979
),
958980
] = None
959981
pdfe_claim: Annotated[
960982
bool | None,
983+
BeforeValidator(demo_bool_false_or_passthrough),
961984
Field(
962985
description="Indicates whether the document claims to conform to the "
963986
"PDF/E standard. The result is true or false."
964987
),
965988
] = None
966989
pdfx_claim: Annotated[
967990
bool | None,
991+
BeforeValidator(demo_bool_false_or_passthrough),
968992
Field(
969993
description="Indicates whether the document claims to conform to the "
970994
"PDF/X standard. The result is true or false."
971995
),
972996
] = None
973997
requires_password_to_open: Annotated[
974998
bool | None,
999+
BeforeValidator(demo_bool_false_or_passthrough),
9751000
Field(
9761001
description=(
9771002
"Indicates whether the PDF requires a password to open. The result "
@@ -982,6 +1007,7 @@ class PdfRestInfoResponse(BaseModel):
9821007
] = None
9831008
all_queries_processed: Annotated[
9841009
bool,
1010+
BeforeValidator(demo_bool_true_or_passthrough),
9851011
Field(
9861012
validation_alias=AliasChoices(
9871013
"all_queries_processed", "allQueriesProcessed"

0 commit comments

Comments
 (0)