toil 6.1.0a1__py3-none-any.whl → 8.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. toil/__init__.py +122 -315
  2. toil/batchSystems/__init__.py +1 -0
  3. toil/batchSystems/abstractBatchSystem.py +173 -89
  4. toil/batchSystems/abstractGridEngineBatchSystem.py +272 -148
  5. toil/batchSystems/awsBatch.py +244 -135
  6. toil/batchSystems/cleanup_support.py +26 -16
  7. toil/batchSystems/contained_executor.py +31 -28
  8. toil/batchSystems/gridengine.py +86 -50
  9. toil/batchSystems/htcondor.py +166 -89
  10. toil/batchSystems/kubernetes.py +632 -382
  11. toil/batchSystems/local_support.py +20 -15
  12. toil/batchSystems/lsf.py +134 -81
  13. toil/batchSystems/lsfHelper.py +13 -11
  14. toil/batchSystems/mesos/__init__.py +41 -29
  15. toil/batchSystems/mesos/batchSystem.py +290 -151
  16. toil/batchSystems/mesos/executor.py +79 -50
  17. toil/batchSystems/mesos/test/__init__.py +31 -23
  18. toil/batchSystems/options.py +46 -28
  19. toil/batchSystems/registry.py +53 -19
  20. toil/batchSystems/singleMachine.py +296 -125
  21. toil/batchSystems/slurm.py +603 -138
  22. toil/batchSystems/torque.py +47 -33
  23. toil/bus.py +186 -76
  24. toil/common.py +664 -368
  25. toil/cwl/__init__.py +1 -1
  26. toil/cwl/cwltoil.py +1136 -483
  27. toil/cwl/utils.py +17 -22
  28. toil/deferred.py +63 -42
  29. toil/exceptions.py +5 -3
  30. toil/fileStores/__init__.py +5 -5
  31. toil/fileStores/abstractFileStore.py +140 -60
  32. toil/fileStores/cachingFileStore.py +717 -269
  33. toil/fileStores/nonCachingFileStore.py +116 -87
  34. toil/job.py +1225 -368
  35. toil/jobStores/abstractJobStore.py +416 -266
  36. toil/jobStores/aws/jobStore.py +863 -477
  37. toil/jobStores/aws/utils.py +201 -120
  38. toil/jobStores/conftest.py +3 -2
  39. toil/jobStores/fileJobStore.py +292 -154
  40. toil/jobStores/googleJobStore.py +140 -74
  41. toil/jobStores/utils.py +36 -15
  42. toil/leader.py +668 -272
  43. toil/lib/accelerators.py +115 -18
  44. toil/lib/aws/__init__.py +74 -31
  45. toil/lib/aws/ami.py +122 -87
  46. toil/lib/aws/iam.py +284 -108
  47. toil/lib/aws/s3.py +31 -0
  48. toil/lib/aws/session.py +214 -39
  49. toil/lib/aws/utils.py +287 -231
  50. toil/lib/bioio.py +13 -5
  51. toil/lib/compatibility.py +11 -6
  52. toil/lib/conversions.py +104 -47
  53. toil/lib/docker.py +131 -103
  54. toil/lib/ec2.py +361 -199
  55. toil/lib/ec2nodes.py +174 -106
  56. toil/lib/encryption/_dummy.py +5 -3
  57. toil/lib/encryption/_nacl.py +10 -6
  58. toil/lib/encryption/conftest.py +1 -0
  59. toil/lib/exceptions.py +26 -7
  60. toil/lib/expando.py +5 -3
  61. toil/lib/ftp_utils.py +217 -0
  62. toil/lib/generatedEC2Lists.py +127 -19
  63. toil/lib/humanize.py +6 -2
  64. toil/lib/integration.py +341 -0
  65. toil/lib/io.py +141 -15
  66. toil/lib/iterables.py +4 -2
  67. toil/lib/memoize.py +12 -8
  68. toil/lib/misc.py +66 -21
  69. toil/lib/objects.py +2 -2
  70. toil/lib/resources.py +68 -15
  71. toil/lib/retry.py +126 -81
  72. toil/lib/threading.py +299 -82
  73. toil/lib/throttle.py +16 -15
  74. toil/options/common.py +843 -409
  75. toil/options/cwl.py +175 -90
  76. toil/options/runner.py +50 -0
  77. toil/options/wdl.py +73 -17
  78. toil/provisioners/__init__.py +117 -46
  79. toil/provisioners/abstractProvisioner.py +332 -157
  80. toil/provisioners/aws/__init__.py +70 -33
  81. toil/provisioners/aws/awsProvisioner.py +1145 -715
  82. toil/provisioners/clusterScaler.py +541 -279
  83. toil/provisioners/gceProvisioner.py +282 -179
  84. toil/provisioners/node.py +155 -79
  85. toil/realtimeLogger.py +34 -22
  86. toil/resource.py +137 -75
  87. toil/server/app.py +128 -62
  88. toil/server/celery_app.py +3 -1
  89. toil/server/cli/wes_cwl_runner.py +82 -53
  90. toil/server/utils.py +54 -28
  91. toil/server/wes/abstract_backend.py +64 -26
  92. toil/server/wes/amazon_wes_utils.py +21 -15
  93. toil/server/wes/tasks.py +121 -63
  94. toil/server/wes/toil_backend.py +142 -107
  95. toil/server/wsgi_app.py +4 -3
  96. toil/serviceManager.py +58 -22
  97. toil/statsAndLogging.py +224 -70
  98. toil/test/__init__.py +282 -183
  99. toil/test/batchSystems/batchSystemTest.py +460 -210
  100. toil/test/batchSystems/batch_system_plugin_test.py +90 -0
  101. toil/test/batchSystems/test_gridengine.py +173 -0
  102. toil/test/batchSystems/test_lsf_helper.py +67 -58
  103. toil/test/batchSystems/test_slurm.py +110 -49
  104. toil/test/cactus/__init__.py +0 -0
  105. toil/test/cactus/test_cactus_integration.py +56 -0
  106. toil/test/cwl/cwlTest.py +496 -287
  107. toil/test/cwl/measure_default_memory.cwl +12 -0
  108. toil/test/cwl/not_run_required_input.cwl +29 -0
  109. toil/test/cwl/scatter_duplicate_outputs.cwl +40 -0
  110. toil/test/cwl/seqtk_seq.cwl +1 -1
  111. toil/test/docs/scriptsTest.py +69 -46
  112. toil/test/jobStores/jobStoreTest.py +427 -264
  113. toil/test/lib/aws/test_iam.py +118 -50
  114. toil/test/lib/aws/test_s3.py +16 -9
  115. toil/test/lib/aws/test_utils.py +5 -6
  116. toil/test/lib/dockerTest.py +118 -141
  117. toil/test/lib/test_conversions.py +113 -115
  118. toil/test/lib/test_ec2.py +58 -50
  119. toil/test/lib/test_integration.py +104 -0
  120. toil/test/lib/test_misc.py +12 -5
  121. toil/test/mesos/MesosDataStructuresTest.py +23 -10
  122. toil/test/mesos/helloWorld.py +7 -6
  123. toil/test/mesos/stress.py +25 -20
  124. toil/test/options/__init__.py +13 -0
  125. toil/test/options/options.py +42 -0
  126. toil/test/provisioners/aws/awsProvisionerTest.py +320 -150
  127. toil/test/provisioners/clusterScalerTest.py +440 -250
  128. toil/test/provisioners/clusterTest.py +166 -44
  129. toil/test/provisioners/gceProvisionerTest.py +174 -100
  130. toil/test/provisioners/provisionerTest.py +25 -13
  131. toil/test/provisioners/restartScript.py +5 -4
  132. toil/test/server/serverTest.py +188 -141
  133. toil/test/sort/restart_sort.py +137 -68
  134. toil/test/sort/sort.py +134 -66
  135. toil/test/sort/sortTest.py +91 -49
  136. toil/test/src/autoDeploymentTest.py +141 -101
  137. toil/test/src/busTest.py +20 -18
  138. toil/test/src/checkpointTest.py +8 -2
  139. toil/test/src/deferredFunctionTest.py +49 -35
  140. toil/test/src/dockerCheckTest.py +32 -24
  141. toil/test/src/environmentTest.py +135 -0
  142. toil/test/src/fileStoreTest.py +539 -272
  143. toil/test/src/helloWorldTest.py +7 -4
  144. toil/test/src/importExportFileTest.py +61 -31
  145. toil/test/src/jobDescriptionTest.py +46 -21
  146. toil/test/src/jobEncapsulationTest.py +2 -0
  147. toil/test/src/jobFileStoreTest.py +74 -50
  148. toil/test/src/jobServiceTest.py +187 -73
  149. toil/test/src/jobTest.py +121 -71
  150. toil/test/src/miscTests.py +19 -18
  151. toil/test/src/promisedRequirementTest.py +82 -36
  152. toil/test/src/promisesTest.py +7 -6
  153. toil/test/src/realtimeLoggerTest.py +10 -6
  154. toil/test/src/regularLogTest.py +71 -37
  155. toil/test/src/resourceTest.py +80 -49
  156. toil/test/src/restartDAGTest.py +36 -22
  157. toil/test/src/resumabilityTest.py +9 -2
  158. toil/test/src/retainTempDirTest.py +45 -14
  159. toil/test/src/systemTest.py +12 -8
  160. toil/test/src/threadingTest.py +44 -25
  161. toil/test/src/toilContextManagerTest.py +10 -7
  162. toil/test/src/userDefinedJobArgTypeTest.py +8 -5
  163. toil/test/src/workerTest.py +73 -23
  164. toil/test/utils/toilDebugTest.py +103 -33
  165. toil/test/utils/toilKillTest.py +4 -5
  166. toil/test/utils/utilsTest.py +245 -106
  167. toil/test/wdl/wdltoil_test.py +818 -149
  168. toil/test/wdl/wdltoil_test_kubernetes.py +91 -0
  169. toil/toilState.py +120 -35
  170. toil/utils/toilConfig.py +13 -4
  171. toil/utils/toilDebugFile.py +44 -27
  172. toil/utils/toilDebugJob.py +214 -27
  173. toil/utils/toilDestroyCluster.py +11 -6
  174. toil/utils/toilKill.py +8 -3
  175. toil/utils/toilLaunchCluster.py +256 -140
  176. toil/utils/toilMain.py +37 -16
  177. toil/utils/toilRsyncCluster.py +32 -14
  178. toil/utils/toilSshCluster.py +49 -22
  179. toil/utils/toilStats.py +356 -273
  180. toil/utils/toilStatus.py +292 -139
  181. toil/utils/toilUpdateEC2Instances.py +3 -1
  182. toil/version.py +12 -12
  183. toil/wdl/utils.py +5 -5
  184. toil/wdl/wdltoil.py +3913 -1033
  185. toil/worker.py +367 -184
  186. {toil-6.1.0a1.dist-info → toil-8.0.0.dist-info}/LICENSE +25 -0
  187. toil-8.0.0.dist-info/METADATA +173 -0
  188. toil-8.0.0.dist-info/RECORD +253 -0
  189. {toil-6.1.0a1.dist-info → toil-8.0.0.dist-info}/WHEEL +1 -1
  190. toil-6.1.0a1.dist-info/METADATA +0 -125
  191. toil-6.1.0a1.dist-info/RECORD +0 -237
  192. {toil-6.1.0a1.dist-info → toil-8.0.0.dist-info}/entry_points.txt +0 -0
  193. {toil-6.1.0a1.dist-info → toil-8.0.0.dist-info}/top_level.txt +0 -0
