Skip to content

Commit 37b4d3d

Browse files
authored
Merge pull request #461 from nextstrain/register-pathogen-workflows
Register pathogen workflows in `nextstrain-pathogen.yaml`
2 parents 30798a9 + 1d1a90f commit 37b4d3d

6 files changed

Lines changed: 79 additions & 21 deletions

File tree

CHANGES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ development source code and as such may not be routinely kept up to date.
1515

1616
## Improvements
1717

18+
* `nextstrain setup <pathogen>` and `nextstrain version --pathogens` now list
19+
the available workflows for a pathogen if the pathogen lists the workflows
20+
in the it's top-level `nextstrain-pathogen.yaml` file.
21+
([#461](https://github.com/nextstrain/cli/pull/461))
22+
1823
* Snakemake's storage support downloaded files (stored in `.snakemake/storage/`)
1924
are now downloaded from AWS Batch builds by default.
2025

doc/changes.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ development source code and as such may not be routinely kept up to date.
1919
(v-next-improvements)=
2020
### Improvements
2121

22+
* `nextstrain setup <pathogen>` and `nextstrain version --pathogens` now list
23+
the available workflows for a pathogen if the pathogen lists the workflows
24+
in the it's top-level `nextstrain-pathogen.yaml` file.
25+
([#461](https://github.com/nextstrain/cli/pull/461))
26+
2227
* Snakemake's storage support downloaded files (stored in `.snakemake/storage/`)
2328
are now downloaded from AWS Batch builds by default.
2429

doc/commands/run.rst

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,11 @@ positional arguments
6060
Available workflows may vary per pathogen (and possibly between
6161
pathogen version). Some pathogens may provide multiple variants or
6262
base configurations of a top-level workflow, e.g. as in
63-
``phylogenetic/mpxv`` and ``phylogenetic/hmpxv1``. Refer to the
64-
pathogen's own documentation for valid workflow names.
63+
``phylogenetic/mpxv`` and ``phylogenetic/hmpxv1``.
64+
Run ``nextstrain version --pathogens`` to see a list of registered
65+
workflows per pathogen version. If the pathogen does not have
66+
registered workflows, then refer to the pathogen's own documentation
67+
for valid workflow names.
6568

6669
Workflow names conventionally correspond directly to directory
6770
paths in the pathogen source, but this may not always be the case.

nextstrain/cli/command/run.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,11 @@ def register_parser(subparser):
6767
Available workflows may vary per pathogen (and possibly between
6868
pathogen version). Some pathogens may provide multiple variants or
6969
base configurations of a top-level workflow, e.g. as in
70-
``phylogenetic/mpxv`` and ``phylogenetic/hmpxv1``. Refer to the
71-
pathogen's own documentation for valid workflow names.
70+
``phylogenetic/mpxv`` and ``phylogenetic/hmpxv1``.
71+
Run ``nextstrain version --pathogens`` to see a list of registered
72+
workflows per pathogen version. If the pathogen does not have
73+
registered workflows, then refer to the pathogen's own documentation
74+
for valid workflow names.
7275
7376
Workflow names conventionally correspond directly to directory
7477
paths in the pathogen source, but this may not always be the case.
@@ -225,6 +228,9 @@ def run(opts):
225228
# Resolve pathogen and workflow names to a local workflow directory.
226229
pathogen = PathogenVersion(opts.pathogen)
227230

231+
if opts.workflow not in pathogen.registered_workflows():
232+
print(f"The {opts.workflow!r} workflow is not registered as a compatible workflow, but trying to run anyways.")
233+
228234
workflow_directory = pathogen.workflow_path(opts.workflow)
229235

230236
if not workflow_directory.is_dir() or not (workflow_directory / "Snakefile").is_file():

nextstrain/cli/command/version.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,5 +71,12 @@ def run(opts):
7171
print(" " + str(version) + (f"={version.url or ''}" if opts.verbose else ""), "(default)" if is_default else "")
7272
if opts.verbose:
7373
print(" " + str(version.path))
74+
75+
if registered_workflows := version.registered_workflows():
76+
print(" " + "Available workflows:")
77+
for workflow in registered_workflows:
78+
print(" " + workflow)
79+
else:
80+
print(" " + "No workflows listed, please refer to pathogen docs.")
7481
else:
7582
print(" (none)")

nextstrain/cli/pathogens.py

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ class PathogenVersion:
130130

131131
setup_receipt: Optional[dict] = None
132132
url: Optional[URL] = None
133+
registration: Optional[dict] = None
133134

134135

135136
def __init__(self, name_version_url: str, new_setup: bool = False):
@@ -243,6 +244,9 @@ def __init__(self, name_version_url: str, new_setup: bool = False):
243244
self.registration_path = self.path / "nextstrain-pathogen.yaml"
244245
self.setup_receipt_path = self.path.with_suffix(self.path.suffix + ".json")
245246

247+
if self.registration_path.exists():
248+
self.registration = read_pathogen_registration(self.registration_path)
249+
246250
if not new_setup:
247251
if not self.path.is_dir():
248252
# XXX TODO: This error case should maybe be handled outside of
@@ -301,6 +305,23 @@ def __init__(self, name_version_url: str, new_setup: bool = False):
301305
self.url = url
302306

303307

308+
def registered_workflows(self) -> Dict[str, Dict]:
309+
"""
310+
Parses :attr:`.registration` to return a dict of registered
311+
compatible workflows, where the keys are workflow names.
312+
"""
313+
if self.registration is None:
314+
debug("pathogen does not have a registration")
315+
return {}
316+
317+
workflows = self.registration.get("compatibility", {}).get("nextstrain run")
318+
if not isinstance(workflows, dict):
319+
debug(f"pathogen registration.compatibility['nextstrain runs'] is not a dict (got a {type(workflows).__name__})")
320+
return {}
321+
322+
return workflows
323+
324+
304325
def workflow_path(self, workflow: str) -> Path:
305326
return self.path / workflow
306327

@@ -448,27 +469,31 @@ def setup(self, dry_run: bool = False, force: bool = False) -> SetupStatus:
448469
json.dump(self.setup_receipt, f, indent = " ")
449470
print(file = f)
450471

472+
self.registration = read_pathogen_registration(self.registration_path)
473+
451474
return True
452475

453476

454477
def test_setup(self) -> SetupTestResults:
455478
def test_compatibility() -> SetupTestResult:
456479
msg = "nextstrain-pathogen.yaml declares `nextstrain run` compatibility"
457480

458-
try:
459-
registration = read_pathogen_registration(self.registration_path)
460-
except (OSError, yaml.YAMLError, ValueError):
461-
if DEBUGGING:
462-
traceback.print_exc()
481+
if self.registration is None:
463482
return msg + "\n(couldn't read registration)", False
464483

465484
try:
466-
compatibility = registration["compatibility"]["nextstrain run"]
485+
compatibility = self.registration["compatibility"]["nextstrain run"]
467486
except (KeyError, IndexError, TypeError):
468487
if DEBUGGING:
469488
traceback.print_exc()
470489
return msg + "\n(couldn't find 'compatibility: nextstrain run: …' field)", False
471490

491+
if compatibility:
492+
if workflows := self.registered_workflows():
493+
msg += f"\nAvailable workflows: {list(workflows.keys())}"
494+
else:
495+
msg += f"\nNo workflows listed, please refer to pathogen docs."
496+
472497
return msg, bool(compatibility)
473498

474499
return [
@@ -841,21 +866,28 @@ def sorted_versions(vs: Iterable[str]) -> List[str]:
841866
return [v.original for v in [*reversed(compliant), *non_compliant]]
842867

843868

844-
def read_pathogen_registration(path: Path) -> Dict:
869+
def read_pathogen_registration(path: Path) -> Optional[Dict]:
845870
"""
846871
Reads a ``nextstrain-pathogen.yaml`` file at *path* and returns a dict of
847872
its deserialized contents.
848-
"""
849-
with path.open("r", encoding = "utf-8") as f:
850-
registration = yaml.safe_load(f)
851873
852-
# XXX TODO SOON: Consider doing actual schema validation here in the
853-
# future.
854-
# -trs, 12 Dec 2024
855-
if not isinstance(registration, dict):
856-
raise ValueError(f"pathogen registration not a dict (got a {type(registration).__name__}): {str(path)!r}")
857-
858-
return registration
874+
Returns ``None`` if there was an issue reading the registration.
875+
"""
876+
try:
877+
with path.open("r", encoding = "utf-8") as f:
878+
registration = yaml.safe_load(f)
879+
880+
# XXX TODO SOON: Consider doing actual schema validation here in the
881+
# future.
882+
# -trs, 12 Dec 2024
883+
if not isinstance(registration, dict):
884+
raise ValueError(f"pathogen registration not a dict (got a {type(registration).__name__}): {str(path)!r}")
885+
886+
return registration
887+
except (OSError, yaml.YAMLError, ValueError):
888+
if DEBUGGING:
889+
traceback.print_exc()
890+
return None
859891

860892

861893
# We query a nextstrain.org API instead of querying GitHub's API directly for a

0 commit comments

Comments
 (0)