zrb 1.0.0a16__py3-none-any.whl → 1.0.0a20__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 (196) hide show
  1. zrb/__init__.py +5 -0
  2. zrb/__main__.py +3 -0
  3. zrb/builtin/__init__.py +2 -2
  4. zrb/builtin/git.py +10 -2
  5. zrb/builtin/git_subtree.py +4 -0
  6. zrb/builtin/llm/tool/rag.py +2 -2
  7. zrb/builtin/project/add/fastapp/fastapp_input.py +16 -0
  8. zrb/builtin/project/add/fastapp/fastapp_task.py +78 -0
  9. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/.flake8 +3 -0
  10. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/column/create_column_task.py +14 -0
  11. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_task.py +128 -0
  12. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +213 -0
  13. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/any_client_method.py +27 -0
  14. zrb/builtin/project/add/{fastapp_template/_zrb/entity/module_template → fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/module/my_module}/service/my_entity/my_entity_usecase.py +9 -10
  15. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/module/my_module/service/my_entity/repository/factory.py +13 -0
  16. zrb/builtin/project/add/{fastapp_template/_zrb/entity/module_template → fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/module/my_module}/service/my_entity/repository/my_entity_db_repository.py +14 -9
  17. zrb/builtin/project/add/{fastapp_template/_zrb/entity/module_template → fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/module/my_module}/service/my_entity/repository/my_entity_repository.py +6 -7
  18. zrb/builtin/project/add/{fastapp_template/_zrb/entity/schema.template.py → fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/schema/my_entity.py} +8 -0
  19. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/format_task.py +17 -0
  20. zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/_zrb/input.py +1 -4
  21. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_task.py +85 -0
  22. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +154 -0
  23. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/client/any_client.py +7 -0
  24. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/client/api_client.py +6 -0
  25. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/client/direct_client.py +6 -0
  26. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/client/factory.py +9 -0
  27. zrb/builtin/project/add/{fastapp_template/_zrb/module/module_template → fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module}/migration/env.py +2 -4
  28. zrb/builtin/project/add/{fastapp_template/module/auth → fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module}/migration/script.py.mako +1 -0
  29. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/route.py +33 -0
  30. zrb/builtin/project/add/{fastapp_template/_zrb/main.py → fastapp/fastapp_template/my_app_name/_zrb/task.py} +12 -14
  31. zrb/builtin/project/add/{fastapp_template/_zrb/helper.py → fastapp/fastapp_template/my_app_name/_zrb/util.py} +1 -1
  32. zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/_zrb/venv_task.py +1 -1
  33. zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/common/app.py +2 -2
  34. zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/common/base_db_repository.py +1 -1
  35. zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/common/base_usecase.py +19 -6
  36. zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/common/db_engine.py +1 -1
  37. zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/config.py +1 -0
  38. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/main.py +7 -0
  39. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/migrate.py +3 -0
  40. zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/module/auth/client/any_client.py +10 -4
  41. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/api_client.py +7 -0
  42. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/direct_client.py +6 -0
  43. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/factory.py +9 -0
  44. zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/module/auth/migration/env.py +2 -4
  45. zrb/builtin/project/add/{fastapp_template/module/gateway → fastapp/fastapp_template/my_app_name/module/auth}/migration/script.py.mako +1 -0
  46. zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/module/auth/migration_metadata.py +1 -1
  47. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/route.py +37 -0
  48. zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/module/auth/service/user/repository/user_db_repository.py +13 -7
  49. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_repository.py +42 -0
  50. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_repository_factory.py +13 -0
  51. zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/module/auth/service/user/user_usecase.py +13 -12
  52. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_usecase_factory.py +6 -0
  53. zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/module/gateway/migration/env.py +2 -4
  54. zrb/builtin/project/add/{fastapp_template/_zrb/module/module_template → fastapp/fastapp_template/my_app_name/module/gateway}/migration/script.py.mako +1 -0
  55. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/route.py +37 -0
  56. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/subroute/auth.py +44 -0
  57. zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/requirements.txt +1 -1
  58. zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/schema/permission.py +8 -0
  59. zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/schema/role.py +8 -0
  60. zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/schema/user.py +8 -0
  61. zrb/builtin/project/add/fastapp/fastapp_util.py +46 -0
  62. zrb/builtin/project/create/{create.py → project_task.py} +1 -1
  63. zrb/builtin/python.py +4 -1
  64. zrb/builtin/setup/asdf/asdf_helper.py +4 -8
  65. zrb/builtin/setup/tmux/tmux.py +7 -12
  66. zrb/builtin/todo.py +42 -26
  67. zrb/callback/callback.py +0 -1
  68. zrb/cmd/cmd_val.py +2 -2
  69. zrb/config.py +18 -0
  70. zrb/content_transformer/content_transformer.py +8 -7
  71. zrb/context/any_context.py +6 -6
  72. zrb/group/group.py +0 -1
  73. zrb/input/any_input.py +4 -0
  74. zrb/input/base_input.py +17 -5
  75. zrb/input/bool_input.py +1 -1
  76. zrb/input/float_input.py +2 -2
  77. zrb/input/int_input.py +1 -1
  78. zrb/input/option_input.py +2 -2
  79. zrb/input/password_input.py +2 -2
  80. zrb/input/text_input.py +6 -6
  81. zrb/runner/cli.py +9 -35
  82. zrb/runner/common_util.py +31 -0
  83. zrb/runner/web_app.py +169 -46
  84. zrb/runner/web_config.py +288 -0
  85. zrb/runner/web_controller/error_page/controller.py +27 -0
  86. zrb/runner/web_controller/error_page/view.html +33 -0
  87. zrb/runner/web_controller/group_info_page/controller.py +40 -0
  88. zrb/runner/web_controller/group_info_page/view.html +36 -0
  89. zrb/runner/web_controller/home_page/controller.py +14 -57
  90. zrb/runner/web_controller/home_page/view.html +29 -20
  91. zrb/runner/web_controller/login_page/controller.py +25 -0
  92. zrb/runner/web_controller/login_page/view.html +50 -0
  93. zrb/runner/web_controller/logout_page/controller.py +26 -0
  94. zrb/runner/web_controller/logout_page/view.html +40 -0
  95. zrb/runner/web_controller/{task_ui → session_page}/controller.py +36 -34
  96. zrb/runner/web_controller/{task_ui → session_page}/partial/input.html +1 -1
  97. zrb/runner/web_controller/session_page/view.html +91 -0
  98. zrb/runner/web_controller/static/common.css +11 -0
  99. zrb/runner/web_controller/static/login/event.js +33 -0
  100. zrb/runner/web_controller/static/logout/event.js +20 -0
  101. zrb/runner/web_controller/static/pico.min.css +1 -1
  102. zrb/runner/web_controller/static/session/common-util.js +63 -0
  103. zrb/runner/web_controller/static/session/current-session.js +164 -0
  104. zrb/runner/web_controller/static/session/event.js +120 -0
  105. zrb/runner/web_controller/static/session/past-session.js +138 -0
  106. zrb/runner/web_util.py +53 -0
  107. zrb/session_state_logger/any_session_state_logger.py +0 -1
  108. zrb/session_state_logger/file_session_state_logger.py +4 -8
  109. zrb/task/base_trigger.py +0 -1
  110. zrb/task/cmd_task.py +1 -1
  111. zrb/task/llm_task.py +3 -6
  112. zrb/task/make_task.py +0 -1
  113. zrb/task/scaffolder.py +18 -4
  114. zrb/task/scheduler.py +0 -1
  115. zrb/util/cmd/command.py +0 -1
  116. zrb/util/codemod/{add_code_to_class.py → append_code_to_class.py} +4 -4
  117. zrb/util/codemod/{add_code_to_function.py → append_code_to_function.py} +5 -3
  118. zrb/util/codemod/{add_code_to_method.py → append_code_to_method.py} +3 -3
  119. zrb/util/codemod/{add_key_to_dict.py → append_key_to_dict.py} +1 -1
  120. zrb/util/codemod/{add_param_to_function_call.py → append_param_to_function_call.py} +1 -1
  121. zrb/util/codemod/{add_code_to_module.py → prepend_code_to_module.py} +2 -2
  122. zrb/util/codemod/{add_parent_to_class.py → prepend_parent_to_class.py} +1 -1
  123. zrb/util/codemod/{add_property_to_class.py → prepend_property_to_class.py} +1 -1
  124. zrb/util/file.py +18 -0
  125. zrb/util/git_subtree.py +3 -4
  126. zrb/util/todo.py +105 -24
  127. zrb/xcom/xcom.py +0 -1
  128. {zrb-1.0.0a16.dist-info → zrb-1.0.0a20.dist-info}/METADATA +3 -2
  129. zrb-1.0.0a20.dist-info/RECORD +243 -0
  130. zrb/builtin/project/add/fastapp.py +0 -87
  131. zrb/builtin/project/add/fastapp_template/_zrb/column/create_column_task.py +0 -11
  132. zrb/builtin/project/add/fastapp_template/_zrb/entity/create_entity_task.py +0 -196
  133. zrb/builtin/project/add/fastapp_template/_zrb/entity/module_template/service/my_entity/repository/factory.py +0 -13
  134. zrb/builtin/project/add/fastapp_template/_zrb/module/create_module_task.py +0 -136
  135. zrb/builtin/project/add/fastapp_template/_zrb/module/module_template/client/any_client.py +0 -27
  136. zrb/builtin/project/add/fastapp_template/_zrb/module/module_template/client/api_client.py +0 -6
  137. zrb/builtin/project/add/fastapp_template/_zrb/module/module_template/client/direct_client.py +0 -6
  138. zrb/builtin/project/add/fastapp_template/_zrb/module/module_template/client/factory.py +0 -9
  139. zrb/builtin/project/add/fastapp_template/_zrb/module/module_template/route.py +0 -19
  140. zrb/builtin/project/add/fastapp_template/main.py +0 -7
  141. zrb/builtin/project/add/fastapp_template/migrate.py +0 -3
  142. zrb/builtin/project/add/fastapp_template/module/auth/client/api_client.py +0 -7
  143. zrb/builtin/project/add/fastapp_template/module/auth/client/direct_client.py +0 -6
  144. zrb/builtin/project/add/fastapp_template/module/auth/client/factory.py +0 -9
  145. zrb/builtin/project/add/fastapp_template/module/auth/migration/versions/3093c7336477_add_user_table.py +0 -37
  146. zrb/builtin/project/add/fastapp_template/module/auth/route.py +0 -22
  147. zrb/builtin/project/add/fastapp_template/module/auth/service/user/repository/factory.py +0 -13
  148. zrb/builtin/project/add/fastapp_template/module/auth/service/user/repository/user_repository.py +0 -34
  149. zrb/builtin/project/add/fastapp_template/module/gateway/route.py +0 -27
  150. zrb/runner/web_controller/group_info_ui/controller.py +0 -91
  151. zrb/runner/web_controller/group_info_ui/partial/group_info.html +0 -2
  152. zrb/runner/web_controller/group_info_ui/partial/group_li.html +0 -1
  153. zrb/runner/web_controller/group_info_ui/partial/task_info.html +0 -2
  154. zrb/runner/web_controller/group_info_ui/partial/task_li.html +0 -1
  155. zrb/runner/web_controller/group_info_ui/view.html +0 -31
  156. zrb/runner/web_controller/home_page/partial/group_info.html +0 -2
  157. zrb/runner/web_controller/home_page/partial/group_li.html +0 -1
  158. zrb/runner/web_controller/home_page/partial/task_info.html +0 -2
  159. zrb/runner/web_controller/home_page/partial/task_li.html +0 -1
  160. zrb/runner/web_controller/task_ui/partial/common-util.js +0 -37
  161. zrb/runner/web_controller/task_ui/partial/main.js +0 -195
  162. zrb/runner/web_controller/task_ui/partial/show-existing-session.js +0 -97
  163. zrb/runner/web_controller/task_ui/partial/visualize-history.js +0 -104
  164. zrb/runner/web_controller/task_ui/view.html +0 -87
  165. zrb-1.0.0a16.dist-info/RECORD +0 -231
  166. /zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/.gitignore +0 -0
  167. /zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/README.md +0 -0
  168. /zrb/builtin/project/add/{__init__.py → fastapp/fastapp_template/my_app_name/__init__.py} +0 -0
  169. /zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/_zrb/config.py +0 -0
  170. /zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/_zrb/group.py +0 -0
  171. /zrb/builtin/project/add/{fastapp_template/__init__.py → fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/gateway/subroute/my_module.py} +0 -0
  172. /zrb/builtin/project/add/{fastapp_template/_zrb/module/module_template → fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module}/alembic.ini +0 -0
  173. /zrb/builtin/project/add/{fastapp_template/_zrb/module/module_template → fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module}/migration/README +0 -0
  174. /zrb/builtin/project/add/{fastapp_template/module/gateway → fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module}/migration/versions/.gitkeep +0 -0
  175. /zrb/builtin/project/add/{fastapp_template/_zrb/module/module_template → fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module}/migration_metadata.py +0 -0
  176. /zrb/builtin/project/add/{fastapp_template/_zrb/module/module_template → fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module}/service/__init__.py +0 -0
  177. /zrb/builtin/project/add/{fastapp_template/_zrb/module/run_module.template.py → fastapp/fastapp_template/my_app_name/_zrb/module/template/module_task_definition.py} +0 -0
  178. /zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/common/__init__.py +0 -0
  179. /zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/common/error.py +0 -0
  180. /zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/common/schema.py +0 -0
  181. /zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/module/__init__.py +0 -0
  182. /zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/module/auth/alembic.ini +0 -0
  183. /zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/module/auth/migration/README +0 -0
  184. /zrb/builtin/project/add/{fastapp_template/_zrb/module/module_template → fastapp/fastapp_template/my_app_name/module/auth}/migration/versions/3093c7336477_add_user_table.py +0 -0
  185. /zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/module/auth/service/__init__.py +0 -0
  186. /zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/module/auth/service/user/__init__.py +0 -0
  187. /zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/module/gateway/alembic.ini +0 -0
  188. /zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/module/gateway/migration/README +0 -0
  189. /zrb/builtin/project/add/{fastapp_template/module/auth/service/user/repository/__init__.py → fastapp/fastapp_template/my_app_name/module/gateway/migration/versions/.gitkeep} +0 -0
  190. /zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/module/gateway/migration_metadata.py +0 -0
  191. /zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/schema/__init__.py +0 -0
  192. /zrb/builtin/project/add/{fastapp_template → fastapp/fastapp_template/my_app_name}/template.env +0 -0
  193. /zrb/runner/web_controller/{group_info_ui → group_info_page}/__init__.py +0 -0
  194. /zrb/runner/web_controller/{task_ui → session_page}/__init__.py +0 -0
  195. {zrb-1.0.0a16.dist-info → zrb-1.0.0a20.dist-info}/WHEEL +0 -0
  196. {zrb-1.0.0a16.dist-info → zrb-1.0.0a20.dist-info}/entry_points.txt +0 -0
