Completed
Push — master ( b89757...67321d )
by Satoru
01:13
created

_do_query()   A

Complexity

Conditions 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
c 1
b 0
f 1
dl 0
loc 7
rs 9.4285
1
#
2
# Author: Satoru SATOH <ssato redhat.com>
3
# License: MIT
4
#
5
"""CLI frontend module for anyconfig.
6
"""
7
from __future__ import absolute_import, print_function
8
9
import codecs
10
import locale
11
import logging
12
import optparse
13
import os
14
import sys
15
16
import anyconfig.api as API
17
import anyconfig.compat
18
import anyconfig.globals
19
import anyconfig.mdicts
20
import anyconfig.parser
21
22
23
_ENCODING = locale.getdefaultlocale()[1]
24
25
LOGGER = logging.getLogger("anyconfig")
26
LOGGER.addHandler(logging.StreamHandler())
27
LOGGER.setLevel(logging.WARN)
28
29
if anyconfig.compat.IS_PYTHON_3:
30
    import io
31
32
    _ENCODING = _ENCODING.lower()
33
34
    # TODO: What should be done for an error, "AttributeError: '_io.StringIO'
35
    # object has no attribute 'buffer'"?
36
    try:
37
        sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding=_ENCODING)
38
        sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding=_ENCODING)
39
    except AttributeError:
40
        pass
41
else:
42
    sys.stdout = codecs.getwriter(_ENCODING)(sys.stdout)
43
    sys.stderr = codecs.getwriter(_ENCODING)(sys.stderr)
44
45
USAGE = """\
46
%prog [Options...] CONF_PATH_OR_PATTERN_0 [CONF_PATH_OR_PATTERN_1 ..]
47
48
Examples:
49
  %prog --list  # -> Supported config types: configobj, ini, json, ...
50
  # Merge and/or convert input config to output config [file]
51
  %prog -I yaml -O yaml /etc/xyz/conf.d/a.conf
52
  %prog -I yaml '/etc/xyz/conf.d/*.conf' -o xyz.conf --otype json
53
  %prog '/etc/xyz/conf.d/*.json' -o xyz.yml \\
54
    --atype json -A '{"obsoletes": "syscnf", "conflicts": "syscnf-old"}'
55
  %prog '/etc/xyz/conf.d/*.json' -o xyz.yml \\
56
    -A obsoletes:syscnf;conflicts:syscnf-old
57
  %prog /etc/foo.json /etc/foo/conf.d/x.json /etc/foo/conf.d/y.json
58
  %prog '/etc/foo.d/*.json' -M noreplace
59
  # Get/set part of input config
60
  %prog '/etc/foo.d/*.json' --get a.b.c
61
  %prog '/etc/foo.d/*.json' --set a.b.c=1
62
  # Validate with JSON schema or generate JSON schema:
63
  %prog --validate -S foo.conf.schema.yml '/etc/foo.d/*.xml'
64
  %prog --gen-schema '/etc/foo.d/*.xml' -o foo.conf.schema.yml"""
65
66
DEFAULTS = dict(loglevel=1, list=False, output=None, itype=None,
67
                otype=None, atype=None, merge=API.MS_DICTS,
68
                ignore_missing=False, template=False, env=False,
69
                schema=None, validate=False, gen_schema=False)
70
71
72
def to_log_level(level):
73
    """
74
    :param level: Logging level in int = 0 .. 2
75
76
    >>> to_log_level(0) == logging.WARN
77
    True
78
    >>> to_log_level(5)  # doctest: +IGNORE_EXCEPTION_DETAIL, +ELLIPSIS
79
    Traceback (most recent call last):
80
        ...
81
    ValueError: wrong log level passed: 5
82
    >>>
83
    """
84
    if not (level >= 0 and level < 3):
85
        raise ValueError("wrong log level passed: " + str(level))
86
87
    return [logging.WARN, logging.INFO, logging.DEBUG][level]
