fraiseql-confiture 0.3.4__cp311-cp311-win_amd64.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.cp311-win_amd64.pyd +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 +1656 -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 +132 -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 +793 -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 +0 -0
- confiture/models/lint.py +193 -0
- confiture/models/migration.py +180 -0
- confiture/models/schema.py +203 -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 +38 -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/utils/__init__.py +0 -0
- fraiseql_confiture-0.3.4.dist-info/METADATA +438 -0
- fraiseql_confiture-0.3.4.dist-info/RECORD +119 -0
- fraiseql_confiture-0.3.4.dist-info/WHEEL +4 -0
- fraiseql_confiture-0.3.4.dist-info/entry_points.txt +2 -0
- fraiseql_confiture-0.3.4.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)
|