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.
- confiture/__init__.py +48 -0
- confiture/_core.cpython-311-darwin.so +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 +1893 -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 +184 -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 +882 -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 +24 -0
- confiture/models/lint.py +193 -0
- confiture/models/migration.py +265 -0
- confiture/models/schema.py +203 -0
- confiture/models/sql_file_migration.py +225 -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 +100 -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/loader.py +225 -0
- confiture/testing/pytest/__init__.py +38 -0
- confiture/testing/pytest_plugin.py +190 -0
- confiture/testing/sandbox.py +304 -0
- confiture/testing/utils/__init__.py +0 -0
- fraiseql_confiture-0.3.7.dist-info/METADATA +438 -0
- fraiseql_confiture-0.3.7.dist-info/RECORD +124 -0
- fraiseql_confiture-0.3.7.dist-info/WHEEL +4 -0
- fraiseql_confiture-0.3.7.dist-info/entry_points.txt +4 -0
- fraiseql_confiture-0.3.7.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Data models for schema representation.
|
|
2
|
+
|
|
3
|
+
These models represent database schema objects (tables, columns, indexes, etc.)
|
|
4
|
+
in a structured format for diff detection and comparison.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from enum import Enum
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ColumnType(str, Enum):
|
|
12
|
+
"""PostgreSQL column types."""
|
|
13
|
+
|
|
14
|
+
# Integer types
|
|
15
|
+
SMALLINT = "SMALLINT"
|
|
16
|
+
INTEGER = "INTEGER"
|
|
17
|
+
BIGINT = "BIGINT"
|
|
18
|
+
SERIAL = "SERIAL"
|
|
19
|
+
BIGSERIAL = "BIGSERIAL"
|
|
20
|
+
|
|
21
|
+
# Numeric types
|
|
22
|
+
NUMERIC = "NUMERIC"
|
|
23
|
+
DECIMAL = "DECIMAL"
|
|
24
|
+
REAL = "REAL"
|
|
25
|
+
DOUBLE_PRECISION = "DOUBLE PRECISION"
|
|
26
|
+
|
|
27
|
+
# Text types
|
|
28
|
+
VARCHAR = "VARCHAR"
|
|
29
|
+
CHAR = "CHAR"
|
|
30
|
+
TEXT = "TEXT"
|
|
31
|
+
|
|
32
|
+
# Boolean
|
|
33
|
+
BOOLEAN = "BOOLEAN"
|
|
34
|
+
|
|
35
|
+
# Date/Time
|
|
36
|
+
DATE = "DATE"
|
|
37
|
+
TIME = "TIME"
|
|
38
|
+
TIMESTAMP = "TIMESTAMP"
|
|
39
|
+
TIMESTAMPTZ = "TIMESTAMPTZ"
|
|
40
|
+
|
|
41
|
+
# UUID
|
|
42
|
+
UUID = "UUID"
|
|
43
|
+
|
|
44
|
+
# JSON
|
|
45
|
+
JSON = "JSON"
|
|
46
|
+
JSONB = "JSONB"
|
|
47
|
+
|
|
48
|
+
# Binary
|
|
49
|
+
BYTEA = "BYTEA"
|
|
50
|
+
|
|
51
|
+
# Unknown/Custom
|
|
52
|
+
UNKNOWN = "UNKNOWN"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class Column:
|
|
57
|
+
"""Represents a database column."""
|
|
58
|
+
|
|
59
|
+
name: str
|
|
60
|
+
type: ColumnType
|
|
61
|
+
nullable: bool = True
|
|
62
|
+
default: str | None = None
|
|
63
|
+
primary_key: bool = False
|
|
64
|
+
unique: bool = False
|
|
65
|
+
length: int | None = None # For VARCHAR(n), etc.
|
|
66
|
+
|
|
67
|
+
def __eq__(self, other: object) -> bool:
|
|
68
|
+
"""Compare columns for equality."""
|
|
69
|
+
if not isinstance(other, Column):
|
|
70
|
+
return NotImplemented
|
|
71
|
+
return (
|
|
72
|
+
self.name == other.name
|
|
73
|
+
and self.type == other.type
|
|
74
|
+
and self.nullable == other.nullable
|
|
75
|
+
and self.default == other.default
|
|
76
|
+
and self.primary_key == other.primary_key
|
|
77
|
+
and self.unique == other.unique
|
|
78
|
+
and self.length == other.length
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def __hash__(self) -> int:
|
|
82
|
+
"""Make column hashable for use in sets."""
|
|
83
|
+
return hash(
|
|
84
|
+
(
|
|
85
|
+
self.name,
|
|
86
|
+
self.type,
|
|
87
|
+
self.nullable,
|
|
88
|
+
self.default,
|
|
89
|
+
self.primary_key,
|
|
90
|
+
self.unique,
|
|
91
|
+
self.length,
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class Table:
|
|
98
|
+
"""Represents a database table."""
|
|
99
|
+
|
|
100
|
+
name: str
|
|
101
|
+
columns: list[Column] = field(default_factory=list)
|
|
102
|
+
indexes: list[str] = field(default_factory=list) # Simplified for MVP
|
|
103
|
+
constraints: list[str] = field(default_factory=list) # Simplified for MVP
|
|
104
|
+
|
|
105
|
+
def get_column(self, name: str) -> Column | None:
|
|
106
|
+
"""Get column by name."""
|
|
107
|
+
for col in self.columns:
|
|
108
|
+
if col.name == name:
|
|
109
|
+
return col
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
def has_column(self, name: str) -> bool:
|
|
113
|
+
"""Check if table has column."""
|
|
114
|
+
return self.get_column(name) is not None
|
|
115
|
+
|
|
116
|
+
def __eq__(self, other: object) -> bool:
|
|
117
|
+
"""Compare tables for equality."""
|
|
118
|
+
if not isinstance(other, Table):
|
|
119
|
+
return NotImplemented
|
|
120
|
+
return (
|
|
121
|
+
self.name == other.name
|
|
122
|
+
and self.columns == other.columns
|
|
123
|
+
and self.indexes == other.indexes
|
|
124
|
+
and self.constraints == other.constraints
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class Schema:
|
|
130
|
+
"""Represents a complete database schema."""
|
|
131
|
+
|
|
132
|
+
tables: list[Table] = field(default_factory=list)
|
|
133
|
+
|
|
134
|
+
def get_table(self, name: str) -> Table | None:
|
|
135
|
+
"""Get table by name."""
|
|
136
|
+
for table in self.tables:
|
|
137
|
+
if table.name == name:
|
|
138
|
+
return table
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
def has_table(self, name: str) -> bool:
|
|
142
|
+
"""Check if schema has table."""
|
|
143
|
+
return self.get_table(name) is not None
|
|
144
|
+
|
|
145
|
+
def table_names(self) -> list[str]:
|
|
146
|
+
"""Get list of all table names."""
|
|
147
|
+
return [table.name for table in self.tables]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass
|
|
151
|
+
class SchemaChange:
|
|
152
|
+
"""Represents a single change between two schemas."""
|
|
153
|
+
|
|
154
|
+
type: str # ADD_TABLE, DROP_TABLE, ADD_COLUMN, etc.
|
|
155
|
+
table: str | None = None
|
|
156
|
+
column: str | None = None
|
|
157
|
+
old_value: str | None = None
|
|
158
|
+
new_value: str | None = None
|
|
159
|
+
details: dict[str, str] | None = None
|
|
160
|
+
|
|
161
|
+
def __str__(self) -> str:
|
|
162
|
+
"""String representation of change."""
|
|
163
|
+
if self.type == "ADD_TABLE":
|
|
164
|
+
return f"ADD TABLE {self.table}"
|
|
165
|
+
elif self.type == "DROP_TABLE":
|
|
166
|
+
return f"DROP TABLE {self.table}"
|
|
167
|
+
elif self.type == "RENAME_TABLE":
|
|
168
|
+
return f"RENAME TABLE {self.old_value} TO {self.new_value}"
|
|
169
|
+
elif self.type == "ADD_COLUMN":
|
|
170
|
+
return f"ADD COLUMN {self.table}.{self.column}"
|
|
171
|
+
elif self.type == "DROP_COLUMN":
|
|
172
|
+
return f"DROP COLUMN {self.table}.{self.column}"
|
|
173
|
+
elif self.type == "RENAME_COLUMN":
|
|
174
|
+
return f"RENAME COLUMN {self.table}.{self.old_value} TO {self.new_value}"
|
|
175
|
+
elif self.type == "CHANGE_COLUMN_TYPE":
|
|
176
|
+
return f"CHANGE COLUMN TYPE {self.table}.{self.column} FROM {self.old_value} TO {self.new_value}"
|
|
177
|
+
elif self.type == "CHANGE_COLUMN_NULLABLE":
|
|
178
|
+
return f"CHANGE COLUMN NULLABLE {self.table}.{self.column} FROM {self.old_value} TO {self.new_value}"
|
|
179
|
+
elif self.type == "CHANGE_COLUMN_DEFAULT":
|
|
180
|
+
return f"CHANGE COLUMN DEFAULT {self.table}.{self.column}"
|
|
181
|
+
else:
|
|
182
|
+
return f"{self.type}: {self.table}.{self.column if self.column else ''}"
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass
|
|
186
|
+
class SchemaDiff:
|
|
187
|
+
"""Represents the difference between two schemas."""
|
|
188
|
+
|
|
189
|
+
changes: list[SchemaChange] = field(default_factory=list)
|
|
190
|
+
|
|
191
|
+
def has_changes(self) -> bool:
|
|
192
|
+
"""Check if there are any changes."""
|
|
193
|
+
return len(self.changes) > 0
|
|
194
|
+
|
|
195
|
+
def count_by_type(self, change_type: str) -> int:
|
|
196
|
+
"""Count changes of a specific type."""
|
|
197
|
+
return sum(1 for c in self.changes if c.type == change_type)
|
|
198
|
+
|
|
199
|
+
def __str__(self) -> str:
|
|
200
|
+
"""String representation of diff."""
|
|
201
|
+
if not self.has_changes():
|
|
202
|
+
return "No changes detected"
|
|
203
|
+
return "\n".join(str(c) for c in self.changes)
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""SQL file-based migrations.
|
|
2
|
+
|
|
3
|
+
Provides support for pure SQL migration files without Python boilerplate.
|
|
4
|
+
Migrations are discovered from .up.sql/.down.sql file pairs.
|
|
5
|
+
|
|
6
|
+
Example file structure:
|
|
7
|
+
db/migrations/
|
|
8
|
+
├── 001_create_users.py # Python migration
|
|
9
|
+
├── 002_add_posts.py # Python migration
|
|
10
|
+
├── 003_move_catalog_tables.up.sql # SQL migration (up)
|
|
11
|
+
├── 003_move_catalog_tables.down.sql # SQL migration (down)
|
|
12
|
+
|
|
13
|
+
The migrator will automatically detect and load SQL file pairs alongside
|
|
14
|
+
Python migrations.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import psycopg
|
|
20
|
+
|
|
21
|
+
from confiture.models.migration import Migration
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FileSQLMigration(Migration):
|
|
25
|
+
"""Migration loaded from .up.sql/.down.sql file pair.
|
|
26
|
+
|
|
27
|
+
This class is instantiated dynamically by the migrator when it discovers
|
|
28
|
+
SQL file pairs. Users don't create these directly - they just create the
|
|
29
|
+
SQL files.
|
|
30
|
+
|
|
31
|
+
The version and name are extracted from the filename:
|
|
32
|
+
- `003_move_catalog_tables.up.sql` → version="003", name="move_catalog_tables"
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
up_file: Path to the .up.sql file
|
|
36
|
+
down_file: Path to the .down.sql file
|
|
37
|
+
|
|
38
|
+
Note:
|
|
39
|
+
This class is instantiated by the migration loader, not directly by users.
|
|
40
|
+
To create a SQL migration, simply create the .up.sql and .down.sql files.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
connection: psycopg.Connection,
|
|
46
|
+
up_file: Path,
|
|
47
|
+
down_file: Path,
|
|
48
|
+
):
|
|
49
|
+
"""Initialize file-based SQL migration.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
connection: psycopg3 database connection
|
|
53
|
+
up_file: Path to the .up.sql file
|
|
54
|
+
down_file: Path to the .down.sql file
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
FileNotFoundError: If either SQL file doesn't exist
|
|
58
|
+
"""
|
|
59
|
+
# Extract version and name from filename before calling super().__init__
|
|
60
|
+
# Filename format: 003_move_catalog_tables.up.sql
|
|
61
|
+
base_name = up_file.name.replace(".up.sql", "")
|
|
62
|
+
parts = base_name.split("_", 1)
|
|
63
|
+
|
|
64
|
+
# Set class attributes dynamically for this instance
|
|
65
|
+
# We need to do this before super().__init__ because it validates version/name
|
|
66
|
+
self.__class__ = type(
|
|
67
|
+
f"FileSQLMigration_{base_name}",
|
|
68
|
+
(FileSQLMigration,),
|
|
69
|
+
{
|
|
70
|
+
"version": parts[0] if parts else "???",
|
|
71
|
+
"name": parts[1] if len(parts) > 1 else base_name,
|
|
72
|
+
"up_file": up_file,
|
|
73
|
+
"down_file": down_file,
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
self.up_file = up_file
|
|
78
|
+
self.down_file = down_file
|
|
79
|
+
|
|
80
|
+
# Validate files exist
|
|
81
|
+
if not up_file.exists():
|
|
82
|
+
raise FileNotFoundError(f"Migration up file not found: {up_file}")
|
|
83
|
+
if not down_file.exists():
|
|
84
|
+
raise FileNotFoundError(f"Migration down file not found: {down_file}")
|
|
85
|
+
|
|
86
|
+
super().__init__(connection)
|
|
87
|
+
|
|
88
|
+
def up(self) -> None:
|
|
89
|
+
"""Apply the migration by executing the .up.sql file."""
|
|
90
|
+
sql = self.up_file.read_text()
|
|
91
|
+
self.execute(sql)
|
|
92
|
+
|
|
93
|
+
def down(self) -> None:
|
|
94
|
+
"""Rollback the migration by executing the .down.sql file."""
|
|
95
|
+
sql = self.down_file.read_text()
|
|
96
|
+
self.execute(sql)
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def from_files(
|
|
100
|
+
cls,
|
|
101
|
+
up_file: Path,
|
|
102
|
+
down_file: Path,
|
|
103
|
+
) -> type["FileSQLMigration"]:
|
|
104
|
+
"""Create a migration class from SQL file pair.
|
|
105
|
+
|
|
106
|
+
This creates a new class (not instance) that can be used with the
|
|
107
|
+
standard migration system. The class has version and name extracted
|
|
108
|
+
from the filename.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
up_file: Path to the .up.sql file
|
|
112
|
+
down_file: Path to the .down.sql file
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
A new Migration class (not instance)
|
|
116
|
+
|
|
117
|
+
Example:
|
|
118
|
+
>>> MigrationClass = FileSQLMigration.from_files(
|
|
119
|
+
... Path("db/migrations/003_move_tables.up.sql"),
|
|
120
|
+
... Path("db/migrations/003_move_tables.down.sql"),
|
|
121
|
+
... )
|
|
122
|
+
>>> migration = MigrationClass(connection=conn)
|
|
123
|
+
>>> migration.up()
|
|
124
|
+
"""
|
|
125
|
+
# Extract version and name from filename
|
|
126
|
+
base_name = up_file.name.replace(".up.sql", "")
|
|
127
|
+
parts = base_name.split("_", 1)
|
|
128
|
+
version = parts[0] if parts else "???"
|
|
129
|
+
name = parts[1] if len(parts) > 1 else base_name
|
|
130
|
+
|
|
131
|
+
# Create a new class dynamically
|
|
132
|
+
class_name = f"FileSQLMigration_{base_name}"
|
|
133
|
+
|
|
134
|
+
def init_method(self: "FileSQLMigration", connection: psycopg.Connection) -> None:
|
|
135
|
+
self.up_file = up_file
|
|
136
|
+
self.down_file = down_file
|
|
137
|
+
self.connection = connection
|
|
138
|
+
|
|
139
|
+
# Validate files exist
|
|
140
|
+
if not up_file.exists():
|
|
141
|
+
raise FileNotFoundError(f"Migration up file not found: {up_file}")
|
|
142
|
+
if not down_file.exists():
|
|
143
|
+
raise FileNotFoundError(f"Migration down file not found: {down_file}")
|
|
144
|
+
|
|
145
|
+
def up_method(self: "FileSQLMigration") -> None:
|
|
146
|
+
sql = self.up_file.read_text()
|
|
147
|
+
self.execute(sql)
|
|
148
|
+
|
|
149
|
+
def down_method(self: "FileSQLMigration") -> None:
|
|
150
|
+
sql = self.down_file.read_text()
|
|
151
|
+
self.execute(sql)
|
|
152
|
+
|
|
153
|
+
# Create the class
|
|
154
|
+
new_class = type(
|
|
155
|
+
class_name,
|
|
156
|
+
(Migration,),
|
|
157
|
+
{
|
|
158
|
+
"version": version,
|
|
159
|
+
"name": name,
|
|
160
|
+
"up_file": up_file,
|
|
161
|
+
"down_file": down_file,
|
|
162
|
+
"__init__": init_method,
|
|
163
|
+
"up": up_method,
|
|
164
|
+
"down": down_method,
|
|
165
|
+
},
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return new_class # type: ignore[return-value]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def find_sql_migration_files(migrations_dir: Path) -> list[tuple[Path, Path]]:
|
|
172
|
+
"""Find all SQL migration file pairs in a directory.
|
|
173
|
+
|
|
174
|
+
Searches for .up.sql files and matches them with corresponding .down.sql files.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
migrations_dir: Directory to search for SQL migrations
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
List of (up_file, down_file) tuples, sorted by version
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
ValueError: If an .up.sql file has no matching .down.sql file
|
|
184
|
+
|
|
185
|
+
Example:
|
|
186
|
+
>>> pairs = find_sql_migration_files(Path("db/migrations"))
|
|
187
|
+
>>> for up_file, down_file in pairs:
|
|
188
|
+
... print(f"Found: {up_file.name}")
|
|
189
|
+
"""
|
|
190
|
+
pairs: list[tuple[Path, Path]] = []
|
|
191
|
+
|
|
192
|
+
# Find all .up.sql files
|
|
193
|
+
for up_file in sorted(migrations_dir.glob("*.up.sql")):
|
|
194
|
+
# Find matching .down.sql
|
|
195
|
+
base_name = up_file.name.replace(".up.sql", "")
|
|
196
|
+
down_file = migrations_dir / f"{base_name}.down.sql"
|
|
197
|
+
|
|
198
|
+
if not down_file.exists():
|
|
199
|
+
raise ValueError(
|
|
200
|
+
f"SQL migration {up_file.name} has no matching .down.sql file.\n"
|
|
201
|
+
f"Expected: {down_file}\n"
|
|
202
|
+
f"Hint: Create {down_file.name} with the rollback SQL"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
pairs.append((up_file, down_file))
|
|
206
|
+
|
|
207
|
+
return pairs
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def get_sql_migration_version(up_file: Path) -> str:
|
|
211
|
+
"""Extract version from SQL migration filename.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
up_file: Path to the .up.sql file
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Version string (e.g., "003")
|
|
218
|
+
|
|
219
|
+
Example:
|
|
220
|
+
>>> get_sql_migration_version(Path("003_move_tables.up.sql"))
|
|
221
|
+
'003'
|
|
222
|
+
"""
|
|
223
|
+
base_name = up_file.name.replace(".up.sql", "")
|
|
224
|
+
parts = base_name.split("_", 1)
|
|
225
|
+
return parts[0] if parts else "???"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Real-world anonymization scenarios.
|
|
2
|
+
|
|
3
|
+
Provides ready-to-use anonymization profiles for common business domains:
|
|
4
|
+
- E-commerce: Customer data, orders, payments
|
|
5
|
+
- Healthcare: HIPAA-compliant PHI anonymization
|
|
6
|
+
- Financial: Loan applications, credit data
|
|
7
|
+
- SaaS: User accounts, subscription data
|
|
8
|
+
- Multi-tenant: Data isolation with deterministic seeding
|
|
9
|
+
|
|
10
|
+
Each scenario includes:
|
|
11
|
+
- Pre-configured strategy profiles
|
|
12
|
+
- Batch anonymization support
|
|
13
|
+
- Domain-specific validation
|
|
14
|
+
- Usage examples
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
>>> from confiture.scenarios import ECommerceScenario
|
|
18
|
+
>>> data = {"first_name": "John", "email": "john@example.com"}
|
|
19
|
+
>>> anonymized = ECommerceScenario.anonymize(data)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# Import strategies to ensure registration (must come before scenario imports)
|
|
23
|
+
import confiture.core.anonymization.strategies # noqa: F401
|
|
24
|
+
from confiture.scenarios.ecommerce import ECommerceScenario
|
|
25
|
+
from confiture.scenarios.financial import FinancialScenario
|
|
26
|
+
from confiture.scenarios.healthcare import HealthcareScenario
|
|
27
|
+
from confiture.scenarios.multi_tenant import MultiTenantScenario
|
|
28
|
+
from confiture.scenarios.saas import SaaSScenario
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"ECommerceScenario",
|
|
32
|
+
"HealthcareScenario",
|
|
33
|
+
"FinancialScenario",
|
|
34
|
+
"SaaSScenario",
|
|
35
|
+
"MultiTenantScenario",
|
|
36
|
+
]
|