88
89
90
_ATYPE_HELP_FMT = """\
91
Explicitly select type of argument to provide configs from %s.
92
93
If this option is not set, original parser is used: 'K:V' will become {K: V},
94
'K:V_0,V_1,..' will become {K: [V_0, V_1, ...]}, and 'K_0:V_0;K_1:V_1' will
95
become {K_0: V_0, K_1: V_1} (where the tyep of K is str, type of V is one of
96
Int, str, etc."""
97
98
_QUERY_HELP = ("Query with JMESPath expression language. See "
99
               "http://jmespath.org for more about JMESPath expression. "
100
               "This option is not used with --get option at the same time. "
101
               "Please note that python module to support JMESPath "
102
               "expression (https://pypi.python.org/pypi/jmespath/) is "
103
               "required to use this option")
104
_GET_HELP = ("Specify key path to get part of config, for example, "
105
             "'--get a.b.c' to config {'a': {'b': {'c': 0, 'd': 1}}} "
106
             "gives 0 and '--get a.b' to the same config gives "
107
             "{'c': 0, 'd': 1}. Path expression can be JSON Pointer "
108
             "expression (http://tools.ietf.org/html/rfc6901) such like "
109
             "'', '/a~1b', '/m~0n'. "
110
             "This option is not used with --query option at the same time. ")
111
_SET_HELP = ("Specify key path to set (update) part of config, for "
112
             "example, '--set a.b.c=1' to a config {'a': {'b': {'c': 0, "
113
             "'d': 1}}} gives {'a': {'b': {'c': 1, 'd': 1}}}.")
114
115
116
def parse_args(argv=None, defaults=None):
117
    """
118
    Make up an option and arguments parser.
119
120
    :param defaults: Default option values
121
    """
122
    if defaults is None:
123
        defaults = DEFAULTS
124
125
    ctypes = API.list_types()
126
    ctypes_s = ", ".join(ctypes)
127
    type_help = "Select type of %s config files from " + \
128
        ctypes_s + " [Automatically detected by file ext]"
129
130
    mts = API.MERGE_STRATEGIES
131
    mts_s = ", ".join(mts)
132
    mt_help = "Select strategy to merge multiple configs from " + \
133
        mts_s + " [%(merge)s]" % defaults
134
135
    parser = optparse.OptionParser(USAGE, version="%%prog %s" %
136
                                   anyconfig.globals.VERSION)
137
    parser.set_defaults(**defaults)
138
139
    lpog = optparse.OptionGroup(parser, "List specific options")
140
    lpog.add_option("-L", "--list", help="List supported config types",
141
                    action="store_true")
142
    parser.add_option_group(lpog)
143
144
    spog = optparse.OptionGroup(parser, "Schema specific options")
145
    spog.add_option("", "--validate", action="store_true",
146
                    help="Only validate input files and do not output. "
147
                         "You must specify schema file with -S/--schema "
148
                         "option.")
149
    spog.add_option("", "--gen-schema", action="store_true",
150
                    help="Generate JSON schema for givne config file[s] "
151
                         "and output it instead of (merged) configuration.")
152
    parser.add_option_group(spog)
153
154
    gspog = optparse.OptionGroup(parser, "Query/Get/set options")
155
    gspog.add_option("-Q", "--query", help=_QUERY_HELP)
156
    gspog.add_option("", "--get", help=_GET_HELP)
157
    gspog.add_option("", "--set", help=_SET_HELP)
158
    parser.add_option_group(gspog)
159
160
    parser.add_option("-o", "--output", help="Output file path")
161
    parser.add_option("-I", "--itype", choices=ctypes,
162
                      help=(type_help % "Input"))
163
    parser.add_option("-O", "--otype", choices=ctypes,
164
                      help=(type_help % "Output"))
165
    parser.add_option("-M", "--merge", choices=mts, help=mt_help)
