aeat.adapters.inbound.pdf._scrub module

Deterministic PII scrubbing for AEAT filing PDF fixtures.

Real filings carry the operator’s NIF, real amounts, their name, an AEAT-assigned CSV, and occasionally their IBAN / address. Committing any of that to a test corpus is a PII leak.

This module deterministically rewrites a source PDF so the output carries synthetic but structurally faithful replacements – matching the important digit counts, separators, and token shapes enough for extractor tests to exercise their primitive stack without leaking personal data. It is fixture hygiene for contributor-local corpus preparation, not a production anonymisation service.

Scrub rules:

  • NIF (8 digits + 1 letter or 1 letter + 7 digits + 1 letter): replaced with 00000000T (individual) or B00000000 (empresa).

  • Amounts (Spanish 1.234,56 or plain 1234.56): replaced with a seeded-RNG synthetic of matching digit count, preserving formatting.

  • Names (capitalised 2+ word strings AEAT conventionally prints): replaced with DEMO AUTÓNOMO / DEMO EMPRESA.

  • CSV (16-char upper-alphanum): replaced with a hash-derived synthetic.

  • IBAN: replaced with ESXX XXXX XXXX XX XXXXXXXXXX.

  • Dates: preserved (GDPR Art. 4 does not treat calendar dates as PII).

  • Presentation IDs (10+ char upper-alphanum): replaced with seeded synthetic.

Each scrubbed file pairs with a ScrubSidecar JSON sidecar recording the original and scrubbed digests, scrub-rule version, touched field kinds, and consent window. The sidecar is the provenance record reviewers inspect when a private L2 fixture is proposed.

The library is never invoked at runtime from production code — only from contributor-local workflows (just scrub-from-drive) and src/aeat/adapters/inbound/pdf/test_scrub.py tests.

exception ScrubError(message=None, *, context=None, suggestion=None, translated_message=None)[source]

Bases: PdfModeloImportError

Raised when scrubbing cannot produce a safe output.

Parameters:
  • message (str | None)

  • context (Mapping[str, object] | None)

  • suggestion (str | None)

  • translated_message (str | None)

Return type:

None

code: ClassVar[ErrorCode]
class ScrubSidecar(**data)[source]

Bases: BaseModel

Provenance + consent record for one scrubbed L2 fixture file.

The hashes identify the original and scrubbed bytes without storing the cleartext source. scrub_version makes future rule changes auditable, and consent_revocable_until records whether the contributor can later request removal of the fixture.

Parameters:
  • original_sha256 (str)

  • scrubbed_sha256 (str)

  • scrub_version (str)

  • scrubbed_at (datetime)

  • fields_touched (tuple[str, ...])

  • consent_revocable_until (datetime | None)

  • fixture_tier (Literal['l2'])

  • original_filename (str)

original_sha256: str
scrubbed_sha256: str
scrub_version: str
scrubbed_at: datetime
fields_touched: tuple[str, ...]
consent_revocable_until: datetime | None
fixture_tier: Literal['l2']
original_filename: str
scrub_text(text, *, filename)[source]

Apply the scrub-rule set to a text stream; return (scrubbed, fields_touched).

Parameters:
  • text (str) – Raw text extracted from the source PDF.

  • filename (str) – Source filename — used to seed deterministic replacements.

Return type:

tuple[str, tuple[str, ...]]

Returns:

A tuple (scrubbed_text, fields_touched) where fields_touched enumerates the rule kinds that matched at least once.

compute_sidecar(*, original_path, scrubbed_path, fields_touched, consent_revocable_until=None)[source]

Build a ScrubSidecar describing one scrub run.

Both paths are hashed through the shared PDF provenance helper so sidecar generation follows the same redacted-error discipline as runtime parsers.

Return type:

ScrubSidecar

Parameters: