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.
Files changed (124) hide show
  1. confiture/__init__.py +48 -0
  2. confiture/_core.cpython-311-darwin.so +0 -0
  3. confiture/cli/__init__.py +0 -0
  4. confiture/cli/dry_run.py +116 -0
  5. confiture/cli/lint_formatter.py +193 -0
  6. confiture/cli/main.py +1893 -0
  7. confiture/config/__init__.py +0 -0
  8. confiture/config/environment.py +263 -0
  9. confiture/core/__init__.py +51 -0
  10. confiture/core/anonymization/__init__.py +0 -0
  11. confiture/core/anonymization/audit.py +485 -0
  12. confiture/core/anonymization/benchmarking.py +372 -0
  13. confiture/core/anonymization/breach_notification.py +652 -0
  14. confiture/core/anonymization/compliance.py +617 -0
  15. confiture/core/anonymization/composer.py +298 -0
  16. confiture/core/anonymization/data_subject_rights.py +669 -0
  17. confiture/core/anonymization/factory.py +319 -0
  18. confiture/core/anonymization/governance.py +737 -0
  19. confiture/core/anonymization/performance.py +1092 -0
  20. confiture/core/anonymization/profile.py +284 -0
  21. confiture/core/anonymization/registry.py +195 -0
  22. confiture/core/anonymization/security/kms_manager.py +547 -0
  23. confiture/core/anonymization/security/lineage.py +888 -0
  24. confiture/core/anonymization/security/token_store.py +686 -0
  25. confiture/core/anonymization/strategies/__init__.py +41 -0
  26. confiture/core/anonymization/strategies/address.py +359 -0
  27. confiture/core/anonymization/strategies/credit_card.py +374 -0
  28. confiture/core/anonymization/strategies/custom.py +161 -0
  29. confiture/core/anonymization/strategies/date.py +218 -0
  30. confiture/core/anonymization/strategies/differential_privacy.py +398 -0
  31. confiture/core/anonymization/strategies/email.py +141 -0
  32. confiture/core/anonymization/strategies/format_preserving_encryption.py +310 -0
  33. confiture/core/anonymization/strategies/hash.py +150 -0
  34. confiture/core/anonymization/strategies/ip_address.py +235 -0
  35. confiture/core/anonymization/strategies/masking_retention.py +252 -0
  36. confiture/core/anonymization/strategies/name.py +298 -0
  37. confiture/core/anonymization/strategies/phone.py +119 -0
  38. confiture/core/anonymization/strategies/preserve.py +85 -0
  39. confiture/core/anonymization/strategies/redact.py +101 -0
  40. confiture/core/anonymization/strategies/salted_hashing.py +322 -0
  41. confiture/core/anonymization/strategies/text_redaction.py +183 -0
  42. confiture/core/anonymization/strategies/tokenization.py +334 -0
  43. confiture/core/anonymization/strategy.py +241 -0
  44. confiture/core/anonymization/syncer_audit.py +357 -0
  45. confiture/core/blue_green.py +683 -0
  46. confiture/core/builder.py +500 -0
  47. confiture/core/checksum.py +358 -0
  48. confiture/core/connection.py +184 -0
  49. confiture/core/differ.py +522 -0
  50. confiture/core/drift.py +564 -0
  51. confiture/core/dry_run.py +182 -0
  52. confiture/core/health.py +313 -0
  53. confiture/core/hooks/__init__.py +87 -0
  54. confiture/core/hooks/base.py +232 -0
  55. confiture/core/hooks/context.py +146 -0
  56. confiture/core/hooks/execution_strategies.py +57 -0
  57. confiture/core/hooks/observability.py +220 -0
  58. confiture/core/hooks/phases.py +53 -0
  59. confiture/core/hooks/registry.py +295 -0
  60. confiture/core/large_tables.py +775 -0
  61. confiture/core/linting/__init__.py +70 -0
  62. confiture/core/linting/composer.py +192 -0
  63. confiture/core/linting/libraries/__init__.py +17 -0
  64. confiture/core/linting/libraries/gdpr.py +168 -0
  65. confiture/core/linting/libraries/general.py +184 -0
  66. confiture/core/linting/libraries/hipaa.py +144 -0
  67. confiture/core/linting/libraries/pci_dss.py +104 -0
  68. confiture/core/linting/libraries/sox.py +120 -0
  69. confiture/core/linting/schema_linter.py +491 -0
  70. confiture/core/linting/versioning.py +151 -0
  71. confiture/core/locking.py +389 -0
  72. confiture/core/migration_generator.py +298 -0
  73. confiture/core/migrator.py +882 -0
  74. confiture/core/observability/__init__.py +44 -0
  75. confiture/core/observability/audit.py +323 -0
  76. confiture/core/observability/logging.py +187 -0
  77. confiture/core/observability/metrics.py +174 -0
  78. confiture/core/observability/tracing.py +192 -0
  79. confiture/core/pg_version.py +418 -0
  80. confiture/core/pool.py +406 -0
  81. confiture/core/risk/__init__.py +39 -0
  82. confiture/core/risk/predictor.py +188 -0
  83. confiture/core/risk/scoring.py +248 -0
  84. confiture/core/rollback_generator.py +388 -0
  85. confiture/core/schema_analyzer.py +769 -0
  86. confiture/core/schema_to_schema.py +590 -0
  87. confiture/core/security/__init__.py +32 -0
  88. confiture/core/security/logging.py +201 -0
  89. confiture/core/security/validation.py +416 -0
  90. confiture/core/signals.py +371 -0
  91. confiture/core/syncer.py +540 -0
  92. confiture/exceptions.py +192 -0
  93. confiture/integrations/__init__.py +0 -0
  94. confiture/models/__init__.py +24 -0
  95. confiture/models/lint.py +193 -0
  96. confiture/models/migration.py +265 -0
  97. confiture/models/schema.py +203 -0
  98. confiture/models/sql_file_migration.py +225 -0
  99. confiture/scenarios/__init__.py +36 -0
  100. confiture/scenarios/compliance.py +586 -0
  101. confiture/scenarios/ecommerce.py +199 -0
  102. confiture/scenarios/financial.py +253 -0
  103. confiture/scenarios/healthcare.py +315 -0
  104. confiture/scenarios/multi_tenant.py +340 -0
  105. confiture/scenarios/saas.py +295 -0
  106. confiture/testing/FRAMEWORK_API.md +722 -0
  107. confiture/testing/__init__.py +100 -0
  108. confiture/testing/fixtures/__init__.py +11 -0
  109. confiture/testing/fixtures/data_validator.py +229 -0
  110. confiture/testing/fixtures/migration_runner.py +167 -0
  111. confiture/testing/fixtures/schema_snapshotter.py +352 -0
  112. confiture/testing/frameworks/__init__.py +10 -0
  113. confiture/testing/frameworks/mutation.py +587 -0
  114. confiture/testing/frameworks/performance.py +479 -0
  115. confiture/testing/loader.py +225 -0
  116. confiture/testing/pytest/__init__.py +38 -0
  117. confiture/testing/pytest_plugin.py +190 -0
  118. confiture/testing/sandbox.py +304 -0
  119. confiture/testing/utils/__init__.py +0 -0
  120. fraiseql_confiture-0.3.7.dist-info/METADATA +438 -0
  121. fraiseql_confiture-0.3.7.dist-info/RECORD +124 -0
  122. fraiseql_confiture-0.3.7.dist-info/WHEEL +4 -0
  123. fraiseql_confiture-0.3.7.dist-info/entry_points.txt +4 -0
  124. fraiseql_confiture-0.3.7.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,298 @@