166
    parser.add_option("-A", "--args", help="Argument configs to override")
167
    parser.add_option("", "--atype", choices=ctypes,
168
                      help=_ATYPE_HELP_FMT % ctypes_s)
169
170
    parser.add_option("-x", "--ignore-missing", action="store_true",
171
                      help="Ignore missing input files")
172
    parser.add_option("-T", "--template", action="store_true",
173
                      help="Enable template config support")
174
    parser.add_option("-E", "--env", action="store_true",
175
                      help="Load configuration defaults from "
176
                           "environment values")
177
    parser.add_option("-S", "--schema", help="Specify Schema file[s] path")
178
    parser.add_option("-s", "--silent", action="store_const", dest="loglevel",
179
                      const=0, help="Silent or quiet mode")
180
    parser.add_option("-q", "--quiet", action="store_const", dest="loglevel",
181
                      const=0, help="Same as --silent option")
182
    parser.add_option("-v", "--verbose", action="store_const", dest="loglevel",
183
                      const=2, help="Verbose mode")
184
185
    if argv is None:
186
        argv = sys.argv
187
188
    (options, args) = parser.parse_args(argv[1:])
189
    return (parser, options, args)
190
191
192
def _exit_with_output(content, exit_code=0):
193
    """
194
    Exit the program with printing out messages.
195
196
    :param content: content to print out
197
    :param exit_code: Exit code
198
    """
199
    (sys.stdout if exit_code == 0 else sys.stderr).write(content + "\n")
200
    sys.exit(exit_code)
201
202
203
def _check_options_and_args(parser, options, args):
204
    """
205
    Show supported config format types or usage.
206
207
    :param parser: Option parser object
208
    :param options: Options optparse.OptionParser.parse_args returns
209
    :param args: Arguments optparse.OptionParser.parse_args returns
210
    """
211
    if not args:
212
        if options.list:
213
            tlist = ", ".join(API.list_types())
214
            _exit_with_output("Supported config types: " + tlist)
215
        elif options.env:
216
            cnf = API.to_container(os.environ.copy())
217
            _output_result(cnf, options.output, options.otype or "json",
218
                           None, None)
219
            sys.exit(0)
220
        else:
221
            parser.print_usage()
222
            sys.exit(1)
223
224
    if options.validate and options.schema is None:
225
        _exit_with_output("--validate option requires --scheme option", 1)
226
227
228
def _exit_if_load_failure(cnf, msg):
229
    """
230
    :param cnf: Loaded configuration object or None indicates load failure
231
    :param msg: Message to print out if failure
232
    """
233
    if cnf is None:
234
        _exit_with_output(msg, 1)
235
236
237
def _exit_if_only_to_validate(only_to_validate):
238
    """
239
    :param only_to_validate: True if it's only to validate
240
    """
241
    if only_to_validate:
242
        _exit_with_output("Validation succeds")
243
244
245
def _do_query(cnf, exp):
246
    """
247
    :param cnf: Configuration object to print out
248
    :param exp: A string represents JMESPath expression to query loaded data
249
    :return: Query result object if no error
250
    """
251
    return API.query(cnf, exp)
252
253
254
def _do_get(cnf, get_path):
255
    """
256
    :param cnf: Configuration object to print out
257
    :param get_path: key path given in --get option
258
    :return: updated Configuration object if no error
259
    """
260
    (cnf, err) = API.get(cnf, get_path)
261
    if cnf is None:  # Failed to get the result.
262
        _exit_with_output("Failed to get result: err=%s" % err, 1)
263
264
    return cnf
265
266
267
def _output_type_by_input_path(inpath, itype, fmsg):
268
    """
269
    :param inpath: Input file path
270
    :param itype: Input type or None
271
    :param fmsg: message if it cannot detect otype by `inpath`
272
    :return: Output type :: str
273
    """
