pseudonymize 0.6.0__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.agents/PRIVATE_RELEASE_PLAN.md +9 -9
  2. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.gitignore +1 -0
  3. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/CHANGELOG.md +28 -0
  4. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/PKG-INFO +2 -2
  5. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/README.md +1 -1
  6. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/ROADMAP.md +39 -33
  7. pseudonymize-0.7.0/benchmarks/evaluate_quality.py +154 -0
  8. pseudonymize-0.7.0/docs/detectors.md +12 -0
  9. pseudonymize-0.7.0/docs/quality_benchmarks.md +27 -0
  10. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/mkdocs.yml +1 -0
  11. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/pyproject.toml +4 -1
  12. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/backends/rules.py +2 -0
  13. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/cli.py +18 -1
  14. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/detectors/iban.py +4 -1
  15. pseudonymize-0.7.0/src/pseudonymize/detectors/italian.py +122 -0
  16. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/detectors/registry.py +3 -0
  17. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/formats.py +3 -0
  18. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/inspection/pdf.py +116 -5
  19. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/normalization.py +2 -2
  20. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/policy.py +2 -0
  21. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/result.py +2 -0
  22. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/spans.py +2 -0
  23. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/compatibility/test_b1_contract.py +2 -0
  24. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/integration/test_inspection_adapters.py +149 -0
  25. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/backends/test_onnx.py +2 -1
  26. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/detectors/test_detectors.py +38 -0
  27. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/test_cli_coverage.py +41 -5
  28. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/test_inspection.py +15 -1
  29. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/test_normalization.py +2 -0
  30. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/test_policy.py +2 -0
  31. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/uv.lock +953 -12
  32. pseudonymize-0.6.0/docs/detectors.md +0 -8
  33. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.editorconfig +0 -0
  34. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.gitattributes +0 -0
  35. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.github/CODEOWNERS +0 -0
  36. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.github/ISSUE_TEMPLATE/bug.yml +0 -0
  37. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  38. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.github/ISSUE_TEMPLATE/detector-request.yml +0 -0
  39. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.github/ISSUE_TEMPLATE/feature.yml +0 -0
  40. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.github/ISSUE_TEMPLATE/question.yml +0 -0
  41. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.github/PUBLICATION_CHECKLIST.md +0 -0
  42. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.github/dependabot.yml +0 -0
  43. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.github/pull_request_template.md +0 -0
  44. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.github/workflows/ci.yml +0 -0
  45. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.github/workflows/docs.yml +0 -0
  46. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.github/workflows/package.yml +0 -0
  47. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.github/workflows/release.yml +0 -0
  48. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/.pre-commit-config.yaml +0 -0
  49. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/CODE_OF_CONDUCT.md +0 -0
  50. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/CONTRIBUTING.md +0 -0
  51. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/GEMINI.md +0 -0
  52. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/LICENSE +0 -0
  53. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/SECURITY.md +0 -0
  54. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/SUPPORT.md +0 -0
  55. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/VISION.md +0 -0
  56. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/benchmarks/README.md +0 -0
  57. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/benchmarks/benchmark_detectors.py +0 -0
  58. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/benchmarks/benchmark_engine.py +0 -0
  59. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/benchmarks/datasets/synthetic_messages.jsonl +0 -0
  60. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/adr/0001-dependency-free-core.md +0 -0
  61. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/adr/0002-token-format.md +0 -0
  62. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/adr/0003-no-reversible-storage.md +0 -0
  63. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/adr/0004-optional-ml-backend.md +0 -0
  64. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/api.md +0 -0
  65. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/architecture.md +0 -0
  66. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/benchmarks.md +0 -0
  67. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/compatibility.md +0 -0
  68. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/contributing.md +0 -0
  69. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/deployment.md +0 -0
  70. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/index.md +0 -0
  71. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/limitations.md +0 -0
  72. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/llm-gateways.md +0 -0
  73. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/llm-payloads.md +0 -0
  74. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/migration-a2.md +0 -0
  75. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/migration-a3.md +0 -0
  76. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/policies.md +0 -0
  77. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/quickstart.md +0 -0
  78. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/releases/0.1.0.md +0 -0
  79. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/releases/0.1.0a2.md +0 -0
  80. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/releases/0.1.0a3.md +0 -0
  81. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/releases/0.1.0b1.md +0 -0
  82. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/releases/0.1.0rc1.md +0 -0
  83. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/releasing.md +0 -0
  84. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/security.md +0 -0
  85. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/docs/threat-model.md +0 -0
  86. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/examples/llm_gateway.py +0 -0
  87. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/models/README.md +0 -0
  88. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/scripts/__init__.py +0 -0
  89. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/scripts/audit_install.py +0 -0
  90. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/scripts/smoke_wheel.py +0 -0
  91. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/scripts/verify_release.py +0 -0
  92. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/__init__.py +0 -0
  93. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/adapters.py +0 -0
  94. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/api.py +0 -0
  95. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/backends/__init__.py +0 -0
  96. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/backends/base.py +0 -0
  97. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/backends/composite.py +0 -0
  98. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/backends/ml/__init__.py +0 -0
  99. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/backends/ml/onnx.py +0 -0
  100. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/backends/remote.py +0 -0
  101. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/detectors/__init__.py +0 -0
  102. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/detectors/base.py +0 -0
  103. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/detectors/email.py +0 -0
  104. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/detectors/ip_address.py +0 -0
  105. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/detectors/payment_card.py +0 -0
  106. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/detectors/phone.py +0 -0
  107. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/detectors/secret.py +0 -0
  108. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/detectors/url.py +0 -0
  109. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/document.py +0 -0
  110. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/engine.py +0 -0
  111. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/exceptions.py +0 -0
  112. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/inspection/__init__.py +0 -0
  113. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/inspection/image.py +0 -0
  114. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/inspection/office.py +0 -0
  115. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/processing.py +0 -0
  116. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/py.typed +0 -0
  117. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/resolution.py +0 -0
  118. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/transforms/__init__.py +0 -0
  119. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/transforms/alias.py +0 -0
  120. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/transforms/base.py +0 -0
  121. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/transforms/hmac.py +0 -0
  122. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/transforms/mode.py +0 -0
  123. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/transforms/placeholder.py +0 -0
  124. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/src/pseudonymize/transforms/redact.py +0 -0
  125. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/corpus/de.jsonl +0 -0
  126. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/corpus/en.jsonl +0 -0
  127. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/corpus/files.json +0 -0
  128. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/corpus/it.jsonl +0 -0
  129. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/integration/test_adversarial_documents.py +0 -0
  130. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/integration/test_builtin_files.py +0 -0
  131. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/integration/test_cli.py +0 -0
  132. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/integration/test_cross_platform_corpus.py +0 -0
  133. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/integration/test_file_processing.py +0 -0
  134. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/integration/test_llm_gateway_example.py +0 -0
  135. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/integration/test_public_api.py +0 -0
  136. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/integration/test_release_stress.py +0 -0
  137. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/integration/test_release_verifier.py +0 -0
  138. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/property/test_invariants.py +0 -0
  139. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/backends/test_remote.py +0 -0
  140. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/test_backends.py +0 -0
  141. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/test_document.py +0 -0
  142. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/test_engine.py +0 -0
  143. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/test_formats.py +0 -0
  144. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/test_modes.py +0 -0
  145. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/test_processing.py +0 -0
  146. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/test_spans.py +0 -0
  147. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/test_transforms.py +0 -0
  148. {pseudonymize-0.6.0 → pseudonymize-0.7.0}/tests/unit/test_types.py +0 -0
