Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (15.2 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
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
    """raw_input wrapper is used to help unittests"""
77
    return raw_input()
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 items:
280
        return
281
    if not (isinstance(items, dict) or isinstance(items, list) or isinstance(
282
                items, tuple)):
283
        _print('%s' % items)
284
        return
285

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

    
309

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

    
330

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

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

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

    
357

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

    
370

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

    
380
# Split input auxiliary
381

    
382

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

    
387

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

    
397

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

    
420

    
421
def ask_user(msg, true_resp=('y', )):
422
    """Print msg and read user response
423

424
    :param true_resp: (tuple of chars)
425

426
    :returns: (bool) True if reponse in true responses, False otherwise
427
    """
428
    _write('%s [%s/N]: ' % (msg, ', '.join(true_resp)))
429
    _flush()
430
    user_response = _readline()
431
    return user_response[0].lower() in true_resp
432

    
433

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

    
447

    
448
def get_path_size(testpath):
449
    if path.isfile(testpath):
450
        return path.getsize(testpath)
451
    total_size = 0
452
    for top, dirs, files in walk(path.abspath(testpath)):
453
        for f in files:
454
            f = path.join(top, f)
455
            if path.isfile(f):
456
                total_size += path.getsize(f)
457
    return total_size
458

    
459

    
460
def remove_from_items(list_of_dicts, key_to_remove):
461
    for item in list_of_dicts:
462
        assert isinstance(item, dict), 'Item %s not a dict' % item
463
        item.pop(key_to_remove, None)
464

    
465

    
466
def filter_dicts_by_dict(
467
    list_of_dicts, filters,
468
    exact_match=True, case_sensitive=False):
469
    """
470
    :param list_of_dicts: (list) each dict contains "raw" key-value pairs
471

472
    :param filters: (dict) filters in key-value form
473

474
    :param exact_match: (bool) if false, check if the filter value is part of
475
        the actual value
476

477
    :param case_sensitive: (bool) revers to values only (not keys)
478

479
    :returns: (list) only the dicts that match all filters
480
    """
481
    new_dicts = []
482
    for d in list_of_dicts:
483
        if set(filters).difference(d):
484
            continue
485
        match = True
486
        for k, v in filters.items():
487
            dv, v = ('%s' % d[k]), ('%s' % v)
488
            if not case_sensitive:
489
                dv, v = dv.lower(), v.lower()
490
            if not ((
491
                    exact_match and v == dv) or (
492
                    (not exact_match) and v in dv)):
493
                match = False
494
                break
495
        if match:
496
            new_dicts.append(d)
497
    return new_dicts