zrb 0.9.2__py3-none-any.whl → 0.10.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 (55) hide show
  1. zrb/__init__.py +2 -0
  2. zrb/builtin/generator/docker_compose_task/template/src/kebab-zrb-task-name/image/Dockerfile +1 -0
  3. zrb/builtin/generator/docker_compose_task/template/src/kebab-zrb-task-name/image/pyproject.toml +1 -1
  4. zrb/builtin/generator/fastapp/add.py +17 -5
  5. zrb/builtin/generator/fastapp/template/src/kebab-zrb-app-name/loadtest/pyproject.toml +1 -1
  6. zrb/builtin/generator/fastapp/template/src/kebab-zrb-app-name/src/Dockerfile +1 -0
  7. zrb/builtin/generator/fastapp/template/src/kebab-zrb-app-name/src/config.py +3 -1
  8. zrb/builtin/generator/fastapp/template/src/kebab-zrb-app-name/src/module/auth/entity/group/api.py +67 -52
  9. zrb/builtin/generator/fastapp/template/src/kebab-zrb-app-name/src/module/auth/entity/permission/api.py +67 -54
  10. zrb/builtin/generator/fastapp/template/src/kebab-zrb-app-name/src/module/auth/entity/user/api.py +85 -67
  11. zrb/builtin/generator/fastapp/template/src/kebab-zrb-app-name/src/module/log/entity/activity/api.py +30 -23
  12. zrb/builtin/generator/fastapp/template/src/kebab-zrb-app-name/src/module/log/entity/activity/event.py +1 -3
  13. zrb/builtin/generator/fastapp/template/src/kebab-zrb-app-name/src/pyproject.toml +1 -1
  14. zrb/builtin/generator/fastapp/template/src/kebab-zrb-app-name/src/start.sh +20 -15
  15. zrb/builtin/generator/fastapp_crud/template/src/kebab-zrb-app-name/src/module/snake_zrb_module_name/entity/snake_zrb_entity_name/api.py +82 -58
  16. zrb/builtin/generator/fastapp_crud/template/src/kebab-zrb-app-name/src/module/snake_zrb_module_name/schema/snake_zrb_entity_name.py +1 -1
  17. zrb/builtin/generator/fastapp_field/helper.py +1 -1
  18. zrb/builtin/generator/pip_package/template/_automate/snake_zrb_package_name/cmd/publish.sh +1 -1
  19. zrb/builtin/generator/pip_package/template/_automate/snake_zrb_package_name/local.py +1 -9
  20. zrb/builtin/generator/pip_package/template/src/kebab-zrb-package-name/pyproject.toml +1 -1
  21. zrb/builtin/generator/plugin/template/_cmd/publish.sh +1 -1
  22. zrb/builtin/generator/plugin/template/pyproject.toml +1 -1
  23. zrb/builtin/generator/plugin/template/zrb_init.py +1 -9
  24. zrb/builtin/generator/project/template/pyproject.toml +1 -1
  25. zrb/builtin/generator/simple_python_app/template/src/kebab-zrb-app-name/src/Dockerfile +1 -0
  26. zrb/builtin/generator/simple_python_app/template/src/kebab-zrb-app-name/src/pyproject.toml +1 -1
  27. zrb/config/config.py +10 -7
  28. zrb/helper/accessories/name.py +60 -116
  29. zrb/helper/codemod/add_property_to_class.py +18 -1
  30. zrb/shell-scripts/ensure-podman-is-installed.sh +55 -0
  31. zrb/task/any_task.py +83 -0
  32. zrb/task/base_remote_cmd_task.py +2 -0
  33. zrb/task/base_task/base_task.py +53 -15
  34. zrb/task/base_task/component/base_task_model.py +2 -0
  35. zrb/task/base_task/component/common_task_model.py +26 -0
  36. zrb/task/checker.py +2 -0
  37. zrb/task/cmd_task.py +2 -0
  38. zrb/task/docker_compose_task.py +27 -21
  39. zrb/task/flow_task.py +2 -0
  40. zrb/task/http_checker.py +2 -0
  41. zrb/task/notifier.py +2 -0
  42. zrb/task/path_checker.py +2 -0
  43. zrb/task/path_watcher.py +2 -0
  44. zrb/task/port_checker.py +2 -0
  45. zrb/task/recurring_task.py +2 -0
  46. zrb/task/remote_cmd_task.py +2 -0
  47. zrb/task/resource_maker.py +2 -0
  48. zrb/task/rsync_task.py +2 -0
  49. zrb/task/time_watcher.py +2 -0
  50. zrb/task/wiki_task.py +119 -0
  51. {zrb-0.9.2.dist-info → zrb-0.10.0.dist-info}/METADATA +1 -1
  52. {zrb-0.9.2.dist-info → zrb-0.10.0.dist-info}/RECORD +55 -53
  53. {zrb-0.9.2.dist-info → zrb-0.10.0.dist-info}/LICENSE +0 -0
  54. {zrb-0.9.2.dist-info → zrb-0.10.0.dist-info}/WHEEL +0 -0
  55. {zrb-0.9.2.dist-info → zrb-0.10.0.dist-info}/entry_points.txt +0 -0
