mcli-framework 7.10.1__py3-none-any.whl → 7.11.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.
Potentially problematic release.
This version of mcli-framework might be problematic. Click here for more details.
- mcli/app/commands_cmd.py +150 -58
- mcli/app/main.py +21 -27
- mcli/lib/custom_commands.py +62 -12
- mcli/lib/optional_deps.py +240 -0
- mcli/lib/paths.py +129 -5
- mcli/self/migrate_cmd.py +261 -0
- mcli/self/self_cmd.py +8 -0
- mcli/workflow/git_commit/ai_service.py +13 -2
- mcli/workflow/notebook/__init__.py +16 -0
- mcli/workflow/notebook/converter.py +375 -0
- mcli/workflow/notebook/notebook_cmd.py +441 -0
- mcli/workflow/notebook/schema.py +402 -0
- mcli/workflow/notebook/validator.py +313 -0
- mcli/workflow/secrets/__init__.py +4 -0
- mcli/workflow/secrets/secrets_cmd.py +192 -0
- mcli/workflow/workflow.py +35 -5
- {mcli_framework-7.10.1.dist-info → mcli_framework-7.11.0.dist-info}/METADATA +86 -55
- {mcli_framework-7.10.1.dist-info → mcli_framework-7.11.0.dist-info}/RECORD +22 -34
- mcli/ml/features/political_features.py +0 -677
- mcli/ml/preprocessing/politician_trading_preprocessor.py +0 -570
- mcli/workflow/politician_trading/__init__.py +0 -4
- mcli/workflow/politician_trading/config.py +0 -134
- mcli/workflow/politician_trading/connectivity.py +0 -492
- mcli/workflow/politician_trading/data_sources.py +0 -654
- mcli/workflow/politician_trading/database.py +0 -412
- mcli/workflow/politician_trading/demo.py +0 -249
- mcli/workflow/politician_trading/models.py +0 -327
- mcli/workflow/politician_trading/monitoring.py +0 -413
- mcli/workflow/politician_trading/scrapers.py +0 -1074
- mcli/workflow/politician_trading/scrapers_california.py +0 -434
- mcli/workflow/politician_trading/scrapers_corporate_registry.py +0 -797
- mcli/workflow/politician_trading/scrapers_eu.py +0 -376
- mcli/workflow/politician_trading/scrapers_free_sources.py +0 -509
- mcli/workflow/politician_trading/scrapers_third_party.py +0 -373
- mcli/workflow/politician_trading/scrapers_uk.py +0 -378
- mcli/workflow/politician_trading/scrapers_us_states.py +0 -471
- mcli/workflow/politician_trading/seed_database.py +0 -520
- mcli/workflow/politician_trading/supabase_functions.py +0 -354
- mcli/workflow/politician_trading/workflow.py +0 -879
- {mcli_framework-7.10.1.dist-info → mcli_framework-7.11.0.dist-info}/WHEEL +0 -0
- {mcli_framework-7.10.1.dist-info → mcli_framework-7.11.0.dist-info}/entry_points.txt +0 -0
- {mcli_framework-7.10.1.dist-info → mcli_framework-7.11.0.dist-info}/licenses/LICENSE +0 -0
- {mcli_framework-7.10.1.dist-info → mcli_framework-7.11.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilities for graceful handling of optional dependencies.
|
|
3
|
+
|
|
4
|
+
This module provides helper functions and decorators to handle optional
|
|
5
|
+
dependencies gracefully, with clear error messages when features are unavailable.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import functools
|
|
9
|
+
from typing import Any, Callable, Dict, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
from mcli.lib.logger.logger import get_logger
|
|
12
|
+
|
|
13
|
+
logger = get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class OptionalDependency:
|
|
17
|
+
"""
|
|
18
|
+
Container for an optional dependency with availability tracking.
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
>>> ollama = OptionalDependency("ollama")
|
|
22
|
+
>>> if ollama.available:
|
|
23
|
+
... client = ollama.module.Client()
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
module_name: str,
|
|
29
|
+
import_name: Optional[str] = None,
|
|
30
|
+
install_hint: Optional[str] = None,
|
|
31
|
+
):
|
|
32
|
+
"""
|
|
33
|
+
Initialize optional dependency handler.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
module_name: Name of the module to import (e.g., "ollama")
|
|
37
|
+
import_name: Alternative import name if different from module_name
|
|
38
|
+
install_hint: Custom installation instruction
|
|
39
|
+
"""
|
|
40
|
+
self.module_name = module_name
|
|
41
|
+
self.import_name = import_name or module_name
|
|
42
|
+
self.install_hint = install_hint or f"pip install {module_name}"
|
|
43
|
+
self.module: Optional[Any] = None
|
|
44
|
+
self.available = False
|
|
45
|
+
self.error: Optional[Exception] = None
|
|
46
|
+
|
|
47
|
+
self._try_import()
|
|
48
|
+
|
|
49
|
+
def _try_import(self):
|
|
50
|
+
"""Attempt to import the module."""
|
|
51
|
+
try:
|
|
52
|
+
self.module = __import__(self.import_name)
|
|
53
|
+
self.available = True
|
|
54
|
+
logger.debug(f"Optional dependency '{self.module_name}' is available")
|
|
55
|
+
except ImportError as e:
|
|
56
|
+
self.available = False
|
|
57
|
+
self.error = e
|
|
58
|
+
logger.debug(f"Optional dependency '{self.module_name}' is not available: {e}")
|
|
59
|
+
|
|
60
|
+
def require(self, feature_name: Optional[str] = None) -> Any:
|
|
61
|
+
"""
|
|
62
|
+
Require the dependency to be available, raising an error if not.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
feature_name: Name of the feature requiring this dependency
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The imported module
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ImportError: If the dependency is not available
|
|
72
|
+
"""
|
|
73
|
+
if not self.available:
|
|
74
|
+
feature_msg = f" for {feature_name}" if feature_name else ""
|
|
75
|
+
raise ImportError(
|
|
76
|
+
f"'{self.module_name}' is required{feature_msg} but not installed.\n"
|
|
77
|
+
f"Install it with: {self.install_hint}"
|
|
78
|
+
)
|
|
79
|
+
return self.module
|
|
80
|
+
|
|
81
|
+
def __getattr__(self, name: str) -> Any:
|
|
82
|
+
"""Allow direct attribute access to the module."""
|
|
83
|
+
if not self.available:
|
|
84
|
+
raise ImportError(
|
|
85
|
+
f"Cannot access '{name}' from '{self.module_name}' - module not installed.\n"
|
|
86
|
+
f"Install it with: {self.install_hint}"
|
|
87
|
+
)
|
|
88
|
+
return getattr(self.module, name)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def optional_import(
|
|
92
|
+
module_name: str, import_name: Optional[str] = None, install_hint: Optional[str] = None
|
|
93
|
+
) -> Tuple[Optional[Any], bool]:
|
|
94
|
+
"""
|
|
95
|
+
Try to import an optional dependency.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
module_name: Name of the module to import
|
|
99
|
+
import_name: Alternative import name if different from module_name
|
|
100
|
+
install_hint: Custom installation instruction
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Tuple of (module, available) where module is None if unavailable
|
|
104
|
+
|
|
105
|
+
Example:
|
|
106
|
+
>>> ollama, OLLAMA_AVAILABLE = optional_import("ollama")
|
|
107
|
+
>>> if OLLAMA_AVAILABLE:
|
|
108
|
+
... client = ollama.Client()
|
|
109
|
+
"""
|
|
110
|
+
dep = OptionalDependency(module_name, import_name, install_hint)
|
|
111
|
+
return (dep.module, dep.available)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def require_dependency(
|
|
115
|
+
module_name: str, feature_name: str, install_hint: Optional[str] = None
|
|
116
|
+
) -> Any:
|
|
117
|
+
"""
|
|
118
|
+
Require a dependency, raising clear error if not available.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
module_name: Name of the module to import
|
|
122
|
+
feature_name: Name of the feature requiring this dependency
|
|
123
|
+
install_hint: Custom installation instruction
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
The imported module
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
ImportError: If the dependency is not available
|
|
130
|
+
|
|
131
|
+
Example:
|
|
132
|
+
>>> streamlit = require_dependency("streamlit", "dashboard")
|
|
133
|
+
"""
|
|
134
|
+
dep = OptionalDependency(module_name, install_hint=install_hint)
|
|
135
|
+
return dep.require(feature_name)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def requires(*dependencies: str, install_all_hint: Optional[str] = None):
|
|
139
|
+
"""
|
|
140
|
+
Decorator to mark a function as requiring specific dependencies.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
*dependencies: Module names required by the function
|
|
144
|
+
install_all_hint: Custom installation instruction for all dependencies
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
ImportError: If any required dependency is not available
|
|
148
|
+
|
|
149
|
+
Example:
|
|
150
|
+
>>> @requires("torch", "transformers")
|
|
151
|
+
... def train_model():
|
|
152
|
+
... import torch
|
|
153
|
+
... import transformers
|
|
154
|
+
... # training code
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
def decorator(func: Callable) -> Callable:
|
|
158
|
+
@functools.wraps(func)
|
|
159
|
+
def wrapper(*args, **kwargs):
|
|
160
|
+
missing = []
|
|
161
|
+
for dep_name in dependencies:
|
|
162
|
+
dep = OptionalDependency(dep_name)
|
|
163
|
+
if not dep.available:
|
|
164
|
+
missing.append(dep_name)
|
|
165
|
+
|
|
166
|
+
if missing:
|
|
167
|
+
if install_all_hint:
|
|
168
|
+
hint = install_all_hint
|
|
169
|
+
else:
|
|
170
|
+
hint = f"pip install {' '.join(missing)}"
|
|
171
|
+
|
|
172
|
+
raise ImportError(
|
|
173
|
+
f"Function '{func.__name__}' requires missing dependencies: {', '.join(missing)}\n"
|
|
174
|
+
f"Install them with: {hint}"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
return func(*args, **kwargs)
|
|
178
|
+
|
|
179
|
+
return wrapper
|
|
180
|
+
|
|
181
|
+
return decorator
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# Common optional dependencies registry
|
|
185
|
+
OPTIONAL_DEPS: Dict[str, OptionalDependency] = {}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def register_optional_dependency(
|
|
189
|
+
module_name: str, import_name: Optional[str] = None, install_hint: Optional[str] = None
|
|
190
|
+
) -> OptionalDependency:
|
|
191
|
+
"""
|
|
192
|
+
Register and cache an optional dependency.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
module_name: Name of the module to import
|
|
196
|
+
import_name: Alternative import name if different from module_name
|
|
197
|
+
install_hint: Custom installation instruction
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
OptionalDependency instance
|
|
201
|
+
"""
|
|
202
|
+
if module_name not in OPTIONAL_DEPS:
|
|
203
|
+
OPTIONAL_DEPS[module_name] = OptionalDependency(module_name, import_name, install_hint)
|
|
204
|
+
return OPTIONAL_DEPS[module_name]
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def check_dependencies(*module_names: str) -> Dict[str, bool]:
|
|
208
|
+
"""
|
|
209
|
+
Check availability of multiple dependencies.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
*module_names: Module names to check
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Dictionary mapping module names to availability status
|
|
216
|
+
|
|
217
|
+
Example:
|
|
218
|
+
>>> status = check_dependencies("torch", "transformers", "streamlit")
|
|
219
|
+
>>> print(status)
|
|
220
|
+
{'torch': True, 'transformers': False, 'streamlit': True}
|
|
221
|
+
"""
|
|
222
|
+
return {
|
|
223
|
+
name: OptionalDependency(name).available for name in module_names
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# Pre-register common optional dependencies
|
|
228
|
+
_COMMON_DEPS = {
|
|
229
|
+
"ollama": ("ollama", "pip install ollama"),
|
|
230
|
+
"streamlit": ("streamlit", "pip install streamlit"),
|
|
231
|
+
"torch": ("torch", "pip install torch"),
|
|
232
|
+
"transformers": ("transformers", "pip install transformers"),
|
|
233
|
+
"mlflow": ("mlflow", "pip install mlflow"),
|
|
234
|
+
"plotly": ("plotly", "pip install plotly"),
|
|
235
|
+
"pandas": ("pandas", "pip install pandas"),
|
|
236
|
+
"numpy": ("numpy", "pip install numpy"),
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
for module_name, (import_name, hint) in _COMMON_DEPS.items():
|
|
240
|
+
register_optional_dependency(module_name, import_name, hint)
|
mcli/lib/paths.py
CHANGED
|
@@ -82,13 +82,137 @@ def get_cache_dir() -> Path:
|
|
|
82
82
|
return cache_dir
|
|
83
83
|
|
|
84
84
|
|
|
85
|
-
def
|
|
85
|
+
def is_git_repository(path: Optional[Path] = None) -> bool:
|
|
86
86
|
"""
|
|
87
|
-
|
|
87
|
+
Check if the current directory (or specified path) is inside a git repository.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
path: Path to check (defaults to current working directory)
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
True if inside a git repository, False otherwise
|
|
94
|
+
"""
|
|
95
|
+
check_path = path or Path.cwd()
|
|
96
|
+
|
|
97
|
+
# Walk up the directory tree looking for .git
|
|
98
|
+
current = check_path.resolve()
|
|
99
|
+
while current != current.parent:
|
|
100
|
+
if (current / ".git").exists():
|
|
101
|
+
return True
|
|
102
|
+
current = current.parent
|
|
103
|
+
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_git_root(path: Optional[Path] = None) -> Optional[Path]:
|
|
108
|
+
"""
|
|
109
|
+
Get the root directory of the git repository.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
path: Path to check (defaults to current working directory)
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Path to git root, or None if not in a git repository
|
|
116
|
+
"""
|
|
117
|
+
check_path = path or Path.cwd()
|
|
118
|
+
|
|
119
|
+
# Walk up the directory tree looking for .git
|
|
120
|
+
current = check_path.resolve()
|
|
121
|
+
while current != current.parent:
|
|
122
|
+
if (current / ".git").exists():
|
|
123
|
+
return current
|
|
124
|
+
current = current.parent
|
|
125
|
+
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def get_local_mcli_dir() -> Optional[Path]:
|
|
130
|
+
"""
|
|
131
|
+
Get the local .mcli directory for the current git repository.
|
|
88
132
|
|
|
89
133
|
Returns:
|
|
90
|
-
Path to
|
|
134
|
+
Path to .mcli directory in git root, or None if not in a git repository
|
|
91
135
|
"""
|
|
136
|
+
git_root = get_git_root()
|
|
137
|
+
if git_root:
|
|
138
|
+
local_mcli = git_root / ".mcli"
|
|
139
|
+
return local_mcli
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_local_commands_dir() -> Optional[Path]:
|
|
144
|
+
"""
|
|
145
|
+
Get the local workflows directory for the current git repository.
|
|
146
|
+
|
|
147
|
+
Note: This function name is kept for backward compatibility but now returns
|
|
148
|
+
the workflows directory. Checks workflows first, then commands for migration.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Path to .mcli/workflows (or .mcli/commands for migration) in git root,
|
|
152
|
+
or None if not in a git repository
|
|
153
|
+
"""
|
|
154
|
+
local_mcli = get_local_mcli_dir()
|
|
155
|
+
if local_mcli:
|
|
156
|
+
# Check for new workflows directory first
|
|
157
|
+
workflows_dir = local_mcli / "workflows"
|
|
158
|
+
if workflows_dir.exists():
|
|
159
|
+
return workflows_dir
|
|
160
|
+
|
|
161
|
+
# Fall back to old commands directory for migration support
|
|
162
|
+
commands_dir = local_mcli / "commands"
|
|
163
|
+
return commands_dir
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def get_custom_commands_dir(global_mode: bool = False) -> Path:
|
|
168
|
+
"""
|
|
169
|
+
Get the custom workflows directory for mcli.
|
|
170
|
+
|
|
171
|
+
Note: This function name is kept for backward compatibility but now returns
|
|
172
|
+
the workflows directory. Checks workflows first, then commands for migration.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
global_mode: If True, always use global directory. If False, use local if in git repo.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Path to custom workflows directory, created if it doesn't exist
|
|
179
|
+
"""
|
|
180
|
+
# If not in global mode and we're in a git repository, use local directory
|
|
181
|
+
if not global_mode:
|
|
182
|
+
local_dir = get_local_commands_dir()
|
|
183
|
+
if local_dir:
|
|
184
|
+
local_dir.mkdir(parents=True, exist_ok=True)
|
|
185
|
+
return local_dir
|
|
186
|
+
|
|
187
|
+
# Otherwise, use global directory
|
|
188
|
+
# Check for new workflows directory first
|
|
189
|
+
workflows_dir = get_mcli_home() / "workflows"
|
|
190
|
+
if workflows_dir.exists():
|
|
191
|
+
return workflows_dir
|
|
192
|
+
|
|
193
|
+
# Check for old commands directory (for migration support)
|
|
92
194
|
commands_dir = get_mcli_home() / "commands"
|
|
93
|
-
commands_dir.
|
|
94
|
-
|
|
195
|
+
if commands_dir.exists():
|
|
196
|
+
# Return commands directory if it exists (user hasn't migrated yet)
|
|
197
|
+
return commands_dir
|
|
198
|
+
|
|
199
|
+
# If neither exists, create the new workflows directory
|
|
200
|
+
workflows_dir.mkdir(parents=True, exist_ok=True)
|
|
201
|
+
return workflows_dir
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def get_lockfile_path(global_mode: bool = False) -> Path:
|
|
205
|
+
"""
|
|
206
|
+
Get the lockfile path for workflow management.
|
|
207
|
+
|
|
208
|
+
Note: Lockfile remains named commands.lock.json for compatibility.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
global_mode: If True, use global lockfile. If False, use local if in git repo.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Path to the lockfile
|
|
215
|
+
"""
|
|
216
|
+
workflows_dir = get_custom_commands_dir(global_mode=global_mode)
|
|
217
|
+
# Keep the old lockfile name for compatibility
|
|
218
|
+
return workflows_dir / "commands.lock.json"
|
mcli/self/migrate_cmd.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Migration commands for mcli self-management.
|
|
3
|
+
|
|
4
|
+
Handles migrations between different versions of mcli, including:
|
|
5
|
+
- Directory structure changes
|
|
6
|
+
- Configuration format changes
|
|
7
|
+
- Command structure changes
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import shutil
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import List, Tuple
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.panel import Panel
|
|
19
|
+
from rich.table import Table
|
|
20
|
+
|
|
21
|
+
from mcli.lib.logger.logger import get_logger
|
|
22
|
+
from mcli.lib.ui.styling import error, info, success, warning
|
|
23
|
+
|
|
24
|
+
logger = get_logger(__name__)
|
|
25
|
+
console = Console()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_migration_status() -> dict:
|
|
29
|
+
"""
|
|
30
|
+
Check the current migration status.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Dictionary with migration status information
|
|
34
|
+
"""
|
|
35
|
+
mcli_home = Path.home() / ".mcli"
|
|
36
|
+
old_commands_dir = mcli_home / "commands"
|
|
37
|
+
new_workflows_dir = mcli_home / "workflows"
|
|
38
|
+
|
|
39
|
+
status = {
|
|
40
|
+
"old_dir_exists": old_commands_dir.exists(),
|
|
41
|
+
"old_dir_path": str(old_commands_dir),
|
|
42
|
+
"new_dir_exists": new_workflows_dir.exists(),
|
|
43
|
+
"new_dir_path": str(new_workflows_dir),
|
|
44
|
+
"needs_migration": False,
|
|
45
|
+
"files_to_migrate": [],
|
|
46
|
+
"migration_done": False,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Check if migration is needed
|
|
50
|
+
if old_commands_dir.exists():
|
|
51
|
+
# Count files that need migration (excluding hidden files)
|
|
52
|
+
files = [
|
|
53
|
+
f for f in old_commands_dir.iterdir()
|
|
54
|
+
if f.is_file() and not f.name.startswith('.')
|
|
55
|
+
]
|
|
56
|
+
status["files_to_migrate"] = [f.name for f in files]
|
|
57
|
+
status["needs_migration"] = len(files) > 0
|
|
58
|
+
|
|
59
|
+
# Check if migration already done
|
|
60
|
+
if new_workflows_dir.exists() and not old_commands_dir.exists():
|
|
61
|
+
status["migration_done"] = True
|
|
62
|
+
|
|
63
|
+
return status
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def migrate_commands_to_workflows(dry_run: bool = False, force: bool = False) -> Tuple[bool, str]:
|
|
67
|
+
"""
|
|
68
|
+
Migrate ~/.mcli/commands to ~/.mcli/workflows.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
dry_run: If True, show what would be done without actually doing it
|
|
72
|
+
force: If True, proceed even if workflows directory exists
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Tuple of (success, message)
|
|
76
|
+
"""
|
|
77
|
+
mcli_home = Path.home() / ".mcli"
|
|
78
|
+
old_dir = mcli_home / "commands"
|
|
79
|
+
new_dir = mcli_home / "workflows"
|
|
80
|
+
|
|
81
|
+
# Check if old directory exists
|
|
82
|
+
if not old_dir.exists():
|
|
83
|
+
return False, f"Nothing to migrate: {old_dir} does not exist"
|
|
84
|
+
|
|
85
|
+
# Check if new directory already exists
|
|
86
|
+
if new_dir.exists() and not force:
|
|
87
|
+
return False, f"Target directory {new_dir} already exists. Use --force to override."
|
|
88
|
+
|
|
89
|
+
# Get list of files to migrate
|
|
90
|
+
files_to_migrate = [
|
|
91
|
+
f for f in old_dir.iterdir()
|
|
92
|
+
if f.is_file() and not f.name.startswith('.')
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
if not files_to_migrate:
|
|
96
|
+
return False, f"No files to migrate in {old_dir}"
|
|
97
|
+
|
|
98
|
+
if dry_run:
|
|
99
|
+
message = f"[DRY RUN] Would migrate {len(files_to_migrate)} files from {old_dir} to {new_dir}"
|
|
100
|
+
return True, message
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
# Create new directory if it doesn't exist
|
|
104
|
+
new_dir.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
|
|
106
|
+
# Track migrated files
|
|
107
|
+
migrated_files = []
|
|
108
|
+
skipped_files = []
|
|
109
|
+
|
|
110
|
+
# Move files
|
|
111
|
+
for file_path in files_to_migrate:
|
|
112
|
+
target_path = new_dir / file_path.name
|
|
113
|
+
|
|
114
|
+
# Check if file already exists in target
|
|
115
|
+
if target_path.exists():
|
|
116
|
+
if force:
|
|
117
|
+
# Backup existing file
|
|
118
|
+
backup_path = target_path.with_suffix(f".backup.{datetime.now().strftime('%Y%m%d%H%M%S')}")
|
|
119
|
+
shutil.move(str(target_path), str(backup_path))
|
|
120
|
+
logger.info(f"Backed up existing file to {backup_path}")
|
|
121
|
+
else:
|
|
122
|
+
skipped_files.append(file_path.name)
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
# Move the file
|
|
126
|
+
shutil.move(str(file_path), str(target_path))
|
|
127
|
+
migrated_files.append(file_path.name)
|
|
128
|
+
logger.info(f"Migrated: {file_path.name}")
|
|
129
|
+
|
|
130
|
+
# Check if old directory is now empty (only hidden files remain)
|
|
131
|
+
remaining_files = [
|
|
132
|
+
f for f in old_dir.iterdir()
|
|
133
|
+
if f.is_file() and not f.name.startswith('.')
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
# If empty, remove old directory
|
|
137
|
+
if not remaining_files:
|
|
138
|
+
# Keep hidden files like .gitignore but remove directory if truly empty
|
|
139
|
+
all_remaining = list(old_dir.iterdir())
|
|
140
|
+
if not all_remaining:
|
|
141
|
+
old_dir.rmdir()
|
|
142
|
+
logger.info(f"Removed empty directory: {old_dir}")
|
|
143
|
+
|
|
144
|
+
# Create migration report
|
|
145
|
+
report_lines = [
|
|
146
|
+
f"Successfully migrated {len(migrated_files)} files from {old_dir} to {new_dir}"
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
if skipped_files:
|
|
150
|
+
report_lines.append(f"Skipped {len(skipped_files)} files (already exist in target)")
|
|
151
|
+
|
|
152
|
+
if remaining_files:
|
|
153
|
+
report_lines.append(f"Note: {len(remaining_files)} files remain in {old_dir}")
|
|
154
|
+
|
|
155
|
+
return True, "\n".join(report_lines)
|
|
156
|
+
|
|
157
|
+
except Exception as e:
|
|
158
|
+
logger.error(f"Migration failed: {e}")
|
|
159
|
+
return False, f"Migration failed: {str(e)}"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@click.command(name="migrate", help="Perform system migrations for mcli")
|
|
163
|
+
@click.option(
|
|
164
|
+
"--dry-run",
|
|
165
|
+
is_flag=True,
|
|
166
|
+
help="Show what would be done without actually doing it",
|
|
167
|
+
)
|
|
168
|
+
@click.option(
|
|
169
|
+
"--force",
|
|
170
|
+
is_flag=True,
|
|
171
|
+
help="Force migration even if target directory exists",
|
|
172
|
+
)
|
|
173
|
+
@click.option(
|
|
174
|
+
"--status",
|
|
175
|
+
is_flag=True,
|
|
176
|
+
help="Show migration status without performing migration",
|
|
177
|
+
)
|
|
178
|
+
def migrate_command(dry_run: bool, force: bool, status: bool):
|
|
179
|
+
"""
|
|
180
|
+
Migrate mcli configuration and data to new structure.
|
|
181
|
+
|
|
182
|
+
Currently handles:
|
|
183
|
+
- Moving ~/.mcli/commands to ~/.mcli/workflows
|
|
184
|
+
|
|
185
|
+
Examples:
|
|
186
|
+
mcli self migrate --status # Check migration status
|
|
187
|
+
mcli self migrate --dry-run # See what would be done
|
|
188
|
+
mcli self migrate # Perform migration
|
|
189
|
+
mcli self migrate --force # Force migration (overwrite existing)
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
# Get current status
|
|
193
|
+
migration_status = get_migration_status()
|
|
194
|
+
|
|
195
|
+
# If --status flag, just show status and exit
|
|
196
|
+
if status:
|
|
197
|
+
console.print("\n[bold cyan]Migration Status[/bold cyan]")
|
|
198
|
+
console.print(f"\n[bold]Old location:[/bold] {migration_status['old_dir_path']}")
|
|
199
|
+
console.print(f" Exists: {'✓ Yes' if migration_status['old_dir_exists'] else '✗ No'}")
|
|
200
|
+
|
|
201
|
+
console.print(f"\n[bold]New location:[/bold] {migration_status['new_dir_path']}")
|
|
202
|
+
console.print(f" Exists: {'✓ Yes' if migration_status['new_dir_exists'] else '✗ No'}")
|
|
203
|
+
|
|
204
|
+
if migration_status['needs_migration']:
|
|
205
|
+
console.print(f"\n[yellow]⚠ Migration needed[/yellow]")
|
|
206
|
+
console.print(f"Files to migrate: {len(migration_status['files_to_migrate'])}")
|
|
207
|
+
|
|
208
|
+
if migration_status['files_to_migrate']:
|
|
209
|
+
table = Table(title="Files to Migrate")
|
|
210
|
+
table.add_column("File Name", style="cyan")
|
|
211
|
+
|
|
212
|
+
for filename in sorted(migration_status['files_to_migrate']):
|
|
213
|
+
table.add_row(filename)
|
|
214
|
+
|
|
215
|
+
console.print(table)
|
|
216
|
+
|
|
217
|
+
console.print(f"\n[dim]Run 'mcli self migrate' to perform migration[/dim]")
|
|
218
|
+
elif migration_status['migration_done']:
|
|
219
|
+
console.print(f"\n[green]✓ Migration already completed[/green]")
|
|
220
|
+
else:
|
|
221
|
+
console.print(f"\n[green]✓ No migration needed[/green]")
|
|
222
|
+
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
# Check if migration is needed
|
|
226
|
+
if not migration_status['needs_migration']:
|
|
227
|
+
if migration_status['migration_done']:
|
|
228
|
+
info("Migration already completed")
|
|
229
|
+
info(f"Workflows directory: {migration_status['new_dir_path']}")
|
|
230
|
+
else:
|
|
231
|
+
info("No migration needed")
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
# Show what will be migrated
|
|
235
|
+
console.print("\n[bold cyan]Migration Plan[/bold cyan]")
|
|
236
|
+
console.print(f"\nSource: [cyan]{migration_status['old_dir_path']}[/cyan]")
|
|
237
|
+
console.print(f"Target: [cyan]{migration_status['new_dir_path']}[/cyan]")
|
|
238
|
+
console.print(f"Files: [yellow]{len(migration_status['files_to_migrate'])}[/yellow]")
|
|
239
|
+
|
|
240
|
+
if dry_run:
|
|
241
|
+
console.print(f"\n[yellow]DRY RUN MODE - No changes will be made[/yellow]")
|
|
242
|
+
|
|
243
|
+
# Perform migration
|
|
244
|
+
success_flag, message = migrate_commands_to_workflows(dry_run=dry_run, force=force)
|
|
245
|
+
|
|
246
|
+
if success_flag:
|
|
247
|
+
if dry_run:
|
|
248
|
+
info(message)
|
|
249
|
+
else:
|
|
250
|
+
success(message)
|
|
251
|
+
console.print("\n[green]✓ Migration completed successfully[/green]")
|
|
252
|
+
console.print(f"\nYour workflows are now in: [cyan]{migration_status['new_dir_path']}[/cyan]")
|
|
253
|
+
console.print("\n[dim]You can now use 'mcli workflow' to manage and 'mcli workflows' to run them[/dim]")
|
|
254
|
+
else:
|
|
255
|
+
error(message)
|
|
256
|
+
if not force and "already exists" in message:
|
|
257
|
+
console.print("\n[yellow]Tip: Use --force to proceed anyway (will backup existing files)[/yellow]")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
if __name__ == "__main__":
|
|
261
|
+
migrate_command()
|
mcli/self/self_cmd.py
CHANGED
|
@@ -1045,6 +1045,14 @@ try:
|
|
|
1045
1045
|
except ImportError as e:
|
|
1046
1046
|
logger.debug(f"Could not load visual command: {e}")
|
|
1047
1047
|
|
|
1048
|
+
try:
|
|
1049
|
+
from mcli.self.migrate_cmd import migrate_command
|
|
1050
|
+
|
|
1051
|
+
self_app.add_command(migrate_command, name="migrate")
|
|
1052
|
+
logger.debug("Added migrate command to self group")
|
|
1053
|
+
except ImportError as e:
|
|
1054
|
+
logger.debug(f"Could not load migrate command: {e}")
|
|
1055
|
+
|
|
1048
1056
|
# NOTE: store command has been moved to mcli.app.commands_cmd for better organization
|
|
1049
1057
|
|
|
1050
1058
|
# This part is important to make the command available to the CLI
|
|
@@ -2,11 +2,13 @@ import json
|
|
|
2
2
|
import logging
|
|
3
3
|
from typing import Any, Dict, Optional
|
|
4
4
|
|
|
5
|
-
import ollama
|
|
6
|
-
|
|
7
5
|
from mcli.lib.logger.logger import get_logger
|
|
6
|
+
from mcli.lib.optional_deps import optional_import
|
|
8
7
|
from mcli.lib.toml.toml import read_from_toml
|
|
9
8
|
|
|
9
|
+
# Gracefully handle optional ollama dependency
|
|
10
|
+
ollama, OLLAMA_AVAILABLE = optional_import("ollama")
|
|
11
|
+
|
|
10
12
|
logger = get_logger(__name__)
|
|
11
13
|
|
|
12
14
|
|
|
@@ -204,6 +206,15 @@ Generate ONLY the commit message, nothing else:"""
|
|
|
204
206
|
def generate_commit_message(self, changes: Dict[str, Any], diff_content: str) -> str:
|
|
205
207
|
"""Generate an AI-powered commit message"""
|
|
206
208
|
try:
|
|
209
|
+
# Check if ollama is available
|
|
210
|
+
if not OLLAMA_AVAILABLE:
|
|
211
|
+
logger.warning(
|
|
212
|
+
"Ollama is not installed. Install it with: pip install ollama\n"
|
|
213
|
+
"Falling back to rule-based commit message generation."
|
|
214
|
+
)
|
|
215
|
+
analysis = self._analyze_file_patterns(changes)
|
|
216
|
+
return self._generate_fallback_message(changes, analysis)
|
|
217
|
+
|
|
207
218
|
# Analyze the changes first
|
|
208
219
|
analysis = self._analyze_file_patterns(changes)
|
|
209
220
|
|