Passed
Push — main ( cdf0f7...3de8e8 )
by Douglas
01:40
created

mandos.cli._init_commands()   A

Complexity

Conditions 2

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 10
nop 0
dl 0
loc 17
rs 9.9
c 0
b 0
f 0
1
"""
2
Command-line interface for mandos.
3
"""
4
5
from __future__ import annotations
6
7
import logging
0 ignored issues
show
Unused Code introduced by
The import logging seems to be unused.
Loading history...
8
import sys
0 ignored issues
show
Unused Code introduced by
The import sys seems to be unused.
Loading history...
9
from pathlib import Path
10
from typing import Type
11
12
import pandas as pd
0 ignored issues
show
introduced by
Unable to import 'pandas'
Loading history...
13
import typer
0 ignored issues
show
introduced by
Unable to import 'typer'
Loading history...
14
from typer.models import CommandInfo
0 ignored issues
show
introduced by
Unable to import 'typer.models'
Loading history...
15
from typeddfs import TypedDfs
0 ignored issues
show
introduced by
Unable to import 'typeddfs'
Loading history...
16
17
from mandos import logger, MandosLogging
18
from mandos.model.settings import MANDOS_SETTINGS
19
from mandos.model.taxonomy import TaxonomyDf
20
from mandos.model.taxonomy_caches import TaxonomyFactories
21
from mandos.entries.entries import Entries
22
from mandos.entries.args import EntryArgs
23
from mandos.entries.api_singletons import Apis
24
from mandos.entries.multi_searches import MultiSearch
25
from mandos.entries.searcher import SearcherUtils
26
27
cli = typer.Typer()
28
29
30
class Commands:
31
    """
32
    Entry points for mandos.
33
    """
34
35
    @staticmethod
36
    def search(
37
        config: Path = typer.Argument(
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
38
            None,
39
            help=".toml config file. See docs.",
40
            exists=True,
41
            dir_okay=False,
42
            readable=True,
43
        )
44
    ) -> None:
45
        """
46
        Run multiple searches.
47
        """
48
        MultiSearch(path, config).search()
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'path'
Loading history...
Comprehensibility Best Practice introduced by
The variable path does not seem to be defined.
Loading history...
49
50
    @staticmethod
51
    def find(
52
        path: Path = EntryArgs.path,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
53
        pubchem: bool = typer.Option(True, help="Download data from PubChem"),
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
54
        chembl: bool = typer.Option(True, help="Download data from ChEMBL"),
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
55
        hmdb: bool = typer.Option(True, help="Download data from HMDB"),
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
56
    ) -> None:
57
        """
58
        Fetches and caches compound data.
59
        Useful to check what you can see before running a search.
60
        """
61
        out_path = path.with_suffix(".ids.csv")
62
        if out_path.exists():
63
            raise FileExistsError(out_path)
64
        inchikeys = SearcherUtils.read(path)
65
        df = SearcherUtils.dl(inchikeys, pubchem=pubchem, chembl=chembl, hmdb=hmdb)
0 ignored issues
show
Coding Style Naming introduced by
Variable name "df" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
66
        df.to_csv(out_path)
67
        typer.echo(f"Wrote to {out_path}")
68
69
    @staticmethod
70
    def build_taxonomy(
0 ignored issues
show
Coding Style Naming introduced by
Argument name "to" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
71
        taxa: str = EntryArgs.taxa,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
72
        to: Path = typer.Option(
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
73
            None,
74
            show_default=False,
75
            help="""
76
        Output file; can be CSV, TSV, feather, etc.
77
        If it starts with '.', uses the default path but changes the format and filename extension.
78
79
        [default: <taxon-id,<taxon-id>,...>.feather]
80
        """,
81
        ),
82
    ):
83
        """
84
        Writes a CSV file of the descendents of given taxa.
85
        """
86
        taxon_ids = [
87
            int(taxon.strip()) if taxon.strip().isdigit() else taxon.strip()
88
            for taxon in taxa.split(",")
89
        ]
