zrb 1.0.0a2__py3-none-any.whl → 1.0.0a4__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 (174) hide show
  1. zrb/__init__.py +49 -40
  2. zrb/__main__.py +5 -3
  3. zrb/attr/type.py +2 -1
  4. zrb/builtin/__init__.py +42 -2
  5. zrb/builtin/base64.py +34 -0
  6. zrb/builtin/git.py +156 -0
  7. zrb/builtin/git_subtree.py +88 -0
  8. zrb/builtin/group.py +34 -0
  9. zrb/builtin/llm/llm_chat.py +47 -0
  10. zrb/builtin/llm/tool/cli.py +9 -0
  11. zrb/builtin/llm/tool/rag.py +189 -0
  12. zrb/builtin/llm/tool/web.py +74 -0
  13. zrb/builtin/md5.py +36 -0
  14. zrb/builtin/project/add/fastapp.py +72 -0
  15. zrb/builtin/project/add/fastapp_template/.gitignore +4 -0
  16. zrb/builtin/project/add/fastapp_template/README.md +7 -0
  17. zrb/builtin/project/add/fastapp_template/_zrb/config.py +17 -0
  18. zrb/builtin/project/add/fastapp_template/_zrb/group.py +16 -0
  19. zrb/builtin/project/add/fastapp_template/_zrb/helper.py +97 -0
  20. zrb/builtin/project/add/fastapp_template/_zrb/main.py +132 -0
  21. zrb/builtin/project/add/fastapp_template/_zrb/venv_task.py +22 -0
  22. zrb/builtin/project/add/fastapp_template/common/app.py +18 -0
  23. zrb/builtin/project/add/fastapp_template/common/db_engine.py +5 -0
  24. zrb/builtin/project/add/fastapp_template/common/db_repository.py +134 -0
  25. zrb/builtin/project/add/fastapp_template/common/error.py +8 -0
  26. zrb/builtin/project/add/fastapp_template/common/schema.py +5 -0
  27. zrb/builtin/project/add/fastapp_template/common/usecase.py +232 -0
  28. zrb/builtin/project/add/fastapp_template/config.py +29 -0
  29. zrb/builtin/project/add/fastapp_template/main.py +7 -0
  30. zrb/builtin/project/add/fastapp_template/migrate.py +3 -0
  31. zrb/builtin/project/add/fastapp_template/module/__init__.py +0 -0
  32. zrb/builtin/project/add/fastapp_template/module/auth/alembic.ini +117 -0
  33. zrb/builtin/project/add/fastapp_template/module/auth/client/api_client.py +7 -0
  34. zrb/builtin/project/add/fastapp_template/module/auth/client/base_client.py +27 -0
  35. zrb/builtin/project/add/fastapp_template/module/auth/client/direct_client.py +6 -0
  36. zrb/builtin/project/add/fastapp_template/module/auth/client/factory.py +9 -0
  37. zrb/builtin/project/add/fastapp_template/module/auth/migration/README +1 -0
  38. zrb/builtin/project/add/fastapp_template/module/auth/migration/env.py +108 -0
  39. zrb/builtin/project/add/fastapp_template/module/auth/migration/script.py.mako +26 -0
  40. zrb/builtin/project/add/fastapp_template/module/auth/migration/versions/3093c7336477_add_user_table.py +37 -0
  41. zrb/builtin/project/add/fastapp_template/module/auth/migration_metadata.py +6 -0
  42. zrb/builtin/project/add/fastapp_template/module/auth/route.py +22 -0
  43. zrb/builtin/project/add/fastapp_template/module/auth/service/__init__.py +0 -0
  44. zrb/builtin/project/add/fastapp_template/module/auth/service/user/__init__.py +0 -0
  45. zrb/builtin/project/add/fastapp_template/module/auth/service/user/repository/__init__.py +0 -0
  46. zrb/builtin/project/add/fastapp_template/module/auth/service/user/repository/db_repository.py +39 -0
  47. zrb/builtin/project/add/fastapp_template/module/auth/service/user/repository/factory.py +13 -0
  48. zrb/builtin/project/add/fastapp_template/module/auth/service/user/repository/repository.py +34 -0
  49. zrb/builtin/project/add/fastapp_template/module/auth/service/user/usecase.py +45 -0
  50. zrb/builtin/project/add/fastapp_template/module/gateway/alembic.ini +117 -0
  51. zrb/builtin/project/add/fastapp_template/module/gateway/migration/README +1 -0
  52. zrb/builtin/project/add/fastapp_template/module/gateway/migration/env.py +108 -0
  53. zrb/builtin/project/add/fastapp_template/module/gateway/migration/script.py.mako +26 -0
  54. zrb/builtin/project/add/fastapp_template/module/gateway/migration/versions/.gitkeep +0 -0
  55. zrb/builtin/project/add/fastapp_template/module/gateway/migration_metadata.py +3 -0
  56. zrb/builtin/project/add/fastapp_template/module/gateway/route.py +27 -0
  57. zrb/builtin/project/add/fastapp_template/requirements.txt +6 -0
  58. zrb/builtin/project/add/fastapp_template/schema/__init__.py +0 -0
  59. zrb/builtin/project/add/fastapp_template/schema/role.py +31 -0
  60. zrb/builtin/project/add/fastapp_template/schema/user.py +31 -0
  61. zrb/builtin/project/add/fastapp_template/template.env +2 -0
  62. zrb/builtin/project/create/__init__.py +0 -0
  63. zrb/builtin/project/create/create.py +41 -0
  64. zrb/builtin/project/create/project-template/README.md +3 -0
  65. zrb/builtin/project/create/project-template/zrb_init.py +7 -0
  66. zrb/builtin/python.py +11 -0
  67. zrb/builtin/shell/__init__.py +0 -5
  68. zrb/builtin/shell/autocomplete/__init__.py +0 -9
  69. zrb/builtin/shell/autocomplete/bash.py +5 -6
  70. zrb/builtin/shell/autocomplete/subcmd.py +7 -8
  71. zrb/builtin/shell/autocomplete/zsh.py +5 -6
  72. zrb/builtin/todo.py +219 -0
  73. zrb/callback/any_callback.py +1 -1
  74. zrb/callback/callback.py +5 -5
  75. zrb/cmd/cmd_val.py +2 -2
  76. zrb/config.py +16 -3
  77. zrb/content_transformer/any_content_transformer.py +1 -1
  78. zrb/content_transformer/content_transformer.py +2 -2
  79. zrb/context/any_context.py +1 -1
  80. zrb/context/any_shared_context.py +3 -3
  81. zrb/context/context.py +10 -8
  82. zrb/context/shared_context.py +9 -8
  83. zrb/env/__init__.py +0 -3
  84. zrb/env/any_env.py +1 -1
  85. zrb/env/env.py +3 -4
  86. zrb/env/env_file.py +4 -4
  87. zrb/env/env_map.py +2 -2
  88. zrb/group/__init__.py +0 -3
  89. zrb/group/any_group.py +3 -3
  90. zrb/group/group.py +7 -6
  91. zrb/input/any_input.py +1 -1
  92. zrb/input/base_input.py +4 -4
  93. zrb/input/bool_input.py +5 -5
  94. zrb/input/float_input.py +3 -3
  95. zrb/input/int_input.py +3 -3
  96. zrb/input/option_input.py +51 -0
  97. zrb/input/password_input.py +2 -2
  98. zrb/input/str_input.py +1 -1
  99. zrb/input/text_input.py +12 -10
  100. zrb/runner/cli.py +80 -45
  101. zrb/runner/web_app.py +150 -0
  102. zrb/runner/web_controller/__init__.py +0 -0
  103. zrb/runner/web_controller/group_info_ui/__init__.py +0 -0
  104. zrb/runner/{web_app → web_controller}/group_info_ui/controller.py +7 -8
  105. zrb/runner/{web_app → web_controller}/group_info_ui/view.html +2 -2
  106. zrb/runner/web_controller/home_page/__init__.py +0 -0
  107. zrb/runner/{web_app → web_controller}/home_page/controller.py +7 -6
  108. zrb/runner/{web_app → web_controller}/home_page/view.html +2 -2
  109. zrb/runner/web_controller/task_ui/__init__.py +0 -0
  110. zrb/runner/{web_app → web_controller}/task_ui/controller.py +8 -12
  111. zrb/runner/{web_app → web_controller}/task_ui/view.html +2 -2
  112. zrb/runner/web_util.py +5 -35
  113. zrb/session/any_session.py +13 -7
  114. zrb/session/session.py +78 -40
  115. zrb/session_state_log/session_state_log.py +7 -5
  116. zrb/session_state_logger/any_session_state_logger.py +1 -1
  117. zrb/session_state_logger/default_session_state_logger.py +2 -2
  118. zrb/session_state_logger/file_session_state_logger.py +19 -27
  119. zrb/task/any_task.py +4 -4
  120. zrb/task/base_task.py +33 -23
  121. zrb/task/base_trigger.py +11 -12
  122. zrb/task/cmd_task.py +72 -65
  123. zrb/task/http_check.py +13 -13
  124. zrb/task/llm_task.py +215 -0
  125. zrb/task/make_task.py +9 -9
  126. zrb/task/rsync_task.py +25 -25
  127. zrb/task/scaffolder.py +18 -15
  128. zrb/task/scheduler.py +6 -7
  129. zrb/task/task.py +1 -1
  130. zrb/task/tcp_check.py +11 -13
  131. zrb/util/attr.py +19 -3
  132. zrb/util/cli/style.py +71 -2
  133. zrb/util/cli/subcommand.py +2 -2
  134. zrb/util/codemod/__init__.py +0 -0
  135. zrb/util/codemod/add_code_to_class.py +35 -0
  136. zrb/util/codemod/add_code_to_function.py +36 -0
  137. zrb/util/codemod/add_code_to_method.py +55 -0
  138. zrb/util/codemod/add_key_to_dict.py +51 -0
  139. zrb/util/codemod/add_param_to_function_call.py +39 -0
  140. zrb/util/codemod/add_property_to_class.py +55 -0
  141. zrb/util/git.py +156 -0
  142. zrb/util/git_subtree.py +94 -0
  143. zrb/util/group.py +2 -2
  144. zrb/util/llm/tool.py +63 -0
  145. zrb/util/string/conversion.py +7 -0
  146. zrb/util/todo.py +259 -0
  147. {zrb-1.0.0a2.dist-info → zrb-1.0.0a4.dist-info}/METADATA +13 -5
  148. zrb-1.0.0a4.dist-info/RECORD +197 -0
  149. zrb/builtin/shell/_group.py +0 -9
  150. zrb/builtin/shell/autocomplete/_group.py +0 -6
  151. zrb/runner/web_app/any_request_handler.py +0 -24
  152. zrb/runner/web_server.py +0 -224
  153. zrb-1.0.0a2.dist-info/RECORD +0 -120
  154. /zrb/{runner/web_app → builtin/project}/__init__.py +0 -0
  155. /zrb/{runner/web_app/group_info_ui → builtin/project/add}/__init__.py +0 -0
  156. /zrb/{runner/web_app/home_page → builtin/project/add/fastapp_template}/__init__.py +0 -0
  157. /zrb/{runner/web_app/task_ui → builtin/project/add/fastapp_template/common}/__init__.py +0 -0
  158. /zrb/runner/{web_app → web_controller}/group_info_ui/partial/group_info.html +0 -0
  159. /zrb/runner/{web_app → web_controller}/group_info_ui/partial/group_li.html +0 -0
  160. /zrb/runner/{web_app → web_controller}/group_info_ui/partial/task_info.html +0 -0
  161. /zrb/runner/{web_app → web_controller}/group_info_ui/partial/task_li.html +0 -0
  162. /zrb/runner/{web_app → web_controller}/home_page/partial/group_info.html +0 -0
  163. /zrb/runner/{web_app → web_controller}/home_page/partial/group_li.html +0 -0
  164. /zrb/runner/{web_app → web_controller}/home_page/partial/task_info.html +0 -0
  165. /zrb/runner/{web_app → web_controller}/home_page/partial/task_li.html +0 -0
  166. /zrb/runner/{web_app → web_controller}/static/favicon-32x32.png +0 -0
  167. /zrb/runner/{web_app → web_controller}/static/pico.min.css +0 -0
  168. /zrb/runner/{web_app → web_controller}/task_ui/partial/common-util.js +0 -0
  169. /zrb/runner/{web_app → web_controller}/task_ui/partial/input.html +0 -0
  170. /zrb/runner/{web_app → web_controller}/task_ui/partial/main.js +0 -0
  171. /zrb/runner/{web_app → web_controller}/task_ui/partial/show-existing-session.js +0 -0
  172. /zrb/runner/{web_app → web_controller}/task_ui/partial/visualize-history.js +0 -0
  173. {zrb-1.0.0a2.dist-info → zrb-1.0.0a4.dist-info}/WHEEL +0 -0
  174. {zrb-1.0.0a2.dist-info → zrb-1.0.0a4.dist-info}/entry_points.txt +0 -0