@@ -9,124 +9,68 @@ def get_random_name(
9
9
  separator: str = "-", add_random_digit: bool = True, digit_count: int = 4
10
10
  ) -> str:
11
11
  prefixes = [
12
- "albedo",
13
- "argent",
14
- "argentum",
15
- "aurora",
16
- "aurum",
17
- "azure",
18
- "basilisk",
19
- "cerulean",
20
- "chimeric",
21
- "citrin",
22
- "coral",
23
- "crimson",
24
- "diamond",
25
- "draco",
26
- "dragon",
27
- "emerald",
28
- "ethereal",
29
- "ferrum",
30
- "flammeus",
31
- "garnet",
32
- "glacial",
33
- "glimmering",
34
- "glistening",
35
- "golden",
36
- "helios",
37
- "igneous",
38
- "imperial",
39
- "jade",
40
- "luminous",
41
- "luna",
42
- "lunar",
43
- "mystic",
44
- "nephrite",
45
- "nocturnal",
46
- "obsidian",
47
- "opal",
48
- "pearl",
49
- "platinum",
50
- "prismatic",
51
- "ruby",
52
- "sapphire",
53
- "serpentine",
54
- "silver",
55
- "sol",
56
- "solar",
57
- "spiritual",
58
- "stellar",
59
- "tempest",
60
- "topaz",
61
- "turquoise",
62
- "verde",
63
- "vermillion",
64
- "vitreous",
65
- "zephyr",
66
- "zircon",
12
+ "bold",
13
+ "calm",
14
+ "dark",
15
+ "deep",
16
+ "fast",
17
+ "firm",
18
+ "glad",
19
+ "grey",
20
+ "hard",
21
+ "high",
22
+ "kind",
23
+ "late",
24
+ "lean",
25
+ "long",
26
+ "loud",
27
+ "mild",
28
+ "neat",
29
+ "pure",
30
+ "rare",
31
+ "rich",
32
+ "safe",
33
+ "slow",
34
+ "soft",
35
+ "tall",
36
+ "thin",
37
+ "trim",
38
+ "vast",
39
+ "warm",
40
+ "weak",
41
+ "wild",
67
42
  ]
68
43
  suffixes = [
69
- "aether",
70
- "albedo",
71
- "alchemy",
72
- "arcana",
73
- "aureum",
74
- "aetheris",
75
- "anima",
76
- "astralis",
77
- "caelestis",
78
- "chrysopoeia",
79
- "cosmicum",
80
- "crystallum",
81
- "deum",
82
- "divinitas",
83
- "draconis",
84
- "elementorum",
85
- "elixir",
86
- "essentia",
87
- "eternis",
88
- "ethereus",
89
- "fatum",
90
- "flamma",
91
- "fulgur",
92
- "hermetica",
93
- "ignis",
94
- "illuminationis",
95
- "imperium",
96
- "incantatum",
97
- "infinitum",
98
- "lapis",
99
- "lux",
100
- "magicae",
101
- "magnum",
102
- "materia",
103
- "metallum",
104
- "mysticum",
105
- "natura",
106
- "occultum",
107
- "omnipotentis",
108
- "opulentia",
109
- "philosophia",
110
- "philosophorum",
111
- "praeparatum",
112
- "praestantissimum",
113
- "prima",
114
- "primordium",
115
- "quintessentia",
116
- "regeneratio",
117
- "ritualis",
118
- "sanctum",
119
- "spiritus",
120
- "tenebris",
121
- "terra",
122
- "tinctura",
123
- "transmutationis",
124
- "universalis",
125
- "vapores",
126
- "venenum",
127
- "veritas",
128
- "vitae",
129
- "volatus",
44
+ "arch",
45
+ "area",
46
+ "atom",
47
+ "base",
48
+ "beam",
49
+ "bell",
50
+ "bolt",
51
+ "bone",
52
+ "bulk",
53
+ "bush",
54
+ "cell",
55
+ "chip",
56
+ "clay",
57
+ "coal",
58
+ "coil",
59
+ "cone",
60
+ "cube",
61
+ "disk",
62
+ "dust",
63
+ "face",
64
+ "film",
65
+ "foam",
66
+ "frog",
67
+ "fuel",
68
+ "gate",
69
+ "gear",
70
+ "hall",
71
+ "hand",
72
+ "horn",
73
+ "leaf",
130
74
  ]
