Skip to content

Commit 3e593d2

Browse files
committed
cleaning and fixing bug in the interaction with the cli
1 parent 79351c7 commit 3e593d2

8 files changed

Lines changed: 168 additions & 62 deletions

File tree

src/maestro/cli_client.py

Lines changed: 45 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -59,18 +59,31 @@ def create(
5959

6060
@app.command()
6161
def run(
62-
dag_id: str = typer.Argument(..., help="DAG ID to run"),
62+
dag_input: str = typer.Argument(..., help="DAG ID or path to DAG YAML file"),
6363
resume: bool = typer.Option(False, "--resume", help="Resume from last checkpoint"),
6464
fail_fast: bool = typer.Option(True, "--fail-fast/--no-fail-fast", help="Stop on first failure"),
6565
server_url: str = typer.Option("http://localhost:8000", "--server", help="Maestro server URL")
6666
):
67-
"""Run a previously created DAG"""
67+
"""Run a DAG - either by ID or by creating from a YAML file"""
6868
api_client.base_url = server_url
6969
check_server_connection()
7070

7171
try:
72+
# Check if the input is a file path or a DAG ID
73+
if os.path.exists(dag_input) and dag_input.endswith(('.yaml', '.yml')):
74+
# It's a file path - create the DAG first
75+
dag_file_path = os.path.abspath(dag_input)
76+
console.print(f"[blue]Creating DAG from file: {dag_file_path}[/blue]")
77+
create_response = api_client.create_dag(dag_file_path)
78+
dag_id = create_response['dag_id']
79+
console.print(f"[bold green]✓ DAG created successfully with ID: {dag_id}[/bold green]")
80+
else:
81+
# It's a DAG ID
82+
dag_id = dag_input
83+
84+
# Now run the DAG
7285
response = api_client.run_dag(dag_id, resume, fail_fast)
73-
console.print(f"[bold green]✓ {response['message']}[/bold green]")
86+
console.print(f"[bold green]✓ DAG started successfully![/bold green]")
7487
console.print(f"[cyan]DAG ID:[/cyan] {response['dag_id']}")
7588
console.print(f"[cyan]Execution ID:[/cyan] {response['execution_id']}")
7689
console.print(f"[cyan]Status:[/cyan] [yellow]{response['status']}[/yellow]")
@@ -85,49 +98,6 @@ def run(
8598
console.print(f"[red]Error: {e}[/red]")
8699
raise typer.Exit(1)
87100

88-
@app.command()
89-
def submit(
90-
dag_file: str = typer.Argument(..., help="Path to the DAG YAML file"),
91-
resume: bool = typer.Option(False, "--resume", help="Resume from last checkpoint"),
92-
fail_fast: bool = typer.Option(True, "--fail-fast/--no-fail-fast", help="Stop on first failure"),
93-
server_url: str = typer.Option("http://localhost:8000", "--server", help="Maestro server URL")
94-
):
95-
"""Submit a DAG for execution (legacy command)"""
96-
console.print("[yellow]Warning: 'maestro submit' is a legacy command. Use 'maestro create' followed by 'maestro run' for better control.[/yellow]")
97-
api_client.base_url = server_url
98-
check_server_connection()
99-
100-
try:
101-
# Convert a relative path to absolute
102-
dag_file_path = os.path.abspath(dag_file)
103-
104-
if not os.path.exists(dag_file_path):
105-
console.print(f"[red]Error: DAG file not found: {dag_file_path}[/red]")
106-
raise typer.Exit(1)
107-
108-
# First, create the DAG
109-
create_response = api_client.create_dag(dag_file_path)
110-
dag_id = create_response['dag_id']
111-
console.print(f"[bold green]✓ DAG created successfully with ID: {dag_id}[/bold green]")
112-
113-
# Then, run the DAG
114-
run_response = api_client.run_dag(dag_id, resume, fail_fast)
115-
116-
console.print(f"[bold green]✓ DAG submitted successfully![/bold green]")
117-
console.print(f"[cyan]DAG ID:[/cyan] {run_response['dag_id']}")
118-
console.print(f"[cyan]Execution ID:[/cyan] {run_response['execution_id']}")
119-
console.print(f"[cyan]Status:[/cyan] [yellow]{run_response['status']}[/yellow]")
120-
console.print()
121-
console.print("[bold blue]Available commands:[/bold blue]")
122-
console.print(f" • [bold]maestro status {run_response['dag_id']}[/bold] - Check DAG status")
123-
console.print(f" • [bold]maestro log {run_response['dag_id']}[/bold] - View logs")
124-
console.print(f" • [bold]maestro attach {run_response['dag_id']}[/bold] - Attach to live log stream")
125-
console.print(f" • [bold]maestro stop {run_response['dag_id']}[/bold] - Stop execution")
126-
console.print()
127-
128-
except Exception as e:
129-
console.print(f"[red]Error: {e}[/red]")
130-
raise typer.Exit(1)
131101

132102
@app.command()
133103
def status(
@@ -150,7 +120,7 @@ def status(
150120
table.add_row("Execution ID", response["execution_id"])
151121
table.add_row("Status", response["status"])
152122
table.add_row("Started", response["started_at"] or "N/A")
153-
table.add_row("Completed", response["completed_at"] or "Running...")
123+
table.add_row("Completed", response["completed_at"] or "N/A")
154124
table.add_row("Thread ID", response["thread_id"] or "N/A")
155125

156126
console.print(table)
@@ -281,17 +251,40 @@ def handle_exit(signum, frame):
281251

282252
@app.command()
283253
def rm(
284-
dag_id: str = typer.Argument(..., help="DAG ID to remove"),
285-
force: bool = typer.Option(False, "--force", "-f", help="Force removal of a running DAG"),
254+
dag_id: Optional[str] = typer.Argument(None, help="DAG ID to remove"),
255+
all: bool = typer.Option(False, "--all", "-a", help="Remove all non-running DAGs (or all DAGs with --force)"),
256+
force: bool = typer.Option(False, "--force", "-f", help="Force removal, even for running DAGs"),
286257
server_url: str = typer.Option("http://localhost:8000", "--server", help="Maestro server URL")
287258
):
288259
"""Remove a DAG and its executions"""
289260
api_client.base_url = server_url
290261
check_server_connection()
291262

292263
try:
293-
response = api_client.remove_dag(dag_id, force)
294-
console.print(f"[bold green]✓ {response['message']}[/bold green]")
264+
if all:
265+
# Get all DAGs
266+
dags = api_client.list_dags_v1()
267+
removed_count = 0
268+
269+
for dag in dags:
270+
# Remove all non-running DAGs, or all if --force is used
271+
if force or dag['status'] not in ['running']:
272+
response = api_client.remove_dag(dag['dag_id'], force)
273+
console.print(f"[bold green]✓ Removed DAG: {dag['dag_id']} (status: {dag['status']})[/bold green]")
274+
removed_count += 1
275+
else:
276+
console.print(f"[yellow]⚠ Skipped running DAG: {dag['dag_id']}[/yellow]")
277+
278+
if removed_count == 0:
279+
console.print("[yellow]No DAGs to remove.[/yellow]")
280+
else:
281+
console.print(f"[bold green]✓ Removed {removed_count} DAG(s).[/bold green]")
282+
else:
283+
if not dag_id:
284+
console.print("[red]Error: Please provide a DAG ID or use --all flag[/red]")
285+
raise typer.Exit(1)
286+
response = api_client.remove_dag(dag_id, force)
287+
console.print(f"[bold green]✓ {response['message']}[/bold green]")
295288
except Exception as e:
296289
console.print(f"[red]Error: {e}[/red]")
297290
raise typer.Exit(1)
@@ -388,7 +381,7 @@ def list_dags(
388381
dag["execution_id"][:8] + "..." if dag["execution_id"] else "N/A",
389382
f"[{status_style}]{dag.get('status', 'N/A')}[/{status_style}]",
390383
dag.get("started_at", "N/A"),
391-
dag.get("completed_at", "N/A") or "Running...",
384+
dag.get("completed_at", "N/A"),
392385
str(dag.get("thread_id", "N/A"))
393386
)
394387

src/maestro/core/dag.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,27 @@
33
from collections import deque
44
from datetime import datetime
55
from croniter import croniter
6+
from enum import Enum
67

78
from maestro.core.task import Task
89

10+
11+
class DAGStatus(str, Enum):
12+
"""Enumeration for the status of a DAG."""
13+
PENDING = "pending"
14+
RUNNING = "running"
15+
COMPLETED = "completed"
16+
FAILED = "failed"
17+
18+
919
class DAG:
1020
def __init__(self, dag_id: str = "default_dag", start_time: Optional[datetime] = None, cron_schedule: Optional[str] = None):
1121
self.dag_id = dag_id
1222
self.start_time = start_time
1323
self.cron_schedule = cron_schedule
1424
self.tasks: Dict[str, Task] = {}
25+
self.status: DAGStatus = DAGStatus.PENDING
26+
self.execution_id: Optional[str] = None
1527

1628
# Validate that only one scheduling method is used
1729
if start_time is not None and cron_schedule is not None:

src/maestro/core/orchestrator.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from rich import get_console
88
from rich.logging import RichHandler
99

10-
from maestro.core.dag import DAG
10+
from maestro.core.dag import DAG, DAGStatus
1111
from maestro.core.task import TaskStatus
1212
from maestro.tasks.base import BaseTask
1313
from maestro.core.Task_Registry import TaskRegistry
@@ -101,15 +101,29 @@ def load_dag_from_file(self, filepath: str, dag_id: Optional[str] = None) -> DAG
101101
def run_dag_in_thread(
102102
self,
103103
dag: DAG,
104+
execution_id: str = None,
104105
resume: bool = False,
105106
fail_fast: bool = True,
106107
status_callback=None
107108
) -> str:
108109
"""Execute DAG in a separate thread with concurrency."""
109-
execution_id = str(uuid.uuid4())
110-
# Create execution record in database synchronously
110+
# Use provided execution_id or generate a new one
111+
if execution_id is None:
112+
execution_id = str(uuid.uuid4())
113+
114+
dag.execution_id = execution_id
115+
116+
# Create or update execution record in database synchronously
111117
with self.status_manager as sm:
112-
sm.create_dag_execution(dag.dag_id, execution_id)
118+
# Check if execution already exists (e.g., with 'created' status)
119+
existing = sm.get_latest_execution(dag.dag_id)
120+
if existing and existing["execution_id"] == execution_id:
121+
# Update existing execution to 'running'
122+
sm.update_dag_execution_status(dag.dag_id, execution_id, "running")
123+
else:
124+
# Create new execution
125+
sm.create_dag_execution(dag.dag_id, execution_id)
126+
113127
# Initialize all tasks with pending status only if not resuming
114128
if not resume:
115129
task_ids = list(dag.tasks.keys())
@@ -136,14 +150,18 @@ def execute():
136150
# Determine overall DAG status
137151
if has_running:
138152
# Should not happen after execution completes, but handle it
153+
dag.status = DAGStatus.RUNNING
139154
sm.update_dag_execution_status(dag.dag_id, execution_id, "running")
140155
elif has_failed:
156+
dag.status = DAGStatus.FAILED
141157
sm.update_dag_execution_status(dag.dag_id, execution_id, "failed")
142158
else:
159+
dag.status = DAGStatus.COMPLETED
143160
sm.update_dag_execution_status(dag.dag_id, execution_id, "completed")
144161

145162
except Exception as e:
146163
with self.status_manager as sm:
164+
dag.status = DAGStatus.FAILED
147165
# Mark any running or pending tasks as failed when DAG execution fails
148166
sm.mark_incomplete_tasks_as_failed(dag.dag_id, execution_id)
149167

@@ -169,6 +187,7 @@ def run_dag(
169187
):
170188
"""Execute DAG with improved error handling and persistence."""
171189
dag_id = dag.dag_id
190+
dag.status = DAGStatus.RUNNING
172191
execution_order = dag.get_execution_order()
173192

174193
# Set up database logging if execution_id is provided
@@ -317,4 +336,4 @@ def get_dag_status(self, dag: DAG) -> Dict[str, Any]:
317336
"tasks": tasks_status,
318337
"summary": summary,
319338
"total_tasks": len(dag.tasks)
320-
}
339+
}

src/maestro/core/status_manager.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,33 @@ def create_dag_execution(self, dag_id: str, execution_id: str) -> str:
259259

260260
return execution_id
261261

262+
def create_dag_execution_with_status(self, dag_id: str, execution_id: str, status: str) -> str:
263+
"""Create a new DAG execution record with a specific status."""
264+
with self._lock:
265+
self._create_dag_if_not_exists(dag_id)
266+
current_time = datetime.now().isoformat()
267+
thread_id = threading.current_thread().ident
268+
pid = threading.current_thread().ident
269+
270+
with self._conn:
271+
if status == "created":
272+
# For created status, don't set started_at or thread_id
273+
self._conn.execute("""
274+
INSERT OR REPLACE INTO executions
275+
(id, dag_id, status, started_at, thread_id, pid)
276+
VALUES (?, ?, ?, NULL, NULL, ?)
277+
""", (execution_id, dag_id, status, pid))
278+
else:
279+
# For other statuses, set started_at
280+
self._conn.execute("""
281+
INSERT OR REPLACE INTO executions
282+
(id, dag_id, status, started_at, thread_id, pid)
283+
VALUES (?, ?, ?, ?, ?, ?)
284+
""", (execution_id, dag_id, status, current_time, str(thread_id), pid))
285+
self._conn.commit()
286+
287+
return execution_id
288+
262289
def initialize_tasks_for_execution(self, dag_id: str, execution_id: str, task_ids: List[str]):
263290
"""Initialize all tasks for a DAG execution with pending status."""
264291
with self._lock:
@@ -278,6 +305,7 @@ def update_dag_execution_status(self, dag_id: str,
278305
"""Update DAG execution status."""
279306
with self._lock:
280307
current_time = datetime.now().isoformat()
308+
thread_id = threading.current_thread().ident
281309

282310
with self._conn:
283311
if status in ["completed", "failed", "cancelled"]:
@@ -286,6 +314,15 @@ def update_dag_execution_status(self, dag_id: str,
286314
SET status = ?, completed_at = ?
287315
WHERE dag_id = ? AND id = ?
288316
""", (status, current_time, dag_id, execution_id))
317+
elif status == "running":
318+
# When transitioning to running, set thread_id and started_at if not already set
319+
self._conn.execute("""
320+
UPDATE executions
321+
SET status = ?,
322+
thread_id = CASE WHEN thread_id IS NULL THEN ? ELSE thread_id END,
323+
started_at = CASE WHEN started_at IS NULL THEN ? ELSE started_at END
324+
WHERE dag_id = ? AND id = ?
325+
""", (status, str(thread_id), current_time, dag_id, execution_id))
289326
else:
290327
self._conn.execute("""
291328
UPDATE executions
@@ -586,6 +623,29 @@ def generate_unique_dag_id(self) -> str:
586623
suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
587624
return f"{base_name}_{suffix}"
588625

626+
def get_latest_execution(self, dag_id: str) -> Optional[Dict[str, Any]]:
627+
"""Get the latest execution for a DAG."""
628+
with self._lock:
629+
with self._conn:
630+
cursor = self._conn.execute("""
631+
SELECT id, status, started_at, completed_at, thread_id, pid
632+
FROM executions
633+
WHERE dag_id = ?
634+
ORDER BY COALESCE(started_at, datetime('now')) DESC
635+
LIMIT 1
636+
""", (dag_id,))
637+
row = cursor.fetchone()
638+
if row:
639+
return {
640+
"execution_id": row[0],
641+
"status": row[1],
642+
"started_at": row[2],
643+
"completed_at": row[3],
644+
"thread_id": row[4],
645+
"pid": row[5]
646+
}
647+
return None
648+
589649
def save_dag_definition(self, dag):
590650
"""Save the DAG definition to the database."""
591651
with self._lock:

src/maestro/server/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def print_logo():
7979
return
8080

8181
img = Image.open(logo_path).convert("RGBA")
82-
img = img.resize((40, 20), Image.Resampling.LANCZOS) # Resize for a reasonable terminal display
82+
img = img.resize((37, 15), Image.Resampling.LANCZOS) # Resize for a reasonable terminal display
8383

8484
for y in range(img.height):
8585
for x in range(img.width):

src/maestro/server/docker_api.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import Optional, List, Dict, Any
66
import asyncio
77
import json
8+
import uuid
89

910
from maestro.core.orchestrator import Orchestrator
1011
from maestro.core.status_manager import StatusManager
@@ -83,8 +84,13 @@ async def create_dag(request: DAGCreateRequest):
8384
# Load and validate DAG
8485
dag = orchestrator.load_dag_from_file(request.dag_file_path, dag_id=dag_id)
8586

87+
# Create a new execution ID
88+
execution_id = str(uuid.uuid4())
89+
8690
with orchestrator.status_manager as sm:
8791
sm.save_dag_definition(dag)
92+
# Create initial execution with 'created' status
93+
sm.create_dag_execution_with_status(dag_id, execution_id, "created")
8894

8995
return DAGCreateResponse(
9096
dag_id=dag.dag_id,
@@ -103,11 +109,23 @@ async def run_dag(dag_id: str, request: DAGRunRequest, background_tasks: Backgro
103109
dag_definition = sm.get_dag_definition(dag_id)
104110
if not dag_definition:
105111
raise HTTPException(status_code=404, detail=f"DAG '{dag_id}' not found.")
112+
113+
# Get the latest execution for this DAG
114+
latest_execution = sm.get_latest_execution(dag_id)
115+
116+
if latest_execution and latest_execution["status"] == "created":
117+
# Use the existing execution ID from the created DAG
118+
execution_id = latest_execution["execution_id"]
119+
else:
120+
# Create a new execution ID for subsequent runs
121+
execution_id = str(uuid.uuid4())
106122

107123
dag = orchestrator.dag_loader.load_dag_from_dict(dag_definition)
108-
109-
execution_id = orchestrator.run_dag_in_thread(
124+
125+
# Run the DAG with the determined execution ID
126+
orchestrator.run_dag_in_thread(
110127
dag=dag,
128+
execution_id=execution_id,
111129
resume=request.resume,
112130
fail_fast=request.fail_fast
113131
)

0 commit comments

Comments
 (0)