"""Build the NHS DM+D database from the XML distribution files. This
will build the main file and the bonus file. Currently this has only
been tested against SQLite and MySQL backends and there may be some
adjustment to the ORM classes to get it working with other backends.
"""
# Importing the version number (in __init__.py) and the package name
from dmd import (
    __version__,
    __name__ as pkg_name,
    common,
    orm
)
from sqlalchemy_config import config as cfg
from zipfile import ZipFile
from datetime import datetime
from collections import namedtuple
from xml.etree import ElementTree
from tqdm import tqdm
import shutil
import glob
import argparse
import os
import re
import tempfile


_PROG_NAME = 'dmd-build'
"""The name of the program (`str`)
"""

_EXP_FILES = 7
"""The expected number of XML files in the zip archive (`int`)
"""

_BONUS_EXP_FILES = 1
"""The expected number of XML files in the bonus zip archive (`int`)
"""

FileInfo = namedtuple('FileInfo', ['path', 'type', 'version', 'date'])
"""Will hold information on the ZIP archive and the XML files extracted from
the zip file (`FileInfo`)
"""


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def main():
    """The main entry point for the script. For API usage see
    ``dmd.build.build_dmd``.
    """
    # Initialise and parse the command line arguments
    parser = _init_cmd_args()
    args = _parse_cmd_args(parser)

    # Start a msg output, this will respect the verbosity and will output to
    # STDERR
    m = common.Msg(prefix='', verbose=args.verbose, file=common.MSG_OUT)
    m.msg("= {2} ({0} v{1}) =".format(
        pkg_name, __version__, _PROG_NAME
    ))
    m.prefix = common.MSG_PREFIX
    m.msg_atts(args)

    # Get a sessionmaker to create sessions to interact with the database
    sm = cfg.get_sessionmaker(
        args.config_section, common._DEFAULT_PREFIX, url_arg=args.dburl,
        config_arg=args.config, config_env=None,
        config_default=common._DEFAULT_CONFIG
    )

    # Make sure the database exists (or does not exist)
    cfg.create_db(sm, exists=args.exists)

    try:
        # Now ensure that all the database tables are created
        # note that if the database exists then we do not error on tables
        # existing
        session = sm()
        orm.Base.metadata.create_all(
            session.get_bind(), checkfirst=not args.exists
        )
    except Exception:
        # Could not create tables for some reason
        session.close()

    # Kick off the build process
    build_dmd(
        sm, args.indata, tmpdir=args.tmp, verbose=args.verbose,
        bonus_zip=args.bonus_files
    )
    m.msg("*** END ***")


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def build_dmd(sm, zip_file, tmpdir=None, verbose=False, bonus_zip=None):
    """Kick of the build process for the DM+D database. This should be the
    main API entry point for any external programs using this.

    Parameters
    ----------
    sm : `sqlalchemy.Sessionmaker`
        The SQLAchemy session maker to use to create sessions.
    zip_file : `str`
        The path to the downloaded DM+D zip archive. This should not be renamed
        or extracted. It should be exactly as downloaded from TRUD.
        It will have a format like:
        ``nhsbsa_dmd_<major version>.<minor version>.<patch>_YYYYMMDD<some other digits>.zip``
    tmpdir : `str` or `NoneType`, optional, default: `NoneType`
        The tmp directory to extract the ZIP archive into. If ``NoneType`` then
        the system tmp directory is used. Note that the zip archives will be
        extracted into sub directories within here. These will be deleted when
        this exits (either through failure of success).
    verbose : `bool`, optional, default: `False`
        Show progress of the individual table loads.
    bonus_zip_file : `str` or `NoneType`, optional, default: `NoneType`
        The path to the downloaded DM+D bonus zip archive. This should not be
        renamed or extracted. It should be exactly as downloaded from TRUD.
        It will have a format like:
        ``nhsbsa_dmdbonus_<major version>.<minor version>.<patch>_YYYYMMDD<some other digits>.zip``
    """
    # Create a tmp output directory to zip into
    zip_out = tempfile.mkdtemp(dir=tmpdir)

    # We create a directory for the bonus file even if we do not use it
    # as it will be deleted anyway
    bonus_out = tempfile.mkdtemp(dir=tmpdir)

    try:
        # Extract the information from the zip file
        zip_info = get_zip_file_data(zip_file)

        # Now extract the zip files from the archive
        extract_data(zip_file, out_dir=zip_out)

        # Get all the required data from the filenames
        file_data = get_file_data(zip_out)

        if bonus_zip is not None:
            bonus_info = extract_bonus_file(
                bonus_zip, tmpdir=bonus_out
            )
            file_data = {**file_data, **bonus_info}

        # Load up all the file data into the database
        load_file_info(
            sm, zip_info,
            *list(file_data.values())
        )

        # Load the lookup data
        load_lookups(sm, file_data['lookup'], verbose=verbose)

        # Load the ingredient data
        load_ingredient(sm, file_data['ingredient'], verbose=verbose)

        # Load the vtm data
        load_vtm(sm, file_data['vtm'], verbose=verbose)

        # Load the vmp data
        load_vmp(sm, file_data['vmp'], verbose=verbose)

        # Load the vmpp data
        load_vmpp(sm, file_data['vmpp'], verbose=verbose)

        # Load the amp data
        load_amp(sm, file_data['amp'], verbose=verbose)

        # Load the ampp data
        load_ampp(sm, file_data['ampp'], verbose=verbose)

        try:
            # Load the ampp data
            load_bnf(sm, file_data['bnf'], verbose=verbose)
        except KeyError:
            pass
    finally:
        shutil.rmtree(zip_out)
        shutil.rmtree(bonus_out)


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def extract_bonus_file(bonus_zip_file, tmpdir=None):
    """Extract the files and file information from the bonus zip archive.

    Parameters
    ----------
    bonus_zip_file : `str`
        The path to the downloaded DM+D bonus zip archive. This should not be
        renamed or extracted. It should be exactly as downloaded from TRUD.
        it will have a format like:
        ``nhsbsa_dmdbonus_<major version>.<minor version>.<patch>_YYYYMMDD<some other digits>.zip``
    tmpdir : `str` or `NoneType`, optional, default: `NoneType`
        The tmp directory to extract the ZIP archive into. If ``NoneType`` then
        the system tmp directory is used. A tmp directory will be created in
        this directory for the extraction and will be deleted if this errors
        out.

    Returns
    -------
    file_info : `dict`
        The information extracted from the XML file and the bonus zip file.
        The keys are file type names (str) and the values are `FileInfo`
        `namedtuples`.

    Raises
    ------
    FileNotFoundError
        If the sub-zip archive can't be found.
    """
    bonus_out = tempfile.mkdtemp(dir=tmpdir)

    try:
        # Extract the information from the zip file name
        bonus_zip_info = get_zip_file_data(bonus_zip_file)

        # Now extract the zip files from the archive
        unzipped = extract_data(bonus_zip_file, out_dir=bonus_out)

        # The actual XML file is in a sub zip archive for some reason!?
        # so we search for that and extract again if we find it
        sub_zip_file = [i for i in unzipped if i.endswith("BNF.zip")]
        if len(sub_zip_file) != 1:
            raise FileNotFoundError("can't find sub-zip")
        unzipped = extract_data(sub_zip_file[0], out_dir=bonus_out)

        # Get all the required data from the filenames
        file_data = get_file_data(bonus_out, expected=_BONUS_EXP_FILES)
        file_data[bonus_zip_info.type] = bonus_zip_info
        return file_data
    except Exception:
        shutil.rmtree(bonus_out)
        raise


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def load_file_info(sm, *files):
    """Load the file information into the database. These serve as a reference
    for version numbers.

    Parameters
    ----------
    sm : `sqlalchemy.Sessionmaker`
        The SQLAchemy session maker to use to create sessions.
    *files
        One or more `dmd.build.FileInfo` `namedtuples`.
    """
    session = sm()

    for i in files:
        row = orm.FileInfo(
            file_type=i.type,
            file_basename=os.path.basename(i.path),
            file_version=i.version,
            file_date=i.date
        )
        session.add(row)

    session.commit()
    session.close()


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def load_lookups(sm, file_info, verbose=False):
    """Load the data from the lookup XML file into the database.

    Parameters
    ----------
    sm : `sqlalchemy.Sessionmaker`
        The SQLAchemy session maker to use to create sessions.
    file_info : `FileInfo`
        A ``FileInfo`` named tuple with the ``type`` attribute ``lookup``.
    verbose : `bool`, optional, default: `False`
        Show load progress using tqdm.
    """
    if file_info.type != 'lookup':
        raise ValueError("not a lookup XML file")
    _load_multi_table(sm, file_info, verbose=verbose)


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def load_vtm(sm, file_info, verbose=False):
    """Load the data from the virtual therapeutic moiety (vtm) XML file into
    the database.

    Parameters
    ----------
    sm : `sqlalchemy.Sessionmaker`
        The SQLAchemy session maker to use to create sessions.
    file_info : `FileInfo`
        A ``FileInfo`` named tuple with the ``type`` attribute ``vtm``.
    verbose : `bool`, optional, default: `False`
        Show load progress using tqdm.
    """
    if file_info.type != 'vtm':
        raise ValueError("not a lookup XML file")
    _load_single_table(sm, file_info, verbose=verbose)


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def load_ingredient(sm, file_info, verbose=False):
    """Load the data from the ingredient XML file into the database.

    Parameters
    ----------
    sm : `sqlalchemy.Sessionmaker`
        The SQLAchemy session maker to use to create sessions.
    file_info : `FileInfo`
        A ``FileInfo`` named tuple with the ``type`` attribute ``ingredient``.
    verbose : `bool`, optional, default: `False`
        Show load progress using tqdm.
    """
    if file_info.type != 'ingredient':
        raise ValueError("not a lookup XML file")
    _load_single_table(sm, file_info, verbose=verbose)


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def load_vmp(sm, file_info, verbose=False):
    """Load the data from the virtual medicinal product (vmp) XML file into
    the database.

    Parameters
    ----------
    sm : `sqlalchemy.Sessionmaker`
        The SQLAchemy session maker to use to create sessions.
    file_info : `FileInfo`
        A ``FileInfo`` named tuple with the ``type`` attribute ``vmp``.
    verbose : `bool`, optional, default: `False`
        Show load progress using tqdm.
    """
    if file_info.type != 'vmp':
        raise ValueError("not a lookup XML file")

    _load_multi_table(
        sm, file_info,
        verbose=verbose
    )


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def load_vmpp(sm, file_info, verbose=False):
    """Load the data from the virtual medicinal product pack (vmpp) XML file
    into the database.

    Parameters
    ----------
    sm : `sqlalchemy.Sessionmaker`
        The SQLAchemy session maker to use to create sessions.
    file_info : `FileInfo`
        A ``FileInfo`` named tuple with the ``type`` attribute ``vmpp``.
    verbose : `bool`, optional, default: `False`
        Show load progress using tqdm.
    """
    if file_info.type != 'vmpp':
        raise ValueError("not a lookup XML file")
    _load_multi_table(
        sm, file_info,
        verbose=verbose
    )


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def load_amp(sm, file_info, verbose=False):
    """Load the data from the actual medicinal product (amp) XML file into
    the database.

    Parameters
    ----------
    sm : `sqlalchemy.Sessionmaker`
        The SQLAchemy session maker to use to create sessions.
    file_info : `FileInfo`
        A ``FileInfo`` named tuple with the ``type`` attribute ``amp``.
    verbose : `bool`, optional, default: `False`
        Show load progress using tqdm.
    """
    if file_info.type != 'amp':
        raise ValueError("not a lookup XML file")
    _load_multi_table(
        sm, file_info,
        verbose=verbose
    )


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def load_ampp(sm, file_info, verbose=False):
    """Load the data from the actual medicinal product pack (ampp) XML file
    into the database.

    Parameters
    ----------
    sm : `sqlalchemy.Sessionmaker`
        The SQLAchemy session maker to use to create sessions.
    file_info : `FileInfo`
        A ``FileInfo`` named tuple with the ``type`` attribute ``ampp``.
    verbose : `bool`, optional, default: `False`
        Show load progress using tqdm.
    """
    if file_info.type != 'ampp':
        raise ValueError("not a lookup XML file")
    _load_multi_table(
        sm, file_info,
        verbose=verbose
    )


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def load_bnf(sm, file_info, verbose=False):
    """Load the data from the mapping bonus (bnf) XML file into the database.

    Parameters
    ----------
    sm : `sqlalchemy.Sessionmaker`
        The SQLAchemy session maker to use to create sessions.
    file_info : `FileInfo`
        A ``FileInfo`` named tuple with the ``type`` attribute ``bnf``.
    verbose : `bool`, optional, default: `False`
        Show load progress using tqdm.
    """
    if file_info.type != 'bnf':
        raise ValueError("not a lookup XML file")
    _load_multi_table(
        sm, file_info,
        verbose=verbose
    )


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def _load_multi_table(sm, file_info, debug=None, verbose=False):
    """Load a all the tables from the XML file. This is used by XML files that
    only contain data for multiple tables, which are sub elements from the
    root element.

    Parameters
    ----------
    sm : `sqlalchemy.Sessionmaker`
        The SQLAchemy session maker to use to create sessions.
    file_info : `FileInfo`
        A ``FileInfo`` named tuple with the ``type`` attribute ``zip`` or
        ``bonus_zip.
    debug : `NoneType` or `list` of `str`, optional, default: `NoneType`
        Only load data from the XML elements in the debug list. This will
        eventually be removed but is handy to have at the moment. If
        ``NoneType``, then all tables will be loaded.
    verbose : `bool`, optional, default: `False`
        Show load progress using tqdm.

    Notes
    -----
    The session will commit after each has been added.
    """

    session = sm()
    tree = ElementTree.parse(file_info.path)
    root = tree.getroot()
    for section in root:
        table_name = section.tag

        # So I can test individual tables
        if debug is not None and table_name not in debug:
            continue

        orm_class = orm.MAPPINGS[file_info.type][table_name]

        # try:
        #     orm_class.__table__.create(bind=session.get_bind())
        # except Exception as e:
        #     print(e)
        #     pass

        for entry in tqdm(
            section, disable=not verbose, unit=' rows',
            ncols=0, dynamic_ncols=False,
            desc='[info] loading {0}.{1}'.format(file_info.type, table_name)
        ):
            row = dict()
            for field in entry:
                attr, parser = orm_class.XML_MAPPINGS[field.tag]
                row[attr] = parser(field.text)
            session.add(orm_class(**row))
            # pp.pprint(row)
        session.commit()
    session.close()


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def _load_single_table(sm, file_info, verbose=False):
    """Load a single table from the XML file. This is used by XML files that
    only contain data for a single table.

    Parameters
    ----------
    sm : `sqlalchemy.Sessionmaker`
        The SQLAchemy session maker to use to create sessions.
    file_info : `FileInfo`
        A ``FileInfo`` named tuple with the ``type`` attribute ``zip`` or
        ``bonus_zip.
    verbose : `bool`, optional, default: `False`
        Show load progress using tqdm.

    Notes
    -----
    The session will commit after the whole table has been added to it.
    """
    session = sm()
    tree = ElementTree.parse(file_info.path)
    section = tree.getroot()

    table_name = section.tag
    orm_class = orm.MAPPINGS[file_info.type][table_name]
    for entry in tqdm(
        section, disable=not verbose, unit=' rows',
        ncols=0, dynamic_ncols=False,
        desc='[info] loading {0}.{1}'.format(file_info.type, table_name)
    ):
        row = dict()
        for field in entry:
            attr, parser = orm_class.XML_MAPPINGS[field.tag]
            row[attr] = parser(field.text)
        session.add(orm_class(**row))
    session.commit()
    session.close()


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def extract_data(inzip, out_dir):
    """Extract all the files from the ZIP archive.

    Parameters
    ----------
    inzip : `str`
        The input ZIP archive
    out_dir : `str`
        The directory to extract the ZIP archive into

    Returns
    -------
    extracted_files : `list` or `str`
        the paths to the files extracted from the archive.
    """
    start_files = glob.glob(os.path.join(out_dir, "*"))
    with ZipFile(inzip, 'r') as zip:
        # extract all files to another directory
        zip.extractall(out_dir)
    end_files = glob.glob(os.path.join(out_dir, "*"))
    return [i for i in end_files if i not in start_files]


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def get_zip_file_data(zip_file):
    """Extract the version and file type information from the file name of the
    zip file.

    Parameters
    ----------
    zip_file : `str`
        The path to the zip file.

    Returns
    -------
    file_info : `FileInfo`
        A ``FileInfo`` named tuple with the ``type`` attribute ``zip`` or
        ``bonus_zip``.

    Notes
    -----
    The expectation is that the zip file base name will have either of the
    structures below:

    ``nhsbsa_dmd_<major version>.<minor version>.<patch>_YYYYMMDD<some other digits>.zip``
    ``nhsbsa_dmdbonus_<major version>.<minor version>.<patch>_YYYYMMDD<some other digits>.zip``
    """
    # /data/dmd/nhsbsa_dmd_11.4.0_20211129000001.zip
    fn_regex = re.compile(
        r"""
        nhsbsa_dmd(?P<BONUS>bonus)?_ # prefix
        (?P<MAJOR>\d+)\. # The major version number and delimiter
        (?P<MINOR>\d+)\. # The minor version number
        (?P<PATCH>\d+)_ # The patch version number
        (?P<DATE>\d{8})\d+\.zip # Date and file extension
        """,
        re.VERBOSE
    )

    # https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior
    date_format = "%Y%m%d"

    m = fn_regex.match(os.path.basename(zip_file))
    date = datetime.strptime(m.group("DATE"), date_format)
    version = "{0}.{1}.{2}".format(
        m.group("MAJOR"), m.group("MINOR"), m.group("PATCH")
    )

    file_type = 'zip'
    if m.group("BONUS"):
        file_type = 'bonus_zip'

    fi = FileInfo(path=zip_file, type=file_type, version=version,
                  date=date)
    return fi


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def get_file_data(data_dir, expected=_EXP_FILES):
    """Extract XML file type and version information from the XML file name.

    Parameters
    ----------
    data_dir : `str`
        The directory containing the XML files
    expected : `int`, optional, default: `7`
        The number of expected XML files. This defaults to the number expected
        in the main file but is adjustable so this function can be used to
        extract information from the bonus file.

    Returns
    -------
    file_info : `dict`
        The information extracted from the XML files. The keys are file type
        names (str) and the values are `FileInfo` `namedtuples`.

    Raises
    ------
    FileNotFoundError
        If we do not file the expected number of XML files or do not parse
        the data out from the expected number of files.
    """
    glob_str = os.path.join(data_dir, "*.xml")
    files = glob.glob(glob_str)

    if len(files) != expected:
        raise FileNotFoundError(
            "expected '{0}' files got '{1}'".format(expected, len(files))
        )

    fn_regex = re.compile(
        r"""
        f_(?P<FT>bnf|vtm|amp|vmpp|ingredient|lookup|ampp|vmp) # The file type
        (?P<MAJOR>\d+)_ # The major version number and delimiter
        (?P<MINOR>\d+) # The minor version number
        (?P<DATE>\d{6})\.xml # Date and file extension
        """,
        re.VERBOSE
    )

    # https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior
    date_format = "%d%m%y"
    file_info = dict()

    for i in files:
        m = fn_regex.match(os.path.basename(i))
        date = datetime.strptime(m.group("DATE"), date_format)
        version = "{0}.{1}".format(m.group("MAJOR"), m.group("MINOR"))
        fi = FileInfo(path=i, type=m.group('FT'), version=version, date=date)
        file_info[fi.type] = fi

    # Test the file info to see if we have the expected count
    if len(file_info) != expected:
        raise FileNotFoundError(
            "expected '{0}' files parsed '{1}'".format(
                expected, len(file_info)
            )
        )

    return file_info


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def _init_cmd_args():
    """Initialise the command line arguments and return the parser.

    Returns
    -------
    args : `argparse.ArgumentParser`
        The argparse parser object with arguments added.
    """
    parser = argparse.ArgumentParser(
        description="Build a relational database from the NHS DM+D data that"
        " is stored within zipped XML files."
    )

    parser.add_argument('indata',
                        type=str,
                        help="The input zip archive downloaded from NHS "
                        "TRUD, do not unzip")
    parser.add_argument('dburl',
                        nargs='?',
                        type=str,
                        help="An SQLAlchemy connection URL or filename if "
                        "using SQLite. If you do not want to put full "
                        "connection parameters on the cmd-line use the "
                        "config file to supply the parameters")
    parser.add_argument('-c', '--config',
                        type=str,
                        default="~/{0}".format(
                            os.path.basename(common._DEFAULT_CONFIG)
                        ),
                        help="The location of the config file")
    parser.add_argument('-B', '--bonus-files',
                        type=str,
                        help="The location of the DM+D bonus files")
    parser.add_argument('-s', '--config-section',
                        type=str,
                        default=common._DEFAULT_SECTION,
                        help="The section name in the config file")
    parser.add_argument('-T', '--tmp',
                        type=str,
                        help="The location of tmp, if not provided will "
                        "use the system tmp")
    parser.add_argument('-v', '--verbose',  action="store_true",
                        help="give more output")
    parser.add_argument('-e', '--exists',  action="store_true",
                        help="The database already exists, this will assume"
                        " that the user has pre-created a database and will "
                        "not throw errors if the DB/tables already exist ")

    return parser


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def _parse_cmd_args(parser):
    """Parse the command line arguments.

    Parameters
    ----------
    parser : :obj:`argparse.ArgumentParser`
        The argparse parser object with arguments added.

    Returns
    -------
    args : :obj:`argparse.Namespace`
        The argparse namespace object containing the arguments
    """
    args = parser.parse_args()

    # Because the default is ~/ for Sphinx docs
    args.config = os.path.expanduser(args.config)
    return args


# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
if __name__ == '__main__':
    main()
