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,352 @@
1
+ """Schema snapshot utility for migration testing.
2
+
3
+ Captures and compares database schema states to validate migrations work correctly.
4
+ Can be extracted to confiture-testing package in the future.
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+ from datetime import datetime
9
+ from typing import Any
10
+
11
+ import psycopg
12
+
13
+
14
+ @dataclass
15
+ class ColumnInfo:
16
+ """Information about a database column."""
17
+
18
+ name: str
19
+ data_type: str
20
+ is_nullable: bool
21
+ column_default: str | None = None
22
+
23
+
24
+ @dataclass
25
+ class ConstraintInfo:
26
+ """Information about a table constraint."""
27
+
28
+ name: str
29
+ constraint_type: str # PRIMARY KEY, FOREIGN KEY, UNIQUE, CHECK, etc.
30
+ columns: list[str] = field(default_factory=list)
31
+
32
+
33
+ @dataclass
34
+ class IndexInfo:
35
+ """Information about a database index."""
36
+
37
+ name: str
38
+ table_name: str
39
+ is_unique: bool
40
+ columns: list[str] = field(default_factory=list)
41
+
42
+
43
+ @dataclass
44
+ class ForeignKeyInfo:
45
+ """Information about a foreign key relationship."""
46
+
47
+ constraint_name: str
48
+ column_name: str
49
+ referenced_table: str
50
+ referenced_column: str
51
+
52
+
53
+ @dataclass
54
+ class TableSchema:
55
+ """Complete schema information for a single table."""
56
+
57
+ name: str
58
+ schema_name: str
59
+ columns: dict[str, ColumnInfo] = field(default_factory=dict)
60
+ constraints: list[ConstraintInfo] = field(default_factory=list)
61
+ indexes: list[IndexInfo] = field(default_factory=list)
62
+ foreign_keys: list[ForeignKeyInfo] = field(default_factory=list)
63
+
64
+
65
+ @dataclass
66
+ class SchemaSnapshot:
67
+ """Complete snapshot of database schema at a point in time."""
68
+
69
+ tables: dict[str, TableSchema] = field(default_factory=dict)
70
+ views: set[str] = field(default_factory=set)
71
+ materialized_views: set[str] = field(default_factory=set)
72
+ functions: set[str] = field(default_factory=set)
73
+ timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
74
+
75
+
76
+ @dataclass
77
+ class SchemaChange:
78
+ """Represents a detected schema change."""
79
+
80
+ change_type: str # added, removed, modified
81
+ object_type: str # table, column, index, constraint, etc.
82
+ object_name: str
83
+ details: dict[str, Any] = field(default_factory=dict)
84
+
85
+
86
+ class SchemaSnapshotter:
87
+ """Capture and compare database schema states.
88
+
89
+ Generic schema introspection that can be extracted to confiture-testing.
90
+ """
91
+
92
+ def __init__(self, connection: psycopg.Connection):
93
+ """Initialize schema snapshotter.
94
+
95
+ Args:
96
+ connection: PostgreSQL connection for schema introspection
97
+ """
98
+ self.connection = connection
99
+
100
+ def capture(self) -> SchemaSnapshot:
101
+ """Capture current schema state.
102
+
103
+ Returns:
104
+ SchemaSnapshot with complete schema information
105
+ """
106
+ snapshot = SchemaSnapshot()
107
+
108
+ with self.connection.cursor() as cur:
109
+ # Get all tables
110
+ cur.execute(
111
+ """
112
+ SELECT table_schema, table_name
113
+ FROM information_schema.tables
114
+ WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
115
+ ORDER BY table_schema, table_name
116
+ """
117
+ )
118
+ tables = cur.fetchall()
119
+
120
+ # Capture each table's schema
121
+ for schema_name, table_name in tables:
122
+ table_key = f"{schema_name}.{table_name}"
123
+ snapshot.tables[table_key] = self._capture_table_schema(
124
+ cur, schema_name, table_name
125
+ )
126
+
127
+ # Get views
128
+ cur.execute(
129
+ """
130
+ SELECT table_name
131
+ FROM information_schema.views
132
+ WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
133
+ ORDER BY table_name
134
+ """
135
+ )
136
+ snapshot.views = {row[0] for row in cur.fetchall()}
137
+
138
+ # Get materialized views
139
+ cur.execute("SELECT matviewname FROM pg_matviews WHERE schemaname != 'pg_catalog'")
140
+ snapshot.materialized_views = {row[0] for row in cur.fetchall()}
141
+
142
+ # Get functions
143
+ cur.execute(
144
+ """
145
+ SELECT routine_name
146
+ FROM information_schema.routines
147
+ WHERE routine_schema NOT IN ('pg_catalog', 'information_schema')
148
+ ORDER BY routine_name
149
+ """
150
+ )
151
+ snapshot.functions = {row[0] for row in cur.fetchall()}
152
+
153
+ return snapshot
154
+
155
+ def _capture_table_schema(
156
+ self, cur: psycopg.Cursor, schema_name: str, table_name: str
157
+ ) -> TableSchema:
158
+ """Capture schema for a single table.
159
+
160
+ Args:
161
+ cur: Database cursor
162
+ schema_name: Schema name
163
+ table_name: Table name
164
+
165
+ Returns:
166
+ TableSchema with complete table information
167
+ """
168
+ table = TableSchema(name=table_name, schema_name=schema_name)
169
+
170
+ # Get columns
171
+ cur.execute(
172
+ """
173
+ SELECT column_name, data_type, is_nullable, column_default
174
+ FROM information_schema.columns
175
+ WHERE table_schema = %s AND table_name = %s
176
+ ORDER BY ordinal_position
177
+ """,
178
+ (schema_name, table_name),
179
+ )
180
+
181
+ for col_name, data_type, is_nullable, col_default in cur.fetchall():
182
+ table.columns[col_name] = ColumnInfo(
183
+ name=col_name,
184
+ data_type=data_type,
185
+ is_nullable=is_nullable == "YES",
186
+ column_default=col_default,
187
+ )
188
+
189
+ # Get constraints
190
+ cur.execute(
191
+ """
192
+ SELECT constraint_name, constraint_type
193
+ FROM information_schema.table_constraints
194
+ WHERE table_schema = %s AND table_name = %s
195
+ ORDER BY constraint_name
196
+ """,
197
+ (schema_name, table_name),
198
+ )
199
+
200
+ for constraint_name, constraint_type in cur.fetchall():
201
+ # Get columns for this constraint
202
+ cur.execute(
203
+ """
204
+ SELECT column_name
205
+ FROM information_schema.constraint_column_usage
206
+ WHERE constraint_schema = %s
207
+ AND constraint_name = %s
208
+ ORDER BY column_name
209
+ """,
210
+ (schema_name, constraint_name),
211
+ )
212
+ columns = [row[0] for row in cur.fetchall()]
213
+
214
+ table.constraints.append(
215
+ ConstraintInfo(
216
+ name=constraint_name, constraint_type=constraint_type, columns=columns
217
+ )
218
+ )
219
+
220
+ # Get indexes
221
+ cur.execute(
222
+ """
223
+ SELECT indexname, indexdef
224
+ FROM pg_indexes
225
+ WHERE schemaname = %s AND tablename = %s
226
+ ORDER BY indexname
227
+ """,
228
+ (schema_name, table_name),
229
+ )
230
+
231
+ for index_name, index_def in cur.fetchall():
232
+ is_unique = "UNIQUE" in (index_def or "").upper()
233
+ table.indexes.append(
234
+ IndexInfo(
235
+ name=index_name,
236
+ table_name=table_name,
237
+ is_unique=is_unique,
238
+ columns=[], # Would need to parse index_def to get columns
239
+ )
240
+ )
241
+
242
+ # Get foreign keys
243
+ cur.execute(
244
+ """
245
+ SELECT
246
+ kcu.constraint_name,
247
+ kcu.column_name,
248
+ ccu.table_name,
249
+ ccu.column_name
250
+ FROM information_schema.key_column_usage kcu
251
+ JOIN information_schema.constraint_column_usage ccu
252
+ ON kcu.constraint_name = ccu.constraint_name
253
+ AND kcu.table_schema = ccu.table_schema
254
+ WHERE kcu.table_schema = %s
255
+ AND kcu.table_name = %s
256
+ AND kcu.constraint_name IN (
257
+ SELECT constraint_name
258
+ FROM information_schema.table_constraints
259
+ WHERE table_schema = %s
260
+ AND table_name = %s
261
+ AND constraint_type = 'FOREIGN KEY'
262
+ )
263
+ """,
264
+ (schema_name, table_name, schema_name, table_name),
265
+ )
266
+
267
+ for constraint_name, col_name, ref_table, ref_col in cur.fetchall():
268
+ table.foreign_keys.append(
269
+ ForeignKeyInfo(
270
+ constraint_name=constraint_name,
271
+ column_name=col_name,
272
+ referenced_table=ref_table,
273
+ referenced_column=ref_col,
274
+ )
275
+ )
276
+
277
+ return table
278
+
279
+ def compare(self, before: SchemaSnapshot, after: SchemaSnapshot) -> dict[str, Any]:
280
+ """Compare two schema snapshots.
281
+
282
+ Args:
283
+ before: Schema snapshot before migration
284
+ after: Schema snapshot after migration
285
+
286
+ Returns:
287
+ Dictionary of detected changes
288
+ """
289
+ changes = {
290
+ "tables_added": set(after.tables.keys()) - set(before.tables.keys()),
291
+ "tables_removed": set(before.tables.keys()) - set(after.tables.keys()),
292
+ "tables_modified": [],
293
+ "views_added": after.views - before.views,
294
+ "views_removed": before.views - after.views,
295
+ "mat_views_added": after.materialized_views - before.materialized_views,
296
+ "mat_views_removed": before.materialized_views - after.materialized_views,
297
+ "functions_added": after.functions - before.functions,
298
+ "functions_removed": before.functions - after.functions,
299
+ }
300
+
301
+ # Check for modified tables
302
+ common_tables = set(before.tables.keys()) & set(after.tables.keys())
303
+ for table_name in common_tables:
304
+ before_table = before.tables[table_name]
305
+ after_table = after.tables[table_name]
306
+
307
+ table_changes = {
308
+ "table": table_name,
309
+ "columns_added": set(after_table.columns.keys()) - set(before_table.columns.keys()),
310
+ "columns_removed": set(before_table.columns.keys())
311
+ - set(after_table.columns.keys()),
312
+ "columns_modified": [],
313
+ }
314
+
315
+ # Check for modified columns
316
+ common_cols = set(before_table.columns.keys()) & set(after_table.columns.keys())
317
+ for col_name in common_cols:
318
+ before_col = before_table.columns[col_name]
319
+ after_col = after_table.columns[col_name]
320
+
321
+ if (
322
+ before_col.data_type != after_col.data_type
323
+ or before_col.is_nullable != after_col.is_nullable
324
+ ):
325
+ table_changes["columns_modified"].append(
326
+ {
327
+ "column": col_name,
328
+ "before_type": before_col.data_type,
329
+ "after_type": after_col.data_type,
330
+ "before_nullable": before_col.is_nullable,
331
+ "after_nullable": after_col.is_nullable,
332
+ }
333
+ )
334
+
335
+ # Check for constraint changes
336
+ before_constraint_names = {c.name for c in before_table.constraints}
337
+ after_constraint_names = {c.name for c in after_table.constraints}
338
+
339
+ table_changes["constraints_added"] = after_constraint_names - before_constraint_names
340
+ table_changes["constraints_removed"] = before_constraint_names - after_constraint_names
341
+
342
+ # Only add to modified list if there are actual changes
343
+ if (
344
+ table_changes["columns_added"]
345
+ or table_changes["columns_removed"]
346
+ or table_changes["columns_modified"]
347
+ or table_changes["constraints_added"]
348
+ or table_changes["constraints_removed"]
349
+ ):
350
+ changes["tables_modified"].append(table_changes)
351
+
352
+ return changes
@@ -0,0 +1,10 @@
1
+ """Testing frameworks for Confiture migration validation."""
2
+
3
+ from confiture.testing.frameworks.mutation import MutationRegistry, MutationRunner
4
+ from confiture.testing.frameworks.performance import MigrationPerformanceProfiler
5
+
6
+ __all__ = [
7
+ "MutationRegistry",
8
+ "MutationRunner",
9
+ "MigrationPerformanceProfiler",
10
+ ]