mcli-framework 7.8.1__py3-none-any.whl → 7.8.3__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 (75) hide show
  1. mcli/ml/dashboard/app_supabase.py +1 -0
  2. mcli/self/self_cmd.py +8 -0
  3. mcli/self/store_cmd.py +422 -0
  4. mcli/workflow/dashboard/dashboard_cmd.py +179 -0
  5. mcli_framework-7.8.3.dist-info/METADATA +615 -0
  6. {mcli_framework-7.8.1.dist-info → mcli_framework-7.8.3.dist-info}/RECORD +10 -72
  7. mcli/__init__.py +0 -160
  8. mcli/__main__.py +0 -14
  9. mcli/app/__init__.py +0 -23
  10. mcli/app/model/__init__.py +0 -0
  11. mcli/app/video/__init__.py +0 -5
  12. mcli/chat/__init__.py +0 -34
  13. mcli/lib/__init__.py +0 -0
  14. mcli/lib/api/__init__.py +0 -0
  15. mcli/lib/auth/__init__.py +0 -1
  16. mcli/lib/config/__init__.py +0 -1
  17. mcli/lib/erd/__init__.py +0 -25
  18. mcli/lib/files/__init__.py +0 -0
  19. mcli/lib/fs/__init__.py +0 -1
  20. mcli/lib/logger/__init__.py +0 -3
  21. mcli/lib/performance/__init__.py +0 -17
  22. mcli/lib/pickles/__init__.py +0 -1
  23. mcli/lib/shell/__init__.py +0 -0
  24. mcli/lib/toml/__init__.py +0 -1
  25. mcli/lib/watcher/__init__.py +0 -0
  26. mcli/ml/__init__.py +0 -16
  27. mcli/ml/api/__init__.py +0 -30
  28. mcli/ml/api/routers/__init__.py +0 -27
  29. mcli/ml/auth/__init__.py +0 -41
  30. mcli/ml/backtesting/__init__.py +0 -33
  31. mcli/ml/cli/__init__.py +0 -5
  32. mcli/ml/config/__init__.py +0 -33
  33. mcli/ml/configs/__init__.py +0 -16
  34. mcli/ml/dashboard/__init__.py +0 -12
  35. mcli/ml/dashboard/components/__init__.py +0 -7
  36. mcli/ml/dashboard/pages/__init__.py +0 -6
  37. mcli/ml/data_ingestion/__init__.py +0 -29
  38. mcli/ml/database/__init__.py +0 -40
  39. mcli/ml/experimentation/__init__.py +0 -29
  40. mcli/ml/features/__init__.py +0 -39
  41. mcli/ml/mlops/__init__.py +0 -19
  42. mcli/ml/models/__init__.py +0 -90
  43. mcli/ml/monitoring/__init__.py +0 -25
  44. mcli/ml/optimization/__init__.py +0 -27
  45. mcli/ml/predictions/__init__.py +0 -5
  46. mcli/ml/preprocessing/__init__.py +0 -24
  47. mcli/ml/scripts/__init__.py +0 -1
  48. mcli/ml/trading/__init__.py +0 -63
  49. mcli/ml/training/__init__.py +0 -7
  50. mcli/mygroup/__init__.py +0 -3
  51. mcli/public/__init__.py +0 -1
  52. mcli/public/commands/__init__.py +0 -2
  53. mcli/self/__init__.py +0 -3
  54. mcli/test/__init__.py +0 -1
  55. mcli/workflow/__init__.py +0 -0
  56. mcli/workflow/daemon/__init__.py +0 -15
  57. mcli/workflow/dashboard/__init__.py +0 -5
  58. mcli/workflow/docker/__init__.py +0 -0
  59. mcli/workflow/file/__init__.py +0 -0
  60. mcli/workflow/gcloud/__init__.py +0 -1
  61. mcli/workflow/git_commit/__init__.py +0 -0
  62. mcli/workflow/interview/__init__.py +0 -0
  63. mcli/workflow/politician_trading/__init__.py +0 -4
  64. mcli/workflow/registry/__init__.py +0 -0
  65. mcli/workflow/repo/__init__.py +0 -0
  66. mcli/workflow/scheduler/__init__.py +0 -25
  67. mcli/workflow/search/__init__.py +0 -0
  68. mcli/workflow/sync/__init__.py +0 -5
  69. mcli/workflow/videos/__init__.py +0 -1
  70. mcli/workflow/wakatime/__init__.py +0 -80
  71. mcli_framework-7.8.1.dist-info/METADATA +0 -491
  72. {mcli_framework-7.8.1.dist-info → mcli_framework-7.8.3.dist-info}/WHEEL +0 -0
  73. {mcli_framework-7.8.1.dist-info → mcli_framework-7.8.3.dist-info}/entry_points.txt +0 -0
  74. {mcli_framework-7.8.1.dist-info → mcli_framework-7.8.3.dist-info}/licenses/LICENSE +0 -0
  75. {mcli_framework-7.8.1.dist-info → mcli_framework-7.8.3.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,7 @@
1
1
  """Streamlit dashboard for ML system monitoring - Supabase version"""
2
2
 
3
3
  import asyncio
4
+ import os
4
5
  from datetime import datetime, timedelta
5
6
 
6
7
  import numpy as np
mcli/self/self_cmd.py CHANGED
@@ -1280,6 +1280,14 @@ try:
1280
1280
  except ImportError as e:
1281
1281
  logger.debug(f"Could not load visual command: {e}")
1282
1282
 
1283
+ try:
1284
+ from mcli.self.store_cmd import store
1285
+
1286
+ self_app.add_command(store, name="store")
1287
+ logger.debug("Added store command to self group")
1288
+ except ImportError as e:
1289
+ logger.debug(f"Could not load store command: {e}")
1290
+
1283
1291
  # This part is important to make the command available to the CLI
1284
1292
  if __name__ == "__main__":
1285
1293
  self_app()
mcli/self/store_cmd.py ADDED
@@ -0,0 +1,422 @@
1
+ """
2
+ Command Store Management - Sync ~/.mcli/commands/ to git
3
+ Similar to lsh secrets but for workflow commands
4
+ """
5
+
6
+ import click
7
+ import os
8
+ import shutil
9
+ import subprocess
10
+ from pathlib import Path
11
+ from datetime import datetime
12
+ from mcli.lib.ui.styling import success, error, info, warning
13
+ from mcli.lib.logger.logger import get_logger
14
+
15
+ logger = get_logger()
16
+
17
+ # Default store location
18
+ DEFAULT_STORE_PATH = Path.home() / "repos" / "mcli-commands"
19
+ COMMANDS_PATH = Path.home() / ".mcli" / "commands"
20
+
21
+
22
+ @click.group(name="store")
23
+ def store():
24
+ """Manage command store - sync ~/.mcli/commands/ to git"""
25
+ pass
26
+
27
+
28
+ @store.command(name="init")
29
+ @click.option("--path", "-p", type=click.Path(), help=f"Store path (default: {DEFAULT_STORE_PATH})")
30
+ @click.option("--remote", "-r", help="Git remote URL (optional)")
31
+ def init_store(path, remote):
32
+ """Initialize command store with git"""
33
+ store_path = Path(path) if path else DEFAULT_STORE_PATH
34
+
35
+ try:
36
+ # Create store directory
37
+ store_path.mkdir(parents=True, exist_ok=True)
38
+
39
+ # Initialize git if not already initialized
40
+ git_dir = store_path / ".git"
41
+ if not git_dir.exists():
42
+ subprocess.run(["git", "init"], cwd=store_path, check=True, capture_output=True)
43
+ success(f"Initialized git repository at {store_path}")
44
+
45
+ # Create .gitignore
46
+ gitignore = store_path / ".gitignore"
47
+ gitignore.write_text("*.backup\n.DS_Store\n")
48
+
49
+ # Create README
50
+ readme = store_path / "README.md"
51
+ readme.write_text(
52
+ f"""# MCLI Commands Store
53
+
54
+ Personal workflow commands for mcli framework.
55
+
56
+ ## Usage
57
+
58
+ Push commands:
59
+ ```bash
60
+ mcli self store push
61
+ ```
62
+
63
+ Pull commands:
64
+ ```bash
65
+ mcli self store pull
66
+ ```
67
+
68
+ Sync (bidirectional):
69
+ ```bash
70
+ mcli self store sync
71
+ ```
72
+
73
+ ## Structure
74
+
75
+ All JSON command files from `~/.mcli/commands/` are stored here and version controlled.
76
+
77
+ Last updated: {datetime.now().isoformat()}
78
+ """
79
+ )
80
+
81
+ # Add remote if provided
82
+ if remote:
83
+ subprocess.run(
84
+ ["git", "remote", "add", "origin", remote], cwd=store_path, check=True
85
+ )
86
+ success(f"Added remote: {remote}")
87
+ else:
88
+ info(f"Git repository already exists at {store_path}")
89
+
90
+ # Save store path to config
91
+ config_file = Path.home() / ".mcli" / "store.conf"
92
+ config_file.parent.mkdir(parents=True, exist_ok=True)
93
+ config_file.write_text(str(store_path))
94
+
95
+ success(f"Command store initialized at {store_path}")
96
+ info(f"Store path saved to {config_file}")
97
+
98
+ except subprocess.CalledProcessError as e:
99
+ error(f"Git command failed: {e}")
100
+ logger.error(f"Git init failed: {e}")
101
+ except Exception as e:
102
+ error(f"Failed to initialize store: {e}")
103
+ logger.exception(e)
104
+
105
+
106
+ @store.command(name="push")
107
+ @click.option("--message", "-m", help="Commit message")
108
+ @click.option("--all", "-a", is_flag=True, help="Push all files (including backups)")
109
+ def push_commands(message, all):
110
+ """Push commands from ~/.mcli/commands/ to git store"""
111
+ try:
112
+ store_path = _get_store_path()
113
+
114
+ # Copy commands to store
115
+ info(f"Copying commands from {COMMANDS_PATH} to {store_path}...")
116
+
117
+ copied_count = 0
118
+ for item in COMMANDS_PATH.glob("*"):
119
+ # Skip backups unless --all specified
120
+ if not all and item.name.endswith(".backup"):
121
+ continue
122
+
123
+ dest = store_path / item.name
124
+ if item.is_file():
125
+ shutil.copy2(item, dest)
126
+ copied_count += 1
127
+ elif item.is_dir():
128
+ shutil.copytree(item, dest, dirs_exist_ok=True)
129
+ copied_count += 1
130
+
131
+ success(f"Copied {copied_count} items to store")
132
+
133
+ # Git add, commit, push
134
+ subprocess.run(["git", "add", "."], cwd=store_path, check=True)
135
+
136
+ # Check if there are changes
137
+ result = subprocess.run(
138
+ ["git", "status", "--porcelain"], cwd=store_path, capture_output=True, text=True
139
+ )
140
+
141
+ if not result.stdout.strip():
142
+ info("No changes to commit")
143
+ return
144
+
145
+ # Commit with message
146
+ commit_msg = message or f"Update commands {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
147
+ subprocess.run(["git", "commit", "-m", commit_msg], cwd=store_path, check=True)
148
+ success(f"Committed changes: {commit_msg}")
149
+
150
+ # Push to remote if configured
151
+ try:
152
+ subprocess.run(["git", "push"], cwd=store_path, check=True, capture_output=True)
153
+ success("Pushed to remote")
154
+ except subprocess.CalledProcessError:
155
+ warning("No remote configured or push failed. Commands committed locally.")
156
+
157
+ except Exception as e:
158
+ error(f"Failed to push commands: {e}")
159
+ logger.exception(e)
160
+
161
+
162
+ @store.command(name="pull")
163
+ @click.option("--force", "-f", is_flag=True, help="Overwrite local commands without backup")
164
+ def pull_commands(force):
165
+ """Pull commands from git store to ~/.mcli/commands/"""
166
+ try:
167
+ store_path = _get_store_path()
168
+
169
+ # Pull from remote
170
+ try:
171
+ subprocess.run(["git", "pull"], cwd=store_path, check=True)
172
+ success("Pulled latest changes from remote")
173
+ except subprocess.CalledProcessError:
174
+ warning("No remote configured or pull failed. Using local store.")
175
+
176
+ # Backup existing commands if not force
177
+ if not force and COMMANDS_PATH.exists():
178
+ backup_dir = (
179
+ COMMANDS_PATH.parent / f"commands_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
180
+ )
181
+ shutil.copytree(COMMANDS_PATH, backup_dir)
182
+ info(f"Backed up existing commands to {backup_dir}")
183
+
184
+ # Copy from store to commands directory
185
+ info(f"Copying commands from {store_path} to {COMMANDS_PATH}...")
186
+
187
+ COMMANDS_PATH.mkdir(parents=True, exist_ok=True)
188
+
189
+ copied_count = 0
190
+ for item in store_path.glob("*"):
191
+ # Skip git directory and README
192
+ if item.name in [".git", "README.md", ".gitignore"]:
193
+ continue
194
+
195
+ dest = COMMANDS_PATH / item.name
196
+ if item.is_file():
197
+ shutil.copy2(item, dest)
198
+ copied_count += 1
199
+ elif item.is_dir():
200
+ shutil.copytree(item, dest, dirs_exist_ok=True)
201
+ copied_count += 1
202
+
203
+ success(f"Pulled {copied_count} items from store")
204
+
205
+ except Exception as e:
206
+ error(f"Failed to pull commands: {e}")
207
+ logger.exception(e)
208
+
209
+
210
+ @store.command(name="sync")
211
+ @click.option("--message", "-m", help="Commit message if pushing")
212
+ def sync_commands(message):
213
+ """Sync commands bidirectionally (pull then push if changes)"""
214
+ try:
215
+ store_path = _get_store_path()
216
+
217
+ # First pull
218
+ info("Pulling latest changes...")
219
+ try:
220
+ subprocess.run(["git", "pull"], cwd=store_path, check=True, capture_output=True)
221
+ success("Pulled from remote")
222
+ except subprocess.CalledProcessError:
223
+ warning("No remote or pull failed")
224
+
225
+ # Then push local changes
226
+ info("Pushing local changes...")
227
+
228
+ # Copy commands
229
+ for item in COMMANDS_PATH.glob("*"):
230
+ if item.name.endswith(".backup"):
231
+ continue
232
+ dest = store_path / item.name
233
+ if item.is_file():
234
+ shutil.copy2(item, dest)
235
+ elif item.is_dir():
236
+ shutil.copytree(item, dest, dirs_exist_ok=True)
237
+
238
+ # Check for changes
239
+ result = subprocess.run(
240
+ ["git", "status", "--porcelain"], cwd=store_path, capture_output=True, text=True
241
+ )
242
+
243
+ if not result.stdout.strip():
244
+ success("Everything in sync!")
245
+ return
246
+
247
+ # Commit and push
248
+ subprocess.run(["git", "add", "."], cwd=store_path, check=True)
249
+ commit_msg = message or f"Sync commands {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
250
+ subprocess.run(["git", "commit", "-m", commit_msg], cwd=store_path, check=True)
251
+
252
+ try:
253
+ subprocess.run(["git", "push"], cwd=store_path, check=True, capture_output=True)
254
+ success("Synced and pushed to remote")
255
+ except subprocess.CalledProcessError:
256
+ success("Synced locally (no remote configured)")
257
+
258
+ except Exception as e:
259
+ error(f"Sync failed: {e}")
260
+ logger.exception(e)
261
+
262
+
263
+ @store.command(name="list")
264
+ @click.option("--store", "-s", is_flag=True, help="List store instead of local")
265
+ def list_commands(store):
266
+ """List all commands"""
267
+ try:
268
+ if store:
269
+ store_path = _get_store_path()
270
+ path = store_path
271
+ title = f"Commands in store ({store_path})"
272
+ else:
273
+ path = COMMANDS_PATH
274
+ title = f"Local commands ({COMMANDS_PATH})"
275
+
276
+ click.echo(f"\n{title}:\n")
277
+
278
+ if not path.exists():
279
+ warning(f"Directory does not exist: {path}")
280
+ return
281
+
282
+ items = sorted(path.glob("*"))
283
+ if not items:
284
+ info("No commands found")
285
+ return
286
+
287
+ for item in items:
288
+ if item.name in [".git", ".gitignore", "README.md"]:
289
+ continue
290
+
291
+ if item.is_file():
292
+ size = item.stat().st_size / 1024
293
+ modified = datetime.fromtimestamp(item.stat().st_mtime).strftime("%Y-%m-%d %H:%M")
294
+ click.echo(f" 📄 {item.name:<40} {size:>8.1f} KB {modified}")
295
+ elif item.is_dir():
296
+ count = len(list(item.glob("*")))
297
+ click.echo(f" 📁 {item.name:<40} {count:>3} files")
298
+
299
+ click.echo()
300
+
301
+ except Exception as e:
302
+ error(f"Failed to list commands: {e}")
303
+ logger.exception(e)
304
+
305
+
306
+ @store.command(name="status")
307
+ def store_status():
308
+ """Show git status of command store"""
309
+ try:
310
+ store_path = _get_store_path()
311
+
312
+ click.echo(f"\n📦 Store: {store_path}\n")
313
+
314
+ # Git status
315
+ result = subprocess.run(
316
+ ["git", "status", "--short", "--branch"], cwd=store_path, capture_output=True, text=True
317
+ )
318
+
319
+ if result.stdout:
320
+ click.echo(result.stdout)
321
+
322
+ # Show remote
323
+ result = subprocess.run(
324
+ ["git", "remote", "-v"], cwd=store_path, capture_output=True, text=True
325
+ )
326
+
327
+ if result.stdout:
328
+ click.echo("\n🌐 Remotes:")
329
+ click.echo(result.stdout)
330
+ else:
331
+ info("\nNo remote configured")
332
+
333
+ click.echo()
334
+
335
+ except Exception as e:
336
+ error(f"Failed to get status: {e}")
337
+ logger.exception(e)
338
+
339
+
340
+ @store.command(name="show")
341
+ @click.argument("command_name")
342
+ @click.option("--store", "-s", is_flag=True, help="Show from store instead of local")
343
+ def show_command(command_name, store):
344
+ """Show command file contents"""
345
+ try:
346
+ if store:
347
+ store_path = _get_store_path()
348
+ path = store_path / command_name
349
+ else:
350
+ path = COMMANDS_PATH / command_name
351
+
352
+ if not path.exists():
353
+ error(f"Command not found: {command_name}")
354
+ return
355
+
356
+ if path.is_file():
357
+ click.echo(f"\n📄 {path}:\n")
358
+ click.echo(path.read_text())
359
+ else:
360
+ info(f"{command_name} is a directory")
361
+ for item in sorted(path.glob("*")):
362
+ click.echo(f" {item.name}")
363
+
364
+ click.echo()
365
+
366
+ except Exception as e:
367
+ error(f"Failed to show command: {e}")
368
+ logger.exception(e)
369
+
370
+
371
+ @store.command(name="config")
372
+ @click.option("--remote", "-r", help="Set git remote URL")
373
+ @click.option("--path", "-p", type=click.Path(), help="Change store path")
374
+ def configure_store(remote, path):
375
+ """Configure store settings"""
376
+ try:
377
+ store_path = _get_store_path()
378
+
379
+ if path:
380
+ new_path = Path(path).expanduser().resolve()
381
+ config_file = Path.home() / ".mcli" / "store.conf"
382
+ config_file.write_text(str(new_path))
383
+ success(f"Store path updated to: {new_path}")
384
+ return
385
+
386
+ if remote:
387
+ # Check if remote exists
388
+ result = subprocess.run(
389
+ ["git", "remote"], cwd=store_path, capture_output=True, text=True
390
+ )
391
+
392
+ if "origin" in result.stdout:
393
+ subprocess.run(
394
+ ["git", "remote", "set-url", "origin", remote], cwd=store_path, check=True
395
+ )
396
+ success(f"Updated remote URL: {remote}")
397
+ else:
398
+ subprocess.run(
399
+ ["git", "remote", "add", "origin", remote], cwd=store_path, check=True
400
+ )
401
+ success(f"Added remote URL: {remote}")
402
+
403
+ except Exception as e:
404
+ error(f"Configuration failed: {e}")
405
+ logger.exception(e)
406
+
407
+
408
+ def _get_store_path() -> Path:
409
+ """Get store path from config or default"""
410
+ config_file = Path.home() / ".mcli" / "store.conf"
411
+
412
+ if config_file.exists():
413
+ store_path = Path(config_file.read_text().strip())
414
+ if store_path.exists():
415
+ return store_path
416
+
417
+ # Use default
418
+ return DEFAULT_STORE_PATH
419
+
420
+
421
+ if __name__ == "__main__":
422
+ store()
@@ -0,0 +1,179 @@
1
+ """ML Dashboard commands for mcli."""
2
+
3
+ import subprocess
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import click
8
+
9
+ from mcli.lib.logger.logger import get_logger
10
+
11
+ logger = get_logger(__name__)
12
+
13
+
14
+ @click.group(name="dashboard")
15
+ def dashboard():
16
+ """ML monitoring dashboard commands."""
17
+ pass
18
+
19
+
20
+ @dashboard.command()
21
+ @click.option("--port", "-p", default=8501, help="Port to run dashboard on")
22
+ @click.option("--host", "-h", default="localhost", help="Host to bind to")
23
+ @click.option("--debug", is_flag=True, help="Run in debug mode")
24
+ @click.option(
25
+ "--variant",
26
+ "-v",
27
+ type=click.Choice(["integrated", "supabase", "training"]),
28
+ default="supabase",
29
+ help="Dashboard variant to launch (default: supabase)",
30
+ )
31
+ def launch(port, host, debug, variant):
32
+ """Launch the ML monitoring dashboard.
33
+
34
+ Variants:
35
+ - supabase: Politician trading dashboard with Supabase integration (default)
36
+ - integrated: Full ML dashboard with local database
37
+ - training: ML training dashboard
38
+ """
39
+
40
+ click.echo(f"🚀 Starting {variant.title()} Dashboard on http://{host}:{port}")
41
+
42
+ # Get the dashboard app path based on variant
43
+ dashboard_dir = Path(__file__).parent.parent.parent / "ml" / "dashboard"
44
+
45
+ if variant == "supabase":
46
+ dashboard_path = dashboard_dir / "app_supabase.py"
47
+ elif variant == "integrated":
48
+ dashboard_path = dashboard_dir / "app_integrated.py"
49
+ elif variant == "training":
50
+ dashboard_path = dashboard_dir / "app_training.py"
51
+ else:
52
+ dashboard_path = dashboard_dir / "app.py"
53
+
54
+ if not dashboard_path.exists():
55
+ click.echo(f"❌ Dashboard app not found at {dashboard_path}!")
56
+ logger.error(f"Dashboard app not found at {dashboard_path}")
57
+ sys.exit(1)
58
+
59
+ # Build streamlit command
60
+ cmd = [
61
+ sys.executable,
62
+ "-m",
63
+ "streamlit",
64
+ "run",
65
+ str(dashboard_path),
66
+ "--server.port",
67
+ str(port),
68
+ "--server.address",
69
+ host,
70
+ "--browser.gatherUsageStats",
71
+ "false",
72
+ ]
73
+
74
+ if debug:
75
+ cmd.extend(["--logger.level", "debug"])
76
+
77
+ click.echo(f"📊 Dashboard is starting from {dashboard_path.name}...")
78
+ click.echo("Press Ctrl+C to stop")
79
+
80
+ try:
81
+ subprocess.run(cmd, check=True)
82
+ except KeyboardInterrupt:
83
+ click.echo("\n⏹️ Dashboard stopped")
84
+ except subprocess.CalledProcessError as e:
85
+ click.echo(f"❌ Failed to start dashboard: {e}")
86
+ logger.error(f"Dashboard failed to start: {e}")
87
+ sys.exit(1)
88
+
89
+
90
+ @dashboard.command()
91
+ def info():
92
+ """Show dashboard information and status."""
93
+
94
+ click.echo("📊 ML Dashboard Information")
95
+ click.echo("━" * 40)
96
+
97
+ # Check if dependencies are installed
98
+ try:
99
+ import plotly
100
+ import streamlit
101
+
102
+ click.echo("✅ Dashboard dependencies installed")
103
+ click.echo(f" Streamlit version: {streamlit.__version__}")
104
+ click.echo(f" Plotly version: {plotly.__version__}")
105
+ except ImportError as e:
106
+ click.echo(f"❌ Missing dependencies: {e}")
107
+ click.echo(" Run: uv pip install streamlit plotly")
108
+
109
+ # Check Supabase configuration
110
+ import os
111
+
112
+ supabase_url = os.getenv("SUPABASE_URL")
113
+ supabase_key = os.getenv("SUPABASE_KEY") or os.getenv("SUPABASE_ANON_KEY")
114
+
115
+ click.echo("\n🔌 Supabase Configuration:")
116
+ if supabase_url:
117
+ click.echo(f" URL: {supabase_url}")
118
+ else:
119
+ click.echo(" ❌ SUPABASE_URL not set")
120
+
121
+ if supabase_key:
122
+ click.echo(f" Key: {'*' * 20}...{supabase_key[-8:]}")
123
+ else:
124
+ click.echo(" ❌ SUPABASE_KEY not set")
125
+
126
+ if not supabase_url or not supabase_key:
127
+ click.echo("\n⚠️ To configure Supabase, set environment variables:")
128
+ click.echo(" export SUPABASE_URL=https://your-project.supabase.co")
129
+ click.echo(" export SUPABASE_KEY=your-anon-key")
130
+
131
+ click.echo("\n💡 Quick start:")
132
+ click.echo(" mcli workflow dashboard launch # Launch Supabase dashboard")
133
+ click.echo(" mcli workflow dashboard launch --variant training # Launch training dashboard")
134
+ click.echo(" mcli workflow dashboard launch --port 8502 # Custom port")
135
+ click.echo(" mcli workflow dashboard launch --host 0.0.0.0 # Expose to network")
136
+
137
+
138
+ @dashboard.command()
139
+ def test():
140
+ """Test Supabase connection."""
141
+ import os
142
+ from mcli.ml.dashboard.common import get_supabase_client
143
+
144
+ click.echo("🔍 Testing Supabase connection...")
145
+ click.echo("━" * 40)
146
+
147
+ # Check environment variables
148
+ url = os.getenv("SUPABASE_URL")
149
+ key = os.getenv("SUPABASE_KEY") or os.getenv("SUPABASE_ANON_KEY")
150
+
151
+ if not url:
152
+ click.echo("❌ SUPABASE_URL not set")
153
+ sys.exit(1)
154
+
155
+ if not key:
156
+ click.echo("❌ SUPABASE_KEY or SUPABASE_ANON_KEY not set")
157
+ sys.exit(1)
158
+
159
+ click.echo(f"✅ URL configured: {url}")
160
+ click.echo(f"✅ Key configured: {'*' * 20}...{key[-8:]}")
161
+
162
+ # Try to connect
163
+ client = get_supabase_client()
164
+
165
+ if not client:
166
+ click.echo("❌ Failed to create Supabase client")
167
+ sys.exit(1)
168
+
169
+ click.echo("✅ Supabase client created")
170
+
171
+ # Try to query a table
172
+ try:
173
+ response = client.table("politicians").select("id").limit(1).execute()
174
+ click.echo(f"✅ Successfully queried 'politicians' table ({len(response.data)} records)")
175
+ except Exception as e:
176
+ click.echo(f"⚠️ Could not query 'politicians' table: {e}")
177
+ click.echo(" (Table might not exist yet)")
178
+
179
+ click.echo("\n✅ Connection test passed!")