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.
Files changed (132) 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/content_transformer/any_content_transformer.py +7 -0
  61. zrb/content_transformer/content_transformer.py +6 -0
  62. zrb/runner/cli.py +4 -6
  63. zrb/runner/web_app.py +28 -280
  64. zrb/runner/web_config/config.py +91 -0
  65. zrb/runner/web_config/config_factory.py +26 -0
  66. zrb/runner/web_route/docs_route.py +17 -0
  67. zrb/runner/web_route/error_page/serve_default_404.py +28 -0
  68. zrb/runner/{web_controller/error_page/controller.py → web_route/error_page/show_error_page.py} +2 -2
  69. zrb/runner/{web_controller → web_route}/error_page/view.html +5 -0
  70. zrb/runner/web_route/home_page/home_page_route.py +51 -0
  71. zrb/runner/web_route/login_api_route.py +31 -0
  72. zrb/runner/web_route/login_page/login_page_route.py +39 -0
  73. zrb/runner/web_route/logout_api_route.py +18 -0
  74. zrb/runner/web_route/logout_page/logout_page_route.py +40 -0
  75. zrb/runner/{web_controller/group_info_page/controller.py → web_route/node_page/group/show_group_page.py} +3 -3
  76. zrb/runner/web_route/node_page/node_page_route.py +50 -0
  77. zrb/runner/{web_controller/session_page/controller.py → web_route/node_page/task/show_task_page.py} +3 -3
  78. zrb/runner/web_route/refresh_token_api_route.py +38 -0
  79. zrb/runner/web_route/static/static_route.py +44 -0
  80. zrb/runner/web_route/task_input_api_route.py +47 -0
  81. zrb/runner/web_route/task_session_api_route.py +102 -0
  82. zrb/runner/web_schema/session.py +5 -0
  83. zrb/runner/web_schema/token.py +11 -0
  84. zrb/runner/web_schema/user.py +32 -0
  85. zrb/runner/web_util/cookie.py +29 -0
  86. zrb/runner/{web_util.py → web_util/html.py} +1 -23
  87. zrb/runner/web_util/token.py +68 -0
  88. zrb/runner/web_util/user.py +63 -0
  89. zrb/session/session.py +6 -4
  90. zrb/session_state_logger/{default_session_state_logger.py → session_state_logger_factory.py} +1 -1
  91. zrb/task/base_task.py +29 -4
  92. zrb/task/base_trigger.py +2 -0
  93. zrb/task/cmd_task.py +2 -0
  94. zrb/task/http_check.py +2 -0
  95. zrb/task/llm_task.py +2 -0
  96. zrb/task/make_task.py +2 -0
  97. zrb/task/rsync_task.py +2 -0
  98. zrb/task/scaffolder.py +8 -5
  99. zrb/task/scheduler.py +2 -0
  100. zrb/task/tcp_check.py +2 -0
  101. zrb/task_status/task_status.py +4 -3
  102. {zrb-1.0.0a21.dist-info → zrb-1.0.0b1.dist-info}/METADATA +1 -1
  103. {zrb-1.0.0a21.dist-info → zrb-1.0.0b1.dist-info}/RECORD +125 -81
  104. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_usecase_factory.py +0 -6
  105. zrb/runner/web_config.py +0 -274
  106. zrb/runner/web_controller/home_page/__init__.py +0 -0
  107. zrb/runner/web_controller/home_page/controller.py +0 -33
  108. zrb/runner/web_controller/login_page/controller.py +0 -25
  109. zrb/runner/web_controller/logout_page/controller.py +0 -26
  110. zrb/runner/web_controller/session_page/__init__.py +0 -0
  111. /zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/column/{create_column_task.py → add_column_task.py} +0 -0
  112. /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
  113. /zrb/runner/{web_controller → web_route}/__init__.py +0 -0
  114. /zrb/runner/{web_controller/group_info_page → web_route/home_page}/__init__.py +0 -0
  115. /zrb/runner/{web_controller → web_route}/home_page/view.html +0 -0
  116. /zrb/runner/{web_controller → web_route}/login_page/view.html +0 -0
  117. /zrb/runner/{web_controller → web_route}/logout_page/view.html +0 -0
  118. /zrb/runner/{web_controller/group_info_page → web_route/node_page/group}/view.html +0 -0
  119. /zrb/runner/{web_controller/session_page → web_route/node_page/task}/partial/input.html +0 -0
  120. /zrb/runner/{web_controller/session_page → web_route/node_page/task}/view.html +0 -0
  121. /zrb/runner/{refresh-token.template.js → web_route/static/refresh-token.template.js} +0 -0
  122. /zrb/runner/{web_controller/static → web_route/static/resources}/common.css +0 -0
  123. /zrb/runner/{web_controller/static → web_route/static/resources}/favicon-32x32.png +0 -0
  124. /zrb/runner/{web_controller/static → web_route/static/resources}/login/event.js +0 -0
  125. /zrb/runner/{web_controller/static → web_route/static/resources}/logout/event.js +0 -0
  126. /zrb/runner/{web_controller/static → web_route/static/resources}/pico.min.css +0 -0
  127. /zrb/runner/{web_controller/static → web_route/static/resources}/session/common-util.js +0 -0
  128. /zrb/runner/{web_controller/static → web_route/static/resources}/session/current-session.js +0 -0
  129. /zrb/runner/{web_controller/static → web_route/static/resources}/session/event.js +0 -0
  130. /zrb/runner/{web_controller/static → web_route/static/resources}/session/past-session.js +0 -0
  131. {zrb-1.0.0a21.dist-info → zrb-1.0.0b1.dist-info}/WHEEL +0 -0
  132. {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,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,23 +1,9 @@
1
- import os
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
- 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.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
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
- return [
88
- ContentTransformer(match=".*", transform=self._content_transformers)
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
@@ -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 = True
66
- if not self.is_completed and not self.is_permanently_failed:
67
- self._history.append((TASK_TERMINATED, datetime.datetime.now()))
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: zrb
3
- Version: 1.0.0a21
3
+ Version: 1.0.0b1
4
4
  Summary: Your Automation Powerhouse
5
5
  Home-page: https://github.com/state-alchemists/zrb
6
6
  License: AGPL-3.0-or-later