diff --git a/README.md b/README.md index 8c08c2b..740d7e6 100644 --- a/README.md +++ b/README.md @@ -1,79 +1,83 @@ # pmotools -A collection of tools to interact with [portable microhaplotype object (pmo) file format](https://github.com/PlasmoGenEpi/portable-microhaplotype-object) +A collection of tools to interact with the [portable microhaplotype object (PMO) file format](https://github.com/PlasmoGenEpi/portable-microhaplotype-object). -# Setup +Documentation on the format and pmotools-python, including detailed tutorials can be found [here](https://plasmogenepi.github.io/PMO_Docs/). + +The manual for pmotools-python can be found [here](https://plasmogenepi.github.io/pmotools-python/index.html). + +# Installation + +Requires Python 3.11+. -Install using pip. Currently only supports python 3.11+ ```bash -pip install . +pip install pmotools ``` +This installs the `pmotools-python` command-line tool and the `pmotools` Python library. + # Usage -This package is built to either be used as a library in python projects and a command line interface already created which can be called from the commandline `pmotools-python` which will install with `pip install .`. +pmotools-python can be used from the command line or imported as a Python library. +## Command line -## Auto completion +After installation, the `pmotools-python` command is available: + +```bash +pmotools-python --help +pmotools-python validate_pmo --help +``` -If you want to add auto-completion to the scripts master function [pmotools-python](scripts/pmotools-runner.py) you can add the following to your `~/.bash_completion`. This can also be found in etc/bash_completion in the current directory. Or can be generated with `pmotools-python --bash-completion` +Examples: ```bash -# 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: - # "\t\t" - 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 +pmotools-python validate_pmo --pmo my_data.pmo.json +pmotools-python combine_pmos --pmo_files run1.pmo.json run2.pmo.json --output combined.pmo.json +``` + +Commands are grouped by task: + +| Group | Purpose | +| --- | --- | +| `convertors_to_json` | Convert tables and metadata into PMO inputs | +| `extractors_from_pmo` | Subset PMOs by metadata, samples, targets, or read counts | +| `pmo_to_table` | Export PMO contents to spreadsheets or other formats | +| `extract_basic_info_from_pmo` | Inspect metadata and counts | +| `working_with_multiple_pmos` | Combine PMOs | +| `validation` | Validate PMO files against the JSON schema | + +See the [command-line manual](https://plasmogenepi.github.io/pmotools-python/commands/index.html) for full documentation of each command. + +## Python library + +Import pmotools in your own code to read, validate, write, and build PMO files: + +```python +from pmotools.pmo_engine.pmo_reader import PMOReader +from pmotools.pmo_engine.pmo_checker import PMOChecker + +pmo = PMOReader.read_in_pmo("example.pmo.json") +checker = PMOChecker() +checker.validate_pmo_json(pmo) +``` + +Core modules: + +- `pmotools.pmo_engine` — read, write, validate, and export PMOs +- `pmotools.pmo_builder` — build PMOs from tables and metadata +- `pmotools.scripts` — the same logic used by the CLI + +See the [API reference](https://plasmogenepi.github.io/pmotools-python/api/modules.html) for module-level documentation. + +## Auto completion + +To enable bash tab completion for `pmotools-python`, append the script from `etc/bash_completion` to your `~/.bash_completion`, or generate it with: + +```bash +pmotools-python --bash-completion >> ~/.bash_completion +source ~/.bash_completion ``` ## Developer Setup @@ -144,3 +148,17 @@ make update_autodocs make html ``` You can open the html to review changes. + +### Releasing pmotools-python + +To release pmotools-python you need to update `version` in `pyproject.toml`, adhering to semantic versioning conventions. + +**Note:** It is not always the case, but sometimes a release of pmotools-python will coincide with a new schema version. See the section below for notes on updating the schema version. + +Once you have merged these updates into the `main` branch, create a new release with notes. GitHub actions will automatically deploy to PyPi. + +### Updating the schema version + +You can release pmotools-python without updating the schema version. However, for the schema to be updated and become the default within pmotools-python, you must update the schema and then do a release of pmotools-python by following the instructions above. + +Update `__schema_version__` in `src/pmotools/__init__.py` to the new version of the schema. Do not include the `v` here (e.g. 1.0.0 not v1.0.0). diff --git a/pyproject.toml b/pyproject.toml index 0f20290..7fa2574 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pmotools" -version = "v1.0.0" +version = "v1.1.0" description = "Tools for building and analyzing PMO files" readme = "README.md" authors = [ diff --git a/src/pmotools/__init__.py b/src/pmotools/__init__.py index 30d85c6..3195d93 100644 --- a/src/pmotools/__init__.py +++ b/src/pmotools/__init__.py @@ -5,7 +5,7 @@ #: Version of the PMO schema this package targets. Single source of truth; #: bump when the schema changes. Independent of the package release version. -__schema_version__ = "1.0.0" +__schema_version__ = "1.1.0" try: # Distribution version from installed metadata (matches [project].version) diff --git a/tests/test_pmo_builder/test_merge_to_pmo.py b/tests/test_pmo_builder/test_merge_to_pmo.py index c653cba..ed5c112 100644 --- a/tests/test_pmo_builder/test_merge_to_pmo.py +++ b/tests/test_pmo_builder/test_merge_to_pmo.py @@ -15,14 +15,6 @@ class TestMergeToPMO(unittest.TestCase): def setUp(self): self.ref_list = [{"name": "name1"}, {"name": "name2"}, {"name": "name3"}] - self.pmo_header_v1_0_0 = { - "pmo_version": "1.0.0", - "creation_date": "2025-07-22", - "generation_method": { - "program_name": "pmotools-python", - "program_version": "1.0.0", - }, - } self.pmo_header_v1_1_0 = { "pmo_version": "1.1.0", "creation_date": "2025-07-22", @@ -299,10 +291,10 @@ def test_merge_to_pmo_minimum_info_raise_multi_panel_no_lib(self): def test_generate_pmo_header(self, mock_date): mock_date.today.return_value = date(2025, 7, 22) mock_date.side_effect = lambda *args, **kwargs: date(*args, **kwargs) - actual = _generate_pmo_header("1.0.0") - # expected = {'pmo_version': '1.0.0', 'creation_date': '2025-07-22', 'generation_method': { - # 'program_name': 'pmotools-python', 'program_version': '1.0.0'}} - self.assertEqual(actual, self.pmo_header_v1_0_0) + actual = _generate_pmo_header( + pmotools_version="1.1.0", pmo_schema_version="1.1.0" + ) + self.assertEqual(actual, self.pmo_header_v1_1_0) def test_replace_key_with_id(self): test_target_list = [ @@ -357,7 +349,7 @@ def test_make_lookup(self): @patch("pmotools.pmo_builder.merge_to_pmo._replace_names_with_IDs") @patch("pmotools.pmo_builder.merge_to_pmo._generate_pmo_header") def test_merge_to_pmo(self, mock_generate_pmo_header, _): - mock_generate_pmo_header.return_value = self.pmo_header_v1_0_0 + mock_generate_pmo_header.return_value = self.pmo_header_v1_1_0 actual = merge_to_pmo( specimen_info=[{"specimens": "specinfo"}], library_sample_info=[{"library_samples": "library_samples"}], @@ -376,7 +368,7 @@ def test_merge_to_pmo(self, mock_generate_pmo_header, _): ) expected = { - "pmo_header": self.pmo_header_v1_0_0, + "pmo_header": self.pmo_header_v1_1_0, "library_sample_info": [{"library_samples": "library_samples"}], "specimen_info": [{"specimens": "specinfo"}], "sequencing_info": [{"sequencing": "sequencing"}],