131
75
  prefix = random.choice(prefixes)
132
76
  suffix = random.choice(suffixes)
@@ -65,7 +65,7 @@ def add_property_to_class(
65
65
  ) -> str:
66
66
  module = cst.parse_module(code)
67
67
  property_name_node = cst.Name(value=property_name)
68
- property_type_node = cst.Annotation(cst.Name(value=property_type))
68
+ property_type_node = _get_property_type_node(property_type)
69
69
  property_value_node = _get_property_value_node(property_value)
70
70
  transformed_module = module.visit(
71
71
  AddPropertyTransformer(
@@ -78,6 +78,23 @@ def add_property_to_class(
78
78
  return transformed_module.code
79
79
 
80
80
 
81
+ def _get_property_type_node(property_type: str) -> cst.Annotation:
82
+ if property_type.startswith("Optional[") and property_type.endswith("]"):
83
+ inner_type = property_type[len("Optional[") : -1]
84
+ return cst.Annotation(
85
+ annotation=cst.Subscript(
86
+ value=cst.Name("Optional"),
87
+ slice=[
88
+ cst.SubscriptElement(
89
+ slice=cst.Index(value=cst.Name(value=inner_type))
90
+ )
91
+ ],
92
+ )
93
+ )
94
+ else:
95
+ return cst.Annotation(cst.Name(value=property_type))
96
+
97
+
81
98
  def _get_property_value_node(
82
99
  property_value: Optional[str],
83
100
  ) -> Optional[cst.BaseExpression]:
@@ -0,0 +1,55 @@
1
+ set -e
2
+ if command_exists podman
3
+ then
4
+ log_info "Podman is already installed."
5
+ else
6
+ log_info "Installing Podman..."
7
+ if [ "$OS_TYPE" = "Darwin" ]
8
+ then
9
+ if command_exists brew
10
+ then
11
+ brew install --cask podman
12
+ log_info "Please start Podman before proceeding."
13
+ else
14
+ log_info "Homebrew not found. Please install Homebrew and try again."
15
+ exit 1
16
+ fi
17
+ elif [ "$OS_TYPE" = "Linux" ]
18
+ then
19
+ if command_exists apt
20
+ then
21
+ try_sudo apt update
22
+ try_sudo apt install -y podman
23
+ elif command_exists yum
24
+ then
25
+ try_sudo yum install -y podman
26
+ elif command_exists dnf
27
+ then
28
+ try_sudo dnf install -y podman
29
+ elif command_exists pacman
30
+ then
31
+ try_sudo pacman -Syu --noconfirm podman
32
+ else
33
+ log_info "No known package manager found. Please install Podman manually."
34
+ exit 1
35
+ fi
36
+ else
37
+ log_info "Unsupported OS type. Please install Podman manually."
38
+ exit 1
39
+ fi
40
+ fi
41
+
42
+ if ! command_exists podman-compose
43
+ then
44
+ log_info "Installing Podman Compose plugin..."
45
+ pip install podman-compose
46
+ fi
47
+
48
+ # Check Podman Compose plugin installation
49
+ if command_exists podman && command_exists podman-compose
50
+ then
51
+ log_info "Podman Compose plugin is already installed."
52
+ else
53
+ log_info "Podman Compose plugin is not installed or podman is not running. Please check your installation."
54
+ exit 1
55
+ fi
zrb/task/any_task.py CHANGED
@@ -384,6 +384,44 @@ class AnyTask(ABC):
384
384
  """
385
385
  pass
386
386
 
387
+ @abstractmethod
388
+ def insert_fallback(self, *fallbacks: TAnyTask):
389
+ """
390
+ Inserts one or more `AnyTask` instances at the beginning of the current task's fallback list.
391
+
392
+ This method is used to define dependencies for the current task. Tasks in the fallback list are executed when the task is failed.
393
+ Adding a task to the beginning of the list means it will be
394
+ executed earlier than those already in the list.
395
+
396
+ Args:
397
+ fallbacks (TAnyTask): One or more task instances to be added to the fallback list.
398
+
399
+ Examples:
400
+ >>> from zrb import Task
401
+ >>> task = Task(name='task')
402
+ >>> fallback_task = Task(name='fallback-task')
403
+ >>> task.insert_fallback(fallback_task)
404
+ """
405
+ pass
406
+
407
+ @abstractmethod
408
+ def add_fallback(self, *fallbacks: TAnyTask):
409
+ """
410
+ Adds one or more `AnyTask` instances to the end of the current task's fallback list.
411
+
412
+ This method appends tasks to the fallback list, indicating that these tasks should be executed when the task is failed.
413
+
414
+ Args:
415
+ fallbacks (TAnyTask): One or more task instances to be added to the fallback list.
416
+
417
+ Examples:
418
+ >>> from zrb import Task
419
+ >>> task = Task(name='task')
420
+ >>> fallback_task = Task(name='fallback-task')
421
+ >>> task.add_fallback(fallback_task)
422
+ """
423
+ pass
424
+
387
425
  @abstractmethod
388
426
  def add_upstream(self, *upstreams: TAnyTask):
389
427
  """
@@ -519,6 +557,20 @@ class AnyTask(ABC):
519
557
  """
520
558
  pass
521
559
 
560
+ @abstractmethod
561
+ def _lock_upstreams(self):
562
+ """
563
+ Lock upstreams so that it cannot be altered anymore
564
+ """
565
+ pass
566
+
567
+ @abstractmethod
568
+ def _lock_fallbacks(self):
569
+ """
570
+ Lock fallbacks so that it cannot be altered anymore
571
+ """
572
+ pass
573
+
522
574
  @abstractmethod
523
575
  def _set_execution_id(self, execution_id: str):
524
576
  """
@@ -885,6 +937,37 @@ class AnyTask(ABC):
885
937
  """
886
938
  pass
887
939
 
940
+ @abstractmethod
941
+ def inject_fallbacks(self):
942
+ """
943
+ Injects fallback tasks into the current task.
944
+
945
+ This method is used for programmatically adding fallback to the task.
946
+ fallback tasks are those that must be completed when the task is failed.
947
+ Override this method in subclasses to specify such dependencies.
948
+
949
+ Examples:
950
+ >>> from zrb import Task
951
+ >>> class MyTask(Task):
952
+ >>> def inject_fallbacks(self):
953
+ >>> self.add_fallback(another_task)
954
+ """
955
+ pass
956
+
957
+ @abstractmethod
958
+ def _get_fallbacks(self) -> Iterable[TAnyTask]:
959
+ """
960
+ Retrieves the fallback tasks of the current task.
961
+
962
+ An internal method to get the list of fallback tasks that have been set for the
963
+ task, either statically or through `inject_fallbacks`. This is essential for task
964
+ fallback scenario.
965
+
966
+ Returns:
967
+ Iterable[TAnyTask]: An iterable of fallback tasks.
968
+ """
969
+ pass
970
+
888
971
  @abstractmethod
889
972
  def _get_combined_inputs(self) -> Iterable[AnyInput]:
890
973
  """
@@ -78,6 +78,7 @@ class SingleBaseRemoteCmdTask(CmdTask):
78
78
  post_cmd_path: CmdVal = "",
79
79
  cwd: Optional[Union[str, pathlib.Path]] = None,
80
80
  upstreams: Iterable[AnyTask] = [],
81
+ fallbacks: Iterable[AnyTask] = [],
81
82
  on_triggered: Optional[OnTriggered] = None,
82
83
  on_waiting: Optional[OnWaiting] = None,
83
84
  on_skipped: Optional[OnSkipped] = None,
@@ -110,6 +111,7 @@ class SingleBaseRemoteCmdTask(CmdTask):
110
111
  cmd_path=cmd_path,
111
112
  cwd=cwd,
112
113
  upstreams=upstreams,
114
+ fallbacks=fallbacks,
113
115
  on_triggered=on_triggered,
114
116
  on_waiting=on_waiting,
115
117
  on_skipped=on_skipped,
@@ -39,6 +39,7 @@ class BaseTask(FinishTracker, AttemptTracker, Renderer, BaseTaskModel, AnyTask):
39
39
  """
40
40
 
41
41
  __xcom: Mapping[str, Mapping[str, str]] = {}
42
+ __running_tasks: List[AnyTask] = []
42
43
 
43
44
  def __init__(
44
45
  self,
@@ -53,6 +54,7 @@ class BaseTask(FinishTracker, AttemptTracker, Renderer, BaseTaskModel, AnyTask):
53
54
  retry: int = 2,
54
55
  retry_interval: Union[float, int] = 1,
55
56
  upstreams: Iterable[AnyTask] = [],
57
+ fallbacks: Iterable[AnyTask] = [],
56
58
  checkers: Iterable[AnyTask] = [],
57
59
  checking_interval: Union[float, int] = 0,
58
60
  run: Optional[Callable[..., Any]] = None,
@@ -87,6 +89,7 @@ class BaseTask(FinishTracker, AttemptTracker, Renderer, BaseTaskModel, AnyTask):
87
89
  retry=retry,
88
90
  retry_interval=retry_interval,
89
91
  upstreams=upstreams,
92
+ fallbacks=fallbacks,
90
93
  checkers=checkers,
91
94
  checking_interval=checking_interval,
92
95
  run=run,
@@ -257,7 +260,7 @@ class BaseTask(FinishTracker, AttemptTracker, Renderer, BaseTaskModel, AnyTask):
257
260
  except Exception as e:
258
261
  self.log_error(f"{e}")
259
262
  if raise_error:
260
- raise
263
+ raise e
261
264
  finally:
262
265
  if show_done_info:
263
266
  self._show_env_prefix()
@@ -303,7 +306,7 @@ class BaseTask(FinishTracker, AttemptTracker, Renderer, BaseTaskModel, AnyTask):
303
306
  self.log_debug("Waiting execution to be started")
304
307
  while not self.__is_execution_started:
305
308
  # Don't start checking before the execution itself has been started
306
- await asyncio.sleep(0.1)
309
+ await asyncio.sleep(0.05)
307
310
  check_coroutines: Iterable[asyncio.Task] = []
308
311
  for checker_task in self._get_checkers():
309
312
  checker_task._set_execution_id(self.get_execution_id())
@@ -334,15 +337,7 @@ class BaseTask(FinishTracker, AttemptTracker, Renderer, BaseTaskModel, AnyTask):
334
337
  await self.on_triggered()
335
338
  self.__is_execution_triggered = True
336
339
  await self.on_waiting()
337
- # get upstream checker
338
- upstream_check_processes: Iterable[asyncio.Task] = []
339
- self._lock_upstreams()
340
- for upstream_task in self._get_upstreams():
341
- upstream_check_processes.append(
342
- asyncio.create_task(upstream_task._loop_check())
343
- )
344
- # wait all upstream checkers to complete
345
- await asyncio.gather(*upstream_check_processes)
340
+ await self.__check_upstreams()
346
341
  # mark execution as started, so that checkers can start checking
347
342
  self.__is_execution_started = True
348
343
  local_kwargs = dict(kwargs)
@@ -353,27 +348,70 @@ class BaseTask(FinishTracker, AttemptTracker, Renderer, BaseTaskModel, AnyTask):
353
348
  return None
354
349
  # start running task
355
350
  result: Any = None
351
+ is_failed: bool = False
356
352
  while self._should_attempt():
357
353
  try:
358
354
  self.log_debug(f"Started with args: {args} and kwargs: {local_kwargs}")
359
355
  await self.on_started()
356
+ self.__running_tasks.append(self)
360
357
  result = await run_async(self.run, *args, **local_kwargs)
358
+ self.__running_tasks.remove(self)
361
359
  break
362
360
  except Exception as e:
363
- is_last_attempt = self._is_last_attempt()
364
- await self.on_failed(is_last_attempt, e)
365
- if is_last_attempt:
366
- raise e
367
361
  attempt = self._get_attempt()
368
362
  self.log_error(f"Encounter error on attempt {attempt}")
363
+ if self._is_last_attempt():
364
+ is_failed = True
365
+ raise e
366
+ self.__running_tasks.remove(self)
367
+ await self.on_failed(is_last_attempt=False, exception=e)
369
368
  self._increase_attempt()
370
369
  await asyncio.sleep(self._retry_interval)
371
370
  await self.on_retry()
371
+ finally:
372
+ if is_failed:
373
+ running_tasks = self.__running_tasks
374
+ self.__running_tasks = []
375
+ await self.__trigger_failure(running_tasks)
376
+ await self.__trigger_fallbacks(running_tasks, kwargs)
372
377
  self.set_xcom(self.get_name(), f"{result}")
373
378
  self.log_debug(f"XCom: {self.__xcom}")
374
379
  await self._mark_done()
375
380
  return result
376
381
 
382
+ async def __check_upstreams(self):
383
+ coroutines: Iterable[asyncio.Task] = []
384
+ self._lock_upstreams()
385
+ for upstream_task in self._get_upstreams():
386
+ coroutines.append(asyncio.create_task(upstream_task._loop_check()))
387
+ # wait all upstream checkers to complete
388
+ await asyncio.gather(*coroutines)
389
+
390
+ async def __trigger_failure(self, tasks: List[AnyTask]):
391
+ coroutines = [
392
+ task.on_failed(is_last_attempt=True, exception=Exception("canceled"))
393
+ for task in tasks
394
+ ]
395
+ await asyncio.gather(*coroutines)
396
+
397
+ async def __trigger_fallbacks(
398
+ self, tasks: List[AnyTask], kwargs: Mapping[str, Any]
399
+ ):
400
+ coroutines: Iterable[asyncio.Task] = []
401
+ for fallback in self.__get_all_fallbacks(tasks):
402
+ fallback._set_execution_id(self.get_execution_id())
403
+ coroutines.append(asyncio.create_task(fallback._run_all(**kwargs)))
404
+ await asyncio.gather(*coroutines)
405
+
406
+ def __get_all_fallbacks(self, tasks: List[AnyTask]) -> List[AnyTask]:
407
+ all_fallbacks: List[AnyTask] = []
408
+ for task in tasks:
409
+ task._lock_fallbacks()
410
+ for fallback in task._get_fallbacks():
411
+ if fallback not in all_fallbacks:
412
+ all_fallbacks.append(fallback)
413
+ return all_fallbacks
414
+
377
415
  async def _check_should_execute(self, *args: Any, **kwargs: Any) -> bool:
378
416
  if callable(self._should_execute):
379
417
  return await run_async(self._should_execute, *args, **kwargs)
@@ -56,6 +56,7 @@ class BaseTaskModel(CommonTaskModel, PidModel, TimeTracker):
56
56
  retry: int = 2,
57
57
  retry_interval: Union[int, float] = 1,
58
58
  upstreams: Iterable[AnyTask] = [],
59
+ fallbacks: Iterable[AnyTask] = [],
59
60
  checkers: Iterable[AnyTask] = [],
60
61
  checking_interval: Union[int, float] = 0,
61
62
  run: Optional[Callable[..., Any]] = None,
@@ -85,6 +86,7 @@ class BaseTaskModel(CommonTaskModel, PidModel, TimeTracker):
85
86
  retry=retry,
86
87
  retry_interval=retry_interval,
87
88
  upstreams=upstreams,
89
+ fallbacks=fallbacks,
88
90
  checkers=checkers,
89
91
  checking_interval=checking_interval,
90
92
  run=run,
@@ -47,6 +47,7 @@ class CommonTaskModel:
47
47
  retry: int = 2,
48
48
  retry_interval: Union[float, int] = 1,
49
49
  upstreams: Iterable[AnyTask] = [],
50
+ fallbacks: Iterable[AnyTask] = [],
50
51
  checkers: Iterable[AnyTask] = [],
51
52
  checking_interval: Union[float, int] = 0,
52
53
  run: Optional[Callable[..., Any]] = None,
@@ -73,6 +74,7 @@ class CommonTaskModel:
73
74
  self._retry = retry
74
75
  self._retry_interval = retry_interval
75
76
  self._upstreams = upstreams
77
+ self._fallbacks = fallbacks
76
78
  self._checkers = [checker.copy() for checker in checkers]
77
79
  self._checking_interval = checking_interval
78
80
  self._run_function: Optional[Callable[..., Any]] = run
@@ -90,16 +92,21 @@ class CommonTaskModel:
90
92
  self.__allow_add_env_files = True
91
93
  self.__allow_add_inputs = True
92
94
  self.__allow_add_upstreams: bool = True
95
+ self.__allow_add_fallbacks: bool = True
93
96
  self.__allow_add_checkers: bool = True
94
97
  self.__has_already_inject_env_files: bool = False
95
98
  self.__has_already_inject_envs: bool = False
96
99
  self.__has_already_inject_inputs: bool = False
97
100
  self.__has_already_inject_upstreams: bool = False
101
+ self.__has_already_inject_fallbacks: bool = False
98
102
  self.__all_inputs: Optional[List[AnyInput]] = None
99
103
 
100
104
  def _lock_upstreams(self):
101
105
  self.__allow_add_upstreams = False
102
106
 
107
+ def _lock_fallbacks(self):
108
+ self.__allow_add_fallbacks = False
109
+
103
110
  def _set_execution_id(self, execution_id: str):
104
111
  if self.__execution_id == "":
105
112
  self.__execution_id = execution_id
@@ -276,6 +283,25 @@ class CommonTaskModel:
276
283
  self.__has_already_inject_upstreams = True
277
284
  return list(self._upstreams)
278
285
 
286
+ def insert_fallback(self, *fallbacks: AnyTask):
287
+ if not self.__allow_add_fallbacks:
288
+ raise Exception(f"Cannot insert fallbacks to `{self.get_name()}`")
289
+ self._fallbacks = list(fallbacks) + list(self._fallbacks)
290
+
291
+ def add_fallback(self, *fallbacks: AnyTask):
292
+ if not self.__allow_add_fallbacks:
293
+ raise Exception(f"Cannot add fallbacks to `{self.get_name()}`")
294
+ self._fallbacks = list(self._fallbacks) + list(fallbacks)
295
+
296
+ def inject_fallbacks(self):
297
+ pass
298
+
299
+ def _get_fallbacks(self) -> List[AnyTask]:
300
+ if not self.__has_already_inject_fallbacks:
301
+ self.inject_fallbacks()
302
+ self.__has_already_inject_fallbacks = True
303
+ return list(self._fallbacks)
304
+
279
305
  def get_icon(self) -> str:
280
306
  return self._icon
281
307
 
zrb/task/checker.py CHANGED
@@ -32,6 +32,7 @@ class Checker(BaseTask):
32
32
  color: Optional[str] = None,
33
33
  description: str = "",
34
34
  upstreams: Iterable[AnyTask] = [],
35
+ fallbacks: Iterable[AnyTask] = [],
35
36
  on_triggered: Optional[OnTriggered] = None,
36
37
  on_waiting: Optional[OnWaiting] = None,
37
38
  on_skipped: Optional[OnSkipped] = None,
@@ -55,6 +56,7 @@ class Checker(BaseTask):
55
56
  color=color,
56
57
  description=description,
57
58
  upstreams=upstreams,
59
+ fallbacks=fallbacks,
58
60
  on_triggered=on_triggered,
59
61
  on_waiting=on_waiting,
60
62
  on_skipped=on_skipped,
zrb/task/cmd_task.py CHANGED
@@ -113,6 +113,7 @@ class CmdTask(BaseTask):
113
113
  cmd_path: CmdVal = "",
114
114
  cwd: Optional[Union[str, pathlib.Path]] = None,
115
115
  upstreams: Iterable[AnyTask] = [],
116
+ fallbacks: Iterable[AnyTask] = [],
116
117
  on_triggered: Optional[OnTriggered] = None,
117
118
  on_waiting: Optional[OnWaiting] = None,
118
119
  on_skipped: Optional[OnSkipped] = None,
@@ -141,6 +142,7 @@ class CmdTask(BaseTask):
141
142
  color=color,
142
143
  description=description,
143
144
  upstreams=upstreams,
145
+ fallbacks=fallbacks,
144
146
  on_triggered=on_triggered,
145
147
  on_waiting=on_waiting,
146
148
  on_skipped=on_skipped,