zrb/env/env_map.py CHANGED
@@ -1,8 +1,8 @@
1
1
  import os
2
2
  from collections.abc import Callable
3
3
 
4
- from ..context.shared_context import SharedContext
5
- from .any_env import AnyEnv
4
+ from zrb.context.shared_context import SharedContext
5
+ from zrb.env.any_env import AnyEnv
6
6
 
7
7
 
8
8
  class EnvMap(AnyEnv):
zrb/group/__init__.py CHANGED
@@ -1,3 +0,0 @@
1
- from .group import Group
2
-
3
- assert Group
zrb/group/any_group.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Optional
2
+ from typing import Optional, Union
3
3
 
4
- from ..task.any_task import AnyTask
4
+ from zrb.task.any_task import AnyTask
5
5
 
6
6
 
7
7
  class AnyGroup(ABC):
@@ -36,7 +36,7 @@ class AnyGroup(ABC):
36
36
  pass
37
37
 
38
38
  @abstractmethod
39
- def add_group(self, group: "AnyGroup") -> "AnyGroup":
39
+ def add_group(self, group: Union["AnyGroup", str]) -> "AnyGroup":
40
40
  pass
41
41
 
42
42
  @abstractmethod
zrb/group/group.py CHANGED
@@ -1,5 +1,5 @@
1
- from ..task.any_task import AnyTask
2
- from .any_group import AnyGroup
1
+ from zrb.group.any_group import AnyGroup
2
+ from zrb.task.any_task import AnyTask
3
3
 
