bactopia 2.1.0__tar.gz → 2.1.2__tar.gz
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.
- {bactopia-2.1.0 → bactopia-2.1.2}/PKG-INFO +1 -1
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/catalog.py +12 -9
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/lint.py +20 -2
- bactopia-2.1.2/bactopia/cli/scaffold.py +376 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/sysinfo.py +8 -8
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/update.py +6 -50
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/workflows.py +1 -1
- bactopia-2.1.2/bactopia/conda.py +137 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/lint/rules/subworkflow_rules.py +94 -1
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/lint/rules/workflow_rules.py +28 -1
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/lint/runner.py +20 -2
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/nf.py +44 -5
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/parsables.py +2 -2
- bactopia-2.1.2/bactopia/scaffold.py +502 -0
- bactopia-2.1.2/bactopia/templates/scaffold/module/main.nf.j2 +131 -0
- bactopia-2.1.2/bactopia/templates/scaffold/module/module.config.j2 +35 -0
- bactopia-2.1.2/bactopia/templates/scaffold/module/schema.json.j2 +31 -0
- bactopia-2.1.2/bactopia/templates/scaffold/module/tests/main.nf.test.j2 +44 -0
- bactopia-2.1.2/bactopia/templates/scaffold/module/tests/nextflow.config.j2 +36 -0
- bactopia-2.1.2/bactopia/templates/scaffold/module/tests/nf-test.config.j2 +11 -0
- bactopia-2.1.2/bactopia/templates/scaffold/subworkflow/main.nf.j2 +96 -0
- bactopia-2.1.2/bactopia/templates/scaffold/subworkflow/tests/main.nf.test.j2 +60 -0
- bactopia-2.1.2/bactopia/templates/scaffold/subworkflow/tests/nextflow.config.j2 +42 -0
- bactopia-2.1.2/bactopia/templates/scaffold/subworkflow/tests/nf-test.config.j2 +11 -0
- bactopia-2.1.2/bactopia/templates/scaffold/subworkflow/tests/nftignore.j2 +2 -0
- bactopia-2.1.2/bactopia/templates/scaffold/workflow/main.nf.j2 +94 -0
- bactopia-2.1.2/bactopia/templates/scaffold/workflow/nextflow.config.j2 +93 -0
- bactopia-2.1.2/bactopia/templates/scaffold/workflow/tests/main.nf.test.j2 +43 -0
- bactopia-2.1.2/bactopia/templates/scaffold/workflow/tests/nf-test.config.j2 +11 -0
- bactopia-2.1.2/bactopia/templates/scaffold/workflow/tests/nftignore.j2 +3 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/pyproject.toml +3 -2
- {bactopia-2.1.0 → bactopia-2.1.2}/LICENSE +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/README.md +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/__init__.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/atb.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/__init__.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/atb/__init__.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/atb/atb_downloader.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/atb/atb_formatter.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/citations.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/datasets.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/docs.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/download.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/helpers/__init__.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/helpers/merge_schemas.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/jsonify.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/pipeline/__init__.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/pipeline/bracken_to_excel.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/pipeline/check_assembly_accession.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/pipeline/check_fastqs.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/pipeline/cleanup_coverage.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/pipeline/kraken_bracken_summary.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/pipeline/mask_consensus.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/pipeline/scrubber_summary.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/pipeline/teton_prepare.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/prepare.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/prune.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/pubmlst/build.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/pubmlst/setup.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/review.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/search.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/status.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/summary.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/testing.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/databases/__init__.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/databases/ena.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/databases/ncbi.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/databases/pubmlst/__init__.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/databases/pubmlst/constants.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/databases/pubmlst/utils.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/lint/__init__.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/lint/citations.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/lint/docs.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/lint/models.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/lint/rules/__init__.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/lint/rules/module_rules.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/outputs.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parse.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/__init__.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/amrfinderplus.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/annotator.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/ariba.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/assembler.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/blast.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/citations.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/coverage.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/error.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/gather.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/generic.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/kraken.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/mapping.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/mlst.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/nextflow.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/qc.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/sketcher.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/variants.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/versions.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/workflows.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/reports/__init__.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/reports/templates/__init__.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/summary.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/templates/__init__.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/templates/bactopia/llms.txt.j2 +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/templates/logos.py +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/templates/nextflow/nextflow.config.j2 +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/templates/nextflow/params.config.j2 +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/templates/nextflow/process.config.j2 +0 -0
- {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/utils.py +0 -0
|
@@ -82,13 +82,11 @@ def _parse_output_fields(raw_lines: list[str]) -> dict[str, list[str]]:
|
|
|
82
82
|
return channels
|
|
83
83
|
|
|
84
84
|
|
|
85
|
-
def _infer_scope(
|
|
86
|
-
"""Infer subworkflow scope from emit
|
|
87
|
-
|
|
88
|
-
has_run = "run_outputs" in channels
|
|
89
|
-
if has_sample:
|
|
85
|
+
def _infer_scope(emits: dict[str, list[str]]) -> str:
|
|
86
|
+
"""Infer subworkflow scope from emit channels and their documented fields."""
|
|
87
|
+
if emits.get("sample_outputs"):
|
|
90
88
|
return "sample"
|
|
91
|
-
if
|
|
89
|
+
if "run_outputs" in emits:
|
|
92
90
|
return "run"
|
|
93
91
|
return "custom"
|
|
94
92
|
|
|
@@ -137,8 +135,8 @@ def _extract_tool_info(ext: dict) -> dict | None:
|
|
|
137
135
|
if "::" in pkg:
|
|
138
136
|
pkg = pkg.split("::", 1)[1]
|
|
139
137
|
if "=" in pkg:
|
|
140
|
-
|
|
141
|
-
return {"name":
|
|
138
|
+
parts = pkg.split("=")
|
|
139
|
+
return {"name": parts[0], "version": parts[1]}
|
|
142
140
|
return {"name": pkg, "version": "unknown"}
|
|
143
141
|
|
|
144
142
|
|
|
@@ -240,7 +238,12 @@ def _build_subworkflow_entry(
|
|
|
240
238
|
channel_fields = _parse_output_fields(groovydoc.get("raw_lines", []))
|
|
241
239
|
# Build dict: every declared channel gets an entry, even if no fields documented
|
|
242
240
|
entry["emits"] = {ch: channel_fields.get(ch, []) for ch in channel_names}
|
|
243
|
-
entry["scope"] = _infer_scope(
|
|
241
|
+
entry["scope"] = _infer_scope(entry["emits"])
|
|
242
|
+
|
|
243
|
+
# Merlin dynamically dispatches to species-specific subworkflows so its
|
|
244
|
+
# sample_outputs has no fixed field names to document, but it is sample scope.
|
|
245
|
+
if component_name == "merlin":
|
|
246
|
+
entry["scope"] = "sample"
|
|
244
247
|
|
|
245
248
|
# Calls
|
|
246
249
|
calls = {}
|
|
@@ -28,6 +28,8 @@ click.rich_click.OPTION_GROUPS = {
|
|
|
28
28
|
"--subworkflows",
|
|
29
29
|
"--workflows",
|
|
30
30
|
"--module",
|
|
31
|
+
"--subworkflow",
|
|
32
|
+
"--workflow",
|
|
31
33
|
],
|
|
32
34
|
},
|
|
33
35
|
{
|
|
@@ -142,6 +144,18 @@ def print_rich(
|
|
|
142
144
|
default=None,
|
|
143
145
|
help="Lint a single module by name (e.g. 'mlst', 'bakta/run')",
|
|
144
146
|
)
|
|
147
|
+
@click.option(
|
|
148
|
+
"--subworkflow",
|
|
149
|
+
"subworkflow_filter",
|
|
150
|
+
default=None,
|
|
151
|
+
help="Lint a single subworkflow by name (e.g. 'mlst')",
|
|
152
|
+
)
|
|
153
|
+
@click.option(
|
|
154
|
+
"--workflow",
|
|
155
|
+
"workflow_filter",
|
|
156
|
+
default=None,
|
|
157
|
+
help="Lint a single workflow by name (e.g. 'mlst', 'bactopia-tools/mlst')",
|
|
158
|
+
)
|
|
145
159
|
@click.option(
|
|
146
160
|
"-q",
|
|
147
161
|
"--quiet",
|
|
@@ -159,6 +173,8 @@ def lint(
|
|
|
159
173
|
subworkflows,
|
|
160
174
|
workflows,
|
|
161
175
|
module_filter,
|
|
176
|
+
subworkflow_filter,
|
|
177
|
+
workflow_filter,
|
|
162
178
|
quiet,
|
|
163
179
|
use_json,
|
|
164
180
|
pretty,
|
|
@@ -189,8 +205,8 @@ def lint(
|
|
|
189
205
|
logging.error(f"No main.nf found in {bp}, is this a valid Bactopia repository?")
|
|
190
206
|
sys.exit(1)
|
|
191
207
|
|
|
192
|
-
# If filtering by module, only lint modules
|
|
193
|
-
if module_filter:
|
|
208
|
+
# If filtering by module, only lint modules (unless other filters also set)
|
|
209
|
+
if module_filter and not subworkflow_filter and not workflow_filter:
|
|
194
210
|
subworkflows = False
|
|
195
211
|
workflows = False
|
|
196
212
|
|
|
@@ -201,6 +217,8 @@ def lint(
|
|
|
201
217
|
lint_subworkflows=subworkflows,
|
|
202
218
|
lint_workflows=workflows,
|
|
203
219
|
module_filter=module_filter,
|
|
220
|
+
subworkflow_filter=subworkflow_filter,
|
|
221
|
+
workflow_filter=workflow_filter,
|
|
204
222
|
)
|
|
205
223
|
|
|
206
224
|
# Build serializable output
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""Scaffold Bactopia components from bioconda/conda-forge packages."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import rich
|
|
9
|
+
import rich.console
|
|
10
|
+
import rich.traceback
|
|
11
|
+
import rich_click as click
|
|
12
|
+
from rich.logging import RichHandler
|
|
13
|
+
|
|
14
|
+
import bactopia
|
|
15
|
+
from bactopia.conda import (
|
|
16
|
+
check_component_exists,
|
|
17
|
+
construct_container_refs,
|
|
18
|
+
get_latest_info_with_fallback,
|
|
19
|
+
)
|
|
20
|
+
from bactopia.scaffold import (
|
|
21
|
+
FIELD_PATTERNS,
|
|
22
|
+
discover_test_data,
|
|
23
|
+
render_all_files,
|
|
24
|
+
render_module_files,
|
|
25
|
+
render_subworkflow_files,
|
|
26
|
+
render_workflow_files,
|
|
27
|
+
validate_config,
|
|
28
|
+
write_files,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Set up Rich
|
|
32
|
+
stderr = rich.console.Console(stderr=True)
|
|
33
|
+
rich.traceback.install(console=stderr, width=200, word_wrap=True, extra_lines=1)
|
|
34
|
+
click.rich_click.USE_RICH_MARKUP = True
|
|
35
|
+
click.rich_click.OPTION_GROUPS = {
|
|
36
|
+
"bactopia-scaffold": [
|
|
37
|
+
{"name": "Commands", "options": []},
|
|
38
|
+
{
|
|
39
|
+
"name": "Additional Options",
|
|
40
|
+
"options": ["--verbose", "--silent", "--version", "--help"],
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
"bactopia-scaffold lookup": [
|
|
44
|
+
{"name": "Required Options", "options": ["--bactopia-path"]},
|
|
45
|
+
{
|
|
46
|
+
"name": "Query Options",
|
|
47
|
+
"options": ["--channel", "--max-retry"],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "Output Options",
|
|
51
|
+
"options": ["--json", "--pretty"],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "Additional Options",
|
|
55
|
+
"options": ["--verbose", "--silent", "--help"],
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
"bactopia-scaffold test-data": [
|
|
59
|
+
{"name": "Required Options", "options": ["--input-type", "--bactopia-path"]},
|
|
60
|
+
{
|
|
61
|
+
"name": "Output Options",
|
|
62
|
+
"options": ["--json", "--pretty"],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "Additional Options",
|
|
66
|
+
"options": ["--verbose", "--silent", "--help"],
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _setup_logging(verbose: bool, silent: bool) -> None:
|
|
73
|
+
logging.basicConfig(
|
|
74
|
+
format="%(asctime)s:%(name)s:%(levelname)s - %(message)s",
|
|
75
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
76
|
+
handlers=[
|
|
77
|
+
RichHandler(rich_tracebacks=True, console=rich.console.Console(stderr=True))
|
|
78
|
+
],
|
|
79
|
+
)
|
|
80
|
+
logging.getLogger().setLevel(
|
|
81
|
+
logging.ERROR if silent else logging.DEBUG if verbose else logging.INFO
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@click.group()
|
|
86
|
+
@click.version_option(bactopia.__version__, "--version")
|
|
87
|
+
def scaffold():
|
|
88
|
+
"""Scaffold Bactopia components from bioconda/conda-forge packages."""
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@scaffold.command()
|
|
93
|
+
@click.argument("package")
|
|
94
|
+
@click.option(
|
|
95
|
+
"--bactopia-path",
|
|
96
|
+
required=True,
|
|
97
|
+
help="Directory where Bactopia repository is stored.",
|
|
98
|
+
)
|
|
99
|
+
@click.option(
|
|
100
|
+
"--channel",
|
|
101
|
+
default=None,
|
|
102
|
+
help="Force a specific channel (bioconda or conda-forge). Default: try bioconda first, then conda-forge.",
|
|
103
|
+
)
|
|
104
|
+
@click.option(
|
|
105
|
+
"--max-retry",
|
|
106
|
+
default=3,
|
|
107
|
+
help="Maximum times to attempt API queries. (Default: 3)",
|
|
108
|
+
)
|
|
109
|
+
@click.option("--json", "output_json", is_flag=True, help="Output flat JSON.")
|
|
110
|
+
@click.option("--pretty", is_flag=True, help="Output pretty-printed JSON.")
|
|
111
|
+
@click.option("--verbose", is_flag=True, help="Print debug related text.")
|
|
112
|
+
@click.option("--silent", is_flag=True, help="Only critical errors will be printed.")
|
|
113
|
+
def lookup(
|
|
114
|
+
package, bactopia_path, channel, max_retry, output_json, pretty, verbose, silent
|
|
115
|
+
):
|
|
116
|
+
"""Look up package info from Anaconda and check for existing components."""
|
|
117
|
+
_setup_logging(verbose, silent)
|
|
118
|
+
|
|
119
|
+
bactopia_path = str(Path(bactopia_path).absolute())
|
|
120
|
+
logging.debug(f"Using bactopia path: {bactopia_path}")
|
|
121
|
+
|
|
122
|
+
if channel:
|
|
123
|
+
from bactopia.conda import get_latest_info
|
|
124
|
+
|
|
125
|
+
info = get_latest_info(package, max_retry=max_retry, channel=channel)
|
|
126
|
+
if info:
|
|
127
|
+
info["channel"] = channel
|
|
128
|
+
else:
|
|
129
|
+
info = get_latest_info_with_fallback(package, max_retry=max_retry)
|
|
130
|
+
|
|
131
|
+
if info is None:
|
|
132
|
+
logging.error(f"Package '{package}' not found on bioconda or conda-forge.")
|
|
133
|
+
sys.exit(1)
|
|
134
|
+
|
|
135
|
+
refs = construct_container_refs(package, info["version"], info["build"])
|
|
136
|
+
existing = check_component_exists(bactopia_path, package)
|
|
137
|
+
|
|
138
|
+
result = {
|
|
139
|
+
"package": package,
|
|
140
|
+
"channel": info.get("channel", "bioconda"),
|
|
141
|
+
"version": info["version"],
|
|
142
|
+
"build": info["build"],
|
|
143
|
+
"summary": info.get("summary", ""),
|
|
144
|
+
"home": info.get("home", ""),
|
|
145
|
+
"container_refs": refs,
|
|
146
|
+
"existing_components": existing,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if output_json:
|
|
150
|
+
print(json.dumps(result))
|
|
151
|
+
elif pretty:
|
|
152
|
+
print(json.dumps(result, indent=2))
|
|
153
|
+
else:
|
|
154
|
+
print(f"Package: {result['package']}")
|
|
155
|
+
print(f"Channel: {result['channel']}")
|
|
156
|
+
print(f"Version: {result['version']}")
|
|
157
|
+
print(f"Build: {result['build']}")
|
|
158
|
+
print(f"Summary: {result['summary']}")
|
|
159
|
+
print(f"Home: {result['home']}")
|
|
160
|
+
print()
|
|
161
|
+
print("Container References:")
|
|
162
|
+
print(f" toolName: {refs['toolName']}")
|
|
163
|
+
print(f" docker: {refs['docker']}")
|
|
164
|
+
print(f" image: {refs['image']}")
|
|
165
|
+
print()
|
|
166
|
+
print("Existing Components:")
|
|
167
|
+
for component, exists in existing.items():
|
|
168
|
+
status = "EXISTS" if exists else "not found"
|
|
169
|
+
print(f" {component}: {status}")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@scaffold.command("test-data")
|
|
173
|
+
@click.option(
|
|
174
|
+
"--input-type",
|
|
175
|
+
required=True,
|
|
176
|
+
type=click.Choice(sorted(FIELD_PATTERNS.keys())),
|
|
177
|
+
help="Input type to search for in existing tests.",
|
|
178
|
+
)
|
|
179
|
+
@click.option(
|
|
180
|
+
"--bactopia-path",
|
|
181
|
+
required=True,
|
|
182
|
+
help="Directory where Bactopia repository is stored.",
|
|
183
|
+
)
|
|
184
|
+
@click.option("--json", "output_json", is_flag=True, help="Output flat JSON.")
|
|
185
|
+
@click.option("--pretty", is_flag=True, help="Output pretty-printed JSON.")
|
|
186
|
+
@click.option("--verbose", is_flag=True, help="Print debug related text.")
|
|
187
|
+
@click.option("--silent", is_flag=True, help="Only critical errors will be printed.")
|
|
188
|
+
def test_data(input_type, bactopia_path, output_json, pretty, verbose, silent):
|
|
189
|
+
"""Discover test data paths from existing module tests."""
|
|
190
|
+
_setup_logging(verbose, silent)
|
|
191
|
+
bactopia_path = Path(bactopia_path).absolute()
|
|
192
|
+
|
|
193
|
+
results = discover_test_data(bactopia_path, input_type)
|
|
194
|
+
|
|
195
|
+
output = {"input_type": input_type, "test_data": results}
|
|
196
|
+
|
|
197
|
+
if output_json:
|
|
198
|
+
print(json.dumps(output))
|
|
199
|
+
elif pretty:
|
|
200
|
+
print(json.dumps(output, indent=2))
|
|
201
|
+
else:
|
|
202
|
+
print(f"Input type: {input_type}")
|
|
203
|
+
print(f"Found {len(results)} species/accession combinations:\n")
|
|
204
|
+
for entry in results:
|
|
205
|
+
print(f" {entry['species']}/{entry['accession']}")
|
|
206
|
+
print(f" Used by: {', '.join(entry['modules_using'])}")
|
|
207
|
+
print(f" compressed: {'yes' if entry['has_compressed'] else 'no'}")
|
|
208
|
+
print(f" uncompressed: {'yes' if entry['has_uncompressed'] else 'no'}")
|
|
209
|
+
print(f" test_data_path: {entry['test_data_path']}")
|
|
210
|
+
print(f" test_uncompressed_path: {entry['test_uncompressed_path']}")
|
|
211
|
+
if entry["datasets"]:
|
|
212
|
+
print(f" datasets: {', '.join(entry['datasets'])}")
|
|
213
|
+
print()
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _run_generate(
|
|
217
|
+
config_path,
|
|
218
|
+
bactopia_path,
|
|
219
|
+
dry_run,
|
|
220
|
+
output_json,
|
|
221
|
+
pretty,
|
|
222
|
+
verbose,
|
|
223
|
+
silent,
|
|
224
|
+
tier,
|
|
225
|
+
render_fn,
|
|
226
|
+
):
|
|
227
|
+
"""Shared logic for module/subworkflow/tool subcommands."""
|
|
228
|
+
_setup_logging(verbose, silent)
|
|
229
|
+
bactopia_path = Path(bactopia_path).absolute()
|
|
230
|
+
|
|
231
|
+
with open(config_path) as f:
|
|
232
|
+
config = json.load(f)
|
|
233
|
+
|
|
234
|
+
errors = validate_config(config, tier)
|
|
235
|
+
if errors:
|
|
236
|
+
for err in errors:
|
|
237
|
+
logging.error(err)
|
|
238
|
+
sys.exit(1)
|
|
239
|
+
|
|
240
|
+
# Inject container_refs if not already present
|
|
241
|
+
if "container_refs" not in config:
|
|
242
|
+
config["container_refs"] = construct_container_refs(
|
|
243
|
+
config["package"], config["version"], config.get("build")
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
files = render_fn(config, bactopia_path)
|
|
247
|
+
created = write_files(files, bactopia_path, dry_run=dry_run)
|
|
248
|
+
|
|
249
|
+
result = {"tier": tier, "created_files": created, "dry_run": dry_run}
|
|
250
|
+
if output_json:
|
|
251
|
+
print(json.dumps(result))
|
|
252
|
+
elif pretty:
|
|
253
|
+
print(json.dumps(result, indent=2))
|
|
254
|
+
else:
|
|
255
|
+
action = "Would create" if dry_run else "Created"
|
|
256
|
+
print(f"{action} {len(created)} files:")
|
|
257
|
+
for path in created:
|
|
258
|
+
print(f" {path}")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@scaffold.command()
|
|
262
|
+
@click.option(
|
|
263
|
+
"--config",
|
|
264
|
+
"config_path",
|
|
265
|
+
required=True,
|
|
266
|
+
type=click.Path(exists=True),
|
|
267
|
+
help="JSON design config file.",
|
|
268
|
+
)
|
|
269
|
+
@click.option(
|
|
270
|
+
"--bactopia-path",
|
|
271
|
+
required=True,
|
|
272
|
+
help="Directory where Bactopia repository is stored.",
|
|
273
|
+
)
|
|
274
|
+
@click.option(
|
|
275
|
+
"--dry-run", is_flag=True, help="Show what would be created without writing files."
|
|
276
|
+
)
|
|
277
|
+
@click.option("--json", "output_json", is_flag=True, help="Output flat JSON.")
|
|
278
|
+
@click.option("--pretty", is_flag=True, help="Output pretty-printed JSON.")
|
|
279
|
+
@click.option("--verbose", is_flag=True, help="Print debug related text.")
|
|
280
|
+
@click.option("--silent", is_flag=True, help="Only critical errors will be printed.")
|
|
281
|
+
def module(config_path, bactopia_path, dry_run, output_json, pretty, verbose, silent):
|
|
282
|
+
"""Generate module files from a design config."""
|
|
283
|
+
_run_generate(
|
|
284
|
+
config_path,
|
|
285
|
+
bactopia_path,
|
|
286
|
+
dry_run,
|
|
287
|
+
output_json,
|
|
288
|
+
pretty,
|
|
289
|
+
verbose,
|
|
290
|
+
silent,
|
|
291
|
+
"module",
|
|
292
|
+
render_module_files,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@scaffold.command()
|
|
297
|
+
@click.option(
|
|
298
|
+
"--config",
|
|
299
|
+
"config_path",
|
|
300
|
+
required=True,
|
|
301
|
+
type=click.Path(exists=True),
|
|
302
|
+
help="JSON design config file.",
|
|
303
|
+
)
|
|
304
|
+
@click.option(
|
|
305
|
+
"--bactopia-path",
|
|
306
|
+
required=True,
|
|
307
|
+
help="Directory where Bactopia repository is stored.",
|
|
308
|
+
)
|
|
309
|
+
@click.option(
|
|
310
|
+
"--dry-run", is_flag=True, help="Show what would be created without writing files."
|
|
311
|
+
)
|
|
312
|
+
@click.option("--json", "output_json", is_flag=True, help="Output flat JSON.")
|
|
313
|
+
@click.option("--pretty", is_flag=True, help="Output pretty-printed JSON.")
|
|
314
|
+
@click.option("--verbose", is_flag=True, help="Print debug related text.")
|
|
315
|
+
@click.option("--silent", is_flag=True, help="Only critical errors will be printed.")
|
|
316
|
+
def subworkflow(
|
|
317
|
+
config_path, bactopia_path, dry_run, output_json, pretty, verbose, silent
|
|
318
|
+
):
|
|
319
|
+
"""Generate subworkflow files from a design config."""
|
|
320
|
+
_run_generate(
|
|
321
|
+
config_path,
|
|
322
|
+
bactopia_path,
|
|
323
|
+
dry_run,
|
|
324
|
+
output_json,
|
|
325
|
+
pretty,
|
|
326
|
+
verbose,
|
|
327
|
+
silent,
|
|
328
|
+
"subworkflow",
|
|
329
|
+
render_subworkflow_files,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@scaffold.command()
|
|
334
|
+
@click.option(
|
|
335
|
+
"--config",
|
|
336
|
+
"config_path",
|
|
337
|
+
required=True,
|
|
338
|
+
type=click.Path(exists=True),
|
|
339
|
+
help="JSON design config file.",
|
|
340
|
+
)
|
|
341
|
+
@click.option(
|
|
342
|
+
"--bactopia-path",
|
|
343
|
+
required=True,
|
|
344
|
+
help="Directory where Bactopia repository is stored.",
|
|
345
|
+
)
|
|
346
|
+
@click.option(
|
|
347
|
+
"--dry-run", is_flag=True, help="Show what would be created without writing files."
|
|
348
|
+
)
|
|
349
|
+
@click.option("--json", "output_json", is_flag=True, help="Output flat JSON.")
|
|
350
|
+
@click.option("--pretty", is_flag=True, help="Output pretty-printed JSON.")
|
|
351
|
+
@click.option("--verbose", is_flag=True, help="Print debug related text.")
|
|
352
|
+
@click.option("--silent", is_flag=True, help="Only critical errors will be printed.")
|
|
353
|
+
def tool(config_path, bactopia_path, dry_run, output_json, pretty, verbose, silent):
|
|
354
|
+
"""Generate all three tiers (module + subworkflow + workflow) for a bactopia-tool."""
|
|
355
|
+
_run_generate(
|
|
356
|
+
config_path,
|
|
357
|
+
bactopia_path,
|
|
358
|
+
dry_run,
|
|
359
|
+
output_json,
|
|
360
|
+
pretty,
|
|
361
|
+
verbose,
|
|
362
|
+
silent,
|
|
363
|
+
"tool",
|
|
364
|
+
render_all_files,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def main():
|
|
369
|
+
if len(sys.argv) == 1:
|
|
370
|
+
scaffold.main(["--help"])
|
|
371
|
+
else:
|
|
372
|
+
scaffold()
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
if __name__ == "__main__":
|
|
376
|
+
main()
|
|
@@ -13,7 +13,7 @@ stderr = rich.console.Console(stderr=True)
|
|
|
13
13
|
rich.traceback.install(console=stderr, width=200, word_wrap=True, extra_lines=1)
|
|
14
14
|
click.rich_click.USE_RICH_MARKUP = True
|
|
15
15
|
|
|
16
|
-
MEM_CAP =
|
|
16
|
+
MEM_CAP = 144
|
|
17
17
|
MEM_FLOOR = 4
|
|
18
18
|
CPU_CAP = 12
|
|
19
19
|
|
|
@@ -70,9 +70,6 @@ def sysinfo(ctx, args):
|
|
|
70
70
|
- both `--max_memory` and `--max_cpus` are already set by the user
|
|
71
71
|
- the invocation is informational (`--help`, `--help_all`, `--list_wfs`)
|
|
72
72
|
"""
|
|
73
|
-
if not args or args == ("--help",) or args == ("-h",):
|
|
74
|
-
click.echo(ctx.get_help())
|
|
75
|
-
return
|
|
76
73
|
if args == ("--version",) or args == ("-V",):
|
|
77
74
|
click.echo(f"bactopia-sysinfo {bactopia.__version__}")
|
|
78
75
|
return
|
|
@@ -94,7 +91,8 @@ def sysinfo(ctx, args):
|
|
|
94
91
|
total_gb = psutil.virtual_memory().total // (1024**3)
|
|
95
92
|
mem = min(total_gb - 1, MEM_CAP)
|
|
96
93
|
if mem >= MEM_FLOOR:
|
|
97
|
-
|
|
94
|
+
if mem != MEM_CAP:
|
|
95
|
+
additions.append(f"--max_memory {mem}.GB")
|
|
98
96
|
else:
|
|
99
97
|
click.echo(
|
|
100
98
|
f"[bactopia-sysinfo] detected only {total_gb} GB RAM "
|
|
@@ -104,12 +102,14 @@ def sysinfo(ctx, args):
|
|
|
104
102
|
|
|
105
103
|
if not _has_flag(args, "--max_cpus"):
|
|
106
104
|
cpus = min(psutil.cpu_count(logical=True) or 1, CPU_CAP)
|
|
107
|
-
|
|
105
|
+
if cpus != CPU_CAP:
|
|
106
|
+
additions.append(f"--max_cpus {cpus}")
|
|
108
107
|
|
|
109
108
|
if additions:
|
|
110
109
|
line = " ".join(additions)
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
if line:
|
|
111
|
+
click.echo(line)
|
|
112
|
+
click.echo(f"[bactopia-sysinfo] auto-detected: {line}", err=True)
|
|
113
113
|
|
|
114
114
|
|
|
115
115
|
def main():
|
|
@@ -4,7 +4,6 @@ import sys
|
|
|
4
4
|
import time
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
-
import requests
|
|
8
7
|
import rich
|
|
9
8
|
import rich.console
|
|
10
9
|
import rich.traceback
|
|
@@ -12,6 +11,7 @@ import rich_click as click
|
|
|
12
11
|
from rich.logging import RichHandler
|
|
13
12
|
|
|
14
13
|
import bactopia
|
|
14
|
+
from bactopia.conda import construct_container_refs, get_latest_info
|
|
15
15
|
from bactopia.nf import parse_all_conda_tools
|
|
16
16
|
|
|
17
17
|
# Set up Rich
|
|
@@ -47,48 +47,6 @@ click.rich_click.OPTION_GROUPS = {
|
|
|
47
47
|
},
|
|
48
48
|
]
|
|
49
49
|
}
|
|
50
|
-
ANACONDA_API = "https://api.anaconda.org/package/bioconda"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def get_latest_info(tool: str, max_retry: int) -> dict | None:
|
|
54
|
-
"""Query Anaconda API for the latest version and build of a bioconda tool.
|
|
55
|
-
|
|
56
|
-
Args:
|
|
57
|
-
tool: The bioconda package name (e.g. "bakta").
|
|
58
|
-
max_retry: Maximum number of query attempts.
|
|
59
|
-
|
|
60
|
-
Returns:
|
|
61
|
-
Dict with 'version' and 'build' keys, or None on failure.
|
|
62
|
-
"""
|
|
63
|
-
attempt = 1
|
|
64
|
-
url = f"{ANACONDA_API}/{tool}"
|
|
65
|
-
while attempt <= max_retry:
|
|
66
|
-
logging.debug(f"Querying {url} (attempt {attempt} of {max_retry})")
|
|
67
|
-
r = requests.get(url)
|
|
68
|
-
if r.status_code == requests.codes.ok:
|
|
69
|
-
data = r.json()
|
|
70
|
-
version = data.get("latest_version") or data.get("versions", [None])[-1]
|
|
71
|
-
|
|
72
|
-
# Find the latest linux-64 build string from the files array
|
|
73
|
-
# Bioconda publishes separate builds per platform (linux-64,
|
|
74
|
-
# osx-64, linux-aarch64, etc.) and Bactopia only targets linux-64.
|
|
75
|
-
build = None
|
|
76
|
-
if "files" in data:
|
|
77
|
-
for f in reversed(data["files"]):
|
|
78
|
-
attrs = f.get("attrs", {})
|
|
79
|
-
if (
|
|
80
|
-
f.get("version") == version
|
|
81
|
-
and attrs.get("subdir") == "linux-64"
|
|
82
|
-
):
|
|
83
|
-
build = attrs.get("build")
|
|
84
|
-
break
|
|
85
|
-
|
|
86
|
-
return {"version": version, "build": build}
|
|
87
|
-
else:
|
|
88
|
-
attempt += 1
|
|
89
|
-
time.sleep(5)
|
|
90
|
-
logging.warning(f"Unable to query {url} after {max_retry} attempts.")
|
|
91
|
-
return None
|
|
92
50
|
|
|
93
51
|
|
|
94
52
|
@click.command(
|
|
@@ -243,14 +201,12 @@ def update(
|
|
|
243
201
|
}
|
|
244
202
|
|
|
245
203
|
if latest and latest["build"]:
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
entry["latest_toolName"] = f"bioconda::{tool_name}={version}"
|
|
249
|
-
entry["latest_docker"] = f"biocontainers/{tool_name}:{version}--{build}"
|
|
250
|
-
entry["latest_image"] = (
|
|
251
|
-
f"https://depot.galaxyproject.org/singularity/"
|
|
252
|
-
f"{tool_name}:{version}--{build}"
|
|
204
|
+
refs = construct_container_refs(
|
|
205
|
+
tool_name, latest["version"], latest["build"]
|
|
253
206
|
)
|
|
207
|
+
entry["latest_toolName"] = refs["toolName"]
|
|
208
|
+
entry["latest_docker"] = refs["docker"]
|
|
209
|
+
entry["latest_image"] = refs["image"]
|
|
254
210
|
|
|
255
211
|
results.append(entry)
|
|
256
212
|
|
|
@@ -102,7 +102,7 @@ def download(
|
|
|
102
102
|
workflows = catalog["workflows"]
|
|
103
103
|
else:
|
|
104
104
|
logging.error(
|
|
105
|
-
f"'catalog.json' could not be found in {bactopia_path}
|
|
105
|
+
f"'catalog.json' could not be found in {bactopia_path}, is this a valid Bactopia installation?"
|
|
106
106
|
)
|
|
107
107
|
sys.exit(1)
|
|
108
108
|
|