#!/usr/bin/env python3
import argparse
from dataclasses import dataclass
from typing import Callable, Dict, Tuple
from pmotools import __version__
from pmotools.utils.color_text import ColorText as CT
# convertors_to_pmo
from pmotools.scripts.convertors_to_pmo.text_meta_to_json_meta import (
text_meta_to_json_meta,
)
from pmotools.scripts.convertors_to_pmo.excel_meta_to_json_meta import (
excel_meta_to_json_meta,
)
from pmotools.scripts.convertors_to_pmo.microhaplotype_table_to_json_file import (
microhaplotype_table_to_json_file,
)
from pmotools.scripts.convertors_to_pmo.terra_amp_output_to_json import (
terra_amp_output_to_json,
)
# extractors_from_pmo
from pmotools.scripts.extractors_from_pmo.extract_pmo_with_selected_meta import (
extract_pmo_with_selected_meta,
)
from pmotools.scripts.extractors_from_pmo.extract_pmo_with_select_specimen_names import (
extract_pmo_with_select_specimen_names,
)
from pmotools.scripts.extractors_from_pmo.extract_pmo_with_select_library_sample_names import (
extract_pmo_with_select_library_sample_names,
)
from pmotools.scripts.extractors_from_pmo.extract_pmo_with_select_targets import (
extract_pmo_with_select_targets,
)
from pmotools.scripts.extractors_from_pmo.extract_pmo_with_read_filter import (
extract_pmo_with_read_filter,
)
# pmo_utils
from pmotools.scripts.pmo_utils.combine_pmos import combine_pmos
from pmotools.scripts.pmo_utils.validate_pmo import validate_pmo
# extract_info_from_pmo
from pmotools.scripts.extract_info_from_pmo.list_library_sample_names_per_specimen_name import (
list_library_sample_names_per_specimen_name,
)
from pmotools.scripts.extract_info_from_pmo.list_specimen_meta_fields import (
list_specimen_meta_fields,
)
from pmotools.scripts.extract_info_from_pmo.list_bioinformatics_run_names import (
list_bioinformatics_run_names,
)
from pmotools.scripts.extract_info_from_pmo.count_specimen_meta import (
count_specimen_meta,
)
from pmotools.scripts.extract_info_from_pmo.count_targets_per_library_sample import (
count_targets_per_library_sample,
)
from pmotools.scripts.extract_info_from_pmo.count_library_samples_per_target import (
count_library_samples_per_target,
)
# panel info subset
from pmotools.scripts.pmo_to_tables.extract_insert_of_panels import (
extract_insert_of_panels,
)
from pmotools.scripts.pmo_to_tables.extract_refseq_of_inserts_of_panels import (
extract_refseq_of_inserts_of_panels,
)
# pmo to tables
from pmotools.scripts.pmo_to_tables.export_pmo_into_xlsx import (
export_pmo_into_xlsx,
)
from pmotools.scripts.pmo_to_tables.export_specimen_meta_table import (
export_specimen_meta_table,
)
from pmotools.scripts.pmo_to_tables.export_library_sample_meta_table import (
export_library_sample_meta_table,
)
from pmotools.scripts.pmo_to_tables.export_project_info_meta_table import (
export_project_info_meta_table,
)
from pmotools.scripts.pmo_to_tables.export_sequencing_info_meta_table import (
export_sequencing_info_meta_table,
)
from pmotools.scripts.pmo_to_tables.export_specimen_travel_meta_table import (
export_specimen_travel_meta_table,
)
from pmotools.scripts.pmo_to_tables.export_bioinformatics_run_info_meta_table import (
export_bioinformatics_run_info_meta_table,
)
from pmotools.scripts.pmo_to_tables.export_bioinformatics_methods_info_meta_table import (
export_bioinformatics_methods_info_meta_table,
)
from pmotools.scripts.pmo_to_tables.export_targeted_genomes_meta_table import (
export_targeted_genomes_meta_table,
)
from pmotools.scripts.pmo_to_tables.export_target_info_meta_table import (
export_target_info_meta_table,
)
from pmotools.scripts.pmo_to_tables.export_panel_info_meta_table import (
export_panel_info_meta_table,
)
from pmotools.scripts.pmo_to_tables.extract_allele_table import (
extract_for_allele_table,
)
[docs]@dataclass(frozen=True)
class PmoCommand:
func: Callable[[], None]
help: str
REGISTRY: Dict[str, Dict[str, PmoCommand]] = {
"convertors_to_json": {
"text_meta_to_json_meta": PmoCommand(
text_meta_to_json_meta, "Convert text file meta to JSON Meta"
),
"excel_meta_to_json_meta": PmoCommand(
excel_meta_to_json_meta, "Convert Excel file meta to JSON Meta"
),
"microhaplotype_table_to_json_file": PmoCommand(
microhaplotype_table_to_json_file,
"Convert microhaplotype table to a JSON file",
),
"terra_amp_output_to_json": PmoCommand(
terra_amp_output_to_json, "Convert Terra output to JSON sequence table"
),
},
"extractors_from_pmo": {
"extract_pmo_with_selected_meta": PmoCommand(
extract_pmo_with_selected_meta,
"Extract samples + haplotypes using selected meta",
),
"extract_pmo_with_select_specimen_names": PmoCommand(
extract_pmo_with_select_specimen_names,
"Extract specific samples from the specimens table",
),
"extract_pmo_with_select_library_sample_names": PmoCommand(
extract_pmo_with_select_library_sample_names,
"Extract library sample names from library_sample_info table",
),
"extract_pmo_with_select_targets": PmoCommand(
extract_pmo_with_select_targets, "Extract specific targets"
),
"extract_pmo_with_read_filter": PmoCommand(
extract_pmo_with_read_filter, "Extract with a read filter"
),
},
"working_with_multiple_pmos": {
"combine_pmos": PmoCommand(
combine_pmos, "Combine multiple PMOs of the same panel"
),
},
"extract_basic_info_from_pmo": {
"list_library_sample_names_per_specimen_name": PmoCommand(
list_library_sample_names_per_specimen_name,
"List library_sample_names per specimen_name",
),
"list_specimen_meta_fields": PmoCommand(
list_specimen_meta_fields,
"List specimen meta fields in the specimen_info section",
),
"list_bioinformatics_run_names": PmoCommand(
list_bioinformatics_run_names,
"List all tar_amp_bioinformatics_info_names in a PMO",
),
"count_specimen_meta": PmoCommand(
count_specimen_meta, "Count values of selected specimen meta fields"
),
"count_targets_per_library_sample": PmoCommand(
count_targets_per_library_sample, "Count number of targets per sample"
),
"count_library_samples_per_target": PmoCommand(
count_library_samples_per_target, "Count number of samples per target"
),
},
"validation": {
"validate_pmo": PmoCommand(
validate_pmo, "Validate a PMO file against a JSON Schema"
)
},
"pmo_to_table": {
"export_pmo_into_xlsx": PmoCommand(
export_pmo_into_xlsx, "export all parts of a PMO into a .xlsx file"
),
"export_specimen_meta_table": PmoCommand(
export_specimen_meta_table, "export the specimen meta table from a PMO file"
),
"export_library_sample_meta_table": PmoCommand(
export_library_sample_meta_table,
"export the library_sample meta table from a PMO file",
),
"export_project_info_meta_table": PmoCommand(
export_project_info_meta_table,
"export the project_info meta table from a PMO file",
),
"export_sequencing_info_meta_table": PmoCommand(
export_sequencing_info_meta_table,
"export the sequencing_info meta table from a PMO file",
),
"export_specimen_travel_meta_table": PmoCommand(
export_specimen_travel_meta_table,
"export the specimen travel_info meta table from a PMO file",
),
"export_targeted_genomes_meta_table": PmoCommand(
export_targeted_genomes_meta_table,
"export the targeted genomes info meta table from a PMO file",
),
"export_target_info_meta_table": PmoCommand(
export_target_info_meta_table,
"export the target info meta table from a PMO file",
),
"export_panel_info_meta_table": PmoCommand(
export_panel_info_meta_table,
"export the panel info meta table from a PMO file",
),
"export_bioinformatics_run_info_meta_table": PmoCommand(
export_bioinformatics_run_info_meta_table,
"export the bioinformatics_run_info meta table from a PMO file",
),
"export_bioinformatics_methods_info_meta_table": PmoCommand(
export_bioinformatics_methods_info_meta_table,
"export the bioinformatics_methods_info meta table from a PMO file",
),
"extract_allele_table": PmoCommand(
extract_for_allele_table,
"Extract allele tables for tools like dcifer or moire",
),
"extract_insert_of_panels": PmoCommand(
extract_insert_of_panels, "Extract inserts of panels from a PMO"
),
"extract_refseq_of_inserts_of_panels": PmoCommand(
extract_refseq_of_inserts_of_panels,
"Extract ref_seq of panel inserts from a PMO",
),
},
}
def _iter_all_commands():
for group, commands in REGISTRY.items():
for name, cmd in commands.items():
yield group, name, cmd.help
def _print_catalog_plain():
"""
Print commands in a machine-friendly, no-color format:
'<command>\t<group>\t<help>'
One per line; used by bash completion.
"""
import sys
for group, name, cmdhelp in _iter_all_commands():
sys.stdout.write(f"{name}\t{group}\t{cmdhelp}\n")
def _print_catalog() -> None:
"""Print all groups and their commands like your previous version."""
import sys
sys.stdout.write(
f"pmotools-python v{__version__} - A suite of tools for interacting with "
+ CT.boldGreen("Portable Microhaplotype Object (PMO)")
+ " file format\n\n"
)
sys.stdout.write("Available functions organized by groups are\n")
for group, commands in REGISTRY.items():
sys.stdout.write(CT.boldBlue(group) + "\n")
for name, cmd in commands.items():
sys.stdout.write(f"\t{name} - {cmd.help}\n")
sys.stdout.write("\n")
def _print_group(group: str) -> int:
"""Print a single group's commands (blue header) if it exists."""
import sys
if group not in REGISTRY:
sys.stdout.write(
CT.boldRed("Did not find group ") + CT.boldWhite(group) + "\n\n"
)
_print_catalog()
return 2
sys.stdout.write(CT.boldBlue(group) + "\n")
for name, cmd in REGISTRY[group].items():
sys.stdout.write(f"\t{name} - {cmd.help}\n")
sys.stdout.write("\n")
return 0
def _print_bash_completion():
# NOTE: this uses --list-plain to avoid ANSI color parsing and be stable.
script = r"""# bash completion for pmotools-python
# add the below to your ~/.bash_completion
_pmotools_python_complete()
{
# Make sure underscores (and =) are NOT treated as word breaks
# so options like --pmo_files or --file=path complete as one token.
local _OLD_WB="${COMP_WORDBREAKS-}"
COMP_WORDBREAKS="${COMP_WORDBREAKS//_/}"
COMP_WORDBREAKS="${COMP_WORDBREAKS//=}"
local cur prev
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
# 1) Completing the command name (1st arg): list all commands
if [[ ${COMP_CWORD} -eq 1 ]]; then
# Our CLI prints machine-friendly list via --list-plain:
# "<command>\t<group>\t<help>"
local lines cmds
lines="$(${COMP_WORDS[0]} --list-plain 2>/dev/null)"
cmds="$(printf '%s\n' "${lines}" | awk -F'\t' '{print $1}')"
COMPREPLY=( $(compgen -W "${cmds}" -- "${cur}") )
# restore wordbreaks before returning
COMP_WORDBREAKS="$_OLD_WB"
return 0
fi
# 2) Completing flags for a leaf command: scrape leaf -h
if [[ "${cur}" == -* ]]; then
local helps opts
helps="$(${COMP_WORDS[0]} ${COMP_WORDS[1]} -h 2>/dev/null)"
# Pull out flag tokens and split comma-separated forms
# Keep underscores intact in the tokens.
opts="$(printf '%s\n' "${helps}" \
| sed -n 's/^[[:space:]]\{0,\}\(-[-[:alnum:]_][-[:alnum:]_]*\)\(, *-[[:alnum:]_][-[:alnum:]_]*\)\{0,\}.*/\1/p' \
| sed 's/, / /g')"
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
COMP_WORDBREAKS="$_OLD_WB"
return 0
fi
# 3) Otherwise, fall back to filename completion for positional args
COMPREPLY=( $(compgen -f -- "${cur}") )
# restore original word breaks
COMP_WORDBREAKS="$_OLD_WB"
return 0
}
complete -F _pmotools_python_complete pmotools-python
"""
import sys
sys.stdout.write(script)
def _build_parser() -> (
Tuple[argparse.ArgumentParser, Dict[str, Tuple[str, PmoCommand]]]
):
"""
Build a flat CLI:
pmotools-python <command> [args...]
Returns the parser and an index mapping command_name -> (group, PmoCommand)
"""
description = (
f"pmotools-python v{__version__} – A suite of tools for interacting with "
f"{CT.boldGreen('Portable Microhaplotype Object (PMO)')} files"
)
parser = argparse.ArgumentParser(
prog="pmotools-python",
description=description,
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument(
"--list-plain",
action="store_true",
help=argparse.SUPPRESS, # keep it hidden; for completion script
)
parser.add_argument(
"--bash-completion",
action="store_true",
help="Print bash completion script for pmotools-python",
)
parser.add_argument(
"-V", "--version", action="version", version=f"%(prog)s {__version__}"
)
parser.add_argument(
"--list",
nargs="?",
const="__ALL__",
metavar="[group]",
help="List all commands, or only those within a specific group",
)
subparsers = parser.add_subparsers(
title="Commands", dest="command", metavar="<command>"
)
command_index: Dict[str, Tuple[str, PmoCommand]] = {}
for group, commands in REGISTRY.items():
for cmd_name, cmd in commands.items():
if cmd_name in command_index:
# Hard fail early if duplicate command names exist across groups
raise RuntimeError(
f"Duplicate command name detected: '{cmd_name}'. "
f"Please rename one of the commands or add an alias."
)
sp = subparsers.add_parser(
cmd_name,
help=f"{cmd.help} [{group}]",
description=f"{cmd.help} (group: {group})",
add_help=False,
)
sp.set_defaults(_handler=cmd.func, _group=group, _cmd_name=cmd_name)
command_index[cmd_name] = (group, cmd)
return parser, command_index
[docs]def main(argv: list[str] | None = None) -> int:
parser, command_index = _build_parser()
args, unknown = parser.parse_known_args(argv)
if getattr(args, "bash_completion", False):
_print_bash_completion()
return 0
if getattr(args, "list_plain", False):
_print_catalog_plain()
return 0
if getattr(args, "list", None):
group = args.list
if group == "__ALL__":
_print_catalog()
return 0
else:
return _print_group(group)
# No command provided: show the catalog
if not getattr(args, "command", None):
_print_catalog()
return 0
# Dispatch to the leaf and forward remaining args to its own argparse
handler = getattr(args, "_handler", None)
if handler is None:
parser.error("No handler bound for this command (internal error).")
import sys
leaf_prog = f"pmotools-python {getattr(args, '_cmd_name', 'unknown')}"
old_argv = sys.argv[:]
try:
sys.argv = [leaf_prog, *unknown]
handler()
finally:
sys.argv = old_argv
return 0
if __name__ == "__main__":
raise SystemExit(main())