zrb 0.0.45__py3-none-any.whl → 0.0.47__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 (57) hide show
  1. zrb/__init__.py +1 -1
  2. zrb/action/runner.py +2 -3
  3. zrb/builtin/__init__.py +2 -0
  4. zrb/builtin/_group.py +1 -1
  5. zrb/builtin/env.py +0 -2
  6. zrb/builtin/generator/_common.py +15 -16
  7. zrb/builtin/generator/docker_compose_task/template/src/kebab-task-name/docker-compose.yml +7 -1
  8. zrb/builtin/generator/docker_compose_task/template/src/kebab-task-name/image/Dockerfile +3 -1
  9. zrb/builtin/generator/docker_compose_task/template/src/kebab-task-name/image/main.py +0 -5
  10. zrb/builtin/generator/fastapp/add.py +3 -3
  11. zrb/builtin/generator/fastapp/template/_automate/snake_app_name/_common.py +17 -7
  12. zrb/builtin/generator/fastapp/template/_automate/snake_app_name/config/docker-compose.env +3 -0
  13. zrb/builtin/generator/fastapp/template/_automate/snake_app_name/container.py +14 -5
  14. zrb/builtin/generator/fastapp/template/_automate/snake_app_name/image.py +1 -1
  15. zrb/builtin/generator/fastapp/template/_automate/snake_app_name/local.py +1 -2
  16. zrb/builtin/generator/fastapp/template/src/kebab-app-name/docker-compose.yml +24 -6
  17. zrb/builtin/generator/fastapp/template/src/kebab-app-name/src/Dockerfile +3 -1
  18. zrb/builtin/generator/fastapp/template/src/kebab-app-name/src/template.env +1 -1
  19. zrb/builtin/generator/fastapp_module/add.py +266 -83
  20. zrb/builtin/generator/project/template/README.md +19 -3
  21. zrb/builtin/generator/project_task/add.py +12 -13
  22. zrb/builtin/generator/project_task/task_factory.py +12 -14
  23. zrb/builtin/generator/simple_python_app/add.py +3 -3
  24. zrb/builtin/generator/simple_python_app/template/src/kebab-app-name/docker-compose.yml +7 -1
  25. zrb/builtin/generator/simple_python_app/template/src/kebab-app-name/src/Dockerfile +3 -1
  26. zrb/builtin/generator/simple_python_app/template/src/kebab-app-name/src/main.py +0 -5
  27. zrb/builtin/md5.py +5 -4
  28. zrb/builtin/project.py +32 -0
  29. zrb/helper/docker_compose/file.py +22 -0
  30. zrb/helper/env_map/fetch.py +68 -0
  31. zrb/helper/file/copy_tree.py +6 -7
  32. zrb/helper/file/text.py +20 -0
  33. zrb/helper/string/jinja.py +11 -0
  34. zrb/task/base_task.py +457 -4
  35. zrb/task/cmd_task.py +1 -2
  36. zrb/task/decorator.py +1 -1
  37. zrb/task/docker_compose_task.py +3 -4
  38. zrb/task/http_checker.py +1 -2
  39. zrb/task/installer/factory.py +6 -4
  40. zrb/task/path_checker.py +3 -4
  41. zrb/task/port_checker.py +1 -2
  42. zrb/task/resource_maker.py +2 -3
  43. zrb/task_env/env.py +4 -3
  44. {zrb-0.0.45.dist-info → zrb-0.0.47.dist-info}/METADATA +3 -1
  45. {zrb-0.0.45.dist-info → zrb-0.0.47.dist-info}/RECORD +52 -51
  46. zrb/builtin/generator/fastapp/template/src/kebab-app-name/src/module_disabled.env +0 -0
  47. zrb/builtin/generator/fastapp/template/src/kebab-app-name/src/module_enabled.env +0 -0
  48. zrb/helper/dockercompose/read.py +0 -9
  49. zrb/task/base_model.py +0 -456
  50. zrb/task_group/group.py +0 -36
  51. /zrb/builtin/generator/fastapp/template/{_automate/snake_app_name/config/module_disabled.env → src/kebab-app-name/all-module-disabled.env} +0 -0
  52. /zrb/builtin/generator/fastapp/template/{_automate/snake_app_name/config/module_enabled.env → src/kebab-app-name/all-module-enabled.env} +0 -0
  53. /zrb/helper/{dockercompose → docker_compose}/fetch_external_env.py +0 -0
  54. /zrb/{task_group → helper/env_map}/__init__.py +0 -0
  55. {zrb-0.0.45.dist-info → zrb-0.0.47.dist-info}/LICENSE +0 -0
  56. {zrb-0.0.45.dist-info → zrb-0.0.47.dist-info}/WHEEL +0 -0
  57. {zrb-0.0.45.dist-info → zrb-0.0.47.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,68 @@
1
+ from typing import List, Mapping
2
+ from ...task.base_task import Group, BaseTask
3
+ from ...task_env.env import Env
4
+ from ..string.jinja import is_probably_jinja
5
+
6
+
7
+ def fetch_env_map_from_group(
8
+ env_map: Mapping[str, str], group: Group
9
+ ) -> Mapping[str, str]:
10
+ for task in group.tasks:
11
+ env_map = fetch_env_map_from_task(env_map, task)
12
+ for sub_group in group.children:
13
+ sub_env_map: Mapping[str, str] = fetch_env_map_from_group(
14
+ env_map, sub_group
15
+ )
16
+ env_map = cascade_env_map(env_map, sub_env_map)
17
+ return env_map
18
+
19
+
20
+ def fetch_env_map_from_task(
21
+ env_map: Mapping[str, str], task: BaseTask
22
+ ):
23
+ task_env_map: Mapping[str, str] = {}
24
+ for env_file in task.env_files:
25
+ envs = env_file.get_envs()
26
+ task_env_map = add_envs_to_env_map(task_env_map, envs)
27
+ task_env_map = add_envs_to_env_map(task_env_map, task.envs)
28
+ env_map = cascade_env_map(env_map, task_env_map)
29
+ for upstream in task.upstreams:
30
+ task_env_map = fetch_env_map_from_task(env_map, upstream)
31
+ for checker in task.checkers:
32
+ task_env_map = fetch_env_map_from_task(env_map, checker)
33
+ return env_map
34
+
35
+
36
+ def add_envs_to_env_map(
37
+ env_map: Mapping[str, str], envs: List[Env]
38
+ ) -> Mapping[str, str]:
39
+ for env in envs:
40
+ if env.os_name == '':
41
+ continue
42
+ env_name = get_env_name(env)
43
+ env_default = get_env_default(env)
44
+ env_map[env_name] = env_default
45
+ return env_map
46
+
47
+
48
+ def cascade_env_map(
49
+ env_map: Mapping[str, str],
50
+ other_env_map: Mapping[str, str]
51
+ ) -> Mapping[str, str]:
52
+ for key, value in other_env_map.items():
53
+ if key in env_map:
54
+ continue
55
+ env_map[key] = value
56
+ return env_map
57
+
58
+
59
+ def get_env_name(env: Env) -> str:
60
+ if env.os_name is None:
61
+ return env.name
62
+ return env.os_name
63
+
64
+
65
+ def get_env_default(env: Env) -> str:
66
+ if is_probably_jinja(env.default):
67
+ return ''
68
+ return env.default
@@ -1,6 +1,7 @@
1
1
  from typing import Iterable, Mapping, Optional
2
- from ..string.parse_replacement import parse_replacement
3
2
  from typeguard import typechecked
3
+ from .text import read_text_file_async, write_text_file_async
4
+ from ..string.parse_replacement import parse_replacement
4
5
  from ..log import logger
5
6
 
6
7
  import os
@@ -9,7 +10,7 @@ import fnmatch
9
10
 
10
11
 
11
12
  @typechecked
12
- def copy_tree(
13
+ async def copy_tree(
13
14
  src: str,
14
15
  dst: str,
15
16
  replacements: Optional[Mapping[str, str]] = None,
@@ -29,15 +30,13 @@ def copy_tree(
29
30
  continue
30
31
  dst_name = os.path.join(dst, name)
31
32
  if os.path.isdir(src_name):
32
- copy_tree(src_name, dst_name, replacements, excludes)
33
+ await copy_tree(src_name, dst_name, replacements, excludes)
33
34
  continue
34
35
  new_dst_name = parse_replacement(dst_name, replacements)
35
36
  shutil.copy2(src_name, new_dst_name)
36
37
  try:
37
- with open(new_dst_name, 'r') as file:
38
- file_content = file.read()
38
+ file_content = await read_text_file_async(new_dst_name)
39
39
  new_file_content = parse_replacement(file_content, replacements)
40
- with open(new_dst_name, 'w') as file:
41
- file.write(new_file_content)
40
+ await write_text_file_async(new_dst_name, new_file_content)
42
41
  except Exception:
43
42
  logger.error(f'Cannot parse file: {new_dst_name}')
@@ -0,0 +1,20 @@
1
+ import aiofiles
2
+
3
+
4
+ async def read_text_file_async(file_name: str) -> str:
5
+ async with aiofiles.open(file_name, mode='r') as file:
6
+ content = await file.read()
7
+ return content
8
+
9
+
10
+ async def write_text_file_async(file_name: str, content: str):
11
+ async with aiofiles.open(file_name, mode='w') as file:
12
+ await file.write(content)
13
+
14
+
15
+ async def append_text_file_async(file_name: str, additional_content: str):
16
+ content = await read_text_file_async(file_name)
17
+ if content != '':
18
+ additional_content = '\n' + additional_content
19
+ new_content = content + additional_content
20
+ await write_text_file_async(file_name, new_content)
@@ -0,0 +1,11 @@
1
+ from typing import Any
2
+
3
+
4
+ def is_probably_jinja(value: Any) -> bool:
5
+ if not isinstance(value, str):
6
+ return False
7
+ if '{{' in value and '}}' in value:
8
+ return True
9
+ if '{%' in value and '%}' in value:
10
+ return True
11
+ return False
zrb/task/base_task.py CHANGED
@@ -2,23 +2,474 @@ from typing import (
2
2
  Any, Callable, Iterable, List, Mapping, Optional, TypeVar, Union
3
3
  )
4
4
  from typeguard import typechecked
5
- from .base_model import TaskModel
6
5
  from ..advertisement import advertisements
7
6
  from ..task_env.env import Env
8
7
  from ..task_env.env_file import EnvFile
9
- from ..task_group.group import Group
10
8
  from ..task_input.base_input import BaseInput
11
- from ..helper.accessories.color import colored
9
+ from ..task_input._constant import RESERVED_INPUT_NAMES
10
+ from ..helper.accessories.color import (
11
+ get_random_color, is_valid_color, colored
12
+ )
13
+ from ..helper.accessories.icon import get_random_icon
12
14
  from ..helper.advertisement import get_advertisement
13
15
  from ..helper.list.ensure_uniqueness import ensure_uniqueness
16
+ from ..helper.log import logger
17
+ from ..helper.render_data import DEFAULT_RENDER_DATA
14
18
  from ..helper.string.double_quote import double_quote
19
+ from ..helper.string.conversion import (
20
+ to_cmd_name, to_variable_name, to_boolean
21
+ )
22
+ from ..helper.string.jinja import is_probably_jinja
15
23
 
16
24
  import asyncio
17
- import inspect
18
25
  import copy
26
+ import datetime
27
+ import inspect
28
+ import jinja2
29
+ import os
19
30
  import sys
31
+ import time
32
+
33
+
34
+ MAX_NAME_LENGTH = 20
35
+ MULTILINE_INDENT = ' ' * 8
20
36
 
21
37
  TTask = TypeVar('TTask', bound='BaseTask')
38
+ TGroup = TypeVar('TGroup', bound='Group')
39
+
40
+
41
+ @typechecked
42
+ class Group():
43
+ def __init__(
44
+ self,
45
+ name: str,
46
+ description: Optional[str] = None,
47
+ parent: Optional[TGroup] = None
48
+ ):
49
+ self.name = name
50
+ self.description = description
51
+ self.parent = parent
52
+ if parent is not None:
53
+ parent.children.append(self)
54
+ self.children: List[TGroup] = []
55
+ self.tasks: List[TTask] = []
56
+
57
+ def get_cmd_name(self) -> str:
58
+ return to_cmd_name(self.name)
59
+
60
+ def get_complete_name(self) -> str:
61
+ cmd_name = self.get_cmd_name()
62
+ if self.parent is None:
63
+ return cmd_name
64
+ parent_cmd_name = self.parent.get_complete_name()
65
+ return f'{parent_cmd_name} {cmd_name}'
66
+
67
+ def get_id(self) -> str:
68
+ group_id = self.get_cmd_name()
69
+ if self.parent is None:
70
+ return group_id
71
+ parent_group_id = self.parent.get_id()
72
+ return f'{parent_group_id} {group_id}'
73
+
74
+
75
+ class AnyExtensionFileSystemLoader(jinja2.FileSystemLoader):
76
+ def get_source(self, environment, template):
77
+ for search_dir in self.searchpath:
78
+ file_path = os.path.join(search_dir, template)
79
+ if os.path.exists(file_path):
80
+ with open(file_path, 'r') as file:
81
+ contents = file.read()
82
+ return contents, file_path, lambda: False
83
+ raise jinja2.TemplateNotFound(template)
84
+
85
+
86
+ @typechecked
87
+ class TimeTracker():
88
+
89
+ def __init__(self):
90
+ self._start_time: float = 0
91
+ self._end_time: float = 0
92
+
93
+ def start_timer(self):
94
+ self._start_time = time.time()
95
+
96
+ def end_timer(self):
97
+ self._end_time = time.time()
98
+
99
+ def get_elapsed_time(self) -> float:
100
+ return self._end_time - self._start_time
101
+
102
+
103
+ @typechecked
104
+ class AttemptTracker():
105
+
106
+ def __init__(self, retry: int = 2):
107
+ self.retry = retry
108
+ self._attempt: int = 1
109
+
110
+ def get_max_attempt(self) -> int:
111
+ return self.retry + 1
112
+
113
+ def get_attempt(self) -> int:
114
+ return self._attempt
115
+
116
+ def increase_attempt(self):
117
+ self._attempt += 1
118
+
119
+ def should_attempt(self) -> bool:
120
+ attempt = self.get_attempt()
121
+ max_attempt = self.get_max_attempt()
122
+ return attempt <= max_attempt
123
+
124
+ def is_last_attempt(self) -> bool:
125
+ attempt = self.get_attempt()
126
+ max_attempt = self.get_max_attempt()
127
+ return attempt >= max_attempt
128
+
129
+
130
+ @typechecked
131
+ class FinishTracker():
132
+
133
+ def __init__(self):
134
+ self._execution_queue: Optional[asyncio.Queue] = None
135
+ self._counter = 0
136
+
137
+ async def mark_start(self):
138
+ if self._execution_queue is None:
139
+ self._execution_queue = asyncio.Queue()
140
+ self._counter += 1
141
+
142
+ async def mark_as_done(self):
143
+ # Tracker might be started several times
144
+ # However, when the execution is marked as done, it applied globally
145
+ # Thus, we need to send event as much as the counter.
146
+ for i in range(self._counter):
147
+ await self._execution_queue.put(True)
148
+
149
+ async def is_done(self) -> bool:
150
+ while self._execution_queue is None:
151
+ await asyncio.sleep(0.05)
152
+ return await self._execution_queue.get()
153
+
154
+
155
+ @typechecked
156
+ class PidModel():
157
+
158
+ def __init__(self):
159
+ self.zrb_task_pid: int = os.getpid()
160
+
161
+ def set_task_pid(self, pid: int):
162
+ self.zrb_task_pid = pid
163
+
164
+ def get_task_pid(self) -> int:
165
+ return self.zrb_task_pid
166
+
167
+
168
+ @typechecked
169
+ class TaskModel(
170
+ PidModel, FinishTracker, AttemptTracker, TimeTracker
171
+ ):
172
+ def __init__(
173
+ self,
174
+ name: str,
175
+ group: Optional[Group] = None,
176
+ envs: Iterable[Env] = [],
177
+ env_files: Iterable[EnvFile] = [],
178
+ icon: Optional[str] = None,
179
+ color: Optional[str] = None,
180
+ retry: int = 2,
181
+ ):
182
+ # init properties
183
+ self.name = name
184
+ self.group = group
185
+ self.envs = envs
186
+ self.env_files = env_files
187
+ self.icon = icon
188
+ self.color = color
189
+ # init parent classes
190
+ PidModel.__init__(self)
191
+ FinishTracker.__init__(self)
192
+ TimeTracker.__init__(self)
193
+ non_negative_retry = self.ensure_non_negative(
194
+ retry, 'Find negative retry'
195
+ )
196
+ AttemptTracker.__init__(self, retry=non_negative_retry)
197
+ # init private properties
198
+ self._input_map: Mapping[str, Any] = {}
199
+ self._env_map: Mapping[str, str] = {}
200
+ self._complete_name: Optional[str] = None
201
+ self._filled_complete_name: Optional[str] = None
202
+ self._rendered_str: Mapping[str, str] = {}
203
+ self._is_keyval_set = False # Flag
204
+ self._has_cli_interface = False
205
+ self._render_data: Optional[Mapping[str, Any]] = None
206
+ self._all_inputs: Optional[List[BaseInput]] = None
207
+
208
+ def get_icon(self) -> str:
209
+ if self.icon is None or self.icon == '':
210
+ self.icon = get_random_icon()
211
+ return self.icon
212
+
213
+ def get_color(self) -> str:
214
+ if self.color is None or not is_valid_color(self.color):
215
+ self.color = get_random_color()
216
+ return self.color
217
+
218
+ def get_cmd_name(self) -> str:
219
+ return to_cmd_name(self.name)
220
+
221
+ def ensure_non_negative(self, value: float, error_label: str) -> float:
222
+ if value < 0:
223
+ self.log_warn(f'{error_label}: {value}')
224
+ return 0
225
+ return value
226
+
227
+ def show_done_info(self):
228
+ complete_name = self._get_complete_name()
229
+ elapsed_time = self.get_elapsed_time()
230
+ message = '\n'.join([
231
+ f'{complete_name} completed in {elapsed_time} seconds',
232
+ ])
233
+ self.print_out_dark(message)
234
+ self.play_bell()
235
+
236
+ def log_debug(self, message: Any):
237
+ prefix = self._get_log_prefix()
238
+ colored_message = colored(
239
+ f'{prefix} • {message}', attrs=['dark']
240
+ )
241
+ logger.debug(colored_message)
242
+
243
+ def log_warn(self, message: Any):
244
+ prefix = self._get_log_prefix()
245
+ colored_message = colored(
246
+ f'{prefix} • {message}', attrs=['dark']
247
+ )
248
+ logger.warning(colored_message)
249
+
250
+ def log_info(self, message: Any):
251
+ prefix = self._get_log_prefix()
252
+ colored_message = colored(
253
+ f'{prefix} • {message}', attrs=['dark']
254
+ )
255
+ logger.info(colored_message)
256
+
257
+ def log_error(self, message: Any):
258
+ prefix = self._get_log_prefix()
259
+ colored_message = colored(
260
+ f'{prefix} • {message}', color='red', attrs=['bold']
261
+ )
262
+ logger.error(colored_message, exc_info=True)
263
+
264
+ def log_critical(self, message: Any):
265
+ prefix = self._get_log_prefix()
266
+ colored_message = colored(
267
+ f'{prefix} • {message}', color='red', attrs=['bold']
268
+ )
269
+ logger.critical(colored_message, exc_info=True)
270
+
271
+ def print_out(self, msg: Any):
272
+ prefix = self._get_colored_print_prefix()
273
+ print(f'🤖 ➜ {prefix} • {msg}'.rstrip(), file=sys.stderr)
274
+
275
+ def print_err(self, msg: Any):
276
+ prefix = self._get_colored_print_prefix()
277
+ print(f'🤖 ⚠ {prefix} • {msg}'.rstrip(), file=sys.stderr)
278
+
279
+ def print_out_dark(self, msg: Any):
280
+ self.print_out(colored(msg, attrs=['dark']))
281
+
282
+ def colored(self, text: str) -> str:
283
+ return colored(text, color=self.get_color())
284
+
285
+ def play_bell(self):
286
+ print('\a', end='', file=sys.stderr)
287
+
288
+ def _get_colored_print_prefix(self) -> str:
289
+ return self.colored(self._get_print_prefix())
290
+
291
+ def _get_print_prefix(self) -> str:
292
+ common_prefix = self._get_common_prefix(show_time=True)
293
+ icon = self.get_icon()
294
+ truncated_name = self._get_filled_complete_name()
295
+ return f'{common_prefix} • {icon} {truncated_name}'
296
+
297
+ def _get_log_prefix(self) -> str:
298
+ common_prefix = self._get_common_prefix(show_time=False)
299
+ icon = self.get_icon()
300
+ filled_name = self._get_filled_complete_name()
301
+ return f'{common_prefix} • {icon} {filled_name}'
302
+
303
+ def _get_common_prefix(self, show_time: bool) -> str:
304
+ attempt = self.get_attempt()
305
+ max_attempt = self.get_max_attempt()
306
+ pid = self.get_task_pid()
307
+ if show_time:
308
+ now = datetime.datetime.now().isoformat()
309
+ return f'{now} ⚙ {pid} ➤ {attempt} of {max_attempt}'
310
+ return f'⚙ {pid} ➤ {attempt} of {max_attempt}'
311
+
312
+ def _get_filled_complete_name(self) -> str:
313
+ if self._filled_complete_name is not None:
314
+ return self._filled_complete_name
315
+ complete_name = self._get_complete_name()
316
+ self._filled_complete_name = complete_name.rjust(MAX_NAME_LENGTH, ' ')
317
+ return self._filled_complete_name
318
+
319
+ def get_input_map(self) -> Mapping[str, Any]:
320
+ return self._input_map
321
+
322
+ def get_env_map(self) -> Mapping[str, str]:
323
+ return self._env_map
324
+
325
+ def _inject_env_map(
326
+ self, env_map: Mapping[str, str], override: bool = False
327
+ ):
328
+ for key, val in env_map.items():
329
+ if override or key not in self._env_map:
330
+ self._env_map[key] = val
331
+
332
+ def render_any(self, val: Any) -> Any:
333
+ if isinstance(val, str):
334
+ return self.render_str(val)
335
+ return val
336
+
337
+ def render_float(self, val: Union[str, float]) -> float:
338
+ if isinstance(val, str):
339
+ return float(self.render_str(val))
340
+ return val
341
+
342
+ def render_int(self, val: Union[str, int]) -> int:
343
+ if isinstance(val, str):
344
+ return int(self.render_str(val))
345
+ return val
346
+
347
+ def render_bool(self, val: Union[str, bool]) -> bool:
348
+ if isinstance(val, str):
349
+ return to_boolean(self.render_str(val))
350
+ return val
351
+
352
+ def render_str(self, val: str) -> str:
353
+ if val in self._rendered_str:
354
+ return self._rendered_str[val]
355
+ if not is_probably_jinja(val):
356
+ return val
357
+ template = jinja2.Template(val)
358
+ data = self._get_render_data()
359
+ self.log_debug(f'Render string template: {val}, with data: {data}')
360
+ try:
361
+ rendered_text = template.render(data)
362
+ self.log_debug(f'Get rendered result: {rendered_text}')
363
+ except Exception:
364
+ self.log_error(f'Fail to render "{val}" with data: {data}')
365
+ self._rendered_str[val] = rendered_text
366
+ return rendered_text
367
+
368
+ def render_file(self, location: str) -> str:
369
+ location_dir = os.path.dirname(location)
370
+ env = jinja2.Environment(
371
+ loader=AnyExtensionFileSystemLoader([location_dir])
372
+ )
373
+ template = env.get_template(location)
374
+ data = self._get_render_data()
375
+ data['TEMPLATE_DIR'] = location_dir
376
+ self.log_debug(f'Render template file: {template}, with data: {data}')
377
+ rendered_text = template.render(data)
378
+ self.log_debug(f'Get rendered result: {rendered_text}')
379
+ return rendered_text
380
+
381
+ def _get_render_data(self) -> Mapping[str, Any]:
382
+ if self._render_data is not None:
383
+ return self._render_data
384
+ render_data = dict(DEFAULT_RENDER_DATA)
385
+ render_data.update({
386
+ 'env': self._env_map,
387
+ 'input': self._input_map,
388
+ })
389
+ self._render_data = render_data
390
+ return render_data
391
+
392
+ def _get_multiline_repr(self, text: str) -> str:
393
+ lines_repr: Iterable[str] = []
394
+ lines = text.split('\n')
395
+ if len(lines) == 1:
396
+ return lines[0]
397
+ for index, line in enumerate(lines):
398
+ line_number_repr = str(index + 1).rjust(4, '0')
399
+ lines_repr.append(f'{MULTILINE_INDENT}{line_number_repr} | {line}')
400
+ return '\n' + '\n'.join(lines_repr)
401
+
402
+ async def _set_local_keyval(
403
+ self,
404
+ kwargs: Mapping[str, Any],
405
+ env_prefix: str = ''
406
+ ):
407
+ if self._is_keyval_set:
408
+ return True
409
+ self._is_keyval_set = True
410
+ # Add self.inputs to input_map
411
+ self.log_info('Set input map')
412
+ for task_input in self.get_all_inputs():
413
+ map_key = self._get_normalized_input_key(task_input.name)
414
+ self._input_map[map_key] = self.render_any(
415
+ kwargs.get(map_key, task_input.default)
416
+ )
417
+ self.log_debug(f'Input map: {self._input_map}')
418
+ # Construct envs based on self.env_files and self.envs
419
+ self.log_info('Merging task env_files and task envs')
420
+ envs: List[Env] = []
421
+ for env_file in self.env_files:
422
+ envs += env_file.get_envs()
423
+ envs += list(self.envs)
424
+ envs.reverse()
425
+ envs = ensure_uniqueness(envs, lambda x, y: x.name == y.name)
426
+ envs.reverse()
427
+ # Add envs to env_map
428
+ self.log_info('Set env map')
429
+ for task_env in envs:
430
+ env_name = task_env.name
431
+ if env_name in self._env_map:
432
+ continue
433
+ self._env_map[env_name] = self.render_any(
434
+ task_env.get(env_prefix)
435
+ )
436
+ self.log_info('Add os environment to env map')
437
+ for key in os.environ:
438
+ if key in self._env_map:
439
+ continue
440
+ self._env_map[key] = os.getenv(key, '')
441
+ self.log_debug(f'Env map: {self._env_map}')
442
+
443
+ def get_all_inputs(self) -> Iterable[BaseInput]:
444
+ # Override this method!!!
445
+ return self._all_inputs
446
+
447
+ def _get_normalized_input_key(self, key: str) -> str:
448
+ if key in RESERVED_INPUT_NAMES:
449
+ return key
450
+ return to_variable_name(key)
451
+
452
+ def _get_complete_name(self) -> str:
453
+ if self._complete_name is not None:
454
+ return self._complete_name
455
+ executable_prefix = ''
456
+ if self._has_cli_interface:
457
+ executable_prefix += self._get_executable_name() + ' '
458
+ cmd_name = self.get_cmd_name()
459
+ if self.group is None:
460
+ self._complete_name = f'{executable_prefix}{cmd_name}'
461
+ return self._complete_name
462
+ group_cmd_name = self.group.get_complete_name()
463
+ self._complete_name = f'{executable_prefix}{group_cmd_name} {cmd_name}'
464
+ return self._complete_name
465
+
466
+ def _get_executable_name(self) -> str:
467
+ if len(sys.argv) > 0 and sys.argv[0] != '':
468
+ return os.path.basename(sys.argv[0])
469
+ return 'zrb'
470
+
471
+ def set_has_cli_interface(self):
472
+ self._has_cli_interface = True
22
473
 
23
474
 
24
475
  @typechecked
@@ -64,6 +515,8 @@ class BaseTask(TaskModel):
64
515
  )
65
516
  if checking_interval == 0:
66
517
  checking_interval = 0.05 if len(checkers) == 0 else 0.1
518
+ if group is not None:
519
+ group.tasks.append(self)
67
520
  self.inputs = inputs
68
521
  self.description = description
69
522
  self.retry_interval = retry_interval
zrb/task/cmd_task.py CHANGED
@@ -1,10 +1,9 @@
1
1
  from typing import Any, Callable, Iterable, Optional, Union
2
2
  from typeguard import typechecked
3
- from .base_task import BaseTask
3
+ from .base_task import BaseTask, Group
4
4
  from ..task_env.env import Env
5
5
  from ..task_env.env_file import EnvFile
6
6
  from ..task_input.base_input import BaseInput
7
- from ..task_group.group import Group
8
7
  from ..config.config import default_shell
9
8
 
10
9
  import asyncio
zrb/task/decorator.py CHANGED
@@ -5,7 +5,7 @@ from typeguard import typechecked
5
5
  from ..task_input.base_input import BaseInput
6
6
  from ..task_env.env import Env
7
7
  from ..task_env.env_file import EnvFile
8
- from ..task_group.group import Group
8
+ from ..task.base_task import Group
9
9
  from ..action.runner import Runner
10
10
  from .base_task import BaseTask
11
11
  from .task import Task
@@ -1,14 +1,13 @@
1
1
  from typing import Any, Callable, Iterable, Mapping, Optional, Union
2
2
  from typeguard import typechecked
3
- from .base_task import BaseTask
3
+ from .base_task import BaseTask, Group
4
4
  from .cmd_task import CmdTask
5
5
  from ..task_env.env import Env
6
6
  from ..task_env.env_file import EnvFile
7
7
  from ..task_input.base_input import BaseInput
8
- from ..task_group.group import Group
9
8
  from ..helper.string.double_quote import double_quote
10
- from ..helper.dockercompose.read import read_file as read_compose_file
11
- from ..helper.dockercompose.fetch_external_env import fetch_external_env
9
+ from ..helper.docker_compose.file import read_compose_file
10
+ from ..helper.docker_compose.fetch_external_env import fetch_external_env
12
11
 
13
12
  import os
14
13
  import pathlib
zrb/task/http_checker.py CHANGED
@@ -1,10 +1,9 @@
1
1
  from typing import Any, Callable, Iterable, Optional, Union
2
2
  from typeguard import typechecked
3
3
  from http.client import HTTPConnection, HTTPSConnection
4
- from .base_task import BaseTask
4
+ from .base_task import BaseTask, Group
5
5
  from ..task_env.env import Env
6
6
  from ..task_env.env_file import EnvFile
7
- from ..task_group.group import Group
8
7
  from ..task_input.base_input import BaseInput
9
8
 
10
9
  import asyncio