zrb 0.17.0__py3-none-any.whl → 0.18.0__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 (38) hide show
  1. zrb/builtin/devtool/install/helix/helix.py +1 -4
  2. zrb/builtin/project/add/fastapp/app/template/src/kebab-zrb-app-name/src/component/rpc/messagebus/caller.py +1 -1
  3. zrb/task/any_task.py +6 -9
  4. zrb/task/base_remote_cmd_task.py +66 -8
  5. zrb/task/base_task/base_task.py +13 -14
  6. zrb/task/base_task/component/base_task_model.py +1 -1
  7. zrb/task/base_task/component/common_task_model.py +1 -1
  8. zrb/task/base_task/component/trackers.py +4 -3
  9. zrb/task/checker.py +1 -1
  10. zrb/task/cmd_task.py +1 -1
  11. zrb/task/decorator.py +2 -2
  12. zrb/task/docker_compose_task.py +2 -2
  13. zrb/task/flow_task.py +45 -34
  14. zrb/task/http_checker.py +1 -1
  15. zrb/task/looper.py +3 -0
  16. zrb/task/notifier.py +1 -1
  17. zrb/task/path_checker.py +1 -1
  18. zrb/task/path_watcher.py +1 -1
  19. zrb/task/port_checker.py +1 -1
  20. zrb/task/recurring_task.py +2 -2
  21. zrb/task/remote_cmd_task.py +1 -1
  22. zrb/task/resource_maker.py +1 -1
  23. zrb/task/rsync_task.py +5 -5
  24. zrb/task/server.py +26 -18
  25. zrb/task/task.py +35 -1
  26. zrb/task/time_watcher.py +3 -3
  27. zrb/task/watcher.py +5 -2
  28. zrb/task_env/env.py +4 -3
  29. zrb/task_env/env_file.py +3 -2
  30. zrb/task_group/group.py +1 -1
  31. zrb/task_input/any_input.py +5 -3
  32. zrb/task_input/base_input.py +3 -3
  33. zrb/task_input/task_input.py +1 -1
  34. {zrb-0.17.0.dist-info → zrb-0.18.0.dist-info}/METADATA +14 -11
  35. {zrb-0.17.0.dist-info → zrb-0.18.0.dist-info}/RECORD +38 -38
  36. {zrb-0.17.0.dist-info → zrb-0.18.0.dist-info}/LICENSE +0 -0
  37. {zrb-0.17.0.dist-info → zrb-0.18.0.dist-info}/WHEEL +0 -0
  38. {zrb-0.17.0.dist-info → zrb-0.18.0.dist-info}/entry_points.txt +0 -0
