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.
- zrb/__init__.py +1 -1
- zrb/action/runner.py +2 -3
- zrb/builtin/__init__.py +2 -0
- zrb/builtin/_group.py +1 -1
- zrb/builtin/env.py +0 -2
- zrb/builtin/generator/_common.py +15 -16
- zrb/builtin/generator/docker_compose_task/template/src/kebab-task-name/docker-compose.yml +7 -1
- zrb/builtin/generator/docker_compose_task/template/src/kebab-task-name/image/Dockerfile +3 -1
- zrb/builtin/generator/docker_compose_task/template/src/kebab-task-name/image/main.py +0 -5
- zrb/builtin/generator/fastapp/add.py +3 -3
- zrb/builtin/generator/fastapp/template/_automate/snake_app_name/_common.py +17 -7
- zrb/builtin/generator/fastapp/template/_automate/snake_app_name/config/docker-compose.env +3 -0
- zrb/builtin/generator/fastapp/template/_automate/snake_app_name/container.py +14 -5
- zrb/builtin/generator/fastapp/template/_automate/snake_app_name/image.py +1 -1
- zrb/builtin/generator/fastapp/template/_automate/snake_app_name/local.py +1 -2
- zrb/builtin/generator/fastapp/template/src/kebab-app-name/docker-compose.yml +24 -6
- zrb/builtin/generator/fastapp/template/src/kebab-app-name/src/Dockerfile +3 -1
- zrb/builtin/generator/fastapp/template/src/kebab-app-name/src/template.env +1 -1
- zrb/builtin/generator/fastapp_module/add.py +266 -83
- zrb/builtin/generator/project/template/README.md +19 -3
- zrb/builtin/generator/project_task/add.py +12 -13
- zrb/builtin/generator/project_task/task_factory.py +12 -14
- zrb/builtin/generator/simple_python_app/add.py +3 -3
- zrb/builtin/generator/simple_python_app/template/src/kebab-app-name/docker-compose.yml +7 -1
- zrb/builtin/generator/simple_python_app/template/src/kebab-app-name/src/Dockerfile +3 -1
- zrb/builtin/generator/simple_python_app/template/src/kebab-app-name/src/main.py +0 -5
- zrb/builtin/md5.py +5 -4
- zrb/builtin/project.py +32 -0
- zrb/helper/docker_compose/file.py +22 -0
- zrb/helper/env_map/fetch.py +68 -0
- zrb/helper/file/copy_tree.py +6 -7
- zrb/helper/file/text.py +20 -0
- zrb/helper/string/jinja.py +11 -0
- zrb/task/base_task.py +457 -4
- zrb/task/cmd_task.py +1 -2
- zrb/task/decorator.py +1 -1
- zrb/task/docker_compose_task.py +3 -4
- zrb/task/http_checker.py +1 -2
- zrb/task/installer/factory.py +6 -4
- zrb/task/path_checker.py +3 -4
- zrb/task/port_checker.py +1 -2
- zrb/task/resource_maker.py +2 -3
- zrb/task_env/env.py +4 -3
- {zrb-0.0.45.dist-info → zrb-0.0.47.dist-info}/METADATA +3 -1
- {zrb-0.0.45.dist-info → zrb-0.0.47.dist-info}/RECORD +52 -51
- zrb/builtin/generator/fastapp/template/src/kebab-app-name/src/module_disabled.env +0 -0
- zrb/builtin/generator/fastapp/template/src/kebab-app-name/src/module_enabled.env +0 -0
- zrb/helper/dockercompose/read.py +0 -9
- zrb/task/base_model.py +0 -456
- zrb/task_group/group.py +0 -36
- /zrb/builtin/generator/fastapp/template/{_automate/snake_app_name/config/module_disabled.env → src/kebab-app-name/all-module-disabled.env} +0 -0
- /zrb/builtin/generator/fastapp/template/{_automate/snake_app_name/config/module_enabled.env → src/kebab-app-name/all-module-enabled.env} +0 -0
- /zrb/helper/{dockercompose → docker_compose}/fetch_external_env.py +0 -0
- /zrb/{task_group → helper/env_map}/__init__.py +0 -0
- {zrb-0.0.45.dist-info → zrb-0.0.47.dist-info}/LICENSE +0 -0
- {zrb-0.0.45.dist-info → zrb-0.0.47.dist-info}/WHEEL +0 -0
- {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
|
zrb/helper/file/copy_tree.py
CHANGED
@@ -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
|
-
|
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
|
-
|
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}')
|
zrb/helper/file/text.py
ADDED
@@ -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)
|
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 ..
|
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 ..
|
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
|
zrb/task/docker_compose_task.py
CHANGED
@@ -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.
|
11
|
-
from ..helper.
|
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
|