argparse_wrapper.py 7.92 KB
Newer Older
Paulo Medeiros's avatar
Paulo Medeiros committed
1
#!/usr/bin/env python3
2
"""Wrappers for argparse functionality."""
3
4
import argparse
import os
Paulo Medeiros's avatar
Paulo Medeiros committed
5
import subprocess
6
7
8
from datetime import datetime
from pathlib import Path

9
from . import __version__
10
from .commands_functions import (
11
    cluster_obs_single_dtg,
Paulo Medeiros's avatar
Paulo Medeiros committed
12
    csv2obsoul,
Paulo Medeiros's avatar
Paulo Medeiros committed
13
    select_stations,
14
    show,
15
16
17
)


Paulo Medeiros's avatar
Paulo Medeiros committed
18
class StoreDictKeyPair(argparse.Action):
19
    """Enable args="key1=val1, ..., keyN=valN" in command line args."""
20

21
22
    # Source: <https://stackoverflow.com/questions/29986185/
    #          python-argparse-dict-arg/42355279>
Paulo Medeiros's avatar
Paulo Medeiros committed
23
24

    def __init__(self, option_strings, dest, nargs=None, **kwargs):
25
        """Initialise nargs."""
Paulo Medeiros's avatar
Paulo Medeiros committed
26
        self._nargs = nargs
27
        super().__init__(option_strings, dest, nargs=nargs, **kwargs)
Paulo Medeiros's avatar
Paulo Medeiros committed
28
29

    def __call__(self, parser, namespace, values, option_string=None):
30
        """Get a key/value dict from passed string."""
Paulo Medeiros's avatar
Paulo Medeiros committed
31
        my_dict = {}
32
33
34
        for k_v in values:
            key, val = k_v.split("=")
            my_dict[key] = val
Paulo Medeiros's avatar
Paulo Medeiros committed
35
36
37
        setattr(namespace, self.dest, my_dict)


38
def get_parsed_args(program_name):
39
40
41
42
43
44
45
46
47
    """Get parsed command line arguments.

    Args:
        program_name (str): The name of the program.

    Returns:
        argparse.Namespace: Parsed command line arguments.

    """
48
49
50
51
52
53
54
55
56
    # Define main parser and general options
    parser = argparse.ArgumentParser(
        prog=program_name,
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )

    try:
        fpath = Path(os.getenv("NETATMOQC_CONFIG_PATH", "config.toml"))
        default_conf_path = fpath.resolve(strict=True)
57
    except FileNotFoundError:
58
59
60
        default_conf_path = (
            Path(os.getenv("HOME")) / ".netatmoqc" / "config.toml"
        )
61
62
63
    parser.add_argument(
        "--version", "-v", action="version", version="%(prog)s v" + __version__
    )
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
    parser.add_argument(
        "-config_file",
        metavar="CONFIG_FILE_PATH",
        default=default_conf_path,
        type=Path,
        help=(
            "Path to the config file. The default is whichever of the "
            + "following is first encountered: "
            + "(i) The value of the 'NETATMOQC_CONFIG_PATH' envvar or "
            + "(ii) './config.toml'. If both (i) and (ii) are missing, "
            + "then the default will become "
            + "'"
            + str(Path("$HOME/.netatmoqc/config.toml"))
            + "'"
        ),
    )
    parser.add_argument(
        "-loglevel",
        default="info",
        choices=["critical", "error", "warning", "info", "debug", "notset"],
        help="What type of info should be printed to the log",
    )
    parser.add_argument(
        "--mpi",
        action="store_true",
        help=(
            "Enable MPI parallelisation in some parts of the code. "
            + "Requires that the code be installed with support to MPI."
        ),
    )

    # Configure the main parser to handle the commands
    # From python v3.7, add_subparsers accepts a 'required' arg. We
    # could use required=True in the definition below, but, since we
    # want to be able to use python >= 3.6.10, we won't. The requirement
    # of having a command passed will be enforced in the main.py file.
    subparsers = parser.add_subparsers(
        title="commands",
        dest="command",
        description=(
            "Valid commands for {0} (note that commands also accept their "
            + "own arguments, in particular [-h]):"
        ).format(program_name),
        help="command description",
    )

Paulo Medeiros's avatar
Paulo Medeiros committed
110
111
112
    ##############################################
    # Configure parser for the "cluster" command #
    ##############################################
