zrb 1.0.0a20__py3-none-any.whl → 1.0.0b1__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 (133) hide show
  1. zrb/__init__.py +2 -1
  2. zrb/builtin/llm/llm_chat.py +2 -2
  3. zrb/builtin/llm/tool/web.py +1 -1
  4. zrb/builtin/project/add/fastapp/fastapp_task.py +2 -0
  5. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/config.py +4 -1
  6. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_task.py +16 -1
  7. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +91 -9
  8. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/module/my_module/service/my_entity/{my_entity_usecase.py → my_entity_service.py} +7 -13
  9. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/module/my_module/service/my_entity/my_entity_service_factory.py +8 -0
  10. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/gateway_subroute.py +37 -0
  11. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/format_task.py +1 -1
  12. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/input.py +13 -0
  13. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_task.py +22 -0
  14. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +42 -0
  15. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/gateway/subroute/my_module.py +7 -0
  16. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/client/api_client.py +1 -1
  17. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/client/direct_client.py +1 -2
  18. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/client/factory.py +3 -3
  19. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/route.py +10 -10
  20. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/task.py +4 -4
  21. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/app.py +42 -5
  22. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/{base_usecase.py → base_service.py} +3 -3
  23. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/view.py +37 -0
  24. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/config.py +24 -0
  25. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/api_client.py +2 -2
  26. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/direct_client.py +2 -2
  27. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/route.py +2 -2
  28. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/{user_usecase.py → user_service.py} +7 -7
  29. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_service_factory.py +6 -0
  30. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/route.py +42 -13
  31. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/util/view.py +74 -0
  32. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/error.html +6 -0
  33. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/homepage.html +6 -0
  34. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/images/android-chrome-192x192.png +0 -0
  35. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/images/android-chrome-512x512.png +0 -0
  36. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/images/favicon-32x32.png +0 -0
  37. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.amber.min.css +4 -0
  38. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.blue.min.css +4 -0
  39. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.cyan.min.css +4 -0
  40. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.fuchsia.min.css +4 -0
  41. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.green.min.css +4 -0
  42. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.grey.min.css +4 -0
  43. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.indigo.min.css +4 -0
  44. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.jade.min.css +4 -0
  45. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.lime.min.css +4 -0
  46. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.min.css +4 -0
  47. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.orange.min.css +4 -0
  48. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.pink.min.css +4 -0
  49. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.pumpkin.min.css +4 -0
  50. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.purple.min.css +4 -0
  51. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.red.min.css +4 -0
  52. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.sand.min.css +4 -0
  53. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.slate.min.css +4 -0
  54. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.violet.min.css +4 -0
  55. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.yellow.min.css +4 -0
  56. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.zinc.min.css +4 -0
  57. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/template/default.html +34 -0
  58. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/requirements.txt +1 -0
  59. zrb/builtin/python.py +1 -1
  60. zrb/config.py +2 -2
  61. zrb/content_transformer/any_content_transformer.py +7 -0
  62. zrb/content_transformer/content_transformer.py +6 -0
  63. zrb/runner/cli.py +4 -6
  64. zrb/runner/web_app.py +28 -238
  65. zrb/runner/web_config/config.py +91 -0
  66. zrb/runner/web_config/config_factory.py +26 -0
  67. zrb/runner/web_route/docs_route.py +17 -0
  68. zrb/runner/web_route/error_page/serve_default_404.py +28 -0
  69. zrb/runner/{web_controller/error_page/controller.py → web_route/error_page/show_error_page.py} +2 -2
  70. zrb/runner/{web_controller → web_route}/error_page/view.html +6 -0
  71. zrb/runner/web_route/home_page/home_page_route.py +51 -0
  72. zrb/runner/{web_controller → web_route}/home_page/view.html +1 -0
  73. zrb/runner/web_route/login_api_route.py +31 -0
  74. zrb/runner/web_route/login_page/login_page_route.py +39 -0
  75. zrb/runner/{web_controller → web_route}/login_page/view.html +1 -0
  76. zrb/runner/web_route/logout_api_route.py +18 -0
  77. zrb/runner/web_route/logout_page/logout_page_route.py +40 -0
  78. zrb/runner/{web_controller → web_route}/logout_page/view.html +1 -0
  79. zrb/runner/{web_controller/group_info_page/controller.py → web_route/node_page/group/show_group_page.py} +3 -3
  80. zrb/runner/{web_controller/group_info_page → web_route/node_page/group}/view.html +1 -0
  81. zrb/runner/web_route/node_page/node_page_route.py +50 -0
  82. zrb/runner/{web_controller/session_page/controller.py → web_route/node_page/task/show_task_page.py} +5 -5
  83. zrb/runner/{web_controller/session_page → web_route/node_page/task}/view.html +1 -0
  84. zrb/runner/web_route/refresh_token_api_route.py +38 -0
  85. zrb/runner/web_route/static/refresh-token.template.js +22 -0
  86. zrb/runner/{web_controller/static → web_route/static/resources}/session/current-session.js +1 -1
  87. zrb/runner/{web_controller/static → web_route/static/resources}/session/event.js +5 -6
  88. zrb/runner/{web_controller/static → web_route/static/resources}/session/past-session.js +9 -3
  89. zrb/runner/web_route/static/static_route.py +44 -0
  90. zrb/runner/web_route/task_input_api_route.py +47 -0
  91. zrb/runner/web_route/task_session_api_route.py +102 -0
  92. zrb/runner/web_schema/session.py +5 -0
  93. zrb/runner/web_schema/token.py +11 -0
  94. zrb/runner/web_schema/user.py +32 -0
  95. zrb/runner/web_util/cookie.py +29 -0
  96. zrb/runner/{web_util.py → web_util/html.py} +1 -18
  97. zrb/runner/web_util/token.py +68 -0
  98. zrb/runner/web_util/user.py +63 -0
  99. zrb/session/session.py +6 -4
  100. zrb/session_state_logger/{default_session_state_logger.py → session_state_logger_factory.py} +1 -1
  101. zrb/task/base_task.py +29 -4
  102. zrb/task/base_trigger.py +2 -0
  103. zrb/task/cmd_task.py +2 -0
  104. zrb/task/http_check.py +2 -0
  105. zrb/task/llm_task.py +2 -0
  106. zrb/task/make_task.py +2 -0
  107. zrb/task/rsync_task.py +2 -0
  108. zrb/task/scaffolder.py +8 -5
  109. zrb/task/scheduler.py +2 -0
  110. zrb/task/tcp_check.py +2 -0
  111. zrb/task_status/task_status.py +4 -3
  112. {zrb-1.0.0a20.dist-info → zrb-1.0.0b1.dist-info}/METADATA +8 -52
  113. {zrb-1.0.0a20.dist-info → zrb-1.0.0b1.dist-info}/RECORD +126 -81
  114. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_usecase_factory.py +0 -6
  115. zrb/runner/web_config.py +0 -288
  116. zrb/runner/web_controller/home_page/__init__.py +0 -0
  117. zrb/runner/web_controller/home_page/controller.py +0 -33
  118. zrb/runner/web_controller/login_page/controller.py +0 -25
  119. zrb/runner/web_controller/logout_page/controller.py +0 -26
  120. zrb/runner/web_controller/session_page/__init__.py +0 -0
  121. /zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/column/{create_column_task.py → add_column_task.py} +0 -0
  122. /zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/module/my_module/service/my_entity/repository/{factory.py → my_entity_repository_factory.py} +0 -0
  123. /zrb/runner/{web_controller → web_route}/__init__.py +0 -0
  124. /zrb/runner/{web_controller/group_info_page → web_route/home_page}/__init__.py +0 -0
  125. /zrb/runner/{web_controller/session_page → web_route/node_page/task}/partial/input.html +0 -0
  126. /zrb/runner/{web_controller/static → web_route/static/resources}/common.css +0 -0
  127. /zrb/runner/{web_controller/static → web_route/static/resources}/favicon-32x32.png +0 -0
  128. /zrb/runner/{web_controller/static → web_route/static/resources}/login/event.js +0 -0
  129. /zrb/runner/{web_controller/static → web_route/static/resources}/logout/event.js +0 -0
  130. /zrb/runner/{web_controller/static → web_route/static/resources}/pico.min.css +0 -0
  131. /zrb/runner/{web_controller/static → web_route/static/resources}/session/common-util.js +0 -0
  132. {zrb-1.0.0a20.dist-info → zrb-1.0.0b1.dist-info}/WHEEL +0 -0
  133. {zrb-1.0.0a20.dist-info → zrb-1.0.0b1.dist-info}/entry_points.txt +0 -0
