bactopia 2.0.2__tar.gz → 2.1.0__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 (90) hide show
  1. {bactopia-2.0.2 → bactopia-2.1.0}/PKG-INFO +2 -1
  2. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/catalog.py +65 -1
  3. bactopia-2.1.0/bactopia/cli/citations.py +190 -0
  4. bactopia-2.1.0/bactopia/cli/docs.py +208 -0
  5. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/helpers/merge_schemas.py +1 -1
  6. bactopia-2.1.0/bactopia/cli/sysinfo.py +120 -0
  7. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/workflows.py +1 -1
  8. bactopia-2.1.0/bactopia/lint/citations.py +424 -0
  9. bactopia-2.1.0/bactopia/lint/docs.py +751 -0
  10. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/lint/rules/module_rules.py +9 -3
  11. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/nf.py +111 -23
  12. bactopia-2.1.0/bactopia/templates/bactopia/llms.txt.j2 +84 -0
  13. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/templates/nextflow/nextflow.config.j2 +1 -1
  14. {bactopia-2.0.2 → bactopia-2.1.0}/pyproject.toml +4 -1
  15. bactopia-2.0.2/bactopia/cli/citations.py +0 -70
  16. {bactopia-2.0.2 → bactopia-2.1.0}/LICENSE +0 -0
  17. {bactopia-2.0.2 → bactopia-2.1.0}/README.md +0 -0
  18. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/__init__.py +0 -0
  19. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/atb.py +0 -0
  20. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/__init__.py +0 -0
  21. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/atb/__init__.py +0 -0
  22. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/atb/atb_downloader.py +0 -0
  23. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/atb/atb_formatter.py +0 -0
  24. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/datasets.py +0 -0
  25. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/download.py +0 -0
  26. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/helpers/__init__.py +0 -0
  27. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/jsonify.py +0 -0
  28. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/lint.py +0 -0
  29. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/pipeline/__init__.py +0 -0
  30. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/pipeline/bracken_to_excel.py +0 -0
  31. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/pipeline/check_assembly_accession.py +0 -0
  32. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/pipeline/check_fastqs.py +0 -0
  33. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/pipeline/cleanup_coverage.py +0 -0
  34. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/pipeline/kraken_bracken_summary.py +0 -0
  35. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/pipeline/mask_consensus.py +0 -0
  36. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/pipeline/scrubber_summary.py +0 -0
  37. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/pipeline/teton_prepare.py +0 -0
  38. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/prepare.py +0 -0
  39. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/prune.py +0 -0
  40. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/pubmlst/build.py +0 -0
  41. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/pubmlst/setup.py +0 -0
  42. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/review.py +0 -0
  43. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/search.py +0 -0
  44. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/status.py +0 -0
  45. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/summary.py +0 -0
  46. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/testing.py +0 -0
  47. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/update.py +0 -0
  48. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/databases/__init__.py +0 -0
  49. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/databases/ena.py +0 -0
  50. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/databases/ncbi.py +0 -0
  51. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/databases/pubmlst/__init__.py +0 -0
  52. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/databases/pubmlst/constants.py +0 -0
  53. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/databases/pubmlst/utils.py +0 -0
  54. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/lint/__init__.py +0 -0
  55. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/lint/models.py +0 -0
  56. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/lint/rules/__init__.py +0 -0
  57. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/lint/rules/subworkflow_rules.py +0 -0
  58. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/lint/rules/workflow_rules.py +0 -0
  59. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/lint/runner.py +0 -0
  60. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/outputs.py +0 -0
  61. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parse.py +0 -0
  62. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/__init__.py +0 -0
  63. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/amrfinderplus.py +0 -0
  64. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/annotator.py +0 -0
  65. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/ariba.py +0 -0
  66. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/assembler.py +0 -0
  67. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/blast.py +0 -0
  68. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/citations.py +0 -0
  69. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/coverage.py +0 -0
  70. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/error.py +0 -0
  71. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/gather.py +0 -0
  72. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/generic.py +0 -0
  73. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/kraken.py +0 -0
  74. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/mapping.py +0 -0
  75. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/mlst.py +0 -0
  76. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/nextflow.py +0 -0
  77. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/parsables.py +0 -0
  78. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/qc.py +0 -0
  79. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/sketcher.py +0 -0
  80. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/variants.py +0 -0
  81. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/versions.py +0 -0
  82. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/workflows.py +0 -0
  83. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/reports/__init__.py +0 -0
  84. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/reports/templates/__init__.py +0 -0
  85. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/summary.py +0 -0
  86. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/templates/__init__.py +0 -0
  87. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/templates/logos.py +0 -0
  88. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/templates/nextflow/params.config.j2 +0 -0
  89. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/templates/nextflow/process.config.j2 +0 -0
  90. {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bactopia
3
- Version: 2.0.2
3
+ Version: 2.1.0
4
4
  Summary: A Python package for working with Bactopia
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -19,6 +19,7 @@ Requires-Dist: biopython (>=1.80)
19
19
  Requires-Dist: jinja2 (>=3.1.6)
20
20
  Requires-Dist: openpyxl (>=3.1.0)
21
21
  Requires-Dist: pandas (>=2.2.0)
22
+ Requires-Dist: psutil (>=5.9.0)
22
23
  Requires-Dist: pyyaml (>=6.0)
23
24
  Requires-Dist: rauth (>=0.7.3)
24
25
  Requires-Dist: requests (>=2.28.2)
@@ -380,6 +380,29 @@ def generate_catalog(bactopia_path: Path) -> dict:
380
380
  return catalog
381
381
 
382
382
 
383
+ def render_llms_txt(catalog: dict, template_path: Path) -> str:
384
+ """Render llms.txt from a Jinja2 template using catalog data.
385
+
386
+ Args:
387
+ catalog: Catalog dict as returned by generate_catalog().
388
+ template_path: Path to the Jinja2 template file.
389
+
390
+ Returns:
391
+ Rendered llms.txt content as a string.
392
+ """
393
+ from jinja2 import Environment, FileSystemLoader, StrictUndefined
394
+
395
+ env = Environment(
396
+ loader=FileSystemLoader(str(template_path.parent)),
397
+ undefined=StrictUndefined,
398
+ keep_trailing_newline=True,
399
+ trim_blocks=True,
400
+ lstrip_blocks=True,
401
+ )
402
+ template = env.get_template(template_path.name)
403
+ return template.render(catalog=catalog)
404
+
405
+
383
406
  @click.command()
384
407
  @click.version_option(bactopia.__version__, "--version")
385
408
  @click.option(
@@ -395,13 +418,37 @@ def generate_catalog(bactopia_path: Path) -> dict:
395
418
  help="Output path for catalog.json (default: stdout)",
396
419
  )
397
420
  @click.option("--pretty", is_flag=True, help="Pretty-print JSON output.")
421
+ @click.option(
422
+ "--llms-output",
423
+ "llms_output_path",
424
+ type=click.Path(),
425
+ default=None,
426
+ help="Also render llms.txt to this path. Uses the bundled template at bactopia/templates/bactopia/llms.txt.j2 unless --llms-template is provided.",
427
+ )
428
+ @click.option(
429
+ "--llms-template",
430
+ "llms_template_path",
431
+ type=click.Path(exists=True, dir_okay=False),
432
+ default=None,
433
+ help="Jinja2 template for llms.txt. Defaults to the template bundled inside bactopia-py.",
434
+ )
398
435
  @click.option("--verbose", is_flag=True, help="Print debug related text.")
399
- def catalog(bactopia_path, output_path, pretty, verbose):
436
+ def catalog(
437
+ bactopia_path,
438
+ output_path,
439
+ pretty,
440
+ llms_output_path,
441
+ llms_template_path,
442
+ verbose,
443
+ ):
400
444
  """Generate machine-readable catalog of all Bactopia components.
401
445
 
402
446
  Produces catalog.json containing workflows, subworkflows, and modules
403
447
  with their contracts (takes/emits), dependencies, and metadata.
404
448
  Replaces data/workflows.yml as the authoritative component index.
449
+
450
+ Optionally also renders llms.txt from a Jinja2 template when
451
+ --llms-output is provided.
405
452
  """
406
453
  # Setup logs
407
454
  logging.basicConfig(
@@ -445,6 +492,23 @@ def catalog(bactopia_path, output_path, pretty, verbose):
445
492
  else:
446
493
  print(output_json)
447
494
 
495
+ # Optionally render llms.txt
496
+ if llms_output_path:
497
+ if llms_template_path:
498
+ tpl = Path(llms_template_path)
499
+ else:
500
+ # Bundled template ships inside bactopia-py
501
+ tpl = (
502
+ Path(__file__).parent.parent / "templates" / "bactopia" / "llms.txt.j2"
503
+ )
504
+ tpl = tpl.resolve()
505
+ if not tpl.exists():
506
+ logging.error(f"llms.txt template not found: {tpl}")
507
+ sys.exit(1)
508
+ rendered = render_llms_txt(data, tpl)
509
+ Path(llms_output_path).write_text(rendered)
510
+ console.print(f"llms.txt written to {llms_output_path}")
511
+
448
512
 
449
513
  def main():
450
514
  if len(sys.argv) == 1:
@@ -0,0 +1,190 @@
1
+ import json
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ import rich
6
+ import rich.console
7
+ import rich.traceback
8
+ import rich_click as click
9
+ from rich.console import Console
10
+ from rich.markdown import Markdown
11
+ from rich.table import Table
12
+
13
+ import bactopia
14
+ from bactopia.lint.citations import validate_citations
15
+ from bactopia.parsers.citations import parse_citations
16
+ from bactopia.utils import validate_file
17
+
18
+ # Set up Rich
19
+ stderr = rich.console.Console(stderr=True)
20
+ rich.traceback.install(console=stderr, width=200, word_wrap=True, extra_lines=1)
21
+ click.rich_click.USE_RICH_MARKUP = True
22
+
23
+
24
+ def _render_validation_report(report: dict, silent: bool, plain_text: bool) -> None:
25
+ """Pretty-print a validate_citations() report using Rich tables."""
26
+ console = Console(color_system=None if plain_text else "auto")
27
+ orphans = report["orphans"]
28
+ expected_orphans = report.get("expected_orphans", {})
29
+ missing = report["missing_workflow_keys"]
30
+ summary = report["summary"]
31
+
32
+ if summary["orphans_total"] == 0 and summary["missing_total"] == 0:
33
+ if not silent:
34
+ console.print(
35
+ f"[green]All {summary['yml_total']} citations are referenced "
36
+ "and all workflow @citation keys resolve.[/green]"
37
+ )
38
+ if summary.get("expected_orphans_total"):
39
+ _render_expected_orphans(console, expected_orphans)
40
+ return
41
+
42
+ if summary["orphans_total"]:
43
+ table = Table(title="Orphan citation keys", show_header=True)
44
+ table.add_column("Section")
45
+ table.add_column("Keys")
46
+ for section, keys in orphans.items():
47
+ if not keys:
48
+ continue
49
+ table.add_row(section, ", ".join(keys))
50
+ console.print(table)
51
+
52
+ if summary["missing_total"]:
53
+ table = Table(title="Workflow @citation keys not in citations.yml")
54
+ table.add_column("Component")
55
+ table.add_column("File")
56
+ table.add_column("Line")
57
+ table.add_column("Key")
58
+ for item in missing:
59
+ table.add_row(
60
+ item["component"],
61
+ item["file"],
62
+ str(item["line"]) if item["line"] else "?",
63
+ item["key"],
64
+ )
65
+ console.print(table)
66
+
67
+ if summary.get("expected_orphans_total"):
68
+ _render_expected_orphans(console, expected_orphans)
69
+
70
+ console.print(
71
+ f"\nSummary: {summary['orphans_total']} orphan key(s), "
72
+ f"{summary['missing_total']} workflow reference(s) unresolved."
73
+ )
74
+
75
+
76
+ def _render_expected_orphans(console: Console, expected_orphans: dict) -> None:
77
+ """Render the provenance-only orphan bucket as an informational note."""
78
+ flat = sorted(key for keys in expected_orphans.values() for key in keys)
79
+ if not flat:
80
+ return
81
+ console.print(
82
+ f"[dim]Expected orphans (provenance-only, not flagged): {', '.join(flat)}[/dim]"
83
+ )
84
+
85
+
86
+ @click.command()
87
+ @click.version_option(bactopia.__version__, "--version", "-V")
88
+ @click.option(
89
+ "--bactopia-path",
90
+ "-b",
91
+ required=True,
92
+ help="Directory where Bactopia repository is stored",
93
+ )
94
+ @click.option("--name", "-n", help="Only print citation matching a given name")
95
+ @click.option("--plain-text", "-p", is_flag=True, help="Disable rich formatting")
96
+ @click.option(
97
+ "--validate",
98
+ is_flag=True,
99
+ help="Validate citation integrity: orphan keys + workflow @citation references",
100
+ )
101
+ @click.option(
102
+ "--json",
103
+ "as_json",
104
+ is_flag=True,
105
+ help="Emit validation results as JSON (use with --validate)",
106
+ )
107
+ @click.option(
108
+ "--silent",
109
+ is_flag=True,
110
+ help="Suppress non-error output when validation is clean",
111
+ )
112
+ def citations(
113
+ bactopia_path: str,
114
+ name: str,
115
+ plain_text: bool,
116
+ validate: bool,
117
+ as_json: bool,
118
+ silent: bool,
119
+ ) -> None:
120
+ """Print or validate citations used throughout Bactopia.
121
+
122
+ Default mode prints the full citation list (or one entry with --name).
123
+ Pass --validate to instead scan the repo for orphan keys (defined but
124
+ never referenced) and workflow @citation keys that don't resolve to
125
+ an entry in citations.yml. Module and subworkflow @citation keys are
126
+ validated by bactopia-lint (rules M035 and S019).
127
+ """
128
+
129
+ if validate:
130
+ # Validation mode requires the repo root so we can locate
131
+ # data/citations.yml and walk modules/subworkflows/workflows.
132
+ repo_root = Path(bactopia_path)
133
+ if not (repo_root / "data" / "citations.yml").exists():
134
+ raise click.ClickException(
135
+ f"{repo_root}/data/citations.yml not found. "
136
+ "Pass the Bactopia repo root via --bactopia-path."
137
+ )
138
+ report = validate_citations(repo_root)
139
+
140
+ if as_json:
141
+ click.echo(json.dumps(report, indent=2, sort_keys=True))
142
+ else:
143
+ _render_validation_report(report, silent=silent, plain_text=plain_text)
144
+
145
+ has_issues = (
146
+ report["summary"]["orphans_total"] or report["summary"]["missing_total"]
147
+ )
148
+ if has_issues:
149
+ sys.exit(1)
150
+ return
151
+
152
+ # Default (print) mode — unchanged behaviour.
153
+ citations_yml = validate_file(f"{bactopia_path}/citations.yml")
154
+ citations, module_citations = parse_citations(citations_yml)
155
+
156
+ markdown = []
157
+ if name:
158
+ if name.lower() in module_citations:
159
+ markdown.append(f"{module_citations[name.lower()]['name']} ")
160
+ markdown.append(module_citations[name.lower()]["cite"].rstrip())
161
+ else:
162
+ raise KeyError(f'"{name}" does not match available citations')
163
+ else:
164
+ for group, refs in citations.items():
165
+ if group.startswith("datasets"):
166
+ markdown.append(f"# {group.replace('_', ' ').title()}")
167
+ else:
168
+ markdown.append(f"# {group.title()}")
169
+ for ref, vals in refs.items():
170
+ markdown.append(f"{vals['name']} ")
171
+ markdown.append(vals["cite"])
172
+
173
+ md = None
174
+ if plain_text:
175
+ md = "\n".join(markdown)
176
+ else:
177
+ md = Markdown("\n".join(markdown))
178
+ console = Console(color_system=None if plain_text else "auto")
179
+ console.print(md)
180
+
181
+
182
+ def main():
183
+ if len(sys.argv) == 1:
184
+ citations.main(["--help"])
185
+ else:
186
+ citations()
187
+
188
+
189
+ if __name__ == "__main__":
190
+ main()
@@ -0,0 +1,208 @@
1
+ """bactopia-docs CLI: validate reference-doc staleness."""
2
+
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import rich
8
+ import rich.console
9
+ import rich.traceback
10
+ import rich_click as click
11
+ from rich.console import Console
12
+ from rich.table import Table
13
+
14
+ import bactopia
15
+ from bactopia.lint.docs import (
16
+ DEFAULT_DOCS_PATH,
17
+ DEFAULT_PATTERNS_FILE,
18
+ validate_docs,
19
+ )
20
+
21
+ stderr = rich.console.Console(stderr=True)
22
+ rich.traceback.install(console=stderr, width=200, word_wrap=True, extra_lines=1)
23
+ click.rich_click.USE_RICH_MARKUP = True
24
+
25
+
26
+ def _render_report(report: dict, silent: bool, plain_text: bool) -> None:
27
+ """Pretty-print a validate_docs() report using Rich tables."""
28
+ console = Console(color_system=None if plain_text else "auto")
29
+ summary = report["summary"]
30
+ deprecated = report["deprecated_patterns"]
31
+ violations = report["ground_truth_violations"]
32
+
33
+ if summary["fail"] == 0 and summary["warn"] == 0:
34
+ if not silent:
35
+ console.print(
36
+ f"[green]All {summary['files_scanned']} docs clean. "
37
+ f"({summary['patterns_loaded']} deprecated patterns, "
38
+ f"{report['ground_truth']['cli_commands_total']} CLI commands, "
39
+ f"{report['ground_truth']['lint_rule_ids_total']} lint rule IDs "
40
+ f"checked.)[/green]"
41
+ )
42
+ return
43
+
44
+ if deprecated:
45
+ table = Table(title="Deprecated patterns (D0xx)", show_header=True)
46
+ table.add_column("Rule")
47
+ table.add_column("File")
48
+ table.add_column("Line", justify="right")
49
+ table.add_column("Match")
50
+ table.add_column("Hint")
51
+ for hit in deprecated:
52
+ table.add_row(
53
+ hit["rule_id"],
54
+ hit["file"],
55
+ str(hit["line"]),
56
+ hit["match"][:80],
57
+ hit.get("hint", ""),
58
+ )
59
+ console.print(table)
60
+
61
+ if violations:
62
+ table = Table(title="Ground-truth violations (D1xx)", show_header=True)
63
+ table.add_column("Rule")
64
+ table.add_column("File")
65
+ table.add_column("Line", justify="right")
66
+ table.add_column("Detail")
67
+ for hit in violations:
68
+ if "actual" in hit and "claim" in hit:
69
+ detail = f"claim: {hit['claim']} | actual: {hit['actual']}"
70
+ elif "reference" in hit:
71
+ detail = f"reference: {hit['reference']} — {hit.get('hint', '')}"
72
+ else:
73
+ detail = hit.get("hint", "")
74
+ table.add_row(
75
+ hit["rule_id"],
76
+ hit["file"],
77
+ str(hit["line"]),
78
+ detail[:100],
79
+ )
80
+ console.print(table)
81
+
82
+ console.print(
83
+ f"\nSummary: {summary['deprecated_pattern_hits']} deprecated-pattern hit(s), "
84
+ f"{summary['ground_truth_violations']} ground-truth violation(s) "
85
+ f"across {summary['files_scanned']} doc(s)."
86
+ )
87
+
88
+
89
+ @click.command()
90
+ @click.version_option(bactopia.__version__, "--version", "-V")
91
+ @click.option(
92
+ "--bactopia-path",
93
+ "-b",
94
+ required=True,
95
+ help="Directory where Bactopia repository is stored",
96
+ )
97
+ @click.option(
98
+ "--docs-path",
99
+ default=DEFAULT_DOCS_PATH,
100
+ show_default=True,
101
+ help="Docs directory relative to --bactopia-path",
102
+ )
103
+ @click.option(
104
+ "--patterns-file",
105
+ default=DEFAULT_PATTERNS_FILE,
106
+ show_default=True,
107
+ help="Deprecated-patterns YAML relative to --bactopia-path",
108
+ )
109
+ @click.option(
110
+ "--bactopia-py-path",
111
+ default=None,
112
+ type=click.Path(file_okay=False, path_type=Path),
113
+ help="Path to bactopia-py repo (for D105 CLI / D106 lint-rule checks). "
114
+ "Defaults to <bactopia-path>/../bactopia-py.",
115
+ )
116
+ @click.option(
117
+ "--skip-path-check",
118
+ is_flag=True,
119
+ help="Skip D108 markdown-link target resolution",
120
+ )
121
+ @click.option(
122
+ "--validate",
123
+ is_flag=True,
124
+ help="Run validation (default action; flag is accepted for parity with bactopia-citations).",
125
+ )
126
+ @click.option(
127
+ "--json",
128
+ "as_json",
129
+ is_flag=True,
130
+ help="Emit results as JSON",
131
+ )
132
+ @click.option(
133
+ "--silent",
134
+ is_flag=True,
135
+ help="Suppress non-error output when validation is clean",
136
+ )
137
+ @click.option(
138
+ "--plain-text",
139
+ "-p",
140
+ is_flag=True,
141
+ help="Disable rich formatting",
142
+ )
143
+ def docs(
144
+ bactopia_path: str,
145
+ docs_path: str,
146
+ patterns_file: str,
147
+ bactopia_py_path: Path | None,
148
+ skip_path_check: bool,
149
+ validate: bool,
150
+ as_json: bool,
151
+ silent: bool,
152
+ plain_text: bool,
153
+ ) -> None:
154
+ """Validate reference-doc staleness across a Bactopia repo.
155
+
156
+ Two checks run against every .md file under [b]--docs-path[/b]:
157
+
158
+ [b]Deprecated patterns (D0xx)[/b]: regex matches against
159
+ [b]--patterns-file[/b] entries — phrases retired by past migrations
160
+ (e.g. ``flattenPaths``, the 4-channel emission framing).
161
+
162
+ [b]Ground-truth assertions (D1xx)[/b]: counts (D101-D103), Nextflow
163
+ version (D104), bactopia-py CLI references (D105), lint rule IDs
164
+ (D106), markdown link targets (D108).
165
+
166
+ Suppress a rule on a single line with
167
+ ``<!-- bactopia-docs: ignore D0xx -->`` (or a comma-separated list).
168
+
169
+ Exits 1 if any FAIL is found.
170
+ """
171
+ repo_root = Path(bactopia_path)
172
+ if not repo_root.is_dir():
173
+ raise click.ClickException(f"--bactopia-path {repo_root} is not a directory.")
174
+ if not (repo_root / docs_path).is_dir():
175
+ raise click.ClickException(
176
+ f"Docs directory {repo_root / docs_path} not found. "
177
+ "Pass --bactopia-path pointing at the Bactopia repo root."
178
+ )
179
+
180
+ # validate flag is decorative — there's no other mode for now.
181
+ _ = validate
182
+
183
+ report = validate_docs(
184
+ bactopia_path=repo_root,
185
+ docs_path=docs_path,
186
+ patterns_file=patterns_file,
187
+ bactopia_py_path=bactopia_py_path,
188
+ skip_path_check=skip_path_check,
189
+ )
190
+
191
+ if as_json:
192
+ click.echo(json.dumps(report, indent=2, sort_keys=True))
193
+ else:
194
+ _render_report(report, silent=silent, plain_text=plain_text)
195
+
196
+ if report["summary"]["fail"]:
197
+ sys.exit(1)
198
+
199
+
200
+ def main():
201
+ if len(sys.argv) == 1:
202
+ docs.main(["--help"])
203
+ else:
204
+ docs()
205
+
206
+
207
+ if __name__ == "__main__":
208
+ main()
@@ -140,7 +140,7 @@ def merge_schemas(
140
140
  sys.exit(1)
141
141
 
142
142
  # parse the catalog
143
- with open(f"{bactopia_path}/data/catalog.json") as fh:
143
+ with open(f"{bactopia_path}/catalog.json") as fh:
144
144
  catalog = json.load(fh)
145
145
 
146
146
  if wf not in catalog["workflows"]:
@@ -0,0 +1,120 @@
1
+ """Auto-detect host resources and emit Nextflow CLI overrides for local profiles."""
2
+
3
+ import psutil
4
+ import rich
5
+ import rich.console
6
+ import rich.traceback
7
+ import rich_click as click
8
+
9
+ import bactopia
10
+
11
+ # Set up Rich
12
+ stderr = rich.console.Console(stderr=True)
13
+ rich.traceback.install(console=stderr, width=200, word_wrap=True, extra_lines=1)
14
+ click.rich_click.USE_RICH_MARKUP = True
15
+
16
+ MEM_CAP = 32
17
+ MEM_FLOOR = 4
18
+ CPU_CAP = 12
19
+
20
+ LOCAL_PROFILES = frozenset(
21
+ {
22
+ "standard",
23
+ "conda",
24
+ "mamba",
25
+ "docker",
26
+ "arm",
27
+ "apptainer",
28
+ "singularity",
29
+ "podman",
30
+ "charliecloud",
31
+ "shifter",
32
+ "wave",
33
+ }
34
+ )
35
+
36
+ INFORMATIONAL_FLAGS = frozenset({"--help", "-h", "--help_all", "--list_wfs"})
37
+
38
+
39
+ def _flag_value(args, *flag_names):
40
+ """Return the value of the first matching flag in args, or None."""
41
+ for i, arg in enumerate(args):
42
+ for flag in flag_names:
43
+ if arg == flag:
44
+ return args[i + 1] if i + 1 < len(args) else ""
45
+ if arg.startswith(f"{flag}="):
46
+ return arg[len(flag) + 1 :]
47
+ return None
48
+
49
+
50
+ def _has_flag(args, *flag_names):
51
+ """True if any of flag_names appears in args (bare or `flag=value`)."""
52
+ return _flag_value(args, *flag_names) is not None
53
+
54
+
55
+ @click.command(
56
+ context_settings={"ignore_unknown_options": True, "allow_extra_args": True},
57
+ add_help_option=False,
58
+ )
59
+ @click.argument("args", nargs=-1, type=click.UNPROCESSED)
60
+ @click.pass_context
61
+ def sysinfo(ctx, args):
62
+ """Auto-detect host RAM and CPUs, emit Nextflow CLI fragments for local profiles.
63
+
64
+ Reads the bactopia wrapper's argv as a passthrough. Emits to stdout the
65
+ additional `--max_memory <N>.GB` / `--max_cpus <N>` flags that should be
66
+ appended to the `nextflow run` command line. Emits nothing when:
67
+
68
+ - a custom config is supplied (`-c` or `--nfconfig`)
69
+ - any `-profile` value is not in the local-executor allow-list
70
+ - both `--max_memory` and `--max_cpus` are already set by the user
71
+ - the invocation is informational (`--help`, `--help_all`, `--list_wfs`)
72
+ """
73
+ if not args or args == ("--help",) or args == ("-h",):
74
+ click.echo(ctx.get_help())
75
+ return
76
+ if args == ("--version",) or args == ("-V",):
77
+ click.echo(f"bactopia-sysinfo {bactopia.__version__}")
78
+ return
79
+
80
+ if any(a in INFORMATIONAL_FLAGS for a in args):
81
+ return
82
+
83
+ if _has_flag(args, "-c", "--nfconfig"):
84
+ return
85
+
86
+ profile = _flag_value(args, "-profile", "--profile")
87
+ profiles = set(profile.split(",")) if profile else {"standard"}
88
+ if not profiles.issubset(LOCAL_PROFILES):
89
+ return
90
+
91
+ additions = []
92
+
93
+ if not _has_flag(args, "--max_memory"):
94
+ total_gb = psutil.virtual_memory().total // (1024**3)
95
+ mem = min(total_gb - 1, MEM_CAP)
96
+ if mem >= MEM_FLOOR:
97
+ additions.append(f"--max_memory {mem}.GB")
98
+ else:
99
+ click.echo(
100
+ f"[bactopia-sysinfo] detected only {total_gb} GB RAM "
101
+ f"(below floor of {MEM_FLOOR} GB); skipping --max_memory",
102
+ err=True,
103
+ )
104
+
105
+ if not _has_flag(args, "--max_cpus"):
106
+ cpus = min(psutil.cpu_count(logical=True) or 1, CPU_CAP)
107
+ additions.append(f"--max_cpus {cpus}")
108
+
109
+ if additions:
110
+ line = " ".join(additions)
111
+ click.echo(line)
112
+ click.echo(f"[bactopia-sysinfo] auto-detected: {line}", err=True)
113
+
114
+
115
+ def main():
116
+ sysinfo()
117
+
118
+
119
+ if __name__ == "__main__":
120
+ main()
@@ -95,7 +95,7 @@ def download(
95
95
  bactopia_path = str(Path(bactopia_path).absolute().resolve())
96
96
 
97
97
  # Load catalog.json
98
- catalog_path = Path(f"{bactopia_path}/data/catalog.json")
98
+ catalog_path = Path(f"{bactopia_path}/catalog.json")
99
99
  if catalog_path.exists():
100
100
  with open(catalog_path) as fh:
101
101
  catalog = json.load(fh)