Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / utils / __init__.py @ 23963422

History | View | Annotate | Download (14.1 kB)

1
# Copyright 2011-2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
from sys import stdout, stdin
35
from re import compile as regex_compile
36
from time import sleep
37
from os import walk, path
38
from json import dumps
39

    
40
from kamaki.cli.errors import raiseCLIError
41

    
42

    
43
INDENT_TAB = 4
44

    
45

    
46
suggest = dict(ansicolors=dict(
47
        active=False,
48
        url='#install-ansicolors-progress',
49
        description='Add colors to console responses'))
50

    
51
try:
52
    from colors import magenta, red, yellow, bold
53
except ImportError:
54
    def dummy(val):
55
        return val
56
    red = yellow = magenta = bold = dummy
57
    suggest['ansicolors']['active'] = True
58

    
59

    
60
def _print(w):
61
    """Print wrapper is used to help unittests check what is printed"""
62
    print w
63

    
64

    
65
def _write(w):
66
    """stdout.write wrapper is used to help unittests check what is printed"""
67
    stdout.write(w)
68

    
69

    
70
def _flush():
71
    """stdout.flush wrapper is used to help unittests check what is called"""
72
    stdout.flush()
73

    
74

    
75
def _readline():
76
    """stdout.readline wrapper is used to help unittests"""
77
    return stdout.readline()
78

    
79

    
80
def suggest_missing(miss=None, exclude=[]):
81
    global suggest
82
    sgs = dict(suggest)
83
    for exc in exclude:
84
        try:
85
            sgs.pop(exc)
86
        except KeyError:
87
            pass
88
    kamaki_docs = 'http://www.synnefo.org/docs/kamaki/latest'
89
    for k, v in (miss, sgs[miss]) if miss else sgs.items():
90
        if v['active'] and stdout.isatty():
91
            print('Suggestion: for better user experience install %s' % k)
92
            print('\t%s' % v['description'])
93
            print('\tIt is easy, here are the instructions:')
94
            print('\t%s/installation.html%s' % (kamaki_docs, v['url']))
95
            print('')
96

    
97

    
98
def guess_mime_type(
99
        filename,
100
        default_content_type='application/octet-stream',
101
        default_encoding=None):
102
    assert filename, 'Cannot guess mimetype for empty filename'
103
    try:
104
        from mimetypes import guess_type
105
        ctype, cenc = guess_type(filename)
106
        return ctype or default_content_type, cenc or default_encoding
107
    except ImportError:
108
        print 'WARNING: Cannot import mimetypes, using defaults'
109
        return (default_content_type, default_encoding)
110

    
111

    
112
def remove_colors():
113
    global bold
114
    global red
115
    global yellow
116
    global magenta
117

    
118
    def dummy(val):
119
        return val
120
    red = yellow = magenta = bold = dummy
121

    
122

    
123
def pretty_keys(d, delim='_', recursive=False):
124
    """<term>delim<term> to <term> <term> transformation"""
125
    new_d = dict(d)
126
    for k, v in d.items():
127
        new_v = new_d.pop(k)
128
        new_d[k.replace(delim, ' ').strip()] = pretty_keys(
129
            new_v, delim, True) if (
130
                recursive and isinstance(v, dict)) else new_v
131
    return new_d
132

    
133

    
134
def print_json(data):
135
    """Print a list or dict as json in console
136

137
    :param data: json-dumpable data
138
    """
139
    _print(dumps(data, indent=INDENT_TAB))
140

    
141

    
142
def pretty_dict(d, *args, **kwargs):
143
    print_dict(pretty_keys(d, *args, **kwargs))
144

    
145

    
146
def print_dict(
147
        d,
148
        exclude=(), indent=0,
149
        with_enumeration=False, recursive_enumeration=False):
150
    """Pretty-print a dictionary object
151
    <indent>key: <non iterable item>
152
    <indent>key:
153
    <indent + INDENT_TAB><pretty-print iterable>
154

155
    :param d: (dict)
156

157
    :param exclude: (iterable of strings) keys to exclude from printing
158

159
    :param indent: (int) initial indentation (recursive)
160

161
    :param with_enumeration: (bool) enumerate 1st-level keys
162

163
    :param recursive_enumeration: (bool) recursively enumerate iterables (does
164
        not enumerate 1st level keys)
165

166
    :raises CLIError: if preconditions fail
167
    """
168
    assert isinstance(d, dict), 'print_dict input must be a dict'
169
    assert indent >= 0, 'print_dict indent must be >= 0'
170

    
171
    for i, (k, v) in enumerate(d.items()):
172
        k = ('%s' % k).strip()
173
        if k in exclude:
174
            continue
175
        print_str = ' ' * indent
176
        print_str += '%s.' % (i + 1) if with_enumeration else ''
177
        print_str += '%s:' % k
178
        if isinstance(v, dict):
179
            _print(print_str)
180
            print_dict(
181
                v, exclude, indent + INDENT_TAB,
182
                recursive_enumeration, recursive_enumeration)