zrb/runner/web_app.py CHANGED
@@ -1,30 +1,44 @@
1
1
  import asyncio
2
+ import json
2
3
  import os
3
4
  import sys
4
5
  from datetime import datetime, timedelta
5
- from typing import Any
6
+ from typing import TYPE_CHECKING, Annotated
6
7
 
7
- from zrb.config import BANNER, WEB_HTTP_PORT
8
+ from zrb.config import BANNER, VERSION
8
9
  from zrb.context.shared_context import SharedContext
9
10
  from zrb.group.any_group import AnyGroup
10
- from zrb.runner.web_controller.group_info_ui.controller import handle_group_info_ui
11
- from zrb.runner.web_controller.home_page.controller import handle_home_page
12
- from zrb.runner.web_controller.task_ui.controller import handle_task_ui
11
+ from zrb.runner.common_util import get_run_kwargs
12
+ from zrb.runner.web_config import Token, WebConfig
13
+ from zrb.runner.web_controller.error_page.controller import show_error_page
14
+ from zrb.runner.web_controller.group_info_page.controller import show_group_info_page
15
+ from zrb.runner.web_controller.home_page.controller import show_home_page
16
+ from zrb.runner.web_controller.login_page.controller import show_login_page
17
+ from zrb.runner.web_controller.logout_page.controller import show_logout_page
18
+ from zrb.runner.web_controller.session_page.controller import show_session_page
13
19
  from zrb.runner.web_util import NewSessionResponse
