mcli-framework 7.5.1__py3-none-any.whl → 7.6.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mcli-framework might be problematic. Click here for more details.

Files changed (56) hide show
  1. mcli/app/commands_cmd.py +51 -39
  2. mcli/app/completion_helpers.py +4 -13
  3. mcli/app/main.py +21 -25
  4. mcli/app/model_cmd.py +119 -9
  5. mcli/lib/custom_commands.py +16 -11
  6. mcli/ml/api/app.py +1 -5
  7. mcli/ml/dashboard/app.py +2 -2
  8. mcli/ml/dashboard/app_integrated.py +168 -116
  9. mcli/ml/dashboard/app_supabase.py +7 -3
  10. mcli/ml/dashboard/app_training.py +3 -6
  11. mcli/ml/dashboard/components/charts.py +74 -115
  12. mcli/ml/dashboard/components/metrics.py +24 -44
  13. mcli/ml/dashboard/components/tables.py +32 -40
  14. mcli/ml/dashboard/overview.py +102 -78
  15. mcli/ml/dashboard/pages/cicd.py +103 -56
  16. mcli/ml/dashboard/pages/debug_dependencies.py +35 -28
  17. mcli/ml/dashboard/pages/gravity_viz.py +374 -313
  18. mcli/ml/dashboard/pages/monte_carlo_predictions.py +50 -48
  19. mcli/ml/dashboard/pages/predictions_enhanced.py +396 -248
  20. mcli/ml/dashboard/pages/scrapers_and_logs.py +299 -273
  21. mcli/ml/dashboard/pages/test_portfolio.py +153 -121
  22. mcli/ml/dashboard/pages/trading.py +238 -169
  23. mcli/ml/dashboard/pages/workflows.py +129 -84
  24. mcli/ml/dashboard/streamlit_extras_utils.py +70 -79
  25. mcli/ml/dashboard/utils.py +24 -21
  26. mcli/ml/dashboard/warning_suppression.py +6 -4
  27. mcli/ml/database/session.py +16 -5
  28. mcli/ml/mlops/pipeline_orchestrator.py +1 -3
  29. mcli/ml/predictions/monte_carlo.py +6 -18
  30. mcli/ml/trading/alpaca_client.py +95 -96
  31. mcli/ml/trading/migrations.py +76 -40
  32. mcli/ml/trading/models.py +78 -60
  33. mcli/ml/trading/paper_trading.py +92 -74
  34. mcli/ml/trading/risk_management.py +106 -85
  35. mcli/ml/trading/trading_service.py +155 -110
  36. mcli/ml/training/train_model.py +1 -3
  37. mcli/{app → self}/completion_cmd.py +6 -6
  38. mcli/self/self_cmd.py +100 -57
  39. mcli/test/test_cmd.py +30 -0
  40. mcli/workflow/daemon/daemon.py +2 -0
  41. mcli/workflow/model_service/openai_adapter.py +347 -0
  42. mcli/workflow/politician_trading/models.py +6 -2
  43. mcli/workflow/politician_trading/scrapers_corporate_registry.py +39 -88
  44. mcli/workflow/politician_trading/scrapers_free_sources.py +32 -39
  45. mcli/workflow/politician_trading/scrapers_third_party.py +21 -39
  46. mcli/workflow/politician_trading/seed_database.py +70 -89
  47. {mcli_framework-7.5.1.dist-info → mcli_framework-7.6.1.dist-info}/METADATA +1 -1
  48. {mcli_framework-7.5.1.dist-info → mcli_framework-7.6.1.dist-info}/RECORD +56 -54
  49. /mcli/{app → self}/logs_cmd.py +0 -0
  50. /mcli/{app → self}/redis_cmd.py +0 -0
  51. /mcli/{app → self}/visual_cmd.py +0 -0
  52. /mcli/{app → test}/cron_test_cmd.py +0 -0
  53. {mcli_framework-7.5.1.dist-info → mcli_framework-7.6.1.dist-info}/WHEEL +0 -0
  54. {mcli_framework-7.5.1.dist-info → mcli_framework-7.6.1.dist-info}/entry_points.txt +0 -0
  55. {mcli_framework-7.5.1.dist-info → mcli_framework-7.6.1.dist-info}/licenses/LICENSE +0 -0
  56. {mcli_framework-7.5.1.dist-info → mcli_framework-7.6.1.dist-info}/top_level.txt +0 -0
