console.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. # -*- coding: utf-8 -*-
  2. import datetime
  3. import json
  4. import numbers
  5. import sys
  6. import time
  7. import click
  8. import yaml
  9. try:
  10. from urllib.parse import urlsplit, urlunsplit
  11. except ImportError: # NOQA
  12. from urlparse import urlsplit, urlunsplit
  13. # global state is evil!
  14. # anyway, we are using this as a convenient hack to switch output formats
  15. GLOBAL_STATE = {'output_format': 'text'}
  16. def is_json_output():
  17. return GLOBAL_STATE.get('output_format') == 'json'
  18. def is_yaml_output():
  19. return GLOBAL_STATE.get('output_format') == 'yaml'
  20. def is_tsv_output():
  21. return GLOBAL_STATE.get('output_format') == 'tsv'
  22. def is_text_output():
  23. return GLOBAL_STATE.get('output_format') == 'text'
  24. def secho(*args, **kwargs):
  25. args = list(args)
  26. if len(args) > 0:
  27. args[0] = '{}'.format(args[0])
  28. if 'err' not in kwargs:
  29. if not sys.stdout.isatty():
  30. kwargs['err'] = True
  31. if not is_text_output():
  32. kwargs['err'] = True
  33. click.secho(*args, **kwargs)
  34. def action(msg, **kwargs):
  35. secho(msg.format(**kwargs), nl=False, bold=True)
  36. def ok(msg=' OK', **kwargs):
  37. secho(msg, fg='green', bold=True, **kwargs)
  38. def error(msg, **kwargs):
  39. secho(msg, fg='red', bold=True, **kwargs)
  40. def fatal_error(msg, **kwargs):
  41. error(msg, **kwargs)
  42. sys.exit(1)
  43. def warning(msg, **kwargs):
  44. secho(msg, fg='yellow', bold=True, **kwargs)
  45. def info(msg):
  46. secho(msg, fg='blue', bold=True)
  47. class OutputFormat:
  48. def __init__(self, fmt):
  49. self.fmt = fmt
  50. self._old_fmt = None
  51. def __enter__(self):
  52. self._old_fmt = GLOBAL_STATE.get('output_format')
  53. GLOBAL_STATE['output_format'] = self.fmt
  54. def __exit__(self, exc_type, exc_val, exc_tb):
  55. GLOBAL_STATE['output_format'] = self._old_fmt
  56. class Action:
  57. def __init__(self, msg, ok_msg=' OK', nl=False, **kwargs):
  58. self.msg = msg
  59. self.ok_msg = ok_msg
  60. self.msg_args = kwargs
  61. self.nl = nl
  62. self.errors = []
  63. self._suppress_exception = False
  64. def __enter__(self):
  65. action(self.msg, **self.msg_args)
  66. if self.nl:
  67. secho('')
  68. return self
  69. def __exit__(self, exc_type, exc_val, exc_tb):
  70. if exc_type is None:
  71. if not self.errors:
  72. ok(self.ok_msg)
  73. elif not self._suppress_exception:
  74. error(' EXCEPTION OCCURRED: {}'.format(exc_val))
  75. def fatal_error(self, msg, **kwargs):
  76. self._suppress_exception = True # Avoid printing "EXCEPTION OCCURRED: -1" on exit
  77. fatal_error(' {}'.format(msg), **kwargs)
  78. def error(self, msg, **kwargs):
  79. error(' {}'.format(msg), **kwargs)
  80. self.errors.append(msg)
  81. def progress(self):
  82. secho(' .', nl=False)
  83. def warning(self, msg, **kwargs):
  84. warning(' {}'.format(msg), **kwargs)
  85. self.errors.append(msg)
  86. def ok(self, msg):
  87. self.ok_msg = ' {}'.format(msg)
  88. def get_now():
  89. return datetime.datetime.now()
  90. def format_time(ts):
  91. if ts == 0:
  92. return ''
  93. now = get_now()
  94. try:
  95. dt = datetime.datetime.fromtimestamp(ts)
  96. except:
  97. return ts
  98. diff = now - dt
  99. s = diff.total_seconds()
  100. if s > (3600 * 49):
  101. t = '{:.0f}d'.format(s / (3600 * 24))
  102. elif s > 3600:
  103. t = '{:.0f}h'.format(s / 3600)
  104. elif s > 70:
  105. t = '{:.0f}m'.format(s / 60)
  106. else:
  107. t = '{:.0f}s'.format(s)
  108. return '{} ago'.format(t)
  109. def format(col, val):
  110. if val is None:
  111. val = ''
  112. elif col.endswith('_time'):
  113. val = format_time(val)
  114. elif isinstance(val, bool):
  115. val = 'yes' if val else 'no'
  116. else:
  117. val = str(val)
  118. return val
  119. def print_tsv_table(cols, rows):
  120. sys.stdout.write('\t'.join(cols))
  121. sys.stdout.write('\n')
  122. for row in rows:
  123. first_col = True
  124. for col in cols:
  125. if not first_col:
  126. sys.stdout.write('\t')
  127. val = row.get(col)
  128. sys.stdout.write(format(col, val))
  129. first_col = False
  130. sys.stdout.write('\n')
  131. def print_table(cols, rows, styles=None, titles=None, max_column_widths=None):
  132. if is_json_output() or is_yaml_output():
  133. new_rows = []
  134. for row in rows:
  135. new_row = {}
  136. for col in cols:
  137. new_row[col] = row.get(col)
  138. new_rows.append(new_row)
  139. if is_json_output():
  140. print(json.dumps(new_rows, sort_keys=True))
  141. else:
  142. print(yaml.safe_dump_all(new_rows, default_flow_style=False))
  143. return
  144. elif is_tsv_output():
  145. return print_tsv_table(cols, rows)
  146. if not styles or type(styles) != dict:
  147. styles = {}
  148. if not titles or type(titles) != dict:
  149. titles = {}
  150. if not max_column_widths or type(max_column_widths) != dict:
  151. max_column_widths = {}
  152. colwidths = {}
  153. for col in cols:
  154. colwidths[col] = len(titles.get(col, col))
  155. for row in rows:
  156. for col in cols:
  157. val = row.get(col)
  158. colwidths[col] = min(max(colwidths[col], len(format(col, val))), max_column_widths.get(col, 1000))
  159. for i, col in enumerate(cols):
  160. click.secho(('{:' + str(colwidths[col]) + '}').format(titles.get(col, col.title().replace('_', ' '))),
  161. nl=False, fg='black', bg='white')
  162. if i < len(cols) - 1:
  163. click.secho('│', nl=False, fg='black', bg='white')
  164. click.echo('')
  165. for row in rows:
  166. for col in cols:
  167. val = row.get(col)
  168. align = ''
  169. try:
  170. style = styles.get(val, {})
  171. except:
  172. # val might not be hashable
  173. style = {}
  174. if val is not None and col.endswith('_time') and isinstance(val, numbers.Number):
  175. align = '>'
  176. diff = time.time() - val
  177. if diff < 900:
  178. style = {'fg': 'green', 'bold': True}
  179. elif diff < 3600:
  180. style = {'fg': 'green'}
  181. elif isinstance(val, int) or isinstance(val, float):
  182. align = '>'
  183. val = format(col, val)
  184. if len(val) > max_column_widths.get(col, 1000):
  185. val = val[:max_column_widths.get(col, 1000) - 2] + '..'
  186. click.secho(('{:' + align + str(colwidths[col]) + '}').format(val), nl=False, **style)
  187. click.echo(' ', nl=False)
  188. click.echo('')
  189. def choice(prompt, options, default=None):
  190. """
  191. Ask to user to select one option and return it
  192. """
  193. stderr = True
  194. if sys.stdout.isatty():
  195. stderr = False
  196. click.secho(prompt, err=stderr)
  197. promptdefault = None
  198. for i, option in enumerate(options):
  199. if isinstance(option, tuple):
  200. value, label = option
  201. else:
  202. value = label = option
  203. if value == default:
  204. promptdefault = i + 1
  205. click.secho('{}) {}'.format(i + 1, label), err=stderr)
  206. while True:
  207. selection = click.prompt('Please select (1-{})'.format(len(options)),
  208. type=int, default=promptdefault, err=stderr)
  209. try:
  210. result = options[int(selection) - 1]
  211. if isinstance(result, tuple):
  212. value, label = result
  213. else:
  214. value = result
  215. return value
  216. except:
  217. pass
  218. class AliasedGroup(click.Group):
  219. """
  220. Click group which allows using abbreviated commands
  221. """
  222. def get_command(self, ctx, cmd_name):
  223. rv = click.Group.get_command(self, ctx, cmd_name)
  224. if rv is not None:
  225. return rv
  226. matches = [x for x in self.list_commands(ctx)
  227. if x.startswith(cmd_name)]
  228. if not matches:
  229. return None
  230. elif len(matches) == 1:
  231. return click.Group.get_command(self, ctx, matches[0])
  232. ctx.fail('Too many matches: %s' % ', '.join(sorted(matches)))
  233. class FloatRange(click.types.FloatParamType):
  234. """A parameter that works similar to :data:`click.FLOAT` but restricts
  235. the value to fit into a range. The default behavior is to fail if the
  236. value falls outside the range, but it can also be silently clamped
  237. between the two edges.
  238. """
  239. name = 'float range'
  240. def __init__(self, min=None, max=None, clamp=False):
  241. self.min = min
  242. self.max = max
  243. self.clamp = clamp
  244. def convert(self, value, param, ctx):
  245. rv = click.types.FloatParamType.convert(self, value, param, ctx)
  246. if self.clamp:
  247. if self.min is not None and rv < self.min:
  248. return self.min
  249. if self.max is not None and rv > self.max:
  250. return self.max
  251. if self.min is not None and rv < self.min or \
  252. self.max is not None and rv > self.max:
  253. if self.min is None:
  254. self.fail('%s is bigger than the maximum valid value '
  255. '%s.' % (rv, self.max), param, ctx)
  256. elif self.max is None:
  257. self.fail('%s is smaller than the minimum valid value '
  258. '%s.' % (rv, self.min), param, ctx)
  259. else:
  260. self.fail('%s is not in the valid range of %s to %s.'
  261. % (rv, self.min, self.max), param, ctx)
  262. return rv
  263. def __repr__(self):
  264. return 'FloatRange(%r, %r)' % (self.min, self.max)
  265. class UrlType(click.types.ParamType):
  266. name = 'url'
  267. def __init__(self, default_scheme='https', allowed_schemes=('http', 'https')):
  268. self.default_scheme = default_scheme
  269. self.allowed_schemes = allowed_schemes
  270. def convert(self, value, param, ctx):
  271. value = value.strip()
  272. if not value:
  273. self.fail('"{}" is not a valid URL'.format(value))
  274. if self.default_scheme and '://' not in value:
  275. value = '{}://{}'.format(self.default_scheme, value)
  276. url = urlsplit(value)
  277. if self.allowed_schemes and url.scheme not in self.allowed_schemes:
  278. self.fail('"{}" is not one of the allowed URL schemes ({})'.format(
  279. url.scheme, ', '.join(self.allowed_schemes)))
  280. return urlunsplit(url)
  281. def __repr__(self):
  282. return 'UrlType(%r, %r)' % (self.default_scheme, self.allowed_schemes)