zrb 1.5.11__py3-none-any.whl → 1.5.13__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.
Files changed (43) hide show
  1. zrb/builtin/llm/llm_chat.py +1 -1
  2. zrb/builtin/llm/tool/__init__.py +0 -0
  3. zrb/builtin/llm/tool/sub_agent.py +125 -0
  4. zrb/builtin/llm/tool/web.py +0 -2
  5. zrb/config.py +0 -3
  6. zrb/llm_config.py +16 -2
  7. zrb/task/base_task.py +38 -2
  8. zrb/task/llm/agent.py +5 -8
  9. zrb/task/llm/context.py +17 -8
  10. zrb/task/llm/context_enrichment.py +52 -13
  11. zrb/task/llm/history_summarization.py +3 -5
  12. zrb/task/llm/prompt.py +7 -4
  13. zrb/task/llm/tool_wrapper.py +115 -53
  14. zrb/task/llm_task.py +16 -1
  15. zrb/util/attr.py +84 -1
  16. zrb/util/cli/style.py +147 -0
  17. zrb/util/cli/subcommand.py +22 -1
  18. zrb/util/cmd/command.py +18 -0
  19. zrb/util/cmd/remote.py +15 -0
  20. zrb/util/codemod/modification_mode.py +4 -0
  21. zrb/util/codemod/modify_class.py +72 -0
  22. zrb/util/codemod/modify_class_parent.py +68 -0
  23. zrb/util/codemod/modify_class_property.py +67 -0
  24. zrb/util/codemod/modify_dict.py +62 -0
  25. zrb/util/codemod/modify_function.py +75 -3
  26. zrb/util/codemod/modify_function_call.py +72 -0
  27. zrb/util/codemod/modify_method.py +77 -0
  28. zrb/util/codemod/modify_module.py +10 -0
  29. zrb/util/cron.py +37 -3
  30. zrb/util/file.py +32 -0
  31. zrb/util/git.py +113 -0
  32. zrb/util/git_subtree.py +58 -0
  33. zrb/util/group.py +64 -2
  34. zrb/util/load.py +29 -0
  35. zrb/util/run.py +9 -0
  36. zrb/util/string/conversion.py +86 -0
  37. zrb/util/string/format.py +20 -0
  38. zrb/util/string/name.py +12 -0
  39. zrb/util/todo.py +165 -4
  40. {zrb-1.5.11.dist-info → zrb-1.5.13.dist-info}/METADATA +4 -6
  41. {zrb-1.5.11.dist-info → zrb-1.5.13.dist-info}/RECORD +43 -41
  42. {zrb-1.5.11.dist-info → zrb-1.5.13.dist-info}/WHEEL +0 -0
  43. {zrb-1.5.11.dist-info → zrb-1.5.13.dist-info}/entry_points.txt +0 -0
@@ -1,58 +1,107 @@
1
1
  import functools
2
2
  import inspect
3
3
  import traceback
4
+ import typing
4
5
  from collections.abc import Callable
5
6
 
7
+ from pydantic_ai import RunContext, Tool
8
+
9
+ from zrb.context.any_context import AnyContext
6
10
  from zrb.task.llm.error import ToolExecutionError
7
11
  from zrb.util.run import run_async
8
12
 
9
13
 
10
- def wrap_tool(func: Callable) -> Callable:
11
- """Wraps a tool function to handle exceptions and context propagation.
14
+ def wrap_tool(func: Callable, ctx: AnyContext) -> Tool:
15
+ """Wraps a tool function to handle exceptions and context propagation."""
16
+ original_sig = inspect.signature(func)
17
+ # Use helper function for clarity
18
+ needs_run_context_for_pydantic = _has_context_parameter(original_sig, RunContext)
19
+ needs_any_context_for_injection = _has_context_parameter(original_sig, AnyContext)
20
+ takes_no_args = len(original_sig.parameters) == 0
21
+ # Pass individual flags to the wrapper creator
22
+ wrapper = _create_wrapper(func, original_sig, ctx, needs_any_context_for_injection)
23
+ # Adjust signature - _adjust_signature determines exclusions based on type
24
+ _adjust_signature(wrapper, original_sig, takes_no_args)
25
+ # takes_ctx in pydantic-ai Tool is specifically for RunContext
26
+ return Tool(wrapper, takes_ctx=needs_run_context_for_pydantic)
27
+
12
28
 
13
- - Catches exceptions during tool execution and returns a structured
14
- JSON error message (`ToolExecutionError`).
15
- - Inspects the original function signature for a 'ctx' parameter.
16
- If found, the wrapper will accept 'ctx' and pass it to the function.
17
- - If the original function has no parameters, injects a dummy '_dummy'
18
- parameter into the wrapper's signature to ensure schema generation
19
- for pydantic-ai.
29
+ def _has_context_parameter(original_sig: inspect.Signature, context_type: type) -> bool:
30
+ """
31
+ Checks if the function signature includes a parameter with the specified
32
+ context type annotation.
33
+ """
34
+ return any(
35
+ _is_annotated_with_context(param.annotation, context_type)
36
+ for param in original_sig.parameters.values()
37
+ )
20
38
 
21
- Args:
22
- func: The tool function (sync or async) to wrap.
23
39
 
24
- Returns:
25
- An async wrapper function.
40
+ def _is_annotated_with_context(param_annotation, context_type):
26
41
  """
