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.

Files changed (186) hide show
  1. mcli/app/chat_cmd.py +42 -0
  2. mcli/app/commands_cmd.py +226 -0
  3. mcli/app/completion_cmd.py +216 -0
  4. mcli/app/completion_helpers.py +288 -0
  5. mcli/app/cron_test_cmd.py +697 -0
  6. mcli/app/logs_cmd.py +419 -0
  7. mcli/app/main.py +492 -0
  8. mcli/app/model/model.py +1060 -0
  9. mcli/app/model_cmd.py +227 -0
  10. mcli/app/redis_cmd.py +269 -0
  11. mcli/app/video/video.py +1114 -0
  12. mcli/app/visual_cmd.py +303 -0
  13. mcli/chat/chat.py +2409 -0
  14. mcli/chat/command_rag.py +514 -0
  15. mcli/chat/enhanced_chat.py +652 -0
  16. mcli/chat/system_controller.py +1010 -0
  17. mcli/chat/system_integration.py +1016 -0
  18. mcli/cli.py +25 -0
  19. mcli/config.toml +20 -0
  20. mcli/lib/api/api.py +586 -0
  21. mcli/lib/api/daemon_client.py +203 -0
  22. mcli/lib/api/daemon_client_local.py +44 -0
  23. mcli/lib/api/daemon_decorator.py +217 -0
  24. mcli/lib/api/mcli_decorators.py +1032 -0
  25. mcli/lib/auth/auth.py +85 -0
  26. mcli/lib/auth/aws_manager.py +85 -0
  27. mcli/lib/auth/azure_manager.py +91 -0
  28. mcli/lib/auth/credential_manager.py +192 -0
  29. mcli/lib/auth/gcp_manager.py +93 -0
  30. mcli/lib/auth/key_manager.py +117 -0
  31. mcli/lib/auth/mcli_manager.py +93 -0
  32. mcli/lib/auth/token_manager.py +75 -0
  33. mcli/lib/auth/token_util.py +1011 -0
  34. mcli/lib/config/config.py +47 -0
  35. mcli/lib/discovery/__init__.py +1 -0
  36. mcli/lib/discovery/command_discovery.py +274 -0
  37. mcli/lib/erd/erd.py +1345 -0
  38. mcli/lib/erd/generate_graph.py +453 -0
  39. mcli/lib/files/files.py +76 -0
  40. mcli/lib/fs/fs.py +109 -0
  41. mcli/lib/lib.py +29 -0
  42. mcli/lib/logger/logger.py +611 -0
  43. mcli/lib/performance/optimizer.py +409 -0
  44. mcli/lib/performance/rust_bridge.py +502 -0
  45. mcli/lib/performance/uvloop_config.py +154 -0
  46. mcli/lib/pickles/pickles.py +50 -0
  47. mcli/lib/search/cached_vectorizer.py +479 -0
  48. mcli/lib/services/data_pipeline.py +460 -0
  49. mcli/lib/services/lsh_client.py +441 -0
  50. mcli/lib/services/redis_service.py +387 -0
  51. mcli/lib/shell/shell.py +137 -0
  52. mcli/lib/toml/toml.py +33 -0
  53. mcli/lib/ui/styling.py +47 -0
  54. mcli/lib/ui/visual_effects.py +634 -0
  55. mcli/lib/watcher/watcher.py +185 -0
  56. mcli/ml/api/app.py +215 -0
  57. mcli/ml/api/middleware.py +224 -0
  58. mcli/ml/api/routers/admin_router.py +12 -0
  59. mcli/ml/api/routers/auth_router.py +244 -0
  60. mcli/ml/api/routers/backtest_router.py +12 -0
  61. mcli/ml/api/routers/data_router.py +12 -0
  62. mcli/ml/api/routers/model_router.py +302 -0
  63. mcli/ml/api/routers/monitoring_router.py +12 -0
  64. mcli/ml/api/routers/portfolio_router.py +12 -0
  65. mcli/ml/api/routers/prediction_router.py +267 -0
  66. mcli/ml/api/routers/trade_router.py +12 -0
  67. mcli/ml/api/routers/websocket_router.py +76 -0
  68. mcli/ml/api/schemas.py +64 -0
  69. mcli/ml/auth/auth_manager.py +425 -0
  70. mcli/ml/auth/models.py +154 -0
  71. mcli/ml/auth/permissions.py +302 -0
  72. mcli/ml/backtesting/backtest_engine.py +502 -0
  73. mcli/ml/backtesting/performance_metrics.py +393 -0
  74. mcli/ml/cache.py +400 -0
  75. mcli/ml/cli/main.py +398 -0
  76. mcli/ml/config/settings.py +394 -0
  77. mcli/ml/configs/dvc_config.py +230 -0
  78. mcli/ml/configs/mlflow_config.py +131 -0
  79. mcli/ml/configs/mlops_manager.py +293 -0
  80. mcli/ml/dashboard/app.py +532 -0
  81. mcli/ml/dashboard/app_integrated.py +738 -0
  82. mcli/ml/dashboard/app_supabase.py +560 -0
  83. mcli/ml/dashboard/app_training.py +615 -0
  84. mcli/ml/dashboard/cli.py +51 -0
  85. mcli/ml/data_ingestion/api_connectors.py +501 -0
  86. mcli/ml/data_ingestion/data_pipeline.py +567 -0
  87. mcli/ml/data_ingestion/stream_processor.py +512 -0
  88. mcli/ml/database/migrations/env.py +94 -0
  89. mcli/ml/database/models.py +667 -0
  90. mcli/ml/database/session.py +200 -0
  91. mcli/ml/experimentation/ab_testing.py +845 -0
  92. mcli/ml/features/ensemble_features.py +607 -0
  93. mcli/ml/features/political_features.py +676 -0
  94. mcli/ml/features/recommendation_engine.py +809 -0
  95. mcli/ml/features/stock_features.py +573 -0
  96. mcli/ml/features/test_feature_engineering.py +346 -0
  97. mcli/ml/logging.py +85 -0
  98. mcli/ml/mlops/data_versioning.py +518 -0
  99. mcli/ml/mlops/experiment_tracker.py +377 -0
  100. mcli/ml/mlops/model_serving.py +481 -0
  101. mcli/ml/mlops/pipeline_orchestrator.py +614 -0
  102. mcli/ml/models/base_models.py +324 -0
  103. mcli/ml/models/ensemble_models.py +675 -0
  104. mcli/ml/models/recommendation_models.py +474 -0
  105. mcli/ml/models/test_models.py +487 -0
  106. mcli/ml/monitoring/drift_detection.py +676 -0
  107. mcli/ml/monitoring/metrics.py +45 -0
  108. mcli/ml/optimization/portfolio_optimizer.py +834 -0
  109. mcli/ml/preprocessing/data_cleaners.py +451 -0
  110. mcli/ml/preprocessing/feature_extractors.py +491 -0
  111. mcli/ml/preprocessing/ml_pipeline.py +382 -0
  112. mcli/ml/preprocessing/politician_trading_preprocessor.py +569 -0
  113. mcli/ml/preprocessing/test_preprocessing.py +294 -0
  114. mcli/ml/scripts/populate_sample_data.py +200 -0
  115. mcli/ml/tasks.py +400 -0
  116. mcli/ml/tests/test_integration.py +429 -0
  117. mcli/ml/tests/test_training_dashboard.py +387 -0
  118. mcli/public/oi/oi.py +15 -0
  119. mcli/public/public.py +4 -0
  120. mcli/self/self_cmd.py +1246 -0
  121. mcli/workflow/daemon/api_daemon.py +800 -0
  122. mcli/workflow/daemon/async_command_database.py +681 -0
  123. mcli/workflow/daemon/async_process_manager.py +591 -0
  124. mcli/workflow/daemon/client.py +530 -0
  125. mcli/workflow/daemon/commands.py +1196 -0
  126. mcli/workflow/daemon/daemon.py +905 -0
  127. mcli/workflow/daemon/daemon_api.py +59 -0
  128. mcli/workflow/daemon/enhanced_daemon.py +571 -0
  129. mcli/workflow/daemon/process_cli.py +244 -0
  130. mcli/workflow/daemon/process_manager.py +439 -0
  131. mcli/workflow/daemon/test_daemon.py +275 -0
  132. mcli/workflow/dashboard/dashboard_cmd.py +113 -0
  133. mcli/workflow/docker/docker.py +0 -0
  134. mcli/workflow/file/file.py +100 -0
  135. mcli/workflow/gcloud/config.toml +21 -0
  136. mcli/workflow/gcloud/gcloud.py +58 -0
  137. mcli/workflow/git_commit/ai_service.py +328 -0
  138. mcli/workflow/git_commit/commands.py +430 -0
  139. mcli/workflow/lsh_integration.py +355 -0
  140. mcli/workflow/model_service/client.py +594 -0
  141. mcli/workflow/model_service/download_and_run_efficient_models.py +288 -0
  142. mcli/workflow/model_service/lightweight_embedder.py +397 -0
  143. mcli/workflow/model_service/lightweight_model_server.py +714 -0
  144. mcli/workflow/model_service/lightweight_test.py +241 -0
  145. mcli/workflow/model_service/model_service.py +1955 -0
  146. mcli/workflow/model_service/ollama_efficient_runner.py +425 -0
  147. mcli/workflow/model_service/pdf_processor.py +386 -0
  148. mcli/workflow/model_service/test_efficient_runner.py +234 -0
  149. mcli/workflow/model_service/test_example.py +315 -0
  150. mcli/workflow/model_service/test_integration.py +131 -0
  151. mcli/workflow/model_service/test_new_features.py +149 -0
  152. mcli/workflow/openai/openai.py +99 -0
  153. mcli/workflow/politician_trading/commands.py +1790 -0
  154. mcli/workflow/politician_trading/config.py +134 -0
  155. mcli/workflow/politician_trading/connectivity.py +490 -0
  156. mcli/workflow/politician_trading/data_sources.py +395 -0
  157. mcli/workflow/politician_trading/database.py +410 -0
  158. mcli/workflow/politician_trading/demo.py +248 -0
  159. mcli/workflow/politician_trading/models.py +165 -0
  160. mcli/workflow/politician_trading/monitoring.py +413 -0
  161. mcli/workflow/politician_trading/scrapers.py +966 -0
  162. mcli/workflow/politician_trading/scrapers_california.py +412 -0
  163. mcli/workflow/politician_trading/scrapers_eu.py +377 -0
  164. mcli/workflow/politician_trading/scrapers_uk.py +350 -0
  165. mcli/workflow/politician_trading/scrapers_us_states.py +438 -0
  166. mcli/workflow/politician_trading/supabase_functions.py +354 -0
  167. mcli/workflow/politician_trading/workflow.py +852 -0
  168. mcli/workflow/registry/registry.py +180 -0
  169. mcli/workflow/repo/repo.py +223 -0
  170. mcli/workflow/scheduler/commands.py +493 -0
  171. mcli/workflow/scheduler/cron_parser.py +238 -0
  172. mcli/workflow/scheduler/job.py +182 -0
  173. mcli/workflow/scheduler/monitor.py +139 -0
  174. mcli/workflow/scheduler/persistence.py +324 -0
  175. mcli/workflow/scheduler/scheduler.py +679 -0
  176. mcli/workflow/sync/sync_cmd.py +437 -0
  177. mcli/workflow/sync/test_cmd.py +314 -0
  178. mcli/workflow/videos/videos.py +242 -0
  179. mcli/workflow/wakatime/wakatime.py +11 -0
  180. mcli/workflow/workflow.py +37 -0
  181. mcli_framework-7.0.0.dist-info/METADATA +479 -0
  182. mcli_framework-7.0.0.dist-info/RECORD +186 -0
  183. mcli_framework-7.0.0.dist-info/WHEEL +5 -0
  184. mcli_framework-7.0.0.dist-info/entry_points.txt +7 -0
  185. mcli_framework-7.0.0.dist-info/licenses/LICENSE +21 -0
  186. 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)
@@ -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)