zrb 1.8.10__py3-none-any.whl → 1.21.29__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.
Potentially problematic release.
This version of zrb might be problematic. Click here for more details.
- zrb/__init__.py +126 -113
- zrb/__main__.py +1 -1
- zrb/attr/type.py +10 -7
- zrb/builtin/__init__.py +2 -50
- zrb/builtin/git.py +12 -1
- zrb/builtin/group.py +31 -15
- zrb/builtin/http.py +7 -8
- zrb/builtin/llm/attachment.py +40 -0
- zrb/builtin/llm/chat_completion.py +274 -0
- zrb/builtin/llm/chat_session.py +152 -85
- zrb/builtin/llm/chat_session_cmd.py +288 -0
- zrb/builtin/llm/chat_trigger.py +79 -0
- zrb/builtin/llm/history.py +7 -9
- zrb/builtin/llm/llm_ask.py +221 -98
- zrb/builtin/llm/tool/api.py +74 -52
- zrb/builtin/llm/tool/cli.py +46 -17
- zrb/builtin/llm/tool/code.py +71 -90
- zrb/builtin/llm/tool/file.py +301 -241
- zrb/builtin/llm/tool/note.py +84 -0
- zrb/builtin/llm/tool/rag.py +38 -8
- zrb/builtin/llm/tool/sub_agent.py +67 -50
- zrb/builtin/llm/tool/web.py +146 -122
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +7 -7
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +5 -5
- zrb/builtin/project/add/fastapp/fastapp_util.py +1 -1
- zrb/builtin/searxng/config/settings.yml +5671 -0
- zrb/builtin/searxng/start.py +21 -0
- zrb/builtin/setup/latex/ubuntu.py +1 -0
- zrb/builtin/setup/ubuntu.py +1 -1
- zrb/builtin/shell/autocomplete/bash.py +4 -3
- zrb/builtin/shell/autocomplete/zsh.py +4 -3
- zrb/builtin/todo.py +13 -2
- zrb/config/config.py +614 -0
- zrb/config/default_prompt/file_extractor_system_prompt.md +112 -0
- zrb/config/default_prompt/interactive_system_prompt.md +29 -0
- zrb/config/default_prompt/persona.md +1 -0
- zrb/config/default_prompt/repo_extractor_system_prompt.md +112 -0
- zrb/config/default_prompt/repo_summarizer_system_prompt.md +29 -0
- zrb/config/default_prompt/summarization_prompt.md +57 -0
- zrb/config/default_prompt/system_prompt.md +38 -0
- zrb/config/llm_config.py +339 -0
- zrb/config/llm_context/config.py +166 -0
- zrb/config/llm_context/config_parser.py +40 -0
- zrb/config/llm_context/workflow.py +81 -0
- zrb/config/llm_rate_limitter.py +190 -0
- zrb/{runner → config}/web_auth_config.py +17 -22
- zrb/context/any_shared_context.py +17 -1
- zrb/context/context.py +16 -2
- zrb/context/shared_context.py +18 -8
- zrb/group/any_group.py +12 -5
- zrb/group/group.py +67 -3
- zrb/input/any_input.py +5 -1
- zrb/input/base_input.py +18 -6
- zrb/input/option_input.py +13 -1
- zrb/input/text_input.py +8 -25
- zrb/runner/cli.py +25 -23
- zrb/runner/common_util.py +24 -19
- zrb/runner/web_app.py +3 -3
- zrb/runner/web_route/docs_route.py +1 -1
- zrb/runner/web_route/error_page/serve_default_404.py +1 -1
- zrb/runner/web_route/error_page/show_error_page.py +1 -1
- zrb/runner/web_route/home_page/home_page_route.py +2 -2
- zrb/runner/web_route/login_api_route.py +1 -1
- zrb/runner/web_route/login_page/login_page_route.py +2 -2
- zrb/runner/web_route/logout_api_route.py +1 -1
- zrb/runner/web_route/logout_page/logout_page_route.py +2 -2
- zrb/runner/web_route/node_page/group/show_group_page.py +1 -1
- zrb/runner/web_route/node_page/node_page_route.py +1 -1
- zrb/runner/web_route/node_page/task/show_task_page.py +1 -1
- zrb/runner/web_route/refresh_token_api_route.py +1 -1
- zrb/runner/web_route/static/static_route.py +1 -1
- zrb/runner/web_route/task_input_api_route.py +6 -6
- zrb/runner/web_route/task_session_api_route.py +20 -12
- zrb/runner/web_util/cookie.py +1 -1
- zrb/runner/web_util/token.py +1 -1
- zrb/runner/web_util/user.py +8 -4
- zrb/session/any_session.py +24 -17
- zrb/session/session.py +50 -25
- zrb/session_state_logger/any_session_state_logger.py +9 -4
- zrb/session_state_logger/file_session_state_logger.py +16 -6
- zrb/session_state_logger/session_state_logger_factory.py +1 -1
- zrb/task/any_task.py +30 -9
- zrb/task/base/context.py +17 -9
- zrb/task/base/execution.py +15 -8
- zrb/task/base/lifecycle.py +8 -4
- zrb/task/base/monitoring.py +12 -7
- zrb/task/base_task.py +69 -5
- zrb/task/base_trigger.py +12 -5
- zrb/task/cmd_task.py +1 -1
- zrb/task/llm/agent.py +154 -161
- zrb/task/llm/agent_runner.py +152 -0
- zrb/task/llm/config.py +47 -18
- zrb/task/llm/conversation_history.py +209 -0
- zrb/task/llm/conversation_history_model.py +67 -0
- zrb/task/llm/default_workflow/coding/workflow.md +41 -0
- zrb/task/llm/default_workflow/copywriting/workflow.md +68 -0
- zrb/task/llm/default_workflow/git/workflow.md +118 -0
- zrb/task/llm/default_workflow/golang/workflow.md +128 -0
- zrb/task/llm/default_workflow/html-css/workflow.md +135 -0
- zrb/task/llm/default_workflow/java/workflow.md +146 -0
- zrb/task/llm/default_workflow/javascript/workflow.md +158 -0
- zrb/task/llm/default_workflow/python/workflow.md +160 -0
- zrb/task/llm/default_workflow/researching/workflow.md +153 -0
- zrb/task/llm/default_workflow/rust/workflow.md +162 -0
- zrb/task/llm/default_workflow/shell/workflow.md +299 -0
- zrb/task/llm/error.py +24 -10
- zrb/task/llm/file_replacement.py +206 -0
- zrb/task/llm/file_tool_model.py +57 -0
- zrb/task/llm/history_processor.py +206 -0
- zrb/task/llm/history_summarization.py +11 -166
- zrb/task/llm/print_node.py +193 -69
- zrb/task/llm/prompt.py +242 -45
- zrb/task/llm/subagent_conversation_history.py +41 -0
- zrb/task/llm/tool_wrapper.py +260 -57
- zrb/task/llm/workflow.py +76 -0
- zrb/task/llm_task.py +182 -171
- zrb/task/make_task.py +2 -3
- zrb/task/rsync_task.py +26 -11
- zrb/task/scheduler.py +4 -4
- zrb/util/attr.py +54 -39
- zrb/util/callable.py +23 -0
- zrb/util/cli/markdown.py +12 -0
- zrb/util/cli/text.py +30 -0
- zrb/util/file.py +29 -11
- zrb/util/git.py +8 -11
- zrb/util/git_diff_model.py +10 -0
- zrb/util/git_subtree.py +9 -14
- zrb/util/git_subtree_model.py +32 -0
- zrb/util/init_path.py +1 -1
- zrb/util/markdown.py +62 -0
- zrb/util/string/conversion.py +2 -2
- zrb/util/todo.py +17 -50
- zrb/util/todo_model.py +46 -0
- zrb/util/truncate.py +23 -0
- zrb/util/yaml.py +204 -0
- zrb/xcom/xcom.py +10 -0
- zrb-1.21.29.dist-info/METADATA +270 -0
- {zrb-1.8.10.dist-info → zrb-1.21.29.dist-info}/RECORD +140 -98
- {zrb-1.8.10.dist-info → zrb-1.21.29.dist-info}/WHEEL +1 -1
- zrb/config.py +0 -335
- zrb/llm_config.py +0 -411
- zrb/llm_rate_limitter.py +0 -125
- zrb/task/llm/context.py +0 -102
- zrb/task/llm/context_enrichment.py +0 -199
- zrb/task/llm/history.py +0 -211
- zrb-1.8.10.dist-info/METADATA +0 -264
- {zrb-1.8.10.dist-info → zrb-1.21.29.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
from collections import deque
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
from zrb.config.config import CFG
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LLMRateLimitter:
|
|
11
|
+
"""
|
|
12
|
+
Helper class to enforce LLM API rate limits and throttling.
|
|
13
|
+
Tracks requests and tokens in a rolling 60-second window.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
max_requests_per_minute: int | None = None,
|
|
19
|
+
max_tokens_per_minute: int | None = None,
|
|
20
|
+
max_tokens_per_request: int | None = None,
|
|
21
|
+
max_tokens_per_tool_call_result: int | None = None,
|
|
22
|
+
throttle_sleep: float | None = None,
|
|
23
|
+
use_tiktoken: bool | None = None,
|
|
24
|
+
tiktoken_encoding_name: str | None = None,
|
|
25
|
+
):
|
|
26
|
+
self._max_requests_per_minute = max_requests_per_minute
|
|
27
|
+
self._max_tokens_per_minute = max_tokens_per_minute
|
|
28
|
+
self._max_tokens_per_request = max_tokens_per_request
|
|
29
|
+
self._max_tokens_per_tool_call_result = max_tokens_per_tool_call_result
|
|
30
|
+
self._throttle_sleep = throttle_sleep
|
|
31
|
+
self._use_tiktoken = use_tiktoken
|
|
32
|
+
self._tiktoken_encoding_name = tiktoken_encoding_name
|
|
33
|
+
self.request_times = deque()
|
|
34
|
+
self.token_times = deque()
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def max_requests_per_minute(self) -> int:
|
|
38
|
+
if self._max_requests_per_minute is not None:
|
|
39
|
+
return self._max_requests_per_minute
|
|
40
|
+
return CFG.LLM_MAX_REQUESTS_PER_MINUTE
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def max_tokens_per_minute(self) -> int:
|
|
44
|
+
if self._max_tokens_per_minute is not None:
|
|
45
|
+
return self._max_tokens_per_minute
|
|
46
|
+
return CFG.LLM_MAX_TOKENS_PER_MINUTE
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def max_tokens_per_request(self) -> int:
|
|
50
|
+
if self._max_tokens_per_request is not None:
|
|
51
|
+
return self._max_tokens_per_request
|
|
52
|
+
return CFG.LLM_MAX_TOKENS_PER_REQUEST
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def max_tokens_per_tool_call_result(self) -> int:
|
|
56
|
+
if self._max_tokens_per_tool_call_result is not None:
|
|
57
|
+
return self._max_tokens_per_tool_call_result
|
|
58
|
+
return CFG.LLM_MAX_TOKENS_PER_TOOL_CALL_RESULT
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def throttle_sleep(self) -> float:
|
|
62
|
+
if self._throttle_sleep is not None:
|
|
63
|
+
return self._throttle_sleep
|
|
64
|
+
return CFG.LLM_THROTTLE_SLEEP
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def use_tiktoken(self) -> bool:
|
|
68
|
+
if self._use_tiktoken is not None:
|
|
69
|
+
return self._use_tiktoken
|
|
70
|
+
return CFG.USE_TIKTOKEN
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def tiktoken_encoding_name(self) -> str:
|
|
74
|
+
if self._tiktoken_encoding_name is not None:
|
|
75
|
+
return self._tiktoken_encoding_name
|
|
76
|
+
return CFG.TIKTOKEN_ENCODING_NAME
|
|
77
|
+
|
|
78
|
+
def set_max_requests_per_minute(self, value: int):
|
|
79
|
+
self._max_requests_per_minute = value
|
|
80
|
+
|
|
81
|
+
def set_max_tokens_per_minute(self, value: int):
|
|
82
|
+
self._max_tokens_per_minute = value
|
|
83
|
+
|
|
84
|
+
def set_max_tokens_per_request(self, value: int):
|
|
85
|
+
self._max_tokens_per_request = value
|
|
86
|
+
|
|
87
|
+
def set_max_tokens_per_tool_call_result(self, value: int):
|
|
88
|
+
self._max_tokens_per_tool_call_result = value
|
|
89
|
+
|
|
90
|
+
def set_throttle_sleep(self, value: float):
|
|
91
|
+
self._throttle_sleep = value
|
|
92
|
+
|
|
93
|
+
def count_token(self, prompt: Any) -> int:
|
|
94
|
+
str_prompt = self._prompt_to_str(prompt)
|
|
95
|
+
if not self.use_tiktoken:
|
|
96
|
+
return self._fallback_count_token(str_prompt)
|
|
97
|
+
try:
|
|
98
|
+
import tiktoken
|
|
99
|
+
|
|
100
|
+
enc = tiktoken.get_encoding(self.tiktoken_encoding_name)
|
|
101
|
+
return len(enc.encode(str_prompt))
|
|
102
|
+
except Exception:
|
|
103
|
+
return self._fallback_count_token(str_prompt)
|
|
104
|
+
|
|
105
|
+
def _fallback_count_token(self, str_prompt: str) -> int:
|
|
106
|
+
return len(str_prompt) // 4
|
|
107
|
+
|
|
108
|
+
def clip_prompt(self, prompt: Any, limit: int) -> str:
|
|
109
|
+
str_prompt = self._prompt_to_str(prompt)
|
|
110
|
+
if not self.use_tiktoken:
|
|
111
|
+
return self._fallback_clip_prompt(str_prompt, limit)
|
|
112
|
+
try:
|
|
113
|
+
import tiktoken
|
|
114
|
+
|
|
115
|
+
enc = tiktoken.get_encoding(self.tiktoken_encoding_name)
|
|
116
|
+
tokens = enc.encode(str_prompt)
|
|
117
|
+
if len(tokens) <= limit:
|
|
118
|
+
return str_prompt
|
|
119
|
+
truncated = tokens[: limit - 3]
|
|
120
|
+
clipped_text = enc.decode(truncated)
|
|
121
|
+
return clipped_text + "..."
|
|
122
|
+
except Exception:
|
|
123
|
+
return self._fallback_clip_prompt(str_prompt, limit)
|
|
124
|
+
|
|
125
|
+
def _fallback_clip_prompt(self, str_prompt: str, limit: int) -> str:
|
|
126
|
+
char_limit = limit * 4 if limit * 4 <= 10 else limit * 4 - 10
|
|
127
|
+
return str_prompt[:char_limit] + "..."
|
|
128
|
+
|
|
129
|
+
async def throttle(
|
|
130
|
+
self,
|
|
131
|
+
prompt: Any,
|
|
132
|
+
throttle_notif_callback: Callable[[str], Any] | None = None,
|
|
133
|
+
):
|
|
134
|
+
now = time.time()
|
|
135
|
+
str_prompt = self._prompt_to_str(prompt)
|
|
136
|
+
tokens = self.count_token(str_prompt)
|
|
137
|
+
# Clean up old entries
|
|
138
|
+
while self.request_times and now - self.request_times[0] > 60:
|
|
139
|
+
self.request_times.popleft()
|
|
140
|
+
while self.token_times and now - self.token_times[0][0] > 60:
|
|
141
|
+
self.token_times.popleft()
|
|
142
|
+
# Check per-request token limit
|
|
143
|
+
if tokens > self.max_tokens_per_request:
|
|
144
|
+
raise ValueError(
|
|
145
|
+
(
|
|
146
|
+
"Request exceeds max_tokens_per_request "
|
|
147
|
+
f"({tokens} > {self.max_tokens_per_request})."
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
if tokens > self.max_tokens_per_minute:
|
|
151
|
+
raise ValueError(
|
|
152
|
+
(
|
|
153
|
+
"Request exceeds max_tokens_per_minute "
|
|
154
|
+
f"({tokens} > {self.max_tokens_per_minute})."
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
# Wait if over per-minute request or token limit
|
|
158
|
+
while (
|
|
159
|
+
len(self.request_times) >= self.max_requests_per_minute
|
|
160
|
+
or sum(t for _, t in self.token_times) + tokens > self.max_tokens_per_minute
|
|
161
|
+
):
|
|
162
|
+
if throttle_notif_callback is not None:
|
|
163
|
+
if len(self.request_times) >= self.max_requests_per_minute:
|
|
164
|
+
rpm = len(self.request_times)
|
|
165
|
+
throttle_notif_callback(
|
|
166
|
+
f"Max request per minute exceeded: {rpm} of {self.max_requests_per_minute}"
|
|
167
|
+
)
|
|
168
|
+
else:
|
|
169
|
+
tpm = sum(t for _, t in self.token_times) + tokens
|
|
170
|
+
throttle_notif_callback(
|
|
171
|
+
f"Max token per minute exceeded: {tpm} of {self.max_tokens_per_minute}"
|
|
172
|
+
)
|
|
173
|
+
await asyncio.sleep(self.throttle_sleep)
|
|
174
|
+
now = time.time()
|
|
175
|
+
while self.request_times and now - self.request_times[0] > 60:
|
|
176
|
+
self.request_times.popleft()
|
|
177
|
+
while self.token_times and now - self.token_times[0][0] > 60:
|
|
178
|
+
self.token_times.popleft()
|
|
179
|
+
# Record this request
|
|
180
|
+
self.request_times.append(now)
|
|
181
|
+
self.token_times.append((now, tokens))
|
|
182
|
+
|
|
183
|
+
def _prompt_to_str(self, prompt: Any) -> str:
|
|
184
|
+
try:
|
|
185
|
+
return json.dumps(prompt)
|
|
186
|
+
except Exception:
|
|
187
|
+
return f"{prompt}"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
llm_rate_limitter = LLMRateLimitter()
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
from typing import Callable
|
|
1
|
+
from typing import TYPE_CHECKING, Callable
|
|
2
2
|
|
|
3
|
-
from zrb.config import CFG
|
|
4
|
-
from zrb.runner.web_schema.user import User
|
|
3
|
+
from zrb.config.config import CFG
|
|
5
4
|
from zrb.task.any_task import AnyTask
|
|
6
5
|
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from zrb.runner.web_schema.user import User
|
|
8
|
+
|
|
7
9
|
|
|
8
10
|
class WebAuthConfig:
|
|
9
11
|
def __init__(
|
|
10
12
|
self,
|
|
11
|
-
port: int | None = None,
|
|
12
13
|
secret_key: str | None = None,
|
|
13
14
|
access_token_expire_minutes: int | None = None,
|
|
14
15
|
refresh_token_expire_minutes: int | None = None,
|
|
@@ -19,9 +20,8 @@ class WebAuthConfig:
|
|
|
19
20
|
super_admin_password: str | None = None,
|
|
20
21
|
guest_username: str | None = None,
|
|
21
22
|
guest_accessible_tasks: list[AnyTask | str] = [],
|
|
22
|
-
find_user_by_username: Callable[[str], User | None] | None = None,
|
|
23
|
+
find_user_by_username: Callable[[str], "User | None"] | None = None,
|
|
23
24
|
):
|
|
24
|
-
self._port = port
|
|
25
25
|
self._secret_key = secret_key
|
|
26
26
|
self._access_token_expire_minutes = access_token_expire_minutes
|
|
27
27
|
self._refresh_token_expire_minutes = refresh_token_expire_minutes
|
|
@@ -31,16 +31,10 @@ class WebAuthConfig:
|
|
|
31
31
|
self._super_admin_username = super_admin_username
|
|
32
32
|
self._super_admin_password = super_admin_password
|
|
33
33
|
self._guest_username = guest_username
|
|
34
|
-
self._user_list = []
|
|
34
|
+
self._user_list: list["User"] = []
|
|
35
35
|
self._guest_accessible_tasks = guest_accessible_tasks
|
|
36
36
|
self._find_user_by_username = find_user_by_username
|
|
37
37
|
|
|
38
|
-
@property
|
|
39
|
-
def port(self) -> int:
|
|
40
|
-
if self._port is not None:
|
|
41
|
-
return self._port
|
|
42
|
-
return CFG.WEB_HTTP_PORT
|
|
43
|
-
|
|
44
38
|
@property
|
|
45
39
|
def secret_key(self) -> str:
|
|
46
40
|
if self._secret_key is not None:
|
|
@@ -100,7 +94,9 @@ class WebAuthConfig:
|
|
|
100
94
|
return self._guest_accessible_tasks
|
|
101
95
|
|
|
102
96
|
@property
|
|
103
|
-
def default_user(self) -> User:
|
|
97
|
+
def default_user(self) -> "User":
|
|
98
|
+
from zrb.runner.web_schema.user import User
|
|
99
|
+
|
|
104
100
|
if self.enable_auth:
|
|
105
101
|
return User(
|
|
106
102
|
username=self.guest_username,
|
|
@@ -116,7 +112,9 @@ class WebAuthConfig:
|
|
|
116
112
|
)
|
|
117
113
|
|
|
118
114
|
@property
|
|
119
|
-
def super_admin(self) -> User:
|
|
115
|
+
def super_admin(self) -> "User":
|
|
116
|
+
from zrb.runner.web_schema.user import User
|
|
117
|
+
|
|
120
118
|
return User(
|
|
121
119
|
username=self.super_admin_username,
|
|
122
120
|
password=self.super_admin_password,
|
|
@@ -124,14 +122,11 @@ class WebAuthConfig:
|
|
|
124
122
|
)
|
|
125
123
|
|
|
126
124
|
@property
|
|
127
|
-
def user_list(self) -> list[User]:
|
|
125
|
+
def user_list(self) -> list["User"]:
|
|
128
126
|
if not self.enable_auth:
|
|
129
127
|
return [self.default_user]
|
|
130
128
|
return self._user_list + [self.super_admin, self.default_user]
|
|
131
129
|
|
|
132
|
-
def set_port(self, port: int):
|
|
133
|
-
self._port = port
|
|
134
|
-
|
|
135
130
|
def set_secret_key(self, secret_key: str):
|
|
136
131
|
self._secret_key = secret_key
|
|
137
132
|
|
|
@@ -163,11 +158,11 @@ class WebAuthConfig:
|
|
|
163
158
|
self._guest_accessible_tasks = tasks
|
|
164
159
|
|
|
165
160
|
def set_find_user_by_username(
|
|
166
|
-
self, find_user_by_username: Callable[[str], User | None]
|
|
161
|
+
self, find_user_by_username: Callable[[str], "User | None"]
|
|
167
162
|
):
|
|
168
163
|
self._find_user_by_username = find_user_by_username
|
|
169
164
|
|
|
170
|
-
def append_user(self, user: User):
|
|
165
|
+
def append_user(self, user: "User"):
|
|
171
166
|
duplicates = [
|
|
172
167
|
existing_user
|
|
173
168
|
for existing_user in self.user_list
|
|
@@ -177,7 +172,7 @@ class WebAuthConfig:
|
|
|
177
172
|
raise ValueError(f"User already exists {user.username}")
|
|
178
173
|
self._user_list.append(user)
|
|
179
174
|
|
|
180
|
-
def find_user_by_username(self, username: str) -> User | None:
|
|
175
|
+
def find_user_by_username(self, username: str) -> "User | None":
|
|
181
176
|
user = None
|
|
182
177
|
if self._find_user_by_username is not None:
|
|
183
178
|
user = self._find_user_by_username(username)
|
|
@@ -19,26 +19,42 @@ class AnySharedContext(ABC):
|
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
21
|
@property
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def is_web_mode(self) -> bool:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def is_tty(self) -> bool:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
@abstractmethod
|
|
22
33
|
def input(self) -> DotDict:
|
|
23
34
|
pass
|
|
24
35
|
|
|
25
36
|
@property
|
|
37
|
+
@abstractmethod
|
|
26
38
|
def env(self) -> DotDict:
|
|
27
39
|
pass
|
|
28
40
|
|
|
29
41
|
@property
|
|
42
|
+
@abstractmethod
|
|
30
43
|
def args(self) -> list[Any]:
|
|
31
44
|
pass
|
|
32
45
|
|
|
33
46
|
@property
|
|
34
|
-
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def xcom(self) -> DotDict:
|
|
35
49
|
pass
|
|
36
50
|
|
|
37
51
|
@property
|
|
52
|
+
@abstractmethod
|
|
38
53
|
def shared_log(self) -> list[str]:
|
|
39
54
|
pass
|
|
40
55
|
|
|
41
56
|
@property
|
|
57
|
+
@abstractmethod
|
|
42
58
|
def session(self) -> any_session.AnySession | None:
|
|
43
59
|
pass
|
|
44
60
|
|
zrb/context/context.py
CHANGED
|
@@ -33,6 +33,14 @@ class Context(AnyContext):
|
|
|
33
33
|
class_name = self.__class__.__name__
|
|
34
34
|
return f"<{class_name} shared_ctx={self._shared_ctx}>"
|
|
35
35
|
|
|
36
|
+
@property
|
|
37
|
+
def is_web_mode(self) -> bool:
|
|
38
|
+
return self._shared_ctx.is_web_mode
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def is_tty(self) -> bool:
|
|
42
|
+
return self._shared_ctx.is_tty
|
|
43
|
+
|
|
36
44
|
@property
|
|
37
45
|
def input(self) -> DotDict:
|
|
38
46
|
return self._shared_ctx.input
|
|
@@ -55,7 +63,7 @@ class Context(AnyContext):
|
|
|
55
63
|
|
|
56
64
|
@property
|
|
57
65
|
def session(self) -> AnySession | None:
|
|
58
|
-
return self._shared_ctx.
|
|
66
|
+
return self._shared_ctx.session
|
|
59
67
|
|
|
60
68
|
def update_task_env(self, task_env: dict[str, str]):
|
|
61
69
|
self._env.update(task_env)
|
|
@@ -111,7 +119,13 @@ class Context(AnyContext):
|
|
|
111
119
|
return
|
|
112
120
|
color = self._color
|
|
113
121
|
icon = self._icon
|
|
114
|
-
|
|
122
|
+
# Handle case where session is None (e.g., in tests)
|
|
123
|
+
if self.session is None:
|
|
124
|
+
max_name_length = len(self._task_name) + len(icon)
|
|
125
|
+
else:
|
|
126
|
+
max_name_length = max(
|
|
127
|
+
len(name) + len(icon) for name in self.session.task_names
|
|
128
|
+
)
|
|
115
129
|
styled_task_name = f"{icon} {self._task_name}"
|
|
116
130
|
padded_styled_task_name = styled_task_name.rjust(max_name_length + 1)
|
|
117
131
|
if self._attempt == 0:
|
zrb/context/shared_context.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import datetime
|
|
2
|
+
import sys
|
|
2
3
|
from typing import Any
|
|
3
4
|
|
|
4
|
-
from zrb.config import CFG
|
|
5
|
+
from zrb.config.config import CFG
|
|
5
6
|
from zrb.context.any_shared_context import AnySharedContext
|
|
6
7
|
from zrb.dot_dict.dot_dict import DotDict
|
|
7
8
|
from zrb.session.any_session import AnySession
|
|
@@ -26,6 +27,7 @@ class SharedContext(AnySharedContext):
|
|
|
26
27
|
env: dict[str, str] = {},
|
|
27
28
|
xcom: dict[str, Xcom] = {},
|
|
28
29
|
logging_level: int | None = None,
|
|
30
|
+
is_web_mode: bool = False,
|
|
29
31
|
):
|
|
30
32
|
self.__logging_level = logging_level
|
|
31
33
|
self._input = DotDict(input)
|
|
@@ -34,14 +36,22 @@ class SharedContext(AnySharedContext):
|
|
|
34
36
|
self._xcom = DotDict(xcom)
|
|
35
37
|
self._session: AnySession | None = None
|
|
36
38
|
self._log = []
|
|
39
|
+
self._is_web_mode = is_web_mode
|
|
37
40
|
|
|
38
41
|
def __repr__(self):
|
|
39
42
|
class_name = self.__class__.__name__
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return
|
|
43
|
+
return f"<{class_name}>"
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def is_web_mode(self) -> bool:
|
|
47
|
+
return self._is_web_mode
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def is_tty(self) -> bool:
|
|
51
|
+
try:
|
|
52
|
+
return sys.stdin.isatty()
|
|
53
|
+
except Exception:
|
|
54
|
+
return False
|
|
45
55
|
|
|
46
56
|
@property
|
|
47
57
|
def input(self) -> DotDict:
|
|
@@ -56,7 +66,7 @@ class SharedContext(AnySharedContext):
|
|
|
56
66
|
return self._args
|
|
57
67
|
|
|
58
68
|
@property
|
|
59
|
-
def xcom(self) -> DotDict
|
|
69
|
+
def xcom(self) -> DotDict:
|
|
60
70
|
return self._xcom
|
|
61
71
|
|
|
62
72
|
@property
|
|
@@ -71,7 +81,7 @@ class SharedContext(AnySharedContext):
|
|
|
71
81
|
self._log.append(message)
|
|
72
82
|
session = self.session
|
|
73
83
|
if session is not None:
|
|
74
|
-
session_parent: AnySession = session.parent
|
|
84
|
+
session_parent: AnySession | None = session.parent
|
|
75
85
|
if session_parent is not None:
|
|
76
86
|
session_parent.shared_ctx.append_to_shared_log(message)
|
|
77
87
|
|
zrb/group/any_group.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
-
from typing import Optional, Union
|
|
3
2
|
|
|
4
3
|
from zrb.task.any_task import AnyTask
|
|
5
4
|
|
|
@@ -31,16 +30,24 @@ class AnyGroup(ABC):
|
|
|
31
30
|
|
|
32
31
|
@property
|
|
33
32
|
@abstractmethod
|
|
34
|
-
def subgroups(self) -> dict[str,
|
|
33
|
+
def subgroups(self) -> "dict[str, AnyGroup]":
|
|
35
34
|
"""Group subgroups"""
|
|
36
35
|
pass
|
|
37
36
|
|
|
38
37
|
@abstractmethod
|
|
39
|
-
def add_group(self, group:
|
|
38
|
+
def add_group(self, group: "AnyGroup", alias: str | None = None) -> "AnyGroup":
|
|
40
39
|
pass
|
|
41
40
|
|
|
42
41
|
@abstractmethod
|
|
43
|
-
def add_task(self, task: AnyTask, alias: str | None = None) -> AnyTask:
|
|
42
|
+
def add_task(self, task: "AnyTask", alias: str | None = None) -> "AnyTask":
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def remove_group(self, group: "AnyGroup | str"):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def remove_task(self, task: "AnyTask | str"):
|
|
44
51
|
pass
|
|
45
52
|
|
|
46
53
|
@abstractmethod
|
|
@@ -48,5 +55,5 @@ class AnyGroup(ABC):
|
|
|
48
55
|
pass
|
|
49
56
|
|
|
50
57
|
@abstractmethod
|
|
51
|
-
def get_group_by_alias(self,
|
|
58
|
+
def get_group_by_alias(self, alias: str) -> "AnyGroup | None":
|
|
52
59
|
pass
|
zrb/group/group.py
CHANGED
|
@@ -33,15 +33,15 @@ class Group(AnyGroup):
|
|
|
33
33
|
def subgroups(self) -> dict[str, AnyGroup]:
|
|
34
34
|
names = list(self._groups.keys())
|
|
35
35
|
names.sort()
|
|
36
|
-
return {name: self._groups
|
|
36
|
+
return {name: self._groups[name] for name in names}
|
|
37
37
|
|
|
38
38
|
@property
|
|
39
39
|
def subtasks(self) -> dict[str, AnyTask]:
|
|
40
40
|
alias = list(self._tasks.keys())
|
|
41
41
|
alias.sort()
|
|
42
|
-
return {name: self._tasks
|
|
42
|
+
return {name: self._tasks[name] for name in alias}
|
|
43
43
|
|
|
44
|
-
def add_group(self, group: AnyGroup
|
|
44
|
+
def add_group(self, group: AnyGroup, alias: str | None = None) -> AnyGroup:
|
|
45
45
|
real_group = Group(group) if isinstance(group, str) else group
|
|
46
46
|
alias = alias if alias is not None else real_group.name
|
|
47
47
|
self._groups[alias] = real_group
|
|
@@ -52,6 +52,70 @@ class Group(AnyGroup):
|
|
|
52
52
|
self._tasks[alias] = task
|
|
53
53
|
return task
|
|
54
54
|
|
|
55
|
+
def remove_group(self, group: "AnyGroup | str"):
|
|
56
|
+
original_groups_len = len(self._groups)
|
|
57
|
+
if isinstance(group, AnyGroup):
|
|
58
|
+
new_groups = {
|
|
59
|
+
alias: existing_group
|
|
60
|
+
for alias, existing_group in self._groups.items()
|
|
61
|
+
if group != existing_group
|
|
62
|
+
}
|
|
63
|
+
if len(new_groups) == original_groups_len:
|
|
64
|
+
raise ValueError(f"Cannot remove group {group} from {self}")
|
|
65
|
+
self._groups = new_groups
|
|
66
|
+
return
|
|
67
|
+
# group is string, try to remove by alias
|
|
68
|
+
new_groups = {
|
|
69
|
+
alias: existing_group
|
|
70
|
+
for alias, existing_group in self._groups.items()
|
|
71
|
+
if alias != group
|
|
72
|
+
}
|
|
73
|
+
if len(new_groups) < original_groups_len:
|
|
74
|
+
self._groups = new_groups
|
|
75
|
+
return
|
|
76
|
+
# if alias removal didn't work, try to remove by name
|
|
77
|
+
new_groups = {
|
|
78
|
+
alias: existing_group
|
|
79
|
+
for alias, existing_group in self._groups.items()
|
|
80
|
+
if existing_group.name != group
|
|
81
|
+
}
|
|
82
|
+
if len(new_groups) < original_groups_len:
|
|
83
|
+
self._groups = new_groups
|
|
84
|
+
return
|
|
85
|
+
raise ValueError(f"Cannot remove group {group} from {self}")
|
|
86
|
+
|
|
87
|
+
def remove_task(self, task: "AnyTask | str"):
|
|
88
|
+
original_tasks_len = len(self._tasks)
|
|
89
|
+
if isinstance(task, AnyTask):
|
|
90
|
+
new_tasks = {
|
|
91
|
+
alias: existing_task
|
|
92
|
+
for alias, existing_task in self._tasks.items()
|
|
93
|
+
if task != existing_task
|
|
94
|
+
}
|
|
95
|
+
if len(new_tasks) == original_tasks_len:
|
|
96
|
+
raise ValueError(f"Cannot remove task {task} from {self}")
|
|
97
|
+
self._tasks = new_tasks
|
|
98
|
+
return
|
|
99
|
+
# task is string, try to remove by alias
|
|
100
|
+
new_tasks = {
|
|
101
|
+
alias: existing_task
|
|
102
|
+
for alias, existing_task in self._tasks.items()
|
|
103
|
+
if alias != task
|
|
104
|
+
}
|
|
105
|
+
if len(new_tasks) < original_tasks_len:
|
|
106
|
+
self._tasks = new_tasks
|
|
107
|
+
return
|
|
108
|
+
# if alias removal didn't work, try to remove by name
|
|
109
|
+
new_tasks = {
|
|
110
|
+
alias: existing_task
|
|
111
|
+
for alias, existing_task in self._tasks.items()
|
|
112
|
+
if existing_task.name != task
|
|
113
|
+
}
|
|
114
|
+
if len(new_tasks) < original_tasks_len:
|
|
115
|
+
self._tasks = new_tasks
|
|
116
|
+
return
|
|
117
|
+
raise ValueError(f"Cannot remove task {task} from {self}")
|
|
118
|
+
|
|
55
119
|
def get_task_by_alias(self, alias: str) -> AnyTask | None:
|
|
56
120
|
return self._tasks.get(alias)
|
|
57
121
|
|
zrb/input/any_input.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any
|
|
2
3
|
|
|
3
4
|
from zrb.context.any_shared_context import AnySharedContext
|
|
4
5
|
|
|
@@ -35,7 +36,10 @@ class AnyInput(ABC):
|
|
|
35
36
|
|
|
36
37
|
@abstractmethod
|
|
37
38
|
def update_shared_context(
|
|
38
|
-
self,
|
|
39
|
+
self,
|
|
40
|
+
shared_ctx: AnySharedContext,
|
|
41
|
+
str_value: str | None = None,
|
|
42
|
+
value: Any = None,
|
|
39
43
|
):
|
|
40
44
|
pass
|
|
41
45
|
|
zrb/input/base_input.py
CHANGED
|
@@ -58,11 +58,15 @@ class BaseInput(AnyInput):
|
|
|
58
58
|
return f'<input name="{name}" placeholder="{description}" value="{default}" />'
|
|
59
59
|
|
|
60
60
|
def update_shared_context(
|
|
61
|
-
self,
|
|
61
|
+
self,
|
|
62
|
+
shared_ctx: AnySharedContext,
|
|
63
|
+
str_value: str | None = None,
|
|
64
|
+
value: Any = None,
|
|
62
65
|
):
|
|
63
|
-
if
|
|
64
|
-
str_value
|
|
65
|
-
|
|
66
|
+
if value is None:
|
|
67
|
+
if str_value is None:
|
|
68
|
+
str_value = self.get_default_str(shared_ctx)
|
|
69
|
+
value = self._parse_str_value(str_value)
|
|
66
70
|
if self.name in shared_ctx.input:
|
|
67
71
|
raise ValueError(f"Input already defined in the context: {self.name}")
|
|
68
72
|
shared_ctx.input[self.name] = value
|
|
@@ -91,12 +95,20 @@ class BaseInput(AnyInput):
|
|
|
91
95
|
default_str = self.get_default_str(shared_ctx)
|
|
92
96
|
if default_str != "":
|
|
93
97
|
prompt_message = f"{prompt_message} [{default_str}]"
|
|
94
|
-
|
|
95
|
-
value = input()
|
|
98
|
+
value = self._read_line(shared_ctx, prompt_message)
|
|
96
99
|
if value.strip() == "":
|
|
97
100
|
value = default_str
|
|
98
101
|
return value
|
|
99
102
|
|
|
103
|
+
def _read_line(self, shared_ctx: AnySharedContext, prompt_message: str) -> str:
|
|
104
|
+
if not shared_ctx.is_tty:
|
|
105
|
+
print(f"{prompt_message}: ", end="")
|
|
106
|
+
return input()
|
|
107
|
+
from prompt_toolkit import PromptSession
|
|
108
|
+
|
|
109
|
+
reader = PromptSession()
|
|
110
|
+
return reader.prompt(f"{prompt_message}: ")
|
|
111
|
+
|
|
100
112
|
def get_default_str(self, shared_ctx: AnySharedContext) -> str:
|
|
101
113
|
"""Get default value as str"""
|
|
102
114
|
default_value = get_attr(
|
zrb/input/option_input.py
CHANGED
|
@@ -47,9 +47,21 @@ class OptionInput(BaseInput):
|
|
|
47
47
|
option_str = ", ".join(options)
|
|
48
48
|
if default_value != "":
|
|
49
49
|
prompt_message = f"{prompt_message} ({option_str}) [{default_value}]"
|
|
50
|
-
value =
|
|
50
|
+
value = self._get_value_from_user_input(shared_ctx, prompt_message, options)
|
|
51
51
|
if value.strip() != "" and value.strip() not in options:
|
|
52
52
|
value = self._prompt_cli_str(shared_ctx)
|
|
53
53
|
if value.strip() == "":
|
|
54
54
|
value = default_value
|
|
55
55
|
return value
|
|
56
|
+
|
|
57
|
+
def _get_value_from_user_input(
|
|
58
|
+
self, shared_ctx: AnySharedContext, prompt_message: str, options: list[str]
|
|
59
|
+
) -> str:
|
|
60
|
+
from prompt_toolkit import PromptSession
|
|
61
|
+
from prompt_toolkit.completion import WordCompleter
|
|
62
|
+
|
|
63
|
+
if shared_ctx.is_tty:
|
|
64
|
+
reader = PromptSession()
|
|
65
|
+
option_completer = WordCompleter(options, ignore_case=True)
|
|
66
|
+
return reader.prompt(f"{prompt_message}: ", completer=option_completer)
|
|
67
|
+
return input(f"{prompt_message}: ")
|