meerschaum 2.3.5.dev0__py3-none-any.whl → 2.4.0.dev0__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.
- meerschaum/_internal/arguments/__init__.py +2 -1
- meerschaum/_internal/arguments/_parse_arguments.py +86 -7
- meerschaum/_internal/entry.py +29 -13
- meerschaum/actions/api.py +16 -16
- meerschaum/actions/bootstrap.py +36 -10
- meerschaum/actions/start.py +16 -15
- meerschaum/api/_events.py +11 -7
- meerschaum/api/dash/__init__.py +7 -6
- meerschaum/api/dash/callbacks/__init__.py +1 -0
- meerschaum/api/dash/callbacks/dashboard.py +7 -5
- meerschaum/api/dash/callbacks/pipes.py +42 -0
- meerschaum/api/dash/pages/__init__.py +1 -0
- meerschaum/api/dash/pages/pipes.py +16 -0
- meerschaum/api/dash/pipes.py +79 -47
- meerschaum/api/dash/users.py +19 -6
- meerschaum/api/routes/_actions.py +0 -98
- meerschaum/api/routes/_jobs.py +38 -18
- meerschaum/api/routes/_login.py +4 -4
- meerschaum/api/routes/_pipes.py +3 -3
- meerschaum/config/_default.py +9 -2
- meerschaum/config/_version.py +1 -1
- meerschaum/config/stack/__init__.py +59 -18
- meerschaum/config/static/__init__.py +2 -0
- meerschaum/connectors/Connector.py +19 -13
- meerschaum/connectors/__init__.py +9 -5
- meerschaum/connectors/api/_actions.py +22 -36
- meerschaum/connectors/api/_jobs.py +1 -0
- meerschaum/connectors/poll.py +30 -24
- meerschaum/connectors/sql/_pipes.py +126 -154
- meerschaum/connectors/sql/_plugins.py +45 -43
- meerschaum/connectors/sql/_users.py +46 -38
- meerschaum/connectors/valkey/ValkeyConnector.py +535 -0
- meerschaum/connectors/valkey/__init__.py +8 -0
- meerschaum/connectors/valkey/_fetch.py +75 -0
- meerschaum/connectors/valkey/_pipes.py +839 -0
- meerschaum/connectors/valkey/_plugins.py +265 -0
- meerschaum/connectors/valkey/_users.py +305 -0
- meerschaum/core/Pipe/__init__.py +2 -0
- meerschaum/core/Pipe/_attributes.py +1 -2
- meerschaum/core/Pipe/_drop.py +4 -4
- meerschaum/core/Pipe/_dtypes.py +14 -14
- meerschaum/core/Pipe/_edit.py +15 -14
- meerschaum/core/Pipe/_sync.py +134 -51
- meerschaum/core/User/_User.py +14 -12
- meerschaum/jobs/_Job.py +26 -8
- meerschaum/jobs/systemd.py +20 -8
- meerschaum/plugins/_Plugin.py +17 -13
- meerschaum/utils/_get_pipes.py +14 -20
- meerschaum/utils/dataframe.py +288 -101
- meerschaum/utils/dtypes/__init__.py +31 -6
- meerschaum/utils/dtypes/sql.py +4 -4
- meerschaum/utils/misc.py +3 -3
- meerschaum/utils/packages/_packages.py +1 -0
- meerschaum/utils/prompt.py +1 -1
- {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dev0.dist-info}/METADATA +3 -1
- {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dev0.dist-info}/RECORD +62 -54
- {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dev0.dist-info}/WHEEL +1 -1
- {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dev0.dist-info}/LICENSE +0 -0
- {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dev0.dist-info}/NOTICE +0 -0
- {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dev0.dist-info}/entry_points.txt +0 -0
- {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dev0.dist-info}/top_level.txt +0 -0
- {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dev0.dist-info}/zip-safe +0 -0
meerschaum/api/dash/pipes.py
CHANGED
@@ -48,13 +48,14 @@ def pipe_from_ctx(ctx, trigger_property: str = 'n_clicks') -> Union[mrsm.Pipe, N
|
|
48
48
|
return None
|
49
49
|
return mrsm.Pipe(**meta)
|
50
50
|
|
51
|
+
|
51
52
|
def keys_from_state(
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
53
|
+
state: Dict[str, Any],
|
54
|
+
with_params: bool = False
|
55
|
+
) -> Union[
|
56
|
+
Tuple[List[str], List[str], List[str]],
|
57
|
+
Tuple[List[str], List[str], List[str], str],
|
58
|
+
]:
|
58
59
|
"""
|
59
60
|
Read the current state and return the selected keys lists.
|
60
61
|
"""
|
@@ -85,10 +86,11 @@ def keys_from_state(
|
|
85
86
|
keys.append(params)
|
86
87
|
return tuple(keys)
|
87
88
|
|
89
|
+
|
88
90
|
def pipes_from_state(
|
89
|
-
|
90
|
-
|
91
|
-
|
91
|
+
state: Dict[str, Any],
|
92
|
+
**kw
|
93
|
+
):
|
92
94
|
_ck, _mk, _lk, _params = keys_from_state(state, with_params=True)
|
93
95
|
try:
|
94
96
|
_pipes = _get_pipes(
|
@@ -103,10 +105,10 @@ def pipes_from_state(
|
|
103
105
|
|
104
106
|
|
105
107
|
def build_pipe_card(
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
108
|
+
pipe: mrsm.Pipe,
|
109
|
+
authenticated: bool = False,
|
110
|
+
_build_children_num: int = 10,
|
111
|
+
) -> 'dbc.Card':
|
110
112
|
"""
|
111
113
|
Return a card for the given pipe.
|
112
114
|
|
@@ -128,11 +130,11 @@ def build_pipe_card(
|
|
128
130
|
dbc.Col(
|
129
131
|
(
|
130
132
|
dbc.DropdownMenu(
|
131
|
-
label
|
132
|
-
children
|
133
|
+
label="Manage",
|
134
|
+
children=[
|
133
135
|
dbc.DropdownMenuItem(
|
134
136
|
'Open in Python',
|
135
|
-
id
|
137
|
+
id={
|
136
138
|
'type': 'manage-pipe-button',
|
137
139
|
'index': meta_str,
|
138
140
|
'action': 'python',
|
@@ -140,7 +142,7 @@ def build_pipe_card(
|
|
140
142
|
),
|
141
143
|
dbc.DropdownMenuItem(
|
142
144
|
'Delete',
|
143
|
-
id
|
145
|
+
id={
|
144
146
|
'type': 'manage-pipe-button',
|
145
147
|
'index': meta_str,
|
146
148
|
'action': 'delete',
|
@@ -148,7 +150,7 @@ def build_pipe_card(
|
|
148
150
|
),
|
149
151
|
dbc.DropdownMenuItem(
|
150
152
|
'Drop',
|
151
|
-
id
|
153
|
+
id={
|
152
154
|
'type': 'manage-pipe-button',
|
153
155
|
'index': meta_str,
|
154
156
|
'action': 'drop',
|
@@ -156,7 +158,7 @@ def build_pipe_card(
|
|
156
158
|
),
|
157
159
|
dbc.DropdownMenuItem(
|
158
160
|
'Clear',
|
159
|
-
id
|
161
|
+
id={
|
160
162
|
'type': 'manage-pipe-button',
|
161
163
|
'index': meta_str,
|
162
164
|
'action': 'clear',
|
@@ -164,7 +166,7 @@ def build_pipe_card(
|
|
164
166
|
),
|
165
167
|
dbc.DropdownMenuItem(
|
166
168
|
'Verify',
|
167
|
-
id
|
169
|
+
id={
|
168
170
|
'type': 'manage-pipe-button',
|
169
171
|
'index': meta_str,
|
170
172
|
'action': 'verify',
|
@@ -172,56 +174,86 @@ def build_pipe_card(
|
|
172
174
|
),
|
173
175
|
dbc.DropdownMenuItem(
|
174
176
|
'Sync',
|
175
|
-
id
|
177
|
+
id={
|
176
178
|
'type': 'manage-pipe-button',
|
177
179
|
'index': meta_str,
|
178
180
|
'action': 'sync',
|
179
181
|
},
|
180
182
|
),
|
181
183
|
],
|
182
|
-
direction
|
183
|
-
menu_variant
|
184
|
-
size
|
185
|
-
color
|
184
|
+
direction="up",
|
185
|
+
menu_variant="dark",
|
186
|
+
size='sm',
|
187
|
+
color='secondary',
|
186
188
|
)
|
187
189
|
) if authenticated else [],
|
188
|
-
width
|
190
|
+
width=2,
|
189
191
|
),
|
190
192
|
dbc.Col(width=6),
|
191
193
|
dbc.Col(
|
192
194
|
dbc.Button(
|
193
195
|
'Download CSV',
|
194
|
-
size
|
195
|
-
color
|
196
|
-
style
|
197
|
-
id
|
196
|
+
size='sm',
|
197
|
+
color='link',
|
198
|
+
style={'float': 'right'},
|
199
|
+
id={'type': 'pipe-download-csv-button', 'index': meta_str},
|
198
200
|
),
|
199
|
-
width
|
201
|
+
width=4,
|
200
202
|
),
|
201
203
|
],
|
202
|
-
justify
|
204
|
+
justify='start',
|
203
205
|
)
|
204
206
|
card_body_children = [
|
205
|
-
html.H5(
|
206
|
-
html.B(str(pipe)),
|
207
|
-
className = 'card-title',
|
208
|
-
style = {'font-family': ['monospace']}
|
209
|
-
),
|
210
207
|
html.Div(
|
211
208
|
dbc.Accordion(
|
212
209
|
accordion_items_from_pipe(
|
213
210
|
pipe,
|
214
|
-
authenticated
|
215
|
-
_build_children_num
|
211
|
+
authenticated=authenticated,
|
212
|
+
_build_children_num=_build_children_num,
|
216
213
|
),
|
217
|
-
flush
|
218
|
-
start_collapsed
|
219
|
-
id
|
214
|
+
flush=True,
|
215
|
+
start_collapsed=True,
|
216
|
+
id={'type': 'pipe-accordion', 'index': meta_str},
|
220
217
|
)
|
221
218
|
)
|
222
219
|
|
223
220
|
]
|
221
|
+
|
222
|
+
pipe_url = (
|
223
|
+
f"/dash/pipes/{pipe.connector_keys}/{pipe.metric_key}/{pipe.location_key}"
|
224
|
+
)
|
225
|
+
|
226
|
+
card_header_children = dbc.Row(
|
227
|
+
[
|
228
|
+
dbc.Col(
|
229
|
+
html.H5(
|
230
|
+
html.B(str(pipe)),
|
231
|
+
className='card-title',
|
232
|
+
style={'font-family': ['monospace']}
|
233
|
+
),
|
234
|
+
width=11,
|
235
|
+
),
|
236
|
+
dbc.Col(
|
237
|
+
dbc.Button(
|
238
|
+
[
|
239
|
+
html.I(
|
240
|
+
className="bi bi-share",
|
241
|
+
),
|
242
|
+
],
|
243
|
+
# href=pipe_url,
|
244
|
+
style={'float': 'right'},
|
245
|
+
outline=True,
|
246
|
+
color='link',
|
247
|
+
id={'type': 'share-pipe-button', 'index': meta_str},
|
248
|
+
),
|
249
|
+
width=1,
|
250
|
+
),
|
251
|
+
],
|
252
|
+
justify='start',
|
253
|
+
)
|
254
|
+
|
224
255
|
return dbc.Card([
|
256
|
+
dbc.CardHeader(children=card_header_children),
|
225
257
|
dbc.CardBody(children=card_body_children),
|
226
258
|
dbc.CardFooter(children=footer_children),
|
227
259
|
])
|
@@ -270,11 +302,11 @@ def get_pipes_cards(*keys, session_data: Optional[Dict[str, Any]] = None):
|
|
270
302
|
|
271
303
|
|
272
304
|
def accordion_items_from_pipe(
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
305
|
+
pipe: mrsm.Pipe,
|
306
|
+
active_items: Optional[List[str]] = None,
|
307
|
+
authenticated: bool = False,
|
308
|
+
_build_children_num: int = 10,
|
309
|
+
) -> 'List[dbc.AccordionItem]':
|
278
310
|
"""
|
279
311
|
Build the accordion items for a given pipe.
|
280
312
|
"""
|
meerschaum/api/dash/users.py
CHANGED
@@ -18,6 +18,7 @@ from meerschaum.core import User
|
|
18
18
|
dcc, html = import_dcc(check_update=CHECK_UPDATE), import_html(check_update=CHECK_UPDATE)
|
19
19
|
dbc = attempt_import('dash_bootstrap_components', lazy=False, check_update=CHECK_UPDATE)
|
20
20
|
|
21
|
+
|
21
22
|
def get_users_cards(state: WebState) -> Tuple[List[dbc.Card], List[SuccessTuple]]:
|
22
23
|
"""
|
23
24
|
Return the cards and alerts for users.
|
@@ -52,21 +53,33 @@ def is_session_authenticated(session_id: str) -> bool:
|
|
52
53
|
"""
|
53
54
|
if no_auth:
|
54
55
|
return True
|
56
|
+
if session_id not in active_sessions:
|
57
|
+
return False
|
55
58
|
if session_id in unauthenticated_sessions:
|
56
59
|
return False
|
57
60
|
if session_id in authenticated_sessions:
|
58
61
|
return True
|
62
|
+
|
59
63
|
permissions = get_config('system', 'api', 'permissions')
|
60
64
|
allow_non_admin = permissions.get('actions', {}).get('non_admin', False)
|
61
|
-
|
62
|
-
|
63
|
-
conn = get_api_connector()
|
65
|
+
|
66
|
+
is_auth = True if allow_non_admin else session_is_admin(session_id)
|
64
67
|
username = active_sessions.get(session_id, {}).get('username', None)
|
65
|
-
|
66
|
-
user_type = conn.get_user_type(user, debug=debug)
|
67
|
-
is_auth = user_type == 'admin'
|
68
|
+
|
68
69
|
if is_auth:
|
69
70
|
authenticated_sessions[session_id] = username
|
70
71
|
else:
|
71
72
|
unauthenticated_sessions[session_id] = username
|
73
|
+
|
72
74
|
return is_auth
|
75
|
+
|
76
|
+
|
77
|
+
def session_is_admin(session_id: str) -> bool:
|
78
|
+
"""
|
79
|
+
Check whether a session ID corresponds to an admin user.
|
80
|
+
"""
|
81
|
+
conn = get_api_connector()
|
82
|
+
username = active_sessions.get(session_id, {}).get('username', None)
|
83
|
+
user = User(username, instance=conn)
|
84
|
+
user_type = conn.get_user_type(user, debug=debug)
|
85
|
+
return user_type == 'admin'
|
@@ -67,104 +67,6 @@ def get_actions(
|
|
67
67
|
return list(actions)
|
68
68
|
|
69
69
|
|
70
|
-
async def notify_client(client, content: str):
|
71
|
-
"""
|
72
|
-
Send a line of text to a client.
|
73
|
-
"""
|
74
|
-
try:
|
75
|
-
await client.send_text(content)
|
76
|
-
except (RuntimeError, WebSocketDisconnect, Exception):
|
77
|
-
raise StopMonitoringLogs
|
78
|
-
|
79
|
-
_temp_jobs = {}
|
80
|
-
@app.websocket(actions_endpoint + '/ws')
|
81
|
-
async def do_action_websocket(websocket: WebSocket):
|
82
|
-
"""
|
83
|
-
Execute an action and stream the output to the client.
|
84
|
-
"""
|
85
|
-
await websocket.accept()
|
86
|
-
|
87
|
-
stop_event = asyncio.Event()
|
88
|
-
job_name = '.' + generate_password(12)
|
89
|
-
job = None
|
90
|
-
|
91
|
-
async def monitor_logs(job):
|
92
|
-
success, msg = job.start()
|
93
|
-
await job.monitor_logs_async(
|
94
|
-
partial(notify_client, websocket),
|
95
|
-
stop_event=stop_event,
|
96
|
-
stop_on_exit=True,
|
97
|
-
)
|
98
|
-
|
99
|
-
try:
|
100
|
-
token = await websocket.receive_text()
|
101
|
-
user = await manager.get_current_user(token) if not no_auth else None
|
102
|
-
if user is None and not no_auth:
|
103
|
-
stop_event.set()
|
104
|
-
if job is not None:
|
105
|
-
job.delete()
|
106
|
-
_ = _temp_jobs.pop(job_name, None)
|
107
|
-
raise fastapi.HTTPException(
|
108
|
-
status_code=401,
|
109
|
-
detail="Invalid credentials.",
|
110
|
-
)
|
111
|
-
|
112
|
-
auth_success, auth_msg = (
|
113
|
-
is_user_allowed_to_execute(user)
|
114
|
-
if not no_auth
|
115
|
-
else (True, "Success")
|
116
|
-
)
|
117
|
-
auth_payload = {
|
118
|
-
'is_authenticated': auth_success,
|
119
|
-
'timestamp': datetime.now(timezone.utc).isoformat(),
|
120
|
-
}
|
121
|
-
await websocket.send_json(auth_payload)
|
122
|
-
if not auth_success:
|
123
|
-
stop_event.set()
|
124
|
-
job.stop()
|
125
|
-
await websocket.close()
|
126
|
-
|
127
|
-
sysargs = clean_sysargs(await websocket.receive_json())
|
128
|
-
job = Job(
|
129
|
-
job_name,
|
130
|
-
sysargs,
|
131
|
-
executor_keys='local',
|
132
|
-
_properties={
|
133
|
-
'logs': {
|
134
|
-
'write_timestamps': False,
|
135
|
-
},
|
136
|
-
},
|
137
|
-
)
|
138
|
-
_temp_jobs[job_name] = job
|
139
|
-
monitor_task = asyncio.create_task(monitor_logs(job))
|
140
|
-
|
141
|
-
### NOTE: Await incoming text to trigger `WebSocketDisconnect`.
|
142
|
-
while True:
|
143
|
-
await websocket.receive_text()
|
144
|
-
|
145
|
-
except fastapi.HTTPException:
|
146
|
-
await websocket.send_text("Invalid credentials.")
|
147
|
-
await websocket.close()
|
148
|
-
except (WebSocketDisconnect, asyncio.CancelledError):
|
149
|
-
stop_event.set()
|
150
|
-
job.stop()
|
151
|
-
except Exception:
|
152
|
-
stop_event.set()
|
153
|
-
job.stop()
|
154
|
-
warn(f"Error in logs websocket:\n{traceback.format_exc()}")
|
155
|
-
finally:
|
156
|
-
stop_event.set()
|
157
|
-
job.stop()
|
158
|
-
monitor_task.cancel()
|
159
|
-
if job is not None:
|
160
|
-
job.delete()
|
161
|
-
_ = _temp_jobs.pop(job_name, None)
|
162
|
-
try:
|
163
|
-
await websocket.close()
|
164
|
-
except RuntimeError:
|
165
|
-
pass
|
166
|
-
|
167
|
-
|
168
70
|
@app.post(actions_endpoint + "/{action}", tags=['Actions'])
|
169
71
|
def do_action_legacy(
|
170
72
|
action: str,
|
meerschaum/api/routes/_jobs.py
CHANGED
@@ -40,7 +40,15 @@ from meerschaum.config.static import STATIC_CONFIG
|
|
40
40
|
|
41
41
|
JOBS_STDIN_MESSAGE: str = STATIC_CONFIG['api']['jobs']['stdin_message']
|
42
42
|
JOBS_STOP_MESSAGE: str = STATIC_CONFIG['api']['jobs']['stop_message']
|
43
|
-
EXECUTOR_KEYS: str =
|
43
|
+
EXECUTOR_KEYS: str = 'local'
|
44
|
+
|
45
|
+
|
46
|
+
def _get_job(name: str):
|
47
|
+
systemd_job = Job(name, executor_keys='systemd')
|
48
|
+
if systemd_job.exists():
|
49
|
+
return systemd_job
|
50
|
+
|
51
|
+
return Job(name, executor_keys=EXECUTOR_KEYS)
|
44
52
|
|
45
53
|
|
46
54
|
@app.get(endpoints['jobs'], tags=['Jobs'])
|
@@ -52,7 +60,7 @@ def get_jobs(
|
|
52
60
|
"""
|
53
61
|
Return metadata about the current jobs.
|
54
62
|
"""
|
55
|
-
jobs = _get_jobs(executor_keys=EXECUTOR_KEYS, combine_local_and_systemd=
|
63
|
+
jobs = _get_jobs(executor_keys=EXECUTOR_KEYS, combine_local_and_systemd=True)
|
56
64
|
return {
|
57
65
|
name: {
|
58
66
|
'sysargs': job.sysargs,
|
@@ -83,7 +91,7 @@ def get_job(
|
|
83
91
|
"""
|
84
92
|
Return metadata for a single job.
|
85
93
|
"""
|
86
|
-
job =
|
94
|
+
job = _get_job(name)
|
87
95
|
if not job.exists():
|
88
96
|
raise fastapi.HTTPException(
|
89
97
|
status_code=404,
|
@@ -163,7 +171,7 @@ def delete_job(
|
|
163
171
|
"""
|
164
172
|
Delete a job.
|
165
173
|
"""
|
166
|
-
job =
|
174
|
+
job = _get_job(name)
|
167
175
|
return job.delete()
|
168
176
|
|
169
177
|
|
@@ -177,7 +185,7 @@ def get_job_exists(
|
|
177
185
|
"""
|
178
186
|
Return whether a job exists.
|
179
187
|
"""
|
180
|
-
job =
|
188
|
+
job = _get_job(name)
|
181
189
|
return job.exists()
|
182
190
|
|
183
191
|
|
@@ -192,7 +200,7 @@ def get_logs(
|
|
192
200
|
Return a job's log text.
|
193
201
|
To stream log text, connect to the WebSocket endpoint `/logs/{name}/ws`.
|
194
202
|
"""
|
195
|
-
job =
|
203
|
+
job = _get_job(name)
|
196
204
|
if not job.exists():
|
197
205
|
raise fastapi.HTTPException(
|
198
206
|
status_code=404,
|
@@ -212,7 +220,7 @@ def start_job(
|
|
212
220
|
"""
|
213
221
|
Start a job if stopped.
|
214
222
|
"""
|
215
|
-
job =
|
223
|
+
job = _get_job(name)
|
216
224
|
if not job.exists():
|
217
225
|
raise fastapi.HTTPException(
|
218
226
|
status_code=404,
|
@@ -231,7 +239,7 @@ def stop_job(
|
|
231
239
|
"""
|
232
240
|
Stop a job if running.
|
233
241
|
"""
|
234
|
-
job =
|
242
|
+
job = _get_job(name)
|
235
243
|
if not job.exists():
|
236
244
|
raise fastapi.HTTPException(
|
237
245
|
status_code=404,
|
@@ -250,7 +258,7 @@ def pause_job(
|
|
250
258
|
"""
|
251
259
|
Pause a job if running.
|
252
260
|
"""
|
253
|
-
job =
|
261
|
+
job = _get_job(name)
|
254
262
|
if not job.exists():
|
255
263
|
raise fastapi.HTTPException(
|
256
264
|
status_code=404,
|
@@ -269,7 +277,7 @@ def get_stop_time(
|
|
269
277
|
"""
|
270
278
|
Get the timestamp when the job was manually stopped.
|
271
279
|
"""
|
272
|
-
job =
|
280
|
+
job = _get_job(name)
|
273
281
|
return job.stop_time
|
274
282
|
|
275
283
|
|
@@ -283,12 +291,13 @@ def get_is_blocking_on_stdin(
|
|
283
291
|
"""
|
284
292
|
Return whether a job is blocking on stdin.
|
285
293
|
"""
|
286
|
-
job =
|
294
|
+
job = _get_job(name)
|
287
295
|
return job.is_blocking_on_stdin()
|
288
296
|
|
289
297
|
|
290
298
|
_job_clients = defaultdict(lambda: [])
|
291
299
|
_job_stop_events = defaultdict(lambda: asyncio.Event())
|
300
|
+
_job_queues = defaultdict(lambda: asyncio.Queue())
|
292
301
|
async def notify_clients(name: str, websocket: WebSocket, content: str):
|
293
302
|
"""
|
294
303
|
Write the given content to all connected clients.
|
@@ -315,12 +324,16 @@ async def get_input_from_clients(name: str, websocket: WebSocket) -> str:
|
|
315
324
|
async def _read_client(client):
|
316
325
|
try:
|
317
326
|
await client.send_text(JOBS_STDIN_MESSAGE)
|
318
|
-
data = await
|
327
|
+
data = await _job_queues[name].get()
|
319
328
|
except WebSocketDisconnect:
|
320
329
|
if client in _job_clients[name]:
|
321
330
|
_job_clients[name].remove(client)
|
331
|
+
if not _job_clients[name]:
|
332
|
+
_job_stop_events[name].set()
|
322
333
|
except Exception:
|
323
334
|
pass
|
335
|
+
finally:
|
336
|
+
_job_queues[name].task_done()
|
324
337
|
return data
|
325
338
|
|
326
339
|
read_tasks = [
|
@@ -357,10 +370,12 @@ async def logs_websocket(name: str, websocket: WebSocket):
|
|
357
370
|
Stream logs from a job over a websocket.
|
358
371
|
"""
|
359
372
|
await websocket.accept()
|
360
|
-
job =
|
373
|
+
job = _get_job(name)
|
361
374
|
_job_clients[name].append(websocket)
|
362
375
|
|
376
|
+
_task = None
|
363
377
|
async def monitor_logs():
|
378
|
+
nonlocal _task
|
364
379
|
try:
|
365
380
|
callback_function = partial(
|
366
381
|
notify_clients,
|
@@ -377,16 +392,17 @@ async def logs_websocket(name: str, websocket: WebSocket):
|
|
377
392
|
name,
|
378
393
|
websocket,
|
379
394
|
)
|
380
|
-
|
395
|
+
_task = asyncio.create_task(job.monitor_logs_async(
|
381
396
|
callback_function=callback_function,
|
382
397
|
input_callback_function=input_callback_function,
|
383
398
|
stop_callback_function=stop_callback_function,
|
384
399
|
stop_event=_job_stop_events[name],
|
385
400
|
stop_on_exit=True,
|
386
401
|
accept_input=True,
|
387
|
-
)
|
402
|
+
))
|
388
403
|
except Exception:
|
389
404
|
warn(traceback.format_exc())
|
405
|
+
_task.cancel()
|
390
406
|
|
391
407
|
try:
|
392
408
|
token = await websocket.receive_text()
|
@@ -397,13 +413,17 @@ async def logs_websocket(name: str, websocket: WebSocket):
|
|
397
413
|
detail="Invalid credentials.",
|
398
414
|
)
|
399
415
|
monitor_task = asyncio.create_task(monitor_logs())
|
400
|
-
|
416
|
+
while True:
|
417
|
+
text = await websocket.receive_text()
|
418
|
+
await _job_queues[name].put(text)
|
419
|
+
|
401
420
|
except fastapi.HTTPException:
|
402
421
|
await websocket.send_text("Invalid credentials.")
|
403
422
|
await websocket.close()
|
404
423
|
except WebSocketDisconnect:
|
405
|
-
|
406
|
-
|
424
|
+
if not _job_clients[name]:
|
425
|
+
_job_stop_events[name].set()
|
426
|
+
monitor_task.cancel()
|
407
427
|
except asyncio.CancelledError:
|
408
428
|
pass
|
409
429
|
except Exception:
|
meerschaum/api/routes/_login.py
CHANGED
@@ -23,8 +23,8 @@ from meerschaum.api._oauth2 import CustomOAuth2PasswordRequestForm
|
|
23
23
|
|
24
24
|
@manager.user_loader()
|
25
25
|
def load_user(
|
26
|
-
|
27
|
-
|
26
|
+
username: str
|
27
|
+
) -> User:
|
28
28
|
"""
|
29
29
|
Create the `meerschaum.core.User` object from the username.
|
30
30
|
"""
|
@@ -33,8 +33,8 @@ def load_user(
|
|
33
33
|
|
34
34
|
@app.post(endpoints['login'], tags=['Users'])
|
35
35
|
def login(
|
36
|
-
|
37
|
-
|
36
|
+
data: CustomOAuth2PasswordRequestForm = fastapi.Depends()
|
37
|
+
) -> Dict[str, Any]:
|
38
38
|
"""
|
39
39
|
Login and set the session token.
|
40
40
|
"""
|
meerschaum/api/routes/_pipes.py
CHANGED
@@ -474,9 +474,9 @@ def get_pipe_data(
|
|
474
474
|
df[col] = df[col].apply(lambda x: f'{x:f}' if isinstance(x, Decimal) else x)
|
475
475
|
|
476
476
|
json_content = df.to_json(
|
477
|
-
date_format
|
478
|
-
orient
|
479
|
-
date_unit
|
477
|
+
date_format='iso',
|
478
|
+
orient='records',
|
479
|
+
date_unit='us',
|
480
480
|
)
|
481
481
|
|
482
482
|
return fastapi.Response(
|
meerschaum/config/_default.py
CHANGED
@@ -16,7 +16,6 @@ default_meerschaum_config = {
|
|
16
16
|
'api_instance': 'MRSM{meerschaum:instance}',
|
17
17
|
'web_instance': 'MRSM{meerschaum:instance}',
|
18
18
|
'default_repository': 'api:mrsm',
|
19
|
-
# 'default_executor': 'local',
|
20
19
|
'connectors': {
|
21
20
|
'sql': {
|
22
21
|
'default': {},
|
@@ -53,6 +52,14 @@ default_meerschaum_config = {
|
|
53
52
|
'protocol': 'https',
|
54
53
|
},
|
55
54
|
},
|
55
|
+
'valkey': {
|
56
|
+
'main': {
|
57
|
+
'host': 'localhost',
|
58
|
+
'username': 'default',
|
59
|
+
'password': 'mrsm',
|
60
|
+
'port': 6379,
|
61
|
+
},
|
62
|
+
},
|
56
63
|
},
|
57
64
|
}
|
58
65
|
default_system_config = {
|
@@ -75,7 +82,7 @@ default_system_config = {
|
|
75
82
|
},
|
76
83
|
},
|
77
84
|
|
78
|
-
'api'
|
85
|
+
'api': {
|
79
86
|
},
|
80
87
|
},
|
81
88
|
### not to be confused with system_config['connectors']['api'], this is the configuration
|
meerschaum/config/_version.py
CHANGED