zrb 1.0.0a1__py3-none-any.whl → 1.0.0a3__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 (156) hide show
  1. zrb/__init__.py +48 -39
  2. zrb/__main__.py +3 -3
  3. zrb/attr/type.py +2 -1
  4. zrb/builtin/__init__.py +40 -2
  5. zrb/builtin/base64.py +32 -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.py +31 -0
  10. zrb/builtin/md5.py +34 -0
  11. zrb/builtin/project/__init__.py +0 -0
  12. zrb/builtin/project/add/__init__.py +0 -0
  13. zrb/builtin/project/add/fastapp.py +72 -0
  14. zrb/builtin/project/add/fastapp_template/.gitignore +4 -0
  15. zrb/builtin/project/add/fastapp_template/README.md +7 -0
  16. zrb/builtin/project/add/fastapp_template/__init__.py +0 -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/__init__.py +0 -0
  23. zrb/builtin/project/add/fastapp_template/common/app.py +18 -0
  24. zrb/builtin/project/add/fastapp_template/common/db_engine.py +5 -0
  25. zrb/builtin/project/add/fastapp_template/common/db_repository.py +134 -0
  26. zrb/builtin/project/add/fastapp_template/common/error.py +8 -0
  27. zrb/builtin/project/add/fastapp_template/common/schema.py +5 -0
  28. zrb/builtin/project/add/fastapp_template/common/usecase.py +232 -0
  29. zrb/builtin/project/add/fastapp_template/config.py +29 -0
  30. zrb/builtin/project/add/fastapp_template/main.py +7 -0
  31. zrb/builtin/project/add/fastapp_template/migrate.py +3 -0
  32. zrb/builtin/project/add/fastapp_template/module/__init__.py +0 -0
  33. zrb/builtin/project/add/fastapp_template/module/auth/alembic.ini +117 -0
  34. zrb/builtin/project/add/fastapp_template/module/auth/client/api_client.py +7 -0
  35. zrb/builtin/project/add/fastapp_template/module/auth/client/base_client.py +27 -0
  36. zrb/builtin/project/add/fastapp_template/module/auth/client/direct_client.py +6 -0
  37. zrb/builtin/project/add/fastapp_template/module/auth/client/factory.py +9 -0
  38. zrb/builtin/project/add/fastapp_template/module/auth/migration/README +1 -0
  39. zrb/builtin/project/add/fastapp_template/module/auth/migration/env.py +108 -0
  40. zrb/builtin/project/add/fastapp_template/module/auth/migration/script.py.mako +26 -0
  41. zrb/builtin/project/add/fastapp_template/module/auth/migration/versions/3093c7336477_add_user_table.py +37 -0
  42. zrb/builtin/project/add/fastapp_template/module/auth/migration_metadata.py +6 -0
  43. zrb/builtin/project/add/fastapp_template/module/auth/route.py +22 -0
  44. zrb/builtin/project/add/fastapp_template/module/auth/service/__init__.py +0 -0
  45. zrb/builtin/project/add/fastapp_template/module/auth/service/user/__init__.py +0 -0
  46. zrb/builtin/project/add/fastapp_template/module/auth/service/user/repository/__init__.py +0 -0
  47. zrb/builtin/project/add/fastapp_template/module/auth/service/user/repository/db_repository.py +39 -0
  48. zrb/builtin/project/add/fastapp_template/module/auth/service/user/repository/factory.py +13 -0
  49. zrb/builtin/project/add/fastapp_template/module/auth/service/user/repository/repository.py +34 -0
  50. zrb/builtin/project/add/fastapp_template/module/auth/service/user/usecase.py +45 -0
  51. zrb/builtin/project/add/fastapp_template/module/gateway/alembic.ini +117 -0
  52. zrb/builtin/project/add/fastapp_template/module/gateway/migration/README +1 -0
  53. zrb/builtin/project/add/fastapp_template/module/gateway/migration/env.py +108 -0
  54. zrb/builtin/project/add/fastapp_template/module/gateway/migration/script.py.mako +26 -0
  55. zrb/builtin/project/add/fastapp_template/module/gateway/migration/versions/.gitkeep +0 -0
  56. zrb/builtin/project/add/fastapp_template/module/gateway/migration_metadata.py +3 -0
  57. zrb/builtin/project/add/fastapp_template/module/gateway/route.py +27 -0
  58. zrb/builtin/project/add/fastapp_template/requirements.txt +6 -0
  59. zrb/builtin/project/add/fastapp_template/schema/__init__.py +0 -0
  60. zrb/builtin/project/add/fastapp_template/schema/role.py +31 -0
  61. zrb/builtin/project/add/fastapp_template/schema/user.py +31 -0
  62. zrb/builtin/project/add/fastapp_template/template.env +2 -0
  63. zrb/builtin/project/create/__init__.py +0 -0
  64. zrb/builtin/project/create/create.py +41 -0
  65. zrb/builtin/project/create/project-template/README.md +3 -0
  66. zrb/builtin/project/create/project-template/zrb_init.py +7 -0
  67. zrb/builtin/python.py +11 -0
  68. zrb/builtin/shell/__init__.py +0 -5
  69. zrb/builtin/shell/autocomplete/__init__.py +0 -9
  70. zrb/builtin/shell/autocomplete/bash.py +5 -6
  71. zrb/builtin/shell/autocomplete/subcmd.py +7 -8
  72. zrb/builtin/shell/autocomplete/zsh.py +5 -6
  73. zrb/builtin/todo.py +186 -0
  74. zrb/callback/any_callback.py +1 -1
  75. zrb/callback/callback.py +5 -5
  76. zrb/cmd/cmd_val.py +2 -2
  77. zrb/config.py +4 -1
  78. zrb/content_transformer/any_content_transformer.py +1 -1
  79. zrb/content_transformer/content_transformer.py +2 -2
  80. zrb/context/any_context.py +5 -1
  81. zrb/context/any_shared_context.py +3 -3
  82. zrb/context/context.py +15 -9
  83. zrb/context/shared_context.py +9 -8
  84. zrb/env/__init__.py +0 -3
  85. zrb/env/any_env.py +2 -2
  86. zrb/env/env.py +4 -5
  87. zrb/env/env_file.py +4 -4
  88. zrb/env/env_map.py +4 -4
  89. zrb/group/__init__.py +0 -3
  90. zrb/group/any_group.py +3 -3
  91. zrb/group/group.py +7 -6
  92. zrb/input/any_input.py +1 -1
  93. zrb/input/base_input.py +4 -4
  94. zrb/input/bool_input.py +5 -5
  95. zrb/input/float_input.py +3 -3
  96. zrb/input/int_input.py +3 -3
  97. zrb/input/option_input.py +51 -0
  98. zrb/input/password_input.py +2 -2
  99. zrb/input/str_input.py +1 -1
  100. zrb/input/text_input.py +12 -10
  101. zrb/runner/cli.py +79 -44
  102. zrb/runner/web_app/group_info_ui/controller.py +7 -8
  103. zrb/runner/web_app/group_info_ui/view.html +2 -2
  104. zrb/runner/web_app/home_page/controller.py +7 -6
  105. zrb/runner/web_app/home_page/view.html +2 -2
  106. zrb/runner/web_app/task_ui/controller.py +13 -13
  107. zrb/runner/web_app/task_ui/partial/common-util.js +37 -0
  108. zrb/runner/web_app/task_ui/partial/main.js +9 -2
  109. zrb/runner/web_app/task_ui/partial/show-existing-session.js +20 -5
  110. zrb/runner/web_app/task_ui/partial/visualize-history.js +1 -41
  111. zrb/runner/web_app/task_ui/view.html +4 -2
  112. zrb/runner/web_server.py +137 -211
  113. zrb/runner/web_util.py +5 -35
  114. zrb/session/any_session.py +13 -7
  115. zrb/session/session.py +80 -41
  116. zrb/session_state_log/session_state_log.py +7 -5
  117. zrb/session_state_logger/any_session_state_logger.py +1 -1
  118. zrb/session_state_logger/default_session_state_logger.py +2 -2
  119. zrb/session_state_logger/file_session_state_logger.py +19 -27
  120. zrb/task/any_task.py +8 -3
  121. zrb/task/base_task.py +47 -33
  122. zrb/task/base_trigger.py +11 -12
  123. zrb/task/cmd_task.py +55 -43
  124. zrb/task/http_check.py +8 -8
  125. zrb/task/llm_task.py +160 -0
  126. zrb/task/make_task.py +9 -9
  127. zrb/task/rsync_task.py +7 -7
  128. zrb/task/scaffolder.py +14 -11
  129. zrb/task/scheduler.py +6 -7
  130. zrb/task/task.py +1 -1
  131. zrb/task/tcp_check.py +8 -8
  132. zrb/util/attr.py +19 -3
  133. zrb/util/cli/style.py +71 -2
  134. zrb/util/cli/subcommand.py +2 -2
  135. zrb/util/codemod/__init__.py +0 -0
  136. zrb/util/codemod/add_code_to_class.py +35 -0
  137. zrb/util/codemod/add_code_to_function.py +36 -0
  138. zrb/util/codemod/add_code_to_method.py +55 -0
  139. zrb/util/codemod/add_key_to_dict.py +51 -0
  140. zrb/util/codemod/add_param_to_function_call.py +39 -0
  141. zrb/util/codemod/add_property_to_class.py +55 -0
  142. zrb/util/git.py +156 -0
  143. zrb/util/git_subtree.py +94 -0
  144. zrb/util/group.py +2 -2
  145. zrb/util/llm/tool.py +63 -0
  146. zrb/util/string/conversion.py +7 -0
  147. zrb/util/todo.py +135 -0
  148. {zrb-1.0.0a1.dist-info → zrb-1.0.0a3.dist-info}/METADATA +11 -7
  149. zrb-1.0.0a3.dist-info/RECORD +194 -0
  150. zrb/builtin/shell/_group.py +0 -9
  151. zrb/builtin/shell/autocomplete/_group.py +0 -6
  152. zrb/runner/web_app/any_request_handler.py +0 -24
  153. zrb/runner/web_server.bak.py +0 -208
  154. zrb-1.0.0a1.dist-info/RECORD +0 -120
  155. {zrb-1.0.0a1.dist-info → zrb-1.0.0a3.dist-info}/WHEEL +0 -0
  156. {zrb-1.0.0a1.dist-info → zrb-1.0.0a3.dist-info}/entry_points.txt +0 -0
