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,53 @@
1
+ """Hook event categorization.
2
+
3
+ Three categories with distinct semantics:
4
+ 1. LIFECYCLE EVENTS - Fired before/after specific operations
5
+ 2. STATE EVENTS - Fired when migration enters/leaves a state
6
+ 3. ALERT EVENTS - Fired when metrics cross thresholds
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from enum import Enum
12
+
13
+
14
+ class HookPhase(Enum):
15
+ """Lifecycle events - operation boundaries."""
16
+
17
+ BEFORE_ANALYZE_SCHEMA = "before_analyze_schema"
18
+ AFTER_ANALYZE_SCHEMA = "after_analyze_schema"
19
+ BEFORE_DIFF_SCHEMAS = "before_diff_schemas"
20
+ AFTER_DIFF_SCHEMAS = "after_diff_schemas"
21
+ BEFORE_PLAN_MIGRATION = "before_plan_migration"
22
+ AFTER_PLAN_MIGRATION = "after_plan_migration"
23
+ BEFORE_DRY_RUN = "before_dry_run"
24
+ AFTER_DRY_RUN = "after_dry_run"
25
+ BEFORE_EXECUTE = "before_execute"
26
+ AFTER_EXECUTE = "after_execute"
27
+ BEFORE_VALIDATE = "before_validate"
28
+ AFTER_VALIDATE = "after_validate"
29
+ BEFORE_ROLLBACK = "before_rollback"
30
+ AFTER_ROLLBACK = "after_rollback"
31
+
32
+
33
+ class HookEvent(Enum):
34
+ """State change events - observable state transitions."""
35
+
36
+ MIGRATION_STARTED = "migration_started"
37
+ MIGRATION_PAUSED = "migration_paused"
38
+ MIGRATION_RESUMED = "migration_resumed"
39
+ MIGRATION_COMPLETED = "migration_completed"
40
+ MIGRATION_FAILED = "migration_failed"
41
+ MIGRATION_ROLLED_BACK = "migration_rolled_back"
42
+ MIGRATION_CANCELLED = "migration_cancelled"
43
+
44
+
45
+ class HookAlert(Enum):
46
+ """Threshold alerts - reactive to metric crossings."""
47
+
48
+ DATA_ANOMALY_DETECTED = "data_anomaly_detected"
49
+ LOCK_TIMEOUT_EXCEEDED = "lock_timeout_exceeded"
50
+ PERFORMANCE_DEGRADED = "performance_degraded"
51
+ MEMORY_THRESHOLD_EXCEEDED = "memory_threshold_exceeded"
52
+ LONG_TRANSACTION_DETECTED = "long_transaction_detected"
53
+ CONNECTION_POOL_EXHAUSTED = "connection_pool_exhausted"
@@ -0,0 +1,295 @@
1
+ """Hook registry and execution engine."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ from datetime import UTC, datetime
8
+ from typing import Any, Generic, TypeVar
9
+
10
+ from .base import Hook, HookResult
11
+ from .context import HookContext
12
+ from .execution_strategies import (
13
+ HookErrorStrategy,
14
+ HookExecutionStrategy,
15
+ HookPhaseConfig,
16
+ )
17
+ from .observability import (
18
+ CircuitBreaker,
19
+ HookExecutionError,
20
+ HookExecutionEvent,
21
+ HookExecutionResult,
22
+ HookExecutionStatus,
23
+ HookExecutionTracer,
24
+ )
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ T = TypeVar("T")
29
+
30
+
31
+ def _extract_phase_value(phase: Any) -> str:
32
+ """Extract string value from phase enum or convert to string.
33
+
34
+ Args:
35
+ phase: A phase/event/alert that may be an enum or string.
36
+
37
+ Returns:
38
+ String representation of the phase value.
39
+ """
40
+ return phase.value if hasattr(phase, "value") else str(phase)
41
+
42
+
43
+ class HookRegistry(Generic[T]):
44
+ """Manage hook registration and execution."""
45
+
46
+ def __init__(self, execution_config: dict[Any, HookPhaseConfig] | None = None):
47
+ self.hooks: dict[str, list[Hook]] = {} # phase/event/alert -> hooks
48
+ self.execution_config = execution_config or {}
49
+ self.circuit_breakers: dict[str, CircuitBreaker] = {}
50
+ self.execution_log: list[HookExecutionEvent] = []
51
+ self.tracer = HookExecutionTracer()
52
+
53
+ def register(self, phase_key: str | Any, hook: Hook) -> None:
54
+ """Register a hook for a phase/event/alert."""
55
+ phase_value = _extract_phase_value(phase_key)
56
+
57
+ if phase_value not in self.hooks:
58
+ self.hooks[phase_value] = []
59
+
60
+ self.hooks[phase_value].append(hook)
61
+
62
+ # Create circuit breaker for this hook
63
+ if hook.id not in self.circuit_breakers:
64
+ self.circuit_breakers[hook.id] = CircuitBreaker(hook.id)
65
+
66
+ logger.info(f"Registered hook {hook.name} ({hook.id}) for phase {phase_value}")
67
+
68
+ async def trigger(
69
+ self,
70
+ phase: Any, # HookPhase | HookEvent | HookAlert
71
+ context: HookContext[T],
72
+ ) -> HookExecutionResult:
73
+ """Trigger hooks for a phase/event/alert."""
74
+ phase_value = _extract_phase_value(phase)
75
+ config = self.execution_config.get(phase, HookPhaseConfig(phase=phase))
76
+ hooks = self.hooks.get(phase_value, [])
77
+
78
+ if not hooks:
79
+ return HookExecutionResult(phase=phase_value, hooks_executed=0)
80
+
81
+ # Execute according to strategy
82
+ if config.execution_strategy == HookExecutionStrategy.SEQUENTIAL:
83
+ return await self._execute_sequential(context, hooks, config)
84
+ elif config.execution_strategy == HookExecutionStrategy.PARALLEL:
85
+ return await self._execute_parallel(context, hooks, config)
86
+ elif config.execution_strategy == HookExecutionStrategy.PARALLEL_WITH_DEPS:
87
+ return await self._execute_dag(context, hooks, config)
88
+
89
+ return HookExecutionResult(phase=phase_value, hooks_executed=0)
90
+
91
+ async def _execute_sequential(
92
+ self,
93
+ context: HookContext[T],
94
+ hooks: list[Hook],
95
+ config: HookPhaseConfig,
96
+ ) -> HookExecutionResult:
97
+ """Execute hooks one-by-one."""
98
+ results = []
99
+
100
+ for hook in sorted(hooks, key=lambda h: h.priority):
101
+ try:
102
+ result = await self._execute_hook_with_timeout(hook, context, config)
103
+ results.append(result)
104
+
105
+ # Check if we should fail fast
106
+ if (
107
+ result.status == HookExecutionStatus.FAILED
108
+ and config.error_strategy == HookErrorStrategy.FAIL_FAST
109
+ ):
110
+ raise HookExecutionError(f"Hook {hook.name} failed: {result.error}")
111
+
112
+ except Exception as e:
113
+ if config.error_strategy == HookErrorStrategy.FAIL_SAFE:
114
+ logger.error(f"Hook {hook.name} failed: {e}")
115
+ results.append(
116
+ HookExecutionEvent(
117
+ execution_id=context.execution_id,
118
+ hook_id=hook.id,
119
+ phase=_extract_phase_value(config.phase),
120
+ status=HookExecutionStatus.FAILED,
121
+ error=str(e),
122
+ duration_ms=0,
123
+ )
124
+ )
125
+ elif config.error_strategy == HookErrorStrategy.RETRY:
126
+ result = await self._retry_hook(hook, context, config)
127
+ results.append(result)
128
+ else:
129
+ raise
130
+
131
+ return HookExecutionResult(
132
+ phase=_extract_phase_value(config.phase),
133
+ hooks_executed=len(results),
134
+ results=results,
135
+ total_duration_ms=sum(r.duration_ms for r in results),
136
+ failed_count=sum(1 for r in results if r.status == HookExecutionStatus.FAILED),
137
+ timeout_count=sum(1 for r in results if r.status == HookExecutionStatus.TIMEOUT),
138
+ )
139
+
140
+ async def _execute_parallel(
141
+ self,
142
+ context: HookContext[T],
143
+ hooks: list[Hook],
144
+ config: HookPhaseConfig,
145
+ ) -> HookExecutionResult:
146
+ """Execute hooks in parallel."""
147
+ # Limit parallelism
148
+ semaphore = asyncio.Semaphore(config.max_parallel_hooks)
149
+
150
+ async def execute_with_semaphore(hook: Hook) -> HookExecutionEvent:
151
+ async with semaphore:
152
+ return await self._execute_hook_with_timeout(hook, context, config)
153
+
154
+ tasks = [execute_with_semaphore(hook) for hook in hooks]
155
+ results = await asyncio.gather(*tasks, return_exceptions=True)
156
+
157
+ # Process results
158
+ failed = [
159
+ r for r in results if isinstance(r, Exception) or r.status == HookExecutionStatus.FAILED
160
+ ]
161
+ if failed and config.error_strategy == HookErrorStrategy.FAIL_FAST:
162
+ raise HookExecutionError(f"{len(failed)} hooks failed in parallel execution")
163
+
164
+ return HookExecutionResult(
165
+ phase=_extract_phase_value(config.phase),
166
+ hooks_executed=len(results),
167
+ results=[r for r in results if not isinstance(r, Exception)],
168
+ total_duration_ms=sum(r.duration_ms for r in results if not isinstance(r, Exception)),
169
+ failed_count=sum(
170
+ 1
171
+ for r in results
172
+ if not isinstance(r, Exception) and r.status == HookExecutionStatus.FAILED
173
+ ),
174
+ timeout_count=sum(
175
+ 1
176
+ for r in results
177
+ if not isinstance(r, Exception) and r.status == HookExecutionStatus.TIMEOUT
178
+ ),
179
+ )
180
+
181
+ async def _execute_dag(
182
+ self,
183
+ context: HookContext[T],
184
+ hooks: list[Hook],
185
+ config: HookPhaseConfig,
186
+ ) -> HookExecutionResult:
187
+ """Execute hooks with dependency resolution."""
188
+ # For now, fall back to sequential execution
189
+ # Full DAG implementation would go here
190
+ return await self._execute_sequential(context, hooks, config)
191
+
192
+ async def _execute_hook_with_timeout(
193
+ self,
194
+ hook: Hook,
195
+ context: HookContext[T],
196
+ config: HookPhaseConfig,
197
+ ) -> HookExecutionEvent:
198
+ """Execute single hook with timeout enforcement."""
199
+ start = datetime.now(UTC)
200
+ circuit_breaker = self.circuit_breakers.get(hook.id)
201
+
202
+ if circuit_breaker and circuit_breaker.is_open:
203
+ return HookExecutionEvent(
204
+ execution_id=context.execution_id,
205
+ hook_id=hook.id,
206
+ phase=_extract_phase_value(config.phase),
207
+ status=HookExecutionStatus.SKIPPED,
208
+ reason="Circuit breaker open",
209
+ duration_ms=0,
210
+ )
211
+
212
+ try:
213
+ result: HookResult = await asyncio.wait_for(
214
+ hook.execute(context),
215
+ timeout=config.timeout_per_hook_ms / 1000,
216
+ )
217
+ duration = (datetime.now(UTC) - start).total_seconds() * 1000
218
+
219
+ event = HookExecutionEvent(
220
+ execution_id=context.execution_id,
221
+ hook_id=hook.id,
222
+ phase=_extract_phase_value(config.phase),
223
+ status=HookExecutionStatus.COMPLETED,
224
+ duration_ms=int(duration),
225
+ rows_affected=result.rows_affected,
226
+ stats=result.stats,
227
+ )
228
+
229
+ if circuit_breaker:
230
+ circuit_breaker.record_success()
231
+
232
+ self.tracer.record_execution(event)
233
+ return event
234
+
235
+ except TimeoutError:
236
+ if circuit_breaker:
237
+ circuit_breaker.record_failure()
238
+ return HookExecutionEvent(
239
+ execution_id=context.execution_id,
240
+ hook_id=hook.id,
241
+ phase=_extract_phase_value(config.phase),
242
+ status=HookExecutionStatus.TIMEOUT,
243
+ reason=f"Exceeded {config.timeout_per_hook_ms}ms timeout",
244
+ duration_ms=config.timeout_per_hook_ms,
245
+ )
246
+ except Exception as e:
247
+ if circuit_breaker:
248
+ circuit_breaker.record_failure()
249
+ return HookExecutionEvent(
250
+ execution_id=context.execution_id,
251
+ hook_id=hook.id,
252
+ phase=_extract_phase_value(config.phase),
253
+ status=HookExecutionStatus.FAILED,
254
+ error=str(e),
255
+ duration_ms=int((datetime.now(UTC) - start).total_seconds() * 1000),
256
+ )
257
+
258
+ async def _retry_hook(
259
+ self,
260
+ hook: Hook,
261
+ context: HookContext[T],
262
+ config: HookPhaseConfig,
263
+ ) -> HookExecutionEvent:
264
+ """Retry hook with exponential backoff."""
265
+ if not config.retry_config:
266
+ return await self._execute_hook_with_timeout(hook, context, config)
267
+
268
+ retry_config = config.retry_config
269
+ last_error = None
270
+
271
+ for attempt in range(retry_config.max_attempts):
272
+ try:
273
+ return await self._execute_hook_with_timeout(hook, context, config)
274
+ except Exception as e:
275
+ last_error = e
276
+ if attempt < retry_config.max_attempts - 1:
277
+ delay_ms = min(
278
+ retry_config.initial_delay_ms * (retry_config.backoff_multiplier**attempt),
279
+ retry_config.max_delay_ms,
280
+ )
281
+ logger.warning(
282
+ f"Hook {hook.name} failed, retrying in {delay_ms}ms "
283
+ f"(attempt {attempt + 1}/{retry_config.max_attempts})"
284
+ )
285
+ await asyncio.sleep(delay_ms / 1000)
286
+
287
+ # All retries failed
288
+ return HookExecutionEvent(
289
+ execution_id=context.execution_id,
290
+ hook_id=hook.id,
291
+ phase=_extract_phase_value(config.phase),
292
+ status=HookExecutionStatus.FAILED,
293
+ error=f"Failed after {retry_config.max_attempts} attempts: {last_error}",
294
+ duration_ms=0,
295
+ )