mcli/app/commands_cmd.py CHANGED
@@ -290,7 +290,9 @@ def {name}_command(name: str = "World"):
290
290
  return template
291
291
 
292
292
 
293
- def open_editor_for_command(command_name: str, command_group: str, description: str) -> Optional[str]:
293
+ def open_editor_for_command(
294
+ command_name: str, command_group: str, description: str
295
+ ) -> Optional[str]:
294
296
  """
295
297
  Open the user's default editor to allow them to write command logic.
296
298
 
@@ -306,16 +308,18 @@ def open_editor_for_command(command_name: str, command_group: str, description:
306
308
  import sys
307
309
 
308
310
  # Get the user's default editor
309
- editor = os.environ.get('EDITOR')
311
+ editor = os.environ.get("EDITOR")
310
312
  if not editor:
311
313
  # Try common editors in order of preference
312
- for common_editor in ['vim', 'nano', 'code', 'subl', 'atom', 'emacs']:
313
- if subprocess.run(['which', common_editor], capture_output=True).returncode == 0:
314
+ for common_editor in ["vim", "nano", "code", "subl", "atom", "emacs"]:
315
+ if subprocess.run(["which", common_editor], capture_output=True).returncode == 0:
314
316
  editor = common_editor
315
317
  break
316
318
 
317
319
  if not editor:
318
- click.echo("No editor found. Please set the EDITOR environment variable or install vim/nano.")
320
+ click.echo(
321
+ "No editor found. Please set the EDITOR environment variable or install vim/nano."
322
+ )
319
323
  return None
320
324
 
321
325
  # Create a temporary file with the template
@@ -337,7 +341,7 @@ Example Click command structure:
337
341
  @click.command()
338
342
  @click.argument('name', default='World')
339
343
  def my_command(name):
340
- \"""My custom command.\"""
344
+ """My custom command."""
341
345
  click.echo(f"Hello, {{name}}!")
342
346
  """
343
347
  import click