4
4
 
5
5
  class Group(AnyGroup):
@@ -42,10 +42,11 @@ class Group(AnyGroup):
42
42
  alias.sort()
43
43
  return {name: self._tasks.get(name) for name in alias}
44
44
 
45
- def add_group(self, group: AnyGroup, alias: str | None = None) -> AnyGroup:
46
- alias = alias if alias is not None else group.name
47
- self._groups[alias] = group
48
- return group
45
+ def add_group(self, group: AnyGroup | str, alias: str | None = None) -> AnyGroup:
46
+ real_group = Group(group) if isinstance(group, str) else group
47
+ alias = alias if alias is not None else real_group.name
48
+ self._groups[alias] = real_group
49
+ return real_group
49
50
 
50
51
  def add_task(self, task: AnyTask, alias: str | None = None) -> AnyTask:
51
52
  alias = alias if alias is not None else task.name
zrb/input/any_input.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from abc import ABC, abstractmethod
2
2
 
3
- from ..context.any_shared_context import AnySharedContext
3
+ from zrb.context.any_shared_context import AnySharedContext
4
4
 
5
5
 
6
6
  class AnyInput(ABC):
zrb/input/base_input.py CHANGED
@@ -1,9 +1,9 @@
1
1
  from typing import Any
2
2
 
