ask-shell 0.0.1__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.
ask_shell/__init__.py ADDED
@@ -0,0 +1,46 @@
1
+ # Generated by pkg-ext
2
+ # flake8: noqa
3
+ from ask_shell._run import kill, run, run_and_wait, run_error, wait_on_ok_errors
4
+ from ask_shell._run_env import interactive_shell
5
+ from ask_shell.models import ShellConfig, ShellError, ShellRun
6
+ from ask_shell.interactive import (
7
+ ChoiceTyped,
8
+ SelectOptions,
9
+ confirm,
10
+ select_dict,
11
+ select_list,
12
+ select_list_choice,
13
+ select_list_multiple,
14
+ select_list_multiple_choices,
15
+ text,
16
+ )
17
+ from ask_shell.rich_live import print_to_live
18
+ from ask_shell.rich_progress import new_task
19
+ from ask_shell.settings import AskShellSettings
20
+ from ask_shell.typer_command import configure_logging
21
+
22
+ VERSION = "0.0.1"
23
+ __all__ = [
24
+ "AskShellSettings",
25
+ "ChoiceTyped",
26
+ "SelectOptions",
27
+ "ShellConfig",
28
+ "ShellError",
29
+ "ShellRun",
30
+ "configure_logging",
31
+ "confirm",
32
+ "interactive_shell",
33
+ "kill",
34
+ "new_task",
35
+ "print_to_live",
36
+ "run",
37
+ "run_and_wait",
38
+ "run_error",
39
+ "select_dict",
40
+ "select_list",
41
+ "select_list_choice",
42
+ "select_list_multiple",
43
+ "select_list_multiple_choices",
44
+ "text",
45
+ "wait_on_ok_errors",
46
+ ]
ask_shell/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ import sys
2
+
3
+ from ask_shell import run_and_wait
4
+
5
+ _, *script_args = sys.argv
6
+ run_and_wait(" ".join(script_args))
@@ -0,0 +1 @@
1
+ ENV_PREFIX = "ASK_SHELL_"
ask_shell/_run.py ADDED
@@ -0,0 +1,514 @@
1
+ """Design Principles:
2
+ 1. ShellConfig used to provide user configuration of the run.
3
+ 2. A ShellRun is created for each run, which manages the execution and output.
4
+ 3. The events are communicated through a queue, guaranteeing the order of messages.
5
+ 4. You can either run or run_and_wait, where the latter waits for completion while the 1st will exit after command has started.
6
+ 5. Message Callbacks can be used to direct output to the console, by default it is directed to a `.log` file which supports dumping ANSI content at the end of the run to a `.html` file.
7
+ 6. Retries are supported, with a configurable number of attempts and a retry condition.
8
+ 7. Any errors are converted into a `ShellError` which contains the run and base exception. Use `allow_non_zero_exit` to allow runs to complete with a non-zero exit code without raising this error.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import atexit
14
+ import logging
15
+ import os
16
+ import signal
17
+ import subprocess
18
+ import sys
19
+ from concurrent.futures import ThreadPoolExecutor, wait
20
+ from contextlib import suppress
21
+ from dataclasses import dataclass
22
+ from os import getenv, setsid
23
+ from pathlib import Path
24
+ from typing import IO, Any, Callable
25
+
26
+ from model_lib.pydantic_utils import copy_and_validate
27
+ from rich.ansi import AnsiDecoder
28
+ from rich.console import Console
29
+
30
+ from ask_shell.models import (
31
+ RunIncompleteError,
32
+ ShellConfig,
33
+ ShellRun,
34
+ ShellRunAfter,
35
+ ShellRunBefore,
36
+ ShellRunEventT,
37
+ ShellRunPOpenStarted,
38
+ ShellRunQueueT,
39
+ ShellRunRetryAttempt,
40
+ ShellRunStdOutput,
41
+ ShellRunStdReadError,
42
+ ShellRunStdStarted,
43
+ )
44
+ from ask_shell.settings import AskShellSettings
45
+
46
+ logger = logging.getLogger(__name__)
47
+ THREADS_PER_RUN = 4 # Each run will take 4 threads: 1 for stdout, 1 for stderr, 1 for consuming queue messages and 1 for popen wait.
48
+ THREAD_POOL_FULL_WAIT_TIME_SECONDS = float(
49
+ getenv(
50
+ AskShellSettings.ENV_NAME_THREAD_POOL_FULL_WAIT_TIME_SECONDS,
51
+ AskShellSettings.THREAD_POOL_FULL_WAIT_TIME_SECONDS_DEFAULT,
52
+ )
53
+ )
54
+
55
+ _pool = ThreadPoolExecutor(
56
+ max_workers=int(
57
+ getenv(
58
+ AskShellSettings.ENV_NAME_RUN_THREAD_COUNT,
59
+ AskShellSettings.RUN_THREAD_COUNT_DEFAULT,
60
+ )
61
+ )
62
+ )
63
+ _runs: dict[
64
+ int, ShellRun
65
+ ] = {} # internal to store running ShellRuns to support stopping them on exit
66
+
67
+
68
+ def current_run_count() -> int:
69
+ return len(_runs)
70
+
71
+
72
+ def stop_runs_and_pool(reason: str = "atexit", immediate: bool = False):
73
+ if _runs:
74
+ logger.warning("STOPPING stop_runs_and_pool")
75
+ kill_all_runs(reason=reason, immediate=immediate)
76
+
77
+ _pool.shutdown(wait=True)
78
+
79
+
80
+ atexit.register(stop_runs_and_pool)
81
+
82
+
83
+ def run(
84
+ config: ShellConfig | str,
85
+ *,
86
+ allow_non_zero_exit: bool | None = None,
87
+ ansi_content: bool | None = None,
88
+ attempts: int | None = None,
89
+ cwd: str | Path | None = None,
90
+ env: dict[str, str] | None = None,
91
+ extra_popen_kwargs: dict | None = None,
92
+ is_binary_call: bool | None = None,
93
+ message_callbacks: list[Callable[[ShellRunEventT], bool]] | None = None,
94
+ print_prefix: str | None = None,
95
+ run_log_stem_prefix: str | None = None,
96
+ run_output_dir: Path | None | None = None,
97
+ settings: AskShellSettings | None = None,
98
+ should_retry: Callable[[ShellRun], bool] | None = None,
99
+ skip_binary_check: bool | None = None,
100
+ skip_html_log_files: bool | None = None,
101
+ include_log_time: bool | None = None,
102
+ skip_os_env: bool | None = None,
103
+ start_timeout: float | None = None,
104
+ terminal_width: int | None = None,
105
+ skip_interactive_check: bool | None = None,
106
+ ) -> ShellRun:
107
+ config = _as_config(
108
+ config,
109
+ allow_non_zero_exit=allow_non_zero_exit,
110
+ ansi_content=ansi_content,
111
+ attempts=attempts,
112
+ cwd=cwd,
113
+ env=env,
114
+ extra_popen_kwargs=extra_popen_kwargs,
115
+ is_binary_call=is_binary_call,
116
+ message_callbacks=message_callbacks,
117
+ print_prefix=print_prefix,
118
+ run_log_stem_prefix=run_log_stem_prefix,
119
+ run_output_dir=run_output_dir,
120
+ settings=settings,
121
+ should_retry=should_retry,
122
+ skip_binary_check=skip_binary_check,
123
+ skip_html_log_files=skip_html_log_files,
124
+ include_log_time=include_log_time,
125
+ skip_os_env=skip_os_env,
126
+ terminal_width=terminal_width,
127
+ skip_interactive_check=skip_interactive_check,
128
+ )
129
+ assert not config.user_input, (
130
+ "run() does not support user_input (only 1 should be active at a time), use run_and_wait() instead"
131
+ )
132
+ run = ShellRun(config)
133
+ _pool.submit(_execute_run, run)
134
+ with handle_interrupt_wait(f"interrupt when starting {run}"):
135
+ return run.wait_on_started(start_timeout)
136
+
137
+
138
+ def run_and_wait(
139
+ script: ShellConfig | str,
140
+ timeout: float | None = None,
141
+ *,
142
+ allow_non_zero_exit: bool | None = None,
143
+ ansi_content: bool | None = None,
144
+ attempts: int | None = None,
145
+ cwd: str | Path | None = None,
146
+ env: dict[str, str] | None = None,
147
+ extra_popen_kwargs: dict | None = None,
148
+ is_binary_call: bool | None = None,
149
+ message_callbacks: list[Callable[[ShellRunEventT], bool]] | None = None,
150
+ print_prefix: str | None = None,
151
+ run_log_stem_prefix: str | None = None,
152
+ run_output_dir: Path | None | None = None,
153
+ settings: AskShellSettings | None = None,
154
+ should_retry: Callable[[ShellRun], bool] | None = None,
155
+ skip_binary_check: bool | None = None,
156
+ skip_html_log_files: bool | None = None,
157
+ include_log_time: bool | None = None,
158
+ skip_os_env: bool | None = None,
159
+ user_input: bool | None = None,
160
+ terminal_width: int | None = None,
161
+ skip_interactive_check: bool | None = None,
162
+ ) -> ShellRun:
163
+ config = _as_config(
164
+ script,
165
+ allow_non_zero_exit=allow_non_zero_exit,
166
+ ansi_content=ansi_content,
167
+ attempts=attempts,
168
+ cwd=cwd,
169
+ env=env,
170
+ extra_popen_kwargs=extra_popen_kwargs,
171
+ is_binary_call=is_binary_call,
172
+ message_callbacks=message_callbacks,
173
+ print_prefix=print_prefix,
174
+ run_log_stem_prefix=run_log_stem_prefix,
175
+ run_output_dir=run_output_dir,
176
+ settings=settings,
177
+ should_retry=should_retry,
178
+ skip_binary_check=skip_binary_check,
179
+ skip_html_log_files=skip_html_log_files,
180
+ include_log_time=include_log_time,
181
+ skip_os_env=skip_os_env,
182
+ user_input=user_input,
183
+ terminal_width=terminal_width,
184
+ skip_interactive_check=skip_interactive_check,
185
+ )
186
+ run = ShellRun(config)
187
+ _pool.submit(_execute_run, run)
188
+ with handle_interrupt_wait(f"interrupt when waiting for {run}"):
189
+ run.wait_until_complete(timeout)
190
+ return run
191
+
192
+
193
+ @dataclass
194
+ class handle_interrupt_wait:
195
+ interrupt_message: str
196
+ immediate_kill: bool = False
197
+
198
+ def __enter__(self):
199
+ """Context manager to ensure that all runs are stopped on exit."""
200
+ return self
201
+
202
+ def __exit__(self, exc_type, exc_value, traceback):
203
+ """Handle exit by stopping all runs."""
204
+ if exc_type is KeyboardInterrupt or exc_type is InterruptedError:
205
+ interrupt_error = f"{self.interrupt_message} {exc_value!r}"
206
+ stop_runs_and_pool(interrupt_error, immediate=self.immediate_kill)
207
+
208
+
209
+ def _as_config(config: ShellConfig | str, **kwargs) -> ShellConfig:
210
+ kwargs_not_none = {k: v for k, v in kwargs.items() if v is not None}
211
+ if isinstance(config, str):
212
+ return ShellConfig(shell_input=config, **kwargs_not_none)
213
+ assert isinstance(config, ShellConfig), f"not a ShellConfig or str: {config!r}"
214
+ return copy_and_validate(config, **kwargs_not_none)
215
+
216
+
217
+ def run_error(run: ShellRun, timeout: float | None = 1) -> BaseException | None:
218
+ try:
219
+ run._complete_flag.result(timeout=timeout)
220
+ except BaseException as e:
221
+ return e
222
+
223
+
224
+ def wait_on_ok_errors(
225
+ *runs: ShellRun, # type: ignore
226
+ timeout: float | None = None,
227
+ skip_kill_timeouts: bool = False, # type: ignore
228
+ ) -> tuple[list[ShellRun], list[tuple[BaseException, ShellRun]]]:
229
+ future_runs = {run._complete_flag: run for run in runs}
230
+ with handle_interrupt_wait("interrupt when waiting for runs"):
231
+ done, not_done = wait(
232
+ [run._complete_flag for run in runs], timeout, return_when="ALL_COMPLETED"
233
+ )
234
+ errors: list[tuple[BaseException, ShellRun]] = []
235
+ oks: list[ShellRun] = []
236
+
237
+ if not_done:
238
+ if skip_kill_timeouts:
239
+ errors.extend(
240
+ (RunIncompleteError(future_runs[run]), future_runs[run])
241
+ for run in not_done
242
+ )
243
+ runs: list[ShellRun] = [future_runs[f] for f in done] # type: ignore
244
+ else:
245
+ for run in runs:
246
+ if run.is_running:
247
+ kill(run, immediate=True, reason="timeout")
248
+
249
+ for run in runs:
250
+ if error := run_error(run):
251
+ errors.append((error, run))
252
+ else:
253
+ oks.append(run)
254
+ return oks, errors
255
+
256
+
257
+ def kill_all_runs(
258
+ immediate: bool = False, reason: str = "", abort_timeout: float = 3.0
259
+ ):
260
+ for run in _runs.values():
261
+ kill(
262
+ run,
263
+ immediate=immediate,
264
+ reason=reason,
265
+ abort_timeout=abort_timeout,
266
+ )
267
+
268
+
269
+ def kill(
270
+ run: ShellRun,
271
+ immediate: bool = False,
272
+ reason: str = "",
273
+ abort_timeout: float = 3.0,
274
+ ):
275
+ """https://stackoverflow.com/questions/4789837/how-to-terminate-a-python-subprocess-launched-with-shell-true"""
276
+ proc = run.p_open
277
+ if not proc or proc.returncode is not None:
278
+ logger.info(f"killing run already completed: {run} {reason}")
279
+ return
280
+
281
+ logger.warning(f"killing starting: {run} {reason}")
282
+ try:
283
+ pgid = os.getpgid(proc.pid)
284
+ # proc.terminate() and proc.send_signal() doesn't work across platforms.
285
+ if immediate:
286
+ os.killpg(pgid, signal.SIGTERM)
287
+ else:
288
+ os.killpg(pgid, signal.SIGINT)
289
+ proc.wait(timeout=abort_timeout)
290
+ logger.info(f"killing completed: {run} {reason}")
291
+ except subprocess.TimeoutExpired:
292
+ logger.warning(
293
+ f"killing timeout after {abort_timeout}s! forcing a kill: {run} {reason}"
294
+ )
295
+ os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
296
+ with suppress(subprocess.TimeoutExpired):
297
+ proc.wait(timeout=1)
298
+ except (OSError, ValueError) as e:
299
+ logger.warning(f"unable to get output when shutting down: {run} {e!r}")
300
+ finally:
301
+ run.wait_until_complete(timeout=1, no_raise=True)
302
+
303
+
304
+ def _execute_run(shell_run: ShellRun) -> ShellRun:
305
+ """
306
+ Principles:
307
+ 1. This function is executed in a separate thread. Interrupts will not affect it.
308
+ 2. It should handle every error condition and not raise exceptions.
309
+ """
310
+ config = shell_run.config
311
+ queue = shell_run._queue
312
+
313
+ def queue_consumer():
314
+ for message in queue:
315
+ try:
316
+ shell_run._on_event(message)
317
+ except BaseException as e:
318
+ logger.warning(f"Error processing message for {shell_run}: {e!r}")
319
+ logger.exception(e)
320
+
321
+ try:
322
+ shell_run._on_event(
323
+ ShellRunBefore(run=shell_run)
324
+ ) # can block, for example if the thread pool doesn't have enough threads free
325
+ except BaseException as e:
326
+ logger.warning(f"Error before starting run {shell_run}: {e!r}")
327
+ shell_run._complete(error=e)
328
+ return shell_run
329
+
330
+ consumer_future = _pool.submit(queue_consumer)
331
+ output_dir = config.run_output_dir_resolved()
332
+ output_dir.mkdir(parents=True, exist_ok=True)
333
+ error: BaseException | None = None
334
+ for attempt in range(1, config.attempts + 1):
335
+ if attempt > 1:
336
+ queue.put_nowait(ShellRunRetryAttempt(attempt=attempt))
337
+ logger.warning(
338
+ f"Retrying run {shell_run} attempt {attempt} of {config.attempts}"
339
+ )
340
+ attempt_log_prefix = config.run_log_stem(attempt)
341
+
342
+ result = _attempt_run(shell_run, output_dir, attempt_log_prefix)
343
+ match result:
344
+ case ShellRun() if result.clean_complete or not config.should_retry(result):
345
+ break
346
+ case BaseException():
347
+ error = result
348
+ break
349
+ queue.put_nowait(ShellRunAfter(run=shell_run, error=error))
350
+ shell_run._complete(error=error, queue_consumer=consumer_future)
351
+ return shell_run
352
+
353
+
354
+ def _attempt_run(
355
+ shell_run: ShellRun,
356
+ output_dir: Path,
357
+ file_name: str,
358
+ ) -> ShellRun | BaseException:
359
+ """Run the shell command and handle the error, never raises an exception."""
360
+ config = shell_run.config
361
+ key = id(shell_run)
362
+ _runs[key] = shell_run
363
+ try:
364
+ _run(
365
+ config,
366
+ shell_run._queue,
367
+ output_dir,
368
+ file_name,
369
+ )
370
+ return shell_run
371
+ except BaseException as e:
372
+ logger.warning(f"Error running {shell_run}: {e!r}")
373
+ logger.exception(e)
374
+ return e
375
+ finally:
376
+ _runs.pop(key, None)
377
+
378
+
379
+ def _run(
380
+ config: ShellConfig,
381
+ queue: ShellRunQueueT,
382
+ output_dir: Path,
383
+ file_name: str,
384
+ ) -> None:
385
+ kwargs = (
386
+ dict(
387
+ stdout=subprocess.PIPE,
388
+ stderr=subprocess.PIPE,
389
+ preexec_fn=setsid,
390
+ universal_newlines=True,
391
+ )
392
+ | config.popen_kwargs
393
+ )
394
+ with subprocess.Popen(config.shell_input, shell=True, **kwargs) as proc: # type: ignore
395
+ queue.put_nowait(ShellRunPOpenStarted(proc))
396
+
397
+ def stdout_started(is_stdout: bool, console: Console, log_path: Path):
398
+ queue.put_nowait(
399
+ ShellRunStdStarted(
400
+ is_stdout=is_stdout, console=console, log_path=log_path
401
+ )
402
+ )
403
+
404
+ def add_stdout_line(line: str):
405
+ queue.put_nowait(ShellRunStdOutput(is_stdout=True, content=line))
406
+
407
+ def read_stdout():
408
+ output_path = output_dir / f"{file_name}.stdout.log"
409
+ try:
410
+ _read_until_complete(
411
+ stream=proc.stdout, # type: ignore
412
+ output_path=output_path,
413
+ on_console_ready=lambda console: stdout_started(
414
+ is_stdout=True, console=console, log_path=output_path
415
+ ),
416
+ on_line=add_stdout_line,
417
+ config=config,
418
+ )
419
+ except BaseException as e:
420
+ queue.put_nowait(ShellRunStdReadError(is_stdout=True, error=e))
421
+
422
+ def add_stderr_line(line: str):
423
+ queue.put_nowait(ShellRunStdOutput(is_stdout=False, content=line))
424
+
425
+ def read_stderr():
426
+ output_path = output_dir / f"{file_name}.stderr.log"
427
+ try:
428
+ _read_until_complete(
429
+ stream=proc.stderr, # type: ignore
430
+ output_path=output_path,
431
+ on_console_ready=lambda console: stdout_started(
432
+ is_stdout=False, console=console, log_path=output_path
433
+ ),
434
+ on_line=add_stderr_line,
435
+ config=config,
436
+ )
437
+ except BaseException as e:
438
+ queue.put_nowait(ShellRunStdReadError(is_stdout=False, error=e))
439
+
440
+ fut_stdout = _pool.submit(read_stdout)
441
+ fut_stderr = _pool.submit(read_stderr)
442
+ # no proc.wait/communicate, not sure if it will work
443
+ wait([fut_stdout, fut_stderr])
444
+
445
+
446
+ def _read_until_complete(
447
+ stream: IO[str],
448
+ output_path: Path,
449
+ on_console_ready: Callable[[Console], None],
450
+ on_line: Callable[[str], None],
451
+ config: ShellConfig,
452
+ ):
453
+ with open(output_path, "w") as f:
454
+ console = Console(
455
+ file=f,
456
+ record=True,
457
+ log_path=False,
458
+ soft_wrap=True,
459
+ log_time=config.include_log_time,
460
+ width=config.terminal_width,
461
+ )
462
+ on_console_ready(console)
463
+ old_write = f.write
464
+
465
+ def write_hook(text: str):
466
+ text_no_extras = [line.strip() for line in text.splitlines()]
467
+ return old_write("\n".join(text_no_extras))
468
+
469
+ decoder = AnsiDecoder()
470
+
471
+ def write_hook_ansi(text: str):
472
+ plain_text = "\n".join(
473
+ decoder.decode_line(line).plain.strip() for line in text.splitlines()
474
+ )
475
+ return old_write(plain_text)
476
+
477
+ f.write = write_hook_ansi if config.ansi_content else write_hook
478
+ if config.user_input:
479
+ out_stream = sys.stdout if ".stdout." in output_path.name else sys.stderr
480
+
481
+ def _on_line(line: str):
482
+ console.log(line, end="")
483
+ on_line(line)
484
+
485
+ def _on_char(char: str):
486
+ out_stream.write(char)
487
+ out_stream.flush()
488
+
489
+ _stream_one_character_at_a_time(
490
+ stream,
491
+ on_line=_on_line,
492
+ on_char=_on_char,
493
+ )
494
+ else:
495
+ for line in iter(stream.readline, ""):
496
+ console.log(line, end="")
497
+ on_line(line)
498
+
499
+
500
+ def _stream_one_character_at_a_time(
501
+ stream: IO[str], on_line: Callable[[str], None], on_char: Callable[[str], Any]
502
+ ):
503
+ buffer = ""
504
+ while True:
505
+ chunk = stream.read(1) # Read one character at a time
506
+ if not chunk:
507
+ break
508
+ buffer += chunk
509
+ on_char(chunk)
510
+ if chunk == "\n":
511
+ on_line(buffer)
512
+ buffer = ""
513
+ if buffer: # Handle any remaining data (incomplete line)
514
+ on_line(buffer)
ask_shell/_run_env.py ADDED
@@ -0,0 +1,42 @@
1
+ import logging
2
+ import sys
3
+ from functools import lru_cache
4
+ from os import getenv
5
+
6
+ from zero_3rdparty.run_env import in_test_env, running_in_container_environment
7
+
8
+ from ask_shell._constants import ENV_PREFIX
9
+
10
+ ENV_NAME_FORCE_INTERACTIVE_SHELL = f"{ENV_PREFIX}FORCE_INTERACTIVE_SHELL"
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ @lru_cache
15
+ def interactive_shell() -> bool:
16
+ if getenv(ENV_NAME_FORCE_INTERACTIVE_SHELL, "false").lower() in (
17
+ "true",
18
+ "1",
19
+ "yes",
20
+ ):
21
+ logger.debug(
22
+ f"Interactive shell forced by environment variable {ENV_NAME_FORCE_INTERACTIVE_SHELL}"
23
+ )
24
+ return True
25
+ if non_interactive_reason := _not_interactive_reason():
26
+ logger.debug(f"Interactive shell not available: {non_interactive_reason}")
27
+ return False
28
+ return True
29
+
30
+
31
+ def _not_interactive_reason() -> str:
32
+ if in_test_env():
33
+ return "Running in test environment"
34
+ if getenv("TERM", "") in ("dumb", "unknown"):
35
+ return "TERM environment variable is set to 'dumb' or 'unknown'"
36
+ if not sys.stdout.isatty():
37
+ return "Standard output is not a TTY"
38
+ if getenv("CI", "false").lower() in ("true", "1", "yes"):
39
+ return "Running in CI environment"
40
+ if running_in_container_environment():
41
+ return "Running in container environment"
42
+ return ""
@@ -0,0 +1,20 @@
1
+ import logging
2
+ import time
3
+
4
+ import ask_shell._run
5
+ from ask_shell.models import ShellRunBefore, ShellRunEventT
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ def wait_on_available_threads(message: ShellRunEventT) -> bool:
11
+ if isinstance(message, ShellRunBefore):
12
+ max_workers = ask_shell._run._pool._max_workers
13
+ max_count = max_workers // ask_shell._run.THREADS_PER_RUN - 1
14
+ while ask_shell._run.current_run_count() > max_count:
15
+ logger.warning(
16
+ f"Run count={ask_shell._run.current_run_count()} exceeds max {max_count}. "
17
+ f"Waiting for threads to finish before starting {message.run}..."
18
+ )
19
+ time.sleep(ask_shell._run.THREAD_POOL_FULL_WAIT_TIME_SECONDS)
20
+ return True # always remove the callback from the run, BeforeRunMessage should be the first message