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/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
- shared_ctx: AnySharedContext, attr: StrListAttr | None, auto_render: bool = True
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
- shared_ctx (AnySharedContext): The shared context object.
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(shared_ctx)
32
- return {get_str_attr(shared_ctx, val, "", auto_render) for val in attr}
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
- shared_ctx: AnySharedContext, attr: StrDictAttr | None, auto_render: bool = True
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
- shared_ctx (AnySharedContext): The shared context object.
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(shared_ctx)
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
- shared_ctx: AnySharedContext,
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
- shared_ctx (AnySharedContext): The shared context object.
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(shared_ctx, attr, default, auto_render)
75
- if not isinstance(val, str):
76
- return str(val)
77
- return val
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
- shared_ctx: AnySharedContext,
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
- shared_ctx (AnySharedContext): The shared context object.
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(shared_ctx, attr, default, auto_render)
99
- if isinstance(val, str):
100
- return to_boolean(val)
101
- return val
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
- shared_ctx: AnySharedContext,
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
- shared_ctx (AnySharedContext): The shared context object.
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(shared_ctx, attr, default, auto_render)
123
- if isinstance(val, str):
124
- return int(val)
125
- return val
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
- shared_ctx: AnySharedContext,
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
- shared_ctx (AnySharedContext): The shared context object.
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(shared_ctx, attr, default, auto_render)
147
- if isinstance(val, str):
148
- return float(val)
149
- return val
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
- shared_ctx: AnySharedContext,
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
- shared_ctx (AnySharedContext): The shared context object.
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(shared_ctx)
188
+ return default(ctx)
174
189
  return default
175
190
  if callable(attr):
176
- return attr(shared_ctx)
191
+ return attr(ctx)
177
192
  if isinstance(attr, str) and auto_render:
178
- return shared_ctx.render(attr)
193
+ return ctx.render(attr)
179
194
  return attr
zrb/util/callable.py ADDED
@@ -0,0 +1,23 @@
1
+ from types import BuiltinMethodType, MethodType
2
+
3
+
4
+ def get_callable_name(obj):
5
+ import functools
6
+ import inspect
7
+
8
+ # 1. Unwrap decorated functions
9
+ obj = inspect.unwrap(obj, stop=lambda f: not hasattr(f, "__wrapped__"))
10
+ # 2. functools.partial – delegate to the wrapped function
11
+ if isinstance(obj, functools.partial):
12
+ return get_callable_name(obj.func)
13
+ # 3. Plain functions, built‑ins, methods
14
+ if hasattr(obj, "__name__"):
15
+ return obj.__name__
16
+ # 4. Bound or unbound methods of a class
17
+ if isinstance(obj, (MethodType, BuiltinMethodType)):
18
+ return obj.__func__.__name__
19
+ # 5. Instances of classes defining __call__
20
+ if callable(obj):
21
+ return type(obj).__name__
22
+ # 6. Fallback
23
+ return repr(obj)
@@ -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
- content = (
18
- _read_pdf_file_content(abs_file_path)
19
- if is_pdf
20
- else _read_text_file_content(abs_file_path)
21
- )
22
- for key, val in replace_map.items():
23
- content = content.replace(key, val)
24
- return content
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:
@@ -32,8 +40,10 @@ def _read_text_file_content(file_path: str) -> str:
32
40
 
33
41
  def _read_pdf_file_content(file_path: str) -> str:
34
42
  import pdfplumber
43
+ from pdfplumber.pdf import PDF
35
44
 
36
45
  with pdfplumber.open(file_path) as pdf:
46
+ pdf: PDF
37
47
  return "\n".join(
38
48
  page.extract_text() for page in pdf.pages if page.extract_text()
39
49
  )
