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,284 @@
1
+ """Anonymization profile management with Pydantic schema validation.
2
+
3
+ This module provides secure YAML profile loading with:
4
+ - yaml.safe_load() to prevent injection attacks
5
+ - Pydantic schema validation to enforce structure
6
+ - Strategy type whitelist to prevent unknown strategies
7
+
8
+ Security Note:
9
+ ✅ Uses yaml.safe_load() instead of yaml.load() - prevents code execution
10
+ ✅ Pydantic validates all structure before use
11
+ ✅ StrategyType enum whitelists allowed strategies
12
+ ❌ Never use yaml.load() - it can execute arbitrary Python code
13
+
14
+ Example:
15
+ >>> profile = AnonymizationProfile.load(Path("profiles/production.yaml"))
16
+ >>> print(profile.name)
17
+ 'production'
18
+ >>> print(list(profile.strategies.keys()))
19
+ ['email_mask', 'phone_mask']
20
+ """
21
+
22
+ from enum import Enum
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ import yaml
27
+ from pydantic import BaseModel, field_validator
28
+
29
+
30
+ class StrategyType(str, Enum):
31
+ """Whitelist of allowed strategy types.
32
+
33
+ Only these strategies are permitted in YAML profiles. This prevents
34
+ arbitrary strategy types that could be used for injection attacks.
35
+
36
+ Attributes:
37
+ HASH: DeterministicHashStrategy - HMAC-based hashing
38
+ EMAIL: EmailMaskingStrategy - Format-preserving email masking
39
+ PHONE: PhoneMaskingStrategy - Format-preserving phone masking
40
+ REDACT: SimpleRedactStrategy - Simple redaction
41
+ """
42
+
43
+ HASH = "hash"
44
+ """HMAC-based deterministic hashing strategy."""
45
+
46
+ EMAIL = "email"
47
+ """Format-preserving email masking strategy."""
48
+
49
+ PHONE = "phone"
50
+ """Format-preserving phone number masking strategy."""
51
+
52
+ REDACT = "redact"
53
+ """Simple redaction strategy (all values → [REDACTED])."""
54
+
55
+
56
+ class StrategyDefinition(BaseModel):
57
+ """Pydantic model for strategy definition in profiles.
58
+
59
+ Each strategy in the profile must match this structure. Pydantic validates:
60
+ - Type is in the StrategyType whitelist
61
+ - Config (if provided) is a dictionary
62
+ - All required fields are present
63
+
64
+ Attributes:
65
+ type: Strategy type (must be in StrategyType enum)
66
+ config: Optional configuration dict for the strategy
67
+ seed_env_var: Optional environment variable for deterministic seed
68
+ """
69
+
70
+ type: str
71
+ """Strategy type name (must be in StrategyType enum)."""
72
+
73
+ config: dict[str, Any] | None = None
74
+ """Optional configuration dict for strategy-specific settings."""
75
+
76
+ seed_env_var: str | None = None
77
+ """Optional environment variable containing seed for determinism."""
78
+
79
+ @field_validator("type")
80
+ @classmethod
81
+ def validate_type(cls, v: str) -> str:
82
+ """Validate strategy type is in whitelist.
83
+
84
+ Args:
85
+ v: Strategy type name
86
+
87
+ Returns:
88
+ Validated strategy type name
89
+
90
+ Raises:
91
+ ValueError: If type is not in StrategyType enum
92
+ """
93
+ allowed = {st.value for st in StrategyType}
94
+ if v not in allowed:
95
+ raise ValueError(
96
+ f"Strategy type '{v}' not allowed. Allowed types: {', '.join(sorted(allowed))}"
97
+ )
98
+ return v
99
+
100
+
101
+ class AnonymizationRule(BaseModel):
102
+ """Rule for anonymizing a specific column.
103
+
104
+ Attributes:
105
+ column: Name of the column to anonymize
106
+ strategy: Strategy to apply (must reference a defined strategy)
107
+ seed: Optional column-specific seed (overrides global_seed)
108
+ options: Optional strategy-specific options
109
+ """
110
+
111
+ column: str
112
+ """Name of the column to anonymize."""
113
+
114
+ strategy: str
115
+ """Strategy name to apply (must be defined in strategies section)."""
116
+
117
+ seed: int | None = None
118
+ """Column-specific seed (overrides global_seed if provided)."""
119
+
120
+ options: dict[str, Any] | None = None
121
+ """Strategy-specific configuration options."""
122
+
123
+
124
+ class TableDefinition(BaseModel):
125
+ """Rules for anonymizing a specific table.
126
+
127
+ Attributes:
128
+ rules: List of anonymization rules for this table
129
+ """
130
+
131
+ rules: list[AnonymizationRule]
132
+ """Rules for anonymizing columns in this table."""
133
+
134
+
135
+ class AnonymizationProfile(BaseModel):
136
+ """Anonymization profile with schema validation.
137
+
138
+ This Pydantic model validates the entire profile structure before use:
139
+ - All required fields are present
140
+ - Strategy types are whitelisted
141
+ - Global seed and column seeds have proper precedence
142
+
143
+ Attributes:
144
+ name: Profile name (for identification)
145
+ version: Profile version (for tracking changes)
146
+ global_seed: Optional seed applied to all columns (if env var not provided)
147
+ strategies: Dictionary of strategy definitions (validated)
148
+ tables: Dictionary of table rules (validated)
149
+
150
+ Example:
151
+ >>> profile = AnonymizationProfile(
152
+ ... name="production",
153
+ ... version="1.0",
154
+ ... global_seed=12345,
155
+ ... strategies={
156
+ ... "email_mask": StrategyDefinition(type="email")
157
+ ... },
158
+ ... tables={
159
+ ... "users": TableDefinition(rules=[
160
+ ... AnonymizationRule(column="email", strategy="email_mask")
161
+ ... ])
162
+ ... }
163
+ ... )
164
+ """
165
+
166
+ name: str
167
+ """Profile name for identification."""
168
+
169
+ version: str
170
+ """Profile version (e.g., "1.0")."""
171
+
172
+ global_seed: int | None = None
173
+ """Optional seed applied to all columns unless overridden."""
174
+
175
+ strategies: dict[str, StrategyDefinition]
176
+ """Dictionary of strategy definitions by name."""
177
+
178
+ tables: dict[str, TableDefinition]
179
+ """Dictionary of table rules by table name."""
180
+
181
+ @classmethod
182
+ def load(cls, path: Path | str) -> "AnonymizationProfile":
183
+ """Load profile from YAML file with safe loading and validation.
184
+
185
+ Uses yaml.safe_load() to prevent code injection attacks, then validates
186
+ the loaded structure with Pydantic schema validation.
187
+
188
+ Args:
189
+ path: Path to YAML profile file
190
+
191
+ Returns:
192
+ Validated AnonymizationProfile instance
193
+
194
+ Raises:
195
+ FileNotFoundError: If profile file doesn't exist
196
+ yaml.YAMLError: If YAML is malformed
197
+ ValueError: If profile structure doesn't match schema
198
+
199
+ Example:
200
+ >>> profile = AnonymizationProfile.load("profiles/production.yaml")
201
+ >>> print(f"Profile: {profile.name} v{profile.version}")
202
+ Profile: production v1.0
203
+ """
204
+ path = Path(path)
205
+
206
+ if not path.exists():
207
+ raise FileNotFoundError(f"Profile file not found: {path}")
208
+
209
+ try:
210
+ with open(path) as f:
211
+ # ✅ SAFE: Use safe_load, not load
212
+ raw_data = yaml.safe_load(f)
213
+ except yaml.YAMLError as e:
214
+ raise ValueError(f"Invalid YAML in profile {path}: {e}") from e
215
+
216
+ if raw_data is None:
217
+ raise ValueError(f"Profile {path} is empty")
218
+
219
+ # ✅ SAFE: Pydantic validates structure and types
220
+ try:
221
+ profile = cls(**raw_data)
222
+ except Exception as e:
223
+ raise ValueError(f"Invalid profile {path}: {e}") from e
224
+
225
+ return profile
226
+
227
+ @classmethod
228
+ def from_dict(cls, data: dict[str, Any]) -> "AnonymizationProfile":
229
+ """Create profile from dictionary (for testing).
230
+
231
+ Args:
232
+ data: Dictionary with profile data
233
+
234
+ Returns:
235
+ Validated AnonymizationProfile instance
236
+
237
+ Raises:
238
+ ValueError: If profile structure doesn't match schema
239
+ """
240
+ return cls(**data)
241
+
242
+
243
+ def resolve_seed_for_column(rule: AnonymizationRule, profile: AnonymizationProfile) -> int:
244
+ """Resolve seed for a column with proper precedence.
245
+
246
+ Resolution order:
247
+ 1. Column-specific seed (highest priority)
248
+ 2. Global profile seed
249
+ 3. Default seed (0)
250
+
251
+ Args:
252
+ rule: Anonymization rule for the column
253
+ profile: Parent anonymization profile
254
+
255
+ Returns:
256
+ Resolved seed value as integer
257
+
258
+ Example:
259
+ >>> profile = AnonymizationProfile.from_dict({
260
+ ... "name": "test",
261
+ ... "version": "1.0",
262
+ ... "global_seed": 12345,
263
+ ... "strategies": {},
264
+ ... "tables": {}
265
+ ... })
266
+ >>> rule = AnonymizationRule(column="email", strategy="email_mask")
267
+ >>> resolve_seed_for_column(rule, profile)
268
+ 12345
269
+ >>> rule2 = AnonymizationRule(
270
+ ... column="special", strategy="email_mask", seed=99999
271
+ ... )
272
+ >>> resolve_seed_for_column(rule2, profile)
273
+ 99999
274
+ """
275
+ # Column-specific seed takes precedence
276
+ if rule.seed is not None:
277
+ return rule.seed
278
+
279
+ # Global seed applies to all columns
280
+ if profile.global_seed is not None:
281
+ return profile.global_seed
282
+
283
+ # Default seed
284
+ return 0
@@ -0,0 +1,195 @@
1
+ """Strategy registry for managing anonymization strategies.
2
+
3
+ Provides a singleton registry for dynamically registering and retrieving
4
+ anonymization strategies, enabling extensibility without modifying core code.
5
+
6
+ Features:
7
+ - Dynamic strategy registration
8
+ - Type-safe strategy retrieval
9
+ - Strategy discovery and listing
10
+ - Configuration validation
11
+ """
12
+
13
+ from collections.abc import Callable
14
+ from typing import Any
15
+
16
+ from confiture.core.anonymization.strategy import AnonymizationStrategy, StrategyConfig
17
+
18
+
19
+ class StrategyRegistry:
20
+ """Singleton registry for anonymization strategies.
21
+
22
+ Manages registration and retrieval of strategy implementations,
23
+ enabling dynamic discovery and extensibility.
24
+
25
+ Example:
26
+ >>> StrategyRegistry.register("email", EmailMaskingStrategy)
27
+ >>> strategy = StrategyRegistry.get("email", {"seed": 12345})
28
+ >>> print(StrategyRegistry.list_available())
29
+ ['email', 'hash', 'phone', ...]
30
+ """
31
+
32
+ _registry: dict[str, type[AnonymizationStrategy]] = {}
33
+
34
+ @classmethod
35
+ def register(cls, name: str, strategy_class: type[AnonymizationStrategy]) -> None:
36
+ """Register a strategy implementation.
37
+
38
+ Args:
39
+ name: Unique strategy name (e.g., "email", "hash")
40
+ strategy_class: Strategy class inheriting from AnonymizationStrategy
41
+
42
+ Raises:
43
+ TypeError: If strategy_class doesn't inherit from AnonymizationStrategy
44
+ ValueError: If name is already registered
45
+
46
+ Example:
47
+ >>> StrategyRegistry.register("custom", CustomStrategy)
48
+ """
49
+ if not issubclass(strategy_class, AnonymizationStrategy):
50
+ raise TypeError(f"{strategy_class.__name__} must inherit from AnonymizationStrategy")
51
+
52
+ if name in cls._registry:
53
+ raise ValueError(f"Strategy '{name}' is already registered")
54
+
55
+ cls._registry[name] = strategy_class
56
+
57
+ @classmethod
58
+ def get(
59
+ cls, name: str, config: dict[str, Any] | StrategyConfig | None = None
60
+ ) -> AnonymizationStrategy:
61
+ """Get a strategy instance by name.
62
+
63
+ Args:
64
+ name: Strategy name to retrieve
65
+ config: Configuration dict or StrategyConfig instance
66
+
67
+ Returns:
68
+ Instantiated strategy with given configuration
69
+
70
+ Raises:
71
+ ValueError: If strategy name not found
72
+
73
+ Example:
74
+ >>> strategy = StrategyRegistry.get("email", {"seed": 12345})
75
+ >>> anonymized = strategy.anonymize("john@example.com")
76
+ """
77
+ if name not in cls._registry:
78
+ available = ", ".join(cls.list_available())
79
+ raise ValueError(f"Unknown strategy: '{name}'. Available: {available}")
80
+
81
+ strategy_class = cls._registry[name]
82
+
83
+ # Handle config conversion if needed
84
+ if config is None:
85
+ config = {}
86
+
87
+ if isinstance(config, dict):
88
+ config = strategy_class.config_type(**config)
89
+
90
+ return strategy_class(config)
91
+
92
+ @classmethod
93
+ def is_registered(cls, name: str) -> bool:
94
+ """Check if a strategy is registered.
95
+
96
+ Args:
97
+ name: Strategy name to check
98
+
99
+ Returns:
100
+ True if strategy is registered, False otherwise
101
+ """
102
+ return name in cls._registry
103
+
104
+ @classmethod
105
+ def list_available(cls) -> list[str]:
106
+ """List all registered strategy names.
107
+
108
+ Returns:
109
+ Sorted list of available strategy names
110
+
111
+ Example:
112
+ >>> strategies = StrategyRegistry.list_available()
113
+ >>> print(strategies)
114
+ ['address', 'date', 'email', 'hash', 'name', 'phone', ...]
115
+ """
116
+ return sorted(cls._registry.keys())
117
+
118
+ @classmethod
119
+ def get_strategy_class(cls, name: str) -> type[AnonymizationStrategy]:
120
+ """Get the strategy class (not instance).
121
+
122
+ Useful for introspection, documentation, or creating multiple instances
123
+ with different configurations.
124
+
125
+ Args:
126
+ name: Strategy name
127
+
128
+ Returns:
129
+ Strategy class
130
+
131
+ Raises:
132
+ ValueError: If strategy name not found
133
+
134
+ Example:
135
+ >>> EmailStrategy = StrategyRegistry.get_strategy_class("email")
136
+ >>> print(EmailStrategy.__doc__)
137
+ """
138
+ if name not in cls._registry:
139
+ raise ValueError(f"Unknown strategy: '{name}'")
140
+
141
+ return cls._registry[name]
142
+
143
+ @classmethod
144
+ def unregister(cls, name: str) -> None:
145
+ """Unregister a strategy (mainly for testing).
146
+
147
+ Args:
148
+ name: Strategy name to unregister
149
+
150
+ Raises:
151
+ ValueError: If strategy not found
152
+ """
153
+ if name not in cls._registry:
154
+ raise ValueError(f"Strategy '{name}' not registered")
155
+
156
+ del cls._registry[name]
157
+
158
+ @classmethod
159
+ def reset(cls) -> None:
160
+ """Reset registry (mainly for testing).
161
+
162
+ Clears all registered strategies.
163
+ """
164
+ cls._registry.clear()
165
+
166
+ @classmethod
167
+ def count(cls) -> int:
168
+ """Get number of registered strategies.
169
+
170
+ Returns:
171
+ Count of registered strategies
172
+ """
173
+ return len(cls._registry)
174
+
175
+
176
+ def register_strategy(
177
+ name: str,
178
+ ) -> Callable[[type[AnonymizationStrategy]], type[AnonymizationStrategy]]:
179
+ """Decorator for registering a strategy class.
180
+
181
+ Enables cleaner registration syntax:
182
+
183
+ Example:
184
+ >>> @register_strategy("custom_email")
185
+ ... class CustomEmailStrategy(AnonymizationStrategy):
186
+ ... ...
187
+ """
188
+
189
+ def decorator(
190
+ strategy_class: type[AnonymizationStrategy],
191
+ ) -> type[AnonymizationStrategy]:
192
+ StrategyRegistry.register(name, strategy_class)
193
+ return strategy_class
194
+
195
+ return decorator