zrb/runner/web_server.py CHANGED
@@ -1,224 +1,150 @@
1
1
  import asyncio
2
- import datetime
3
- import json
4
2
  import os
5
- from concurrent.futures import ThreadPoolExecutor
6
- from functools import partial
7
- from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
8
- from threading import Thread
9
- from typing import Any
10
- from urllib.parse import parse_qs, urlparse
11
-
12
- from ..config import BANNER, SESSION_LOG_DIR, WEB_HTTP_PORT
13
- from ..context.any_context import AnyContext
14
- from ..context.shared_context import SharedContext
15
- from ..group.any_group import AnyGroup
16
- from ..session.session import Session
17
- from ..session_state_logger.default_session_state_logger import (
3
+ import sys
4
+ from contextlib import asynccontextmanager
5
+ from datetime import datetime, timedelta
6
+ from typing import Any, Dict, List
7
+
8
+ from fastapi import FastAPI, HTTPException, Request
9
+ from fastapi.responses import FileResponse, HTMLResponse
10
+ from fastapi.staticfiles import StaticFiles
11
+ from uvicorn import Config, Server
12
+
13
+ from zrb.config import BANNER, WEB_HTTP_PORT
14
+ from zrb.context.shared_context import SharedContext
15
+ from zrb.group.any_group import AnyGroup
16
+ from zrb.runner.web_app.group_info_ui.controller import handle_group_info_ui
17
+ from zrb.runner.web_app.home_page.controller import handle_home_page
18
+ from zrb.runner.web_app.task_ui.controller import handle_task_ui
19
+ from zrb.runner.web_util import NewSessionResponse
20
+ from zrb.session.session import Session
21
+ from zrb.session_state_log.session_state_log import SessionStateLog, SessionStateLogList
22
+ from zrb.session_state_logger.default_session_state_logger import (
18
23
  default_session_state_logger,
19
24
  )
20
- from ..task.any_task import AnyTask
21
- from ..util.group import extract_node_from_args, get_node_path
22
- from .web_app.group_info_ui.controller import handle_group_info_ui
23
- from .web_app.home_page.controller import handle_home_page
24
- from .web_app.task_ui.controller import handle_task_ui
25
- from .web_util import node_path_to_url, start_event_loop, url_to_args
26
-
27
- _DIR = os.path.dirname(__file__)
28
- _STATIC_DIR = os.path.join(_DIR, "web_app", "static")
29
- executor = ThreadPoolExecutor()
30
-
31
-
32
- class WebRequestHandler(BaseHTTPRequestHandler):
33
- def __init__(
34
- self,
35
- request: Any,
36
- client_address: Any,
37
- server: Any,
38
- root_group: AnyGroup,
39
- event_loop: asyncio.AbstractEventLoop,
40
- session_dir: str,
41
- coros: list,
42
- ):
43
- self._root_group = root_group
44
- self._event_loop = event_loop
45
- self._session_dir = session_dir
46
- self._coros = coros
47
- super().__init__(request=request, client_address=client_address, server=server)
48
-
49
- def do_GET(self):
50
- parsed_url = urlparse(self.path)
51
- if parsed_url.path in ["/", "/ui", "/ui/"]:
52
- handle_home_page(self, self._root_group)
53
- elif parsed_url.path == "/pico.min.css":
54
- self.send_css_response(os.path.join(_STATIC_DIR, "pico.min.css"))
55
- elif parsed_url.path == "/favicon-32x32.png":
56
- self.send_image_response(os.path.join(_STATIC_DIR, "favicon-32x32.png"))
57
- elif parsed_url.path.startswith("/ui/"):
58
- args = url_to_args(parsed_url.path[3:])
59
- node, node_path, residual_args = extract_node_from_args(
60
- self._root_group, args
61
- )
62
- url = f"/ui{node_path_to_url(node_path)}"
63
- if isinstance(node, AnyTask):
25
+ from zrb.task.any_task import AnyTask
26
+ from zrb.util.group import extract_node_from_args, get_node_path
27
+
28
+
29
+ def create_app(root_group: AnyGroup, port: int = WEB_HTTP_PORT):
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()
64
91
  shared_ctx = SharedContext(env=dict(os.environ))
65
- session = Session(shared_ctx=shared_ctx, root_group=self._root_group)
66
- handle_task_ui(
67
- self, self._root_group, node, session, url, residual_args
68
- )
69
- elif isinstance(node, AnyGroup):
70
- handle_group_info_ui(self, self._root_group, node, url)
71
- else:
72
- self.send_error(404, "Not Found")
73
- elif parsed_url.path.startswith("/api/"):
74
- args = url_to_args(parsed_url.path[5:])
75
- node, _, residual_args = extract_node_from_args(self._root_group, args)
76
- if isinstance(node, AnyTask) and len(residual_args) > 0:
77
- if residual_args[0] == "list":
78
- task_path = get_node_path(self._root_group, node)
79
- query_params = parse_qs(parsed_url.query)
80
- self.send_session_list_json_data(task_path, query_params)
81
- else:
82
- self.send_session_json_data(residual_args[0])
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)
83
110
  else:
84
- self.send_error(404, "Not Found")
85
- else:
86
- self.send_error(404, "Not Found")
87
-
88
- def do_POST(self):
89
- parsed_url = urlparse(self.path)
90
- if parsed_url.path.startswith("/api/"):
91
- args = url_to_args(parsed_url.path[5:])
92
- task, _, residual_args = extract_node_from_args(self._root_group, args)
93
- session_name = residual_args[0] if len(residual_args) > 0 else None
94
- if not isinstance(task, AnyTask):
95
- self.send_error(404, "Not found")
96
- elif session_name is None:
97
- str_kwargs: dict[str, str] = self.read_json_request()
98
- shared_ctx = SharedContext(env=dict(os.environ))
99
- session = Session(shared_ctx=shared_ctx, root_group=self._root_group)
100
- coro = asyncio.run_coroutine_threadsafe(
101
- task.async_run(session, str_kwargs=str_kwargs),
102
- self._event_loop,
103
- )
104
- self._coros.append(coro)
105
- coro.add_done_callback(lambda coro: self._coros.remove(coro))
106
- self.send_json_response({"session_name": session.name})
107
- else:
108
- self.send_error(404, "Not Found")
109
-
110
- def send_session_list_json_data(
111
- self, task_path: list[str], query_params: dict[str, list[Any]]
112
- ):
113
- print(query_params)
114
- max_start_time = datetime.datetime.now()
115
- if "to" in query_params and len(query_params["to"]) > 0:
116
- max_start_time = datetime.datetime.strptime(
117
- query_params["to"][0], "%Y-%m-%d %H:%M:%S"
118
- )
119
- min_start_time = max_start_time - datetime.timedelta(hours=1)
120
- if "from" in query_params and len(query_params["from"]) > 0:
121
- min_start_time = datetime.datetime.strptime(
122
- query_params["from"][0], "%Y-%m-%d %H:%M:%S"
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"
123
124
  )