3
- from ..attr.type import StrAttr
4
- from ..context.any_shared_context import AnySharedContext
5
- from ..util.attr import get_str_attr
6
- from .any_input import AnyInput
3
+ from zrb.attr.type import StrAttr
4
+ from zrb.context.any_shared_context import AnySharedContext
5
+ from zrb.input.any_input import AnyInput
6
+ from zrb.util.attr import get_str_attr
7
7
 
8
8
 
9
9
  class BaseInput(AnyInput):
zrb/input/bool_input.py CHANGED
@@ -1,10 +1,10 @@
1
- from ..attr.type import StrAttr
2
- from ..context.any_shared_context import AnySharedContext
3
- from ..util.string.conversion import to_boolean
4
- from .base_input import BaseInput
1
+ from zrb.attr.type import StrAttr
2
+ from zrb.context.any_shared_context import AnySharedContext
3
+ from zrb.input.base_input import BaseInput
4
+ from zrb.util.string.conversion import to_boolean
5
5
 
6
6
 
7
- class IntInput(BaseInput):
7
+ class BoolInput(BaseInput):
8
8
  def __init__(
9
9
  self,
10
10
  name: str,
zrb/input/float_input.py CHANGED
@@ -1,6 +1,6 @@
1
- from ..attr.type import StrAttr
2
- from ..context.any_shared_context import AnySharedContext
3
- from .base_input import BaseInput
1
+ from zrb.attr.type import StrAttr
2
+ from zrb.context.any_shared_context import AnySharedContext
3
+ from zrb.input.base_input import BaseInput
4
4
 
5
5
 
6
6
  class FloatInput(BaseInput):
zrb/input/int_input.py CHANGED
@@ -1,6 +1,6 @@
1
- from ..attr.type import StrAttr
2
- from ..context.any_shared_context import AnySharedContext
3
- from .base_input import BaseInput
1
+ from zrb.attr.type import StrAttr
2
+ from zrb.context.any_shared_context import AnySharedContext
3
+ from zrb.input.base_input import BaseInput
4
4
 
5
5
 
6
6
  class IntInput(BaseInput):
@@ -0,0 +1,51 @@
1
+ from zrb.attr.type import StrAttr, StrListAttr
2
+ from zrb.context.any_shared_context import AnySharedContext
3
+ from zrb.input.base_input import BaseInput
4
+ from zrb.util.attr import get_str_list_attr
5
+
6
+
7
+ class OptionInput(BaseInput):
8
+ def __init__(
9
+ self,
10
+ name: str,
11
+ description: str | None = None,
12
+ prompt: str | None = None,
13
+ options: StrListAttr = [],
14
+ default_str: StrAttr = "",
15
+ auto_render: bool = True,
16
+ allow_empty: bool = True,
17
+ ):
18
+ super().__init__(
19
+ name=name,
20
+ description=description,
21
+ prompt=prompt,
22
+ default_str=default_str,
23
+ auto_render=auto_render,
24
+ allow_empty=allow_empty,
25
+ )
26
+ self._options = options
27
+
28
+ def to_html(self, ctx: AnySharedContext) -> str:
29
+ name = self.name
30
+ description = self.description
31
+ default = self._get_default_str(ctx)
32
+ html = [f'<select name="{name}" placeholder="{description}">']
33
+ for value in get_str_list_attr(ctx, self._options, self._auto_render):
34
+ selected = "selected" if value == default else ""
35
+ html.append(f'<option value="{value}" {selected}>{value}</option>')
36
+ html.append("</select>")
37
+ return "\n".join(html)
38
+
39
+ def _prompt_cli_str(self, shared_ctx: AnySharedContext) -> str:
40
+ prompt_message = self.prompt_message
41
+ default_value = self._get_default_str(shared_ctx)
42
+ options = get_str_list_attr(shared_ctx, self._options, self._auto_render)
43
+ option_str = ", ".join(options)
44
+ if default_value != "":
45
+ prompt_message = f"{prompt_message} ({option_str}) [{default_value}]"
46
+ value = input(f"{prompt_message}: ")
47
+ if value.strip() != "" and value.strip() not in options:
48
+ value = self._prompt_cli_str(shared_ctx)
49
+ if value.strip() == "":
50
+ value = default_value
51
+ return value
@@ -1,8 +1,8 @@
1
1
  import getpass
2
2
  from collections.abc import Callable
3
3
 
4
- from ..context.any_shared_context import AnySharedContext
5
- from .base_input import BaseInput
4
+ from zrb.context.any_shared_context import AnySharedContext
5
+ from zrb.input.base_input import BaseInput
6
6
 
7
7
 
8
8
  class PasswordInput(BaseInput):
zrb/input/str_input.py CHANGED
@@ -1,4 +1,4 @@
1
- from .base_input import BaseInput
1
+ from zrb.input.base_input import BaseInput
2
2
 
3
3
 
4
4
  class StrInput(BaseInput):
zrb/input/text_input.py CHANGED
@@ -1,10 +1,11 @@
1
+ import os
1
2
  import subprocess
2
3
  import tempfile
3
4
  from collections.abc import Callable
4
5
 
5
- from ..config import DEFAULT_EDITOR
6
- from ..context.any_shared_context import AnySharedContext
7
- from .base_input import BaseInput
6
+ from zrb.config import DEFAULT_EDITOR
7
+ from zrb.context.any_shared_context import AnySharedContext
8
+ from zrb.input.base_input import BaseInput
8
9
 
9
10
 
10
11
  class TextInput(BaseInput):
@@ -66,14 +67,15 @@ class TextInput(BaseInput):
66
67
 
67
68
  def _prompt_cli_str(self, shared_ctx: AnySharedContext) -> str:
68
69
  prompt_message = (
69
- f"{self.comment_start}{super().prompt_message}{self.comment_end}\n"
70
+ f"{self.comment_start}{super().prompt_message}{self.comment_end}"
70
71
  )
72
+ prompt_message_eol = f"{prompt_message}\n"
71
73
  default_value = self._get_default_str(shared_ctx)
72
74
  with tempfile.NamedTemporaryFile(
73
75
  delete=False, suffix=self._extension
74
76
  ) as temp_file:
75
77
  temp_file_name = temp_file.name
76
- temp_file.write(prompt_message.encode())
78
+ temp_file.write(prompt_message_eol.encode())
77
79
  # Pre-fill with default content
78
80
  if default_value:
79
81
  temp_file.write(default_value.encode())
@@ -82,8 +84,8 @@ class TextInput(BaseInput):
82
84
  subprocess.call([self._editor, temp_file_name])
83
85
  # Read the edited content
84
86
  with open(temp_file_name, "r") as temp_file:
85
- edited_content = temp_file.read().strip()
86
- parts = edited_content.split(prompt_message)
87
- if len(parts) == 2 and parts[0].strip() == "":
88
- edited_content = parts[1]
89
- return edited_content.strip() if edited_content.strip() else default_value
87
+ edited_content = temp_file.read()
88
+ parts = [text.strip() for text in edited_content.split(prompt_message, 1)]
89
+ edited_content = "\n".join(parts).lstrip()
90
+ os.remove(temp_file_name)
91
+ return edited_content
zrb/runner/cli.py CHANGED
@@ -1,16 +1,22 @@
1
1
  import sys
2
2
  from typing import Any
3
3
 
4
- from ..config import BANNER, WEB_HTTP_PORT
5
- from ..context.shared_context import SharedContext
6
- from ..group.group import Group
7
- from ..session.session import Session
8
- from ..task.any_task import AnyTask
9
- from ..task.task import Task
10
- from ..util.cli.style import stylize_bold_yellow, stylize_faint, stylize_section_header
11
- from ..util.group import extract_node_from_args, get_non_empty_subgroups, get_subtasks
12
- from ..util.load import load_zrb_init
13
- from .web_server import run_web_server
4
+ from zrb.config import BANNER, WEB_HTTP_PORT
5
+ from zrb.context.any_context import AnyContext
6
+ from zrb.context.shared_context import SharedContext
7
+ from zrb.group.group import Group
8
+ from zrb.runner.web_app import create_app
9
+ from zrb.session.session import Session
10
+ from zrb.task.any_task import AnyTask
11
+ from zrb.task.make_task import make_task
12
+ from zrb.util.cli.style import (
13
+ stylize_bold_yellow,
14
+ stylize_faint,
15
+ stylize_section_header,
16
+ )
17
+ from zrb.util.group import extract_node_from_args, get_non_empty_subgroups, get_subtasks
18
+ from zrb.util.load import load_zrb_init
19
+ from zrb.util.string.conversion import double_quote
14
20
 
15
21
 
16
22
  class Cli(Group):
@@ -25,12 +31,15 @@ class Cli(Group):
25
31
  if "h" in kwargs or "help" in kwargs:
26
32
  self._show_task_info(node)
27
33
  return
28
- result = self._run_task(node, args, kwargs)
29
- if result is not None:
30
- print(result)
31
- run_command = self._get_run_command(node_path, kwargs, args)
32
- self._print_run_command(run_command)
33
- return result
34
+ run_kwargs = self._get_run_kwargs(node, args, kwargs)
35
+ try:
36
+ result = self._run_task(node, args, run_kwargs)
37
+ if result is not None:
38
+ print(result)
39
+ return result
40
+ finally:
41
+ run_command = self._get_run_command(node_path, run_kwargs)
42
+ self._print_run_command(run_command)
34
43
 
35
44
  def _print_run_command(self, run_command: str):
36
45
  print(
@@ -39,34 +48,48 @@ class Cli(Group):
39
48
  file=sys.stderr,
40
49
  )
41
50
 
42
- def _get_run_command(
43
- self, node_path: list[str], kwargs: dict[str, Any], args: list[str]
44
- ) -> str:
51
+ def _get_run_command(self, node_path: list[str], run_kwargs: dict[str, str]) -> str:
45
52
  parts = [self.name] + node_path
46
- if len(kwargs) > 0:
47
- parts += [f"--{key}={val}" for key, val in kwargs.items()]
48
- if len(args) > 0:
49
- parts += args
53
+ if len(run_kwargs) > 0:
54
+ parts += [
55
+ self._get_run_command_param(key, val) for key, val in run_kwargs.items()
56
+ ]
50
57
  return " ".join(parts)
51
58
 
52
- def _run_task(self, task: AnyTask, args: list[str], options: list[str]) -> Any:
59
+ def _get_run_command_param(self, key: str, val: str) -> str:
60
+ if '"' in val or "'" in val or " " in val or val == "":
61
+ return f"--{key} {double_quote(val)}"
62
+ return f"--{key} {val}"
63
+
64
+ def _run_task(
65
+ self, task: AnyTask, args: list[str], run_kwargs: dict[str, str]
66
+ ) -> tuple[Any]:
67
+ shared_ctx = SharedContext(args=args)
68
+ for task_input in task.inputs:
69
+ if task_input.name in run_kwargs:
70
+ task_input.update_shared_context(
71
+ shared_ctx, run_kwargs[task_input.name]
72
+ )
73
+ continue
74
+ return task.run(Session(shared_ctx=shared_ctx, root_group=self))
75
+
76
+ def _get_run_kwargs(
77
+ self, task: AnyTask, args: list[str], kwargs: dict[str, str]
78
+ ) -> tuple[Any]:
53
79
  arg_index = 0
54
- str_kwargs = {key: val for key, val in options.items()}
80
+ str_kwargs = {key: val for key, val in kwargs.items()}
81
+ run_kwargs = {**str_kwargs}
55
82
  shared_ctx = SharedContext(args=args)
56
83
  for task_input in task.inputs:
57
84
  if task_input.name in str_kwargs:
58
85
  continue
59
86
  if arg_index < len(args):
60
- str_kwargs[task_input.name] = args[arg_index]
87
+ run_kwargs[task_input.name] = args[arg_index]
61
88
  arg_index += 1
62
89
  continue
63
- str_kwargs[task_input.name] = task_input.prompt_cli_str(shared_ctx)
64
- try:
65
- return task.run(
66
- Session(shared_ctx=shared_ctx, root_group=self), str_kwargs=str_kwargs
67
- )
68
- except KeyboardInterrupt:
69
- pass
90
+ str_value = task_input.prompt_cli_str(shared_ctx)
91
+ run_kwargs[task_input.name] = str_value
92
+ return run_kwargs
70
93
 
71
94
  def _show_task_info(self, task: AnyTask):
72
95
  description = task.description
@@ -77,8 +100,9 @@ class Cli(Group):
77
100
  print()
78
101
  if len(inputs) > 0:
79
102
  print(stylize_section_header("INPUTS"))
103
+ max_input_name_length = max(len(task_input.name) for task_input in inputs)
80
104
  for task_input in inputs:
81
- task_input_name = task_input.name.ljust(20)
105
+ task_input_name = task_input.name.ljust(max_input_name_length + 1)
82
106
  print(f" --{task_input_name}: {task_input.description}")
83
107
  print()
84
108
 
@@ -93,15 +117,17 @@ class Cli(Group):
93
117
  subgroups = get_non_empty_subgroups(group)
94
118
  if len(subgroups) > 0:
95
119
  print(stylize_section_header("GROUPS"))
120
+ max_subgroup_alias_length = max(len(s) for s in subgroups)
96
121
  for alias, subgroup in subgroups.items():
97
- alias = alias.ljust(20)
122
+ alias = alias.ljust(max_subgroup_alias_length + 1)
98
123
  print(f" {alias}: {subgroup.description}")
99
124
  print()
100
125
  subtasks = get_subtasks(group)
101
126
  if len(subtasks) > 0:
102
127
  print(stylize_section_header("TASKS"))
128
+ max_subtask_alias_length = max(len(s) for s in subtasks)
103
129
  for alias, subtask in subtasks.items():
104
- alias = alias.ljust(20)
130
+ alias = alias.ljust(max_subtask_alias_length + 1)
105
131
  print(f" {alias}: {subtask.description}")
106
132
  print()
107
133
 
@@ -139,14 +165,23 @@ class Cli(Group):
139
165
 
140
166
 
141
167
  cli = Cli(name="zrb", description="Your Automation Powerhouse", banner=BANNER)
142
- server = cli.add_group(Group(name="server", description="Server related command"))
143
- server.add_task(
144
- Task(
145
- name="start-server",
146
- description="Make tasks available via HTTP Requests 🚀",
147
- action=lambda ctx: run_web_server(ctx=ctx, root_group=cli, port=WEB_HTTP_PORT),
148
- cli_only=True,
149
- retries=0,
150
- ),
168
+ server_group = cli.add_group(
169
+ Group(name="server", description="🌐 Server related command")
170
+ )
171
+
172
+
173
+ @make_task(
174
+ name="start-server",
175
+ description="🚀 Start Zrb Web Server",
176
+ cli_only=True,
177
+ retries=0,
178
+ group=server_group,
151
179
  alias="start",
152
180
  )
181
+ async def run(_: AnyContext):
182
+ from uvicorn import Config, Server
183
+
184
+ app = create_app(cli, WEB_HTTP_PORT)
185
+ config = Config(app=app, host="0.0.0.0", port=WEB_HTTP_PORT, loop="asyncio")
186
+ server = Server(config)
187
+ await server.serve()
zrb/runner/web_app.py ADDED
@@ -0,0 +1,150 @@
1
+ import asyncio
2
+ import os
3
+ import sys
4
+ from datetime import datetime, timedelta
5
+ from typing import Any, Dict, List
6
+
7
+ from zrb.config import BANNER, WEB_HTTP_PORT
8
+ from zrb.context.shared_context import SharedContext
9
+ from zrb.group.any_group import AnyGroup
10
+ from zrb.runner.web_controller.group_info_ui.controller import handle_group_info_ui
11
+ from zrb.runner.web_controller.home_page.controller import handle_home_page
12
+ from zrb.runner.web_controller.task_ui.controller import handle_task_ui
13
+ from zrb.runner.web_util import NewSessionResponse
14
+ from zrb.session.session import Session
15
+ from zrb.session_state_log.session_state_log import SessionStateLog, SessionStateLogList
16
+ from zrb.session_state_logger.default_session_state_logger import (
17
+ default_session_state_logger,
18
+ )
19
+ from zrb.task.any_task import AnyTask
20
+ from zrb.util.group import extract_node_from_args, get_node_path
21
+
22
+
23
+ def create_app(root_group: AnyGroup, port: int = WEB_HTTP_PORT):
24
+ from contextlib import asynccontextmanager
25
+
26
+ from fastapi import FastAPI, HTTPException, Request
27
+ from fastapi.responses import FileResponse, HTMLResponse
28
+ from fastapi.staticfiles import StaticFiles
29
+
30
+ _STATIC_DIR = os.path.join(os.path.dirname(__file__), "web_app", "static")
31
+ _COROS = []
32
+
33
+ @asynccontextmanager
34
+ async def lifespan(app: FastAPI):
35
+ for line in BANNER.split("\n") + [
36
+ f"Zrb Server running on http://localhost:{port}"
37
+ ]:
38
+ print(line, file=sys.stderr)
39
+ yield
40
+ for coro in _COROS:
41
+ coro.cancel()
42
+ asyncio.gather(*_COROS)
43
+
44
+ app = FastAPI(title="zrb", lifespan=lifespan)
45
+
46
+ # Serve static files
47
+ app.mount("/static", StaticFiles(directory=_STATIC_DIR), name="static")
48
+
49
+ @app.get("/", response_class=HTMLResponse, include_in_schema=False)
50
+ @app.get("/ui", response_class=HTMLResponse, include_in_schema=False)
51
+ @app.get("/ui/", response_class=HTMLResponse, include_in_schema=False)
52
+ async def home_page():
53
+ return handle_home_page(root_group)
54
+
55
+ @app.get("/static/{file_path:path}", include_in_schema=False)
56
+ async def static_files(file_path: str):
57
+ full_path = os.path.join(_STATIC_DIR, file_path)
58
+ if os.path.isfile(full_path):
59
+ return FileResponse(full_path)
60
+ raise HTTPException(status_code=404, detail="File not found")
61
+
62
+ @app.get("/ui/{path:path}", include_in_schema=False)
63
+ async def ui_page(path: str):
64
+ # Avoid capturing '/ui' itself
65
+ if not path:
66
+ raise HTTPException(status_code=404, detail="Not Found")
67
+ args = path.split("/")
68
+ node, node_path, residual_args = extract_node_from_args(root_group, args)
69
+ url = f"/ui/{'/'.join(node_path)}/"
70
+ if isinstance(node, AnyTask):
71
+ shared_ctx = SharedContext(env=dict(os.environ))
72
+ session = Session(shared_ctx=shared_ctx, root_group=root_group)
73
+ return handle_task_ui(root_group, node, session, url, residual_args)
74
+ elif isinstance(node, AnyGroup):
75
+ return handle_group_info_ui(root_group, node, url)
76
+ raise HTTPException(status_code=404, detail="Not Found")
77
+
78
+ @app.post("/api/{path:path}")
79
+ async def create_new_session(
80
+ path: str, request: Request = None
81
+ ) -> NewSessionResponse:
82
+ """
83
+ Creating new session
84
+ """
85
+ args = path.split("/")
86
+ node, _, residual_args = extract_node_from_args(root_group, args)
87
+ if isinstance(node, AnyTask):
88
+ session_name = residual_args[0] if residual_args else None
89
+ if not session_name:
90
+ body = await request.json()
91
+ shared_ctx = SharedContext(env=dict(os.environ))
92
+ session = Session(shared_ctx=shared_ctx, root_group=root_group)
93
+ coro = asyncio.create_task(node.async_run(session, str_kwargs=body))
94
+ _COROS.append(coro)
95
+ coro.add_done_callback(lambda coro: _COROS.remove(coro))
96
+ return NewSessionResponse(session_name=session.name)
97
+ raise HTTPException(status_code=404, detail="Not Found")
98
+
99
+ @app.get("/api/{path:path}", response_model=SessionStateLog | SessionStateLogList)
100
+ async def get_session(path: str, query_params: Dict[str, Any] = {}):
101
+ """
102
+ Getting existing session or sessions
103
+ """
104
+ args = path.split("/")
105
+ node, _, residual_args = extract_node_from_args(root_group, args)
106
+ if isinstance(node, AnyTask) and residual_args:
107
+ if residual_args[0] == "list":
108
+ task_path = get_node_path(root_group, node)
109
+ return list_sessions(task_path, query_params)
110
+ else:
111
+ return read_session(residual_args[0])
112
+ raise HTTPException(status_code=404, detail="Not Found")
113
+
114
+ def list_sessions(
115
+ task_path: List[str], query_params: Dict[str, Any]
116
+ ) -> SessionStateLogList:
117
+ max_start_time = datetime.now()
118
+ if "to" in query_params:
119
+ max_start_time = datetime.strptime(query_params["to"], "%Y-%m-%d %H:%M:%S")
120
+ min_start_time = max_start_time - timedelta(hours=1)
121
+ if "from" in query_params:
122
+ min_start_time = datetime.strptime(
123
+ query_params["from"], "%Y-%m-%d %H:%M:%S"
124
+ )
125
+ page = int(query_params.get("page", 0))
126
+ limit = int(query_params.get("limit", 10))
127
+ try:
128
+ return default_session_state_logger.list(
129
+ task_path,
130
+ min_start_time=min_start_time,
131
+ max_start_time=max_start_time,
132
+ page=page,
133
+ limit=limit,
134
+ )
135
+ except Exception as e:
136
+ raise HTTPException(status_code=500, detail=str(e))
137
+
138
+ def read_session(session_name: str) -> SessionStateLog:
139
+ try:
140
+ return default_session_state_logger.read(session_name)
141
+ except Exception as e:
142
+ raise HTTPException(status_code=500, detail=str(e))
143
+
144
+ return app
145
+
146
+
147
+ # async def run_web_server(app: FastAPI, port: int = WEB_HTTP_PORT):
148
+ # config = Config(app=app, host="0.0.0.0", port=port, loop="asyncio")
149
+ # server = Server(config)
150
+ # await server.serve()
File without changes
File without changes
@@ -1,9 +1,8 @@
1
1
  import os
2
2
 
3
- from ....group.any_group import AnyGroup
4
- from ....util.group import get_non_empty_subgroups, get_subtasks
5
- from ....util.string.format import fstring_format
6
- from ..any_request_handler import AnyRequestHandler
3
+ from zrb.group.any_group import AnyGroup
4
+ from zrb.util.group import get_non_empty_subgroups, get_subtasks
5
+ from zrb.util.string.format import fstring_format
7
6
 
8
7
  _DIR = os.path.dirname(__file__)
9
8
 
@@ -23,9 +22,9 @@ with open(os.path.join(_DIR, "partial", "task_li.html")) as f:
23
22
  _TASK_LI_TEMPLATE = f.read()
24
23
 
25
24
 
26
- def handle_group_info_ui(
27
- handler: AnyRequestHandler, root_group: AnyGroup, group: AnyGroup, url: str
28
- ):
25
+ def handle_group_info_ui(root_group: AnyGroup, group: AnyGroup, url: str):
26
+ from fastapi.responses import HTMLResponse
27
+
29
28
  url_parts = url.split("/")
30
29
  parent_url_parts = url_parts[:-2] + [""]
31
30
  parent_url = "/".join(parent_url_parts)
@@ -75,7 +74,7 @@ def handle_group_info_ui(
75
74
  },
76
75
  )
77
76
  )
