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
zrb/util/todo.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
import re
|
|
3
3
|
import shutil
|
|
4
|
-
|
|
5
|
-
from pydantic import BaseModel, Field, model_validator
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
6
5
|
|
|
7
6
|
from zrb.util.cli.style import (
|
|
8
7
|
stylize_bold_yellow,
|
|
@@ -14,6 +13,9 @@ from zrb.util.cli.style import (
|
|
|
14
13
|
from zrb.util.file import read_file, write_file
|
|
15
14
|
from zrb.util.string.name import get_random_name
|
|
16
15
|
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from zrb.util.todo_model import TodoTaskModel
|
|
18
|
+
|
|
17
19
|
_DATE_TIME_STR_WIDTH = 14
|
|
18
20
|
_MAX_DESCRIPTION_WIDTH = 70
|
|
19
21
|
_PRIORITY_WIDTH = 3
|
|
@@ -23,43 +25,6 @@ _CREATED_AT_WIDTH = _DATE_TIME_STR_WIDTH
|
|
|
23
25
|
_GAP_WIDTH = 2
|
|
24
26
|
|
|
25
27
|
|
|
26
|
-
class TodoTaskModel(BaseModel):
|
|
27
|
-
priority: str | None = Field("D", pattern=r"^[A-Z]$") # Priority like A, B, ...
|
|
28
|
-
completed: bool = False # True if completed, False otherwise
|
|
29
|
-
description: str # Main task description
|
|
30
|
-
projects: list[str] = [] # List of projects (e.g., +Project)
|
|
31
|
-
contexts: list[str] = [] # List of contexts (e.g., @Context)
|
|
32
|
-
keyval: dict[str, str] = {} # Special key (e.g., due:2016-05-30)
|
|
33
|
-
creation_date: datetime.date | None = None # Creation date
|
|
34
|
-
completion_date: datetime.date | None = None # Completion date
|
|
35
|
-
|
|
36
|
-
@model_validator(mode="before")
|
|
37
|
-
def validate_dates(cls, values):
|
|
38
|
-
completion_date = values.get("completion_date")
|
|
39
|
-
creation_date = values.get("creation_date")
|
|
40
|
-
if completion_date and not creation_date:
|
|
41
|
-
raise ValueError(
|
|
42
|
-
"creation_date must be specified if completion_date is set."
|
|
43
|
-
)
|
|
44
|
-
return values
|
|
45
|
-
|
|
46
|
-
def get_additional_info_length(self):
|
|
47
|
-
"""
|
|
48
|
-
Calculate the length of the additional information string (projects, contexts, keyval).
|
|
49
|
-
|
|
50
|
-
Returns:
|
|
51
|
-
int: The length of the combined additional information string.
|
|
52
|
-
"""
|
|
53
|
-
results = []
|
|
54
|
-
for project in self.projects:
|
|
55
|
-
results.append(f"@{project}")
|
|
56
|
-
for context in self.contexts:
|
|
57
|
-
results.append(f"+{context}")
|
|
58
|
-
for key, val in self.keyval.items():
|
|
59
|
-
results.append(f"{key}:{val}")
|
|
60
|
-
return len(", ".join(results))
|
|
61
|
-
|
|
62
|
-
|
|
63
28
|
TODO_TXT_PATTERN = re.compile(
|
|
64
29
|
r"^(?P<status>x)?\s*" # Optional completion mark ('x')
|
|
65
30
|
r"(?:\((?P<priority>[A-Z])\)\s+)?" # Optional priority (e.g., '(A)')
|
|
@@ -69,7 +34,7 @@ TODO_TXT_PATTERN = re.compile(
|
|
|
69
34
|
)
|
|
70
35
|
|
|
71
36
|
|
|
72
|
-
def cascade_todo_task(todo_task: TodoTaskModel):
|
|
37
|
+
def cascade_todo_task(todo_task: "TodoTaskModel"):
|
|
73
38
|
"""
|
|
74
39
|
Populate default values for a TodoTaskModel if they are missing.
|
|
75
40
|
|
|
@@ -87,8 +52,8 @@ def cascade_todo_task(todo_task: TodoTaskModel):
|
|
|
87
52
|
|
|
88
53
|
|
|
89
54
|
def select_todo_task(
|
|
90
|
-
todo_list: list[TodoTaskModel], keyword: str
|
|
91
|
-
) -> TodoTaskModel | None:
|
|
55
|
+
todo_list: list["TodoTaskModel"], keyword: str
|
|
56
|
+
) -> "TodoTaskModel | None":
|
|
92
57
|
"""
|
|
93
58
|
Select a todo task from a list based on a keyword matching ID or description.
|
|
94
59
|
|
|
@@ -118,7 +83,7 @@ def select_todo_task(
|
|
|
118
83
|
return None
|
|
119
84
|
|
|
120
85
|
|
|
121
|
-
def load_todo_list(todo_file_path: str) -> list[TodoTaskModel]:
|
|
86
|
+
def load_todo_list(todo_file_path: str) -> list["TodoTaskModel"]:
|
|
122
87
|
"""
|
|
123
88
|
Load a list of todo tasks from a todo.txt file.
|
|
124
89
|
|
|
@@ -129,7 +94,7 @@ def load_todo_list(todo_file_path: str) -> list[TodoTaskModel]:
|
|
|
129
94
|
list[TodoTaskModel]: A sorted list of todo tasks.
|
|
130
95
|
"""
|
|
131
96
|
todo_lines = read_file(todo_file_path).strip().split("\n")
|
|
132
|
-
todo_list: list[TodoTaskModel] = []
|
|
97
|
+
todo_list: list["TodoTaskModel"] = []
|
|
133
98
|
for todo_line in todo_lines:
|
|
134
99
|
todo_line = todo_line.strip()
|
|
135
100
|
if todo_line == "":
|
|
@@ -147,7 +112,7 @@ def load_todo_list(todo_file_path: str) -> list[TodoTaskModel]:
|
|
|
147
112
|
return todo_list
|
|
148
113
|
|
|
149
114
|
|
|
150
|
-
def save_todo_list(todo_file_path: str, todo_list: list[TodoTaskModel]):
|
|
115
|
+
def save_todo_list(todo_file_path: str, todo_list: list["TodoTaskModel"]):
|
|
151
116
|
"""
|
|
152
117
|
Save a list of todo tasks to a todo.txt file.
|
|
153
118
|
|
|
@@ -160,8 +125,10 @@ def save_todo_list(todo_file_path: str, todo_list: list[TodoTaskModel]):
|
|
|
160
125
|
)
|
|
161
126
|
|
|
162
127
|
|
|
163
|
-
def line_to_todo_task(line: str) -> TodoTaskModel:
|
|
128
|
+
def line_to_todo_task(line: str) -> "TodoTaskModel":
|
|
164
129
|
"""Parses a single todo.txt line into a TodoTask model."""
|
|
130
|
+
from zrb.util.todo_model import TodoTaskModel
|
|
131
|
+
|
|
165
132
|
match = TODO_TXT_PATTERN.match(line)
|
|
166
133
|
if not match:
|
|
167
134
|
raise ValueError(f"Invalid todo.txt line: {line}")
|
|
@@ -214,7 +181,7 @@ def _parse_date(date_str: str | None) -> datetime.date | None:
|
|
|
214
181
|
return None
|
|
215
182
|
|
|
216
183
|
|
|
217
|
-
def todo_task_to_line(task: TodoTaskModel) -> str:
|
|
184
|
+
def todo_task_to_line(task: "TodoTaskModel") -> str:
|
|
218
185
|
"""
|
|
219
186
|
Converts a TodoTask instance back into a todo.txt formatted line.
|
|
220
187
|
|
|
@@ -251,7 +218,7 @@ def todo_task_to_line(task: TodoTaskModel) -> str:
|
|
|
251
218
|
return " ".join(parts)
|
|
252
219
|
|
|
253
220
|
|
|
254
|
-
def get_visual_todo_list(todo_list: list[TodoTaskModel], filter: str) -> str:
|
|
221
|
+
def get_visual_todo_list(todo_list: list["TodoTaskModel"], filter: str) -> str:
|
|
255
222
|
"""
|
|
256
223
|
Generate a visual representation of a filtered todo list.
|
|
257
224
|
|
|
@@ -352,7 +319,7 @@ def get_visual_todo_line(
|
|
|
352
319
|
terminal_width: int,
|
|
353
320
|
max_desc_length: int,
|
|
354
321
|
max_additional_info_length: int,
|
|
355
|
-
todo_task: TodoTaskModel,
|
|
322
|
+
todo_task: "TodoTaskModel",
|
|
356
323
|
) -> str:
|
|
357
324
|
"""
|
|
358
325
|
Generate a single line string for a todo task in the visual todo list.
|
|
@@ -489,7 +456,7 @@ def _get_minimum_width(field_widths: list[int]) -> int:
|
|
|
489
456
|
|
|
490
457
|
|
|
491
458
|
def get_visual_todo_card(
|
|
492
|
-
todo_task: TodoTaskModel, log_work_list: list[dict[str, str]]
|
|
459
|
+
todo_task: "TodoTaskModel", log_work_list: list[dict[str, str]]
|
|
493
460
|
) -> str:
|
|
494
461
|
"""
|
|
495
462
|
Generate a visual card representation of a todo task with log work.
|
zrb/util/todo_model.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TodoTaskModel:
|
|
6
|
+
def __init__(
|
|
7
|
+
self,
|
|
8
|
+
description: str,
|
|
9
|
+
priority: str | None = "D",
|
|
10
|
+
completed: bool = False,
|
|
11
|
+
projects: list[str] | None = None,
|
|
12
|
+
contexts: list[str] | None = None,
|
|
13
|
+
keyval: dict[str, str] | None = None,
|
|
14
|
+
creation_date: datetime.date | None = None,
|
|
15
|
+
completion_date: datetime.date | None = None,
|
|
16
|
+
):
|
|
17
|
+
if priority is not None and not re.match(r"^[A-Z]$", priority):
|
|
18
|
+
raise ValueError("Invalid priority format")
|
|
19
|
+
if completion_date and not creation_date:
|
|
20
|
+
raise ValueError(
|
|
21
|
+
"creation_date must be specified if completion_date is set."
|
|
22
|
+
)
|
|
23
|
+
self.priority = priority
|
|
24
|
+
self.completed = completed
|
|
25
|
+
self.description = description
|
|
26
|
+
self.projects = projects if projects is not None else []
|
|
27
|
+
self.contexts = contexts if contexts is not None else []
|
|
28
|
+
self.keyval = keyval if keyval is not None else {}
|
|
29
|
+
self.creation_date = creation_date
|
|
30
|
+
self.completion_date = completion_date
|
|
31
|
+
|
|
32
|
+
def get_additional_info_length(self):
|
|
33
|
+
"""
|
|
34
|
+
Calculate the length of the additional information string (projects, contexts, keyval).
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
int: The length of the combined additional information string.
|
|
38
|
+
"""
|
|
39
|
+
results = []
|
|
40
|
+
for project in self.projects:
|
|
41
|
+
results.append(f"@{project}")
|
|
42
|
+
for context in self.contexts:
|
|
43
|
+
results.append(f"+{context}")
|
|
44
|
+
for key, val in self.keyval.items():
|
|
45
|
+
results.append(f"{key}:{val}")
|
|
46
|
+
return len(", ".join(results))
|
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]] = []
|