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.
Files changed (108) hide show
  1. {bactopia-2.1.0 → bactopia-2.1.2}/PKG-INFO +1 -1
  2. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/catalog.py +12 -9
  3. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/lint.py +20 -2
  4. bactopia-2.1.2/bactopia/cli/scaffold.py +376 -0
  5. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/sysinfo.py +8 -8
  6. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/update.py +6 -50
  7. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/workflows.py +1 -1
  8. bactopia-2.1.2/bactopia/conda.py +137 -0
  9. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/lint/rules/subworkflow_rules.py +94 -1
  10. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/lint/rules/workflow_rules.py +28 -1
  11. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/lint/runner.py +20 -2
  12. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/nf.py +44 -5
  13. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/parsables.py +2 -2
  14. bactopia-2.1.2/bactopia/scaffold.py +502 -0
  15. bactopia-2.1.2/bactopia/templates/scaffold/module/main.nf.j2 +131 -0
  16. bactopia-2.1.2/bactopia/templates/scaffold/module/module.config.j2 +35 -0
  17. bactopia-2.1.2/bactopia/templates/scaffold/module/schema.json.j2 +31 -0
  18. bactopia-2.1.2/bactopia/templates/scaffold/module/tests/main.nf.test.j2 +44 -0
  19. bactopia-2.1.2/bactopia/templates/scaffold/module/tests/nextflow.config.j2 +36 -0
  20. bactopia-2.1.2/bactopia/templates/scaffold/module/tests/nf-test.config.j2 +11 -0
  21. bactopia-2.1.2/bactopia/templates/scaffold/subworkflow/main.nf.j2 +96 -0
  22. bactopia-2.1.2/bactopia/templates/scaffold/subworkflow/tests/main.nf.test.j2 +60 -0
  23. bactopia-2.1.2/bactopia/templates/scaffold/subworkflow/tests/nextflow.config.j2 +42 -0
  24. bactopia-2.1.2/bactopia/templates/scaffold/subworkflow/tests/nf-test.config.j2 +11 -0
  25. bactopia-2.1.2/bactopia/templates/scaffold/subworkflow/tests/nftignore.j2 +2 -0
  26. bactopia-2.1.2/bactopia/templates/scaffold/workflow/main.nf.j2 +94 -0
  27. bactopia-2.1.2/bactopia/templates/scaffold/workflow/nextflow.config.j2 +93 -0
  28. bactopia-2.1.2/bactopia/templates/scaffold/workflow/tests/main.nf.test.j2 +43 -0
  29. bactopia-2.1.2/bactopia/templates/scaffold/workflow/tests/nf-test.config.j2 +11 -0
  30. bactopia-2.1.2/bactopia/templates/scaffold/workflow/tests/nftignore.j2 +3 -0
  31. {bactopia-2.1.0 → bactopia-2.1.2}/pyproject.toml +3 -2
  32. {bactopia-2.1.0 → bactopia-2.1.2}/LICENSE +0 -0
  33. {bactopia-2.1.0 → bactopia-2.1.2}/README.md +0 -0
  34. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/__init__.py +0 -0
  35. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/atb.py +0 -0
  36. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/__init__.py +0 -0
  37. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/atb/__init__.py +0 -0
  38. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/atb/atb_downloader.py +0 -0
  39. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/atb/atb_formatter.py +0 -0
  40. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/citations.py +0 -0
  41. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/datasets.py +0 -0
  42. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/docs.py +0 -0
  43. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/download.py +0 -0
  44. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/helpers/__init__.py +0 -0
  45. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/helpers/merge_schemas.py +0 -0
  46. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/jsonify.py +0 -0
  47. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/pipeline/__init__.py +0 -0
  48. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/pipeline/bracken_to_excel.py +0 -0
  49. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/pipeline/check_assembly_accession.py +0 -0
  50. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/pipeline/check_fastqs.py +0 -0
  51. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/pipeline/cleanup_coverage.py +0 -0
  52. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/pipeline/kraken_bracken_summary.py +0 -0
  53. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/pipeline/mask_consensus.py +0 -0
  54. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/pipeline/scrubber_summary.py +0 -0
  55. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/pipeline/teton_prepare.py +0 -0
  56. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/prepare.py +0 -0
  57. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/prune.py +0 -0
  58. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/pubmlst/build.py +0 -0
  59. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/pubmlst/setup.py +0 -0
  60. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/review.py +0 -0
  61. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/search.py +0 -0
  62. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/status.py +0 -0
  63. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/summary.py +0 -0
  64. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/cli/testing.py +0 -0
  65. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/databases/__init__.py +0 -0
  66. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/databases/ena.py +0 -0
  67. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/databases/ncbi.py +0 -0
  68. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/databases/pubmlst/__init__.py +0 -0
  69. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/databases/pubmlst/constants.py +0 -0
  70. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/databases/pubmlst/utils.py +0 -0
  71. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/lint/__init__.py +0 -0
  72. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/lint/citations.py +0 -0
  73. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/lint/docs.py +0 -0
  74. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/lint/models.py +0 -0
  75. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/lint/rules/__init__.py +0 -0
  76. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/lint/rules/module_rules.py +0 -0
  77. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/outputs.py +0 -0
  78. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parse.py +0 -0
  79. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/__init__.py +0 -0
  80. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/amrfinderplus.py +0 -0
  81. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/annotator.py +0 -0
  82. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/ariba.py +0 -0
  83. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/assembler.py +0 -0
  84. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/blast.py +0 -0
  85. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/citations.py +0 -0
  86. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/coverage.py +0 -0
  87. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/error.py +0 -0
  88. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/gather.py +0 -0
  89. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/generic.py +0 -0
  90. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/kraken.py +0 -0
  91. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/mapping.py +0 -0
  92. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/mlst.py +0 -0
  93. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/nextflow.py +0 -0
  94. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/qc.py +0 -0
  95. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/sketcher.py +0 -0
  96. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/variants.py +0 -0
  97. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/versions.py +0 -0
  98. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/parsers/workflows.py +0 -0
  99. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/reports/__init__.py +0 -0
  100. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/reports/templates/__init__.py +0 -0
  101. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/summary.py +0 -0
  102. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/templates/__init__.py +0 -0
  103. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/templates/bactopia/llms.txt.j2 +0 -0
  104. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/templates/logos.py +0 -0
  105. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/templates/nextflow/nextflow.config.j2 +0 -0
  106. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/templates/nextflow/params.config.j2 +0 -0
  107. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/templates/nextflow/process.config.j2 +0 -0
  108. {bactopia-2.1.0 → bactopia-2.1.2}/bactopia/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bactopia
3
- Version: 2.1.0
3
+ Version: 2.1.2
4
4
  Summary: A Python package for working with Bactopia
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -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(channels: list[str]) -> str:
86
- """Infer subworkflow scope from emit channel names."""
87
- has_sample = "sample_outputs" in channels
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 has_run:
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
- name, version = pkg.rsplit("=", 1)
141
- return {"name": name, "version": version}
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(channel_names)
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 = 32
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
- additions.append(f"--max_memory {mem}.GB")
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
- additions.append(f"--max_cpus {cpus}")
105
+ if cpus != CPU_CAP:
106
+ additions.append(f"--max_cpus {cpus}")
108
107
 
109
108
  if additions:
110
109
  line = " ".join(additions)
111
- click.echo(line)
112
- click.echo(f"[bactopia-sysinfo] auto-detected: {line}", err=True)
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
- version = latest["version"]
247
- build = latest["build"]
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}/data/, is this a valid Bactopia installation?"
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