fraiseql-confiture 0.3.7__cp311-cp311-macosx_11_0_arm64.whl
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.
- confiture/__init__.py +48 -0
- confiture/_core.cpython-311-darwin.so +0 -0
- confiture/cli/__init__.py +0 -0
- confiture/cli/dry_run.py +116 -0
- confiture/cli/lint_formatter.py +193 -0
- confiture/cli/main.py +1893 -0
- confiture/config/__init__.py +0 -0
- confiture/config/environment.py +263 -0
- confiture/core/__init__.py +51 -0
- confiture/core/anonymization/__init__.py +0 -0
- confiture/core/anonymization/audit.py +485 -0
- confiture/core/anonymization/benchmarking.py +372 -0
- confiture/core/anonymization/breach_notification.py +652 -0
- confiture/core/anonymization/compliance.py +617 -0
- confiture/core/anonymization/composer.py +298 -0
- confiture/core/anonymization/data_subject_rights.py +669 -0
- confiture/core/anonymization/factory.py +319 -0
- confiture/core/anonymization/governance.py +737 -0
- confiture/core/anonymization/performance.py +1092 -0
- confiture/core/anonymization/profile.py +284 -0
- confiture/core/anonymization/registry.py +195 -0
- confiture/core/anonymization/security/kms_manager.py +547 -0
- confiture/core/anonymization/security/lineage.py +888 -0
- confiture/core/anonymization/security/token_store.py +686 -0
- confiture/core/anonymization/strategies/__init__.py +41 -0
- confiture/core/anonymization/strategies/address.py +359 -0
- confiture/core/anonymization/strategies/credit_card.py +374 -0
- confiture/core/anonymization/strategies/custom.py +161 -0
- confiture/core/anonymization/strategies/date.py +218 -0
- confiture/core/anonymization/strategies/differential_privacy.py +398 -0
- confiture/core/anonymization/strategies/email.py +141 -0
- confiture/core/anonymization/strategies/format_preserving_encryption.py +310 -0
- confiture/core/anonymization/strategies/hash.py +150 -0
- confiture/core/anonymization/strategies/ip_address.py +235 -0
- confiture/core/anonymization/strategies/masking_retention.py +252 -0
- confiture/core/anonymization/strategies/name.py +298 -0
- confiture/core/anonymization/strategies/phone.py +119 -0
- confiture/core/anonymization/strategies/preserve.py +85 -0
- confiture/core/anonymization/strategies/redact.py +101 -0
- confiture/core/anonymization/strategies/salted_hashing.py +322 -0
- confiture/core/anonymization/strategies/text_redaction.py +183 -0
- confiture/core/anonymization/strategies/tokenization.py +334 -0
- confiture/core/anonymization/strategy.py +241 -0
- confiture/core/anonymization/syncer_audit.py +357 -0
- confiture/core/blue_green.py +683 -0
- confiture/core/builder.py +500 -0
- confiture/core/checksum.py +358 -0
- confiture/core/connection.py +184 -0
- confiture/core/differ.py +522 -0
- confiture/core/drift.py +564 -0
- confiture/core/dry_run.py +182 -0
- confiture/core/health.py +313 -0
- confiture/core/hooks/__init__.py +87 -0
- confiture/core/hooks/base.py +232 -0
- confiture/core/hooks/context.py +146 -0
- confiture/core/hooks/execution_strategies.py +57 -0
- confiture/core/hooks/observability.py +220 -0
- confiture/core/hooks/phases.py +53 -0
- confiture/core/hooks/registry.py +295 -0
- confiture/core/large_tables.py +775 -0
- confiture/core/linting/__init__.py +70 -0
- confiture/core/linting/composer.py +192 -0
- confiture/core/linting/libraries/__init__.py +17 -0
- confiture/core/linting/libraries/gdpr.py +168 -0
- confiture/core/linting/libraries/general.py +184 -0
- confiture/core/linting/libraries/hipaa.py +144 -0
- confiture/core/linting/libraries/pci_dss.py +104 -0
- confiture/core/linting/libraries/sox.py +120 -0
- confiture/core/linting/schema_linter.py +491 -0
- confiture/core/linting/versioning.py +151 -0
- confiture/core/locking.py +389 -0
- confiture/core/migration_generator.py +298 -0
- confiture/core/migrator.py +882 -0
- confiture/core/observability/__init__.py +44 -0
- confiture/core/observability/audit.py +323 -0
- confiture/core/observability/logging.py +187 -0
- confiture/core/observability/metrics.py +174 -0
- confiture/core/observability/tracing.py +192 -0
- confiture/core/pg_version.py +418 -0
- confiture/core/pool.py +406 -0
- confiture/core/risk/__init__.py +39 -0
- confiture/core/risk/predictor.py +188 -0
- confiture/core/risk/scoring.py +248 -0
- confiture/core/rollback_generator.py +388 -0
- confiture/core/schema_analyzer.py +769 -0
- confiture/core/schema_to_schema.py +590 -0
- confiture/core/security/__init__.py +32 -0
- confiture/core/security/logging.py +201 -0
- confiture/core/security/validation.py +416 -0
- confiture/core/signals.py +371 -0
- confiture/core/syncer.py +540 -0
- confiture/exceptions.py +192 -0
- confiture/integrations/__init__.py +0 -0
- confiture/models/__init__.py +24 -0
- confiture/models/lint.py +193 -0
- confiture/models/migration.py +265 -0
- confiture/models/schema.py +203 -0
- confiture/models/sql_file_migration.py +225 -0
- confiture/scenarios/__init__.py +36 -0
- confiture/scenarios/compliance.py +586 -0
- confiture/scenarios/ecommerce.py +199 -0
- confiture/scenarios/financial.py +253 -0
- confiture/scenarios/healthcare.py +315 -0
- confiture/scenarios/multi_tenant.py +340 -0
- confiture/scenarios/saas.py +295 -0
- confiture/testing/FRAMEWORK_API.md +722 -0
- confiture/testing/__init__.py +100 -0
- confiture/testing/fixtures/__init__.py +11 -0
- confiture/testing/fixtures/data_validator.py +229 -0
- confiture/testing/fixtures/migration_runner.py +167 -0
- confiture/testing/fixtures/schema_snapshotter.py +352 -0
- confiture/testing/frameworks/__init__.py +10 -0
- confiture/testing/frameworks/mutation.py +587 -0
- confiture/testing/frameworks/performance.py +479 -0
- confiture/testing/loader.py +225 -0
- confiture/testing/pytest/__init__.py +38 -0
- confiture/testing/pytest_plugin.py +190 -0
- confiture/testing/sandbox.py +304 -0
- confiture/testing/utils/__init__.py +0 -0
- fraiseql_confiture-0.3.7.dist-info/METADATA +438 -0
- fraiseql_confiture-0.3.7.dist-info/RECORD +124 -0
- fraiseql_confiture-0.3.7.dist-info/WHEEL +4 -0
- fraiseql_confiture-0.3.7.dist-info/entry_points.txt +4 -0
- fraiseql_confiture-0.3.7.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Anonymization strategies module.
|
|
2
|
+
|
|
3
|
+
Provides standard PII anonymization strategies for common data types.
|
|
4
|
+
All strategies are automatically registered with the StrategyRegistry.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from confiture.core.anonymization.registry import StrategyRegistry
|
|
8
|
+
from confiture.core.anonymization.strategies.address import AddressStrategy
|
|
9
|
+
from confiture.core.anonymization.strategies.credit_card import CreditCardStrategy
|
|
10
|
+
from confiture.core.anonymization.strategies.custom import (
|
|
11
|
+
CustomLambdaStrategy,
|
|
12
|
+
CustomStrategy,
|
|
13
|
+
)
|
|
14
|
+
from confiture.core.anonymization.strategies.date import DateMaskingStrategy
|
|
15
|
+
from confiture.core.anonymization.strategies.ip_address import IPAddressStrategy
|
|
16
|
+
from confiture.core.anonymization.strategies.name import NameMaskingStrategy
|
|
17
|
+
from confiture.core.anonymization.strategies.preserve import PreserveStrategy
|
|
18
|
+
from confiture.core.anonymization.strategies.text_redaction import TextRedactionStrategy
|
|
19
|
+
|
|
20
|
+
# Register all strategies
|
|
21
|
+
StrategyRegistry.register("name", NameMaskingStrategy)
|
|
22
|
+
StrategyRegistry.register("date", DateMaskingStrategy)
|
|
23
|
+
StrategyRegistry.register("address", AddressStrategy)
|
|
24
|
+
StrategyRegistry.register("credit_card", CreditCardStrategy)
|
|
25
|
+
StrategyRegistry.register("ip_address", IPAddressStrategy)
|
|
26
|
+
StrategyRegistry.register("text_redaction", TextRedactionStrategy)
|
|
27
|
+
StrategyRegistry.register("preserve", PreserveStrategy)
|
|
28
|
+
StrategyRegistry.register("custom", CustomStrategy)
|
|
29
|
+
StrategyRegistry.register("custom_lambda", CustomLambdaStrategy)
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"NameMaskingStrategy",
|
|
33
|
+
"DateMaskingStrategy",
|
|
34
|
+
"AddressStrategy",
|
|
35
|
+
"CreditCardStrategy",
|
|
36
|
+
"IPAddressStrategy",
|
|
37
|
+
"TextRedactionStrategy",
|
|
38
|
+
"PreserveStrategy",
|
|
39
|
+
"CustomStrategy",
|
|
40
|
+
"CustomLambdaStrategy",
|
|
41
|
+
]
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"""Address masking anonymization strategy.
|
|
2
|
+
|
|
3
|
+
Provides flexible address anonymization with field-level control:
|
|
4
|
+
- Preserve specific fields (city, state, zip, country)
|
|
5
|
+
- Anonymize street address
|
|
6
|
+
- Combine with other strategies for complete address masking
|
|
7
|
+
|
|
8
|
+
Supports multiple address formats and field preservation combinations.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import random
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
|
|
14
|
+
from confiture.core.anonymization.strategy import AnonymizationStrategy, StrategyConfig
|
|
15
|
+
|
|
16
|
+
# Sample streets for anonymization
|
|
17
|
+
SAMPLE_STREETS = [
|
|
18
|
+
"Main",
|
|
19
|
+
"Oak",
|
|
20
|
+
"Elm",
|
|
21
|
+
"Maple",
|
|
22
|
+
"Pine",
|
|
23
|
+
"Cedar",
|
|
24
|
+
"Birch",
|
|
25
|
+
"Ash",
|
|
26
|
+
"Walnut",
|
|
27
|
+
"Cherry",
|
|
28
|
+
"Spruce",
|
|
29
|
+
"Fir",
|
|
30
|
+
"Hickory",
|
|
31
|
+
"Chestnut",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
STREET_TYPES = [
|
|
35
|
+
"Street",
|
|
36
|
+
"Avenue",
|
|
37
|
+
"Boulevard",
|
|
38
|
+
"Drive",
|
|
39
|
+
"Road",
|
|
40
|
+
"Lane",
|
|
41
|
+
"Court",
|
|
42
|
+
"Circle",
|
|
43
|
+
"Trail",
|
|
44
|
+
"Way",
|
|
45
|
+
"Terrace",
|
|
46
|
+
"Place",
|
|
47
|
+
"Square",
|
|
48
|
+
"Parkway",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
# Sample cities (for full anonymization)
|
|
52
|
+
SAMPLE_CITIES = [
|
|
53
|
+
"Springfield",
|
|
54
|
+
"Shelbyville",
|
|
55
|
+
"Capital City",
|
|
56
|
+
"Maple Valley",
|
|
57
|
+
"Riverside",
|
|
58
|
+
"Lakewood",
|
|
59
|
+
"Summerville",
|
|
60
|
+
"Hilltown",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
# US States
|
|
64
|
+
US_STATES = [
|
|
65
|
+
"AL",
|
|
66
|
+
"AK",
|
|
67
|
+
"AZ",
|
|
68
|
+
"AR",
|
|
69
|
+
"CA",
|
|
70
|
+
"CO",
|
|
71
|
+
"CT",
|
|
72
|
+
"DE",
|
|
73
|
+
"FL",
|
|
74
|
+
"GA",
|
|
75
|
+
"HI",
|
|
76
|
+
"ID",
|
|
77
|
+
"IL",
|
|
78
|
+
"IN",
|
|
79
|
+
"IA",
|
|
80
|
+
"KS",
|
|
81
|
+
"KY",
|
|
82
|
+
"LA",
|
|
83
|
+
"ME",
|
|
84
|
+
"MD",
|
|
85
|
+
"MA",
|
|
86
|
+
"MI",
|
|
87
|
+
"MN",
|
|
88
|
+
"MS",
|
|
89
|
+
"MO",
|
|
90
|
+
"MT",
|
|
91
|
+
"NE",
|
|
92
|
+
"NV",
|
|
93
|
+
"NH",
|
|
94
|
+
"NJ",
|
|
95
|
+
"NM",
|
|
96
|
+
"NY",
|
|
97
|
+
"NC",
|
|
98
|
+
"ND",
|
|
99
|
+
"OH",
|
|
100
|
+
"OK",
|
|
101
|
+
"OR",
|
|
102
|
+
"PA",
|
|
103
|
+
"RI",
|
|
104
|
+
"SC",
|
|
105
|
+
"SD",
|
|
106
|
+
"TN",
|
|
107
|
+
"TX",
|
|
108
|
+
"UT",
|
|
109
|
+
"VT",
|
|
110
|
+
"VA",
|
|
111
|
+
"WA",
|
|
112
|
+
"WV",
|
|
113
|
+
"WI",
|
|
114
|
+
"WY",
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class AddressConfig(StrategyConfig):
|
|
120
|
+
"""Configuration for address masking strategy.
|
|
121
|
+
|
|
122
|
+
Attributes:
|
|
123
|
+
seed: Seed for deterministic randomization
|
|
124
|
+
preserve_fields: List of fields to preserve:
|
|
125
|
+
- "city": Keep city name
|
|
126
|
+
- "state": Keep state/province
|
|
127
|
+
- "country": Keep country
|
|
128
|
+
- "zip": Keep postal code
|
|
129
|
+
redact_street: If True, anonymize street address
|
|
130
|
+
format: Address format:
|
|
131
|
+
- "freetext": Freetext address (need to parse)
|
|
132
|
+
- "structured": Structured object with street, city, state, zip
|
|
133
|
+
|
|
134
|
+
Example:
|
|
135
|
+
>>> config = AddressConfig(seed=12345, preserve_fields=["city", "state"])
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
preserve_fields: list[str] = field(default_factory=lambda: ["city", "country"])
|
|
139
|
+
redact_street: bool = True
|
|
140
|
+
format: str = "freetext" # freetext, structured
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class AddressStrategy(AnonymizationStrategy):
|
|
144
|
+
"""Anonymization strategy for masking addresses.
|
|
145
|
+
|
|
146
|
+
Provides flexible address anonymization with field-level preservation:
|
|
147
|
+
- Preserve selected fields (city, state, zip, country)
|
|
148
|
+
- Anonymize street address
|
|
149
|
+
- Generate realistic addresses from samples
|
|
150
|
+
|
|
151
|
+
Features:
|
|
152
|
+
- Field-level control
|
|
153
|
+
- Deterministic output (seed-based)
|
|
154
|
+
- Format-preserving (maintains address structure)
|
|
155
|
+
- Handles multiple address formats
|
|
156
|
+
|
|
157
|
+
Example:
|
|
158
|
+
>>> config = AddressConfig(seed=12345, preserve_fields=["city", "state"])
|
|
159
|
+
>>> strategy = AddressStrategy(config)
|
|
160
|
+
>>> strategy.anonymize("123 Main St, Springfield, IL 62701")
|
|
161
|
+
'456 Oak Avenue, Springfield, IL 62701'
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
config_type = AddressConfig
|
|
165
|
+
strategy_name = "address"
|
|
166
|
+
|
|
167
|
+
def anonymize(self, value: str | None) -> str | dict | None:
|
|
168
|
+
"""Anonymize an address value.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
value: Address string to anonymize
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Anonymized address preserving specified fields
|
|
175
|
+
|
|
176
|
+
Example:
|
|
177
|
+
>>> strategy.anonymize("123 Main St, Springfield, IL 62701")
|
|
178
|
+
'456 Oak Avenue, Springfield, IL 62701'
|
|
179
|
+
"""
|
|
180
|
+
if value is None:
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
if isinstance(value, str) and not value.strip():
|
|
184
|
+
return value
|
|
185
|
+
|
|
186
|
+
if self.config.format == "freetext":
|
|
187
|
+
return self._anonymize_freetext(value)
|
|
188
|
+
elif self.config.format == "structured":
|
|
189
|
+
return self._anonymize_structured(value)
|
|
190
|
+
else:
|
|
191
|
+
raise ValueError(f"Unknown format: {self.config.format}")
|
|
192
|
+
|
|
193
|
+
def _anonymize_freetext(self, value: str) -> str:
|
|
194
|
+
"""Anonymize freetext address.
|
|
195
|
+
|
|
196
|
+
Attempts to parse and preserve specified fields.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
value: Address string
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Anonymized address
|
|
203
|
+
"""
|
|
204
|
+
# Simple parsing: assume "street, city, state zip" format
|
|
205
|
+
parts = [p.strip() for p in value.split(",")]
|
|
206
|
+
|
|
207
|
+
if len(parts) < 2:
|
|
208
|
+
# Cannot parse - return anonymized version
|
|
209
|
+
return self._anonymize_full_address(value)
|
|
210
|
+
|
|
211
|
+
# Try to extract components
|
|
212
|
+
street = parts[0] if len(parts) > 0 else ""
|
|
213
|
+
city_state_zip = " ".join(parts[1:]) if len(parts) > 1 else ""
|
|
214
|
+
|
|
215
|
+
# Parse city, state, zip
|
|
216
|
+
words = city_state_zip.split()
|
|
217
|
+
city = " ".join(words[:-2]) if len(words) >= 2 else ""
|
|
218
|
+
state = words[-2] if len(words) >= 2 else ""
|
|
219
|
+
zip_code = words[-1] if words else ""
|
|
220
|
+
|
|
221
|
+
# Build anonymized address
|
|
222
|
+
result_parts = []
|
|
223
|
+
|
|
224
|
+
# Street
|
|
225
|
+
if "street" not in self.config.preserve_fields and self.config.redact_street:
|
|
226
|
+
result_parts.append(self._anonymize_street())
|
|
227
|
+
else:
|
|
228
|
+
result_parts.append(street)
|
|
229
|
+
|
|
230
|
+
# City
|
|
231
|
+
if "city" in self.config.preserve_fields:
|
|
232
|
+
result_parts.append(city)
|
|
233
|
+
else:
|
|
234
|
+
result_parts.append(self._anonymize_city())
|
|
235
|
+
|
|
236
|
+
# State
|
|
237
|
+
if "state" in self.config.preserve_fields:
|
|
238
|
+
result_parts.append(state)
|
|
239
|
+
else:
|
|
240
|
+
result_parts.append(self._anonymize_state())
|
|
241
|
+
|
|
242
|
+
# Zip
|
|
243
|
+
if "zip" in self.config.preserve_fields:
|
|
244
|
+
result_parts.append(zip_code)
|
|
245
|
+
else:
|
|
246
|
+
result_parts.append(self._anonymize_zip())
|
|
247
|
+
|
|
248
|
+
return ", ".join(p for p in result_parts if p)
|
|
249
|
+
|
|
250
|
+
def _anonymize_structured(self, value: dict) -> dict:
|
|
251
|
+
"""Anonymize structured address dict.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
value: Address dict with keys like street, city, state, zip
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Anonymized address dict
|
|
258
|
+
"""
|
|
259
|
+
result = {}
|
|
260
|
+
|
|
261
|
+
for field_name, field_value in value.items():
|
|
262
|
+
if field_value is None:
|
|
263
|
+
result[field_name] = None
|
|
264
|
+
elif field_name in self.config.preserve_fields:
|
|
265
|
+
result[field_name] = field_value
|
|
266
|
+
elif field_name == "street":
|
|
267
|
+
result[field_name] = self._anonymize_street()
|
|
268
|
+
elif field_name == "city":
|
|
269
|
+
result[field_name] = self._anonymize_city()
|
|
270
|
+
elif field_name == "state":
|
|
271
|
+
result[field_name] = self._anonymize_state()
|
|
272
|
+
elif field_name in ("zip", "postal_code"):
|
|
273
|
+
result[field_name] = self._anonymize_zip()
|
|
274
|
+
else:
|
|
275
|
+
result[field_name] = field_value
|
|
276
|
+
|
|
277
|
+
return result
|
|
278
|
+
|
|
279
|
+
def _anonymize_full_address(self, value: str) -> str:
|
|
280
|
+
"""Fully anonymize address (no parsing).
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
value: Original address
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Anonymized address of similar length
|
|
287
|
+
"""
|
|
288
|
+
rng = random.Random(f"{self.config.seed}:{value}".encode())
|
|
289
|
+
|
|
290
|
+
street = rng.choice(SAMPLE_STREETS)
|
|
291
|
+
street_type = rng.choice(STREET_TYPES)
|
|
292
|
+
street_num = rng.randint(1, 999)
|
|
293
|
+
|
|
294
|
+
city = rng.choice(SAMPLE_CITIES)
|
|
295
|
+
state = rng.choice(US_STATES)
|
|
296
|
+
zip_code = f"{rng.randint(10000, 99999)}"
|
|
297
|
+
|
|
298
|
+
return f"{street_num} {street} {street_type}, {city}, {state} {zip_code}"
|
|
299
|
+
|
|
300
|
+
def _anonymize_street(self) -> str:
|
|
301
|
+
"""Generate anonymized street address.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Street address like "123 Oak Avenue"
|
|
305
|
+
"""
|
|
306
|
+
rng = random.Random(f"{self.config.seed}:street".encode())
|
|
307
|
+
|
|
308
|
+
street_num = rng.randint(1, 999)
|
|
309
|
+
street = rng.choice(SAMPLE_STREETS)
|
|
310
|
+
street_type = rng.choice(STREET_TYPES)
|
|
311
|
+
|
|
312
|
+
return f"{street_num} {street} {street_type}"
|
|
313
|
+
|
|
314
|
+
def _anonymize_city(self) -> str:
|
|
315
|
+
"""Generate anonymized city name.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
City name
|
|
319
|
+
"""
|
|
320
|
+
rng = random.Random(f"{self.config.seed}:city".encode())
|
|
321
|
+
return rng.choice(SAMPLE_CITIES)
|
|
322
|
+
|
|
323
|
+
def _anonymize_state(self) -> str:
|
|
324
|
+
"""Generate anonymized state code.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
State code (e.g., "CA")
|
|
328
|
+
"""
|
|
329
|
+
rng = random.Random(f"{self.config.seed}:state".encode())
|
|
330
|
+
return rng.choice(US_STATES)
|
|
331
|
+
|
|
332
|
+
def _anonymize_zip(self) -> str:
|
|
333
|
+
"""Generate anonymized zip code.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
ZIP code (5 digits)
|
|
337
|
+
"""
|
|
338
|
+
rng = random.Random(f"{self.config.seed}:zip".encode())
|
|
339
|
+
return f"{rng.randint(10000, 99999)}"
|
|
340
|
+
|
|
341
|
+
def validate(self, value: str | dict) -> bool:
|
|
342
|
+
"""Check if strategy can handle this value type.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
value: Sample value to validate
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
True if value is a string, dict, or None
|
|
349
|
+
"""
|
|
350
|
+
return isinstance(value, (str, dict)) or value is None
|
|
351
|
+
|
|
352
|
+
def short_name(self) -> str:
|
|
353
|
+
"""Return short strategy name for logging.
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
Short name (e.g., "address:preserve_city_state")
|
|
357
|
+
"""
|
|
358
|
+
preserved = "_".join(self.config.preserve_fields) if self.config.preserve_fields else "none"
|
|
359
|
+
return f"{self.strategy_name}:{preserved}"
|