mcli-framework 7.1.3__py3-none-any.whl → 7.3.1__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/__init__.py +160 -0
- mcli/__main__.py +14 -0
- mcli/app/__init__.py +23 -0
- mcli/app/main.py +10 -0
- mcli/app/model/__init__.py +0 -0
- mcli/app/video/__init__.py +5 -0
- mcli/chat/__init__.py +34 -0
- mcli/lib/__init__.py +0 -0
- mcli/lib/api/__init__.py +0 -0
- mcli/lib/auth/__init__.py +1 -0
- mcli/lib/config/__init__.py +1 -0
- mcli/lib/custom_commands.py +424 -0
- mcli/lib/erd/__init__.py +25 -0
- mcli/lib/files/__init__.py +0 -0
- mcli/lib/fs/__init__.py +1 -0
- mcli/lib/logger/__init__.py +3 -0
- mcli/lib/paths.py +12 -0
- mcli/lib/performance/__init__.py +17 -0
- mcli/lib/pickles/__init__.py +1 -0
- mcli/lib/shell/__init__.py +0 -0
- mcli/lib/toml/__init__.py +1 -0
- mcli/lib/watcher/__init__.py +0 -0
- mcli/ml/__init__.py +16 -0
- mcli/ml/api/__init__.py +30 -0
- mcli/ml/api/routers/__init__.py +27 -0
- mcli/ml/api/schemas.py +2 -2
- mcli/ml/auth/__init__.py +45 -0
- mcli/ml/auth/models.py +2 -2
- mcli/ml/backtesting/__init__.py +39 -0
- mcli/ml/cli/__init__.py +5 -0
- mcli/ml/cli/main.py +1 -1
- mcli/ml/config/__init__.py +33 -0
- mcli/ml/configs/__init__.py +16 -0
- mcli/ml/dashboard/__init__.py +12 -0
- mcli/ml/dashboard/app.py +13 -13
- mcli/ml/dashboard/app_integrated.py +1309 -148
- mcli/ml/dashboard/app_supabase.py +46 -21
- mcli/ml/dashboard/app_training.py +14 -14
- mcli/ml/dashboard/components/__init__.py +7 -0
- mcli/ml/dashboard/components/charts.py +258 -0
- mcli/ml/dashboard/components/metrics.py +125 -0
- mcli/ml/dashboard/components/tables.py +228 -0
- mcli/ml/dashboard/pages/__init__.py +6 -0
- mcli/ml/dashboard/pages/cicd.py +382 -0
- mcli/ml/dashboard/pages/predictions_enhanced.py +834 -0
- mcli/ml/dashboard/pages/scrapers_and_logs.py +1060 -0
- mcli/ml/dashboard/pages/test_portfolio.py +373 -0
- mcli/ml/dashboard/pages/trading.py +714 -0
- mcli/ml/dashboard/pages/workflows.py +533 -0
- mcli/ml/dashboard/utils.py +154 -0
- mcli/ml/data_ingestion/__init__.py +39 -0
- mcli/ml/database/__init__.py +47 -0
- mcli/ml/experimentation/__init__.py +29 -0
- mcli/ml/features/__init__.py +39 -0
- mcli/ml/mlops/__init__.py +33 -0
- mcli/ml/models/__init__.py +94 -0
- mcli/ml/monitoring/__init__.py +25 -0
- mcli/ml/optimization/__init__.py +27 -0
- mcli/ml/predictions/__init__.py +5 -0
- mcli/ml/preprocessing/__init__.py +28 -0
- mcli/ml/scripts/__init__.py +1 -0
- mcli/ml/trading/__init__.py +60 -0
- mcli/ml/trading/alpaca_client.py +353 -0
- mcli/ml/trading/migrations.py +164 -0
- mcli/ml/trading/models.py +418 -0
- mcli/ml/trading/paper_trading.py +326 -0
- mcli/ml/trading/risk_management.py +370 -0
- mcli/ml/trading/trading_service.py +480 -0
- mcli/ml/training/__init__.py +10 -0
- mcli/ml/training/train_model.py +569 -0
- mcli/mygroup/__init__.py +3 -0
- mcli/public/__init__.py +1 -0
- mcli/public/commands/__init__.py +2 -0
- mcli/self/__init__.py +3 -0
- mcli/self/self_cmd.py +579 -91
- mcli/workflow/__init__.py +0 -0
- mcli/workflow/daemon/__init__.py +15 -0
- mcli/workflow/daemon/daemon.py +21 -3
- mcli/workflow/dashboard/__init__.py +5 -0
- mcli/workflow/docker/__init__.py +0 -0
- mcli/workflow/file/__init__.py +0 -0
- mcli/workflow/gcloud/__init__.py +1 -0
- mcli/workflow/git_commit/__init__.py +0 -0
- mcli/workflow/interview/__init__.py +0 -0
- mcli/workflow/politician_trading/__init__.py +4 -0
- mcli/workflow/politician_trading/data_sources.py +259 -1
- mcli/workflow/politician_trading/models.py +159 -1
- mcli/workflow/politician_trading/scrapers_corporate_registry.py +846 -0
- mcli/workflow/politician_trading/scrapers_free_sources.py +516 -0
- mcli/workflow/politician_trading/scrapers_third_party.py +391 -0
- mcli/workflow/politician_trading/seed_database.py +539 -0
- mcli/workflow/registry/__init__.py +0 -0
- mcli/workflow/repo/__init__.py +0 -0
- mcli/workflow/scheduler/__init__.py +25 -0
- mcli/workflow/search/__init__.py +0 -0
- mcli/workflow/sync/__init__.py +5 -0
- mcli/workflow/videos/__init__.py +1 -0
- mcli/workflow/wakatime/__init__.py +80 -0
- mcli/workflow/workflow.py +8 -27
- {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/METADATA +3 -1
- {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/RECORD +105 -29
- mcli/workflow/daemon/api_daemon.py +0 -800
- mcli/workflow/daemon/commands.py +0 -1196
- mcli/workflow/dashboard/dashboard_cmd.py +0 -120
- mcli/workflow/file/file.py +0 -100
- mcli/workflow/git_commit/commands.py +0 -430
- mcli/workflow/politician_trading/commands.py +0 -1939
- mcli/workflow/scheduler/commands.py +0 -493
- mcli/workflow/sync/sync_cmd.py +0 -437
- mcli/workflow/videos/videos.py +0 -242
- {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/WHEEL +0 -0
- {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/entry_points.txt +0 -0
- {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/licenses/LICENSE +0 -0
- {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/top_level.txt +0 -0
|
@@ -1,800 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import functools
|
|
3
|
-
import hashlib
|
|
4
|
-
import inspect
|
|
5
|
-
import json
|
|
6
|
-
import logging
|
|
7
|
-
import os
|
|
8
|
-
import pickle
|
|
9
|
-
import shutil
|
|
10
|
-
import signal
|
|
11
|
-
import sqlite3
|
|
12
|
-
import subprocess
|
|
13
|
-
import sys
|
|
14
|
-
import tempfile
|
|
15
|
-
import threading
|
|
16
|
-
import time
|
|
17
|
-
import uuid
|
|
18
|
-
from dataclasses import asdict, dataclass
|
|
19
|
-
from datetime import datetime
|
|
20
|
-
from pathlib import Path
|
|
21
|
-
from typing import Any, Callable, Dict, List, Optional, Union
|
|
22
|
-
|
|
23
|
-
import click
|
|
24
|
-
import psutil
|
|
25
|
-
import requests
|
|
26
|
-
import uvicorn
|
|
27
|
-
from fastapi import BackgroundTasks, FastAPI, HTTPException, Request
|
|
28
|
-
from fastapi.middleware.cors import CORSMiddleware
|
|
29
|
-
from pydantic import BaseModel, Field
|
|
30
|
-
|
|
31
|
-
from mcli.lib.api.api import find_free_port, get_api_config
|
|
32
|
-
|
|
33
|
-
# Import existing utilities
|
|
34
|
-
from mcli.lib.logger.logger import get_logger
|
|
35
|
-
from mcli.lib.toml.toml import read_from_toml
|
|
36
|
-
|
|
37
|
-
from .process_manager import ProcessManager
|
|
38
|
-
|
|
39
|
-
logger = get_logger(__name__)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
@dataclass
|
|
43
|
-
class APIDaemonConfig:
|
|
44
|
-
"""Configuration for API Daemon"""
|
|
45
|
-
|
|
46
|
-
enabled: bool = False
|
|
47
|
-
host: str = "0.0.0.0"
|
|
48
|
-
port: Optional[int] = None
|
|
49
|
-
use_random_port: bool = True
|
|
50
|
-
debug: bool = False
|
|
51
|
-
auto_start: bool = False
|
|
52
|
-
command_timeout: int = 300 # 5 minutes
|
|
53
|
-
max_concurrent_commands: int = 10
|
|
54
|
-
enable_command_caching: bool = True
|
|
55
|
-
enable_command_history: bool = True
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
class APIDaemonService:
|
|
59
|
-
"""Daemon service that listens for API commands and executes them"""
|
|
60
|
-
|
|
61
|
-
def __init__(self, config_path: Optional[str] = None):
|
|
62
|
-
self.config = self._load_config(config_path)
|
|
63
|
-
self.app = FastAPI(
|
|
64
|
-
title="MCLI API Daemon", description="Daemon service for MCLI commands", version="1.0.0"
|
|
65
|
-
)
|
|
66
|
-
self.server = None
|
|
67
|
-
self.server_thread = None
|
|
68
|
-
self.running = False
|
|
69
|
-
self.command_executors = {}
|
|
70
|
-
self.command_history = []
|
|
71
|
-
self.active_commands = {}
|
|
72
|
-
|
|
73
|
-
# Setup FastAPI app
|
|
74
|
-
self._setup_fastapi_app()
|
|
75
|
-
|
|
76
|
-
# Load command database
|
|
77
|
-
self.db = CommandDatabase()
|
|
78
|
-
|
|
79
|
-
# Initialize process manager
|
|
80
|
-
self.process_manager = ProcessManager()
|
|
81
|
-
|
|
82
|
-
logger.info(f"API Daemon initialized with config: {self.config}")
|
|
83
|
-
|
|
84
|
-
def _load_config(self, config_path: Optional[str] = None) -> APIDaemonConfig:
|
|
85
|
-
"""Load configuration from TOML file"""
|
|
86
|
-
config = APIDaemonConfig()
|
|
87
|
-
|
|
88
|
-
# Try to load from config.toml files
|
|
89
|
-
config_paths = [
|
|
90
|
-
Path("config.toml"), # Current directory
|
|
91
|
-
Path.home() / ".config" / "mcli" / "config.toml", # User config
|
|
92
|
-
Path(__file__).parent.parent.parent.parent.parent / "config.toml", # Project root
|
|
93
|
-
]
|
|
94
|
-
|
|
95
|
-
if config_path:
|
|
96
|
-
config_paths.insert(0, Path(config_path))
|
|
97
|
-
|
|
98
|
-
for path in config_paths:
|
|
99
|
-
if path.exists():
|
|
100
|
-
try:
|
|
101
|
-
daemon_config = read_from_toml(str(path), "api_daemon")
|
|
102
|
-
if daemon_config:
|
|
103
|
-
for key, value in daemon_config.items():
|
|
104
|
-
if hasattr(config, key):
|
|
105
|
-
setattr(config, key, value)
|
|
106
|
-
logger.debug(f"Loaded API daemon config from {path}")
|
|
107
|
-
break
|
|
108
|
-
except Exception as e:
|
|
109
|
-
logger.debug(f"Could not load API daemon config from {path}: {e}")
|
|
110
|
-
|
|
111
|
-
# Override with environment variables
|
|
112
|
-
if os.environ.get("MCLI_API_DAEMON_ENABLED", "false").lower() in ("true", "1", "yes"):
|
|
113
|
-
config.enabled = True
|
|
114
|
-
|
|
115
|
-
if os.environ.get("MCLI_API_DAEMON_HOST"):
|
|
116
|
-
config.host = os.environ.get("MCLI_API_DAEMON_HOST")
|
|
117
|
-
|
|
118
|
-
if os.environ.get("MCLI_API_DAEMON_PORT"):
|
|
119
|
-
config.port = int(os.environ.get("MCLI_API_DAEMON_PORT"))
|
|
120
|
-
config.use_random_port = False
|
|
121
|
-
|
|
122
|
-
if os.environ.get("MCLI_API_DAEMON_DEBUG", "false").lower() in ("true", "1", "yes"):
|
|
123
|
-
config.debug = True
|
|
124
|
-
|
|
125
|
-
if os.environ.get("MCLI_API_DAEMON_AUTO_START", "false").lower() in ("true", "1", "yes"):
|
|
126
|
-
config.auto_start = True
|
|
127
|
-
|
|
128
|
-
return config
|
|
129
|
-
|
|
130
|
-
def _setup_fastapi_app(self):
|
|
131
|
-
"""Setup FastAPI application with endpoints"""
|
|
132
|
-
# Add CORS middleware
|
|
133
|
-
self.app.add_middleware(
|
|
134
|
-
CORSMiddleware,
|
|
135
|
-
allow_origins=["*"],
|
|
136
|
-
allow_credentials=True,
|
|
137
|
-
allow_methods=["*"],
|
|
138
|
-
allow_headers=["*"],
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
# Health check endpoint
|
|
142
|
-
@self.app.get("/health")
|
|
143
|
-
async def health_check():
|
|
144
|
-
return {
|
|
145
|
-
"status": "healthy",
|
|
146
|
-
"service": "MCLI API Daemon",
|
|
147
|
-
"timestamp": datetime.now().isoformat(),
|
|
148
|
-
"active_commands": len(self.active_commands),
|
|
149
|
-
"config": asdict(self.config),
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
# Root endpoint
|
|
153
|
-
@self.app.get("/")
|
|
154
|
-
async def root():
|
|
155
|
-
return {
|
|
156
|
-
"service": "MCLI API Daemon",
|
|
157
|
-
"version": "1.0.0",
|
|
158
|
-
"status": "running" if self.running else "stopped",
|
|
159
|
-
"endpoints": [
|
|
160
|
-
"/health",
|
|
161
|
-
"/status",
|
|
162
|
-
"/commands",
|
|
163
|
-
"/execute",
|
|
164
|
-
"/processes",
|
|
165
|
-
"/processes/{process_id}",
|
|
166
|
-
"/processes/{process_id}/start",
|
|
167
|
-
"/processes/{process_id}/stop",
|
|
168
|
-
"/processes/{process_id}/logs",
|
|
169
|
-
"/daemon/start",
|
|
170
|
-
"/daemon/stop",
|
|
171
|
-
],
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
# Status endpoint
|
|
175
|
-
@self.app.get("/status")
|
|
176
|
-
async def status():
|
|
177
|
-
return {
|
|
178
|
-
"running": self.running,
|
|
179
|
-
"active_commands": len(self.active_commands),
|
|
180
|
-
"command_history_count": len(self.command_history),
|
|
181
|
-
"config": asdict(self.config),
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
# List available commands
|
|
185
|
-
@self.app.get("/commands")
|
|
186
|
-
async def list_commands():
|
|
187
|
-
commands = self.db.get_all_commands()
|
|
188
|
-
return {"commands": [asdict(cmd) for cmd in commands], "total": len(commands)}
|
|
189
|
-
|
|
190
|
-
# Execute command endpoint
|
|
191
|
-
@self.app.post("/execute")
|
|
192
|
-
async def execute_command(request: Request, background_tasks: BackgroundTasks):
|
|
193
|
-
try:
|
|
194
|
-
body = await request.json()
|
|
195
|
-
command_id = body.get("command_id")
|
|
196
|
-
command_name = body.get("command_name")
|
|
197
|
-
args = body.get("args", [])
|
|
198
|
-
timeout = body.get("timeout", self.config.command_timeout)
|
|
199
|
-
|
|
200
|
-
if not command_id and not command_name:
|
|
201
|
-
raise HTTPException(
|
|
202
|
-
status_code=400, detail="Either command_id or command_name must be provided"
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
# Get command from database
|
|
206
|
-
command = None
|
|
207
|
-
if command_id:
|
|
208
|
-
command = self.db.get_command(command_id)
|
|
209
|
-
elif command_name:
|
|
210
|
-
commands = self.db.search_commands(command_name, limit=1)
|
|
211
|
-
if commands:
|
|
212
|
-
command = commands[0]
|
|
213
|
-
|
|
214
|
-
if not command:
|
|
215
|
-
raise HTTPException(status_code=404, detail="Command not found")
|
|
216
|
-
|
|
217
|
-
# Execute command
|
|
218
|
-
result = await self._execute_command_async(command, args, timeout)
|
|
219
|
-
|
|
220
|
-
# Record execution
|
|
221
|
-
self.db.record_execution(
|
|
222
|
-
command.id,
|
|
223
|
-
"success" if result["success"] else "failed",
|
|
224
|
-
result.get("output"),
|
|
225
|
-
result.get("error"),
|
|
226
|
-
result.get("execution_time_ms"),
|
|
227
|
-
)
|
|
228
|
-
|
|
229
|
-
return result
|
|
230
|
-
|
|
231
|
-
except Exception as e:
|
|
232
|
-
logger.error(f"Error executing command: {e}")
|
|
233
|
-
raise HTTPException(status_code=500, detail=str(e))
|
|
234
|
-
|
|
235
|
-
# Process management endpoints (Docker-like API)
|
|
236
|
-
@self.app.get("/processes")
|
|
237
|
-
async def list_processes(request: Request):
|
|
238
|
-
"""List all processes (like 'docker ps')"""
|
|
239
|
-
all_processes = request.query_params.get("all", "false").lower() == "true"
|
|
240
|
-
processes = self.process_manager.list_processes(all_processes=all_processes)
|
|
241
|
-
return {"processes": processes, "total": len(processes)}
|
|
242
|
-
|
|
243
|
-
@self.app.post("/processes")
|
|
244
|
-
async def create_process(request: Request):
|
|
245
|
-
"""Create a new process container"""
|
|
246
|
-
try:
|
|
247
|
-
body = await request.json()
|
|
248
|
-
name = body.get("name", "unnamed")
|
|
249
|
-
command = body.get("command")
|
|
250
|
-
args = body.get("args", [])
|
|
251
|
-
working_dir = body.get("working_dir")
|
|
252
|
-
environment = body.get("environment")
|
|
253
|
-
auto_start = body.get("auto_start", False)
|
|
254
|
-
|
|
255
|
-
if not command:
|
|
256
|
-
raise HTTPException(status_code=400, detail="Command is required")
|
|
257
|
-
|
|
258
|
-
process_id = self.process_manager.create(
|
|
259
|
-
name=name,
|
|
260
|
-
command=command,
|
|
261
|
-
args=args,
|
|
262
|
-
working_dir=working_dir,
|
|
263
|
-
environment=environment,
|
|
264
|
-
)
|
|
265
|
-
|
|
266
|
-
if auto_start:
|
|
267
|
-
self.process_manager.start(process_id)
|
|
268
|
-
|
|
269
|
-
return {
|
|
270
|
-
"id": process_id,
|
|
271
|
-
"name": name,
|
|
272
|
-
"status": "created" if not auto_start else "starting",
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
except Exception as e:
|
|
276
|
-
logger.error(f"Error creating process: {e}")
|
|
277
|
-
raise HTTPException(status_code=500, detail=str(e))
|
|
278
|
-
|
|
279
|
-
@self.app.get("/processes/{process_id}")
|
|
280
|
-
async def inspect_process(process_id: str):
|
|
281
|
-
"""Get detailed information about a process (like 'docker inspect')"""
|
|
282
|
-
info = self.process_manager.inspect(process_id)
|
|
283
|
-
if info is None:
|
|
284
|
-
raise HTTPException(status_code=404, detail="Process not found")
|
|
285
|
-
return info
|
|
286
|
-
|
|
287
|
-
@self.app.post("/processes/{process_id}/start")
|
|
288
|
-
async def start_process(process_id: str):
|
|
289
|
-
"""Start a process container"""
|
|
290
|
-
success = self.process_manager.start(process_id)
|
|
291
|
-
if not success:
|
|
292
|
-
raise HTTPException(status_code=404, detail="Process not found or failed to start")
|
|
293
|
-
return {"status": "started"}
|
|
294
|
-
|
|
295
|
-
@self.app.post("/processes/{process_id}/stop")
|
|
296
|
-
async def stop_process(process_id: str, request: Request):
|
|
297
|
-
"""Stop a process container"""
|
|
298
|
-
body = (
|
|
299
|
-
await request.json()
|
|
300
|
-
if request.headers.get("content-type") == "application/json"
|
|
301
|
-
else {}
|
|
302
|
-
)
|
|
303
|
-
timeout = body.get("timeout", 10)
|
|
304
|
-
|
|
305
|
-
success = self.process_manager.stop(process_id, timeout)
|
|
306
|
-
if not success:
|
|
307
|
-
raise HTTPException(status_code=404, detail="Process not found")
|
|
308
|
-
return {"status": "stopped"}
|
|
309
|
-
|
|
310
|
-
@self.app.post("/processes/{process_id}/kill")
|
|
311
|
-
async def kill_process(process_id: str):
|
|
312
|
-
"""Kill a process container"""
|
|
313
|
-
success = self.process_manager.kill(process_id)
|
|
314
|
-
if not success:
|
|
315
|
-
raise HTTPException(status_code=404, detail="Process not found")
|
|
316
|
-
return {"status": "killed"}
|
|
317
|
-
|
|
318
|
-
@self.app.delete("/processes/{process_id}")
|
|
319
|
-
async def remove_process(process_id: str, request: Request):
|
|
320
|
-
"""Remove a process container"""
|
|
321
|
-
force = request.query_params.get("force", "false").lower() == "true"
|
|
322
|
-
success = self.process_manager.remove(process_id, force)
|
|
323
|
-
if not success:
|
|
324
|
-
raise HTTPException(status_code=404, detail="Process not found")
|
|
325
|
-
return {"status": "removed"}
|
|
326
|
-
|
|
327
|
-
@self.app.get("/processes/{process_id}/logs")
|
|
328
|
-
async def get_process_logs(process_id: str, request: Request):
|
|
329
|
-
"""Get logs from a process container (like 'docker logs')"""
|
|
330
|
-
lines = request.query_params.get("lines")
|
|
331
|
-
if lines:
|
|
332
|
-
try:
|
|
333
|
-
lines = int(lines)
|
|
334
|
-
except ValueError:
|
|
335
|
-
lines = None
|
|
336
|
-
|
|
337
|
-
logs = self.process_manager.logs(process_id, lines)
|
|
338
|
-
if logs is None:
|
|
339
|
-
raise HTTPException(status_code=404, detail="Process not found")
|
|
340
|
-
return logs
|
|
341
|
-
|
|
342
|
-
@self.app.post("/processes/run")
|
|
343
|
-
async def run_process(request: Request):
|
|
344
|
-
"""Create and start a process in one step (like 'docker run')"""
|
|
345
|
-
try:
|
|
346
|
-
body = await request.json()
|
|
347
|
-
name = body.get("name", "unnamed")
|
|
348
|
-
command = body.get("command")
|
|
349
|
-
args = body.get("args", [])
|
|
350
|
-
working_dir = body.get("working_dir")
|
|
351
|
-
environment = body.get("environment")
|
|
352
|
-
detach = body.get("detach", True)
|
|
353
|
-
|
|
354
|
-
if not command:
|
|
355
|
-
raise HTTPException(status_code=400, detail="Command is required")
|
|
356
|
-
|
|
357
|
-
process_id = self.process_manager.run(
|
|
358
|
-
name=name,
|
|
359
|
-
command=command,
|
|
360
|
-
args=args,
|
|
361
|
-
working_dir=working_dir,
|
|
362
|
-
environment=environment,
|
|
363
|
-
detach=detach,
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
return {"id": process_id, "name": name, "status": "running"}
|
|
367
|
-
|
|
368
|
-
except Exception as e:
|
|
369
|
-
logger.error(f"Error running process: {e}")
|
|
370
|
-
raise HTTPException(status_code=500, detail=str(e))
|
|
371
|
-
|
|
372
|
-
# Daemon control endpoints
|
|
373
|
-
@self.app.post("/daemon/start")
|
|
374
|
-
async def start_daemon():
|
|
375
|
-
if self.running:
|
|
376
|
-
return {"status": "already_running"}
|
|
377
|
-
|
|
378
|
-
self.start()
|
|
379
|
-
return {"status": "started", "url": f"http://{self.config.host}:{self.config.port}"}
|
|
380
|
-
|
|
381
|
-
@self.app.post("/daemon/stop")
|
|
382
|
-
async def stop_daemon():
|
|
383
|
-
if not self.running:
|
|
384
|
-
return {"status": "already_stopped"}
|
|
385
|
-
|
|
386
|
-
self.stop()
|
|
387
|
-
return {"status": "stopped"}
|
|
388
|
-
|
|
389
|
-
async def _execute_command_async(
|
|
390
|
-
self, command: "Command", args: List[str], timeout: int
|
|
391
|
-
) -> Dict[str, Any]:
|
|
392
|
-
"""Execute command asynchronously"""
|
|
393
|
-
command_id = str(uuid.uuid4())
|
|
394
|
-
start_time = time.time()
|
|
395
|
-
|
|
396
|
-
try:
|
|
397
|
-
# Add to active commands
|
|
398
|
-
self.active_commands[command_id] = {
|
|
399
|
-
"command": command,
|
|
400
|
-
"args": args,
|
|
401
|
-
"start_time": start_time,
|
|
402
|
-
"status": "running",
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
# Execute command in thread pool
|
|
406
|
-
loop = asyncio.get_event_loop()
|
|
407
|
-
result = await loop.run_in_executor(
|
|
408
|
-
None, self._execute_command_sync, command, args, timeout
|
|
409
|
-
)
|
|
410
|
-
|
|
411
|
-
execution_time = (time.time() - start_time) * 1000
|
|
412
|
-
|
|
413
|
-
# Update active commands
|
|
414
|
-
self.active_commands[command_id]["status"] = "completed"
|
|
415
|
-
self.active_commands[command_id]["result"] = result
|
|
416
|
-
self.active_commands[command_id]["execution_time"] = execution_time
|
|
417
|
-
|
|
418
|
-
# Add to history
|
|
419
|
-
if self.config.enable_command_history:
|
|
420
|
-
self.command_history.append(
|
|
421
|
-
{
|
|
422
|
-
"id": command_id,
|
|
423
|
-
"command": command,
|
|
424
|
-
"args": args,
|
|
425
|
-
"result": result,
|
|
426
|
-
"execution_time": execution_time,
|
|
427
|
-
"timestamp": datetime.now().isoformat(),
|
|
428
|
-
}
|
|
429
|
-
)
|
|
430
|
-
|
|
431
|
-
result["execution_time_ms"] = int(execution_time)
|
|
432
|
-
return result
|
|
433
|
-
|
|
434
|
-
except Exception as e:
|
|
435
|
-
execution_time = (time.time() - start_time) * 1000
|
|
436
|
-
|
|
437
|
-
# Update active commands
|
|
438
|
-
self.active_commands[command_id]["status"] = "failed"
|
|
439
|
-
self.active_commands[command_id]["error"] = str(e)
|
|
440
|
-
|
|
441
|
-
return {"success": False, "error": str(e), "execution_time_ms": int(execution_time)}
|
|
442
|
-
finally:
|
|
443
|
-
# Clean up active command after timeout
|
|
444
|
-
def cleanup():
|
|
445
|
-
time.sleep(300) # 5 minutes
|
|
446
|
-
if command_id in self.active_commands:
|
|
447
|
-
del self.active_commands[command_id]
|
|
448
|
-
|
|
449
|
-
threading.Thread(target=cleanup, daemon=True).start()
|
|
450
|
-
|
|
451
|
-
def _execute_command_sync(
|
|
452
|
-
self, command: "Command", args: List[str], timeout: int
|
|
453
|
-
) -> Dict[str, Any]:
|
|
454
|
-
"""Execute command synchronously"""
|
|
455
|
-
executor = CommandExecutor()
|
|
456
|
-
return executor.execute_command(command, args)
|
|
457
|
-
|
|
458
|
-
def start(self):
|
|
459
|
-
"""Start the API daemon server"""
|
|
460
|
-
if self.running:
|
|
461
|
-
logger.warning("API daemon is already running")
|
|
462
|
-
return
|
|
463
|
-
|
|
464
|
-
# Determine port
|
|
465
|
-
port = self.config.port
|
|
466
|
-
if port is None and self.config.use_random_port:
|
|
467
|
-
port = find_free_port()
|
|
468
|
-
self.config.port = port
|
|
469
|
-
|
|
470
|
-
if port is None:
|
|
471
|
-
port = 8000
|
|
472
|
-
|
|
473
|
-
# Start server in background thread
|
|
474
|
-
def run_server():
|
|
475
|
-
uvicorn.run(
|
|
476
|
-
self.app,
|
|
477
|
-
host=self.config.host,
|
|
478
|
-
port=int(port),
|
|
479
|
-
log_level="error", # Suppress info messages
|
|
480
|
-
access_log=False, # Suppress access logs
|
|
481
|
-
)
|
|
482
|
-
|
|
483
|
-
self.server_thread = threading.Thread(target=run_server, daemon=True)
|
|
484
|
-
self.server_thread.start()
|
|
485
|
-
|
|
486
|
-
# Wait for server to be ready by checking health endpoint
|
|
487
|
-
server_url = f"http://{self.config.host}:{port}"
|
|
488
|
-
max_wait = 10 # Maximum wait time in seconds
|
|
489
|
-
wait_interval = 0.5 # Check every 0.5 seconds
|
|
490
|
-
|
|
491
|
-
for attempt in range(int(max_wait / wait_interval)):
|
|
492
|
-
time.sleep(wait_interval)
|
|
493
|
-
try:
|
|
494
|
-
response = requests.get(f"{server_url}/health", timeout=1)
|
|
495
|
-
if response.status_code == 200:
|
|
496
|
-
self.running = True
|
|
497
|
-
logger.info(f"API daemon started on {server_url}")
|
|
498
|
-
return
|
|
499
|
-
except requests.exceptions.RequestException:
|
|
500
|
-
continue # Server not ready yet
|
|
501
|
-
|
|
502
|
-
# If we get here, the server didn't start properly
|
|
503
|
-
logger.error(f"Failed to start API daemon on {server_url} after {max_wait} seconds")
|
|
504
|
-
self.running = False
|
|
505
|
-
|
|
506
|
-
def stop(self):
|
|
507
|
-
"""Stop the API daemon server"""
|
|
508
|
-
if not self.running:
|
|
509
|
-
logger.warning("API daemon is not running")
|
|
510
|
-
return
|
|
511
|
-
|
|
512
|
-
self.running = False
|
|
513
|
-
logger.info("API daemon stopped")
|
|
514
|
-
|
|
515
|
-
def status(self) -> Dict[str, Any]:
|
|
516
|
-
"""Get daemon status"""
|
|
517
|
-
return {
|
|
518
|
-
"running": self.running,
|
|
519
|
-
"config": asdict(self.config),
|
|
520
|
-
"active_commands": len(self.active_commands),
|
|
521
|
-
"command_history_count": len(self.command_history),
|
|
522
|
-
"database_commands": len(self.db.get_all_commands()),
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
# Import the existing Command and CommandDatabase classes
|
|
527
|
-
from mcli.workflow.daemon.commands import Command, CommandDatabase, CommandExecutor
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
@click.group(name="api-daemon")
|
|
531
|
-
def api_daemon():
|
|
532
|
-
"""API Daemon service for MCLI commands"""
|
|
533
|
-
pass
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
@api_daemon.command()
|
|
537
|
-
@click.option("--config", help="Path to configuration file")
|
|
538
|
-
@click.option("--host", help="Host to bind to")
|
|
539
|
-
@click.option("--port", type=int, help="Port to bind to")
|
|
540
|
-
@click.option("--debug", is_flag=True, help="Enable debug mode")
|
|
541
|
-
@click.option("--background", "-b", is_flag=True, help="Run daemon in background")
|
|
542
|
-
@click.option("--pid-file", help="Path to PID file for background daemon")
|
|
543
|
-
def start(
|
|
544
|
-
config: Optional[str],
|
|
545
|
-
host: Optional[str],
|
|
546
|
-
port: Optional[int],
|
|
547
|
-
debug: bool,
|
|
548
|
-
background: bool,
|
|
549
|
-
pid_file: Optional[str],
|
|
550
|
-
):
|
|
551
|
-
"""Start the API daemon service"""
|
|
552
|
-
daemon = APIDaemonService(config)
|
|
553
|
-
|
|
554
|
-
# Override config with command line options
|
|
555
|
-
if host:
|
|
556
|
-
daemon.config.host = host
|
|
557
|
-
if port:
|
|
558
|
-
daemon.config.port = port
|
|
559
|
-
daemon.config.use_random_port = False
|
|
560
|
-
if debug:
|
|
561
|
-
daemon.config.debug = debug
|
|
562
|
-
|
|
563
|
-
logger.info("Starting API daemon service...")
|
|
564
|
-
|
|
565
|
-
if background:
|
|
566
|
-
# Run in background
|
|
567
|
-
import os
|
|
568
|
-
import sys
|
|
569
|
-
|
|
570
|
-
# Fork the process
|
|
571
|
-
try:
|
|
572
|
-
pid = os.fork()
|
|
573
|
-
if pid > 0:
|
|
574
|
-
# Parent process - exit
|
|
575
|
-
logger.info(f"API daemon started in background with PID {pid}")
|
|
576
|
-
if pid_file:
|
|
577
|
-
with open(pid_file, "w") as f:
|
|
578
|
-
f.write(str(pid))
|
|
579
|
-
logger.info(f"PID written to {pid_file}")
|
|
580
|
-
sys.exit(0)
|
|
581
|
-
else:
|
|
582
|
-
# Child process - run daemon
|
|
583
|
-
# Detach from terminal
|
|
584
|
-
os.setsid()
|
|
585
|
-
os.chdir("/")
|
|
586
|
-
os.umask(0)
|
|
587
|
-
|
|
588
|
-
# Redirect output to /dev/null
|
|
589
|
-
sys.stdout.flush()
|
|
590
|
-
sys.stderr.flush()
|
|
591
|
-
with open("/dev/null", "r") as dev_null_r:
|
|
592
|
-
os.dup2(dev_null_r.fileno(), sys.stdin.fileno())
|
|
593
|
-
with open("/dev/null", "a+") as dev_null_w:
|
|
594
|
-
os.dup2(dev_null_w.fileno(), sys.stdout.fileno())
|
|
595
|
-
os.dup2(dev_null_w.fileno(), sys.stderr.fileno())
|
|
596
|
-
|
|
597
|
-
# Start daemon
|
|
598
|
-
daemon.start()
|
|
599
|
-
|
|
600
|
-
# Keep running in background
|
|
601
|
-
try:
|
|
602
|
-
while daemon.running:
|
|
603
|
-
time.sleep(1)
|
|
604
|
-
except KeyboardInterrupt:
|
|
605
|
-
logger.info("Received interrupt, shutting down...")
|
|
606
|
-
daemon.stop()
|
|
607
|
-
except OSError as e:
|
|
608
|
-
logger.error(f"Failed to start daemon in background: {e}")
|
|
609
|
-
sys.exit(1)
|
|
610
|
-
else:
|
|
611
|
-
# Run in foreground
|
|
612
|
-
daemon.start()
|
|
613
|
-
|
|
614
|
-
try:
|
|
615
|
-
# Keep the main thread alive
|
|
616
|
-
while daemon.running:
|
|
617
|
-
time.sleep(1)
|
|
618
|
-
except KeyboardInterrupt:
|
|
619
|
-
logger.info("Received interrupt, shutting down...")
|
|
620
|
-
daemon.stop()
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
@api_daemon.command()
|
|
624
|
-
@click.option("--pid-file", help="Path to PID file for background daemon")
|
|
625
|
-
def restart(pid_file: Optional[str]):
|
|
626
|
-
"""Restart the API daemon service"""
|
|
627
|
-
logger.info("Restarting API daemon service...")
|
|
628
|
-
|
|
629
|
-
# Stop the daemon
|
|
630
|
-
stop(pid_file)
|
|
631
|
-
|
|
632
|
-
# Wait a moment for shutdown
|
|
633
|
-
time.sleep(2)
|
|
634
|
-
|
|
635
|
-
# Start the daemon again
|
|
636
|
-
# Note: This will start in foreground mode
|
|
637
|
-
# For background restart, user should use: start --background --pid-file <file>
|
|
638
|
-
logger.info("Starting daemon in foreground mode...")
|
|
639
|
-
daemon = APIDaemonService()
|
|
640
|
-
daemon.start()
|
|
641
|
-
|
|
642
|
-
try:
|
|
643
|
-
while daemon.running:
|
|
644
|
-
time.sleep(1)
|
|
645
|
-
except KeyboardInterrupt:
|
|
646
|
-
logger.info("Received interrupt, shutting down...")
|
|
647
|
-
daemon.stop()
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
@api_daemon.command()
|
|
651
|
-
@click.option("--pid-file", help="Path to PID file for background daemon")
|
|
652
|
-
def stop(pid_file: Optional[str]):
|
|
653
|
-
"""Stop the API daemon service"""
|
|
654
|
-
# Try to stop via HTTP request first
|
|
655
|
-
try:
|
|
656
|
-
response = requests.post("http://localhost:8000/daemon/stop")
|
|
657
|
-
if response.status_code == 200:
|
|
658
|
-
logger.info("API daemon stopped successfully via HTTP")
|
|
659
|
-
return
|
|
660
|
-
except requests.exceptions.RequestException:
|
|
661
|
-
logger.debug("Could not connect to API daemon via HTTP")
|
|
662
|
-
|
|
663
|
-
# If HTTP stop failed, try PID file method
|
|
664
|
-
if pid_file and Path(pid_file).exists():
|
|
665
|
-
try:
|
|
666
|
-
with open(pid_file, "r") as f:
|
|
667
|
-
pid = int(f.read().strip())
|
|
668
|
-
|
|
669
|
-
# Send SIGTERM to the process
|
|
670
|
-
os.kill(pid, signal.SIGTERM)
|
|
671
|
-
logger.info(f"Sent SIGTERM to daemon process {pid}")
|
|
672
|
-
|
|
673
|
-
# Wait a moment and check if process is still running
|
|
674
|
-
time.sleep(2)
|
|
675
|
-
try:
|
|
676
|
-
os.kill(pid, 0) # Check if process exists
|
|
677
|
-
# Process still running, send SIGKILL
|
|
678
|
-
os.kill(pid, signal.SIGKILL)
|
|
679
|
-
logger.info(f"Sent SIGKILL to daemon process {pid}")
|
|
680
|
-
except OSError:
|
|
681
|
-
# Process already terminated
|
|
682
|
-
pass
|
|
683
|
-
|
|
684
|
-
# Remove PID file
|
|
685
|
-
Path(pid_file).unlink()
|
|
686
|
-
logger.info(f"Removed PID file {pid_file}")
|
|
687
|
-
|
|
688
|
-
except (ValueError, OSError) as e:
|
|
689
|
-
logger.error(f"Failed to stop daemon using PID file: {e}")
|
|
690
|
-
else:
|
|
691
|
-
logger.error("Could not stop API daemon - no PID file provided and HTTP connection failed")
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
@api_daemon.command()
|
|
695
|
-
@click.option("--pid-file", help="Path to PID file for background daemon")
|
|
696
|
-
def status(pid_file: Optional[str]):
|
|
697
|
-
"""Show API daemon status"""
|
|
698
|
-
# Check if daemon is running via HTTP
|
|
699
|
-
try:
|
|
700
|
-
response = requests.get("http://localhost:8000/status")
|
|
701
|
-
if response.status_code == 200:
|
|
702
|
-
status_data = response.json()
|
|
703
|
-
logger.info(f"API Daemon Status:")
|
|
704
|
-
logger.info(f" Running: {status_data['running']}")
|
|
705
|
-
logger.info(f" Active Commands: {status_data['active_commands']}")
|
|
706
|
-
logger.info(f" Command History: {status_data['command_history_count']}")
|
|
707
|
-
logger.info(f" Config: {status_data['config']}")
|
|
708
|
-
return
|
|
709
|
-
except requests.exceptions.RequestException:
|
|
710
|
-
logger.debug("Could not connect to API daemon via HTTP")
|
|
711
|
-
|
|
712
|
-
# Check if background daemon is running via PID file
|
|
713
|
-
if pid_file and Path(pid_file).exists():
|
|
714
|
-
try:
|
|
715
|
-
with open(pid_file, "r") as f:
|
|
716
|
-
pid = int(f.read().strip())
|
|
717
|
-
|
|
718
|
-
# Check if process is running
|
|
719
|
-
try:
|
|
720
|
-
os.kill(pid, 0) # Check if process exists
|
|
721
|
-
logger.info(f"API Daemon Status:")
|
|
722
|
-
logger.info(f" Running: True (PID: {pid})")
|
|
723
|
-
logger.info(f" PID File: {pid_file}")
|
|
724
|
-
logger.info(f" Note: Daemon is running in background mode")
|
|
725
|
-
return
|
|
726
|
-
except OSError:
|
|
727
|
-
logger.info(f"API Daemon Status:")
|
|
728
|
-
logger.info(f" Running: False")
|
|
729
|
-
logger.info(f" Note: PID file exists but process is not running")
|
|
730
|
-
# Remove stale PID file
|
|
731
|
-
Path(pid_file).unlink()
|
|
732
|
-
logger.info(f" Removed stale PID file {pid_file}")
|
|
733
|
-
return
|
|
734
|
-
except (ValueError, OSError) as e:
|
|
735
|
-
logger.error(f"Failed to read PID file: {e}")
|
|
736
|
-
|
|
737
|
-
logger.info("API Daemon Status:")
|
|
738
|
-
logger.info(" Running: False")
|
|
739
|
-
logger.info(" Note: No daemon process found")
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
@api_daemon.command()
|
|
743
|
-
@click.option("--command-name", help="Name of the command to execute")
|
|
744
|
-
@click.option("--command-id", help="ID of the command to execute")
|
|
745
|
-
@click.option("--args", "-a", multiple=True, help="Command arguments")
|
|
746
|
-
@click.option("--timeout", type=int, help="Command timeout in seconds")
|
|
747
|
-
def execute(
|
|
748
|
-
command_name: Optional[str], command_id: Optional[str], args: tuple, timeout: Optional[int]
|
|
749
|
-
):
|
|
750
|
-
"""Execute a command via the API daemon"""
|
|
751
|
-
if not command_name and not command_id:
|
|
752
|
-
logger.error("Either --command-name or --command-id must be provided")
|
|
753
|
-
return
|
|
754
|
-
|
|
755
|
-
try:
|
|
756
|
-
# Try to execute via HTTP API
|
|
757
|
-
response = requests.post(
|
|
758
|
-
"http://localhost:8000/execute",
|
|
759
|
-
json={
|
|
760
|
-
"command_name": command_name,
|
|
761
|
-
"command_id": command_id,
|
|
762
|
-
"args": list(args),
|
|
763
|
-
"timeout": timeout,
|
|
764
|
-
},
|
|
765
|
-
)
|
|
766
|
-
|
|
767
|
-
if response.status_code == 200:
|
|
768
|
-
result = response.json()
|
|
769
|
-
if result.get("success"):
|
|
770
|
-
logger.info("✅ Command executed successfully")
|
|
771
|
-
if result.get("output"):
|
|
772
|
-
logger.info("Output:")
|
|
773
|
-
print(result["output"])
|
|
774
|
-
else:
|
|
775
|
-
logger.error("❌ Command execution failed")
|
|
776
|
-
if result.get("error"):
|
|
777
|
-
logger.error(f"Error: {result['error']}")
|
|
778
|
-
else:
|
|
779
|
-
logger.error(f"Failed to execute command: {response.status_code}")
|
|
780
|
-
|
|
781
|
-
except requests.exceptions.RequestException as e:
|
|
782
|
-
logger.error(f"Could not connect to API daemon: {e}")
|
|
783
|
-
logger.error("Make sure the API daemon is running")
|
|
784
|
-
logger.error("Start it with: python -m mcli workflow api-daemon start")
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
@api_daemon.command()
|
|
788
|
-
def commands():
|
|
789
|
-
"""List available commands"""
|
|
790
|
-
try:
|
|
791
|
-
response = requests.get("http://localhost:8000/commands")
|
|
792
|
-
if response.status_code == 200:
|
|
793
|
-
data = response.json()
|
|
794
|
-
logger.info(f"Available Commands ({data['total']}):")
|
|
795
|
-
for cmd in data["commands"]:
|
|
796
|
-
logger.info(f" {cmd['name']}: {cmd['description']}")
|
|
797
|
-
else:
|
|
798
|
-
logger.error("Failed to get commands")
|
|
799
|
-
except requests.exceptions.RequestException:
|
|
800
|
-
logger.error("Could not connect to API daemon")
|