90
        # get the filename
91
        # by default we'll just use the inputs
92
        if to is None:
93
            to = Path(",".join([str(t) for t in taxon_ids]) + ".tab.gz")
94
        elif str(to).startswith("."):
95
            to = Path(",".join([str(t) for t in taxon_ids]) + str(to))
96
        to.parent.mkdir(exist_ok=True, parents=True)
97
        # TODO: this is quite inefficient
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
98
        # we're potentially reading in the vertebrata file multiple times
99
        # we could instead read it in, then concatenate the matching subtrees
100
        # however, this is moderately efficient if you ask for, e.g., Mammalia and Plantae
101
        # then it'll download Plantae but just get Mammalia from the resource-file Vertebrata
102
        logger.error(to)
103
        taxes = []
104
        for taxon in taxon_ids:
105
            tax = TaxonomyFactories.from_uniprot(MANDOS_SETTINGS.taxonomy_cache_path).load(taxon)
106
            taxes.append(tax.to_df())
107
        final_tax = TaxonomyDf(pd.concat(taxes, ignore_index=True))
108
        final_tax = final_tax.drop_duplicates().sort_values("taxon")
109
        # if it's text, just write one taxon ID per line
110
        is_text = any((to.name.endswith(".txt" + c) for c in {"", ".gz", ".zip", ".xz", ".bz2"}))
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable c does not seem to be defined.
Loading history...
111
        if is_text:
112
            final_tax = TypedDfs.wrap(final_tax[["taxon"]])
113
        # write the file
114
        final_tax.write_file(to)
115
116
    @staticmethod
117
    def dl_tax(
118
        taxon: int = typer.Argument(None, help="The **ID** of the UniProt taxon"),
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
119
    ) -> None:
120
        """
121
        Preps a new taxonomy file for use in mandos.
122
        Just returns if a corresponding file already exists in the resources dir or mandos cache (``~/.mandos``).
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (113/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
123
        Otherwise, downloads a tab-separated file from UniProt.
124
        (To find manually, follow the ``All lower taxonomy nodes`` link and click ``Download``.)
125
        Then applies fixes and reduces the file size, creating a new file alongside.
126
        Puts both the raw data and fixed data in the cache under ``~/.mandos/taxonomy/``.
127
        """
128
        TaxonomyFactories.from_uniprot(MANDOS_SETTINGS.taxonomy_cache_path).load(taxon)
129
130
131
def _init_commands():
132
    # Oh dear this is a nightmare
133
    # it's really hard to create typer commands with dynamically configured params --
134
    # we really need to rely on its inferring of params
135
    # that makes this really hard to do well
136
    for entry in Entries:
137
138
        info = CommandInfo(entry.cmd(), callback=entry.run)
139
        cli.registered_commands.append(info)
140
        # print(f"Registered {entry.cmd()} to {entry}")
141
        setattr(Commands, entry.cmd(), entry.run)
142
143
    cli.registered_commands.extend(
144
        [
145
            CommandInfo("@search", callback=Commands.search),
146
            CommandInfo("@tax-tree", callback=Commands.build_taxonomy),
147
            CommandInfo("@tax-dl", callback=Commands.dl_tax, hidden=True),
148
        ]
149
    )
150
151
152
_init_commands()
153
154
155
class MandosCli:
156
    """
157
    Global entry point for various stuff. For import by consumers.
158
    """
159
160
    settings = MANDOS_SETTINGS
161
    logger = logger
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable logger does not seem to be defined.
Loading history...
162
    logging = MandosLogging
163
    main = cli
164
    commands = Commands
165
166
    @classmethod
167
    def init(cls) -> Type[MandosCli]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
168
        MandosLogging.init()
169
        Apis.set_default()
170
        return cls
171
172
173
if __name__ == "__main__":
174
    MandosCli.init().main()
175
176
177
__all__ = ["MandosCli"]
178