toil/utils/toilStats.py CHANGED
@@ -14,18 +14,57 @@
14
14
  """Reports statistical data about a given Toil workflow."""
15
15
  import json
16
16
  import logging
17
+ import math
18
+ import sys
17
19
  from argparse import ArgumentParser, Namespace
18
20
  from functools import partial
19
- from typing import Any, Callable, Dict, List, Optional, TextIO
21
+ from typing import Any, Callable, Optional, TextIO, Union
20
22
 
21
23
  from toil.common import Config, Toil, parser_with_common_options
22
24
  from toil.job import Job
23
- from toil.jobStores.abstractJobStore import AbstractJobStore
25
+ from toil.jobStores.abstractJobStore import AbstractJobStore, NoSuchJobStoreException
24
26
  from toil.lib.expando import Expando
27
+ from toil.options.common import SYS_MAX_SIZE
25
28
  from toil.statsAndLogging import set_logging_from_options
26
29
 
27
30
  logger = logging.getLogger(__name__)
28
31
 
32
+ # These categories of stat will be reported
33
+ CATEGORIES = ["time", "clock", "wait", "memory", "disk"]
34
+ # These are the units they are stored in
35
+ CATEGORY_UNITS = {
36
+ "time": "s",
37
+ "clock": "core-s",
38
+ "wait": "core-s",
39
+ "memory": "KiB",
40
+ "disk": "B",
41
+ }
42
+ # These are what we call them to the user
43
+ TITLES = {
44
+ "time": "Real Time",
45
+ "clock": "CPU Time",
46
+ "wait": "CPU Wait",
47
+ "memory": "Memory",
48
+ "disk": "Disk",
49
+ }
50
+
51
+ # Of those, these are in time
52
+ TIME_CATEGORIES = {"time", "clock", "wait"}
53
+ # And these are in space
54
+ SPACE_CATEGORIES = {"memory", "disk"}
55
+ # These categories aren't stored and need to be computed
56
+ COMPUTED_CATEGORIES = {"wait"}
57
+
58
+ # The different kinds of summaries have both short and long names, and we need
59
+ # to convert between them.
60
+ LONG_FORMS = {
61
+ "med": "median",
62
+ "ave": "average",
63
+ "min": "min",
64
+ "total": "total",
65
+ "max": "max",
66
+ }
67
+
29
68
 
