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,374 @@
1
+ """Credit card masking anonymization strategy.
2
+
3
+ Provides PCI-DSS compliant credit card anonymization with:
4
+ - Preserve last 4 digits (identifies card variant)
5
+ - Preserve BIN (Bank Identification Number - first 6 digits)
6
+ - Luhn validation for checksums
7
+ - Card type detection (Visa, Mastercard, Amex, Discover, etc)
8
+ - Deterministic anonymization based on seed
9
+
10
+ Security Note:
11
+ Does NOT mask full PAN in production without proper PCI-DSS controls.
12
+ Use with secure storage and access controls. This is for data masking only.
13
+ """
14
+
15
+ import random
16
+ from dataclasses import dataclass
17
+
18
+ from confiture.core.anonymization.strategy import AnonymizationStrategy, StrategyConfig
19
+
20
+ # Card type patterns: (name, digit_lengths, prefix_patterns)
21
+ CARD_TYPES = {
22
+ "visa": (16, [4]),
23
+ "mastercard": (16, [51, 52, 53, 54, 55]),
24
+ "amex": (15, [34, 37]),
25
+ "discover": (16, [6011, 622, 644, 645, 646, 647, 648, 649, 65]),
26
+ "diners": (14, [36, 38, 39]),
27
+ "jcb": (16, [35]),
28
+ }
29
+
30
+
31
+ def luhn_checksum(card_number: str) -> int:
32
+ """Calculate Luhn checksum for card number.
33
+
34
+ Args:
35
+ card_number: Card number string (digits only, without checksum)
36
+
37
+ Returns:
38
+ Luhn checksum digit
39
+ """
40
+ digits = [int(d) for d in card_number]
41
+ # Double every second digit from right to left (before checksum)
42
+ # Since we're calculating the checksum digit, start from the right and skip first position
43
+ for i in range(len(digits) - 1, -1, -2):
44
+ digits[i] *= 2
45
+ if digits[i] > 9:
46
+ digits[i] -= 9
47
+
48
+ total = sum(digits)
49
+ checksum = (10 - (total % 10)) % 10
50
+ return checksum
51
+
52
+
53
+ def detect_card_type(card_number: str) -> str:
54
+ """Detect card type from card number.
55
+
56
+ Args:
57
+ card_number: Card number string (digits only)
58
+
59
+ Returns:
60
+ Card type (visa, mastercard, amex, etc) or 'unknown'
61
+ """
62
+ if not card_number or not card_number.isdigit():
63
+ return "unknown"
64
+
65
+ for card_type, (expected_length, prefixes) in CARD_TYPES.items():
66
+ if len(card_number) == expected_length:
67
+ for prefix in prefixes:
68
+ if card_number.startswith(str(prefix)):
69
+ return card_type
70
+
71
+ return "unknown"
72
+
73
+
74
+ def is_valid_card_number(card_number: str) -> bool:
75
+ """Validate card number using Luhn algorithm.
76
+
77
+ Args:
78
+ card_number: Card number string (may include spaces/dashes)
79
+
80
+ Returns:
81
+ True if card number passes Luhn validation
82
+ """
83
+ if not card_number:
84
+ return False
85
+
86
+ # Remove spaces/dashes
87
+ digits_str = card_number.replace(" ", "").replace("-", "")
88
+
89
+ if not digits_str.isdigit():
90
+ return False
91
+
92
+ # Check valid length
93
+ if len(digits_str) < 13 or len(digits_str) > 19:
94
+ return False
95
+
96
+ # Verify Luhn checksum
97
+ digits = [int(d) for d in digits_str]
98
+ # Double every second digit from right to left
99
+ for i in range(len(digits) - 2, -1, -2):
100
+ digits[i] *= 2
101
+ if digits[i] > 9:
102
+ digits[i] -= 9
103
+
104
+ return sum(digits) % 10 == 0
105
+
106
+
107
+ @dataclass
108
+ class CreditCardConfig(StrategyConfig):
109
+ """Configuration for credit card masking strategy.
110
+
111
+ Attributes:
112
+ seed: Seed for deterministic randomization
113
+ preserve_last4: If True, preserve last 4 digits (default True)
114
+ preserve_bin: If True, preserve first 6 digits (default False)
115
+ mask_char: Character to use for masking (default '*')
116
+ validate: If True, validate card number with Luhn (default True)
117
+
118
+ Example:
119
+ >>> config = CreditCardConfig(seed=12345, preserve_last4=True)
120
+ """
121
+
122
+ preserve_last4: bool = True
123
+ preserve_bin: bool = False
124
+ mask_char: str = "*"
125
+ validate: bool = True
126
+
127
+
128
+ class CreditCardStrategy(AnonymizationStrategy):
129
+ """Anonymization strategy for masking credit card numbers.
130
+
131
+ Provides PCI-DSS compliant card masking with configurable preservation:
132
+ - Preserve last 4 digits (identifies card type for customer)
133
+ - Preserve BIN (first 6 digits, bank identifier)
134
+ - Generate realistic valid card numbers (pass Luhn check)
135
+ - Deterministic output (same seed = same output)
136
+
137
+ Features:
138
+ - Luhn validation
139
+ - Card type detection
140
+ - Format preservation
141
+ - PCI-DSS compliant
142
+
143
+ Example:
144
+ >>> config = CreditCardConfig(seed=12345, preserve_last4=True)
145
+ >>> strategy = CreditCardStrategy(config)
146
+ >>> strategy.anonymize("4532-1111-1111-1234")
147
+ '4532-****-****-1234' # Last 4 preserved
148
+ """
149
+
150
+ config_type = CreditCardConfig
151
+ strategy_name = "credit_card"
152
+
153
+ def anonymize(self, value: str | None) -> str | None:
154
+ """Anonymize a credit card number.
155
+
156
+ Args:
157
+ value: Card number (with or without separators)
158
+
159
+ Returns:
160
+ Anonymized card number
161
+
162
+ Example:
163
+ >>> strategy.anonymize("4532-1111-1111-1234")
164
+ '4532-****-****-1234'
165
+ """
166
+ if value is None:
167
+ return None
168
+
169
+ if isinstance(value, str) and not value.strip():
170
+ return value
171
+
172
+ # Clean card number
173
+ cleaned = value.replace(" ", "").replace("-", "")
174
+
175
+ # Validate if required
176
+ if self.config.validate and not is_valid_card_number(cleaned):
177
+ # If validation fails, return masked version
178
+ return self._mask_simple(value)
179
+
180
+ # Generate anonymized card
181
+ if self.config.preserve_bin:
182
+ return self._anonymize_preserve_bin(cleaned, value)
183
+ elif self.config.preserve_last4:
184
+ return self._anonymize_preserve_last4(cleaned, value)
185
+ else:
186
+ return self._anonymize_full(cleaned, value)
187
+
188
+ def validate(self, value: str) -> bool:
189
+ """Check if strategy can handle this value type.
190
+
191
+ Args:
192
+ value: Sample value to validate
193
+
194
+ Returns:
195
+ True if value is a string or None
196
+ """
197
+ return isinstance(value, str) or value is None
198
+
199
+ def _mask_simple(self, card_number: str) -> str:
200
+ """Simple masking for invalid cards.
201
+
202
+ Args:
203
+ card_number: Original card number
204
+
205
+ Returns:
206
+ Masked card with same format
207
+ """
208
+ cleaned = card_number.replace(" ", "").replace("-", "")
209
+ mask_count = max(len(cleaned) - 4, 0)
210
+
211
+ masked = self.config.mask_char * mask_count + cleaned[-4:] if mask_count > 0 else cleaned
212
+
213
+ # Return in original format
214
+ return self._apply_format(card_number, masked)
215
+
216
+ def _anonymize_preserve_last4(self, cleaned: str, original: str) -> str:
217
+ """Anonymize but preserve last 4 digits.
218
+
219
+ Args:
220
+ cleaned: Card number without separators
221
+ original: Original card number with formatting
222
+
223
+ Returns:
224
+ Anonymized card number in same format as original
225
+ """
226
+ rng = random.Random(f"{self.config.seed}:{cleaned}".encode())
227
+
228
+ last4 = cleaned[-4:]
229
+ first_part = cleaned[:-4]
230
+
231
+ # Generate random middle digits (excluding the check digit space)
232
+ # We need to preserve last 4, so randomize everything before that
233
+ middle_length = len(first_part) - 6
234
+ if middle_length > 0:
235
+ middle = "".join(str(rng.randint(0, 9)) for _ in range(middle_length))
236
+ else:
237
+ middle = ""
238
+
239
+ # Generate BIN (first 6 digits) - keep valid card type if possible
240
+ card_type = detect_card_type(cleaned)
241
+ bin_digits = self._generate_bin(card_type, rng)
242
+
243
+ # Reconstruct card number WITHOUT checksum first
244
+ # We preserve the last 4, then calculate checksum for the rest
245
+ partial_card = bin_digits + middle + last4[:-1] # First 3 of last4
246
+ checksum = luhn_checksum(partial_card)
247
+ anon_card = partial_card + str(checksum)
248
+
249
+ # Apply original format
250
+ return self._apply_format(original, anon_card)
251
+
252
+ def _anonymize_preserve_bin(self, cleaned: str, original: str) -> str:
253
+ """Anonymize but preserve BIN (first 6 digits).
254
+
255
+ Args:
256
+ cleaned: Card number without separators
257
+ original: Original card number with formatting
258
+
259
+ Returns:
260
+ Anonymized card number in same format as original
261
+ """
262
+ rng = random.Random(f"{self.config.seed}:{cleaned}".encode())
263
+
264
+ bin_digits = cleaned[:6]
265
+ last4 = cleaned[-4:]
266
+
267
+ # Generate random middle digits
268
+ middle_length = len(cleaned) - 10
269
+ if middle_length > 0:
270
+ middle = "".join(str(rng.randint(0, 9)) for _ in range(middle_length))
271
+ else:
272
+ middle = ""
273
+
274
+ # Reconstruct card number
275
+ anon_card = bin_digits + middle + last4
276
+
277
+ # Calculate and append Luhn checksum
278
+ checksum = luhn_checksum(anon_card[:-1])
279
+ anon_card = anon_card[:-1] + str(checksum)
280
+
281
+ # Apply original format
282
+ return self._apply_format(original, anon_card)
283
+
284
+ def _anonymize_full(self, cleaned: str, original: str) -> str:
285
+ """Fully anonymize card number.
286
+
287
+ Args:
288
+ cleaned: Card number without separators
289
+ original: Original card number with formatting
290
+
291
+ Returns:
292
+ Anonymized card number in same format as original
293
+ """
294
+ rng = random.Random(f"{self.config.seed}:{cleaned}".encode())
295
+
296
+ card_type = detect_card_type(cleaned)
297
+ bin_digits = self._generate_bin(card_type, rng)
298
+
299
+ # Generate random remaining digits
300
+ remaining_length = len(cleaned) - 7
301
+ remaining = "".join(str(rng.randint(0, 9)) for _ in range(remaining_length))
302
+
303
+ # Reconstruct card number
304
+ anon_card = bin_digits + remaining
305
+
306
+ # Calculate and append Luhn checksum
307
+ checksum = luhn_checksum(anon_card[:-1])
308
+ anon_card = anon_card[:-1] + str(checksum)
309
+
310
+ # Apply original format
311
+ return self._apply_format(original, anon_card)
312
+
313
+ def _generate_bin(self, card_type: str, rng: random.Random) -> str:
314
+ """Generate valid BIN for card type.
315
+
316
+ Args:
317
+ card_type: Card type (visa, mastercard, etc)
318
+ rng: Random number generator
319
+
320
+ Returns:
321
+ Valid 6-digit BIN
322
+ """
323
+ if card_type == "visa":
324
+ return "4" + "".join(str(rng.randint(0, 9)) for _ in range(5))
325
+ elif card_type == "mastercard":
326
+ prefix = rng.choice([51, 52, 53, 54, 55])
327
+ return str(prefix) + "".join(str(rng.randint(0, 9)) for _ in range(4))
328
+ elif card_type == "amex":
329
+ prefix = rng.choice([34, 37])
330
+ return str(prefix) + "".join(str(rng.randint(0, 9)) for _ in range(4))
331
+ elif card_type == "discover":
332
+ prefix = rng.choice([6011, 622, 644, 645, 646, 647, 648, 649, 65])
333
+ prefix_str = str(prefix)
334
+ remaining = 6 - len(prefix_str)
335
+ return prefix_str + "".join(str(rng.randint(0, 9)) for _ in range(remaining))
336
+ else:
337
+ # Default: generate random BIN
338
+ return "".join(str(rng.randint(0, 9)) for _ in range(6))
339
+
340
+ def _apply_format(self, original: str, cleaned: str) -> str:
341
+ """Apply original formatting to cleaned card number.
342
+
343
+ Args:
344
+ original: Original card number with formatting
345
+ cleaned: Cleaned anonymized card number
346
+
347
+ Returns:
348
+ Card number with original formatting applied
349
+ """
350
+ result = []
351
+ cleaned_idx = 0
352
+
353
+ for char in original:
354
+ if char.isdigit():
355
+ if cleaned_idx < len(cleaned):
356
+ result.append(cleaned[cleaned_idx])
357
+ cleaned_idx += 1
358
+ else:
359
+ result.append(char)
360
+
361
+ return "".join(result)
362
+
363
+ def short_name(self) -> str:
364
+ """Return short strategy name for logging.
365
+
366
+ Returns:
367
+ Short name (e.g., "credit_card:preserve_last4")
368
+ """
369
+ if self.config.preserve_bin:
370
+ return "credit_card:preserve_bin"
371
+ elif self.config.preserve_last4:
372
+ return "credit_card:preserve_last4"
373
+ else:
374
+ return "credit_card:full"
@@ -0,0 +1,161 @@
1
+ """Custom anonymization strategy.
2
+
3
+ Provides a framework for implementing custom anonymization logic:
4
+ - Callable-based strategy (use any Python function)
5
+ - Deterministic with seed
6
+ - Type validation
7
+ - Logging-friendly
8
+
9
+ Useful for domain-specific anonymization that doesn't fit into built-in strategies.
10
+ """
11
+
12
+ from collections.abc import Callable
13
+ from dataclasses import dataclass
14
+ from typing import Any
15
+
16
+ from confiture.core.anonymization.strategy import AnonymizationStrategy, StrategyConfig
17
+
18
+
19
+ @dataclass
20
+ class CustomConfig(StrategyConfig):
21
+ """Configuration for custom anonymization strategy.
22
+
23
+ Attributes:
24
+ seed: Seed for deterministic randomization
25
+ func: Callable that performs anonymization
26
+ name: Human-readable name for custom function
27
+ accepts_seed: If True, func receives (value, seed) else just (value)
28
+
29
+ Example:
30
+ >>> def upper_mask(value):
31
+ ... return value.upper() if isinstance(value, str) else value
32
+ >>> config = CustomConfig(seed=12345, func=upper_mask, name="uppercase")
33
+ """
34
+
35
+ func: Callable[[Any], Any] | None = None
36
+ name: str = "custom"
37
+ accepts_seed: bool = False
38
+
39
+
40
+ class CustomStrategy(AnonymizationStrategy):
41
+ """Custom anonymization strategy using callable functions.
42
+
43
+ Allows implementing domain-specific anonymization logic without
44
+ creating a full strategy class. Useful for one-off anonymization needs.
45
+
46
+ Features:
47
+ - Function-based anonymization
48
+ - Optional seed parameter
49
+ - Type validation
50
+ - Clear error handling
51
+
52
+ Example:
53
+ >>> def hash_value(value):
54
+ ... return f"hash_{hash(str(value))}"
55
+ >>> config = CustomConfig(seed=12345, func=hash_value)
56
+ >>> strategy = CustomStrategy(config)
57
+ >>> strategy.anonymize("secret")
58
+ 'hash_...'
59
+ """
60
+
61
+ config_type = CustomConfig
62
+ strategy_name = "custom"
63
+
64
+ def anonymize(self, value: Any) -> Any:
65
+ """Apply custom anonymization function.
66
+
67
+ Args:
68
+ value: Value to anonymize
69
+
70
+ Returns:
71
+ Anonymized value
72
+
73
+ Raises:
74
+ RuntimeError: If custom function is not configured
75
+ Exception: Any exception from custom function
76
+ """
77
+ if self.config.func is None:
78
+ raise RuntimeError("Custom strategy requires 'func' to be configured")
79
+
80
+ try:
81
+ if self.config.accepts_seed:
82
+ return self.config.func(value, self.config.seed)
83
+ else:
84
+ return self.config.func(value)
85
+ except Exception as e:
86
+ raise Exception(
87
+ f"Error in custom anonymization function '{self.config.name}': {e}"
88
+ ) from e
89
+
90
+ def validate(self, value: Any) -> bool: # noqa: ARG002
91
+ """Check if strategy can handle this value type.
92
+
93
+ Args:
94
+ value: Sample value to validate (unused, custom accepts any)
95
+
96
+ Returns:
97
+ True (custom accepts any value type)
98
+ """
99
+ return True
100
+
101
+ def short_name(self) -> str:
102
+ """Return short strategy name for logging.
103
+
104
+ Returns:
105
+ Short name (e.g., "custom:my_function")
106
+ """
107
+ return f"{self.strategy_name}:{self.config.name}"
108
+
109
+
110
+ class CustomLambdaStrategy(AnonymizationStrategy):
111
+ """Custom strategy using inline lambda functions.
112
+
113
+ Simplified version using lambda expressions for very simple
114
+ anonymization logic.
115
+
116
+ Example:
117
+ >>> config = CustomConfig(
118
+ ... func=lambda x: f"anon_{hash(x) % 10000}",
119
+ ... name="hash_last4"
120
+ ... )
121
+ >>> strategy = CustomLambdaStrategy(config)
122
+ """
123
+
124
+ config_type = CustomConfig
125
+ strategy_name = "custom_lambda"
126
+
127
+ def anonymize(self, value: Any) -> Any:
128
+ """Apply lambda-based anonymization.
129
+
130
+ Args:
131
+ value: Value to anonymize
132
+
133
+ Returns:
134
+ Anonymized value
135
+ """
136
+ if self.config.func is None:
137
+ raise RuntimeError("Lambda strategy requires 'func' to be configured")
138
+
139
+ try:
140
+ return self.config.func(value)
141
+ except Exception as e:
142
+ raise Exception(f"Error in lambda anonymization: {e}") from e
143
+
144
+ def validate(self, value: Any) -> bool: # noqa: ARG002
145
+ """Check if strategy can handle this value type.
146
+
147
+ Args:
148
+ value: Sample value to validate (unused, lambda accepts any)
149
+
150
+ Returns:
151
+ True (lambda accepts any value type)
152
+ """
153
+ return True
154
+
155
+ def short_name(self) -> str:
156
+ """Return short strategy name for logging.
157
+
158
+ Returns:
159
+ Short name
160
+ """
161
+ return f"{self.strategy_name}:{self.config.name}"