zrb 1.0.0a21__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.
- zrb/__init__.py +2 -1
- zrb/builtin/llm/llm_chat.py +2 -2
- zrb/builtin/llm/tool/web.py +1 -1
- zrb/builtin/project/add/fastapp/fastapp_task.py +2 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/config.py +4 -1
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_task.py +16 -1
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +91 -9
- 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
- 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
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/gateway_subroute.py +37 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/format_task.py +1 -1
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/input.py +13 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_task.py +22 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +42 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/gateway/subroute/my_module.py +7 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/client/api_client.py +1 -1
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/client/direct_client.py +1 -2
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/client/factory.py +3 -3
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/route.py +10 -10
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/task.py +4 -4
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/app.py +42 -5
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/{base_usecase.py → base_service.py} +3 -3
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/view.py +37 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/config.py +24 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/api_client.py +2 -2
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/direct_client.py +2 -2
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/route.py +2 -2
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/{user_usecase.py → user_service.py} +7 -7
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_service_factory.py +6 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/route.py +42 -13
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/util/view.py +74 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/error.html +6 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/homepage.html +6 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/images/android-chrome-192x192.png +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/images/android-chrome-512x512.png +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/images/favicon-32x32.png +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.amber.min.css +4 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.blue.min.css +4 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.cyan.min.css +4 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.fuchsia.min.css +4 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.green.min.css +4 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.grey.min.css +4 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.indigo.min.css +4 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.jade.min.css +4 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.lime.min.css +4 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.min.css +4 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.orange.min.css +4 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.pink.min.css +4 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.pumpkin.min.css +4 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.purple.min.css +4 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.red.min.css +4 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.sand.min.css +4 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.slate.min.css +4 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.violet.min.css +4 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.yellow.min.css +4 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.zinc.min.css +4 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/template/default.html +34 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/requirements.txt +1 -0
- zrb/builtin/python.py +1 -1
- zrb/content_transformer/any_content_transformer.py +7 -0
- zrb/content_transformer/content_transformer.py +6 -0
- zrb/runner/cli.py +4 -6
- zrb/runner/web_app.py +28 -280
- zrb/runner/web_config/config.py +91 -0
- zrb/runner/web_config/config_factory.py +26 -0
- zrb/runner/web_route/docs_route.py +17 -0
- zrb/runner/web_route/error_page/serve_default_404.py +28 -0
- zrb/runner/{web_controller/error_page/controller.py → web_route/error_page/show_error_page.py} +2 -2
- zrb/runner/{web_controller → web_route}/error_page/view.html +5 -0
- zrb/runner/web_route/home_page/home_page_route.py +51 -0
- zrb/runner/web_route/login_api_route.py +31 -0
- zrb/runner/web_route/login_page/login_page_route.py +39 -0
- zrb/runner/web_route/logout_api_route.py +18 -0
- zrb/runner/web_route/logout_page/logout_page_route.py +40 -0
- zrb/runner/{web_controller/group_info_page/controller.py → web_route/node_page/group/show_group_page.py} +3 -3
- zrb/runner/web_route/node_page/node_page_route.py +50 -0
- zrb/runner/{web_controller/session_page/controller.py → web_route/node_page/task/show_task_page.py} +3 -3
- zrb/runner/web_route/refresh_token_api_route.py +38 -0
- zrb/runner/web_route/static/static_route.py +44 -0
- zrb/runner/web_route/task_input_api_route.py +47 -0
- zrb/runner/web_route/task_session_api_route.py +102 -0
- zrb/runner/web_schema/session.py +5 -0
- zrb/runner/web_schema/token.py +11 -0
- zrb/runner/web_schema/user.py +32 -0
- zrb/runner/web_util/cookie.py +29 -0
- zrb/runner/{web_util.py → web_util/html.py} +1 -23
- zrb/runner/web_util/token.py +68 -0
- zrb/runner/web_util/user.py +63 -0
- zrb/session/session.py +6 -4
- zrb/session_state_logger/{default_session_state_logger.py → session_state_logger_factory.py} +1 -1
- zrb/task/base_task.py +29 -4
- zrb/task/base_trigger.py +2 -0
- zrb/task/cmd_task.py +2 -0
- zrb/task/http_check.py +2 -0
- zrb/task/llm_task.py +2 -0
- zrb/task/make_task.py +2 -0
- zrb/task/rsync_task.py +2 -0
- zrb/task/scaffolder.py +8 -5
- zrb/task/scheduler.py +2 -0
- zrb/task/tcp_check.py +2 -0
- zrb/task_status/task_status.py +4 -3
- {zrb-1.0.0a21.dist-info → zrb-1.0.0b1.dist-info}/METADATA +1 -1
- {zrb-1.0.0a21.dist-info → zrb-1.0.0b1.dist-info}/RECORD +125 -81
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_usecase_factory.py +0 -6
- zrb/runner/web_config.py +0 -274
- zrb/runner/web_controller/home_page/__init__.py +0 -0
- zrb/runner/web_controller/home_page/controller.py +0 -33
- zrb/runner/web_controller/login_page/controller.py +0 -25
- zrb/runner/web_controller/logout_page/controller.py +0 -26
- zrb/runner/web_controller/session_page/__init__.py +0 -0
- /zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/column/{create_column_task.py → add_column_task.py} +0 -0
- /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
- /zrb/runner/{web_controller → web_route}/__init__.py +0 -0
- /zrb/runner/{web_controller/group_info_page → web_route/home_page}/__init__.py +0 -0
- /zrb/runner/{web_controller → web_route}/home_page/view.html +0 -0
- /zrb/runner/{web_controller → web_route}/login_page/view.html +0 -0
- /zrb/runner/{web_controller → web_route}/logout_page/view.html +0 -0
- /zrb/runner/{web_controller/group_info_page → web_route/node_page/group}/view.html +0 -0
- /zrb/runner/{web_controller/session_page → web_route/node_page/task}/partial/input.html +0 -0
- /zrb/runner/{web_controller/session_page → web_route/node_page/task}/view.html +0 -0
- /zrb/runner/{refresh-token.template.js → web_route/static/refresh-token.template.js} +0 -0
- /zrb/runner/{web_controller/static → web_route/static/resources}/common.css +0 -0
- /zrb/runner/{web_controller/static → web_route/static/resources}/favicon-32x32.png +0 -0
- /zrb/runner/{web_controller/static → web_route/static/resources}/login/event.js +0 -0
- /zrb/runner/{web_controller/static → web_route/static/resources}/logout/event.js +0 -0
- /zrb/runner/{web_controller/static → web_route/static/resources}/pico.min.css +0 -0
- /zrb/runner/{web_controller/static → web_route/static/resources}/session/common-util.js +0 -0
- /zrb/runner/{web_controller/static → web_route/static/resources}/session/current-session.js +0 -0
- /zrb/runner/{web_controller/static → web_route/static/resources}/session/event.js +0 -0
- /zrb/runner/{web_controller/static → web_route/static/resources}/session/past-session.js +0 -0
- {zrb-1.0.0a21.dist-info → zrb-1.0.0b1.dist-info}/WHEEL +0 -0
- {zrb-1.0.0a21.dist-info → zrb-1.0.0b1.dist-info}/entry_points.txt +0 -0
@@ -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,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,23 +1,9 @@
|
|
1
|
-
import os
|
2
|
-
|
3
1
|
from zrb.group.any_group import AnyGroup
|
4
|
-
from zrb.runner.
|
2
|
+
from zrb.runner.web_schema.user import User
|
5
3
|
from zrb.task.any_task import AnyTask
|
6
|
-
from zrb.util.file import read_file
|
7
4
|
from zrb.util.group import get_non_empty_subgroups, get_subtasks
|
8
5
|
|
9
6
|
|
10
|
-
def url_to_args(url: str) -> list[str]:
|
11
|
-
stripped_url = url.strip("/")
|
12
|
-
return [part for part in stripped_url.split("/") if part.strip() != ""]
|
13
|
-
|
14
|
-
|
15
|
-
def node_path_to_url(args: list[str]) -> str:
|
16
|
-
pruned_args = [part for part in args if part.strip() != ""]
|
17
|
-
stripped_url = "/".join(pruned_args)
|
18
|
-
return f"/{stripped_url}/"
|
19
|
-
|
20
|
-
|
21
7
|
def get_html_auth_link(user: User) -> str:
|
22
8
|
if user.is_guest and user.is_super_admin:
|
23
9
|
return f"Hi, {user.username}"
|
@@ -26,14 +12,6 @@ def get_html_auth_link(user: User) -> str:
|
|
26
12
|
return f'Hi, {user.username} <a href="/logout">Logout 🚪</a>'
|
27
13
|
|
28
14
|
|
29
|
-
def get_refresh_token_js(refresh_interval_seconds: int):
|
30
|
-
_DIR = os.path.dirname(__file__)
|
31
|
-
return read_file(
|
32
|
-
os.path.join(_DIR, "refresh-token.template.js"),
|
33
|
-
{"refreshIntervalSeconds": f"{refresh_interval_seconds}"},
|
34
|
-
)
|
35
|
-
|
36
|
-
|
37
15
|
def get_html_subtask_info(user: User, parent_url: str, parent_group: AnyGroup) -> str:
|
38
16
|
subtasks = get_subtasks(parent_group, web_only=True)
|
39
17
|
task_li = "\n".join(
|
@@ -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.
|
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
|
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]:
|
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.
|
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
|
zrb/task/scaffolder.py
CHANGED
@@ -11,6 +11,7 @@ from zrb.input.any_input import AnyInput
|
|
11
11
|
from zrb.task.any_task import AnyTask
|
12
12
|
from zrb.task.base_task import BaseTask
|
13
13
|
from zrb.util.attr import get_str_attr
|
14
|
+
from zrb.util.cli.style import stylize_faint
|
14
15
|
|
15
16
|
TransformConfig = dict[str, str] | Callable[[AnyContext, str], str]
|
16
17
|
|
@@ -46,6 +47,7 @@ class Scaffolder(BaseTask):
|
|
46
47
|
monitor_readiness: bool = False,
|
47
48
|
upstream: list[AnyTask] | AnyTask | None = None,
|
48
49
|
fallback: list[AnyTask] | AnyTask | None = None,
|
50
|
+
successor: list[AnyTask] | AnyTask | None = None,
|
49
51
|
):
|
50
52
|
super().__init__(
|
51
53
|
name=name,
|
@@ -66,6 +68,7 @@ class Scaffolder(BaseTask):
|
|
66
68
|
monitor_readiness=monitor_readiness,
|
67
69
|
upstream=upstream,
|
68
70
|
fallback=fallback,
|
71
|
+
successor=successor,
|
69
72
|
)
|
70
73
|
self._source_path = source_path
|
71
74
|
self._render_source_path = render_source_path
|
@@ -83,13 +86,12 @@ class Scaffolder(BaseTask):
|
|
83
86
|
return get_str_attr(ctx, self._destination_path, "", auto_render=True)
|
84
87
|
|
85
88
|
def _get_content_transformers(self) -> list[AnyContentTransformer]:
|
86
|
-
if callable(self._content_transformers)
|
87
|
-
|
88
|
-
|
89
|
-
]
|
90
|
-
if isinstance(self._content_transformers, dict):
|
89
|
+
if callable(self._content_transformers) or isinstance(
|
90
|
+
self._content_transformers, dict
|
91
|
+
):
|
91
92
|
return [
|
92
93
|
ContentTransformer(
|
94
|
+
name="default-transform",
|
93
95
|
match=".*",
|
94
96
|
transform=self._content_transformers,
|
95
97
|
auto_render=self._render_content_transformers,
|
@@ -109,6 +111,7 @@ class Scaffolder(BaseTask):
|
|
109
111
|
for transformer in transformers:
|
110
112
|
if transformer.match(ctx, file_path):
|
111
113
|
try:
|
114
|
+
ctx.print(stylize_faint(f"{transformer.name}: {file_path}"))
|
112
115
|
transformer.transform_file(ctx, file_path)
|
113
116
|
except UnicodeDecodeError:
|
114
117
|
pass
|
zrb/task/scheduler.py
CHANGED
@@ -39,6 +39,7 @@ class Scheduler(BaseTrigger):
|
|
39
39
|
monitor_readiness: bool = False,
|
40
40
|
upstream: list[AnyTask] | AnyTask | None = None,
|
41
41
|
fallback: list[AnyTask] | AnyTask | None = None,
|
42
|
+
successor: list[AnyTask] | AnyTask | None = None,
|
42
43
|
):
|
43
44
|
super().__init__(
|
44
45
|
name=name,
|
@@ -61,6 +62,7 @@ class Scheduler(BaseTrigger):
|
|
61
62
|
monitor_readiness=monitor_readiness,
|
62
63
|
upstream=upstream,
|
63
64
|
fallback=fallback,
|
65
|
+
successor=successor,
|
64
66
|
)
|
65
67
|
self._cron_pattern = schedule
|
66
68
|
|
zrb/task/tcp_check.py
CHANGED
@@ -28,6 +28,7 @@ class TcpCheck(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 TcpCheck(BaseTask):
|
|
41
42
|
retries=0,
|
42
43
|
upstream=upstream,
|
43
44
|
fallback=fallback,
|
45
|
+
successor=successor,
|
44
46
|
)
|
45
47
|
self._host = host
|
46
48
|
self._render_host = render_host
|
zrb/task_status/task_status.py
CHANGED
@@ -62,9 +62,10 @@ class TaskStatus:
|
|
62
62
|
self._history.append((TASK_PERMANENTLY_FAILED, datetime.datetime.now()))
|
63
63
|
|
64
64
|
def mark_as_terminated(self):
|
65
|
-
self._is_terminated
|
66
|
-
|
67
|
-
self.
|
65
|
+
if not self._is_terminated:
|
66
|
+
self._is_terminated = True
|
67
|
+
if not (self.is_skipped or self.is_completed or self.is_permanently_failed):
|
68
|
+
self._history.append((TASK_TERMINATED, datetime.datetime.now()))
|
68
69
|
|
69
70
|
@property
|
70
71
|
def is_started(self) -> bool:
|