113
114
115
116
117
118
119
120
121
122
123
    parser_cluster = subparsers.add_parser(
        "cluster", help="Run cluster on NetAtmo obs for a single DTG"
    )
    parser_cluster.add_argument(
        "-dtg",
        type=lambda s: datetime.strptime(s, "%Y%m%d%H"),
        default=None,
        help="""
        If not passed, then the first DTG in the config file will be used.
        """,
    )
124
125
126
127
128
129
130
131
132
133
    parser_cluster.add_argument(
        "--show",
        action="store_true",
        help="Open a browser window to show results on a map",
    )
    parser_cluster.add_argument(
        "--savefig",
        action="store_true",
        help="Save an html file showing results on a map",
    )
134
135
    parser_cluster.set_defaults(func=cluster_obs_single_dtg)

Paulo Medeiros's avatar
Paulo Medeiros committed
136
137
138
    #############################################
    # Configure parser for the "select" command #
    #############################################
139
140
    parser_select = subparsers.add_parser(
        "select",
141
142
143
144
        help=(
            "Go over a list of DTGs and use clustering to select NetAtmo "
            + "stations that are reliable throughout the survey"
        ),
145
    )
146
147
148
149
150
151
    parser_select.add_argument(
        "--save-obsoul",
        action="store_true",
        help="Re-read the input NetAtmo files and save parsed data in OBSOUL "
        + "format, keeping only data related to the the accepted stations",
    )
152
153
    parser_select.set_defaults(func=select_stations)

Paulo Medeiros's avatar
Paulo Medeiros committed
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
    #################################################
    # Configure parser for the "csv2obsoul" command #
    #################################################
    parser_csv2obsoul = subparsers.add_parser(
        "csv2obsoul",
        help="Convert data from NetAtmo CSV files into OBSOUL format",
    )
    parser_csv2obsoul.add_argument(
        "--rm-duplicate-stations",
        action="store_true",
        help="Remove duplicates of station reports within same DTG. It keeps "
        + "the one whose obs time is closest to the asismilation time",
    )
    parser_csv2obsoul.add_argument(
        "--rm-moving-stations",
        action="store_true",
        help="Remove stations that change any of (lat, lon, alt) within the "
        + "selected asismilation windows",
    )
    parser_csv2obsoul.add_argument(
        "--dropna",
        action="store_true",
        help="Drop NA (NaN, not a number) values",
    )
    parser_csv2obsoul.add_argument(
        "--fillna",
        # 0.1699999976e39 is obsoul's sentinel value
        default=0.1699999976e39,
        action=StoreDictKeyPair,
        nargs="+",
        metavar="data_column=fillna_value_for_col",
    )
186
    parser_csv2obsoul.add_argument(
187
        "--selected-stations-fpath",
188
189
190
191
192
193
194
195
196
        metavar="STATIONS_FILE_PATH",
        default=None,
        type=Path,
        help=(
            "Path to the optional selected stations file. If specified, "
            + "then only these stations are kept."
        ),
    )

Paulo Medeiros's avatar
Paulo Medeiros committed
197
198
199
200
201
    parser_csv2obsoul.set_defaults(func=csv2obsoul)

    ###########################################
    # Configure parser for the "apps" command #
    ###########################################
202
    parser_apps = subparsers.add_parser(
203
204
205
206
207
        "apps",
        help=(
            "Start a server for the provided Dash apps. "
            + "N.B.: The apps are meant to be used for tests only."
        ),
208
209
210
211
212
213
    )
    parser_apps.set_defaults(
        func=lambda args: subprocess.run(
            [
                Path(__file__).parent.resolve() / "bin" / "start_apps",
                args.config_file.resolve(),
214
215
            ],
            check=True,
216
217
218
        )
    )

Paulo Medeiros's avatar
Paulo Medeiros committed
219
220
221
    ###########################################
    # Configure parser for the "show" command #
    ###########################################
222
    parser_show = subparsers.add_parser(
223
224
        "show",
        help="Display results from netatmoqc output files, as well as configs",
225
226
227
228
229
230
    )
    parser_show.add_argument(
        "file_list",
        nargs="*",
        type=Path,
        default=list(Path(".").glob("*.csv")),
231
        help="List of files the results should be read from",
232
    )
233
234
235
236
237
    parser_show.add_argument(
        "--config",
        action="store_true",
        help="Dump the used configs, including defaults, in TOML format",
    )
238
239
240
241
242
    parser_show.add_argument(
        "--domain",
        action="store_true",
        help="Open a browser window and show the configured domain",
    )
243
244
    parser_show.set_defaults(func=show)

245
246
247
    args = parser.parse_args()

    return args