Source code for geomodels.cli

"""Command Line Interface (CLI) for the geomodels Python package."""

import os
import enum
import glob
import logging
import pathlib
import argparse

from . import __version__
from .data import (
    get_default_data_path,
    get_base_url,
    install,
    EModelGroup,
    EModelType,
    EGeoidModel,
    EGravityModel,
    EMagneticModel,
)
from .wmmf import import_igrf_txt
from .tests import print_versions
from ._typing import PathType

EX_FAILURE = 1
EX_INTERRUPT = 130

PROG = __package__ + "-cli"
LOGFMT = "%(levelname)s: %(message)s"
DEFAULT_LOGLEVEL = "WARNING"


[docs] class EInfoMode(enum.Enum): """Enumeration describing which king of info shall be provided.""" INFO = "info" DATA = "data" ALL = "all"
def _autocomplete(parser: argparse.ArgumentParser) -> None: try: import argcomplete except ImportError: pass else: argcomplete.autocomplete(parser) def _format_data_info(datadir=None): if datadir is None: datadir = get_default_data_path() lines = [f"data directory: {datadir!r}"] for modelenum in (EGeoidModel, EGravityModel, EMagneticModel): modeltype = modelenum.get_model_type().value modeltype_dir = os.path.join(datadir, modeltype) lines.append(f"* model: {modeltype} ({modeltype_dir!r})") for item in modelenum: pattern = os.path.join(modeltype_dir, item.value + "*") installed = "INSTALLED " if glob.glob(pattern) else "NOT INSTALLED" lines.append(f" {item.name:12s} - {installed}") return "\n".join(lines)
[docs] def info(mode=EInfoMode.ALL, datadir=None): """Provide information about the installation and environment. Information provided include: the platform, the library versions and installed data. """ if mode in (EInfoMode.INFO, EInfoMode.ALL): print_versions() if mode in (EInfoMode.DATA, EInfoMode.ALL): print(_format_data_info(datadir))
[docs] def install_data(model, datadir=None, base_url=None, no_progress=False): """Download and install the data necessary for models computation. GeoModels uses external data to perform geoid, gravity and magnetic field computations. It is possible to install different subsets of data: :minimal: only data for the default model of each kind (geoid, gravity and magnetic field) are installed, :recommended: install the `minimal` set of data (see above) plus few additional and commonly used data (it is guaranteed that the `recommended` subset always includes all data that are necessary to run the test suite), :all: install all available data (about 670MB of disk space required), :geoids: install data for all supported geoids, :gravity: install data for all supported gravity models, :magnetic: install data for all supported magnetic field models. Additionally the it is possible to install data for a single model. """ if datadir is None: datadir = get_default_data_path() if base_url is None: base_url = get_base_url() enums = ( EModelGroup, EModelType, EGeoidModel, EGravityModel, EMagneticModel, ) for enumtype in enums: try: model = enumtype(model) except ValueError: pass else: break else: raise RuntimeError(f"unexpected model: {model!r}") progress = not no_progress install(model, datadir, base_url, progress=progress)
[docs] def import_igrf( path: PathType, outpath: PathType | None = None, force: bool = False ): """Import magnetic field data from IGRF text format. Import Spherical Harmonics coefficients for the IGRF magnetic field model from text file in IGRF standard format. See: https://www.ngdc.noaa.gov/IAGA/vmod/igrf.html. """ wmmdata = import_igrf_txt(path) if outpath is None: outpath = pathlib.Path(get_default_data_path()) / "magnetic" wmmdata.save(outpath, force)
def _add_logging_control_args( parser: argparse.ArgumentParser, default_loglevel: str = DEFAULT_LOGLEVEL ) -> argparse.ArgumentParser: """Add command line options for logging control.""" loglevels = [logging.getLevelName(level) for level in range(10, 60, 10)] parser.add_argument( "--loglevel", default=default_loglevel, choices=loglevels, help="logging level (default: %(default)s)", ) parser.add_argument( "-q", "--quiet", dest="loglevel", action="store_const", const="ERROR", help=( "suppress standard output messages, only errors are printed " "to screen (set 'loglevel' to 'ERROR')" ), ) parser.add_argument( "-v", "--verbose", dest="loglevel", action="store_const", const="INFO", help="print verbose output messages (set 'loglevel' to 'INFO')", ) parser.add_argument( # "-d", "--debug", dest="loglevel", action="store_const", const="DEBUG", help="print debug messages (set 'loglevel' to 'DEBUG')", ) return parser def _get_synopsis(docstring: str | None) -> str: return docstring.splitlines()[0] if docstring is not None else ""
[docs] def get_info_parser(subparsers=None) -> argparse.ArgumentParser: """Set up the argument parser for the `info` sub-command.""" name = "info" synopsis = _get_synopsis(info.__doc__) doc = info.__doc__ if subparsers is None: parser = argparse.ArgumentParser(prog=name, description=doc) else: parser = subparsers.add_parser(name, description=doc, help=synopsis) parser.set_defaults(func=info) # command line options parser.add_argument( "-d", "--datadir", default=get_default_data_path(), help=( "specifies where the model data are stored (default: %(default)r)." ), ) parser.add_argument( "-a", "--all", dest="mode", action="store_const", const=EInfoMode.ALL, default=EInfoMode.INFO, help=( "show both versions and platform info and also information " "about installed data" ), ) parser.add_argument( "--data", dest="mode", action="store_const", const=EInfoMode.DATA, help="show info about installed data", ) return parser
[docs] def get_install_data_parser(subparsers=None) -> argparse.ArgumentParser: """Set up the argument parser for the `install-data` sub-command.""" name = "install-data" synopsis = _get_synopsis(install_data.__doc__) doc = install_data.__doc__ if subparsers is None: parser = argparse.ArgumentParser( prog=name, description=doc, formatter_class=argparse.RawDescriptionHelpFormatter, ) else: parser = subparsers.add_parser( name, description=doc, help=synopsis, formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.set_defaults(func=install_data) # command line options parser.add_argument( "-b", "--base-url", default=get_base_url(), help="specifies the base URL for the download (default: %(default)r).", ) parser.add_argument( "-d", "--datadir", default=get_default_data_path(), help=( "specifies where the datasets should be stored " "(default: %(default)r)." ), ) parser.add_argument( "--no-progress", action="store_true", default=False, help="suppress progress bar display", ) # positional arguments choices = [model.value for model in EModelGroup] choices.extend(model.value for model in EModelType) choices.extend(model.value for model in EGeoidModel) choices.extend(model.value for model in EGravityModel) choices.extend(model.value for model in EMagneticModel) parser.add_argument( "model", choices=choices, help="model(s) to be installed" ) return parser
[docs] def get_import_igrf_parser(subparsers=None) -> argparse.ArgumentParser: """Set up the argument parser for the `import-igrf` sub-command.""" name = "import-igrf" synopsis = _get_synopsis(import_igrf.__doc__) doc = import_igrf.__doc__ if subparsers is None: parser = argparse.ArgumentParser(prog=name, description=doc) else: parser = subparsers.add_parser(name, description=doc, help=synopsis) parser.set_defaults(func=import_igrf) # command line options parser.add_argument( "-o", "--outpath", default=pathlib.Path(get_default_data_path()) / "magnetic", help='specifies the output data path (default: "%(default)s").', ) parser.add_argument( "--force", action="store_true", default=False, help="overwrite existing files (default: %(default)s).", ) # positional arguments parser.add_argument("path", help="path or URL of the IGRF text file") return parser
[docs] def get_parser() -> argparse.ArgumentParser: """Instantiate the command line argument (sub-)parser.""" parser = argparse.ArgumentParser(prog=PROG, description=__doc__) parser.add_argument( "--version", action="version", version="%(prog)s v" + __version__ ) # Command line options _add_logging_control_args(parser) # Sub-command management subparsers = parser.add_subparsers(title="sub-commands") # , metavar="") get_info_parser(subparsers) get_install_data_parser(subparsers) get_import_igrf_parser(subparsers) _autocomplete(parser) return parser
[docs] def parse_args(args=None, namespace=None, parser=None): """Parse command line arguments.""" if parser is None: parser = get_parser() args = parser.parse_args(args, namespace) # Common pre-processing of parsed arguments and consistency checks # ... if getattr(args, "func", None) is None: parser.error("no sub-command specified.") return args
def _get_kwargs(args): kwargs = vars(args).copy() kwargs.pop("func", None) kwargs.pop("loglevel", None) return kwargs
[docs] def main(*argv): """Implement the main CLI interface.""" # setup logging logging.basicConfig(format=LOGFMT, level=DEFAULT_LOGLEVEL) logging.captureWarnings(True) log = logging.getLogger(__name__) # parse cmd line arguments args = parse_args(argv if argv else None) try: # NOTE: use the root logger to set the logging level logging.getLogger().setLevel(args.loglevel) log.debug("args: %s", args) kwargs = _get_kwargs(args) return args.func(**kwargs) except Exception as exc: # noqa: B902 log.critical( "unexpected exception caught: %r %s", type(exc).__name__, exc ) log.debug("stacktrace:", exc_info=True) return EX_FAILURE except KeyboardInterrupt: log.warning("Keyboard interrupt received: exit the program") return EX_INTERRUPT