30
69
  class ColumnWidths:
31
70
  """
@@ -33,33 +72,33 @@ class ColumnWidths:
33
72
  """
34
73
 
35
74
  def __init__(self) -> None:
36
- self.categories = ["time", "clock", "wait", "memory"]
75
+ self.categories = CATEGORIES
37
76
  self.fields_count = ["count", "min", "med", "ave", "max", "total"]
38
77
  self.fields = ["min", "med", "ave", "max", "total"]
39
- self.data: Dict[str, int] = {}
78
+ self.data: dict[str, int] = {}
40
79
  for category in self.categories:
41
80
  for field in self.fields_count:
42
- self.setWidth(category, field, 8)
81
+ self.set_width(category, field, 8)
43
82
 
44
83
  def title(self, category: str) -> int:
45
84
  """Return the total printed length of this category item."""
46
- return sum(self.getWidth(category, x) for x in self.fields)
85
+ return sum(self.get_width(category, x) for x in self.fields)
47
86
 
48
- def getWidth(self, category: str, field: str) -> int:
87
+ def get_width(self, category: str, field: str) -> int:
49
88
  category = category.lower()
50
89
  return self.data[f"{category}_{field}"]
51
90
 
52
- def setWidth(self, category: str, field: str, width: int) -> None:
91
+ def set_width(self, category: str, field: str, width: int) -> None:
53
92
  category = category.lower()
54
93
  self.data[f"{category}_{field}"] = width
55
94
 
56
95
  def report(self) -> None:
57
96
  for c in self.categories:
58
97
  for f in self.fields:
59
- print("%s %s %d" % (c, f, self.getWidth(c, f)))
98
+ print("%s %s %d" % (c, f, self.get_width(c, f)))
60
99
 
61
100
 
62
- def padStr(s: str, field: Optional[int] = None) -> str:
101
+ def pad_str(s: str, field: Optional[int] = None) -> str:
63
102
  """Pad the beginning of a string with spaces, if necessary."""
64
103
  if field is None or len(s) >= field:
65
104
  return s
@@ -67,48 +106,60 @@ def padStr(s: str, field: Optional[int] = None) -> str:
67
106
  return " " * (field - len(s)) + s
68
107
 
69
108
 
70
- def prettyMemory(k: float, field: Optional[int] = None, isBytes: bool = False) -> str:
71
- """Given input k as kilobytes, return a nicely formatted string."""
72
- if isBytes:
73
- k /= 1024
109
+ def pretty_space(k: float, field: Optional[int] = None, alone: bool = False) -> str:
110
+ """Given input k as kibibytes, return a nicely formatted string."""
111
+ # If we don't have a header to say bytes, include the B.
112
+ trailer = "B" if alone else ""
74
113
  if k < 1024:
75
- return padStr("%gK" % k, field)
114
+ return pad_str("{:g}Ki{}".format(k, trailer), field)
76
115
  if k < (1024 * 1024):
77
- return padStr("%.1fM" % (k / 1024.0), field)
116
+ return pad_str("{:.1f}Mi{}".format(k / 1024.0, trailer), field)
78
117
  if k < (1024 * 1024 * 1024):
79
- return padStr("%.1fG" % (k / 1024.0 / 1024.0), field)
118
+ return pad_str("{:.1f}Gi{}".format(k / 1024.0 / 1024.0, trailer), field)
80
119
  if k < (1024 * 1024 * 1024 * 1024):
81
- return padStr("%.1fT" % (k / 1024.0 / 1024.0 / 1024.0), field)
120
+ return pad_str(
121
+ "{:.1f}Ti{}".format(k / 1024.0 / 1024.0 / 1024.0, trailer), field
122
+ )
82
123
  if k < (1024 * 1024 * 1024 * 1024 * 1024):
83
- return padStr("%.1fP" % (k / 1024.0 / 1024.0 / 1024.0 / 1024.0), field)
124
+ return pad_str(
125
+ "{:.1f}Pi{}".format(k / 1024.0 / 1024.0 / 1024.0 / 1024.0, trailer), field
126
+ )
84
127
 
85
128
  # due to https://stackoverflow.com/questions/47149154
86
129
  assert False
87
130
 
88
131
 
89
- def prettyTime(t: float, field: Optional[int] = None) -> str:
90
- """Given input t as seconds, return a nicely formatted string."""
132
+ def pretty_time(
133
+ t: float, field: Optional[int] = None, unit: str = "s", alone: bool = False
134
+ ) -> str:
135
+ """
136
+ Given input t as seconds, return a nicely formatted string.
137
+ """
138
+ assert unit in ("s", "core-s")
139
+ # Qualify our CPU times as CPU time if we aren't in a table that does that
140
+ unit_str = report_unit(unit) if alone else "s"
141
+
91
142
  from math import floor
92
143
 
93
144
  pluralDict = {True: "s", False: ""}
94
145
  if t < 120:
95
- return padStr("%ds" % t, field)
146
+ return pad_str("%d%s" % (t, unit_str), field)
96
147
  if t < 120 * 60:
97
148
  m = floor(t / 60.0)
98
149
  s = t % 60
99
- return padStr("%dm%ds" % (m, s), field)
150
+ return pad_str("%dm%d%s" % (m, s, unit_str), field)
100
151
  if t < 25 * 60 * 60:
101
152
  h = floor(t / 60.0 / 60.0)
102
153
  m = floor((t - (h * 60.0 * 60.0)) / 60.0)
103
154
  s = t % 60
104
- return padStr("%dh%gm%ds" % (h, m, s), field)
155
+ return pad_str("%dh%gm%d%s" % (h, m, s, unit_str), field)
105
156
  if t < 7 * 24 * 60 * 60:
106
157
  d = floor(t / 24.0 / 60.0 / 60.0)
107
158
  h = floor((t - (d * 24.0 * 60.0 * 60.0)) / 60.0 / 60.0)
108
159
  m = floor((t - (d * 24.0 * 60.0 * 60.0) - (h * 60.0 * 60.0)) / 60.0)
109
160
  s = t % 60
110
161
  dPlural = pluralDict[d > 1]
111
- return padStr("%dday%s%dh%dm%ds" % (d, dPlural, h, m, s), field)
162
+ return pad_str("%dday%s%dh%dm%d%s" % (d, dPlural, h, m, s, unit_str), field)
112
163
  w = floor(t / 7.0 / 24.0 / 60.0 / 60.0)
113
164
  d = floor((t - (w * 7 * 24 * 60 * 60)) / 24.0 / 60.0 / 60.0)
