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,387 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Redis Service Manager - Manages Redis as a background process
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
import signal
|
|
8
|
+
import subprocess
|
|
9
|
+
import tempfile
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, Optional
|
|
13
|
+
|
|
14
|
+
import psutil
|
|
15
|
+
import redis.asyncio as redis
|
|
16
|
+
|
|
17
|
+
from mcli.lib.logger.logger import get_logger
|
|
18
|
+
from mcli.workflow.daemon.async_process_manager import AsyncProcessManager, ProcessStatus
|
|
19
|
+
|
|
20
|
+
logger = get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RedisService:
|
|
24
|
+
"""
|
|
25
|
+
Manages Redis server as a background process integrated with MCLI's job system
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
port: int = 6379,
|
|
31
|
+
host: str = "127.0.0.1",
|
|
32
|
+
data_dir: Optional[Path] = None,
|
|
33
|
+
process_manager: Optional[AsyncProcessManager] = None,
|
|
34
|
+
):
|
|
35
|
+
self.port = port
|
|
36
|
+
self.host = host
|
|
37
|
+
self.data_dir = data_dir or Path.home() / ".mcli" / "redis-data"
|
|
38
|
+
self.config_file = None
|
|
39
|
+
self.process_manager = process_manager or AsyncProcessManager()
|
|
40
|
+
self.process_id = None
|
|
41
|
+
self.redis_client = None
|
|
42
|
+
|
|
43
|
+
# Ensure data directory exists
|
|
44
|
+
self.data_dir.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
|
|
46
|
+
async def start(self) -> bool:
|
|
47
|
+
"""Start Redis server as a managed background process"""
|
|
48
|
+
try:
|
|
49
|
+
# Check if Redis is already running on this port
|
|
50
|
+
if await self.is_running():
|
|
51
|
+
logger.info(f"Redis already running on {self.host}:{self.port}")
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
# Find Redis server executable
|
|
55
|
+
redis_server_path = self._find_redis_server()
|
|
56
|
+
if not redis_server_path:
|
|
57
|
+
logger.error("Redis server not found. Install with: brew install redis")
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
# Generate Redis configuration
|
|
61
|
+
config_file = await self._generate_config()
|
|
62
|
+
|
|
63
|
+
# Start Redis process through the process manager
|
|
64
|
+
command_args = [str(redis_server_path), str(config_file)]
|
|
65
|
+
|
|
66
|
+
logger.info(f"Starting Redis server: {' '.join(command_args)}")
|
|
67
|
+
|
|
68
|
+
self.process_id = await self.process_manager.start_process(
|
|
69
|
+
name="redis-server",
|
|
70
|
+
command=str(redis_server_path),
|
|
71
|
+
args=command_args[1:], # Skip the executable name
|
|
72
|
+
working_dir=str(self.data_dir),
|
|
73
|
+
environment={"REDIS_PORT": str(self.port)},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Wait a moment for startup
|
|
77
|
+
await asyncio.sleep(1)
|
|
78
|
+
|
|
79
|
+
# Verify Redis is running
|
|
80
|
+
if await self.is_running():
|
|
81
|
+
logger.info(f"✅ Redis server started successfully on {self.host}:{self.port}")
|
|
82
|
+
logger.info(f" Process ID: {self.process_id}")
|
|
83
|
+
logger.info(f" Data directory: {self.data_dir}")
|
|
84
|
+
return True
|
|
85
|
+
else:
|
|
86
|
+
logger.error("Failed to start Redis server")
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
except Exception as e:
|
|
90
|
+
logger.error(f"Error starting Redis server: {e}")
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
async def stop(self) -> bool:
|
|
94
|
+
"""Stop Redis server"""
|
|
95
|
+
try:
|
|
96
|
+
if self.process_id:
|
|
97
|
+
# Use process manager to stop cleanly
|
|
98
|
+
success = await self.process_manager.stop_process(self.process_id)
|
|
99
|
+
if success:
|
|
100
|
+
logger.info("✅ Redis server stopped successfully")
|
|
101
|
+
self.process_id = None
|
|
102
|
+
return True
|
|
103
|
+
else:
|
|
104
|
+
logger.warning("Process manager couldn't stop Redis, trying direct approach")
|
|
105
|
+
|
|
106
|
+
# Fallback: find and stop Redis process directly
|
|
107
|
+
return await self._stop_redis_direct()
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.error(f"Error stopping Redis server: {e}")
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
async def restart(self) -> bool:
|
|
114
|
+
"""Restart Redis server"""
|
|
115
|
+
logger.info("Restarting Redis server...")
|
|
116
|
+
await self.stop()
|
|
117
|
+
await asyncio.sleep(1)
|
|
118
|
+
return await self.start()
|
|
119
|
+
|
|
120
|
+
async def is_running(self) -> bool:
|
|
121
|
+
"""Check if Redis server is running and accepting connections"""
|
|
122
|
+
try:
|
|
123
|
+
if not self.redis_client:
|
|
124
|
+
self.redis_client = redis.Redis(
|
|
125
|
+
host=self.host,
|
|
126
|
+
port=self.port,
|
|
127
|
+
decode_responses=True,
|
|
128
|
+
socket_connect_timeout=2,
|
|
129
|
+
socket_timeout=2,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
await self.redis_client.ping()
|
|
133
|
+
return True
|
|
134
|
+
except (redis.ConnectionError, redis.TimeoutError, asyncio.TimeoutError):
|
|
135
|
+
return False
|
|
136
|
+
except Exception as e:
|
|
137
|
+
logger.debug(f"Redis connection check failed: {e}")
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
async def get_status(self) -> Dict[str, Any]:
|
|
141
|
+
"""Get detailed Redis server status"""
|
|
142
|
+
status = {
|
|
143
|
+
"running": False,
|
|
144
|
+
"host": self.host,
|
|
145
|
+
"port": self.port,
|
|
146
|
+
"data_dir": str(self.data_dir),
|
|
147
|
+
"process_id": self.process_id,
|
|
148
|
+
"process_status": None,
|
|
149
|
+
"memory_usage": None,
|
|
150
|
+
"connected_clients": None,
|
|
151
|
+
"uptime": None,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# Check if running
|
|
155
|
+
status["running"] = await self.is_running()
|
|
156
|
+
|
|
157
|
+
if not status["running"]:
|
|
158
|
+
return status
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
# Get process information from process manager
|
|
162
|
+
if self.process_id:
|
|
163
|
+
process_info = await self.process_manager.get_process_info(self.process_id)
|
|
164
|
+
if process_info:
|
|
165
|
+
status["process_status"] = process_info.status.value
|
|
166
|
+
|
|
167
|
+
# Get Redis server info
|
|
168
|
+
if self.redis_client:
|
|
169
|
+
info = await self.redis_client.info()
|
|
170
|
+
status.update(
|
|
171
|
+
{
|
|
172
|
+
"memory_usage": info.get("used_memory_human", "unknown"),
|
|
173
|
+
"connected_clients": info.get("connected_clients", 0),
|
|
174
|
+
"uptime": info.get("uptime_in_seconds", 0),
|
|
175
|
+
"version": info.get("redis_version", "unknown"),
|
|
176
|
+
"total_commands": info.get("total_commands_processed", 0),
|
|
177
|
+
"keyspace_hits": info.get("keyspace_hits", 0),
|
|
178
|
+
"keyspace_misses": info.get("keyspace_misses", 0),
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
except Exception as e:
|
|
183
|
+
logger.debug(f"Failed to get Redis status details: {e}")
|
|
184
|
+
|
|
185
|
+
return status
|
|
186
|
+
|
|
187
|
+
async def get_connection_url(self) -> str:
|
|
188
|
+
"""Get Redis connection URL"""
|
|
189
|
+
return f"redis://{self.host}:{self.port}"
|
|
190
|
+
|
|
191
|
+
async def test_connection(self) -> Dict[str, Any]:
|
|
192
|
+
"""Test Redis connection and performance"""
|
|
193
|
+
if not await self.is_running():
|
|
194
|
+
return {"status": "failed", "error": "Redis not running"}
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
start_time = time.perf_counter()
|
|
198
|
+
|
|
199
|
+
# Test basic operations
|
|
200
|
+
test_key = "mcli:test:connection"
|
|
201
|
+
await self.redis_client.set(test_key, "test_value", ex=10)
|
|
202
|
+
value = await self.redis_client.get(test_key)
|
|
203
|
+
await self.redis_client.delete(test_key)
|
|
204
|
+
|
|
205
|
+
latency = (time.perf_counter() - start_time) * 1000 # ms
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
"status": "success",
|
|
209
|
+
"latency_ms": round(latency, 2),
|
|
210
|
+
"connection_url": await self.get_connection_url(),
|
|
211
|
+
"test_result": value == "test_value",
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
except Exception as e:
|
|
215
|
+
return {
|
|
216
|
+
"status": "failed",
|
|
217
|
+
"error": str(e),
|
|
218
|
+
"connection_url": await self.get_connection_url(),
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
def _find_redis_server(self) -> Optional[Path]:
|
|
222
|
+
"""Find Redis server executable"""
|
|
223
|
+
possible_paths = [
|
|
224
|
+
"/opt/homebrew/bin/redis-server", # Homebrew on Apple Silicon
|
|
225
|
+
"/usr/local/bin/redis-server", # Homebrew on Intel
|
|
226
|
+
"/usr/bin/redis-server", # System install
|
|
227
|
+
Path.home() / ".local/bin/redis-server", # Local install
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
# Check PATH first
|
|
231
|
+
try:
|
|
232
|
+
result = subprocess.run(
|
|
233
|
+
["which", "redis-server"], capture_output=True, text=True, timeout=5
|
|
234
|
+
)
|
|
235
|
+
if result.returncode == 0:
|
|
236
|
+
path = Path(result.stdout.strip())
|
|
237
|
+
if path.exists():
|
|
238
|
+
return path
|
|
239
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
# Check known locations
|
|
243
|
+
for path in possible_paths:
|
|
244
|
+
if isinstance(path, str):
|
|
245
|
+
path = Path(path)
|
|
246
|
+
if path.exists():
|
|
247
|
+
return path
|
|
248
|
+
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
async def _generate_config(self) -> Path:
|
|
252
|
+
"""Generate Redis configuration file"""
|
|
253
|
+
config_content = f"""
|
|
254
|
+
# MCLI Redis Configuration
|
|
255
|
+
port {self.port}
|
|
256
|
+
bind {self.host}
|
|
257
|
+
dir {self.data_dir}
|
|
258
|
+
|
|
259
|
+
# Performance settings
|
|
260
|
+
save 60 1000
|
|
261
|
+
stop-writes-on-bgsave-error no
|
|
262
|
+
rdbcompression yes
|
|
263
|
+
rdbchecksum yes
|
|
264
|
+
dbfilename mcli-redis.rdb
|
|
265
|
+
|
|
266
|
+
# Memory settings
|
|
267
|
+
maxmemory-policy allkeys-lru
|
|
268
|
+
maxmemory 256mb
|
|
269
|
+
|
|
270
|
+
# Logging
|
|
271
|
+
loglevel notice
|
|
272
|
+
logfile {self.data_dir}/redis.log
|
|
273
|
+
|
|
274
|
+
# Security (basic)
|
|
275
|
+
protected-mode yes
|
|
276
|
+
|
|
277
|
+
# Disable potentially dangerous commands in production
|
|
278
|
+
rename-command FLUSHDB ""
|
|
279
|
+
rename-command FLUSHALL ""
|
|
280
|
+
rename-command DEBUG ""
|
|
281
|
+
|
|
282
|
+
# Client settings
|
|
283
|
+
timeout 300
|
|
284
|
+
tcp-keepalive 300
|
|
285
|
+
|
|
286
|
+
# Persistence
|
|
287
|
+
appendonly yes
|
|
288
|
+
appendfilename "mcli-redis.aof"
|
|
289
|
+
appendfsync everysec
|
|
290
|
+
no-appendfsync-on-rewrite no
|
|
291
|
+
auto-aof-rewrite-percentage 100
|
|
292
|
+
auto-aof-rewrite-min-size 64mb
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
config_file = self.data_dir / "redis.conf"
|
|
296
|
+
config_file.write_text(config_content.strip())
|
|
297
|
+
self.config_file = config_file
|
|
298
|
+
|
|
299
|
+
logger.info(f"Generated Redis config: {config_file}")
|
|
300
|
+
return config_file
|
|
301
|
+
|
|
302
|
+
async def _stop_redis_direct(self) -> bool:
|
|
303
|
+
"""Stop Redis by finding process directly"""
|
|
304
|
+
try:
|
|
305
|
+
# Find Redis processes
|
|
306
|
+
redis_processes = []
|
|
307
|
+
for proc in psutil.process_iter(["pid", "name", "cmdline"]):
|
|
308
|
+
try:
|
|
309
|
+
if proc.info["name"] == "redis-server":
|
|
310
|
+
# Check if it's our instance by port
|
|
311
|
+
cmdline = proc.info.get("cmdline", [])
|
|
312
|
+
if any(str(self.port) in arg for arg in cmdline):
|
|
313
|
+
redis_processes.append(proc)
|
|
314
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
315
|
+
continue
|
|
316
|
+
|
|
317
|
+
if not redis_processes:
|
|
318
|
+
logger.info("No Redis processes found to stop")
|
|
319
|
+
return True
|
|
320
|
+
|
|
321
|
+
# Stop processes gracefully
|
|
322
|
+
for proc in redis_processes:
|
|
323
|
+
try:
|
|
324
|
+
logger.info(f"Stopping Redis process {proc.pid}")
|
|
325
|
+
proc.send_signal(signal.SIGTERM)
|
|
326
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
|
|
327
|
+
logger.debug(f"Could not stop process {proc.pid}: {e}")
|
|
328
|
+
|
|
329
|
+
# Wait for graceful shutdown
|
|
330
|
+
await asyncio.sleep(2)
|
|
331
|
+
|
|
332
|
+
# Force kill if still running
|
|
333
|
+
for proc in redis_processes:
|
|
334
|
+
try:
|
|
335
|
+
if proc.is_running():
|
|
336
|
+
logger.warning(f"Force killing Redis process {proc.pid}")
|
|
337
|
+
proc.kill()
|
|
338
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
339
|
+
pass
|
|
340
|
+
|
|
341
|
+
return True
|
|
342
|
+
|
|
343
|
+
except Exception as e:
|
|
344
|
+
logger.error(f"Failed to stop Redis directly: {e}")
|
|
345
|
+
return False
|
|
346
|
+
|
|
347
|
+
async def cleanup(self):
|
|
348
|
+
"""Cleanup resources"""
|
|
349
|
+
if self.redis_client:
|
|
350
|
+
await self.redis_client.close()
|
|
351
|
+
await self.stop()
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
# Global Redis service instance
|
|
355
|
+
_redis_service = None
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
async def get_redis_service() -> RedisService:
|
|
359
|
+
"""Get or create global Redis service instance"""
|
|
360
|
+
global _redis_service
|
|
361
|
+
if _redis_service is None:
|
|
362
|
+
_redis_service = RedisService()
|
|
363
|
+
return _redis_service
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
async def ensure_redis_running() -> bool:
|
|
367
|
+
"""Ensure Redis is running, start if needed"""
|
|
368
|
+
service = await get_redis_service()
|
|
369
|
+
|
|
370
|
+
if await service.is_running():
|
|
371
|
+
logger.info("Redis already running")
|
|
372
|
+
return True
|
|
373
|
+
|
|
374
|
+
logger.info("Starting Redis service...")
|
|
375
|
+
return await service.start()
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
async def get_redis_connection() -> Optional[redis.Redis]:
|
|
379
|
+
"""Get Redis connection, starting service if needed"""
|
|
380
|
+
service = await get_redis_service()
|
|
381
|
+
|
|
382
|
+
if not await service.is_running():
|
|
383
|
+
success = await service.start()
|
|
384
|
+
if not success:
|
|
385
|
+
return None
|
|
386
|
+
|
|
387
|
+
return redis.Redis(host=service.host, port=service.port, decode_responses=True)
|
mcli/lib/shell/shell.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from mcli.lib.logger.logger import get_logger, register_subprocess
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def shell_exec(script_path: str, function_name: str, *args) -> Dict[str, Any]:
|
|
16
|
+
"""Execute a shell script function with security checks and better error handling"""
|
|
17
|
+
# Validate script path
|
|
18
|
+
script_path = Path(script_path).resolve()
|
|
19
|
+
if not script_path.exists():
|
|
20
|
+
raise FileNotFoundError(f"Script not found: {script_path}")
|
|
21
|
+
|
|
22
|
+
# Prepare the full command with the shell script, function name, and arguments
|
|
23
|
+
command = [str(script_path), function_name]
|
|
24
|
+
result = {"success": False, "stdout": "", "stderr": ""}
|
|
25
|
+
logger.info(f"Running command: {command}")
|
|
26
|
+
try:
|
|
27
|
+
# Run the shell script with the function name and arguments
|
|
28
|
+
proc = subprocess.Popen(
|
|
29
|
+
command + list(args), stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Register the process for system monitoring
|
|
33
|
+
register_subprocess(proc)
|
|
34
|
+
|
|
35
|
+
# Wait for the process to complete and get output
|
|
36
|
+
stdout, stderr = proc.communicate()
|
|
37
|
+
|
|
38
|
+
# Check return code
|
|
39
|
+
if proc.returncode != 0:
|
|
40
|
+
raise subprocess.CalledProcessError(proc.returncode, command, stdout, stderr)
|
|
41
|
+
|
|
42
|
+
# Store the result for later reference
|
|
43
|
+
result = subprocess.CompletedProcess(command, proc.returncode, stdout, stderr)
|
|
44
|
+
|
|
45
|
+
# Output from the shell script
|
|
46
|
+
if result.stdout:
|
|
47
|
+
logger.info(f"Script output stdout:\n{result.stdout}")
|
|
48
|
+
|
|
49
|
+
if result.stderr:
|
|
50
|
+
logger.info(f"Script output stderr:\n{result.stderr}")
|
|
51
|
+
# return output # Should contain the "result" key with the list of files
|
|
52
|
+
except subprocess.CalledProcessError as e:
|
|
53
|
+
logger.info(f"Command failed with error: {e}")
|
|
54
|
+
logger.info(f"Standard Output: {e.stdout}")
|
|
55
|
+
logger.info(f"Error Output: {e.stderr}")
|
|
56
|
+
except json.JSONDecodeError as e:
|
|
57
|
+
logger.info(f"Failed to decode JSON: {e}")
|
|
58
|
+
logger.info(f"Raw Output: {result.stdout.strip() if result else 'No output'}")
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_shell_script_path(command: str, command_path: str):
|
|
63
|
+
# Get the path to the shell script
|
|
64
|
+
base_dir = os.path.dirname(os.path.realpath(command_path))
|
|
65
|
+
scripts_path = f"{base_dir}/scripts/{command}.sh"
|
|
66
|
+
return scripts_path
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def shell_recurse(root_path):
|
|
70
|
+
"""
|
|
71
|
+
Recursively applies a given function to all files in the directory tree starting from root_path.
|
|
72
|
+
|
|
73
|
+
:param func: function, a function that takes a file path as its argument and executes on the file
|
|
74
|
+
:param root_path: str, the root directory from which to start applying the function
|
|
75
|
+
"""
|
|
76
|
+
# Check if the current root_path is a directory
|
|
77
|
+
if os.path.isdir(root_path):
|
|
78
|
+
# List all entries in the directory
|
|
79
|
+
for entry in os.listdir(root_path):
|
|
80
|
+
# Construct the full path
|
|
81
|
+
full_path = os.path.join(root_path, entry)
|
|
82
|
+
# Recursively apply the function if it's a directory
|
|
83
|
+
shell_recurse(full_path, shell_exec)
|
|
84
|
+
else:
|
|
85
|
+
# If it's a file, apply the function
|
|
86
|
+
shell_exec(root_path)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def is_executable_available(executable):
|
|
90
|
+
return shutil.which(executable) is not None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def fatal_error(msg):
|
|
94
|
+
logger.critical(msg + " Unable to recover from the error, exiting.")
|
|
95
|
+
if not logger.isEnabledFor(logging.DEBUG):
|
|
96
|
+
logger.error(
|
|
97
|
+
"Debug output may help you to fix this issue or will be useful for maintainers of this tool."
|
|
98
|
+
" Please try to rerun tool with `-d` flag to enable debug output"
|
|
99
|
+
)
|
|
100
|
+
sys.exit(1)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def execute_os_command(command, fail_on_error=True, stdin=None):
|
|
104
|
+
logger.debug("Executing command '%s'", command)
|
|
105
|
+
process = subprocess.Popen(
|
|
106
|
+
command,
|
|
107
|
+
shell=True,
|
|
108
|
+
stdout=subprocess.PIPE,
|
|
109
|
+
stderr=subprocess.PIPE,
|
|
110
|
+
stdin=subprocess.PIPE,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Register the process for system monitoring
|
|
114
|
+
register_subprocess(process)
|
|
115
|
+
|
|
116
|
+
if stdin is not None:
|
|
117
|
+
stdin = stdin.encode()
|
|
118
|
+
stdout, stderr = [stream.decode().strip() for stream in process.communicate(input=stdin)]
|
|
119
|
+
|
|
120
|
+
logger.debug("rc > %s", process.returncode)
|
|
121
|
+
if stdout:
|
|
122
|
+
logger.debug("stdout> %s", stdout)
|
|
123
|
+
if stderr:
|
|
124
|
+
logger.debug("stderr> %s", stderr)
|
|
125
|
+
|
|
126
|
+
if process.returncode:
|
|
127
|
+
msg = f'Failed to execute command "{command}", error:\n{stdout}{stderr}'
|
|
128
|
+
if fail_on_error:
|
|
129
|
+
fatal_error(msg)
|
|
130
|
+
else:
|
|
131
|
+
raise RuntimeError(msg)
|
|
132
|
+
|
|
133
|
+
return stdout
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def cli_exec():
|
|
137
|
+
pass
|
mcli/lib/toml/toml.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
def read_from_toml(file_path: str, key: str):
|
|
2
|
+
"""
|
|
3
|
+
Reads a TOML file and returns the value associated with the provided key.
|
|
4
|
+
|
|
5
|
+
Args:
|
|
6
|
+
file_path (str): The path to the TOML file.
|
|
7
|
+
key (str): The key whose value will be retrieved.
|
|
8
|
+
|
|
9
|
+
Returns:
|
|
10
|
+
The value corresponding to the key if it exists in the TOML file, or None otherwise.
|
|
11
|
+
|
|
12
|
+
Raises:
|
|
13
|
+
FileNotFoundError: If the specified file does not exist.
|
|
14
|
+
tomllib.TOMLDecodeError or toml.TomlDecodeError: If the file contains invalid TOML.
|
|
15
|
+
"""
|
|
16
|
+
try:
|
|
17
|
+
# Attempt to use the built-in tomllib (available in Python 3.11+)
|
|
18
|
+
import tomllib
|
|
19
|
+
|
|
20
|
+
with open(file_path, "rb") as file:
|
|
21
|
+
config_data = tomllib.load(file)
|
|
22
|
+
except ModuleNotFoundError:
|
|
23
|
+
# Fall back to the third-party 'toml' package for earlier Python versions.
|
|
24
|
+
import toml
|
|
25
|
+
|
|
26
|
+
with open(file_path, "r", encoding="utf-8") as file:
|
|
27
|
+
config_data = toml.load(file)
|
|
28
|
+
|
|
29
|
+
# Return the value for the provided key, or None if the key is not found.
|
|
30
|
+
return config_data.get(key)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
__all__ = ["read_from_toml"]
|
mcli/lib/ui/styling.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from rich.box import HEAVY, ROUNDED
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.panel import Panel
|
|
4
|
+
from rich.text import Text
|
|
5
|
+
|
|
6
|
+
console = Console()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def info(message: str) -> None:
|
|
10
|
+
"""Display an informational message with enhanced styling.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
message: The text to display
|
|
14
|
+
"""
|
|
15
|
+
panel = Panel(f"ℹ️ {message}", box=ROUNDED, border_style="bright_cyan", padding=(0, 1))
|
|
16
|
+
console.print(panel)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def warning(message: str) -> None:
|
|
20
|
+
"""Display a warning message with enhanced styling."""
|
|
21
|
+
panel = Panel(f"⚠️ {message}", box=ROUNDED, border_style="bright_yellow", padding=(0, 1))
|
|
22
|
+
console.print(panel)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def success(message: str) -> None:
|
|
26
|
+
"""Display a success message with enhanced styling."""
|
|
27
|
+
panel = Panel(f"✅ {message}", box=ROUNDED, border_style="bright_green", padding=(0, 1))
|
|
28
|
+
console.print(panel)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def error(message: str) -> None:
|
|
32
|
+
"""Display an error message with enhanced styling."""
|
|
33
|
+
panel = Panel(f"❌ {message}", box=HEAVY, border_style="bright_red", padding=(0, 1))
|
|
34
|
+
console.print(panel)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def celebrate(message: str) -> None:
|
|
38
|
+
"""Display a celebration message with extra flair."""
|
|
39
|
+
panel = Panel(
|
|
40
|
+
f"🎉 {message} 🎉",
|
|
41
|
+
title="🌟 SUCCESS 🌟",
|
|
42
|
+
title_align="center",
|
|
43
|
+
box=HEAVY,
|
|
44
|
+
border_style="bright_magenta",
|
|
45
|
+
padding=(1, 2),
|
|
46
|
+
)
|
|
47
|
+
console.print(panel)
|