mcpbr 0.4.16__py3-none-any.whl → 0.5.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.
- mcpbr/config_migration.py +470 -0
- mcpbr/config_wizard.py +647 -0
- mcpbr/dashboard.py +619 -0
- mcpbr/dataset_streaming.py +491 -0
- mcpbr/docker_cache.py +539 -0
- mcpbr/docker_prewarm.py +369 -0
- mcpbr/dry_run.py +532 -0
- mcpbr/formatting.py +444 -0
- mcpbr/harness.py +38 -4
- mcpbr/resource_limits.py +487 -0
- mcpbr/result_streaming.py +519 -0
- mcpbr/task_batching.py +403 -0
- mcpbr/task_scheduler.py +468 -0
- {mcpbr-0.4.16.dist-info → mcpbr-0.5.0.dist-info}/METADATA +1 -1
- {mcpbr-0.4.16.dist-info → mcpbr-0.5.0.dist-info}/RECORD +25 -13
- {mcpbr-0.4.16.data → mcpbr-0.5.0.data}/data/mcpbr/data/templates/brave-search.yaml +0 -0
- {mcpbr-0.4.16.data → mcpbr-0.5.0.data}/data/mcpbr/data/templates/filesystem.yaml +0 -0
- {mcpbr-0.4.16.data → mcpbr-0.5.0.data}/data/mcpbr/data/templates/github.yaml +0 -0
- {mcpbr-0.4.16.data → mcpbr-0.5.0.data}/data/mcpbr/data/templates/google-maps.yaml +0 -0
- {mcpbr-0.4.16.data → mcpbr-0.5.0.data}/data/mcpbr/data/templates/postgres.yaml +0 -0
- {mcpbr-0.4.16.data → mcpbr-0.5.0.data}/data/mcpbr/data/templates/slack.yaml +0 -0
- {mcpbr-0.4.16.data → mcpbr-0.5.0.data}/data/mcpbr/data/templates/sqlite.yaml +0 -0
- {mcpbr-0.4.16.dist-info → mcpbr-0.5.0.dist-info}/WHEEL +0 -0
- {mcpbr-0.4.16.dist-info → mcpbr-0.5.0.dist-info}/entry_points.txt +0 -0
- {mcpbr-0.4.16.dist-info → mcpbr-0.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
"""Configuration migration tool for mcpbr.
|
|
2
|
+
|
|
3
|
+
Detects old config formats and migrates them to the current format.
|
|
4
|
+
Supports chained migrations (V1 -> V2 -> V3 -> V4_CURRENT), dry-run
|
|
5
|
+
preview, and automatic backup of originals.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import copy
|
|
9
|
+
import shutil
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import yaml
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.panel import Panel
|
|
19
|
+
from rich.table import Table
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ConfigVersion(Enum):
|
|
23
|
+
"""Configuration format versions.
|
|
24
|
+
|
|
25
|
+
Each version corresponds to a major release era of mcpbr.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
V1 = "v1" # Pre-0.3.0: api_key in config, "server" field
|
|
29
|
+
V2 = "v2" # 0.3.x: mcp_server, no benchmark field
|
|
30
|
+
V3 = "v3" # 0.4.x: benchmark field, sample_size, infrastructure
|
|
31
|
+
V4_CURRENT = "v4" # 0.5.0+: resource_limits, streaming config
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Migration:
|
|
36
|
+
"""A single migration step between two config versions.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
from_version: Source config version.
|
|
40
|
+
to_version: Target config version.
|
|
41
|
+
description: Human-readable description of what this migration does.
|
|
42
|
+
migrate: Callable that transforms a config dict from one version to the next.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from_version: ConfigVersion
|
|
46
|
+
to_version: ConfigVersion
|
|
47
|
+
description: str
|
|
48
|
+
migrate: Callable[[dict[str, Any]], dict[str, Any]]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class MigrationResult:
|
|
53
|
+
"""Result of a config migration operation.
|
|
54
|
+
|
|
55
|
+
Attributes:
|
|
56
|
+
original_version: Detected version of the original config.
|
|
57
|
+
target_version: Target version after migration.
|
|
58
|
+
migrations_applied: List of migration descriptions that were applied.
|
|
59
|
+
warnings: List of warning messages (e.g., removed fields).
|
|
60
|
+
config: The migrated config dictionary.
|
|
61
|
+
changes_preview: List of human-readable change descriptions for dry-run.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
original_version: ConfigVersion
|
|
65
|
+
target_version: ConfigVersion
|
|
66
|
+
migrations_applied: list[str] = field(default_factory=list)
|
|
67
|
+
warnings: list[str] = field(default_factory=list)
|
|
68
|
+
config: dict[str, Any] = field(default_factory=dict)
|
|
69
|
+
changes_preview: list[str] = field(default_factory=list)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class MigrationChain:
|
|
73
|
+
"""Manages a chain of config migrations from any old version to current.
|
|
74
|
+
|
|
75
|
+
Registers all known migration steps and provides methods to detect
|
|
76
|
+
config versions, find applicable migrations, and execute them.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(self) -> None:
|
|
80
|
+
"""Initialize the migration chain with all known migrations."""
|
|
81
|
+
self._migrations: list[Migration] = []
|
|
82
|
+
self._register_all_migrations()
|
|
83
|
+
|
|
84
|
+
def _register_all_migrations(self) -> None:
|
|
85
|
+
"""Register all known migration steps."""
|
|
86
|
+
self._migrations.append(
|
|
87
|
+
Migration(
|
|
88
|
+
from_version=ConfigVersion.V1,
|
|
89
|
+
to_version=ConfigVersion.V2,
|
|
90
|
+
description="V1 -> V2: Move api_key to env vars, rename 'server' to 'mcp_server'",
|
|
91
|
+
migrate=self._migrate_v1_to_v2,
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
self._migrations.append(
|
|
95
|
+
Migration(
|
|
96
|
+
from_version=ConfigVersion.V2,
|
|
97
|
+
to_version=ConfigVersion.V3,
|
|
98
|
+
description=(
|
|
99
|
+
"V2 -> V3: Add benchmark field, rename max_tasks to sample_size, "
|
|
100
|
+
"add infrastructure section"
|
|
101
|
+
),
|
|
102
|
+
migrate=self._migrate_v2_to_v3,
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
self._migrations.append(
|
|
106
|
+
Migration(
|
|
107
|
+
from_version=ConfigVersion.V3,
|
|
108
|
+
to_version=ConfigVersion.V4_CURRENT,
|
|
109
|
+
description=(
|
|
110
|
+
"V3 -> V4: Add resource_limits defaults, add streaming config section"
|
|
111
|
+
),
|
|
112
|
+
migrate=self._migrate_v3_to_v4,
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def detect_version(self, config: dict[str, Any]) -> ConfigVersion:
|
|
117
|
+
"""Detect the config version based on field presence/absence.
|
|
118
|
+
|
|
119
|
+
Detection heuristics (checked in order, most specific first):
|
|
120
|
+
- V1: Has "api_key" or "server" (not "mcp_server") top-level key.
|
|
121
|
+
- V4_CURRENT: Has "resource_limits" or "streaming" key, or
|
|
122
|
+
none of the older markers are present.
|
|
123
|
+
- V2: Has "mcp_server" but no "benchmark" or "infrastructure" key.
|
|
124
|
+
- V3: Has "mcp_server" and "benchmark" or "infrastructure" but
|
|
125
|
+
no "resource_limits" or "streaming" key.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
config: Parsed config dictionary.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Detected ConfigVersion.
|
|
132
|
+
"""
|
|
133
|
+
has_api_key = "api_key" in config
|
|
134
|
+
has_server = "server" in config and "mcp_server" not in config
|
|
135
|
+
has_mcp_server = "mcp_server" in config
|
|
136
|
+
has_benchmark = "benchmark" in config
|
|
137
|
+
has_infrastructure = "infrastructure" in config
|
|
138
|
+
has_resource_limits = "resource_limits" in config
|
|
139
|
+
has_streaming = "streaming" in config
|
|
140
|
+
|
|
141
|
+
# V1: legacy fields present
|
|
142
|
+
if has_api_key or has_server:
|
|
143
|
+
return ConfigVersion.V1
|
|
144
|
+
|
|
145
|
+
# V4: has current-version fields (check before V2/V3 to avoid false matches)
|
|
146
|
+
if has_resource_limits or has_streaming:
|
|
147
|
+
return ConfigVersion.V4_CURRENT
|
|
148
|
+
|
|
149
|
+
# V2: has mcp_server but no benchmark/infrastructure
|
|
150
|
+
if has_mcp_server and not has_benchmark and not has_infrastructure:
|
|
151
|
+
return ConfigVersion.V2
|
|
152
|
+
|
|
153
|
+
# V3: has mcp_server with benchmark or infrastructure but not V4 fields
|
|
154
|
+
if has_mcp_server and (has_benchmark or has_infrastructure):
|
|
155
|
+
return ConfigVersion.V3
|
|
156
|
+
|
|
157
|
+
# Default to current if no distinguishing markers found
|
|
158
|
+
return ConfigVersion.V4_CURRENT
|
|
159
|
+
|
|
160
|
+
def get_migrations(self, from_ver: ConfigVersion, to_ver: ConfigVersion) -> list[Migration]:
|
|
161
|
+
"""Get the ordered list of migrations needed to go from one version to another.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
from_ver: Starting config version.
|
|
165
|
+
to_ver: Target config version.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Ordered list of Migration objects to apply. Empty list if no
|
|
169
|
+
migration is needed or if from_ver >= to_ver.
|
|
170
|
+
"""
|
|
171
|
+
version_order = list(ConfigVersion)
|
|
172
|
+
from_idx = version_order.index(from_ver)
|
|
173
|
+
to_idx = version_order.index(to_ver)
|
|
174
|
+
|
|
175
|
+
if from_idx >= to_idx:
|
|
176
|
+
return []
|
|
177
|
+
|
|
178
|
+
result: list[Migration] = []
|
|
179
|
+
current = from_ver
|
|
180
|
+
for migration in self._migrations:
|
|
181
|
+
if (
|
|
182
|
+
migration.from_version == current
|
|
183
|
+
and version_order.index(migration.to_version) <= to_idx
|
|
184
|
+
):
|
|
185
|
+
result.append(migration)
|
|
186
|
+
current = migration.to_version
|
|
187
|
+
|
|
188
|
+
return result
|
|
189
|
+
|
|
190
|
+
def migrate(self, config: dict[str, Any], dry_run: bool = False) -> MigrationResult:
|
|
191
|
+
"""Migrate a config dict from its detected version to the current version.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
config: Parsed config dictionary to migrate.
|
|
195
|
+
dry_run: If True, compute changes but do not modify the config.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
MigrationResult with migration details and the (possibly modified) config.
|
|
199
|
+
"""
|
|
200
|
+
original_version = self.detect_version(config)
|
|
201
|
+
target_version = ConfigVersion.V4_CURRENT
|
|
202
|
+
|
|
203
|
+
result = MigrationResult(
|
|
204
|
+
original_version=original_version,
|
|
205
|
+
target_version=target_version,
|
|
206
|
+
config=copy.deepcopy(config),
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
if original_version == target_version:
|
|
210
|
+
result.changes_preview.append("Config is already at the current version (V4).")
|
|
211
|
+
return result
|
|
212
|
+
|
|
213
|
+
migrations = self.get_migrations(original_version, target_version)
|
|
214
|
+
if not migrations:
|
|
215
|
+
result.warnings.append(
|
|
216
|
+
f"No migration path found from {original_version.value} to {target_version.value}."
|
|
217
|
+
)
|
|
218
|
+
return result
|
|
219
|
+
|
|
220
|
+
working_config = copy.deepcopy(config)
|
|
221
|
+
|
|
222
|
+
for migration in migrations:
|
|
223
|
+
result.changes_preview.append(f"Apply: {migration.description}")
|
|
224
|
+
if not dry_run:
|
|
225
|
+
working_config = migration.migrate(working_config)
|
|
226
|
+
result.migrations_applied.append(migration.description)
|
|
227
|
+
|
|
228
|
+
if not dry_run:
|
|
229
|
+
result.config = working_config
|
|
230
|
+
else:
|
|
231
|
+
# For dry-run, keep original config but show what would change
|
|
232
|
+
result.config = copy.deepcopy(config)
|
|
233
|
+
|
|
234
|
+
return result
|
|
235
|
+
|
|
236
|
+
# -- Individual migration implementations --
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def _migrate_v1_to_v2(config: dict[str, Any]) -> dict[str, Any]:
|
|
240
|
+
"""Migrate V1 config to V2 format.
|
|
241
|
+
|
|
242
|
+
Changes:
|
|
243
|
+
- Remove "api_key" (moved to environment variables).
|
|
244
|
+
- Rename "server" to "mcp_server" and convert to new structure.
|
|
245
|
+
- Remove "dataset" if present (replaced by benchmark in V3).
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
config: V1 config dictionary.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
V2 config dictionary.
|
|
252
|
+
"""
|
|
253
|
+
result = copy.deepcopy(config)
|
|
254
|
+
|
|
255
|
+
# Remove api_key (should be in env vars now)
|
|
256
|
+
if "api_key" in result:
|
|
257
|
+
del result["api_key"]
|
|
258
|
+
|
|
259
|
+
# Rename "server" to "mcp_server" with structure conversion
|
|
260
|
+
if "server" in result:
|
|
261
|
+
server = result.pop("server")
|
|
262
|
+
if isinstance(server, dict):
|
|
263
|
+
# Convert old server format to mcp_server format
|
|
264
|
+
mcp_server: dict[str, Any] = {}
|
|
265
|
+
if "command" in server:
|
|
266
|
+
mcp_server["command"] = server["command"]
|
|
267
|
+
if "args" in server:
|
|
268
|
+
mcp_server["args"] = server["args"]
|
|
269
|
+
if "env" in server:
|
|
270
|
+
mcp_server["env"] = server["env"]
|
|
271
|
+
if "name" in server:
|
|
272
|
+
mcp_server["name"] = server["name"]
|
|
273
|
+
result["mcp_server"] = mcp_server
|
|
274
|
+
elif isinstance(server, str):
|
|
275
|
+
# Simple string server specification -> convert to command
|
|
276
|
+
result["mcp_server"] = {"command": server, "args": []}
|
|
277
|
+
|
|
278
|
+
return result
|
|
279
|
+
|
|
280
|
+
@staticmethod
|
|
281
|
+
def _migrate_v2_to_v3(config: dict[str, Any]) -> dict[str, Any]:
|
|
282
|
+
"""Migrate V2 config to V3 format.
|
|
283
|
+
|
|
284
|
+
Changes:
|
|
285
|
+
- Add "benchmark" field (default: "swe-bench-verified").
|
|
286
|
+
- Rename "max_tasks" to "sample_size".
|
|
287
|
+
- Add "infrastructure" section with default local mode.
|
|
288
|
+
- Convert "dataset" field to "benchmark" if present.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
config: V2 config dictionary.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
V3 config dictionary.
|
|
295
|
+
"""
|
|
296
|
+
result = copy.deepcopy(config)
|
|
297
|
+
|
|
298
|
+
# Map old dataset names to benchmark identifiers
|
|
299
|
+
dataset_to_benchmark = {
|
|
300
|
+
"SWE-bench/SWE-bench_Lite": "swe-bench-lite",
|
|
301
|
+
"SWE-bench/SWE-bench_Verified": "swe-bench-verified",
|
|
302
|
+
"SWE-bench/SWE-bench": "swe-bench-full",
|
|
303
|
+
"princeton-nlp/SWE-bench_Lite": "swe-bench-lite",
|
|
304
|
+
"princeton-nlp/SWE-bench_Verified": "swe-bench-verified",
|
|
305
|
+
"princeton-nlp/SWE-bench": "swe-bench-full",
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
# Convert dataset to benchmark if present
|
|
309
|
+
if "dataset" in result:
|
|
310
|
+
dataset_val = result.pop("dataset")
|
|
311
|
+
if dataset_val in dataset_to_benchmark:
|
|
312
|
+
result["benchmark"] = dataset_to_benchmark[dataset_val]
|
|
313
|
+
else:
|
|
314
|
+
result["benchmark"] = "swe-bench-verified"
|
|
315
|
+
|
|
316
|
+
# Add benchmark default if not present
|
|
317
|
+
if "benchmark" not in result:
|
|
318
|
+
result["benchmark"] = "swe-bench-verified"
|
|
319
|
+
|
|
320
|
+
# Rename max_tasks to sample_size
|
|
321
|
+
if "max_tasks" in result:
|
|
322
|
+
result["sample_size"] = result.pop("max_tasks")
|
|
323
|
+
|
|
324
|
+
# Add infrastructure section if not present
|
|
325
|
+
if "infrastructure" not in result:
|
|
326
|
+
result["infrastructure"] = {"mode": "local"}
|
|
327
|
+
|
|
328
|
+
return result
|
|
329
|
+
|
|
330
|
+
@staticmethod
|
|
331
|
+
def _migrate_v3_to_v4(config: dict[str, Any]) -> dict[str, Any]:
|
|
332
|
+
"""Migrate V3 config to V4 (current) format.
|
|
333
|
+
|
|
334
|
+
Changes:
|
|
335
|
+
- Add "resource_limits" section with defaults.
|
|
336
|
+
- Add "streaming" config section.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
config: V3 config dictionary.
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
V4 config dictionary.
|
|
343
|
+
"""
|
|
344
|
+
result = copy.deepcopy(config)
|
|
345
|
+
|
|
346
|
+
# Add resource_limits with sensible defaults
|
|
347
|
+
if "resource_limits" not in result:
|
|
348
|
+
result["resource_limits"] = {
|
|
349
|
+
"max_memory_mb": 4096,
|
|
350
|
+
"max_cpu_percent": 80,
|
|
351
|
+
"max_disk_mb": 10240,
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
# Add streaming config section
|
|
355
|
+
if "streaming" not in result:
|
|
356
|
+
result["streaming"] = {
|
|
357
|
+
"enabled": True,
|
|
358
|
+
"console_updates": True,
|
|
359
|
+
"progressive_json": None,
|
|
360
|
+
"progressive_yaml": None,
|
|
361
|
+
"progressive_markdown": None,
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return result
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def migrate_config_file(path: Path, dry_run: bool = False, backup: bool = True) -> MigrationResult:
|
|
368
|
+
"""Migrate a config file from an old format to the current format.
|
|
369
|
+
|
|
370
|
+
Reads the YAML file, detects its version, applies all necessary
|
|
371
|
+
migrations, and writes back the updated config. Optionally creates
|
|
372
|
+
a backup of the original file.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
path: Path to the config YAML file.
|
|
376
|
+
dry_run: If True, preview changes without modifying the file.
|
|
377
|
+
backup: If True, create a backup (.bak) of the original before writing.
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
MigrationResult with details of the migration.
|
|
381
|
+
|
|
382
|
+
Raises:
|
|
383
|
+
FileNotFoundError: If the config file does not exist.
|
|
384
|
+
yaml.YAMLError: If the file cannot be parsed as YAML.
|
|
385
|
+
"""
|
|
386
|
+
path = Path(path)
|
|
387
|
+
if not path.exists():
|
|
388
|
+
raise FileNotFoundError(f"Config file not found: {path}")
|
|
389
|
+
|
|
390
|
+
with open(path) as f:
|
|
391
|
+
config = yaml.safe_load(f) or {}
|
|
392
|
+
|
|
393
|
+
chain = MigrationChain()
|
|
394
|
+
result = chain.migrate(config, dry_run=dry_run)
|
|
395
|
+
|
|
396
|
+
if dry_run:
|
|
397
|
+
return result
|
|
398
|
+
|
|
399
|
+
# No migrations needed
|
|
400
|
+
if result.original_version == result.target_version:
|
|
401
|
+
return result
|
|
402
|
+
|
|
403
|
+
# Create backup before writing
|
|
404
|
+
if backup:
|
|
405
|
+
backup_path = path.with_suffix(path.suffix + ".bak")
|
|
406
|
+
shutil.copy2(path, backup_path)
|
|
407
|
+
result.changes_preview.append(f"Backup created: {backup_path}")
|
|
408
|
+
|
|
409
|
+
# Write migrated config
|
|
410
|
+
with open(path, "w") as f:
|
|
411
|
+
yaml.dump(
|
|
412
|
+
result.config,
|
|
413
|
+
f,
|
|
414
|
+
default_flow_style=False,
|
|
415
|
+
sort_keys=False,
|
|
416
|
+
allow_unicode=True,
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
return result
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def format_migration_report(result: MigrationResult) -> None:
|
|
423
|
+
"""Print a rich-formatted migration report to the console.
|
|
424
|
+
|
|
425
|
+
Displays the detected version, target version, applied migrations,
|
|
426
|
+
warnings, and a preview of changes.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
result: MigrationResult to format and display.
|
|
430
|
+
"""
|
|
431
|
+
console = Console(stderr=True)
|
|
432
|
+
|
|
433
|
+
# Header
|
|
434
|
+
title = f"Config Migration: {result.original_version.value} -> {result.target_version.value}"
|
|
435
|
+
|
|
436
|
+
if result.original_version == result.target_version:
|
|
437
|
+
console.print(
|
|
438
|
+
Panel(
|
|
439
|
+
"[green]Config is already at the current version. No migration needed.[/green]",
|
|
440
|
+
title=title,
|
|
441
|
+
)
|
|
442
|
+
)
|
|
443
|
+
return
|
|
444
|
+
|
|
445
|
+
# Migrations table
|
|
446
|
+
if result.migrations_applied:
|
|
447
|
+
table = Table(title="Migrations Applied")
|
|
448
|
+
table.add_column("#", style="dim", width=4)
|
|
449
|
+
table.add_column("Description", style="cyan")
|
|
450
|
+
|
|
451
|
+
for i, desc in enumerate(result.migrations_applied, 1):
|
|
452
|
+
table.add_row(str(i), desc)
|
|
453
|
+
|
|
454
|
+
console.print(table)
|
|
455
|
+
|
|
456
|
+
# Warnings
|
|
457
|
+
if result.warnings:
|
|
458
|
+
console.print()
|
|
459
|
+
console.print("[yellow]Warnings:[/yellow]")
|
|
460
|
+
for warning in result.warnings:
|
|
461
|
+
console.print(f" [yellow]- {warning}[/yellow]")
|
|
462
|
+
|
|
463
|
+
# Changes preview
|
|
464
|
+
if result.changes_preview:
|
|
465
|
+
console.print()
|
|
466
|
+
console.print("[blue]Changes:[/blue]")
|
|
467
|
+
for change in result.changes_preview:
|
|
468
|
+
console.print(f" [blue]- {change}[/blue]")
|
|
469
|
+
|
|
470
|
+
console.print()
|