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
+ """Strategy composition system for chaining multiple anonymization strategies.
2
+
3
+ Provides:
4
+ - Sequential strategy chaining (apply strategies one after another)
5
+ - Composite strategy containers
6
+ - Configuration-driven composition
7
+ - Error handling and validation
8
+ - Logging and monitoring
9
+
10
+ Enables complex anonymization scenarios like:
11
+ - Apply name masking first, then custom hash
12
+ - Redact emails, then preserve remaining text
13
+ - Multiple transforms on same column
14
+ """
15
+
16
+ from dataclasses import dataclass, field
17
+ from typing import Any
18
+
19
+ from confiture.core.anonymization.registry import StrategyRegistry
20
+ from confiture.core.anonymization.strategy import AnonymizationStrategy, StrategyConfig
21
+
22
+
23
+ @dataclass
24
+ class CompositionConfig(StrategyConfig):
25
+ """Configuration for strategy composition.
26
+
27
+ Attributes:
28
+ seed: Seed for deterministic randomization
29
+ strategies: List of strategy names or config dicts to chain
30
+ stop_on_none: If True, stop chain if any strategy returns None (default False)
31
+ stop_on_error: If True, stop chain on error (default False)
32
+ continue_on_empty: If True, skip empty strings/None in chain (default False)
33
+
34
+ Example:
35
+ >>> config = CompositionConfig(
36
+ ... seed=12345,
37
+ ... strategies=["name", "custom"],
38
+ ... stop_on_none=False
39
+ ... )
40
+ """
41
+
42
+ strategies: list[str] = field(default_factory=list)
43
+ stop_on_none: bool = False
44
+ stop_on_error: bool = False
45
+ continue_on_empty: bool = False
46
+
47
+
48
+ class StrategyComposer(AnonymizationStrategy):
49
+ """Composite strategy that chains multiple strategies sequentially.
50
+
51
+ Applies strategies one after another, passing output of each as input
52
+ to the next. Useful for complex anonymization workflows.
53
+
54
+ Features:
55
+ - Sequential strategy chaining
56
+ - Configuration-driven composition
57
+ - Error handling and recovery
58
+ - Logging of applied strategies
59
+ - Deterministic output
60
+
61
+ Example:
62
+ >>> config = CompositionConfig(
63
+ ... seed=12345,
64
+ ... strategies=["name:firstname_lastname", "custom:hash"]
65
+ ... )
66
+ >>> composer = StrategyComposer(config)
67
+ >>> composer.anonymize("John Doe")
68
+ 'hashed_michael_johnson'
69
+ """
70
+
71
+ config_type = CompositionConfig
72
+ strategy_name = "compose"
73
+
74
+ def __init__(self, config: CompositionConfig | None = None):
75
+ """Initialize composer with loaded strategies."""
76
+ super().__init__(config or CompositionConfig())
77
+ self._strategies = self._load_strategies()
78
+
79
+ def anonymize(self, value: Any) -> Any:
80
+ """Apply chained strategies to value.
81
+
82
+ Args:
83
+ value: Value to anonymize
84
+
85
+ Returns:
86
+ Anonymized value after all strategies applied
87
+
88
+ Example:
89
+ >>> composer.anonymize("test data")
90
+ 'transformed_data'
91
+ """
92
+ if value is None:
93
+ return None
94
+
95
+ current_value = value
96
+
97
+ for strategy, strategy_name in self._strategies:
98
+ try:
99
+ # Check skip conditions
100
+ if self.config.continue_on_empty and (
101
+ current_value is None
102
+ or (isinstance(current_value, str) and not current_value.strip())
103
+ ):
104
+ continue
105
+
106
+ # Apply strategy
107
+ current_value = strategy.anonymize(current_value)
108
+
109
+ # Check stop conditions
110
+ if self.config.stop_on_none and current_value is None:
111
+ break
112
+
113
+ except Exception as e:
114
+ if self.config.stop_on_error:
115
+ raise Exception(f"Error in strategy '{strategy_name}': {e}") from e
116
+ else:
117
+ # Skip failing strategy and continue
118
+ continue
119
+
120
+ return current_value
121
+
122
+ def validate(self, value: Any) -> bool:
123
+ """Check if strategy can handle this value type.
124
+
125
+ Args:
126
+ value: Sample value to validate
127
+
128
+ Returns:
129
+ True if any strategy can handle this type
130
+ """
131
+ if not self._strategies:
132
+ return True
133
+
134
+ # Check if any strategy accepts this type
135
+ return any(strategy.validate(value) for strategy, _ in self._strategies)
136
+
137
+ def _load_strategies(self) -> list[tuple]:
138
+ """Load strategies from configuration.
139
+
140
+ Returns:
141
+ List of (strategy_instance, strategy_name) tuples
142
+
143
+ Raises:
144
+ ValueError: If strategy not found in registry
145
+ """
146
+ loaded = []
147
+
148
+ for strategy_spec in self.config.strategies:
149
+ try:
150
+ # Handle strategy name or config dict
151
+ if isinstance(strategy_spec, str):
152
+ # Extract base strategy name (remove config suffix if present)
153
+ # "name:firstname_lastname" -> get "name" from registry
154
+ base_name = strategy_spec.split(":")[0]
155
+
156
+ # Get from registry with seed
157
+ strategy = StrategyRegistry.get(base_name, {"seed": self.config.seed})
158
+ strategy_name = strategy_spec
159
+ else:
160
+ # Config dict or StrategyConfig object
161
+ raise ValueError("Strategy dict config not yet supported")
162
+
163
+ loaded.append((strategy, strategy_name))
164
+
165
+ except Exception as e:
166
+ raise ValueError(f"Failed to load strategy '{strategy_spec}': {e}") from e
167
+
168
+ return loaded
169
+
170
+ def short_name(self) -> str:
171
+ """Return short strategy name for logging.
172
+
173
+ Returns:
174
+ Short name (e.g., "compose:name_custom")
175
+ """
176
+ if not self._strategies:
177
+ return f"{self.strategy_name}:empty"
178
+
179
+ strategy_names = [name for _, name in self._strategies[:3]]
180
+ strategies_str = "_".join(strategy_names)
181
+ return f"{self.strategy_name}:{strategies_str}"
182
+
183
+ def get_strategy_chain(self) -> list[str]:
184
+ """Get list of strategies in chain.
185
+
186
+ Returns:
187
+ List of strategy names in order
188
+ """
189
+ return [name for _, name in self._strategies]
190
+
191
+
192
+ class StrategySequence:
193
+ """Builder for composing strategies with fluent API.
194
+
195
+ Provides convenient syntax for building strategy chains.
196
+
197
+ Example:
198
+ >>> sequence = StrategySequence(seed=12345)
199
+ >>> sequence.add("name:firstname_lastname").add("custom:hash").build()
200
+ StrategyComposer instance
201
+ """
202
+
203
+ def __init__(self, seed: int = 0):
204
+ """Initialize sequence builder.
205
+
206
+ Args:
207
+ seed: Seed for deterministic randomization
208
+ """
209
+ self.seed = seed
210
+ self.strategies = []
211
+ self.stop_on_none = False
212
+ self.stop_on_error = False
213
+ self.continue_on_empty = False
214
+
215
+ def add(self, strategy_name: str) -> "StrategySequence":
216
+ """Add strategy to sequence.
217
+
218
+ Args:
219
+ strategy_name: Name of strategy to add
220
+
221
+ Returns:
222
+ Self for chaining
223
+ """
224
+ self.strategies.append(strategy_name)
225
+ return self
226
+
227
+ def add_many(self, *strategy_names: str) -> "StrategySequence":
228
+ """Add multiple strategies to sequence.
229
+
230
+ Args:
231
+ *strategy_names: Variable number of strategy names
232
+
233
+ Returns:
234
+ Self for chaining
235
+
236
+ Example:
237
+ >>> seq.add_many("name", "email", "phone")
238
+ """
239
+ self.strategies.extend(strategy_names)
240
+ return self
241
+
242
+ def on_none(self, stop: bool = True) -> "StrategySequence":
243
+ """Configure stopping on None values.
244
+
245
+ Args:
246
+ stop: If True, stop chain on None
247
+
248
+ Returns:
249
+ Self for chaining
250
+ """
251
+ self.stop_on_none = stop
252
+ return self
253
+
254
+ def on_error(self, stop: bool = True) -> "StrategySequence":
255
+ """Configure stopping on errors.
256
+
257
+ Args:
258
+ stop: If True, stop chain on error
259
+
260
+ Returns:
261
+ Self for chaining
262
+ """
263
+ self.stop_on_error = stop
264
+ return self
265
+
266
+ def skip_empty(self, skip: bool = True) -> "StrategySequence":
267
+ """Configure skipping empty strings/None.
268
+
269
+ Args:
270
+ skip: If True, skip empty values
271
+
272
+ Returns:
273
+ Self for chaining
274
+ """
275
+ self.continue_on_empty = skip
276
+ return self
277
+
278
+ def build(self) -> StrategyComposer:
279
+ """Build the composed strategy.
280
+
281
+ Returns:
282
+ StrategyComposer instance
283
+
284
+ Raises:
285
+ ValueError: If no strategies configured
286
+ """
287
+ if not self.strategies:
288
+ raise ValueError("No strategies configured")
289
+
290
+ config = CompositionConfig(
291
+ seed=self.seed,
292
+ strategies=self.strategies,
293
+ stop_on_none=self.stop_on_none,
294
+ stop_on_error=self.stop_on_error,
295
+ continue_on_empty=self.continue_on_empty,
296
+ )
297
+
298
+ return StrategyComposer(config)