labdata 0.1.16__tar.gz → 0.1.18__tar.gz
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.
- {labdata-0.1.16 → labdata-0.1.18}/PKG-INFO +1 -1
- {labdata-0.1.16 → labdata-0.1.18}/labdata/__init__.py +1 -1
- {labdata-0.1.16 → labdata-0.1.18}/labdata/cli.py +15 -10
- {labdata-0.1.16 → labdata-0.1.18}/labdata/compute/pose.py +13 -1
- {labdata-0.1.16 → labdata-0.1.18}/labdata/compute/utils.py +104 -17
- {labdata-0.1.16 → labdata-0.1.18}/labdata/dashboard/admin.py +5 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/dashboard/compute.py +53 -20
- labdata-0.1.18/labdata/dashboard/drift.py +331 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/dashboard/index.py +87 -36
- {labdata-0.1.16 → labdata-0.1.18}/labdata/dashboard/notes.py +21 -11
- labdata-0.1.18/labdata/dashboard/procedures.py +818 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/dashboard/segmentation.py +46 -28
- {labdata-0.1.16 → labdata-0.1.18}/labdata/dashboard/sorting.py +378 -197
- {labdata-0.1.16 → labdata-0.1.18}/labdata/dashboard/video.py +191 -64
- labdata-0.1.18/labdata/dashboard/writes.py +116 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/schema/ephys.py +54 -20
- {labdata-0.1.16 → labdata-0.1.18}/labdata/schema/procedures.py +5 -3
- {labdata-0.1.16 → labdata-0.1.18}/labdata/schema/video.py +143 -40
- {labdata-0.1.16 → labdata-0.1.18}/labdata/utils.py +5 -1
- {labdata-0.1.16 → labdata-0.1.18}/labdata.egg-info/PKG-INFO +1 -1
- {labdata-0.1.16 → labdata-0.1.18}/labdata.egg-info/SOURCES.txt +2 -0
- labdata-0.1.16/labdata/dashboard/procedures.py +0 -318
- {labdata-0.1.16 → labdata-0.1.18}/LICENSE +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/README.md +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/compute/__init__.py +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/compute/caiman.py +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/compute/ec2.py +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/compute/ephys.py +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/compute/schedulers.py +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/compute/singularity.py +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/compute/suite2p.py +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/copy.py +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/dashboard/__init__.py +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/dashboard/ai_utils.py +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/dashboard/console.py +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/dashboard/decisiontask.py +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/dashboard/docstrings.py +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/dashboard/schema_graph.py +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/export.py +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/rules/__init__.py +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/rules/ephys.py +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/rules/imaging.py +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/rules/utils.py +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/s3.py +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/schema/__init__.py +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/schema/general.py +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/schema/histology.py +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/schema/onephoton.py +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/schema/tasks.py +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/schema/twophoton.py +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/schema/utils.py +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/stacks.py +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata/widgets.py +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata.egg-info/dependency_links.txt +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata.egg-info/entry_points.txt +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata.egg-info/requires.txt +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/labdata.egg-info/top_level.txt +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/pyproject.toml +0 -0
- {labdata-0.1.16 → labdata-0.1.18}/setup.cfg +0 -0
|
@@ -625,16 +625,21 @@ Available analysis are
|
|
|
625
625
|
return
|
|
626
626
|
from .compute import parse_analysis, run_analysis
|
|
627
627
|
# parse analysis will check if the analysis is defined
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
628
|
+
try:
|
|
629
|
+
jobids,obj = parse_analysis(analysis = args.analysis,
|
|
630
|
+
job_id = args.job,
|
|
631
|
+
subject = args.subject,
|
|
632
|
+
session = args.session,
|
|
633
|
+
datatype = args.datatype,
|
|
634
|
+
secondary_args = secondary_args,
|
|
635
|
+
multisession = args.multisession,
|
|
636
|
+
force_submit = args.force_submit,
|
|
637
|
+
full_command = ' '.join(sys.argv[1:]),
|
|
638
|
+
project = args.project)
|
|
639
|
+
except (ValueError, NotImplementedError) as err:
|
|
640
|
+
# what the request asked for is not there: say what, and stop
|
|
641
|
+
print(f'{tcolor["r"]("Nothing to run:")} {err}')
|
|
642
|
+
sys.exit(1)
|
|
638
643
|
if not len(jobids):
|
|
639
644
|
print('Nothing to run.')
|
|
640
645
|
return
|
|
@@ -159,7 +159,19 @@ class DeeplabcutCompute(BaseCompute):
|
|
|
159
159
|
raise(NotImplementedError('Specifying no session is not yet implemented'))
|
|
160
160
|
datasets = []
|
|
161
161
|
for k in keys:
|
|
162
|
-
|
|
162
|
+
videos = (self.schema.DatasetVideo() & k).fetch(as_dict = True)
|
|
163
|
+
if not len(videos):
|
|
164
|
+
raise ValueError(f'Session {k["session_name"]} of {k["subject_name"]} has no video '
|
|
165
|
+
'(nothing in DatasetVideo); videos are registered when the '
|
|
166
|
+
'session is uploaded.')
|
|
167
|
+
video_name = self.parameters.get('video_name')
|
|
168
|
+
if video_name is not None:
|
|
169
|
+
names = sorted({v['video_name'] for v in videos})
|
|
170
|
+
videos = [v for v in videos if v['video_name'] == video_name]
|
|
171
|
+
if not len(videos):
|
|
172
|
+
raise ValueError(f'Session {k["session_name"]} of {k["subject_name"]} has no video '
|
|
173
|
+
f'named {video_name}; it has {", ".join(names)}.')
|
|
174
|
+
datasets += videos
|
|
163
175
|
datasets = [dict(subject_name = d['subject_name'],
|
|
164
176
|
session_name = d['session_name'],
|
|
165
177
|
dataset_name = d['dataset_name']) for d in datasets]
|
|
@@ -1,13 +1,36 @@
|
|
|
1
1
|
from ..utils import *
|
|
2
2
|
import traceback
|
|
3
3
|
|
|
4
|
-
def
|
|
4
|
+
def available_analyses(project=None):
|
|
5
|
+
'''The compute classes of the preference file that are loaded, by analysis name.
|
|
6
|
+
|
|
7
|
+
An entry whose class is not there, because it comes from a plugin that is
|
|
8
|
+
not loaded, or is not a compute class at all, is left out. With
|
|
9
|
+
``project`` the project's plugins are loaded first.
|
|
10
|
+
'''
|
|
11
|
+
import labdata
|
|
12
|
+
if project is not None:
|
|
13
|
+
load_project_schema(project)
|
|
14
|
+
analyses = {}
|
|
15
|
+
for name, path in prefs['compute']['analysis'].items():
|
|
16
|
+
try:
|
|
17
|
+
obj = eval(path)
|
|
18
|
+
except (AttributeError, NameError):
|
|
19
|
+
continue
|
|
20
|
+
if isinstance(obj, type) and issubclass(obj, BaseCompute):
|
|
21
|
+
analyses[name] = obj
|
|
22
|
+
return analyses
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_analysis_object(analysis, project=None):
|
|
5
26
|
'''Load a compute class by its analysis name from user preferences.
|
|
6
27
|
|
|
7
28
|
Parameters
|
|
8
29
|
----------
|
|
9
30
|
analysis : str
|
|
10
31
|
Key in ``prefs['compute']['analysis']`` (e.g. ``'spks'``, ``'suite2p'``).
|
|
32
|
+
project : str or None, optional
|
|
33
|
+
Project whose plugins to load before looking for the class.
|
|
11
34
|
|
|
12
35
|
Returns
|
|
13
36
|
-------
|
|
@@ -17,23 +40,22 @@ def load_analysis_object(analysis):
|
|
|
17
40
|
Raises
|
|
18
41
|
------
|
|
19
42
|
SystemExit
|
|
20
|
-
If the analysis
|
|
21
|
-
listing
|
|
43
|
+
If the analysis is not among the loaded ones, prints a help message
|
|
44
|
+
listing the analyses that are and exits.
|
|
22
45
|
'''
|
|
23
|
-
|
|
46
|
+
analyses = available_analyses(project)
|
|
47
|
+
if analysis not in analyses:
|
|
48
|
+
path = prefs['compute']['analysis'].get(analysis)
|
|
49
|
+
where = f' for project {project}' if project else ''
|
|
50
|
+
because = (f' ({path} is not loaded{where}, or is not a compute class)' if path else '')
|
|
24
51
|
print(f'''
|
|
25
|
-
Could not find {tcolor['r'](analysis)} analysis.
|
|
26
|
-
|
|
27
|
-
The analysis are {tcolor['g'](','.join(prefs["compute"]["analysis"].keys()))}
|
|
28
|
-
|
|
52
|
+
Could not find {tcolor['r'](analysis)} analysis{because}.
|
|
53
|
+
The analyses are {tcolor['g'](','.join(analyses.keys()))}
|
|
29
54
|
Add the analysis to the "{tcolor['y']("compute")}" section of the preference file {tcolor['y']("{analysis_name:analysis_object}")}.
|
|
30
|
-
|
|
31
55
|
''')
|
|
32
|
-
sys.exit()
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
import labdata
|
|
36
|
-
return eval(prefs['compute']['analysis'][analysis])
|
|
56
|
+
sys.exit(1)
|
|
57
|
+
return analyses[analysis]
|
|
58
|
+
|
|
37
59
|
|
|
38
60
|
def handle_compute(job_id, project=None):
|
|
39
61
|
'''Instantiate and return the correct compute object for a queued job.
|
|
@@ -61,7 +83,7 @@ def handle_compute(job_id, project=None):
|
|
|
61
83
|
jobinfo = jobinfo.iloc[0]
|
|
62
84
|
if jobinfo.task_waiting == 0:
|
|
63
85
|
print(f'Task {job_id} is running on {jobinfo.task_host}')
|
|
64
|
-
obj = load_analysis_object(jobinfo.task_name)(jobinfo.job_id,project)
|
|
86
|
+
obj = load_analysis_object(jobinfo.task_name, project=project)(jobinfo.job_id,project)
|
|
65
87
|
return obj
|
|
66
88
|
|
|
67
89
|
def parse_analysis(analysis, job_id=None,
|
|
@@ -115,7 +137,7 @@ def parse_analysis(analysis, job_id=None,
|
|
|
115
137
|
obj : BaseCompute
|
|
116
138
|
Instantiated compute object.
|
|
117
139
|
'''
|
|
118
|
-
obj = load_analysis_object(analysis)(job_id, project=project)
|
|
140
|
+
obj = load_analysis_object(analysis, project=project)(job_id, project=project)
|
|
119
141
|
obj.secondary_parse(secondary_args, parameter_number)
|
|
120
142
|
if obj.job_id is None:
|
|
121
143
|
# then we have to create jobs and assign
|
|
@@ -123,6 +145,9 @@ def parse_analysis(analysis, job_id=None,
|
|
|
123
145
|
schema = load_project_schema(project)
|
|
124
146
|
else:
|
|
125
147
|
schema = obj.schema
|
|
148
|
+
# a subject or session that is not in the project is the usual reason
|
|
149
|
+
# for finding nothing; say so, with the closest names, before looking
|
|
150
|
+
check_subject_and_session(schema, subject, session, project = project)
|
|
126
151
|
# first check if there is a task that has already been submitted with the exact same command.
|
|
127
152
|
# this has a caveat: if the order of the arguments is switched, it wont work..
|
|
128
153
|
if not full_command is None:
|
|
@@ -135,6 +160,12 @@ def parse_analysis(analysis, job_id=None,
|
|
|
135
160
|
return [], None
|
|
136
161
|
if not subject is None or not session is None:
|
|
137
162
|
datasets = obj.find_datasets(subject_name = subject, session_name = session)
|
|
163
|
+
if datasets is not None and not len(datasets):
|
|
164
|
+
asked = ' '.join(f'{name} {", ".join(_as_names(v))}' for name, v in
|
|
165
|
+
(('subject', subject), ('session', session)) if _as_names(v))
|
|
166
|
+
print(tcolor['r'](f'No dataset for {analysis} matches {asked}.')
|
|
167
|
+
+ _datasets_on_record(schema, subject, session))
|
|
168
|
+
return [], obj
|
|
138
169
|
else:
|
|
139
170
|
datasets = None
|
|
140
171
|
job_ids = obj.place_tasks_in_queue(datasets,
|
|
@@ -144,6 +175,60 @@ def parse_analysis(analysis, job_id=None,
|
|
|
144
175
|
# now we have the job ids, need to figure out how to launch the jobs
|
|
145
176
|
return job_ids, obj # returns the job ids and the task
|
|
146
177
|
|
|
178
|
+
def _as_names(value):
|
|
179
|
+
'''The names in a command line argument: a string, a list of strings, or None.'''
|
|
180
|
+
if value is None:
|
|
181
|
+
return []
|
|
182
|
+
if isinstance(value, str):
|
|
183
|
+
value = [value]
|
|
184
|
+
return [str(v) for v in value if str(v).strip()]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _datasets_on_record(schema, subject, session):
|
|
188
|
+
'''One line saying what datasets the requested sessions do have, or nothing.'''
|
|
189
|
+
keys = [dict(subject_name = a, session_name = s) for a in _as_names(subject) for s in _as_names(session)]
|
|
190
|
+
if not keys:
|
|
191
|
+
return ''
|
|
192
|
+
rows = (schema.Dataset() & keys).fetch('session_name', 'dataset_name', 'dataset_type', as_dict = True)
|
|
193
|
+
if not rows:
|
|
194
|
+
return ' The session has no dataset on record.'
|
|
195
|
+
have = sorted({f'{r["dataset_name"]} ({r["dataset_type"] or "no type"})' for r in rows})
|
|
196
|
+
return f' On record: {", ".join(have)}.'
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def check_subject_and_session(schema, subject=None, session=None, project=None):
|
|
200
|
+
'''Raise a ValueError naming what is not in the project, before a compute looks for datasets.
|
|
201
|
+
|
|
202
|
+
A subject that is not in ``Subject`` is reported with the closest names
|
|
203
|
+
on record; a session that the subject does not have is reported with the
|
|
204
|
+
subject's latest sessions. Nothing happens when neither is given.
|
|
205
|
+
'''
|
|
206
|
+
import difflib
|
|
207
|
+
where = f'project {project}' if project else 'the project'
|
|
208
|
+
subjects = _as_names(subject)
|
|
209
|
+
sessions = _as_names(session)
|
|
210
|
+
if not subjects and not sessions:
|
|
211
|
+
return
|
|
212
|
+
known = list(schema.Subject().fetch('subject_name'))
|
|
213
|
+
for name in subjects:
|
|
214
|
+
if name not in known:
|
|
215
|
+
close = difflib.get_close_matches(name, known, n = 5, cutoff = 0.6)
|
|
216
|
+
hint = f' Close names: {", ".join(close)}.' if close else ''
|
|
217
|
+
raise ValueError(f'Subject {name} is not in {where}.{hint}')
|
|
218
|
+
if sessions and not subjects:
|
|
219
|
+
raise ValueError('A session needs its subject: pass the subject name as well.')
|
|
220
|
+
for name in subjects:
|
|
221
|
+
have = list((schema.Session() & dict(subject_name = name)).fetch(
|
|
222
|
+
'session_name', order_by = 'session_datetime DESC'))
|
|
223
|
+
for ses in sessions:
|
|
224
|
+
if ses not in have:
|
|
225
|
+
close = difflib.get_close_matches(ses, have, n = 3, cutoff = 0.6)
|
|
226
|
+
hint = (f' Close names: {", ".join(close)}.' if close else
|
|
227
|
+
f' Its latest sessions: {", ".join(have[:5])}.' if have else
|
|
228
|
+
' It has no sessions.')
|
|
229
|
+
raise ValueError(f'Subject {name} has no session {ses} in {where}.{hint}')
|
|
230
|
+
|
|
231
|
+
|
|
147
232
|
def check_archived(jobid, check_local=False, schema=None):
|
|
148
233
|
'''Check whether files assigned to a compute job are in S3 Glacier.
|
|
149
234
|
|
|
@@ -591,7 +676,9 @@ class BaseCompute():
|
|
|
591
676
|
idx += list(filter(lambda x: not x is None,[i if f in s else None for i,s in enumerate(
|
|
592
677
|
files.file_path.values)]))
|
|
593
678
|
if len(idx) == 0:
|
|
594
|
-
|
|
679
|
+
found = ', '.join(Path(f).name for f in files.file_path.values[:8]) if len(files) else 'no files at all'
|
|
680
|
+
raise ValueError(f'{self.name} needs a file of type {", ".join(self.file_filters)} '
|
|
681
|
+
f'and dataset {dataset} has {found}.')
|
|
595
682
|
files = files.iloc[idx]
|
|
596
683
|
if type(dataset) is dict:
|
|
597
684
|
key = dict(dataset,task_name = self.name)
|
|
@@ -3,6 +3,7 @@ from datetime import datetime, timedelta
|
|
|
3
3
|
|
|
4
4
|
import pandas as pd
|
|
5
5
|
import streamlit as st
|
|
6
|
+
from writes import log
|
|
6
7
|
import plotly.express as px
|
|
7
8
|
|
|
8
9
|
|
|
@@ -285,8 +286,10 @@ def _add_user_dialog(project, user):
|
|
|
285
286
|
add_user_to_project(project, user)
|
|
286
287
|
get_project_users.clear()
|
|
287
288
|
st.session_state['admin_msg'] = ('success', f'Added {user} to {project}.')
|
|
289
|
+
log(f'added user {user} to project {project}')
|
|
288
290
|
except Exception as e:
|
|
289
291
|
st.session_state['admin_msg'] = ('error', str(e))
|
|
292
|
+
log(f'could not add user {user} to project {project}: {e}')
|
|
290
293
|
st.rerun()
|
|
291
294
|
if col2.button("Cancel", key="add_cancel"):
|
|
292
295
|
st.rerun()
|
|
@@ -304,8 +307,10 @@ def _remove_user_dialog(project, user):
|
|
|
304
307
|
remove_user_from_project(project, user)
|
|
305
308
|
get_project_users.clear()
|
|
306
309
|
st.session_state['admin_msg'] = ('success', f'Removed {user} from {project}.')
|
|
310
|
+
log(f'removed user {user} from project {project}')
|
|
307
311
|
except Exception as e:
|
|
308
312
|
st.session_state['admin_msg'] = ('error', str(e))
|
|
313
|
+
log(f'could not remove user {user} from project {project}: {e}')
|
|
309
314
|
st.rerun()
|
|
310
315
|
if col2.button("Cancel", key="remove_cancel"):
|
|
311
316
|
st.rerun()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import pandas as pd
|
|
2
2
|
import numpy as np
|
|
3
3
|
import streamlit as st
|
|
4
|
+
from writes import write, show_flash, log
|
|
4
5
|
|
|
5
6
|
STATES = ['COMPLETED', 'WORKING', 'WAITING', 'FAILED', 'CANCELED','ON SERVER']
|
|
6
7
|
STATE_COLORS = ['#01D186', '#48CAE4', '#F3A21C', '#FF5733', '#F3A21C','#F3A21C']
|
|
@@ -50,6 +51,21 @@ def _resubmit_jobs(schema, job_ids, project, target_override=None):
|
|
|
50
51
|
return errors
|
|
51
52
|
|
|
52
53
|
|
|
54
|
+
def _fetch_rows(table):
|
|
55
|
+
'''Fetch all rows of a job table, reconnecting once if the first attempt fails.
|
|
56
|
+
|
|
57
|
+
The dashboard shares one database connection between browser sessions, and
|
|
58
|
+
the client can be left half closed (a stale socket, a packet sequence
|
|
59
|
+
error, or ``'NoneType' object has no attribute 'read'`` from pymysql).
|
|
60
|
+
Such failures clear on a fresh connection, so retry once before giving up.
|
|
61
|
+
'''
|
|
62
|
+
try:
|
|
63
|
+
return table.fetch(as_dict=True)
|
|
64
|
+
except Exception:
|
|
65
|
+
table.connection.connect()
|
|
66
|
+
return table.fetch(as_dict=True)
|
|
67
|
+
|
|
68
|
+
|
|
53
69
|
def _jobs_panel(table, status_field, target_field, cols, delete_key,
|
|
54
70
|
resubmit_fn=None, allow_delete_completed=False, on_server_fn=None):
|
|
55
71
|
|
|
@@ -59,20 +75,13 @@ def _jobs_panel(table, status_field, target_field, cols, delete_key,
|
|
|
59
75
|
if st.session_state.get(f'{delete_key}_resubmit_errors'):
|
|
60
76
|
for e in st.session_state.pop(f'{delete_key}_resubmit_errors'):
|
|
61
77
|
st.error(e)
|
|
78
|
+
show_flash(f'{delete_key}_msg')
|
|
62
79
|
|
|
63
80
|
try:
|
|
64
|
-
rows = table
|
|
81
|
+
rows = _fetch_rows(table)
|
|
65
82
|
except Exception as e:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
table.connection.connect()
|
|
69
|
-
rows = table.fetch(as_dict=True)
|
|
70
|
-
except Exception as e2:
|
|
71
|
-
st.error(f'Database connection error: {e2}')
|
|
72
|
-
return
|
|
73
|
-
else:
|
|
74
|
-
st.error(f'Error fetching jobs: {e}')
|
|
75
|
-
return
|
|
83
|
+
st.error(f'Could not fetch the jobs: {e}')
|
|
84
|
+
return
|
|
76
85
|
if not rows:
|
|
77
86
|
st.info('There are no jobs in the **ComputeTask** table.')
|
|
78
87
|
return
|
|
@@ -95,9 +104,14 @@ def _jobs_panel(table, status_field, target_field, cols, delete_key,
|
|
|
95
104
|
st.warning(f'Delete all {n} COMPLETED job(s)? This cannot be undone.')
|
|
96
105
|
col_yes, col_no, _ = st.columns([1, 1, 4])
|
|
97
106
|
if col_yes.button('Yes, delete', type='primary', key=f'{delete_key}_del_completed_yes'):
|
|
98
|
-
(table
|
|
107
|
+
ok, _ = write(table, 'delete the completed jobs',
|
|
108
|
+
lambda: (table & f'{status_field} = "COMPLETED"').delete(
|
|
109
|
+
safemode=False, transaction=False),
|
|
110
|
+
success=f'Deleted {n} completed job(s).',
|
|
111
|
+
flash_key=f'{delete_key}_msg')
|
|
99
112
|
st.session_state.pop(f'{delete_key}_del_completed_pending', None)
|
|
100
|
-
|
|
113
|
+
if ok:
|
|
114
|
+
st.rerun()
|
|
101
115
|
if col_no.button('Cancel', key=f'{delete_key}_del_completed_no'):
|
|
102
116
|
st.session_state.pop(f'{delete_key}_del_completed_pending', None)
|
|
103
117
|
st.rerun()
|
|
@@ -175,9 +189,14 @@ def _jobs_panel(table, status_field, target_field, cols, delete_key,
|
|
|
175
189
|
st.warning(f'Delete all {len(jobs_to_delete)} filtered job(s)? This cannot be undone.')
|
|
176
190
|
col_yes, col_no, _ = st.columns([1, 1, 4])
|
|
177
191
|
if col_yes.button('Yes, delete', type='primary', key=f'{delete_key}_del_filtered_yes'):
|
|
178
|
-
|
|
192
|
+
ok, _ = write(table, 'delete the filtered jobs',
|
|
193
|
+
lambda: (table & [dict(job_id=j) for j in jobs_to_delete]).delete(
|
|
194
|
+
safemode=False, transaction=False),
|
|
195
|
+
success=f'Deleted {len(jobs_to_delete)} job(s).',
|
|
196
|
+
flash_key=f'{delete_key}_msg')
|
|
179
197
|
st.session_state.pop(f'{delete_key}_del_filtered_pending', None)
|
|
180
|
-
|
|
198
|
+
if ok:
|
|
199
|
+
st.rerun()
|
|
181
200
|
if col_no.button('Cancel', key=f'{delete_key}_del_filtered_no'):
|
|
182
201
|
st.session_state.pop(f'{delete_key}_del_filtered_pending', None)
|
|
183
202
|
st.rerun()
|
|
@@ -221,10 +240,14 @@ def _jobs_panel(table, status_field, target_field, cols, delete_key,
|
|
|
221
240
|
st.warning(f'Are you sure you want to delete {len(jobs_to_delete)} job(s)? This cannot be undone and scheduled jobs might fail.')
|
|
222
241
|
col_yes, col_no, _ = st.columns([1, 1, 4])
|
|
223
242
|
if col_yes.button('Yes, delete', type='primary', key=f'{delete_key}_yes'):
|
|
224
|
-
|
|
243
|
+
ok, _ = write(table, 'delete the selected jobs',
|
|
244
|
+
lambda: (table & [dict(job_id=j) for j in jobs_to_delete]).delete(
|
|
245
|
+
safemode=False, transaction=False),
|
|
246
|
+
success=f'Deleted {len(jobs_to_delete)} job(s).',
|
|
247
|
+
flash_key=f'{delete_key}_msg')
|
|
225
248
|
st.session_state.pop(f'{delete_key}_pending', None)
|
|
226
|
-
|
|
227
|
-
|
|
249
|
+
if ok:
|
|
250
|
+
st.rerun()
|
|
228
251
|
if col_no.button('Cancel', key=f'{delete_key}_no'):
|
|
229
252
|
st.session_state.pop(f'{delete_key}_pending', None)
|
|
230
253
|
st.rerun()
|
|
@@ -245,6 +268,13 @@ def _jobs_panel(table, status_field, target_field, cols, delete_key,
|
|
|
245
268
|
st.session_state.pop(f'{delete_key}_resubmit_pending', None)
|
|
246
269
|
if errors:
|
|
247
270
|
st.session_state[f'{delete_key}_resubmit_errors'] = errors
|
|
271
|
+
log(f'could not re-submit {len(errors)} of {len(jobs_to_resubmit)} job(s): '
|
|
272
|
+
+ '; '.join(str(e) for e in errors))
|
|
273
|
+
else:
|
|
274
|
+
st.session_state[f'{delete_key}_msg'] = (
|
|
275
|
+
f'Re-submitted {len(jobs_to_resubmit)} job(s).')
|
|
276
|
+
log(f're-submitted job(s) {", ".join(str(j) for j in jobs_to_resubmit)}'
|
|
277
|
+
+ (f' to {override}' if override else ''))
|
|
248
278
|
st.rerun()
|
|
249
279
|
if col_no.button('Cancel', key=f'{delete_key}_resubmit_no'):
|
|
250
280
|
st.session_state.pop(f'{delete_key}_resubmit_pending', None)
|
|
@@ -255,9 +285,12 @@ def _jobs_panel(table, status_field, target_field, cols, delete_key,
|
|
|
255
285
|
st.warning(f'Reset {len(jobs_to_reset)} job(s) to **ON SERVER** / waiting?')
|
|
256
286
|
col_yes, col_no, _ = st.columns([1, 1, 4])
|
|
257
287
|
if col_yes.button('Yes, reset', type='primary', key=f'{delete_key}_on_server_yes'):
|
|
258
|
-
on_server_fn(jobs_to_reset)
|
|
288
|
+
ok, _ = write(table, 'reset the jobs', lambda: on_server_fn(jobs_to_reset),
|
|
289
|
+
success=f'Reset {len(jobs_to_reset)} job(s) to ON SERVER.',
|
|
290
|
+
flash_key=f'{delete_key}_msg')
|
|
259
291
|
st.session_state.pop(f'{delete_key}_on_server_pending', None)
|
|
260
|
-
|
|
292
|
+
if ok:
|
|
293
|
+
st.rerun()
|
|
261
294
|
if col_no.button('Cancel', key=f'{delete_key}_on_server_no'):
|
|
262
295
|
st.session_state.pop(f'{delete_key}_on_server_pending', None)
|
|
263
296
|
st.rerun()
|