1""" 2 SoftLayer.formatting 3 ~~~~~~~~~~~~~~~~~~~~ 4 Provider classes and helper functions to display output onto a command-line. 5 6""" 7# pylint: disable=E0202, consider-merging-isinstance, arguments-differ, keyword-arg-before-vararg 8import collections 9import json 10import os 11 12import click 13 14# If both PTable and prettytable are installed, its impossible to use the new version 15try: 16 from prettytable import prettytable 17except ImportError: 18 import prettytable 19 20from SoftLayer.CLI import exceptions 21from SoftLayer import utils 22 23FALSE_VALUES = ['0', 'false', 'FALSE', 'no', 'False'] 24 25 26def format_output(data, fmt='table'): # pylint: disable=R0911,R0912 27 """Given some data, will format it for console output. 28 29 :param data: One of: String, Table, FormattedItem, List, Tuple, 30 SequentialOutput 31 :param string fmt (optional): One of: table, raw, json, python 32 """ 33 if isinstance(data, str): 34 if fmt in ('json', 'jsonraw'): 35 return json.dumps(data) 36 return data 37 38 # responds to .prettytable() 39 if hasattr(data, 'prettytable'): 40 if fmt == 'table': 41 return str(format_prettytable(data)) 42 elif fmt == 'raw': 43 return str(format_no_tty(data)) 44 45 # responds to .to_python() 46 if hasattr(data, 'to_python'): 47 if fmt == 'json': 48 return json.dumps( 49 format_output(data, fmt='python'), 50 indent=4, 51 cls=CLIJSONEncoder) 52 elif fmt == 'jsonraw': 53 return json.dumps(format_output(data, fmt='python'), 54 cls=CLIJSONEncoder) 55 elif fmt == 'python': 56 return data.to_python() 57 58 # responds to .formatted 59 if hasattr(data, 'formatted'): 60 if fmt == 'table': 61 return data.formatted 62 63 # responds to .separator 64 if hasattr(data, 'separator'): 65 output = [format_output(d, fmt=fmt) for d in data if d] 66 return str(SequentialOutput(data.separator, output)) 67 68 # is iterable 69 if isinstance(data, list) or isinstance(data, tuple): 70 output = [format_output(d, fmt=fmt) for d in data] 71 if fmt == 'python': 72 return output 73 return format_output(listing(output, separator=os.linesep)) 74 75 # fallback, convert this odd object to a string 76 return data 77 78 79def format_prettytable(table): 80 """Converts SoftLayer.CLI.formatting.Table instance to a prettytable.""" 81 for i, row in enumerate(table.rows): 82 for j, item in enumerate(row): 83 table.rows[i][j] = format_output(item) 84 85 ptable = table.prettytable() 86 ptable.hrules = prettytable.FRAME 87 ptable.horizontal_char = '.' 88 ptable.vertical_char = ':' 89 ptable.junction_char = ':' 90 return ptable 91 92 93def format_no_tty(table): 94 """Converts SoftLayer.CLI.formatting.Table instance to a prettytable.""" 95 96 for i, row in enumerate(table.rows): 97 for j, item in enumerate(row): 98 table.rows[i][j] = format_output(item, fmt='raw') 99 ptable = table.prettytable() 100 101 for col in table.columns: 102 ptable.align[col] = 'l' 103 104 ptable.hrules = prettytable.NONE 105 ptable.border = False 106 ptable.header = False 107 ptable.left_padding_width = 0 108 ptable.right_padding_width = 2 109 return ptable 110 111 112def mb_to_gb(megabytes): 113 """Converts number of megabytes to a FormattedItem in gigabytes. 114 115 :param int megabytes: number of megabytes 116 """ 117 return FormattedItem(megabytes, "%dG" % (float(megabytes) / 1024)) 118 119 120def b_to_gb(_bytes): 121 """Converts number of bytes to a FormattedItem in gigabytes. 122 123 :param int _bytes: number of bytes 124 """ 125 return FormattedItem(_bytes, 126 "%.2fG" % (float(_bytes) / 1024 / 1024 / 1024)) 127 128 129def gb(gigabytes): # pylint: disable=C0103 130 """Converts number of gigabytes to a FormattedItem in gigabytes. 131 132 :param int gigabytes: number of gigabytes 133 """ 134 return FormattedItem(int(float(gigabytes)) * 1024, 135 "%dG" % int(float(gigabytes))) 136 137 138def blank(): 139 """Returns a blank FormattedItem.""" 140 return FormattedItem(None, '-') 141 142 143def listing(items, separator=','): 144 """Given an iterable return a FormattedItem which display the list of items 145 146 :param items: An iterable that outputs strings 147 :param string separator: the separator to use 148 """ 149 return SequentialOutput(separator, items) 150 151 152def active_txn(item): 153 """Returns a FormattedItem describing the active transaction on a object. 154 155 If no active transaction is running, returns a blank FormattedItem. 156 157 :param item: An object capable of having an active transaction 158 """ 159 return transaction_status(utils.lookup(item, 'activeTransaction')) 160 161 162def transaction_status(transaction): 163 """Returns a FormattedItem describing the given transaction. 164 165 :param item: An object capable of having an active transaction 166 """ 167 if not transaction or not transaction.get('transactionStatus'): 168 return blank() 169 170 return FormattedItem( 171 transaction['transactionStatus'].get('name'), 172 transaction['transactionStatus'].get('friendlyName')) 173 174 175def tags(tag_references): 176 """Returns a formatted list of tags.""" 177 if not tag_references: 178 return blank() 179 180 tag_row = [] 181 for tag_detail in tag_references: 182 tag = utils.lookup(tag_detail, 'tag', 'name') 183 if tag is not None: 184 tag_row.append(tag) 185 186 return listing(tag_row, separator=', ') 187 188 189def confirm(prompt_str, default=False): 190 """Show a confirmation prompt to a command-line user. 191 192 :param string prompt_str: prompt to give to the user 193 :param bool default: Default value to True or False 194 """ 195 if default: 196 default_str = 'y' 197 prompt = '%s [Y/n]' % prompt_str 198 else: 199 default_str = 'n' 200 prompt = '%s [y/N]' % prompt_str 201 202 ans = click.prompt(prompt, default=default_str, show_default=False) 203 if ans.lower() in ('y', 'yes', 'yeah', 'yup', 'yolo'): 204 return True 205 206 return False 207 208 209def no_going_back(confirmation): 210 """Show a confirmation to a user. 211 212 :param confirmation str: the string the user has to enter in order to 213 confirm their action. 214 """ 215 if not confirmation: 216 confirmation = 'yes' 217 218 prompt = ('This action cannot be undone! Type "%s" or press Enter ' 219 'to abort' % confirmation) 220 221 ans = click.prompt(prompt, default='', show_default=False) 222 if ans.lower() == str(confirmation): 223 return True 224 225 return False 226 227 228class SequentialOutput(list): 229 """SequentialOutput is used for outputting sequential items. 230 231 The purpose is to de-couple the separator from the output itself. 232 233 :param separator str: string to use as a default separator 234 """ 235 236 def __init__(self, separator=os.linesep, *args, **kwargs): 237 self.separator = separator 238 super().__init__(*args, **kwargs) 239 240 def to_python(self): 241 """returns itself, since it itself is a list.""" 242 return self 243 244 def __str__(self): 245 return self.separator.join(str(x) for x in self) 246 247 248class CLIJSONEncoder(json.JSONEncoder): 249 """A JSON encoder which is able to use a .to_python() method on objects.""" 250 251 def default(self, o): 252 """Encode object if it implements to_python().""" 253 if hasattr(o, 'to_python'): 254 return o.to_python() 255 return super().default(o) 256 257 258class Table(object): 259 """A Table structure used for output. 260 261 :param list columns: a list of column names 262 """ 263 264 def __init__(self, columns, title=None): 265 duplicated_cols = [col for col, count 266 in collections.Counter(columns).items() 267 if count > 1] 268 if len(duplicated_cols) > 0: 269 raise exceptions.CLIAbort("Duplicated columns are not allowed: %s" 270 % ','.join(duplicated_cols)) 271 272 self.columns = columns 273 self.rows = [] 274 self.align = {} 275 self.sortby = None 276 self.title = title 277 278 def add_row(self, row): 279 """Add a row to the table. 280 281 :param list row: the row of string to be added 282 """ 283 self.rows.append(row) 284 285 def to_python(self): 286 """Decode this Table object to standard Python types.""" 287 # Adding rows 288 items = [] 289 for row in self.rows: 290 formatted_row = [_format_python_value(v) for v in row] 291 items.append(dict(zip(self.columns, formatted_row))) 292 return items 293 294 def prettytable(self): 295 """Returns a new prettytable instance.""" 296 table = prettytable.PrettyTable(self.columns) 297 298 if self.sortby: 299 if self.sortby in self.columns: 300 table.sortby = self.sortby 301 else: 302 msg = "Column (%s) doesn't exist to sort by" % self.sortby 303 raise exceptions.CLIAbort(msg) 304 305 if isinstance(self.align, str): 306 table.align = self.align 307 else: 308 # Required because PrettyTable has a strict setter function for alignment 309 for a_col, alignment in self.align.items(): 310 table.align[a_col] = alignment 311 312 if self.title: 313 table.title = self.title 314 # Adding rows 315 for row in self.rows: 316 table.add_row(row) 317 return table 318 319 320class KeyValueTable(Table): 321 """A table that is oriented towards key-value pairs.""" 322 323 def to_python(self): 324 """Decode this KeyValueTable object to standard Python types.""" 325 mapping = {} 326 for row in self.rows: 327 mapping[row[0]] = _format_python_value(row[1]) 328 return mapping 329 330 331class FormattedItem(object): 332 """This is an object that can be displayed as a human readable and raw. 333 334 :param original: raw (machine-readable) value 335 :param string formatted: human-readable value 336 """ 337 338 def __init__(self, original, formatted=None): 339 self.original = original 340 if formatted is not None: 341 self.formatted = formatted 342 else: 343 self.formatted = self.original 344 345 def to_python(self): 346 """returns the original (raw) value.""" 347 return self.original 348 349 def __str__(self): 350 """returns the formatted value.""" 351 # If the original value is None, represent this as 'NULL' 352 if self.original is None: 353 return 'NULL' 354 355 try: 356 return str(self.original) 357 except UnicodeError: 358 return 'invalid' 359 360 def __repr__(self): 361 return 'FormattedItem(%r, %r)' % (self.original, self.formatted) 362 363 # Implement sorting methods. 364 # NOTE(kmcdonald): functools.total_ordering could be used once support for 365 # Python 2.6 is dropped 366 def __eq__(self, other): 367 return self.original == getattr(other, 'original', other) 368 369 def __lt__(self, other): 370 if self.original is None: 371 return True 372 373 other_val = getattr(other, 'original', other) 374 if other_val is None: 375 return False 376 return self.original < other_val 377 378 def __gt__(self, other): 379 return not (self < other or self == other) 380 381 def __le__(self, other): 382 return self < other or self == other 383 384 def __ge__(self, other): 385 return self >= other 386 387 388def _format_python_value(value): 389 """If the value has to_python() defined then return that.""" 390 if hasattr(value, 'to_python'): 391 return value.to_python() 392 return value 393 394 395def iter_to_table(value): 396 """Convert raw API responses to response tables.""" 397 if isinstance(value, list): 398 return _format_list(value) 399 if isinstance(value, dict): 400 return _format_dict(value) 401 return value 402 403 404def _format_dict(result): 405 """Format dictionary responses into key-value table.""" 406 407 table = KeyValueTable(['name', 'value']) 408 table.align['name'] = 'r' 409 table.align['value'] = 'l' 410 411 for key, value in result.items(): 412 value = iter_to_table(value) 413 table.add_row([key, value]) 414 415 return table 416 417 418def _format_list(result): 419 """Format list responses into a table.""" 420 421 if not result: 422 return result 423 424 new_result = [item for item in result if item] 425 426 if isinstance(new_result[0], dict): 427 return _format_list_objects(new_result) 428 429 table = Table(['value']) 430 for item in new_result: 431 table.add_row([iter_to_table(item)]) 432 return table 433 434 435def _format_list_objects(result): 436 """Format list of objects into a table.""" 437 438 all_keys = set() 439 for item in result: 440 if isinstance(item, dict): 441 all_keys = all_keys.union(item.keys()) 442 443 all_keys = sorted(all_keys) 444 table = Table(all_keys) 445 446 for item in result: 447 if not item: 448 continue 449 values = [] 450 for key in all_keys: 451 value = iter_to_table(item.get(key)) 452 values.append(value) 453 454 table.add_row(values) 455 456 return table 457