274
    msg = ("Specify inpath and/or outpath type[s] with -I/--itype "
275
           "or -O/--otype option explicitly")
276
    if itype is None:
277
        try:
278
            otype = API.find_loader(inpath).type()
279
        except API.UnknownFileTypeError:
280
            _exit_with_output((fmsg % inpath) + msg, 1)
281
        except ValueError:
282
            _exit_with_output(msg, 1)
283
    else:
284
        otype = itype
285
286
    return otype
287
288
289
def _try_dump(cnf, outpath, otype, fmsg):
290
    """
291
    :param cnf: Configuration object to print out
292
    :param outpath: Output file path or None
293
    :param otype: Output type or None
294
    :param fmsg: message if it cannot detect otype by `inpath`
295
    """
296
    try:
297
        API.dump(cnf, outpath, otype)
298
    except API.UnknownFileTypeError:
299
        _exit_with_output(fmsg % outpath, 1)
300
    except API.UnknownParserTypeError:
301
        _exit_with_output("Invalid output type '%s'" % otype, 1)
302
303
304
def _output_result(cnf, outpath, otype, inpath, itype):
305
    """
306
    :param cnf: Configuration object to print out
307
    :param outpath: Output file path or None
308
    :param otype: Output type or None
309
    :param inpath: Input file path
310
    :param itype: Input type or None
311
    """
312
    fmsg = ("Uknown file type and cannot detect appropriate backend "
313
            "from its extension, '%s'")
314
315
    if not anyconfig.mdicts.is_dict_like(cnf):
316
        _exit_with_output(str(cnf))  # Print primitive types as it is.
317
318
    if not outpath or outpath == "-":
319
        outpath = sys.stdout
320
        if otype is None:
321
            otype = _output_type_by_input_path(inpath, itype, fmsg)
322
323
    _try_dump(cnf, outpath, otype, fmsg)
324
325
326
def _load_diff(args, options):
327
    """
328
    :param args: Extra argument list
329
    :param options: :class:`optparse.Values` object
330
    """
331
    try:
332
        diff = API.load(args, options.itype,
333
                        ignore_missing=options.ignore_missing,
334
                        ac_merge=options.merge,
335
                        ac_template=options.template,
336
                        ac_schema=options.schema)
337
    except API.UnknownParserTypeError:
338
        _exit_with_output("Wrong input type '%s'" % options.itype, 1)
339
    except API.UnknownFileTypeError:
340
        _exit_with_output("No appropriate backend was found for given file "
341
                          "'%s'" % options.itype, 1)
342
    _exit_if_load_failure(diff, "Failed to load: args=%s" % ", ".join(args))
343
344
    return diff
345
346
347
def main(argv=None):
348
    """
349
    :param argv: Argument list to parse or None (sys.argv will be set).
350
    """
351
    (parser, options, args) = parse_args(argv=argv)
352
    LOGGER.setLevel(to_log_level(options.loglevel))
353
354
    _check_options_and_args(parser, options, args)
355
356
    cnf = API.to_container(os.environ.copy() if options.env else {})
357
    diff = _load_diff(args, options)
358
    cnf.update(diff)
359
360
    if options.args:
361
        diff = anyconfig.parser.parse(options.args)
362
        cnf.update(diff)
363
364
    _exit_if_only_to_validate(options.validate)
365
366
    if options.gen_schema:
367
        cnf = API.gen_schema(cnf)
368
369
    if options.query:
370
        cnf = _do_query(cnf, options.query)
371
    elif options.get:
372
        cnf = _do_get(cnf, options.get)
373
374
    if options.set:
375
        (key, val) = options.set.split('=')
376
        API.set_(cnf, key, anyconfig.parser.parse(val))
377
378
    _output_result(cnf, options.output, options.otype, args[0], options.itype)
379
380
381
if __name__ == '__main__':
382
    main(sys.argv)
383
384
# vim:sw=4:ts=4:et:
385