@@ -192,16 +192,16 @@ Starting commit: 6f6525bb1689a8b8ea5cbf7473a57d360dc339f7
192
192
  Starting PyPI version: 0.5.1
193
193
  Baseline downloads day/week/month: 0/0/0
194
194
  Baseline stars/forks/watchers: 0/0/0
195
- Files changed:
196
- Compatibility tests added:
197
- Artifact smoke environments:
198
- Known limitations:
199
- Deferred items:
200
- Release commit:
201
- Tag:
202
- PyPI upload verified:
195
+ Files changed: 7
196
+ Compatibility tests added: Yes
197
+ Artifact smoke environments: Win local isolated wheel
198
+ Known limitations: Remote processing is restricted to allowed/configured backends.
199
+ Deferred items: None
200
+ Release commit: f9517983550048983640cf4fe0e1680fcb6da0b5
201
+ Tag: v0.6.0
202
+ PyPI upload verified: Yes
203
203
  7-day metrics:
204
204
  28-day metrics:
205
- Status: In progress
205
+ Status: Released
206
206
 
207
207
 
@@ -177,6 +177,7 @@ dmypy.json
177
177
 
178
178
  # test temp files
179
179
  .pytest_temp/
180
+ .cache/
180
181
  *.onnx
181
182
 
182
183
  # Pyre type checker