183
        elif isinstance(v, list) or isinstance(v, tuple):
184
            _print(print_str)
185
            print_list(
186
                v, exclude, indent + INDENT_TAB,
187
                recursive_enumeration, recursive_enumeration)
188
        else:
189
            _print('%s %s' % (print_str, v))
190

    
191

    
192
def print_list(
193
        l,
194
        exclude=(), indent=0,
195
        with_enumeration=False, recursive_enumeration=False):
196
    """Pretty-print a list of items
197
    <indent>key: <non iterable item>
198
    <indent>key:
199
    <indent + INDENT_TAB><pretty-print iterable>
200

201
    :param l: (list)
202

203
    :param exclude: (iterable of strings) items to exclude from printing
204

205
    :param indent: (int) initial indentation (recursive)
206

207
    :param with_enumeration: (bool) enumerate 1st-level items
208

209
    :param recursive_enumeration: (bool) recursively enumerate iterables (does
210
        not enumerate 1st level keys)
211

212
    :raises CLIError: if preconditions fail
213
    """
214
    assert isinstance(l, list) or isinstance(l, tuple), (
215
        'print_list prinbts a list or tuple')
216
    assert indent >= 0, 'print_list indent must be >= 0'
217

    
218
    for i, item in enumerate(l):
219
        print_str = ' ' * indent
220
        print_str += '%s.' % (i + 1) if with_enumeration else ''
221
        if isinstance(item, dict):
222
            if with_enumeration:
223
                _print(print_str)
224
            elif i and i < len(l):
225
                _print('')
226
            print_dict(
227
                item, exclude,
228
                indent + (INDENT_TAB if with_enumeration else 0),
229
                recursive_enumeration, recursive_enumeration)
230
        elif isinstance(item, list) or isinstance(item, tuple):
231
            if with_enumeration:
232
                _print(print_str)
233
            elif i and i < len(l):
234
                _print()
235
            print_list(
236
                item, exclude, indent + INDENT_TAB,
237
                recursive_enumeration, recursive_enumeration)
238
        else:
239
            item = ('%s' % item).strip()
240
            if item in exclude:
241
                continue
242
            _print('%s%s' % (print_str, item))
243

    
244

    
245
def page_hold(index, limit, maxlen):
246
    """Check if there are results to show, and hold the page when needed
247
    :param index: (int) > 0, index of current element
248
    :param limit: (int) 0 < limit <= max, page hold if limit mod index == 0
249
    :param maxlen: (int) Don't hold if index reaches maxlen
250

251
    :returns: True if there are more to show, False if all results are shown
252
    """
253
    if index >= maxlen:
254
        return False
255
    if index and index % limit == 0:
256
        raw_input('(%s listed - %s more - "enter" to continue)' % (
257
            index, maxlen - index))
258
    return True
259

    
260

    
261
def print_items(
262
        items, title=('id', 'name'),
263
        with_enumeration=False, with_redundancy=False,
264
        page_size=0):
265
    """print dict or list items in a list, using some values as title
266
    Objects of next level don't inherit enumeration (default: off) or titles
267

268
    :param items: (list) items are lists or dict
269

270
    :param title: (tuple) keys to use their values as title
271

272
    :param with_enumeration: (boolean) enumerate items (order id on title)
273

274
    :param with_redundancy: (boolean) values in title also appear on body
275

276
    :param page_size: (int) show results in pages of page_size items, enter to
277
        continue
278
    """
279
    if not (isinstance(items, dict) or isinstance(items, list) or isinstance(
280
                items, tuple)):
281
        _print('%s' % items if items is not None else '')
282
        return
283

    
284
    page_size = int(page_size)
285
    try:
286
        page_size = page_size if page_size > 0 else len(items)
287
    except:
288
        page_size = len(items)
289
    num_of_pages = len(items) // page_size
290
    num_of_pages += 1 if len(items) % page_size else 0
291
    for i, item in enumerate(items):
292
        if with_enumeration:
293
            _write('%s. ' % (i + 1))
294
        if isinstance(item, dict):
295
            item = dict(item)
296
            title = sorted(set(title).intersection(item))
297
            pick = item.get if with_redundancy else item.pop
298
            header = ' '.join('%s' % pick(key) for key in title)
299
            _print(bold(header))
300
            print_dict(item, indent=INDENT_TAB)
301
        elif isinstance(item, list) or isinstance(item, tuple):
302
            print_list(item, indent=INDENT_TAB)
303
        else:
304
            _print(' %s' % item)
305
        page_hold(i + 1, page_size, len(items))
306

    
307

    
308
def format_size(size, decimal_factors=False):
309
    units = ('B', 'KB', 'MB', 'GB', 'TB') if decimal_factors else (
310
        'B', 'KiB', 'MiB', 'GiB', 'TiB')
311
    step = 1000 if decimal_factors else 1024
312
    fstep = float(step)
313
    try:
314
        size = float(size)
315
    except (ValueError, TypeError) as err:
316
        raiseCLIError(err, 'Cannot format %s in bytes' % (
317
            ','.join(size) if isinstance(size, tuple) else size))
