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.
- {bactopia-2.0.2 → bactopia-2.1.0}/PKG-INFO +2 -1
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/catalog.py +65 -1
- bactopia-2.1.0/bactopia/cli/citations.py +190 -0
- bactopia-2.1.0/bactopia/cli/docs.py +208 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/helpers/merge_schemas.py +1 -1
- bactopia-2.1.0/bactopia/cli/sysinfo.py +120 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/workflows.py +1 -1
- bactopia-2.1.0/bactopia/lint/citations.py +424 -0
- bactopia-2.1.0/bactopia/lint/docs.py +751 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/lint/rules/module_rules.py +9 -3
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/nf.py +111 -23
- bactopia-2.1.0/bactopia/templates/bactopia/llms.txt.j2 +84 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/templates/nextflow/nextflow.config.j2 +1 -1
- {bactopia-2.0.2 → bactopia-2.1.0}/pyproject.toml +4 -1
- bactopia-2.0.2/bactopia/cli/citations.py +0 -70
- {bactopia-2.0.2 → bactopia-2.1.0}/LICENSE +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/README.md +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/__init__.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/atb.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/__init__.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/atb/__init__.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/atb/atb_downloader.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/atb/atb_formatter.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/datasets.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/download.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/helpers/__init__.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/jsonify.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/lint.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/pipeline/__init__.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/pipeline/bracken_to_excel.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/pipeline/check_assembly_accession.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/pipeline/check_fastqs.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/pipeline/cleanup_coverage.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/pipeline/kraken_bracken_summary.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/pipeline/mask_consensus.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/pipeline/scrubber_summary.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/pipeline/teton_prepare.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/prepare.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/prune.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/pubmlst/build.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/pubmlst/setup.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/review.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/search.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/status.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/summary.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/testing.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/cli/update.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/databases/__init__.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/databases/ena.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/databases/ncbi.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/databases/pubmlst/__init__.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/databases/pubmlst/constants.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/databases/pubmlst/utils.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/lint/__init__.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/lint/models.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/lint/rules/__init__.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/lint/rules/subworkflow_rules.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/lint/rules/workflow_rules.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/lint/runner.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/outputs.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parse.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/__init__.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/amrfinderplus.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/annotator.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/ariba.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/assembler.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/blast.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/citations.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/coverage.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/error.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/gather.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/generic.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/kraken.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/mapping.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/mlst.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/nextflow.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/parsables.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/qc.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/sketcher.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/variants.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/versions.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/parsers/workflows.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/reports/__init__.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/reports/templates/__init__.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/summary.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/templates/__init__.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/templates/logos.py +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/templates/nextflow/params.config.j2 +0 -0
- {bactopia-2.0.2 → bactopia-2.1.0}/bactopia/templates/nextflow/process.config.j2 +0 -0
- {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
|
|
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(
|
|
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()
|
|
@@ -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}/
|
|
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)
|