124
- page = 0
125
- if "page" in query_params and len(query_params["page"]) > 0:
126
- page = int(query_params["page"][0])
127
- limit = 10
128
- if "limit" in query_params and len(query_params["limit"]) > 0:
129
- limit = int(query_params["limit"][0])
125
+ page = int(query_params.get("page", 0))
126
+ limit = int(query_params.get("limit", 10))
130
127
  try:
131
- self.send_json_response(
132
- default_session_state_logger.list(
133
- task_path,
134
- min_start_time=min_start_time,
135
- max_start_time=max_start_time,
136
- page=page,
137
- limit=limit,
138
- )
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,
139
134
  )
140
135
  except Exception as e:
141
- self.send_json_response({"error": f"{e}"}, 500)
136
+ raise HTTPException(status_code=500, detail=str(e))
142
137
 
143
- def send_session_json_data(self, session_name: str):
138
+ def read_session(session_name: str) -> SessionStateLog:
144
139
  try:
145
- self.send_json_response(default_session_state_logger.read(session_name))
140
+ return default_session_state_logger.read(session_name)
146
141
  except Exception as e:
147
- self.send_json_response({"error": f"{e}"}, 500)
148
-
149
- def send_json_response(self, data: Any, http_status: int = 200):
150
- self.send_response(http_status)
151
- self.send_header("Content-type", "application/json")
152
- self.end_headers()
153
- self.wfile.write(f"{json.dumps(data)}".encode())
154
-
155
- def send_html_response(self, html: str, http_status: int = 200):
156
- self.send_response(http_status)
157
- self.send_header("Content-type", "text/html")
158
- self.end_headers()
159
- self.wfile.write(f"{html}".encode())
160
-
161
- def send_css_response(self, css_path: str):
162
- self.send_response(200)
163
- self.send_header("Content-type", "text/css")
164
- self.end_headers()
165
- # If css_content is provided, send it; otherwise, you could read from a file here
166
- with open(css_path, "r") as f:
167
- css_content = f.read()
168
- self.wfile.write(css_content.encode())
169
-
170
- def send_image_response(self, image_path: str):
171
- try:
172
- with open(image_path, "rb") as img:
173
- self.send_response(200)
174
- if image_path.endswith(".png"):
175
- self.send_header("Content-type", "image/png")
176
- elif image_path.endswith(".jpg") or image_path.endswith(".jpeg"):
177
- self.send_header("Content-type", "image/jpeg")
178
- self.end_headers()
179
- self.wfile.write(img.read())
180
- except FileNotFoundError:
181
- self.send_error(404, "Image Not Found")
182
-
183
- def read_json_request(self) -> Any:
184
- content_length = int(self.headers["Content-Length"])
185
- post_data = self.rfile.read(content_length)
186
- try:
187
- return json.loads(post_data.decode())
188
- except json.JSONDecodeError:
189
- self.send_json_response({"error": "Invalid JSON"}, http_status=400)
190
- return None
191
-
192
-
193
- def run_web_server(ctx: AnyContext, root_group: AnyGroup, port: int = WEB_HTTP_PORT):
194
- server_address = ("", port)
195
- event_loop = asyncio.new_event_loop()
196
- coros = []
197
- # Use functools.partial to bind the custom attribute
198
- handler = partial(
199
- WebRequestHandler,
200
- root_group=root_group,
201
- event_loop=event_loop,
202
- session_dir=SESSION_LOG_DIR,
203
- coros=coros,
204
- )
205
- httpd = ThreadingHTTPServer(server_address, handler)
206
- banner_lines = BANNER.split("\n") + [
207
- f"Zrb Server running on http://localhost:{port}"
208
- ]
209
- for line in banner_lines:
210
- ctx.print(line)
211
- loop_thread = Thread(target=start_event_loop, args=[event_loop], daemon=True)
212
- loop_thread.start()
213
- try:
214
- httpd.serve_forever()
215
- finally:
216
- for coro in coros:
217
- coro.cancel()
218
- httpd.shutdown()
219
- httpd.server_close()
220
- # Cancel all tasks
221
- for task in asyncio.all_tasks(event_loop):
222
- task.cancel()
223
- event_loop.call_soon_threadsafe(event_loop.stop)
224
- loop_thread.join()
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()
zrb/runner/web_util.py CHANGED
@@ -1,4 +1,8 @@
1
- import asyncio
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class NewSessionResponse(BaseModel):
5
+ session_name: str
2
6
 
