pii-protect 0.1.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.
- pii_protect-0.1.0/LICENSE +21 -0
- pii_protect-0.1.0/PKG-INFO +377 -0
- pii_protect-0.1.0/README.md +333 -0
- pii_protect-0.1.0/pyproject.toml +58 -0
- pii_protect-0.1.0/setup.cfg +4 -0
- pii_protect-0.1.0/src/pii_protect.egg-info/PKG-INFO +377 -0
- pii_protect-0.1.0/src/pii_protect.egg-info/SOURCES.txt +23 -0
- pii_protect-0.1.0/src/pii_protect.egg-info/dependency_links.txt +1 -0
- pii_protect-0.1.0/src/pii_protect.egg-info/requires.txt +29 -0
- pii_protect-0.1.0/src/pii_protect.egg-info/top_level.txt +1 -0
- pii_protect-0.1.0/src/pii_shield/__init__.py +78 -0
- pii_protect-0.1.0/src/pii_shield/crypto.py +123 -0
- pii_protect-0.1.0/src/pii_shield/engine.py +351 -0
- pii_protect-0.1.0/src/pii_shield/exceptions.py +49 -0
- pii_protect-0.1.0/src/pii_shield/ner/__init__.py +25 -0
- pii_protect-0.1.0/src/pii_shield/ner/engine.py +625 -0
- pii_protect-0.1.0/src/pii_shield/storage/__init__.py +29 -0
- pii_protect-0.1.0/src/pii_shield/storage/base.py +106 -0
- pii_protect-0.1.0/src/pii_shield/storage/filesystem.py +155 -0
- pii_protect-0.1.0/src/pii_shield/storage/memory.py +57 -0
- pii_protect-0.1.0/src/pii_shield/storage/postgres.py +204 -0
- pii_protect-0.1.0/src/pii_shield/storage/redis_backend.py +166 -0
- pii_protect-0.1.0/src/pii_shield/tokens.py +159 -0
- pii_protect-0.1.0/src/pii_shield/types.py +134 -0
- pii_protect-0.1.0/tests/test_engine.py +78 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Musaib Altaf
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pii-protect
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pluggable, on-premise-first PII masking, unmasking, and redaction library.
|
|
5
|
+
Author: Musaib Altaf
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Repository, https://example.com/innowave360/pii-shield
|
|
8
|
+
Keywords: pii,masking,redaction,privacy,ner,encryption
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Topic :: Security
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: cryptography>=42.0.0
|
|
21
|
+
Provides-Extra: postgres
|
|
22
|
+
Requires-Dist: asyncpg>=0.29.0; extra == "postgres"
|
|
23
|
+
Provides-Extra: redis
|
|
24
|
+
Requires-Dist: redis>=5.0.0; extra == "redis"
|
|
25
|
+
Provides-Extra: spacy
|
|
26
|
+
Requires-Dist: spacy>=3.7.0; extra == "spacy"
|
|
27
|
+
Provides-Extra: privacy-filter
|
|
28
|
+
Requires-Dist: transformers>=4.50.0; extra == "privacy-filter"
|
|
29
|
+
Requires-Dist: torch>=2.0.0; extra == "privacy-filter"
|
|
30
|
+
Requires-Dist: accelerate>=1.0; extra == "privacy-filter"
|
|
31
|
+
Requires-Dist: safetensors>=0.4.0; extra == "privacy-filter"
|
|
32
|
+
Provides-Extra: all
|
|
33
|
+
Requires-Dist: asyncpg>=0.29.0; extra == "all"
|
|
34
|
+
Requires-Dist: redis>=5.0.0; extra == "all"
|
|
35
|
+
Requires-Dist: spacy>=3.7.0; extra == "all"
|
|
36
|
+
Requires-Dist: transformers>=4.50.0; extra == "all"
|
|
37
|
+
Requires-Dist: torch>=2.0.0; extra == "all"
|
|
38
|
+
Requires-Dist: accelerate>=1.0; extra == "all"
|
|
39
|
+
Requires-Dist: safetensors>=0.4.0; extra == "all"
|
|
40
|
+
Provides-Extra: dev
|
|
41
|
+
Requires-Dist: pytest>=8.2.0; extra == "dev"
|
|
42
|
+
Requires-Dist: pytest-asyncio>=0.23.6; extra == "dev"
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
45
|
+
# pii-shield
|
|
46
|
+
|
|
47
|
+
A pluggable, on-premise-first PII masking, unmasking, and redaction library for Python.
|
|
48
|
+
|
|
49
|
+
`pii-shield` detects personally identifiable and sensitive business
|
|
50
|
+
information in free text (emails, phone numbers, GST/PAN/IBAN numbers,
|
|
51
|
+
person and organisation names, bank details, invoice/PO numbers, and more),
|
|
52
|
+
and gives you three ways to handle it:
|
|
53
|
+
|
|
54
|
+
- **mask** it into a reversible placeholder token, encrypted at rest
|
|
55
|
+
- **unmask** a previously masked token back to its original value
|
|
56
|
+
- **redact** it permanently, with no way to recover the original
|
|
57
|
+
|
|
58
|
+
There is no server, no API, and no hard dependency on any particular
|
|
59
|
+
database — it's a library you import and call directly. Where your
|
|
60
|
+
encrypted PII values live is a pluggable choice: in-memory, a local file,
|
|
61
|
+
Redis, or PostgreSQL, or a backend you write yourself.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Install
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install pii-shield # core: regex detection + in-memory/filesystem storage
|
|
69
|
+
pip install "pii-shield[postgres]" # + PostgreSQL storage backend
|
|
70
|
+
pip install "pii-shield[redis]" # + Redis storage backend
|
|
71
|
+
pip install "pii-shield[spacy]" # + spaCy NER layer (PERSON/ORG/GPE)
|
|
72
|
+
pip install "pii-shield[privacy-filter]" # + transformer token-classification layer
|
|
73
|
+
pip install "pii-shield[all]" # everything
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Only `cryptography` is a hard dependency. `asyncpg`, `redis`, `spacy`, and
|
|
77
|
+
`transformers`/`torch` are all opt-in extras. If you use a backend or
|
|
78
|
+
detection layer without installing its extra, you get a clear
|
|
79
|
+
`OptionalDependencyMissingError` telling you exactly what to install —
|
|
80
|
+
never a bare `ImportError` or a silent failure.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Quick start
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
import asyncio
|
|
88
|
+
from pii_shield import PIIMaskingEngine
|
|
89
|
+
from pii_shield.storage import InMemoryStorage
|
|
90
|
+
|
|
91
|
+
async def main():
|
|
92
|
+
async with PIIMaskingEngine(storage=InMemoryStorage()) as engine:
|
|
93
|
+
result = await engine.mask("Contact john@acme.com about GST 27AAPFU0939F1ZV")
|
|
94
|
+
print(result.masked_text)
|
|
95
|
+
# "Contact {{EMAIL:abcc2}} about GST {{GST:9a03b}}"
|
|
96
|
+
|
|
97
|
+
original = await engine.unmask(result.masked_text)
|
|
98
|
+
print(original)
|
|
99
|
+
# "Contact john@acme.com about GST 27AAPFU0939F1ZV"
|
|
100
|
+
|
|
101
|
+
print(engine.redact("Contact john@acme.com about GST 27AAPFU0939F1ZV"))
|
|
102
|
+
# "Contact [REDACTED:EMAIL] about GST [REDACTED:GST]" — irreversible, nothing stored
|
|
103
|
+
|
|
104
|
+
asyncio.run(main())
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`PIIMaskingEngine` is an async context manager — `initialise()` connects
|
|
108
|
+
the storage backend, `close()` releases it. Use `async with` unless you
|
|
109
|
+
need to control that lifecycle yourself.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## The three operations
|
|
114
|
+
|
|
115
|
+
| Method | Reversible? | Touches storage? | Use for |
|
|
116
|
+
|---|---|---|---|
|
|
117
|
+
| `mask(text, scope=None)` | Yes, via `unmask()` | Yes | Sending documents to an LLM/cloud service while keeping raw PII on-premise |
|
|
118
|
+
| `unmask(masked_text, scope=None)` | — | Yes (read) | Restoring original values before writing back to source systems |
|
|
119
|
+
| `redact(text)` | **No** | **No** | Logs, analytics exports, anything that must never contain recoverable PII |
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
result = await engine.mask(text, scope="invoice-2026-00417")
|
|
123
|
+
# result.masked_text -> text with {{TYPE:xxxxx}} placeholders
|
|
124
|
+
# result.token_count -> number of PII spans masked
|
|
125
|
+
# result.entity_counts -> {"EMAIL": 1, "GST": 1, ...}
|
|
126
|
+
# result.entities -> per-span detail (type, offsets, token, confidence, source)
|
|
127
|
+
|
|
128
|
+
text_back = await engine.unmask(result.masked_text, scope="invoice-2026-00417")
|
|
129
|
+
|
|
130
|
+
scrubbed = engine.redact(text) # synchronous — no storage or encryption involved
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`mask_dict()` / `unmask_dict()` do the same over JSON-serialisable dicts,
|
|
134
|
+
masking all string leaf values in one pass so a repeated value across
|
|
135
|
+
fields still maps to the same token.
|
|
136
|
+
|
|
137
|
+
`scope` is a free-form string (e.g. a document or invoice ID). The same
|
|
138
|
+
PII value repeated within one scope deduplicates to a single stored token
|
|
139
|
+
instead of being encrypted and stored twice — pass the same scope to
|
|
140
|
+
`mask()` and the matching `unmask()` call.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Architecture
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
┌─────────────────────────────────────────┐
|
|
148
|
+
│ PIIMaskingEngine │
|
|
149
|
+
│ (pii_shield.engine) │
|
|
150
|
+
│ │
|
|
151
|
+
│ mask() unmask() redact() │
|
|
152
|
+
└───────┬───────────┬───────────┬───────────┘
|
|
153
|
+
│ │ │
|
|
154
|
+
┌─────────────┘ │ └─── (redact never
|
|
155
|
+
│ │ leaves this box —
|
|
156
|
+
▼ │ no encrypt, no store)
|
|
157
|
+
┌─────────────────────┐ │
|
|
158
|
+
│ NEREngine │ │
|
|
159
|
+
│ (pii_shield.ner) │ │
|
|
160
|
+
│ │ │
|
|
161
|
+
│ RegexNERLayer (always on) │
|
|
162
|
+
│ SpacyNERLayer (optional) │
|
|
163
|
+
│ PrivacyFilterLayer (optional) │
|
|
164
|
+
│ │ │
|
|
165
|
+
│ TokenizerSafeSpanMerger │
|
|
166
|
+
│ SpanConflictResolver │
|
|
167
|
+
└──────────┬─────────────┘ │
|
|
168
|
+
│ DetectedSpan[] │
|
|
169
|
+
▼ │
|
|
170
|
+
┌─────────────────────┐ │
|
|
171
|
+
│ DeterministicToken- │ │
|
|
172
|
+
│ Generator │ │
|
|
173
|
+
│ (pii_shield.tokens) │ │
|
|
174
|
+
│ │ │
|
|
175
|
+
│ {{TYPE:xxxxx}} │ │
|
|
176
|
+
│ find_tokens_in_text() │◄────────────┘
|
|
177
|
+
└──────────┬─────────────┘
|
|
178
|
+
│ token, value_hash
|
|
179
|
+
▼
|
|
180
|
+
┌─────────────────────┐ ┌───────────────────────────────┐
|
|
181
|
+
│ AESGCMCipher │ │ StorageBackend │
|
|
182
|
+
│ (pii_shield.crypto) │──────▶ (pii_shield.storage) │
|
|
183
|
+
│ │ │ │
|
|
184
|
+
│ encrypt() / decrypt() │ │ InMemoryStorage │
|
|
185
|
+
│ AES-256-GCM │ │ FileSystemStorage │
|
|
186
|
+
└────────────────────────┘ │ RedisStorage (extra) │
|
|
187
|
+
│ PostgresStorage (extra) │
|
|
188
|
+
└───────────────────────────────┘
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Components
|
|
192
|
+
|
|
193
|
+
**`PIIMaskingEngine`** (`pii_shield.engine`) is the single public entry
|
|
194
|
+
point. It owns one `NEREngine`, one `DeterministicTokenGenerator`, one
|
|
195
|
+
`AESGCMCipher`, and one `StorageBackend`, and wires them together for
|
|
196
|
+
`mask()` / `unmask()` / `redact()`. This is the only class most callers
|
|
197
|
+
need to import.
|
|
198
|
+
|
|
199
|
+
**`NEREngine`** (`pii_shield.ner`) does detection only — it never touches
|
|
200
|
+
encryption or storage. It runs one or more layers over the input text and
|
|
201
|
+
merges their output into a single non-overlapping span list:
|
|
202
|
+
|
|
203
|
+
- `RegexNERLayer` — always on, no extra dependencies. High-precision
|
|
204
|
+
patterns for structured PII: GST, PAN, TAN, ABN, VAT, IBAN, SWIFT,
|
|
205
|
+
account/sort-code/routing numbers, credit cards, email, phone
|
|
206
|
+
(India + international), invoice and PO references.
|
|
207
|
+
- `SpacyNERLayer` — optional. Adds PERSON / ORGANISATION / ADDRESS
|
|
208
|
+
detection via a local spaCy model. Requires `pii-shield[spacy]`.
|
|
209
|
+
- `PrivacyFilterLayer` — optional. Adds detection via any HuggingFace
|
|
210
|
+
token-classification model you point it at, run entirely on-premise
|
|
211
|
+
through `transformers.pipeline`. Requires `pii-shield[privacy-filter]`.
|
|
212
|
+
|
|
213
|
+
When layers disagree or overlap, `SpanConflictResolver` picks a winner
|
|
214
|
+
(regex-validated spans win first, then financial-entity-over-phone,
|
|
215
|
+
then higher confidence, then longer span), and
|
|
216
|
+
`TokenizerSafeSpanMerger` stitches back together sub-word fragments
|
|
217
|
+
that some transformer models emit at token boundaries.
|
|
218
|
+
|
|
219
|
+
**`DeterministicTokenGenerator`** (`pii_shield.tokens`) turns a detected
|
|
220
|
+
span into a `{{ENTITY_TYPE:xxxxx}}` placeholder — a 5-hex-character
|
|
221
|
+
suffix derived from `SHA-256(value | entity_type | salt)`. Same value +
|
|
222
|
+
same entity type always produces the same token within one salted
|
|
223
|
+
instance, which is what makes within-document deduplication and
|
|
224
|
+
`find_tokens_in_text()` (used by `unmask()` to locate placeholders) work.
|
|
225
|
+
It also computes an unsalted `value_hash` used purely for storage-side
|
|
226
|
+
deduplication, so multiple engine instances backed by the same storage
|
|
227
|
+
can recognise a value they've each seen before, even though their
|
|
228
|
+
salted tokens differ.
|
|
229
|
+
|
|
230
|
+
**`AESGCMCipher`** (`pii_shield.crypto`) is the only component that ever
|
|
231
|
+
sees plaintext PII outside of the `NEREngine`. Each value is encrypted
|
|
232
|
+
with AES-256-GCM using a fresh 96-bit IV, with the entity type bound in
|
|
233
|
+
as additional authenticated data (AAD) — so a stored ciphertext can't be
|
|
234
|
+
replayed under a different entity type. Storage backends only ever
|
|
235
|
+
receive ciphertext, IV, and tag; they never see plaintext.
|
|
236
|
+
|
|
237
|
+
**`StorageBackend`** (`pii_shield.storage`) is an abstract interface with
|
|
238
|
+
five methods a backend must implement: `put`, `get`, `get_many`,
|
|
239
|
+
`find_by_value_hash`, `touch` (plus an optional `log_access` audit hook).
|
|
240
|
+
`PIIMaskingEngine` depends only on this interface, which is what makes
|
|
241
|
+
storage swappable without touching detection, tokenisation, or
|
|
242
|
+
encryption code. Four implementations ship out of the box:
|
|
243
|
+
|
|
244
|
+
| Backend | Persistence | Extra required | Notes |
|
|
245
|
+
|---|---|---|---|
|
|
246
|
+
| `InMemoryStorage` | None (process lifetime) | — | tests, short scripts |
|
|
247
|
+
| `FileSystemStorage` | Single JSON file, atomic writes | — | single-process, no external infra |
|
|
248
|
+
| `RedisStorage` | Redis hashes per token | `pii-shield[redis]` | shared across processes/hosts |
|
|
249
|
+
| `PostgresStorage` | Relational table, auto-migrated schema | `pii-shield[postgres]` | shared, queryable, audit-loggable |
|
|
250
|
+
|
|
251
|
+
Writing a fifth backend (S3, DynamoDB, Vault, etc.) means subclassing
|
|
252
|
+
`StorageBackend` and implementing those five methods — `PIIMaskingEngine`
|
|
253
|
+
needs no changes.
|
|
254
|
+
|
|
255
|
+
### Data flow
|
|
256
|
+
|
|
257
|
+
**mask()**: `NEREngine.detect()` finds spans → for each span, compute
|
|
258
|
+
`value_hash` and check the backend for an existing token in this `scope`
|
|
259
|
+
(dedup) → if new, `AESGCMCipher.encrypt()` the value → `StorageBackend.put()`
|
|
260
|
+
the ciphertext/IV/tag → splice the `{{TYPE:xxxxx}}` token into the text in
|
|
261
|
+
place of the original span.
|
|
262
|
+
|
|
263
|
+
**unmask()**: `DeterministicTokenGenerator.find_tokens_in_text()` locates
|
|
264
|
+
every placeholder → `StorageBackend.get_many()` fetches all matching
|
|
265
|
+
records in one round trip → `AESGCMCipher.decrypt()` each → splice the
|
|
266
|
+
decrypted values back into the text. Tokens with no matching record are
|
|
267
|
+
left in place with a `[UNRESOLVED]` suffix rather than raising, so a
|
|
268
|
+
partially-available vault degrades instead of failing the whole call.
|
|
269
|
+
|
|
270
|
+
**redact()**: `NEREngine.detect()` finds spans → each span is replaced
|
|
271
|
+
in-place with `[REDACTED:ENTITY_TYPE]`. Nothing downstream of detection
|
|
272
|
+
is invoked — no cipher, no storage — which is what makes it genuinely
|
|
273
|
+
irreversible rather than just "not currently reversed."
|
|
274
|
+
|
|
275
|
+
### Design choices worth knowing about
|
|
276
|
+
|
|
277
|
+
- **Encryption keys are supplied by the caller**, not derived from or
|
|
278
|
+
stored alongside vault data. This keeps a compromised storage backend
|
|
279
|
+
from being sufficient on its own to decrypt anything, and keeps key
|
|
280
|
+
rotation an application-level concern independent of which storage
|
|
281
|
+
backend you choose.
|
|
282
|
+
- **Detection is fully separated from storage.** You can swap
|
|
283
|
+
`InMemoryStorage` for `PostgresStorage` without changing anything about
|
|
284
|
+
how PII is found, and you can add spaCy/transformer layers without
|
|
285
|
+
touching storage at all.
|
|
286
|
+
- **All storage backend methods are `async`**, including `InMemoryStorage`
|
|
287
|
+
and `FileSystemStorage` — so the same calling code works unmodified
|
|
288
|
+
whether the backend is a Python dict or a networked database.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## Configuring detection
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
from pii_shield import NEREngine, PIIMaskingEngine
|
|
296
|
+
from pii_shield.storage import InMemoryStorage
|
|
297
|
+
|
|
298
|
+
ner = NEREngine(
|
|
299
|
+
enable_spacy=True, # PERSON / ORGANISATION / ADDRESS
|
|
300
|
+
spacy_model="en_core_web_sm",
|
|
301
|
+
enable_privacy_filter=True, # any HF token-classification model
|
|
302
|
+
privacy_filter_model="your/token-classification-model",
|
|
303
|
+
privacy_filter_threshold=0.5,
|
|
304
|
+
privacy_filter_device="cpu",
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
engine = PIIMaskingEngine(storage=InMemoryStorage(), ner_engine=ner)
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`NEREngine()` with no arguments runs regex detection only, with zero
|
|
311
|
+
extra dependencies.
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## Encryption key
|
|
316
|
+
|
|
317
|
+
```python
|
|
318
|
+
from pii_shield.crypto import AESGCMCipher
|
|
319
|
+
|
|
320
|
+
engine = PIIMaskingEngine(storage=..., encryption_key="<64-char hex string>")
|
|
321
|
+
# or
|
|
322
|
+
engine = PIIMaskingEngine(storage=..., encryption_key=AESGCMCipher.generate_key())
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
If you omit `encryption_key`, an ephemeral one is generated and a warning
|
|
326
|
+
is logged — anything masked in that session becomes permanently
|
|
327
|
+
unrecoverable once the process exits. Always pass a stable key outside of
|
|
328
|
+
quick experiments; losing the key makes every previously masked value
|
|
329
|
+
permanently unrecoverable, by design.
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Storage backend examples
|
|
334
|
+
|
|
335
|
+
```python
|
|
336
|
+
from pii_shield.storage import InMemoryStorage, FileSystemStorage, RedisStorage, PostgresStorage
|
|
337
|
+
|
|
338
|
+
InMemoryStorage()
|
|
339
|
+
FileSystemStorage("./vault.json")
|
|
340
|
+
RedisStorage("redis://localhost:6379/0")
|
|
341
|
+
PostgresStorage("postgresql://user:pass@host:5432/mydb") # creates its own schema on connect()
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Custom backend:
|
|
345
|
+
|
|
346
|
+
```python
|
|
347
|
+
from pii_shield.storage import StorageBackend
|
|
348
|
+
from pii_shield.types import TokenRecord
|
|
349
|
+
|
|
350
|
+
class MyBackend(StorageBackend):
|
|
351
|
+
async def put(self, record: TokenRecord) -> None: ...
|
|
352
|
+
async def get(self, token_value: str) -> TokenRecord | None: ...
|
|
353
|
+
async def get_many(self, token_values: list[str]) -> dict[str, TokenRecord]: ...
|
|
354
|
+
async def find_by_value_hash(self, value_hash: str, scope: str | None) -> str | None: ...
|
|
355
|
+
async def touch(self, token_value: str) -> None: ...
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## Error handling
|
|
361
|
+
|
|
362
|
+
```python
|
|
363
|
+
from pii_shield import (
|
|
364
|
+
PIIShieldError, # base class for everything below
|
|
365
|
+
EngineNotInitialisedError, # mask()/unmask() called before initialise()
|
|
366
|
+
DecryptionError, # AES-GCM tag verification failed
|
|
367
|
+
StorageBackendError, # backend-specific I/O/connection failure
|
|
368
|
+
OptionalDependencyMissingError, # used a backend/layer without its extra installed
|
|
369
|
+
)
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
## Running the tests
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
pip install -e ".[dev]"
|
|
376
|
+
pytest
|
|
377
|
+
```
|