27
- original_sig = inspect.signature(func)
28
- needs_ctx = "ctx" in original_sig.parameters
29
- takes_no_args = len(original_sig.parameters) == 0
42
+ Checks if the parameter annotation is the specified context type
43
+ or a generic type containing it (e.g., Optional[ContextType]).
44
+ """
45
+ if param_annotation is inspect.Parameter.empty:
46
+ return False
47
+ if param_annotation is context_type:
48
+ return True
49
+ # Check for generic types like Optional[ContextType] or Union[ContextType, ...]
50
+ origin = typing.get_origin(param_annotation)
51
+ args = typing.get_args(param_annotation)
52
+ if origin is not None and args:
53
+ # Check if context_type is one of the arguments of the generic type
54
+ return any(arg is context_type for arg in args)
55
+ return False
56
+
57
+
58
+ def _create_wrapper(
59
+ func: Callable,
60
+ original_sig: inspect.Signature,
61
+ ctx: AnyContext, # Accept ctx
62
+ needs_any_context_for_injection: bool,
63
+ ) -> Callable:
64
+ """Creates the core wrapper function."""
30
65
 
31
66
  @functools.wraps(func)
32
67
  async def wrapper(*args, **kwargs):
33
- ctx_arg = None
34
- if needs_ctx:
35
- if args:
36
- ctx_arg = args[0]
37
- args = args[1:] # Remove ctx from args for the actual call if needed
38
- elif "ctx" in kwargs:
39
- ctx_arg = kwargs.pop("ctx")
68
+ # Identify AnyContext parameter name from the original signature if needed
69
+ any_context_param_name = None
70
+
71
+ if needs_any_context_for_injection:
72
+ for param in original_sig.parameters.values():
73
+ if _is_annotated_with_context(param.annotation, AnyContext):
74
+ any_context_param_name = param.name
75
+ break # Found it, no need to continue
76
+
77
+ if any_context_param_name is None:
78
+ # This should not happen if needs_any_context_for_injection is True,
79
+ # but check for safety
80
+ raise ValueError(
81
+ "AnyContext parameter name not found in function signature."
82
+ )
83
+ # Inject the captured ctx into kwargs. This will overwrite if the LLM
84
+ # somehow provided it.
85
+ kwargs[any_context_param_name] = ctx
86
+
87
+ # If the dummy argument was added for schema generation and is present in kwargs,
88
+ # remove it before calling the original function, unless the original function
89
+ # actually expects a parameter named '_dummy'.
90
+ if "_dummy" in kwargs and "_dummy" not in original_sig.parameters:
91
+ del kwargs["_dummy"]
40
92
 
41
93
  try:
42
- # Remove dummy argument if it was added for no-arg functions
43
- if takes_no_args and "_dummy" in kwargs:
44
- del kwargs["_dummy"]
45
-
46
- if needs_ctx:
47
- # Ensure ctx is passed correctly, even if original func had only ctx
48
- if ctx_arg is None:
49
- # This case should ideally not happen if takes_ctx is True in Tool
50
- raise ValueError("Context (ctx) was expected but not provided.")
51
- # Call with context
52
- return await run_async(func(ctx_arg, *args, **kwargs))
53
- else:
54
- # Call without context
55
- return await run_async(func(*args, **kwargs))
94
+ # Call the original function.
95
+ # pydantic-ai is responsible for injecting RunContext if takes_ctx is True.
96
+ # Our wrapper injects AnyContext if needed.
97
+ # The arguments received by the wrapper (*args, **kwargs) are those
98
+ # provided by the LLM, potentially with RunContext already injected by
99
+ # pydantic-ai if takes_ctx is True. We just need to ensure AnyContext
100
+ # is injected if required by the original function.
101
+ # The dummy argument handling is moved to _adjust_signature's logic
102
+ # for schema generation, it's not needed here before calling the actual
103
+ # function.
104
+ return await run_async(func(*args, **kwargs))
56
105
  except Exception as e:
57
106
  error_model = ToolExecutionError(
58
107
  tool_name=func.__name__,
@@ -62,8 +111,32 @@ def wrap_tool(func: Callable) -> Callable:
62
111
  )
63
112
  return error_model.model_dump_json()
64
113
 
65
- if takes_no_args:
66
- # Inject dummy parameter for schema generation if original func took no args
114
+ return wrapper
115
+
116
+
117
+ def _adjust_signature(
118
+ wrapper: Callable, original_sig: inspect.Signature, takes_no_args: bool
119
+ ):
120
+ """Adjusts the wrapper function's signature for schema generation."""
121
+ # The wrapper's signature should represent the arguments the *LLM* needs to provide.
122
+ # The LLM does not provide RunContext (pydantic-ai injects it) or AnyContext
123
+ # (we inject it). So, the wrapper's signature should be the original signature,
124
+ # minus any parameters annotated with RunContext or AnyContext.
125
+
126
+ params_for_schema = [
127
+ param
128
+ for param in original_sig.parameters.values()
129
+ if not _is_annotated_with_context(param.annotation, RunContext)
130
+ and not _is_annotated_with_context(param.annotation, AnyContext)
131
+ ]
132
+
133
+ # If after removing context parameters, there are no parameters left,
134
+ # and the original function took no args, keep the dummy.
135
+ # If after removing context parameters, there are no parameters left,
136
+ # but the original function *did* take args (only context), then the schema
137
+ # should have no parameters.
138
+ if not params_for_schema and takes_no_args:
139
+ # Keep the dummy if the original function truly had no parameters
67
140
  new_sig = inspect.Signature(
68
141
  parameters=[
69
142
  inspect.Parameter(
@@ -71,18 +144,7 @@ def wrap_tool(func: Callable) -> Callable:
71
144
  )
72
145
  ]
73
146
  )
