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.
@@ -0,0 +1,647 @@
1
+ '''General schema classes for lab data management.
2
+
3
+ This module defines the core database schema for managing laboratory data using DataJoint.
4
+ It includes tables for tracking files, subjects, sessions, datasets and analysis results.
5
+
6
+ Configuration
7
+ ------------
8
+ Uses the following settings from user_preferences.json:
9
+ - database.host : Database server hostname
10
+ - database.user : Database username
11
+ - database.password : Database password
12
+ - database.name : Name of the database
13
+
14
+ Schemas
15
+ -------
16
+ - dataschema : DataJoint schema
17
+ Primary schema for raw experimental data tables
18
+ - analysisschema : DataJoint schema
19
+ Schema for computed/analyzed data tables
20
+
21
+ Classes
22
+ -------
23
+ - ``File`` : Table for tracking raw data files with paths, checksums and metadata
24
+ - ``AnalysisFile`` : Table for tracking analysis output files
25
+ - ``ProcessedFile`` : Table for tracking processed data files
26
+ - ``LabMember`` : Table of lab personnel
27
+ - ``Species`` : Table of experimental animal species
28
+ - ``Strain`` : Table of animal strains/lines
29
+ - ``Subject`` : Table of experimental subjects
30
+ - ``Session`` : Table of experimental sessions
31
+ - ``Dataset`` : Table of experimental datasets
32
+ - ``SetupLocation`` : Table of experimental setup locations
33
+ - ``Setup`` : Table of experimental setups and equipment
34
+ - ``Note`` : Table for general notes and comments
35
+ - ``DatasetType`` : Table of dataset type definitions
36
+ - ``DatasetEvents`` : Table of events within datasets
37
+ - ``StreamSync`` : Table for synchronization between data streams
38
+ - ``UploadJob`` : Table for tracking data uploads
39
+ - ``ComputeTask`` : Table for tracking analysis computations
40
+ '''
41
+
42
+ from ..utils import *
43
+ import datajoint as dj
44
+
45
+ if 'database' in prefs.keys():
46
+ for k in prefs['database'].keys():
47
+ if prefs['database'][k] is None:
48
+ if k in ['database.user', 'database.password']:
49
+ import getpass
50
+ # get the password and write to file
51
+ prefs['database'][k] = getpass.getpass(prompt=k)
52
+ gotpass = True
53
+ if not prefs['database'][k] is None:
54
+ dj.config[k] = prefs['database'][k]
55
+ if 'gotpass' in dir():
56
+ # overwrite the preference file (save the datajoint password.)
57
+ save_labdata_preferences(prefs, LABDATA_FILE)
58
+
59
+ dataschema = dj.schema(dj.config['database.name'])
60
+ analysisschema = dj.schema(dj.config['database.name']+'_computed')
61
+
62
+ DEFAULT_RAW_STORAGE = 'data'
63
+ DEFAULT_ANALYSIS_STORAGE = 'analysis'
64
+
65
+
66
+ @dataschema
67
+ class File(dj.Manual):
68
+ '''Table for tracking files stored in S3 storage.
69
+
70
+ This table stores metadata about files including their path, storage location,
71
+ creation date, size and MD5 checksum. It provides methods for:
72
+
73
+ - Deleting files from both the database and S3 storage
74
+ - Downloading files from S3 to local storage
75
+ - Checking if files are archived in S3 Glacier storage
76
+
77
+ The table is used as a base class for AnalysisFile which handles analysis outputs.
78
+ '''
79
+ definition = '''
80
+ file_path : varchar(300) # Path to the file
81
+ storage = "{0}" : varchar(12) # storage name
82
+ ---
83
+ file_datetime : datetime # date created
84
+ file_size : double # using double because int64 does not exist
85
+ file_md5 = NULL : varchar(32) # md5 checksum
86
+ '''.format(DEFAULT_RAW_STORAGE)
87
+ storage = DEFAULT_RAW_STORAGE
88
+ # Files get deleted from AWS if the user has permissions
89
+ def delete(
90
+ self,
91
+ transaction = True,
92
+ safemode = None,
93
+ force_parts = False):
94
+ '''Delete files from both the database and S3 storage.
95
+
96
+ Parameters
97
+ ----------
98
+ transaction : bool, optional
99
+ Whether to perform deletion as a transaction, by default True
100
+ safemode : bool, optional
101
+ Whether to run in safe mode, by default None
102
+ force_parts : bool, optional
103
+ Whether to force deletion of parts, by default False
104
+
105
+ Raises
106
+ ------
107
+ ValueError
108
+ If files are deleted from database but not from S3
109
+ '''
110
+ from ..s3 import s3_delete_file
111
+ from tqdm import tqdm
112
+ filesdict = [f for f in self]
113
+ super().delete(transaction = transaction,
114
+ safemode = safemode,
115
+ force_parts = force_parts)
116
+ if len(self) == 0:
117
+ files_not_deleted = []
118
+ for s in tqdm(filesdict,desc = f'Deleting objects from s3 {"storage"}:'):
119
+ fname = s["file_path"]
120
+ try:
121
+ s3_delete_file(fname,
122
+ storage = prefs['storage'][s['storage']],
123
+ remove_versions = True)
124
+
125
+ except Exception as err:
126
+ print(f'Could not delete {fname}.')
127
+ files_not_deleted.append(fname)
128
+ if len(files_not_deleted):
129
+ print('\n'.join(files_not_deleted))
130
+ raise(ValueError('''
131
+
132
+ [Integrity error] Files were deleted from the database but not from AWS.
133
+
134
+ Save this message and show it to your database ADMIN.
135
+
136
+ {0}
137
+
138
+ '''.format('\n'.join(files_not_deleted))))
139
+
140
+ def check_if_files_local(self, local_paths = None):
141
+ '''
142
+ Checks if files are in a local path, searches accross all local paths
143
+
144
+ Parameters
145
+ ----------
146
+ local_paths : list of str or Path, optional
147
+ List of local paths to check for files, by default None uses paths in preferences
148
+
149
+ Returns
150
+ -------
151
+ tuple
152
+ Tuple of local file paths and missing files
153
+
154
+ Raises
155
+ ------
156
+ ValueError
157
+ If no files in the object
158
+ '''
159
+ if local_paths is None:
160
+ local_paths = prefs['local_paths']
161
+ if not len(self):
162
+ raise(ValueError('No files to get.'))
163
+ # this does not work with multiple storages
164
+ files = [f['file_path'] for f in self]
165
+ localfiles = [find_local_filepath(a, local_paths = local_paths) for a in files]
166
+ # check if they exist and download only missing files.
167
+ missingfiles = []
168
+ for f in files:
169
+ if not np.any([str(l).endswith(f) for l in localfiles]):
170
+ missingfiles.append(f)
171
+ return localfiles, missingfiles
172
+
173
+ def get(self,local_paths = None, check_if_archived = True, restore=True, download = True):
174
+ '''Download files from S3 to local storage.
175
+
176
+ Parameters
177
+ ----------
178
+ local_paths : list of str or Path, optional
179
+ List of local paths to download files to, by default None uses paths in preferences
180
+ check_if_archived : bool, optional
181
+ Whether to check if files are in Glacier storage, by default True
182
+ restore : bool, optional
183
+ Whether to restore archived files, by default True
184
+ download : bool, optional
185
+ Whether to actually download the files, by default True
186
+
187
+ Returns
188
+ -------
189
+ list
190
+ List of local file paths that were downloaded
191
+
192
+ Raises
193
+ ------
194
+ ValueError
195
+ If no files are found to download
196
+ '''
197
+ if local_paths is None:
198
+ local_paths = prefs['local_paths']
199
+ if not len(self):
200
+ raise(ValueError('No files to get.'))
201
+
202
+ localfiles, remotefiles = self.check_if_files_local(local_paths = local_paths)
203
+ storage = [f['storage'] for f in self][0]
204
+ remotefiles = self & [dict(file_path = f) for f in remotefiles]
205
+ if len(remotefiles):
206
+ if check_if_archived:
207
+ # TODO: add to the preference file to not restore by default.
208
+ self.check_if_files_archived(files = remotefiles, restore = restore)
209
+ if download:
210
+ print(f'Downloading {len(remotefiles)} files from S3 [{storage}].')
211
+ remotefiles = [r['file_path'] for r in remotefiles]
212
+ dstfiles = [Path(local_paths[0])/f for f in remotefiles] # place to store file.
213
+ from ..s3 import copy_from_s3
214
+ copy_from_s3(remotefiles,dstfiles,storage_name = storage)
215
+ localfiles, _ = self.check_if_files_local(local_paths = local_paths)
216
+ return localfiles
217
+
218
+ def check_if_files_archived(self, files = None, restore = True, suppress_error = False):
219
+ '''Check if files are archived in S3 Glacier storage.
220
+
221
+ Parameters
222
+ ----------
223
+ restore : bool, optional
224
+ Whether to initiate restore of archived files, by default True
225
+ suppress_error : bool, optional
226
+ Whether to suppress error if files are being restored, by default False
227
+
228
+ Returns
229
+ -------
230
+ bool
231
+ True if files are archived, False otherwise
232
+
233
+ Raises
234
+ ------
235
+ OSError
236
+ If storage is not in preferences or if files are being restored
237
+ '''
238
+ files_restoring = []
239
+ import boto3
240
+ if files is None:
241
+ files = self
242
+ for f in files:
243
+ # check if files are archived
244
+ # TODO: run this in parallel because it takes a while.
245
+ if not f['storage'] in prefs['storage'].keys():
246
+ raise(OSError(f"Store {f['storage']} is not in the preference file."))
247
+ store = prefs['storage'][f['storage']]
248
+
249
+ s3 = boto3.resource('s3',aws_access_key_id = store['access_key'],
250
+ aws_secret_access_key = store['secret_key'])
251
+
252
+ obj = s3.Object(bucket_name = store['bucket'],
253
+ key = f['file_path'])
254
+
255
+ if not obj.archive_status is None and 'ARCHIVE' in obj.archive_status:
256
+ if obj.restore is None:
257
+ if restore:
258
+ resp = obj.restore_object(RestoreRequest = {})
259
+ files_restoring.append(f['file_path'])
260
+ elif 'true' in obj.restore:
261
+ files_restoring.append(f['file_path'])
262
+ if len(files_restoring):
263
+ import warnings
264
+ warnings.warn(f"Files are being restored [{files_restoring}]")
265
+ if not suppress_error:
266
+ raise(OSError(f"Files are being restored [{files_restoring}]"))
267
+ return True # files are in arquive
268
+ return False # files are not in archive
269
+
270
+
271
+
272
+ # This is in "analysisschema" so users can delete analysis files.
273
+ @analysisschema
274
+ class AnalysisFile(File):
275
+ definition = '''
276
+ file_path : varchar(300) # Path to the file
277
+ storage = "{0}" : varchar(12) # storage name
278
+ ---
279
+ file_datetime : datetime # date created
280
+ file_size : double # using double because int64 does not exist
281
+ file_md5 = NULL : varchar(32) # md5 checksum
282
+ '''.format(DEFAULT_ANALYSIS_STORAGE)
283
+ storage = DEFAULT_ANALYSIS_STORAGE
284
+ # All users with permission to run analysis should also have permission to add and remove files from the analysis bucket in AWS
285
+ def upload_files(self,src,dataset, force = True):
286
+ assert 'subject_name' in dataset.keys(), ValueError('dataset must have subject_name')
287
+ assert 'session_name' in dataset.keys(), ValueError('dataset must have session_name')
288
+ assert 'dataset_name' in dataset.keys(), ValueError('dataset must have dataset_name')
289
+
290
+ destpath = '{subject_name}/{session_name}/{dataset_name}/'.format(**dataset)
291
+ dst = [destpath+k.name for k in src]
292
+ for d in dst:
293
+ if len(AnalysisFile() & dict(file_path = d)) > 0:
294
+ if not force:
295
+ ValueError(f'File is already in database, delete it to re-upload {d}.')
296
+ else:
297
+ (AnalysisFile() & dict(file_path = d)).delete(safemode = False)
298
+ assert self.storage in prefs['storage'].keys(),ValueError(
299
+ 'Specify an {self.storage} bucket in preferences["storage"].')
300
+ from ..s3 import copy_to_s3
301
+
302
+ copy_to_s3(src, dst, md5_checksum=None, storage_name = self.storage)
303
+ dates = [datetime.utcfromtimestamp(Path(f).stat().st_mtime) for f in src]
304
+ sizes = [Path(f).stat().st_size for f in src]
305
+ md5 = compute_md5s(src)
306
+ # insert in AnalysisFile if all went well
307
+ self.insert([dict(file_path = f,
308
+ storage = self.storage,
309
+ file_datetime = d,
310
+ file_md5 = m,
311
+ file_size = s) for f,d,s,m in zip(dst,dates,sizes,md5)])
312
+ return [dict(file_path = f,storage = self.storage) for f in dst]
313
+
314
+
315
+ # This table stores file name and checksums of files that were sent to upload but were processed by upload rules
316
+ # There are no actual files associated with these paths
317
+ @dataschema
318
+ class ProcessedFile(dj.Manual):
319
+ definition = '''
320
+ file_path : varchar(300) # Path to the file that was processe (these are not in S3)
321
+ ---
322
+ file_datetime : datetime # date created
323
+ file_size : double # using double because int64 does not exist
324
+ file_md5 = NULL : varchar(32) # md5 checksum
325
+ '''
326
+
327
+ @dataschema
328
+ class LabMember(dj.Manual):
329
+ definition = """
330
+ user_name : varchar(32) # username
331
+ ---
332
+ email=null : varchar(128) # email address
333
+ first_name = null : varchar(32) # first name
334
+ last_name = null : varchar(32) # last name
335
+ date_joined : date # when the user joined the lab
336
+ is_active = 1 : boolean # active or left the lab
337
+ """
338
+
339
+ @dataschema
340
+ class Species(dj.Lookup):
341
+ definition = """
342
+ species_name : varchar(32) # species nickname
343
+ ---
344
+ species_scientific_name : varchar(56) # scientific name
345
+ species_description=null : varchar(256) # description
346
+ """
347
+
348
+ @dataschema
349
+ class Strain(dj.Lookup):
350
+ definition = """
351
+ strain_name : varchar(56) # strain name
352
+ ---
353
+ -> Species
354
+ strain_description=null : varchar(256) # description
355
+ """
356
+
357
+ @dataschema
358
+ class Subject(dj.Manual):
359
+ ''' Experimental subject.'''
360
+ definition = """
361
+ subject_name : varchar(20) # unique mouse id
362
+ ---
363
+ subject_dob : date # mouse date of birth
364
+ subject_sex : enum('M', 'F', 'U') # sex of mouse - Male, Female, or Unknown
365
+ -> Strain
366
+ -> LabMember
367
+ """
368
+
369
+ @dataschema
370
+ class SetupLocation(dj.Lookup):
371
+ definition = """
372
+ setup_location : varchar(255) # room
373
+ ---
374
+ """
375
+ contents = zip(['CHS-74100'])
376
+
377
+ @dataschema
378
+ class Setup(dj.Lookup):
379
+ definition = """
380
+ setup_name : varchar(54) # setup name
381
+ ---
382
+ -> [nullable] SetupLocation
383
+ setup_description = NULL : varchar(512)
384
+ """
385
+
386
+ @dataschema
387
+ class Note(dj.Manual):
388
+ definition = """
389
+ -> LabMember.proj(notetaker='user_name')
390
+ note_datetime : datetime
391
+ ---
392
+ notes = '' : varchar(4000) # free-text notes
393
+ """
394
+ class Image(dj.Part):
395
+ definition = """
396
+ -> Note
397
+ image_id : int
398
+ ---
399
+ image : longblob
400
+ caption = NULL : varchar(256)
401
+ """
402
+ class Attachment(dj.Part):
403
+ definition = """
404
+ -> Note
405
+ -> File
406
+ ---
407
+ caption = NULL : varchar(256)
408
+ """
409
+
410
+ @dataschema
411
+ class Session(dj.Manual):
412
+ definition = """
413
+ -> Subject
414
+ session_name : varchar(54) # session identifier
415
+ ---
416
+ session_datetime : datetime # experiment date
417
+ -> [nullable] LabMember.proj(experimenter = 'user_name')
418
+ """
419
+
420
+ @dataschema
421
+ class DatasetType(dj.Lookup):
422
+ definition = """
423
+ dataset_type: varchar(32)
424
+ """
425
+ contents = zip(dataset_type_names)
426
+
427
+ @dataschema
428
+ class Dataset(dj.Manual):
429
+ definition = """
430
+ -> Subject
431
+ -> Session
432
+ dataset_name : varchar(128)
433
+ ---
434
+ -> [nullable] DatasetType
435
+ -> [nullable] Setup
436
+ -> [nullable] Note
437
+ """
438
+ class DataFiles(dj.Part): # the files that were acquired on that dataset.
439
+ definition = '''
440
+ -> master
441
+ -> File
442
+ '''
443
+
444
+ # Synchronization variables for the dataset live here; these can come from different streams
445
+ @dataschema
446
+ class DatasetEvents(dj.Imported):
447
+ definition = '''
448
+ -> Dataset
449
+ stream_name : varchar(54) # which clock is used e.g. btss, nidq, bpod, imecX
450
+ ---
451
+ stream_time = NULL : longblob # for e.g. the analog channels
452
+ '''
453
+ class Digital(dj.Part):
454
+ definition = '''
455
+ -> master
456
+ event_name : varchar(54)
457
+ ---
458
+ event_timestamps = NULL : longblob # timestamps of the events
459
+ event_values = NULL : longblob # event value or count
460
+ '''
461
+ projkeys = ['subject_name','session_name','dataset_name','stream_name','event_name']
462
+
463
+ def fetch_synced(self, force = False):
464
+ '''
465
+ Returned events already synchronized between data streams, following the StreamSync table.
466
+ '''
467
+ keys = [dict(subject_name = s["subject_name"],
468
+ session_name = s["session_name"],
469
+ dataset_name = s["dataset_name"],
470
+ stream_name = s["stream_name"]) for s in self]
471
+ evnts = []
472
+ streams = (StreamSync() & keys)
473
+ if not len(streams):
474
+ from warnings import warn
475
+ warn(f'There are no StreamSync for events {self.proj()}. This will return only the clock stream.')
476
+ else:
477
+ for s in streams:
478
+ evs = (self & dict(stream_name = s["stream_name"])).fetch(as_dict = True)
479
+ func = (StreamSync() & s).apply(None, force = force)
480
+ for evnt in evs:
481
+ if not evnt['event_timestamps'] is None:
482
+ evnt['event_timestamps'] = func(evnt['event_timestamps'])
483
+ evnts.append(evnt)
484
+ # add the events from the clock stream
485
+ if len(streams):
486
+ evs = (self & dict(stream_name = streams.clock_stream())).fetch(as_dict = True)
487
+ else:
488
+ evs = self
489
+ for evnt in evs:
490
+ evnts.append(evnt)
491
+ return evnts
492
+
493
+ def plot_synced(self, stream_colors = 'krbgyb', overlay_original = False, lw = 1,force = True):
494
+ ''' Plot DatasetEvents.Digital.'''
495
+ evnts = self.fetch_synced(force = force)
496
+ ustreams = [n for n in np.unique([e['stream_name'] for e in evnts])]
497
+
498
+ import pylab as plt
499
+ caption = []
500
+ ticks = []
501
+ lns = []
502
+ for i,e in enumerate(evnts):
503
+ ln = plt.vlines(e['event_timestamps'],i,i+0.7,
504
+ color = stream_colors[np.mod(ustreams.index(e['stream_name']),len(stream_colors))],
505
+ lw = lw)
506
+ if overlay_original:
507
+ ee = (DatasetEvents.Digital() & {k:e[k] for k in self.projkeys}).fetch('event_timestamps')[0]
508
+ plt.vlines(ee,i+0.6,i+0.9,color='gray',lw = lw)
509
+ lns.append(ln)
510
+ ticks.append(i+0.35)
511
+ caption.append(f'{e["stream_name"]}_{e["event_name"]}')
512
+ plt.yticks(ticks,caption);
513
+ return lns
514
+
515
+ class AnalogChannel(dj.Part):
516
+ definition = '''
517
+ -> master
518
+ channel_name : varchar(54)
519
+ ---
520
+ channel_values = NULL : longblob # analog values for channel
521
+ '''
522
+
523
+ @analysisschema
524
+ class StreamSync(dj.Manual):
525
+ definition = '''
526
+ -> Dataset
527
+ -> DatasetEvents.Digital
528
+ -> DatasetEvents.Digital.proj(clock_stream='stream_name',clock_stream_event='event_name',clock_dataset = 'dataset_name')
529
+ '''
530
+ def get_interp_data(self,force = False, warn = True, allowed_offset = 2):
531
+ ''' Force will attempt to remove events from the longest stream so the streams are matched. '''
532
+
533
+ assert len(self)==1, ValueError(f"This function only takes one element at a time not {len(self)}.")
534
+ s = self.fetch1()
535
+ clock = (DatasetEvents.Digital() & dict(subject_name = s['subject_name'],
536
+ session_name = s['session_name'],
537
+ dataset_name = s['clock_dataset'],
538
+ stream_name = s['clock_stream'],
539
+ event_name = s['clock_stream_event'])).fetch1()
540
+ clock_onsets = clock['event_timestamps']#[clock['event_values'] == 1]
541
+ sync = (DatasetEvents.Digital() & dict(subject_name = s['subject_name'],
542
+ session_name = s['session_name'],
543
+ dataset_name = s['dataset_name'],
544
+ stream_name = s['stream_name'],
545
+ event_name = s['event_name'])).fetch1()
546
+ sync_onsets = sync['event_timestamps']
547
+ if ((len(sync_onsets)>=(len(clock_onsets)//2)-allowed_offset) and
548
+ (len(sync_onsets)<=(len(clock_onsets)//2)+allowed_offset)): # in case clock has both onsets and offsets
549
+ if clock['event_values'] is None:
550
+ clock_onsets = clock_onsets[::2]
551
+ else:
552
+ clock_onsets = clock_onsets[clock['event_values'] == 1]
553
+ if ((len(clock_onsets)>=(len(sync_onsets)//2)-allowed_offset) and
554
+ (len(clock_onsets)<=(len(sync_onsets)//2)+allowed_offset)): # in case sync has both onsets and offsets
555
+ if sync['event_values'] is None:
556
+ sync_onsets = sync_onsets[::2]
557
+ else:
558
+ sync_onsets = sync_onsets[sync['event_values']==1]
559
+ if warn:
560
+ if not len(sync_onsets) == len(clock_onsets):
561
+ import warnings
562
+ warnings.warn(f"There is a potential issue with the syncronization of sessions: {s['subject_name']} {s['session_name']}", UserWarning)
563
+ print(f" - stream {s['clock_stream']} channel {s['clock_stream_event']} {len(clock_onsets)}")
564
+ print(f" - stream {s['stream_name']} channel {s['event_name']} {len(sync_onsets)}")
565
+ if not force: # by default this is set to false.
566
+ assert len(clock_onsets) == len(sync_onsets), ValueError(f'\n\n Length of the clock and sync not the same? \n\n {self}')
567
+ N = np.min([len(sync_onsets),len(clock_onsets)])
568
+ return sync_onsets[:N],clock_onsets[:N]
569
+
570
+ def apply(self, values, sync_onsets = None, warn = True, clock_onsets = None, force = False):
571
+ '''
572
+ Returns synchronized signals according a sync pulse shared from a clock.
573
+ "clock" is main, "sync" is the same acquisition system as "values"
574
+ '''
575
+ if sync_onsets is None or clock_onsets is None:
576
+ sync_onsets, clock_onsets = self.get_interp_data(force = force, warn = warn)
577
+ if values is None: # return a function if no values passed
578
+ return lambda x: np.interp(x,sync_onsets,clock_onsets)
579
+ # linear interpolation to get the time of events syncronized across streams
580
+ return np.interp(values,sync_onsets,clock_onsets)
581
+
582
+ def clock_stream(self):
583
+ '''
584
+ Returns the name of the clock stream(s)
585
+ '''
586
+ clkstreams = np.unique(self.fetch('clock_stream'))
587
+ if len(clkstreams) == 1:
588
+ return clkstreams[0]
589
+ return clkstreams
590
+
591
+ ####################################################
592
+ #######################QUEUES#######################
593
+ ####################################################
594
+ # Upload queue, so that experimental computers are not transfering data
595
+ # @dataschema
596
+ @analysisschema
597
+ class UploadJob(dj.Manual):
598
+ definition = '''
599
+ job_id : int auto_increment
600
+ ---
601
+ job_waiting = 1 : tinyint # 1 if the job is up for grabs
602
+ job_status = NULL : varchar(52) # status of the job (did it fail?)
603
+ job_host = NULL : varchar(52) # where the job is running
604
+ job_rule = NULL : varchar(52) # what rule is it following
605
+ job_log = NULL : varchar(500) # LOG
606
+ -> [nullable] Dataset # optionally insert to dataset
607
+ upload_storage = NULL : varchar(12) # storage name, where to upload
608
+
609
+ '''
610
+
611
+ class AssignedFiles(dj.Part):
612
+ definition = '''
613
+ -> master
614
+ src_path : varchar(300) # local file path
615
+ ---
616
+ src_datetime : datetime # date created
617
+ src_size : double # using double because int64 does not exist
618
+ src_md5 = NULL : varchar(32) # md5 checksum
619
+ '''
620
+
621
+ # Jobs to perform computations, like spike sorting or segmentation
622
+ # @dataschema
623
+ @analysisschema
624
+ class ComputeTask(dj.Manual):
625
+ definition = '''
626
+ job_id : int auto_increment
627
+ ---
628
+ task_waiting = 1 : tinyint # 1 if the job is up for grabs
629
+ task_name = NULL : varchar(52) # what task to run
630
+ task_status = NULL : varchar(52) # status of the job (did it fail?)
631
+ task_target = NULL : varchar(52) # where to run the job, so it only runs where selected
632
+ task_host = NULL : varchar(52) # where the job is running
633
+ task_cmd = NULL : varchar(500) # command to run
634
+ task_parameters = NULL : varchar(2000) # command to run after the job finishes
635
+ task_log = NULL : varchar(2000) # LOG
636
+ task_starttime = NULL : datetime # time of task start
637
+ task_endtime = NULL : datetime # time of task completion
638
+
639
+ -> [nullable] Dataset # dataset
640
+ '''
641
+
642
+ class AssignedFiles(dj.Part):
643
+ definition = '''
644
+ -> master
645
+ -> File
646
+ '''
647
+