3
7
 
4
8
  def url_to_args(url: str) -> list[str]:
@@ -10,37 +14,3 @@ def node_path_to_url(args: list[str]) -> str:
10
14
  pruned_args = [part for part in args if part.strip() != ""]
11
15
  stripped_url = "/".join(pruned_args)
12
16
  return f"/{stripped_url}/"
13
-
14
-
15
- class SessionSnapshotCondition:
16
- def __init__(self):
17
- self._should_stop = False
18
-
19
- @property
20
- def should_stop(self) -> bool:
21
- return self._should_stop
22
-
23
- def stop(self):
24
- self._should_stop = True
25
-
26
-
27
- def start_event_loop(event_loop: asyncio.AbstractEventLoop):
28
- asyncio.set_event_loop(event_loop)
29
- event_loop.run_forever()
30
-
31
-
32
- async def shutdown_event_loop(event_loop: asyncio.AbstractEventLoop):
33
- tasks = [
34
- t
35
- for t in asyncio.all_tasks(event_loop)
36
- if not t.done() and t is not asyncio.current_task(event_loop)
37
- ]
38
- print("TASKS", tasks)
39
- for task in tasks:
40
- print("CANCEL TASK", task)
41
- task.cancel()
42
- print("CANCELED", task)
43
- await asyncio.gather(*tasks, return_exceptions=True)
44
- print("DONE")
45
- event_loop.stop()
46
- print("LOOP STOP")
@@ -3,15 +3,15 @@ from __future__ import annotations # Enables forward references
3
3
  from abc import ABC, abstractmethod