74
- wrapper.__signature__ = new_sig
75
- elif needs_ctx:
76
- # Adjust signature if ctx was the *only* parameter originally
77
- # This ensures the wrapper signature matches what pydantic-ai expects
78
- params = list(original_sig.parameters.values())
79
- if len(params) == 1 and params[0].name == "ctx":
80
- new_sig = inspect.Signature(
81
- parameters=[
82
- inspect.Parameter("ctx", inspect.Parameter.POSITIONAL_OR_KEYWORD)
83
- ]
84
- )
85
- wrapper.__signature__ = new_sig
86
- # Otherwise, the original signature (including ctx) is fine for the wrapper
147
+ else:
148
+ new_sig = inspect.Signature(parameters=params_for_schema)
87
149
 
88
- return wrapper
150
+ wrapper.__signature__ = new_sig
zrb/task/llm_task.py CHANGED
@@ -77,6 +77,8 @@ class LLMTask(BaseTask):
77
77
  render_enrich_context: bool = True,
78
78
  context_enrichment_prompt: StrAttr | None = None,
79
79
  render_context_enrichment_prompt: bool = True,
80
+ context_enrichment_threshold: IntAttr | None = None,
81
+ render_context_enrichment_threshold: bool = True,
80
82
  tools: (
81
83
  list[ToolOrCallable] | Callable[[AnySharedContext], list[ToolOrCallable]]
82
84
  ) = [],
@@ -161,6 +163,8 @@ class LLMTask(BaseTask):
161
163
  self._render_enrich_context = render_enrich_context
162
164
  self._context_enrichment_prompt = context_enrichment_prompt
163
165
  self._render_context_enrichment_prompt = render_context_enrichment_prompt
