mcli-framework 7.0.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/chat_cmd.py +42 -0
- mcli/app/commands_cmd.py +226 -0
- mcli/app/completion_cmd.py +216 -0
- mcli/app/completion_helpers.py +288 -0
- mcli/app/cron_test_cmd.py +697 -0
- mcli/app/logs_cmd.py +419 -0
- mcli/app/main.py +492 -0
- mcli/app/model/model.py +1060 -0
- mcli/app/model_cmd.py +227 -0
- mcli/app/redis_cmd.py +269 -0
- mcli/app/video/video.py +1114 -0
- mcli/app/visual_cmd.py +303 -0
- mcli/chat/chat.py +2409 -0
- mcli/chat/command_rag.py +514 -0
- mcli/chat/enhanced_chat.py +652 -0
- mcli/chat/system_controller.py +1010 -0
- mcli/chat/system_integration.py +1016 -0
- mcli/cli.py +25 -0
- mcli/config.toml +20 -0
- mcli/lib/api/api.py +586 -0
- mcli/lib/api/daemon_client.py +203 -0
- mcli/lib/api/daemon_client_local.py +44 -0
- mcli/lib/api/daemon_decorator.py +217 -0
- mcli/lib/api/mcli_decorators.py +1032 -0
- mcli/lib/auth/auth.py +85 -0
- mcli/lib/auth/aws_manager.py +85 -0
- mcli/lib/auth/azure_manager.py +91 -0
- mcli/lib/auth/credential_manager.py +192 -0
- mcli/lib/auth/gcp_manager.py +93 -0
- mcli/lib/auth/key_manager.py +117 -0
- mcli/lib/auth/mcli_manager.py +93 -0
- mcli/lib/auth/token_manager.py +75 -0
- mcli/lib/auth/token_util.py +1011 -0
- mcli/lib/config/config.py +47 -0
- mcli/lib/discovery/__init__.py +1 -0
- mcli/lib/discovery/command_discovery.py +274 -0
- mcli/lib/erd/erd.py +1345 -0
- mcli/lib/erd/generate_graph.py +453 -0
- mcli/lib/files/files.py +76 -0
- mcli/lib/fs/fs.py +109 -0
- mcli/lib/lib.py +29 -0
- mcli/lib/logger/logger.py +611 -0
- mcli/lib/performance/optimizer.py +409 -0
- mcli/lib/performance/rust_bridge.py +502 -0
- mcli/lib/performance/uvloop_config.py +154 -0
- mcli/lib/pickles/pickles.py +50 -0
- mcli/lib/search/cached_vectorizer.py +479 -0
- mcli/lib/services/data_pipeline.py +460 -0
- mcli/lib/services/lsh_client.py +441 -0
- mcli/lib/services/redis_service.py +387 -0
- mcli/lib/shell/shell.py +137 -0
- mcli/lib/toml/toml.py +33 -0
- mcli/lib/ui/styling.py +47 -0
- mcli/lib/ui/visual_effects.py +634 -0
- mcli/lib/watcher/watcher.py +185 -0
- mcli/ml/api/app.py +215 -0
- mcli/ml/api/middleware.py +224 -0
- mcli/ml/api/routers/admin_router.py +12 -0
- mcli/ml/api/routers/auth_router.py +244 -0
- mcli/ml/api/routers/backtest_router.py +12 -0
- mcli/ml/api/routers/data_router.py +12 -0
- mcli/ml/api/routers/model_router.py +302 -0
- mcli/ml/api/routers/monitoring_router.py +12 -0
- mcli/ml/api/routers/portfolio_router.py +12 -0
- mcli/ml/api/routers/prediction_router.py +267 -0
- mcli/ml/api/routers/trade_router.py +12 -0
- mcli/ml/api/routers/websocket_router.py +76 -0
- mcli/ml/api/schemas.py +64 -0
- mcli/ml/auth/auth_manager.py +425 -0
- mcli/ml/auth/models.py +154 -0
- mcli/ml/auth/permissions.py +302 -0
- mcli/ml/backtesting/backtest_engine.py +502 -0
- mcli/ml/backtesting/performance_metrics.py +393 -0
- mcli/ml/cache.py +400 -0
- mcli/ml/cli/main.py +398 -0
- mcli/ml/config/settings.py +394 -0
- mcli/ml/configs/dvc_config.py +230 -0
- mcli/ml/configs/mlflow_config.py +131 -0
- mcli/ml/configs/mlops_manager.py +293 -0
- mcli/ml/dashboard/app.py +532 -0
- mcli/ml/dashboard/app_integrated.py +738 -0
- mcli/ml/dashboard/app_supabase.py +560 -0
- mcli/ml/dashboard/app_training.py +615 -0
- mcli/ml/dashboard/cli.py +51 -0
- mcli/ml/data_ingestion/api_connectors.py +501 -0
- mcli/ml/data_ingestion/data_pipeline.py +567 -0
- mcli/ml/data_ingestion/stream_processor.py +512 -0
- mcli/ml/database/migrations/env.py +94 -0
- mcli/ml/database/models.py +667 -0
- mcli/ml/database/session.py +200 -0
- mcli/ml/experimentation/ab_testing.py +845 -0
- mcli/ml/features/ensemble_features.py +607 -0
- mcli/ml/features/political_features.py +676 -0
- mcli/ml/features/recommendation_engine.py +809 -0
- mcli/ml/features/stock_features.py +573 -0
- mcli/ml/features/test_feature_engineering.py +346 -0
- mcli/ml/logging.py +85 -0
- mcli/ml/mlops/data_versioning.py +518 -0
- mcli/ml/mlops/experiment_tracker.py +377 -0
- mcli/ml/mlops/model_serving.py +481 -0
- mcli/ml/mlops/pipeline_orchestrator.py +614 -0
- mcli/ml/models/base_models.py +324 -0
- mcli/ml/models/ensemble_models.py +675 -0
- mcli/ml/models/recommendation_models.py +474 -0
- mcli/ml/models/test_models.py +487 -0
- mcli/ml/monitoring/drift_detection.py +676 -0
- mcli/ml/monitoring/metrics.py +45 -0
- mcli/ml/optimization/portfolio_optimizer.py +834 -0
- mcli/ml/preprocessing/data_cleaners.py +451 -0
- mcli/ml/preprocessing/feature_extractors.py +491 -0
- mcli/ml/preprocessing/ml_pipeline.py +382 -0
- mcli/ml/preprocessing/politician_trading_preprocessor.py +569 -0
- mcli/ml/preprocessing/test_preprocessing.py +294 -0
- mcli/ml/scripts/populate_sample_data.py +200 -0
- mcli/ml/tasks.py +400 -0
- mcli/ml/tests/test_integration.py +429 -0
- mcli/ml/tests/test_training_dashboard.py +387 -0
- mcli/public/oi/oi.py +15 -0
- mcli/public/public.py +4 -0
- mcli/self/self_cmd.py +1246 -0
- mcli/workflow/daemon/api_daemon.py +800 -0
- mcli/workflow/daemon/async_command_database.py +681 -0
- mcli/workflow/daemon/async_process_manager.py +591 -0
- mcli/workflow/daemon/client.py +530 -0
- mcli/workflow/daemon/commands.py +1196 -0
- mcli/workflow/daemon/daemon.py +905 -0
- mcli/workflow/daemon/daemon_api.py +59 -0
- mcli/workflow/daemon/enhanced_daemon.py +571 -0
- mcli/workflow/daemon/process_cli.py +244 -0
- mcli/workflow/daemon/process_manager.py +439 -0
- mcli/workflow/daemon/test_daemon.py +275 -0
- mcli/workflow/dashboard/dashboard_cmd.py +113 -0
- mcli/workflow/docker/docker.py +0 -0
- mcli/workflow/file/file.py +100 -0
- mcli/workflow/gcloud/config.toml +21 -0
- mcli/workflow/gcloud/gcloud.py +58 -0
- mcli/workflow/git_commit/ai_service.py +328 -0
- mcli/workflow/git_commit/commands.py +430 -0
- mcli/workflow/lsh_integration.py +355 -0
- mcli/workflow/model_service/client.py +594 -0
- mcli/workflow/model_service/download_and_run_efficient_models.py +288 -0
- mcli/workflow/model_service/lightweight_embedder.py +397 -0
- mcli/workflow/model_service/lightweight_model_server.py +714 -0
- mcli/workflow/model_service/lightweight_test.py +241 -0
- mcli/workflow/model_service/model_service.py +1955 -0
- mcli/workflow/model_service/ollama_efficient_runner.py +425 -0
- mcli/workflow/model_service/pdf_processor.py +386 -0
- mcli/workflow/model_service/test_efficient_runner.py +234 -0
- mcli/workflow/model_service/test_example.py +315 -0
- mcli/workflow/model_service/test_integration.py +131 -0
- mcli/workflow/model_service/test_new_features.py +149 -0
- mcli/workflow/openai/openai.py +99 -0
- mcli/workflow/politician_trading/commands.py +1790 -0
- mcli/workflow/politician_trading/config.py +134 -0
- mcli/workflow/politician_trading/connectivity.py +490 -0
- mcli/workflow/politician_trading/data_sources.py +395 -0
- mcli/workflow/politician_trading/database.py +410 -0
- mcli/workflow/politician_trading/demo.py +248 -0
- mcli/workflow/politician_trading/models.py +165 -0
- mcli/workflow/politician_trading/monitoring.py +413 -0
- mcli/workflow/politician_trading/scrapers.py +966 -0
- mcli/workflow/politician_trading/scrapers_california.py +412 -0
- mcli/workflow/politician_trading/scrapers_eu.py +377 -0
- mcli/workflow/politician_trading/scrapers_uk.py +350 -0
- mcli/workflow/politician_trading/scrapers_us_states.py +438 -0
- mcli/workflow/politician_trading/supabase_functions.py +354 -0
- mcli/workflow/politician_trading/workflow.py +852 -0
- mcli/workflow/registry/registry.py +180 -0
- mcli/workflow/repo/repo.py +223 -0
- mcli/workflow/scheduler/commands.py +493 -0
- mcli/workflow/scheduler/cron_parser.py +238 -0
- mcli/workflow/scheduler/job.py +182 -0
- mcli/workflow/scheduler/monitor.py +139 -0
- mcli/workflow/scheduler/persistence.py +324 -0
- mcli/workflow/scheduler/scheduler.py +679 -0
- mcli/workflow/sync/sync_cmd.py +437 -0
- mcli/workflow/sync/test_cmd.py +314 -0
- mcli/workflow/videos/videos.py +242 -0
- mcli/workflow/wakatime/wakatime.py +11 -0
- mcli/workflow/workflow.py +37 -0
- mcli_framework-7.0.0.dist-info/METADATA +479 -0
- mcli_framework-7.0.0.dist-info/RECORD +186 -0
- mcli_framework-7.0.0.dist-info/WHEEL +5 -0
- mcli_framework-7.0.0.dist-info/entry_points.txt +7 -0
- mcli_framework-7.0.0.dist-info/licenses/LICENSE +21 -0
- mcli_framework-7.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from mcli.lib.logger.logger import get_logger
|
|
10
|
+
from mcli.lib.toml.toml import read_from_toml
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class DaemonClientConfig:
|
|
17
|
+
"""Configuration for daemon client"""
|
|
18
|
+
|
|
19
|
+
host: str = "localhost"
|
|
20
|
+
port: int = 8000
|
|
21
|
+
timeout: int = 30
|
|
22
|
+
retry_attempts: int = 3
|
|
23
|
+
retry_delay: float = 1.0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class APIDaemonClient:
|
|
27
|
+
"""Client for interacting with the MCLI API Daemon or Flask shell daemon"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, config: Optional[DaemonClientConfig] = None, shell_mode: bool = False):
|
|
30
|
+
self.config = config or self._load_config()
|
|
31
|
+
self.shell_mode = shell_mode
|
|
32
|
+
# If shell_mode, always use Flask test server at 0.0.0.0:5005
|
|
33
|
+
if shell_mode:
|
|
34
|
+
self.base_url = "http://localhost:5005"
|
|
35
|
+
else:
|
|
36
|
+
self.base_url = f"http://{self.config.host}:{self.config.port}"
|
|
37
|
+
self.session = requests.Session()
|
|
38
|
+
|
|
39
|
+
def execute_shell_command(self, command: str) -> Dict[str, Any]:
|
|
40
|
+
"""Execute a raw shell command via the Flask test server"""
|
|
41
|
+
url = f"http://localhost:5005/execute"
|
|
42
|
+
try:
|
|
43
|
+
response = self.session.post(
|
|
44
|
+
url, json={"command": command}, timeout=self.config.timeout
|
|
45
|
+
)
|
|
46
|
+
response.raise_for_status()
|
|
47
|
+
return response.json()
|
|
48
|
+
except requests.exceptions.RequestException as e:
|
|
49
|
+
raise Exception(f"Failed to connect to Flask shell daemon at {url}: {e}")
|
|
50
|
+
|
|
51
|
+
def _load_config(self) -> DaemonClientConfig:
|
|
52
|
+
"""Load configuration from config files"""
|
|
53
|
+
config = DaemonClientConfig()
|
|
54
|
+
|
|
55
|
+
# Try to load from config.toml files
|
|
56
|
+
config_paths = [
|
|
57
|
+
Path("config.toml"), # Current directory
|
|
58
|
+
Path.home() / ".config" / "mcli" / "config.toml", # User config
|
|
59
|
+
Path(__file__).parent.parent.parent.parent.parent / "config.toml", # Project root
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
for path in config_paths:
|
|
63
|
+
if path.exists():
|
|
64
|
+
try:
|
|
65
|
+
daemon_config = read_from_toml(str(path), "api_daemon")
|
|
66
|
+
if daemon_config:
|
|
67
|
+
if daemon_config.get("host"):
|
|
68
|
+
config.host = daemon_config["host"]
|
|
69
|
+
if daemon_config.get("port"):
|
|
70
|
+
config.port = daemon_config["port"]
|
|
71
|
+
logger.debug(f"Loaded daemon client config from {path}")
|
|
72
|
+
break
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logger.debug(f"Could not load daemon client config from {path}: {e}")
|
|
75
|
+
|
|
76
|
+
return config
|
|
77
|
+
|
|
78
|
+
def _make_request(
|
|
79
|
+
self,
|
|
80
|
+
method: str,
|
|
81
|
+
endpoint: str,
|
|
82
|
+
data: Optional[Dict] = None,
|
|
83
|
+
params: Optional[Dict] = None,
|
|
84
|
+
**kwargs,
|
|
85
|
+
) -> Dict[str, Any]:
|
|
86
|
+
"""Make HTTP request to daemon with retry logic"""
|
|
87
|
+
url = f"{self.base_url}{endpoint}"
|
|
88
|
+
for attempt in range(self.config.retry_attempts):
|
|
89
|
+
try:
|
|
90
|
+
if method.upper() == "GET":
|
|
91
|
+
response = self.session.request(
|
|
92
|
+
method=method, url=url, params=params, timeout=self.config.timeout, **kwargs
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
response = self.session.request(
|
|
96
|
+
method=method, url=url, json=data, timeout=self.config.timeout, **kwargs
|
|
97
|
+
)
|
|
98
|
+
response.raise_for_status()
|
|
99
|
+
return response.json()
|
|
100
|
+
except requests.exceptions.RequestException as e:
|
|
101
|
+
if attempt == self.config.retry_attempts - 1:
|
|
102
|
+
raise Exception(f"Failed to connect to API daemon at {url}: {e}")
|
|
103
|
+
logger.warning(
|
|
104
|
+
f"Attempt {attempt + 1} failed, retrying in {self.config.retry_delay}s..."
|
|
105
|
+
)
|
|
106
|
+
time.sleep(self.config.retry_delay)
|
|
107
|
+
return {} # Always return a dict
|
|
108
|
+
|
|
109
|
+
def health_check(self) -> Dict[str, Any]:
|
|
110
|
+
"""Check daemon health"""
|
|
111
|
+
return self._make_request("GET", "/health")
|
|
112
|
+
|
|
113
|
+
def status(self) -> Dict[str, Any]:
|
|
114
|
+
"""Get daemon status"""
|
|
115
|
+
return self._make_request("GET", "/status")
|
|
116
|
+
|
|
117
|
+
def list_commands(self, all: bool = False) -> Dict[str, Any]:
|
|
118
|
+
"""List available commands with metadata. If all=True, include inactive."""
|
|
119
|
+
params = {"all": str(all).lower()} if all else None
|
|
120
|
+
return self._make_request("GET", "/commands", params=params)
|
|
121
|
+
|
|
122
|
+
def get_command_details(self, command_id: str) -> Optional[Dict[str, Any]]:
|
|
123
|
+
"""Get detailed information about a specific command"""
|
|
124
|
+
return self._make_request("GET", f"/commands/{command_id}")
|
|
125
|
+
|
|
126
|
+
def execute_command(
|
|
127
|
+
self,
|
|
128
|
+
command_id: Optional[str] = None,
|
|
129
|
+
command_name: Optional[str] = None,
|
|
130
|
+
args: Optional[List[str]] = None,
|
|
131
|
+
timeout: Optional[int] = None,
|
|
132
|
+
) -> Dict[str, Any]:
|
|
133
|
+
"""Execute a command via the daemon"""
|
|
134
|
+
if not command_id and not command_name:
|
|
135
|
+
raise ValueError("Either command_id or command_name must be provided")
|
|
136
|
+
|
|
137
|
+
data = {
|
|
138
|
+
"args": args or [],
|
|
139
|
+
}
|
|
140
|
+
if command_id is not None:
|
|
141
|
+
data["command_id"] = command_id
|
|
142
|
+
if command_name is not None:
|
|
143
|
+
data["command_name"] = command_name
|
|
144
|
+
if timeout is not None:
|
|
145
|
+
data["timeout"] = timeout
|
|
146
|
+
return self._make_request("POST", "/execute", data=data)
|
|
147
|
+
|
|
148
|
+
def start_daemon(self) -> Dict[str, Any]:
|
|
149
|
+
"""Start the daemon via HTTP"""
|
|
150
|
+
return self._make_request("POST", "/daemon/start")
|
|
151
|
+
|
|
152
|
+
def stop_daemon(self) -> Dict[str, Any]:
|
|
153
|
+
"""Stop the daemon via HTTP"""
|
|
154
|
+
return self._make_request("POST", "/daemon/stop")
|
|
155
|
+
|
|
156
|
+
def is_running(self) -> bool:
|
|
157
|
+
"""Check if daemon is running"""
|
|
158
|
+
try:
|
|
159
|
+
status = self.status()
|
|
160
|
+
return status.get("running", False)
|
|
161
|
+
except Exception:
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
def wait_for_daemon(self, timeout: int = 30) -> bool:
|
|
165
|
+
"""Wait for daemon to be ready"""
|
|
166
|
+
start_time = time.time()
|
|
167
|
+
while time.time() - start_time < timeout:
|
|
168
|
+
if self.is_running():
|
|
169
|
+
return True
|
|
170
|
+
time.sleep(1)
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# Convenience functions for easy usage
|
|
175
|
+
def execute_shell_command_via_flask(command: str) -> Dict[str, Any]:
|
|
176
|
+
"""Execute a raw shell command via the Flask test server (convenience function)"""
|
|
177
|
+
client = APIDaemonClient(shell_mode=True)
|
|
178
|
+
return client.execute_shell_command(command)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def get_daemon_client() -> APIDaemonClient:
|
|
182
|
+
"""Get a configured daemon client"""
|
|
183
|
+
return APIDaemonClient()
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def execute_command_via_daemon(
|
|
187
|
+
command_name: str, args: Optional[List[str]] = None
|
|
188
|
+
) -> Dict[str, Any]:
|
|
189
|
+
"""Execute a command via the daemon (convenience function)"""
|
|
190
|
+
client = get_daemon_client()
|
|
191
|
+
return client.execute_command(command_name=command_name, args=args)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def check_daemon_status() -> Dict[str, Any]:
|
|
195
|
+
"""Check daemon status (convenience function)"""
|
|
196
|
+
client = get_daemon_client()
|
|
197
|
+
return client.status()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def list_available_commands() -> Dict[str, Any]:
|
|
201
|
+
"""List available commands (convenience function)"""
|
|
202
|
+
client = get_daemon_client()
|
|
203
|
+
return client.list_commands()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import subprocess
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from mcli.lib.logger.logger import get_logger
|
|
7
|
+
from mcli.lib.toml.toml import read_from_toml
|
|
8
|
+
|
|
9
|
+
logger = get_logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LocalDaemonClient:
|
|
13
|
+
"""Client for interacting with the MCLI Daemon via CLI subprocess (local IPC)"""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self.daemon_cmd = ["python", "-m", "mcli.workflow.daemon.daemon"]
|
|
17
|
+
# Optionally, you could use the installed CLI: ["mcli-daemon"]
|
|
18
|
+
|
|
19
|
+
def list_commands(self) -> Dict[str, Any]:
|
|
20
|
+
logger.info("[LocalDaemonClient] Invoking 'list-commands' via subprocess")
|
|
21
|
+
result = subprocess.run(
|
|
22
|
+
self.daemon_cmd + ["list-commands", "--json"], capture_output=True, text=True
|
|
23
|
+
)
|
|
24
|
+
logger.info(f"[LocalDaemonClient] stdout: {result.stdout}\nstderr: {result.stderr}")
|
|
25
|
+
if result.returncode != 0:
|
|
26
|
+
raise Exception(f"Daemon list-commands failed: {result.stderr}")
|
|
27
|
+
return json.loads(result.stdout)
|
|
28
|
+
|
|
29
|
+
def execute_command(
|
|
30
|
+
self, command_name: str, args: Optional[List[str]] = None
|
|
31
|
+
) -> Dict[str, Any]:
|
|
32
|
+
logger.info(f"[LocalDaemonClient] Invoking 'execute' for {command_name} via subprocess")
|
|
33
|
+
cmd = self.daemon_cmd + ["execute", command_name] + (args or []) + ["--json"]
|
|
34
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
35
|
+
logger.info(f"[LocalDaemonClient] stdout: {result.stdout}\nstderr: {result.stderr}")
|
|
36
|
+
if result.returncode != 0:
|
|
37
|
+
raise Exception(f"Daemon execute failed: {result.stderr}")
|
|
38
|
+
return json.loads(result.stdout)
|
|
39
|
+
|
|
40
|
+
# Add more methods as needed for other daemon features
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_local_daemon_client():
|
|
44
|
+
return LocalDaemonClient()
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import inspect
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from mcli.lib.logger.logger import get_logger
|
|
8
|
+
from mcli.lib.toml.toml import read_from_toml
|
|
9
|
+
|
|
10
|
+
from .daemon_client import APIDaemonClient, get_daemon_client
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def daemon_command(
|
|
16
|
+
command_name: Optional[str] = None,
|
|
17
|
+
auto_route: bool = True,
|
|
18
|
+
fallback_to_local: bool = True,
|
|
19
|
+
timeout: Optional[int] = None,
|
|
20
|
+
):
|
|
21
|
+
"""
|
|
22
|
+
Decorator to route Click commands to the API daemon when enabled.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
command_name: Name of the command (defaults to function name)
|
|
26
|
+
auto_route: Whether to automatically route to daemon if enabled
|
|
27
|
+
fallback_to_local: Whether to fallback to local execution if daemon fails
|
|
28
|
+
timeout: Command timeout in seconds
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def decorator(func: Callable) -> Callable:
|
|
32
|
+
# Get command name
|
|
33
|
+
cmd_name = command_name or func.__name__
|
|
34
|
+
|
|
35
|
+
@functools.wraps(func)
|
|
36
|
+
def wrapper(*args, **kwargs):
|
|
37
|
+
# Check if daemon routing is enabled
|
|
38
|
+
if not auto_route or not _is_daemon_routing_enabled():
|
|
39
|
+
return func(*args, **kwargs)
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
# Try to execute via daemon
|
|
43
|
+
client = get_daemon_client()
|
|
44
|
+
|
|
45
|
+
# Convert args and kwargs to command arguments
|
|
46
|
+
cmd_args = _convert_to_command_args(args, kwargs, func)
|
|
47
|
+
|
|
48
|
+
# Execute via daemon
|
|
49
|
+
result = client.execute_command(
|
|
50
|
+
command_name=cmd_name, args=cmd_args, timeout=timeout
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if result.get("success"):
|
|
54
|
+
logger.info(f"Command '{cmd_name}' executed successfully via daemon")
|
|
55
|
+
return result.get("result")
|
|
56
|
+
else:
|
|
57
|
+
logger.warning(
|
|
58
|
+
f"Daemon execution failed for '{cmd_name}': {result.get('error')}"
|
|
59
|
+
)
|
|
60
|
+
if fallback_to_local:
|
|
61
|
+
logger.info(f"Falling back to local execution for '{cmd_name}'")
|
|
62
|
+
return func(*args, **kwargs)
|
|
63
|
+
else:
|
|
64
|
+
raise Exception(f"Daemon execution failed: {result.get('error')}")
|
|
65
|
+
|
|
66
|
+
except Exception as e:
|
|
67
|
+
logger.warning(f"Failed to execute '{cmd_name}' via daemon: {e}")
|
|
68
|
+
if fallback_to_local:
|
|
69
|
+
logger.info(f"Falling back to local execution for '{cmd_name}'")
|
|
70
|
+
return func(*args, **kwargs)
|
|
71
|
+
else:
|
|
72
|
+
raise
|
|
73
|
+
|
|
74
|
+
return wrapper
|
|
75
|
+
|
|
76
|
+
return decorator
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _is_daemon_routing_enabled() -> bool:
|
|
80
|
+
"""Check if daemon routing is enabled in configuration"""
|
|
81
|
+
# Check environment variable
|
|
82
|
+
if os.environ.get("MCLI_DAEMON_ROUTING", "false").lower() in ("true", "1", "yes"):
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
# Check config files
|
|
86
|
+
config_paths = [
|
|
87
|
+
Path("config.toml"), # Current directory
|
|
88
|
+
Path.home() / ".config" / "mcli" / "config.toml", # User config
|
|
89
|
+
Path(__file__).parent.parent.parent.parent.parent / "config.toml", # Project root
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
for path in config_paths:
|
|
93
|
+
if path.exists():
|
|
94
|
+
try:
|
|
95
|
+
daemon_config = read_from_toml(str(path), "api_daemon")
|
|
96
|
+
if daemon_config and daemon_config.get("enabled", False):
|
|
97
|
+
return True
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.debug(f"Could not read daemon config from {path}: {e}")
|
|
100
|
+
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _convert_to_command_args(args: tuple, kwargs: dict, func: Callable) -> List[str]:
|
|
105
|
+
"""Convert function arguments to command line arguments"""
|
|
106
|
+
cmd_args = []
|
|
107
|
+
|
|
108
|
+
# Get function signature
|
|
109
|
+
sig = inspect.signature(func)
|
|
110
|
+
bound_args = sig.bind(*args, **kwargs)
|
|
111
|
+
bound_args.apply_defaults()
|
|
112
|
+
|
|
113
|
+
# Convert to command line arguments
|
|
114
|
+
for param_name, param_value in bound_args.arguments.items():
|
|
115
|
+
param = sig.parameters[param_name]
|
|
116
|
+
|
|
117
|
+
# Skip self parameter for methods
|
|
118
|
+
if param_name == "self":
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
# Handle different parameter types
|
|
122
|
+
if param.kind == inspect.Parameter.POSITIONAL_ONLY:
|
|
123
|
+
# Positional argument
|
|
124
|
+
if param_value is not None:
|
|
125
|
+
cmd_args.append(str(param_value))
|
|
126
|
+
elif param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
|
|
127
|
+
# Can be positional or keyword
|
|
128
|
+
if param_value is not None:
|
|
129
|
+
if param.default == inspect.Parameter.empty:
|
|
130
|
+
# Required argument
|
|
131
|
+
cmd_args.append(str(param_value))
|
|
132
|
+
else:
|
|
133
|
+
# Optional argument with value
|
|
134
|
+
cmd_args.extend([f"--{param_name}", str(param_value)])
|
|
135
|
+
elif param.kind == inspect.Parameter.KEYWORD_ONLY:
|
|
136
|
+
# Keyword-only argument
|
|
137
|
+
if param_value is not None and param_value != param.default:
|
|
138
|
+
cmd_args.extend([f"--{param_name}", str(param_value)])
|
|
139
|
+
elif param.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
140
|
+
# *args
|
|
141
|
+
if isinstance(param_value, (list, tuple)):
|
|
142
|
+
cmd_args.extend([str(arg) for arg in param_value])
|
|
143
|
+
elif param.kind == inspect.Parameter.VAR_KEYWORD:
|
|
144
|
+
# **kwargs
|
|
145
|
+
if isinstance(param_value, dict):
|
|
146
|
+
for key, value in param_value.items():
|
|
147
|
+
cmd_args.extend([f"--{key}", str(value)])
|
|
148
|
+
|
|
149
|
+
return cmd_args
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def daemon_group(
|
|
153
|
+
group_name: Optional[str] = None, auto_route: bool = True, fallback_to_local: bool = True
|
|
154
|
+
):
|
|
155
|
+
"""
|
|
156
|
+
Decorator to route Click groups to the API daemon when enabled.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
group_name: Name of the group (defaults to function name)
|
|
160
|
+
auto_route: Whether to automatically route to daemon if enabled
|
|
161
|
+
fallback_to_local: Whether to fallback to local execution if daemon fails
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
def decorator(func: Callable) -> Callable:
|
|
165
|
+
# Get group name
|
|
166
|
+
grp_name = group_name or func.__name__
|
|
167
|
+
|
|
168
|
+
@functools.wraps(func)
|
|
169
|
+
def wrapper(*args, **kwargs):
|
|
170
|
+
# Check if daemon routing is enabled
|
|
171
|
+
if not auto_route or not _is_daemon_routing_enabled():
|
|
172
|
+
return func(*args, **kwargs)
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
# Try to execute via daemon
|
|
176
|
+
client = get_daemon_client()
|
|
177
|
+
|
|
178
|
+
# For groups, we'll just check if the daemon is available
|
|
179
|
+
# and then fall back to local execution
|
|
180
|
+
if client.is_running():
|
|
181
|
+
logger.info(
|
|
182
|
+
f"Daemon is running, but group '{grp_name}' will be executed locally"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
return func(*args, **kwargs)
|
|
186
|
+
|
|
187
|
+
except Exception as e:
|
|
188
|
+
logger.warning(f"Failed to check daemon for group '{grp_name}': {e}")
|
|
189
|
+
if fallback_to_local:
|
|
190
|
+
logger.info(f"Falling back to local execution for group '{grp_name}'")
|
|
191
|
+
return func(*args, **kwargs)
|
|
192
|
+
else:
|
|
193
|
+
raise
|
|
194
|
+
|
|
195
|
+
return wrapper
|
|
196
|
+
|
|
197
|
+
return decorator
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# Convenience function to check if daemon is available
|
|
201
|
+
def is_daemon_available() -> bool:
|
|
202
|
+
"""Check if the API daemon is available and running"""
|
|
203
|
+
try:
|
|
204
|
+
client = get_daemon_client()
|
|
205
|
+
return client.is_running()
|
|
206
|
+
except Exception:
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# Convenience function to get daemon status
|
|
211
|
+
def get_daemon_status() -> Optional[Dict[str, Any]]:
|
|
212
|
+
"""Get daemon status if available"""
|
|
213
|
+
try:
|
|
214
|
+
client = get_daemon_client()
|
|
215
|
+
return client.status()
|
|
216
|
+
except Exception:
|
|
217
|
+
return None
|