@@ -4,6 +4,34 @@ All notable changes follow Keep a Changelog and Semantic Versioning.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.7.0] - 2026-08-28
8
+
9
+ ### Added
10
+
11
+ - Added a new quality evaluation suite (enchmarks/evaluate_quality.py) capable of streaming real-world datasets (like AI4Privacy/PII-Masking-200k) to establish rigorous precision, recall, and F1 baselines for core detectors and ML backends.
12
+
13
+ ## [0.6.1] - 2026-08-28
14
+
15
+ ### Added
16
+
17
+ - Generic `NATIONAL_ID` and `TAX_ID` entity types with checksum-validated Italian fiscal-code and
18
+ VAT-number detectors. Unprefixed VAT numbers require an explicit tax label.
19
+
20
+ ### Fixed
21
+
22
+ - ONNX test artifacts now persist outside pytest's temporary directory, avoiding a verified
23
+ model download on every local test invocation.
24
+ - `inspect-file` now serializes coordinate and structural locations used by PDF, image, and
25
+ Office inspection instead of failing after a detection is found.
26
+ - IBAN detection now accepts spaces between the country code and check digits, including values
27
+ extracted from visually grouped documents.
28
+ - PDF processing now preserves unchanged text blocks and renders transformed blocks legibly
29
+ instead of covering the complete document text with black redaction rectangles.
30
+ - PDF processing now localizes changed spans inside text blocks when possible, preserving the
31
+ surrounding text's original visual representation while securely removing detected values.
32
+ - PDF redaction now preserves existing colored backgrounds and source text colors instead of
33
+ placing white rectangles with mismatched replacement text over styled fields.
34
+
7
35
  ## [0.6.0] - 2026-08-25
8
36
 
9
37
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pseudonymize
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: Local-first PII pseudonymization for text, structured data, and LLM payloads.
5
5
  Project-URL: Homepage, https://github.com/ma2za/pseudonymize
6
6
  Project-URL: Documentation, https://ma2za.github.io/pseudonymize/
@@ -88,7 +88,7 @@ no telemetry or model downloads, and denies remote-capable backends by default.
88
88
  | --- | --- | --- |
89
89
  | Strings | Shipped | Pseudonymization, redaction, batch processing, and safe reports |
90
90
  | Nested Python data | Shipped | Dictionaries, lists, tuples, JSON scalars, and path policies |
91
- | Structured detection | Shipped | Email, phone, IP, IBAN, payment card, URL credentials, and common secrets |
91
+ | Structured detection | Shipped | Email, phone, IP, IBAN, payment card, Italian fiscal and VAT identifiers, URL credentials, and common secrets |
92
92
  | Document representation | Shipped | Immutable blocks, typed locations, sanitized metadata, and inspection |
93
93
  | Generic file orchestration | Shipped | Built-in or caller-provided adapters and atomic safe-copy output |
94
94
  | TXT, Markdown, log, JSON, JSONL, and CSV files | Shipped | Explicit format or recognized suffix; no content guessing |
@@ -44,7 +44,7 @@ no telemetry or model downloads, and denies remote-capable backends by default.
44
44
  | --- | --- | --- |
45
45
  | Strings | Shipped | Pseudonymization, redaction, batch processing, and safe reports |
46
46
  | Nested Python data | Shipped | Dictionaries, lists, tuples, JSON scalars, and path policies |
47
- | Structured detection | Shipped | Email, phone, IP, IBAN, payment card, URL credentials, and common secrets |
47
+ | Structured detection | Shipped | Email, phone, IP, IBAN, payment card, Italian fiscal and VAT identifiers, URL credentials, and common secrets |
48
48
  | Document representation | Shipped | Immutable blocks, typed locations, sanitized metadata, and inspection |
49
49
  | Generic file orchestration | Shipped | Built-in or caller-provided adapters and atomic safe-copy output |
50
50
  | TXT, Markdown, log, JSON, JSONL, and CSV files | Shipped | Explicit format or recognized suffix; no content guessing |
@@ -7,14 +7,14 @@ publishable without requiring unfinished later layers.
7
7
 
8
8
  | Release | Status | Outcome |
9
9
  | --- | --- | --- |
10
- | `0.1.0a1` | Published | Dependency-free text and nested-data core |
11
- | `0.1.0a2` | Published | Document representation, extension contracts, safe reports, and generic file orchestration |
12
- | `0.1.0a3` | Published | Built-in dependency-free text and machine-readable file adapters |
13
- | `0.1.0b1` | Published | Core API freeze and production-oriented examples |
14
- | `0.1.0rc1` | Published | External installation and release validation |
15
10
  | `0.1.0` | Published | First stable text and machine-readable release |
16
11
  | `0.2.0` | Published | Optional local machine learning identification for PII |
17
- | `0.3.0` | Next | Document inspection (PDF, DOCX, XLSX, PPTX) |
12
+ | `0.3.0` | Published | Document inspection (PDF, DOCX, XLSX, PPTX) |
13
+ | `0.4.0` | Published | Format-preserving documents |
14
+ | `0.5.0` | Published | OCR and scanned documents |
15
+ | `0.6.0` | Published | Remote detection |
16
+ | `0.6.1` | Published | Italian identifiers, PDF spans, and inspection fixes |
17
+ | `0.7.0` | Next | Real-world corpus benchmarking |
18
18
 
19
19
  Alpha releases optimize for the cleanest safe architecture, not backward compatibility. They may
20
20
  remove, rename, or replace public APIs without aliases or shims. Material changes are documented,
