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,611 @@
1
+ import inspect
2
+ import logging
3
+ import os
4
+ import platform
5
+ import signal
6
+ import subprocess
7
+ import sys
8
+ import threading
9
+ import time
10
+ import traceback
11
+ from datetime import datetime
12
+ from pathlib import Path
13
+ from types import FrameType
14
+ from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
15
+
16
+ import psutil
17
+
18
+
19
+ class McliLogger:
20
+ """
21
+ Central logger for mcli that logs only to file, not to console.
22
+ """
23
+
24
+ _instance = None
25
+ _runtime_tracing_enabled = False
26
+ _system_tracing_enabled = False
27
+ _system_trace_interval = 5 # Default to check every 5 seconds
28
+ _system_trace_process_ids = set() # Process IDs to monitor
29
+ _system_trace_thread = None
30
+ _excluded_modules: Set[str] = set()
31
+ _trace_level = 0 # 0=off, 1=function calls, 2=line by line, 3=verbose
32
+ _system_trace_level = 0 # 0=off, 1=basic, 2=detailed
33
+
34
+ @classmethod
35
+ def get_logger(cls, name="mcli.out"):
36
+ """Get or create the singleton logger instance."""
37
+ if cls._instance is None:
38
+ cls._instance = cls(name)
39
+ return cls._instance.logger
40
+
41
+ @classmethod
42
+ def get_trace_logger(cls):
43
+ """Get the trace logger instance for interpreter trace events."""
44
+ if cls._instance is None:
45
+ cls._instance = cls("mcli.out")
46
+ return cls._instance.trace_logger
47
+
48
+ @classmethod
49
+ def get_system_trace_logger(cls):
50
+ """Get the system trace logger instance for OS process monitoring."""
51
+ if cls._instance is None:
52
+ cls._instance = cls("mcli.out")
53
+ return cls._instance.system_trace_logger
54
+
55
+ @classmethod
56
+ def enable_runtime_tracing(cls, level: int = 1, excluded_modules: Optional[List[str]] = None):
57
+ """
58
+ Enable Python interpreter runtime tracing.
59
+
60
+ Args:
61
+ level: Tracing detail level (0=off, 1=function calls only, 2=line by line, 3=verbose)
62
+ excluded_modules: List of module prefixes to exclude from tracing
63
+ """
64
+ if cls._instance is None:
65
+ cls._instance = cls("mcli.out")
66
+
67
+ cls._trace_level = max(0, min(level, 3)) # Clamp to 0-3
68
+
69
+ if excluded_modules:
70
+ cls._excluded_modules = set(excluded_modules)
71
+ else:
72
+ # Default exclusions to avoid excessive logging
73
+ cls._excluded_modules = {
74
+ "logging",
75
+ "importlib",
76
+ "typing",
77
+ "abc",
78
+ "inspect",
79
+ "pkg_resources",
80
+ "encodings",
81
+ "_weakrefset",
82
+ "weakref",
83
+ "sre_",
84
+ "re",
85
+ "functools",
86
+ "threading",
87
+ "copyreg",
88
+ "collections",
89
+ "_collections_abc",
90
+ "enum",
91
+ }
92
+
93
+ if cls._trace_level > 0 and not cls._runtime_tracing_enabled:
94
+ # Enable tracing
95
+ sys.settrace(cls._instance._trace_callback)
96
+ threading.settrace(cls._instance._trace_callback)
97
+ cls._runtime_tracing_enabled = True
98
+ cls._instance.trace_logger.info(
99
+ f"Python interpreter tracing enabled (level={cls._trace_level})"
100
+ )
101
+ elif cls._trace_level == 0 and cls._runtime_tracing_enabled:
102
+ # Disable tracing
103
+ sys.settrace(None)
104
+ threading.settrace(None)
105
+ cls._runtime_tracing_enabled = False
106
+ cls._instance.trace_logger.info("Python interpreter tracing disabled")
107
+
108
+ @classmethod
109
+ def enable_system_tracing(cls, level: int = 1, interval: int = 5):
110
+ """
111
+ Enable OS-level system tracing for process monitoring.
112
+
113
+ Args:
114
+ level: System trace level (0=off, 1=basic info, 2=detailed)
115
+ interval: Monitoring interval in seconds
116
+ """
117
+ if cls._instance is None:
118
+ cls._instance = cls("mcli.out")
119
+
120
+ # Add current process to monitoring
121
+ cls._system_trace_process_ids.add(os.getpid())
122
+ cls._system_trace_interval = max(1, interval) # Minimum 1 second
123
+ cls._system_trace_level = max(0, min(level, 2)) # Clamp to 0-2
124
+
125
+ # Start monitoring thread if not already running
126
+ if cls._system_trace_level > 0 and not cls._system_tracing_enabled:
127
+ if cls._system_trace_thread is None or not cls._system_trace_thread.is_alive():
128
+ cls._system_tracing_enabled = True
129
+ cls._system_trace_thread = threading.Thread(
130
+ target=cls._instance._system_trace_worker, daemon=True
131
+ )
132
+ cls._system_trace_thread.start()
133
+ cls._instance.system_trace_logger.info(
134
+ f"System process tracing enabled (level={cls._system_trace_level}, interval={cls._system_trace_interval}s)"
135
+ )
136
+ elif cls._system_trace_level == 0 and cls._system_tracing_enabled:
137
+ # Disable tracing
138
+ cls._system_tracing_enabled = False
139
+ if cls._system_trace_thread and cls._system_trace_thread.is_alive():
140
+ # Thread will terminate on its own when _system_tracing_enabled is False
141
+ cls._instance.system_trace_logger.info("System process tracing disabled")
142
+
143
+ @classmethod
144
+ def disable_system_tracing(cls):
145
+ """Disable OS-level system tracing."""
146
+ cls.enable_system_tracing(level=0)
147
+
148
+ @classmethod
149
+ def register_process(cls, pid: int):
150
+ """Register a process for monitoring."""
151
+ if cls._instance is None:
152
+ cls._instance = cls("mcli.out")
153
+
154
+ if pid > 0:
155
+ cls._system_trace_process_ids.add(pid)
156
+ cls._instance.system_trace_logger.info(f"Registered process ID {pid} for monitoring")
157
+ return True
158
+ return False
159
+
160
+ @classmethod
161
+ def register_subprocess(cls, proc: subprocess.Popen) -> int:
162
+ """
163
+ Register a subprocess.Popen object for monitoring.
164
+ Returns the process ID if successful, 0 otherwise.
165
+ """
166
+ if proc and proc.pid:
167
+ if cls.register_process(proc.pid):
168
+ return proc.pid
169
+ return 0
170
+
171
+ @classmethod
172
+ def unregister_process(cls, pid: int):
173
+ """Remove a process from monitoring."""
174
+ if cls._instance is None:
175
+ return
176
+
177
+ if pid in cls._system_trace_process_ids:
178
+ cls._system_trace_process_ids.remove(pid)
179
+ cls._instance.system_trace_logger.info(f"Unregistered process ID {pid} from monitoring")
180
+
181
+ @classmethod
182
+ def disable_runtime_tracing(cls):
183
+ """Disable Python interpreter runtime tracing."""
184
+ cls.enable_runtime_tracing(level=0)
185
+
186
+ def __init__(self, name="mcli.out"):
187
+ self.name = name
188
+ self.logger = logging.getLogger(name)
189
+ self.trace_logger = logging.getLogger(f"{name}.trace")
190
+ self.system_trace_logger = logging.getLogger(f"{name}.system")
191
+
192
+ # Set to DEBUG to capture all levels in the log file
193
+ self.logger.setLevel(logging.DEBUG)
194
+ self.trace_logger.setLevel(logging.DEBUG)
195
+ self.system_trace_logger.setLevel(logging.DEBUG)
196
+
197
+ self.logger.propagate = False
198
+ self.trace_logger.propagate = False
199
+ self.system_trace_logger.propagate = False
200
+
201
+ # Clear any existing handlers
202
+ if self.logger.handlers:
203
+ self.logger.handlers.clear()
204
+ if self.trace_logger.handlers:
205
+ self.trace_logger.handlers.clear()
206
+ if self.system_trace_logger.handlers:
207
+ self.system_trace_logger.handlers.clear()
208
+
209
+ # Set up file handler with path resolution
210
+ try:
211
+ # Find the project root directory (where logs dir should be)
212
+ # Start with the current file and navigate up the directory structure
213
+ current_file = Path(__file__)
214
+ # Go up 5 levels: file -> logger -> lib -> mcli -> src -> repo_root
215
+ project_root = current_file.parents[4]
216
+ log_dir = project_root / "logs"
217
+ log_dir.mkdir(exist_ok=True)
218
+
219
+ # Create daily log file
220
+ timestamp = datetime.now().strftime("%Y%m%d")
221
+ log_file = log_dir / f"mcli_{timestamp}.log"
222
+ trace_log_file = log_dir / f"mcli_trace_{timestamp}.log"
223
+ system_trace_log_file = log_dir / f"mcli_system_{timestamp}.log"
224
+
225
+ # Configure regular file handler
226
+ file_handler = logging.FileHandler(log_file)
227
+ file_handler.setLevel(logging.DEBUG) # Capture all levels in the file
228
+ file_formatter = logging.Formatter("%(asctime)s [%(levelname)s] [%(name)s] %(message)s")
229
+ file_handler.setFormatter(file_formatter)
230
+ self.logger.addHandler(file_handler)
231
+
232
+ # Configure trace file handler
233
+ trace_handler = logging.FileHandler(trace_log_file)
234
+ trace_handler.setLevel(logging.DEBUG)
235
+ trace_formatter = logging.Formatter("%(asctime)s [TRACE] %(message)s")
236
+ trace_handler.setFormatter(trace_formatter)
237
+ self.trace_logger.addHandler(trace_handler)
238
+
239
+ # Configure system trace file handler
240
+ system_trace_handler = logging.FileHandler(system_trace_log_file)
241
+ system_trace_handler.setLevel(logging.DEBUG)
242
+ system_trace_formatter = logging.Formatter("%(asctime)s [SYSTEM] %(message)s")
243
+ system_trace_handler.setFormatter(system_trace_formatter)
244
+ self.system_trace_logger.addHandler(system_trace_handler)
245
+
246
+ # Log the path to help with debugging
247
+ self.logger.debug(f"Logging to: {log_file}")
248
+ self.trace_logger.debug(f"Trace logging to: {trace_log_file}")
249
+ self.system_trace_logger.debug(f"System trace logging to: {system_trace_log_file}")
250
+ except Exception as e:
251
+ # If we can't set up file logging, fall back to stderr
252
+ # This should only happen during development or in unusual environments
253
+ fallback_handler = logging.StreamHandler(sys.stderr)
254
+ fallback_handler.setLevel(logging.ERROR)
255
+ fallback_formatter = logging.Formatter(
256
+ "[FALLBACK] %(asctime)s [%(levelname)s] %(message)s"
257
+ )
258
+ fallback_handler.setFormatter(fallback_formatter)
259
+ self.logger.addHandler(fallback_handler)
260
+ self.trace_logger.addHandler(fallback_handler)
261
+ self.system_trace_logger.addHandler(fallback_handler)
262
+ self.logger.error(f"Failed to set up file logging: {e}. Using stderr fallback.")
263
+
264
+ def _should_trace(self, filename: str) -> bool:
265
+ """Determine if a file should be traced based on exclusion rules."""
266
+ # Skip files in standard library
267
+ if filename.startswith(sys.prefix) or "<" in filename:
268
+ return False
269
+
270
+ # Get module name from filename
271
+ module_name = os.path.basename(filename)
272
+ if module_name.endswith(".py"):
273
+ module_name = module_name[:-3]
274
+
275
+ # Skip excluded modules
276
+ for excluded in self._excluded_modules:
277
+ if module_name.startswith(excluded):
278
+ return False
279
+
280
+ return True
281
+
282
+ def _get_process_info(self, pid: int, detailed: bool = False) -> Dict[str, Any]:
283
+ """Get information about a process given its PID."""
284
+ try:
285
+ # Check if process exists
286
+ if not psutil.pid_exists(pid):
287
+ return {"pid": pid, "status": "NOT_FOUND"}
288
+
289
+ # Get process info
290
+ proc = psutil.Process(pid)
291
+ basic_info = {
292
+ "pid": pid,
293
+ "name": proc.name(),
294
+ "status": proc.status(),
295
+ "cpu_percent": proc.cpu_percent(interval=0.1), # Quick sampling
296
+ "memory_percent": proc.memory_percent(),
297
+ "create_time": datetime.fromtimestamp(proc.create_time()).strftime(
298
+ "%Y-%m-%d %H:%M:%S"
299
+ ),
300
+ "running_time": time.time() - proc.create_time(),
301
+ "num_threads": proc.num_threads(),
302
+ "username": proc.username(),
303
+ }
304
+
305
+ # Add more detailed information if requested
306
+ if detailed:
307
+ try:
308
+ # These can sometimes raise exceptions depending on permissions
309
+ children = proc.children(recursive=True)
310
+ io_counters = proc.io_counters()
311
+ connections = proc.connections()
312
+ open_files = proc.open_files()
313
+
314
+ detailed_info = {
315
+ "cmdline": " ".join(proc.cmdline()),
316
+ "exe": proc.exe(),
317
+ "cwd": proc.cwd(),
318
+ "nice": proc.nice(),
319
+ "io_counters": {
320
+ "read_bytes": io_counters.read_bytes,
321
+ "write_bytes": io_counters.write_bytes,
322
+ },
323
+ "num_fds": proc.num_fds() if hasattr(proc, "num_fds") else None,
324
+ "num_connections": len(connections),
325
+ "num_open_files": len(open_files),
326
+ "open_files": [f.path for f in open_files[:5]], # First 5 files
327
+ "children": [
328
+ {"pid": child.pid, "name": child.name()} for child in children[:5]
329
+ ], # First 5 children
330
+ }
331
+
332
+ basic_info.update(detailed_info)
333
+ except Exception as e:
334
+ # Add error info but don't fail
335
+ basic_info["detailed_error"] = str(e)
336
+
337
+ return basic_info
338
+
339
+ except Exception as e:
340
+ return {"pid": pid, "status": "ERROR", "error": str(e)}
341
+
342
+ def _format_process_info(self, info: Dict[str, Any]) -> str:
343
+ """Format process information for logging."""
344
+ if info.get("status") == "NOT_FOUND":
345
+ return f"Process {info['pid']} no longer exists"
346
+
347
+ if info.get("status") == "ERROR":
348
+ return f"Error getting info for process {info['pid']}: {info.get('error', 'Unknown error')}"
349
+
350
+ # Format basic info
351
+ lines = [
352
+ f"Process {info['pid']} ({info['name']}):",
353
+ f" Status: {info['status']}",
354
+ f" CPU: {info['cpu_percent']:.1f}%, Memory: {info['memory_percent']:.1f}%",
355
+ f" Running time: {info['running_time']:.1f} seconds",
356
+ f" Threads: {info['num_threads']}",
357
+ ]
358
+
359
+ # Add detailed info if available
360
+ if "cmdline" in info:
361
+ lines.extend(
362
+ [
363
+ f" Command: {info['cmdline'][:100]}{'...' if len(info['cmdline']) > 100 else ''}",
364
+ f" Working directory: {info['cwd']}",
365
+ f" IO: read={info['io_counters']['read_bytes']/1024:.1f} KB, write={info['io_counters']['write_bytes']/1024:.1f} KB",
366
+ f" Open files: {info['num_open_files']} (sample: {', '.join(info['open_files'][:3]) if info['open_files'] else 'none'})",
367
+ f" Child processes: {len(info['children'])} (sample: child info available)",
368
+ ]
369
+ )
370
+
371
+ return "\n".join(lines)
372
+
373
+ def _system_trace_worker(self):
374
+ """Worker thread that periodically collects and logs process information."""
375
+ while self._system_tracing_enabled:
376
+ try:
377
+ # Check all registered processes
378
+ current_pids = set(
379
+ self._system_trace_process_ids
380
+ ) # Make a copy to avoid modification during iteration
381
+
382
+ for pid in current_pids:
383
+ try:
384
+ detailed = self._system_trace_level >= 2
385
+ process_info = self._get_process_info(pid, detailed)
386
+
387
+ # Format and log the information
388
+ if process_info.get("status") != "NOT_FOUND":
389
+ formatted_info = self._format_process_info(process_info)
390
+ self.system_trace_logger.info(formatted_info)
391
+ else:
392
+ # Process no longer exists
393
+ self.system_trace_logger.info(
394
+ f"Process {pid} no longer exists, removing from monitoring"
395
+ )
396
+ self._system_trace_process_ids.discard(pid)
397
+
398
+ # Look for child processes if detailed tracing is enabled
399
+ if detailed and process_info.get("status") not in ["NOT_FOUND", "ERROR"]:
400
+ try:
401
+ proc = psutil.Process(pid)
402
+ children = proc.children(recursive=False)
403
+
404
+ for child in children:
405
+ # If we find a child not already being monitored, add it
406
+ if child.pid not in self._system_trace_process_ids:
407
+ self._system_trace_process_ids.add(child.pid)
408
+ self.system_trace_logger.info(
409
+ f"Added child process {child.pid} ({child.name()}) to monitoring"
410
+ )
411
+ except Exception as e:
412
+ self.system_trace_logger.error(
413
+ f"Error getting child processes for {pid}: {e}"
414
+ )
415
+
416
+ except Exception as e:
417
+ self.system_trace_logger.error(
418
+ f"Error in system trace for process {pid}: {e}"
419
+ )
420
+
421
+ # Add a separator between trace cycles for readability
422
+ if current_pids:
423
+ self.system_trace_logger.info("-" * 50)
424
+
425
+ except Exception as e:
426
+ self.system_trace_logger.error(f"Error in system trace worker: {e}")
427
+
428
+ # Sleep until next collection cycle
429
+ time.sleep(self._system_trace_interval)
430
+
431
+ def _trace_callback(self, frame: FrameType, event: str, arg: Any) -> Callable:
432
+ """Trace callback function for sys.settrace()."""
433
+ if self._trace_level == 0:
434
+ return None
435
+
436
+ try:
437
+ filename = frame.f_code.co_filename
438
+ lineno = frame.f_lineno
439
+ function = frame.f_code.co_name
440
+
441
+ # Skip if we should not trace this file
442
+ if not self._should_trace(filename):
443
+ return None
444
+
445
+ # Log based on trace level and event type
446
+ if event == "call" and self._trace_level >= 1:
447
+ module = os.path.basename(filename)
448
+ if module.endswith(".py"):
449
+ module = module[:-3]
450
+
451
+ # Get function arguments for detailed tracing
452
+ if self._trace_level >= 3:
453
+ args = inspect.getargvalues(frame)
454
+ args_str = []
455
+ for arg_name in args.args:
456
+ if arg_name in args.locals:
457
+ # Safely get string representation with limits
458
+ try:
459
+ arg_val = str(args.locals[arg_name])
460
+ # Truncate long values
461
+ if len(arg_val) > 100:
462
+ arg_val = arg_val[:97] + "..."
463
+ args_str.append(f"{arg_name}={arg_val}")
464
+ except:
465
+ args_str.append(f"{arg_name}=<error>")
466
+ args_repr = ", ".join(args_str)
467
+ self.trace_logger.debug(
468
+ f"CALL {module}.{function}({args_repr}) at {filename}:{lineno}"
469
+ )
470
+ else:
471
+ self.trace_logger.debug(f"CALL {module}.{function}() at {filename}:{lineno}")
472
+
473
+ elif event == "line" and self._trace_level >= 2:
474
+ # For line-by-line tracing (high volume)
475
+ if self._trace_level >= 3:
476
+ # Include source line in verbose mode
477
+ try:
478
+ with open(filename, "r") as f:
479
+ lines = f.readlines()
480
+ source = (
481
+ lines[lineno - 1].strip()
482
+ if lineno <= len(lines)
483
+ else "<source not available>"
484
+ )
485
+ self.trace_logger.debug(f"LINE {filename}:{lineno} -> {source}")
486
+ except:
487
+ self.trace_logger.debug(f"LINE {filename}:{lineno}")
488
+ else:
489
+ self.trace_logger.debug(f"LINE {filename}:{lineno}")
490
+
491
+ elif event == "return" and self._trace_level >= 2:
492
+ if self._trace_level >= 3:
493
+ # Include return value in verbose mode
494
+ ret_val = str(arg)
495
+ if len(ret_val) > 100:
496
+ ret_val = ret_val[:97] + "..."
497
+ self.trace_logger.debug(f"RETURN from {function} -> {ret_val}")
498
+ else:
499
+ self.trace_logger.debug(f"RETURN from {function}")
500
+
501
+ elif event == "exception" and self._trace_level >= 1:
502
+ exc_type, exc_value, exc_tb = arg
503
+ self.trace_logger.debug(
504
+ f"EXCEPTION in {filename}:{lineno} -> {exc_type.__name__}: {exc_value}"
505
+ )
506
+ if self._trace_level >= 3:
507
+ # Include traceback in verbose mode
508
+ tb_str = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
509
+ self.trace_logger.debug(f"Traceback:\n{tb_str}")
510
+ except Exception as e:
511
+ # Never let tracing errors crash the program
512
+ try:
513
+ self.trace_logger.error(f"Error in trace callback: {e}")
514
+ except:
515
+ pass
516
+ return None
517
+
518
+ # Continue tracing this thread if tracing is enabled
519
+ return self._trace_callback if self._trace_level > 0 else None
520
+
521
+
522
+ # Singleton instance accessor function
523
+ def get_logger(name="mcli.out"):
524
+ """
525
+ Get the mcli logger instance.
526
+
527
+ Args:
528
+ name: Optional logger name. Defaults to "mcli.out".
529
+
530
+ Returns:
531
+ A configured Logger instance that logs only to file.
532
+ """
533
+ return McliLogger.get_logger(name)
534
+
535
+
536
+ def get_system_trace_logger():
537
+ """
538
+ Get the system trace logger instance.
539
+
540
+ Returns:
541
+ The system trace logger for OS-level process tracing.
542
+ """
543
+ return McliLogger.get_system_trace_logger()
544
+
545
+
546
+ def enable_runtime_tracing(level: int = 1, excluded_modules: Optional[List[str]] = None):
547
+ """
548
+ Enable Python interpreter runtime tracing.
549
+
550
+ Args:
551
+ level: Tracing detail level (0=off, 1=function calls only, 2=line by line, 3=verbose)
552
+ excluded_modules: List of module prefixes to exclude from tracing
553
+ """
554
+ McliLogger.enable_runtime_tracing(level, excluded_modules)
555
+
556
+
557
+ def disable_runtime_tracing():
558
+ """Disable Python interpreter runtime tracing."""
559
+ McliLogger.disable_runtime_tracing()
560
+
561
+
562
+ def enable_system_tracing(level: int = 1, interval: int = 5):
563
+ """
564
+ Enable OS-level system tracing for process monitoring.
565
+
566
+ Args:
567
+ level: System trace level (0=off, 1=basic info, 2=detailed)
568
+ interval: Monitoring interval in seconds (minimum 1 second)
569
+ """
570
+ McliLogger.enable_system_tracing(level, interval)
571
+
572
+
573
+ def disable_system_tracing():
574
+ """Disable OS-level system tracing."""
575
+ McliLogger.disable_system_tracing()
576
+
577
+
578
+ def register_process(pid: int) -> bool:
579
+ """
580
+ Register a process for monitoring.
581
+
582
+ Args:
583
+ pid: Process ID to monitor
584
+
585
+ Returns:
586
+ True if successfully registered, False otherwise
587
+ """
588
+ return McliLogger.register_process(pid)
589
+
590
+
591
+ def register_subprocess(proc: subprocess.Popen) -> int:
592
+ """
593
+ Register a subprocess.Popen object for monitoring.
594
+
595
+ Args:
596
+ proc: A subprocess.Popen object to monitor
597
+
598
+ Returns:
599
+ The process ID if successfully registered, 0 otherwise
600
+ """
601
+ return McliLogger.register_subprocess(proc)
602
+
603
+
604
+ def unregister_process(pid: int):
605
+ """
606
+ Remove a process from monitoring.
607
+
608
+ Args:
609
+ pid: Process ID to stop monitoring
610
+ """
611
+ McliLogger.unregister_process(pid)