14
20
  from zrb.session.session import Session
15
21
  from zrb.session_state_log.session_state_log import SessionStateLog, SessionStateLogList
16
- from zrb.session_state_logger.default_session_state_logger import (
17
- default_session_state_logger,
18
- )
22
+ from zrb.session_state_logger.any_session_state_logger import AnySessionStateLogger
19
23
  from zrb.task.any_task import AnyTask
20
- from zrb.util.group import extract_node_from_args, get_node_path
24
+ from zrb.util.group import NodeNotFoundError, extract_node_from_args, get_node_path
21
25
 
26
+ if TYPE_CHECKING:
27
+ # We want fastapi to only be loaded when necessary to decrease footprint
28
+ from fastapi import FastAPI
22
29
 
23
- def create_app(root_group: AnyGroup, port: int = WEB_HTTP_PORT):
30
+
31
+ def create_app(
32
+ root_group: AnyGroup,
33
+ web_config: WebConfig,
34
+ session_state_logger: AnySessionStateLogger,
35
+ ) -> "FastAPI":
24
36
  from contextlib import asynccontextmanager
25
37
 
26
- from fastapi import FastAPI, HTTPException, Query, Request
38
+ from fastapi import Depends, FastAPI, HTTPException, Query, Request, Response
39
+ from fastapi.openapi.docs import get_swagger_ui_html
27
40
  from fastapi.responses import FileResponse, HTMLResponse
