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/lib/bioio.py CHANGED
@@ -27,15 +27,19 @@ def system(command):
27
27
  will be passed to subprocess.check_call.
28
28
  :type command: str | sequence[string]
29
29
  """
30
- logger.warning('Deprecated toil method that will be moved/replaced in a future release."')
31
- logger.debug(f'Running: {command}')
30
+ logger.warning(
31
+ 'Deprecated toil method that will be moved/replaced in a future release."'
32
+ )
33
+ logger.debug(f"Running: {command}")
32
34
  subprocess.check_call(command, shell=isinstance(command, str), bufsize=-1)
33
35
 
34
36
 
35
37
  # Used by cactus; now a wrapper and not used in Toil.
36
38
  # TODO: Remove from cactus and then remove from Toil.
37
39
  def getLogLevelString(logger=None):
38
- root_logger.warning('Deprecated toil method. Please call "logging.getLevelName" directly.')
40
+ root_logger.warning(
41
+ 'Deprecated toil method. Please call "logging.getLevelName" directly.'
42
+ )
39
43
  if logger is None:
40
44
  logger = root_logger
41
45
  return logging.getLevelName(logger.getEffectiveLevel())
@@ -44,12 +48,16 @@ def getLogLevelString(logger=None):
44
48
  # Used by cactus; now a wrapper and not used in Toil.
45
49
  # TODO: Remove from cactus and then remove from Toil.
46
50
  def setLoggingFromOptions(options):
47
- logger.warning('Deprecated toil method. Please use "toil.statsAndLogging.set_logging_from_options()" instead."')
51
+ logger.warning(
52
+ 'Deprecated toil method. Please use "toil.statsAndLogging.set_logging_from_options()" instead."'
53
+ )
48
54
  set_logging_from_options(options)
49
55
 
50
56
 
51
57
  # Used by cactus; now a wrapper and not used in Toil.
52
58
  # TODO: Remove from cactus and then remove from Toil.
53
59
  def getTempFile(suffix="", rootDir=None):
54
- logger.warning('Deprecated toil method. Please use "toil.test.get_temp_file()" instead."')
60
+ logger.warning(
61
+ 'Deprecated toil method. Please use "toil.test.get_temp_file()" instead."'
62
+ )
55
63
  return get_temp_file(suffix, rootDir)
toil/lib/compatibility.py CHANGED
@@ -7,15 +7,20 @@ def deprecated(new_function_name: str) -> Callable[..., Any]:
7
7
  def decorate(func: Callable[..., Any]) -> Callable[..., Any]:
8
8
  @functools.wraps(func)
9
9
  def call(*args: Any, **kwargs: Any) -> Any:
10
- warnings.warn(f'WARNING: "{func.__name__}()" is deprecated. Please use "{new_function_name}()" instead.',
11
- DeprecationWarning)
10
+ warnings.warn(
11
+ f'WARNING: "{func.__name__}()" is deprecated. Please use "{new_function_name}()" instead.',
12
+ DeprecationWarning,
13
+ )
12
14
  return func(*args, **kwargs)
15
+
13
16
  return call
17
+
14
18
  return decorate
15
19
 
16
20
 
17
21
  def compat_bytes(s: Union[bytes, str]) -> str:
18
- return s.decode('utf-8') if isinstance(s, bytes) else s
22
+ return s.decode("utf-8") if isinstance(s, bytes) else s
23
+
19
24
 
20
25
  # MyPy can't yet support the recursive type we would need to say "we go through
21
26
  # any structure of dicts, tuples, lists, and sets and convert all bytes types
@@ -28,13 +33,13 @@ def compat_bytes_recursive(data: Any) -> Any:
28
33
  """
29
34
  if isinstance(data, dict):
30
35
  # Keyed collection
31
- return type(data)((compat_bytes_recursive(i) for i in data.items()))
36
+ return type(data)(compat_bytes_recursive(i) for i in data.items())
32
37
  elif isinstance(data, (tuple, list, set)):
33
38
  # Flat collection
34
- return type(data)((compat_bytes_recursive(i) for i in data))
39
+ return type(data)(compat_bytes_recursive(i) for i in data)
35
40
  elif isinstance(data, bytes):
36
41
  # Leaf bytes
37
- return data.decode('utf-8')
42
+ return data.decode("utf-8")
38
43
  else:
39
44
  # Leaf non-bytes
40
45
  return data
