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,319 @@
|
|
|
1
|
+
"""Strategy factory for creating strategies from profile configurations.
|
|
2
|
+
|
|
3
|
+
Provides:
|
|
4
|
+
- Profile-based strategy creation
|
|
5
|
+
- Column-to-strategy mapping
|
|
6
|
+
- Strategy suggestion engine
|
|
7
|
+
- Factory caching and optimization
|
|
8
|
+
- Configuration validation
|
|
9
|
+
|
|
10
|
+
Enables declarative strategy configuration via profiles.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from confiture.core.anonymization.registry import StrategyRegistry
|
|
17
|
+
from confiture.core.anonymization.strategy import AnonymizationStrategy
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class StrategyProfile:
|
|
22
|
+
"""Configuration profile mapping columns to strategies.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
name: Profile name (e.g., "ecommerce", "healthcare")
|
|
26
|
+
seed: Global seed for all strategies (can be overridden per column)
|
|
27
|
+
columns: Dict mapping column name to strategy name/config
|
|
28
|
+
defaults: Default strategy for unmapped columns
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
>>> profile = StrategyProfile(
|
|
32
|
+
... name="ecommerce",
|
|
33
|
+
... seed=12345,
|
|
34
|
+
... columns={
|
|
35
|
+
... "customer_name": "name:firstname_lastname",
|
|
36
|
+
... "email": "email_mask",
|
|
37
|
+
... "phone": "phone_mask"
|
|
38
|
+
... }
|
|
39
|
+
... )
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
name: str
|
|
43
|
+
seed: int = 0
|
|
44
|
+
columns: dict[str, str] = field(default_factory=dict)
|
|
45
|
+
defaults: str = "preserve"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class StrategyFactory:
|
|
49
|
+
"""Factory for creating strategies from profiles.
|
|
50
|
+
|
|
51
|
+
Creates strategy instances based on profile configuration,
|
|
52
|
+
with caching and validation.
|
|
53
|
+
|
|
54
|
+
Features:
|
|
55
|
+
- Profile-based strategy creation
|
|
56
|
+
- Column mapping
|
|
57
|
+
- Strategy caching
|
|
58
|
+
- Configuration validation
|
|
59
|
+
- Suggestion engine
|
|
60
|
+
|
|
61
|
+
Example:
|
|
62
|
+
>>> profile = StrategyProfile(
|
|
63
|
+
... name="ecommerce",
|
|
64
|
+
... columns={"name": "name", "email": "email"}
|
|
65
|
+
... )
|
|
66
|
+
>>> factory = StrategyFactory(profile)
|
|
67
|
+
>>> strategy = factory.get_strategy("name")
|
|
68
|
+
>>> anonymized = strategy.anonymize("John Doe")
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, profile: StrategyProfile):
|
|
72
|
+
"""Initialize factory with profile.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
profile: Strategy profile configuration
|
|
76
|
+
"""
|
|
77
|
+
self.profile = profile
|
|
78
|
+
self._cache = {}
|
|
79
|
+
self._validate_profile()
|
|
80
|
+
|
|
81
|
+
def get_strategy(self, column_name: str) -> AnonymizationStrategy:
|
|
82
|
+
"""Get strategy for column.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
column_name: Name of column
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
AnonymizationStrategy instance
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
ValueError: If strategy not found
|
|
92
|
+
"""
|
|
93
|
+
# Check cache first
|
|
94
|
+
if column_name in self._cache:
|
|
95
|
+
return self._cache[column_name]
|
|
96
|
+
|
|
97
|
+
# Get strategy name from profile
|
|
98
|
+
strategy_name = self.profile.columns.get(column_name, self.profile.defaults)
|
|
99
|
+
|
|
100
|
+
# Create strategy
|
|
101
|
+
try:
|
|
102
|
+
strategy = StrategyRegistry.get(strategy_name, {"seed": self.profile.seed})
|
|
103
|
+
self._cache[column_name] = strategy
|
|
104
|
+
return strategy
|
|
105
|
+
except ValueError as e:
|
|
106
|
+
raise ValueError(f"Failed to create strategy for column '{column_name}': {e}") from e
|
|
107
|
+
|
|
108
|
+
def get_strategies(self, column_names: list[str]) -> dict[str, AnonymizationStrategy]:
|
|
109
|
+
"""Get strategies for multiple columns.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
column_names: List of column names
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Dict mapping column name to strategy
|
|
116
|
+
"""
|
|
117
|
+
return {col: self.get_strategy(col) for col in column_names}
|
|
118
|
+
|
|
119
|
+
def anonymize(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
120
|
+
"""Anonymize entire data dictionary using profile.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
data: Dict mapping column names to values
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Dict with anonymized values
|
|
127
|
+
|
|
128
|
+
Example:
|
|
129
|
+
>>> data = {"name": "John", "email": "john@example.com"}
|
|
130
|
+
>>> anonymized = factory.anonymize(data)
|
|
131
|
+
"""
|
|
132
|
+
result = {}
|
|
133
|
+
|
|
134
|
+
for column_name, value in data.items():
|
|
135
|
+
strategy = self.get_strategy(column_name)
|
|
136
|
+
result[column_name] = strategy.anonymize(value)
|
|
137
|
+
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
def _validate_profile(self) -> None:
|
|
141
|
+
"""Validate profile configuration.
|
|
142
|
+
|
|
143
|
+
Raises:
|
|
144
|
+
ValueError: If profile is invalid
|
|
145
|
+
"""
|
|
146
|
+
if not self.profile.name:
|
|
147
|
+
raise ValueError("Profile must have a name")
|
|
148
|
+
|
|
149
|
+
# Validate strategy names exist
|
|
150
|
+
all_strategies = list(self.profile.columns.values()) + [self.profile.defaults]
|
|
151
|
+
|
|
152
|
+
for strategy_name in all_strategies:
|
|
153
|
+
# Extract base strategy name (remove config suffix if present)
|
|
154
|
+
# "name:firstname_lastname" -> "name"
|
|
155
|
+
base_name = strategy_name.split(":")[0] if strategy_name else ""
|
|
156
|
+
|
|
157
|
+
if not StrategyRegistry.is_registered(base_name):
|
|
158
|
+
raise ValueError(f"Unknown strategy: {strategy_name}")
|
|
159
|
+
|
|
160
|
+
def list_column_strategies(self) -> dict[str, str]:
|
|
161
|
+
"""List all column-to-strategy mappings.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Dict mapping column names to strategy names
|
|
165
|
+
"""
|
|
166
|
+
return dict(self.profile.columns)
|
|
167
|
+
|
|
168
|
+
def clear_cache(self) -> None:
|
|
169
|
+
"""Clear strategy cache (useful for testing)."""
|
|
170
|
+
self._cache.clear()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class StrategySuggester:
|
|
174
|
+
"""Suggests appropriate strategies based on column characteristics.
|
|
175
|
+
|
|
176
|
+
Analyzes column names, data types, and sample values to suggest
|
|
177
|
+
anonymization strategies.
|
|
178
|
+
|
|
179
|
+
Features:
|
|
180
|
+
- Pattern-based column analysis
|
|
181
|
+
- Data type detection
|
|
182
|
+
- Multi-strategy suggestion
|
|
183
|
+
- Confidence scoring
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
# Patterns for column name detection
|
|
187
|
+
NAME_PATTERNS = [
|
|
188
|
+
"name",
|
|
189
|
+
"fullname",
|
|
190
|
+
"full_name",
|
|
191
|
+
"firstname",
|
|
192
|
+
"first_name",
|
|
193
|
+
"lastname",
|
|
194
|
+
"last_name",
|
|
195
|
+
"personname",
|
|
196
|
+
"person_name",
|
|
197
|
+
]
|
|
198
|
+
EMAIL_PATTERNS = ["email", "email_address", "e_mail", "mail"]
|
|
199
|
+
PHONE_PATTERNS = ["phone", "telephone", "mobile", "cellphone", "cell_phone"]
|
|
200
|
+
ADDRESS_PATTERNS = ["address", "street", "city", "state", "zip", "postal"]
|
|
201
|
+
DATE_PATTERNS = ["date", "born", "birthday", "birthdate", "dob", "created"]
|
|
202
|
+
CC_PATTERNS = ["credit", "card", "cc", "cardnumber", "card_number"]
|
|
203
|
+
IP_PATTERNS = ["ip", "ipaddress", "ip_address", "server"]
|
|
204
|
+
|
|
205
|
+
def suggest(self, column_name: str, sample_value: str | None = None) -> list[tuple]:
|
|
206
|
+
"""Suggest strategies for column.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
column_name: Name of column
|
|
210
|
+
sample_value: Optional sample value for analysis
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
List of (strategy_name, confidence) tuples, sorted by confidence
|
|
214
|
+
"""
|
|
215
|
+
suggestions = []
|
|
216
|
+
|
|
217
|
+
# Analyze column name
|
|
218
|
+
col_lower = column_name.lower()
|
|
219
|
+
|
|
220
|
+
if any(p in col_lower for p in self.NAME_PATTERNS):
|
|
221
|
+
suggestions.append(("name:firstname_lastname", 0.95))
|
|
222
|
+
|
|
223
|
+
if any(p in col_lower for p in self.EMAIL_PATTERNS):
|
|
224
|
+
suggestions.append(("email", 0.95))
|
|
225
|
+
|
|
226
|
+
if any(p in col_lower for p in self.PHONE_PATTERNS):
|
|
227
|
+
suggestions.append(("phone", 0.90))
|
|
228
|
+
|
|
229
|
+
if any(p in col_lower for p in self.ADDRESS_PATTERNS):
|
|
230
|
+
suggestions.append(("address", 0.85))
|
|
231
|
+
|
|
232
|
+
if any(p in col_lower for p in self.DATE_PATTERNS):
|
|
233
|
+
suggestions.append(("date", 0.85))
|
|
234
|
+
|
|
235
|
+
if any(p in col_lower for p in self.CC_PATTERNS):
|
|
236
|
+
suggestions.append(("credit_card", 0.90))
|
|
237
|
+
|
|
238
|
+
if any(p in col_lower for p in self.IP_PATTERNS):
|
|
239
|
+
suggestions.append(("ip_address", 0.85))
|
|
240
|
+
|
|
241
|
+
# Analyze sample value if provided
|
|
242
|
+
if sample_value:
|
|
243
|
+
value_suggestions = self._analyze_value(sample_value)
|
|
244
|
+
suggestions.extend(value_suggestions)
|
|
245
|
+
|
|
246
|
+
# Remove duplicates, keep highest confidence
|
|
247
|
+
seen = {}
|
|
248
|
+
for strategy, confidence in suggestions:
|
|
249
|
+
if strategy not in seen or confidence > seen[strategy]:
|
|
250
|
+
seen[strategy] = confidence
|
|
251
|
+
|
|
252
|
+
# Sort by confidence descending
|
|
253
|
+
return sorted(
|
|
254
|
+
seen.items(),
|
|
255
|
+
key=lambda x: x[1],
|
|
256
|
+
reverse=True,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def _analyze_value(self, value: str) -> list[tuple]:
|
|
260
|
+
"""Analyze sample value for pattern matching.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
value: Sample value
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
List of (strategy_name, confidence) tuples
|
|
267
|
+
"""
|
|
268
|
+
suggestions = []
|
|
269
|
+
|
|
270
|
+
# Check for email pattern
|
|
271
|
+
if "@" in value and "." in value:
|
|
272
|
+
suggestions.append(("email", 0.85))
|
|
273
|
+
|
|
274
|
+
# Check for phone pattern (simple)
|
|
275
|
+
if (
|
|
276
|
+
any(c.isdigit() for c in value)
|
|
277
|
+
and len(value) >= 10
|
|
278
|
+
and ("-" in value or "(" in value or ")" in value)
|
|
279
|
+
):
|
|
280
|
+
suggestions.append(("phone", 0.75))
|
|
281
|
+
|
|
282
|
+
# Check for IP pattern
|
|
283
|
+
if value.count(".") == 3:
|
|
284
|
+
parts = value.split(".")
|
|
285
|
+
if all(p.isdigit() and 0 <= int(p) <= 255 for p in parts):
|
|
286
|
+
suggestions.append(("ip_address", 0.90))
|
|
287
|
+
|
|
288
|
+
# Check for credit card pattern
|
|
289
|
+
if all(c.isdigit() or c in " -" for c in value):
|
|
290
|
+
cleaned = value.replace(" ", "").replace("-", "")
|
|
291
|
+
if 13 <= len(cleaned) <= 19:
|
|
292
|
+
suggestions.append(("credit_card", 0.70))
|
|
293
|
+
|
|
294
|
+
return suggestions
|
|
295
|
+
|
|
296
|
+
def create_profile(self, name: str, columns: list[str], seed: int = 0) -> StrategyProfile:
|
|
297
|
+
"""Create profile based on column suggestions.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
name: Profile name
|
|
301
|
+
columns: List of column names
|
|
302
|
+
seed: Global seed
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
StrategyProfile with suggested strategies
|
|
306
|
+
"""
|
|
307
|
+
column_map = {}
|
|
308
|
+
|
|
309
|
+
for col in columns:
|
|
310
|
+
suggestions = self.suggest(col)
|
|
311
|
+
if suggestions:
|
|
312
|
+
# Use strategy with highest confidence
|
|
313
|
+
strategy_name = suggestions[0][0]
|
|
314
|
+
column_map[col] = strategy_name
|
|
315
|
+
else:
|
|
316
|
+
# Default to preserve
|
|
317
|
+
column_map[col] = "preserve"
|
|
318
|
+
|
|
319
|
+
return StrategyProfile(name=name, seed=seed, columns=column_map, defaults="preserve")
|