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 +2 -2
- zrb/runner/refresh-token.template.js +22 -0
- zrb/runner/web_app.py +67 -25
- zrb/runner/web_config.py +51 -65
- zrb/runner/web_controller/error_page/view.html +1 -0
- zrb/runner/web_controller/group_info_page/view.html +1 -0
- zrb/runner/web_controller/home_page/view.html +1 -0
- zrb/runner/web_controller/login_page/view.html +1 -0
- zrb/runner/web_controller/logout_page/view.html +1 -0
- zrb/runner/web_controller/session_page/controller.py +2 -2
- zrb/runner/web_controller/session_page/view.html +1 -0
- zrb/runner/web_controller/static/session/current-session.js +1 -1
- zrb/runner/web_controller/static/session/event.js +5 -6
- zrb/runner/web_controller/static/session/past-session.js +9 -3
- zrb/runner/web_util.py +10 -5
- {zrb-1.0.0a20.dist-info → zrb-1.0.0a21.dist-info}/METADATA +8 -52
- {zrb-1.0.0a20.dist-info → zrb-1.0.0a21.dist-info}/RECORD +19 -18
- {zrb-1.0.0a20.dist-info → zrb-1.0.0a21.dist-info}/WHEEL +0 -0
- {zrb-1.0.0a20.dist-info → zrb-1.0.0a21.dist-info}/entry_points.txt +0 -0
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("
|
74
|
+
os.getenv("ZRB_WEB_ACCESS_TOKEN_EXPIRE_MINUTES", "30")
|
75
75
|
)
|
76
76
|
WEB_AUTH_REFRESH_TOKEN_EXPIRE_MINUTES = int(
|
77
|
-
os.getenv("
|
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
|
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
|
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
|
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.
|
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.
|
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.
|
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.
|
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.
|
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,
|
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
|
142
|
-
|
143
|
-
|
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=
|
151
|
-
expires=
|
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=
|
158
|
-
expires=
|
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
|
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.
|
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.
|
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.
|
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.
|
77
|
-
self.
|
78
|
-
self.
|
79
|
-
self.
|
80
|
-
self.
|
81
|
-
self.
|
82
|
-
self.
|
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.
|
85
|
-
self.
|
86
|
-
self.
|
87
|
-
self.
|
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.
|
100
|
+
if self.enable_auth:
|
113
101
|
return User(
|
114
|
-
username=self.
|
102
|
+
username=self.guest_username,
|
115
103
|
password="",
|
116
104
|
is_guest=True,
|
117
|
-
accessible_tasks=self.
|
105
|
+
accessible_tasks=self.guest_accessible_tasks,
|
118
106
|
)
|
119
107
|
return User(
|
120
|
-
username=self.
|
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.
|
130
|
-
password=self.
|
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.
|
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.
|
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
|
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.
|
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.
|
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.
|
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
|
200
|
+
return None
|
219
201
|
return user
|
220
202
|
|
221
|
-
def
|
222
|
-
|
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
|
-
|
227
|
-
|
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
|
218
|
+
def _generate_access_token(self, username: str) -> str:
|
233
219
|
from jose import jwt
|
234
220
|
|
235
|
-
expire = datetime.now() + timedelta(minutes=self.
|
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.
|
223
|
+
return jwt.encode(to_encode, self.secret_key)
|
238
224
|
|
239
|
-
def
|
225
|
+
def _generate_refresh_token(self, username: str) -> str:
|
240
226
|
from jose import jwt
|
241
227
|
|
242
|
-
expire = datetime.now() + timedelta(minutes=self.
|
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.
|
230
|
+
return jwt.encode(to_encode, self.secret_key)
|
245
231
|
|
246
|
-
def
|
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.
|
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.
|
269
|
-
new_refresh_token = self.
|
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,
|
@@ -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)
|
@@ -1,5 +1,5 @@
|
|
1
1
|
const CURRENT_SESSION = {
|
2
|
-
async
|
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.
|
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
|
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
|
4
|
-
|
5
|
-
|
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
|
-
|
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.
|
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
|

|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
61
|
+
Furthermore, Zrb has some builtin tasks to manage monorepo, generate FastAPI application, or play around with LLM.
|
106
62
|
|
107
|
-
|
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
|
[](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=
|
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/
|
156
|
-
zrb/runner/
|
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=
|
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=
|
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=
|
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
|
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=
|
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=
|
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=
|
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=
|
181
|
-
zrb/runner/web_controller/static/session/event.js,sha256=
|
182
|
-
zrb/runner/web_controller/static/session/past-session.js,sha256=
|
183
|
-
zrb/runner/web_util.py,sha256=
|
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.
|
241
|
-
zrb-1.0.
|
242
|
-
zrb-1.0.
|
243
|
-
zrb-1.0.
|
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
|
File without changes
|