ask-shell 0.5.2__tar.gz → 0.6.0__tar.gz
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-0.5.2 → ask_shell-0.6.0}/PKG-INFO +1 -1
- {ask_shell-0.5.2 → ask_shell-0.6.0}/ask_shell/__init__.py +1 -1
- ask_shell-0.6.0/ask_shell/_internal/run_pool.py +121 -0
- {ask_shell-0.5.2 → ask_shell-0.6.0}/ask_shell/_internal/typer_command.py +45 -12
- {ask_shell-0.5.2 → ask_shell-0.6.0}/ask_shell/settings.py +1 -1
- {ask_shell-0.5.2 → ask_shell-0.6.0}/pyproject.toml +1 -1
- ask_shell-0.5.2/ask_shell/_internal/run_pool.py +0 -100
- {ask_shell-0.5.2 → ask_shell-0.6.0}/.gitignore +0 -0
- {ask_shell-0.5.2 → ask_shell-0.6.0}/LICENSE +0 -0
- {ask_shell-0.5.2 → ask_shell-0.6.0}/README.md +0 -0
- {ask_shell-0.5.2 → ask_shell-0.6.0}/ask_shell/__main__.py +0 -0
- {ask_shell-0.5.2 → ask_shell-0.6.0}/ask_shell/_internal/__init__.py +0 -0
- {ask_shell-0.5.2 → ask_shell-0.6.0}/ask_shell/_internal/_run.py +0 -0
- {ask_shell-0.5.2 → ask_shell-0.6.0}/ask_shell/_internal/_run_env.py +0 -0
- {ask_shell-0.5.2 → ask_shell-0.6.0}/ask_shell/_internal/events.py +0 -0
- {ask_shell-0.5.2 → ask_shell-0.6.0}/ask_shell/_internal/global_callbacks.py +0 -0
- {ask_shell-0.5.2 → ask_shell-0.6.0}/ask_shell/_internal/interactive.py +0 -0
- {ask_shell-0.5.2 → ask_shell-0.6.0}/ask_shell/_internal/models.py +0 -0
- {ask_shell-0.5.2 → ask_shell-0.6.0}/ask_shell/_internal/rich_live.py +0 -0
- {ask_shell-0.5.2 → ask_shell-0.6.0}/ask_shell/_internal/rich_live_callback.py +0 -0
- {ask_shell-0.5.2 → ask_shell-0.6.0}/ask_shell/_internal/rich_progress.py +0 -0
- {ask_shell-0.5.2 → ask_shell-0.6.0}/ask_shell/_internal/rich_run_state.py +0 -0
- {ask_shell-0.5.2 → ask_shell-0.6.0}/ask_shell/ask.py +0 -0
- {ask_shell-0.5.2 → ask_shell-0.6.0}/ask_shell/console.py +0 -0
- {ask_shell-0.5.2 → ask_shell-0.6.0}/ask_shell/py.typed +0 -0
- {ask_shell-0.5.2 → ask_shell-0.6.0}/ask_shell/shell.py +0 -0
- {ask_shell-0.5.2 → ask_shell-0.6.0}/ask_shell/shell_events.py +0 -0
- {ask_shell-0.5.2 → ask_shell-0.6.0}/ask_shell/test_docs.py +0 -0
- {ask_shell-0.5.2 → ask_shell-0.6.0}/scripts/fix_source_links.py +0 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
from concurrent.futures import Future, ThreadPoolExecutor
|
|
4
|
+
from concurrent.futures import wait as futures_wait
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from math import ceil
|
|
7
|
+
from threading import RLock
|
|
8
|
+
from typing import Any, Callable, Protocol, TypeVar
|
|
9
|
+
|
|
10
|
+
from ask_shell._internal._run import (
|
|
11
|
+
THREADS_PER_RUN,
|
|
12
|
+
get_pool,
|
|
13
|
+
handle_interrupt_wait,
|
|
14
|
+
max_run_count_for_workers,
|
|
15
|
+
wait_if_many_runs,
|
|
16
|
+
)
|
|
17
|
+
from ask_shell._internal.rich_progress import new_task
|
|
18
|
+
from ask_shell.settings import AskShellSettings
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
T_co = TypeVar("T_co", covariant=True)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SubmitFunc(Protocol[T_co]):
|
|
25
|
+
def __call__(self, *args: Any, **kwargs: Any) -> T_co: ...
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class run_pool:
|
|
30
|
+
task_name: str
|
|
31
|
+
total: int = 0
|
|
32
|
+
max_concurrent_submits: int = field(default=4)
|
|
33
|
+
threads_used_per_submit: int = (
|
|
34
|
+
THREADS_PER_RUN + 1
|
|
35
|
+
) # If you are using `run` or `run_and_wait` this should be `THREADS_PER_RUN` + extra threads for your own tasks
|
|
36
|
+
pool_thread_count: int | None = None
|
|
37
|
+
sleep_time: float = 1
|
|
38
|
+
sleep_callback: Callable[[], Any] | None = None
|
|
39
|
+
exit_wait_timeout: float | None = (
|
|
40
|
+
None # If set, will wait for the pool to finish before exiting the context manager
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
pool: ThreadPoolExecutor = field(init=False)
|
|
44
|
+
_owns_pool: bool = field(init=False, default=False)
|
|
45
|
+
_pool_max_workers: int = field(init=False)
|
|
46
|
+
_max_run_count_with_this_pool: int = field(init=False)
|
|
47
|
+
_lock: RLock = field(init=False, default_factory=RLock)
|
|
48
|
+
_pending_count: int = field(init=False, default=0)
|
|
49
|
+
_task: new_task | None = field(init=False, default=None)
|
|
50
|
+
_futures: list[Future] = field(init=False, default_factory=list)
|
|
51
|
+
|
|
52
|
+
def __post_init__(self):
|
|
53
|
+
if self.pool_thread_count is not None:
|
|
54
|
+
self.pool = ThreadPoolExecutor(max_workers=self.pool_thread_count)
|
|
55
|
+
self._owns_pool = True
|
|
56
|
+
self._pool_max_workers = self.pool_thread_count
|
|
57
|
+
# dedicated pool: each concurrent submit reserves 1 run slot on the global pool
|
|
58
|
+
runs_needed = self.max_concurrent_submits
|
|
59
|
+
runs_available = max_run_count_for_workers()
|
|
60
|
+
logger.debug(
|
|
61
|
+
f"run_pool '{self.task_name}': dedicated pool with {self.pool_thread_count} workers, "
|
|
62
|
+
f"global pool reserves {runs_needed}/{runs_available} run slots"
|
|
63
|
+
)
|
|
64
|
+
else:
|
|
65
|
+
self.pool = get_pool()
|
|
66
|
+
self._pool_max_workers = self.pool._max_workers
|
|
67
|
+
# shared pool: submits + their shell runs share the same threads
|
|
68
|
+
workers_at_full_load = self.max_concurrent_submits * self.threads_used_per_submit
|
|
69
|
+
runs_needed = ceil(workers_at_full_load / THREADS_PER_RUN)
|
|
70
|
+
runs_available = max_run_count_for_workers(self._pool_max_workers)
|
|
71
|
+
|
|
72
|
+
assert runs_needed < runs_available, (
|
|
73
|
+
f"Run slots needed ({runs_needed}) exceed capacity ({runs_available}). "
|
|
74
|
+
f"Adjust {AskShellSettings.ENV_NAME_THREAD_COUNT} or decrease `max_concurrent_submits`."
|
|
75
|
+
)
|
|
76
|
+
self._max_run_count_with_this_pool = runs_available - runs_needed
|
|
77
|
+
|
|
78
|
+
def _on_submit_done(self, _future: Future):
|
|
79
|
+
with self._lock:
|
|
80
|
+
self._pending_count -= 1
|
|
81
|
+
if task := self._task:
|
|
82
|
+
task.update(advance=1)
|
|
83
|
+
|
|
84
|
+
def submit(self, fn: SubmitFunc[T_co], /, *args, **kwargs) -> Future[T_co]:
|
|
85
|
+
"""Submit a task to the pool. Blocks if max_concurrent_submits are already in flight."""
|
|
86
|
+
with self._lock:
|
|
87
|
+
self._pending_count += 1
|
|
88
|
+
with handle_interrupt_wait(interrupt_message=f"run_pool submit for {self.task_name}"):
|
|
89
|
+
while self._pending_count > self.max_concurrent_submits:
|
|
90
|
+
if self.sleep_callback:
|
|
91
|
+
self.sleep_callback()
|
|
92
|
+
time.sleep(self.sleep_time)
|
|
93
|
+
wait_if_many_runs(
|
|
94
|
+
max_run_count=self._max_run_count_with_this_pool,
|
|
95
|
+
sleep_time=self.sleep_time,
|
|
96
|
+
sleep_callback=self.sleep_callback,
|
|
97
|
+
)
|
|
98
|
+
future = self.pool.submit(fn, *args, **kwargs)
|
|
99
|
+
future.add_done_callback(self._on_submit_done)
|
|
100
|
+
with self._lock:
|
|
101
|
+
self._futures.append(future)
|
|
102
|
+
return future
|
|
103
|
+
|
|
104
|
+
def __enter__(self):
|
|
105
|
+
self._task = new_task(self.task_name, self.total)
|
|
106
|
+
self._task.__enter__()
|
|
107
|
+
return self
|
|
108
|
+
|
|
109
|
+
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
|
110
|
+
with self._lock:
|
|
111
|
+
futures = list(self._futures)
|
|
112
|
+
if futures:
|
|
113
|
+
with handle_interrupt_wait(interrupt_message=f"interrupt in `run_pool` exit method for {self.task_name}"):
|
|
114
|
+
futures_wait(futures, timeout=self.exit_wait_timeout)
|
|
115
|
+
with self._lock:
|
|
116
|
+
self._futures.clear()
|
|
117
|
+
|
|
118
|
+
if self._owns_pool:
|
|
119
|
+
self.pool.shutdown(wait=True)
|
|
120
|
+
if task := self._task:
|
|
121
|
+
task.__exit__(exc_type, exc_value, traceback)
|
|
@@ -88,6 +88,42 @@ def track_progress_decorator(
|
|
|
88
88
|
return decorator
|
|
89
89
|
|
|
90
90
|
|
|
91
|
+
def _wrap_typer_tree_commands(
|
|
92
|
+
app: typer.Typer,
|
|
93
|
+
*,
|
|
94
|
+
settings: AskShellSettings,
|
|
95
|
+
log_path_prefix: str,
|
|
96
|
+
skip_except_hook: bool,
|
|
97
|
+
use_app_name_command_for_logs: bool,
|
|
98
|
+
render_rich_error_on_sys_exit: bool,
|
|
99
|
+
) -> None:
|
|
100
|
+
for command in app.registered_commands:
|
|
101
|
+
command.callback = track_progress_decorator(
|
|
102
|
+
skip_except_hook=skip_except_hook,
|
|
103
|
+
settings=settings,
|
|
104
|
+
use_app_name_command_for_logs=use_app_name_command_for_logs,
|
|
105
|
+
app_name=log_path_prefix,
|
|
106
|
+
command_name=command.name or command.callback.__name__, # type: ignore
|
|
107
|
+
skip_rich_exception=not render_rich_error_on_sys_exit,
|
|
108
|
+
)(
|
|
109
|
+
command.callback # type: ignore
|
|
110
|
+
)
|
|
111
|
+
for group in app.registered_groups:
|
|
112
|
+
nested = group.typer_instance
|
|
113
|
+
if nested is None:
|
|
114
|
+
continue
|
|
115
|
+
segment = group.name or nested.info.name or "group"
|
|
116
|
+
child_prefix = f"{log_path_prefix}/{segment}"
|
|
117
|
+
_wrap_typer_tree_commands(
|
|
118
|
+
nested,
|
|
119
|
+
settings=settings,
|
|
120
|
+
log_path_prefix=child_prefix,
|
|
121
|
+
skip_except_hook=skip_except_hook,
|
|
122
|
+
use_app_name_command_for_logs=use_app_name_command_for_logs,
|
|
123
|
+
render_rich_error_on_sys_exit=render_rich_error_on_sys_exit,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
91
127
|
def remove_secrets(message: str, secrets: list[str]) -> str:
|
|
92
128
|
for secret in secrets:
|
|
93
129
|
message = message.replace(secret, "***")
|
|
@@ -141,18 +177,15 @@ def configure_logging(
|
|
|
141
177
|
render_rich_error_on_sys_exit: bool = False,
|
|
142
178
|
) -> logging.Handler:
|
|
143
179
|
settings = settings or AskShellSettings.from_env()
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
)(
|
|
154
|
-
command.callback # type: ignore
|
|
155
|
-
)
|
|
180
|
+
root_prefix = app.info.name or "typer_app"
|
|
181
|
+
_wrap_typer_tree_commands(
|
|
182
|
+
app,
|
|
183
|
+
settings=settings,
|
|
184
|
+
log_path_prefix=root_prefix,
|
|
185
|
+
skip_except_hook=skip_except_hook,
|
|
186
|
+
use_app_name_command_for_logs=use_app_name_command_for_logs,
|
|
187
|
+
render_rich_error_on_sys_exit=render_rich_error_on_sys_exit,
|
|
188
|
+
)
|
|
156
189
|
handler = RichHandler(rich_tracebacks=False, level=settings.log_level, console=get_live_console())
|
|
157
190
|
logging.basicConfig(
|
|
158
191
|
level=settings.log_level,
|
|
@@ -102,7 +102,7 @@ class AskShellSettings(StaticSettings):
|
|
|
102
102
|
)
|
|
103
103
|
ENV_NAME_THREAD_COUNT: ClassVar[str] = f"{ENV_PREFIX}THREAD_COUNT"
|
|
104
104
|
thread_count: int = Field(
|
|
105
|
-
default=
|
|
105
|
+
default=100,
|
|
106
106
|
alias=ENV_NAME_THREAD_COUNT,
|
|
107
107
|
description="Thread count for ask-shell pool",
|
|
108
108
|
)
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import time
|
|
2
|
-
from concurrent.futures import Future, ThreadPoolExecutor
|
|
3
|
-
from dataclasses import dataclass, field
|
|
4
|
-
from math import ceil
|
|
5
|
-
from threading import Event, RLock
|
|
6
|
-
from typing import Any, Callable, Protocol, TypeVar
|
|
7
|
-
|
|
8
|
-
from ask_shell._internal._run import (
|
|
9
|
-
THREADS_PER_RUN,
|
|
10
|
-
get_pool,
|
|
11
|
-
handle_interrupt_wait,
|
|
12
|
-
max_run_count_for_workers,
|
|
13
|
-
wait_if_many_runs,
|
|
14
|
-
)
|
|
15
|
-
from ask_shell._internal.rich_progress import new_task
|
|
16
|
-
from ask_shell.settings import AskShellSettings
|
|
17
|
-
|
|
18
|
-
T_co = TypeVar("T_co", covariant=True)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class SubmitFunc(Protocol[T_co]):
|
|
22
|
-
def __call__(self, *args: Any, **kwargs: Any) -> T_co: ...
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@dataclass
|
|
26
|
-
class run_pool:
|
|
27
|
-
task_name: str
|
|
28
|
-
total: int = 0
|
|
29
|
-
max_concurrent_submits: int = field(default=4)
|
|
30
|
-
threads_used_per_submit: int = (
|
|
31
|
-
THREADS_PER_RUN + 1
|
|
32
|
-
) # If you are using `run` or `run_and_wait` this should be `THREADS_PER_RUN` + extra threads for your own tasks
|
|
33
|
-
sleep_time: float = 1
|
|
34
|
-
sleep_callback: Callable[[], Any] | None = None
|
|
35
|
-
exit_wait_timeout: float | None = (
|
|
36
|
-
None # If set, will wait for the pool to finish before exiting the context manager
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
pool: ThreadPoolExecutor = field(init=False, default_factory=get_pool)
|
|
40
|
-
_pool_max_workers: int = field(init=False)
|
|
41
|
-
_max_run_count_with_this_pool: int = field(init=False)
|
|
42
|
-
_lock: RLock = field(init=False, default_factory=RLock)
|
|
43
|
-
_current_submit_count: int = field(init=False, default=0)
|
|
44
|
-
_task: new_task | None = field(init=False, default=None)
|
|
45
|
-
_event: Event = field(init=False, default_factory=Event)
|
|
46
|
-
|
|
47
|
-
def __post_init__(self):
|
|
48
|
-
self._pool_max_workers = self.pool._max_workers
|
|
49
|
-
max_run_count = max_run_count_for_workers(self._pool_max_workers)
|
|
50
|
-
workers_required_if_full = self.max_concurrent_submits * self.threads_used_per_submit
|
|
51
|
-
run_count_used_by_this_pool = ceil(workers_required_if_full / THREADS_PER_RUN)
|
|
52
|
-
assert run_count_used_by_this_pool < max_run_count, (
|
|
53
|
-
f"Run count used by this pool ({run_count_used_by_this_pool}) exceeds max run count ({max_run_count}). Adjust {AskShellSettings.ENV_NAME_THREAD_COUNT} environment variable or decrease `max_concurrent_submits` parameter."
|
|
54
|
-
)
|
|
55
|
-
self._max_run_count_with_this_pool = max_run_count - run_count_used_by_this_pool
|
|
56
|
-
|
|
57
|
-
def _on_submit_done(self):
|
|
58
|
-
"""Callback to be called when a submit is done. This is used to decrement the current submit count."""
|
|
59
|
-
with self._lock:
|
|
60
|
-
self._current_submit_count -= 1
|
|
61
|
-
if task := self._task:
|
|
62
|
-
task.update(advance=1)
|
|
63
|
-
if self._current_submit_count == 0:
|
|
64
|
-
self._event.set()
|
|
65
|
-
|
|
66
|
-
def submit(self, fn: SubmitFunc[T_co], /, *args, **kwargs) -> Future[T_co]:
|
|
67
|
-
"""Submit a task to the pool. Might block if the pool is full."""
|
|
68
|
-
|
|
69
|
-
# problem: There is a bit of lag from submit until the run is actually started,
|
|
70
|
-
with self._lock:
|
|
71
|
-
self._current_submit_count += 1
|
|
72
|
-
if self._current_submit_count == 1:
|
|
73
|
-
self._event = Event() # reset the event when the first submit is made
|
|
74
|
-
with handle_interrupt_wait(interrupt_message=f"run_pool submit for {self.task_name}"):
|
|
75
|
-
while self._current_submit_count >= self.max_concurrent_submits:
|
|
76
|
-
if self.sleep_callback:
|
|
77
|
-
self.sleep_callback()
|
|
78
|
-
time.sleep(self.sleep_time)
|
|
79
|
-
# in case more runs are already submitted
|
|
80
|
-
wait_if_many_runs(
|
|
81
|
-
max_run_count=self._max_run_count_with_this_pool,
|
|
82
|
-
sleep_time=self.sleep_time,
|
|
83
|
-
sleep_callback=self.sleep_callback,
|
|
84
|
-
)
|
|
85
|
-
future = self.pool.submit(fn, *args, **kwargs)
|
|
86
|
-
future.add_done_callback(lambda _: self._on_submit_done())
|
|
87
|
-
return future
|
|
88
|
-
|
|
89
|
-
def __enter__(self):
|
|
90
|
-
self._task = new_task(self.task_name, self.total)
|
|
91
|
-
self._task.__enter__()
|
|
92
|
-
return self
|
|
93
|
-
|
|
94
|
-
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
|
95
|
-
# no cleanup necessary, the pool will be cleaned up automatically due to atexit call
|
|
96
|
-
with handle_interrupt_wait(interrupt_message=f"interrupt in `run_pool` exit method for {self.task_name}"):
|
|
97
|
-
self._event.wait(self.exit_wait_timeout)
|
|
98
|
-
|
|
99
|
-
if task := self._task:
|
|
100
|
-
task.__exit__(exc_type, exc_value, traceback)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|