166
+ self._context_enrichment_threshold = context_enrichment_threshold
167
+ self._render_context_enrichment_threshold = render_context_enrichment_threshold
164
168
  self._tools = tools
165
169
  self._additional_tools: list[ToolOrCallable] = []
166
170
  self._mcp_servers = mcp_servers
@@ -180,14 +184,23 @@ class LLMTask(BaseTask):
180
184
  self._conversation_context = conversation_context
181
185
 
182
186
  def add_tool(self, tool: ToolOrCallable):
187
+ self.append_tool(tool)
188
+
189
+ def append_tool(self, tool: ToolOrCallable):
183
190
  self._additional_tools.append(tool)
184
191
 
185
192
  def add_mcp_server(self, mcp_server: MCPServer):
193
+ self.append_mcp_server(mcp_server)
194
+
195
+ def append_mcp_server(self, mcp_server: MCPServer):
186
196
  self._additional_mcp_servers.append(mcp_server)
187
197
 
188
198
  def set_should_enrich_context(self, enrich_context: bool):
189
199
  self._should_enrich_context = enrich_context
190
200
 
201
+ def set_context_enrichment_threshold(self, enrichment_threshold: int):
202
+ self._context_enrichment_threshold = enrichment_threshold
203
+
191
204
  def set_should_summarize_history(self, summarize_history: bool):
192
205
  self._should_summarize_history = summarize_history
193
206
 
@@ -244,6 +257,8 @@ class LLMTask(BaseTask):
244
257
  conversation_context=conversation_context,
245
258
  should_enrich_context_attr=self._should_enrich_context,
246
259
  render_enrich_context=self._render_enrich_context,
260
+ context_enrichment_threshold_attr=self._context_enrichment_threshold,
261
+ render_context_enrichment_threshold=self._render_context_enrichment_threshold,
247
262
  model=model,
248
263
  model_settings=model_settings,
249
264
  context_enrichment_prompt=context_enrichment_prompt,
@@ -321,7 +336,7 @@ class LLMTask(BaseTask):
321
336
  usage = agent_run.result.usage()
322
337
  ctx.xcom.get(xcom_usage_key).push(usage)
323
338
  ctx.print(stylize_faint(f"[USAGE] {usage}"))
324
- return agent_run.result.data
339
+ return agent_run.result.output
325
340
  else:
326
341
  ctx.log_warning("Agent run did not produce a result.")
327
342
  return None # Or handle as appropriate
zrb/util/attr.py CHANGED
@@ -16,6 +16,17 @@ from zrb.util.string.conversion import to_boolean
16
16
  def get_str_list_attr(
17
17
  shared_ctx: AnySharedContext, attr: StrListAttr | None, auto_render: bool = True
18
18
  ) -> list[str]:
19
+ """
20
+ Retrieve a list of strings from shared context attributes.
21
+
22
+ Args:
23
+ shared_ctx (AnySharedContext): The shared context object.
24
+ attr (StrListAttr | None): The string list attribute to retrieve.
25
+ auto_render (bool): Whether to auto-render the attribute values.
26
+
27
+ Returns:
28
+ list[str]: A list of string attributes.
29
+ """
19
30
  if callable(attr):
20
31
  return attr(shared_ctx)
21
32
  return {get_str_attr(shared_ctx, val, "", auto_render) for val in attr}
