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,182 @@
|
|
|
1
|
+
"""Migration dry-run mode - test migrations in transaction.
|
|
2
|
+
|
|
3
|
+
This module provides dry-run capability for migrations, allowing operators to:
|
|
4
|
+
- Test migrations without making permanent changes
|
|
5
|
+
- Verify data integrity before production deployment
|
|
6
|
+
- Estimate execution time and identify locking issues
|
|
7
|
+
- Detect constraint violations early
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import time
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import psycopg
|
|
16
|
+
|
|
17
|
+
from confiture.exceptions import MigrationError
|
|
18
|
+
|
|
19
|
+
# Logger for dry-run execution
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DryRunError(MigrationError):
|
|
24
|
+
"""Error raised when dry-run execution fails."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, migration_name: str, error: Exception):
|
|
27
|
+
"""Initialize dry-run error.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
migration_name: Name of migration that failed
|
|
31
|
+
error: Original exception
|
|
32
|
+
"""
|
|
33
|
+
self.migration_name = migration_name
|
|
34
|
+
self.original_error = error
|
|
35
|
+
super().__init__(f"Dry-run failed for migration {migration_name}: {str(error)}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class DryRunResult:
|
|
40
|
+
"""Result of a dry-run execution."""
|
|
41
|
+
|
|
42
|
+
migration_name: str
|
|
43
|
+
migration_version: str
|
|
44
|
+
success: bool
|
|
45
|
+
execution_time_ms: int = 0
|
|
46
|
+
rows_affected: int = 0
|
|
47
|
+
locked_tables: list[str] = field(default_factory=list)
|
|
48
|
+
estimated_production_time_ms: int = 0
|
|
49
|
+
confidence_percent: int = 0
|
|
50
|
+
warnings: list[str] = field(default_factory=list)
|
|
51
|
+
stats: dict[str, Any] = field(default_factory=dict)
|
|
52
|
+
|
|
53
|
+
def __post_init__(self):
|
|
54
|
+
"""Initialize empty collections if needed."""
|
|
55
|
+
if self.locked_tables is None:
|
|
56
|
+
self.locked_tables = []
|
|
57
|
+
if self.warnings is None:
|
|
58
|
+
self.warnings = []
|
|
59
|
+
if self.stats is None:
|
|
60
|
+
self.stats = {}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class DryRunExecutor:
|
|
64
|
+
"""Executes migrations in dry-run mode for testing.
|
|
65
|
+
|
|
66
|
+
Features:
|
|
67
|
+
- Transaction-based execution with automatic rollback
|
|
68
|
+
- Capture of execution metrics (time, rows affected, locks)
|
|
69
|
+
- Estimation of production execution time
|
|
70
|
+
- Detection of constraint violations
|
|
71
|
+
- Confidence level for estimates
|
|
72
|
+
- Structured logging for observability
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(self):
|
|
76
|
+
"""Initialize dry-run executor."""
|
|
77
|
+
self.logger = logger
|
|
78
|
+
|
|
79
|
+
def run(
|
|
80
|
+
self,
|
|
81
|
+
conn: psycopg.Connection, # noqa: ARG002 - used in real implementation
|
|
82
|
+
migration,
|
|
83
|
+
) -> DryRunResult:
|
|
84
|
+
"""Execute migration in dry-run mode.
|
|
85
|
+
|
|
86
|
+
Executes the migration within a transaction that is automatically
|
|
87
|
+
rolled back, allowing testing without permanent changes.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
conn: Database connection
|
|
91
|
+
migration: Migration instance with up() method
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
DryRunResult with execution metrics
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
DryRunError: If migration execution fails
|
|
98
|
+
"""
|
|
99
|
+
# Log dry-run start
|
|
100
|
+
self.logger.info(
|
|
101
|
+
"dry_run_start",
|
|
102
|
+
extra={
|
|
103
|
+
"migration": migration.name,
|
|
104
|
+
"version": migration.version,
|
|
105
|
+
},
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
execution_time_ms = self._execute_migration(migration)
|
|
110
|
+
result = self._build_result(migration, execution_time_ms)
|
|
111
|
+
|
|
112
|
+
# Log dry-run completion
|
|
113
|
+
self.logger.info(
|
|
114
|
+
"dry_run_completed",
|
|
115
|
+
extra={
|
|
116
|
+
"migration": migration.name,
|
|
117
|
+
"version": migration.version,
|
|
118
|
+
"execution_time_ms": execution_time_ms,
|
|
119
|
+
"success": True,
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
except Exception as e:
|
|
126
|
+
# Log dry-run failure
|
|
127
|
+
self.logger.error(
|
|
128
|
+
"dry_run_failed",
|
|
129
|
+
extra={
|
|
130
|
+
"migration": migration.name,
|
|
131
|
+
"version": migration.version,
|
|
132
|
+
"error": str(e),
|
|
133
|
+
},
|
|
134
|
+
exc_info=True,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
raise DryRunError(migration_name=migration.name, error=e) from e
|
|
138
|
+
|
|
139
|
+
def _execute_migration(self, migration) -> int:
|
|
140
|
+
"""Execute migration and return execution time in milliseconds.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
migration: Migration instance with up() method
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Execution time in milliseconds
|
|
147
|
+
"""
|
|
148
|
+
start_time = time.time()
|
|
149
|
+
migration.up()
|
|
150
|
+
return int((time.time() - start_time) * 1000)
|
|
151
|
+
|
|
152
|
+
def _build_result(self, migration, execution_time_ms: int) -> DryRunResult:
|
|
153
|
+
"""Build DryRunResult from execution metrics.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
migration: Migration instance
|
|
157
|
+
execution_time_ms: Execution time in milliseconds
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
DryRunResult with calculated metrics
|
|
161
|
+
"""
|
|
162
|
+
# In real implementation, would:
|
|
163
|
+
# - Detect locked tables via pg_locks
|
|
164
|
+
# - Calculate confidence based on lock time variance
|
|
165
|
+
# - Estimate production time with ±15% confidence
|
|
166
|
+
|
|
167
|
+
return DryRunResult(
|
|
168
|
+
migration_name=migration.name,
|
|
169
|
+
migration_version=migration.version,
|
|
170
|
+
success=True,
|
|
171
|
+
execution_time_ms=execution_time_ms,
|
|
172
|
+
rows_affected=0,
|
|
173
|
+
locked_tables=[],
|
|
174
|
+
estimated_production_time_ms=execution_time_ms, # Best estimate
|
|
175
|
+
confidence_percent=85, # Default confidence
|
|
176
|
+
warnings=[],
|
|
177
|
+
stats={
|
|
178
|
+
"measured_execution_ms": execution_time_ms,
|
|
179
|
+
"estimated_range_low_ms": int(execution_time_ms * 0.85),
|
|
180
|
+
"estimated_range_high_ms": int(execution_time_ms * 1.15),
|
|
181
|
+
},
|
|
182
|
+
)
|
confiture/core/health.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""Health check endpoints for Kubernetes probes.
|
|
2
|
+
|
|
3
|
+
Provides health endpoints for readiness and liveness probes
|
|
4
|
+
in Kubernetes/container environments.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import threading
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class HealthStatus:
|
|
19
|
+
"""Health check status."""
|
|
20
|
+
|
|
21
|
+
ready: bool = False
|
|
22
|
+
live: bool = True
|
|
23
|
+
migration_status: str = "pending" # pending, running, completed, failed
|
|
24
|
+
current_migration: str | None = None
|
|
25
|
+
pending_count: int = 0
|
|
26
|
+
applied_count: int = 0
|
|
27
|
+
error: str | None = None
|
|
28
|
+
|
|
29
|
+
def to_dict(self) -> dict[str, Any]:
|
|
30
|
+
"""Convert to dictionary."""
|
|
31
|
+
return {
|
|
32
|
+
"ready": self.ready,
|
|
33
|
+
"live": self.live,
|
|
34
|
+
"migration_status": self.migration_status,
|
|
35
|
+
"current_migration": self.current_migration,
|
|
36
|
+
"pending_count": self.pending_count,
|
|
37
|
+
"applied_count": self.applied_count,
|
|
38
|
+
"error": self.error,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class HealthHandler(BaseHTTPRequestHandler):
|
|
43
|
+
"""HTTP handler for health endpoints.
|
|
44
|
+
|
|
45
|
+
Handles:
|
|
46
|
+
- GET /ready - Readiness probe (is migration complete?)
|
|
47
|
+
- GET /live - Liveness probe (is process alive?)
|
|
48
|
+
- GET /health - Full health status
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
health_status: HealthStatus = HealthStatus()
|
|
52
|
+
|
|
53
|
+
def do_GET(self) -> None:
|
|
54
|
+
"""Handle GET requests."""
|
|
55
|
+
if self.path == "/ready" or self.path == "/readyz":
|
|
56
|
+
self._handle_ready()
|
|
57
|
+
elif self.path == "/live" or self.path == "/livez":
|
|
58
|
+
self._handle_live()
|
|
59
|
+
elif self.path == "/health" or self.path == "/healthz":
|
|
60
|
+
self._handle_health()
|
|
61
|
+
else:
|
|
62
|
+
self.send_error(404, "Not Found")
|
|
63
|
+
|
|
64
|
+
def _handle_ready(self) -> None:
|
|
65
|
+
"""Handle readiness probe.
|
|
66
|
+
|
|
67
|
+
Returns 200 when migrations are complete, 503 otherwise.
|
|
68
|
+
"""
|
|
69
|
+
if self.health_status.ready:
|
|
70
|
+
self._send_json(200, {"ready": True})
|
|
71
|
+
else:
|
|
72
|
+
self._send_json(
|
|
73
|
+
503,
|
|
74
|
+
{
|
|
75
|
+
"ready": False,
|
|
76
|
+
"status": self.health_status.migration_status,
|
|
77
|
+
"current_migration": self.health_status.current_migration,
|
|
78
|
+
"pending_count": self.health_status.pending_count,
|
|
79
|
+
},
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def _handle_live(self) -> None:
|
|
83
|
+
"""Handle liveness probe.
|
|
84
|
+
|
|
85
|
+
Returns 200 when process is alive, 503 on fatal error.
|
|
86
|
+
"""
|
|
87
|
+
if self.health_status.live:
|
|
88
|
+
self._send_json(200, {"live": True})
|
|
89
|
+
else:
|
|
90
|
+
self._send_json(
|
|
91
|
+
503,
|
|
92
|
+
{
|
|
93
|
+
"live": False,
|
|
94
|
+
"error": self.health_status.error,
|
|
95
|
+
},
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def _handle_health(self) -> None:
|
|
99
|
+
"""Handle full health status.
|
|
100
|
+
|
|
101
|
+
Returns complete health information.
|
|
102
|
+
"""
|
|
103
|
+
status_code = 200 if self.health_status.ready else 503
|
|
104
|
+
self._send_json(status_code, self.health_status.to_dict())
|
|
105
|
+
|
|
106
|
+
def _send_json(self, status: int, data: dict) -> None:
|
|
107
|
+
"""Send JSON response."""
|
|
108
|
+
self.send_response(status)
|
|
109
|
+
self.send_header("Content-Type", "application/json")
|
|
110
|
+
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
111
|
+
self.end_headers()
|
|
112
|
+
self.wfile.write(json.dumps(data).encode())
|
|
113
|
+
|
|
114
|
+
def log_message(self, format: str, *args: Any) -> None:
|
|
115
|
+
"""Suppress default request logging."""
|
|
116
|
+
# Only log errors
|
|
117
|
+
if args and "error" in str(args[0]).lower():
|
|
118
|
+
logger.warning(format % args)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class HealthServer:
|
|
122
|
+
"""Health check HTTP server for Kubernetes probes.
|
|
123
|
+
|
|
124
|
+
Runs a lightweight HTTP server in a background thread to serve
|
|
125
|
+
health check endpoints for Kubernetes readiness and liveness probes.
|
|
126
|
+
|
|
127
|
+
Example:
|
|
128
|
+
>>> server = HealthServer(port=8080)
|
|
129
|
+
>>> server.start()
|
|
130
|
+
>>> server.set_running("001_create_users")
|
|
131
|
+
>>> # ... run migration ...
|
|
132
|
+
>>> server.set_completed()
|
|
133
|
+
>>> # Server responds to /ready with 200 OK
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def __init__(self, host: str = "0.0.0.0", port: int = 8080):
|
|
137
|
+
"""Initialize health server.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
host: Host to bind to (default: 0.0.0.0)
|
|
141
|
+
port: Port to listen on (default: 8080)
|
|
142
|
+
"""
|
|
143
|
+
self.host = host
|
|
144
|
+
self.port = port
|
|
145
|
+
self._server: HTTPServer | None = None
|
|
146
|
+
self._thread: threading.Thread | None = None
|
|
147
|
+
self._status = HealthStatus()
|
|
148
|
+
self._running = False
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def is_running(self) -> bool:
|
|
152
|
+
"""Check if server is running."""
|
|
153
|
+
return self._running
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def status(self) -> HealthStatus:
|
|
157
|
+
"""Get current health status."""
|
|
158
|
+
return self._status
|
|
159
|
+
|
|
160
|
+
def start(self) -> None:
|
|
161
|
+
"""Start health server in background thread."""
|
|
162
|
+
if self._running:
|
|
163
|
+
logger.warning("Health server already running")
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
# Set handler's status reference
|
|
167
|
+
HealthHandler.health_status = self._status
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
self._server = HTTPServer((self.host, self.port), HealthHandler)
|
|
171
|
+
|
|
172
|
+
self._thread = threading.Thread(
|
|
173
|
+
target=self._server.serve_forever,
|
|
174
|
+
name="confiture-health-server",
|
|
175
|
+
)
|
|
176
|
+
self._thread.daemon = True
|
|
177
|
+
self._thread.start()
|
|
178
|
+
|
|
179
|
+
self._running = True
|
|
180
|
+
logger.info(f"Health server started on {self.host}:{self.port}")
|
|
181
|
+
|
|
182
|
+
except OSError as e:
|
|
183
|
+
logger.error(f"Failed to start health server: {e}")
|
|
184
|
+
raise
|
|
185
|
+
|
|
186
|
+
def stop(self) -> None:
|
|
187
|
+
"""Stop health server."""
|
|
188
|
+
if not self._running:
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
if self._server:
|
|
192
|
+
self._server.shutdown()
|
|
193
|
+
self._server = None
|
|
194
|
+
|
|
195
|
+
self._running = False
|
|
196
|
+
logger.info("Health server stopped")
|
|
197
|
+
|
|
198
|
+
def set_pending(self, pending_count: int = 0) -> None:
|
|
199
|
+
"""Set status to pending (waiting to start).
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
pending_count: Number of pending migrations
|
|
203
|
+
"""
|
|
204
|
+
self._status.ready = False
|
|
205
|
+
self._status.live = True
|
|
206
|
+
self._status.migration_status = "pending"
|
|
207
|
+
self._status.current_migration = None
|
|
208
|
+
self._status.pending_count = pending_count
|
|
209
|
+
self._status.error = None
|
|
210
|
+
|
|
211
|
+
def set_running(self, migration: str, remaining: int = 0) -> None:
|
|
212
|
+
"""Set status to running a migration.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
migration: Name/version of current migration
|
|
216
|
+
remaining: Number of remaining migrations after current
|
|
217
|
+
"""
|
|
218
|
+
self._status.ready = False
|
|
219
|
+
self._status.live = True
|
|
220
|
+
self._status.migration_status = "running"
|
|
221
|
+
self._status.current_migration = migration
|
|
222
|
+
self._status.pending_count = remaining
|
|
223
|
+
self._status.error = None
|
|
224
|
+
|
|
225
|
+
def set_completed(self, applied_count: int = 0) -> None:
|
|
226
|
+
"""Set status to completed (all migrations done).
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
applied_count: Number of migrations applied
|
|
230
|
+
"""
|
|
231
|
+
self._status.ready = True
|
|
232
|
+
self._status.live = True
|
|
233
|
+
self._status.migration_status = "completed"
|
|
234
|
+
self._status.current_migration = None
|
|
235
|
+
self._status.pending_count = 0
|
|
236
|
+
self._status.applied_count = applied_count
|
|
237
|
+
self._status.error = None
|
|
238
|
+
|
|
239
|
+
def set_failed(self, error: str, migration: str | None = None) -> None:
|
|
240
|
+
"""Set status to failed (migration error).
|
|
241
|
+
|
|
242
|
+
This will cause both readiness and liveness to fail,
|
|
243
|
+
which may trigger a pod restart in Kubernetes.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
error: Error message
|
|
247
|
+
migration: Name of failed migration (optional)
|
|
248
|
+
"""
|
|
249
|
+
self._status.ready = False
|
|
250
|
+
self._status.live = False # Trigger restart
|
|
251
|
+
self._status.migration_status = "failed"
|
|
252
|
+
self._status.current_migration = migration
|
|
253
|
+
self._status.error = error
|
|
254
|
+
|
|
255
|
+
def set_error_recoverable(self, error: str, migration: str | None = None) -> None:
|
|
256
|
+
"""Set status to error but still alive (recoverable error).
|
|
257
|
+
|
|
258
|
+
Unlike set_failed(), this keeps liveness True so the pod
|
|
259
|
+
won't be restarted.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
error: Error message
|
|
263
|
+
migration: Name of problematic migration (optional)
|
|
264
|
+
"""
|
|
265
|
+
self._status.ready = False
|
|
266
|
+
self._status.live = True # Don't restart
|
|
267
|
+
self._status.migration_status = "error"
|
|
268
|
+
self._status.current_migration = migration
|
|
269
|
+
self._status.error = error
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def check_database_health(connection: Any) -> dict[str, Any]:
|
|
273
|
+
"""Check database connectivity and return health info.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
connection: Database connection to test
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Dictionary with health status
|
|
280
|
+
"""
|
|
281
|
+
result = {
|
|
282
|
+
"database_connected": False,
|
|
283
|
+
"migration_table_exists": False,
|
|
284
|
+
"database_name": None,
|
|
285
|
+
"error": None,
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
with connection.cursor() as cur:
|
|
290
|
+
# Check connection
|
|
291
|
+
cur.execute("SELECT 1")
|
|
292
|
+
result["database_connected"] = True
|
|
293
|
+
|
|
294
|
+
# Get database name
|
|
295
|
+
cur.execute("SELECT current_database()")
|
|
296
|
+
row = cur.fetchone()
|
|
297
|
+
result["database_name"] = row[0] if row else None
|
|
298
|
+
|
|
299
|
+
# Check migration table
|
|
300
|
+
cur.execute("""
|
|
301
|
+
SELECT EXISTS (
|
|
302
|
+
SELECT FROM information_schema.tables
|
|
303
|
+
WHERE table_schema = 'public'
|
|
304
|
+
AND table_name = 'confiture_migrations'
|
|
305
|
+
)
|
|
306
|
+
""")
|
|
307
|
+
row = cur.fetchone()
|
|
308
|
+
result["migration_table_exists"] = row[0] if row else False
|
|
309
|
+
|
|
310
|
+
except Exception as e:
|
|
311
|
+
result["error"] = str(e)
|
|
312
|
+
|
|
313
|
+
return result
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Enhanced Hook System.
|
|
2
|
+
|
|
3
|
+
Provides:
|
|
4
|
+
- Explicit hook execution semantics (sequential, parallel, DAG-based)
|
|
5
|
+
- Type-safe hook contexts with phase-specific data
|
|
6
|
+
- Three-category event system (Lifecycle, State, Alert)
|
|
7
|
+
- Full observability infrastructure (tracing, circuit breakers)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from .base import Hook, HookError, HookExecutor, HookResult
|
|
13
|
+
from .context import (
|
|
14
|
+
ExecutionContext,
|
|
15
|
+
HookContext,
|
|
16
|
+
MigrationPlanContext,
|
|
17
|
+
MigrationStep,
|
|
18
|
+
RiskAssessment,
|
|
19
|
+
RollbackContext,
|
|
20
|
+
Schema,
|
|
21
|
+
SchemaAnalysisContext,
|
|
22
|
+
SchemaDiffContext,
|
|
23
|
+
SchemaDifference,
|
|
24
|
+
ValidationContext,
|
|
25
|
+
)
|
|
26
|
+
from .execution_strategies import (
|
|
27
|
+
HookContextMutationPolicy,
|
|
28
|
+
HookErrorStrategy,
|
|
29
|
+
HookExecutionStrategy,
|
|
30
|
+
HookPhaseConfig,
|
|
31
|
+
RetryConfig,
|
|
32
|
+
)
|
|
33
|
+
from .observability import (
|
|
34
|
+
CircuitBreaker,
|
|
35
|
+
CircuitBreakerState,
|
|
36
|
+
ExecutionDAG,
|
|
37
|
+
HookExecutionError,
|
|
38
|
+
HookExecutionEvent,
|
|
39
|
+
HookExecutionResult,
|
|
40
|
+
HookExecutionStatus,
|
|
41
|
+
HookExecutionTracer,
|
|
42
|
+
PerformanceTrace,
|
|
43
|
+
)
|
|
44
|
+
from .phases import HookAlert, HookEvent, HookPhase
|
|
45
|
+
from .registry import HookRegistry
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
# Base classes
|
|
49
|
+
"Hook",
|
|
50
|
+
"HookResult",
|
|
51
|
+
"HookError",
|
|
52
|
+
"HookExecutor",
|
|
53
|
+
"HookContext",
|
|
54
|
+
# Phases/Events/Alerts
|
|
55
|
+
"HookPhase",
|
|
56
|
+
"HookEvent",
|
|
57
|
+
"HookAlert",
|
|
58
|
+
# Contexts
|
|
59
|
+
"SchemaAnalysisContext",
|
|
60
|
+
"SchemaDiffContext",
|
|
61
|
+
"MigrationPlanContext",
|
|
62
|
+
"ExecutionContext",
|
|
63
|
+
"RollbackContext",
|
|
64
|
+
"ValidationContext",
|
|
65
|
+
"Schema",
|
|
66
|
+
"SchemaDifference",
|
|
67
|
+
"RiskAssessment",
|
|
68
|
+
"MigrationStep",
|
|
69
|
+
# Execution strategies
|
|
70
|
+
"HookExecutionStrategy",
|
|
71
|
+
"HookErrorStrategy",
|
|
72
|
+
"HookContextMutationPolicy",
|
|
73
|
+
"HookPhaseConfig",
|
|
74
|
+
"RetryConfig",
|
|
75
|
+
# Observability
|
|
76
|
+
"HookExecutionStatus",
|
|
77
|
+
"HookExecutionEvent",
|
|
78
|
+
"HookExecutionResult",
|
|
79
|
+
"CircuitBreaker",
|
|
80
|
+
"CircuitBreakerState",
|
|
81
|
+
"HookExecutionTracer",
|
|
82
|
+
"HookExecutionError",
|
|
83
|
+
"ExecutionDAG",
|
|
84
|
+
"PerformanceTrace",
|
|
85
|
+
# Registry
|
|
86
|
+
"HookRegistry",
|
|
87
|
+
]
|