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,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
|