zrb 1.0.0a20__py3-none-any.whl → 1.0.0a21__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/config.py CHANGED
@@ -71,10 +71,10 @@ WEB_REFRESH_TOKEN_COOKIE_NAME = os.getenv(
71
71
  WEB_SECRET_KEY = os.getenv("ZRB_WEB_SECRET", "zrb")
72
72
  WEB_ENABLE_AUTH = to_boolean(os.getenv("ZRB_WEB_ENABLE_AUTH", "0"))
73
73
  WEB_AUTH_ACCESS_TOKEN_EXPIRE_MINUTES = int(
74
- os.getenv("ZRB_WEB_ACCESS_TOKEN_EXPIRE", "30")
74
+ os.getenv("ZRB_WEB_ACCESS_TOKEN_EXPIRE_MINUTES", "30")
75
75
  )
76
76
  WEB_AUTH_REFRESH_TOKEN_EXPIRE_MINUTES = int(
77
- os.getenv("ZRB_WEB_ACCESS_TOKEN_EXPIRE", f"{60*24*7}")
77
+ os.getenv("ZRB_WEB_REFRESH_TOKEN_EXPIRE_MINUTES", "60")
78
78
  )
79
79
  LLM_MODEL = os.getenv("ZRB_LLM_MODEL", "ollama_chat/llama3.1")
80
80
  LLM_SYSTEM_PROMPT = os.getenv("ZRB_LLM_SYSTEM_PROMPT", "You are a helpful assistant")
@@ -0,0 +1,22 @@
1
+ function refreshAuthToken(){
2
+ const refreshUrl = "/api/v1/refresh-token";
3
+ async function refresh() {
4
+ try {
5
+ const response = await fetch(refreshUrl, {
6
+ method: "POST",
7
+ headers: { "Content-Type": "application/json" },
8
+ credentials: "include", // Include cookies in the request
9
+ });
10
+
11
+ if (!response.ok) {
12
+ throw new Error(`HTTP error! Status: ${response.status}`);
13
+ }
14
+ console.log("Token refreshed successfully");
15
+ } catch (error) {
16
+ console.error("Cannot refresh token", error)
17
+ }
18
+ }
19
+ setInterval(refresh, refreshIntervalSeconds * 1000);
20
+ refresh();
21
+ }
22
+ refreshAuthToken();
zrb/runner/web_app.py CHANGED
@@ -2,21 +2,26 @@ import asyncio
2
2
  import json
3
3
  import os
4
4
  import sys
5
- from datetime import datetime, timedelta
5
+ from datetime import datetime, timedelta, timezone
6
6
  from typing import TYPE_CHECKING, Annotated
7
7
 
8
8
  from zrb.config import BANNER, VERSION
9
9
  from zrb.context.shared_context import SharedContext
10
10
  from zrb.group.any_group import AnyGroup
11
11
  from zrb.runner.common_util import get_run_kwargs
12
- from zrb.runner.web_config import Token, WebConfig
12
+ from zrb.runner.web_config import (
13
+ NewSessionResponse,
14
+ RefreshTokenRequest,
15
+ Token,
16
+ WebConfig,
17
+ )
13
18
  from zrb.runner.web_controller.error_page.controller import show_error_page
14
19
  from zrb.runner.web_controller.group_info_page.controller import show_group_info_page
15
20
  from zrb.runner.web_controller.home_page.controller import show_home_page
16
21
  from zrb.runner.web_controller.login_page.controller import show_login_page
17
22
  from zrb.runner.web_controller.logout_page.controller import show_logout_page
18
23
  from zrb.runner.web_controller.session_page.controller import show_session_page
19
- from zrb.runner.web_util import NewSessionResponse
24
+ from zrb.runner.web_util import get_refresh_token_js
20
25
  from zrb.session.session import Session
21
26
  from zrb.session_state_log.session_state_log import SessionStateLog, SessionStateLogList
22
27
  from zrb.session_state_logger.any_session_state_logger import AnySessionStateLogger
@@ -35,9 +40,17 @@ def create_app(
35
40
  ) -> "FastAPI":
36
41
  from contextlib import asynccontextmanager
37
42
 
38
- from fastapi import Depends, FastAPI, HTTPException, Query, Request, Response
43
+ from fastapi import (
44
+ Cookie,
45
+ Depends,
46
+ FastAPI,
47
+ HTTPException,
48
+ Query,
49
+ Request,
50
+ Response,
51
+ )
39
52
  from fastapi.openapi.docs import get_swagger_ui_html
40
- from fastapi.responses import FileResponse, HTMLResponse
53
+ from fastapi.responses import FileResponse, HTMLResponse, PlainTextResponse
41
54
  from fastapi.security import OAuth2PasswordRequestForm
42
55
  from fastapi.staticfiles import StaticFiles
43
56
 
@@ -72,6 +85,15 @@ def create_app(
72
85
  return FileResponse(full_path)
73
86
  raise HTTPException(status_code=404, detail="File not found")
74
87
 
88
+ @app.get("/refresh-token.js", include_in_schema=False)
89
+ async def refresh_token_js():
90
+ return PlainTextResponse(
91
+ content=get_refresh_token_js(
92
+ 60 * web_config.refresh_token_expire_minutes / 3
93
+ ),
94
+ media_type="application/javascript",
95
+ )
96
+
75
97
  @app.get("/docs", include_in_schema=False)
76
98
  async def swagger_ui_html():
77
99
  return get_swagger_ui_html(
@@ -85,12 +107,12 @@ def create_app(
85
107
  @app.get("/ui", response_class=HTMLResponse, include_in_schema=False)
86
108
  @app.get("/ui/", response_class=HTMLResponse, include_in_schema=False)
87
109
  async def home_page_ui(request: Request) -> HTMLResponse:
88
- user = await web_config.get_user_by_request(request)
110
+ user = await web_config.get_user_from_request(request)
89
111
  return show_home_page(user, root_group)
90
112
 
91
113
  @app.get("/ui/{path:path}", response_class=HTMLResponse, include_in_schema=False)
92
114
  async def ui_page(path: str, request: Request) -> HTMLResponse:
93
- user = await web_config.get_user_by_request(request)
115
+ user = await web_config.get_user_from_request(request)
94
116
  # Avoid capturing '/ui' itself
95
117
  if not path:
96
118
  return show_error_page(user, root_group, 422, "Undefined path")
@@ -116,46 +138,66 @@ def create_app(
116
138
 
117
139
  @app.get("/login", response_class=HTMLResponse, include_in_schema=False)
118
140
  async def login(request: Request) -> HTMLResponse:
119
- user = await web_config.get_user_by_request(request)
141
+ user = await web_config.get_user_from_request(request)
120
142
  return show_login_page(user, root_group)
121
143
 
122
144
  @app.get("/logout", response_class=HTMLResponse, include_in_schema=False)
123
145
  async def logout(request: Request) -> HTMLResponse:
124
- user = await web_config.get_user_by_request(request)
146
+ user = await web_config.get_user_from_request(request)
125
147
  return show_logout_page(user, root_group)
126
148
 
127
149
  @app.post("/api/v1/login")
128
150
  async def login_api(
129
151
  response: Response, form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
130
152
  ):
131
- token = web_config.generate_tokens(
153
+ token = web_config.generate_tokens_by_credentials(
132
154
  username=form_data.username, password=form_data.password
133
155
  )
156
+ if token is None:
157
+ raise HTTPException(
158
+ status_code=400, detail="Incorrect username or password"
159
+ )
134
160
  _set_auth_cookie(response, token)
135
161
  return token
136
162
 
137
163
  @app.post("/api/v1/refresh-token")
138
164
  async def refresh_token_api(
139
- response: Response, refresh_token: str = Query(..., description="Refresh token")
165
+ response: Response,
166
+ body: RefreshTokenRequest = None,
167
+ refresh_token_cookie: str = Cookie(
168
+ None, alias=web_config.refresh_token_cookie_name
169
+ ),
140
170
  ):
141
- token = web_config.refresh_tokens(refresh_token)
142
- _set_auth_cookie(response, token)
143
- return token
171
+ # Try to get the refresh token from the request body first
172
+ refresh_token = body.refresh_token if body else None
173
+ # If not in the body, try to get it from the cookie
174
+ if not refresh_token:
175
+ refresh_token = refresh_token_cookie
176
+ # If we still don't have a refresh token, raise an exception
177
+ if not refresh_token:
178
+ raise HTTPException(status_code=400, detail="Refresh token not provided")
179
+ # Get token
180
+ new_token = web_config.regenerate_tokens(refresh_token)
181
+ _set_auth_cookie(response, new_token)
182
+ return new_token
144
183
 
145
184
  def _set_auth_cookie(response: Response, token: Token):
185
+ access_token_max_age = web_config.access_token_expire_minutes * 60
186
+ refresh_token_max_age = web_config.refresh_token_expire_minutes * 60
187
+ now = datetime.now(timezone.utc)
146
188
  response.set_cookie(
147
189
  key=web_config.access_token_cookie_name,
148
190
  value=token.access_token,
149
191
  httponly=True,
150
- max_age=web_config.access_token_max_age,
151
- expires=web_config.access_token_max_age,
192
+ max_age=access_token_max_age,
193
+ expires=now + timedelta(seconds=access_token_max_age),
152
194
  )
153
195
  response.set_cookie(
154
196
  key=web_config.refresh_token_cookie_name,
155
197
  value=token.refresh_token,
156
198
  httponly=True,
157
- max_age=web_config.refresh_token_max_age,
158
- expires=web_config.refresh_token_max_age,
199
+ max_age=refresh_token_max_age,
200
+ expires=now + timedelta(seconds=refresh_token_max_age),
159
201
  )
160
202
 
161
203
  @app.get("/api/v1/logout")
@@ -165,15 +207,15 @@ def create_app(
165
207
  response.delete_cookie(web_config.refresh_token_cookie_name)
166
208
  return {"message": "Logout successful"}
167
209
 
168
- @app.post("/api/sessions/{path:path}")
169
- async def create_new_session_api(
210
+ @app.post("/api/v1/task-sessions/{path:path}")
211
+ async def create_new_task_session_api(
170
212
  path: str,
171
213
  request: Request,
172
214
  ) -> NewSessionResponse:
173
215
  """
174
216
  Creating new session
175
217
  """
176
- user = await web_config.get_user_by_request(request)
218
+ user = await web_config.get_user_from_request(request)
177
219
  args = path.strip("/").split("/")
178
220
  task, _, residual_args = extract_node_from_args(root_group, args)
179
221
  if isinstance(task, AnyTask):
@@ -190,7 +232,7 @@ def create_app(
190
232
  return NewSessionResponse(session_name=session.name)
191
233
  raise HTTPException(status_code=404)
192
234
 
193
- @app.get("/api/inputs/{path:path}", response_model=dict[str, str])
235
+ @app.get("/api/v1/task-inputs/{path:path}", response_model=dict[str, str])
194
236
  async def get_default_inputs_api(
195
237
  path: str,
196
238
  request: Request,
@@ -199,7 +241,7 @@ def create_app(
199
241
  """
200
242
  Getting input completion for path
201
243
  """
202
- user = await web_config.get_user_by_request(request)
244
+ user = await web_config.get_user_from_request(request)
203
245
  args = path.strip("/").split("/")
204
246
  task, _, _ = extract_node_from_args(root_group, args)
205
247
  if isinstance(task, AnyTask):
@@ -213,7 +255,7 @@ def create_app(
213
255
  raise HTTPException(status_code=404, detail="Not Found")
214
256
 
215
257
  @app.get(
216
- "/api/sessions/{path:path}",
258
+ "/api/v1/task-sessions/{path:path}",
217
259
  response_model=SessionStateLog | SessionStateLogList,
218
260
  )
219
261
  async def get_session_api(
@@ -227,7 +269,7 @@ def create_app(
227
269
  """
228
270
  Getting existing session or sessions
229
271
  """
230
- user = await web_config.get_user_by_request(request)
272
+ user = await web_config.get_user_from_request(request)
231
273
  args = path.strip("/").split("/")
232
274
  task, _, residual_args = extract_node_from_args(root_group, args)
233
275
  if isinstance(task, AnyTask) and residual_args:
zrb/runner/web_config.py CHANGED
@@ -24,6 +24,14 @@ if TYPE_CHECKING:
24
24
  from fastapi import Request
25
25
 
26
26
 
27
+ class NewSessionResponse(BaseModel):
28
+ session_name: str
29
+
30
+
31
+ class RefreshTokenRequest(BaseModel):
32
+ refresh_token: str
33
+
34
+
27
35
  class User(BaseModel):
28
36
  model_config = ConfigDict(arbitrary_types_allowed=True)
29
37
  username: str
@@ -73,51 +81,31 @@ class WebConfig:
73
81
  guest_accessible_tasks: list[AnyTask | str] = [],
74
82
  find_user_by_username: Callable[[str], User | None] | None = None,
75
83
  ):
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
84
+ self.secret_key = secret_key
85
+ self.access_token_expire_minutes = access_token_expire_minutes
86
+ self.refresh_token_expire_minutes = refresh_token_expire_minutes
87
+ self.access_token_cookie_name = access_token_cookie_name
88
+ self.refresh_token_cookie_name = refresh_token_cookie_name
89
+ self.enable_auth = enable_auth
90
+ self.port = port
83
91
  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
92
+ self.super_admin_username = super_admin_username
93
+ self.super_admin_password = super_admin_password
94
+ self.guest_username = guest_username
95
+ self.guest_accessible_tasks = guest_accessible_tasks
88
96
  self._find_user_by_username = find_user_by_username
89
97
 
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
98
  @property
111
99
  def default_user(self) -> User:
112
- if self._enable_auth:
100
+ if self.enable_auth:
113
101
  return User(
114
- username=self._guest_username,
102
+ username=self.guest_username,
115
103
  password="",
116
104
  is_guest=True,
117
- accessible_tasks=self._guest_accessible_tasks,
105
+ accessible_tasks=self.guest_accessible_tasks,
118
106
  )
119
107
  return User(
120
- username=self._guest_username,
108
+ username=self.guest_username,
121
109
  password="",
122
110
  is_guest=True,
123
111
  is_super_admin=True,
@@ -126,19 +114,19 @@ class WebConfig:
126
114
  @property
127
115
  def super_admin(self) -> User:
128
116
  return User(
129
- username=self._super_admin_username,
130
- password=self._super_admin_password,
117
+ username=self.super_admin_username,
118
+ password=self.super_admin_password,
131
119
  is_super_admin=True,
132
120
  )
133
121
 
134
122
  @property
135
123
  def user_list(self) -> list[User]:
136
- if not self._enable_auth:
124
+ if not self.enable_auth:
137
125
  return [self.default_user]
138
126
  return self._user_list + [self.super_admin, self.default_user]
139
127
 
140
128
  def set_guest_accessible_tasks(self, tasks: list[AnyTask | str]):
141
- self._guest_accessible_tasks = tasks
129
+ self.guest_accessible_tasks = tasks
142
130
 
143
131
  def set_find_user_by_username(
144
132
  self, find_user_by_username: Callable[[str], User | None]
@@ -155,12 +143,6 @@ class WebConfig:
155
143
  raise ValueError(f"User already exists {user.username}")
156
144
  self._user_list.append(user)
157
145
 
158
- def enable_auth(self):
159
- self._enable_auth = True
160
-
161
- def disable_auth(self):
162
- self._enable_auth = False
163
-
164
146
  def find_user_by_username(self, username: str) -> User | None:
165
147
  user = None
166
148
  if self._find_user_by_username is not None:
@@ -169,10 +151,10 @@ class WebConfig:
169
151
  user = next((u for u in self.user_list if u.username == username), None)
170
152
  return user
171
153
 
172
- async def get_user_by_request(self, request: "Request") -> User | None:
154
+ async def get_user_from_request(self, request: "Request") -> User | None:
173
155
  from fastapi.security import OAuth2PasswordBearer
174
156
 
175
- if not self._enable_auth:
157
+ if not self.enable_auth:
176
158
  return self.default_user
177
159
  # Normally we use "Depends"
178
160
  get_bearer_token = OAuth2PasswordBearer(
@@ -193,7 +175,7 @@ class WebConfig:
193
175
 
194
176
  payload = jwt.decode(
195
177
  token,
196
- self._secret_key,
178
+ self.secret_key,
197
179
  options={"require_sub": True, "require_exp": True},
198
180
  )
199
181
  username: str = payload.get("sub")
@@ -207,43 +189,47 @@ class WebConfig:
207
189
  return None
208
190
 
209
191
  def _get_user_from_cookie(self, request: "Request") -> User | None:
210
- token = request.cookies.get(self._access_token_cookie_name)
192
+ token = request.cookies.get(self.access_token_cookie_name)
211
193
  if token:
212
194
  return self._get_user_from_token(token)
213
195
  return None
214
196
 
215
- def get_user_by_credentials(self, username: str, password: str) -> User:
197
+ def get_user_by_credentials(self, username: str, password: str) -> User | None:
216
198
  user = self.find_user_by_username(username)
217
199
  if user is None or not user.is_password_match(password):
218
- return self.default_user
200
+ return None
219
201
  return user
220
202
 
221
- def generate_tokens(self, username: str, password: str) -> Token:
222
- if not self._enable_auth:
203
+ def generate_tokens_by_credentials(
204
+ self, username: str, password: str
205
+ ) -> Token | None:
206
+ if not self.enable_auth:
223
207
  user = self.default_user
224
208
  else:
225
209
  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)
210
+ if user is None:
211
+ return None
212
+ access_token = self._generate_access_token(user.username)
213
+ refresh_token = self._generate_refresh_token(user.username)
228
214
  return Token(
229
215
  access_token=access_token, refresh_token=refresh_token, token_type="bearer"
230
216
  )
231
217
 
232
- def create_access_token(self, username: str) -> str:
218
+ def _generate_access_token(self, username: str) -> str:
233
219
  from jose import jwt
234
220
 
235
- expire = datetime.now() + timedelta(minutes=self._access_token_expire_minutes)
221
+ expire = datetime.now() + timedelta(minutes=self.access_token_expire_minutes)
236
222
  to_encode = {"sub": username, "exp": expire, "type": "access"}
237
- return jwt.encode(to_encode, self._secret_key)
223
+ return jwt.encode(to_encode, self.secret_key)
238
224
 
239
- def create_refresh_token(self, username: str) -> str:
225
+ def _generate_refresh_token(self, username: str) -> str:
240
226
  from jose import jwt
241
227
 
242
- expire = datetime.now() + timedelta(minutes=self._refresh_token_expire_minutes)
228
+ expire = datetime.now() + timedelta(minutes=self.refresh_token_expire_minutes)
243
229
  to_encode = {"sub": username, "exp": expire, "type": "refresh"}
244
- return jwt.encode(to_encode, self._secret_key)
230
+ return jwt.encode(to_encode, self.secret_key)
245
231
 
246
- def refresh_tokens(self, refresh_token: str) -> Token:
232
+ def regenerate_tokens(self, refresh_token: str) -> Token:
247
233
  from fastapi import HTTPException
248
234
  from jose import jwt
249
235
 
@@ -251,7 +237,7 @@ class WebConfig:
251
237
  try:
252
238
  payload = jwt.decode(
253
239
  refresh_token,
254
- self._secret_key,
240
+ self.secret_key,
255
241
  options={"require_exp": True, "require_sub": True},
256
242
  )
257
243
  except Exception:
@@ -265,8 +251,8 @@ class WebConfig:
265
251
  if user is None:
266
252
  raise HTTPException(status_code=401, detail="User not found")
267
253
  # Create new token
268
- new_access_token = self.create_access_token(username)
269
- new_refresh_token = self.create_refresh_token(username)
254
+ new_access_token = self._generate_access_token(username)
255
+ new_refresh_token = self._generate_refresh_token(username)
270
256
  return Token(
271
257
  access_token=new_access_token,
272
258
  refresh_token=new_refresh_token,
@@ -30,4 +30,5 @@
30
30
  <p>{error_message}</p>
31
31
  </main>
32
32
  </body>
33
+ <script src="/refresh-token.js"></script>
33
34
  </html>
@@ -33,4 +33,5 @@
33
33
  {task_info}
34
34
  </main>
35
35
  </body>
36
+ <script src="/refresh-token.js"></script>
36
37
  </html>
@@ -29,4 +29,5 @@
29
29
  {task_info}
30
30
  </main>
31
31
  </body>
32
+ <script src="/refresh-token.js"></script>
32
33
  </html>
@@ -45,6 +45,7 @@
45
45
  </article>
46
46
  </main>
47
47
  <script src="/static/login/event.js"></script>
48
+ <script src="/refresh-token.js"></script>
48
49
  </body>
49
50
 
50
51
  </html>
@@ -35,6 +35,7 @@
35
35
  </article>
36
36
  </main>
37
37
  <script src="/static/logout/event.js"></script>
38
+ <script src="/refresh-token.js"></script>
38
39
  </body>
39
40
 
40
41
  </html>
@@ -33,11 +33,11 @@ def show_session_page(
33
33
  parent_url = "/".join(parent_url_parts)
34
34
  # Assemble session api url
35
35
  session_url_parts = list(url_parts)
36
- session_url_parts[1] = "api/sessions"
36
+ session_url_parts[1] = "api/v1/task-sessions"
37
37
  session_api_url = "/".join(session_url_parts)
38
38
  # Assemble input api url
39
39
  input_url_parts = list(url_parts)
40
- input_url_parts[1] = "api/inputs"
40
+ input_url_parts[1] = "api/v1/task-inputs"
41
41
  input_api_url = "/".join(input_url_parts)
42
42
  # Assemble ui url
43
43
  ui_url_parts = list(url_parts)
@@ -88,4 +88,5 @@
88
88
  <script src="/static/session/past-session.js"></script>
89
89
  <script src="/static/session/current-session.js"></script>
90
90
  <script src="/static/session/event.js"></script>
91
+ <script src="/refresh-token.js"></script>
91
92
  </html>
@@ -1,5 +1,5 @@
1
1
  const CURRENT_SESSION = {
2
- async pollCurrentSession() {
2
+ async startPolling() {
3
3
  const resultTextarea = document.getElementById("result-textarea");
4
4
  const logTextarea = document.getElementById("log-textarea");
5
5
  const submitTaskForm = document.getElementById("submit-task-form");
@@ -1,7 +1,7 @@
1
1
  window.addEventListener("load", async function () {
2
2
  // Get current session
3
3
  if (cfg.SESSION_NAME != "") {
4
- CURRENT_SESSION.pollCurrentSession();
4
+ CURRENT_SESSION.startPolling();
5
5
  }
6
6
  // set maxStartDate to today
7
7
  const tomorrow = new Date();
@@ -16,8 +16,6 @@ window.addEventListener("load", async function () {
16
16
  const formattedToday = UTIL.toLocalDateInputValue(today);
17
17
  const minStartAtInput = document.getElementById("min-start-at-input");
18
18
  minStartAtInput.value = formattedToday;
19
- // Update session
20
- PAST_SESSION.pollPastSession();
21
19
  });
22
20
 
23
21
 
@@ -63,11 +61,12 @@ submitTaskForm.addEventListener("input", async function(event) {
63
61
  } catch (error) {
64
62
  console.error("Error during fetch:", error);
65
63
  }
66
- })
64
+ });
67
65
 
68
66
 
69
67
  function openPastSessionDialog(event) {
70
68
  event.preventDefault();
69
+ PAST_SESSION.startPolling();
71
70
  const dialog = document.getElementById("past-session-dialog")
72
71
  dialog.showModal();
73
72
  }
@@ -75,6 +74,7 @@ function openPastSessionDialog(event) {
75
74
 
76
75
  function closePastSessionDialog(event) {
77
76
  event.preventDefault();
77
+ PAST_SESSION.stopPolling();
78
78
  const dialog = document.getElementById("past-session-dialog")
79
79
  dialog.close();
80
80
  }
@@ -109,8 +109,7 @@ async function submitNewSessionForm(event) {
109
109
  const data = await response.json();
110
110
  cfg.SESSION_NAME = data.session_name;
111
111
  history.pushState(null, "", `${cfg.CURRENT_URL}${cfg.SESSION_NAME}`);
112
- await PAST_SESSION.getAndRenderPastSession(0);
113
- await CURRENT_SESSION.pollCurrentSession();
112
+ await CURRENT_SESSION.startPolling();
114
113
  } else {
115
114
  console.error("Error:", response);
116
115
  }
@@ -1,12 +1,18 @@
1
1
  const PAST_SESSION = {
2
+ shouldPoll: true,
2
3
 
3
- async pollPastSession() {
4
- while (true) {
5
- await this.getAndRenderPastSession(cfg.PAGE);
4
+ async startPolling() {
5
+ await this.getAndRenderPastSession(cfg.PAGE);
6
+ while (this.shouldPoll) {
6
7
  await UTIL.delay(5000);
8
+ await this.getAndRenderPastSession(cfg.PAGE);
7
9
  }
8
10
  },
9
11
 
12
+ stopPolling() {
13
+ this.shouldPoll = false;
14
+ },
15
+
10
16
  async getAndRenderPastSession(page) {
11
17
  cfg.PAGE=page
12
18
  const minStartAtInput = document.getElementById("min-start-at-input");
zrb/runner/web_util.py CHANGED
@@ -1,15 +1,12 @@
1
- from pydantic import BaseModel
1
+ import os
2
2
 
3
3
  from zrb.group.any_group import AnyGroup
4
4
  from zrb.runner.web_config import User
5
5
  from zrb.task.any_task import AnyTask
6
+ from zrb.util.file import read_file
6
7
  from zrb.util.group import get_non_empty_subgroups, get_subtasks
7
8
 
8
9
 
9
- class NewSessionResponse(BaseModel):
10
- session_name: str
11
-
12
-
13
10
  def url_to_args(url: str) -> list[str]:
14
11
  stripped_url = url.strip("/")
15
12
  return [part for part in stripped_url.split("/") if part.strip() != ""]
@@ -29,6 +26,14 @@ def get_html_auth_link(user: User) -> str:
29
26
  return f'Hi, {user.username} <a href="/logout">Logout 🚪</a>'
30
27
 
31
28
 
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
+
32
37
  def get_html_subtask_info(user: User, parent_url: str, parent_group: AnyGroup) -> str:
33
38
  subtasks = get_subtasks(parent_group, web_only=True)
34
39
  task_li = "\n".join(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: zrb
3
- Version: 1.0.0a20
3
+ Version: 1.0.0a21
4
4
  Summary: Your Automation Powerhouse
5
5
  Home-page: https://github.com/state-alchemists/zrb
6
6
  License: AGPL-3.0-or-later
@@ -32,9 +32,11 @@ Description-Content-Type: text/markdown
32
32
 
33
33
  ![](https://raw.githubusercontent.com/state-alchemists/zrb/main/_images/zrb/android-chrome-192x192.png)
34
34
 
35
+ [Documentation](https://github.com/state-alchemists/zrb/blob/main/docs/README.md)
36
+
35
37
  # 🤖 Zrb: Your Automation Powerhouse
36
38
 
37
- With Zrb, you can write your automation tasks like this:
39
+ Zrb allows you to write your automation tasks in Python and declaratively:
38
40
 
39
41
 
40
42
  ```python
@@ -52,59 +54,13 @@ math.add_task(Task(
52
54
  ))
53
55
  ```
54
56
 
55
- You can then access the task in various ways.
56
-
57
- __Using CLI with arguments__
58
-
59
- ```bash
60
- zrb math add 4 5
61
- ```
62
-
63
- Result:
64
-
65
- ```
66
- 9
67
- To run again: zrb math add --a=4 --b=5
68
- ```
69
-
70
- __Using CLI with keyword arguments__
71
-
72
- ```bash
73
- zrb math add --a 4 --b 5
74
- ```
75
-
76
- Result:
77
-
78
- ```
79
- 9
80
- To run again: zrb math add --a=4 --b=5
81
- ```
82
-
83
- __Using CLI with incomplete arguments__
84
-
85
- ```bash
86
- zrb math add 4
87
- ```
88
-
89
- Result:
90
-
91
- ```
92
- b [0]: 5
93
- 9
94
- To run again: zrb math add 4
95
- ```
96
-
97
- __Using Web Interface__
98
-
99
- ```bash
100
- zrb server start
101
- ```
57
+ Once defined, you will be able to access your automation tasks from the CLI, Web Interface, or via HTTP API.
102
58
 
103
- Result (you need to access `http://localhost:21213`)
59
+ For more complex scenario, you can also defined Task dependencies (upstreams) and retry mechanisms. You can also make a scheduled tasks, just like in Apache Airflow.
104
60
 
105
- ![](https://raw.githubusercontent.com/state-alchemists/zrb/refs/heads/1.0.0/_images/web.png)
61
+ Furthermore, Zrb has some builtin tasks to manage monorepo, generate FastAPI application, or play around with LLM.
106
62
 
107
- __More:__
63
+ See the [getting started guide](https://github.com/state-alchemists/zrb/blob/main/docs/recipes/getting-started/README.md) for more information. Or just watch the demo:
108
64
 
109
65
  [![Video Title](https://img.youtube.com/vi/W7dgk96l__o/0.jpg)](https://www.youtube.com/watch?v=W7dgk96l__o)
110
66
 
@@ -120,7 +120,7 @@ zrb/callback/callback.py,sha256=hKefB_Jd1XGjPSLQdMKDsGLHPzEGO2dqrIArLl_EmD0,848
120
120
  zrb/cmd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
121
121
  zrb/cmd/cmd_result.py,sha256=L8bQJzWCpcYexIxHBNsXj2pT3BtLmWex0iJSMkvimOA,597
122
122
  zrb/cmd/cmd_val.py,sha256=hr8Ge0FRe7FZStvkDYnd1MUOOiJW2lDOQqBv338Ymas,963
123
- zrb/config.py,sha256=tqOcu19vUxElbOOr0t55dbgVVD3ya6WoUP2PqlfY9N0,3823
123
+ zrb/config.py,sha256=vZHW6ydp-o27FhMkz6aUYwH1UfEPbKacT55hb_GuSMk,3832
124
124
  zrb/content_transformer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
125
125
  zrb/content_transformer/any_content_transformer.py,sha256=3XHM6ZdsJFXxRD7YlUkv0Gn7-mexsH8c8zdHt3C0x8k,741
126
126
  zrb/content_transformer/content_transformer.py,sha256=vNR8Z_fS7dG2A42O7scDG96JYKBz6IDuFa_B4zMVzZY,2012
@@ -152,35 +152,36 @@ zrb/input/text_input.py,sha256=7i0lvWYw79ILp0NEOBn-Twwnv7sBEgxJsqVaai6LzAA,3033
152
152
  zrb/runner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
153
153
  zrb/runner/cli.py,sha256=ycus4N2T4sTdvLSxocyqjK3Atacm2vGRwpZuPhk9dDA,6573
154
154
  zrb/runner/common_util.py,sha256=JYBgNPSGyJHZo0X-Qp9sDi4B14NdcTi20n1fJP4SW3M,1298
155
- zrb/runner/web_app.py,sha256=7_j-FAJqHvFNN1q03iOC7Dw4tnd4uBwpAheH-LwMmnI,11404
156
- zrb/runner/web_config.py,sha256=9T21eEkyQYZpjdV9uAIn8um1leKL3VGB7M2SBrrxxvk,9852
155
+ zrb/runner/refresh-token.template.js,sha256=v_nF7nU1AXp-KtsHNNzamhciEi7NCSTPEDT5hCxn29g,735
156
+ zrb/runner/web_app.py,sha256=OjLYX3tRw1PiLkQdrmZkZ_cykknw4W9L8NFODL3tUGA,12866
157
+ zrb/runner/web_config.py,sha256=pF6PE92fSAwsG1IobFbW4TGJY3Z4TUnlRzX1qxLFyqY,9428
157
158
  zrb/runner/web_controller/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
158
159
  zrb/runner/web_controller/error_page/controller.py,sha256=Dh2vanfPMvc5aAW0rLYOd2WwUrcO1fGdNVk80-8Qzzs,857
159
- zrb/runner/web_controller/error_page/view.html,sha256=ZlC0oLRv5WXNI-iwP0TcOoQfP5jYZJ-8ojWx_055CcE,1110
160
+ zrb/runner/web_controller/error_page/view.html,sha256=VI-q18Rffyn3T_EQptvn7tZ4qIYLVxj82YbazbXnYfY,1156
160
161
  zrb/runner/web_controller/group_info_page/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
161
162
  zrb/runner/web_controller/group_info_page/controller.py,sha256=PBPCyVXILa7gCIC8d-0Exip7EAnTm3ch5GWe1Fs3WxI,1311
162
- zrb/runner/web_controller/group_info_page/view.html,sha256=X-LtSQcbJ2HwTWGSZhN0MhXaSI4OqDF26DzyXh1COJo,1234
163
+ zrb/runner/web_controller/group_info_page/view.html,sha256=wISun627ciFZcvGpxANG0pr1zgUtSd1m1rNhCAYjRQw,1280
163
164
  zrb/runner/web_controller/home_page/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
164
165
  zrb/runner/web_controller/home_page/controller.py,sha256=2e_Oarts9nnau7ZSdBDiqqvsw7qd7jsIB14BXAsu4FI,1008
165
- zrb/runner/web_controller/home_page/view.html,sha256=p1ZoX604_q8JEqyU2awXnqKUI2bR8RXSEF4hUsLLiLc,1024
166
+ zrb/runner/web_controller/home_page/view.html,sha256=ee0O1bgoZO0qYVqaa67ad7wX_Zawqsy6F-ioxbjEQGk,1070
166
167
  zrb/runner/web_controller/login_page/controller.py,sha256=vCRGgp8mW5JnYt-sPzWdJLLqlaARQO2zTOHAcPzkqrw,733
167
- zrb/runner/web_controller/login_page/view.html,sha256=XANr3qHbAx3g8dGwx677zVeYbKiX5znEZPVi4oqnfcc,1715
168
+ zrb/runner/web_controller/login_page/view.html,sha256=-MeHcSb3r0ouUhs-4aFgrlPjV_V7iz2a6PkobVpk77c,1761
168
169
  zrb/runner/web_controller/logout_page/controller.py,sha256=0mX3wyY5jNtI1Jnxfl_7N8fHtLfBwPE81x9yh2zO_u4,764
169
- zrb/runner/web_controller/logout_page/view.html,sha256=5hGZeil1Z4_oLRmkXdvXsDW12UFwlUt6u7wjDHvQS5s,1278
170
+ zrb/runner/web_controller/logout_page/view.html,sha256=O17ow4-KMbxTkwaQgR880Rlt1B9pnHrRTlgu5nsM4LA,1324
170
171
  zrb/runner/web_controller/session_page/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
171
- zrb/runner/web_controller/session_page/controller.py,sha256=bsnslbWLEGWyKdMtqvqXRZzGLAkvjIY2tVkvSOizN3s,2616
172
+ zrb/runner/web_controller/session_page/controller.py,sha256=q7O8BeytX9kWyU24CMPXVW2cjkN5BQG5tAkr_kuUOXY,2632
172
173
  zrb/runner/web_controller/session_page/partial/input.html,sha256=X2jy0q7TLQGP853exZMed0lqPezL3gzn6mnhB5QKfkc,178
173
- zrb/runner/web_controller/session_page/view.html,sha256=k-jAF_AOOCcNg1vdqsXEHEQWOzkyyxPaaU2WqCIM3uM,3547
174
+ zrb/runner/web_controller/session_page/view.html,sha256=CwrIiPJAwHEEIVNp_wkTzW7kQJjTh41FSDamCswI3S8,3593
174
175
  zrb/runner/web_controller/static/common.css,sha256=u5rGLsPx2943z324iQ2X81krM3z-kc-8e1SkBdYAvKU,157
175
176
  zrb/runner/web_controller/static/favicon-32x32.png,sha256=yu9AIU4k_qD4YHpul6XwJgOxIbmu0thv9ymm2QOsrAk,1456
176
177
  zrb/runner/web_controller/static/login/event.js,sha256=1-NxaUwU-X7Tu2RAwVkzU7gngS0OdooH7Ple4_KDrh4,1135
177
178
  zrb/runner/web_controller/static/logout/event.js,sha256=MfZxrTa2yL49Lbh7cCZDdqsIcf9e1q3W8-WjmZXV5pA,692
178
179
  zrb/runner/web_controller/static/pico.min.css,sha256=_Esfkjs_U_igYn-tXBUaK3AEKb7d4l9DlmaOiw9bXfI,82214
179
180
  zrb/runner/web_controller/static/session/common-util.js,sha256=t7_s5DXgMyZlT8L8LYZTkzOT6vWVeZvmCKjt-bflQY0,2117
180
- zrb/runner/web_controller/static/session/current-session.js,sha256=oA1OFWeRYJzowL8-5mMpy32m9hv-_T6MwAuVruP7XDw,6515
181
- zrb/runner/web_controller/static/session/event.js,sha256=NpFPV2kejUu_WSqfZgW57Un791L4NbmATujavcoIhrM,4217
182
- zrb/runner/web_controller/static/session/past-session.js,sha256=LP0ABMTm7Ap9t9G-JF4b_pRm7BCbh96D_IFb5p91FgA,5325
183
- zrb/runner/web_util.py,sha256=4t6DQ1Avb-aPaLCPB_Q3qaDOZssZ1oNsMBIW5MDqCIU,2262
181
+ zrb/runner/web_controller/static/session/current-session.js,sha256=JV0VRFeizFigPqQiXIeW3By36FDDZhkRM_a8UI4mh-E,6509
182
+ zrb/runner/web_controller/static/session/event.js,sha256=Bd3iW_XqWDlD6gJtSvFju-eT8vQzpkLHOcQU85quCWk,4154
183
+ zrb/runner/web_controller/static/session/past-session.js,sha256=RwGJYKSp75K8NZ-iZP58XppWgdzkiKFaiC5wgcMLxDo,5470
184
+ zrb/runner/web_util.py,sha256=9zAH9FkPM7anVNjRcwTjHyfFocfwpksaWQVYth_nuBc,2464
184
185
  zrb/session/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
185
186
  zrb/session/any_session.py,sha256=x57mS15E-AfUjdVxwOWEzCBjW32zjer7WoeBw0guoDc,5266
186
187
  zrb/session/session.py,sha256=b89UdwPs0U9XXmTHNhCXFmcYXxuRVhNr94Dp_bVO3n4,9827
@@ -237,7 +238,7 @@ zrb/util/string/name.py,sha256=8picJfUBXNpdh64GNaHv3om23QHhUZux7DguFLrXHp8,1163
237
238
  zrb/util/todo.py,sha256=1nDdwPc22oFoK_1ZTXyf3638Bg6sqE2yp_U4_-frHoc,16015
238
239
  zrb/xcom/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
239
240
  zrb/xcom/xcom.py,sha256=o79rxR9wphnShrcIushA0Qt71d_p3ZTxjNf7x9hJB78,1571
240
- zrb-1.0.0a20.dist-info/METADATA,sha256=u-FbzxbG3e53o9Q9_YN93QeDzSKbsF6jq82fJB7EHhQ,4164
241
- zrb-1.0.0a20.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
242
- zrb-1.0.0a20.dist-info/entry_points.txt,sha256=-Pg3ElWPfnaSM-XvXqCxEAa-wfVI6BEgcs386s8C8v8,46
243
- zrb-1.0.0a20.dist-info/RECORD,,
241
+ zrb-1.0.0a21.dist-info/METADATA,sha256=GzXMD38Yc32hIwuW55Pe1tMFB3_2j56o8Iz11qJtQOo,4185
242
+ zrb-1.0.0a21.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
243
+ zrb-1.0.0a21.dist-info/entry_points.txt,sha256=-Pg3ElWPfnaSM-XvXqCxEAa-wfVI6BEgcs386s8C8v8,46
244
+ zrb-1.0.0a21.dist-info/RECORD,,
File without changes