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.
Files changed (119) hide show
  1. confiture/__init__.py +48 -0
  2. confiture/_core.cp311-win_amd64.pyd +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 +1656 -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 +132 -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 +793 -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 +0 -0
  95. confiture/models/lint.py +193 -0
  96. confiture/models/migration.py +180 -0
  97. confiture/models/schema.py +203 -0
  98. confiture/scenarios/__init__.py +36 -0
  99. confiture/scenarios/compliance.py +586 -0
  100. confiture/scenarios/ecommerce.py +199 -0
  101. confiture/scenarios/financial.py +253 -0
  102. confiture/scenarios/healthcare.py +315 -0
  103. confiture/scenarios/multi_tenant.py +340 -0
  104. confiture/scenarios/saas.py +295 -0
  105. confiture/testing/FRAMEWORK_API.md +722 -0
  106. confiture/testing/__init__.py +38 -0
  107. confiture/testing/fixtures/__init__.py +11 -0
  108. confiture/testing/fixtures/data_validator.py +229 -0
  109. confiture/testing/fixtures/migration_runner.py +167 -0
  110. confiture/testing/fixtures/schema_snapshotter.py +352 -0
  111. confiture/testing/frameworks/__init__.py +10 -0
  112. confiture/testing/frameworks/mutation.py +587 -0
  113. confiture/testing/frameworks/performance.py +479 -0
  114. confiture/testing/utils/__init__.py +0 -0
  115. fraiseql_confiture-0.3.4.dist-info/METADATA +438 -0
  116. fraiseql_confiture-0.3.4.dist-info/RECORD +119 -0
  117. fraiseql_confiture-0.3.4.dist-info/WHEEL +4 -0
  118. fraiseql_confiture-0.3.4.dist-info/entry_points.txt +2 -0
  119. fraiseql_confiture-0.3.4.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,358 @@
1
+ """Migration file checksum computation and verification.
2
+
3
+ Provides SHA-256 checksum computation and verification for migration files
4
+ to detect unauthorized modifications after migrations are applied.
5
+
6
+ This helps prevent:
7
+ - Silent schema drift between environments
8
+ - Production/staging mismatches
9
+ - Debugging nightmares from modified migrations
10
+ """
11
+
12
+ import hashlib
13
+ import logging
14
+ from dataclasses import dataclass, field
15
+ from enum import Enum
16
+ from pathlib import Path
17
+ from typing import TYPE_CHECKING
18
+
19
+ if TYPE_CHECKING:
20
+ import psycopg
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class ChecksumMismatchBehavior(Enum):
26
+ """Behavior when checksum mismatch is detected."""
27
+
28
+ FAIL = "fail" # Raise error, stop migrations
29
+ WARN = "warn" # Log warning, continue
30
+ IGNORE = "ignore" # Silently continue
31
+
32
+
33
+ @dataclass
34
+ class ChecksumConfig:
35
+ """Configuration for checksum verification.
36
+
37
+ Attributes:
38
+ enabled: Whether checksum verification is enabled (default: True)
39
+ on_mismatch: Behavior when mismatch detected (default: FAIL)
40
+ algorithm: Hash algorithm to use (default: sha256)
41
+
42
+ Example:
43
+ >>> config = ChecksumConfig(on_mismatch=ChecksumMismatchBehavior.WARN)
44
+ >>> config = ChecksumConfig(enabled=False) # Disable verification
45
+ """
46
+
47
+ enabled: bool = True
48
+ on_mismatch: ChecksumMismatchBehavior = field(default=ChecksumMismatchBehavior.FAIL)
49
+ algorithm: str = "sha256"
50
+
51
+
52
+ @dataclass
53
+ class ChecksumMismatch:
54
+ """Record of a checksum mismatch.
55
+
56
+ Attributes:
57
+ version: Migration version
58
+ name: Migration name
59
+ file_path: Path to migration file
60
+ expected: Expected checksum (stored in database)
61
+ actual: Actual checksum (computed from file)
62
+ """
63
+
64
+ version: str
65
+ name: str
66
+ file_path: Path
67
+ expected: str
68
+ actual: str
69
+
70
+
71
+ class ChecksumVerificationError(Exception):
72
+ """Raised when checksum verification fails.
73
+
74
+ Attributes:
75
+ mismatches: List of ChecksumMismatch objects
76
+ """
77
+
78
+ def __init__(self, mismatches: list[ChecksumMismatch]):
79
+ self.mismatches = mismatches
80
+ files = ", ".join(m.version for m in mismatches)
81
+ super().__init__(
82
+ f"Checksum verification failed for {len(mismatches)} migration(s): {files}. "
83
+ "Migration files have been modified after application. "
84
+ "This can cause schema drift between environments."
85
+ )
86
+
87
+
88
+ def compute_checksum(file_path: Path, algorithm: str = "sha256") -> str:
89
+ """Compute checksum of a migration file.
90
+
91
+ Args:
92
+ file_path: Path to migration file
93
+ algorithm: Hash algorithm (default: sha256)
94
+
95
+ Returns:
96
+ Hex-encoded checksum string (64 characters for SHA-256)
97
+
98
+ Example:
99
+ >>> checksum = compute_checksum(Path("001_create_users.py"))
100
+ >>> print(checksum)
101
+ "a3f2b8c9d4e5f6a1b2c3d4e5f6a7b8c9..."
102
+ """
103
+ hasher = hashlib.new(algorithm)
104
+
105
+ with open(file_path, "rb") as f:
106
+ # Read in chunks for memory efficiency with large files
107
+ for chunk in iter(lambda: f.read(8192), b""):
108
+ hasher.update(chunk)
109
+
110
+ return hasher.hexdigest()
111
+
112
+
113
+ def compute_checksum_from_content(content: str | bytes, algorithm: str = "sha256") -> str:
114
+ """Compute checksum from content directly.
115
+
116
+ Useful for computing checksums without writing to disk.
117
+
118
+ Args:
119
+ content: File content as string or bytes
120
+ algorithm: Hash algorithm (default: sha256)
121
+
122
+ Returns:
123
+ Hex-encoded checksum string
124
+
125
+ Example:
126
+ >>> checksum = compute_checksum_from_content("def up(): pass")
127
+ >>> print(len(checksum))
128
+ 64
129
+ """
130
+ hasher = hashlib.new(algorithm)
131
+
132
+ if isinstance(content, str):
133
+ content = content.encode("utf-8")
134
+
135
+ hasher.update(content)
136
+ return hasher.hexdigest()
137
+
138
+
139
+ class MigrationChecksumVerifier:
140
+ """Verifies migration file integrity against stored checksums.
141
+
142
+ Compares the SHA-256 checksums of migration files on disk against
143
+ the checksums that were stored when the migrations were applied.
144
+
145
+ Example:
146
+ >>> import psycopg
147
+ >>> conn = psycopg.connect("postgresql://localhost/mydb")
148
+ >>> verifier = MigrationChecksumVerifier(conn)
149
+ >>> mismatches = verifier.verify_all(Path("db/migrations"))
150
+ >>> if mismatches:
151
+ ... print(f"Found {len(mismatches)} modified migrations!")
152
+
153
+ >>> # With custom config
154
+ >>> config = ChecksumConfig(on_mismatch=ChecksumMismatchBehavior.WARN)
155
+ >>> verifier = MigrationChecksumVerifier(conn, config)
156
+ >>> verifier.verify_all(Path("db/migrations")) # Logs warnings instead of raising
157
+ """
158
+
159
+ def __init__(
160
+ self,
161
+ connection: "psycopg.Connection",
162
+ config: ChecksumConfig | None = None,
163
+ ):
164
+ """Initialize verifier.
165
+
166
+ Args:
167
+ connection: psycopg3 database connection
168
+ config: Checksum configuration (uses defaults if None)
169
+ """
170
+ self.connection = connection
171
+ self.config = config or ChecksumConfig()
172
+
173
+ def verify_all(self, migrations_dir: Path) -> list[ChecksumMismatch]:
174
+ """Verify all applied migrations against their stored checksums.
175
+
176
+ Args:
177
+ migrations_dir: Directory containing migration files
178
+
179
+ Returns:
180
+ List of mismatches (empty if all match)
181
+
182
+ Raises:
183
+ ChecksumVerificationError: If mismatches found and behavior is FAIL
184
+ """
185
+ if not self.config.enabled:
186
+ logger.debug("Checksum verification disabled")
187
+ return []
188
+
189
+ # Get stored checksums from database
190
+ stored = self._get_stored_checksums()
191
+
192
+ if not stored:
193
+ logger.debug("No stored checksums to verify")
194
+ return []
195
+
196
+ mismatches = []
197
+
198
+ for version, (name, expected_checksum) in stored.items():
199
+ # Find migration file
200
+ file_path = self._find_migration_file(migrations_dir, version, name)
201
+
202
+ if file_path is None:
203
+ logger.warning(f"Migration file not found for {version}_{name}")
204
+ continue
205
+
206
+ # Compute current checksum
207
+ actual_checksum = compute_checksum(file_path, self.config.algorithm)
208
+
209
+ # Missing checksum is treated as mismatch
210
+ if expected_checksum is None or actual_checksum != expected_checksum:
211
+ mismatches.append(
212
+ ChecksumMismatch(
213
+ version=version,
214
+ name=name,
215
+ file_path=file_path,
216
+ expected=expected_checksum,
217
+ actual=actual_checksum,
218
+ )
219
+ )
220
+
221
+ # Handle mismatches based on config
222
+ if mismatches:
223
+ self._handle_mismatches(mismatches)
224
+
225
+ return mismatches
226
+
227
+ def verify_single(self, migration_file: Path, expected_checksum: str) -> bool:
228
+ """Verify a single migration file against expected checksum.
229
+
230
+ Args:
231
+ migration_file: Path to migration file
232
+ expected_checksum: Expected checksum value
233
+
234
+ Returns:
235
+ True if checksums match, False otherwise
236
+ """
237
+ if not self.config.enabled:
238
+ return True
239
+
240
+ actual = compute_checksum(migration_file, self.config.algorithm)
241
+ return actual == expected_checksum
242
+
243
+ def _get_stored_checksums(self) -> dict[str, tuple[str, str | None]]:
244
+ """Get stored checksums from database.
245
+
246
+ Returns:
247
+ Dict mapping version -> (name, checksum)
248
+ """
249
+ with self.connection.cursor() as cur:
250
+ cur.execute("""
251
+ SELECT version, name, checksum
252
+ FROM confiture_migrations
253
+ ORDER BY version
254
+ """)
255
+ return {row[0]: (row[1], row[2]) for row in cur.fetchall()}
256
+
257
+ def _find_migration_file(
258
+ self,
259
+ migrations_dir: Path,
260
+ version: str,
261
+ name: str,
262
+ ) -> Path | None:
263
+ """Find migration file by version and name.
264
+
265
+ Args:
266
+ migrations_dir: Directory to search
267
+ version: Migration version
268
+ name: Migration name
269
+
270
+ Returns:
271
+ Path to migration file, or None if not found
272
+ """
273
+ # Try exact match first
274
+ exact_path = migrations_dir / f"{version}_{name}.py"
275
+ if exact_path.exists():
276
+ return exact_path
277
+
278
+ # Try pattern match (in case name has slight differences)
279
+ for f in migrations_dir.glob(f"{version}_*.py"):
280
+ return f
281
+
282
+ return None
283
+
284
+ def _handle_mismatches(self, mismatches: list[ChecksumMismatch]) -> None:
285
+ """Handle checksum mismatches based on configuration.
286
+
287
+ Args:
288
+ mismatches: List of detected mismatches
289
+
290
+ Raises:
291
+ ChecksumVerificationError: If behavior is FAIL
292
+ """
293
+ behavior = self.config.on_mismatch
294
+
295
+ if behavior == ChecksumMismatchBehavior.IGNORE:
296
+ return
297
+
298
+ # Build message
299
+ msg_lines = ["Checksum verification found modified migrations:"]
300
+ for m in mismatches:
301
+ msg_lines.append(
302
+ f" - {m.version}_{m.name}: expected {m.expected[:12]}..., got {m.actual[:12]}..."
303
+ )
304
+
305
+ message = "\n".join(msg_lines)
306
+
307
+ if behavior == ChecksumMismatchBehavior.WARN:
308
+ logger.warning(message)
309
+ elif behavior == ChecksumMismatchBehavior.FAIL:
310
+ raise ChecksumVerificationError(mismatches)
311
+
312
+ def update_checksum(self, version: str, new_checksum: str) -> None:
313
+ """Update stored checksum for a migration.
314
+
315
+ Use with caution - this should only be used when you're certain
316
+ the file modification was intentional.
317
+
318
+ Args:
319
+ version: Migration version
320
+ new_checksum: New checksum value
321
+ """
322
+ with self.connection.cursor() as cur:
323
+ cur.execute(
324
+ """
325
+ UPDATE confiture_migrations
326
+ SET checksum = %s
327
+ WHERE version = %s
328
+ """,
329
+ (new_checksum, version),
330
+ )
331
+ self.connection.commit()
332
+ logger.info(f"Updated checksum for migration {version}")
333
+
334
+ def update_all_checksums(self, migrations_dir: Path) -> int:
335
+ """Update all stored checksums from current files.
336
+
337
+ WARNING: This should only be used when you're certain all
338
+ file modifications were intentional.
339
+
340
+ Args:
341
+ migrations_dir: Directory containing migration files
342
+
343
+ Returns:
344
+ Number of checksums updated
345
+ """
346
+ stored = self._get_stored_checksums()
347
+ updated = 0
348
+
349
+ for version, (name, _) in stored.items():
350
+ file_path = self._find_migration_file(migrations_dir, version, name)
351
+ if file_path is None:
352
+ continue
353
+
354
+ new_checksum = compute_checksum(file_path, self.config.algorithm)
355
+ self.update_checksum(version, new_checksum)
356
+ updated += 1
357
+
358
+ return updated
@@ -0,0 +1,132 @@
1
+ """Database connection management for CLI commands."""
2
+
3
+ import importlib.util
4
+ import sys
5
+ from pathlib import Path
6
+ from types import ModuleType
7
+ from typing import Any
8
+
9
+ import psycopg
10
+ import yaml
11
+
12
+ from confiture.exceptions import MigrationError
13
+
14
+
15
+ def load_config(config_file: Path) -> dict[str, Any]:
16
+ """Load configuration from YAML file.
17
+
18
+ Args:
19
+ config_file: Path to configuration file
20
+
21
+ Returns:
22
+ Configuration dictionary
23
+
24
+ Raises:
25
+ MigrationError: If config file is invalid
26
+ """
27
+ if not config_file.exists():
28
+ raise MigrationError(f"Configuration file not found: {config_file}")
29
+
30
+ try:
31
+ with open(config_file) as f:
32
+ config: dict[str, Any] = yaml.safe_load(f)
33
+ return config
34
+ except yaml.YAMLError as e:
35
+ raise MigrationError(f"Invalid YAML configuration: {e}") from e
36
+
37
+
38
+ def create_connection(config: dict[str, Any] | Any) -> psycopg.Connection:
39
+ """Create database connection from configuration.
40
+
41
+ Args:
42
+ config: Configuration dictionary with 'database' section, 'database_url', or DatabaseConfig instance
43
+
44
+ Returns:
45
+ PostgreSQL connection
46
+
47
+ Raises:
48
+ MigrationError: If connection fails
49
+ """
50
+ from confiture.config.environment import DatabaseConfig
51
+
52
+ try:
53
+ # Handle DatabaseConfig instance
54
+ if isinstance(config, DatabaseConfig):
55
+ config_dict = config.to_dict()
56
+ db_config = config_dict.get("database", {})
57
+ conn = psycopg.connect(
58
+ host=db_config.get("host", "localhost"),
59
+ port=db_config.get("port", 5432),
60
+ dbname=db_config.get("database", "postgres"),
61
+ user=db_config.get("user", "postgres"),
62
+ password=db_config.get("password", ""),
63
+ )
64
+ else:
65
+ # Check for database_url first
66
+ database_url = config.get("database_url")
67
+ if database_url:
68
+ conn = psycopg.connect(database_url)
69
+ else:
70
+ # Fall back to database section
71
+ db_config = config.get("database", {})
72
+ conn = psycopg.connect(
73
+ host=db_config.get("host", "localhost"),
74
+ port=db_config.get("port", 5432),
75
+ dbname=db_config.get("database", "postgres"),
76
+ user=db_config.get("user", "postgres"),
77
+ password=db_config.get("password", ""),
78
+ )
79
+ return conn
80
+ except psycopg.Error as e:
81
+ raise MigrationError(f"Failed to connect to database: {e}") from e
82
+
83
+
84
+ def load_migration_module(migration_file: Path) -> ModuleType:
85
+ """Dynamically load a migration Python module.
86
+
87
+ Args:
88
+ migration_file: Path to migration .py file
89
+
90
+ Returns:
91
+ Loaded module
92
+
93
+ Raises:
94
+ MigrationError: If module cannot be loaded
95
+ """
96
+ try:
97
+ # Create module spec
98
+ spec = importlib.util.spec_from_file_location(migration_file.stem, migration_file)
99
+ if spec is None or spec.loader is None:
100
+ raise MigrationError(f"Cannot load migration: {migration_file}")
101
+
102
+ # Load module
103
+ module = importlib.util.module_from_spec(spec)
104
+ sys.modules[migration_file.stem] = module
105
+ spec.loader.exec_module(module)
106
+
107
+ return module
108
+ except Exception as e:
109
+ raise MigrationError(f"Failed to load migration {migration_file}: {e}") from e
110
+
111
+
112
+ def get_migration_class(module: ModuleType) -> type:
113
+ """Extract Migration subclass from loaded module.
114
+
115
+ Args:
116
+ module: Loaded Python module
117
+
118
+ Returns:
119
+ Migration class
120
+
121
+ Raises:
122
+ MigrationError: If no Migration class found
123
+ """
124
+ from confiture.models.migration import Migration
125
+
126
+ # Find Migration subclass in module
127
+ for attr_name in dir(module):
128
+ attr = getattr(module, attr_name)
129
+ if isinstance(attr, type) and issubclass(attr, Migration) and attr is not Migration:
130
+ return attr
131
+
132
+ raise MigrationError(f"No Migration subclass found in {module}")