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.

Files changed (147) hide show
  1. zrb/__init__.py +126 -113
  2. zrb/__main__.py +1 -1
  3. zrb/attr/type.py +10 -7
  4. zrb/builtin/__init__.py +2 -50
  5. zrb/builtin/git.py +12 -1
  6. zrb/builtin/group.py +31 -15
  7. zrb/builtin/http.py +7 -8
  8. zrb/builtin/llm/attachment.py +40 -0
  9. zrb/builtin/llm/chat_completion.py +274 -0
  10. zrb/builtin/llm/chat_session.py +152 -85
  11. zrb/builtin/llm/chat_session_cmd.py +288 -0
  12. zrb/builtin/llm/chat_trigger.py +79 -0
  13. zrb/builtin/llm/history.py +7 -9
  14. zrb/builtin/llm/llm_ask.py +221 -98
  15. zrb/builtin/llm/tool/api.py +74 -52
  16. zrb/builtin/llm/tool/cli.py +46 -17
  17. zrb/builtin/llm/tool/code.py +71 -90
  18. zrb/builtin/llm/tool/file.py +301 -241
  19. zrb/builtin/llm/tool/note.py +84 -0
  20. zrb/builtin/llm/tool/rag.py +38 -8
  21. zrb/builtin/llm/tool/sub_agent.py +67 -50
  22. zrb/builtin/llm/tool/web.py +146 -122
  23. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +7 -7
  24. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +5 -5
  25. zrb/builtin/project/add/fastapp/fastapp_util.py +1 -1
  26. zrb/builtin/searxng/config/settings.yml +5671 -0
  27. zrb/builtin/searxng/start.py +21 -0
  28. zrb/builtin/setup/latex/ubuntu.py +1 -0
  29. zrb/builtin/setup/ubuntu.py +1 -1
  30. zrb/builtin/shell/autocomplete/bash.py +4 -3
  31. zrb/builtin/shell/autocomplete/zsh.py +4 -3
  32. zrb/builtin/todo.py +13 -2
  33. zrb/config/config.py +614 -0
  34. zrb/config/default_prompt/file_extractor_system_prompt.md +112 -0
  35. zrb/config/default_prompt/interactive_system_prompt.md +29 -0
  36. zrb/config/default_prompt/persona.md +1 -0
  37. zrb/config/default_prompt/repo_extractor_system_prompt.md +112 -0
  38. zrb/config/default_prompt/repo_summarizer_system_prompt.md +29 -0
  39. zrb/config/default_prompt/summarization_prompt.md +57 -0
  40. zrb/config/default_prompt/system_prompt.md +38 -0
  41. zrb/config/llm_config.py +339 -0
  42. zrb/config/llm_context/config.py +166 -0
  43. zrb/config/llm_context/config_parser.py +40 -0
  44. zrb/config/llm_context/workflow.py +81 -0
  45. zrb/config/llm_rate_limitter.py +190 -0
  46. zrb/{runner → config}/web_auth_config.py +17 -22
  47. zrb/context/any_shared_context.py +17 -1
  48. zrb/context/context.py +16 -2
  49. zrb/context/shared_context.py +18 -8
  50. zrb/group/any_group.py +12 -5
  51. zrb/group/group.py +67 -3
  52. zrb/input/any_input.py +5 -1
  53. zrb/input/base_input.py +18 -6
  54. zrb/input/option_input.py +13 -1
  55. zrb/input/text_input.py +8 -25
  56. zrb/runner/cli.py +25 -23
  57. zrb/runner/common_util.py +24 -19
  58. zrb/runner/web_app.py +3 -3
  59. zrb/runner/web_route/docs_route.py +1 -1
  60. zrb/runner/web_route/error_page/serve_default_404.py +1 -1
  61. zrb/runner/web_route/error_page/show_error_page.py +1 -1
  62. zrb/runner/web_route/home_page/home_page_route.py +2 -2
  63. zrb/runner/web_route/login_api_route.py +1 -1
  64. zrb/runner/web_route/login_page/login_page_route.py +2 -2
  65. zrb/runner/web_route/logout_api_route.py +1 -1
  66. zrb/runner/web_route/logout_page/logout_page_route.py +2 -2
  67. zrb/runner/web_route/node_page/group/show_group_page.py +1 -1
  68. zrb/runner/web_route/node_page/node_page_route.py +1 -1
  69. zrb/runner/web_route/node_page/task/show_task_page.py +1 -1
  70. zrb/runner/web_route/refresh_token_api_route.py +1 -1
  71. zrb/runner/web_route/static/static_route.py +1 -1
  72. zrb/runner/web_route/task_input_api_route.py +6 -6
  73. zrb/runner/web_route/task_session_api_route.py +20 -12
  74. zrb/runner/web_util/cookie.py +1 -1
  75. zrb/runner/web_util/token.py +1 -1
  76. zrb/runner/web_util/user.py +8 -4
  77. zrb/session/any_session.py +24 -17
  78. zrb/session/session.py +50 -25
  79. zrb/session_state_logger/any_session_state_logger.py +9 -4
  80. zrb/session_state_logger/file_session_state_logger.py +16 -6
  81. zrb/session_state_logger/session_state_logger_factory.py +1 -1
  82. zrb/task/any_task.py +30 -9
  83. zrb/task/base/context.py +17 -9
  84. zrb/task/base/execution.py +15 -8
  85. zrb/task/base/lifecycle.py +8 -4
  86. zrb/task/base/monitoring.py +12 -7
  87. zrb/task/base_task.py +69 -5
  88. zrb/task/base_trigger.py +12 -5
  89. zrb/task/cmd_task.py +1 -1
  90. zrb/task/llm/agent.py +154 -161
  91. zrb/task/llm/agent_runner.py +152 -0
  92. zrb/task/llm/config.py +47 -18
  93. zrb/task/llm/conversation_history.py +209 -0
  94. zrb/task/llm/conversation_history_model.py +67 -0
  95. zrb/task/llm/default_workflow/coding/workflow.md +41 -0
  96. zrb/task/llm/default_workflow/copywriting/workflow.md +68 -0
  97. zrb/task/llm/default_workflow/git/workflow.md +118 -0
  98. zrb/task/llm/default_workflow/golang/workflow.md +128 -0
  99. zrb/task/llm/default_workflow/html-css/workflow.md +135 -0
  100. zrb/task/llm/default_workflow/java/workflow.md +146 -0
  101. zrb/task/llm/default_workflow/javascript/workflow.md +158 -0
  102. zrb/task/llm/default_workflow/python/workflow.md +160 -0
  103. zrb/task/llm/default_workflow/researching/workflow.md +153 -0
  104. zrb/task/llm/default_workflow/rust/workflow.md +162 -0
  105. zrb/task/llm/default_workflow/shell/workflow.md +299 -0
  106. zrb/task/llm/error.py +24 -10
  107. zrb/task/llm/file_replacement.py +206 -0
  108. zrb/task/llm/file_tool_model.py +57 -0
  109. zrb/task/llm/history_processor.py +206 -0
  110. zrb/task/llm/history_summarization.py +11 -166
  111. zrb/task/llm/print_node.py +193 -69
  112. zrb/task/llm/prompt.py +242 -45
  113. zrb/task/llm/subagent_conversation_history.py +41 -0
  114. zrb/task/llm/tool_wrapper.py +260 -57
  115. zrb/task/llm/workflow.py +76 -0
  116. zrb/task/llm_task.py +182 -171
  117. zrb/task/make_task.py +2 -3
  118. zrb/task/rsync_task.py +26 -11
  119. zrb/task/scheduler.py +4 -4
  120. zrb/util/attr.py +54 -39
  121. zrb/util/callable.py +23 -0
  122. zrb/util/cli/markdown.py +12 -0
  123. zrb/util/cli/text.py +30 -0
  124. zrb/util/file.py +29 -11
  125. zrb/util/git.py +8 -11
  126. zrb/util/git_diff_model.py +10 -0
  127. zrb/util/git_subtree.py +9 -14
  128. zrb/util/git_subtree_model.py +32 -0
  129. zrb/util/init_path.py +1 -1
  130. zrb/util/markdown.py +62 -0
  131. zrb/util/string/conversion.py +2 -2
  132. zrb/util/todo.py +17 -50
  133. zrb/util/todo_model.py +46 -0
  134. zrb/util/truncate.py +23 -0
  135. zrb/util/yaml.py +204 -0
  136. zrb/xcom/xcom.py +10 -0
  137. zrb-1.21.29.dist-info/METADATA +270 -0
  138. {zrb-1.8.10.dist-info → zrb-1.21.29.dist-info}/RECORD +140 -98
  139. {zrb-1.8.10.dist-info → zrb-1.21.29.dist-info}/WHEEL +1 -1
  140. zrb/config.py +0 -335
  141. zrb/llm_config.py +0 -411
  142. zrb/llm_rate_limitter.py +0 -125
  143. zrb/task/llm/context.py +0 -102
  144. zrb/task/llm/context_enrichment.py +0 -199
  145. zrb/task/llm/history.py +0 -211
  146. zrb-1.8.10.dist-info/METADATA +0 -264
  147. {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]] = []