4
4
  from typing import TYPE_CHECKING, Any, Coroutine, TypeVar
5
5
 
6
- from ..context.any_context import AnyContext
7
- from ..group.any_group import AnyGroup
8
- from ..session_state_log.session_state_log import SessionStateLog
9
- from ..session_state_logger.any_session_state_logger import AnySessionStateLogger
10
- from ..task_status.task_status import TaskStatus
6
+ from zrb.context.any_context import AnyContext
7
+ from zrb.group.any_group import AnyGroup
8
+ from zrb.session_state_log.session_state_log import SessionStateLog
9
+ from zrb.session_state_logger.any_session_state_logger import AnySessionStateLogger
10
+ from zrb.task_status.task_status import TaskStatus
11
11
 
12
12
  if TYPE_CHECKING:
13
- from ..context import any_shared_context
14
- from ..task import any_task
13
+ from zrb.context import any_shared_context
14
+ from zrb.task import any_task
15
15
 
16
16
  TAnySession = TypeVar("TAnySession", bound="AnySession")
17
17
 
@@ -36,6 +36,12 @@ class AnySession(ABC):
36
36
  """Session root group"""
37
37
  pass
38
38
 
39
+ @property
40
+ @abstractmethod
41
+ def task_names(self) -> list[str]:
42
+ """Task names in this session"""
43
+ pass
44
+
39
45
  @property
40
46
  @abstractmethod
41
47
  def shared_ctx(self) -> any_shared_context.AnySharedContext:
zrb/session/session.py CHANGED
@@ -1,21 +1,37 @@
1
1
  import asyncio
2
2
  from typing import Any, Coroutine
3
3
 