114
165
  h = floor(
@@ -126,39 +177,109 @@ def prettyTime(t: float, field: Optional[int] = None) -> str:
126
177
  s = t % 60
127
178
  wPlural = pluralDict[w > 1]
128
179
  dPlural = pluralDict[d > 1]
129
- return padStr("%dweek%s%dday%s%dh%dm%ds" % (w, wPlural, d, dPlural, h, m, s), field)
180
+ return pad_str(
181
+ "%dweek%s%dday%s%dh%dm%d%s" % (w, wPlural, d, dPlural, h, m, s, unit_str), field
182
+ )
183
+
184
+
185
+ def report_unit(unit: str) -> str:
186
+ """
187
+ Format a unit name for display.
188
+ """
189
+ if unit == "core-s":
190
+ return "core·s"
191
+ return unit
130
192
 
131
193
 
132
- def reportTime(t: float, options: Namespace, field: Optional[int] = None) -> str:
194
+ def report_time(
195
+ t: float,
196
+ options: Namespace,
197
+ field: Optional[int] = None,
198
+ unit: str = "s",
199
+ alone: bool = False,
200
+ ) -> str:
133
201
  """Given t seconds, report back the correct format as string."""
202
+ assert unit in ("s", "core-s")
134
203
  if options.pretty:
135
- return prettyTime(t, field=field)
136
- elif field is not None:
137
- return "%*.2f" % (field, t)
138
- return "%.2f" % t
204
+ return pretty_time(t, field=field, unit=unit, alone=alone)
205
+ unit_text = f" {report_unit(unit)}" if alone else ""
206
+ if field is not None:
207
+ assert field >= len(unit_text)
208
+ return "%*.2f%s" % (field - len(unit_text), t, unit_text)
209
+ return "{:.2f}{}".format(t, unit_text)
139
210
 
140
211
 
141
- def reportMemory(
142
- k: float, options: Namespace, field: Optional[int] = None, isBytes: bool = False
212
+ def report_space(
213
+ k: float,
214
+ options: Namespace,
215
+ field: Optional[int] = None,
216
+ unit: str = "KiB",
217
+ alone: bool = False,
143
218
  ) -> str:
144
- """Given k kilobytes, report back the correct format as string."""
219
+ """
220
+ Given k kibibytes, report back the correct format as string.
221
+
222
+ If unit is set to B, convert to KiB first.
223
+ """
224
+ if unit == "B":
225
+ k /= 1024.0
226
+ unit = "KiB"
227
+ assert unit == "KiB"
145
228
  if options.pretty:
146
- return prettyMemory(int(k), field=field, isBytes=isBytes)
229
+ return pretty_space(int(k), field=field, alone=alone)
147
230
  else:
148
- if isBytes:
149
- k /= 1024.0
231
+ # If we don't have a heading to say bytes, include the B
232
+ trailer = "KiB" if alone else "Ki"
150
233
  if field is not None:
151
- return "%*dK" % (field - 1, k) # -1 for the "K"
234
+ assert field >= len(trailer)
235
+ return "%*d%s" % (field - len(trailer), k, trailer)
152
236
  else:
153
- return "%dK" % int(k)
237
+ return "%d%s" % (int(k), trailer)
238
+
239
+
240
+ def report_number(
241
+ n: Union[int, float, None], field: Optional[int] = None, nan_value: str = "NaN"
242
+ ) -> str:
243
+ """
244
+ Given a number, report back the correct format as string.
245
+
246
+ If it is a NaN or None, use nan_value to represent it instead.
247
+ """
248
+ if n is None or math.isnan(n):
249
+ return pad_str(nan_value, field=field)
250
+ else:
251
+ # Make sure not to format with too much precision for the field size;
252
+ # leave room for . and the spacing to the previous field.
253
+ return "%*.*g" % (field, field - 2, n) if field else "%g" % n
254
+
255
+
256
+ def report(
257
+ v: float,
258
+ category: str,
259
+ options: Namespace,
260
+ field: Optional[int] = None,
261
+ alone=False,
262
+ ) -> str:
263
+ """
264
+ Report a value of the given category formatted as a string.
154
265
 
266
+ Uses the given field width if set.
155
267
 
156
- def reportNumber(n: float, field: Optional[int] = None) -> str:
157
- """Given n an integer, report back the correct format as string."""
158
- return "%*g" % (field, n) if field else "%g" % n
268
+ If alone is set, the field is being formatted outside a table and might need a unit.
269
+ """
270
+
271
+ unit = CATEGORY_UNITS.get(category)
272
+ if unit in ("s", "core-s"):
273
+ # This is time.
274
+ return report_time(v, options, field=field, unit=unit, alone=alone)
275
+ elif unit in ("B", "KiB"):
276
+ # This is space.
277
+ return report_space(v, options, field=field, unit=unit, alone=alone)
278
+ else:
279
+ raise ValueError(f"Unimplemented unit {unit} for category {category}")
159
280
 
160
281
 
161
- def sprintTag(
282
+ def sprint_tag(
162
283
  key: str,
163
284
  tag: Expando,
164
285
  options: Namespace,
@@ -167,9 +288,9 @@ def sprintTag(
167
288
  """Generate a pretty-print ready string from a JTTag()."""
168
289
  if columnWidths is None:
169
290
  columnWidths = ColumnWidths()
170
- header = " %7s " % decorateTitle("Count", options)
291
+ header = " %7s " % decorate_title("count", "Count", options)
171
292
  sub_header = " %7s " % "n"
172
- tag_str = f" {reportNumber(n=tag.total_number, field=7)}"
293
+ tag_str = f" {report_number(n=tag.total_number, field=7)}"
173
294
  out_str = ""
174
295
  if key == "job":
175
296
  out_str += " {:<12} | {:>7}{:>7}{:>7}{:>7}\n".format(
@@ -182,109 +303,82 @@ def sprintTag(
182
303
  tag.average_number_per_worker,
183
304
  tag.max_number_per_worker,
184
305
  ]:
185
- worker_str += reportNumber(n=t, field=7)
306
+ worker_str += report_number(n=t, field=7)
186
307
  out_str += worker_str + "\n"
187
- if "time" in options.categories:
188
- header += "| %*s " % (
189
- columnWidths.title("time"),
190
- decorateTitle("Time", options),
191
- )
192
- sub_header += decorateSubHeader("Time", columnWidths, options)
193
- tag_str += " | "
194
- for t, width in [
195
- (tag.min_time, columnWidths.getWidth("time", "min")),
196
- (tag.median_time, columnWidths.getWidth("time", "med")),
197
- (tag.average_time, columnWidths.getWidth("time", "ave")),
198
- (tag.max_time, columnWidths.getWidth("time", "max")),
199
- (tag.total_time, columnWidths.getWidth("time", "total")),
200
- ]:
201
- tag_str += reportTime(t, options, field=width)
202
- if "clock" in options.categories:
203
- header += "| %*s " % (
204
- columnWidths.title("clock"),
205
- decorateTitle("Clock", options),
206
- )
207
- sub_header += decorateSubHeader("Clock", columnWidths, options)
208
- tag_str += " | "
209
- for t, width in [
210
- (tag.min_clock, columnWidths.getWidth("clock", "min")),
211
- (tag.median_clock, columnWidths.getWidth("clock", "med")),
212
- (tag.average_clock, columnWidths.getWidth("clock", "ave")),
213
- (tag.max_clock, columnWidths.getWidth("clock", "max")),
214
- (tag.total_clock, columnWidths.getWidth("clock", "total")),
215
- ]:
216
- tag_str += reportTime(t, options, field=width)
217
- if "wait" in options.categories:
218
- header += "| %*s " % (
219
- columnWidths.title("wait"),
220
- decorateTitle("Wait", options),
221
- )
222
- sub_header += decorateSubHeader("Wait", columnWidths, options)
223
- tag_str += " | "
224
- for t, width in [
225
- (tag.min_wait, columnWidths.getWidth("wait", "min")),
226
- (tag.median_wait, columnWidths.getWidth("wait", "med")),
227
- (tag.average_wait, columnWidths.getWidth("wait", "ave")),
228
- (tag.max_wait, columnWidths.getWidth("wait", "max")),
229
- (tag.total_wait, columnWidths.getWidth("wait", "total")),
230
- ]:
231
- tag_str += reportTime(t, options, field=width)
232
- if "memory" in options.categories:
308
+
309
+ for category in CATEGORIES:
310
+ if category not in options.categories:
311
+ continue
312
+
233
313
  header += "| %*s " % (
234
- columnWidths.title("memory"),
235
- decorateTitle("Memory", options),
314
+ columnWidths.title(category),
315
+ decorate_title(category, TITLES[category], options),
236
316
  )
237
- sub_header += decorateSubHeader("Memory", columnWidths, options)
317
+ sub_header += decorate_subheader(category, columnWidths, options)
238
318
  tag_str += " | "
239
- for t, width in [
240
- (tag.min_memory, columnWidths.getWidth("memory", "min")),
241
- (tag.median_memory, columnWidths.getWidth("memory", "med")),
242
- (tag.average_memory, columnWidths.getWidth("memory", "ave")),
243
- (tag.max_memory, columnWidths.getWidth("memory", "max")),
244
- (tag.total_memory, columnWidths.getWidth("memory", "total")),
245
- ]:
246
- tag_str += reportMemory(t, options, field=width)
319
+
320
+ for field in ["min", "med", "ave", "max", "total"]:
321
+ t = getattr(tag, f"{LONG_FORMS[field]}_{category}")
322
+ width = columnWidths.get_width(category, field)
323
+ s = report(t, category, options, field=width)
324
+ tag_str += s
325
+
247
326
  out_str += header + "\n"
248
327
  out_str += sub_header + "\n"
249
328
  out_str += tag_str + "\n"
250
329
  return out_str
251
330
 
252
331
 
253
- def decorateTitle(title: str, options: Namespace) -> str:
254
- """Add a marker to TITLE if the TITLE is sorted on."""
255
- if title.lower() == options.sortCategory:
332
+ def decorate_title(category: str, title: str, options: Namespace) -> str:
333
+ """
334
+ Add extra parts to the category titles.
335
+
336
+ Add units to title if they won't appear in the formatted values.
337
+ Add a marker to TITLE if the TITLE is sorted on.
338
+ """
339
+ unit = CATEGORY_UNITS.get(category)
340
+ if unit in ("s", "core-s") and not options.pretty:
341
+ # This is a time and we won't write it out as text, so add a unit.
342
+ title = f"{title} ({report_unit(unit)})"
343
+ elif unit == "core-s" and options.pretty:
344
+ # This is a core-second category and we won't be putting the core unit
345
+ # in the value, so note that here.
346
+ title = f"{title} (core)"
347
+ elif unit in ("B", "KiB"):
348
+ # The Ki part will appear in the cell so we need a B
349
+ title = f"{title} (B)"
350
+ if category.lower() == options.sortCategory:
256
351
  return "%s*" % title
257
352
  else:
258
353
  return title
259
354
 
260
355
 
261
- def decorateSubHeader(
262
- title: str, columnWidths: ColumnWidths, options: Namespace
356
+ def decorate_subheader(
357
+ category: str, columnWidths: ColumnWidths, options: Namespace
263
358
  ) -> str:
264
359
  """Add a marker to the correct field if the TITLE is sorted on."""
265
- title = title.lower()
266
- if title != options.sortCategory:
360
+ if category != options.sortCategory:
267
361
  s = "| %*s%*s%*s%*s%*s " % (
268
- columnWidths.getWidth(title, "min"),
362
+ columnWidths.get_width(category, "min"),
269
363
  "min",
270
- columnWidths.getWidth(title, "med"),
364
+ columnWidths.get_width(category, "med"),
271
365
  "med",
272
- columnWidths.getWidth(title, "ave"),
366
+ columnWidths.get_width(category, "ave"),
273
367
  "ave",
274
- columnWidths.getWidth(title, "max"),
368
+ columnWidths.get_width(category, "max"),
275
369
  "max",
276
- columnWidths.getWidth(title, "total"),
370
+ columnWidths.get_width(category, "total"),
277
371
  "total",
278
372
  )
279
373
  return s
280
374
  else:
281
375
  s = "| "
282
376
  for field, width in [
283
- ("min", columnWidths.getWidth(title, "min")),
284
- ("med", columnWidths.getWidth(title, "med")),
285
- ("ave", columnWidths.getWidth(title, "ave")),
286
- ("max", columnWidths.getWidth(title, "max")),
287
- ("total", columnWidths.getWidth(title, "total")),
377
+ ("min", columnWidths.get_width(category, "min")),
378
+ ("med", columnWidths.get_width(category, "med")),
379
+ ("ave", columnWidths.get_width(category, "ave")),
380
+ ("max", columnWidths.get_width(category, "max")),
381
+ ("total", columnWidths.get_width(category, "total")),
288
382
  ]:
289
383
  if options.sortField == field:
290
384
  s += "%*s*" % (width - 1, field)
@@ -302,190 +396,165 @@ def get(tree: Expando, name: str) -> float:
302
396
  return float("nan")
303
397
 
304
398
 
305
- def sortJobs(jobTypes: List[Any], options: Namespace) -> List[Any]:
399
+ def sort_jobs(jobTypes: list[Any], options: Namespace) -> list[Any]:
306
400
  """Return a jobTypes all sorted."""
307
- longforms = {
308
- "med": "median",
309
- "ave": "average",
310
- "min": "min",
311
- "total": "total",
312
- "max": "max",
313
- }
314
- sortField = longforms[options.sortField]
315
- if (
316
- options.sortCategory == "time"
317
- or options.sortCategory == "clock"
318
- or options.sortCategory == "wait"
319
- or options.sortCategory == "memory"
320
- ):
401
+ sortField = LONG_FORMS[options.sortField]
402
+ if options.sortCategory in CATEGORIES:
321
403
  return sorted(
322
404
  jobTypes,
323
- key=lambda tag: getattr(tag, "%s_%s" % (sortField, options.sortCategory)),
324
- reverse=options.sortReverse,
405
+ key=lambda tag: getattr(
406
+ tag, "{}_{}".format(sortField, options.sortCategory)
407
+ ),
408
+ reverse=options.sort == "decending",
325
409
  )
326
410
  elif options.sortCategory == "alpha":
327
411
  return sorted(
328
412
  jobTypes,
329
413
  key=lambda tag: tag.name, # type: ignore
330
- reverse=options.sortReverse,
414
+ reverse=options.sort == "decending",
331
415
  )
332
416
  elif options.sortCategory == "count":
333
417
  return sorted(
334
418
  jobTypes,
335
419
  key=lambda tag: tag.total_number, # type: ignore
336
- reverse=options.sortReverse,
420
+ reverse=options.sort == "decending",
337
421
  )
338
422
 
339
423
  # due to https://stackoverflow.com/questions/47149154
340
424
  assert False
341
425
 
342
426
 
343
- def reportPrettyData(
427
+ def report_pretty_data(
344
428
  root: Expando,
345
- worker: List[Job],
346
- job: List[Job],
347
- job_types: List[Any],
429
+ worker: Expando,
430
+ job: Expando,
431
+ job_types: list[Any],
348
432
  options: Namespace,
349
433
  ) -> str:
350
434
  """Print the important bits out."""
351
435
  out_str = "Batch System: %s\n" % root.batch_system
352
436
  out_str += "Default Cores: %s Default Memory: %s\n" "Max Cores: %s\n" % (
353
- reportNumber(n=get(root, "default_cores")),
354
- reportMemory(get(root, "default_memory"), options, isBytes=True),
355
- reportNumber(n=get(root, "max_cores")),
437
+ report_number(n=get(root, "default_cores")),
438
+ # Although per-job memory usage is in KiB, our default is stored in bytes.
439
+ report_space(get(root, "default_memory"), options, unit="B", alone=True),
440
+ report_number(n=get(root, "max_cores"), nan_value="unlimited"),
356
441
  )
357
- out_str += "Total Clock: {} Total Runtime: {}\n".format(
358
- reportTime(get(root, "total_clock"), options),
359
- reportTime(get(root, "total_run_time"), options),
442
+ out_str += "Local CPU Time: {} Overall Runtime: {}\n".format(
443
+ report(get(root, "total_clock"), "clock", options, alone=True),
444
+ report(get(root, "total_run_time"), "time", options, alone=True),
360
445
  )
361
- job_types = sortJobs(job_types, options)
362
- columnWidths = computeColumnWidths(job_types, worker, job, options)
446
+ job_types = sort_jobs(job_types, options)
447
+ columnWidths = compute_column_widths(job_types, worker, job, options)
363
448
  out_str += "Worker\n"
364
- out_str += sprintTag("worker", worker, options, columnWidths=columnWidths)
449
+ out_str += sprint_tag("worker", worker, options, columnWidths=columnWidths)
365
450
  out_str += "Job\n"
366
- out_str += sprintTag("job", job, options, columnWidths=columnWidths)
451
+ out_str += sprint_tag("job", job, options, columnWidths=columnWidths)
367
452
  for t in job_types:
368
453
  out_str += f" {t.name}\n"
369
454
  out_str += f" Total Cores: {t.total_cores}\n"
370
- out_str += sprintTag(t.name, t, options, columnWidths=columnWidths)
455
+ out_str += sprint_tag(t.name, t, options, columnWidths=columnWidths)
371
456
  return out_str
372
457
 
373
458
 
374
- def computeColumnWidths(
375
- job_types: List[Any], worker: List[Job], job: List[Job], options: Expando
459
+ def compute_column_widths(
460
+ job_types: list[Any], worker: Expando, job: Expando, options: Namespace
376
461
  ) -> ColumnWidths:
377
462
  """Return a ColumnWidths() object with the correct max widths."""
378
463
  cw = ColumnWidths()
379
464
  for t in job_types:
380
- updateColumnWidths(t, cw, options)
381
- updateColumnWidths(worker, cw, options)
382
- updateColumnWidths(job, cw, options)
465
+ update_column_widths(t, cw, options)
466
+ update_column_widths(worker, cw, options)
467
+ update_column_widths(job, cw, options)
383
468
  return cw
384
469
 
385
470
 
386
- def updateColumnWidths(tag: Expando, cw: ColumnWidths, options: Expando) -> None:
471
+ def update_column_widths(tag: Expando, cw: ColumnWidths, options: Namespace) -> None:
387
472
  """Update the column width attributes for this tag's fields."""
388
- longforms = {
389
- "med": "median",
390
- "ave": "average",
391
- "min": "min",
392
- "total": "total",
393
- "max": "max",
394
- }
395
- for category in ["time", "clock", "wait", "memory"]:
473
+ # TODO: Deduplicate with actual printing code!
474
+ for category in CATEGORIES:
396
475
  if category in options.categories:
397
476
  for field in ["min", "med", "ave", "max", "total"]:
398
- t = getattr(tag, f"{longforms[field]}_{category}")
399
- if category in ["time", "clock", "wait"]:
400
- s = reportTime(
401
- t, options, field=cw.getWidth(category, field)
402
- ).strip()
403
- else:
404
- s = reportMemory(
405
- t, options, field=cw.getWidth(category, field), isBytes=True
406
- ).strip()
407
- if len(s) >= cw.getWidth(category, field):
477
+ t = getattr(tag, f"{LONG_FORMS[field]}_{category}")
478
+ width = cw.get_width(category, field)
479
+ s = report(t, category, options, field=width).strip()
480
+ if len(s) >= cw.get_width(category, field):
408
481
  # this string is larger than max, width must be increased
409
- cw.setWidth(category, field, len(s) + 1)
482
+ cw.set_width(category, field, len(s) + 1)
410
483
 
411
484
 
412
- def buildElement(element: Expando, items: List[Job], itemName: str) -> Expando:
485
+ def build_element(
486
+ element: Expando, items: list[Job], item_name: str, defaults: dict[str, float]
487
+ ) -> Expando:
413
488
  """Create an element for output."""
414
489
 
415
490
  def assertNonnegative(i: float, name: str) -> float:
416
491
  if i < 0:
417
- raise RuntimeError("Negative value %s reported for %s" % (i, name))
492
+ raise RuntimeError("Negative value {} reported for {}".format(i, name))
418
493
  else:
419
494
  return float(i)
420
495
 
421
- totalCores = 0
422
-
423
- itemTimes = []
424
- itemClocks = []
425
- itemMemory = []
496
+ # Make lists of all values for all items in each category, plus requested cores.
497
+ item_values = {category: [] for category in (CATEGORIES + ["cores"])}
426
498
 
427
499
  for item in items:
428
500
  # If something lacks an entry, assume it used none of that thing.
429
501
  # This avoids crashing when jobs e.g. aren't done.
430
- itemTimes.append(assertNonnegative(float(item.get("time", 0)), "time"))
431
- itemClocks.append(assertNonnegative(float(item.get("clock", 0)), "clock"))
432
- itemMemory.append(assertNonnegative(float(item.get("memory", 0)), "memory"))
433
- totalCores += assertNonnegative(
434
- float(item.get("requested_cores", 0)), "requested_cores"
502
+ for category, values in item_values.items():
503
+ if category in COMPUTED_CATEGORIES:
504
+ continue
505
+ category_key = category if category != "cores" else "requested_cores"
506
+ category_value = assertNonnegative(
507
+ float(item.get(category_key, defaults[category])), category
508
+ )
509
+ values.append(category_value)
510
+
511
+ for index in range(0, len(item_values[CATEGORIES[0]])):
512
+ # For each item, compute the computed categories
513
+ item_values["wait"].append(
514
+ item_values["time"][index] * item_values["cores"][index]
515
+ - item_values["clock"][index]
435
516
  )
436
517
 
437
- assert len(itemClocks) == len(itemTimes) == len(itemMemory)
438
-
439
- itemWaits = []
440
- for index in range(0, len(itemTimes)):
441
- itemWaits.append(itemTimes[index] - itemClocks[index])
442
-
443
- itemWaits.sort()
444
- itemTimes.sort()
445
- itemClocks.sort()
446
- itemMemory.sort()
447
-
448
- if len(itemTimes) == 0:
449
- itemTimes.append(0)
450
- itemClocks.append(0)
451
- itemWaits.append(0)
452
- itemMemory.append(0)
453
-
454
- element[itemName] = Expando(
455
- total_number=float(len(items)),
456
- total_time=float(sum(itemTimes)),
457
- median_time=float(itemTimes[len(itemTimes) // 2]),
458
- average_time=float(sum(itemTimes) / len(itemTimes)),
459
- min_time=float(min(itemTimes)),
460
- max_time=float(max(itemTimes)),
461
- total_clock=float(sum(itemClocks)),
462
- median_clock=float(itemClocks[len(itemClocks) // 2]),
463
- average_clock=float(sum(itemClocks) / len(itemClocks)),
464
- min_clock=float(min(itemClocks)),
465
- max_clock=float(max(itemClocks)),
466
- total_wait=float(sum(itemWaits)),
467
- median_wait=float(itemWaits[len(itemWaits) // 2]),
468
- average_wait=float(sum(itemWaits) / len(itemWaits)),
469
- min_wait=float(min(itemWaits)),
470
- max_wait=float(max(itemWaits)),
471
- total_memory=float(sum(itemMemory)),
472
- median_memory=float(itemMemory[len(itemMemory) // 2]),
473
- average_memory=float(sum(itemMemory) / len(itemMemory)),
474
- min_memory=float(min(itemMemory)),
475
- max_memory=float(max(itemMemory)),
476
- total_cores=totalCores,
477
- name=itemName,
478
- )
479
- return element[itemName]
518
+ for category, values in item_values.items():
519
+ values.sort()
520
+
521
+ if len(item_values[CATEGORIES[0]]) == 0:
522
+ # Nothing actually there so make a 0 value
523
+ for k, v in item_values.items():
524
+ v.append(0)
525
+
526
+ item_element = Expando(total_number=float(len(items)), name=item_name)
480
527
 
528
+ for category, values in item_values.items():
529
+ item_element["total_" + category] = float(sum(values))
530
+ item_element["median_" + category] = float(values[len(values) // 2])
531
+ item_element["average_" + category] = float(sum(values) / len(values))
532
+ item_element["min_" + category] = float(min(values))
533
+ item_element["max_" + category] = float(max(values))
481
534
 
482
- def createSummary(
535
+ element[item_name] = item_element
536
+
537
+ return item_element
538
+
539
+
540
+ def create_summary(
483
541
  element: Expando,
484
- containingItems: List[Job],
542
+ containingItems: list[Expando],
485
543
  containingItemName: str,
486
- getFn: Callable[[Job], List[Optional[Job]]],
544
+ count_contained: Callable[[Expando], int],
487
545
  ) -> None:
488
- itemCounts = [len(getFn(containingItem)) for containingItem in containingItems]
546
+ """
547
+ Figure out how many jobs (or contained items) ran on each worker (or containing item).
548
+
549
+ Stick a bunch of xxx_number_per_xxx stats into element to describe this.
550
+
551
+ :param count_contained: function that maps from containing item to number of contained items.
552
+ """
553
+
554
+ # TODO: this still thinks like the old XML stats, even though now the
555
+ # worker records no longer actually contain the job records.
556
+
557
+ itemCounts = [count_contained(containingItem) for containingItem in containingItems]
489
558
  itemCounts.sort()
490
559
  if len(itemCounts) == 0:
491
560
  itemCounts.append(0)
@@ -499,10 +568,14 @@ def createSummary(
499
568
  element["max_number_per_%s" % containingItemName] = max(itemCounts)
500
569
 
501
570
 
502
- def getStats(jobStore: AbstractJobStore) -> Expando:
503
- """Collect and return the stats and config data."""
571
+ def get_stats(jobStore: AbstractJobStore) -> Expando:
572
+ """
573
+ Sum together all the stats information in the job store.
504
574
 
505
- def aggregateStats(fileHandle: TextIO, aggregateObject: Expando) -> None:
575
+ Produces one object containing lists of the values from all the summed objects.
576
+ """
577
+
578
+ def aggregate_stats(fileHandle: TextIO, aggregateObject: Expando) -> None:
506
579
  try:
507
580
  stats = json.load(fileHandle, object_hook=Expando)
508
581
  for key in list(stats.keys()):
@@ -517,12 +590,12 @@ def getStats(jobStore: AbstractJobStore) -> Expando:
517
590
  pass # The file is corrupted.
518
591
 
519
592
  aggregateObject = Expando()
520
- callBack = partial(aggregateStats, aggregateObject=aggregateObject)
593
+ callBack = partial(aggregate_stats, aggregateObject=aggregateObject)
521
594
  jobStore.read_logs(callBack, read_all=True)
522
595
  return aggregateObject
523
596
 
524
597
 
525
- def processData(config: Config, stats: Expando) -> Expando:
598
+ def process_data(config: Config, stats: Expando) -> Expando:
526
599
  """
527
600
  Collate the stats and report
528
601
  """
@@ -531,7 +604,11 @@ def processData(config: Config, stats: Expando) -> Expando:
531
604
  stats.total_time = [0.0]
532
605
  stats.total_clock = [0.0]
533
606
 
607
+ # This is actually the sum of *overall* wall clock time as measured by the
608
+ # leader in each leader invocation, not a sum over jobs.
534
609
  stats.total_time = sum(float(number) for number in stats.total_time)
610
+ # And this is CPU clock as measured by the leader, so it will count time
611
+ # used in local jobs but not remote ones.
535
612
  stats.total_clock = sum(float(number) for number in stats.total_clock)
536
613
 
537
614
  collatedStatsTag = Expando(
@@ -540,7 +617,7 @@ def processData(config: Config, stats: Expando) -> Expando:
540
617
  batch_system=config.batchSystem,
541
618
  default_memory=str(config.defaultMemory),
542
619
  default_cores=str(config.defaultCores),
543
- max_cores=str(config.maxCores),
620
+ max_cores=str(config.maxCores if config.maxCores != SYS_MAX_SIZE else None),
544
621
  )
545
622
 
546
623
  # Add worker info
@@ -548,18 +625,16 @@ def processData(config: Config, stats: Expando) -> Expando:
548
625
  jobs = [_f for _f in getattr(stats, "jobs", []) if _f]
549
626
  jobs = [item for sublist in jobs for item in sublist]
550
627
 
551
- def fn4(job: Job) -> List[Optional[Job]]:
552
- try:
553
- return list(jobs)
554
- except TypeError:
555
- return []
628
+ # Work out what usage to assume for things that didn't report
629
+ defaults = {category: 0 for category in CATEGORIES}
630
+ defaults["cores"] = config.defaultCores
556
631
 
557
- buildElement(collatedStatsTag, worker, "worker")
558
- createSummary(
559
- buildElement(collatedStatsTag, jobs, "jobs"),
632
+ build_element(collatedStatsTag, worker, "worker", defaults)
633
+ create_summary(
634
+ build_element(collatedStatsTag, jobs, "jobs", defaults),
560
635
  getattr(stats, "workers", []),
561
636
  "worker",
562
- fn4,
637
+ lambda worker: getattr(worker, "jobs_run", 0),
563
638
  )
564
639
  # Get info for each job
565
640
  jobNames = set()
@@ -569,17 +644,17 @@ def processData(config: Config, stats: Expando) -> Expando:
569
644
  collatedStatsTag.job_types = jobTypesTag
570
645
  for jobName in jobNames:
571
646
  jobTypes = [job for job in jobs if job.class_name == jobName]
572
- buildElement(jobTypesTag, jobTypes, jobName)
647
+ build_element(jobTypesTag, jobTypes, jobName, defaults)
573
648
  collatedStatsTag.name = "collatedStatsTag"
574
649
  return collatedStatsTag
575
650
 
576
651
 
577
- def reportData(tree: Expando, options: Namespace) -> None:
652
+ def report_data(tree: Expando, options: Namespace) -> None:
578
653
  # Now dump it all out to file
579
654
  if options.raw:
580
655
  out_str = json.dumps(tree, indent=4, separators=(",", ": "))
581
656
  else:
582
- out_str = reportPrettyData(
657
+ out_str = report_pretty_data(
583
658
  tree, tree.worker, tree.jobs, tree.job_types.values(), options
584
659
  )
585
660
  if options.outputFile is not None:
@@ -589,8 +664,7 @@ def reportData(tree: Expando, options: Namespace) -> None:
589
664
  print(out_str)
590
665
 
591
666
 
592
- category_choices = ["time", "clock", "wait", "memory"]
593
- sort_category_choices = ["time", "clock", "wait", "memory", "alpha", "count"]
667
+ sort_category_choices = CATEGORIES + ["alpha", "count"]
594
668
  sort_field_choices = ["min", "med", "ave", "max", "total"]
595
669
 
596
670
 
@@ -612,29 +686,28 @@ def add_stats_options(parser: ArgumentParser) -> None:
612
686
  help="if not raw, prettify the numbers to be human readable.",
613
687
  )
614
688
  parser.add_argument(
615
- "--sortReverse",
616
- "--reverseSort",
617
- default=False,
618
- action="store_true",
619
- help="Reverse sort.",
689
+ "--sort",
690
+ default="decending",
691
+ choices=["ascending", "decending"],
692
+ help="Sort direction.",
620
693
  )
621
694
  parser.add_argument(
622
695
  "--categories",
623
- default=",".join(category_choices),
696
+ default=",".join(CATEGORIES),
624
697
  type=str,
625
- help=f"Comma separated list of any of the following: {category_choices}.",
698
+ help=f"Comma separated list of any of the following: {CATEGORIES}.",
626
699
  )
627
700
  parser.add_argument(
628
701
  "--sortCategory",
629
702
  default="time",
630
703
  choices=sort_category_choices,
631
- help=f"How to sort job categories. Choices: {sort_category_choices}. Default: time.",
704
+ help=f"How to sort job categories.",
632
705
  )
633
706
  parser.add_argument(
634
707
  "--sortField",
635
708
  default="med",
636
709
  choices=sort_field_choices,
637
- help=f"How to sort job fields. Choices: {sort_field_choices}. Default: med.",
710
+ help=f"How to sort job fields.",
638
711
  )
639
712
 
640
713
 
@@ -645,14 +718,24 @@ def main() -> None:
645
718
  options = parser.parse_args()
646
719
 
647
720
  for c in options.categories.split(","):
648
- if c.strip() not in category_choices:
649
- raise ValueError(f"{c} not in {category_choices}!")
721
+ if c.strip().lower() not in CATEGORIES:
722
+ logger.critical(
723
+ "Cannot use category %s, options are: %s", c.strip().lower(), CATEGORIES
724
+ )
725
+ sys.exit(1)
650
726
  options.categories = [x.strip().lower() for x in options.categories.split(",")]
651
727
 
652
728
  set_logging_from_options(options)
653
729
  config = Config()
654
730
  config.setOptions(options)
655
- jobStore = Toil.resumeJobStore(config.jobStore)
656
- stats = getStats(jobStore)
657
- collatedStatsTag = processData(jobStore.config, stats)
658
- reportData(collatedStatsTag, options)
731
+ try:
732
+ jobStore = Toil.resumeJobStore(config.jobStore)
733
+ except NoSuchJobStoreException:
734
+ logger.critical("The job store %s does not exist", config.jobStore)
735
+ sys.exit(1)
736
+ logger.info(
737
+ "Gathering stats from jobstore... depending on the number of jobs, this may take a while (e.g. 10 jobs ~= 3 seconds; 100,000 jobs ~= 3,000 seconds or 50 minutes)."
738
+ )
739
+ stats = get_stats(jobStore)
740
+ collatedStatsTag = process_data(jobStore.config, stats)
741
+ report_data(collatedStatsTag, options)