41
+ from fastapi.security import OAuth2PasswordRequestForm
28
42
  from fastapi.staticfiles import StaticFiles
29
43
 
30
44
  _STATIC_DIR = os.path.join(os.path.dirname(__file__), "web_controller", "static")
@@ -33,7 +47,7 @@ def create_app(root_group: AnyGroup, port: int = WEB_HTTP_PORT):
33
47
  @asynccontextmanager
34
48
  async def lifespan(app: FastAPI):
35
49
  for line in BANNER.split("\n") + [
36
- f"Zrb Server running on http://localhost:{port}"
50
+ f"Zrb Server running on http://localhost:{web_config.port}"
37
51
  ]:
38
52
  print(line, file=sys.stderr)
39
53
  yield
@@ -41,17 +55,16 @@ def create_app(root_group: AnyGroup, port: int = WEB_HTTP_PORT):
41
55
  coro.cancel()
42
56
  asyncio.gather(*_COROS)
43
57
 
44
- app = FastAPI(title="zrb", lifespan=lifespan)
45
-
46
- # Serve static files
58
+ app = FastAPI(
59
+ title="Zrb",
60
+ version=VERSION,
61
+ summary="Your Automation Powerhouse",
62
+ lifespan=lifespan,
63
+ docs_url=None,
64
+ )
47
65
  app.mount("/static", StaticFiles(directory=_STATIC_DIR), name="static")
48
66
 
49
- @app.get("/", response_class=HTMLResponse, include_in_schema=False)
50
- @app.get("/ui", response_class=HTMLResponse, include_in_schema=False)
51
- @app.get("/ui/", response_class=HTMLResponse, include_in_schema=False)
52
- async def home_page():
53
- return handle_home_page(root_group)
54
-
67
+ # Serve static files
55
68
  @app.get("/static/{file_path:path}", include_in_schema=False)
56
69
  async def static_files(file_path: str):
57
70
  full_path = os.path.join(_STATIC_DIR, file_path)
@@ -59,59 +72,169 @@ def create_app(root_group: AnyGroup, port: int = WEB_HTTP_PORT):
59
72
  return FileResponse(full_path)
60
73
  raise HTTPException(status_code=404, detail="File not found")
61
74
 
62
- @app.get("/ui/{path:path}", include_in_schema=False)
63
- async def ui_page(path: str):
75
+ @app.get("/docs", include_in_schema=False)
76
+ async def swagger_ui_html():
77
+ return get_swagger_ui_html(
78
+ openapi_url="/openapi.json",
79
+ title="Zrb",
80
+ swagger_favicon_url="/static/favicon-32x32.png",
81
+ )
82
+
83
+ # Serve homepage
84
+ @app.get("/", response_class=HTMLResponse, include_in_schema=False)
85
+ @app.get("/ui", response_class=HTMLResponse, include_in_schema=False)
86
+ @app.get("/ui/", response_class=HTMLResponse, include_in_schema=False)
87
+ async def home_page_ui(request: Request) -> HTMLResponse:
88
+ user = await web_config.get_user_by_request(request)
89
+ return show_home_page(user, root_group)
90
+
91
+ @app.get("/ui/{path:path}", response_class=HTMLResponse, include_in_schema=False)
92
+ async def ui_page(path: str, request: Request) -> HTMLResponse:
93
+ user = await web_config.get_user_by_request(request)
64
94
  # Avoid capturing '/ui' itself
65
95
  if not path:
66
- raise HTTPException(status_code=404, detail="Not Found")
96
+ return show_error_page(user, root_group, 422, "Undefined path")
67
97
  args = path.strip("/").split("/")
68
- node, node_path, residual_args = extract_node_from_args(root_group, args)
98
+ try:
99
+ node, node_path, residual_args = extract_node_from_args(root_group, args)
100
+ except NodeNotFoundError as e:
101
+ return show_error_page(user, root_group, 404, str(e))
69
102
  url = f"/ui/{'/'.join(node_path)}/"