78
- handler.send_html_response(
77
+ return HTMLResponse(
79
78
  fstring_format(
80
79
  _VIEW_TEMPLATE,
81
80
  {
@@ -4,8 +4,8 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <meta name="color-scheme" content="light dark">
7
- <link rel="stylesheet" href="/pico.min.css">
8
- <link rel="icon" href="/favicon-32x32.png" sizes="32x32" type="image/png">
7
+ <link rel="stylesheet" href="/static/pico.min.css">
8
+ <link rel="icon" href="/static/favicon-32x32.png" sizes="32x32" type="image/png">
9
9
  <title>Zrb</title>
10
10
  </head>
11
11
  <body>
File without changes
@@ -1,9 +1,8 @@
1
1
  import os
2
2
 
3
- from ....group.any_group import AnyGroup
4
- from ....util.group import get_non_empty_subgroups, get_subtasks
5
- from ....util.string.format import fstring_format
6
- from ..any_request_handler import AnyRequestHandler
3
+ from zrb.group.any_group import AnyGroup
4
+ from zrb.util.group import get_non_empty_subgroups, get_subtasks
5
+ from zrb.util.string.format import fstring_format
7
6
 
8
7
  _DIR = os.path.dirname(__file__)
9
8
 
@@ -23,7 +22,9 @@ with open(os.path.join(_DIR, "partial", "task_li.html")) as f:
23
22
  _TASK_LI_TEMPLATE = f.read()
24
23
 
25
24
 
26
- def handle_home_page(handler: AnyRequestHandler, root_group: AnyGroup):
25
+ def handle_home_page(root_group: AnyGroup):
26
+ from fastapi.responses import HTMLResponse
27
+
27
28
  subgroups = get_non_empty_subgroups(root_group, web_only=True)
28
29
  group_info = (
29
30
  ""
@@ -62,7 +63,7 @@ def handle_home_page(handler: AnyRequestHandler, root_group: AnyGroup):
62
63
  },
63
64
  )
64
65
  )
65
- handler.send_html_response(
66
+ return HTMLResponse(
66
67
  fstring_format(
67
68
  _VIEW_TEMPLATE,
68
69
  {