labdata 0.0.3__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.
- labdata/__init__.py +17 -0
- labdata/cli.py +499 -0
- labdata/compute/__init__.py +27 -0
- labdata/compute/ec2.py +198 -0
- labdata/compute/ephys.py +469 -0
- labdata/compute/pose.py +281 -0
- labdata/compute/schedulers.py +194 -0
- labdata/compute/singularity.py +95 -0
- labdata/compute/utils.py +561 -0
- labdata/copy.py +351 -0
- labdata/rules/__init__.py +78 -0
- labdata/rules/ephys.py +188 -0
- labdata/rules/imaging.py +618 -0
- labdata/rules/utils.py +290 -0
- labdata/s3.py +317 -0
- labdata/schema/__init__.py +24 -0
- labdata/schema/ephys.py +547 -0
- labdata/schema/general.py +647 -0
- labdata/schema/histology.py +309 -0
- labdata/schema/onephoton.py +93 -0
- labdata/schema/procedures.py +102 -0
- labdata/schema/tasks.py +66 -0
- labdata/schema/twophoton.py +142 -0
- labdata/schema/utils.py +25 -0
- labdata/schema/video.py +243 -0
- labdata/stacks.py +182 -0
- labdata/utils.py +598 -0
- labdata/widgets.py +412 -0
- labdata-0.0.3.dist-info/METADATA +42 -0
- labdata-0.0.3.dist-info/RECORD +36 -0
- labdata-0.0.3.dist-info/WHEEL +5 -0
- labdata-0.0.3.dist-info/entry_points.txt +2 -0
- labdata-0.0.3.dist-info/licenses/LICENSE +674 -0
- labdata-0.0.3.dist-info/top_level.txt +2 -0
- labdata_frontend/Home.py +39 -0
- labdata_frontend/__init__.py +0 -0
labdata/rules/utils.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
from ..utils import *
|
|
2
|
+
from ..s3 import copy_to_s3
|
|
3
|
+
|
|
4
|
+
# has utilities needed by other rules
|
|
5
|
+
def _checksum_files(filepath, local_path):
|
|
6
|
+
'''
|
|
7
|
+
Checksum files that don't need to be copied
|
|
8
|
+
'''
|
|
9
|
+
# construct the path:
|
|
10
|
+
src = Path(local_path)/filepath
|
|
11
|
+
hash = compute_md5_hash(src) # computes the hash
|
|
12
|
+
srcstat = src.stat()
|
|
13
|
+
file_size = srcstat.st_size
|
|
14
|
+
return dict(src_path = filepath,
|
|
15
|
+
src_md5 = hash,
|
|
16
|
+
src_size = file_size,
|
|
17
|
+
src_datetime = datetime.fromtimestamp(srcstat.st_ctime))
|
|
18
|
+
|
|
19
|
+
class UploadRule():
|
|
20
|
+
def __init__(self,job_id,prefs = None):
|
|
21
|
+
'''
|
|
22
|
+
|
|
23
|
+
Rule to apply on upload.
|
|
24
|
+
|
|
25
|
+
1) Checksum on the files; compare with provided (reserve job if use_db)
|
|
26
|
+
2) Apply function
|
|
27
|
+
3) Checksum on the output - the files that changed
|
|
28
|
+
4) Submit upload
|
|
29
|
+
5) Update tables
|
|
30
|
+
|
|
31
|
+
Can submit job on slurm, some of these can be long or take resources.
|
|
32
|
+
|
|
33
|
+
'''
|
|
34
|
+
self.rule_name = 'default'
|
|
35
|
+
|
|
36
|
+
self.job_id = job_id
|
|
37
|
+
self.src_paths = None
|
|
38
|
+
self.processed_paths = None
|
|
39
|
+
self.dst_paths = None
|
|
40
|
+
self.inserted_files = []
|
|
41
|
+
if prefs is None:
|
|
42
|
+
from ..utils import prefs
|
|
43
|
+
self.prefs = prefs
|
|
44
|
+
self.local_path = self.prefs['local_paths'][0]
|
|
45
|
+
self.dataset_key = None # will get written on upload, use in _post_upload
|
|
46
|
+
self.max_concurrent = -1 # maximum number of concurrent UploadJobs that can run [-1 is infinite].
|
|
47
|
+
|
|
48
|
+
def apply(self):
|
|
49
|
+
# parse inputs
|
|
50
|
+
from ..schema import UploadJob, File, dj
|
|
51
|
+
|
|
52
|
+
if not self.job_id is None:
|
|
53
|
+
with dj.conn().transaction:
|
|
54
|
+
self.jobquery = (UploadJob() & dict(job_id = self.job_id))
|
|
55
|
+
job_status = self.jobquery.fetch(as_dict = True)
|
|
56
|
+
if len(job_status):
|
|
57
|
+
if job_status[0]['job_waiting']:
|
|
58
|
+
# check here if there are other jobs running for the same rule on this host
|
|
59
|
+
if self.max_concurrent > 0:
|
|
60
|
+
kk = dict(job_host = self.prefs['hostname'],
|
|
61
|
+
job_waiting = 0,
|
|
62
|
+
job_status = 'WORKING',
|
|
63
|
+
job_rule = None)
|
|
64
|
+
if not self.rule_name == 'default':
|
|
65
|
+
kk['job_rule'] = self.rule_name
|
|
66
|
+
number_of_running_jobs = len(UploadJob() & kk)
|
|
67
|
+
if number_of_running_jobs >= self.max_concurrent:
|
|
68
|
+
print(f"Job {self.job_id} can not run because there are {number_of_running_jobs} for the same rule.")
|
|
69
|
+
self.set_job_status(job_status = 'WAITING', job_waiting = 1, job_log = f'Waiting for {number_of_running_jobs}.')
|
|
70
|
+
return
|
|
71
|
+
self.set_job_status(job_status = 'WORKING', job_waiting = 0) # take the job
|
|
72
|
+
else:
|
|
73
|
+
print(f"Job {self.job_id} is already taken.")
|
|
74
|
+
#print(job_status, flush = True)
|
|
75
|
+
return # exit.
|
|
76
|
+
else:
|
|
77
|
+
raise ValueError(f'job_id {self.job_id} does not exist.')
|
|
78
|
+
# get the paths
|
|
79
|
+
self.src_paths = pd.DataFrame((UploadJob.AssignedFiles() & dict(job_id = self.job_id)).fetch())
|
|
80
|
+
if not len(self.src_paths):
|
|
81
|
+
self.set_job_status(job_status = 'FAILED',
|
|
82
|
+
job_log = f'Could not find files for {self.job_id} in Upload.AssignedFiles.')
|
|
83
|
+
raise ValueError(f'Could not find files for {self.job_id} in Upload.AssignedFiles.')
|
|
84
|
+
self.upload_storage = self.jobquery.fetch('upload_storage')[0]
|
|
85
|
+
if self.upload_storage is None:
|
|
86
|
+
if 'upload_storage' in self.prefs.keys():
|
|
87
|
+
self.upload_storage = self.prefs['upload_storage']
|
|
88
|
+
|
|
89
|
+
# this should not fail because we have to keep track of errors, should update the table
|
|
90
|
+
src = [Path(self.local_path) / p for p in self.src_paths.src_path.values]
|
|
91
|
+
try:
|
|
92
|
+
comparison = compare_md5s(src,self.src_paths.src_md5.values)
|
|
93
|
+
except FileNotFoundError:
|
|
94
|
+
print('File not found for {0}'.format(Path(self.src_paths.src_path.iloc[0]).parent))
|
|
95
|
+
self.set_job_status(job_status = 'FAILED', job_log = 'FILE NOT FOUND; check file transfer.')
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
if not comparison:
|
|
99
|
+
print('CHECKSUM FAILED for {0}'.format(Path(self.src_paths.src_path.iloc[0]).parent))
|
|
100
|
+
self.set_job_status(job_status = 'FAILED',job_log = 'MD5 CHECKSUM failed; check file transfer.')
|
|
101
|
+
return # exit.
|
|
102
|
+
import traceback
|
|
103
|
+
try:
|
|
104
|
+
paths = self._apply_rule() # can use the src_paths
|
|
105
|
+
self._upload() # compare the hashes after
|
|
106
|
+
except Exception as err:
|
|
107
|
+
# log the error
|
|
108
|
+
print('There was an error processing or uploading this dataset.')
|
|
109
|
+
print(err)
|
|
110
|
+
self.set_job_status(job_status = 'FAILED',job_log = f'{traceback.print_exc()}')
|
|
111
|
+
return
|
|
112
|
+
try:
|
|
113
|
+
self._post_upload() # so the rules can insert tables and all.
|
|
114
|
+
except Exception as err:
|
|
115
|
+
# log the error
|
|
116
|
+
print('There was an error with the post-upload this dataset.')
|
|
117
|
+
print(err)
|
|
118
|
+
self.set_job_status(job_status = 'FAILED',job_log = f'POST-UPLOAD: {traceback.print_exc()}')
|
|
119
|
+
return
|
|
120
|
+
return paths # so apply can output paths.
|
|
121
|
+
|
|
122
|
+
def set_job_status(self, job_status = 'FAILED',job_log = '',job_waiting = 0):
|
|
123
|
+
from ..schema import UploadJob
|
|
124
|
+
if not self.job_id is None:
|
|
125
|
+
kk = dict(job_id = self.job_id,
|
|
126
|
+
job_waiting = job_waiting,
|
|
127
|
+
job_status = job_status,
|
|
128
|
+
job_log = job_log)
|
|
129
|
+
if job_status == 'FAILED':
|
|
130
|
+
print(f'Check job_id {self.job_id} : {job_status}')
|
|
131
|
+
kk['job_host'] = self.prefs['hostname'], # write the hostname so we know where it failed.
|
|
132
|
+
UploadJob.update1(kk)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _handle_processed_and_src_paths(self, processed_files,new_files):
|
|
136
|
+
'''
|
|
137
|
+
Put the files in the proper place and compute checksums for new files.
|
|
138
|
+
Call this from the apply method.
|
|
139
|
+
'''
|
|
140
|
+
n_jobs = DEFAULT_N_JOBS
|
|
141
|
+
self.processed_paths = []
|
|
142
|
+
for f in processed_files:
|
|
143
|
+
i = np.where(self.src_paths.src_path == f)[0][0]
|
|
144
|
+
self.processed_paths.append(self.src_paths.iloc[i])
|
|
145
|
+
self.src_paths.drop(self.src_paths.iloc[i].name,axis = 0,inplace = True)
|
|
146
|
+
self.src_paths.reset_index(drop=True,inplace = True)
|
|
147
|
+
self.processed_paths = pd.DataFrame(self.processed_paths).reset_index(drop=True)
|
|
148
|
+
|
|
149
|
+
res = Parallel(n_jobs = n_jobs)(delayed(_checksum_files)(
|
|
150
|
+
path,
|
|
151
|
+
local_path = self.local_path) for path in new_files)
|
|
152
|
+
for r in res:
|
|
153
|
+
r['job_id'] = self.job_id
|
|
154
|
+
self.src_paths = pd.concat([self.src_paths,pd.DataFrame(res)], ignore_index=True)
|
|
155
|
+
# drop duplicate paths in case there are any
|
|
156
|
+
self.src_paths = self.src_paths.drop_duplicates(subset=['src_path'], keep='last')
|
|
157
|
+
|
|
158
|
+
def _post_upload(self):
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
def _upload(self):
|
|
162
|
+
# this reads the attributes and uploads
|
|
163
|
+
# It also puts the files in the Tables
|
|
164
|
+
|
|
165
|
+
# destination in the bucket is actually the path
|
|
166
|
+
dst = [k for k in self.src_paths.src_path.values]
|
|
167
|
+
# source is the place where data are
|
|
168
|
+
src = [Path(self.local_path) / p for p in self.src_paths.src_path.values] # same as md5
|
|
169
|
+
# s3 copy in parallel hashes were compared before so no need to do it now.
|
|
170
|
+
self.set_job_status(job_status = 'WORKING',
|
|
171
|
+
job_log = datetime.now().strftime(f'Uploading {len(src)} files %Y %m %d %H:%M:%S'),
|
|
172
|
+
job_waiting = 0)
|
|
173
|
+
copy_to_s3(src, dst, md5_checksum=None,storage_name=self.upload_storage)
|
|
174
|
+
from ..schema import UploadJob, File, dj, ProcessedFile, Dataset
|
|
175
|
+
self.set_job_status(job_status = 'WORKING',
|
|
176
|
+
job_log = datetime.now().strftime('%Y %m %d %H:%M:%S'),
|
|
177
|
+
job_waiting = 0)
|
|
178
|
+
with dj.conn().transaction: # make it all update at the same time
|
|
179
|
+
# insert to Files so we know where to get the data
|
|
180
|
+
insfiles = []
|
|
181
|
+
for i,f in self.src_paths.iterrows():
|
|
182
|
+
insfiles.append(dict(file_path = f.src_path,
|
|
183
|
+
storage = self.upload_storage,
|
|
184
|
+
file_datetime = f.src_datetime,
|
|
185
|
+
file_size = f.src_size,
|
|
186
|
+
file_md5 = f.src_md5))
|
|
187
|
+
if len(insfiles):
|
|
188
|
+
File.insert(insfiles)
|
|
189
|
+
# Add to dataset?
|
|
190
|
+
job = self.jobquery.fetch(as_dict=True)[0]
|
|
191
|
+
# check if it has a dataset
|
|
192
|
+
if all([not job[a] is None for a in ['subject_name','session_name','dataset_name']]):
|
|
193
|
+
for i,p in enumerate(insfiles):
|
|
194
|
+
insfiles[i] = dict(subject_name = job['subject_name'],
|
|
195
|
+
session_name = job['session_name'],
|
|
196
|
+
dataset_name = job['dataset_name'],
|
|
197
|
+
file_path = p['file_path'],
|
|
198
|
+
storage = self.upload_storage)
|
|
199
|
+
if len(insfiles):
|
|
200
|
+
Dataset.DataFiles.insert(insfiles)
|
|
201
|
+
self.dataset_key = dict(subject_name = job['subject_name'],
|
|
202
|
+
session_name = job['session_name'],
|
|
203
|
+
dataset_name = job['dataset_name'])
|
|
204
|
+
self.inserted_files += insfiles
|
|
205
|
+
# Insert the processed files so the deletions are safe
|
|
206
|
+
if not self.processed_paths is None:
|
|
207
|
+
ins = []
|
|
208
|
+
for i,f in self.processed_paths.iterrows():
|
|
209
|
+
ins.append(dict(file_path = f.src_path,
|
|
210
|
+
file_datetime = f.src_datetime,
|
|
211
|
+
file_size = f.src_size,
|
|
212
|
+
file_md5 = f.src_md5))
|
|
213
|
+
if len(ins):
|
|
214
|
+
ProcessedFile.insert(ins)
|
|
215
|
+
if len(self.inserted_files): # keep the job in the queue
|
|
216
|
+
# completed
|
|
217
|
+
self.set_job_status(job_status = 'COMPLETED',
|
|
218
|
+
job_log = datetime.now().strftime('UPLOADED %Y %m %d %H:%M:%S'),
|
|
219
|
+
job_waiting = 0)
|
|
220
|
+
else:
|
|
221
|
+
self.set_job_status(job_status = 'FAILED',
|
|
222
|
+
job_log = datetime.now().strftime('No files inserted %Y %m %d %H:%M:%S'),
|
|
223
|
+
job_waiting = 0)
|
|
224
|
+
#(UploadJob & dict(job_id = self.job_id)).delete(safemode = False) # only delete if on completed
|
|
225
|
+
|
|
226
|
+
def _apply_rule(self):
|
|
227
|
+
# this rule does nothing, so the src_paths are going to be empty,
|
|
228
|
+
# and the "paths" are going to be the src_paths
|
|
229
|
+
self.processed_paths = None # processed paths are just the same, no file changed, so no need to do anything.
|
|
230
|
+
# needs to compute the checksum on all the new files
|
|
231
|
+
return self.src_paths.src_path.values
|
|
232
|
+
|
|
233
|
+
class ReplaceRule(UploadRule):
|
|
234
|
+
'''
|
|
235
|
+
Check if files exist in storage
|
|
236
|
+
Upload new versions of the files
|
|
237
|
+
Update MD5 checksums in database
|
|
238
|
+
'''
|
|
239
|
+
|
|
240
|
+
def __init__(self, job_id, prefs = None):
|
|
241
|
+
super(ReplaceRule,self).__init__(job_id = job_id, prefs = prefs)
|
|
242
|
+
self.rule_name = 'replace'
|
|
243
|
+
|
|
244
|
+
def _apply_rule(self):
|
|
245
|
+
# can only replace files that were uploaded, the files that are not there will be added to the dataset.
|
|
246
|
+
from ..s3 import s3_delete_file, copyfile_to_s3
|
|
247
|
+
from ..schema import File
|
|
248
|
+
self.processed_paths = None # processed paths are just the same, no file changed, so no need to do anything.
|
|
249
|
+
|
|
250
|
+
same_files = []
|
|
251
|
+
replace_files = []
|
|
252
|
+
new_files = []
|
|
253
|
+
for i,f in self.src_paths.iterrows():
|
|
254
|
+
existing = (File() & dict(file_path = f.src_path,
|
|
255
|
+
storage = self.upload_storage)).fetch(as_dict = True)
|
|
256
|
+
if len(existing):
|
|
257
|
+
existing = existing[0] # there is only one file
|
|
258
|
+
if existing['file_md5'] == f.src_md5:
|
|
259
|
+
same_files.append(dict(f))
|
|
260
|
+
else:
|
|
261
|
+
replace_files.append(dict(f))
|
|
262
|
+
else:
|
|
263
|
+
new_files.append(dict(f))
|
|
264
|
+
txt = f'There are {len(new_files)} new files, {len(replace_files)} files to be replaced and {len(same_files)} unmodified files.'
|
|
265
|
+
print(txt,flush = True)
|
|
266
|
+
self.set_job_status(job_status = 'WORKING',
|
|
267
|
+
job_log = txt,
|
|
268
|
+
job_waiting = 0)
|
|
269
|
+
##################### replace files happens here #########################
|
|
270
|
+
# destination in the bucket is actually the path
|
|
271
|
+
dst = [k['src_path'] for k in replace_files]
|
|
272
|
+
# source is the place where data are (this only works for a UNIX server at the moment)
|
|
273
|
+
src = [Path(self.local_path) / p['src_path'] for p in replace_files] # same as md5
|
|
274
|
+
# s3 copy in parallel hashes were compared before so no need to do it now.
|
|
275
|
+
copy_to_s3(src, dst, md5_checksum=None, storage_name=self.upload_storage)
|
|
276
|
+
for f in replace_files:
|
|
277
|
+
File.update1(dict(file_path = f['src_path'],
|
|
278
|
+
storage = self.upload_storage,
|
|
279
|
+
file_datetime = f['src_datetime'],
|
|
280
|
+
file_size = f['src_size'],
|
|
281
|
+
file_md5 = f['src_md5']))
|
|
282
|
+
print(f'Updated {f["src_path"]}')
|
|
283
|
+
|
|
284
|
+
# replace the src_paths with the new files otherwise delete all rows
|
|
285
|
+
if len(new_files):
|
|
286
|
+
self.src_paths = pd.DataFrame(new_files)
|
|
287
|
+
else:
|
|
288
|
+
self.src_paths.drop(self.src_paths.index, inplace = True)
|
|
289
|
+
|
|
290
|
+
return new_files+replace_files
|
labdata/s3.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# Functions for interacting with S3 storage.
|
|
2
|
+
# This file provides utilities for uploading, downloading and managing files on S3 storage.
|
|
3
|
+
# Key functionality includes:
|
|
4
|
+
# - Validating S3 storage configurations
|
|
5
|
+
# - Copying files to/from S3 buckets
|
|
6
|
+
# - Deleting files from S3
|
|
7
|
+
# - Parallel file transfer operations
|
|
8
|
+
#
|
|
9
|
+
# labdata 2024
|
|
10
|
+
|
|
11
|
+
from .utils import *
|
|
12
|
+
|
|
13
|
+
__all__ = ['validate_storage',
|
|
14
|
+
'copyfile_to_s3',
|
|
15
|
+
'copyfile_from_s3',
|
|
16
|
+
'copy_to_s3',
|
|
17
|
+
'copy_from_s3',
|
|
18
|
+
's3_delete_file']
|
|
19
|
+
|
|
20
|
+
def validate_storage(storage):
|
|
21
|
+
'''
|
|
22
|
+
storage = validate_storage(storage)
|
|
23
|
+
|
|
24
|
+
Checks that there is an s3 access_key and secret_key in the storage dictionary.
|
|
25
|
+
Updates the labdata preference file.
|
|
26
|
+
|
|
27
|
+
Joao Couto - labdata 2024
|
|
28
|
+
|
|
29
|
+
'''
|
|
30
|
+
# lets find the storage in the preferences
|
|
31
|
+
if 'storage' in prefs.keys():
|
|
32
|
+
for key in prefs['storage'].keys():
|
|
33
|
+
if prefs['storage'][key] == storage:
|
|
34
|
+
save_prefs = False
|
|
35
|
+
storage = prefs['storage'][key]
|
|
36
|
+
if not 'protocol' in storage.keys():
|
|
37
|
+
# assume S3
|
|
38
|
+
storage['protocol'] = 's3'
|
|
39
|
+
if 'save_prefs' in dir():
|
|
40
|
+
save_prefs = True
|
|
41
|
+
if storage['protocol'] == 's3':
|
|
42
|
+
# then try to find an access key
|
|
43
|
+
for k in ['access_key','secret_key']:
|
|
44
|
+
if not k in storage.keys():
|
|
45
|
+
storage[k] = None
|
|
46
|
+
if storage[k] is None:
|
|
47
|
+
import getpass
|
|
48
|
+
# get the password and write to file
|
|
49
|
+
storage[k] = getpass.getpass(prompt=f'S3 {k}:')
|
|
50
|
+
if 'save_prefs' in dir():
|
|
51
|
+
save_prefs = True
|
|
52
|
+
if 'save_prefs' in dir():
|
|
53
|
+
if save_prefs:
|
|
54
|
+
save_labdata_preferences(prefs, LABDATA_FILE)
|
|
55
|
+
return storage
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def copyfile_to_s3(source_file,
|
|
59
|
+
destination_file,
|
|
60
|
+
storage,
|
|
61
|
+
md5_checksum = None):
|
|
62
|
+
'''
|
|
63
|
+
Copy a single file to S3 and do a checksum comparisson.
|
|
64
|
+
|
|
65
|
+
Joao Couto - 2024
|
|
66
|
+
'''
|
|
67
|
+
from minio import Minio
|
|
68
|
+
client = Minio(endpoint = storage['endpoint'],
|
|
69
|
+
access_key = storage['access_key'],
|
|
70
|
+
secret_key = storage['secret_key'])
|
|
71
|
+
|
|
72
|
+
if 'folder' in storage.keys():
|
|
73
|
+
if len(storage['folder']):
|
|
74
|
+
destination_file = storage['folder'] + '/' + destination_file
|
|
75
|
+
|
|
76
|
+
if not md5_checksum is None:
|
|
77
|
+
if not md5_checksum == compute_md5_hash(source_file):
|
|
78
|
+
raise OSError(f'Checksum {md5_checksum} does not match {source_file}.')
|
|
79
|
+
res = client.fput_object(
|
|
80
|
+
storage['bucket'], destination_file, source_file)
|
|
81
|
+
return res
|
|
82
|
+
|
|
83
|
+
def copy_to_s3(source_files, destination_files,
|
|
84
|
+
storage = None,
|
|
85
|
+
storage_name = None,
|
|
86
|
+
md5_checksum = None,
|
|
87
|
+
n_jobs = DEFAULT_N_JOBS):
|
|
88
|
+
'''
|
|
89
|
+
Copy S3 and do a checksum comparisson.
|
|
90
|
+
Copy occurs in parallel for multiple files.
|
|
91
|
+
|
|
92
|
+
Joao Couto - 2024
|
|
93
|
+
'''
|
|
94
|
+
if storage is None:
|
|
95
|
+
if storage_name is None:
|
|
96
|
+
raise ValueError("Specify a storage to copy to - either pass the storage dictionary or specify a name from the prefs.")
|
|
97
|
+
storage = prefs['storage'][storage_name] # link to preferences storage from storage_name
|
|
98
|
+
storage = validate_storage(storage) # validate and update keys
|
|
99
|
+
|
|
100
|
+
if not type(source_files) is list: # check type of source
|
|
101
|
+
raise ValueError('source_files has to be a list of paths')
|
|
102
|
+
|
|
103
|
+
if not type(destination_files) is list: # check type of destination
|
|
104
|
+
raise ValueError('destination_files has to be a list of paths')
|
|
105
|
+
# Check if the source and the destination are the correct sizes
|
|
106
|
+
assert len(source_files) == len(destination_files),ValueError('source and destination are the wrong size')
|
|
107
|
+
|
|
108
|
+
if md5_checksum is None:
|
|
109
|
+
md5_checksum = [None]*len(source_files)
|
|
110
|
+
from tqdm import tqdm
|
|
111
|
+
n_jobs = validate_num_jobs_joblib(n_jobs) # check if running inside a joblib worker.
|
|
112
|
+
res = Parallel(n_jobs = n_jobs)(delayed(copyfile_to_s3)(src,
|
|
113
|
+
dst,
|
|
114
|
+
storage = storage,
|
|
115
|
+
md5_checksum = md5)
|
|
116
|
+
for src,dst,md5 in tqdm(zip(source_files,destination_files,md5_checksum),
|
|
117
|
+
desc = f'Pushing to S3 [{storage["bucket"]}]'))
|
|
118
|
+
return res
|
|
119
|
+
|
|
120
|
+
def boto3_copy_from_s3(src,dst,store):
|
|
121
|
+
import boto3
|
|
122
|
+
Path(dst).parent.mkdir(exist_ok=True,parents = True)
|
|
123
|
+
s3 = boto3.resource('s3',aws_access_key_id = store['access_key'],
|
|
124
|
+
aws_secret_access_key = store['secret_key'])
|
|
125
|
+
obj = s3.Object(bucket_name = store['bucket'],
|
|
126
|
+
key = str(src))
|
|
127
|
+
obj.download_file(str(dst))
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
def copyfile_from_s3(source_file,
|
|
131
|
+
destination_file,
|
|
132
|
+
storage,
|
|
133
|
+
md5_checksum = None,
|
|
134
|
+
engine = 'boto3'):
|
|
135
|
+
'''
|
|
136
|
+
Copy a single file to S3 and do a checksum comparisson.
|
|
137
|
+
|
|
138
|
+
This function copies a single file from an S3 bucket to local storage.
|
|
139
|
+
Supports both boto3 and minio engines for S3 operations and handles creating destination directories.
|
|
140
|
+
The function can optionally verify file integrity using an MD5 checksum [NOT IMPLEMENTED].
|
|
141
|
+
|
|
142
|
+
Note: If 'use_awscli' is set to TRUE in the preferences, it will use AWS CLI to copy from S3
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
source_file : str
|
|
147
|
+
Path to file in S3 bucket to copy
|
|
148
|
+
destination_file : str
|
|
149
|
+
Local path to copy file to
|
|
150
|
+
storage : dict
|
|
151
|
+
Dictionary containing S3 storage configuration with:
|
|
152
|
+
- endpoint: S3 endpoint URL
|
|
153
|
+
- access_key: AWS access key
|
|
154
|
+
- secret_key: AWS secret key
|
|
155
|
+
- bucket: S3 bucket name
|
|
156
|
+
md5_checksum : str, optional
|
|
157
|
+
MD5 hash to verify file integrity
|
|
158
|
+
engine : str, default 'boto3'
|
|
159
|
+
Engine to use for S3 operations ('boto3' or 'minio')
|
|
160
|
+
|
|
161
|
+
Returns
|
|
162
|
+
-------
|
|
163
|
+
bool or MinioResponse
|
|
164
|
+
True if copy successful with boto3
|
|
165
|
+
MinioResponse object if using minio engine
|
|
166
|
+
|
|
167
|
+
Joao Couto - 2024
|
|
168
|
+
'''
|
|
169
|
+
if 'use_awscli' in prefs.keys(): # to copy from inside ec2
|
|
170
|
+
if prefs['use_awscli']:
|
|
171
|
+
cmd = f'AWS_ACCESS_KEY_ID={storage["access_key"]} AWS_SECRET_ACCESS_KEY={storage["secret_key"]} aws s3 cp s3://{storage["bucket"]}/{source_file} {destination_file}'
|
|
172
|
+
os.system(cmd)
|
|
173
|
+
return True
|
|
174
|
+
if engine == 'boto3':
|
|
175
|
+
res = boto3_copy_from_s3(source_file,destination_file,storage)
|
|
176
|
+
else:
|
|
177
|
+
from minio import Minio
|
|
178
|
+
client = Minio(endpoint = storage['endpoint'],
|
|
179
|
+
access_key = storage['access_key'],
|
|
180
|
+
secret_key = storage['secret_key'])
|
|
181
|
+
|
|
182
|
+
if 'folder' in storage.keys():
|
|
183
|
+
if len(storage['folder']):
|
|
184
|
+
destination_file = storage['folder'] + '/' + destination_file
|
|
185
|
+
|
|
186
|
+
res = client.fget_object(
|
|
187
|
+
storage['bucket'], source_file, destination_file)
|
|
188
|
+
return res
|
|
189
|
+
|
|
190
|
+
def copy_from_s3(source_files, destination_files,
|
|
191
|
+
storage = None,
|
|
192
|
+
storage_name = None,
|
|
193
|
+
n_jobs = DEFAULT_N_JOBS,
|
|
194
|
+
engine = 'boto3'):
|
|
195
|
+
'''
|
|
196
|
+
copy_from_s3(source_files, destination_files,
|
|
197
|
+
storage = None,
|
|
198
|
+
storage_name = None,
|
|
199
|
+
n_jobs = DEFAULT_N_JOBS,
|
|
200
|
+
engine = 'boto3')
|
|
201
|
+
|
|
202
|
+
This function handles copying multiple files in parallel from an S3 bucket to local storage.
|
|
203
|
+
It supports both boto3 and minio engines for S3 operations. Files can be copied using
|
|
204
|
+
credentials directly provided in a storage dict or from named storage configurations in
|
|
205
|
+
preferences.
|
|
206
|
+
|
|
207
|
+
Parameters
|
|
208
|
+
----------
|
|
209
|
+
source_files : list
|
|
210
|
+
List of file paths to copy from S3
|
|
211
|
+
destination_files : list
|
|
212
|
+
List of local file paths to copy to
|
|
213
|
+
storage : dict, optional
|
|
214
|
+
Dictionary containing S3 storage configuration. Must include:
|
|
215
|
+
- endpoint: S3 endpoint URL
|
|
216
|
+
- access_key: AWS access key
|
|
217
|
+
- secret_key: AWS secret key
|
|
218
|
+
- bucket: S3 bucket name
|
|
219
|
+
storage_name : str, optional
|
|
220
|
+
Name of storage configuration to use from preferences. Required if storage not provided.
|
|
221
|
+
n_jobs : int, default DEFAULT_N_JOBS
|
|
222
|
+
Number of parallel jobs for copying files
|
|
223
|
+
engine : str, default 'boto3'
|
|
224
|
+
Engine to use for S3 operations ('boto3' or 'minio')
|
|
225
|
+
|
|
226
|
+
Returns
|
|
227
|
+
-------
|
|
228
|
+
list
|
|
229
|
+
List of results from copy operations
|
|
230
|
+
|
|
231
|
+
Raises
|
|
232
|
+
------
|
|
233
|
+
ValueError
|
|
234
|
+
If storage or storage_name not provided
|
|
235
|
+
If source_files or destination_files not lists
|
|
236
|
+
If source_files and destination_files different lengths
|
|
237
|
+
Joao Couto - 2024
|
|
238
|
+
'''
|
|
239
|
+
if storage is None:
|
|
240
|
+
if storage_name is None:
|
|
241
|
+
raise ValueError("Specify a storage to copy to - either pass the storage dictionary or specify a name from the prefs.")
|
|
242
|
+
storage = prefs['storage'][storage_name] # link to preferences storage from storage_name
|
|
243
|
+
storage = validate_storage(storage) # validate and update keys, this will store the credentials
|
|
244
|
+
|
|
245
|
+
if not type(source_files) is list: # check type of source
|
|
246
|
+
raise ValueError('source_files has to be a list of paths from s3')
|
|
247
|
+
|
|
248
|
+
if not type(destination_files) is list: # check type of destination
|
|
249
|
+
raise ValueError(f'destination_files has to be a list of paths {destination_files}')
|
|
250
|
+
# Check if the source and the destination are the correct sizes
|
|
251
|
+
assert len(source_files) == len(destination_files),ValueError(f'source {source_files} and destination {destination_files} are the wrong size')
|
|
252
|
+
if len(source_files) == 1: # don't run in parallel if only copying one file.
|
|
253
|
+
print(f'[Downloading] {source_files[0]}')
|
|
254
|
+
res = [copyfile_from_s3(source_files[0],destination_files[0],storage = storage)]
|
|
255
|
+
else:
|
|
256
|
+
from tqdm import tqdm
|
|
257
|
+
n_jobs = validate_num_jobs_joblib(n_jobs) # avoid nested parallelism.
|
|
258
|
+
res = Parallel(n_jobs = n_jobs)(delayed(copyfile_from_s3)(src,
|
|
259
|
+
dst,
|
|
260
|
+
storage = storage,
|
|
261
|
+
engine = engine)
|
|
262
|
+
for src,dst in tqdm(zip(source_files,destination_files),
|
|
263
|
+
desc = f'Pulling from S3 [{storage["bucket"]}]'))
|
|
264
|
+
return res
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def s3_delete_file(filepath,storage, remove_versions = False):
|
|
268
|
+
'''
|
|
269
|
+
s3_delete_file(filepath,storage, remove_versions = False)
|
|
270
|
+
|
|
271
|
+
Deletes files from s3.
|
|
272
|
+
TODO: make this parallel or a parallel version of the function.
|
|
273
|
+
!!Warning!! this does not wait for confirmation.
|
|
274
|
+
remove_versions = True will delete all versions from the bucket.
|
|
275
|
+
|
|
276
|
+
Parameters
|
|
277
|
+
----------
|
|
278
|
+
filepath : str
|
|
279
|
+
Path to file to delete on S3
|
|
280
|
+
storage : dict
|
|
281
|
+
Dictionary containing S3 storage configuration:
|
|
282
|
+
- endpoint: S3 endpoint URL
|
|
283
|
+
- access_key: S3 access key
|
|
284
|
+
- secret_key: S3 secret key
|
|
285
|
+
- bucket: S3 bucket name
|
|
286
|
+
remove_versions : bool, default False
|
|
287
|
+
Whether to remove all versions of the file
|
|
288
|
+
If True, deletes all versions
|
|
289
|
+
If False, only deletes latest version
|
|
290
|
+
|
|
291
|
+
Returns
|
|
292
|
+
-------
|
|
293
|
+
object
|
|
294
|
+
Response from S3 delete operation
|
|
295
|
+
|
|
296
|
+
Raises
|
|
297
|
+
------
|
|
298
|
+
MinioException
|
|
299
|
+
If delete operation fails
|
|
300
|
+
If file does not exist
|
|
301
|
+
If credentials are invalid
|
|
302
|
+
|
|
303
|
+
Joao Couto - 2024
|
|
304
|
+
'''
|
|
305
|
+
from minio import Minio
|
|
306
|
+
client = Minio(endpoint = storage['endpoint'],
|
|
307
|
+
access_key = storage['access_key'],
|
|
308
|
+
secret_key = storage['secret_key'])
|
|
309
|
+
|
|
310
|
+
if remove_versions:
|
|
311
|
+
objects = client.list_objects(storage['bucket'], prefix=filepath,include_version=True)
|
|
312
|
+
for obj in objects:
|
|
313
|
+
res = client.remove_object(storage['bucket'], obj.object_name,version_id = obj.version_id)
|
|
314
|
+
return
|
|
315
|
+
else:
|
|
316
|
+
res = client.remove_object(storage['bucket'], filepath)
|
|
317
|
+
return res
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'''Schema for lab data management.
|
|
2
|
+
|
|
3
|
+
This package provides DataJoint schemas (tables) for accessing and managing laboratory data.
|
|
4
|
+
The schemas are organized into modules by data type:
|
|
5
|
+
|
|
6
|
+
- `general` - Core tables for files, subjects, sessions, datasets
|
|
7
|
+
- `procedures` - Tables for experimental procedures and protocols
|
|
8
|
+
- `ephys` - Tables for electrophysiology recordings and analysis
|
|
9
|
+
- `twophoton` - Tables for two-photon microscopy data
|
|
10
|
+
- `onephoton` - Tables for one-photon imaging (widefield and miniscope)
|
|
11
|
+
- `tasks` - Tables for behavioral task data
|
|
12
|
+
- `video` - Tables for video recordings
|
|
13
|
+
- `histology` - Tables for histology and anatomy data
|
|
14
|
+
'''
|
|
15
|
+
|
|
16
|
+
from .general import *
|
|
17
|
+
from .procedures import *
|
|
18
|
+
from .ephys import *
|
|
19
|
+
from .twophoton import *
|
|
20
|
+
from .onephoton import * # includes widefield and miniscope
|
|
21
|
+
from .tasks import *
|
|
22
|
+
from .video import *
|
|
23
|
+
from .histology import *
|
|
24
|
+
|