70
103
  if isinstance(node, AnyTask):
104
+ if not user.can_access_task(node):
105
+ return show_error_page(user, root_group, 403, "Forbidden")
71
106
  shared_ctx = SharedContext(env=dict(os.environ))
72
107
  session = Session(shared_ctx=shared_ctx, root_group=root_group)
73
- return handle_task_ui(root_group, node, session, url, residual_args)
108
+ return show_session_page(
109
+ user, root_group, node, session, url, residual_args
110
+ )
74
111
  elif isinstance(node, AnyGroup):
75
- return handle_group_info_ui(root_group, node, url)
76
- raise HTTPException(status_code=404, detail="Not Found")
112
+ if not user.can_access_group(node):
113
+ return show_error_page(user, root_group, 403, "Forbidden")
114
+ return show_group_info_page(user, root_group, node, url)
115
+ return show_error_page(user, root_group, 404, "Not found")
116
+
117
+ @app.get("/login", response_class=HTMLResponse, include_in_schema=False)
118
+ async def login(request: Request) -> HTMLResponse:
119
+ user = await web_config.get_user_by_request(request)
120
+ return show_login_page(user, root_group)
77
121
 
78
- @app.post("/api/{path:path}")
79
- async def create_new_session(
80
- path: str, request: Request = None
122
+ @app.get("/logout", response_class=HTMLResponse, include_in_schema=False)
123
+ async def logout(request: Request) -> HTMLResponse:
124
+ user = await web_config.get_user_by_request(request)
125
+ return show_logout_page(user, root_group)
126
+
127
+ @app.post("/api/v1/login")
128
+ async def login_api(
129
+ response: Response, form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
130
+ ):
131
+ token = web_config.generate_tokens(
132
+ username=form_data.username, password=form_data.password
133
+ )
134
+ _set_auth_cookie(response, token)
135
+ return token
136
+
137
+ @app.post("/api/v1/refresh-token")
138
+ async def refresh_token_api(
139
+ response: Response, refresh_token: str = Query(..., description="Refresh token")
140
+ ):
141
+ token = web_config.refresh_tokens(refresh_token)
142
+ _set_auth_cookie(response, token)
143
+ return token
144
+
145
+ def _set_auth_cookie(response: Response, token: Token):
146
+ response.set_cookie(
147
+ key=web_config.access_token_cookie_name,
148
+ value=token.access_token,
149
+ httponly=True,
150
+ max_age=web_config.access_token_max_age,
151
+ expires=web_config.access_token_max_age,
152
+ )
153
+ response.set_cookie(
154
+ key=web_config.refresh_token_cookie_name,
155
+ value=token.refresh_token,
156
+ httponly=True,
157
+ max_age=web_config.refresh_token_max_age,
158
+ expires=web_config.refresh_token_max_age,
159
+ )
160
+
161
+ @app.get("/api/v1/logout")
162
+ @app.post("/api/v1/logout")
163
+ async def logout_api(response: Response):
164
+ response.delete_cookie(web_config.access_token_cookie_name)
165
+ response.delete_cookie(web_config.refresh_token_cookie_name)
166
+ return {"message": "Logout successful"}
167
+
168
+ @app.post("/api/sessions/{path:path}")
169
+ async def create_new_session_api(
170
+ path: str,
171
+ request: Request,
81
172
  ) -> NewSessionResponse:
82
173
  """
83
174
  Creating new session
84
175
  """
176
+ user = await web_config.get_user_by_request(request)
85
177
  args = path.strip("/").split("/")
86
- node, _, residual_args = extract_node_from_args(root_group, args)
87
- if isinstance(node, AnyTask):
178
+ task, _, residual_args = extract_node_from_args(root_group, args)
179
+ if isinstance(task, AnyTask):
180
+ if not user.can_access_task(task):
181
+ raise HTTPException(status_code=403)
88
182
  session_name = residual_args[0] if residual_args else None
89
183
  if not session_name:
90
184
  body = await request.json()
91
185
  shared_ctx = SharedContext(env=dict(os.environ))
92
186
  session = Session(shared_ctx=shared_ctx, root_group=root_group)
93
- coro = asyncio.create_task(node.async_run(session, str_kwargs=body))
187
+ coro = asyncio.create_task(task.async_run(session, str_kwargs=body))
94
188
  _COROS.append(coro)
95
189
  coro.add_done_callback(lambda coro: _COROS.remove(coro))
96
190
  return NewSessionResponse(session_name=session.name)
191
+ raise HTTPException(status_code=404)
192
+
193
+ @app.get("/api/inputs/{path:path}", response_model=dict[str, str])
194
+ async def get_default_inputs_api(
195
+ path: str,
196
+ request: Request,
197
+ query: str = Query("{}", description="JSON encoded inputs"),
198
+ ) -> dict[str, str]:
199
+ """
200
+ Getting input completion for path
201
+ """
202
+ user = await web_config.get_user_by_request(request)
203
+ args = path.strip("/").split("/")
204
+ task, _, _ = extract_node_from_args(root_group, args)
205
+ if isinstance(task, AnyTask):
206
+ if not user.can_access_task(task):
207
+ raise HTTPException(status_code=403)
208
+ query_dict = json.loads(query)
209
+ run_kwargs = get_run_kwargs(
210
+ task=task, args=[], kwargs=query_dict, prompt=False
211
+ )
212
+ return run_kwargs
97
213
  raise HTTPException(status_code=404, detail="Not Found")
98
214
 
99
- @app.get("/api/{path:path}", response_model=SessionStateLog | SessionStateLogList)
100
- async def get_session(
215
+ @app.get(
216
+ "/api/sessions/{path:path}",
217
+ response_model=SessionStateLog | SessionStateLogList,
218
+ )
219
+ async def get_session_api(
101
220
  path: str,
221
+ request: Request,
102
222
  min_start_query: str = Query(default=None, alias="from"),
103
223
  max_start_query: str = Query(default=None, alias="to"),
104
224
  page: int = Query(default=0, alias="page"),
105
225
  limit: int = Query(default=10, alias="limit"),
106
- ):
226
+ ) -> SessionStateLog | SessionStateLogList:
107
227
  """
108
228
  Getting existing session or sessions
109
229
  """
230
+ user = await web_config.get_user_by_request(request)
110
231
  args = path.strip("/").split("/")
111
- node, _, residual_args = extract_node_from_args(root_group, args)
112
- if isinstance(node, AnyTask) and residual_args:
232
+ task, _, residual_args = extract_node_from_args(root_group, args)
233
+ if isinstance(task, AnyTask) and residual_args:
234
+ if not user.can_access_task(task):
235
+ raise HTTPException(status_code=403)
113
236
  if residual_args[0] == "list":
114
- task_path = get_node_path(root_group, node)
237
+ task_path = get_node_path(root_group, task)
115
238
  max_start_time = (
116
239
  datetime.now()
117
240
  if max_start_query is None
@@ -122,14 +245,14 @@ def create_app(root_group: AnyGroup, port: int = WEB_HTTP_PORT):
122
245
  if min_start_query is None
123
246
  else datetime.strptime(min_start_query, "%Y-%m-%d %H:%M:%S")
124
247
  )
125
- return list_sessions(
248
+ return _get_existing_sessions(
126
249
  task_path, min_start_time, max_start_time, page, limit
127
250
  )
128
251
  else:
129
- return read_session(residual_args[0])
252
+ return _read_session(residual_args[0])
130
253
  raise HTTPException(status_code=404, detail="Not Found")
131
254
 
132
- def list_sessions(
255
+ def _get_existing_sessions(
133
256
  task_path: list[str],
134
257
  min_start_time: datetime,
135
258
  max_start_time: datetime,
@@ -137,7 +260,7 @@ def create_app(root_group: AnyGroup, port: int = WEB_HTTP_PORT):
137
260
  limit: int,
138
261
  ) -> SessionStateLogList:
139
262
  try:
140
- return default_session_state_logger.list(
263
+ return session_state_logger.list(
141
264
  task_path,
142
265
  min_start_time=min_start_time,
143
266
  max_start_time=max_start_time,
@@ -147,9 +270,9 @@ def create_app(root_group: AnyGroup, port: int = WEB_HTTP_PORT):
147
270
  except Exception as e:
148
271
  raise HTTPException(status_code=500, detail=str(e))
149
272
 
150
- def read_session(session_name: str) -> SessionStateLog:
273
+ def _read_session(session_name: str) -> SessionStateLog:
151
274
  try:
152
- return default_session_state_logger.read(session_name)
275
+ return session_state_logger.read(session_name)
153
276
  except Exception as e:
154
277
  raise HTTPException(status_code=500, detail=str(e))
155
278
 
@@ -0,0 +1,288 @@
1
+ from datetime import datetime, timedelta
2
+ from typing import TYPE_CHECKING, Callable
3
+
4
+ from pydantic import BaseModel, ConfigDict
5
+
6
+ from zrb.config import (
7
+ WEB_ACCESS_TOKEN_COOKIE_NAME,
8
+ WEB_AUTH_ACCESS_TOKEN_EXPIRE_MINUTES,
9
+ WEB_AUTH_REFRESH_TOKEN_EXPIRE_MINUTES,
10
+ WEB_ENABLE_AUTH,
11
+ WEB_GUEST_USERNAME,
12
+ WEB_HTTP_PORT,
13
+ WEB_REFRESH_TOKEN_COOKIE_NAME,
14
+ WEB_SECRET_KEY,
15
+ WEB_SUPER_ADMIN_PASSWORD,
16
+ WEB_SUPER_ADMIN_USERNAME,
17
+ )
18
+ from zrb.group.any_group import AnyGroup
19
+ from zrb.task.any_task import AnyTask
20
+ from zrb.util.group import get_all_subtasks
21
+
22
+ if TYPE_CHECKING:
23
+ # Import Request only for type checking to reduce runtime dependencies
24
+ from fastapi import Request
25
+
26
+
27
+ class User(BaseModel):
28
+ model_config = ConfigDict(arbitrary_types_allowed=True)
29
+ username: str
30
+ password: str = ""
31
+ is_super_admin: bool = False
32
+ is_guest: bool = False
33
+ accessible_tasks: list[AnyTask | str] = []
34
+
35
+ def is_password_match(self, password: str) -> bool:
36
+ return self.password == password
37
+
38
+ def can_access_group(self, group: AnyGroup) -> bool:
39
+ if self.is_super_admin:
40
+ return True
41
+ all_tasks = get_all_subtasks(group, web_only=True)
42
+ if any(self.can_access_task(task) for task in all_tasks):
43
+ return True
44
+ return False
45
+
46
+ def can_access_task(self, task: AnyTask) -> bool:
47
+ if self.is_super_admin:
48
+ return True
49
+ if task.name in self.accessible_tasks or task in self.accessible_tasks:
50
+ return True
51
+ return False
52
+
53
+
54
+ class Token(BaseModel):
55
+ access_token: str
56
+ refresh_token: str
57
+ token_type: str
58
+
59
+
60
+ class WebConfig:
61
+ def __init__(
62
+ self,
63
+ port: int,
64
+ secret_key: str,
65
+ access_token_expire_minutes: int,
66
+ refresh_token_expire_minutes: int,
67
+ access_token_cookie_name: str,
68
+ refresh_token_cookie_name: str,
69
+ enable_auth: bool,
70
+ super_admin_username: str,
71
+ super_admin_password: str,
72
+ guest_username: str,
73
+ guest_accessible_tasks: list[AnyTask | str] = [],
74
+ find_user_by_username: Callable[[str], User | None] | None = None,
75
+ ):
76
+ self._secret_key = secret_key
77
+ self._access_token_expire_minutes = access_token_expire_minutes
78
+ self._refresh_token_expire_minutes = refresh_token_expire_minutes
79
+ self._access_token_cookie_name = access_token_cookie_name
80
+ self._refresh_token_cookie_name = refresh_token_cookie_name
81
+ self._enable_auth = enable_auth
82
+ self._port = port
83
+ self._user_list = []
84
+ self._super_admin_username = super_admin_username
85
+ self._super_admin_password = super_admin_password
86
+ self._guest_username = guest_username
87
+ self._guest_accessible_tasks = guest_accessible_tasks
88
+ self._find_user_by_username = find_user_by_username
89
+
90
+ @property
91
+ def port(self) -> int:
92
+ return self._port
93
+
94
+ @property
95
+ def access_token_cookie_name(self) -> str:
96
+ return self._access_token_cookie_name
97
+
98
+ @property
99
+ def refresh_token_cookie_name(self) -> str:
100
+ return self._refresh_token_cookie_name
101
+
102
+ @property
103
+ def access_token_max_age(self) -> int:
104
+ self._access_token_expire_minutes * 60
105
+
106
+ @property
107
+ def refresh_token_max_age(self) -> int:
108
+ self._refresh_token_expire_minutes * 60
109
+
110
+ @property
111
+ def default_user(self) -> User:
112
+ if self._enable_auth:
113
+ return User(
114
+ username=self._guest_username,
115
+ password="",
116
+ is_guest=True,
117
+ accessible_tasks=self._guest_accessible_tasks,
118
+ )
119
+ return User(
120
+ username=self._guest_username,
121
+ password="",
122
+ is_guest=True,
123
+ is_super_admin=True,
124
+ )
125
+
126
+ @property
127
+ def super_admin(self) -> User:
128
+ return User(
129
+ username=self._super_admin_username,
130
+ password=self._super_admin_password,
131
+ is_super_admin=True,
132
+ )
133
+
134
+ @property
135
+ def user_list(self) -> list[User]:
136
+ if not self._enable_auth:
137
+ return [self.default_user]
138
+ return self._user_list + [self.super_admin, self.default_user]
139
+
140
+ def set_guest_accessible_tasks(self, tasks: list[AnyTask | str]):
141
+ self._guest_accessible_tasks = tasks
142
+
143
+ def set_find_user_by_username(
144
+ self, find_user_by_username: Callable[[str], User | None]
145
+ ):
146
+ self._find_user_by_username = find_user_by_username
147
+
148
+ def append_user(self, user: User):
149
+ duplicates = [
150
+ existing_user
151
+ for existing_user in self.user_list
152
+ if existing_user.username == user.username
153
+ ]
154
+ if len(duplicates) > 0:
155
+ raise ValueError(f"User already exists {user.username}")
156
+ self._user_list.append(user)
157
+
158
+ def enable_auth(self):
159
+ self._enable_auth = True
160
+
161
+ def disable_auth(self):
162
+ self._enable_auth = False
163
+
164
+ def find_user_by_username(self, username: str) -> User | None:
165
+ user = None
166
+ if self._find_user_by_username is not None:
167
+ user = self._find_user_by_username(username)
168
+ if user is None:
169
+ user = next((u for u in self.user_list if u.username == username), None)
170
+ return user
171
+
172
+ async def get_user_by_request(self, request: "Request") -> User | None:
173
+ from fastapi.security import OAuth2PasswordBearer
174
+
175
+ if not self._enable_auth:
176
+ return self.default_user
177
+ # Normally we use "Depends"
178
+ get_bearer_token = OAuth2PasswordBearer(
179
+ tokenUrl="/api/v1/login", auto_error=False
180
+ )
181
+ bearer_token = await get_bearer_token(request)
182
+ token_user = self._get_user_from_token(bearer_token)
183
+ if token_user is not None:
184
+ return token_user
185
+ cookie_user = self._get_user_from_cookie(request)
186
+ if cookie_user is not None:
187
+ return cookie_user
188
+ return self.default_user
189
+
190
+ def _get_user_from_token(self, token: str) -> User | None:
191
+ try:
192
+ from jose import jwt
193
+
194
+ payload = jwt.decode(
195
+ token,
196
+ self._secret_key,
197
+ options={"require_sub": True, "require_exp": True},
198
+ )
199
+ username: str = payload.get("sub")
200
+ if username is None:
201
+ return None
202
+ user = self.find_user_by_username(username)
203
+ if user is None:
204
+ return None
205
+ return user
206
+ except Exception:
207
+ return None
208
+
209
+ def _get_user_from_cookie(self, request: "Request") -> User | None:
210
+ token = request.cookies.get(self._access_token_cookie_name)
211
+ if token:
212
+ return self._get_user_from_token(token)
213
+ return None
214
+
215
+ def get_user_by_credentials(self, username: str, password: str) -> User:
216
+ user = self.find_user_by_username(username)
217
+ if user is None or not user.is_password_match(password):
218
+ return self.default_user
219
+ return user
220
+
221
+ def generate_tokens(self, username: str, password: str) -> Token:
222
+ if not self._enable_auth:
223
+ user = self.default_user
224
+ else:
225
+ user = self.get_user_by_credentials(username, password)
226
+ access_token = self.create_access_token(user.username)
227
+ refresh_token = self.create_refresh_token(user.username)
228
+ return Token(
229
+ access_token=access_token, refresh_token=refresh_token, token_type="bearer"
230
+ )
231
+
232
+ def create_access_token(self, username: str) -> str:
233
+ from jose import jwt
234
+
235
+ expire = datetime.now() + timedelta(minutes=self._access_token_expire_minutes)
236
+ to_encode = {"sub": username, "exp": expire, "type": "access"}
237
+ return jwt.encode(to_encode, self._secret_key)
238
+
239
+ def create_refresh_token(self, username: str) -> str:
240
+ from jose import jwt
241
+
242
+ expire = datetime.now() + timedelta(minutes=self._refresh_token_expire_minutes)
243
+ to_encode = {"sub": username, "exp": expire, "type": "refresh"}
244
+ return jwt.encode(to_encode, self._secret_key)
245
+
246
+ def refresh_tokens(self, refresh_token: str) -> Token:
247
+ from fastapi import HTTPException
248
+ from jose import jwt
249
+
250
+ # Decode and validate token
251
+ try:
252
+ payload = jwt.decode(
253
+ refresh_token,
254
+ self._secret_key,
255
+ options={"require_exp": True, "require_sub": True},
256
+ )
257
+ except Exception:
258
+ raise HTTPException(status_code=401, detail="Invalid JWT token")
259
+ if payload.get("type") != "refresh":
260
+ raise HTTPException(status_code=401, detail="Invalid token type")
261
+ username: str = payload.get("sub")
262
+ if username is None:
263
+ raise HTTPException(status_code=401, detail="Invalid refresh token")
264
+ user = self.find_user_by_username(username)
265
+ if user is None:
266
+ raise HTTPException(status_code=401, detail="User not found")
267
+ # Create new token
268
+ new_access_token = self.create_access_token(username)
269
+ new_refresh_token = self.create_refresh_token(username)
270
+ return Token(
271
+ access_token=new_access_token,
272
+ refresh_token=new_refresh_token,
273
+ token_type="bearer",
274
+ )
275
+
276
+
277
+ web_config = WebConfig(
278
+ port=WEB_HTTP_PORT,
279
+ secret_key=WEB_SECRET_KEY,
280
+ access_token_expire_minutes=WEB_AUTH_ACCESS_TOKEN_EXPIRE_MINUTES,
281
+ refresh_token_expire_minutes=WEB_AUTH_REFRESH_TOKEN_EXPIRE_MINUTES,
282
+ access_token_cookie_name=WEB_ACCESS_TOKEN_COOKIE_NAME,
283
+ refresh_token_cookie_name=WEB_REFRESH_TOKEN_COOKIE_NAME,
284
+ enable_auth=WEB_ENABLE_AUTH,
285
+ super_admin_username=WEB_SUPER_ADMIN_USERNAME,
286
+ super_admin_password=WEB_SUPER_ADMIN_PASSWORD,
287
+ guest_username=WEB_GUEST_USERNAME,
288
+ )
@@ -0,0 +1,27 @@
1
+ import os
2
+
3
+ from zrb.group.any_group import AnyGroup
4
+ from zrb.runner.web_config import User
5
+ from zrb.runner.web_util import get_html_auth_link
6
+ from zrb.util.file import read_file
7
+ from zrb.util.string.format import fstring_format
8
+
9
+
10
+ def show_error_page(user: User, root_group: AnyGroup, status_code: int, message: str):
11
+ from fastapi.responses import HTMLResponse
12
+
13
+ _DIR = os.path.dirname(__file__)
14
+ _VIEW_TEMPLATE = read_file(os.path.join(_DIR, "view.html"))
15
+ auth_link = get_html_auth_link(user)
16
+ return HTMLResponse(
17
+ fstring_format(
18
+ _VIEW_TEMPLATE,
19
+ {
20
+ "name": root_group.name,
21
+ "description": root_group.description,
22
+ "auth_link": auth_link,
23
+ "error_status_code": status_code,
24
+ "error_message": message,
25
+ },
26
+ )
27
+ )
@@ -0,0 +1,33 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="color-scheme" content="light dark">
7
+ <link rel="stylesheet" href="/static/pico.min.css">
8
+ <link rel="icon" href="/static/favicon-32x32.png" sizes="32x32" type="image/png">
9
+ <title>Zrb</title>
10
+ <link rel="stylesheet" href="/static/common.css">
11
+ </head>
12
+ <body>
13
+ <header class="container">
14
+ <hgroup>
15
+ <h1>{name}</h1>
16
+ <p>{description}</p>
17
+ <nav>
18
+ <ul>
19
+ <li><a href="/">🏠 Home</a></li>
20
+ <li><a href="/docs">💻 API Documentation</a></li>
21
+ </ul>
22
+ <ul>
23
+ <li>{auth_link}</li>
24
+ </ul>
25
+ </nav>
26
+ </hgroup>
27
+ </header>
28
+ <main class="container">
29
+ <h2>{error_status_code}</h2>
30
+ <p>{error_message}</p>
31
+ </main>
32
+ </body>
33
+ </html>