@@ -121,45 +121,51 @@ Exit criteria:
121
121
  Stable local processing for text, nested Python data, and plain or machine-readable files, with a
122
122
  documented compatibility policy and zero base runtime dependencies.
123
123
 
124
- ## Later capabilities
124
+ ## The Road to 1.0 (Hardening and Production Readiness)
125
125
 
126
- ### `0.2.0`: optional local ML
126
+ ### `0.7.0`: Real-World Corpus Benchmarking
127
+ - **Use Case:** Medical/Legal data mining.
128
+ - **Focus:** Integrate public datasets (Enron corpus, MIMIC-III synthetic variants, SEC filings) to establish precision/recall baselines for existing detectors.
127
129
 
128
- - Explicitly installed ONNX models for people, organizations, locations, and contextual addresses
129
- - Initial English, German, and Italian benchmarks
130
- - Pinned model revisions, checksums, licences, memory, and latency measurements
131
- - No model download during import or inference
130
+ ### `0.8.0`: Adversarial Document Defenses
131
+ - **Use Case:** FOIA response redaction failures (e.g., Manafort, AstraZeneca leaks).
132
+ - **Focus:** Prevent visual-only masking, hidden OCR layers, overlapping z-index elements, and scrub incremental document revisions.
132
133
 
133
- ### `0.3.0`: document inspection
134
+ ### `0.9.0`: Exhaustive Metadata & Hidden Structure Extraction
135
+ - **Use Case:** Corporate e-discovery.
136
+ - **Focus:** Parse PDF `/Info` dictionaries, XMP metadata, DOCX headers/footers, and XLSX comments.
134
137
 
135
- - Optional PDF, DOCX, XLSX, and PPTX extraction
136
- - Structural or coordinate-aware locations
137
- - Detection-only output while representation fixtures mature
138
+ ### `0.10.0`: Large-Scale & Streaming Data Pipelines
139
+ - **Use Case:** Database dumps and bulk analytics exports.
140
+ - **Focus:** Bounded-memory processing for multi-gigabyte CSV/JSONL files; parallel block processing.
138
141
 
139
- ### `0.4.0`: format-preserving documents
142
+ ### `0.11.0`: Resilient Document Parsing
143
+ - **Use Case:** Legacy enterprise files.
144
+ - **Focus:** Gracefully handle corrupt XML schemas, custom OOXML namespaces, and safely extract embedded OLE objects.
140
145
 
141
- - Sanitized DOCX, XLSX, and PPTX copies
142
- - Secure text-PDF redaction with underlying content removal
143
- - Relevant metadata cleaning and format-integrity tests
146
+ ### `0.12.0`: Advanced OCR Degradation Handling
147
+ - **Use Case:** Medical faxes and legacy legal scans.
148
+ - **Focus:** Handle low-dpi faxes, skewed pages, noisy backgrounds, and watermark interference mimicking structural data.
144
149
 
145
- ### `0.5.0`: OCR and scanned documents
150
+ ### `0.13.0`: ML Confidence Calibration & Thresholding
151
+ - **Use Case:** Reducing false positives in unstructured clinical notes.
152
+ - **Focus:** Tune ONNX backend confidence scores against ambiguous texts and introduce dynamic, policy-based confidence thresholds.
146
153
 
147
- - Local OCR for images, scanned PDFs, and mixed PDFs
148
- - Bounding-box transformations
149
- - No OCR when reliable native text exists
154
+ ### `0.14.0`: Telemetry, Observability & Auditing
155
+ - **Use Case:** Enterprise compliance reporting and SLA monitoring.
156
+ - **Focus:** Structured JSON logging, processing timeouts, latency tracing, and detailed audit trails without leaking PII.
150
157
 
151
- ### `0.6.0`: remote detection
158
+ ### `0.15.0`: Remote Backend Fault Tolerance
159
+ - **Use Case:** High-throughput API gateway integration.
160
+ - **Focus:** Circuit breakers, adaptive retries, rate-limit handling, and strict latency bounds for the HTTP provider transport.
152
161
 
153
- - Vendor-neutral provider protocol and optional HTTP transport
154
- - Explicit dual consent, bounded timeouts, and bounded retries
155
- - Provider capability reporting and remote-block statistics
156
- - Local structured-value replacement before permitted remote processing
157
- - Offset mapping after local preprocessing
158
-
159
- ### `1.0.0`: mature compatibility commitment
162
+ ### `0.16.0`: Cross-Lingual & Typographical Boundary Hardening (Pre-1.0 Audit)
163
+ - **Use Case:** Global support ticket sanitization.
164
+ - **Focus:** Audit detectors against non-Latin scripts, CJK spacing, RTL text, and bidirectional text overrides. Final security fuzzing (Zip-bombs, XXE).
160
165
 
166
+ ### `1.0.0`: Mature Compatibility Commitment
161
167
  Long-term compatibility begins after core processing, document rewriting, OCR, and remote-security