@@ -1,12 +1,18 @@
1
1
  const PAST_SESSION = {
2
+ shouldPoll: true,
2
3
 
3
- async pollPastSession() {
4
- while (true) {
5
- await this.getAndRenderPastSession(cfg.PAGE);
4
+ async startPolling() {
5
+ await this.getAndRenderPastSession(cfg.PAGE);
6
+ while (this.shouldPoll) {
6
7
  await UTIL.delay(5000);
8
+ await this.getAndRenderPastSession(cfg.PAGE);
7
9
  }
8
10
  },
9
11
 
12
+ stopPolling() {
13
+ this.shouldPoll = false;
14
+ },
15
+
10
16
  async getAndRenderPastSession(page) {
11
17
  cfg.PAGE=page
12
18
  const minStartAtInput = document.getElementById("min-start-at-input");
@@ -0,0 +1,44 @@
1
+ import os
2
+ from typing import TYPE_CHECKING
3
+
4
+ from zrb.runner.web_config.config import WebConfig
5
+ from zrb.util.file import read_file
6
+
7
+ if TYPE_CHECKING:
8
+ # We want fastapi to only be loaded when necessary to decrease footprint
9
+ from fastapi import FastAPI
10
+
11
+
12
+ def serve_static_resources(app: "FastAPI", web_config: WebConfig) -> None:
13
+ from fastapi import HTTPException
14
+ from fastapi.responses import FileResponse, PlainTextResponse
15
+ from fastapi.staticfiles import StaticFiles
16
+
17
+ _STATIC_DIR = os.path.join(os.path.dirname(__file__), "resources")
18
+
19
+ app.mount("/static", StaticFiles(directory=_STATIC_DIR), name="static")
20
+
21
+ # Serve static files
22
+ @app.get("/static/{file_path:path}", include_in_schema=False)
23
+ async def static_files(file_path: str):
24
+ full_path = os.path.join(_STATIC_DIR, file_path)
25
+ if os.path.isfile(full_path):
26
+ return FileResponse(full_path)
27
+ raise HTTPException(status_code=404, detail="File not found")
28
+
29
+ @app.get("/refresh-token.js", include_in_schema=False)
30
+ async def refresh_token_js():
31
+ return PlainTextResponse(
32
+ content=_get_refresh_token_js(
33
+ 60 * web_config.refresh_token_expire_minutes / 3
34
+ ),
35
+ media_type="application/javascript",
36
+ )
37
+
38
+
39
+ def _get_refresh_token_js(refresh_interval_seconds: int):
40
+ _DIR = os.path.dirname(__file__)
41
+ return read_file(
42
+ os.path.join(_DIR, "refresh-token.template.js"),
43
+ {"refreshIntervalSeconds": f"{refresh_interval_seconds}"},
44
+ )
@@ -0,0 +1,47 @@
1
+ import json
2
+ from typing import TYPE_CHECKING
3
+
4
+ from zrb.group.any_group import AnyGroup
5
+ from zrb.runner.common_util import get_run_kwargs
6
+ from zrb.runner.web_config.config import WebConfig
7
+ from zrb.runner.web_util.user import get_user_from_request
8
+ from zrb.task.any_task import AnyTask
9
+ from zrb.util.group import NodeNotFoundError, extract_node_from_args
10
+
11
+ if TYPE_CHECKING:
12
+ # We want fastapi to only be loaded when necessary to decrease footprint
13
+ from fastapi import FastAPI
14
+
15
+
16
+ def serve_task_input_api(
17
+ app: "FastAPI",
18
+ root_group: AnyGroup,
19
+ web_config: WebConfig,
20
+ ) -> None:
21
+ from fastapi import Query, Request
22
+ from fastapi.responses import JSONResponse
23
+
24
+ @app.get("/api/v1/task-inputs/{path:path}", response_model=dict[str, str])
25
+ async def get_default_inputs_api(
26
+ path: str,
27
+ request: Request,
28
+ query: str = Query("{}", description="JSON encoded inputs"),
29
+ ) -> dict[str, str]:
30
+ """
31
+ Getting input completion for path
32
+ """
33
+ user = await get_user_from_request(web_config, request)
34
+ args = path.strip("/").split("/")
35
+ try:
36
+ task, _, _ = extract_node_from_args(root_group, args)
37
+ except NodeNotFoundError:
38
+ return JSONResponse(content={"detail": "Not found"}, status_code=404)
39
+ if isinstance(task, AnyTask):
40
+ if not user.can_access_task(task):
41
+ return JSONResponse(content={"detail": "Forbidden"}, status_code=403)
42
+ query_dict = json.loads(query)
43
+ run_kwargs = get_run_kwargs(
44
+ task=task, args=[], kwargs=query_dict, prompt=False
45
+ )
46
+ return run_kwargs
47
+ return JSONResponse(content={"detail": "Not found"}, status_code=404)
@@ -0,0 +1,102 @@
1
+ import asyncio
2
+ import os
3
+ from datetime import datetime, timedelta
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from zrb.context.shared_context import SharedContext
7
+ from zrb.group.any_group import AnyGroup
8
+ from zrb.runner.web_config.config import WebConfig
9
+ from zrb.runner.web_schema.session import NewSessionResponse
10
+ from zrb.runner.web_util.user import get_user_from_request
11
+ from zrb.session.session import Session
12
+ from zrb.session_state_log.session_state_log import SessionStateLog, SessionStateLogList
13
+ from zrb.session_state_logger.any_session_state_logger import AnySessionStateLogger
14
+ from zrb.task.any_task import AnyTask
15
+ from zrb.util.group import NodeNotFoundError, extract_node_from_args, get_node_path
16
+
17
+ if TYPE_CHECKING:
18
+ # We want fastapi to only be loaded when necessary to decrease footprint
19
+
20
+ from fastapi import FastAPI
21
+
22
+
23
+ def serve_task_session_api(
24
+ app: "FastAPI",
25
+ root_group: AnyGroup,
26
+ web_config: WebConfig,
27
+ session_state_logger: AnySessionStateLogger,
28
+ coroutines: list,
29
+ ) -> None:
30
+ from fastapi import Query, Request
31
+ from fastapi.responses import JSONResponse
32
+
33
+ @app.post("/api/v1/task-sessions/{path:path}")
34
+ async def create_new_task_session_api(
35
+ path: str,
36
+ request: Request,
37
+ inputs: dict[str, Any],
38
+ ) -> NewSessionResponse:
39
+ """
40
+ Creating new session
41
+ """
42
+ user = await get_user_from_request(web_config, request)
43
+ args = path.strip("/").split("/")
44
+ try:
45
+ task, _, residual_args = extract_node_from_args(root_group, args)
46
+ except NodeNotFoundError:
47
+ return JSONResponse(content={"detail": "Not found"}, status_code=404)
48
+ if isinstance(task, AnyTask):
49
+ if not user.can_access_task(task):
50
+ return JSONResponse(content={"detail": "Forbidden"}, status_code=403)
51
+ session_name = residual_args[0] if residual_args else None
52
+ if not session_name:
53
+ shared_ctx = SharedContext(env=dict(os.environ))
54
+ session = Session(shared_ctx=shared_ctx, root_group=root_group)
55
+ coro = asyncio.create_task(task.async_run(session, str_kwargs=inputs))
56
+ coroutines.append(coro)
57
+ coro.add_done_callback(lambda coro: coroutines.remove(coro))
58
+ return NewSessionResponse(session_name=session.name)
59
+ return JSONResponse(content={"detail": "Not found"}, status_code=404)
60
+
61
+ @app.get(
62
+ "/api/v1/task-sessions/{path:path}",
63
+ response_model=SessionStateLog | SessionStateLogList,
64
+ )
65
+ async def get_task_session_api(
66
+ path: str,
67
+ request: Request,
68
+ min_start_query: str = Query(default=None, alias="from"),
69
+ max_start_query: str = Query(default=None, alias="to"),
70
+ page: int = Query(default=0, alias="page"),
71
+ limit: int = Query(default=10, alias="limit"),
72
+ ) -> SessionStateLog | SessionStateLogList:
73
+ """
74
+ Getting existing session or sessions
75
+ """
76
+ user = await get_user_from_request(web_config, request)
77
+ args = path.strip("/").split("/")
78
+ try:
79
+ task, _, residual_args = extract_node_from_args(root_group, args)
80
+ except NodeNotFoundError:
81
+ return JSONResponse(content={"detail": "Not found"}, status_code=404)
82
+ if isinstance(task, AnyTask) and residual_args:
83
+ if not user.can_access_task(task):
84
+ return JSONResponse(content={"detail": "Forbidden"}, status_code=403)
85
+ if residual_args[0] == "list":
86
+ task_path = get_node_path(root_group, task)
87
+ max_start_time = (
88
+ datetime.now()
89
+ if max_start_query is None
90
+ else datetime.strptime(max_start_query, "%Y-%m-%d %H:%M:%S")
91
+ )
92
+ min_start_time = (
93
+ max_start_time - timedelta(hours=1)
94
+ if min_start_query is None
95
+ else datetime.strptime(min_start_query, "%Y-%m-%d %H:%M:%S")
96
+ )
97
+ return session_state_logger.list(
98
+ task_path, min_start_time, max_start_time, page, limit
99
+ )
100
+ else:
101
+ return session_state_logger.read(residual_args[0])
102
+ return JSONResponse(content={"detail": "Not found"}, status_code=404)
@@ -0,0 +1,5 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class NewSessionResponse(BaseModel):
5
+ session_name: str
@@ -0,0 +1,11 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class RefreshTokenRequest(BaseModel):
5
+ refresh_token: str
6
+
7
+
8
+ class Token(BaseModel):
9
+ access_token: str
10
+ refresh_token: str
11
+ token_type: str
@@ -0,0 +1,32 @@
1
+ from pydantic import BaseModel, ConfigDict
2
+
3
+ from zrb.group.any_group import AnyGroup
4
+ from zrb.task.any_task import AnyTask
5
+ from zrb.util.group import get_all_subtasks
6
+
7
+
8
+ class User(BaseModel):
9
+ model_config = ConfigDict(arbitrary_types_allowed=True)
10
+ username: str
11
+ password: str = ""
12
+ is_super_admin: bool = False
13
+ is_guest: bool = False
14
+ accessible_tasks: list[AnyTask | str] = []
15
+
16
+ def is_password_match(self, password: str) -> bool:
17
+ return self.password == password
18
+
19
+ def can_access_group(self, group: AnyGroup) -> bool:
20
+ if self.is_super_admin:
21
+ return True
22
+ all_tasks = get_all_subtasks(group, web_only=True)
23
+ if any(self.can_access_task(task) for task in all_tasks):
24
+ return True
25
+ return False
26
+
27
+ def can_access_task(self, task: AnyTask) -> bool:
28
+ if self.is_super_admin:
29
+ return True
30
+ if task.name in self.accessible_tasks or task in self.accessible_tasks:
31
+ return True
32
+ return False
@@ -0,0 +1,29 @@
1
+ from datetime import datetime, timedelta, timezone
2
+ from typing import TYPE_CHECKING
3
+
4
+ from zrb.runner.web_config.config import WebConfig
5
+ from zrb.runner.web_schema.token import Token
6
+
7
+ if TYPE_CHECKING:
8
+ # We want fastapi to only be loaded when necessary to decrease footprint
9
+ from fastapi import Response
10
+
11
+
12
+ def set_auth_cookie(web_config: WebConfig, response: "Response", token: Token):
13
+ access_token_max_age = web_config.access_token_expire_minutes * 60
14
+ refresh_token_max_age = web_config.refresh_token_expire_minutes * 60
15
+ now = datetime.now(timezone.utc)
16
+ response.set_cookie(
17
+ key=web_config.access_token_cookie_name,
18
+ value=token.access_token,
19
+ httponly=True,
20
+ max_age=access_token_max_age,
21
+ expires=now + timedelta(seconds=access_token_max_age),
22
+ )
23
+ response.set_cookie(
24
+ key=web_config.refresh_token_cookie_name,
25
+ value=token.refresh_token,
26
+ httponly=True,
27
+ max_age=refresh_token_max_age,
28
+ expires=now + timedelta(seconds=refresh_token_max_age),
29
+ )
@@ -1,26 +1,9 @@
1
- from pydantic import BaseModel
2
-
3
1
  from zrb.group.any_group import AnyGroup
4
- from zrb.runner.web_config import User
2
+ from zrb.runner.web_schema.user import User
5
3
  from zrb.task.any_task import AnyTask
6
4
  from zrb.util.group import get_non_empty_subgroups, get_subtasks
7
5
 
8
6
 
9
- class NewSessionResponse(BaseModel):
10
- session_name: str
11
-
12
-
13
- def url_to_args(url: str) -> list[str]:
14
- stripped_url = url.strip("/")
15
- return [part for part in stripped_url.split("/") if part.strip() != ""]
16
-
17
-
18
- def node_path_to_url(args: list[str]) -> str:
19
- pruned_args = [part for part in args if part.strip() != ""]
20
- stripped_url = "/".join(pruned_args)
21
- return f"/{stripped_url}/"
22
-
23
-
24
7
  def get_html_auth_link(user: User) -> str:
25
8
  if user.is_guest and user.is_super_admin:
26
9
  return f"Hi, {user.username}"
@@ -0,0 +1,68 @@
1
+ from datetime import datetime, timedelta
2
+
3
+ from zrb.runner.web_config.config import WebConfig
4
+ from zrb.runner.web_schema.token import Token
5
+ from zrb.runner.web_util.user import get_user_by_credentials
6
+
7
+
8
+ def generate_tokens_by_credentials(
9
+ web_config: WebConfig, username: str, password: str
10
+ ) -> Token | None:
11
+ if not web_config.enable_auth:
12
+ user = web_config.default_user
13
+ else:
14
+ user = get_user_by_credentials(web_config, username, password)
15
+ if user is None:
16
+ return None
17
+ access_token = _generate_access_token(web_config, user.username)
18
+ refresh_token = _generate_refresh_token(web_config, user.username)
19
+ return Token(
20
+ access_token=access_token, refresh_token=refresh_token, token_type="bearer"
21
+ )
22
+
23
+
24
+ def regenerate_tokens(web_config: WebConfig, refresh_token: str) -> Token:
25
+ from fastapi import HTTPException
26
+ from jose import jwt
27
+
28
+ # Decode and validate token
29
+ try:
30
+ payload = jwt.decode(
31
+ refresh_token,
32
+ web_config.secret_key,
33
+ options={"require_exp": True, "require_sub": True},
34
+ )
35
+ except Exception:
36
+ raise HTTPException(status_code=401, detail="Invalid JWT token")
37
+ if payload.get("type") != "refresh":
38
+ raise HTTPException(status_code=401, detail="Invalid token type")
39
+ username: str = payload.get("sub")
40
+ if username is None:
41
+ raise HTTPException(status_code=401, detail="Invalid refresh token")
42
+ user = web_config.find_user_by_username(username)
43
+ if user is None:
44
+ raise HTTPException(status_code=401, detail="User not found")
45
+ # Create new token
46
+ new_access_token = _generate_access_token(web_config, username)
47
+ new_refresh_token = _generate_refresh_token(web_config, username)
48
+ return Token(
49
+ access_token=new_access_token,
50
+ refresh_token=new_refresh_token,
51
+ token_type="bearer",
52
+ )
53
+
54
+
55
+ def _generate_access_token(web_config: WebConfig, username: str) -> str:
56
+ from jose import jwt
57
+
58
+ expire = datetime.now() + timedelta(minutes=web_config.access_token_expire_minutes)
59
+ to_encode = {"sub": username, "exp": expire, "type": "access"}
60
+ return jwt.encode(to_encode, web_config.secret_key)
61
+
62
+
63
+ def _generate_refresh_token(web_config: WebConfig, username: str) -> str:
64
+ from jose import jwt
65
+
66
+ expire = datetime.now() + timedelta(minutes=web_config.refresh_token_expire_minutes)
67
+ to_encode = {"sub": username, "exp": expire, "type": "refresh"}
68
+ return jwt.encode(to_encode, web_config.secret_key)
@@ -0,0 +1,63 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from zrb.runner.web_config.config import WebConfig
4
+ from zrb.runner.web_schema.user import User
5
+
6
+ if TYPE_CHECKING:
7
+ # Import Request only for type checking to reduce runtime dependencies
8
+ from fastapi import Request
9
+
10
+
11
+ def get_user_by_credentials(
12
+ web_config: WebConfig, username: str, password: str
13
+ ) -> User | None:
14
+ user = web_config.find_user_by_username(username)
15
+ if user is None or not user.is_password_match(password):
16
+ return None
17
+ return user
18
+
19
+
20
+ async def get_user_from_request(
21
+ web_config: WebConfig, request: "Request"
22
+ ) -> User | None:
23
+ from fastapi.security import OAuth2PasswordBearer
24
+
25
+ if not web_config.enable_auth:
26
+ return web_config.default_user
27
+ # Normally we use "Depends"
28
+ get_bearer_token = OAuth2PasswordBearer(tokenUrl="/api/v1/login", auto_error=False)
29
+ bearer_token = await get_bearer_token(request)
30
+ token_user = _get_user_from_token(web_config, bearer_token)
31
+ if token_user is not None:
32
+ return token_user
33
+ cookie_user = _get_user_from_cookie(web_config, request)
34
+ if cookie_user is not None:
35
+ return cookie_user
36
+ return web_config.default_user
37
+
38
+
39
+ def _get_user_from_cookie(web_config: WebConfig, request: "Request") -> User | None:
40
+ token = request.cookies.get(web_config.access_token_cookie_name)
41
+ if token:
42
+ return _get_user_from_token(web_config, token)
43
+ return None
44
+
45
+
46
+ def _get_user_from_token(web_config: WebConfig, token: str) -> User | None:
47
+ try:
48
+ from jose import jwt
49
+
50
+ payload = jwt.decode(
51
+ token,
52
+ web_config.secret_key,
53
+ options={"require_sub": True, "require_exp": True},
54
+ )
55
+ username: str = payload.get("sub")
56
+ if username is None:
57
+ return None
58
+ user = web_config.find_user_by_username(username)
59
+ if user is None:
60
+ return None
61
+ return user
62
+ except Exception:
63
+ return None
zrb/session/session.py CHANGED
@@ -11,9 +11,7 @@ from zrb.session_state_log.session_state_log import (
11
11
  TaskStatusStateLog,
12
12
  )
13
13
  from zrb.session_state_logger.any_session_state_logger import AnySessionStateLogger
14
- from zrb.session_state_logger.default_session_state_logger import (
15
- default_session_state_logger,
16
- )
14
+ from zrb.session_state_logger.session_state_logger_factory import session_state_logger
17
15
  from zrb.task.any_task import AnyTask
18
16
  from zrb.task_status.task_status import TaskStatus
19
17
  from zrb.util.cli.style import (
@@ -126,7 +124,7 @@ class Session(AnySession):
126
124
  @property
127
125
  def state_logger(self) -> AnySessionStateLogger:
128
126
  if self._state_logger is None:
129
- return default_session_state_logger
127
+ return session_state_logger
130
128
  return self._state_logger
131
129
 
132
130
  def set_main_task(self, main_task: AnyTask):
@@ -215,6 +213,10 @@ class Session(AnySession):
215
213
  self._register_single_task(task)
216
214
  for readiness_check in task.readiness_checks:
217
215
  self.register_task(readiness_check)
216
+ for successor in task.successors:
217
+ self.register_task(successor)
218
+ for fallback in task.fallbacks:
219
+ self.register_task(fallback)
218
220
  for upstream in task.upstreams:
219
221
  self.register_task(upstream)
220
222
  if task not in self._downstreams[upstream]:
@@ -1,4 +1,4 @@
1
1
  from zrb.config import SESSION_LOG_DIR
2
2
  from zrb.session_state_logger.file_session_state_logger import FileSessionStateLogger
3
3
 
4
- default_session_state_logger = FileSessionStateLogger(SESSION_LOG_DIR)
4
+ session_state_logger = FileSessionStateLogger(SESSION_LOG_DIR)
zrb/task/base_task.py CHANGED
@@ -265,11 +265,9 @@ class BaseTask(AnyTask):
265
265
  def __fill_shared_context_envs(self, shared_context: AnySharedContext):
266
266
  # Inject os environ
267
267
  os_env_map = {
268
- key: val
269
- for key, val in os.environ.items()
270
- if key not in shared_context._env
268
+ key: val for key, val in os.environ.items() if key not in shared_context.env
271
269
  }
272
- shared_context._env.update(os_env_map)
270
+ shared_context.env.update(os_env_map)
273
271
 
274
272
  async def exec_root_tasks(self, session: AnySession):
275
273
  session.set_main_task(self)
@@ -416,6 +414,23 @@ class BaseTask(AnyTask):
416
414
  ctx.log_info("Continue monitoring")
417
415
 
418
416
  async def __exec_action_and_retry(self, session: AnySession) -> Any:
417
+ """
418
+ Executes an action with retry logic.
419
+
420
+ This method attempts to execute the action defined in `_exec_action` with a specified number of retries.
421
+ If the action fails, it will retry after a specified period until the maximum number of attempts is reached.
422
+ If the action succeeds, it marks the task as completed and executes any successors.
423
+ If the action fails permanently, it marks the task as permanently failed and executes any fallbacks.
424
+
425
+ Args:
426
+ session (AnySession): The session object containing the task status and context.
427
+
428
+ Returns:
429
+ Any: The result of the executed action if successful.
430
+
431
+ Raises:
432
+ Exception: If the action fails permanently after all retry attempts.
433
+ """
419
434
  ctx = self.get_ctx(session)
420
435
  max_attempt = self._retries + 1
421
436
  ctx.set_max_attempt(max_attempt)
@@ -433,6 +448,7 @@ class BaseTask(AnyTask):
433
448
  # Put result on xcom
434
449
  task_xcom: Xcom = ctx.xcom.get(self.name)
435
450
  task_xcom.push(result)
451
+ self.__skip_fallbacks(session)
436
452
  await run_async(self.__exec_successors(session))
437
453
  return result
438
454
  except (asyncio.CancelledError, KeyboardInterrupt):
@@ -447,6 +463,7 @@ class BaseTask(AnyTask):
447
463
  continue
448
464
  ctx.log_info("Marked as permanently failed")
449
465
  session.get_task_status(self).mark_as_permanently_failed()
466
+ self.__skip_successors(session)
450
467
  await run_async(self.__exec_fallbacks(session))
451
468
  raise e
452
469
 
@@ -457,6 +474,10 @@ class BaseTask(AnyTask):
457
474
  ]
458
475
  await asyncio.gather(*successor_coros)
459
476
 
477
+ def __skip_successors(self, session: AnySession) -> Any:
478
+ for successor in self.successors:
479
+ session.get_task_status(successor).mark_as_skipped()
480
+
460
481
  async def __exec_fallbacks(self, session: AnySession) -> Any:
461
482
  fallbacks: list[AnyTask] = self.fallbacks
462
483
  fallback_coros = [
@@ -464,6 +485,10 @@ class BaseTask(AnyTask):
464
485
  ]
465
486
  await asyncio.gather(*fallback_coros)
466
487
 
488
+ def __skip_fallbacks(self, session: AnySession) -> Any:
489
+ for fallback in self.fallbacks:
490
+ session.get_task_status(fallback).mark_as_skipped()
491
+
467
492
  async def _exec_action(self, ctx: AnyContext) -> Any:
468
493
  """Execute the main action of the task.
469
494
  By default will render and run the _action attribute.
zrb/task/base_trigger.py CHANGED
@@ -42,6 +42,7 @@ class BaseTrigger(BaseTask):
42
42
  monitor_readiness: bool = False,
43
43
  upstream: list[AnyTask] | AnyTask | None = None,
44
44
  fallback: list[AnyTask] | AnyTask | None = None,
45
+ successor: list[AnyTask] | AnyTask | None = None,
45
46
  ):
46
47
  super().__init__(
47
48
  name=name,
@@ -63,6 +64,7 @@ class BaseTrigger(BaseTask):
63
64
  monitor_readiness=monitor_readiness,
64
65
  upstream=upstream,
65
66
  fallback=fallback,
67
+ successor=successor,
66
68
  )
67
69
  self._callbacks = callback
68
70
  self._queue_name = queue_name
zrb/task/cmd_task.py CHANGED
@@ -56,6 +56,7 @@ class CmdTask(BaseTask):
56
56
  monitor_readiness: bool = False,
57
57
  upstream: list[AnyTask] | AnyTask | None = None,
58
58
  fallback: list[AnyTask] | AnyTask | None = None,
59
+ successor: list[AnyTask] | AnyTask | None = None,
59
60
  ):
60
61
  super().__init__(
61
62
  name=name,
@@ -76,6 +77,7 @@ class CmdTask(BaseTask):
76
77
  monitor_readiness=monitor_readiness,
77
78
  upstream=upstream,
78
79
  fallback=fallback,
80
+ successor=successor,
79
81
  )
80
82
  self._shell = shell
81
83
  self._render_shell = render_shell
zrb/task/http_check.py CHANGED
@@ -28,6 +28,7 @@ class HttpCheck(BaseTask):
28
28
  execute_condition: bool | str | Callable[[Context], bool] = True,
29
29
  upstream: list[AnyTask] | AnyTask | None = None,
30
30
  fallback: list[AnyTask] | AnyTask | None = None,
31
+ successor: list[AnyTask] | AnyTask | None = None,
31
32
  ):
32
33
  super().__init__(
33
34
  name=name,
@@ -41,6 +42,7 @@ class HttpCheck(BaseTask):
41
42
  retries=0,
42
43
  upstream=upstream,
43
44
  fallback=fallback,
45
+ successor=successor,
44
46
  )
45
47
  self._url = url
46
48
  self._render_url = render_url
zrb/task/llm_task.py CHANGED
@@ -67,6 +67,7 @@ class LLMTask(BaseTask):
67
67
  monitor_readiness: bool = False,
68
68
  upstream: list[AnyTask] | AnyTask | None = None,
69
69
  fallback: list[AnyTask] | AnyTask | None = None,
70
+ successor: list[AnyTask] | AnyTask | None = None,
70
71
  ):
71
72
  super().__init__(
72
73
  name=name,
@@ -87,6 +88,7 @@ class LLMTask(BaseTask):
87
88
  monitor_readiness=monitor_readiness,
88
89
  upstream=upstream,
89
90
  fallback=fallback,
91
+ successor=successor,
90
92
  )
91
93
  self._model = model
92
94
  self._render_model = render_model
zrb/task/make_task.py CHANGED
@@ -29,6 +29,7 @@ def make_task(
29
29
  monitor_readiness: bool = False,
30
30
  upstream: list[AnyTask] | AnyTask | None = None,
31
31
  fallback: list[AnyTask] | AnyTask | None = None,
32
+ successor: list[AnyTask] | AnyTask | None = None,
32
33
  group: AnyGroup | None = None,
33
34
  alias: str | None = None,
34
35
  ) -> Callable[[Callable[[AnyContext], Any]], AnyTask]:
@@ -53,6 +54,7 @@ def make_task(
53
54
  monitor_readiness=monitor_readiness,
54
55
  upstream=upstream,
55
56
  fallback=fallback,
57
+ successor=successor,
56
58
  )
57
59
  if group is not None:
58
60
  return group.add_task(task, alias=alias)
zrb/task/rsync_task.py CHANGED
@@ -49,6 +49,7 @@ class RsyncTask(CmdTask):
49
49
  readiness_check: list[AnyTask] | AnyTask | None = None,
50
50
  upstream: list[AnyTask] | AnyTask | None = None,
51
51
  fallback: list[AnyTask] | AnyTask | None = None,
52
+ successor: list[AnyTask] | AnyTask | None = None,
52
53
  ):
53
54
  super().__init__(
54
55
  name=name,
@@ -80,6 +81,7 @@ class RsyncTask(CmdTask):
80
81
  readiness_check=readiness_check,
81
82
  upstream=upstream,
82
83
  fallback=fallback,
84
+ successor=successor,
83
85
  )
84
86
  self._remote_source_path = remote_source_path
85
87
  self._render_remote_source_path = render_remote_source_path