318
    for i, unit in enumerate(units):
319
        if size < step or i + 1 == len(units):
320
            break
321
        size /= fstep
322
    s = ('%.2f' % size)
323
    s = s.replace('%s' % step, '%s.99' % (step - 1)) if size <= fstep else s
324
    while '.' in s and s[-1] in ('0', '.'):
325
        s = s[:-1]
326
    return s + unit
327

    
328

    
329
def to_bytes(size, format):
330
    """
331
    :param size: (float) the size in the given format
332
    :param format: (case insensitive) KiB, KB, MiB, MB, GiB, GB, TiB, TB
333

334
    :returns: (int) the size in bytes
335
    :raises ValueError: if invalid size or format
336
    :raises AttributeError: if format is not str
337
    :raises TypeError: if size is not arithmetic or convertible to arithmetic
338
    """
339
    format = format.upper()
340
    if format == 'B':
341
        return int(size)
342
    size = float(size)
343
    units_dc = ('KB', 'MB', 'GB', 'TB')
344
    units_bi = ('KIB', 'MIB', 'GIB', 'TIB')
345

    
346
    factor = 1024 if format in units_bi else 1000 if format in units_dc else 0
347
    if not factor:
348
        raise ValueError('Invalid data size format %s' % format)
349
    for prefix in ('K', 'M', 'G', 'T'):
350
        size *= factor
351
        if format.startswith(prefix):
352
            break
353
    return int(size)
354

    
355

    
356
def dict2file(d, f, depth=0):
357
    for k, v in d.items():
358
        f.write('%s%s: ' % (' ' * INDENT_TAB * depth, k))
359
        if isinstance(v, dict):
360
            f.write('\n')
361
            dict2file(v, f, depth + 1)
362
        elif isinstance(v, list) or isinstance(v, tuple):
363
            f.write('\n')
364
            list2file(v, f, depth + 1)
365
        else:
366
            f.write('%s\n' % v)
367

    
368

    
369
def list2file(l, f, depth=1):
370
    for item in l:
371
        if isinstance(item, dict):
372
            dict2file(item, f, depth + 1)
373
        elif isinstance(item, list) or isinstance(item, tuple):
374
            list2file(item, f, depth + 1)
375
        else:
376
            f.write('%s%s\n' % (' ' * INDENT_TAB * depth, item))
377

    
378
# Split input auxiliary
379

    
380

    
381
def _parse_with_regex(line, regex):
382
    re_parser = regex_compile(regex)
383
    return (re_parser.split(line), re_parser.findall(line))
384

    
385

    
386
def _get_from_parsed(parsed_str):
387
    try:
388
        parsed_str = parsed_str.strip()
389
    except:
390
        return None
391
    return ([parsed_str[1:-1]] if (
392
        parsed_str[0] == parsed_str[-1] and parsed_str[0] in ("'", '"')) else (
393
            parsed_str.split(' '))) if parsed_str else None
394

    
395

    
396
def split_input(line):
397
    if not line:
398
        return []
399
    reg_expr = '\'.*?\'|".*?"|^[\S]*$'
400
    (trivial_parts, interesting_parts) = _parse_with_regex(line, reg_expr)
401
    assert(len(trivial_parts) == 1 + len(interesting_parts))
402
    terms = []
403
    for i, tpart in enumerate(trivial_parts):
404
        part = _get_from_parsed(tpart)
405
        if part:
406
            terms += part
407
        try:
408
            part = _get_from_parsed(interesting_parts[i])
409
        except IndexError:
410
            break
411
        if part:
412
            terms += part
413
    return terms
414

    
415

    
416
def ask_user(msg, true_resp=('y', )):
417
    """Print msg and read user response
418

419
    :param true_resp: (tuple of chars)
420

421
    :returns: (bool) True if reponse in true responses, False otherwise
422
    """
423
    _write('%s [%s/N]: ' % (msg, ', '.join(true_resp)))
424
    _flush()
425
    user_response = _readline()
426
    return user_response[0].lower() in true_resp
427

    
428

    
429
def spiner(size=None):
430
    spins = ('/', '-', '\\', '|')
431
    _write(' ')
432
    size = size or -1
433
    i = 0
434
    while size - i:
435
        _write('\b%s' % spins[i % len(spins)])
436
        _flush()
437
        i += 1
438
        sleep(0.1)
439
        yield
440
    yield
441

    
442

    
443
def get_path_size(testpath):
444
    if path.isfile(testpath):
445
        return path.getsize(testpath)
446
    total_size = 0
447
    for top, dirs, files in walk(path.abspath(testpath)):
448
        for f in files:
449
            f = path.join(top, f)
450
            if path.isfile(f):
451
                total_size += path.getsize(f)
452
    return total_size
453

    
454

    
455
def remove_from_items(list_of_dicts, key_to_remove):
456
    for item in list_of_dicts:
457
        assert isinstance(item, dict), 'Item %s not a dict' % item
458
        item.pop(key_to_remove, None)