162
- contracts have production fixtures, published benchmarks, and independent usage feedback.
168
+ contracts have production fixtures, published benchmarks, and independent usage feedback against the hardening milestones above.
163
169
 
164
170
  ## Optional dependency policy
165
171
 
@@ -0,0 +1,154 @@
1
+ import argparse
2
+ import logging
3
+ import sys
4
+ import time
5
+ from pathlib import Path
6
+
7
+ try:
8
+ from datasets import load_dataset
9
+ except ImportError:
10
+ print("Error: 'datasets' library not found.")
11
+ print("Run: uv run --with datasets python benchmarks/evaluate_quality.py")
12
+ sys.exit(1)
13
+
14
+ from pseudonymize.backends.ml.onnx import LocalONNXPIIBackend
15
+ from pseudonymize.engine import Pseudonymizer
16
+
17
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
18
+ logger = logging.getLogger("evaluate_quality")
19
+
20
+ # We evaluate precision and recall against these specific AI4Privacy labels
21
+ # that map to the capabilities of pseudonymize's core detectors and ML backend.
22
+ SUPPORTED_LABELS = {
23
+ # Core detectors
24
+ "EMAIL",
25
+ "PHONENUMBER",
26
+ "CREDITCARD",
27
+ "IBAN",
28
+ "IPADDRESS",
29
+ "URL",
30
+ # ML Backend (DistilBERT ONNX)
31
+ "FIRSTNAME",
32
+ "LASTNAME",
33
+ "MIDDLENAME",
34
+ "COMPANYNAME",
35
+ "CITY",
36
+ "STATE",
37
+ "COUNTY",
38
+ "STREET",
39
+ "ZIPCODE",
40
+ }
41
+
42
+
43
+ def evaluate(num_samples: int, use_ml: bool):
44
+ logger.info("Loading ai4privacy/pii-masking-200k (English subset)...")
45
+ # Use streaming to avoid downloading the entire 200K dataset
46
+ ds = load_dataset("ai4privacy/pii-masking-200k", split="train", streaming=True)
47
+
48
+ engine = Pseudonymizer()
49
+ if use_ml:
50
+ # We need the model downloaded. The test suite uses the distilbert model.
51
+ # Let's assume it's already cached or we can fetch it.
52
+ # To keep it simple, we'll try to initialize it. If it fails, we fall back or error.
53
+ CACHE_DIR = Path(".cache/pseudonymize-tests/models/distilbert-ml")
54
+ onnx_model_path = CACHE_DIR / "model_int8.onnx"
55
+ tokenizer_path = CACHE_DIR / "tokenizer.json"
56
+
57
+ if not onnx_model_path.exists() or not tokenizer_path.exists():
58
+ logger.error(f"ML artifacts not found in {CACHE_DIR}.")
59
+ logger.error("Please run 'uv run pytest tests/unit/backends/test_onnx.py' first.")
60
+ sys.exit(1)
61
+
62
+ backend = LocalONNXPIIBackend(model_path=onnx_model_path, tokenizer_path=tokenizer_path)
63
+ engine = Pseudonymizer(backends=[*engine.backends, backend])
64
+
65
+ true_positives = 0
66
+ false_positives = 0
67
+ false_negatives = 0
68
+
69
+ count = 0
70
+ start_time = time.time()
71
+
72
+ for row in ds:
73
+ if row["language"] != "en":
74
+ continue
75
+
76
+ text = row["source_text"]
77
+ masks = row["privacy_mask"]
78
+
79
+ # Filter ground truth masks to only those we claim to support
80
+ ground_truth_spans = [
81
+ (mask["start"], mask["end"], mask["label"])
82
+ for mask in masks
83
+ if mask["label"] in SUPPORTED_LABELS
84
+ ]
85
+
86
+ # Run detection
87
+ result = engine.process_with_report(text)
88
+
89
+ # Extract detected spans
90
+ detected_spans = [(detection.start, detection.end) for detection in result.detections]
91
+
92
+ # Calculate overlap
93
+ # For simplicity in this benchmark:
94
+ # TP: a detected span overlaps with a ground truth span.
95
+ # FP: a detected span does not overlap with any ground truth span.
96
+ # FN: a ground truth span is not overlapped by any detected span.
97
+
98
+ matched_gt = set()
99
+ for d_start, d_end in detected_spans:
100
+ matched = False
101
+ for i, (g_start, g_end, _g_label) in enumerate(ground_truth_spans):
102
+ # Check overlap
103
+ if max(d_start, g_start) < min(d_end, g_end):
104
+ matched = True
105
+ matched_gt.add(i)
106
+ break
107
+ if matched:
108
+ true_positives += 1
109
+ else:
110
+ false_positives += 1
111
+
112
+ for i in range(len(ground_truth_spans)):
113
+ if i not in matched_gt:
114
+ false_negatives += 1
115
+
116
+ count += 1
117
+ if count >= num_samples:
118
+ break
119
+
120
+ elapsed = time.time() - start_time
121
+
122
+ precision = (
123
+ true_positives / (true_positives + false_positives)
124
+ if (true_positives + false_positives) > 0
125
+ else 0
126
+ )
127
+ recall = (
128
+ true_positives / (true_positives + false_negatives)
129
+ if (true_positives + false_negatives) > 0
130
+ else 0
131
+ )
132
+ f1 = 2 * (precision * recall) / (precision + recall) if (precision + recall) > 0 else 0
133
+
134
+ logger.info(f"--- Benchmark Results ({count} samples) ---")
135
+ logger.info(f"Time elapsed: {elapsed:.2f}s")
136
+ logger.info(f"True Positives: {true_positives}")
137
+ logger.info(f"False Positives: {false_positives}")
138
+ logger.info(f"False Negatives: {false_negatives}")
139
+ logger.info(f"Precision: {precision:.4f}")
140
+ logger.info(f"Recall: {recall:.4f}")
141
+ logger.info(f"F1 Score: {f1:.4f}")
142
+
143
+
144
+ if __name__ == "__main__":
145
+ parser = argparse.ArgumentParser(
146
+ description="Evaluate pseudonymize precision and recall against real-world datasets."
147
+ )
148
+ parser.add_argument("--samples", type=int, default=500, help="Number of samples to evaluate.")
149
+ parser.add_argument(
150
+ "--ml", action="store_true", help="Include the ONNX ML backend in evaluation."
151
+ )
152
+ args = parser.parse_args()
153
+
154
+ evaluate(args.samples, args.ml)
@@ -0,0 +1,12 @@
1
+ # Detectors
2
+
3
+ Bundled detectors cover email, phone, IP address, IBAN, payment card, URL credential, and secret
4
+ entities. Italian fiscal codes and VAT identifiers are also supported as `NATIONAL_ID` and
5
+ `TAX_ID`. Candidate extraction is followed by normalization and validation. IBANs require MOD-97,
6
+ cards require Luhn, Italian identifiers require their official check character or digit, and IP
7
+ candidates must parse with the standard library. An Italian VAT number without an `IT` prefix is
8
+ considered only next to an explicit VAT label to avoid treating arbitrary 11-digit values as tax
9
+ identifiers.
10
+
11
+ Detector results contain type, offsets, confidence, and detector name. They never contain the raw
12
+ matched value. Custom thread-safe detectors can implement the `Detector` protocol.
@@ -0,0 +1,27 @@
1
+ # Detection Quality Benchmarks
2
+
3
+ This document tracks the precision and recall of the `pseudonymize` engine across real-world and synthetic evaluation corpora. Unlike performance benchmarks (which measure throughput and latency), quality benchmarks ensure the core detectors and ML backends accurately identify PII without excessive false positives.
4
+
5
+ ## `0.7.0` Baseline (AI4Privacy PII-Masking-200k)
6
+
7
+ Evaluated against the English subset of the `ai4privacy/pii-masking-200k` dataset, comparing the engine's detections against the dataset's ground-truth `privacy_mask`.
8
+
9
+ **Setup:**
10
+ - Model: `LocalONNXPIIBackend` (DistilBERT ONNX int8)
11
+ - Target Entities: `EMAIL`, `PHONENUMBER`, `CREDITCARD`, `IBAN`, `IPADDRESS`, `URL`, `FIRSTNAME`, `LASTNAME`, `MIDDLENAME`, `COMPANYNAME`, `CITY`, `STATE`, `COUNTY`, `STREET`, `ZIPCODE`
12
+ - Sample Size: 50 documents (Streaming)
13
+
14
+ **Results (Preliminary Baseline):**
15
+
16
+ | Metric | Score |
17
+ | --- | --- |
18
+ | Precision | 0.4091 |
19
+ | Recall | 0.1875 |
20
+ | F1 Score | 0.2571 |
21
+
22
+ *Note: These baseline metrics reflect zero-configuration default detectors and the stock ML backend on a highly ambiguous, fine-grained synthetic dataset. Future releases (e.g., 0.13.0 ML Calibration) will optimize these metrics through tuned thresholds and improved boundaries.*
23
+
24
+ To reproduce this benchmark locally:
25
+ ```console
26
+ uv run python benchmarks/evaluate_quality.py --ml --samples 50
27
+ ```
@@ -27,6 +27,7 @@ nav:
27
27
  - Threat model: threat-model.md
