Skip to content

Commit cf9df02

Browse files
committed
WIP: workflow progress bar.
1 parent 03840ad commit cf9df02

4 files changed

Lines changed: 191 additions & 3 deletions

File tree

planemo/commands/cmd_workflow_test_on_invocation.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@
1515

1616
@click.command("workflow_test_on_invocation")
1717
@options.optional_tools_arg(multiple=False, allow_uris=False, metavar="TEST.YML")
18-
@options.required_invocation_id_arg()
19-
@options.galaxy_url_option(required=True)
20-
@options.galaxy_user_key_option(required=True)
18+
@options.invocation_target_options()
2119
@options.test_index_option()
2220
@options.test_output_options()
2321
@command_function
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""Module describing the planemo ``workflow_track`` command."""
2+
3+
import click
4+
5+
from planemo import options
6+
from planemo.cli import command_function
7+
from planemo.galaxy.workflow_progress import WorkflowProgress
8+
from planemo.engine.factory import engine_context
9+
10+
11+
@click.command("workflow_track")
12+
@options.invocation_target_options()
13+
@command_function
14+
def cli(ctx, invocation_id, **kwds):
15+
"""Run defined tests against existing workflow invocation."""
16+
with WorkflowProgress() as workflow_progress:
17+
workflow_progress.add_bars()
18+
import time
19+
time.sleep(1)
20+
new_step = {"state": "new"}
21+
scheduled_step = {"state": "scheduled"}
22+
new_steps = [new_step, new_step, new_step]
23+
one_scheduled_steps = [scheduled_step, new_step, new_step]
24+
two_scheduled_steps = [scheduled_step, scheduled_step, new_step]
25+
all_scheduled_steps = [scheduled_step, scheduled_step, scheduled_step]
26+
state_pairs = [
27+
({"state": "new"}, {}),
28+
({"state": "ready", "steps": new_steps}, {}),
29+
({"state": "ready", "steps": one_scheduled_steps}, {"states": {"new": 1}}),
30+
({"state": "ready", "steps": two_scheduled_steps}, {"states": {"new": 2}}),
31+
({"state": "ready", "steps": two_scheduled_steps}, {"states": {"new": 1, "running": 1}}),
32+
({"state": "ready", "steps": two_scheduled_steps}, {"states": {"new": 1, "ok": 1}}),
33+
({"state": "ready", "steps": two_scheduled_steps}, {"states": {"ok": 2}}),
34+
({"state": "scheduled", "steps": all_scheduled_steps}, {"states": {"ok": 2, "new": 3}}),
35+
({"state": "scheduled", "steps": all_scheduled_steps}, {"states": {"ok": 2, "running": 1, "new": 2}}),
36+
({"state": "scheduled", "steps": all_scheduled_steps}, {"states": {"ok": 3, "running": 1, "new": 1}}),
37+
({"state": "scheduled", "steps": all_scheduled_steps}, {"states": {"ok": 4, "running": 1}}),
38+
({"state": "scheduled", "steps": all_scheduled_steps}, {"states": {"ok": 5}}),
39+
]
40+
for (invocation, job_states_summary) in state_pairs:
41+
workflow_progress.handle_invocation(invocation, job_states_summary)
42+
time.sleep(1)
43+
44+
with engine_context(ctx, engine="external_galaxy", **kwds) as engine, engine.ensure_runnables_served([]) as config:
45+
user_gi = config.user_gi
46+
invocation = user_gi.invocations.show_invocation(invocation_id)
47+
# https://stackoverflow.com/questions/23113494/double-progress-bar-in-python
48+
49+
ctx.exit(0)
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
from typing import (
2+
Dict,
3+
Optional,
4+
)
5+
6+
from rich.console import Console
7+
from rich.progress import (
8+
BarColumn,
9+
Progress,
10+
TextColumn,
11+
TaskProgressColumn,
12+
)
13+
from rich.theme import Theme
14+
15+
16+
class WorkflowProgress(Progress):
17+
invocation_state: str = "new"
18+
step_count: Optional[int] = None
19+
job_count: int = 0
20+
steps_color: str = "cyan"
21+
jobs_color: str = "cyan"
22+
step_states: Dict = {}
23+
24+
def __init__(self):
25+
custom_theme = Theme({
26+
"bar.complete": "green",
27+
})
28+
console = Console(theme=custom_theme)
29+
super().__init__(
30+
TextColumn("[progress.description]{task.description}"),
31+
BarColumn(),
32+
TaskProgressColumn(),
33+
TextColumn(text_format="{task.fields[status]}"),
34+
console=console,
35+
)
36+
37+
def handle_invocation(self, invocation: Dict, job_state_summary: Dict):
38+
self.invocation_state = invocation.get("state") or "new"
39+
self.step_count = len(invocation.get("steps") or []) or None
40+
self.step_states = step_states(invocation)
41+
42+
steps_completed = None
43+
44+
steps_status = ""
45+
if self.step_count is None:
46+
steps_status = "Loading steps."
47+
self.steps_color = "cyan"
48+
elif self.invocation_state == "cancelled":
49+
steps_status = "Invocation cancelled"
50+
self.steps_color = "red"
51+
elif self.invocation_state == "failed":
52+
steps_status = "Invocation failed"
53+
self.steps_color = "red"
54+
else:
55+
num_scheduled = self.step_states.get("scheduled") or 0
56+
if num_scheduled > 0:
57+
self.steps_color = "green"
58+
else:
59+
self.steps_color = "cyan"
60+
steps_completed = num_scheduled
61+
steps_status = f"{num_scheduled}/{self.step_count} scheduled"
62+
63+
jobs_status = ""
64+
self.job_count = job_count(job_state_summary)
65+
num_errors = error_count(job_state_summary)
66+
num_ok = ok_count(job_state_summary)
67+
jobs_completed = num_ok + num_errors
68+
jobs_total = self.job_count
69+
if num_errors > 0:
70+
self.jobs_color = "red"
71+
elif self.job_count > 0:
72+
self.jobs_color = "green"
73+
else:
74+
self.jobs_color = "cyan"
75+
jobs_completed = None
76+
jobs_total = None
77+
if self.job_count > 0:
78+
jobs_status = f"{jobs_completed}/{self.job_count} terminal"
79+
self.update(self._steps_task, total=self.step_count, completed=steps_completed, description=f"[{self.steps_color}]Steps...", status=steps_status)
80+
self.update(self._jobs_task, total=jobs_total, completed=jobs_completed, description=f"[{self.jobs_color}]Jobs...", status=jobs_status)
81+
82+
def add_bars(self):
83+
self._steps_task = self.add_task("[green]Steps...", status="")
84+
self._jobs_task = self.add_task("[green]Jobs...", status="")
85+
86+
87+
# converted from Galaxy TypeScript (see util.ts next to WorkflowInvocationState.vue)
88+
def count_states(job_summary: Optional[Dict], query_states: list[str]) -> int:
89+
count = 0
90+
states = job_summary.get("states") if job_summary else None
91+
if states:
92+
for state in query_states:
93+
count += states.get(state, 0)
94+
return count
95+
96+
97+
def job_count(job_summary: Optional[Dict]) -> int:
98+
states = job_summary.get("states") if job_summary else None
99+
count = 0
100+
if states:
101+
for state_count in states.values():
102+
if state_count:
103+
count += state_count
104+
return count
105+
106+
107+
def step_states(invocation: Dict):
108+
step_states = {}
109+
steps = invocation.get("steps") or []
110+
for step in steps:
111+
if not step:
112+
continue
113+
step_state = step.get("state") or "unknown"
114+
if step_state not in step_states:
115+
step_states[step_state] = 0
116+
step_states[step_state] += 1
117+
118+
return step_states
119+
120+
121+
def ok_count(job_summary: Dict) -> int:
122+
return count_states(job_summary, ["ok", "skipped"])
123+
124+
125+
ERROR_STATES = ["error", "deleted"]
126+
127+
128+
def error_count(job_summary: Dict) -> int:
129+
return count_states(job_summary, ERROR_STATES)
130+
131+
132+
def running_count(job_summary: Dict) -> int:
133+
return count_states(job_summary, ["running"])

planemo/options.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2103,6 +2103,14 @@ def mulled_action_option():
21032103
)
21042104

21052105

2106+
def invocation_target_options():
2107+
return _compose(
2108+
required_invocation_id_arg(),
2109+
galaxy_url_option(required=True),
2110+
galaxy_user_key_option(required=True),
2111+
)
2112+
2113+
21062114
def mulled_options():
21072115
return _compose(
21082116
mulled_conda_option(),

0 commit comments

Comments
 (0)