zrb 1.13.1__py3-none-any.whl → 1.21.33__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.
- zrb/__init__.py +2 -6
- zrb/attr/type.py +10 -7
- zrb/builtin/__init__.py +2 -0
- 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 +287 -0
- zrb/builtin/llm/chat_session.py +130 -144
- zrb/builtin/llm/chat_session_cmd.py +288 -0
- zrb/builtin/llm/chat_trigger.py +78 -0
- zrb/builtin/llm/history.py +4 -4
- zrb/builtin/llm/llm_ask.py +218 -110
- zrb/builtin/llm/tool/api.py +74 -62
- zrb/builtin/llm/tool/cli.py +56 -21
- zrb/builtin/llm/tool/code.py +57 -47
- zrb/builtin/llm/tool/file.py +292 -255
- zrb/builtin/llm/tool/note.py +84 -0
- zrb/builtin/llm/tool/rag.py +25 -18
- zrb/builtin/llm/tool/search/__init__.py +1 -0
- zrb/builtin/llm/tool/search/brave.py +66 -0
- zrb/builtin/llm/tool/search/searxng.py +61 -0
- zrb/builtin/llm/tool/search/serpapi.py +61 -0
- zrb/builtin/llm/tool/sub_agent.py +53 -26
- zrb/builtin/llm/tool/web.py +94 -157
- 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/config/config.py +297 -79
- zrb/config/default_prompt/file_extractor_system_prompt.md +109 -9
- zrb/config/default_prompt/interactive_system_prompt.md +25 -28
- zrb/config/default_prompt/persona.md +1 -1
- zrb/config/default_prompt/repo_extractor_system_prompt.md +31 -31
- zrb/config/default_prompt/repo_summarizer_system_prompt.md +27 -8
- zrb/config/default_prompt/summarization_prompt.md +57 -16
- zrb/config/default_prompt/system_prompt.md +29 -25
- zrb/config/llm_config.py +129 -24
- zrb/config/llm_context/config.py +127 -90
- zrb/config/llm_context/config_parser.py +1 -7
- zrb/config/llm_context/workflow.py +81 -0
- zrb/config/llm_rate_limitter.py +100 -47
- zrb/context/any_shared_context.py +7 -1
- zrb/context/context.py +8 -2
- zrb/context/shared_context.py +6 -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 +7 -24
- zrb/runner/cli.py +21 -20
- zrb/runner/common_util.py +24 -19
- zrb/runner/web_route/task_input_api_route.py +5 -5
- zrb/runner/web_route/task_session_api_route.py +1 -4
- zrb/runner/web_util/user.py +7 -3
- zrb/session/any_session.py +12 -6
- zrb/session/session.py +39 -18
- zrb/task/any_task.py +24 -3
- 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/llm/agent.py +130 -145
- zrb/task/llm/agent_runner.py +152 -0
- zrb/task/llm/config.py +45 -13
- zrb/task/llm/conversation_history.py +110 -29
- zrb/task/llm/conversation_history_model.py +4 -179
- 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/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 +2 -192
- zrb/task/llm/print_node.py +192 -64
- zrb/task/llm/prompt.py +198 -153
- zrb/task/llm/subagent_conversation_history.py +41 -0
- zrb/task/llm/tool_confirmation_completer.py +41 -0
- zrb/task/llm/tool_wrapper.py +216 -55
- zrb/task/llm/workflow.py +76 -0
- zrb/task/llm_task.py +122 -70
- zrb/task/make_task.py +2 -3
- zrb/task/rsync_task.py +25 -10
- zrb/task/scheduler.py +4 -4
- zrb/util/attr.py +54 -39
- zrb/util/cli/markdown.py +12 -0
- zrb/util/cli/text.py +30 -0
- zrb/util/file.py +27 -11
- zrb/util/git.py +2 -2
- zrb/util/{llm/prompt.py → markdown.py} +2 -3
- zrb/util/string/conversion.py +1 -1
- zrb/util/truncate.py +23 -0
- zrb/util/yaml.py +204 -0
- zrb/xcom/xcom.py +10 -0
- {zrb-1.13.1.dist-info → zrb-1.21.33.dist-info}/METADATA +40 -20
- {zrb-1.13.1.dist-info → zrb-1.21.33.dist-info}/RECORD +114 -83
- {zrb-1.13.1.dist-info → zrb-1.21.33.dist-info}/WHEEL +1 -1
- zrb/task/llm/default_workflow/coding.md +0 -24
- zrb/task/llm/default_workflow/copywriting.md +0 -17
- zrb/task/llm/default_workflow/researching.md +0 -18
- {zrb-1.13.1.dist-info → zrb-1.21.33.dist-info}/entry_points.txt +0 -0
zrb/util/attr.py
CHANGED
|
@@ -9,52 +9,59 @@ from zrb.attr.type import (
|
|
|
9
9
|
StrDictAttr,
|
|
10
10
|
StrListAttr,
|
|
11
11
|
)
|
|
12
|
+
from zrb.context.any_context import AnyContext
|
|
12
13
|
from zrb.context.any_shared_context import AnySharedContext
|
|
13
14
|
from zrb.util.string.conversion import to_boolean
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
def get_str_list_attr(
|
|
17
|
-
|
|
18
|
+
ctx: AnyContext | AnySharedContext,
|
|
19
|
+
attr: StrListAttr | None,
|
|
20
|
+
auto_render: bool = True,
|
|
18
21
|
) -> list[str]:
|
|
19
22
|
"""
|
|
20
23
|
Retrieve a list of strings from shared context attributes.
|
|
21
24
|
|
|
22
25
|
Args:
|
|
23
|
-
|
|
26
|
+
ctx (AnyContext): The shared context object.
|
|
24
27
|
attr (StrListAttr | None): The string list attribute to retrieve.
|
|
25
28
|
auto_render (bool): Whether to auto-render the attribute values.
|
|
26
29
|
|
|
27
30
|
Returns:
|
|
28
31
|
list[str]: A list of string attributes.
|
|
29
32
|
"""
|
|
33
|
+
if attr is None:
|
|
34
|
+
return []
|
|
30
35
|
if callable(attr):
|
|
31
|
-
return attr(
|
|
32
|
-
return
|
|
36
|
+
return attr(ctx)
|
|
37
|
+
return [get_str_attr(ctx, val, "", auto_render) for val in attr]
|
|
33
38
|
|
|
34
39
|
|
|
35
40
|
def get_str_dict_attr(
|
|
36
|
-
|
|
41
|
+
ctx: AnyContext | AnySharedContext,
|
|
42
|
+
attr: StrDictAttr | None,
|
|
43
|
+
auto_render: bool = True,
|
|
37
44
|
) -> dict[str, Any]:
|
|
38
45
|
"""
|
|
39
46
|
Retrieve a dictionary of strings from shared context attributes.
|
|
40
47
|
|
|
41
48
|
Args:
|
|
42
|
-
|
|
49
|
+
ctx (AnyContext): The shared context object.
|
|
43
50
|
attr (StrDictAttr | None): The string dictionary attribute to retrieve.
|
|
44
51
|
auto_render (bool): Whether to auto-render the attribute values.
|
|
45
52
|
|
|
46
53
|
Returns:
|
|
47
54
|
dict[str, Any]: A dictionary of string attributes.
|
|
48
55
|
"""
|
|
56
|
+
if attr is None:
|
|
57
|
+
return {}
|
|
49
58
|
if callable(attr):
|
|
50
|
-
return attr(
|
|
51
|
-
return {
|
|
52
|
-
key: get_str_attr(shared_ctx, val, "", auto_render) for key, val in attr.items()
|
|
53
|
-
}
|
|
59
|
+
return attr(ctx)
|
|
60
|
+
return {key: get_str_attr(ctx, val, "", auto_render) for key, val in attr.items()}
|
|
54
61
|
|
|
55
62
|
|
|
56
63
|
def get_str_attr(
|
|
57
|
-
|
|
64
|
+
ctx: AnyContext | AnySharedContext,
|
|
58
65
|
attr: StrAttr | None,
|
|
59
66
|
default: StrAttr = "",
|
|
60
67
|
auto_render: bool = True,
|
|
@@ -63,7 +70,7 @@ def get_str_attr(
|
|
|
63
70
|
Retrieve a string from shared context attributes.
|
|
64
71
|
|
|
65
72
|
Args:
|
|
66
|
-
|
|
73
|
+
ctx (AnyContext): The shared context object.
|
|
67
74
|
attr (StrAttr | None): The string attribute to retrieve.
|
|
68
75
|
default (StrAttr): The default value if the attribute is None.
|
|
69
76
|
auto_render (bool): Whether to auto-render the attribute value.
|
|
@@ -71,14 +78,16 @@ def get_str_attr(
|
|
|
71
78
|
Returns:
|
|
72
79
|
str: The string attribute value.
|
|
73
80
|
"""
|
|
74
|
-
val = get_attr(
|
|
75
|
-
if
|
|
76
|
-
return
|
|
77
|
-
|
|
81
|
+
val = get_attr(ctx, attr, default, auto_render)
|
|
82
|
+
if isinstance(val, str):
|
|
83
|
+
return val
|
|
84
|
+
if val is None:
|
|
85
|
+
return ""
|
|
86
|
+
return str(val)
|
|
78
87
|
|
|
79
88
|
|
|
80
89
|
def get_bool_attr(
|
|
81
|
-
|
|
90
|
+
ctx: AnyContext | AnySharedContext,
|
|
82
91
|
attr: BoolAttr | None,
|
|
83
92
|
default: BoolAttr = False,
|
|
84
93
|
auto_render: bool = True,
|
|
@@ -87,7 +96,7 @@ def get_bool_attr(
|
|
|
87
96
|
Retrieve a boolean from shared context attributes.
|
|
88
97
|
|
|
89
98
|
Args:
|
|
90
|
-
|
|
99
|
+
ctx (AnyContext): The shared context object.
|
|
91
100
|
attr (BoolAttr | None): The boolean attribute to retrieve.
|
|
92
101
|
default (BoolAttr): The default value if the attribute is None.
|
|
93
102
|
auto_render (bool): Whether to auto-render the attribute value if it's a string.
|
|
@@ -95,14 +104,16 @@ def get_bool_attr(
|
|
|
95
104
|
Returns:
|
|
96
105
|
bool: The boolean attribute value.
|
|
97
106
|
"""
|
|
98
|
-
val = get_attr(
|
|
99
|
-
if isinstance(val,
|
|
100
|
-
return
|
|
101
|
-
|
|
107
|
+
val = get_attr(ctx, attr, default, auto_render)
|
|
108
|
+
if isinstance(val, bool):
|
|
109
|
+
return val
|
|
110
|
+
if val is None:
|
|
111
|
+
return False
|
|
112
|
+
return to_boolean(val)
|
|
102
113
|
|
|
103
114
|
|
|
104
115
|
def get_int_attr(
|
|
105
|
-
|
|
116
|
+
ctx: AnyContext | AnySharedContext,
|
|
106
117
|
attr: IntAttr | None,
|
|
107
118
|
default: IntAttr = 0,
|
|
108
119
|
auto_render: bool = True,
|
|
@@ -111,7 +122,7 @@ def get_int_attr(
|
|
|
111
122
|
Retrieve an integer from shared context attributes.
|
|
112
123
|
|
|
113
124
|
Args:
|
|
114
|
-
|
|
125
|
+
ctx (AnyContext): The shared context object.
|
|
115
126
|
attr (IntAttr | None): The integer attribute to retrieve.
|
|
116
127
|
default (IntAttr): The default value if the attribute is None.
|
|
117
128
|
auto_render (bool): Whether to auto-render the attribute value if it's a string.
|
|
@@ -119,14 +130,16 @@ def get_int_attr(
|
|
|
119
130
|
Returns:
|
|
120
131
|
int: The integer attribute value.
|
|
121
132
|
"""
|
|
122
|
-
val = get_attr(
|
|
123
|
-
if isinstance(val,
|
|
124
|
-
return
|
|
125
|
-
|
|
133
|
+
val = get_attr(ctx, attr, default, auto_render)
|
|
134
|
+
if isinstance(val, int):
|
|
135
|
+
return val
|
|
136
|
+
if val is None:
|
|
137
|
+
return 0
|
|
138
|
+
return int(val)
|
|
126
139
|
|
|
127
140
|
|
|
128
141
|
def get_float_attr(
|
|
129
|
-
|
|
142
|
+
ctx: AnyContext | AnySharedContext,
|
|
130
143
|
attr: FloatAttr | None,
|
|
131
144
|
default: FloatAttr = 0.0,
|
|
132
145
|
auto_render: bool = True,
|
|
@@ -135,7 +148,7 @@ def get_float_attr(
|
|
|
135
148
|
Retrieve a float from shared context attributes.
|
|
136
149
|
|
|
137
150
|
Args:
|
|
138
|
-
|
|
151
|
+
ctx (AnyContext): The shared context object.
|
|
139
152
|
attr (FloatAttr | None): The float attribute to retrieve.
|
|
140
153
|
default (FloatAttr): The default value if the attribute is None.
|
|
141
154
|
auto_render (bool): Whether to auto-render the attribute value if it's a string.
|
|
@@ -143,14 +156,16 @@ def get_float_attr(
|
|
|
143
156
|
Returns:
|
|
144
157
|
float | None: The float attribute value.
|
|
145
158
|
"""
|
|
146
|
-
val = get_attr(
|
|
147
|
-
if isinstance(val,
|
|
148
|
-
return
|
|
149
|
-
|
|
159
|
+
val = get_attr(ctx, attr, default, auto_render)
|
|
160
|
+
if isinstance(val, (int, float)):
|
|
161
|
+
return val
|
|
162
|
+
if val is None:
|
|
163
|
+
return 0.0
|
|
164
|
+
return float(val)
|
|
150
165
|
|
|
151
166
|
|
|
152
167
|
def get_attr(
|
|
153
|
-
|
|
168
|
+
ctx: AnyContext | AnySharedContext,
|
|
154
169
|
attr: AnyAttr,
|
|
155
170
|
default: AnyAttr,
|
|
156
171
|
auto_render: bool = True,
|
|
@@ -159,7 +174,7 @@ def get_attr(
|
|
|
159
174
|
Retrieve an attribute value from shared context, handling callables and rendering.
|
|
160
175
|
|
|
161
176
|
Args:
|
|
162
|
-
|
|
177
|
+
ctx (AnyContext): The shared context object.
|
|
163
178
|
attr (AnyAttr): The attribute to retrieve. Can be a value, a callable,
|
|
164
179
|
or a string to render.
|
|
165
180
|
default (AnyAttr): The default value if the attribute is None.
|
|
@@ -170,10 +185,10 @@ def get_attr(
|
|
|
170
185
|
"""
|
|
171
186
|
if attr is None:
|
|
172
187
|
if callable(default):
|
|
173
|
-
return default(
|
|
188
|
+
return default(ctx)
|
|
174
189
|
return default
|
|
175
190
|
if callable(attr):
|
|
176
|
-
return attr(
|
|
191
|
+
return attr(ctx)
|
|
177
192
|
if isinstance(attr, str) and auto_render:
|
|
178
|
-
return
|
|
193
|
+
return ctx.render(attr)
|
|
179
194
|
return attr
|
zrb/util/cli/markdown.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
def render_markdown(markdown_text: str) -> str:
|
|
2
|
+
"""
|
|
3
|
+
Renders Markdown to a string, ensuring link URLs are visible.
|
|
4
|
+
"""
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.markdown import Markdown
|
|
7
|
+
|
|
8
|
+
console = Console()
|
|
9
|
+
markdown = Markdown(markdown_text, hyperlinks=False)
|
|
10
|
+
with console.capture() as capture:
|
|
11
|
+
console.print(markdown)
|
|
12
|
+
return capture.get()
|
zrb/util/cli/text.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import tempfile
|
|
4
|
+
|
|
5
|
+
from zrb.util.file import read_file
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def edit_text(
|
|
9
|
+
prompt_message: str,
|
|
10
|
+
value: str,
|
|
11
|
+
editor: str = "vi",
|
|
12
|
+
extension: str = ".txt",
|
|
13
|
+
) -> str:
|
|
14
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=extension) as temp_file:
|
|
15
|
+
temp_file_name = temp_file.name
|
|
16
|
+
if prompt_message.strip() != "":
|
|
17
|
+
prompt_message_eol = f"{prompt_message}\n"
|
|
18
|
+
temp_file.write(prompt_message_eol.encode())
|
|
19
|
+
# Pre-fill with default content
|
|
20
|
+
if value:
|
|
21
|
+
temp_file.write(value.encode())
|
|
22
|
+
temp_file.flush()
|
|
23
|
+
subprocess.call([editor, temp_file_name])
|
|
24
|
+
# Read the edited content
|
|
25
|
+
edited_content = read_file(temp_file_name)
|
|
26
|
+
if prompt_message.strip() != "":
|
|
27
|
+
parts = [text.strip() for text in edited_content.split(prompt_message, 1)]
|
|
28
|
+
edited_content = "\n".join(parts).lstrip()
|
|
29
|
+
os.remove(temp_file_name)
|
|
30
|
+
return edited_content
|
zrb/util/file.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import re
|
|
3
|
+
from typing import Literal
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
def read_file(file_path: str, replace_map: dict[str, str] = {}) -> str:
|
|
@@ -14,14 +15,21 @@ def read_file(file_path: str, replace_map: dict[str, str] = {}) -> str:
|
|
|
14
15
|
"""
|
|
15
16
|
abs_file_path = os.path.abspath(os.path.expanduser(file_path))
|
|
16
17
|
is_pdf = abs_file_path.lower().endswith(".pdf")
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
try:
|
|
19
|
+
content = (
|
|
20
|
+
_read_pdf_file_content(abs_file_path)
|
|
21
|
+
if is_pdf
|
|
22
|
+
else _read_text_file_content(abs_file_path)
|
|
23
|
+
)
|
|
24
|
+
for key, val in replace_map.items():
|
|
25
|
+
content = content.replace(key, val)
|
|
26
|
+
return content
|
|
27
|
+
except Exception:
|
|
28
|
+
import base64
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
data = Path(abs_file_path).read_bytes()
|
|
32
|
+
return base64.b64encode(data).decode("ascii")
|
|
25
33
|
|
|
26
34
|
|
|
27
35
|
def _read_text_file_content(file_path: str) -> str:
|
|
@@ -54,6 +62,8 @@ def read_file_with_line_numbers(
|
|
|
54
62
|
The content of the file with line numbers and replacements applied.
|
|
55
63
|
"""
|
|
56
64
|
content = read_file(file_path, replace_map)
|
|
65
|
+
if not content:
|
|
66
|
+
return ""
|
|
57
67
|
lines = content.splitlines()
|
|
58
68
|
numbered_lines = [f"{i + 1} | {line}" for i, line in enumerate(lines)]
|
|
59
69
|
return "\n".join(numbered_lines)
|
|
@@ -71,16 +81,22 @@ def read_dir(dir_path: str) -> list[str]:
|
|
|
71
81
|
return [f for f in os.listdir(os.path.abspath(os.path.expanduser(dir_path)))]
|
|
72
82
|
|
|
73
83
|
|
|
74
|
-
def write_file(
|
|
84
|
+
def write_file(
|
|
85
|
+
file_path: str,
|
|
86
|
+
content: str | list[str],
|
|
87
|
+
mode: Literal["w", "wt", "tw", "a", "at", "ta", "x", "xt", "tx"] = "w",
|
|
88
|
+
):
|
|
75
89
|
"""Writes content to a file.
|
|
76
90
|
|
|
77
91
|
Args:
|
|
78
92
|
file_path: The path to the file.
|
|
79
93
|
content: The content to write, either a string or a list of strings.
|
|
94
|
+
mode: Writing mode (by default "w")
|
|
80
95
|
"""
|
|
81
96
|
if isinstance(content, list):
|
|
82
97
|
content = "\n".join([line for line in content if line is not None])
|
|
83
|
-
|
|
98
|
+
abs_file_path = os.path.abspath(os.path.expanduser(file_path))
|
|
99
|
+
dir_path = os.path.dirname(abs_file_path)
|
|
84
100
|
os.makedirs(dir_path, exist_ok=True)
|
|
85
101
|
should_add_eol = content.endswith("\n")
|
|
86
102
|
# Remove trailing newlines, but keep one if the file originally ended up with newline
|
|
@@ -88,5 +104,5 @@ def write_file(file_path: str, content: str | list[str]):
|
|
|
88
104
|
content = content.rstrip("\n")
|
|
89
105
|
if should_add_eol:
|
|
90
106
|
content += "\n"
|
|
91
|
-
with open(
|
|
107
|
+
with open(abs_file_path, mode) as f:
|
|
92
108
|
f.write(content)
|
zrb/util/git.py
CHANGED
|
@@ -131,7 +131,7 @@ async def get_branches(
|
|
|
131
131
|
Exception: If the git command returns a non-zero exit code.
|
|
132
132
|
"""
|
|
133
133
|
cmd_result, exit_code = await run_command(
|
|
134
|
-
cmd=["git", "
|
|
134
|
+
cmd=["git", "branch"],
|
|
135
135
|
cwd=repo_dir,
|
|
136
136
|
print_method=print_method,
|
|
137
137
|
)
|
|
@@ -160,7 +160,7 @@ async def delete_branch(
|
|
|
160
160
|
Exception: If the git command returns a non-zero exit code.
|
|
161
161
|
"""
|
|
162
162
|
cmd_result, exit_code = await run_command(
|
|
163
|
-
cmd=["git", "branch", "-
|
|
163
|
+
cmd=["git", "branch", "-d", branch_name],
|
|
164
164
|
cwd=repo_dir,
|
|
165
165
|
print_method=print_method,
|
|
166
166
|
)
|
|
@@ -8,7 +8,6 @@ def _adjust_markdown_headers(md: str, level_change: int) -> str:
|
|
|
8
8
|
for line in lines:
|
|
9
9
|
stripped_line = line.strip()
|
|
10
10
|
fence_match = re.match(r"^([`~]{3,})", stripped_line)
|
|
11
|
-
|
|
12
11
|
if fence_match:
|
|
13
12
|
current_fence = fence_match.group(1)
|
|
14
13
|
if (
|
|
@@ -31,7 +30,7 @@ def _adjust_markdown_headers(md: str, level_change: int) -> str:
|
|
|
31
30
|
new_lines.append(new_header)
|
|
32
31
|
else:
|
|
33
32
|
new_lines.append(line)
|
|
34
|
-
return "\n".join(new_lines)
|
|
33
|
+
return "\n".join(new_lines).rstrip()
|
|
35
34
|
|
|
36
35
|
|
|
37
36
|
def demote_markdown_headers(md: str) -> str:
|
|
@@ -42,7 +41,7 @@ def promote_markdown_headers(md: str) -> str:
|
|
|
42
41
|
return _adjust_markdown_headers(md, level_change=-1)
|
|
43
42
|
|
|
44
43
|
|
|
45
|
-
def
|
|
44
|
+
def make_markdown_section(header: str, content: str, as_code: bool = False) -> str:
|
|
46
45
|
if content.strip() == "":
|
|
47
46
|
return ""
|
|
48
47
|
if as_code:
|
zrb/util/string/conversion.py
CHANGED
zrb/util/truncate.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from collections.abc import Mapping, Sequence
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def truncate_str(value: Any, limit: int):
|
|
6
|
+
# If value is a string, truncate
|
|
7
|
+
if isinstance(value, str):
|
|
8
|
+
if len(value) > limit:
|
|
9
|
+
if limit < 4:
|
|
10
|
+
return value[:limit]
|
|
11
|
+
return value[: limit - 4] + " ..."
|
|
12
|
+
# If value is a dict, process recursively
|
|
13
|
+
elif isinstance(value, Mapping):
|
|
14
|
+
return {k: truncate_str(v, limit) for k, v in value.items()}
|
|
15
|
+
# If value is a list or tuple, process recursively preserving type
|
|
16
|
+
elif isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
|
|
17
|
+
t = type(value)
|
|
18
|
+
return t(truncate_str(v, limit) for v in value)
|
|
19
|
+
# If value is a set, process recursively preserving type
|
|
20
|
+
elif isinstance(value, set):
|
|
21
|
+
return {truncate_str(v, limit) for v in value}
|
|
22
|
+
# Other types are returned unchanged
|
|
23
|
+
return value
|
zrb/util/yaml.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def yaml_dump(obj: Any, key: str = "") -> str:
|
|
5
|
+
"""
|
|
6
|
+
Convert any Python object to a YAML string representation.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
obj: Any Python object to convert to YAML
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
str: YAML string representation of the object
|
|
13
|
+
|
|
14
|
+
Rules:
|
|
15
|
+
- Any non-first level multiline string should be rendered as block (using `|`)
|
|
16
|
+
- None values are rendered correctly (not omitted)
|
|
17
|
+
- Non-primitive/list/dict/set objects are ignored
|
|
18
|
+
"""
|
|
19
|
+
import yaml
|
|
20
|
+
|
|
21
|
+
# Process the object
|
|
22
|
+
processed_obj = _sanitize_obj(obj)
|
|
23
|
+
if key:
|
|
24
|
+
key_parts = _parse_key(key)
|
|
25
|
+
obj_to_dump = _get_obj_value(processed_obj, key_parts)
|
|
26
|
+
else:
|
|
27
|
+
obj_to_dump = processed_obj
|
|
28
|
+
# Add custom representer for multiline strings
|
|
29
|
+
yaml.add_representer(str, _multiline_string_presenter)
|
|
30
|
+
# Generate YAML
|
|
31
|
+
yaml_str = yaml.dump(
|
|
32
|
+
obj_to_dump,
|
|
33
|
+
default_flow_style=False,
|
|
34
|
+
allow_unicode=True,
|
|
35
|
+
sort_keys=False,
|
|
36
|
+
explicit_end=False,
|
|
37
|
+
width=float("inf"),
|
|
38
|
+
)
|
|
39
|
+
if not isinstance(obj_to_dump, (dict, list)):
|
|
40
|
+
# PyYAML appends '...\n' (document-end) for top-level scalars.
|
|
41
|
+
# So, we remove it.
|
|
42
|
+
if yaml_str.endswith("...\n"):
|
|
43
|
+
yaml_str = yaml_str[:-4]
|
|
44
|
+
return yaml_str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def edit_obj(obj: Any, key: str, val: str) -> Any:
|
|
48
|
+
"""
|
|
49
|
+
Edit a property or subproperty of an object using YAML syntax.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
obj: The object to edit
|
|
53
|
+
key: The key to edit, can be nested with '.' as separator
|
|
54
|
+
val: The string value to set, will be parsed as YAML
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Any: The modified object
|
|
58
|
+
|
|
59
|
+
Example:
|
|
60
|
+
edit({"a": {"b": 1}}, "a.b", "2") -> {"a": {"b": 2}}
|
|
61
|
+
edit({"flag": False}, "flag", "true") -> {"flag": True}
|
|
62
|
+
edit({"a": 1}, "", "2") -> 2 # Replace entire object with scalar
|
|
63
|
+
edit({"a": 1}, "", "b: 2") -> {"a": 1, "b": 2} # Patch dict if obj is dict
|
|
64
|
+
"""
|
|
65
|
+
# Parse the value using YAML rules
|
|
66
|
+
parsed_value = _load_yaml(val)
|
|
67
|
+
|
|
68
|
+
# Handle empty key - replace entire object
|
|
69
|
+
if not key:
|
|
70
|
+
if isinstance(obj, dict) and isinstance(parsed_value, dict):
|
|
71
|
+
# Patch/merge the dict values
|
|
72
|
+
return {**obj, **parsed_value}
|
|
73
|
+
# Replace entire object with parsed value
|
|
74
|
+
return parsed_value
|
|
75
|
+
|
|
76
|
+
# Split the key by dots
|
|
77
|
+
key_parts = _parse_key(key)
|
|
78
|
+
# Set the nested value
|
|
79
|
+
return _set_obj_value(obj, key_parts, parsed_value)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _sanitize_obj(obj: Any) -> Any:
|
|
83
|
+
"""Process a value for YAML conversion."""
|
|
84
|
+
if obj is None:
|
|
85
|
+
return None
|
|
86
|
+
elif isinstance(obj, (int, float, bool, str)):
|
|
87
|
+
return obj
|
|
88
|
+
elif isinstance(obj, (list, tuple)):
|
|
89
|
+
return [_sanitize_obj(item) for item in obj if not _is_complex_obj(item)]
|
|
90
|
+
elif isinstance(obj, dict):
|
|
91
|
+
return {k: _sanitize_obj(v) for k, v in obj.items() if not _is_complex_obj(v)}
|
|
92
|
+
elif isinstance(obj, set):
|
|
93
|
+
return [
|
|
94
|
+
_sanitize_obj(item) for item in sorted(obj) if not _is_complex_obj(item)
|
|
95
|
+
]
|
|
96
|
+
else:
|
|
97
|
+
# Ignore non-primitive/list/dict/set objects
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _is_complex_obj(obj: Any) -> bool:
|
|
102
|
+
return obj is not None and not isinstance(
|
|
103
|
+
obj, (int, float, bool, str, list, tuple, dict, set)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _multiline_string_presenter(dumper, data):
|
|
108
|
+
"""Custom representer for multiline strings."""
|
|
109
|
+
if "\n" in data:
|
|
110
|
+
# Clean up the string for block style
|
|
111
|
+
lines = [line.rstrip() for line in data.splitlines()]
|
|
112
|
+
clean_data = "\n".join(lines)
|
|
113
|
+
return dumper.represent_scalar("tag:yaml.org,2002:str", clean_data, style="|")
|
|
114
|
+
return dumper.represent_scalar("tag:yaml.org,2002:str", data)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _parse_key(key: str) -> list[str]:
|
|
118
|
+
return key.split(".")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _load_yaml(value_str: str) -> Any:
|
|
122
|
+
"""Parse a string value using YAML rules."""
|
|
123
|
+
import yaml
|
|
124
|
+
|
|
125
|
+
# Handle empty string explicitly
|
|
126
|
+
if value_str == "":
|
|
127
|
+
return ""
|
|
128
|
+
try:
|
|
129
|
+
# Use yaml.safe_load to parse the value
|
|
130
|
+
parsed = yaml.safe_load(value_str)
|
|
131
|
+
return parsed
|
|
132
|
+
except yaml.YAMLError:
|
|
133
|
+
# If YAML parsing fails, treat as string
|
|
134
|
+
return value_str
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _set_obj_value(obj: Any, keys: list[str], value: Any) -> Any:
|
|
138
|
+
"""Set a value in a nested structure."""
|
|
139
|
+
if not keys:
|
|
140
|
+
return value
|
|
141
|
+
current_key = keys[0]
|
|
142
|
+
remaining_keys = keys[1:]
|
|
143
|
+
if isinstance(obj, dict):
|
|
144
|
+
# Handle dictionary
|
|
145
|
+
if remaining_keys:
|
|
146
|
+
# There are more keys to traverse
|
|
147
|
+
if current_key not in obj:
|
|
148
|
+
obj[current_key] = {}
|
|
149
|
+
obj[current_key] = _set_obj_value(obj[current_key], remaining_keys, value)
|
|
150
|
+
else:
|
|
151
|
+
# This is the final key
|
|
152
|
+
obj[current_key] = value
|
|
153
|
+
return obj
|
|
154
|
+
elif isinstance(obj, list):
|
|
155
|
+
# Handle list - convert key to index
|
|
156
|
+
try:
|
|
157
|
+
index = int(current_key)
|
|
158
|
+
if 0 <= index < len(obj):
|
|
159
|
+
if remaining_keys:
|
|
160
|
+
obj[index] = _set_obj_value(obj[index], remaining_keys, value)
|
|
161
|
+
else:
|
|
162
|
+
obj[index] = value
|
|
163
|
+
else:
|
|
164
|
+
raise IndexError(
|
|
165
|
+
f"Index {index} out of range for list of length {len(obj)}"
|
|
166
|
+
)
|
|
167
|
+
except ValueError:
|
|
168
|
+
raise KeyError(f"Cannot use non-integer key '{current_key}' with list")
|
|
169
|
+
return obj
|
|
170
|
+
else:
|
|
171
|
+
# Handle other types by converting to dict
|
|
172
|
+
if remaining_keys:
|
|
173
|
+
# Create nested structure
|
|
174
|
+
new_obj = {current_key: _set_obj_value({}, remaining_keys, value)}
|
|
175
|
+
return new_obj
|
|
176
|
+
else:
|
|
177
|
+
# Replace the entire object
|
|
178
|
+
return {current_key: value}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _get_obj_value(obj: Any, keys: list[str]) -> Any:
|
|
182
|
+
"""
|
|
183
|
+
Get a value from a nested structure using a list of keys.
|
|
184
|
+
Returns None if the key path does not exist.
|
|
185
|
+
"""
|
|
186
|
+
current_val = obj
|
|
187
|
+
for key in keys:
|
|
188
|
+
if isinstance(current_val, dict):
|
|
189
|
+
if key in current_val:
|
|
190
|
+
current_val = current_val[key]
|
|
191
|
+
else:
|
|
192
|
+
return None
|
|
193
|
+
elif isinstance(current_val, list):
|
|
194
|
+
try:
|
|
195
|
+
index = int(key)
|
|
196
|
+
if 0 <= index < len(current_val):
|
|
197
|
+
current_val = current_val[index]
|
|
198
|
+
else:
|
|
199
|
+
return None
|
|
200
|
+
except (ValueError, TypeError):
|
|
201
|
+
return None
|
|
202
|
+
else:
|
|
203
|
+
return None
|
|
204
|
+
return current_val
|
zrb/xcom/xcom.py
CHANGED
|
@@ -34,6 +34,16 @@ class Xcom(deque):
|
|
|
34
34
|
else:
|
|
35
35
|
raise IndexError("Xcom is empty")
|
|
36
36
|
|
|
37
|
+
def get(self, default_value: Any = None) -> Any:
|
|
38
|
+
if len(self) > 0:
|
|
39
|
+
return self[0]
|
|
40
|
+
return default_value
|
|
41
|
+
|
|
42
|
+
def set(self, new_value: Any):
|
|
43
|
+
self.push(new_value)
|
|
44
|
+
while len(self) > 1:
|
|
45
|
+
self.pop()
|
|
46
|
+
|
|
37
47
|
def add_push_callback(self, callback: Callable[[], Any]):
|
|
38
48
|
if not hasattr(self, "push_callbacks"):
|
|
39
49
|
self.push_callbacks: list[Callable[[], Any]] = []
|