28
28
  - Limitations: limitations.md
29
29
  - Benchmarks: benchmarks.md
30
+ - Quality Benchmarks: quality_benchmarks.md
30
31
  - Contributing: contributing.md
31
32
  - Decisions:
32
33
  - Dependency-free core: adr/0001-dependency-free-core.md
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pseudonymize"
7
- version = "0.6.0"
7
+ version = "0.7.0"
8
8
  description = "Local-first PII pseudonymization for text, structured data, and LLM payloads."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -72,6 +72,9 @@ test = [
72
72
  "fpdf2>=2.7.9",
73
73
  "respx>=0.23.1",
74
74
  ]
75
+ benchmarks = [
76
+ "datasets>=3.0.0",
77
+ ]
75
78
  quality = ["mypy>=1.11", "pre-commit>=4", "ruff>=0.9"]
76
79
  docs = ["mkdocs-material>=9", "mkdocstrings[python]>=1.0.6"]
77
80
  release = ["build>=1", "twine>=6"]
@@ -14,6 +14,8 @@ _RULE_ENTITY_TYPES = frozenset(
14
14
  EntityType.IP_ADDRESS,
15
15
  EntityType.IBAN,
16
16
  EntityType.PAYMENT_CARD,
17
+ EntityType.NATIONAL_ID,
18
+ EntityType.TAX_ID,
17
19
  EntityType.URL_CREDENTIAL,
18
20
  EntityType.SECRET,
19
21
  }
