mcli-framework 7.8.5__py3-none-any.whl → 7.9.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.
- mcli/app/main.py +9 -0
- mcli/lib/lib.py +8 -1
- mcli/lib/secrets/__init__.py +10 -0
- mcli/lib/secrets/commands.py +185 -0
- mcli/lib/secrets/manager.py +213 -0
- mcli/lib/secrets/repl.py +297 -0
- mcli/lib/secrets/store.py +246 -0
- mcli/ml/backtesting/backtest_engine.py +1 -3
- mcli/ml/backtesting/run.py +54 -0
- mcli/ml/models/ensemble_models.py +1 -1
- mcli/ml/models/recommendation_models.py +2 -2
- mcli/ml/optimization/optimize.py +51 -0
- mcli/ml/serving/__init__.py +1 -0
- mcli/ml/serving/serve.py +50 -0
- mcli/ml/training/train.py +73 -0
- mcli/self/self_cmd.py +8 -0
- mcli/self/zsh_cmd.py +259 -0
- {mcli_framework-7.8.5.dist-info → mcli_framework-7.9.1.dist-info}/METADATA +2 -1
- {mcli_framework-7.8.5.dist-info → mcli_framework-7.9.1.dist-info}/RECORD +23 -12
- {mcli_framework-7.8.5.dist-info → mcli_framework-7.9.1.dist-info}/WHEEL +0 -0
- {mcli_framework-7.8.5.dist-info → mcli_framework-7.9.1.dist-info}/entry_points.txt +0 -0
- {mcli_framework-7.8.5.dist-info → mcli_framework-7.9.1.dist-info}/licenses/LICENSE +0 -0
- {mcli_framework-7.8.5.dist-info → mcli_framework-7.9.1.dist-info}/top_level.txt +0 -0
mcli/lib/secrets/repl.py
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""
|
|
2
|
+
REPL (Read-Eval-Print Loop) for LSH secrets management.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
from prompt_toolkit import prompt
|
|
11
|
+
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
|
12
|
+
from prompt_toolkit.completion import WordCompleter
|
|
13
|
+
from prompt_toolkit.history import FileHistory
|
|
14
|
+
|
|
15
|
+
from mcli.lib.logger.logger import get_logger
|
|
16
|
+
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
|
+
from mcli.lib.ui.styling import console, error, info, success, warning
|
|
19
|
+
|
|
20
|
+
from .manager import SecretsManager
|
|
21
|
+
from .store import SecretsStore
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SecretsREPL:
|
|
25
|
+
"""Interactive REPL for secrets management."""
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
"""Initialize the REPL."""
|
|
29
|
+
self.manager = SecretsManager()
|
|
30
|
+
self.store = SecretsStore()
|
|
31
|
+
self.running = False
|
|
32
|
+
self.namespace = "default"
|
|
33
|
+
self.history_file = Path.home() / ".mcli" / "secrets_repl_history"
|
|
34
|
+
|
|
35
|
+
# Commands
|
|
36
|
+
self.commands = {
|
|
37
|
+
"set": self.cmd_set,
|
|
38
|
+
"get": self.cmd_get,
|
|
39
|
+
"list": self.cmd_list,
|
|
40
|
+
"delete": self.cmd_delete,
|
|
41
|
+
"namespace": self.cmd_namespace,
|
|
42
|
+
"export": self.cmd_export,
|
|
43
|
+
"import": self.cmd_import,
|
|
44
|
+
"push": self.cmd_push,
|
|
45
|
+
"pull": self.cmd_pull,
|
|
46
|
+
"sync": self.cmd_sync,
|
|
47
|
+
"status": self.cmd_status,
|
|
48
|
+
"help": self.cmd_help,
|
|
49
|
+
"exit": self.cmd_exit,
|
|
50
|
+
"quit": self.cmd_exit,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Command completer
|
|
54
|
+
self.completer = WordCompleter(list(self.commands.keys()) + ["ns"], ignore_case=True)
|
|
55
|
+
|
|
56
|
+
def run(self):
|
|
57
|
+
"""Run the REPL."""
|
|
58
|
+
self.running = True
|
|
59
|
+
|
|
60
|
+
# Print welcome message
|
|
61
|
+
console.print("[bold cyan]MCLI Secrets Management Shell[/bold cyan]")
|
|
62
|
+
console.print("Type 'help' for available commands or 'exit' to quit.\n")
|
|
63
|
+
|
|
64
|
+
# Create history file directory if needed
|
|
65
|
+
self.history_file.parent.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
|
|
67
|
+
while self.running:
|
|
68
|
+
try:
|
|
69
|
+
# Build prompt
|
|
70
|
+
prompt_text = f"[{self.namespace}]> "
|
|
71
|
+
|
|
72
|
+
# Get user input
|
|
73
|
+
user_input = prompt(
|
|
74
|
+
prompt_text,
|
|
75
|
+
completer=self.completer,
|
|
76
|
+
history=FileHistory(str(self.history_file)),
|
|
77
|
+
auto_suggest=AutoSuggestFromHistory(),
|
|
78
|
+
).strip()
|
|
79
|
+
|
|
80
|
+
if not user_input:
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
# Parse command and arguments
|
|
84
|
+
parts = user_input.split()
|
|
85
|
+
command = parts[0].lower()
|
|
86
|
+
args = parts[1:] if len(parts) > 1 else []
|
|
87
|
+
|
|
88
|
+
# Handle command aliases
|
|
89
|
+
if command == "ns":
|
|
90
|
+
command = "namespace"
|
|
91
|
+
|
|
92
|
+
# Execute command
|
|
93
|
+
if command in self.commands:
|
|
94
|
+
self.commands[command](args)
|
|
95
|
+
else:
|
|
96
|
+
error(f"Unknown command: {command}")
|
|
97
|
+
console.print("Type 'help' for available commands.")
|
|
98
|
+
|
|
99
|
+
except KeyboardInterrupt:
|
|
100
|
+
console.print("\nUse 'exit' or 'quit' to leave the shell.")
|
|
101
|
+
except EOFError:
|
|
102
|
+
self.cmd_exit([])
|
|
103
|
+
except Exception as e:
|
|
104
|
+
error(f"Error: {e}")
|
|
105
|
+
logger.exception("REPL error")
|
|
106
|
+
|
|
107
|
+
def cmd_set(self, args: List[str]):
|
|
108
|
+
"""Set a secret value."""
|
|
109
|
+
if len(args) < 2:
|
|
110
|
+
error("Usage: set <key> <value>")
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
key = args[0]
|
|
114
|
+
value = " ".join(args[1:])
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
self.manager.set(key, value, self.namespace)
|
|
118
|
+
success(f"Secret '{key}' set in namespace '{self.namespace}'")
|
|
119
|
+
except Exception as e:
|
|
120
|
+
error(f"Failed to set secret: {e}")
|
|
121
|
+
|
|
122
|
+
def cmd_get(self, args: List[str]):
|
|
123
|
+
"""Get a secret value."""
|
|
124
|
+
if len(args) != 1:
|
|
125
|
+
error("Usage: get <key>")
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
key = args[0]
|
|
129
|
+
value = self.manager.get(key, self.namespace)
|
|
130
|
+
|
|
131
|
+
if value is not None:
|
|
132
|
+
# Mask the value for security
|
|
133
|
+
masked_value = (
|
|
134
|
+
value[:3] + "*" * (len(value) - 6) + value[-3:]
|
|
135
|
+
if len(value) > 6
|
|
136
|
+
else "*" * len(value)
|
|
137
|
+
)
|
|
138
|
+
info(f"{key} = {masked_value}")
|
|
139
|
+
|
|
140
|
+
if click.confirm("Show full value?", default=False):
|
|
141
|
+
console.print(f"[yellow]{value}[/yellow]")
|
|
142
|
+
else:
|
|
143
|
+
warning(f"Secret '{key}' not found in namespace '{self.namespace}'")
|
|
144
|
+
|
|
145
|
+
def cmd_list(self, args: List[str]):
|
|
146
|
+
"""List all secrets."""
|
|
147
|
+
secrets = self.manager.list(self.namespace if args != ["all"] else None)
|
|
148
|
+
|
|
149
|
+
if secrets:
|
|
150
|
+
console.print("[bold]Secrets:[/bold]")
|
|
151
|
+
for secret in secrets:
|
|
152
|
+
console.print(f" • {secret}")
|
|
153
|
+
else:
|
|
154
|
+
info("No secrets found")
|
|
155
|
+
|
|
156
|
+
def cmd_delete(self, args: List[str]):
|
|
157
|
+
"""Delete a secret."""
|
|
158
|
+
if len(args) != 1:
|
|
159
|
+
error("Usage: delete <key>")
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
key = args[0]
|
|
163
|
+
|
|
164
|
+
if click.confirm(f"Delete secret '{key}' from namespace '{self.namespace}'?"):
|
|
165
|
+
if self.manager.delete(key, self.namespace):
|
|
166
|
+
success(f"Secret '{key}' deleted")
|
|
167
|
+
else:
|
|
168
|
+
warning(f"Secret '{key}' not found")
|
|
169
|
+
|
|
170
|
+
def cmd_namespace(self, args: List[str]):
|
|
171
|
+
"""Switch namespace."""
|
|
172
|
+
if len(args) == 0:
|
|
173
|
+
# List namespaces
|
|
174
|
+
namespaces = set()
|
|
175
|
+
for d in self.manager.secrets_dir.iterdir():
|
|
176
|
+
if d.is_dir() and not d.name.startswith("."):
|
|
177
|
+
namespaces.add(d.name)
|
|
178
|
+
|
|
179
|
+
console.print(f"[bold]Current namespace:[/bold] {self.namespace}")
|
|
180
|
+
if namespaces:
|
|
181
|
+
console.print("[bold]Available namespaces:[/bold]")
|
|
182
|
+
for ns in sorted(namespaces):
|
|
183
|
+
marker = "→" if ns == self.namespace else " "
|
|
184
|
+
console.print(f" {marker} {ns}")
|
|
185
|
+
elif len(args) == 1:
|
|
186
|
+
self.namespace = args[0]
|
|
187
|
+
success(f"Switched to namespace '{self.namespace}'")
|
|
188
|
+
else:
|
|
189
|
+
error("Usage: namespace [<name>]")
|
|
190
|
+
|
|
191
|
+
def cmd_export(self, args: List[str]):
|
|
192
|
+
"""Export secrets as environment variables."""
|
|
193
|
+
env_vars = self.manager.export_env(self.namespace)
|
|
194
|
+
|
|
195
|
+
if env_vars:
|
|
196
|
+
if args and args[0] == "file":
|
|
197
|
+
# Export to file
|
|
198
|
+
filename = args[1] if len(args) > 1 else f"{self.namespace}.env"
|
|
199
|
+
with open(filename, "w") as f:
|
|
200
|
+
for key, value in env_vars.items():
|
|
201
|
+
f.write(f"{key}={value}\n")
|
|
202
|
+
success(f"Exported {len(env_vars)} secrets to {filename}")
|
|
203
|
+
else:
|
|
204
|
+
# Display export commands
|
|
205
|
+
console.print("[bold]Export commands:[/bold]")
|
|
206
|
+
for key, value in env_vars.items():
|
|
207
|
+
masked_value = value[:3] + "***" + value[-3:] if len(value) > 6 else "***"
|
|
208
|
+
console.print(f"export {key}={masked_value}")
|
|
209
|
+
else:
|
|
210
|
+
info("No secrets to export")
|
|
211
|
+
|
|
212
|
+
def cmd_import(self, args: List[str]):
|
|
213
|
+
"""Import secrets from environment file."""
|
|
214
|
+
if len(args) != 1:
|
|
215
|
+
error("Usage: import <env-file>")
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
env_file = Path(args[0])
|
|
219
|
+
if not env_file.exists():
|
|
220
|
+
error(f"File not found: {env_file}")
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
count = self.manager.import_env(env_file, self.namespace)
|
|
224
|
+
success(f"Imported {count} secrets from {env_file}")
|
|
225
|
+
|
|
226
|
+
def cmd_push(self, args: List[str]):
|
|
227
|
+
"""Push secrets to git store."""
|
|
228
|
+
message = " ".join(args) if args else None
|
|
229
|
+
self.store.push(self.manager.secrets_dir, message)
|
|
230
|
+
|
|
231
|
+
def cmd_pull(self, args: List[str]):
|
|
232
|
+
"""Pull secrets from git store."""
|
|
233
|
+
self.store.pull(self.manager.secrets_dir)
|
|
234
|
+
|
|
235
|
+
def cmd_sync(self, args: List[str]):
|
|
236
|
+
"""Sync secrets with git store."""
|
|
237
|
+
message = " ".join(args) if args else None
|
|
238
|
+
self.store.sync(self.manager.secrets_dir, message)
|
|
239
|
+
|
|
240
|
+
def cmd_status(self, args: List[str]):
|
|
241
|
+
"""Show store status."""
|
|
242
|
+
status = self.store.status()
|
|
243
|
+
|
|
244
|
+
console.print("[bold]Secrets Store Status:[/bold]")
|
|
245
|
+
console.print(f" Initialized: {status['initialized']}")
|
|
246
|
+
console.print(f" Path: {status['store_path']}")
|
|
247
|
+
|
|
248
|
+
if status["initialized"]:
|
|
249
|
+
console.print(f" Branch: {status['branch']}")
|
|
250
|
+
console.print(f" Commit: {status['commit']}")
|
|
251
|
+
console.print(f" Clean: {status['clean']}")
|
|
252
|
+
|
|
253
|
+
if status["has_remote"]:
|
|
254
|
+
console.print(f" Remote: {status['remote_url']}")
|
|
255
|
+
else:
|
|
256
|
+
console.print(" Remote: [dim]Not configured[/dim]")
|
|
257
|
+
|
|
258
|
+
def cmd_help(self, args: List[str]):
|
|
259
|
+
"""Show help information."""
|
|
260
|
+
console.print("[bold]Available Commands:[/bold]\n")
|
|
261
|
+
|
|
262
|
+
help_text = {
|
|
263
|
+
"set": "Set a secret value",
|
|
264
|
+
"get": "Get a secret value",
|
|
265
|
+
"list": "List all secrets (use 'list all' for all namespaces)",
|
|
266
|
+
"delete": "Delete a secret",
|
|
267
|
+
"namespace": "Switch namespace or list namespaces (alias: ns)",
|
|
268
|
+
"export": "Export secrets as environment variables",
|
|
269
|
+
"import": "Import secrets from .env file",
|
|
270
|
+
"push": "Push secrets to git store",
|
|
271
|
+
"pull": "Pull secrets from git store",
|
|
272
|
+
"sync": "Sync secrets with git store",
|
|
273
|
+
"status": "Show store status",
|
|
274
|
+
"help": "Show this help",
|
|
275
|
+
"exit": "Exit the shell (alias: quit)",
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
for cmd, desc in help_text.items():
|
|
279
|
+
console.print(f" [cyan]{cmd:12}[/cyan] {desc}")
|
|
280
|
+
|
|
281
|
+
console.print("\n[bold]Examples:[/bold]")
|
|
282
|
+
console.print(" set api-key sk-1234567890")
|
|
283
|
+
console.print(" get api-key")
|
|
284
|
+
console.print(" namespace production")
|
|
285
|
+
console.print(" export file production.env")
|
|
286
|
+
console.print(" import .env.local")
|
|
287
|
+
|
|
288
|
+
def cmd_exit(self, args: List[str]):
|
|
289
|
+
"""Exit the REPL."""
|
|
290
|
+
self.running = False
|
|
291
|
+
console.print("\nGoodbye!")
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def run_repl():
|
|
295
|
+
"""Entry point for the REPL."""
|
|
296
|
+
repl = SecretsREPL()
|
|
297
|
+
repl.run()
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git-based secrets store for synchronization across machines.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
from git import GitCommandError, Repo
|
|
12
|
+
|
|
13
|
+
from mcli.lib.logger.logger import get_logger
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
from mcli.lib.ui.styling import error, info, success, warning
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SecretsStore:
|
|
20
|
+
"""Manages git-based secrets synchronization."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, store_path: Optional[Path] = None):
|
|
23
|
+
"""Initialize the secrets store.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
store_path: Path to git repository for secrets. Defaults to ~/repos/mcli-secrets
|
|
27
|
+
"""
|
|
28
|
+
self.store_path = store_path or Path.home() / "repos" / "mcli-secrets"
|
|
29
|
+
self.config_file = Path.home() / ".mcli" / "secrets-store.conf"
|
|
30
|
+
self.load_config()
|
|
31
|
+
|
|
32
|
+
def load_config(self) -> None:
|
|
33
|
+
"""Load store configuration."""
|
|
34
|
+
self.store_config = {}
|
|
35
|
+
if self.config_file.exists():
|
|
36
|
+
with open(self.config_file) as f:
|
|
37
|
+
for line in f:
|
|
38
|
+
line = line.strip()
|
|
39
|
+
if "=" in line:
|
|
40
|
+
key, value = line.split("=", 1)
|
|
41
|
+
self.store_config[key.strip()] = value.strip()
|
|
42
|
+
|
|
43
|
+
# Override store path if configured
|
|
44
|
+
if "store_path" in self.store_config:
|
|
45
|
+
self.store_path = Path(self.store_config["store_path"])
|
|
46
|
+
|
|
47
|
+
def save_config(self) -> None:
|
|
48
|
+
"""Save store configuration."""
|
|
49
|
+
self.config_file.parent.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
with open(self.config_file, "w") as f:
|
|
51
|
+
for key, value in self.store_config.items():
|
|
52
|
+
f.write(f"{key}={value}\n")
|
|
53
|
+
|
|
54
|
+
def init(self, remote_url: Optional[str] = None) -> None:
|
|
55
|
+
"""Initialize the secrets store repository.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
remote_url: Optional git remote URL
|
|
59
|
+
"""
|
|
60
|
+
if self.store_path.exists() and (self.store_path / ".git").exists():
|
|
61
|
+
error("Store already initialized")
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
self.store_path.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
repo = Repo.init(self.store_path)
|
|
68
|
+
|
|
69
|
+
# Create README
|
|
70
|
+
readme_path = self.store_path / "README.md"
|
|
71
|
+
readme_path.write_text(
|
|
72
|
+
"# MCLI Secrets Store\n\n"
|
|
73
|
+
"This repository stores encrypted secrets for MCLI.\n\n"
|
|
74
|
+
"**WARNING**: This repository contains encrypted sensitive data.\n"
|
|
75
|
+
"Ensure it is kept private and access is restricted.\n"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Create .gitignore
|
|
79
|
+
gitignore_path = self.store_path / ".gitignore"
|
|
80
|
+
gitignore_path.write_text("*.key\n*.tmp\n.DS_Store\n")
|
|
81
|
+
|
|
82
|
+
repo.index.add([readme_path.name, gitignore_path.name])
|
|
83
|
+
repo.index.commit("Initial commit")
|
|
84
|
+
|
|
85
|
+
if remote_url:
|
|
86
|
+
repo.create_remote("origin", remote_url)
|
|
87
|
+
self.store_config["remote_url"] = remote_url
|
|
88
|
+
self.save_config()
|
|
89
|
+
info(f"Remote added: {remote_url}")
|
|
90
|
+
|
|
91
|
+
success(f"Secrets store initialized at {self.store_path}")
|
|
92
|
+
|
|
93
|
+
except GitCommandError as e:
|
|
94
|
+
error(f"Failed to initialize store: {e}")
|
|
95
|
+
|
|
96
|
+
def push(self, secrets_dir: Path, message: Optional[str] = None) -> None:
|
|
97
|
+
"""Push secrets to the store.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
secrets_dir: Directory containing encrypted secrets
|
|
101
|
+
message: Commit message
|
|
102
|
+
"""
|
|
103
|
+
if not self._check_initialized():
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
repo = Repo(self.store_path)
|
|
108
|
+
|
|
109
|
+
# Copy secrets to store
|
|
110
|
+
store_secrets_dir = self.store_path / "secrets"
|
|
111
|
+
|
|
112
|
+
# Remove existing secrets
|
|
113
|
+
if store_secrets_dir.exists():
|
|
114
|
+
shutil.rmtree(store_secrets_dir)
|
|
115
|
+
|
|
116
|
+
# Copy new secrets
|
|
117
|
+
shutil.copytree(secrets_dir, store_secrets_dir)
|
|
118
|
+
|
|
119
|
+
# Add to git
|
|
120
|
+
repo.index.add(["secrets"])
|
|
121
|
+
|
|
122
|
+
# Check if there are changes
|
|
123
|
+
if repo.is_dirty():
|
|
124
|
+
message = message or f"Update secrets from {os.uname().nodename}"
|
|
125
|
+
repo.index.commit(message)
|
|
126
|
+
|
|
127
|
+
# Push if remote exists
|
|
128
|
+
if "origin" in repo.remotes:
|
|
129
|
+
info("Pushing to remote...")
|
|
130
|
+
repo.remotes.origin.push()
|
|
131
|
+
success("Secrets pushed to remote")
|
|
132
|
+
else:
|
|
133
|
+
success("Secrets committed locally")
|
|
134
|
+
else:
|
|
135
|
+
info("No changes to push")
|
|
136
|
+
|
|
137
|
+
except GitCommandError as e:
|
|
138
|
+
error(f"Failed to push secrets: {e}")
|
|
139
|
+
|
|
140
|
+
def pull(self, secrets_dir: Path) -> None:
|
|
141
|
+
"""Pull secrets from the store.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
secrets_dir: Directory to store pulled secrets
|
|
145
|
+
"""
|
|
146
|
+
if not self._check_initialized():
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
repo = Repo(self.store_path)
|
|
151
|
+
|
|
152
|
+
# Pull from remote if exists
|
|
153
|
+
if "origin" in repo.remotes:
|
|
154
|
+
info("Pulling from remote...")
|
|
155
|
+
repo.remotes.origin.pull()
|
|
156
|
+
|
|
157
|
+
# Copy secrets from store
|
|
158
|
+
store_secrets_dir = self.store_path / "secrets"
|
|
159
|
+
|
|
160
|
+
if not store_secrets_dir.exists():
|
|
161
|
+
warning("No secrets found in store")
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
# Backup existing secrets
|
|
165
|
+
if secrets_dir.exists():
|
|
166
|
+
backup_dir = secrets_dir.parent / f"{secrets_dir.name}.backup"
|
|
167
|
+
if backup_dir.exists():
|
|
168
|
+
shutil.rmtree(backup_dir)
|
|
169
|
+
shutil.move(str(secrets_dir), str(backup_dir))
|
|
170
|
+
info(f"Existing secrets backed up to {backup_dir}")
|
|
171
|
+
|
|
172
|
+
# Copy secrets from store
|
|
173
|
+
shutil.copytree(store_secrets_dir, secrets_dir)
|
|
174
|
+
|
|
175
|
+
success(f"Secrets pulled to {secrets_dir}")
|
|
176
|
+
|
|
177
|
+
except GitCommandError as e:
|
|
178
|
+
error(f"Failed to pull secrets: {e}")
|
|
179
|
+
|
|
180
|
+
def sync(self, secrets_dir: Path, message: Optional[str] = None) -> None:
|
|
181
|
+
"""Synchronize secrets (pull then push).
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
secrets_dir: Directory containing secrets
|
|
185
|
+
message: Commit message
|
|
186
|
+
"""
|
|
187
|
+
if not self._check_initialized():
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
info("Synchronizing secrets...")
|
|
191
|
+
|
|
192
|
+
# First pull
|
|
193
|
+
self.pull(secrets_dir)
|
|
194
|
+
|
|
195
|
+
# Then push
|
|
196
|
+
self.push(secrets_dir, message)
|
|
197
|
+
|
|
198
|
+
def status(self) -> Dict[str, Any]:
|
|
199
|
+
"""Get status of the secrets store.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Status information
|
|
203
|
+
"""
|
|
204
|
+
status = {
|
|
205
|
+
"initialized": False,
|
|
206
|
+
"store_path": str(self.store_path),
|
|
207
|
+
"has_remote": False,
|
|
208
|
+
"remote_url": None,
|
|
209
|
+
"clean": True,
|
|
210
|
+
"branch": None,
|
|
211
|
+
"commit": None,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if not self._check_initialized(silent=True):
|
|
215
|
+
return status
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
repo = Repo(self.store_path)
|
|
219
|
+
status["initialized"] = True
|
|
220
|
+
status["clean"] = not repo.is_dirty()
|
|
221
|
+
status["branch"] = repo.active_branch.name
|
|
222
|
+
status["commit"] = str(repo.head.commit)[:8]
|
|
223
|
+
|
|
224
|
+
if "origin" in repo.remotes:
|
|
225
|
+
status["has_remote"] = True
|
|
226
|
+
status["remote_url"] = repo.remotes.origin.url
|
|
227
|
+
|
|
228
|
+
except Exception:
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
return status
|
|
232
|
+
|
|
233
|
+
def _check_initialized(self, silent: bool = False) -> bool:
|
|
234
|
+
"""Check if store is initialized.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
silent: Don't print error message
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
True if initialized
|
|
241
|
+
"""
|
|
242
|
+
if not self.store_path.exists() or not (self.store_path / ".git").exists():
|
|
243
|
+
if not silent:
|
|
244
|
+
error("Store not initialized. Run 'mcli secrets store init' first.")
|
|
245
|
+
return False
|
|
246
|
+
return True
|
|
@@ -13,9 +13,7 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
|
13
13
|
import numpy as np
|
|
14
14
|
import pandas as pd
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
from ml.models.recommendation_models import PortfolioRecommendation, StockRecommendationModel
|
|
16
|
+
from mcli.ml.models.recommendation_models import PortfolioRecommendation, StockRecommendationModel
|
|
19
17
|
|
|
20
18
|
logger = logging.getLogger(__name__)
|
|
21
19
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Entry point for backtesting CLI."""
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from mcli.lib.ui.styling import error, info, success
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group(name="mcli-backtest", help="Backtesting CLI for MCLI trading strategies")
|
|
10
|
+
def cli():
|
|
11
|
+
"""Main CLI group for backtesting."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@cli.command(name="run", help="Run a backtest on historical data")
|
|
16
|
+
@click.option("--strategy", required=True, help="Strategy to backtest")
|
|
17
|
+
@click.option("--start-date", required=True, help="Start date (YYYY-MM-DD)")
|
|
18
|
+
@click.option("--end-date", required=True, help="End date (YYYY-MM-DD)")
|
|
19
|
+
@click.option("--initial-capital", default=100000, help="Initial capital")
|
|
20
|
+
@click.option("--output", help="Output file for results")
|
|
21
|
+
def run_backtest(strategy: str, start_date: str, end_date: str, initial_capital: float, output: str):
|
|
22
|
+
"""Run a backtest with the specified parameters."""
|
|
23
|
+
info(f"Running backtest for strategy: {strategy}")
|
|
24
|
+
info(f"Period: {start_date} to {end_date}")
|
|
25
|
+
info(f"Initial capital: ${initial_capital:,.2f}")
|
|
26
|
+
|
|
27
|
+
# TODO: Implement actual backtesting logic
|
|
28
|
+
error("Backtesting functionality not yet implemented")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@cli.command(name="list", help="List available strategies")
|
|
32
|
+
def list_strategies():
|
|
33
|
+
"""List all available trading strategies."""
|
|
34
|
+
info("Available strategies:")
|
|
35
|
+
# TODO: Implement strategy listing
|
|
36
|
+
error("Strategy listing not yet implemented")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@cli.command(name="analyze", help="Analyze backtest results")
|
|
40
|
+
@click.argument("results_file")
|
|
41
|
+
def analyze_results(results_file: str):
|
|
42
|
+
"""Analyze backtest results from a file."""
|
|
43
|
+
info(f"Analyzing results from: {results_file}")
|
|
44
|
+
# TODO: Implement results analysis
|
|
45
|
+
error("Results analysis not yet implemented")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def main():
|
|
49
|
+
"""Main entry point."""
|
|
50
|
+
cli()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
main()
|
|
@@ -9,7 +9,7 @@ import pandas as pd
|
|
|
9
9
|
import torch
|
|
10
10
|
import torch.nn as nn
|
|
11
11
|
import torch.nn.functional as F
|
|
12
|
-
from base_models import BaseStockModel, ModelMetrics, ValidationResult
|
|
12
|
+
from mcli.ml.models.base_models import BaseStockModel, ModelMetrics, ValidationResult
|
|
13
13
|
|
|
14
14
|
logger = logging.getLogger(__name__)
|
|
15
15
|
|
|
@@ -10,8 +10,8 @@ import pandas as pd
|
|
|
10
10
|
import torch
|
|
11
11
|
import torch.nn as nn
|
|
12
12
|
import torch.nn.functional as F
|
|
13
|
-
from base_models import BaseStockModel, ModelMetrics, ValidationResult
|
|
14
|
-
from ensemble_models import DeepEnsembleModel, EnsembleConfig, ModelConfig
|
|
13
|
+
from mcli.ml.models.base_models import BaseStockModel, ModelMetrics, ValidationResult
|
|
14
|
+
from mcli.ml.models.ensemble_models import DeepEnsembleModel, EnsembleConfig, ModelConfig
|
|
15
15
|
|
|
16
16
|
logger = logging.getLogger(__name__)
|
|
17
17
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Entry point for portfolio optimization CLI."""
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from mcli.lib.ui.styling import error, info, success
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group(name="mcli-optimize", help="Portfolio optimization CLI for MCLI trading system")
|
|
10
|
+
def cli():
|
|
11
|
+
"""Main CLI group for portfolio optimization."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@cli.command(name="portfolio", help="Optimize portfolio allocation")
|
|
16
|
+
@click.option("--symbols", required=True, help="Comma-separated list of symbols")
|
|
17
|
+
@click.option("--start-date", required=True, help="Start date (YYYY-MM-DD)")
|
|
18
|
+
@click.option("--end-date", required=True, help="End date (YYYY-MM-DD)")
|
|
19
|
+
@click.option("--risk-free-rate", default=0.02, help="Risk-free rate")
|
|
20
|
+
@click.option("--output", help="Output file for results")
|
|
21
|
+
def optimize_portfolio(symbols: str, start_date: str, end_date: str, risk_free_rate: float, output: str):
|
|
22
|
+
"""Optimize portfolio allocation for given symbols."""
|
|
23
|
+
symbol_list = [s.strip() for s in symbols.split(",")]
|
|
24
|
+
info(f"Optimizing portfolio for: {', '.join(symbol_list)}")
|
|
25
|
+
info(f"Period: {start_date} to {end_date}")
|
|
26
|
+
info(f"Risk-free rate: {risk_free_rate:.2%}")
|
|
27
|
+
|
|
28
|
+
# TODO: Implement actual optimization
|
|
29
|
+
error("Portfolio optimization not yet implemented")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@cli.command(name="efficient-frontier", help="Generate efficient frontier")
|
|
33
|
+
@click.option("--symbols", required=True, help="Comma-separated list of symbols")
|
|
34
|
+
@click.option("--points", default=100, help="Number of points on frontier")
|
|
35
|
+
def efficient_frontier(symbols: str, points: int):
|
|
36
|
+
"""Generate efficient frontier for given symbols."""
|
|
37
|
+
symbol_list = [s.strip() for s in symbols.split(",")]
|
|
38
|
+
info(f"Generating efficient frontier for: {', '.join(symbol_list)}")
|
|
39
|
+
info(f"Points: {points}")
|
|
40
|
+
|
|
41
|
+
# TODO: Implement efficient frontier generation
|
|
42
|
+
error("Efficient frontier generation not yet implemented")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def main():
|
|
46
|
+
"""Main entry point."""
|
|
47
|
+
cli()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Model serving module for MCLI ML system."""
|