@@ -363,14 +367,16 @@ logger = get_logger()
363
367
  '''
364
368
 
365
369
  # Create temporary file
366
- with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as temp_file:
370
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as temp_file:
367
371
  temp_file.write(enhanced_template)
368
372
  temp_file_path = temp_file.name
369
373
 
370
374
  try:
371
375
  # Check if we're in an interactive environment
372
376
  if not sys.stdin.isatty() or not sys.stdout.isatty():
373
- click.echo("Editor requires an interactive terminal. Use --template flag for non-interactive mode.")
377
+ click.echo(
378
+ "Editor requires an interactive terminal. Use --template flag for non-interactive mode."
379
+ )
374
380
  return None
375
381
 
376
382
  # Open editor
@@ -386,7 +392,7 @@ logger = get_logger()
386
392
  return None
387
393
 
388
394
  # Read the edited content
389
- with open(temp_file_path, 'r') as f:
395
+ with open(temp_file_path, "r") as f:
390
396
  edited_code = f.read()
391
397
 
392
398
  # Check if the file was actually edited (not just the template)
@@ -395,12 +401,12 @@ logger = get_logger()
395
401
  return None
396
402
 
397
403
  # Extract the actual command code (remove the instructions)
398
- lines = edited_code.split('\n')
404
+ lines = edited_code.split("\n")
399
405
  code_lines = []
400
406
  in_code_section = False
401
407
 
402
408
  for line in lines:
403
- if line.strip().startswith('# Your command implementation goes here:'):
409
+ if line.strip().startswith("# Your command implementation goes here:"):
404
410
  in_code_section = True
405
411
  continue
406
412
  if in_code_section:
@@ -410,7 +416,7 @@ logger = get_logger()
410
416
  # Fallback: use the entire file content
411
417
  code_lines = lines
412
418
 
413
- final_code = '\n'.join(code_lines).strip()
419
+ final_code = "\n".join(code_lines).strip()
414
420
 
415
421
  if not final_code:
416
422
  click.echo("No command code found. Command creation cancelled.")
@@ -436,11 +442,12 @@ logger = get_logger()
436
442
  @commands.command("add")
437
443
  @click.argument("command_name", required=True)
438
444
  @click.option("--group", "-g", help="Command group (defaults to 'workflow')", default="workflow")
445
+ @click.option("--description", "-d", help="Description for the command", default="Custom command")
439
446
  @click.option(
440
- "--description", "-d", help="Description for the command", default="Custom command"
441
- )
442
- @click.option(
443
- "--template", "-t", is_flag=True, help="Use template mode (skip editor and use predefined template)"
447
+ "--template",
448
+ "-t",
449
+ is_flag=True,
450
+ help="Use template mode (skip editor and use predefined template)",
444
451
  )
445
452
  def add_command(command_name, group, description, template):
446
453
  """
@@ -734,13 +741,13 @@ def edit_command(command_name, editor):
734
741
  return 1
735
742
 
736
743
  try:
737
- with open(command_file, 'r') as f:
744
+ with open(command_file, "r") as f:
738
745
  command_data = json.load(f)
739
746
  except Exception as e:
740
747
  console.print(f"[red]Failed to load command: {e}[/red]")
741
748
  return 1
742
749
 
743
- code = command_data.get('code', '')
750
+ code = command_data.get("code", "")
744
751
 
745
752
  if not code:
746
753
  console.print(f"[red]Command has no code: {command_name}[/red]")
@@ -748,13 +755,14 @@ def edit_command(command_name, editor):
748
755
 
749
756
  # Determine editor
750
757
  if not editor:
751
- editor = os.environ.get('EDITOR', 'vim')
758
+ editor = os.environ.get("EDITOR", "vim")
752
759
 
753
760
  console.print(f"Opening command in {editor}...")
754
761
 
755
762
  # Create temp file with the code
756
- with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False,
757
- prefix=f"{command_name}_") as tmp:
763
+ with tempfile.NamedTemporaryFile(
764
+ mode="w", suffix=".py", delete=False, prefix=f"{command_name}_"
765
+ ) as tmp:
758
766
  tmp.write(code)
759
767
  tmp_path = tmp.name
760
768
 
@@ -766,7 +774,7 @@ def edit_command(command_name, editor):
766
774
  console.print(f"[yellow]Editor exited with code {result.returncode}[/yellow]")
767
775
 
768
776
  # Read edited content
769
- with open(tmp_path, 'r') as f:
777
+ with open(tmp_path, "r") as f:
770
778
  new_code = f.read()
771
779
 
772
780
  # Check if code changed
@@ -776,20 +784,18 @@ def edit_command(command_name, editor):
776
784
 
777
785
  # Validate syntax
778
786
  try:
779
- compile(new_code, '<string>', 'exec')
787
+ compile(new_code, "<string>", "exec")
780
788
  except SyntaxError as e:
781
789
  console.print(f"[red]Syntax error in edited code: {e}[/red]")
782
- should_save = Prompt.ask(
783
- "Save anyway?", choices=["y", "n"], default="n"
784
- )
790
+ should_save = Prompt.ask("Save anyway?", choices=["y", "n"], default="n")
785
791
  if should_save.lower() != "y":
786
792
  return 1
787
793
 
788
794
  # Update the command
789
- command_data['code'] = new_code
790
- command_data['updated_at'] = datetime.now().isoformat()
795
+ command_data["code"] = new_code
796
+ command_data["updated_at"] = datetime.now().isoformat()
791
797
 
792
- with open(command_file, 'w') as f:
798
+ with open(command_file, "w") as f:
793
799
  json.dump(command_data, f, indent=2)
794
800
 
795
801
  # Update lockfile
@@ -832,7 +838,7 @@ def import_script(script_path, name, group, description, interactive):
832
838
 
833
839
  # Read the script content
834
840
  try:
835
- with open(script_file, 'r') as f:
841
+ with open(script_file, "r") as f:
836
842
  code = f.read()
837
843
  except Exception as e:
838
844
  console.print(f"[red]Failed to read script: {e}[/red]")
@@ -849,11 +855,11 @@ def import_script(script_path, name, group, description, interactive):
849
855
 
850
856
  # Interactive editing
851
857
  if interactive:
852
- editor = os.environ.get('EDITOR', 'vim')
858
+ editor = os.environ.get("EDITOR", "vim")
853
859
  console.print(f"Opening in {editor} for review...")
854
860
 
855
861
  # Create temp file with the code
856
- with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as tmp:
862
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as tmp:
857
863
  tmp.write(code)
858
864
  tmp_path = tmp.name
859
865
 
@@ -861,7 +867,7 @@ def import_script(script_path, name, group, description, interactive):
861
867
  subprocess.run([editor, tmp_path], check=True)
862
868
 
863
869
  # Read edited content
864
- with open(tmp_path, 'r') as f:
870
+ with open(tmp_path, "r") as f:
865
871
  code = f.read()
866
872
  finally:
867
873
  Path(tmp_path).unlink(missing_ok=True)
@@ -870,6 +876,7 @@ def import_script(script_path, name, group, description, interactive):
870
876
  if not description:
871
877
  # Try to extract from docstring
872
878
  import ast
879
+
873
880
  try:
874
881
  tree = ast.parse(code)
875
882
  description = ast.get_docstring(tree) or f"Imported from {script_file.name}"
@@ -887,8 +894,8 @@ def import_script(script_path, name, group, description, interactive):
887
894
  metadata={
888
895
  "source": "import-script",
889
896
  "original_file": str(script_file),
890
- "imported_at": datetime.now().isoformat()
891
- }
897
+ "imported_at": datetime.now().isoformat(),
898
+ },
892
899
  )
893
900
 
894
901
  console.print(f"[green]Imported script as command: {name}[/green]")
@@ -902,7 +909,12 @@ def import_script(script_path, name, group, description, interactive):
902
909
  @commands.command("export-script")
903
910
  @click.argument("command_name")
904
911
  @click.option("--output", "-o", type=click.Path(), help="Output file path")
905
- @click.option("--standalone", "-s", is_flag=True, help="Make script standalone (add if __name__ == '__main__')")
912
+ @click.option(
913
+ "--standalone",
914
+ "-s",
915
+ is_flag=True,
916
+ help="Make script standalone (add if __name__ == '__main__')",
917
+ )
906
918
  def export_script(command_name, output, standalone):
907
919
  """
908
920
  Export a JSON command to a Python script.
@@ -923,14 +935,14 @@ def export_script(command_name, output, standalone):
923
935
  return 1
924
936
 
925
937
  try:
926
- with open(command_file, 'r') as f:
938
+ with open(command_file, "r") as f:
927
939
  command_data = json.load(f)
928
940
  except Exception as e:
929
941
  console.print(f"[red]Failed to load command: {e}[/red]")
930
942
  return 1
931
943
 
932
944
  # Get the code
933
- code = command_data.get('code', '')
945
+ code = command_data.get("code", "")
934
946
 
935
947
  if not code:
936
948
  console.print(f"[red]Command has no code: {command_name}[/red]")
@@ -950,7 +962,7 @@ def export_script(command_name, output, standalone):
950
962
 
951
963
  # Write the script
952
964
  try:
953
- with open(output_file, 'w') as f:
965
+ with open(output_file, "w") as f:
954
966
  f.write(code)
955
967
  except Exception as e:
956
968
  console.print(f"[red]Failed to write script: {e}[/red]")
@@ -183,22 +183,13 @@ class CompletionAwareLazyGroup(click.Group):
183
183
  group = self._load_group()
184
184
  return group.list_commands(ctx)
185
185
 
186
- def shell_complete(self, ctx, incomplete):
186
+ def shell_complete(self, ctx, param, incomplete):
187
187
  """Provide shell completion using static data when possible."""
188
- # For workflow group, provide static subcommand completions
189
- if self.name in LAZY_COMMAND_COMPLETIONS:
190
- data = LAZY_COMMAND_COMPLETIONS[self.name]
191
- if "subcommands" in data:
192
- items = []
193
- for subcommand in data["subcommands"]:
194
- if subcommand.startswith(incomplete):
195
- items.append(CompletionItem(subcommand))
196
- return items
197
-
198
- # Fallback to loading the actual group
188
+ # Load the actual group to get proper completion for nested commands
189
+ # This ensures file path completion works for subcommands
199
190
  group = self._load_group()
200
191
  if hasattr(group, "shell_complete"):
201
- return group.shell_complete(ctx, incomplete)
192
+ return group.shell_complete(ctx, param, incomplete)
202
193
  return []
203
194
 
204
195
  def get_params(self, ctx):
mcli/app/main.py CHANGED
@@ -255,9 +255,15 @@ class LazyCommand(click.Command):
255
255
  def shell_complete(self, ctx, param, incomplete):
256
256
  """Provide shell completion for the lazily loaded command."""
257
257
  cmd = self._load_command()
258
+ # Delegate to the loaded command's completion
258
259
  if hasattr(cmd, "shell_complete"):
259
260
  return cmd.shell_complete(ctx, param, incomplete)
260
- return []
261
+ # Fallback to default Click completion
262
+ return (
263
+ super().shell_complete(ctx, param, incomplete)
264
+ if hasattr(super(), "shell_complete")
265
+ else []
266
+ )
261
267
 
262
268
 
263
269
  class LazyGroup(click.Group):
@@ -309,9 +315,15 @@ class LazyGroup(click.Group):
309
315
  def shell_complete(self, ctx, param, incomplete):
310
316
  """Provide shell completion for the lazily loaded group."""
311
317
  group = self._load_group()
318
+ # Delegate to the loaded group's completion
312
319
  if hasattr(group, "shell_complete"):
313
320
  return group.shell_complete(ctx, param, incomplete)
314
- return []
321
+ # Fallback to default Click completion
322
+ return (
323
+ super().shell_complete(ctx, param, incomplete)
324
+ if hasattr(super(), "shell_complete")
325
+ else []
326
+ )
315
327
 
316
328
 
317
329
  def _add_lazy_commands(app: click.Group):
@@ -334,14 +346,14 @@ def _add_lazy_commands(app: click.Group):
334
346
  except Exception as e:
335
347
  logger.debug(f"Could not load self commands: {e}")
336
348
 
337
- # Shell completion - load immediately as it's lightweight and useful
349
+ # Test group - load immediately for testing commands
338
350
  try:
339
- from mcli.app.completion_cmd import completion
351
+ from mcli.test.test_cmd import test_group
340
352
 
341
- app.add_command(completion, name="completion")
342
- logger.debug("Added completion commands")
343
- except ImportError as e:
344
- logger.debug(f"Could not load completion commands: {e}")
353
+ app.add_command(test_group, name="test")
354
+ logger.debug("Added test group commands")
355
+ except Exception as e:
356
+ logger.debug(f"Could not load test commands: {e}")
345
357
 
346
358
  # Add workflow with completion-aware lazy loading
347
359
  try:
@@ -374,22 +386,6 @@ def _add_lazy_commands(app: click.Group):
374
386
  "import_path": "mcli.app.model_cmd.model",
375
387
  "help": "Model management commands for offline and online model usage",
376
388
  },
377
- "cron-test": {
378
- "import_path": "mcli.app.cron_test_cmd.cron_test",
379
- "help": "🕒 Validate and test MCLI cron/scheduler functionality with comprehensive tests.",
380
- },
381
- "visual": {
382
- "import_path": "mcli.app.visual_cmd.visual",
383
- "help": "🎨 Visual effects and enhancements showcase",
384
- },
385
- "redis": {
386
- "import_path": "mcli.app.redis_cmd.redis_group",
387
- "help": "🗄️ Manage Redis cache service for performance optimization",
388
- },
389
- "logs": {
390
- "import_path": "mcli.app.logs_cmd.logs_group",
391
- "help": "📋 Stream and manage MCLI log files with real-time updates",
392
- },
393
389
  }
394
390
 
395
391
  for cmd_name, cmd_info in lazy_commands.items():
@@ -397,7 +393,7 @@ def _add_lazy_commands(app: click.Group):
397
393
  if cmd_name == "workflow":
398
394
  continue
399
395
 
400
- if cmd_name in ["model", "redis", "logs"]:
396
+ if cmd_name in ["model"]:
401
397
  # Use completion-aware LazyGroup for commands that have subcommands
402
398
  try:
403
399
  from mcli.app.completion_helpers import create_completion_aware_lazy_group
mcli/app/model_cmd.py CHANGED
@@ -18,6 +18,86 @@ from mcli.workflow.model_service.lightweight_model_server import (
18
18
  logger = get_logger(__name__)
19
19
 
20
20
 
21
+ def _start_openai_server(server, host: str, port: int, api_key: Optional[str], model: str):
22
+ """Start FastAPI server with OpenAI compatibility"""
23
+ try:
24
+ import uvicorn
25
+ from fastapi import FastAPI
26
+ from fastapi.middleware.cors import CORSMiddleware
27
+
28
+ from mcli.workflow.model_service.openai_adapter import create_openai_adapter
29
+
30
+ # Create FastAPI app
31
+ app = FastAPI(
32
+ title="MCLI Model Service (OpenAI Compatible)",
33
+ description="OpenAI-compatible API for MCLI lightweight models",
34
+ version="1.0.0",
35
+ )
36
+
37
+ # Add CORS middleware
38
+ app.add_middleware(
39
+ CORSMiddleware,
40
+ allow_origins=["*"],
41
+ allow_credentials=True,
42
+ allow_methods=["*"],
43
+ allow_headers=["*"],
44
+ )
45
+
46
+ # Create OpenAI adapter
47
+ require_auth = api_key is not None
48
+ adapter = create_openai_adapter(server, require_auth=require_auth)
49
+
50
+ # Add API key if provided
51
+ if api_key:
52
+ adapter.api_key_manager.add_key(api_key, name="default")
53
+ click.echo(f"🔐 API key authentication enabled")
54
+
55
+ # Include OpenAI routes
56
+ app.include_router(adapter.router)
57
+
58
+ # Add health check endpoint
59
+ @app.get("/health")
60
+ async def health():
61
+ return {"status": "healthy", "model": model}
62
+
63
+ # Display server info
64
+ click.echo(f"\n📝 Server running at:")
65
+ click.echo(f" - Base URL: http://{host}:{port}")
66
+ click.echo(f" - OpenAI API: http://{host}:{port}/v1")
67
+ click.echo(f" - Models: http://{host}:{port}/v1/models")
68
+ click.echo(f" - Chat: http://{host}:{port}/v1/chat/completions")
69
+ click.echo(f" - Health: http://{host}:{port}/health")
70
+
71
+ if require_auth:
72
+ click.echo(f"\n🔐 Authentication: Required")
73
+ click.echo(f" Use: Authorization: Bearer {api_key}")
74
+ else:
75
+ click.echo(f"\n⚠️ Authentication: Disabled (not recommended for public access)")
76
+
77
+ if host == "0.0.0.0":
78
+ click.echo(f"\n⚠️ Server is publicly accessible on all interfaces!")
79
+
80
+ click.echo(f"\n📚 For aider, use:")
81
+ if require_auth:
82
+ click.echo(f" export OPENAI_API_KEY={api_key}")
83
+ click.echo(f" export OPENAI_API_BASE=http://{host}:{port}/v1")
84
+ click.echo(f" aider --model {model}")
85
+
86
+ click.echo(f"\n Press Ctrl+C to stop the server")
87
+
88
+ # Start server
89
+ uvicorn.run(app, host=host, port=port, log_level="info")
90
+
91
+ except ImportError as e:
92
+ click.echo(f"❌ Missing dependencies for OpenAI-compatible server: {e}")
93
+ click.echo(f" Install with: pip install fastapi uvicorn")
94
+ sys.exit(1)
95
+ except Exception as e:
96
+ click.echo(f"❌ Failed to start OpenAI-compatible server: {e}")
97
+ logger.error(f"Server error: {e}", exc_info=True)
98
+ sys.exit(1)
99
+
100
+
21
101
  @click.group()
22
102
  def model():
23
103
  """Model management commands for offline and online model usage."""
@@ -103,13 +183,34 @@ def download(model_name: str):
103
183
  @click.option(
104
184
  "--port", "-p", default=None, help="Port to run server on (default: from config or 51234)"
105
185
  )
186
+ @click.option(
187
+ "--host", "-h", default="localhost", help="Host to bind to (use 0.0.0.0 for public access)"
188
+ )
106
189
  @click.option(
107
190
  "--auto-download",
108
191
  is_flag=True,
109
192
  default=True,
110
193
  help="Automatically download model if not available",
111
194
  )
112
- def start(model: Optional[str], port: Optional[int], auto_download: bool):
195
+ @click.option(
196
+ "--openai-compatible",
197
+ is_flag=True,
198
+ default=False,
199
+ help="Enable OpenAI-compatible API endpoints",
200
+ )
201
+ @click.option(
202
+ "--api-key",
203
+ default=None,
204
+ help="API key for authentication (if not set, auth is disabled)",
205
+ )
206
+ def start(
207
+ model: Optional[str],
208
+ port: Optional[int],
209
+ host: str,
210
+ auto_download: bool,
211
+ openai_compatible: bool,
212
+ api_key: Optional[str],
213
+ ):
113
214
  """Start the lightweight model server."""
114
215
  # Load port from config if not specified
115
216
  if port is None:
@@ -155,15 +256,24 @@ def start(model: Optional[str], port: Optional[int], auto_download: bool):
155
256
  click.echo(f"❌ Failed to load {model}")
156
257
  sys.exit(1)
157
258
 
158
- # Start server
159
- click.echo(f"🚀 Starting lightweight server on port {port}...")
160
- server.start_server()
259
+ # Start server with OpenAI compatibility if requested
260
+ if openai_compatible:
261
+ click.echo(f"🚀 Starting OpenAI-compatible server on {host}:{port}...")
262
+ _start_openai_server(server, host, port, api_key, model)
263
+ else:
264
+ click.echo(f"🚀 Starting lightweight server on {host}:{port}...")
265
+ server.start_server()
266
+
267
+ click.echo(f"\n📝 Server running at:")
268
+ click.echo(f" - API: http://{host}:{port}")
269
+ click.echo(f" - Health: http://{host}:{port}/health")
270
+ click.echo(f" - Models: http://{host}:{port}/models")
271
+
272
+ if host == "0.0.0.0":
273
+ click.echo(f"\n⚠️ Server is publicly accessible!")
274
+ click.echo(f" Consider using --openai-compatible with --api-key for security")
161
275
 
162
- click.echo(f"\n📝 Server running at:")
163
- click.echo(f" - API: http://localhost:{port}")
164
- click.echo(f" - Health: http://localhost:{port}/health")
165
- click.echo(f" - Models: http://localhost:{port}/models")
166
- click.echo(f"\n Press Ctrl+C to stop the server")
276
+ click.echo(f"\n Press Ctrl+C to stop the server")
167
277
 
168
278
  try:
169
279
  # Keep server running
@@ -5,8 +5,8 @@ This module provides functionality to store user-created commands in a portable
5
5
  format in ~/.mcli/commands/ and automatically load them at startup.
6
6
  """
7
7
 
8
- import json
9
8
  import importlib.util
9
+ import json
10
10
  import sys
11
11
  import tempfile
12
12
  from datetime import datetime
@@ -259,9 +259,7 @@ class CustomCommandManager:
259
259
  module_name = f"mcli_custom_{name}"
260
260
 
261
261
  # Create a temporary file to store the code
262
- with tempfile.NamedTemporaryFile(
263
- mode="w", suffix=".py", delete=False
264
- ) as temp_file:
262
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as temp_file:
265
263
  temp_file.write(code)
266
264
  temp_file_path = temp_file.name
267
265
 
@@ -274,12 +272,23 @@ class CustomCommandManager:
274
272
  spec.loader.exec_module(module)
275
273
 
276
274
  # Look for a command or command group in the module
275
+ # Prioritize Groups over Commands to handle commands with subcommands correctly
277
276
  command_obj = None
277
+ found_commands = []
278
+
278
279
  for attr_name in dir(module):
279
280
  attr = getattr(module, attr_name)
280
- if isinstance(attr, (click.Command, click.Group)):
281
+ if isinstance(attr, click.Group):
282
+ # Found a group - this takes priority
281
283
  command_obj = attr
282
284
  break
285
+ elif isinstance(attr, click.Command):
286
+ # Store command for fallback
287
+ found_commands.append(attr)
288
+
289
+ # If no group found, use the first command
290
+ if not command_obj and found_commands:
291
+ command_obj = found_commands[0]
283
292
 
284
293
  if command_obj:
285
294
  # Register with the target group
@@ -288,9 +297,7 @@ class CustomCommandManager:
288
297
  logger.info(f"Registered custom command: {name}")
289
298
  return True
290
299
  else:
291
- logger.warning(
292
- f"No Click command found in custom command: {name}"
293
- )
300
+ logger.warning(f"No Click command found in custom command: {name}")
294
301
  return False
295
302
  finally:
296
303
  # Clean up temporary file
@@ -320,9 +327,7 @@ class CustomCommandManager:
320
327
  logger.error(f"Failed to export commands: {e}")
321
328
  return False
322
329
 
323
- def import_commands(
324
- self, import_path: Path, overwrite: bool = False
325
- ) -> Dict[str, bool]:
330
+ def import_commands(self, import_path: Path, overwrite: bool = False) -> Dict[str, bool]:
326
331
  """
327
332
  Import commands from a JSON file.
328
333
 
mcli/ml/api/app.py CHANGED
@@ -16,11 +16,7 @@ from mcli.ml.config import settings
16
16
  from mcli.ml.database.session import init_db
17
17
  from mcli.ml.logging import get_logger, setup_logging
18
18
 
19
- from .middleware import (
20
- ErrorHandlingMiddleware,
21
- RateLimitMiddleware,
22
- RequestLoggingMiddleware,
23
- )
19
+ from .middleware import ErrorHandlingMiddleware, RateLimitMiddleware, RequestLoggingMiddleware
24
20
  from .routers import (
25
21
  admin_router,
26
22
  auth_router,
mcli/ml/dashboard/app.py CHANGED
@@ -14,6 +14,8 @@ from plotly.subplots import make_subplots
14
14
 
15
15
  from mcli.ml.cache import cache_manager
16
16
  from mcli.ml.config import settings
17
+ from mcli.ml.dashboard.common import setup_page_config
18
+ from mcli.ml.dashboard.styles import apply_dashboard_styles
17
19
  from mcli.ml.database.models import (
18
20
  BacktestResult,
19
21
  Model,
@@ -25,8 +27,6 @@ from mcli.ml.database.models import (
25
27
  User,
26
28
  )
27
29
  from mcli.ml.database.session import SessionLocal
28
- from mcli.ml.dashboard.common import setup_page_config
29
- from mcli.ml.dashboard.styles import apply_dashboard_styles
30
30
 
31
31
  # Page config - must be first
32
32
  setup_page_config(page_title="MCLI ML Dashboard")