@@ -24,6 +35,17 @@ def get_str_list_attr(
24
35
  def get_str_dict_attr(
25
36
  shared_ctx: AnySharedContext, attr: StrDictAttr | None, auto_render: bool = True
26
37
  ) -> dict[str, Any]:
38
+ """
39
+ Retrieve a dictionary of strings from shared context attributes.
40
+
41
+ Args:
42
+ shared_ctx (AnySharedContext): The shared context object.
43
+ attr (StrDictAttr | None): The string dictionary attribute to retrieve.
44
+ auto_render (bool): Whether to auto-render the attribute values.
45
+
46
+ Returns:
47
+ dict[str, Any]: A dictionary of string attributes.
48
+ """
27
49
  if callable(attr):
28
50
  return attr(shared_ctx)
29
51
  return {
@@ -37,6 +59,18 @@ def get_str_attr(
37
59
  default: StrAttr = "",
38
60
  auto_render: bool = True,
39
61
  ) -> str:
62
+ """
63
+ Retrieve a string from shared context attributes.
64
+
65
+ Args:
66
+ shared_ctx (AnySharedContext): The shared context object.
67
+ attr (StrAttr | None): The string attribute to retrieve.
68
+ default (StrAttr): The default value if the attribute is None.
69
+ auto_render (bool): Whether to auto-render the attribute value.
70
+
71
+ Returns:
72
+ str: The string attribute value.
73
+ """
40
74
  val = get_attr(shared_ctx, attr, default, auto_render)
41
75
  if not isinstance(val, str):
42
76
  return str(val)
@@ -49,6 +83,18 @@ def get_bool_attr(
49
83
  default: BoolAttr = False,
50
84
  auto_render: bool = True,
51
85
  ) -> bool:
86
+ """
87
+ Retrieve a boolean from shared context attributes.
88
+
89
+ Args:
90
+ shared_ctx (AnySharedContext): The shared context object.
91
+ attr (BoolAttr | None): The boolean attribute to retrieve.
92
+ default (BoolAttr): The default value if the attribute is None.
93
+ auto_render (bool): Whether to auto-render the attribute value if it's a string.
94
+
95
+ Returns:
96
+ bool: The boolean attribute value.
97
+ """
52
98
  val = get_attr(shared_ctx, attr, default, auto_render)
53
99
  if isinstance(val, str):
54
100
  return to_boolean(val)
@@ -61,6 +107,18 @@ def get_int_attr(
61
107
  default: IntAttr = 0,
62
108
  auto_render: bool = True,
63
109
  ) -> int:
110
+ """
111
+ Retrieve an integer from shared context attributes.
112
+
113
+ Args:
114
+ shared_ctx (AnySharedContext): The shared context object.
115
+ attr (IntAttr | None): The integer attribute to retrieve.
116
+ default (IntAttr): The default value if the attribute is None.
117
+ auto_render (bool): Whether to auto-render the attribute value if it's a string.
118
+
119
+ Returns:
120
+ int: The integer attribute value.
121
+ """
64
122
  val = get_attr(shared_ctx, attr, default, auto_render)
65
123
  if isinstance(val, str):
66
124
  return int(val)
@@ -72,7 +130,19 @@ def get_float_attr(
72
130
  attr: FloatAttr | None,
73
131
  default: FloatAttr = 0.0,
74
132
  auto_render: bool = True,
75
- ) -> str | None:
133
+ ) -> float | None:
134
+ """
135
+ Retrieve a float from shared context attributes.
136
+
137
+ Args:
138
+ shared_ctx (AnySharedContext): The shared context object.
139
+ attr (FloatAttr | None): The float attribute to retrieve.
140
+ default (FloatAttr): The default value if the attribute is None.
141
+ auto_render (bool): Whether to auto-render the attribute value if it's a string.
142
+
143
+ Returns:
144
+ float | None: The float attribute value.
145
+ """
76
146
  val = get_attr(shared_ctx, attr, default, auto_render)
77
147
  if isinstance(val, str):
78
148
  return float(val)
@@ -85,6 +155,19 @@ def get_attr(
85
155
  default: AnyAttr,
86
156
  auto_render: bool = True,
87
157
  ) -> Any | None:
158
+ """
159
+ Retrieve an attribute value from shared context, handling callables and rendering.
160
+
161
+ Args:
162
+ shared_ctx (AnySharedContext): The shared context object.
163
+ attr (AnyAttr): The attribute to retrieve. Can be a value, a callable,
164
+ or a string to render.
165
+ default (AnyAttr): The default value if the attribute is None.
166
+ auto_render (bool): Whether to auto-render the attribute value if it's a string.
167
+
168
+ Returns:
169
+ Any | None: The retrieved attribute value or the default value.
170
+ """
88
171
  if attr is None:
89
172
  if callable(default):
90
173
  return default(shared_ctx)
zrb/util/cli/style.py CHANGED
@@ -121,6 +121,15 @@ ICONS = [
121
121
 
122
122
 
123
123
  def remove_style(text):
124
+ """
125
+ Remove ANSI escape codes from a string.
126
+
127
+ Args:
128
+ text (str): The input string with potential ANSI escape codes.
129
+
130
+ Returns:
131
+ str: The string with ANSI escape codes removed.
132
+ """
124
133
  ansi_escape = re.compile(r"\x1B[@-_][0-?]*[ -/]*[@-~]")
125
134
  return ansi_escape.sub("", text)
126
135
 
@@ -131,6 +140,18 @@ def stylize(
131
140
  background: int | None = None,
132
141
  style: int | None = None,
133
142
  ):
143
+ """
144
+ Apply ANSI escape codes to a string for terminal styling.
145
+
146
+ Args:
147
+ text (str): The input string to stylize.
148
+ color (int | None): The foreground color code.
149
+ background (int | None): The background color code.
150
+ style (int | None): The text style code (e.g., bold, underline).
151
+
152
+ Returns:
153
+ str: The stylized string with ANSI escape codes.
154
+ """
134
155
  # Start constructing the ANSI escape code
135
156
  code_parts = []
136
157
  if style is not None and style in VALID_STYLES:
@@ -146,56 +167,182 @@ def stylize(
146
167
 
147
168
 
148
169
  def stylize_section_header(text: str):
170
+ """
171
+ Stylize text as a section header.
172
+
173
+ Args:
174
+ text (str): The input string.
175
+
176
+ Returns:
177
+ str: The stylized section header string.
178
+ """
149
179
  return stylize(f" {text} ", color=BLACK, background=BG_WHITE, style=UNDERLINE)
150
180
 
151
181
 
152
182
  def stylize_green(text: str):
183
+ """
184
+ Stylize text with green foreground color.
185
+
186
+ Args:
187
+ text (str): The input string.
188
+
189
+ Returns:
190
+ str: The stylized string.
191
+ """
153
192
  return stylize(text, color=GREEN)
154
193
 
155
194
 
156
195
  def stylize_blue(text: str):
196
+ """
197
+ Stylize text with blue foreground color.
198
+
199
+ Args:
200
+ text (str): The input string.
201
+
202
+ Returns:
203
+ str: The stylized string.
204
+ """
157
205
  return stylize(text, color=BLUE)
158
206
 
159
207
 
160
208
  def stylize_cyan(text: str):
209
+ """
210
+ Stylize text with cyan foreground color.
211
+
212
+ Args:
213
+ text (str): The input string.
214
+
215
+ Returns:
216
+ str: The stylized string.
217
+ """
161
218
  return stylize(text, color=CYAN)
162
219
 
163
220
 
164
221
  def stylize_magenta(text: str):
222
+ """
223
+ Stylize text with magenta foreground color.
224
+
225
+ Args:
226
+ text (str): The input string.
227
+
228
+ Returns:
229
+ str: The stylized string.
230
+ """
165
231
  return stylize(text, color=MAGENTA)
166
232
 
167
233
 
168
234
  def stylize_yellow(text: str):
235
+ """
236
+ Stylize text with yellow foreground color.
237
+
238
+ Args:
239
+ text (str): The input string.
240
+
241
+ Returns:
242
+ str: The stylized string.
243
+ """
169
244
  return stylize(text, color=YELLOW)
170
245
 
171
246
 
172
247
  def stylize_red(text: str):
248
+ """
249
+ Stylize text with red foreground color.
250
+
251
+ Args:
252
+ text (str): The input string.
253
+
254
+ Returns:
255
+ str: The stylized string.
256
+ """
173
257
  return stylize(text, color=RED)
174
258
 
175
259
 
176
260
  def stylize_bold_green(text: str):
261
+ """
262
+ Stylize text with bold green foreground color.
263
+
264
+ Args:
265
+ text (str): The input string.
266
+
267
+ Returns:
268
+ str: The stylized string.
269
+ """
177
270
  return stylize(text, color=GREEN, style=BOLD)
178
271
 
179
272
 
180
273
  def stylize_bold_yellow(text: str):
274
+ """
275
+ Stylize text with bold yellow foreground color.
276
+
277
+ Args:
278
+ text (str): The input string.
279
+
280
+ Returns:
281
+ str: The stylized string.
282
+ """
181
283
  return stylize(text, color=YELLOW, style=BOLD)
182
284
 
183
285
 
184
286
  def stylize_bold_red(text: str):
287
+ """
288
+ Stylize text with bold red foreground color.
289
+
290
+ Args:
291
+ text (str): The input string.
292
+
293
+ Returns:
294
+ str: The stylized string.
295
+ """
185
296
  return stylize(text, color=RED, style=BOLD)
186
297
 
187
298
 
188
299
  def stylize_faint(text: str):
300
+ """
301
+ Stylize text with faint style.
302
+
303
+ Args:
304
+ text (str): The input string.
305
+
306
+ Returns:
307
+ str: The stylized string.
308
+ """
189
309
  return stylize(text, style=FAINT)
190
310
 
191
311
 
192
312
  def stylize_log(text: str):
313
+ """
314
+ Stylize text for log messages (faint).
315
+
316
+ Args:
317
+ text (str): The input string.
318
+
319
+ Returns:
320
+ str: The stylized string.
321
+ """
193
322
  return stylize_faint(text)
194
323
 
195
324
 
196
325
  def stylize_warning(text: str):
326
+ """
327
+ Stylize text for warning messages (bold yellow).
328
+
329
+ Args:
330
+ text (str): The input string.
331
+
332
+ Returns:
333
+ str: The stylized string.
334
+ """
197
335
  return stylize_bold_yellow(text)
198
336
 
199
337
 
200
338
  def stylize_error(text: str):
339
+ """
340
+ Stylize text for error messages (bold red).
341
+
342
+ Args:
343
+ text (str): The input string.
344
+
345
+ Returns:
346
+ str: The stylized string.
347
+ """
201
348
  return stylize_bold_red(text)
@@ -4,16 +4,37 @@ from zrb.util.group import get_non_empty_subgroups, get_subtasks
4
4
 
5
5
  class SubCommand:
6
6
  def __init__(self, paths: list[str] = [], nexts: list[str] = []):
7
+ """
8
+ Initialize a SubCommand object.
9
+
10
+ Args:
11
+ paths (list[str]): The list of path components leading to this subcommand.
12
+ nexts (list[str]): The list of possible next subcommand or task names.
13
+ """
7
14
  self.paths = paths
8
15
  self.nexts = nexts
9
16
 
10
17
  def __repr__(self):
18
+ """
19
+ Return a string representation of the SubCommand object.
20
+ """
11
21
  return f"<{self.__class__.__name__} paths={self.paths} nexts={self.nexts}>"
12
22
 
13
23
 
14
24
  def get_group_subcommands(
15
- group: AnyGroup, previous_path: str = [], subcommands: list[SubCommand] = []
25
+ group: AnyGroup, previous_path: list[str] = [], subcommands: list[SubCommand] = []
16
26
  ) -> list[SubCommand]:
27
+ """
28
+ Recursively get all possible subcommands within a group hierarchy.
29
+
30
+ Args:
31
+ group (AnyGroup): The current group to process.
32
+ previous_path (list[str]): The path leading to the current group.
33
+ subcommands (list[SubCommand]): The list to accumulate subcommands.
34
+
35
+ Returns:
36
+ list[SubCommand]: A list of all discovered subcommands.
37
+ """
17
38
  nexts = []
18
39
  for task_alias in get_subtasks(group):
19
40
  nexts.append(task_alias)
zrb/util/cmd/command.py CHANGED
@@ -10,6 +10,16 @@ from zrb.cmd.cmd_result import CmdResult
10
10
 
11
11
 
12
12
  def check_unrecommended_commands(cmd_script: str) -> dict[str, str]:
13
+ """
14
+ Check a command script for the use of unrecommended or non-POSIX compliant commands.
15
+
16
+ Args:
17
+ cmd_script (str): The command script string to check.
18
+
19
+ Returns:
20
+ dict[str, str]: A dictionary where keys are the violating commands/patterns
21
+ and values are the reasons they are unrecommended.
22
+ """
13
23
  banned_commands = {
14
24
  "<(": "Process substitution isn't POSIX compliant and causes trouble",
15
25
  "column": "Command isn't included in Ubuntu packages and is not POSIX compliant",
@@ -96,6 +106,14 @@ async def run_command(
96
106
 
97
107
 
98
108
  def kill_pid(pid: int, print_method: Callable[..., None] | None = None):
109
+ """
110
+ Kill a process and its children given the parent process ID.
111
+
112
+ Args:
113
+ pid (int): The process ID of the parent process.
114
+ print_method (Callable[..., None] | None): A method to print status messages.
115
+ Defaults to the built-in print function.
116
+ """
99
117
  actual_print_method = print_method if print_method is not None else print
100
118
  parent = psutil.Process(pid)
101
119
  children = parent.children(recursive=True)