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.
- mcli/app/chat_cmd.py +42 -0
- mcli/app/commands_cmd.py +226 -0
- mcli/app/completion_cmd.py +216 -0
- mcli/app/completion_helpers.py +288 -0
- mcli/app/cron_test_cmd.py +697 -0
- mcli/app/logs_cmd.py +419 -0
- mcli/app/main.py +492 -0
- mcli/app/model/model.py +1060 -0
- mcli/app/model_cmd.py +227 -0
- mcli/app/redis_cmd.py +269 -0
- mcli/app/video/video.py +1114 -0
- mcli/app/visual_cmd.py +303 -0
- mcli/chat/chat.py +2409 -0
- mcli/chat/command_rag.py +514 -0
- mcli/chat/enhanced_chat.py +652 -0
- mcli/chat/system_controller.py +1010 -0
- mcli/chat/system_integration.py +1016 -0
- mcli/cli.py +25 -0
- mcli/config.toml +20 -0
- mcli/lib/api/api.py +586 -0
- mcli/lib/api/daemon_client.py +203 -0
- mcli/lib/api/daemon_client_local.py +44 -0
- mcli/lib/api/daemon_decorator.py +217 -0
- mcli/lib/api/mcli_decorators.py +1032 -0
- mcli/lib/auth/auth.py +85 -0
- mcli/lib/auth/aws_manager.py +85 -0
- mcli/lib/auth/azure_manager.py +91 -0
- mcli/lib/auth/credential_manager.py +192 -0
- mcli/lib/auth/gcp_manager.py +93 -0
- mcli/lib/auth/key_manager.py +117 -0
- mcli/lib/auth/mcli_manager.py +93 -0
- mcli/lib/auth/token_manager.py +75 -0
- mcli/lib/auth/token_util.py +1011 -0
- mcli/lib/config/config.py +47 -0
- mcli/lib/discovery/__init__.py +1 -0
- mcli/lib/discovery/command_discovery.py +274 -0
- mcli/lib/erd/erd.py +1345 -0
- mcli/lib/erd/generate_graph.py +453 -0
- mcli/lib/files/files.py +76 -0
- mcli/lib/fs/fs.py +109 -0
- mcli/lib/lib.py +29 -0
- mcli/lib/logger/logger.py +611 -0
- mcli/lib/performance/optimizer.py +409 -0
- mcli/lib/performance/rust_bridge.py +502 -0
- mcli/lib/performance/uvloop_config.py +154 -0
- mcli/lib/pickles/pickles.py +50 -0
- mcli/lib/search/cached_vectorizer.py +479 -0
- mcli/lib/services/data_pipeline.py +460 -0
- mcli/lib/services/lsh_client.py +441 -0
- mcli/lib/services/redis_service.py +387 -0
- mcli/lib/shell/shell.py +137 -0
- mcli/lib/toml/toml.py +33 -0
- mcli/lib/ui/styling.py +47 -0
- mcli/lib/ui/visual_effects.py +634 -0
- mcli/lib/watcher/watcher.py +185 -0
- mcli/ml/api/app.py +215 -0
- mcli/ml/api/middleware.py +224 -0
- mcli/ml/api/routers/admin_router.py +12 -0
- mcli/ml/api/routers/auth_router.py +244 -0
- mcli/ml/api/routers/backtest_router.py +12 -0
- mcli/ml/api/routers/data_router.py +12 -0
- mcli/ml/api/routers/model_router.py +302 -0
- mcli/ml/api/routers/monitoring_router.py +12 -0
- mcli/ml/api/routers/portfolio_router.py +12 -0
- mcli/ml/api/routers/prediction_router.py +267 -0
- mcli/ml/api/routers/trade_router.py +12 -0
- mcli/ml/api/routers/websocket_router.py +76 -0
- mcli/ml/api/schemas.py +64 -0
- mcli/ml/auth/auth_manager.py +425 -0
- mcli/ml/auth/models.py +154 -0
- mcli/ml/auth/permissions.py +302 -0
- mcli/ml/backtesting/backtest_engine.py +502 -0
- mcli/ml/backtesting/performance_metrics.py +393 -0
- mcli/ml/cache.py +400 -0
- mcli/ml/cli/main.py +398 -0
- mcli/ml/config/settings.py +394 -0
- mcli/ml/configs/dvc_config.py +230 -0
- mcli/ml/configs/mlflow_config.py +131 -0
- mcli/ml/configs/mlops_manager.py +293 -0
- mcli/ml/dashboard/app.py +532 -0
- mcli/ml/dashboard/app_integrated.py +738 -0
- mcli/ml/dashboard/app_supabase.py +560 -0
- mcli/ml/dashboard/app_training.py +615 -0
- mcli/ml/dashboard/cli.py +51 -0
- mcli/ml/data_ingestion/api_connectors.py +501 -0
- mcli/ml/data_ingestion/data_pipeline.py +567 -0
- mcli/ml/data_ingestion/stream_processor.py +512 -0
- mcli/ml/database/migrations/env.py +94 -0
- mcli/ml/database/models.py +667 -0
- mcli/ml/database/session.py +200 -0
- mcli/ml/experimentation/ab_testing.py +845 -0
- mcli/ml/features/ensemble_features.py +607 -0
- mcli/ml/features/political_features.py +676 -0
- mcli/ml/features/recommendation_engine.py +809 -0
- mcli/ml/features/stock_features.py +573 -0
- mcli/ml/features/test_feature_engineering.py +346 -0
- mcli/ml/logging.py +85 -0
- mcli/ml/mlops/data_versioning.py +518 -0
- mcli/ml/mlops/experiment_tracker.py +377 -0
- mcli/ml/mlops/model_serving.py +481 -0
- mcli/ml/mlops/pipeline_orchestrator.py +614 -0
- mcli/ml/models/base_models.py +324 -0
- mcli/ml/models/ensemble_models.py +675 -0
- mcli/ml/models/recommendation_models.py +474 -0
- mcli/ml/models/test_models.py +487 -0
- mcli/ml/monitoring/drift_detection.py +676 -0
- mcli/ml/monitoring/metrics.py +45 -0
- mcli/ml/optimization/portfolio_optimizer.py +834 -0
- mcli/ml/preprocessing/data_cleaners.py +451 -0
- mcli/ml/preprocessing/feature_extractors.py +491 -0
- mcli/ml/preprocessing/ml_pipeline.py +382 -0
- mcli/ml/preprocessing/politician_trading_preprocessor.py +569 -0
- mcli/ml/preprocessing/test_preprocessing.py +294 -0
- mcli/ml/scripts/populate_sample_data.py +200 -0
- mcli/ml/tasks.py +400 -0
- mcli/ml/tests/test_integration.py +429 -0
- mcli/ml/tests/test_training_dashboard.py +387 -0
- mcli/public/oi/oi.py +15 -0
- mcli/public/public.py +4 -0
- mcli/self/self_cmd.py +1246 -0
- mcli/workflow/daemon/api_daemon.py +800 -0
- mcli/workflow/daemon/async_command_database.py +681 -0
- mcli/workflow/daemon/async_process_manager.py +591 -0
- mcli/workflow/daemon/client.py +530 -0
- mcli/workflow/daemon/commands.py +1196 -0
- mcli/workflow/daemon/daemon.py +905 -0
- mcli/workflow/daemon/daemon_api.py +59 -0
- mcli/workflow/daemon/enhanced_daemon.py +571 -0
- mcli/workflow/daemon/process_cli.py +244 -0
- mcli/workflow/daemon/process_manager.py +439 -0
- mcli/workflow/daemon/test_daemon.py +275 -0
- mcli/workflow/dashboard/dashboard_cmd.py +113 -0
- mcli/workflow/docker/docker.py +0 -0
- mcli/workflow/file/file.py +100 -0
- mcli/workflow/gcloud/config.toml +21 -0
- mcli/workflow/gcloud/gcloud.py +58 -0
- mcli/workflow/git_commit/ai_service.py +328 -0
- mcli/workflow/git_commit/commands.py +430 -0
- mcli/workflow/lsh_integration.py +355 -0
- mcli/workflow/model_service/client.py +594 -0
- mcli/workflow/model_service/download_and_run_efficient_models.py +288 -0
- mcli/workflow/model_service/lightweight_embedder.py +397 -0
- mcli/workflow/model_service/lightweight_model_server.py +714 -0
- mcli/workflow/model_service/lightweight_test.py +241 -0
- mcli/workflow/model_service/model_service.py +1955 -0
- mcli/workflow/model_service/ollama_efficient_runner.py +425 -0
- mcli/workflow/model_service/pdf_processor.py +386 -0
- mcli/workflow/model_service/test_efficient_runner.py +234 -0
- mcli/workflow/model_service/test_example.py +315 -0
- mcli/workflow/model_service/test_integration.py +131 -0
- mcli/workflow/model_service/test_new_features.py +149 -0
- mcli/workflow/openai/openai.py +99 -0
- mcli/workflow/politician_trading/commands.py +1790 -0
- mcli/workflow/politician_trading/config.py +134 -0
- mcli/workflow/politician_trading/connectivity.py +490 -0
- mcli/workflow/politician_trading/data_sources.py +395 -0
- mcli/workflow/politician_trading/database.py +410 -0
- mcli/workflow/politician_trading/demo.py +248 -0
- mcli/workflow/politician_trading/models.py +165 -0
- mcli/workflow/politician_trading/monitoring.py +413 -0
- mcli/workflow/politician_trading/scrapers.py +966 -0
- mcli/workflow/politician_trading/scrapers_california.py +412 -0
- mcli/workflow/politician_trading/scrapers_eu.py +377 -0
- mcli/workflow/politician_trading/scrapers_uk.py +350 -0
- mcli/workflow/politician_trading/scrapers_us_states.py +438 -0
- mcli/workflow/politician_trading/supabase_functions.py +354 -0
- mcli/workflow/politician_trading/workflow.py +852 -0
- mcli/workflow/registry/registry.py +180 -0
- mcli/workflow/repo/repo.py +223 -0
- mcli/workflow/scheduler/commands.py +493 -0
- mcli/workflow/scheduler/cron_parser.py +238 -0
- mcli/workflow/scheduler/job.py +182 -0
- mcli/workflow/scheduler/monitor.py +139 -0
- mcli/workflow/scheduler/persistence.py +324 -0
- mcli/workflow/scheduler/scheduler.py +679 -0
- mcli/workflow/sync/sync_cmd.py +437 -0
- mcli/workflow/sync/test_cmd.py +314 -0
- mcli/workflow/videos/videos.py +242 -0
- mcli/workflow/wakatime/wakatime.py +11 -0
- mcli/workflow/workflow.py +37 -0
- mcli_framework-7.0.0.dist-info/METADATA +479 -0
- mcli_framework-7.0.0.dist-info/RECORD +186 -0
- mcli_framework-7.0.0.dist-info/WHEEL +5 -0
- mcli_framework-7.0.0.dist-info/entry_points.txt +7 -0
- mcli_framework-7.0.0.dist-info/licenses/LICENSE +21 -0
- mcli_framework-7.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI commands for the MCLI scheduler system
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from mcli.lib.logger.logger import get_logger
|
|
12
|
+
|
|
13
|
+
from .cron_parser import CronExpression, get_next_run_times, validate_cron_expression
|
|
14
|
+
from .job import JobStatus, JobType, ScheduledJob
|
|
15
|
+
from .scheduler import (
|
|
16
|
+
JobScheduler,
|
|
17
|
+
create_desktop_cleanup_job,
|
|
18
|
+
create_system_backup_job,
|
|
19
|
+
create_temp_cleanup_job,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
logger = get_logger(__name__)
|
|
23
|
+
|
|
24
|
+
# Global scheduler instance
|
|
25
|
+
_scheduler: Optional[JobScheduler] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_scheduler() -> JobScheduler:
|
|
29
|
+
"""Get or create the global scheduler instance"""
|
|
30
|
+
global _scheduler
|
|
31
|
+
if _scheduler is None:
|
|
32
|
+
_scheduler = JobScheduler()
|
|
33
|
+
return _scheduler
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@click.group()
|
|
37
|
+
def scheduler():
|
|
38
|
+
"""Robust cron-like job scheduling system"""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@scheduler.command()
|
|
43
|
+
@click.option("--start", is_flag=True, help="Start the scheduler")
|
|
44
|
+
@click.option("--stop", is_flag=True, help="Stop the scheduler")
|
|
45
|
+
@click.option("--restart", is_flag=True, help="Restart the scheduler")
|
|
46
|
+
@click.option("--status", is_flag=True, help="Show scheduler status")
|
|
47
|
+
def control(start: bool, stop: bool, restart: bool, status: bool):
|
|
48
|
+
"""Control the scheduler daemon"""
|
|
49
|
+
sched = get_scheduler()
|
|
50
|
+
|
|
51
|
+
if restart:
|
|
52
|
+
click.echo("Restarting scheduler...")
|
|
53
|
+
sched.stop()
|
|
54
|
+
sched.start()
|
|
55
|
+
click.echo("Scheduler restarted")
|
|
56
|
+
elif start:
|
|
57
|
+
click.echo("Starting scheduler...")
|
|
58
|
+
sched.start()
|
|
59
|
+
click.echo("Scheduler started")
|
|
60
|
+
elif stop:
|
|
61
|
+
click.echo("Stopping scheduler...")
|
|
62
|
+
sched.stop()
|
|
63
|
+
click.echo("Scheduler stopped")
|
|
64
|
+
elif status:
|
|
65
|
+
stats = sched.get_scheduler_stats()
|
|
66
|
+
click.echo(f"Scheduler running: {stats['running']}")
|
|
67
|
+
click.echo(f"Total jobs: {stats['total_jobs']}")
|
|
68
|
+
click.echo(f"Enabled jobs: {stats['enabled_jobs']}")
|
|
69
|
+
click.echo(f"Running jobs: {stats['running_jobs']}")
|
|
70
|
+
else:
|
|
71
|
+
click.echo("Use --start, --stop, --restart, or --status")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@scheduler.command()
|
|
75
|
+
@click.argument("name")
|
|
76
|
+
@click.argument("cron_expression")
|
|
77
|
+
@click.argument("command")
|
|
78
|
+
@click.option(
|
|
79
|
+
"--type",
|
|
80
|
+
"job_type",
|
|
81
|
+
type=click.Choice([t.value for t in JobType]),
|
|
82
|
+
default="command",
|
|
83
|
+
help="Job type",
|
|
84
|
+
)
|
|
85
|
+
@click.option("--description", default="", help="Job description")
|
|
86
|
+
@click.option("--enabled/--disabled", default=True, help="Enable/disable job")
|
|
87
|
+
@click.option("--max-runtime", default=3600, help="Maximum runtime in seconds")
|
|
88
|
+
@click.option("--retry-count", default=0, help="Number of retries on failure")
|
|
89
|
+
@click.option("--retry-delay", default=60, help="Delay between retries in seconds")
|
|
90
|
+
@click.option("--working-dir", help="Working directory for job execution")
|
|
91
|
+
@click.option("--env", multiple=True, help="Environment variables (KEY=VALUE)")
|
|
92
|
+
def add(
|
|
93
|
+
name: str,
|
|
94
|
+
cron_expression: str,
|
|
95
|
+
command: str,
|
|
96
|
+
job_type: str,
|
|
97
|
+
description: str,
|
|
98
|
+
enabled: bool,
|
|
99
|
+
max_runtime: int,
|
|
100
|
+
retry_count: int,
|
|
101
|
+
retry_delay: int,
|
|
102
|
+
working_dir: Optional[str],
|
|
103
|
+
env: tuple,
|
|
104
|
+
):
|
|
105
|
+
"""Add a new scheduled job"""
|
|
106
|
+
|
|
107
|
+
# Validate cron expression
|
|
108
|
+
if not validate_cron_expression(cron_expression):
|
|
109
|
+
click.echo(f"Error: Invalid cron expression: {cron_expression}", err=True)
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
# Parse environment variables
|
|
113
|
+
environment = {}
|
|
114
|
+
for env_var in env:
|
|
115
|
+
if "=" in env_var:
|
|
116
|
+
key, value = env_var.split("=", 1)
|
|
117
|
+
environment[key] = value
|
|
118
|
+
else:
|
|
119
|
+
click.echo(f"Warning: Invalid environment variable format: {env_var}")
|
|
120
|
+
|
|
121
|
+
# Create job
|
|
122
|
+
job = ScheduledJob(
|
|
123
|
+
name=name,
|
|
124
|
+
cron_expression=cron_expression,
|
|
125
|
+
job_type=JobType(job_type),
|
|
126
|
+
command=command,
|
|
127
|
+
description=description,
|
|
128
|
+
enabled=enabled,
|
|
129
|
+
max_runtime=max_runtime,
|
|
130
|
+
retry_count=retry_count,
|
|
131
|
+
retry_delay=retry_delay,
|
|
132
|
+
working_directory=working_dir,
|
|
133
|
+
environment=environment,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Add to scheduler
|
|
137
|
+
sched = get_scheduler()
|
|
138
|
+
if sched.add_job(job):
|
|
139
|
+
click.echo(f"Added job: {name} ({job.id})")
|
|
140
|
+
|
|
141
|
+
# Show next run times
|
|
142
|
+
try:
|
|
143
|
+
cron = CronExpression(cron_expression)
|
|
144
|
+
if not cron.is_reboot:
|
|
145
|
+
next_times = get_next_run_times(cron_expression, 3)
|
|
146
|
+
if next_times:
|
|
147
|
+
click.echo("Next run times:")
|
|
148
|
+
for i, time in enumerate(next_times, 1):
|
|
149
|
+
click.echo(f" {i}. {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.warning(f"Could not calculate next run times: {e}")
|
|
152
|
+
else:
|
|
153
|
+
click.echo("Failed to add job", err=True)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@scheduler.command()
|
|
157
|
+
@click.argument("job_id")
|
|
158
|
+
def remove(job_id: str):
|
|
159
|
+
"""Remove a scheduled job"""
|
|
160
|
+
sched = get_scheduler()
|
|
161
|
+
|
|
162
|
+
# Find job by ID or name
|
|
163
|
+
job = sched.get_job(job_id)
|
|
164
|
+
if not job:
|
|
165
|
+
# Try to find by name
|
|
166
|
+
for j in sched.get_all_jobs():
|
|
167
|
+
if j.name == job_id:
|
|
168
|
+
job = j
|
|
169
|
+
break
|
|
170
|
+
|
|
171
|
+
if not job:
|
|
172
|
+
click.echo(f"Job not found: {job_id}", err=True)
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
if sched.remove_job(job.id):
|
|
176
|
+
click.echo(f"Removed job: {job.name}")
|
|
177
|
+
else:
|
|
178
|
+
click.echo("Failed to remove job", err=True)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@scheduler.command()
|
|
182
|
+
@click.option(
|
|
183
|
+
"--format",
|
|
184
|
+
"output_format",
|
|
185
|
+
type=click.Choice(["table", "json"]),
|
|
186
|
+
default="table",
|
|
187
|
+
help="Output format",
|
|
188
|
+
)
|
|
189
|
+
@click.option("--status", "filter_status", help="Filter by job status")
|
|
190
|
+
@click.option("--enabled-only", is_flag=True, help="Show only enabled jobs")
|
|
191
|
+
def list(output_format: str, filter_status: Optional[str], enabled_only: bool):
|
|
192
|
+
"""List all scheduled jobs"""
|
|
193
|
+
sched = get_scheduler()
|
|
194
|
+
jobs = sched.get_all_jobs()
|
|
195
|
+
|
|
196
|
+
# Apply filters
|
|
197
|
+
if filter_status:
|
|
198
|
+
try:
|
|
199
|
+
status = JobStatus(filter_status)
|
|
200
|
+
jobs = [job for job in jobs if job.status == status]
|
|
201
|
+
except ValueError:
|
|
202
|
+
click.echo(f"Invalid status: {filter_status}", err=True)
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
if enabled_only:
|
|
206
|
+
jobs = [job for job in jobs if job.enabled]
|
|
207
|
+
|
|
208
|
+
if not jobs:
|
|
209
|
+
click.echo("No jobs found")
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
if output_format == "json":
|
|
213
|
+
job_data = [job.to_dict() for job in jobs]
|
|
214
|
+
click.echo(json.dumps(job_data, indent=2))
|
|
215
|
+
else:
|
|
216
|
+
# Table format
|
|
217
|
+
from rich.console import Console
|
|
218
|
+
from rich.table import Table
|
|
219
|
+
|
|
220
|
+
console = Console()
|
|
221
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
222
|
+
|
|
223
|
+
table.add_column("Name", style="cyan")
|
|
224
|
+
table.add_column("Schedule", style="green")
|
|
225
|
+
table.add_column("Type", style="yellow")
|
|
226
|
+
table.add_column("Status", style="red")
|
|
227
|
+
table.add_column("Enabled", style="blue")
|
|
228
|
+
table.add_column("Last Run", style="dim")
|
|
229
|
+
table.add_column("Next Run", style="dim")
|
|
230
|
+
|
|
231
|
+
for job in jobs:
|
|
232
|
+
enabled_str = "✓" if job.enabled else "✗"
|
|
233
|
+
last_run = job.last_run.strftime("%m-%d %H:%M") if job.last_run else "Never"
|
|
234
|
+
next_run = job.next_run.strftime("%m-%d %H:%M") if job.next_run else "Unknown"
|
|
235
|
+
|
|
236
|
+
table.add_row(
|
|
237
|
+
job.name[:20],
|
|
238
|
+
job.cron_expression[:15],
|
|
239
|
+
job.job_type.value,
|
|
240
|
+
job.status.value,
|
|
241
|
+
enabled_str,
|
|
242
|
+
last_run,
|
|
243
|
+
next_run,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
console.print(table)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@scheduler.command()
|
|
250
|
+
@click.argument("job_id")
|
|
251
|
+
@click.option("--history", is_flag=True, help="Show execution history")
|
|
252
|
+
@click.option("--verbose", "-v", is_flag=True, help="Show detailed information")
|
|
253
|
+
def show(job_id: str, history: bool, verbose: bool):
|
|
254
|
+
"""Show detailed information about a job"""
|
|
255
|
+
sched = get_scheduler()
|
|
256
|
+
|
|
257
|
+
# Find job by ID or name
|
|
258
|
+
job = sched.get_job(job_id)
|
|
259
|
+
if not job:
|
|
260
|
+
for j in sched.get_all_jobs():
|
|
261
|
+
if j.name == job_id:
|
|
262
|
+
job = j
|
|
263
|
+
break
|
|
264
|
+
|
|
265
|
+
if not job:
|
|
266
|
+
click.echo(f"Job not found: {job_id}", err=True)
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
# Show basic info
|
|
270
|
+
click.echo(f"Job: {job.name}")
|
|
271
|
+
click.echo(f"ID: {job.id}")
|
|
272
|
+
click.echo(f"Type: {job.job_type.value}")
|
|
273
|
+
click.echo(f"Schedule: {job.cron_expression}")
|
|
274
|
+
click.echo(f"Description: {job.description}")
|
|
275
|
+
click.echo(f"Status: {job.status.value}")
|
|
276
|
+
click.echo(f"Enabled: {'Yes' if job.enabled else 'No'}")
|
|
277
|
+
click.echo(f"Created: {job.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
278
|
+
|
|
279
|
+
if job.last_run:
|
|
280
|
+
click.echo(f"Last Run: {job.last_run.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
281
|
+
if job.next_run:
|
|
282
|
+
click.echo(f"Next Run: {job.next_run.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
283
|
+
|
|
284
|
+
click.echo(f"Run Count: {job.run_count}")
|
|
285
|
+
click.echo(f"Success Count: {job.success_count}")
|
|
286
|
+
click.echo(f"Failure Count: {job.failure_count}")
|
|
287
|
+
|
|
288
|
+
if verbose:
|
|
289
|
+
click.echo(f"\nCommand: {job.command}")
|
|
290
|
+
click.echo(f"Max Runtime: {job.max_runtime}s")
|
|
291
|
+
click.echo(f"Retry Count: {job.retry_count}")
|
|
292
|
+
click.echo(f"Retry Delay: {job.retry_delay}s")
|
|
293
|
+
|
|
294
|
+
if job.working_directory:
|
|
295
|
+
click.echo(f"Working Directory: {job.working_directory}")
|
|
296
|
+
|
|
297
|
+
if job.environment:
|
|
298
|
+
click.echo("Environment Variables:")
|
|
299
|
+
for key, value in job.environment.items():
|
|
300
|
+
click.echo(f" {key}={value}")
|
|
301
|
+
|
|
302
|
+
if job.last_output:
|
|
303
|
+
click.echo(f"\nLast Output:\n{job.last_output}")
|
|
304
|
+
|
|
305
|
+
if job.last_error:
|
|
306
|
+
click.echo(f"\nLast Error:\n{job.last_error}")
|
|
307
|
+
|
|
308
|
+
if history:
|
|
309
|
+
click.echo(f"\nExecution History:")
|
|
310
|
+
job_history = sched.storage.get_job_history(job.id, limit=10)
|
|
311
|
+
|
|
312
|
+
if not job_history:
|
|
313
|
+
click.echo(" No execution history")
|
|
314
|
+
else:
|
|
315
|
+
for record in job_history:
|
|
316
|
+
executed_at = record.get("executed_at", "Unknown")
|
|
317
|
+
status = record.get("status", "unknown")
|
|
318
|
+
runtime = record.get("runtime_seconds", 0)
|
|
319
|
+
click.echo(f" {executed_at}: {status} ({runtime}s)")
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@scheduler.command()
|
|
323
|
+
@click.argument("job_id")
|
|
324
|
+
@click.option("--enabled/--disabled", help="Enable or disable the job")
|
|
325
|
+
@click.option("--cron", help="Update cron expression")
|
|
326
|
+
@click.option("--command", help="Update command")
|
|
327
|
+
@click.option("--description", help="Update description")
|
|
328
|
+
@click.option("--max-runtime", type=int, help="Update max runtime")
|
|
329
|
+
def update(
|
|
330
|
+
job_id: str,
|
|
331
|
+
enabled: Optional[bool],
|
|
332
|
+
cron: Optional[str],
|
|
333
|
+
command: Optional[str],
|
|
334
|
+
description: Optional[str],
|
|
335
|
+
max_runtime: Optional[int],
|
|
336
|
+
):
|
|
337
|
+
"""Update a scheduled job"""
|
|
338
|
+
sched = get_scheduler()
|
|
339
|
+
|
|
340
|
+
# Find job
|
|
341
|
+
job = sched.get_job(job_id)
|
|
342
|
+
if not job:
|
|
343
|
+
for j in sched.get_all_jobs():
|
|
344
|
+
if j.name == job_id:
|
|
345
|
+
job = j
|
|
346
|
+
break
|
|
347
|
+
|
|
348
|
+
if not job:
|
|
349
|
+
click.echo(f"Job not found: {job_id}", err=True)
|
|
350
|
+
return
|
|
351
|
+
|
|
352
|
+
# Update fields
|
|
353
|
+
updated = False
|
|
354
|
+
if enabled is not None:
|
|
355
|
+
job.enabled = enabled
|
|
356
|
+
updated = True
|
|
357
|
+
|
|
358
|
+
if cron:
|
|
359
|
+
if not validate_cron_expression(cron):
|
|
360
|
+
click.echo(f"Error: Invalid cron expression: {cron}", err=True)
|
|
361
|
+
return
|
|
362
|
+
job.cron_expression = cron
|
|
363
|
+
updated = True
|
|
364
|
+
|
|
365
|
+
if command:
|
|
366
|
+
job.command = command
|
|
367
|
+
updated = True
|
|
368
|
+
|
|
369
|
+
if description is not None:
|
|
370
|
+
job.description = description
|
|
371
|
+
updated = True
|
|
372
|
+
|
|
373
|
+
if max_runtime:
|
|
374
|
+
job.max_runtime = max_runtime
|
|
375
|
+
updated = True
|
|
376
|
+
|
|
377
|
+
if updated:
|
|
378
|
+
sched.storage.save_job(job)
|
|
379
|
+
click.echo(f"Updated job: {job.name}")
|
|
380
|
+
else:
|
|
381
|
+
click.echo("No changes made")
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
@scheduler.command()
|
|
385
|
+
@click.argument("cron_expression")
|
|
386
|
+
@click.option("--count", default=5, help="Number of future run times to show")
|
|
387
|
+
def test_cron(cron_expression: str, count: int):
|
|
388
|
+
"""Test a cron expression and show next run times"""
|
|
389
|
+
if not validate_cron_expression(cron_expression):
|
|
390
|
+
click.echo(f"Error: Invalid cron expression: {cron_expression}", err=True)
|
|
391
|
+
return
|
|
392
|
+
|
|
393
|
+
try:
|
|
394
|
+
cron = CronExpression(cron_expression)
|
|
395
|
+
click.echo(f"Cron Expression: {cron_expression}")
|
|
396
|
+
click.echo(f"Description: {cron.get_description()}")
|
|
397
|
+
|
|
398
|
+
if cron.is_reboot:
|
|
399
|
+
click.echo("This is a @reboot job (runs only at scheduler startup)")
|
|
400
|
+
else:
|
|
401
|
+
next_times = get_next_run_times(cron_expression, count)
|
|
402
|
+
if next_times:
|
|
403
|
+
click.echo(f"\nNext {len(next_times)} run times:")
|
|
404
|
+
for i, time in enumerate(next_times, 1):
|
|
405
|
+
click.echo(f" {i}. {time.strftime('%Y-%m-%d %H:%M:%S %A')}")
|
|
406
|
+
else:
|
|
407
|
+
click.echo("Could not calculate next run times")
|
|
408
|
+
except Exception as e:
|
|
409
|
+
click.echo(f"Error: {e}", err=True)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
@scheduler.command()
|
|
413
|
+
@click.option("--json", "output_json", is_flag=True, help="Output JSON for frontend")
|
|
414
|
+
def status(output_json: bool):
|
|
415
|
+
"""Show scheduler status and statistics"""
|
|
416
|
+
sched = get_scheduler()
|
|
417
|
+
stats = sched.get_scheduler_stats()
|
|
418
|
+
|
|
419
|
+
if output_json:
|
|
420
|
+
response = sched.create_json_response()
|
|
421
|
+
click.echo(json.dumps(response, indent=2))
|
|
422
|
+
else:
|
|
423
|
+
click.echo(f"Scheduler Status: {'Running' if stats['running'] else 'Stopped'}")
|
|
424
|
+
click.echo(f"Total Jobs: {stats['total_jobs']}")
|
|
425
|
+
click.echo(f"Enabled Jobs: {stats['enabled_jobs']}")
|
|
426
|
+
click.echo(f"Currently Running: {stats['running_jobs']}")
|
|
427
|
+
|
|
428
|
+
monitor_stats = stats.get("monitor_stats", {})
|
|
429
|
+
if monitor_stats.get("running_job_ids"):
|
|
430
|
+
click.echo(f"Running Job IDs: {', '.join(monitor_stats['running_job_ids'])}")
|
|
431
|
+
|
|
432
|
+
storage_info = stats.get("storage_info", {})
|
|
433
|
+
if storage_info:
|
|
434
|
+
click.echo(f"Storage Directory: {storage_info.get('storage_dir', 'Unknown')}")
|
|
435
|
+
click.echo(f"Jobs in Storage: {storage_info.get('jobs_count', 0)}")
|
|
436
|
+
click.echo(f"History Records: {storage_info.get('history_count', 0)}")
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
@scheduler.group()
|
|
440
|
+
def presets():
|
|
441
|
+
"""Pre-configured job templates"""
|
|
442
|
+
pass
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
@presets.command()
|
|
446
|
+
@click.option("--cron", default="0 9 * * 1", help="Cron expression (default: Monday 9 AM)")
|
|
447
|
+
@click.option("--enabled/--disabled", default=True, help="Enable job")
|
|
448
|
+
def desktop_cleanup(cron: str, enabled: bool):
|
|
449
|
+
"""Add desktop file organization job"""
|
|
450
|
+
job = create_desktop_cleanup_job("Desktop Organization", cron, enabled)
|
|
451
|
+
|
|
452
|
+
sched = get_scheduler()
|
|
453
|
+
if sched.add_job(job):
|
|
454
|
+
click.echo(f"Added desktop cleanup job: {job.name}")
|
|
455
|
+
else:
|
|
456
|
+
click.echo("Failed to add desktop cleanup job", err=True)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
@presets.command()
|
|
460
|
+
@click.option("--path", default="/tmp", help="Path to clean up")
|
|
461
|
+
@click.option("--days", default=7, help="Delete files older than this many days")
|
|
462
|
+
@click.option("--cron", default="0 2 * * *", help="Cron expression (default: daily 2 AM)")
|
|
463
|
+
@click.option("--enabled/--disabled", default=True, help="Enable job")
|
|
464
|
+
def temp_cleanup(path: str, days: int, cron: str, enabled: bool):
|
|
465
|
+
"""Add temporary file cleanup job"""
|
|
466
|
+
job = create_temp_cleanup_job("Temp File Cleanup", cron, path, days, enabled)
|
|
467
|
+
|
|
468
|
+
sched = get_scheduler()
|
|
469
|
+
if sched.add_job(job):
|
|
470
|
+
click.echo(f"Added temp cleanup job: {job.name}")
|
|
471
|
+
else:
|
|
472
|
+
click.echo("Failed to add temp cleanup job", err=True)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
@presets.command()
|
|
476
|
+
@click.argument("backup_command")
|
|
477
|
+
@click.option("--cron", default="0 1 * * 0", help="Cron expression (default: Sunday 1 AM)")
|
|
478
|
+
@click.option("--enabled/--disabled", default=True, help="Enable job")
|
|
479
|
+
def system_backup(backup_command: str, cron: str, enabled: bool):
|
|
480
|
+
"""Add system backup job"""
|
|
481
|
+
job = create_system_backup_job("System Backup", cron, backup_command, enabled)
|
|
482
|
+
|
|
483
|
+
sched = get_scheduler()
|
|
484
|
+
if sched.add_job(job):
|
|
485
|
+
click.echo(f"Added system backup job: {job.name}")
|
|
486
|
+
else:
|
|
487
|
+
click.echo("Failed to add system backup job", err=True)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
# Add the scheduler commands to the main CLI
|
|
491
|
+
def register_scheduler_commands(cli_group):
|
|
492
|
+
"""Register scheduler commands with the main CLI"""
|
|
493
|
+
cli_group.add_command(scheduler)
|