@@ -29,7 +29,6 @@ install_helix = FlowTask(
29
29
  run=write_config(
30
30
  template_file=os.path.join(
31
31
  _CURRENT_DIR,
32
- "helix",
33
32
  "resource",
34
33
  "themes",
35
34
  "gruvbox_transparent.toml", # noqa
@@ -41,9 +40,7 @@ install_helix = FlowTask(
41
40
  Task(
42
41
  name="configure-helix",
43
42
  run=write_config(
44
- template_file=os.path.join(
45
- _CURRENT_DIR, "helix", "resource", "config.toml"
46
- ),
43
+ template_file=os.path.join(_CURRENT_DIR, "resource", "config.toml"),
47
44
  config_file="~/.config/helix/config.toml",
48
45
  remove_old_config=True,
49
46
  ),
@@ -15,7 +15,7 @@ class MessagebusCaller(Caller):
15
15
  publisher: Publisher,
16
16
  consumer_factory: Callable[[], Consumer],
17
17
  timeout: float = 30,
18
- check_reply_interval: float = 0.1,
18
+ check_reply_interval: float = 0.05,
19
19
  ):
20
20
  self.logger = logger
21
21
  self.admin = admin
zrb/task/any_task.py CHANGED
@@ -25,14 +25,12 @@ TAnyTask = TypeVar("TAnyTask", bound="AnyTask")
25
25
 
26
26
  class AnyTask(ABC):
27
27
  """
28
- Abstract base class for defining tasks in a task management system.
28
+ Abstraction for Zrb Task.
29
29
 
30
- This class acts as a template for creating new task types. To define a new task,
31
- extend this class and implement all its abstract methods. The `AnyTask` class is
32
- considered atomic and is not broken into multiple interfaces.
30
+ This class acts as a template for creating new Task type.
33
31
 
34
- Subclasses should implement the abstract methods to define custom behavior for
35
- task execution, state transitions, and other functionalities.
32
+ To define a new Task type, you should extend this class and implement all its methods.
33
+ The easiest way to do so is by extending `Task`
36
34
  """
37
35
 
38
36
  @abstractmethod
@@ -74,7 +72,7 @@ class AnyTask(ABC):
74
72
  Examples:
75
73
  >>> from zrb import Task
76
74
  >>> class MyTask(Task):
77
- >>> async def run(self, *args: Any, **kwargs: Any) -> int:
75
+ >>> def run(self, *args: Any, **kwargs: Any) -> int:
78
76
  >>> self.print_out('Doing some calculation')
79
77
  >>> return 42
80
78
  """
@@ -88,8 +86,7 @@ class AnyTask(ABC):
88
86
  Any other tasks depends on the current task, will be `started` once the current task is `ready`.
89
87
 
90
88
  This method should be implemented to define the criteria for considering the task
91
- `ready`. The specifics of this completion depend on the task's
92
- nature and the subclass implementation.
89
+ `ready`. The specifics of this completion depend on the task's nature and the subclass implementation.
93
90
 
94
91
  Returns:
95
92
  bool: True if the task is completed, False otherwise.
@@ -1,4 +1,3 @@
1
- import copy
2
1
  import os
3
2
  import pathlib
4
3
 
@@ -50,8 +49,10 @@ class RemoteConfig:
50
49
  password: JinjaTemplate = "",
51
50
  ssh_key: JinjaTemplate = "",
52
51
  port: Union[int, JinjaTemplate] = 22,
52
+ name: Optional[str] = None,
53
53
  config_map: Optional[Mapping[str, JinjaTemplate]] = None,
54
54
  ):
55
+ self.name = name if name is not None else host
55
56
  self.host = host
56
57
  self.user = user
57
58
  self.password = password
@@ -91,7 +92,7 @@ class SingleBaseRemoteCmdTask(CmdTask):
91
92
  on_retry: Optional[OnRetry] = None,
92
93
  on_failed: Optional[OnFailed] = None,
93
94
  checkers: Iterable[AnyTask] = [],
94
- checking_interval: Union[float, int] = 0,
95
+ checking_interval: Union[float, int] = 0.05,
95
96
  retry: int = 2,
96
97
  retry_interval: Union[float, int] = 1,
97
98
  max_output_line: int = 1000,
@@ -146,7 +147,7 @@ class SingleBaseRemoteCmdTask(CmdTask):
146
147
  self._remote_config = remote_config
147
148
 
148
149
  def copy(self) -> TSingleBaseRemoteCmdTask:
149
- return copy.deepcopy(self)
150
+ return super().copy()
150
151
 
151
152
  def inject_envs(self):
152
153
  super().inject_envs()
@@ -227,6 +228,7 @@ class BaseRemoteCmdTask(BaseTask):
227
228
  post_cmd_path: CmdVal = "",
228
229
  cwd: Optional[Union[str, pathlib.Path]] = None,
229
230
  upstreams: Iterable[AnyTask] = [],
231
+ fallbacks: Iterable[AnyTask] = [],
230
232
  on_triggered: Optional[OnTriggered] = None,
231
233
  on_waiting: Optional[OnWaiting] = None,
232
234
  on_skipped: Optional[OnSkipped] = None,
@@ -235,7 +237,7 @@ class BaseRemoteCmdTask(BaseTask):
235
237
  on_retry: Optional[OnRetry] = None,
236
238
  on_failed: Optional[OnFailed] = None,
237
239
  checkers: Iterable[AnyTask] = [],
238
- checking_interval: Union[float, int] = 0,
240
+ checking_interval: Union[float, int] = 0.05,
239
241
  retry: int = 2,
240
242
  retry_interval: Union[float, int] = 1,
241
243
  max_output_line: int = 1000,
@@ -247,9 +249,10 @@ class BaseRemoteCmdTask(BaseTask):
247
249
  should_show_cmd: bool = True,
248
250
  should_show_working_directory: bool = True,
249
251
  ):
250
- sub_tasks = [
252
+ self._remote_configs = list(remote_configs)
253
+ self._sub_tasks = [
251
254
  SingleBaseRemoteCmdTask(
252
- name=f"{name}-{remote_config.host}",
255
+ name=f"{name}-{remote_config.name}",
253
256
  remote_config=remote_config,
254
257
  inputs=inputs,
255
258
  envs=envs,
@@ -264,6 +267,7 @@ class BaseRemoteCmdTask(BaseTask):
264
267
  post_cmd_path=post_cmd_path,
265
268
  cwd=cwd,
266
269
  upstreams=upstreams,
270
+ fallbacks=fallbacks,
267
271
  on_triggered=on_triggered,
268
272
  on_waiting=on_waiting,
269
273
  on_skipped=on_skipped,
@@ -284,7 +288,7 @@ class BaseRemoteCmdTask(BaseTask):
284
288
  should_show_cmd=should_show_cmd,
285
289
  should_show_working_directory=should_show_working_directory,
286
290
  )
287
- for remote_config in list(remote_configs)
291
+ for remote_config in self._remote_configs
288
292
  ]
289
293
  BaseTask.__init__(
290
294
  self,
@@ -293,7 +297,61 @@ class BaseRemoteCmdTask(BaseTask):
293
297
  color=color,
294
298
  group=group,
295
299
  description=description,
296
- upstreams=sub_tasks,
300
+ upstreams=self._sub_tasks,
297
301
  retry=0,
298
302
  return_upstream_result=True,
299
303
  )
304
+
305
+ def insert_input(self, *inputs: AnyInput):
306
+ super().insert_input(*inputs)
307
+ for subtask in self._sub_tasks:
308
+ subtask.insert_input(*inputs)
309
+
310
+ def add_input(self, *inputs: AnyInput):
311
+ super().add_input(*inputs)
312
+ for subtask in self._sub_tasks:
313
+ subtask.add_input(*inputs)
314
+
315
+ def insert_env(self, *envs: Env):
316
+ super().insert_env(*envs)
317
+ for subtask in self._sub_tasks:
318
+ subtask.insert_env(*envs)
319
+
320
+ def add_env(self, *envs: Env):
321
+ super().add_env(*envs)
322
+ for subtask in self._sub_tasks:
323
+ subtask.add_env(*envs)
324
+
325
+ def insert_env_file(self, *env_files: EnvFile):
326
+ super().insert_env_file(*env_files)
327
+ for subtask in self._sub_tasks:
328
+ subtask.insert_env_file(*env_files)
329
+
330
+ def add_env_file(self, *env_files: Env):
331
+ super().add_env_file(*env_files)
332
+ for subtask in self._sub_tasks:
333
+ subtask.add_env_file(*env_files)
334
+
335
+ def insert_upstream(self, *upstreams: AnyTask):
336
+ for subtask in self._sub_tasks:
337
+ subtask.insert_upstream(*upstreams)
338
+
339
+ def add_upstream(self, *upstreams: AnyTask):
340
+ for subtask in self._sub_tasks:
341
+ subtask.add_upstream(*upstreams)
342
+
343
+ def insert_fallback(self, *fallbacks: AnyTask):
344
+ for subtask in self._sub_tasks:
345
+ subtask.insert_fallbacks(*fallbacks)
346
+
347
+ def add_fallback(self, *fallbacks: AnyTask):
348
+ for subtask in self._sub_tasks:
349
+ subtask.add_fallback(*fallbacks)
350
+
351
+ def insert_checker(self, *checkers: AnyTask):
352
+ for subtask in self._sub_tasks:
353
+ subtask.insert_checkers(*checkers)
354
+
355
+ def add_checker(self, *checkers: AnyTask):
356
+ for subtask in self._sub_tasks:
357
+ subtask.add_checker(*checkers)
@@ -37,8 +37,8 @@ from zrb.task_input.any_input import AnyInput
37
37
  @typechecked
38
38
  class BaseTask(FinishTracker, AttemptTracker, Renderer, BaseTaskModel, AnyTask):
39
39
  """
40
- Base class for all tasks.
41
- Every task definition should be extended from this class.
40
+ Base class for all Tasks.
41
+ Every Task definition should be extended from this class.
42
42
  """
43
43
 
44
44
  __running_tasks: List[AnyTask] = []
@@ -58,7 +58,7 @@ class BaseTask(FinishTracker, AttemptTracker, Renderer, BaseTaskModel, AnyTask):
58
58
  upstreams: Iterable[AnyTask] = [],
59
59
  fallbacks: Iterable[AnyTask] = [],
60
60
  checkers: Iterable[AnyTask] = [],
61
- checking_interval: Union[float, int] = 0,
61
+ checking_interval: Union[float, int] = 0.05,
62
62
  run: Optional[Callable[..., Any]] = None,
63
63
  on_triggered: Optional[OnTriggered] = None,
64
64
  on_waiting: Optional[OnWaiting] = None,
@@ -72,10 +72,10 @@ class BaseTask(FinishTracker, AttemptTracker, Renderer, BaseTaskModel, AnyTask):
72
72
  ):
73
73
  # init properties
74
74
  retry_interval = retry_interval if retry_interval >= 0 else 0
75
- checking_interval = checking_interval if checking_interval > 0 else 0.1
75
+ checking_interval = checking_interval if checking_interval > 0 else 0
76
76
  retry = retry if retry >= 0 else 0
77
77
  # init parent classes
78
- FinishTracker.__init__(self)
78
+ FinishTracker.__init__(self, checking_interval=checking_interval)
79
79
  Renderer.__init__(self)
80
80
  AttemptTracker.__init__(self, retry=retry)
81
81
  BaseTaskModel.__init__(
@@ -233,7 +233,7 @@ class BaseTask(FinishTracker, AttemptTracker, Renderer, BaseTaskModel, AnyTask):
233
233
  await run_async(self._on_retry, self)
234
234
 
235
235
  async def check(self) -> bool:
236
- return await self._is_done()
236
+ return await run_async(self._is_done)
237
237
 
238
238
  def inject_envs(self):
239
239
  super().inject_envs()
@@ -270,18 +270,17 @@ class BaseTask(FinishTracker, AttemptTracker, Renderer, BaseTaskModel, AnyTask):
270
270
  new_kwargs = self.get_input_map()
271
271
  # make sure args and kwargs['_args'] are the same
272
272
  self.log_info("Set run args")
273
- new_args = copy.deepcopy(args)
273
+ new_args = list(args)
274
274
  if len(args) == 0 and "_args" in kwargs:
275
275
  new_args = kwargs["_args"]
276
- new_kwargs["_args"] = new_args
277
- # inject self as input_map['_task']
278
- new_kwargs["_task"] = self
279
276
  self._set_args(new_args)
280
277
  self._set_kwargs(new_kwargs)
281
278
  # run the task
282
279
  coroutines = [
283
280
  asyncio.create_task(self._loop_check(show_done_info=show_done_info)),
284
- asyncio.create_task(self._run_all(*new_args, **new_kwargs)),
281
+ asyncio.create_task(
282
+ self._run_all(*new_args, _args=new_args, _task=self, **new_kwargs)
283
+ ),
285
284
  ]
286
285
  results = await asyncio.gather(*coroutines)
287
286
  result = results[-1]
@@ -315,7 +314,7 @@ class BaseTask(FinishTracker, AttemptTracker, Renderer, BaseTaskModel, AnyTask):
315
314
  if self.__is_check_triggered:
316
315
  self.log_debug("Waiting readiness flag to be set")
317
316
  while not self.__is_ready:
318
- await asyncio.sleep(0.1)
317
+ await asyncio.sleep(self._checking_interval)
319
318
  return True
320
319
  self.__is_check_triggered = True
321
320
  check_result = await self._check()
@@ -336,7 +335,7 @@ class BaseTask(FinishTracker, AttemptTracker, Renderer, BaseTaskModel, AnyTask):
336
335
  self.log_debug("Waiting execution to be started")
337
336
  while not self.__is_execution_started:
338
337
  # Don't start checking before the execution itself has been started
339
- await asyncio.sleep(0.05)
338
+ await asyncio.sleep(self._checking_interval / 2.0)
340
339
  check_coroutines: Iterable[asyncio.Task] = []
341
340
  for checker_task in self._get_checkers():
342
341
  checker_task._set_execution_id(self.get_execution_id())
@@ -456,7 +455,7 @@ class BaseTask(FinishTracker, AttemptTracker, Renderer, BaseTaskModel, AnyTask):
456
455
  # set current task local keyval
457
456
  await self._set_local_keyval(kwargs=kwargs, env_prefix=env_prefix)
458
457
  # get new_kwargs for upstream and checkers
459
- new_kwargs = copy.deepcopy(kwargs)
458
+ new_kwargs = {key: kwargs[key] for key in kwargs}
460
459
  new_kwargs.update(self.get_input_map())
461
460
  upstream_coroutines = []
462
461
  # set upstreams keyval
@@ -58,7 +58,7 @@ class BaseTaskModel(CommonTaskModel, PidModel, TimeTracker):
58
58
  upstreams: Iterable[AnyTask] = [],
59
59
  fallbacks: Iterable[AnyTask] = [],
60
60
  checkers: Iterable[AnyTask] = [],
61
- checking_interval: Union[int, float] = 0,
61
+ checking_interval: Union[int, float] = 0.05,
62
62
  run: Optional[Callable[..., Any]] = None,
63
63
  on_triggered: Optional[OnTriggered] = None,
64
64
  on_waiting: Optional[OnWaiting] = None,
@@ -49,7 +49,7 @@ class CommonTaskModel:
49
49
  upstreams: Iterable[AnyTask] = [],
50
50
  fallbacks: Iterable[AnyTask] = [],
51
51
  checkers: Iterable[AnyTask] = [],
52
- checking_interval: Union[float, int] = 0,
52
+ checking_interval: Union[float, int] = 0.05,
53
53
  run: Optional[Callable[..., Any]] = None,
54
54
  on_triggered: Optional[OnTriggered] = None,
55
55
  on_waiting: Optional[OnWaiting] = None,
@@ -2,7 +2,7 @@ import asyncio
2
2
  import time
3
3
 
4
4
  from zrb.helper.typecheck import typechecked
5
- from zrb.helper.typing import Optional
5
+ from zrb.helper.typing import Optional, Union
6
6
 
7
7
  LOG_NAME_LENGTH = 20
8
8
 
@@ -51,9 +51,10 @@ class AttemptTracker:
51
51
 
52
52
  @typechecked
53
53
  class FinishTracker:
54
- def __init__(self):
54
+ def __init__(self, checking_interval: Union[float, int]):
55
55
  self.__execution_queue: Optional[asyncio.Queue] = None
56
56
  self.__counter = 0
57
+ self.__checking_interval = checking_interval
57
58
 
58
59
  async def _mark_awaited(self):
59
60
  if self.__execution_queue is None:
@@ -69,5 +70,5 @@ class FinishTracker:
69
70
 
70
71
  async def _is_done(self) -> bool:
71
72
  while self.__execution_queue is None:
72
- await asyncio.sleep(0.05)
73
+ await asyncio.sleep(self.__checking_interval / 2.0)
73
74
  return await self.__execution_queue.get()
zrb/task/checker.py CHANGED
@@ -44,7 +44,7 @@ class Checker(BaseTask):
44
44
  on_ready: Optional[OnReady] = None,
45
45
  on_retry: Optional[OnRetry] = None,
46
46
  on_failed: Optional[OnFailed] = None,
47
- checking_interval: Union[int, float] = 0.1,
47
+ checking_interval: Union[int, float] = 0.05,
48
48
  progress_interval: Union[int, float] = 30,
49
49
  expected_result: bool = True,
50
50
  should_execute: Union[bool, str, Callable[..., bool]] = True,
zrb/task/cmd_task.py CHANGED
@@ -125,7 +125,7 @@ class CmdTask(BaseTask):
125
125
  on_retry: Optional[OnRetry] = None,
126
126
  on_failed: Optional[OnFailed] = None,
127
127
  checkers: Iterable[AnyTask] = [],
128
- checking_interval: Union[float, int] = 0,
128
+ checking_interval: Union[float, int] = 0.05,
129
129
  retry: int = 2,
130
130
  retry_interval: Union[float, int] = 1,
131
131
  max_output_line: int = 1000,
zrb/task/decorator.py CHANGED
@@ -43,9 +43,9 @@ def python_task(
43
43
  on_retry: Optional[OnRetry] = None,
44
44
  on_failed: Optional[OnFailed] = None,
45
45
  checkers: Iterable[AnyTask] = [],
46
- checking_interval: float = 0.1,
46
+ checking_interval: Union[float, int] = 0.05,
47
47
  retry: int = 2,
48
- retry_interval: float = 1,
48
+ retry_interval: Union[float, int] = 1,
49
49
  should_execute: Union[bool, str, Callable[..., bool]] = True,
50
50
  return_upstream_result: bool = False,
51
51
  runner: Optional[Runner] = None,
@@ -115,9 +115,9 @@ class DockerComposeTask(CmdTask):
115
115
  on_retry: Optional[OnRetry] = None,
116
116
  on_failed: Optional[OnFailed] = None,
117
117
  checkers: Iterable[AnyTask] = [],
118
- checking_interval: float = 0.1,
118
+ checking_interval: Union[float, int] = 0.05,
119
119
  retry: int = 2,
120
- retry_interval: float = 1,
120
+ retry_interval: Union[float, int] = 1,
121
121
  max_output_line: int = 1000,
122
122
  max_error_line: int = 1000,
123
123
  preexec_fn: Optional[Callable[[], Any]] = os.setsid,
zrb/task/flow_task.py CHANGED
@@ -45,27 +45,13 @@ class FlowTask(BaseTask):
45
45
  on_retry: Optional[OnRetry] = None,
46
46
  on_failed: Optional[OnFailed] = None,
47
47
  checkers: Iterable[AnyTask] = [],
48
- checking_interval: float = 0,
48
+ checking_interval: Union[float, int] = 0.05,
49
49
  retry: int = 2,
50
- retry_interval: float = 1,
50
+ retry_interval: Union[float, int] = 1,
51
51
  steps: List[Union[AnyTask, List[AnyTask]]] = [],
52
52
  should_execute: Union[bool, str, Callable[..., bool]] = True,
53
53
  return_upstream_result: bool = False,
54
54
  ):
55
- final_upstreams: List[AnyTask] = list(upstreams)
56
- inputs: List[AnyInput] = list(inputs)
57
- envs: List[Env] = list(envs)
58
- env_files: List[EnvFile] = list(env_files)
59
- for step in steps:
60
- tasks = self._step_to_tasks(step)
61
- new_upstreams = self._get_embeded_tasks(
62
- tasks=tasks,
63
- upstreams=final_upstreams,
64
- inputs=inputs,
65
- envs=envs,
66
- env_files=env_files,
67
- )
68
- final_upstreams = new_upstreams
69
55
  BaseTask.__init__(
70
56
  self,
71
57
  name=name,
@@ -76,7 +62,13 @@ class FlowTask(BaseTask):
76
62
  icon=icon,
77
63
  color=color,
78
64
  description=description,
79
- upstreams=final_upstreams,
65
+ upstreams=self._create_flow_upstreams(
66
+ steps=steps,
67
+ upstreams=list(upstreams),
68
+ inputs=list(inputs),
69
+ envs=list(envs),
70
+ env_files=list(env_files),
71
+ ),
80
72
  fallbacks=fallbacks,
81
73
  on_triggered=on_triggered,
82
74
  on_waiting=on_waiting,
@@ -97,12 +89,33 @@ class FlowTask(BaseTask):
97
89
  def copy(self) -> TFlowTask:
98
90
  return super().copy()
99
91
 
100
- def _step_to_tasks(self, node: Union[AnyTask, List[AnyTask]]) -> List[AnyTask]:
101
- if isinstance(node, AnyTask):
102
- return [node]
103
- return node
92
+ def _create_flow_upstreams(
93
+ self,
94
+ steps: List[Union[AnyTask, List[AnyTask]]],
95
+ upstreams: List[AnyTask],
96
+ inputs: List[AnyInput],
97
+ envs: List[Env],
98
+ env_files: List[EnvFile],
99
+ ) -> List[AnyTask]:
100
+ flow_upstreams = upstreams
101
+ for step in steps:
102
+ tasks = [task.copy() for task in self._step_to_tasks(step)]
103
+ new_upstreams = self._create_embeded_tasks(
104
+ tasks=tasks,
105
+ upstreams=flow_upstreams,
106
+ inputs=inputs,
107
+ envs=envs,
108
+ env_files=env_files,
109
+ )
110
+ flow_upstreams = new_upstreams
111
+ return flow_upstreams
112
+
113
+ def _step_to_tasks(self, step: Union[AnyTask, List[AnyTask]]) -> List[AnyTask]:
114
+ if isinstance(step, AnyTask):
115
+ return [step]
116
+ return step
104
117
 
105
- def _get_embeded_tasks(
118
+ def _create_embeded_tasks(
106
119
  self,
107
120
  tasks: List[AnyTask],
108
121
  upstreams: List[AnyTask],
@@ -111,28 +124,26 @@ class FlowTask(BaseTask):
111
124
  env_files: List[EnvFile],
112
125
  ) -> List[AnyTask]:
113
126
  embeded_tasks: List[AnyTask] = []
114
- for task in tasks:
115
- embeded_task = task.copy()
116
- embeded_task_root_upstreams = self._get_root_upstreams(tasks=[embeded_task])
117
- for embeded_task_root_upstream in embeded_task_root_upstreams:
118
- embeded_task_root_upstream.add_upstream(*upstreams)
119
- # embeded_task.add_upstream(*upstreams)
127
+ for embeded_task in tasks:
128
+ embeded_task_upstreams = self._get_all_upstreams(tasks=[embeded_task])
129
+ for embeded_task_upstream in embeded_task_upstreams:
130
+ embeded_task_upstream.add_upstream(*upstreams)
120
131
  embeded_task.add_env(*envs)
121
132
  embeded_task.add_env_file(*env_files)
122
133
  embeded_task.add_input(*inputs)
123
134
  embeded_tasks.append(embeded_task)
124
135
  return embeded_tasks
125
136
 
126
- def _get_root_upstreams(self, tasks: List[AnyTask]):
127
- root_upstreams = []
137
+ def _get_all_upstreams(self, tasks: List[AnyTask]):
138
+ all_upstreams = []
128
139
  for task in tasks:
129
140
  upstreams = task._get_upstreams()
130
141
  if len(upstreams) == 0:
131
- root_upstreams.append(task)
142
+ all_upstreams.append(task)
132
143
  continue
133
144
  for upstream in upstreams:
134
145
  if len(upstream._get_upstreams()) == 0:
135
- root_upstreams.append(upstream)
146
+ all_upstreams.append(upstream)
136
147
  continue
137
- root_upstreams += self._get_root_upstreams([upstream])
138
- return root_upstreams
148
+ all_upstreams += self._get_all_upstreams([upstream])
149
+ return all_upstreams
zrb/task/http_checker.py CHANGED
@@ -87,7 +87,7 @@ class HTTPChecker(Checker):
87
87
  on_ready: Optional[OnReady] = None,
88
88
  on_retry: Optional[OnRetry] = None,
89
89
  on_failed: Optional[OnFailed] = None,
90
- checking_interval: Union[int, float] = 0.1,
90
+ checking_interval: Union[int, float] = 0.05,
91
91
  progress_interval: Union[int, float] = 5,
92
92
  expected_result: bool = True,
93
93
  should_execute: Union[bool, JinjaTemplate, Callable[..., bool]] = True,
zrb/task/looper.py CHANGED
@@ -32,8 +32,11 @@ class Looper:
32
32
  if result is not None:
33
33
  if not result:
34
34
  continue
35
+ while len(self._queue[identifier]) > 1000:
36
+ self._queue[identifier].pop(0)
35
37
  self._queue[identifier].append(result)
36
38
  except KeyboardInterrupt:
39
+ self.stop()
37
40
  break
38
41
 
39
42
 
zrb/task/notifier.py CHANGED
@@ -58,7 +58,7 @@ class Notifier(BaseTask):
58
58
  on_ready: Optional[OnReady] = None,
59
59
  on_retry: Optional[OnRetry] = None,
60
60
  on_failed: Optional[OnFailed] = None,
61
- checking_interval: Union[int, float] = 0,
61
+ checking_interval: Union[int, float] = 0.05,
62
62
  retry: int = 2,
63
63
  retry_interval: Union[float, int] = 1,
64
64
  should_execute: Union[bool, str, Callable[..., bool]] = True,
zrb/task/path_checker.py CHANGED
@@ -56,7 +56,7 @@ class PathChecker(Checker):
56
56
  on_failed: Optional[OnFailed] = None,
57
57
  path: JinjaTemplate = "",
58
58
  ignored_path: Union[JinjaTemplate, Iterable[JinjaTemplate]] = [],
59
- checking_interval: Union[int, float] = 0.1,
59
+ checking_interval: Union[int, float] = 0.05,
60
60
  progress_interval: Union[int, float] = 5,
61
61
  expected_result: bool = True,
62
62
  should_execute: Union[bool, JinjaTemplate, Callable[..., bool]] = True,
zrb/task/path_watcher.py CHANGED
@@ -72,7 +72,7 @@ class PathWatcher(Watcher):
72
72
  on_failed: Optional[OnFailed] = None,
73
73
  path: JinjaTemplate = "",
74
74
  ignored_path: Union[JinjaTemplate, Iterable[JinjaTemplate]] = [],
75
- checking_interval: Union[int, float] = 0.1,
75
+ checking_interval: Union[int, float] = 0.05,
76
76
  progress_interval: Union[int, float] = 30,
77
77
  watch_new_files: bool = True,
78
78
  watch_modified_files: bool = True,
zrb/task/port_checker.py CHANGED
@@ -66,7 +66,7 @@ class PortChecker(Checker):
66
66
  on_ready: Optional[OnReady] = None,
67
67
  on_retry: Optional[OnRetry] = None,
68
68
  on_failed: Optional[OnFailed] = None,
69
- checking_interval: Union[int, float] = 0.1,
69
+ checking_interval: Union[int, float] = 0.05,
70
70
  progress_interval: Union[int, float] = 5,
71
71
  expected_result: bool = True,
72
72
  should_execute: Union[bool, str, Callable[..., bool]] = True,
@@ -76,7 +76,7 @@ class RecurringTask(BaseTask):
76
76
  on_retry: Optional[OnRetry] = None,
77
77
  on_failed: Optional[OnFailed] = None,
78
78
  checkers: Iterable[AnyTask] = [],
79
- checking_interval: float = 0,
79
+ checking_interval: float = 0.05,
80
80
  retry: int = 0,
81
81
  retry_interval: float = 1,
82
82
  should_execute: Union[bool, str, Callable[..., bool]] = True,
@@ -185,7 +185,7 @@ class RecurringTask(BaseTask):
185
185
  async def __run_from_queue(self):
186
186
  while True:
187
187
  if len(self._run_configs) == 0:
188
- await asyncio.sleep(0.1)
188
+ await asyncio.sleep(0.05)
189
189
  continue
190
190
  if self._single_execution:
191
191
  # Drain the queue, leave only the latest task
@@ -66,7 +66,7 @@ class RemoteCmdTask(BaseRemoteCmdTask):
66
66
  on_retry: Optional[OnRetry] = None,
67
67
  on_failed: Optional[OnFailed] = None,
68
68
  checkers: Iterable[AnyTask] = [],
69
- checking_interval: Union[float, int] = 0,
69
+ checking_interval: Union[float, int] = 0.05,
70
70
  retry: int = 2,
71
71
  retry_interval: Union[float, int] = 1,
72
72
  max_output_line: int = 1000,
@@ -123,7 +123,7 @@ class ResourceMaker(BaseTask):
123
123
  on_retry=on_retry,
124
124
  on_failed=on_failed,
125
125
  checkers=[],
126
- checking_interval=0.1,
126
+ checking_interval=0.05,
127
127
  retry=0,
128
128
  retry_interval=0,
129
129
  should_execute=should_execute,
zrb/task/rsync_task.py CHANGED
@@ -48,8 +48,8 @@ class RsyncTask(BaseRemoteCmdTask):
48
48
  remote_configs: Iterable[RemoteConfig],
49
49
  src: JinjaTemplate,
50
50
  dst: JinjaTemplate,
51
- is_remote_src: bool = False,
52
- is_remote_dst: bool = True,
51
+ src_is_remote: bool = False,
52
+ dst_is_remote: bool = True,
53
53
  group: Optional[Group] = None,
54
54
  inputs: Iterable[AnyInput] = [],
55
55
  envs: Iterable[Env] = [],
@@ -69,7 +69,7 @@ class RsyncTask(BaseRemoteCmdTask):
69
69
  on_retry: Optional[OnRetry] = None,
70
70
  on_failed: Optional[OnFailed] = None,
71
71
  checkers: Iterable[AnyTask] = [],
72
- checking_interval: Union[float, int] = 0,
72
+ checking_interval: Union[float, int] = 0.05,
73
73
  retry: int = 2,
74
74
  retry_interval: Union[float, int] = 1,
75
75
  max_output_line: int = 1000,
@@ -77,8 +77,8 @@ class RsyncTask(BaseRemoteCmdTask):
77
77
  preexec_fn: Optional[Callable[[], Any]] = os.setsid,
78
78
  should_execute: Union[bool, str, Callable[..., bool]] = True,
79
79
  ):
80
- parsed_src = self._get_parsed_path(is_remote_src, src)
81
- parsed_dst = self._get_parsed_path(is_remote_dst, dst)
80
+ parsed_src = self._get_parsed_path(src_is_remote, src)
81
+ parsed_dst = self._get_parsed_path(dst_is_remote, dst)
82
82
  cmd = f'auth_rsync "{parsed_src}" "{parsed_dst}"'
83
83
  BaseRemoteCmdTask.__init__(
84
84
  self,
zrb/task/server.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import copy
2
3
 
3
4
  from zrb.helper.accessories.color import colored
4
5
  from zrb.helper.accessories.name import get_random_name
@@ -35,8 +36,8 @@ class Controller:
35
36
  name: Optional[str] = None,
36
37
  ):
37
38
  self._name = get_random_name() if name is None else name
38
- self._triggers = [trigger] if isinstance(trigger, AnyTask) else trigger
39
- self._actions = [action] if isinstance(action, AnyTask) else action
39
+ self._triggers = self._to_task_list(trigger)
40
+ self._actions = self._to_task_list(action)
40
41
  self._args: List[Any] = []
41
42
  self._kwargs: Mapping[str, Any] = {}
42
43
  self._inputs: List[AnyInput] = []
@@ -58,39 +59,45 @@ class Controller:
58
59
  def set_env_files(self, env_files: List[EnvFile]):
59
60
  self._env_files = env_files
60
61
 
61
- def get_sub_env_files(self) -> Iterable[EnvFile]:
62
+ def get_original_env_files(self) -> Iterable[EnvFile]:
62
63
  env_files = []
63
64
  for trigger in self._triggers:
64
- env_files += trigger.copy()._get_env_files()
65
+ env_files += trigger._get_env_files()
65
66
  for action in self._actions:
66
- env_files += action.copy()._get_env_files()
67
+ env_files += action._get_env_files()
67
68
  return env_files
68
69
 
69
- def get_sub_envs(self) -> Iterable[Env]:
70
+ def get_original_envs(self) -> Iterable[Env]:
70
71
  envs = []
71
72
  for trigger in self._triggers:
72
- envs += trigger.copy()._get_envs()
73
+ envs += trigger._get_envs()
73
74
  for action in self._actions:
74
- envs += action.copy()._get_envs()
75
+ envs += action._get_envs()
75
76
  return envs
76
77
 
77
- def get_sub_inputs(self) -> Iterable[AnyInput]:
78
+ def get_original_inputs(self) -> Iterable[AnyInput]:
78
79
  inputs = []
79
80
  for trigger in self._triggers:
80
- inputs += trigger.copy()._get_combined_inputs()
81
+ inputs += trigger._get_combined_inputs()
81
82
  for action in self._actions:
82
- inputs += action.copy()._get_combined_inputs()
83
+ inputs += action._get_combined_inputs()
83
84
  return inputs
84
85
 
85
86
  def to_function(self) -> Callable[..., Any]:
86
87
  task = self._get_task()
87
88
 
88
89
  async def fn() -> Any:
90
+ task.print_out_dark(f"Starting controller: {self._name}")
89
91
  task_fn = task.to_function(is_async=True)
90
92
  return await task_fn(*self._args, **self._kwargs)
91
93
 
92
94
  return fn
93
95
 
96
+ def _to_task_list(self, tasks: Union[AnyTask, List[AnyTask]]) -> List[AnyTask]:
97
+ if isinstance(tasks, AnyTask):
98
+ return [tasks.copy()]
99
+ return [task.copy() for task in tasks]
100
+
94
101
  def _get_task(self) -> AnyTask:
95
102
  actions = [action.copy() for action in self._actions]
96
103
  actions.insert(0, self._get_remonitor_task())
@@ -122,7 +129,7 @@ class Server(BaseTask):
122
129
  def __init__(
123
130
  self,
124
131
  name: str,
125
- controllers: List[Controller],
132
+ controllers: Iterable[Controller],
126
133
  group: Optional[Group] = None,
127
134
  inputs: Iterable[AnyInput] = [],
128
135
  envs: Iterable[Env] = [],
@@ -140,17 +147,18 @@ class Server(BaseTask):
140
147
  on_retry: Optional[OnRetry] = None,
141
148
  on_failed: Optional[OnFailed] = None,
142
149
  checkers: Iterable[AnyTask] = [],
143
- checking_interval: float = 0,
150
+ checking_interval: Union[int, float] = 0.05,
144
151
  retry: int = 0,
145
- retry_interval: float = 1,
152
+ retry_interval: Union[int, float] = 1,
146
153
  should_execute: Union[bool, str, Callable[..., bool]] = True,
147
154
  return_upstream_result: bool = False,
148
155
  ):
149
156
  inputs, envs, env_files = list(inputs), list(envs), list(env_files)
150
157
  for controller in controllers:
151
- inputs += controller.get_sub_inputs()
152
- envs += controller.get_sub_envs()
153
- env_files += controller.get_sub_env_files()
158
+ controller_cp = copy.deepcopy(controller)
159
+ inputs += controller_cp.get_original_inputs()
160
+ envs += controller_cp.get_original_envs()
161
+ env_files += controller_cp.get_original_env_files()
154
162
  BaseTask.__init__(
155
163
  self,
156
164
  name=name,
@@ -177,7 +185,7 @@ class Server(BaseTask):
177
185
  should_execute=should_execute,
178
186
  return_upstream_result=return_upstream_result,
179
187
  )
180
- self._controllers = controllers
188
+ self._controllers = list(controllers)
181
189
 
182
190
  async def run(self, *args: Any, **kwargs: Any):
183
191
  for controller in self._controllers:
zrb/task/task.py CHANGED
@@ -9,7 +9,41 @@ logger.debug(colored("Loading zrb.task.task", attrs=["dark"]))
9
9
  @typechecked
10
10
  class Task(BaseTask):
11
11
  """
12
- Alias for BaseTask.
12
+ Task is the smallest Zrb automation unit.
13
+
14
+ You can configure a Task by using several interfaces:
15
+ - `inputs`: interfaces to read user input at the beginning of the execution.
16
+ - `envs`: interfaces to read and use OS Environment Variables.
17
+ - `env_files`: interfaces to read and use Environment Files.
18
+
19
+ Moreover, you can define Task dependencies by specifying its `upstreams` or by using shift-right operator.
20
+
21
+ Every Zrb Task has its life-cycle state:
22
+ - `Triggered`: The Task is triggered (either by the user or by the other Task).
23
+ - `Waiting`: Zrb has already triggered the Task. The Task is now waiting for all its upstreams to be ready.
24
+ - `Skipped`: Task upstreams are ready, but the Task is not executed and will immediately enter the `Ready` state.
25
+ - `Started`: The upstreams are ready, and Zrb is now starting the Task execution.
26
+ - `Failed`: Zrb failed to execute the Task. It will enter the `Retry` state if the current attempt does not exceed the maximum attempt.
27
+ - `Retry`: The task has already entered the `Failed` state. Now, Zrb will try to start the Task execution.
28
+ - `Ready`: The task is ready.
29
+
30
+ There are several configurations related to Task's life cycle:
31
+ - `retry`: Maximum retry attempt.
32
+ - `retry_interval`: The duration is to wait before Zrb starts the next attempt.
33
+ - `fallbacks`: Action to take if the Task has failed for good.
34
+ - `checkers`: How to determine if a Task is `Ready`.
35
+ - `checking_interval`: The duration to wait before Zrb checks for the Task's readiness.
36
+ - `run`: Action to do when Zrb executes the Task.
37
+ - `on_triggered`: Action to do when a Task is `Triggered`.
38
+ - `on_waiting`: Action to do when a Task is `Waiting`.
39
+ - `on_skipped`: Action to do when a Task is `Skipped`.
40
+ - `on_started`: Action to do when a Task is `Started`.
41
+ - `on_ready`: Action to do when a Task is `Ready`.
42
+ - `on_retry`: Action to do when a Task is `Retry`.
43
+ - `on_failed`: Action to do when a Task is `Failed`.
44
+ - `should_execute`: Condition to determine whether a Task should be `Started` or `Skipped`.
45
+
46
+ Finally, you can put related Tasks under the same `group`.
13
47
  """
14
48
 
15
49
  pass
zrb/task/time_watcher.py CHANGED
@@ -68,7 +68,7 @@ class TimeWatcher(Watcher):
68
68
  on_retry: Optional[OnRetry] = None,
69
69
  on_failed: Optional[OnFailed] = None,
70
70
  schedule: JinjaTemplate = "",
71
- checking_interval: Union[int, float] = 1,
71
+ checking_interval: Union[int, float] = 0.05,
72
72
  progress_interval: Union[int, float] = 30,
73
73
  should_execute: Union[bool, JinjaTemplate, Callable[..., bool]] = True,
74
74
  ):
@@ -128,7 +128,7 @@ class TimeWatcher(Watcher):
128
128
 
129
129
  def create_loop_inspector(self) -> Callable[..., Optional[bool]]:
130
130
  async def loop_inspect() -> bool:
131
- await asyncio.sleep(0.1)
131
+ await asyncio.sleep(self._checking_interval)
132
132
  label = f"Watching {self._rendered_schedule}"
133
133
  identifier = self.get_identifier()
134
134
  scheduled_time = self.__scheduled_times[identifier]
@@ -148,7 +148,7 @@ class TimeWatcher(Watcher):
148
148
  return cron.get_next(datetime.datetime)
149
149
 
150
150
  def _get_cron(self) -> Any:
151
- margin = datetime.timedelta(seconds=0.001)
151
+ margin = datetime.timedelta(seconds=self._checking_interval / 2.0)
152
152
  slightly_before_now = datetime.datetime.now() - margin
153
153
  cron = croniter.croniter(self._rendered_schedule, slightly_before_now)
154
154
  return cron
zrb/task/watcher.py CHANGED
@@ -2,6 +2,7 @@ import asyncio
2
2
 
3
3
  from zrb.helper.accessories.color import colored
4
4
  from zrb.helper.accessories.name import get_random_name
5
+ from zrb.helper.callable import run_async
5
6
  from zrb.helper.log import logger
6
7
  from zrb.helper.typecheck import typechecked
7
8
  from zrb.helper.typing import Any, Callable, Iterable, Optional, Union
@@ -48,7 +49,7 @@ class Watcher(Checker):
48
49
  on_ready: Optional[OnReady] = None,
49
50
  on_retry: Optional[OnRetry] = None,
50
51
  on_failed: Optional[OnFailed] = None,
51
- checking_interval: Union[int, float] = 0.1,
52
+ checking_interval: Union[int, float] = 0.05,
52
53
  progress_interval: Union[int, float] = 30,
53
54
  expected_result: bool = True,
54
55
  should_execute: Union[bool, str, Callable[..., bool]] = True,
@@ -85,7 +86,9 @@ class Watcher(Checker):
85
86
  async def run(self, *args: Any, **kwargs: Any) -> bool:
86
87
  if not looper.is_registered(self._identifier):
87
88
  asyncio.create_task(
88
- looper.register(self._identifier, self.create_loop_inspector())
89
+ run_async(
90
+ looper.register, self._identifier, self.create_loop_inspector()
91
+ )
89
92
  )
90
93
  return await super().run(*args, **kwargs)
91
94
 
zrb/task_env/env.py CHANGED
@@ -15,8 +15,9 @@ logger.debug(colored("Loading zrb.task_env.env", attrs=["dark"]))
15
15
  @typechecked
16
16
  class Env:
17
17
  """
18
- Env Represents an environment configuration for a task, encapsulating details such as environment name, OS-specific
19
- environment name, default values, and rendering behavior.
18
+ Env is an interface for a Task to read and use OS Environment Variable.
19
+
20
+ Env encapsulating details such as environment name, OS environment name, default values, and rendering behavior.
20
21
 
21
22
  Attributes:
22
23
  name (str): Environment name as recognized by the task.
@@ -77,7 +78,7 @@ class Env:
77
78
 
78
79
  def should_render(self) -> bool:
79
80
  """
80
- Determines whether the environment value should be rendered.
81
+ Retrieves whether the environment value should be rendered.
81
82
 
82
83
  Returns:
83
84
  bool: True if the environment value should be rendered, False otherwise.
zrb/task_env/env_file.py CHANGED
@@ -16,8 +16,9 @@ logger.debug(colored("Loading zrb.task_env.env_file", attrs=["dark"]))
16
16
  @typechecked
17
17
  class EnvFile:
18
18
  """
19
- Represents a handler for an environment file, facilitating the creation and management of environment variables
20
- (Env objects) based on the contents of the specified environment file.
19
+ Env is an interface for a Task to read and use OS Environment File.
20
+
21
+ Under the hood, EnvFile creates list of Env based on its `path` property.
21
22
 
22
23
  Attributes:
23
24
  path (str): The path to the environment file.
zrb/task_group/group.py CHANGED
@@ -15,7 +15,7 @@ TGroup = TypeVar("TGroup", bound="Group")
15
15
  @typechecked
16
16
  class Group:
17
17
  """
18
- Represents a group of tasks and subgroups, facilitating organization and hierarchy.
18
+ Group is an umbrella of several related Tasks or sub-groups, facilitating organization and hierarchy.
19
19
 
20
20
  This class allows the creation of a hierarchical structure by grouping tasks and
21
21
  other task groups together. It provides methods to add tasks, retrieve tasks,
@@ -11,10 +11,12 @@ logger.debug(colored("Loading zrb.task_input.any_input", attrs=["dark"]))
11
11
 
12
12
  class AnyInput(ABC):
13
13
  """
14
- Abstract base class representing a generalized input specification.
15
- This class serves as a template for creating various input types,
16
- providing a standardized interface for input handling and processing.
14
+ Abstraction for Zrb Input.
17
15
 
16
+ This class acts as a template for creating new Inputs.
17
+
18
+ To define a new Input type, you should extend this class and implement all its methods.
19
+ The easiest way to do so is by extending `Input`
18
20
  """
19
21
 
20
22
  @abstractmethod
@@ -14,9 +14,9 @@ logger.debug(colored("Loading zrb.task_input.base_input", attrs=["dark"]))
14
14
  @typechecked
15
15
  class BaseInput(AnyInput):
16
16
  """
17
- A concrete implementation of the AnyInput abstract base class, representing a specific type of task input.
18
- This class allows for the creation of interactive and configurable inputs for tasks, with various attributes
19
- to customize its behavior and appearance.
17
+ Base class for all Input.
18
+
19
+ Input is an interface for a Task to read user input at the beginning of the execution.
20
20
 
21
21
  Attributes:
22
22
  name (str): The name of the input, used as a unique identifier.
@@ -9,7 +9,7 @@ logger.debug(colored("Loading zrb.task_input.task_input", attrs=["dark"]))
9
9
 
10
10
  class Input(BaseInput):
11
11
  """
12
- Alias for BaseInput
12
+ Input is an interface for a Task to read user input at the beginning of the execution.
13
13
 
14
14
  Attributes:
15
15
  name (str): The name of the input, used as a unique identifier.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: zrb
3
- Version: 0.17.0
3
+ Version: 0.18.0
4
4
  Summary: A Framework to Enhance Your Workflow
5
5
  Home-page: https://github.com/state-alchemists/zrb
6
6
  License: AGPL-3.0-or-later
@@ -33,7 +33,7 @@ Description-Content-Type: text/markdown
33
33
  ![](https://raw.githubusercontent.com/state-alchemists/zrb/main/_images/zrb/android-chrome-192x192.png)
34
34
 
35
35
 
36
- [🫰 Installation](https://github.com/state-alchemists/zrb/blob/main/docs/installation.md) | [📖 Documentation](https://github.com/state-alchemists/zrb/blob/main/docs/README.md) | [🏁 Getting Started](https://github.com/state-alchemists/zrb/blob/main/docs/getting-started.md) | [💃 Common Mistakes](https://github.com/state-alchemists/zrb/blob/main/docs/oops-i-did-it-again/README.md) | [❓ FAQ](https://github.com/state-alchemists/zrb/blob/main/docs/faq/README.md)
36
+ [🫰 Installation](https://github.com/state-alchemists/zrb/blob/main/docs/installation.md) | [📖 Documentation](https://github.com/state-alchemists/zrb/blob/main/docs/README.md) | [🏁 Getting Started](https://github.com/state-alchemists/zrb/blob/main/docs/getting-started.md) | [💃 Common Mistakes](https://github.com/state-alchemists/zrb/blob/main/docs/common-mistakes/README.md) | [❓ FAQ](https://github.com/state-alchemists/zrb/blob/main/docs/faq/README.md)
37
37
 
38
38
 
39
39
  # 🤖 Zrb: A Framework to Enhance Your Workflow
@@ -43,11 +43,12 @@ Zrb is a [CLI-based](https://en.wikipedia.org/wiki/Command-line_interface) autom
43
43
  - __Automate__ day-to-day tasks.
44
44
  - __Generate__ projects or applications.
45
45
  - __Prepare__, __run__, and __deploy__ your applications with a single command.
46
- - Etc.
46
+ - Deliver __faster__ with __fewer human errors__.
47
47
 
48
- You can also write custom task definitions in [Python](https://www.python.org/), enhancing Zrb's base capabilities. Defining your tasks in Zrb gives you several advantages because:
48
+ Zrb allows you to write custom task definitions in [Python](https://www.python.org/), further enhancing Zrb's capabilities. Defining your tasks in Zrb gives you several advantages because:
49
49
 
50
- - Every task has a __retry mechanism__.
50
+ - Every Zrb Task has a __retry mechanism__.
51
+ - Every Zrb Task is __configurable__ via __environment variables__ or __user inputs__.
51
52
  - Zrb handles your __task dependencies__ automatically.
52
53
  - Zrb runs your task dependencies __concurrently__.
53
54
 
@@ -85,7 +86,9 @@ Download Datasets ──┘
85
86
  ⬇️
86
87
  ```
87
88
 
88
- You can create a file named `zrb_init.py` and define the tasks as follows:
89
+ Zrb Task is the smallest automation unit. You can configure a Zrb Task using user input (`inputs`) or environment variables (`envs` or `env_files`). Every Zrb Task has a configurable `retry` mechanism. Moreover, you can also define Zrb Task dependencies using the shift right operator `>>` or `upstreams` parameter.
90
+
91
+ You can create a file named `zrb_init.py` and define your Zrb Tasks as follows:
89
92
 
90
93
  ```python
91
94
  # File name: zrb_init.py
@@ -129,7 +132,7 @@ def show_stats(*args, **kwargs):
129
132
  # Define dependencies: `show_stat` depends on both, `download_dataset` and `install_pandas`
130
133
  Parallel(download_dataset, install_pandas) >> show_stats
131
134
 
132
- # Register the tasks so that they are accessbie from the CLI
135
+ # Register the tasks so that they are accessble from the CLI
133
136
  runner.register(install_pandas, download_dataset, show_stats)
134
137
  ```
135
138
 
@@ -286,15 +289,15 @@ Visit [our tutorials](https://github.com/state-alchemists/zrb/blob/main/docs/tut
286
289
  - [🫰 Installation](https://github.com/state-alchemists/zrb/blob/main/docs/installation.md)
287
290
  - [🏁 Getting Started](https://github.com/state-alchemists/zrb/blob/main/docs/getting-started.md)
288
291
  - [📖 Documentation](https://github.com/state-alchemists/zrb/blob/main/docs/README.md)
289
- - [💃 Common Mistakes](https://github.com/state-alchemists/zrb/blob/main/docs/oops-i-did-it-again/README.md)
292
+ - [💃 Common Mistakes](https://github.com/state-alchemists/zrb/blob/main/docs/common-mistakes/README.md)
290
293
  - [❓ FAQ](https://github.com/state-alchemists/zrb/blob/main/docs/faq/README.md)
291
294
 
292
295
  # 🐞 Bug Report + Feature Request
293
296
 
294
- You can submit bug report and feature request by creating a new [issue](https://github.com/state-alchemists/zrb/issues) on Zrb Github Repositories. When reporting a bug or requesting a feature, please be sure to:
297
+ You can submit bug reports and feature requests by creating a new [issue](https://github.com/state-alchemists/zrb/issues) on Zrb's GitHub Repositories. When reporting a bug or requesting a feature, please be sure to:
295
298
 
296
299
  - Include the version of Zrb you are using (i.e., `zrb version`)
297
- - Tell us what you have try
300
+ - Tell us what you have tried
298
301
  - Tell us what you expect
299
302
  - Tell us what you get
300
303
 
@@ -326,7 +329,7 @@ We are thankful for the following libraries and services. They accelerate Zrb de
326
329
  - [Jsons](https://pypi.org.project/jsons/): Parse JSON. This package should be part of the standard library.
327
330
  - [Libcst](https://pypi.org/project/libcst/): Turn Python code into a Concrete Syntax Tree.
328
331
  - [Croniter](https://pypi.org/project/croniter/): Parse cron pattern.
329
- - [Flit](https://pypi.org/project/flit), [Twine](https://pypi.org/project/twine/), and many more. See the complete list of Zrb's requirements.txt
332
+ - [Poetry](https://pypi.org/project/poetry), [flake8](https://pypi.org/project/flake8/), [black](https://pypi.org/project/black), [isort](https://pypi.org/project/isort), and many more. See the complete list of Zrb's [pyproject.toml](https://github.com/state-alchemists/zrb/blob/main/pyproject.toml)
330
333
  - Services
331
334
  - [asciiflow.com](https://asciiflow.com/): Creates beautiful ASCII-based diagrams.
332
335
  - [emojipedia.org](https://emojipedia.org/): Find emoji.
@@ -27,7 +27,7 @@ zrb/builtin/devtool/install/gvm/download.sh,sha256=Z4IDsTS4gOeWiezhI-TrRv2nFgMsl
27
27
  zrb/builtin/devtool/install/gvm/finalize.sh,sha256=Han_IDq5XwxNsxyAVc99PoW3fdjTnYtv6rsr5KRhtEE,538
28
28
  zrb/builtin/devtool/install/gvm/gvm.py,sha256=jD5HzHA4eXHwFeSzKUVAp8PMXiibPNF_Rwq01NaEZIo,1693
29
29
  zrb/builtin/devtool/install/gvm/resource/config.sh,sha256=M_r6XjtoYZjx8rJaT3FIwVl3HUd7lJF5_KqUSEJQeo4,253
30
- zrb/builtin/devtool/install/helix/helix.py,sha256=W0Xm_eq0wIHcoq0CzG9WrQIH9tPByT-AZ-zi4Z72-U0,2098
30
+ zrb/builtin/devtool/install/helix/helix.py,sha256=XQzTbokyAXy9-UMBPp8woIOPNFF0vxWcrg0fr4mYj4E,2010
31
31
  zrb/builtin/devtool/install/helix/install-language-server.sh,sha256=ZkV_ARwhTnLjjbAhJe8Pvp1hyRYVn176DYwg7ObkQ1w,1040
32
32
  zrb/builtin/devtool/install/helix/install.sh,sha256=Dsg65aEnpU8YnlvHwiKoxRpj8Jo8j3mejB4bTi2eeKo,1375
33
33
  zrb/builtin/devtool/install/helix/resource/config.toml,sha256=35IwzDzXGfSnUH3O3nyd2IzDVOWyKqj6Kb3QuympXCE,305
@@ -965,7 +965,7 @@ zrb/builtin/project/add/fastapp/app/template/src/kebab-zrb-app-name/src/componen
965
965
  zrb/builtin/project/add/fastapp/app/template/src/kebab-zrb-app-name/src/component/repo/search_filter.py,sha256=lYquQLR4_qr43AlKGNBBNzcGOEOASQHteuLdiewOBoE,147
966
966
  zrb/builtin/project/add/fastapp/app/template/src/kebab-zrb-app-name/src/component/rpc/__init__.py,sha256=i5QOPDiwAwp0EskPSVwJDuVH6pq_DWRi3XN1hMFFw4A,244
967
967
  zrb/builtin/project/add/fastapp/app/template/src/kebab-zrb-app-name/src/component/rpc/messagebus/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
968
- zrb/builtin/project/add/fastapp/app/template/src/kebab-zrb-app-name/src/component/rpc/messagebus/caller.py,sha256=4C6bDNUm-cL-G_5R-GdaSCXht_bJM5q8lelnVftCdaQ,3201
968
+ zrb/builtin/project/add/fastapp/app/template/src/kebab-zrb-app-name/src/component/rpc/messagebus/caller.py,sha256=P90X3d6et457EwD4NohOsPNIcz9lFQmzqTbZubmR9aA,3202
969
969
  zrb/builtin/project/add/fastapp/app/template/src/kebab-zrb-app-name/src/component/rpc/messagebus/server.py,sha256=KFVErPpkRDetDokMAR16thaKtOTqk_xRp4lmM1iQNFo,2197
970
970
  zrb/builtin/project/add/fastapp/app/template/src/kebab-zrb-app-name/src/component/rpc/rpc.py,sha256=obJ8ksA9Io5nRpGoTQE506hj82dPwRZIUWTq2JEOLRo,1625
971
971
  zrb/builtin/project/add/fastapp/app/template/src/kebab-zrb-app-name/src/component/schema/__init__.py,sha256=g1CrqtQukcTOnNaKFLj6j-iVaNQ_cvpmdzROtbeteck,127
@@ -1355,46 +1355,46 @@ zrb/shell-scripts/notify.ps1,sha256=6_xPoIwuxARpYljcjVV-iRJS3gJqGfx-B6kj719cJ9o,
1355
1355
  zrb/shell-scripts/rsync-util.sh,sha256=QzdhSBvUNMxB4U2B4m0Dxg9czGckRjB7Vk4A1ObG0-k,353
1356
1356
  zrb/shell-scripts/ssh-util.sh,sha256=9lXDzw6oO8HuA4vdbfps_uQMMwKyNYX9fZkZgpK52g8,401
1357
1357
  zrb/task/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1358
- zrb/task/any_task.py,sha256=1jc0qVZs1yIMfGuInktgH7EqDbI8IqmsaSlH5VijThc,39435
1358
+ zrb/task/any_task.py,sha256=hrgsYikSe_C6JDSWeHNGCCvefNrkVioJWUunrqSFGLE,39199
1359
1359
  zrb/task/any_task_event_handler.py,sha256=AjTC6lIcprutRusNBGl83EifQe4TbZzxdlVIR4ndWN4,524
1360
- zrb/task/base_remote_cmd_task.py,sha256=tZi3jODMXfTkDAmWiFR2YdA-b4-TDTP1uLtO0ulmM34,10101
1360
+ zrb/task/base_remote_cmd_task.py,sha256=q2Kwo5OMahL5gPSxwp_9zZLYouFfFc6Ru_p6ApOI-pk,12124
1361
1361
  zrb/task/base_task/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1362
- zrb/task/base_task/base_task.py,sha256=qnQSG-nAppmm38JY11wC03-0mZSOjjk-XJ62leco2rw,20354
1362
+ zrb/task/base_task/base_task.py,sha256=IvOBbuzIHLWP3RrlkK0NZXa1L7XlO1m0uLEYIxJt8sU,20388
1363
1363
  zrb/task/base_task/component/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1364
- zrb/task/base_task/component/base_task_model.py,sha256=YlEuvYSzlrK83Kds07bu2drKSw3rKyl7qIR1qwIPNZk,10376
1365
- zrb/task/base_task/component/common_task_model.py,sha256=JoAAf8EvuPx31fCK_TBxX_TBeHZrZyFDjLeK8ts0w3A,12285
1364
+ zrb/task/base_task/component/base_task_model.py,sha256=i6TrtTusZ71ZnOnx8yM0aJl8uF6R1hKdAf62bEdpdCs,10379
1365
+ zrb/task/base_task/component/common_task_model.py,sha256=h81BGqplsaWJCQl0Zigl24LLkCUKPEfJikSbITREQnM,12288
1366
1366
  zrb/task/base_task/component/pid_model.py,sha256=RjJIqOpavucDssnd3q3gT4q8QnP8I9SUdlv1b9pR7kU,292
1367
1367
  zrb/task/base_task/component/renderer.py,sha256=9wP2IW811Ta81IoPWmeQ7yVc7eG-uaSnOVbEyeaOIuk,4439
1368
- zrb/task/base_task/component/trackers.py,sha256=gM9eOukMh6kvNJnRsHscQ_JN--Haa2YA4bIufqh8upE,1950
1369
- zrb/task/checker.py,sha256=fn6gmcCUHsar458NQ4Af4-kt2skcyibd0ewsPTRd5-w,3383
1370
- zrb/task/cmd_task.py,sha256=_wjF9MbKGA0EAr_df47AMC0lROCiaJTUyxT-AFQKKJo,14181
1371
- zrb/task/decorator.py,sha256=j62l7ITIRZtk_qE97d4naVl9gbbMoi2eEXOPOmYdF8M,3039
1372
- zrb/task/docker_compose_task.py,sha256=fnFEOAS9yEi2ve7pSzN9guXOVeYt8HYTdQvtjk-yoRQ,14886
1373
- zrb/task/flow_task.py,sha256=QIIZgq9C7e-kvTRJ0Y1Slb5AQyy15N9H4NZxdFR3FI8,4867
1374
- zrb/task/http_checker.py,sha256=DRIwXjC8StucHo2nP1duM3Ime3zzw1BQH8ib2bFZVmA,5692
1375
- zrb/task/looper.py,sha256=aU7hwXLlfZRhH4rlusIwsusz5fHEhaYfwEpRUVcHx4o,1284
1376
- zrb/task/notifier.py,sha256=19E4EcFgFZ0thU9p2P1dGUYR04721pa0K3lqsj6a4Xc,6217
1368
+ zrb/task/base_task/component/trackers.py,sha256=c5xhZ6agICxKPI5Va1sn66_9OqC92ebF5CNhcwVUNUE,2074
1369
+ zrb/task/checker.py,sha256=raYNBHgeyEqkyfBRsPPgSV7ukEfMlJOCUn97WQNl6mU,3384
1370
+ zrb/task/cmd_task.py,sha256=Bfu29x5Cy10gCazIItCEOzrafU7r7z5WlWS4_wD7Znk,14184
1371
+ zrb/task/decorator.py,sha256=stxrl6aXbuUDK83lVf8m8uni3Ii6egLl0TCR0vxslUQ,3064
1372
+ zrb/task/docker_compose_task.py,sha256=hUKF7W3GwxFuEWmlPPFxa7h8npEnig2sm7KjlidHFBI,14911
1373
+ zrb/task/flow_task.py,sha256=QBOoyIrqc6ToSf3RF8xu8h4yxCWCerUAu2Ba0GxAqgg,5147
1374
+ zrb/task/http_checker.py,sha256=y0cWa2t4YtGQr6FWno5sZ6Ej9gQiLDF-Z1kLU1rijRw,5693
1375
+ zrb/task/looper.py,sha256=0eM3wEIC_RbThg60MRbK4Az16vt81O5p12cORAYTfnI,1430
1376
+ zrb/task/notifier.py,sha256=GbvnndQ-3-1vfGpOIIQ0IMS1ddqDxzgTSYThs_zg1xU,6220
1377
1377
  zrb/task/parallel.py,sha256=-coMuiFlS29GpBgW6plPVaCLesgzzD0bYib29OvhXFg,1193
1378
- zrb/task/path_checker.py,sha256=yvMgGlmMQhlaX3Wlq5yI7-nzEpyCFoXHEOFOdlfI6-o,4673
1379
- zrb/task/path_watcher.py,sha256=UGaGYzWExoVQDK6smXEKsd0leO3FZOQYHnpHgZ9hiZU,7420
1380
- zrb/task/port_checker.py,sha256=IoVIP0QjxKz2SLgnK2GIaxn4WASk6ZKf4cQlKog0Fw8,4592
1381
- zrb/task/recurring_task.py,sha256=FNxV7n4h9AzUCU8oKXwAS_A9j1newS-esWjmMsC33vE,7430
1382
- zrb/task/remote_cmd_task.py,sha256=rmLB5uCcbbfZBy8-nAZI8mgnNd-J2d4SBemLEDwSlV4,3951
1383
- zrb/task/resource_maker.py,sha256=jQSO7PVIuTZi__JcrpRC4Ni_xmuJszJiMAxH_qfJPhs,7644
1384
- zrb/task/rsync_task.py,sha256=bgCeZQTG-4isvjZGGs_05oOEkkwGc930NTyuUYUm_cg,4187
1385
- zrb/task/server.py,sha256=CdzBkdWZvX1PTvloDrOXtCbAbMeK4KK75C33N_Wz3Sc,6408
1386
- zrb/task/task.py,sha256=dHv4cmnd0QFPT9PwrfmHpxTaXj86mm8xf7_jAj_engI,329
1387
- zrb/task/time_watcher.py,sha256=xx82w8ygpL-6pUbeuWjsxSVLSZhkWVWHsAoZUXPV7Jk,5075
1388
- zrb/task/watcher.py,sha256=m72YhUKtQsE4mZSm1y2MKiSdfj6HZo3rj9f3nQLU7Oo,3252
1378
+ zrb/task/path_checker.py,sha256=SHpwtlS5YbO1jFGNnP7SuswkuDNHMOy52Fr4IzdIG98,4674
1379
+ zrb/task/path_watcher.py,sha256=x2SdgdgaZm2wiAyfBJoMVa625ufHtf8oHKmKhhEqAQk,7421
1380
+ zrb/task/port_checker.py,sha256=Za02E0xzR7xvHEujxuPszaydJCpwl_hJa1S9U4j25XQ,4593
1381
+ zrb/task/recurring_task.py,sha256=6JN7YkAY-NQ_zU2EAzqaQAxvfkGssbxvzNtL_foNA3Q,7434
1382
+ zrb/task/remote_cmd_task.py,sha256=DzAt5Vq8YekqAfu9PflGP0XcdLWX_gLKI64dGVgtL6s,3954
1383
+ zrb/task/resource_maker.py,sha256=wvq8Gv11epA_OYeLY071hIfFYkJ0rMcmcnXUAbd3TRg,7645
1384
+ zrb/task/rsync_task.py,sha256=4CB4AEvf8RoZOV2eTpMIH2h-b3cH_AqmcI4fv1Gln40,4190
1385
+ zrb/task/server.py,sha256=inWjVd8rSrSaH6W-0hn2xYCeJVtlGM8eePZtGm8z-Hs,6731
1386
+ zrb/task/task.py,sha256=iHDyUMUh1uVGlMCQdVewdT3epCOHKNRKblC3G1WSKS0,2493
1387
+ zrb/task/time_watcher.py,sha256=D3_rr5Dlaw8xOg9iZMAGzPiEC2VDvIIvNTluQsZeBHo,5122
1388
+ zrb/task/watcher.py,sha256=7yDAS7kxST4Gd_PLyg_7XU6uXed5qbojE-B5X5z9EY4,3344
1389
1389
  zrb/task/wiki_task.py,sha256=Mcugk_6Pd7pzubi2ZP4eegJs8e9niYKh-9mCrNHXE_g,4330
1390
1390
  zrb/task_env/constant.py,sha256=ySdHv2dawWE-UoeBHl8FEOmrBl4vfkRI67TIBdkK6l8,220
1391
- zrb/task_env/env.py,sha256=C9IzavEtWlpap4C92XONi1ID-RK9gDDLQKl5rYGBsyc,5195
1392
- zrb/task_env/env_file.py,sha256=NkPOjxi8lrvBjB13kc9LU3ODvjGsdw3sSGOyC9-GnzE,3087
1391
+ zrb/task_env/env.py,sha256=WoxPlUgD33p6-y2EJU0oXAXBAnPL65k55hAbv6kO-Zs,5206
1392
+ zrb/task_env/env_file.py,sha256=D5cHJBY1DFcBa3KI3ujTe7Gxk7y_WB1zXCzkZNlDexE,3047
1393
1393
  zrb/task_group/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1394
- zrb/task_group/group.py,sha256=RmYpnh1UGiCi0vKYoqLwlCmJzC5-VKkMgDrVR2y3wsk,6091
1394
+ zrb/task_group/group.py,sha256=uF3tyLABTyyBNyE9FUCfJeYSDnaLFQCis3tuEn5FYho,6109
1395
1395
  zrb/task_input/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1396
- zrb/task_input/any_input.py,sha256=cZ2v8qjJ3B-L-jZgAz5EhpZii5sMoepwan5jAbJE0cM,2067
1397
- zrb/task_input/base_input.py,sha256=qNifwst3ArwAbQ9XBDoyo6whMeS2IP-VZ3Pw0vf8efE,5643
1396
+ zrb/task_input/any_input.py,sha256=-uOq1ONXdhW20FRkC3pzqX081tbfmWjvCc_-d-gCNGY,2087
1397
+ zrb/task_input/base_input.py,sha256=kxt2184s8ppC2dsa69J2uLUOUqsdCmXEJiNUIJxKcuQ,5495
1398
1398
  zrb/task_input/bool_input.py,sha256=xlWjqPvHi52TLiFP13wB1SayJlbzmxv31AS-sZJ0jw4,4040
1399
1399
  zrb/task_input/choice_input.py,sha256=GU28ATSayZsmqCW-euAPoWyA-Kh6d5Wf6FwWLXffk5w,4290
1400
1400
  zrb/task_input/constant.py,sha256=VEsnrI0BDdCJ1Z58EJgxXUhZBe5CA8TfURo0cNu5CaQ,200
@@ -1402,9 +1402,9 @@ zrb/task_input/float_input.py,sha256=rtaowHp8RpiQS8a24ULgtydRu6wkYT1Q7Q_IRcCPD7s
1402
1402
  zrb/task_input/int_input.py,sha256=d2fXcm5fCo09472eMAm6PdzLQD82ZBV9ARq5CjKepAo,4198
1403
1403
  zrb/task_input/password_input.py,sha256=g_g8ZWAzDaHx4h2EHY3UCGvTigC6esAUBzXU0T9nDUk,4192
1404
1404
  zrb/task_input/str_input.py,sha256=BNflOhrJvST9bWK0rGdCi7C7y-QDvHj9ISQMRmujIWU,4200
1405
- zrb/task_input/task_input.py,sha256=x1sGHsoSYAYMdQBrCLmcvZa_ZmGggMPj3goAQzewUKI,2181
1406
- zrb-0.17.0.dist-info/LICENSE,sha256=WfnGCl8G60EYOPAEkuc8C9m9pdXWDe08NsKj3TBbxsM,728
1407
- zrb-0.17.0.dist-info/METADATA,sha256=GEA_jDoWaQ2X5kQMiLZFHMRqnYczd1HK-S5g8QwPYfI,16460
1408
- zrb-0.17.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
1409
- zrb-0.17.0.dist-info/entry_points.txt,sha256=xTgXc1kBKYhJHEujdaSPHUcJT3-hbyP1mLgwkv-5sSk,40
1410
- zrb-0.17.0.dist-info/RECORD,,
1405
+ zrb/task_input/task_input.py,sha256=DcHgKie5Oo1sUxj41t1ZQjCIK1aAfTgGzaKr7_ap7ZI,2248
1406
+ zrb-0.18.0.dist-info/LICENSE,sha256=WfnGCl8G60EYOPAEkuc8C9m9pdXWDe08NsKj3TBbxsM,728
1407
+ zrb-0.18.0.dist-info/METADATA,sha256=qEsdwPUflSN8YiBhgngtzvaQLXQemLtihXgGeSpKyPk,17076
1408
+ zrb-0.18.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
1409
+ zrb-0.18.0.dist-info/entry_points.txt,sha256=xTgXc1kBKYhJHEujdaSPHUcJT3-hbyP1mLgwkv-5sSk,40
1410
+ zrb-0.18.0.dist-info/RECORD,,
File without changes
File without changes