toil/lib/conversions.py CHANGED
@@ -4,68 +4,99 @@ Also contains general conversion functions
4
4
  """
5
5
 
6
6
  import math
7
- from typing import SupportsInt, Tuple, Union
7
+ from typing import Optional, SupportsInt, Union
8
8
 
9
9
  # See https://en.wikipedia.org/wiki/Binary_prefix
10
- BINARY_PREFIXES = ['ki', 'mi', 'gi', 'ti', 'pi', 'ei', 'kib', 'mib', 'gib', 'tib', 'pib', 'eib']
11
- DECIMAL_PREFIXES = ['b', 'k', 'm', 'g', 't', 'p', 'e', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb']
10
+ BINARY_PREFIXES = [
11
+ "ki",
12
+ "mi",
13
+ "gi",
14
+ "ti",
15
+ "pi",
16
+ "ei",
17
+ "kib",
18
+ "mib",
19
+ "gib",
20
+ "tib",
21
+ "pib",
22
+ "eib",
23
+ ]
24
+ DECIMAL_PREFIXES = [
25
+ "b",
26
+ "k",
27
+ "m",
28
+ "g",
29
+ "t",
30
+ "p",
31
+ "e",
32
+ "kb",
33
+ "mb",
34
+ "gb",
35
+ "tb",
36
+ "pb",
37
+ "eb",
38
+ ]
12
39
  VALID_PREFIXES = BINARY_PREFIXES + DECIMAL_PREFIXES
13
40
 
14
41
 
15
- def bytes_in_unit(unit: str = 'B') -> int:
42
+ def bytes_in_unit(unit: str = "B") -> int:
16
43
  num_bytes = 1
17
- if unit.lower() in ['ki', 'kib']:
44
+ if unit.lower() in ["ki", "kib"]:
18
45
  num_bytes = 1 << 10
19
- if unit.lower() in ['mi', 'mib']:
46
+ if unit.lower() in ["mi", "mib"]:
20
47
  num_bytes = 1 << 20
21
- if unit.lower() in ['gi', 'gib']:
48
+ if unit.lower() in ["gi", "gib"]:
22
49
  num_bytes = 1 << 30
23
- if unit.lower() in ['ti', 'tib']:
50
+ if unit.lower() in ["ti", "tib"]:
24
51
  num_bytes = 1 << 40
25
- if unit.lower() in ['pi', 'pib']:
52
+ if unit.lower() in ["pi", "pib"]:
26
53
  num_bytes = 1 << 50
27
- if unit.lower() in ['ei', 'eib']:
54
+ if unit.lower() in ["ei", "eib"]:
28
55
  num_bytes = 1 << 60
29
56
 
30
- if unit.lower() in ['k', 'kb']:
57
+ if unit.lower() in ["k", "kb"]:
31
58
  num_bytes = 1000
32
- if unit.lower() in ['m', 'mb']:
33
- num_bytes = 1000 ** 2
34
- if unit.lower() in ['g', 'gb']:
35
- num_bytes = 1000 ** 3
36
- if unit.lower() in ['t', 'tb']:
37
- num_bytes = 1000 ** 4
38
- if unit.lower() in ['p', 'pb']:
39
- num_bytes = 1000 ** 5
40
- if unit.lower() in ['e', 'eb']:
41
- num_bytes = 1000 ** 6
59
+ if unit.lower() in ["m", "mb"]:
60
+ num_bytes = 1000**2
61
+ if unit.lower() in ["g", "gb"]:
62
+ num_bytes = 1000**3
63
+ if unit.lower() in ["t", "tb"]:
64
+ num_bytes = 1000**4
65
+ if unit.lower() in ["p", "pb"]:
66
+ num_bytes = 1000**5
67
+ if unit.lower() in ["e", "eb"]:
68
+ num_bytes = 1000**6
42
69
  return num_bytes
43
70
 
44
71
 
45
- def convert_units(num: float,
46
- src_unit: str,
47
- dst_unit: str = 'B') -> float:
72
+ def convert_units(num: float, src_unit: str, dst_unit: str = "B") -> float:
48
73
  """Returns a float representing the converted input in dst_units."""
49
74
  if not src_unit.lower() in VALID_PREFIXES:
50
- raise RuntimeError(f"{src_unit} not a valid unit, valid units are {VALID_PREFIXES}.")
75
+ raise RuntimeError(
76
+ f"{src_unit} not a valid unit, valid units are {VALID_PREFIXES}."
77
+ )
51
78
  if not dst_unit.lower() in VALID_PREFIXES:
52
- raise RuntimeError(f"{dst_unit} not a valid unit, valid units are {VALID_PREFIXES}.")
79
+ raise RuntimeError(
80
+ f"{dst_unit} not a valid unit, valid units are {VALID_PREFIXES}."
81
+ )
53
82
  return (num * bytes_in_unit(src_unit)) / bytes_in_unit(dst_unit)
54
83
 
55
84
 
56
- def parse_memory_string(string: str) -> Tuple[float, str]:
85
+ def parse_memory_string(string: str) -> tuple[float, str]:
57
86
  """
58
87
  Given a string representation of some memory (i.e. '1024 Mib'), return the
59
88
  number and unit.