@@ -10,7 +10,13 @@ from pathlib import Path
10
10
 
11
11
  from pseudonymize.api import generate_key
12
12
  from pseudonymize.detectors import DEFAULT_DETECTORS
13
- from pseudonymize.document import CSVCellLocation, JSONPathLocation, TextOffsetLocation
13
+ from pseudonymize.document import (
14
+ CoordinateLocation,
15
+ CSVCellLocation,
16
+ JSONPathLocation,
17
+ StructuralLocation,
18
+ TextOffsetLocation,
19
+ )
14
20
  from pseudonymize.engine import Data, Pseudonymizer
15
21
  from pseudonymize.exceptions import PseudonymizeError
16
22
  from pseudonymize.formats import FileFormat
@@ -199,6 +205,17 @@ def _location_payload(report: DetectionReport) -> dict[str, object]:
199
205
  return {"kind": "json_path", "path": location.path}
200
206
  if isinstance(location, CSVCellLocation):
201
207
  return {"kind": "csv_cell", "row": location.row, "column": location.column}
208
+ if isinstance(location, CoordinateLocation):
209
+ return {
210
+ "kind": "coordinate",
211
+ "page": location.page,
212
+ "x0": location.x0,
213
+ "y0": location.y0,
214
+ "x1": location.x1,
215
+ "y1": location.y1,
216
+ }
217
+ if isinstance(location, StructuralLocation):
218
+ return {"kind": "structural", "path": location.path}
202
219
  raise TypeError("unsupported report location")
203
220
 
204
221
 
@@ -3,7 +3,10 @@ from dataclasses import dataclass
3
3
 
4
4
  from pseudonymize.result import Detection, EntityType
5
5
 
6
- _IBAN = re.compile(r"(?<![A-Z0-9])[A-Z]{2}\d{2}(?:[ ]?[A-Z0-9]){11,30}(?![A-Z0-9])", re.I)
6
+ _IBAN = re.compile(
7
+ r"(?<![A-Z0-9])[A-Z]{2}(?:[ ]?\d){2}(?:[ ]?[A-Z0-9]){11,30}(?![A-Z0-9])",
8
+ re.I,
9
+ )
7
10
 
8
11
 
9
12
  def _valid_mod97(value: str) -> bool:
@@ -0,0 +1,122 @@
1
+ import re
2
+ from dataclasses import dataclass
3
+
4
+ from pseudonymize.result import Detection, EntityType
5
+
6
+ _OMOCODIA_DIGITS = str.maketrans("LMNPQRSTUV", "0123456789")
7
+ _FISCAL_CODE = re.compile(
8
+ r"(?<![A-Z0-9])"
9
+ r"[A-Z]{6}[0-9LMNPQRSTUV]{2}[ABCDEHLMPRST][0-9LMNPQRSTUV]{2}"
10
+ r"[A-Z][0-9LMNPQRSTUV]{3}[A-Z]"
11
+ r"(?![A-Z0-9])",
12
+ re.I,
13
+ )
14
+ _ODD_VALUES = dict(
15
+ zip(
16
+ "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ",
17
+ (
18
+ 1,
19
+ 0,
20
+ 5,
21
+ 7,
22
+ 9,
23
+ 13,
24
+ 15,
25
+ 17,
26
+ 19,
27
+ 21,
28
+ 1,
29
+ 0,
30
+ 5,
31
+ 7,
32
+ 9,
33
+ 13,
34
+ 15,
35
+ 17,
36
+ 19,
37
+ 21,
38
+ 2,
39
+ 4,
40
+ 18,
41
+ 20,
42
+ 11,
43
+ 3,
44
+ 6,
45
+ 8,
46
+ 12,
47
+ 14,
48
+ 16,
49
+ 10,
50
+ 22,
51
+ 25,
52
+ 24,
53
+ 23,
54
+ ),
55
+ strict=True,
56
+ )
57
+ )
58
+ _EVEN_VALUES = {
59
+ **{character: value for value, character in enumerate("0123456789")},
60
+ **{character: value for value, character in enumerate("ABCDEFGHIJKLMNOPQRSTUVWXYZ")},
61
+ }
62
+ _VAT_WITH_COUNTRY = re.compile(
63
+ r"(?<![A-Z0-9])(?P<value>IT[ ]?(?:\d[ ]?){10}\d)(?![A-Z0-9])",
64
+ re.I,
65
+ )
66
+ _VAT_WITH_LABEL = re.compile(
67
+ r"\b(?:partita[ ]+iva|p[ .]*iva|p[ .]*i[ .]*|vat(?:[ ]+id)?)"
68
+ r"\s*[:=]?\s*(?P<value>(?:IT[ ]?)?(?:\d[ ]?){10}\d)(?!\d)",
69
+ re.I,
70
+ )
71
+
72
+
73
+ def _valid_fiscal_code(value: str) -> bool:
74
+ compact = value.upper()
75
+ day_text = compact[9:11].translate(_OMOCODIA_DIGITS)
76
+ day = int(day_text)
77
+ if not (1 <= day <= 31 or 41 <= day <= 71):
78
+ return False
79
+ total = sum(
80
+ (_ODD_VALUES if index % 2 == 0 else _EVEN_VALUES)[character]
81
+ for index, character in enumerate(compact[:15])
82
+ )
83
+ return compact[-1] == chr(ord("A") + total % 26)
84
+
85
+
86
+ def _valid_vat(value: str) -> bool:
87
+ compact = "".join(character for character in value if character.isdigit())
88
+ if len(compact) != 11 or len(set(compact)) == 1:
89
+ return False
90
+ total = sum(int(character) for character in compact[:10:2])
91
+ for character in compact[1:10:2]:
92
+ doubled = int(character) * 2
93
+ total += doubled - 9 if doubled > 9 else doubled
94
+ return (10 - total % 10) % 10 == int(compact[-1])
95
+
96
+
97
+ @dataclass(frozen=True, slots=True)
98
+ class ItalianFiscalCodeDetector:
99
+ name: str = "italian_fiscal_code"
100
+
101
+ def detect(self, text: str) -> list[Detection]:
102
+ return [
103
+ Detection(EntityType.NATIONAL_ID, match.start(), match.end(), 1.0, self.name)
104
+ for match in _FISCAL_CODE.finditer(text)
105
+ if _valid_fiscal_code(match.group())
106
+ ]
107
+
108
+
109
+ @dataclass(frozen=True, slots=True)
110
+ class ItalianVATDetector:
111
+ name: str = "italian_vat"
112
+
113
+ def detect(self, text: str) -> list[Detection]:
114
+ spans = {
115
+ match.span("value")
116
+ for pattern in (_VAT_WITH_COUNTRY, _VAT_WITH_LABEL)
117
+ for match in pattern.finditer(text)
118
+ if _valid_vat(match.group("value"))
119
+ }
120
+ return [
121
+ Detection(EntityType.TAX_ID, start, end, 1.0, self.name) for start, end in sorted(spans)
122
+ ]
@@ -2,6 +2,7 @@ from pseudonymize.detectors.base import Detector
2
2
  from pseudonymize.detectors.email import EmailDetector
3
3
  from pseudonymize.detectors.iban import IbanDetector
4
4
  from pseudonymize.detectors.ip_address import IpAddressDetector
5
+ from pseudonymize.detectors.italian import ItalianFiscalCodeDetector, ItalianVATDetector
5
6
  from pseudonymize.detectors.payment_card import PaymentCardDetector
6
7
  from pseudonymize.detectors.phone import PhoneDetector
7
8
  from pseudonymize.detectors.secret import SecretDetector
@@ -12,6 +13,8 @@ DEFAULT_DETECTORS: tuple[Detector, ...] = (
12
13
  IpAddressDetector(),
13
14
  PaymentCardDetector(),
14
15
  IbanDetector(),
16
+ ItalianFiscalCodeDetector(),
17
+ ItalianVATDetector(),
15
18
  PhoneDetector(),
16
19
  UrlDetector(),
17
20
  SecretDetector(),
@@ -339,6 +339,9 @@ def _read_csv(text: str) -> tuple[tuple[str, ...], ...]:
339
339
  previous_limit = csv.field_size_limit()
340
340
  try:
341
341
  csv.field_size_limit(sys.maxsize)
342
+ except OverflowError:
343
+ csv.field_size_limit(2147483647)
344
+ try:
342
345
  return tuple(
343
346
  tuple(row) for row in csv.reader(io.StringIO(text, newline=""), strict=True)
344
347
  )