1
+ """Name masking anonymization strategy.
2
+
3
+ Provides deterministic name masking with multiple format options:
4
+ - Preserve initials only (e.g., "John Doe" → "J.D.")
5
+ - Generate random names (deterministic from seed)
6
+ - Generate name from pools (first names + last names)
7
+
8
+ Uses seeded randomization to ensure same input always produces same output.
9
+ """
10
+
11
+ import random
12
+ from dataclasses import dataclass
13
+
14
+ from confiture.core.anonymization.strategy import AnonymizationStrategy, StrategyConfig
15
+
16
+ # Common first names (50 names for diversity)
17
+ FIRST_NAMES = [
18
+ "James",
19
+ "Mary",
20
+ "Robert",
21
+ "Patricia",
22
+ "Michael",
23
+ "Jennifer",
24
+ "William",
25
+ "Linda",
26
+ "David",
27
+ "Barbara",
28
+ "Richard",
29
+ "Elizabeth",
30
+ "Joseph",
31
+ "Susan",
32
+ "Charles",
33
+ "Jessica",
34
+ "Christopher",
35
+ "Sarah",
36
+ "Daniel",
37
+ "Karen",
38
+ "Matthew",
39
+ "Nancy",
40
+ "Anthony",
41
+ "Lisa",
42
+ "Mark",
43
+ "Betty",
44
+ "Donald",
45
+ "Margaret",
46
+ "Steven",
47
+ "Sandra",
48
+ "Paul",
49
+ "Ashley",
50
+ "Andrew",
51
+ "Kimberly",
52
+ "Joshua",
53
+ "Emily",
54
+ "Kenneth",
55
+ "Donna",
56
+ "Kevin",
57
+ "Michelle",
58
+ "Brian",
59
+ "Dorothy",
60
+ "George",
61
+ "Carol",
62
+ "Edward",
63
+ "Amanda",
64
+ "Ronald",
65
+ "Melissa",
66
+ "Timothy",
67
+ "Deborah",
68
+ "Jason",
69
+ "Stephanie",
70
+ "Jeffrey",
71
+ ]
72
+
73
+ # Common last names (50 names for diversity)
74
+ LAST_NAMES = [
75
+ "Smith",
76
+ "Johnson",
77
+ "Williams",
78
+ "Brown",
79
+ "Jones",
80
+ "Garcia",
81
+ "Miller",
82
+ "Davis",
83
+ "Rodriguez",
84
+ "Martinez",
85
+ "Hernandez",
86
+ "Lopez",
87
+ "Gonzalez",
88
+ "Wilson",
89
+ "Anderson",
90
+ "Thomas",
91
+ "Taylor",
92
+ "Moore",
93
+ "Jackson",
94
+ "Martin",
95
+ "Lee",
96
+ "Perez",
97
+ "Thompson",
98
+ "White",
99
+ "Harris",
100
+ "Sanchez",
101
+ "Clark",
102
+ "Ramirez",
103
+ "Lewis",
104
+ "Robinson",
105
+ "Walker",
106
+ "Young",
107
+ "Allen",
108
+ "King",
109
+ "Wright",
110
+ "Scott",
111
+ "Torres",
112
+ "Peterson",
113
+ "Phillips",
114
+ "Campbell",
115
+ "Parker",
116
+ "Evans",
117
+ "Edwards",
118
+ "Collins",
119
+ "Reyes",
120
+ "Stewart",
121
+ "Morris",
122
+ "Morales",
123
+ "Murphy",
124
+ "Rogers",
125
+ "Morgan",
126
+ "Peterson",
127
+ "Cooper",
128
+ ]
129
+
130
+
131
+ @dataclass
132
+ class NameMaskConfig(StrategyConfig):
133
+ """Configuration for name masking strategy.
134
+
135
+ Attributes:
136
+ seed: Seed for deterministic randomization
137
+ format_type: Output format:
138
+ - "firstname_lastname": "John Doe" → "Michael Patricia" (from name pools)
139
+ - "initials": "John Doe" → "J.D." (preserve initials)
140
+ - "random": "John Doe" → "XyZ4qW9" (random string)
141
+ preserve_initial: If True, keep original first letter
142
+ case_preserving: If True, preserve original case
143
+
144
+ Example:
145
+ >>> config = NameMaskConfig(seed=12345, format_type="firstname_lastname")
146
+ """
147
+
148
+ format_type: str = "firstname_lastname" # firstname_lastname, initials, random
149
+ preserve_initial: bool = False # Only for firstname_lastname format
150
+ case_preserving: bool = True # Preserve original case
151
+
152
+
153
+ class NameMaskingStrategy(AnonymizationStrategy):
154
+ """Anonymization strategy for masking personal names.
155
+
156
+ Provides multiple name masking formats with deterministic output based on seed.
157
+ Same input + same seed = same output (enables foreign key consistency).
158
+
159
+ Features:
160
+ - Format-preserving (maintains name-like structure)
161
+ - Deterministic (seed-based)
162
+ - Configurable output format
163
+ - Handles NULL and edge cases
164
+
165
+ Example:
166
+ >>> config = NameMaskConfig(seed=12345, format_type="firstname_lastname")
167
+ >>> strategy = NameMaskingStrategy(config)
168
+ >>> strategy.anonymize("John Doe")
169
+ 'Michael Johnson'
170
+ >>> strategy.anonymize("John Doe") # Same seed = same output
171
+ 'Michael Johnson'
172
+ """
173
+
174
+ config_type = NameMaskConfig
175
+ strategy_name = "name"
176
+
177
+ def anonymize(self, value: str | None) -> str | None:
178
+ """Anonymize a name value.
179
+
180
+ Args:
181
+ value: Name to anonymize
182
+
183
+ Returns:
184
+ Anonymized name in configured format
185
+
186
+ Example:
187
+ >>> strategy.anonymize("John Doe")
188
+ 'Michael Johnson'
189
+ """
190
+ if value is None:
191
+ return None
192
+
193
+ if isinstance(value, str) and not value.strip():
194
+ return value
195
+
196
+ config = self.config
197
+
198
+ if config.format_type == "firstname_lastname":
199
+ return self._mask_firstname_lastname(value)
200
+ elif config.format_type == "initials":
201
+ return self._mask_initials(value)
202
+ elif config.format_type == "random":
203
+ return self._mask_random(value)
204
+ else:
205
+ raise ValueError(f"Unknown format_type: {config.format_type}")
206
+
207
+ def _mask_firstname_lastname(self, value: str) -> str:
208
+ """Mask with random first and last name.
209
+
210
+ Args:
211
+ value: Name to mask
212
+
213
+ Returns:
214
+ Anonymized firstname lastname
215
+ """
216
+ parts = value.strip().split()
217
+
218
+ if not parts:
219
+ return value
220
+
221
+ # Use seed to generate reproducible random names
222
+ rng = random.Random(f"{self.config.seed}:{value}".encode())
223
+
224
+ # Get random first name
225
+ first_name = rng.choice(FIRST_NAMES)
226
+
227
+ # Get random last name
228
+ last_name = rng.choice(LAST_NAMES)
229
+
230
+ # Apply case preservation if needed
231
+ if self.config.case_preserving and parts:
232
+ # Preserve case of original first name
233
+ if parts[0] and parts[0][0].islower():
234
+ first_name = first_name.lower()
235
+
236
+ # Preserve case of original last name (if exists)
237
+ if len(parts) > 1 and parts[1] and parts[1][0].islower():
238
+ last_name = last_name.lower()
239
+
240
+ return f"{first_name} {last_name}"
241
+
242
+ def _mask_initials(self, value: str) -> str:
243
+ """Mask with initials only.
244
+
245
+ Args:
246
+ value: Name to mask
247
+
248
+ Returns:
249
+ Initials (e.g., "J.D.")
250
+ """
251
+ parts = value.strip().split()
252
+
253
+ if not parts:
254
+ return value
255
+
256
+ # Get initials from original name
257
+ initials = [part[0].upper() for part in parts if part]
258
+
259
+ return ".".join(initials) + "."
260
+
261
+ def _mask_random(self, value: str) -> str:
262
+ """Mask with random string.
263
+
264
+ Args:
265
+ value: Name to mask
266
+
267
+ Returns:
268
+ Random string of same length as original
269
+ """
270
+ if not value:
271
+ return value
272
+
273
+ # Use seed for reproducibility
274
+ rng = random.Random(f"{self.config.seed}:{value}".encode())
275
+
276
+ # Generate random string of same length
277
+ length = len(value.strip())
278
+ chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
279
+ return "".join(rng.choices(chars, k=length))
280
+
281
+ def validate(self, value: str) -> bool:
282
+ """Check if strategy can handle this value type.
283
+
284
+ Args:
285
+ value: Sample value to validate
286
+
287
+ Returns:
288
+ True if value is a string or None
289
+ """
290
+ return isinstance(value, str) or value is None
291
+
292
+ def short_name(self) -> str:
293
+ """Return short strategy name for logging.
294
+
295
+ Returns:
296
+ Short name (e.g., "name:initials")
297
+ """
298
+ return f"{self.strategy_name}:{self.config.format_type}"
@@ -0,0 +1,119 @@
1
+ """Phone number masking anonymization strategy.
2
+
3
+ Generates deterministic fake phone numbers from real ones, useful for:
4
+ - PII protection in test/staging environments
5
+ - Preserving phone-like format for testing
6
+ - Reproducible anonymization (deterministic with seed)
7
+ """
8
+
9
+ import hashlib
10
+ import re
11
+ from dataclasses import dataclass
12
+ from typing import Any
13
+
14
+ from confiture.core.anonymization.strategy import (
15
+ AnonymizationStrategy,
16
+ StrategyConfig,
17
+ )
18
+
19
+
20
+ @dataclass
21
+ class PhoneMaskConfig(StrategyConfig):
22
+ """Configuration for PhoneMaskingStrategy.
23
+
24
+ Attributes:
25
+ format: Phone number format template (use {number} placeholder)
26
+ preserve_country_code: If True, keep original country code
27
+ seed_env_var: Environment variable containing seed
28
+ seed: Hardcoded seed (testing only)
29
+ """
30
+
31
+ format: str = "+1-555-{number}"
32
+ """Phone format template with {number} placeholder."""
33
+
34
+ preserve_country_code: bool = False
35
+ """If True, try to preserve original country code."""
36
+
37
+
38
+ class PhoneMaskingStrategy(AnonymizationStrategy):
39
+ """Generate deterministic fake phone numbers from real ones.
40
+
41
+ Features:
42
+ - Deterministic: Same number + seed = same fake number
43
+ - Format customizable: Template-based generation
44
+ - Format preserving: Output looks like a real phone number
45
+ - Unique: Preserves uniqueness for referential integrity
46
+
47
+ Example:
48
+ >>> config = PhoneMaskConfig(
49
+ ... format="+1-555-{number}",
50
+ ... seed_env_var='ANONYMIZATION_SEED'
51
+ ... )
52
+ >>> strategy = PhoneMaskingStrategy(config)
53
+ >>> result = strategy.anonymize('+1-202-555-0123')
54
+ >>> result # e.g., '+1-555-1234'
55
+ '+1-555-1234'
56
+ """
57
+
58
+ # Basic phone number regex (allows various formats)
59
+ PHONE_REGEX = re.compile(r"[\d\s\-\+\(\)]{10,}")
60
+
61
+ def __init__(self, config: PhoneMaskConfig | None = None):
62
+ """Initialize phone masking strategy.
63
+
64
+ Args:
65
+ config: PhoneMaskConfig instance
66
+ """
67
+ config = config or PhoneMaskConfig()
68
+ super().__init__(config)
69
+ self.config: PhoneMaskConfig = config
70
+
71
+ def anonymize(self, value: Any) -> Any:
72
+ """Generate fake phone number from real number.
73
+
74
+ Args:
75
+ value: Phone number to anonymize
76
+
77
+ Returns:
78
+ Fake phone number with same format as original
79
+
80
+ Example:
81
+ >>> strategy = PhoneMaskingStrategy(PhoneMaskConfig(seed=12345))
82
+ >>> strategy.anonymize('+1-202-555-0123')
83
+ '+1-555-1234'
84
+ """
85
+ # Handle NULL
86
+ if value is None:
87
+ return None
88
+
89
+ # Handle empty string
90
+ value_str = str(value).strip()
91
+ if not value_str:
92
+ return ""
93
+
94
+ # Create deterministic hash from phone number
95
+ hash_value = hashlib.sha256(f"{self._seed}:{value_str}".encode()).hexdigest()
96
+
97
+ # Extract digits to generate phone number
98
+ # Use hash to create a 4-digit phone number suffix
99
+ number_suffix = str(int(hash_value[:8], 16) % 10000).zfill(4)
100
+
101
+ # Format output
102
+ output = self.config.format.format(number=number_suffix)
103
+
104
+ return output
105
+
106
+ def validate(self, value: Any) -> bool:
107
+ """Check if value looks like a phone number.
108
+
109
+ Args:
110
+ value: Value to validate
111
+
112
+ Returns:
113
+ True if value matches basic phone pattern
114
+ """
115
+ if value is None:
116
+ return False
117
+
118
+ value_str = str(value).strip()
119
+ return bool(self.PHONE_REGEX.match(value_str))
@@ -0,0 +1,85 @@
1
+ """Preserve (no-op) anonymization strategy.
2
+
3
+ Provides a no-operation strategy that returns values unchanged.
4
+ Useful for:
5
+ - Marking columns that should NOT be anonymized
6
+ - Placeholder in strategy chains
7
+ - Configuration clarity (explicit "don't anonymize" intent)
8
+ - Testing and debugging
9
+ """
10
+
11
+ from dataclasses import dataclass
12
+ from typing import Any
13
+
14
+ from confiture.core.anonymization.strategy import AnonymizationStrategy, StrategyConfig
15
+
16
+
17
+ @dataclass
18
+ class PreserveConfig(StrategyConfig):
19
+ """Configuration for preserve strategy.
20
+
21
+ This strategy has no configuration options beyond base StrategyConfig.
22
+ It simply returns values unchanged.
23
+
24
+ Example:
25
+ >>> config = PreserveConfig(seed=12345)
26
+ """
27
+
28
+ pass
29
+
30
+
31
+ class PreserveStrategy(AnonymizationStrategy):
32
+ """No-operation anonymization strategy.
33
+
34
+ Returns values unchanged. Useful for marking columns that should not
35
+ be anonymized or as a placeholder in processing chains.
36
+
37
+ Features:
38
+ - Identity operation (returns input unchanged)
39
+ - Handles all value types
40
+ - NULL-safe
41
+ - Useful for explicit "preserve" intent in configurations
42
+
43
+ Example:
44
+ >>> config = PreserveConfig()
45
+ >>> strategy = PreserveStrategy(config)
46
+ >>> strategy.anonymize("sensitive_data")
47
+ 'sensitive_data' # Unchanged
48
+ """
49
+
50
+ config_type = PreserveConfig
51
+ strategy_name = "preserve"
52
+
53
+ def anonymize(self, value):
54
+ """Return value unchanged.
55
+
56
+ Args:
57
+ value: Any value
58
+
59
+ Returns:
60
+ The same value unchanged
61
+
62
+ Example:
63
+ >>> strategy.anonymize("test@example.com")
64
+ 'test@example.com'
65
+ """
66
+ return value
67
+
68
+ def validate(self, value: Any) -> bool: # noqa: ARG002
69
+ """Check if strategy can handle this value type.
70
+
71
+ Args:
72
+ value: Sample value to validate (unused, preserve accepts any)
73
+
74
+ Returns:
75
+ True (preserve accepts any value type)
76
+ """
77
+ return True
78
+
79
+ def short_name(self) -> str:
80
+ """Return short strategy name for logging.
81
+
82
+ Returns:
83
+ Short name (e.g., "preserve")
84
+ """
85
+ return self.strategy_name
@@ -0,0 +1,101 @@
1
+ """Simple redaction anonymization strategy.
2
+
3
+ One-size-fits-all redaction for sensitive data that should be completely hidden,
4
+ not anonymized to a plausible value.
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+ from typing import Any
9
+
10
+ from confiture.core.anonymization.strategy import (
11
+ AnonymizationStrategy,
12
+ StrategyConfig,
13
+ )
14
+
15
+
16
+ @dataclass
17
+ class RedactConfig(StrategyConfig):
18
+ """Configuration for SimpleRedactStrategy.
19
+
20
+ Attributes:
21
+ replacement: Text to replace sensitive values with
22
+ seed_env_var: Environment variable containing seed (unused for redaction)
23
+ seed: Hardcoded seed (unused for redaction)
24
+ """
25
+
26
+ replacement: str = "[REDACTED]"
27
+ """Text to use for all redacted values."""
28
+
29
+
30
+ class SimpleRedactStrategy(AnonymizationStrategy):
31
+ """Simple one-size-fits-all redaction strategy.
32
+
33
+ Features:
34
+ - Fast: No hashing or computation
35
+ - Complete: All data values replaced with same text
36
+ - Safe: Zero information leakage
37
+ - Simple: Easy to understand and audit
38
+
39
+ Use when:
40
+ - PII is highly sensitive (no testing needed with real-like data)
41
+ - You don't need to preserve data format (testing doesn't rely on structure)
42
+ - You want maximum privacy with zero complexity
43
+
44
+ Don't use when:
45
+ - You need to preserve uniqueness for testing (FK constraints)
46
+ - You need format-preserving anonymization (testing email-like values)
47
+ - You need to correlate anonymized data across tables
48
+
49
+ Example:
50
+ >>> config = RedactConfig(replacement="[HIDDEN]")
51
+ >>> strategy = SimpleRedactStrategy(config)
52
+ >>> strategy.anonymize("secret data")
53
+ '[HIDDEN]'
54
+ """
55
+
56
+ def __init__(self, config: RedactConfig | None = None):
57
+ """Initialize redaction strategy.
58
+
59
+ Args:
60
+ config: RedactConfig instance
61
+ """
62
+ config = config or RedactConfig()
63
+ super().__init__(config)
64
+ self.config: RedactConfig = config
65
+
66
+ def anonymize(self, value: Any) -> Any:
67
+ """Redact value to replacement text.
68
+
69
+ Args:
70
+ value: Value to redact
71
+
72
+ Returns:
73
+ Replacement text (same for all values)
74
+
75
+ Example:
76
+ >>> strategy = SimpleRedactStrategy()
77
+ >>> strategy.anonymize("anything")
78
+ '[REDACTED]'
79
+ >>> strategy.anonymize("123")
80
+ '[REDACTED]'
81
+ >>> strategy.anonymize(None) # Special case: NULL stays NULL
82
+ """
83
+ # Special case: NULL stays NULL
84
+ if value is None:
85
+ return None
86
+
87
+ # All other values replaced with redaction text
88
+ return self.config.replacement
89
+
90
+ def validate(self, value: Any) -> bool:
91
+ """Redaction works for any value type.
92
+
93
+ Args:
94
+ value: Value to validate (not really used)
95
+
96
+ Returns:
97
+ Always True (redaction works for all types)
98
+ """
99
+ # Redaction works for all types
100
+ del value
101
+ return True