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/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
+