60
89
  """
61
90
  for i, character in enumerate(string):
62
91
  # find the first character of the unit
63
- if character not in '0123456789.-_ ':
92
+ if character not in "0123456789.-_ ":
64
93
  units = string[i:].strip()
65
94
  if not units.lower() in VALID_PREFIXES:
66
- raise RuntimeError(f"{units} not a valid unit, valid units are {VALID_PREFIXES}.")
95
+ raise RuntimeError(
96
+ f"{units} not a valid unit, valid units are {VALID_PREFIXES}."
97
+ )
67
98
  return float(string[:i]), units
68
- return float(string), 'b'
99
+ return float(string), "b"
69
100
 
70
101
 
71
102
  def human2bytes(string: str) -> int:
@@ -75,7 +106,7 @@ def human2bytes(string: str) -> int:
75
106
  """
76
107
  value, unit = parse_memory_string(string)
77
108
 
78
- return int(convert_units(value, src_unit=unit, dst_unit='b'))
109
+ return int(convert_units(value, src_unit=unit, dst_unit="b"))
79
110
 
80
111
 
81
112
  def bytes2human(n: SupportsInt) -> str:
@@ -84,47 +115,73 @@ def bytes2human(n: SupportsInt) -> str:
84
115
  if n < 0:
85
116
  raise ValueError("n < 0")
86
117
  elif n < 1:
87
- return '0 b'
118
+ return "0 b"
88
119
 
89
120
  power_level = math.floor(math.log(n, 1024))
90
- units = ('b', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei')
121
+ units = ("b", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei")
91
122
 
92
123
  unit = units[power_level if power_level < len(units) else -1]
93
124
  value = convert_units(n, "b", unit)
94
- return f'{value:.1f} {unit}'
95
-
125
+ return f"{value:.1f} {unit}"
126
+
127
+
96
128
  def b_to_mib(n: Union[int, float]) -> float:
97
129
  """
98
130
  Convert a number from bytes to mibibytes.
99
131
  """
100
- return convert_units(n, 'b', 'mib')
132
+ return convert_units(n, "b", "mib")
101
133
 
102
134
 
103
135
  def mib_to_b(n: Union[int, float]) -> float:
104
136
  """
105
137
  Convert a number from mibibytes to bytes.
106
138
  """
107
- return convert_units(n, 'mib', 'b')
139
+ return convert_units(n, "mib", "b")
140
+
108
141
 
109
- #General Conversions
142
+ # General Conversions
110
143
 
111
- def hms_duration_to_seconds(hms: str) -> float:
144
+
145
+ def hms_duration_to_seconds(hms: str) -> float:
112
146
  """
113
- Parses a given time string in hours:minutes:seconds,
147
+ Parses a given time string in hours:minutes:seconds,
114
148
  returns an equivalent total seconds value
115
149
  """
116
- vals_to_convert = hms.split(':')
150
+ vals_to_convert = hms.split(":")
117
151
  seconds = 0.0
118
-
152
+
119
153
  for val in vals_to_convert:
120
- if(float(val) < 0):
154
+ if float(val) < 0:
121
155
  raise ValueError("Invalid Time, negative value")
122
156
 
123
- if(len(vals_to_convert) != 3):
157
+ if len(vals_to_convert) != 3:
124
158
  raise ValueError("Invalid amount of fields, function takes input in 'hh:mm:ss'")
125
159
 
126
- seconds += float(vals_to_convert[0]) * 60 * 60
127
- seconds += float(vals_to_convert[1]) * 60
128
- seconds += float(vals_to_convert[2])
160
+ seconds += float(vals_to_convert[0]) * 60 * 60
161
+ seconds += float(vals_to_convert[1]) * 60
162
+ seconds += float(vals_to_convert[2])
129
163
 
130
164
  return seconds
165
+
166
+
167
+ def strtobool(val: str) -> bool:
168
+ """
169
+ Make a human-readable string into a bool.
170
+
171
+ Convert a string along the lines of "y", "1", "ON", "TrUe", or
172
+ "Yes" to True, and the corresponding false-ish values to False.
173
+ """
174
+ # We only track prefixes, so "y" covers "y", "yes",
175
+ # and "yeah no" and makes them all True.
176
+ TABLE = {True: ["1", "on", "y", "t"], False: ["0", "off", "n", "f"]}
177
+ lowered = val.lower()
178
+ for result, prefixes in TABLE.items():
179
+ for prefix in prefixes:
180
+ if lowered.startswith(prefix):
181
+ return result
182
+ raise ValueError(f'Cannot convert "{val}" to a bool')
183
+
184
+
185
+ def opt_strtobool(b: Optional[str]) -> Optional[bool]:
186
+ """Convert an optional string representation of bool to None or bool"""
187
+ return b if b is None else strtobool(b)