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