4
- from ..context.any_shared_context import AnySharedContext
5
- from ..context.context import AnyContext, Context
6
- from ..group.any_group import AnyGroup
7
- from ..session_state_log.session_state_log import SessionStateLog, TaskStatusStateLog
8
- from ..session_state_logger.any_session_state_logger import AnySessionStateLogger
9
- from ..session_state_logger.default_session_state_logger import (
4
+ from zrb.context.any_shared_context import AnySharedContext
5
+ from zrb.context.context import AnyContext, Context
6
+ from zrb.group.any_group import AnyGroup
7
+ from zrb.session.any_session import AnySession
8
+ from zrb.session_state_log.session_state_log import (
9
+ SessionStateLog,
10
+ TaskStatusHistoryStateLog,
11
+ TaskStatusStateLog,
12
+ )
13
+ from zrb.session_state_logger.any_session_state_logger import AnySessionStateLogger
14
+ from zrb.session_state_logger.default_session_state_logger import (
10
15
  default_session_state_logger,
11
16
  )
12
- from ..task.any_task import AnyTask
13
- from ..task_status.task_status import TaskStatus
14
- from ..util.cli.style import BLUE, CYAN, GREEN, ICONS, MAGENTA, YELLOW
15
- from ..util.group import get_node_path
16
- from ..util.string.name import get_random_name
17
- from ..xcom.xcom import Xcom
18
- from .any_session import AnySession
17
+ from zrb.task.any_task import AnyTask
18
+ from zrb.task_status.task_status import TaskStatus
19
+ from zrb.util.cli.style import (
20
+ BLUE,
21
+ BRIGHT_BLUE,
22
+ BRIGHT_CYAN,
23
+ BRIGHT_GREEN,
24
+ BRIGHT_MAGENTA,
25
+ BRIGHT_YELLOW,
26
+ CYAN,
27
+ GREEN,
28
+ ICONS,
29
+ MAGENTA,
30
+ YELLOW,
31
+ )
32
+ from zrb.util.group import get_node_path
33
+ from zrb.util.string.name import get_random_name
34
+ from zrb.xcom.xcom import Xcom
19
35
 
20
36
 
21
37
  class Session(AnySession):
@@ -39,7 +55,18 @@ class Session(AnySession):
39
55
  self._action_coros: dict[AnyTask, Coroutine] = {}
40
56
  self._monitoring_coros: dict[AnyTask, Coroutine] = {}
41
57
  self._coros: list[Coroutine] = []
42
- self._colors = [GREEN, YELLOW, BLUE, MAGENTA, CYAN]
58
+ self._colors = [
59
+ GREEN,
60
+ YELLOW,
61
+ BLUE,
62
+ MAGENTA,
63
+ CYAN,
64
+ BRIGHT_GREEN,
65
+ BRIGHT_YELLOW,
66
+ BRIGHT_BLUE,
67
+ BRIGHT_MAGENTA,
68
+ BRIGHT_CYAN,
69
+ ]
43
70
  self._icons = ICONS
44
71
  self._color_index = 0
45
72
  self._icon_index = 0
@@ -62,6 +89,10 @@ class Session(AnySession):
62
89
  def root_group(self) -> AnyGroup | None:
63
90
  return self._root_group
64
91
 
92
+ @property
93
+ def task_names(self) -> list[str]:
94
+ return [task.name for task in self._task_status.keys()]
95
+
65
96
  @property
66
97
  def shared_ctx(self) -> AnySharedContext:
67
98
  return self._shared_ctx
@@ -100,39 +131,47 @@ class Session(AnySession):
100
131
  def set_main_task(self, main_task: AnyTask):
101
132
  self.register_task(main_task)
102
133
  self._main_task = main_task
103
- self._main_task_path = get_node_path(self._root_group, main_task)
134
+ main_task_path = get_node_path(self._root_group, main_task)
135
+ self._main_task_path = [] if main_task_path is None else main_task_path
104
136
 
105
137
  def as_state_log(self) -> SessionStateLog:
106
138
  task_status_log: dict[str, TaskStatusStateLog] = {}
139
+ log_start_time = ""
107
140
  for task, task_status in self._task_status.items():
108
- task_status_log[task.name] = {
109
- "is_started": task_status.is_started,
110
- "is_ready": task_status.is_ready,
111
- "is_completed": task_status.is_completed,
112
- "is_skipped": task_status.is_skipped,
113
- "is_failed": task_status.is_failed,
114
- "is_permanently_failed": task_status.is_permanently_failed,
115
- "is_terminated": task_status.is_terminated,
116
- "history": [
117
- {
118
- "status": status,
119
- "time": status_at.strftime("%Y-%m-%d %H:%M:%S.%f"),
120
- }
121
- for status, status_at in task_status.history
122
- ],
123
- }
124
- return {
125
- "name": self.name,
126
- "main_task_name": self._main_task.name,
127
- "path": self.task_path,
128
- "final_result": (
141
+ history_log = [
142
+ TaskStatusHistoryStateLog(
143
+ status=status,
144
+ time=status_at.strftime("%Y-%m-%d %H:%M:%S.%f"),
145
+ )
146
+ for status, status_at in task_status.history
147
+ ]
148
+ if len(history_log) > 0 and (
149
+ log_start_time == "" or history_log[0].time < log_start_time
150
+ ):
151
+ log_start_time = history_log[0].time
152
+ task_status_log[task.name] = TaskStatusStateLog(
153
+ is_started=task_status.is_started,
154
+ is_ready=task_status.is_ready,
155
+ is_completed=task_status.is_completed,
156
+ is_skipped=task_status.is_skipped,
157
+ is_failed=task_status.is_failed,
158
+ is_permanently_failed=task_status.is_permanently_failed,
159
+ is_terminated=task_status.is_terminated,
160
+ history=history_log,
161
+ )
162
+ return SessionStateLog(
163
+ name=self.name,
164
+ start_time=log_start_time,
165
+ main_task_name=self._main_task.name,
166
+ path=self.task_path,
167
+ final_result=(
129
168
  f"{self.final_result}" if self.final_result is not None else ""
130
169
  ),
131
- "finished": self.is_terminated,
132
- "log": self.shared_ctx.shared_log,
133
- "input": self.shared_ctx.input,
134
- "task_status": task_status_log,
135
- }
170
+ finished=self.is_terminated,
171
+ log=self.shared_ctx.shared_log,
172
+ input=self.shared_ctx.input,
173
+ task_status=task_status_log,
174
+ )
136
175
 
137
176
  def get_ctx(self, task: AnyTask) -> AnyContext:
138
177
  self._register_single_task(task)
@@ -1,12 +1,14 @@
1
- from typing import Any, TypedDict
1
+ from typing import Any
2
2
 
3
+ from pydantic import BaseModel
3
4
 
4
- class TaskStatusHistoryStateLog(TypedDict):
5
+
6
+ class TaskStatusHistoryStateLog(BaseModel):
5
7
  status: str
6
8
  time: str
7
9
 
8
10
 
9
- class TaskStatusStateLog(TypedDict):
11
+ class TaskStatusStateLog(BaseModel):
10
12
  history: list[TaskStatusHistoryStateLog]
11
13
  is_started: bool
12
14
  is_ready: bool
@@ -17,7 +19,7 @@ class TaskStatusStateLog(TypedDict):
17
19
  is_terminated: bool
18
20
 
19
21
 
20
- class SessionStateLog(TypedDict):
22
+ class SessionStateLog(BaseModel):
21
23
  name: str
22
24
  start_time: str
23
25
  main_task_name: str
@@ -29,6 +31,6 @@ class SessionStateLog(TypedDict):
29
31
  task_status: dict[str, TaskStatusStateLog]
30
32
 
31
33
 
32
- class SessionStateLogList(TypedDict):
34
+ class SessionStateLogList(BaseModel):
33
35
  total: int
34
36
  data: list[SessionStateLog]
@@ -1,7 +1,7 @@
1
1
  import datetime
2
2
  from abc import ABC, abstractmethod
3
3
 
4
- from ..session_state_log.session_state_log import SessionStateLog, SessionStateLogList
4
+ from zrb.session_state_log.session_state_log import SessionStateLog, SessionStateLogList
5
5
 
6
6
 
7
7
  class AnySessionStateLogger(ABC):
@@ -1,4 +1,4 @@
1
- from ..config import SESSION_LOG_DIR
2
- from .file_session_state_logger import FileSessionStateLogger
1
+ from zrb.config import SESSION_LOG_DIR
2
+ from zrb.session_state_logger.file_session_state_logger import FileSessionStateLogger
3
3
 
4
4
  default_session_state_logger = FileSessionStateLogger(SESSION_LOG_DIR)