The little things give you away... A collection of various small helper stuff
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 

207 lignes
8.8 KiB

  1. #!/usr/bin/env python3
  2. import argparse
  3. import datetime
  4. import json
  5. import math
  6. import re
  7. import sys
  8. import time
  9. import urllib.request
  10. # Column definitions
  11. columns = {
  12. 'jobid': (lambda job, pipelines: job["job_data"]["ident"], ()),
  13. 'url': (lambda job, pipelines: job["job_data"]["url"], ()),
  14. 'user': (lambda job, pipelines: job["job_data"]["started_by"], ()),
  15. 'pipenick': (lambda job, pipelines: pipelines[job["job_data"]["pipeline_id"]] if job["job_data"]["pipeline_id"] in pipelines else "unknown", ()),
  16. 'queued': (lambda job, pipelines: job["job_data"]["queued_at"], ('date',)),
  17. 'started': (lambda job, pipelines: job["job_data"]["started_at"], ('date',)),
  18. 'last active': (lambda job, pipelines: int(job["ts"]), ('date', 'coloured')),
  19. 'dl urls': (lambda job, pipelines: job["job_data"]["items_downloaded"], ()),
  20. 'dl size': (lambda job, pipelines: job["job_data"]["bytes_downloaded"], ('size',)),
  21. 'queue': (lambda job, pipelines: job["job_data"]["items_queued"] - job["job_data"]["items_downloaded"], ()),
  22. 'con': (lambda job, pipelines: job["job_data"]["concurrency"], ()),
  23. 'delay min': (lambda job, pipelines: int(job["job_data"]["delay_min"]), ('hidden',)),
  24. 'delay max': (lambda job, pipelines: int(job["job_data"]["delay_max"]), ('hidden',)),
  25. 'delay': (lambda job, pipelines: str(int(job["job_data"]["delay_min"])) + '-' + str(int(job["job_data"]["delay_max"])) if job["job_data"]["delay_min"] != job["job_data"]["delay_max"] else str(int(job["job_data"]["delay_min"])), ()),
  26. }
  27. defaultSort = 'jobid'
  28. # Parse arguments
  29. class FilterAction(argparse.Action):
  30. def __call__(self, parser, namespace, values, optionString = None):
  31. global columns
  32. match = re.match(r"^(?P<column>[A-Za-z ]+)(?P<op>[=<>^*$~])(?P<value>.*)$", values[0])
  33. if not match:
  34. raise argparse.ArgumentError('Invalid filter')
  35. filterDict = match.groupdict()
  36. filterDict["column"] = filterDict["column"].lower()
  37. assert filterDict["column"] in columns
  38. transform = (lambda x: x.lower() if isinstance(x, str) else x) if optionString in ('--ifilter', '-i') else (lambda x: x)
  39. setattr(namespace, self.dest, (filterDict, transform))
  40. def parse_sort(value):
  41. global columns
  42. sortDesc = value.startswith('-')
  43. if sortDesc:
  44. value = value[1:]
  45. value = value.lower()
  46. if value not in columns:
  47. raise argparse.ArgumentError('Invalid column name')
  48. return (value, sortDesc)
  49. class SortAction(argparse.Action):
  50. def __call__(self, parser, namespace, values, optionString = None):
  51. result = parse_sort(values[0])
  52. if getattr(namespace, self.dest, None) is None:
  53. setattr(namespace, self.dest, [])
  54. getattr(namespace, self.dest).append(result)
  55. parser = argparse.ArgumentParser(formatter_class = argparse.RawTextHelpFormatter)
  56. parser.add_argument('--sort', '-s', nargs = 1, type = str, action = SortAction, help = "Sort the table by a COLUMN (descending if preceded by '-'). This can be used multiple times to refine the sorting.")
  57. parser.add_argument('--filter', '-f', nargs = 1, type = str, action = FilterAction, help = '\n'.join([
  58. 'Filter the table for rows where a COLUMN has a certain VALUE. If specified multiple times, only the last value is used.',
  59. 'FILTER has the format COLUMN{=|<|>|^|*|$|~}VALUE',
  60. ' = means the value must be exactly as specified.',
  61. ' < and > mean it must be less/greater than the specified.',
  62. ' ^ and $ mean it must start/end with the specified.',
  63. ' * means it must contain the specified.',
  64. ' ~ means it must match the specified regex.',
  65. ]))
  66. parser.add_argument('--ifilter', '-i', nargs = 1, type = str, action = FilterAction, dest = 'filter', help = 'Like --filter but case-insensitive')
  67. parser.add_argument('--no-colours', '--no-colors', action = 'store_true', help = "Don't colourise the last activity column if it's been a while. (Table mode only)")
  68. parser.add_argument('--no-table', action = 'store_true', help = 'Raw output without feeding through column(1); columns are separated by tabs. (Table mode only)')
  69. parser.add_argument('--dates', action = 'store_true', help = 'Print dates instead of elapsed times for queued/started/last active columns. (Table mode only)')
  70. parser.add_argument('--mode', choices = ('table', 'dashboard-regex', 'con-d-commands'), default = 'table', help = '\n'.join([
  71. 'Output modes:',
  72. ' table: print a table of the matched jobs',
  73. ' dashboard-regex: compose a regular expression that can be used on the dashboard to actively watch the jobs matched by the filter',
  74. ' con-d-commands: print !con and !d commands for the current settings',
  75. ]))
  76. args = parser.parse_args()
  77. if not args.sort:
  78. args.sort = [parse_sort(defaultSort)]
  79. # Retrieve
  80. def fetch(url):
  81. req = urllib.request.Request(url)
  82. req.add_header('Accept', 'application/json')
  83. with urllib.request.urlopen(req) as f:
  84. if f.getcode() != 200:
  85. raise RuntimeError('Could not fetch job data')
  86. return json.load(f)
  87. jobdata = fetch('http://dashboard.at.ninjawedding.org/logs/recent?count=1')
  88. pipelinedata = fetch('http://dashboard.at.ninjawedding.org/pipelines')
  89. currentTime = time.time()
  90. # Process
  91. pipelines = {p["id"]: p["nickname"] for p in pipelinedata["pipelines"]}
  92. jobs = []
  93. for job in jobdata:
  94. jobs.append({column: columnFunc(job, pipelines) for column, (columnFunc, _) in columns.items()})
  95. if not jobs:
  96. # Nothing to do
  97. sys.exit(0)
  98. # Filter
  99. if args.filter:
  100. filterDict, transform = args.filter
  101. compFunc = {
  102. "=": lambda a, b: a == b,
  103. "<": lambda a, b: a < b,
  104. ">": lambda a, b: a > b,
  105. "^": lambda a, b: a.startswith(b),
  106. "*": lambda a, b: b in a,
  107. "$": lambda a, b: a.endswith(b),
  108. "~": lambda a, b: re.search(b, a) is not None,
  109. }[filterDict["op"]]
  110. if isinstance(jobs[0][filterDict["column"]], (int, float)):
  111. filterDict["value"] = float(filterDict["value"])
  112. jobs = [job for job in jobs if compFunc(transform(job[filterDict["column"]]), transform(filterDict["value"]))]
  113. if not jobs:
  114. sys.exit(0)
  115. # Sort
  116. class reversor: # https://stackoverflow.com/a/56842689
  117. def __init__(self, obj):
  118. self.obj = obj
  119. def __eq__(self, other):
  120. return other.obj == self.obj
  121. def __lt__(self, other):
  122. return other.obj < self.obj
  123. sortColumns = tuple((column, descending, columns[column]) for column, descending in args.sort)
  124. if not args.dates:
  125. # Reverse sorting order for columns which have a date attribute since the column will have elapsed time
  126. sortColumns = tuple((column, not descending if 'date' in columnInfo[1] else descending, columnInfo) for column, descending, columnInfo in sortColumns)
  127. jobs = sorted(jobs, key = lambda job: tuple(job[column] if not descending else reversor(job[column]) for column, descending, _ in sortColumns))
  128. # Non-table output modes
  129. if args.mode == 'dashboard-regex':
  130. print('^(' + '|'.join(re.escape(job['url']) for job in jobs) + ')$')
  131. sys.exit(0)
  132. elif args.mode == 'con-d-commands':
  133. for job in jobs:
  134. print(f'!con {job["jobid"]} {job["con"]}')
  135. print(f'!d {job["jobid"]} {job["delay min"]} {job["delay max"]}')
  136. sys.exit(0)
  137. # Renderers
  138. def render_date(ts, coloured = False):
  139. global args, currentTime
  140. diff = currentTime - ts
  141. colourStr = f"\x1b[{0 if diff < 6 * 3600 else 7};31m" if coloured and diff >= 300 else ""
  142. colourEndStr = "\x1b[0m" if colourStr else ""
  143. if args.dates:
  144. return (colourStr, datetime.datetime.fromtimestamp(ts).isoformat(sep = " "), colourEndStr)
  145. if diff <= 0:
  146. return "now"
  147. elif diff < 60:
  148. return "<1 min ago"
  149. elif diff < 86400:
  150. return (colourStr, (f"{diff // 3600:.0f}h " if diff >= 3600 else "") + f"{(diff % 3600) // 60:.0f}mn ago", colourEndStr)
  151. else:
  152. return (colourStr, f"{diff // 86400:.0f}d {(diff % 86400) // 3600:.0f}h ago", colourEndStr)
  153. def render_size(size):
  154. units = ('B', 'KiB', 'MiB', 'GiB', 'TiB')
  155. unitIdx = min(int(math.log(size, 1024)), len(units) - 1) if size >= 1 else 0
  156. if unitIdx == 0:
  157. return f'{size} B' # No decimal places
  158. return f'{size / 1024 ** unitIdx:.1f} {units[unitIdx]}'
  159. renderers = {}
  160. for column, (_, columnAttr) in columns.items():
  161. if "date" in columnAttr:
  162. if "coloured" in columnAttr:
  163. renderers[column] = lambda x: render_date(x, coloured = not args.no_colours)
  164. else:
  165. renderers[column] = render_date
  166. elif "size" in columnAttr:
  167. renderers[column] = render_size
  168. elif isinstance(jobs[0][column], (int, float)):
  169. renderers[column] = str
  170. # Print
  171. output = []
  172. output.append(tuple(column.upper() for column in columns if "hidden" not in columns[column][1]))
  173. for job in jobs:
  174. for column in renderers:
  175. job[column] = renderers[column](job[column])
  176. output.append(tuple(job[column] for column in columns if "hidden" not in columns[column][1]))
  177. if not args.no_table:
  178. widths = tuple(max(len(field) if isinstance(field, str) else len(field[1]) for field in column) for column in zip(*output))
  179. for row in output:
  180. print(' '.join((value.ljust(width) if isinstance(value, str) else ''.join((value[0], value[1], value[2], ' ' * (width - len(value[1]))))) for value, width in zip(row, widths)))
  181. else:
  182. for row in output:
  183. print('\t'.join(field if isinstance(field, str) else ''.join(field) for field in row))