labdata 0.1.3__tar.gz → 0.1.6__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.3 → labdata-0.1.6}/PKG-INFO +3 -1
- {labdata-0.1.3 → labdata-0.1.6}/labdata/__init__.py +1 -1
- {labdata-0.1.3 → labdata-0.1.6}/labdata/cli.py +4 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/compute/caiman.py +28 -11
- labdata-0.1.6/labdata/dashboard/admin.py +385 -0
- labdata-0.1.6/labdata/dashboard/compute.py +302 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/dashboard/decisiontask.py +38 -22
- {labdata-0.1.3 → labdata-0.1.6}/labdata/dashboard/index.py +69 -10
- labdata-0.1.6/labdata/dashboard/schema_graph.py +376 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/dashboard/segmentation.py +88 -51
- labdata-0.1.6/labdata/dashboard/sorting.py +1049 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/export.py +64 -7
- {labdata-0.1.3 → labdata-0.1.6}/labdata/schema/ephys.py +9 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata.egg-info/PKG-INFO +3 -1
- {labdata-0.1.3 → labdata-0.1.6}/labdata.egg-info/SOURCES.txt +2 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata.egg-info/requires.txt +2 -0
- {labdata-0.1.3 → labdata-0.1.6}/pyproject.toml +3 -1
- labdata-0.1.3/labdata/dashboard/compute.py +0 -112
- labdata-0.1.3/labdata/dashboard/sorting.py +0 -207
- {labdata-0.1.3 → labdata-0.1.6}/LICENSE +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/README.md +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/compute/__init__.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/compute/ec2.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/compute/ephys.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/compute/pose.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/compute/schedulers.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/compute/singularity.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/compute/suite2p.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/compute/utils.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/copy.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/dashboard/__init__.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/dashboard/console.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/dashboard/explorer/__init__.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/dashboard/explorer/cellsegmentation.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/dashboard/explorer/spikesorting.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/dashboard/notes.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/dashboard/procedures.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/dashboard/video.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/rules/__init__.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/rules/ephys.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/rules/imaging.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/rules/utils.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/s3.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/schema/__init__.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/schema/general.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/schema/histology.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/schema/onephoton.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/schema/procedures.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/schema/tasks.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/schema/twophoton.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/schema/utils.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/schema/video.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/stacks.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/utils.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata/widgets.py +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata.egg-info/dependency_links.txt +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata.egg-info/entry_points.txt +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/labdata.egg-info/top_level.txt +0 -0
- {labdata-0.1.3 → labdata-0.1.6}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: labdata
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: Package to manage data in experimental neuroscience labs
|
|
5
5
|
Author-email: Joao Couto <jpcouto@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/jcouto/labdata
|
|
@@ -27,6 +27,8 @@ Requires-Dist: streamlit>1.54; extra == "dashboard"
|
|
|
27
27
|
Requires-Dist: streamlit-bokeh; extra == "dashboard"
|
|
28
28
|
Requires-Dist: scipy; extra == "dashboard"
|
|
29
29
|
Requires-Dist: scikit-image; extra == "dashboard"
|
|
30
|
+
Requires-Dist: networkx; extra == "dashboard"
|
|
31
|
+
Requires-Dist: streamlit-agraph; extra == "dashboard"
|
|
30
32
|
Dynamic: license-file
|
|
31
33
|
|
|
32
34
|
## labdata
|
|
@@ -62,6 +62,8 @@ Server commands (don't run on experimental computers):
|
|
|
62
62
|
parser.add_argument('--debug' ,action = 'store_true', default = False, help = 'To debug the dash (dashboard) errors.')
|
|
63
63
|
parser.add_argument('--console', action = 'store_true', default = False,
|
|
64
64
|
help = 'Include the IPython console tab in the dashboard.')
|
|
65
|
+
parser.add_argument('--admin', action = 'store_true', default = False,
|
|
66
|
+
help = 'Launch dashboard in admin mode (shows admin tab with storage stats and project switcher).')
|
|
65
67
|
project = _get_project()
|
|
66
68
|
parser.add_argument('-p','--project',
|
|
67
69
|
default = project,
|
|
@@ -95,6 +97,8 @@ Server commands (don't run on experimental computers):
|
|
|
95
97
|
command += f' project={args.project}'
|
|
96
98
|
if args.console:
|
|
97
99
|
command += ' console=1'
|
|
100
|
+
if args.admin:
|
|
101
|
+
command += ' admin=1'
|
|
98
102
|
sub.run(command.split(' '))
|
|
99
103
|
|
|
100
104
|
def subjects(self):
|
|
@@ -61,7 +61,7 @@ class CaimanCompute(Suite2pCompute):
|
|
|
61
61
|
help = "Expected halfwidth of the neurons in pixels")
|
|
62
62
|
parser.add_argument('--gsig-filt',
|
|
63
63
|
action='store', default=None, type = int, nargs = 2,
|
|
64
|
-
help = "Filter for 1photon data")
|
|
64
|
+
help = "Filter for 1photon data, will highlight the somas for motion correction (defaults to [6, 6] for 1p data).")
|
|
65
65
|
parser.add_argument('--nb',
|
|
66
66
|
action='store', default=0, type = int,
|
|
67
67
|
help = "number of background components (rank) if positive, set to 0 for CNMFE")
|
|
@@ -123,6 +123,7 @@ class CaimanCompute(Suite2pCompute):
|
|
|
123
123
|
p = int(args.p),
|
|
124
124
|
gSig = [int(a) for a in args.gsig],
|
|
125
125
|
gSiz = [int(a) for a in 2*np.array(args.gsig) + 1],
|
|
126
|
+
gSig_filt = args.gsig_filt,
|
|
126
127
|
merge_thr = float(args.merge_thr),
|
|
127
128
|
rf = int(args.rf),
|
|
128
129
|
stride = int(args.stride),
|
|
@@ -154,8 +155,9 @@ class CaimanCompute(Suite2pCompute):
|
|
|
154
155
|
dset = (self.schema.Miniscope() & self.dataset_key)
|
|
155
156
|
cnmfparams = {
|
|
156
157
|
'motion_correct' : True,
|
|
158
|
+
'gSig_filt' : [6,6], # 1p needs this for motion correction (spatial filter to register somas)
|
|
157
159
|
'method_init': 'corr_pnr', # use this for 1 photon
|
|
158
|
-
'K': None, # for 1p
|
|
160
|
+
'K': None, # for 1p use None
|
|
159
161
|
'nb': 0, # number of background components (rank) if positive, set to 0 for CNMFE
|
|
160
162
|
'nb_patch': 0,
|
|
161
163
|
'low_rank_background': None, # for 1p
|
|
@@ -165,7 +167,6 @@ class CaimanCompute(Suite2pCompute):
|
|
|
165
167
|
'center_psf': True, # True for 1p
|
|
166
168
|
'only_init': True, # set it to True to run CNMF-E
|
|
167
169
|
'method_deconvolution': 'oasis'} # could use 'cvxpy' alternatively
|
|
168
|
-
|
|
169
170
|
if not len(dset):
|
|
170
171
|
dset = (self.schema.TwoPhoton() & self.dataset_key)
|
|
171
172
|
# add the 2p parameters here and do the processing per plane.
|
|
@@ -179,25 +180,43 @@ class CaimanCompute(Suite2pCompute):
|
|
|
179
180
|
parameters = (self.schema.CellSegmentationParams & f'parameter_set_num = {self.parameter_set_num}').fetch1()
|
|
180
181
|
|
|
181
182
|
params = json.loads(parameters['parameters_dict'])
|
|
183
|
+
excluded = ['detrendWin','denoise_dff','quantileMin']
|
|
182
184
|
for k in params.keys():
|
|
183
|
-
|
|
184
|
-
|
|
185
|
+
if not k in excluded:
|
|
186
|
+
cnmfparams[k] = params[k]
|
|
187
|
+
|
|
185
188
|
import logging
|
|
186
189
|
logger = logging.getLogger('caiman')
|
|
187
|
-
# Set to logging.INFO if you want
|
|
190
|
+
# Set to logging.INFO if you want more, not very useful output..
|
|
188
191
|
logger.setLevel(logging.WARNING)
|
|
189
192
|
handler = logging.StreamHandler()
|
|
190
193
|
logger.addHandler(handler)
|
|
191
194
|
|
|
192
195
|
import time
|
|
193
196
|
from caiman.source_extraction.cnmf.params import CNMFParams
|
|
194
|
-
|
|
197
|
+
kk = ['motion_correct','pw_rigid','max_shifts','gSig_filt']
|
|
198
|
+
if not is_two_photon: # for miniscope make sure the registration has gSig_filt
|
|
199
|
+
if 'gSig_filt' in params.keys():
|
|
200
|
+
if params['gSig_filt'] is None: # that disables it so use the default
|
|
201
|
+
cnmfparams['gSig_filt'] = [6,6]
|
|
202
|
+
print('[caiman] Using default gSig_filt: (6,6) because this is 1photon.')
|
|
203
|
+
else: # add it
|
|
204
|
+
cnmfparams['gSig_filt'] = [6,6]
|
|
205
|
+
print('[caiman] Forcing default gSig_filt: (6,6) because this is 1photon.')
|
|
206
|
+
|
|
207
|
+
parameters = CNMFParams(params_dict = dict({k:cnmfparams[k] for k in kk},
|
|
195
208
|
fr = frame_rate))
|
|
196
209
|
|
|
197
210
|
os.environ['CAIMAN_DATA'] = f'{temporary_folder}'
|
|
198
211
|
|
|
199
212
|
paths = export_to_tiff(dat,temporary_folder,
|
|
200
213
|
crop_region = self.parameters['roi'])
|
|
214
|
+
if not self.keep_intermediate:
|
|
215
|
+
if not self.files_existed:
|
|
216
|
+
print('[caiman] Freeing up space by deleting raw data that was downloaded for this compute task.')
|
|
217
|
+
for f in (self.schema.File & dset).get():
|
|
218
|
+
os.unlink(f)
|
|
219
|
+
|
|
201
220
|
pmotion = parameters.get_group('motion')
|
|
202
221
|
|
|
203
222
|
n_cpus = DEFAULT_N_JOBS
|
|
@@ -209,7 +228,7 @@ class CaimanCompute(Suite2pCompute):
|
|
|
209
228
|
mot_correct.motion_correct(save_movie=True)
|
|
210
229
|
print(f'Done with motion correction in {(time.time() - tstart)/60.} min.')
|
|
211
230
|
|
|
212
|
-
[os.unlink(f) for f in paths]
|
|
231
|
+
[os.unlink(f) for f in paths] # clear raw converted tiff
|
|
213
232
|
fname_mc = mot_correct.fname_tot_els if cnmfparams['pw_rigid'] else mot_correct.fname_tot_rig
|
|
214
233
|
if pmotion['pw_rigid']:
|
|
215
234
|
bord_px = np.ceil(np.maximum(np.max(np.abs(mot_correct.x_shifts_els)),
|
|
@@ -222,6 +241,7 @@ class CaimanCompute(Suite2pCompute):
|
|
|
222
241
|
fname_new = save_memmap(fname_mc, base_name='memmap_', order='C',
|
|
223
242
|
border_to_0 = bord_px)
|
|
224
243
|
[os.unlink(f) for f in fname_mc]
|
|
244
|
+
[os.unlink(f) for f in temporary_folder.rglob("*chunk_*order_F*")]; # clear motion correction temporary
|
|
225
245
|
|
|
226
246
|
from caiman import load_memmap
|
|
227
247
|
Yr, dims, T = load_memmap(fname_new)
|
|
@@ -335,9 +355,6 @@ class CaimanCompute(Suite2pCompute):
|
|
|
335
355
|
print(f'[{self.name} job] Removing the temporary folder.')
|
|
336
356
|
import shutil
|
|
337
357
|
shutil.rmtree(temporary_folder)
|
|
338
|
-
if not self.files_existed:
|
|
339
|
-
for f in (self.schema.File & dset).get():
|
|
340
|
-
os.unlink(f)
|
|
341
358
|
else:
|
|
342
359
|
print(f'[{self.name} job] Kept the temporary folder {temporary_folder}.')
|
|
343
360
|
self.schema.CellSegmentationMetrics().populate(dkey, display_progress = True)
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""Admin dashboard tab; file storage statistics per project and users. Adds users to projects"""
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import streamlit as st
|
|
4
|
+
import plotly.express as px
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _query_project(conn, base_db, project_name):
|
|
8
|
+
"""
|
|
9
|
+
Use mysql here because we don't want to be loading all projects but need to access Dataset.DataFiles.
|
|
10
|
+
|
|
11
|
+
Run aggregation queries for one project via the database connection.
|
|
12
|
+
Returns (total_bytes, n_files, dtype_rows) where dtype_rows is a list of
|
|
13
|
+
{'project', 'dataset_type', 'total_bytes'} dicts.
|
|
14
|
+
"""
|
|
15
|
+
project_db = f"{base_db}_{project_name}"
|
|
16
|
+
|
|
17
|
+
# Total raw data size for the project
|
|
18
|
+
total_bytes, n_files = 0, 0
|
|
19
|
+
try:
|
|
20
|
+
cur = conn.query(f"""
|
|
21
|
+
SELECT COALESCE(SUM(f.file_size), 0), COUNT(*)
|
|
22
|
+
FROM `{base_db}`.`file` f
|
|
23
|
+
INNER JOIN `{project_db}`.`dataset__data_files` df
|
|
24
|
+
ON f.file_path = df.file_path AND f.storage = df.storage
|
|
25
|
+
""")
|
|
26
|
+
row = cur.fetchone()
|
|
27
|
+
if row:
|
|
28
|
+
total_bytes = float(row[0] or 0)
|
|
29
|
+
n_files = int(row[1] or 0)
|
|
30
|
+
except Exception:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
# Size per dataset type
|
|
34
|
+
dtype_rows = []
|
|
35
|
+
try:
|
|
36
|
+
cur = conn.query(f"""
|
|
37
|
+
SELECT COALESCE(d.dataset_type, 'unknown'),
|
|
38
|
+
COALESCE(SUM(f.file_size), 0)
|
|
39
|
+
FROM `{base_db}`.`file` f
|
|
40
|
+
INNER JOIN `{project_db}`.`dataset__data_files` df
|
|
41
|
+
ON f.file_path = df.file_path AND f.storage = df.storage
|
|
42
|
+
INNER JOIN `{project_db}`.`dataset` d
|
|
43
|
+
ON df.subject_name = d.subject_name
|
|
44
|
+
AND df.session_name = d.session_name
|
|
45
|
+
AND df.dataset_name = d.dataset_name
|
|
46
|
+
GROUP BY d.dataset_type
|
|
47
|
+
""")
|
|
48
|
+
for dtype, size in cur.fetchall():
|
|
49
|
+
dtype_rows.append({
|
|
50
|
+
'project': project_name,
|
|
51
|
+
'dataset_type': dtype,
|
|
52
|
+
'total_bytes': float(size or 0),
|
|
53
|
+
})
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
return total_bytes, n_files, dtype_rows
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@st.cache_data(ttl=3600)
|
|
61
|
+
def get_storage_stats(_schema):
|
|
62
|
+
"""
|
|
63
|
+
Query file sizes across all projects using the database connection.
|
|
64
|
+
|
|
65
|
+
Returns (summary_df, dtype_df).
|
|
66
|
+
summary_df: project, n_files, total_bytes, size_tb
|
|
67
|
+
dtype_df: project, dataset_type, total_bytes, size_tb
|
|
68
|
+
"""
|
|
69
|
+
base_db = _schema.globalschema.database
|
|
70
|
+
conn = _schema.globalschema.connection
|
|
71
|
+
projects = list(_schema.Project.fetch('project_name'))
|
|
72
|
+
|
|
73
|
+
summary_rows, dtype_rows = [], []
|
|
74
|
+
for project_name in projects:
|
|
75
|
+
total_bytes, n_files, dtype = _query_project(conn, base_db, project_name)
|
|
76
|
+
summary_rows.append({
|
|
77
|
+
'project': project_name,
|
|
78
|
+
'n_files': n_files,
|
|
79
|
+
'total_bytes': total_bytes,
|
|
80
|
+
'size_tb': total_bytes / 1e12,
|
|
81
|
+
})
|
|
82
|
+
dtype_rows.extend(dtype)
|
|
83
|
+
|
|
84
|
+
summary_df = pd.DataFrame(summary_rows).sort_values('total_bytes', ascending=False)
|
|
85
|
+
dtype_df = pd.DataFrame(dtype_rows) if dtype_rows else pd.DataFrame(
|
|
86
|
+
columns=['project', 'dataset_type', 'total_bytes'])
|
|
87
|
+
if not dtype_df.empty:
|
|
88
|
+
dtype_df['size_tb'] = dtype_df['total_bytes'] / 1e12
|
|
89
|
+
|
|
90
|
+
return summary_df, dtype_df
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@st.cache_data(ttl=3600)
|
|
94
|
+
def get_schema_sizes(_schema):
|
|
95
|
+
"""
|
|
96
|
+
Query information_schema for the on-disk size of every database that
|
|
97
|
+
belongs to this lab installation (prefix = globalschema.database).
|
|
98
|
+
Returns a DataFrame with columns: project, schema_type, size_tb.
|
|
99
|
+
Schema types are derived from the suffix after the project name:
|
|
100
|
+
(none) 'data'
|
|
101
|
+
_computed 'computed'
|
|
102
|
+
anything else → that suffix (e.g. a per-user schema)
|
|
103
|
+
The global schema (no project suffix) is labelled project='(global)', type='global'.
|
|
104
|
+
"""
|
|
105
|
+
base_db = _schema.globalschema.database
|
|
106
|
+
conn = _schema.globalschema.connection
|
|
107
|
+
projects = set(_schema.Project.fetch('project_name'))
|
|
108
|
+
|
|
109
|
+
escaped = base_db.replace('_', r'\_')
|
|
110
|
+
try:
|
|
111
|
+
cur = conn.query(
|
|
112
|
+
f"SELECT table_schema,"
|
|
113
|
+
f" COALESCE(SUM(data_length + index_length), 0) AS size_bytes"
|
|
114
|
+
f" FROM information_schema.tables"
|
|
115
|
+
f" WHERE table_schema LIKE '{escaped}%%' ESCAPE '\\\\'"
|
|
116
|
+
f" GROUP BY table_schema"
|
|
117
|
+
)
|
|
118
|
+
rows = cur.fetchall()
|
|
119
|
+
except Exception as e:
|
|
120
|
+
return pd.DataFrame({'_error': [str(e)], 'project': [''], 'schema_type': [''], 'size_tb': [0.0]})
|
|
121
|
+
|
|
122
|
+
records = []
|
|
123
|
+
for schema_name, size_bytes in rows:
|
|
124
|
+
if schema_name == base_db:
|
|
125
|
+
project, schema_type = '(global)', 'global'
|
|
126
|
+
else:
|
|
127
|
+
suffix = schema_name[len(base_db) + 1:] # strip "base_db_"
|
|
128
|
+
# identify which project this belongs to
|
|
129
|
+
matched_project = None
|
|
130
|
+
for p in sorted(projects, key=len, reverse=True):
|
|
131
|
+
if suffix == p or suffix.startswith(p + '_'):
|
|
132
|
+
matched_project = p
|
|
133
|
+
break
|
|
134
|
+
if matched_project is None:
|
|
135
|
+
continue
|
|
136
|
+
remainder = suffix[len(matched_project):] # '' or '_computed' or '_user'
|
|
137
|
+
schema_type = remainder.lstrip('_') or 'data'
|
|
138
|
+
project = matched_project
|
|
139
|
+
|
|
140
|
+
records.append({
|
|
141
|
+
'project': project,
|
|
142
|
+
'schema_type': schema_type,
|
|
143
|
+
'size_tb': float(size_bytes) / 1e12,
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
return pd.DataFrame(records)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@st.cache_data(ttl=300)
|
|
150
|
+
def get_project_users(_schema):
|
|
151
|
+
"""
|
|
152
|
+
One row per project with description and comma-separated user list.
|
|
153
|
+
Shows all projects, even those with no assigned users.
|
|
154
|
+
"""
|
|
155
|
+
try:
|
|
156
|
+
projects = pd.DataFrame(
|
|
157
|
+
_schema.Project.fetch('project_name', 'project_description', as_dict=True))
|
|
158
|
+
users = pd.DataFrame(
|
|
159
|
+
_schema.Project.User().fetch('project_name', 'user_name', as_dict=True))
|
|
160
|
+
if projects.empty:
|
|
161
|
+
return pd.DataFrame()
|
|
162
|
+
if users.empty:
|
|
163
|
+
projects['Users'] = ''
|
|
164
|
+
else:
|
|
165
|
+
user_summary = (
|
|
166
|
+
users.groupby('project_name')['user_name']
|
|
167
|
+
.apply(lambda u: ', '.join(sorted(u)))
|
|
168
|
+
.reset_index()
|
|
169
|
+
.rename(columns={'user_name': 'Users'})
|
|
170
|
+
)
|
|
171
|
+
projects = projects.merge(user_summary, on='project_name', how='left')
|
|
172
|
+
projects['Users'] = projects['Users'].fillna('')
|
|
173
|
+
return (projects
|
|
174
|
+
.rename(columns={'project_name': 'Project',
|
|
175
|
+
'project_description': 'Description'})
|
|
176
|
+
[['Project', 'Description', 'Users']])
|
|
177
|
+
except Exception:
|
|
178
|
+
return pd.DataFrame()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@st.dialog("Add user to project")
|
|
182
|
+
def _add_user_dialog(project, user):
|
|
183
|
+
st.write(f"Add **{user}** to project **{project}**?")
|
|
184
|
+
col1, col2 = st.columns(2)
|
|
185
|
+
if col1.button("Confirm", type="primary", key="add_confirm"):
|
|
186
|
+
from labdata.export import add_user_to_project
|
|
187
|
+
try:
|
|
188
|
+
add_user_to_project(project, user)
|
|
189
|
+
get_project_users.clear()
|
|
190
|
+
st.session_state['admin_msg'] = ('success', f'Added {user} to {project}.')
|
|
191
|
+
except Exception as e:
|
|
192
|
+
st.session_state['admin_msg'] = ('error', str(e))
|
|
193
|
+
st.rerun()
|
|
194
|
+
if col2.button("Cancel", key="add_cancel"):
|
|
195
|
+
st.rerun()
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@st.dialog("Remove user from project")
|
|
199
|
+
def _remove_user_dialog(project, user):
|
|
200
|
+
st.warning(
|
|
201
|
+
f"Remove **{user}** from project **{project}**? "
|
|
202
|
+
f"This will revoke their database permissions for this project.")
|
|
203
|
+
col1, col2 = st.columns(2)
|
|
204
|
+
if col1.button("Confirm", type="primary", key="remove_confirm"):
|
|
205
|
+
from labdata.export import remove_user_from_project
|
|
206
|
+
try:
|
|
207
|
+
remove_user_from_project(project, user)
|
|
208
|
+
get_project_users.clear()
|
|
209
|
+
st.session_state['admin_msg'] = ('success', f'Removed {user} from {project}.')
|
|
210
|
+
except Exception as e:
|
|
211
|
+
st.session_state['admin_msg'] = ('error', str(e))
|
|
212
|
+
st.rerun()
|
|
213
|
+
if col2.button("Cancel", key="remove_cancel"):
|
|
214
|
+
st.rerun()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def admin_tab(schema, **kwargs):
|
|
218
|
+
st.header("``labdata`` administration and data overview ")
|
|
219
|
+
|
|
220
|
+
with st.spinner("Querying storage stats across all projects..."):
|
|
221
|
+
summary_df, dtype_df = get_storage_stats(schema)
|
|
222
|
+
schema_df = get_schema_sizes(schema)
|
|
223
|
+
|
|
224
|
+
_axis_style = dict(
|
|
225
|
+
showline=True, linecolor='black', linewidth=1.5,
|
|
226
|
+
ticks='outside', tickcolor='black', tickfont=dict(color='black'),
|
|
227
|
+
title_font=dict(color='black'),
|
|
228
|
+
)
|
|
229
|
+
_layout_base = dict(
|
|
230
|
+
paper_bgcolor='white', plot_bgcolor='white',
|
|
231
|
+
font=dict(color='black'),
|
|
232
|
+
hoverlabel=dict(font=dict(color='black')),
|
|
233
|
+
xaxis=_axis_style,
|
|
234
|
+
yaxis=dict(**_axis_style, gridcolor='rgba(0,0,0,0.08)'),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if summary_df.empty:
|
|
238
|
+
st.warning("No file data found.")
|
|
239
|
+
else:
|
|
240
|
+
# total size and file count
|
|
241
|
+
col_size, col_files = st.columns(2)
|
|
242
|
+
|
|
243
|
+
with col_size:
|
|
244
|
+
st.subheader("Raw file size per project")
|
|
245
|
+
fig = px.bar(
|
|
246
|
+
summary_df,
|
|
247
|
+
x='project', y='size_tb',
|
|
248
|
+
labels={'project': 'Project', 'size_tb': 'Size (TB)'},
|
|
249
|
+
text=summary_df['size_tb'].apply(lambda v: f'{v:.3f} TB'),
|
|
250
|
+
)
|
|
251
|
+
fig.update_traces(textposition='outside',
|
|
252
|
+
marker_color='rgba(112,112,112,0.7)',
|
|
253
|
+
marker_line_color='black', marker_line_width=1.5)
|
|
254
|
+
fig.update_layout(yaxis_title='Size (TB)', xaxis_title='Project',
|
|
255
|
+
showlegend=False, **_layout_base)
|
|
256
|
+
st.plotly_chart(fig, width='stretch')
|
|
257
|
+
|
|
258
|
+
with col_files:
|
|
259
|
+
st.subheader("Number of raw files per project")
|
|
260
|
+
|
|
261
|
+
fig_n = px.bar(
|
|
262
|
+
summary_df,
|
|
263
|
+
x='project', y='n_files',
|
|
264
|
+
labels={'project': 'Project', 'n_files': 'Number of Files'},
|
|
265
|
+
text='n_files',
|
|
266
|
+
)
|
|
267
|
+
fig_n.update_traces(textposition='outside',
|
|
268
|
+
marker_color='rgba(112,112,112,0.7)',
|
|
269
|
+
marker_line_color='black', marker_line_width=1.5)
|
|
270
|
+
fig_n.update_layout(yaxis_title='Number of Files', xaxis_title='Project',
|
|
271
|
+
showlegend=False, **_layout_base)
|
|
272
|
+
st.plotly_chart(fig_n, width='stretch')
|
|
273
|
+
|
|
274
|
+
# dataset type and schema size
|
|
275
|
+
col_dtype, col_schema = st.columns(2)
|
|
276
|
+
|
|
277
|
+
with col_dtype:
|
|
278
|
+
if not dtype_df.empty:
|
|
279
|
+
st.subheader("Raw file size per dataset type")
|
|
280
|
+
fig2 = px.bar(
|
|
281
|
+
dtype_df,
|
|
282
|
+
x='project', y='size_tb', color='dataset_type',
|
|
283
|
+
barmode='stack',
|
|
284
|
+
labels={'project': 'Project', 'size_tb': 'Size (TB)',
|
|
285
|
+
'dataset_type': 'Dataset Type'},
|
|
286
|
+
)
|
|
287
|
+
fig2.update_layout(
|
|
288
|
+
yaxis_title='Size (TB)', xaxis_title='Project',
|
|
289
|
+
legend_font=dict(color='black'), legend_title_font=dict(color='black'),
|
|
290
|
+
**_layout_base,
|
|
291
|
+
)
|
|
292
|
+
st.plotly_chart(fig2, width='stretch')
|
|
293
|
+
|
|
294
|
+
with col_schema:
|
|
295
|
+
st.subheader("Database size per project")
|
|
296
|
+
if '_error' in schema_df.columns:
|
|
297
|
+
st.warning(f"Could not query schema sizes: {schema_df['_error'].iloc[0]}")
|
|
298
|
+
elif schema_df.empty:
|
|
299
|
+
st.info("No schema size data found.")
|
|
300
|
+
else:
|
|
301
|
+
fig3 = px.bar(
|
|
302
|
+
schema_df,
|
|
303
|
+
x='project', y='size_tb', color='schema_type',
|
|
304
|
+
barmode='group',
|
|
305
|
+
labels={'project': 'Project', 'size_tb': 'Size (TB)',
|
|
306
|
+
'schema_type': 'Schema'},
|
|
307
|
+
)
|
|
308
|
+
fig3.update_layout(
|
|
309
|
+
yaxis_title='Size (TB)', xaxis_title='Project',
|
|
310
|
+
legend_font=dict(color='black'), legend_title_font=dict(color='black'),
|
|
311
|
+
**_layout_base,
|
|
312
|
+
)
|
|
313
|
+
st.plotly_chart(fig3, width='stretch')
|
|
314
|
+
|
|
315
|
+
col_users, col_add = st.columns(2)
|
|
316
|
+
|
|
317
|
+
with col_users:
|
|
318
|
+
st.subheader("Users and project assignments")
|
|
319
|
+
users_df = get_project_users(schema)
|
|
320
|
+
if users_df.empty:
|
|
321
|
+
st.info("No user/project associations found.")
|
|
322
|
+
else:
|
|
323
|
+
st.dataframe(
|
|
324
|
+
users_df.style.map(lambda _: 'font-weight: bold', subset=['Project']),
|
|
325
|
+
hide_index=True, width='stretch')
|
|
326
|
+
|
|
327
|
+
with col_add:
|
|
328
|
+
# show feedback message from the last dialog action
|
|
329
|
+
if 'admin_msg' in st.session_state:
|
|
330
|
+
level, text = st.session_state.pop('admin_msg')
|
|
331
|
+
if level == 'success':
|
|
332
|
+
st.success(text)
|
|
333
|
+
else:
|
|
334
|
+
st.error(text)
|
|
335
|
+
|
|
336
|
+
try:
|
|
337
|
+
all_projects = list(schema.Project.fetch('project_name'))
|
|
338
|
+
all_users = list(schema.LabMember.fetch('user_name'))
|
|
339
|
+
except Exception:
|
|
340
|
+
all_projects, all_users = [], []
|
|
341
|
+
|
|
342
|
+
if all_projects and all_users:
|
|
343
|
+
# add user
|
|
344
|
+
st.subheader("Add user to project")
|
|
345
|
+
target_project = st.selectbox('Project', options=all_projects,
|
|
346
|
+
key='add_project_sel')
|
|
347
|
+
target_user = st.selectbox('User', options=all_users,
|
|
348
|
+
key='add_user_sel')
|
|
349
|
+
if st.button('Add User', type='primary', key='add_user_btn'):
|
|
350
|
+
_add_user_dialog(target_project, target_user)
|
|
351
|
+
|
|
352
|
+
# remove user
|
|
353
|
+
st.markdown("""<style>
|
|
354
|
+
[data-testid="stVerticalBlockBorderWrapper"] {
|
|
355
|
+
border: 2px solid red !important;
|
|
356
|
+
border-radius: 6px;
|
|
357
|
+
}
|
|
358
|
+
</style>""", unsafe_allow_html=True)
|
|
359
|
+
|
|
360
|
+
with st.container(border=True):
|
|
361
|
+
st.subheader("Remove user from project")
|
|
362
|
+
remove_project = st.selectbox(
|
|
363
|
+
'Project', options=all_projects, key='remove_project_sel')
|
|
364
|
+
try:
|
|
365
|
+
project_users = list(
|
|
366
|
+
(schema.Project.User() & f'project_name = "{remove_project}"')
|
|
367
|
+
.fetch('user_name'))
|
|
368
|
+
except Exception:
|
|
369
|
+
project_users = []
|
|
370
|
+
|
|
371
|
+
if project_users:
|
|
372
|
+
remove_user = st.selectbox(
|
|
373
|
+
'User', options=project_users, key='remove_user_sel')
|
|
374
|
+
if st.button('Remove user', type='primary', key='remove_user_btn'):
|
|
375
|
+
_remove_user_dialog(remove_project, remove_user)
|
|
376
|
+
else:
|
|
377
|
+
st.info("No users assigned to this project.")
|
|
378
|
+
else:
|
|
379
|
+
st.info("No projects or users available.")
|
|
380
|
+
|
|
381
|
+
if st.button("Refresh stats"):
|
|
382
|
+
get_storage_stats.clear()
|
|
383
|
+
get_schema_sizes.clear()
|
|
384
|
+
get_project_users.clear()
|
|
385
|
+
st.rerun()
|