Skip to content

Commit 81009c2

Browse files
author
Bruno Grande
authored
Merge pull request #19 from Sage-Bionetworks-Workflows/bgrande/ORCA-217/tower-demo
[ORCA-217] Improve usability of Nextflow Tower service
2 parents 57de998 + 89455a2 commit 81009c2

File tree

6 files changed

+62
-13
lines changed

6 files changed

+62
-13
lines changed

src/orca/__init__.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,15 @@
1414

1515
import logging
1616

17-
18-
# Set default logging handler to avoid "No handler found" warnings
19-
logging.getLogger(__name__).addHandler(logging.NullHandler())
17+
# Capture warnings made with the warnings standard module
2018
logging.captureWarnings(True)
19+
20+
# Configure a stream handler
21+
handler = logging.StreamHandler()
22+
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
23+
handler.setFormatter(formatter)
24+
25+
# Configure a module logger
26+
logger = logging.getLogger(__name__)
27+
logger.setLevel(logging.INFO)
28+
logger.addHandler(handler)

src/orca/services/base/ops.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ class BaseOps(Generic[ConfigClass, ClientClass]):
2020
3) Provide values to all class variables (defined below).
2121
4) Provide implementations for all abstract methods.
2222
5) Update the type hints for attributes and class variables.
23+
6) Update the config attribute to have a default factory set to
24+
the config class using the `dataclasses.field()` function.
2325
2426
Attributes:
2527
config: A configuration object for this service.

src/orca/services/nextflowtower/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from orca.services.nextflowtower.utils import dedup, get_nested
1212

1313

14-
class WorkflowStatus(Enum):
14+
class WorkflowStatus(str, Enum):
1515
"""Valid values for the status of a Tower workflow.
1616
1717
Attributes:
@@ -317,4 +317,4 @@ class Workflow(BaseTowerModel):
317317
@property
318318
def is_done(self) -> bool:
319319
"""Whether the workflow is done running."""
320-
return self.status.value in WorkflowStatus.terminal_states.value
320+
return self.status in WorkflowStatus.terminal_states

src/orca/services/nextflowtower/ops.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import logging
2+
from dataclasses import field
13
from functools import cached_property
24
from typing import ClassVar, Optional
35

@@ -11,6 +13,8 @@
1113
from orca.services.nextflowtower.models import LaunchInfo, Workflow, WorkflowStatus
1214
from orca.services.nextflowtower.utils import increment_suffix
1315

16+
logger = logging.getLogger(__name__)
17+
1418

1519
@dataclass(kw_only=False)
1620
class NextflowTowerOps(BaseOps):
@@ -23,7 +27,7 @@ class NextflowTowerOps(BaseOps):
2327
client_factory_class: The class for constructing clients.
2428
"""
2529

26-
config: NextflowTowerConfig
30+
config: NextflowTowerConfig = field(default_factory=NextflowTowerConfig)
2731

2832
client_factory_class = NextflowTowerClientFactory
2933

@@ -130,13 +134,19 @@ def launch_workflow(
130134
if not ignore_previous_runs:
131135
latest_run = self.get_latest_previous_workflow(launch_info)
132136
if latest_run:
137+
status = latest_run.status.value
138+
run_repr = f"{latest_run.run_name} (id='{latest_run.id}', {status=})"
133139
# Return ID for latest run if ongoing, succeeded, or cancelled
134-
skip_statuses = {"SUCCEEDED", "CANCELLED"}
135-
if not latest_run.is_done or latest_run.status.value in skip_statuses:
140+
if not latest_run.is_done: # pragma: no cover
141+
logger.info(f"Found an ongoing previous run: {run_repr}")
142+
return latest_run.id
143+
if status in {"SUCCEEDED", "UNKNOWN"}:
144+
logger.info(f"Found a previous run: {run_repr}")
136145
return latest_run.id
137146
launch_info.fill_in("resume", True)
138147
launch_info.fill_in("session_id", latest_run.session_id)
139-
launch_info.run_name = increment_suffix(launch_info.run_name)
148+
launch_info.run_name = increment_suffix(latest_run.run_name)
149+
logger.info(f"Relaunching from a previous run: {run_repr}")
140150

141151
# Get relevant compute environment and its resource tags
142152
compute_env_id = self.get_latest_compute_env(compute_env_filter)
@@ -154,7 +164,21 @@ def launch_workflow(
154164
launch_info.fill_in("pre_run_script", compute_env.pre_run_script)
155165
launch_info.add_in("label_ids", label_ids)
156166

157-
return self.client.launch_workflow(launch_info, self.workspace_id)
167+
workflow_id = self.client.launch_workflow(launch_info, self.workspace_id)
168+
workflow_repr = f"{launch_info.run_name} ({workflow_id})"
169+
logger.info(f"Launched a new workflow run: {workflow_repr}")
170+
return workflow_id
171+
172+
def get_workflow(self, workflow_id: str) -> Workflow:
173+
"""Retrieve details about a workflow run.
174+
175+
Args:
176+
workflow_id: Workflow run ID.
177+
178+
Returns:
179+
Workflow instance.
180+
"""
181+
return self.client.get_workflow(workflow_id, self.workspace_id)
158182

159183
# TODO: Consider switching return value to a namedtuple
160184
def get_workflow_status(self, workflow_id: str) -> tuple[WorkflowStatus, bool]:
@@ -166,8 +190,8 @@ def get_workflow_status(self, workflow_id: str) -> tuple[WorkflowStatus, bool]:
166190
Returns:
167191
Workflow status and whether the workflow is done.
168192
"""
169-
workflow = self.client.get_workflow(workflow_id, self.workspace_id)
170-
is_done = workflow.status.value in WorkflowStatus.terminal_states.value
193+
workflow = self.get_workflow(workflow_id)
194+
is_done = workflow.status in WorkflowStatus.terminal_states
171195
return workflow.status, is_done
172196

173197
def list_workflows(self, search_filter: str = "") -> list[Workflow]:

src/orca/services/sevenbridges/ops.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
from dataclasses import field
34
from functools import cached_property
45
from typing import Any, Optional, cast
56

@@ -23,7 +24,7 @@ class SevenBridgesOps(BaseOps):
2324
client_factory_class: The class for constructing clients.
2425
"""
2526

26-
config: SevenBridgesConfig
27+
config: SevenBridgesConfig = field(default_factory=SevenBridgesConfig)
2728

2829
client_factory_class = SevenBridgesClientFactory
2930

tests/services/base/test_ops.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from dataclasses import fields
2+
from typing import get_type_hints
3+
14
from orca.services.base import BaseClientFactory, BaseConfig
25

36

@@ -6,6 +9,17 @@ def test_that_config_is_set(ops):
69
assert isinstance(ops.config, BaseConfig)
710

811

12+
def test_that_config_has_a_default_factory(ops):
13+
config_field = [f for f in fields(ops) if f.name == "config"][0]
14+
assert getattr(config_field, "default_factory", None) is not None
15+
16+
17+
def test_that_config_has_a_matching_default_factory(ops):
18+
config_field = [f for f in fields(ops) if f.name == "config"][0]
19+
config_type = get_type_hints(ops.__class__)["config"]
20+
assert config_field.default_factory == config_type
21+
22+
923
def test_that_client_factory_class_is_set(service):
1024
ops_class = service["ops"]
1125
assert hasattr(ops_class, "client_factory_class")

0 commit comments

Comments
 (0)