pydantic-anonymizer 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.
- pydantic_anonymizer-0.1.0/LICENSE +21 -0
- pydantic_anonymizer-0.1.0/PKG-INFO +270 -0
- pydantic_anonymizer-0.1.0/README.md +245 -0
- pydantic_anonymizer-0.1.0/README_en.md +245 -0
- pydantic_anonymizer-0.1.0/pydantic_anonymizer/__init__.py +4 -0
- pydantic_anonymizer-0.1.0/pydantic_anonymizer/anonymizer.py +59 -0
- pydantic_anonymizer-0.1.0/pydantic_anonymizer/masks.py +66 -0
- pydantic_anonymizer-0.1.0/pydantic_anonymizer/registry.py +24 -0
- pydantic_anonymizer-0.1.0/pydantic_anonymizer.egg-info/PKG-INFO +270 -0
- pydantic_anonymizer-0.1.0/pydantic_anonymizer.egg-info/SOURCES.txt +14 -0
- pydantic_anonymizer-0.1.0/pydantic_anonymizer.egg-info/dependency_links.txt +1 -0
- pydantic_anonymizer-0.1.0/pydantic_anonymizer.egg-info/requires.txt +2 -0
- pydantic_anonymizer-0.1.0/pydantic_anonymizer.egg-info/top_level.txt +1 -0
- pydantic_anonymizer-0.1.0/pyproject.toml +40 -0
- pydantic_anonymizer-0.1.0/setup.cfg +4 -0
- pydantic_anonymizer-0.1.0/tests/test_anonymizer.py +172 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 drawiks
|
|
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,270 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pydantic-anonymizer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: anonymize sensitive data in pydantic models
|
|
5
|
+
Author-email: drawiks <drawksr@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/drawiks/pydantic-anonymizer
|
|
8
|
+
Project-URL: Repository, https://github.com/drawiks/pydantic-anonymizer
|
|
9
|
+
Project-URL: Issues, https://github.com/drawiks/pydantic-anonymizer/issues
|
|
10
|
+
Keywords: pydantic,anonymizer,masking,privacy,gdpr
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: pydantic>=2.0
|
|
23
|
+
Requires-Dist: phonenumbers>=8.13
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
<div align="center">
|
|
27
|
+
<h1>π pydantic-anonymizer</h1>
|
|
28
|
+
<a href="https://pypi.org/project/pydantic-anonymizer/">
|
|
29
|
+
<img alt="PyPI version" src="https://img.shields.io/pypi/v/pydantic-anonymizer?color=blue">
|
|
30
|
+
</a>
|
|
31
|
+
<img height="20" alt="Python 3.9+" src="https://img.shields.io/badge/python-3.9+-blue">
|
|
32
|
+
<img height="20" alt="License MIT" src="https://img.shields.io/badge/license-MIT-green">
|
|
33
|
+
<img height="20" alt="Status" src="https://img.shields.io/badge/status-stable-brightgreen">
|
|
34
|
+
<p><strong>mask sensitive data in Pydantic models</strong></p>
|
|
35
|
+
<p><a href="README.md">Π ΡΡΡΠΊΠΈΠΉ</a></p>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
,--. ,--. ,--.
|
|
42
|
+
,---.,--. ,--.,-| | ,--,--.,--,--, ,-' '-.`--' ,---.
|
|
43
|
+
| .-. |\ ' /' .-. |' ,-. || \'-. .-',--.| .--'
|
|
44
|
+
| '-' ' \ ' \ `-' |\ '-' || || | | | | |\ `--.
|
|
45
|
+
| |-'.-' / `---' `--`--'`--''--' `--' `--' `---'
|
|
46
|
+
`--' `---'
|
|
47
|
+
|
|
48
|
+
,--.
|
|
49
|
+
,--,--.,--,--, ,---. ,--,--, ,--. ,--.,--,--,--.`--',-----. ,---. ,--.--.
|
|
50
|
+
' ,-. || \| .-. || \ \ ' / | |,--.`-. / | .-. :| .--'
|
|
51
|
+
\ '-' || || |' '-' '| || | \ ' | | | || | / `-.\ --.| |
|
|
52
|
+
`--`--'`--''--' `---' `--''--'.-' / `--`--`--'`--'`-----' `----'`--'
|
|
53
|
+
`---'
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## **π¦ installation**
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install pydantic-anonymizer
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## **π quick start**
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from pydantic import BaseModel, Field
|
|
68
|
+
from pydantic_anonymizer import Anonymizer
|
|
69
|
+
|
|
70
|
+
class UserProfile(BaseModel, Anonymizer):
|
|
71
|
+
username: str
|
|
72
|
+
email: str = Field(json_schema_extra={"anonymize": True})
|
|
73
|
+
card_number: str = Field(json_schema_extra={"anonymize": "card"})
|
|
74
|
+
phone_number: str = Field(json_schema_extra={"anonymize": "phone"})
|
|
75
|
+
|
|
76
|
+
user = UserProfile(
|
|
77
|
+
username="ivan_dev",
|
|
78
|
+
email="ivan@mail.com",
|
|
79
|
+
card_number="4242111122223333",
|
|
80
|
+
phone_number="+380500223785"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
print(user.model_dump())
|
|
84
|
+
# {'username': 'ivan_dev', 'email': 'ivan@mail.com', 'card_number': '4242111122223333', 'phone_number': '+380500223785'}
|
|
85
|
+
|
|
86
|
+
print(user.model_dump_anonymized())
|
|
87
|
+
# {'username': 'ivan_dev', 'email': 'i***@***.com', 'card_number': '4242-****-****-3333', 'phone_number': '+380 (***) ***-**-85'}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## **π§© features**
|
|
93
|
+
|
|
94
|
+
- π **automatic masking** - configure via `json_schema_extra` in model fields
|
|
95
|
+
- π§ **generic masking** - partial mask for emails and text (`i***@***.com`)
|
|
96
|
+
- π³ **card masking** - format `4242-****-****-3333`
|
|
97
|
+
- π± **phone masking** - correct country code parsing with [phonenumbers](https://github.com/daviddryden/python-phonenumbers)
|
|
98
|
+
- ποΈ **nested models** - recursive processing of nested Pydantic models
|
|
99
|
+
- π **lists** - support for `list[Model]` with masking of each element
|
|
100
|
+
- π οΈ **custom strategies** - your own masking functions via `MaskRegistry`
|
|
101
|
+
- β
**reliable** - 27 tests covering all scenarios
|
|
102
|
+
- πͺΆ **minimal dependencies** - only `pydantic>=2.0` and `phonenumbers>=8.13`
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## **π usage**
|
|
107
|
+
|
|
108
|
+
### basic usage
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from pydantic import BaseModel, Field
|
|
112
|
+
from pydantic_anonymizer import Anonymizer
|
|
113
|
+
|
|
114
|
+
class User(BaseModel, Anonymizer):
|
|
115
|
+
name: str
|
|
116
|
+
email: str = Field(json_schema_extra={"anonymize": True})
|
|
117
|
+
|
|
118
|
+
user = User(name="ivan", email="ivan@mail.com")
|
|
119
|
+
|
|
120
|
+
# original data
|
|
121
|
+
user.model_dump() # {'name': 'ivan', 'email': 'ivan@mail.com'}
|
|
122
|
+
|
|
123
|
+
# masked data
|
|
124
|
+
user.model_dump_anonymized() # {'name': 'ivan', 'email': 'i***@***.com'}
|
|
125
|
+
|
|
126
|
+
# JSON string
|
|
127
|
+
user.model_dump_json_anonymized() # '{"name": "ivan", "email": "i***@***.com"}'
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### nested models
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from pydantic import BaseModel, Field
|
|
134
|
+
from pydantic_anonymizer import Anonymizer
|
|
135
|
+
|
|
136
|
+
class Address(BaseModel, Anonymizer):
|
|
137
|
+
city: str
|
|
138
|
+
street: str = Field(json_schema_extra={"anonymize": True})
|
|
139
|
+
|
|
140
|
+
class UserProfile(BaseModel, Anonymizer):
|
|
141
|
+
name: str
|
|
142
|
+
address: Address
|
|
143
|
+
|
|
144
|
+
user = UserProfile(
|
|
145
|
+
name="ivan",
|
|
146
|
+
address=Address(city="Moscow", street="Lenina 1")
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
result = user.model_dump_anonymized()
|
|
150
|
+
# {'name': 'ivan', 'address': {'city': 'Moscow', 'street': 'L***a 1'}}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### lists of models
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from pydantic import BaseModel, Field
|
|
157
|
+
from pydantic_anonymizer import Anonymizer
|
|
158
|
+
|
|
159
|
+
class Card(BaseModel, Anonymizer):
|
|
160
|
+
number: str = Field(json_schema_extra={"anonymize": "card"})
|
|
161
|
+
|
|
162
|
+
class Wallet(BaseModel, Anonymizer):
|
|
163
|
+
cards: list[Card]
|
|
164
|
+
|
|
165
|
+
wallet = Wallet(cards=[
|
|
166
|
+
Card(number="4242111122223333"),
|
|
167
|
+
Card(number="5555666677778888")
|
|
168
|
+
])
|
|
169
|
+
|
|
170
|
+
result = wallet.model_dump_anonymized()
|
|
171
|
+
# {'cards': [{'number': '4242-****-****-3333'}, {'number': '5555-****-****-8888'}]}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### custom strategies
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from pydantic import BaseModel, Field
|
|
178
|
+
from pydantic_anonymizer import Anonymizer, MaskRegistry
|
|
179
|
+
|
|
180
|
+
# register your own masking function
|
|
181
|
+
def mask_ssn(value: str) -> str:
|
|
182
|
+
return "***-**-" + value[-4:]
|
|
183
|
+
|
|
184
|
+
MaskRegistry.register("ssn", mask_ssn)
|
|
185
|
+
|
|
186
|
+
class Person(BaseModel, Anonymizer):
|
|
187
|
+
ssn: str = Field(json_schema_extra={"anonymize": "ssn"})
|
|
188
|
+
|
|
189
|
+
person = Person(ssn="123-45-6789")
|
|
190
|
+
person.model_dump_anonymized() # {'ssn': '***-**-6789'}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## **π built-in strategies**
|
|
196
|
+
|
|
197
|
+
| Strategy | Field | Input | Output |
|
|
198
|
+
|----------|-------|-------|--------|
|
|
199
|
+
| `True` (generic) | email | `ivan@mail.com` | `i***@***.com` |
|
|
200
|
+
| `"card"` | card number | `4242111122223333` | `4242-****-****-3333` |
|
|
201
|
+
| `"phone"` | phone | `+380500223785` | `+380 (***) ***-**-85` |
|
|
202
|
+
|
|
203
|
+
### country code support
|
|
204
|
+
|
|
205
|
+
phone masking works correctly with any country codes:
|
|
206
|
+
|
|
207
|
+
| Country | Code | Example |
|
|
208
|
+
|---------|------|---------|
|
|
209
|
+
| Ukraine | +380 | `+380500223785` β `+380 (***) ***-**-85` |
|
|
210
|
+
| USA | +1 | `+14155552671` β `+1 (***) ***-**-71` |
|
|
211
|
+
| UK | +44 | `+447911123456` β `+44 (***) ***-**-56` |
|
|
212
|
+
| Russia | +7 | `+79161234567` β `+7 (***) ***-**-67` |
|
|
213
|
+
| Germany | +49 | `+4915112345678` β `+49 (***) ***-**-78` |
|
|
214
|
+
| China | +86 | `+8613812345678` β `+86 (***) ***-**-78` |
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## **π examples**
|
|
219
|
+
|
|
220
|
+
### FastAPI logging
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
from fastapi import FastAPI
|
|
224
|
+
from pydantic import BaseModel, Field
|
|
225
|
+
from pydantic_anonymizer import Anonymizer
|
|
226
|
+
|
|
227
|
+
app = FastAPI()
|
|
228
|
+
|
|
229
|
+
class UserCreate(BaseModel, Anonymizer):
|
|
230
|
+
username: str
|
|
231
|
+
email: str = Field(json_schema_extra={"anonymize": True})
|
|
232
|
+
card_number: str = Field(json_schema_extra={"anonymize": "card"})
|
|
233
|
+
|
|
234
|
+
@app.post("/users")
|
|
235
|
+
def create_user(user: UserCreate):
|
|
236
|
+
# log with masking
|
|
237
|
+
print(user.model_dump_anonymized())
|
|
238
|
+
# {'username': 'admin', 'email': 'a***@***.com', 'card_number': '4242-****-****-3333'}
|
|
239
|
+
|
|
240
|
+
# original data for processing
|
|
241
|
+
return user.model_dump()
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### safe logging
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
import logging
|
|
248
|
+
from pydantic import BaseModel, Field
|
|
249
|
+
from pydantic_anonymizer import Anonymizer
|
|
250
|
+
|
|
251
|
+
logging.basicConfig(level=logging.INFO)
|
|
252
|
+
logger = logging.getLogger(__name__)
|
|
253
|
+
|
|
254
|
+
class Payment(BaseModel, Anonymizer):
|
|
255
|
+
card_number: str = Field(json_schema_extra={"anonymize": "card"})
|
|
256
|
+
amount: float
|
|
257
|
+
|
|
258
|
+
def process_payment(payment: Payment):
|
|
259
|
+
# safe to log - card number is masked
|
|
260
|
+
logger.info("payment: %s", payment.model_dump_anonymized())
|
|
261
|
+
|
|
262
|
+
# work with original data
|
|
263
|
+
charge_card(payment.card_number, payment.amount)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## **π license**
|
|
269
|
+
|
|
270
|
+
[MIT](https://github.com/drawiks/pydantic-anonymizer/blob/main/LICENSE)
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1>π pydantic-anonymizer</h1>
|
|
3
|
+
<a href="https://pypi.org/project/pydantic-anonymizer/">
|
|
4
|
+
<img alt="PyPI version" src="https://img.shields.io/pypi/v/pydantic-anonymizer?color=blue">
|
|
5
|
+
</a>
|
|
6
|
+
<img height="20" alt="Python 3.9+" src="https://img.shields.io/badge/python-3.9+-blue">
|
|
7
|
+
<img height="20" alt="License MIT" src="https://img.shields.io/badge/license-MIT-green">
|
|
8
|
+
<img height="20" alt="Status" src="https://img.shields.io/badge/status-stable-brightgreen">
|
|
9
|
+
<p><strong>ΠΌΠ°ΡΠΊΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ ΠΏΠ΅ΡΡΠΎΠ½Π°Π»ΡΠ½ΡΡ
Π΄Π°Π½Π½ΡΡ
Π² Pydantic ΠΌΠΎΠ΄Π΅Π»ΡΡ
</strong></p>
|
|
10
|
+
<p><a href="README_en.md">English</a></p>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
,--. ,--. ,--.
|
|
17
|
+
,---.,--. ,--.,-| | ,--,--.,--,--, ,-' '-.`--' ,---.
|
|
18
|
+
| .-. |\ ' /' .-. |' ,-. || \'-. .-',--.| .--'
|
|
19
|
+
| '-' ' \ ' \ `-' |\ '-' || || | | | | |\ `--.
|
|
20
|
+
| |-'.-' / `---' `--`--'`--''--' `--' `--' `---'
|
|
21
|
+
`--' `---'
|
|
22
|
+
|
|
23
|
+
,--.
|
|
24
|
+
,--,--.,--,--, ,---. ,--,--, ,--. ,--.,--,--,--.`--',-----. ,---. ,--.--.
|
|
25
|
+
' ,-. || \| .-. || \ \ ' / | |,--.`-. / | .-. :| .--'
|
|
26
|
+
\ '-' || || |' '-' '| || | \ ' | | | || | / `-.\ --.| |
|
|
27
|
+
`--`--'`--''--' `---' `--''--'.-' / `--`--`--'`--'`-----' `----'`--'
|
|
28
|
+
`---'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## **π¦ ΡΡΡΠ°Π½ΠΎΠ²ΠΊΠ°**
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install pydantic-anonymizer
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## **π Π±ΡΡΡΡΡΠΉ ΡΡΠ°ΡΡ**
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from pydantic import BaseModel, Field
|
|
43
|
+
from pydantic_anonymizer import Anonymizer
|
|
44
|
+
|
|
45
|
+
class UserProfile(BaseModel, Anonymizer):
|
|
46
|
+
username: str
|
|
47
|
+
email: str = Field(json_schema_extra={"anonymize": True})
|
|
48
|
+
card_number: str = Field(json_schema_extra={"anonymize": "card"})
|
|
49
|
+
phone_number: str = Field(json_schema_extra={"anonymize": "phone"})
|
|
50
|
+
|
|
51
|
+
user = UserProfile(
|
|
52
|
+
username="ivan_dev",
|
|
53
|
+
email="ivan@mail.com",
|
|
54
|
+
card_number="4242111122223333",
|
|
55
|
+
phone_number="+380500223785"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
print(user.model_dump())
|
|
59
|
+
# {'username': 'ivan_dev', 'email': 'ivan@mail.com', 'card_number': '4242111122223333', 'phone_number': '+380500223785'}
|
|
60
|
+
|
|
61
|
+
print(user.model_dump_anonymized())
|
|
62
|
+
# {'username': 'ivan_dev', 'email': 'i***@***.com', 'card_number': '4242-****-****-3333', 'phone_number': '+380 (***) ***-**-85'}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## **π§© Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡΠΈ**
|
|
68
|
+
|
|
69
|
+
- π **Π°Π²ΡΠΎΠΌΠ°ΡΠΈΡΠ΅ΡΠΊΠ°Ρ ΠΌΠ°ΡΠΊΠΈΡΠΎΠ²ΠΊΠ°** - Π½Π°ΡΡΡΠΎΠΉΠΊΠ° ΡΠ΅ΡΠ΅Π· `json_schema_extra` Π² ΠΏΠΎΠ»ΡΡ
ΠΌΠΎΠ΄Π΅Π»ΠΈ
|
|
70
|
+
- π§ **generic ΠΌΠ°ΡΠΊΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅** - ΡΠ°ΡΡΠΈΡΠ½Π°Ρ ΠΌΠ°ΡΠΊΠ° Π΄Π»Ρ email ΠΈ ΡΠ΅ΠΊΡΡΠ° (`i***@***.com`)
|
|
71
|
+
- π³ **ΠΌΠ°ΡΠΊΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ ΠΊΠ°ΡΡ** - ΡΠΎΡΠΌΠ°Ρ `4242-****-****-3333`
|
|
72
|
+
- π± **ΠΌΠ°ΡΠΊΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ ΡΠ΅Π»Π΅ΡΠΎΠ½ΠΎΠ²** - ΠΊΠΎΡΡΠ΅ΠΊΡΠ½ΡΠΉ ΠΏΠ°ΡΡΠΈΠ½Π³ ΠΊΠΎΠ΄ΠΎΠ² ΡΡΡΠ°Π½ Π±Π»Π°Π³ΠΎΠ΄Π°ΡΡ [phonenumbers](https://github.com/daviddryden/python-phonenumbers)
|
|
73
|
+
- ποΈ **Π²Π»ΠΎΠΆΠ΅Π½Π½ΡΠ΅ ΠΌΠΎΠ΄Π΅Π»ΠΈ** - ΡΠ΅ΠΊΡΡΡΠΈΠ²Π½Π°Ρ ΠΎΠ±ΡΠ°Π±ΠΎΡΠΊΠ° Π²Π»ΠΎΠΆΠ΅Π½Π½ΡΡ
Pydantic ΠΌΠΎΠ΄Π΅Π»Π΅ΠΉ
|
|
74
|
+
- π **ΡΠΏΠΈΡΠΊΠΈ** - ΠΏΠΎΠ΄Π΄Π΅ΡΠΆΠΊΠ° `list[Model]` Ρ ΠΌΠ°ΡΠΊΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ΠΌ ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ ΡΠ»Π΅ΠΌΠ΅Π½ΡΠ°
|
|
75
|
+
- π οΈ **ΠΊΠ°ΡΡΠΎΠΌΠ½ΡΠ΅ ΡΡΡΠ°ΡΠ΅Π³ΠΈΠΈ** - ΡΠΎΠ±ΡΡΠ²Π΅Π½Π½ΡΠ΅ ΡΡΠ½ΠΊΡΠΈΠΈ ΠΌΠ°ΡΠΊΠΈΡΠΎΠ²Π°Π½ΠΈΡ ΡΠ΅ΡΠ΅Π· `MaskRegistry`
|
|
76
|
+
- β
**Π½Π°Π΄ΡΠΆΠ½ΠΎΡΡΡ** - 27 ΡΠ΅ΡΡΠΎΠ² ΠΏΠΎΠΊΡΡΠ²Π°ΡΡ Π²ΡΠ΅ ΡΡΠ΅Π½Π°ΡΠΈΠΈ
|
|
77
|
+
- πͺΆ **ΠΌΠΈΠ½ΠΈΠΌΡΠΌ Π·Π°Π²ΠΈΡΠΈΠΌΠΎΡΡΠ΅ΠΉ** - ΡΠΎΠ»ΡΠΊΠΎ `pydantic>=2.0` ΠΈ `phonenumbers>=8.13`
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## **π ΠΈΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°Π½ΠΈΠ΅**
|
|
82
|
+
|
|
83
|
+
### Π±Π°Π·ΠΎΠ²ΠΎΠ΅ ΠΈΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°Π½ΠΈΠ΅
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from pydantic import BaseModel, Field
|
|
87
|
+
from pydantic_anonymizer import Anonymizer
|
|
88
|
+
|
|
89
|
+
class User(BaseModel, Anonymizer):
|
|
90
|
+
name: str
|
|
91
|
+
email: str = Field(json_schema_extra={"anonymize": True})
|
|
92
|
+
|
|
93
|
+
user = User(name="ivan", email="ivan@mail.com")
|
|
94
|
+
|
|
95
|
+
# ΠΎΡΠΈΠ³ΠΈΠ½Π°Π»ΡΠ½ΡΠ΅ Π΄Π°Π½Π½ΡΠ΅
|
|
96
|
+
user.model_dump() # {'name': 'ivan', 'email': 'ivan@mail.com'}
|
|
97
|
+
|
|
98
|
+
# Π·Π°ΠΌΠ°ΡΠΊΠΈΡΠΎΠ²Π°Π½Π½ΡΠ΅ Π΄Π°Π½Π½ΡΠ΅
|
|
99
|
+
user.model_dump_anonymized() # {'name': 'ivan', 'email': 'i***@***.com'}
|
|
100
|
+
|
|
101
|
+
# JSON ΡΡΡΠΎΠΊΠ°
|
|
102
|
+
user.model_dump_json_anonymized() # '{"name": "ivan", "email": "i***@***.com"}'
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Π²Π»ΠΎΠΆΠ΅Π½Π½ΡΠ΅ ΠΌΠΎΠ΄Π΅Π»ΠΈ
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from pydantic import BaseModel, Field
|
|
109
|
+
from pydantic_anonymizer import Anonymizer
|
|
110
|
+
|
|
111
|
+
class Address(BaseModel, Anonymizer):
|
|
112
|
+
city: str
|
|
113
|
+
street: str = Field(json_schema_extra={"anonymize": True})
|
|
114
|
+
|
|
115
|
+
class UserProfile(BaseModel, Anonymizer):
|
|
116
|
+
name: str
|
|
117
|
+
address: Address
|
|
118
|
+
|
|
119
|
+
user = UserProfile(
|
|
120
|
+
name="ivan",
|
|
121
|
+
address=Address(city="Moscow", street="Lenina 1")
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
result = user.model_dump_anonymized()
|
|
125
|
+
# {'name': 'ivan', 'address': {'city': 'Moscow', 'street': 'L***a 1'}}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### ΡΠΏΠΈΡΠΊΠΈ ΠΌΠΎΠ΄Π΅Π»Π΅ΠΉ
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from pydantic import BaseModel, Field
|
|
132
|
+
from pydantic_anonymizer import Anonymizer
|
|
133
|
+
|
|
134
|
+
class Card(BaseModel, Anonymizer):
|
|
135
|
+
number: str = Field(json_schema_extra={"anonymize": "card"})
|
|
136
|
+
|
|
137
|
+
class Wallet(BaseModel, Anonymizer):
|
|
138
|
+
cards: list[Card]
|
|
139
|
+
|
|
140
|
+
wallet = Wallet(cards=[
|
|
141
|
+
Card(number="4242111122223333"),
|
|
142
|
+
Card(number="5555666677778888")
|
|
143
|
+
])
|
|
144
|
+
|
|
145
|
+
result = wallet.model_dump_anonymized()
|
|
146
|
+
# {'cards': [{'number': '4242-****-****-3333'}, {'number': '5555-****-****-8888'}]}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### ΠΊΠ°ΡΡΠΎΠΌΠ½ΡΠ΅ ΡΡΡΠ°ΡΠ΅Π³ΠΈΠΈ
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from pydantic import BaseModel, Field
|
|
153
|
+
from pydantic_anonymizer import Anonymizer, MaskRegistry
|
|
154
|
+
|
|
155
|
+
# ΡΠ΅Π³ΠΈΡΡΡΠ°ΡΠΈΡθͺε·±η ΡΡΠ½ΠΊΡΠΈΠΈ ΠΌΠ°ΡΠΊΠΈΡΠΎΠ²Π°Π½ΠΈΡ
|
|
156
|
+
def mask_ssn(value: str) -> str:
|
|
157
|
+
return "***-**-" + value[-4:]
|
|
158
|
+
|
|
159
|
+
MaskRegistry.register("ssn", mask_ssn)
|
|
160
|
+
|
|
161
|
+
class Person(BaseModel, Anonymizer):
|
|
162
|
+
ssn: str = Field(json_schema_extra={"anonymize": "ssn"})
|
|
163
|
+
|
|
164
|
+
person = Person(ssn="123-45-6789")
|
|
165
|
+
person.model_dump_anonymized() # {'ssn': '***-**-6789'}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## **π Π²ΡΡΡΠΎΠ΅Π½Π½ΡΠ΅ ΡΡΡΠ°ΡΠ΅Π³ΠΈΠΈ**
|
|
171
|
+
|
|
172
|
+
| Π‘ΡΡΠ°ΡΠ΅Π³ΠΈΡ | ΠΠΎΠ»Π΅ | ΠΡ
ΠΎΠ΄ | ΠΡΡ
ΠΎΠ΄ |
|
|
173
|
+
|-----------|------|------|-------|
|
|
174
|
+
| `True` (generic) | email | `ivan@mail.com` | `i***@***.com` |
|
|
175
|
+
| `"card"` | Π½ΠΎΠΌΠ΅Ρ ΠΊΠ°ΡΡΡ | `4242111122223333` | `4242-****-****-3333` |
|
|
176
|
+
| `"phone"` | ΡΠ΅Π»Π΅ΡΠΎΠ½ | `+380500223785` | `+380 (***) ***-**-85` |
|
|
177
|
+
|
|
178
|
+
### ΠΏΠΎΠ΄Π΄Π΅ΡΠΆΠΊΠ° ΠΊΠΎΠ΄ΠΎΠ² ΡΡΡΠ°Π½
|
|
179
|
+
|
|
180
|
+
ΠΌΠ°ΡΠΊΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ ΡΠ΅Π»Π΅ΡΠΎΠ½ΠΎΠ² ΠΊΠΎΡΡΠ΅ΠΊΡΠ½ΠΎ ΡΠ°Π±ΠΎΡΠ°Π΅Ρ Ρ Π»ΡΠ±ΡΠΌΠΈ ΠΊΠΎΠ΄Π°ΠΌΠΈ ΡΡΡΠ°Π½:
|
|
181
|
+
|
|
182
|
+
| Π‘ΡΡΠ°Π½Π° | ΠΠΎΠ΄ | ΠΡΠΈΠΌΠ΅Ρ |
|
|
183
|
+
|--------|-----|--------|
|
|
184
|
+
| Π£ΠΊΡΠ°ΠΈΠ½Π° | +380 | `+380500223785` β `+380 (***) ***-**-85` |
|
|
185
|
+
| Π‘Π¨Π | +1 | `+14155552671` β `+1 (***) ***-**-71` |
|
|
186
|
+
| ΠΠ΅Π»ΠΈΠΊΠΎΠ±ΡΠΈΡΠ°Π½ΠΈΡ | +44 | `+447911123456` β `+44 (***) ***-**-56` |
|
|
187
|
+
| Π ΠΎΡΡΠΈΡ | +7 | `+79161234567` β `+7 (***) ***-**-67` |
|
|
188
|
+
| ΠΠ΅ΡΠΌΠ°Π½ΠΈΡ | +49 | `+4915112345678` β `+49 (***) ***-**-78` |
|
|
189
|
+
| ΠΠΈΡΠ°ΠΉ | +86 | `+8613812345678` β `+86 (***) ***-**-78` |
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## **π ΠΏΡΠΈΠΌΠ΅ΡΡ**
|
|
194
|
+
|
|
195
|
+
### Π»ΠΎΠ³ΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ Π² FastAPI
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
from fastapi import FastAPI
|
|
199
|
+
from pydantic import BaseModel, Field
|
|
200
|
+
from pydantic_anonymizer import Anonymizer
|
|
201
|
+
|
|
202
|
+
app = FastAPI()
|
|
203
|
+
|
|
204
|
+
class UserCreate(BaseModel, Anonymizer):
|
|
205
|
+
username: str
|
|
206
|
+
email: str = Field(json_schema_extra={"anonymize": True})
|
|
207
|
+
card_number: str = Field(json_schema_extra={"anonymize": "card"})
|
|
208
|
+
|
|
209
|
+
@app.post("/users")
|
|
210
|
+
def create_user(user: UserCreate):
|
|
211
|
+
# Π»ΠΎΠ³ΠΈΡΡΠ΅ΠΌ Ρ ΠΌΠ°ΡΠΊΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ΠΌ
|
|
212
|
+
print(user.model_dump_anonymized())
|
|
213
|
+
# {'username': 'admin', 'email': 'a***@***.com', 'card_number': '4242-****-****-3333'}
|
|
214
|
+
|
|
215
|
+
# ΠΎΡΠΈΠ³ΠΈΠ½Π°Π»ΡΠ½ΡΠ΅ Π΄Π°Π½Π½ΡΠ΅ Π΄Π»Ρ ΠΎΠ±ΡΠ°Π±ΠΎΡΠΊΠΈ
|
|
216
|
+
return user.model_dump()
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Π±Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΠ΅ Π»ΠΎΠ³ΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
import logging
|
|
223
|
+
from pydantic import BaseModel, Field
|
|
224
|
+
from pydantic_anonymizer import Anonymizer
|
|
225
|
+
|
|
226
|
+
logging.basicConfig(level=logging.INFO)
|
|
227
|
+
logger = logging.getLogger(__name__)
|
|
228
|
+
|
|
229
|
+
class Payment(BaseModel, Anonymizer):
|
|
230
|
+
card_number: str = Field(json_schema_extra={"anonymize": "card"})
|
|
231
|
+
amount: float
|
|
232
|
+
|
|
233
|
+
def process_payment(payment: Payment):
|
|
234
|
+
# Π±Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎ Π»ΠΎΠ³ΠΈΡΡΠ΅ΠΌ - Π½ΠΎΠΌΠ΅Ρ ΠΊΠ°ΡΡΡ Π·Π°ΠΌΠ°ΡΠΊΠΈΡΠΎΠ²Π°Π½
|
|
235
|
+
logger.info("ΠΏΠ»Π°ΡΡΠΆ: %s", payment.model_dump_anonymized())
|
|
236
|
+
|
|
237
|
+
# ΡΠ°Π±ΠΎΡΠ°Π΅ΠΌ Ρ ΠΎΡΠΈΠ³ΠΈΠ½Π°Π»ΡΠ½ΡΠΌΠΈ Π΄Π°Π½Π½ΡΠΌΠΈ
|
|
238
|
+
charge_card(payment.card_number, payment.amount)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## **π Π»ΠΈΡΠ΅Π½Π·ΠΈΡ**
|
|
244
|
+
|
|
245
|
+
[MIT](https://github.com/drawiks/pydantic-anonymizer/blob/main/LICENSE)
|