alma-memory 0.5.0__py3-none-any.whl → 0.7.0__py3-none-any.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.
- alma/__init__.py +296 -194
- alma/compression/__init__.py +33 -0
- alma/compression/pipeline.py +980 -0
- alma/confidence/__init__.py +47 -47
- alma/confidence/engine.py +540 -540
- alma/confidence/types.py +351 -351
- alma/config/loader.py +157 -157
- alma/consolidation/__init__.py +23 -23
- alma/consolidation/engine.py +678 -678
- alma/consolidation/prompts.py +84 -84
- alma/core.py +1189 -322
- alma/domains/__init__.py +30 -30
- alma/domains/factory.py +359 -359
- alma/domains/schemas.py +448 -448
- alma/domains/types.py +272 -272
- alma/events/__init__.py +75 -75
- alma/events/emitter.py +285 -284
- alma/events/storage_mixin.py +246 -246
- alma/events/types.py +126 -126
- alma/events/webhook.py +425 -425
- alma/exceptions.py +49 -49
- alma/extraction/__init__.py +31 -31
- alma/extraction/auto_learner.py +265 -264
- alma/extraction/extractor.py +420 -420
- alma/graph/__init__.py +106 -81
- alma/graph/backends/__init__.py +32 -18
- alma/graph/backends/kuzu.py +624 -0
- alma/graph/backends/memgraph.py +432 -0
- alma/graph/backends/memory.py +236 -236
- alma/graph/backends/neo4j.py +417 -417
- alma/graph/base.py +159 -159
- alma/graph/extraction.py +198 -198
- alma/graph/store.py +860 -860
- alma/harness/__init__.py +35 -35
- alma/harness/base.py +386 -386
- alma/harness/domains.py +705 -705
- alma/initializer/__init__.py +37 -37
- alma/initializer/initializer.py +418 -418
- alma/initializer/types.py +250 -250
- alma/integration/__init__.py +62 -62
- alma/integration/claude_agents.py +444 -432
- alma/integration/helena.py +423 -423
- alma/integration/victor.py +471 -471
- alma/learning/__init__.py +101 -86
- alma/learning/decay.py +878 -0
- alma/learning/forgetting.py +1446 -1446
- alma/learning/heuristic_extractor.py +390 -390
- alma/learning/protocols.py +374 -374
- alma/learning/validation.py +346 -346
- alma/mcp/__init__.py +123 -45
- alma/mcp/__main__.py +156 -156
- alma/mcp/resources.py +122 -122
- alma/mcp/server.py +955 -591
- alma/mcp/tools.py +3254 -511
- alma/observability/__init__.py +91 -0
- alma/observability/config.py +302 -0
- alma/observability/guidelines.py +170 -0
- alma/observability/logging.py +424 -0
- alma/observability/metrics.py +583 -0
- alma/observability/tracing.py +440 -0
- alma/progress/__init__.py +21 -21
- alma/progress/tracker.py +607 -607
- alma/progress/types.py +250 -250
- alma/retrieval/__init__.py +134 -53
- alma/retrieval/budget.py +525 -0
- alma/retrieval/cache.py +1304 -1061
- alma/retrieval/embeddings.py +202 -202
- alma/retrieval/engine.py +850 -366
- alma/retrieval/modes.py +365 -0
- alma/retrieval/progressive.py +560 -0
- alma/retrieval/scoring.py +344 -344
- alma/retrieval/trust_scoring.py +637 -0
- alma/retrieval/verification.py +797 -0
- alma/session/__init__.py +19 -19
- alma/session/manager.py +442 -399
- alma/session/types.py +288 -288
- alma/storage/__init__.py +101 -61
- alma/storage/archive.py +233 -0
- alma/storage/azure_cosmos.py +1259 -1048
- alma/storage/base.py +1083 -525
- alma/storage/chroma.py +1443 -1443
- alma/storage/constants.py +103 -0
- alma/storage/file_based.py +614 -619
- alma/storage/migrations/__init__.py +21 -0
- alma/storage/migrations/base.py +321 -0
- alma/storage/migrations/runner.py +323 -0
- alma/storage/migrations/version_stores.py +337 -0
- alma/storage/migrations/versions/__init__.py +11 -0
- alma/storage/migrations/versions/v1_0_0.py +373 -0
- alma/storage/migrations/versions/v1_1_0_workflow_context.py +551 -0
- alma/storage/pinecone.py +1080 -1080
- alma/storage/postgresql.py +1948 -1452
- alma/storage/qdrant.py +1306 -1306
- alma/storage/sqlite_local.py +3041 -1358
- alma/testing/__init__.py +46 -0
- alma/testing/factories.py +301 -0
- alma/testing/mocks.py +389 -0
- alma/types.py +292 -264
- alma/utils/__init__.py +19 -0
- alma/utils/tokenizer.py +521 -0
- alma/workflow/__init__.py +83 -0
- alma/workflow/artifacts.py +170 -0
- alma/workflow/checkpoint.py +311 -0
- alma/workflow/context.py +228 -0
- alma/workflow/outcomes.py +189 -0
- alma/workflow/reducers.py +393 -0
- {alma_memory-0.5.0.dist-info → alma_memory-0.7.0.dist-info}/METADATA +244 -72
- alma_memory-0.7.0.dist-info/RECORD +112 -0
- alma_memory-0.5.0.dist-info/RECORD +0 -76
- {alma_memory-0.5.0.dist-info → alma_memory-0.7.0.dist-info}/WHEEL +0 -0
- {alma_memory-0.5.0.dist-info → alma_memory-0.7.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ALMA Schema Migration Framework.
|
|
3
|
+
|
|
4
|
+
Provides version tracking and migration capabilities for storage backends.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from alma.storage.migrations.base import (
|
|
8
|
+
Migration,
|
|
9
|
+
MigrationError,
|
|
10
|
+
MigrationRegistry,
|
|
11
|
+
SchemaVersion,
|
|
12
|
+
)
|
|
13
|
+
from alma.storage.migrations.runner import MigrationRunner
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"Migration",
|
|
17
|
+
"MigrationError",
|
|
18
|
+
"MigrationRegistry",
|
|
19
|
+
"MigrationRunner",
|
|
20
|
+
"SchemaVersion",
|
|
21
|
+
]
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ALMA Migration Framework - Base Classes.
|
|
3
|
+
|
|
4
|
+
Provides abstract migration classes and version tracking utilities.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from typing import Any, Callable, Dict, List, Optional, Type
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MigrationError(Exception):
|
|
17
|
+
"""Exception raised when a migration fails."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
message: str,
|
|
22
|
+
version: Optional[str] = None,
|
|
23
|
+
cause: Optional[Exception] = None,
|
|
24
|
+
):
|
|
25
|
+
self.version = version
|
|
26
|
+
self.cause = cause
|
|
27
|
+
super().__init__(message)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class SchemaVersion:
|
|
32
|
+
"""
|
|
33
|
+
Represents a schema version record.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
version: Semantic version string (e.g., "1.0.0")
|
|
37
|
+
applied_at: When the migration was applied
|
|
38
|
+
description: Human-readable description of changes
|
|
39
|
+
checksum: Optional hash for integrity verification
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
version: str
|
|
43
|
+
applied_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
44
|
+
description: str = ""
|
|
45
|
+
checksum: Optional[str] = None
|
|
46
|
+
|
|
47
|
+
def __lt__(self, other: "SchemaVersion") -> bool:
|
|
48
|
+
"""Compare versions for sorting."""
|
|
49
|
+
return self._parse_version(self.version) < self._parse_version(other.version)
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def _parse_version(version: str) -> tuple:
|
|
53
|
+
"""Parse version string into comparable tuple."""
|
|
54
|
+
parts = version.split(".")
|
|
55
|
+
result = []
|
|
56
|
+
for part in parts:
|
|
57
|
+
try:
|
|
58
|
+
result.append(int(part))
|
|
59
|
+
except ValueError:
|
|
60
|
+
result.append(part)
|
|
61
|
+
return tuple(result)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Migration(ABC):
|
|
65
|
+
"""
|
|
66
|
+
Abstract base class for schema migrations.
|
|
67
|
+
|
|
68
|
+
Subclasses must implement upgrade() and optionally downgrade().
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
class AddTagsColumn(Migration):
|
|
72
|
+
version = "1.1.0"
|
|
73
|
+
description = "Add tags column to heuristics table"
|
|
74
|
+
|
|
75
|
+
def upgrade(self, connection):
|
|
76
|
+
connection.execute(
|
|
77
|
+
"ALTER TABLE heuristics ADD COLUMN tags TEXT"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def downgrade(self, connection):
|
|
81
|
+
connection.execute(
|
|
82
|
+
"ALTER TABLE heuristics DROP COLUMN tags"
|
|
83
|
+
)
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
# These must be set by subclasses
|
|
87
|
+
version: str = ""
|
|
88
|
+
description: str = ""
|
|
89
|
+
# Optional: previous version this migration depends on
|
|
90
|
+
depends_on: Optional[str] = None
|
|
91
|
+
|
|
92
|
+
@abstractmethod
|
|
93
|
+
def upgrade(self, connection: Any) -> None:
|
|
94
|
+
"""
|
|
95
|
+
Apply the migration.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
connection: Database connection or storage instance
|
|
99
|
+
"""
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
def downgrade(self, connection: Any) -> None:
|
|
103
|
+
"""
|
|
104
|
+
Revert the migration (optional).
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
connection: Database connection or storage instance
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
NotImplementedError: If downgrade is not supported
|
|
111
|
+
"""
|
|
112
|
+
raise NotImplementedError(
|
|
113
|
+
f"Downgrade not implemented for migration {self.version}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def pre_check(self, connection: Any) -> bool:
|
|
117
|
+
"""
|
|
118
|
+
Optional pre-migration check.
|
|
119
|
+
|
|
120
|
+
Override to verify prerequisites before migration.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
connection: Database connection
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
True if migration can proceed, False otherwise
|
|
127
|
+
"""
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
def post_check(self, connection: Any) -> bool:
|
|
131
|
+
"""
|
|
132
|
+
Optional post-migration verification.
|
|
133
|
+
|
|
134
|
+
Override to verify migration was successful.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
connection: Database connection
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
True if migration was successful
|
|
141
|
+
"""
|
|
142
|
+
return True
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class MigrationRegistry:
|
|
146
|
+
"""
|
|
147
|
+
Registry for available migrations.
|
|
148
|
+
|
|
149
|
+
Manages migration discovery, ordering, and execution planning.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
def __init__(self) -> None:
|
|
153
|
+
self._migrations: Dict[str, Type[Migration]] = {}
|
|
154
|
+
self._backend_migrations: Dict[str, Dict[str, Type[Migration]]] = {}
|
|
155
|
+
|
|
156
|
+
def register(
|
|
157
|
+
self, migration_class: Type[Migration], backend: Optional[str] = None
|
|
158
|
+
) -> Type[Migration]:
|
|
159
|
+
"""
|
|
160
|
+
Register a migration class.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
migration_class: The migration class to register
|
|
164
|
+
backend: Optional backend name (e.g., "sqlite", "postgresql")
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
The migration class (for use as decorator)
|
|
168
|
+
"""
|
|
169
|
+
version = migration_class.version
|
|
170
|
+
if not version:
|
|
171
|
+
raise ValueError(
|
|
172
|
+
f"Migration {migration_class.__name__} must have a version"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if backend:
|
|
176
|
+
if backend not in self._backend_migrations:
|
|
177
|
+
self._backend_migrations[backend] = {}
|
|
178
|
+
self._backend_migrations[backend][version] = migration_class
|
|
179
|
+
logger.debug(f"Registered migration {version} for backend {backend}")
|
|
180
|
+
else:
|
|
181
|
+
self._migrations[version] = migration_class
|
|
182
|
+
logger.debug(f"Registered global migration {version}")
|
|
183
|
+
|
|
184
|
+
return migration_class
|
|
185
|
+
|
|
186
|
+
def get_migration(
|
|
187
|
+
self, version: str, backend: Optional[str] = None
|
|
188
|
+
) -> Optional[Type[Migration]]:
|
|
189
|
+
"""
|
|
190
|
+
Get a migration class by version.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
version: Version string to look up
|
|
194
|
+
backend: Optional backend name
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Migration class or None if not found
|
|
198
|
+
"""
|
|
199
|
+
if backend and backend in self._backend_migrations:
|
|
200
|
+
migration = self._backend_migrations[backend].get(version)
|
|
201
|
+
if migration:
|
|
202
|
+
return migration
|
|
203
|
+
return self._migrations.get(version)
|
|
204
|
+
|
|
205
|
+
def get_all_migrations(
|
|
206
|
+
self, backend: Optional[str] = None
|
|
207
|
+
) -> List[Type[Migration]]:
|
|
208
|
+
"""
|
|
209
|
+
Get all migrations in version order.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
backend: Optional backend name to filter migrations
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
List of migration classes sorted by version
|
|
216
|
+
"""
|
|
217
|
+
migrations = dict(self._migrations)
|
|
218
|
+
if backend and backend in self._backend_migrations:
|
|
219
|
+
migrations.update(self._backend_migrations[backend])
|
|
220
|
+
|
|
221
|
+
return [
|
|
222
|
+
cls
|
|
223
|
+
for _, cls in sorted(
|
|
224
|
+
migrations.items(),
|
|
225
|
+
key=lambda x: SchemaVersion._parse_version(x[0]),
|
|
226
|
+
)
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
def get_pending_migrations(
|
|
230
|
+
self,
|
|
231
|
+
current_version: Optional[str],
|
|
232
|
+
backend: Optional[str] = None,
|
|
233
|
+
) -> List[Type[Migration]]:
|
|
234
|
+
"""
|
|
235
|
+
Get migrations that need to be applied.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
current_version: Current schema version (None if fresh install)
|
|
239
|
+
backend: Optional backend name
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
List of migration classes that need to be applied
|
|
243
|
+
"""
|
|
244
|
+
all_migrations = self.get_all_migrations(backend)
|
|
245
|
+
|
|
246
|
+
if current_version is None:
|
|
247
|
+
return all_migrations
|
|
248
|
+
|
|
249
|
+
current = SchemaVersion._parse_version(current_version)
|
|
250
|
+
return [
|
|
251
|
+
m
|
|
252
|
+
for m in all_migrations
|
|
253
|
+
if SchemaVersion._parse_version(m.version) > current
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
def get_rollback_migrations(
|
|
257
|
+
self,
|
|
258
|
+
current_version: str,
|
|
259
|
+
target_version: str,
|
|
260
|
+
backend: Optional[str] = None,
|
|
261
|
+
) -> List[Type[Migration]]:
|
|
262
|
+
"""
|
|
263
|
+
Get migrations that need to be rolled back.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
current_version: Current schema version
|
|
267
|
+
target_version: Target version to roll back to
|
|
268
|
+
backend: Optional backend name
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
List of migration classes to roll back (in reverse order)
|
|
272
|
+
"""
|
|
273
|
+
all_migrations = self.get_all_migrations(backend)
|
|
274
|
+
|
|
275
|
+
current = SchemaVersion._parse_version(current_version)
|
|
276
|
+
target = SchemaVersion._parse_version(target_version)
|
|
277
|
+
|
|
278
|
+
rollback = [
|
|
279
|
+
m
|
|
280
|
+
for m in all_migrations
|
|
281
|
+
if target < SchemaVersion._parse_version(m.version) <= current
|
|
282
|
+
]
|
|
283
|
+
|
|
284
|
+
# Return in reverse order for rollback
|
|
285
|
+
return list(reversed(rollback))
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# Global registry instance
|
|
289
|
+
_global_registry = MigrationRegistry()
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def register_migration(
|
|
293
|
+
backend: Optional[str] = None,
|
|
294
|
+
) -> Callable[[Type[Migration]], Type[Migration]]:
|
|
295
|
+
"""
|
|
296
|
+
Decorator to register a migration class.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
backend: Optional backend name
|
|
300
|
+
|
|
301
|
+
Example:
|
|
302
|
+
@register_migration()
|
|
303
|
+
class MyMigration(Migration):
|
|
304
|
+
version = "1.0.0"
|
|
305
|
+
...
|
|
306
|
+
|
|
307
|
+
@register_migration(backend="postgresql")
|
|
308
|
+
class PostgresSpecificMigration(Migration):
|
|
309
|
+
version = "1.0.1"
|
|
310
|
+
...
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
def decorator(cls: Type[Migration]) -> Type[Migration]:
|
|
314
|
+
return _global_registry.register(cls, backend)
|
|
315
|
+
|
|
316
|
+
return decorator
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def get_registry() -> MigrationRegistry:
|
|
320
|
+
"""Get the global migration registry."""
|
|
321
|
+
return _global_registry
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ALMA Migration Framework - Migration Runner.
|
|
3
|
+
|
|
4
|
+
Executes migrations and manages schema version tracking.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
import logging
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from typing import Any, Callable, Dict, List, Optional, Protocol, Type
|
|
11
|
+
|
|
12
|
+
from alma.storage.migrations.base import (
|
|
13
|
+
Migration,
|
|
14
|
+
MigrationError,
|
|
15
|
+
MigrationRegistry,
|
|
16
|
+
SchemaVersion,
|
|
17
|
+
get_registry,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class VersionStore(Protocol):
|
|
24
|
+
"""Protocol for schema version storage."""
|
|
25
|
+
|
|
26
|
+
def get_current_version(self) -> Optional[str]:
|
|
27
|
+
"""Get the current schema version."""
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
def get_version_history(self) -> List[SchemaVersion]:
|
|
31
|
+
"""Get all applied versions in order."""
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
def record_version(self, version: SchemaVersion) -> None:
|
|
35
|
+
"""Record a new version."""
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
def remove_version(self, version: str) -> None:
|
|
39
|
+
"""Remove a version record (for rollback)."""
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MigrationRunner:
|
|
44
|
+
"""
|
|
45
|
+
Executes schema migrations and tracks versions.
|
|
46
|
+
|
|
47
|
+
Handles:
|
|
48
|
+
- Forward migrations (upgrades)
|
|
49
|
+
- Backward migrations (rollbacks)
|
|
50
|
+
- Version tracking
|
|
51
|
+
- Pre/post migration checks
|
|
52
|
+
- Dry run mode
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
# Current schema version for fresh installations
|
|
56
|
+
CURRENT_SCHEMA_VERSION = "1.0.0"
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
version_store: VersionStore,
|
|
61
|
+
registry: Optional[MigrationRegistry] = None,
|
|
62
|
+
backend: Optional[str] = None,
|
|
63
|
+
):
|
|
64
|
+
"""
|
|
65
|
+
Initialize migration runner.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
version_store: Storage for version tracking
|
|
69
|
+
registry: Migration registry (uses global if not provided)
|
|
70
|
+
backend: Backend name for filtering migrations
|
|
71
|
+
"""
|
|
72
|
+
self.version_store = version_store
|
|
73
|
+
self.registry = registry or get_registry()
|
|
74
|
+
self.backend = backend
|
|
75
|
+
self._hooks: Dict[str, List[Callable]] = {
|
|
76
|
+
"pre_migrate": [],
|
|
77
|
+
"post_migrate": [],
|
|
78
|
+
"pre_rollback": [],
|
|
79
|
+
"post_rollback": [],
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
def add_hook(self, event: str, callback: Callable) -> None:
|
|
83
|
+
"""
|
|
84
|
+
Add a hook callback for migration events.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
event: Event name (pre_migrate, post_migrate, etc.)
|
|
88
|
+
callback: Function to call
|
|
89
|
+
"""
|
|
90
|
+
if event in self._hooks:
|
|
91
|
+
self._hooks[event].append(callback)
|
|
92
|
+
|
|
93
|
+
def _run_hooks(self, event: str, *args: Any, **kwargs: Any) -> None:
|
|
94
|
+
"""Run all hooks for an event."""
|
|
95
|
+
for callback in self._hooks.get(event, []):
|
|
96
|
+
try:
|
|
97
|
+
callback(*args, **kwargs)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.warning(f"Hook {callback.__name__} failed: {e}")
|
|
100
|
+
|
|
101
|
+
def get_current_version(self) -> Optional[str]:
|
|
102
|
+
"""Get the current schema version."""
|
|
103
|
+
return self.version_store.get_current_version()
|
|
104
|
+
|
|
105
|
+
def get_pending_migrations(self) -> List[Type[Migration]]:
|
|
106
|
+
"""Get list of migrations that need to be applied."""
|
|
107
|
+
current = self.get_current_version()
|
|
108
|
+
return self.registry.get_pending_migrations(current, self.backend)
|
|
109
|
+
|
|
110
|
+
def needs_migration(self) -> bool:
|
|
111
|
+
"""Check if there are pending migrations."""
|
|
112
|
+
return len(self.get_pending_migrations()) > 0
|
|
113
|
+
|
|
114
|
+
def migrate(
|
|
115
|
+
self,
|
|
116
|
+
connection: Any,
|
|
117
|
+
target_version: Optional[str] = None,
|
|
118
|
+
dry_run: bool = False,
|
|
119
|
+
) -> List[str]:
|
|
120
|
+
"""
|
|
121
|
+
Apply pending migrations.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
connection: Database connection or storage instance
|
|
125
|
+
target_version: Optional target version (applies all if not specified)
|
|
126
|
+
dry_run: If True, show what would be done without making changes
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
List of applied migration versions
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
MigrationError: If a migration fails
|
|
133
|
+
"""
|
|
134
|
+
current = self.get_current_version()
|
|
135
|
+
pending = self.registry.get_pending_migrations(current, self.backend)
|
|
136
|
+
|
|
137
|
+
if target_version:
|
|
138
|
+
target_tuple = SchemaVersion._parse_version(target_version)
|
|
139
|
+
pending = [
|
|
140
|
+
m
|
|
141
|
+
for m in pending
|
|
142
|
+
if SchemaVersion._parse_version(m.version) <= target_tuple
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
if not pending:
|
|
146
|
+
logger.info("No pending migrations")
|
|
147
|
+
return []
|
|
148
|
+
|
|
149
|
+
applied = []
|
|
150
|
+
logger.info(f"Found {len(pending)} pending migrations")
|
|
151
|
+
|
|
152
|
+
for migration_class in pending:
|
|
153
|
+
version = migration_class.version
|
|
154
|
+
migration = migration_class()
|
|
155
|
+
|
|
156
|
+
if dry_run:
|
|
157
|
+
logger.info(
|
|
158
|
+
f"[DRY RUN] Would apply migration {version}: {migration.description}"
|
|
159
|
+
)
|
|
160
|
+
applied.append(version)
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
logger.info(f"Applying migration {version}: {migration.description}")
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
# Run pre-check
|
|
167
|
+
if not migration.pre_check(connection):
|
|
168
|
+
raise MigrationError(
|
|
169
|
+
f"Pre-check failed for migration {version}",
|
|
170
|
+
version=version,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Run pre-migrate hooks
|
|
174
|
+
self._run_hooks("pre_migrate", migration, connection)
|
|
175
|
+
|
|
176
|
+
# Apply migration
|
|
177
|
+
migration.upgrade(connection)
|
|
178
|
+
|
|
179
|
+
# Run post-check
|
|
180
|
+
if not migration.post_check(connection):
|
|
181
|
+
raise MigrationError(
|
|
182
|
+
f"Post-check failed for migration {version}",
|
|
183
|
+
version=version,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Record version
|
|
187
|
+
schema_version = SchemaVersion(
|
|
188
|
+
version=version,
|
|
189
|
+
applied_at=datetime.now(timezone.utc),
|
|
190
|
+
description=migration.description,
|
|
191
|
+
checksum=self._compute_checksum(migration_class),
|
|
192
|
+
)
|
|
193
|
+
self.version_store.record_version(schema_version)
|
|
194
|
+
|
|
195
|
+
# Run post-migrate hooks
|
|
196
|
+
self._run_hooks("post_migrate", migration, connection)
|
|
197
|
+
|
|
198
|
+
applied.append(version)
|
|
199
|
+
logger.info(f"Successfully applied migration {version}")
|
|
200
|
+
|
|
201
|
+
except MigrationError:
|
|
202
|
+
raise
|
|
203
|
+
except Exception as e:
|
|
204
|
+
raise MigrationError(
|
|
205
|
+
f"Migration {version} failed: {str(e)}",
|
|
206
|
+
version=version,
|
|
207
|
+
cause=e,
|
|
208
|
+
) from e
|
|
209
|
+
|
|
210
|
+
return applied
|
|
211
|
+
|
|
212
|
+
def rollback(
|
|
213
|
+
self,
|
|
214
|
+
connection: Any,
|
|
215
|
+
target_version: str,
|
|
216
|
+
dry_run: bool = False,
|
|
217
|
+
) -> List[str]:
|
|
218
|
+
"""
|
|
219
|
+
Roll back to a target version.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
connection: Database connection or storage instance
|
|
223
|
+
target_version: Version to roll back to
|
|
224
|
+
dry_run: If True, show what would be done without making changes
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
List of rolled back migration versions
|
|
228
|
+
|
|
229
|
+
Raises:
|
|
230
|
+
MigrationError: If a rollback fails
|
|
231
|
+
"""
|
|
232
|
+
current = self.get_current_version()
|
|
233
|
+
if current is None:
|
|
234
|
+
logger.info("No migrations to roll back")
|
|
235
|
+
return []
|
|
236
|
+
|
|
237
|
+
rollback_migrations = self.registry.get_rollback_migrations(
|
|
238
|
+
current, target_version, self.backend
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if not rollback_migrations:
|
|
242
|
+
logger.info("No migrations to roll back")
|
|
243
|
+
return []
|
|
244
|
+
|
|
245
|
+
rolled_back = []
|
|
246
|
+
logger.info(f"Rolling back {len(rollback_migrations)} migrations")
|
|
247
|
+
|
|
248
|
+
for migration_class in rollback_migrations:
|
|
249
|
+
version = migration_class.version
|
|
250
|
+
migration = migration_class()
|
|
251
|
+
|
|
252
|
+
if dry_run:
|
|
253
|
+
logger.info(f"[DRY RUN] Would roll back migration {version}")
|
|
254
|
+
rolled_back.append(version)
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
logger.info(f"Rolling back migration {version}")
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
# Run pre-rollback hooks
|
|
261
|
+
self._run_hooks("pre_rollback", migration, connection)
|
|
262
|
+
|
|
263
|
+
# Apply downgrade
|
|
264
|
+
migration.downgrade(connection)
|
|
265
|
+
|
|
266
|
+
# Remove version record
|
|
267
|
+
self.version_store.remove_version(version)
|
|
268
|
+
|
|
269
|
+
# Run post-rollback hooks
|
|
270
|
+
self._run_hooks("post_rollback", migration, connection)
|
|
271
|
+
|
|
272
|
+
rolled_back.append(version)
|
|
273
|
+
logger.info(f"Successfully rolled back migration {version}")
|
|
274
|
+
|
|
275
|
+
except NotImplementedError as e:
|
|
276
|
+
raise MigrationError(
|
|
277
|
+
f"Migration {version} does not support rollback",
|
|
278
|
+
version=version,
|
|
279
|
+
) from e
|
|
280
|
+
except Exception as e:
|
|
281
|
+
raise MigrationError(
|
|
282
|
+
f"Rollback of {version} failed: {str(e)}",
|
|
283
|
+
version=version,
|
|
284
|
+
cause=e,
|
|
285
|
+
) from e
|
|
286
|
+
|
|
287
|
+
return rolled_back
|
|
288
|
+
|
|
289
|
+
def get_status(self) -> Dict[str, Any]:
|
|
290
|
+
"""
|
|
291
|
+
Get migration status information.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Dict with current version, pending migrations, and history
|
|
295
|
+
"""
|
|
296
|
+
current = self.get_current_version()
|
|
297
|
+
pending = self.get_pending_migrations()
|
|
298
|
+
history = self.version_store.get_version_history()
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
"current_version": current,
|
|
302
|
+
"target_version": self.CURRENT_SCHEMA_VERSION,
|
|
303
|
+
"pending_count": len(pending),
|
|
304
|
+
"pending_versions": [m.version for m in pending],
|
|
305
|
+
"applied_count": len(history),
|
|
306
|
+
"history": [
|
|
307
|
+
{
|
|
308
|
+
"version": v.version,
|
|
309
|
+
"applied_at": v.applied_at.isoformat(),
|
|
310
|
+
"description": v.description,
|
|
311
|
+
}
|
|
312
|
+
for v in history
|
|
313
|
+
],
|
|
314
|
+
"needs_migration": len(pending) > 0,
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
@staticmethod
|
|
318
|
+
def _compute_checksum(migration_class: Type[Migration]) -> str:
|
|
319
|
+
"""Compute checksum of migration for integrity tracking."""
|
|
320
|
+
import inspect
|
|
321
|
+
|
|
322
|
+
source = inspect.getsource(migration_class)
|
|
323
|
+
return hashlib.sha256(source.encode()).hexdigest()[:16]
|