@@ -52,6 +62,8 @@ def read_file_with_line_numbers(
52
62
  The content of the file with line numbers and replacements applied.
53
63
  """
54
64
  content = read_file(file_path, replace_map)
65
+ if not content:
66
+ return ""
55
67
  lines = content.splitlines()
56
68
  numbered_lines = [f"{i + 1} | {line}" for i, line in enumerate(lines)]
57
69
  return "\n".join(numbered_lines)
@@ -69,16 +81,22 @@ def read_dir(dir_path: str) -> list[str]:
69
81
  return [f for f in os.listdir(os.path.abspath(os.path.expanduser(dir_path)))]
70
82
 
71
83
 
72
- def write_file(file_path: str, content: str | list[str]):
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
+ ):
73
89
  """Writes content to a file.
74
90
 
75
91
  Args:
76
92
  file_path: The path to the file.
77
93
  content: The content to write, either a string or a list of strings.
94
+ mode: Writing mode (by default "w")
78
95
  """
79
96
  if isinstance(content, list):
80
97
  content = "\n".join([line for line in content if line is not None])
81
- dir_path = os.path.dirname(file_path)
98
+ abs_file_path = os.path.abspath(os.path.expanduser(file_path))
99
+ dir_path = os.path.dirname(abs_file_path)
82
100
  os.makedirs(dir_path, exist_ok=True)
83
101
  should_add_eol = content.endswith("\n")
84
102
  # Remove trailing newlines, but keep one if the file originally ended up with newline
@@ -86,5 +104,5 @@ def write_file(file_path: str, content: str | list[str]):
86
104
  content = content.rstrip("\n")
87
105
  if should_add_eol:
88
106
  content += "\n"
89
- with open(os.path.abspath(os.path.expanduser(file_path)), "w") as f:
107
+ with open(abs_file_path, mode) as f:
90
108
  f.write(content)
zrb/util/git.py CHANGED
@@ -1,16 +1,11 @@
1
1
  import os
2
2
  from collections.abc import Callable
3
- from typing import Any
4
-
5
- from pydantic import BaseModel
3
+ from typing import TYPE_CHECKING, Any
6
4
 
7
5
  from zrb.util.cmd.command import run_command
8
6
 
9
-
10
- class DiffResult(BaseModel):
11
- created: list[str]
12
- removed: list[str]
13
- updated: list[str]
7
+ if TYPE_CHECKING:
8
+ from zrb.util.git_diff_model import DiffResult
14
9
 
15
10
 
16
11
  async def get_diff(
@@ -18,7 +13,7 @@ async def get_diff(
18
13
  source_commit: str,
19
14
  current_commit: str,
20
15
  print_method: Callable[..., Any] = print,
21
- ) -> DiffResult:
16
+ ) -> "DiffResult":
22
17
  """
23
18
  Get the difference between two commits in a Git repository.
24
19
 
@@ -34,6 +29,8 @@ async def get_diff(
34
29
  Raises:
35
30
  Exception: If the git command returns a non-zero exit code.
36
31
  """
32
+ from zrb.util.git_diff_model import DiffResult
33
+
37
34
  cmd_result, exit_code = await run_command(
38
35
  cmd=["git", "diff", source_commit, current_commit],
39
36
  cwd=repo_dir,
@@ -134,7 +131,7 @@ async def get_branches(
134
131
  Exception: If the git command returns a non-zero exit code.
135
132
  """
136
133
  cmd_result, exit_code = await run_command(
137
- cmd=["git", "rev-parse", "--abbrev-ref", "HEAD"],
134
+ cmd=["git", "branch"],
138
135
  cwd=repo_dir,
139
136
  print_method=print_method,
140
137
  )
@@ -163,7 +160,7 @@ async def delete_branch(
163
160
  Exception: If the git command returns a non-zero exit code.
164
161
  """
165
162
  cmd_result, exit_code = await run_command(
166
- cmd=["git", "branch", "-D", branch_name],
163
+ cmd=["git", "branch", "-d", branch_name],
167
164
  cwd=repo_dir,
168
165
  print_method=print_method,
169
166
  )
@@ -0,0 +1,10 @@
1
+ class DiffResult:
2
+ def __init__(
3
+ self,
4
+ created: list[str] | None = None,
5
+ removed: list[str] | None = None,
6
+ updated: list[str] | None = None,
7
+ ):
8
+ self.created = created if created is not None else []
9
+ self.removed = removed if removed is not None else []
10
+ self.updated = updated if updated is not None else []
zrb/util/git_subtree.py CHANGED
@@ -1,24 +1,15 @@
1
1
  import os
2
2
  from collections.abc import Callable
3
- from typing import Any
4
-
5
- from pydantic import BaseModel
3
+ from typing import TYPE_CHECKING, Any
6
4
 
7
5
  from zrb.util.cmd.command import run_command
8
6
  from zrb.util.file import read_file, write_file
9
7
 
8
+ if TYPE_CHECKING:
9
+ from zrb.util.git_subtree_model import SubTreeConfig
10
10
 
11
- class SingleSubTreeConfig(BaseModel):
12
- repo_url: str
13
- branch: str
14
- prefix: str
15
-
16
-
17
- class SubTreeConfig(BaseModel):
18
- data: dict[str, SingleSubTreeConfig]
19
11
 
20
-
21
- def load_config(repo_dir: str) -> SubTreeConfig:
12
+ def load_config(repo_dir: str):
22
13
  """
23
14
  Load the subtree configuration from subtrees.json.
24
15
 
@@ -28,13 +19,15 @@ def load_config(repo_dir: str) -> SubTreeConfig:
28
19
  Returns:
29
20
  SubTreeConfig: The loaded subtree configuration.
30
21
  """
22
+ from zrb.util.git_subtree_model import SubTreeConfig
23
+
31
24
  file_path = os.path.join(repo_dir, "subtrees.json")
32
25
  if not os.path.exists(file_path):
33
26
  return SubTreeConfig(data={})
34
27
  return SubTreeConfig.model_validate_json(read_file(file_path))
35
28
 
36
29
 
37
- def save_config(repo_dir: str, config: SubTreeConfig):
30
+ def save_config(repo_dir: str, config: "SubTreeConfig"):
38
31
  """
39
32
  Save the subtree configuration to subtrees.json.
40
33
 
@@ -70,6 +63,8 @@ async def add_subtree(
70
63
  name already exists.
71
64
  Exception: If the git command returns a non-zero exit code.
72
65
  """
66
+ from zrb.util.git_subtree_model import SingleSubTreeConfig
67
+
73
68
  config = load_config(repo_dir)
74
69
  if os.path.isdir(prefix):
75
70
  raise ValueError(f"Directory exists: {prefix}")
@@ -0,0 +1,32 @@
1
+ import json
2
+ from typing import Any
3
+
4
+
5
+ class SingleSubTreeConfig:
6
+ def __init__(self, repo_url: str, branch: str, prefix: str):
7
+ self.repo_url = repo_url
8
+ self.branch = branch
9
+ self.prefix = prefix
10
+
11
+ def to_dict(self) -> dict[str, Any]:
12
+ return {
13
+ "repo_url": self.repo_url,
14
+ "branch": self.branch,
15
+ "prefix": self.prefix,
16
+ }
17
+
18
+ def model_dump_json(self, indent: int = 2) -> str:
19
+ return json.dumps(self.to_dict(), indent=indent)
20
+
21
+
22
+ class SubTreeConfig:
23
+ def __init__(self, data: dict[str, SingleSubTreeConfig]):
24
+ self.data = data
25
+
26
+ def to_dict(self) -> dict[str, Any]:
27
+ return {
28
+ "data": {k: self.data[k].to_dict() for k in self.data},
29
+ }
30
+
31
+ def model_dump_json(self, indent: int = 2) -> str:
32
+ return json.dumps(self.to_dict(), indent=indent)
zrb/util/init_path.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import os
2
2
 
3
- from zrb.config import CFG
3
+ from zrb.config.config import CFG
4
4
 
5
5
 
6
6
  def get_init_path_list() -> list[str]:
zrb/util/markdown.py ADDED
@@ -0,0 +1,62 @@
1
+ import re
2
+
3
+
4
+ def _adjust_markdown_headers(md: str, level_change: int) -> str:
5
+ lines = md.split("\n")
6
+ new_lines = []
7
+ fence_stack = []
8
+ for line in lines:
9
+ stripped_line = line.strip()
10
+ fence_match = re.match(r"^([`~]{3,})", stripped_line)
11
+ if fence_match:
12
+ current_fence = fence_match.group(1)
13
+ if (
14
+ fence_stack
15
+ and fence_stack[-1][0] == current_fence[0]
16
+ and len(current_fence) >= len(fence_stack[-1])
17
+ ):
18
+ fence_stack.pop()
19
+ else:
20
+ fence_stack.append(current_fence)
21
+ new_lines.append(line)
22
+ elif fence_stack:
23
+ new_lines.append(line)
24
+ else:
25
+ match = re.match(r"^(#{1,6})(\s)", line)
26
+ if match:
27
+ current_level = len(match.group(1))
28
+ new_level = max(1, current_level + level_change)
29
+ new_header = "#" * new_level + line[current_level:]
30
+ new_lines.append(new_header)
31
+ else:
32
+ new_lines.append(line)
33
+ return "\n".join(new_lines).rstrip()
34
+
35
+
36
+ def demote_markdown_headers(md: str) -> str:
37
+ return _adjust_markdown_headers(md, level_change=1)
38
+
39
+
40
+ def promote_markdown_headers(md: str) -> str:
41
+ return _adjust_markdown_headers(md, level_change=-1)
42
+
43
+
44
+ def make_markdown_section(header: str, content: str, as_code: bool = False) -> str:
45
+ if content.strip() == "":
46
+ return ""
47
+ if as_code:
48
+ # Find the longest sequence of backticks in the content
49
+ longest_backtick_sequence = 0
50
+ # Use finditer to find all occurrences of backticks
51
+ for match in re.finditer(r"`+", content):
52
+ longest_backtick_sequence = max(
53
+ longest_backtick_sequence, len(match.group(0))
54
+ )
55
+
56
+ # The fence should be one longer than the longest sequence found
57
+ fence_len = 4
58
+ if longest_backtick_sequence >= fence_len:
59
+ fence_len = longest_backtick_sequence + 1
60
+ fence = "`" * fence_len
61
+ return f"# {header}\n{fence}\n{content.strip()}\n{fence}\n"
62
+ return f"# {header}\n{demote_markdown_headers(content.strip())}\n"
@@ -1,7 +1,7 @@
1
1
  import re
2
2
 
3
3
  NON_ALPHA_NUM = re.compile(r"[^a-zA-Z0-9]+")
4
- TRUE_STRS = ["true", "1", "yes", "y", "active", "on"]
4
+ TRUE_STRS = ["true", "1", "yes", "y", "active", "on", "okay", "ok"]
5
5
  FALSE_STRS = ["false", "0", "no", "n", "inactive", "off"]
6
6
 
7
7
 
@@ -178,7 +178,7 @@ def pluralize(noun: str) -> str:
178
178
  if noun.lower() in irregulars:
179
179
  return irregulars[noun.lower()]
180
180
  # Handle words ending in 'y' preceded by a consonant
181
- if noun.endswith("y") and not re.match(r"[aeiou]y$", noun):
181
+ if noun.endswith("y") and not re.match(r".*[aeiou]y$", noun, re.IGNORECASE):
182
182
  return re.sub(r"y$", "ies", noun)
183
183
  # Handle words ending in 's', 'x', 'z', 'ch', or 'sh'
184
184
  if re.search(r"(s|x|z|ch|sh)$", noun):