def update_sample(user: User, sample: Sample, new_data: Dict[str, Any], filename: str = None) -> Sample: """ Change the attributes of the sample file with sample_id If a key exists in both the file and the db, it will be updated in both. :param user: :param sample: :param new_data: :param filename: :return: """ if is_write_permitted(user, sample): # file attributes and database attributes should be separated if 'id' in new_data: if sample.id != int(new_data['id']) and Sample.query.filter_by( id=new_data['id']) is not None: raise ValueError( f'Sample with id {new_data["id"]} already exists!') if 'sample_group_ids' in new_data: new_data['sample_group_ids'] = [ int(sample_group_id) for sample_group_id in new_data['sample_group_ids'] ] new_sample_groups = [ get_sample_group(user, sample_group_id) for sample_group_id in new_data['sample_group_ids'] ] remove_sample_groups = [ sample_group for sample_group in sample.sample_groups if sample_group.id not in new_data['sample_group_ids'] ] for sample_group in new_sample_groups: if not is_write_permitted(user, sample_group): raise AuthException( f'User {user.email} is not permitted to attach sample {sample.id} to sample group {sample_group.id}' ) for sample_group in remove_sample_groups: if not is_write_permitted(user, sample_group): raise AuthException( f'User {user.email} is not permitted to detach sample {sample.id} from sample group {sample_group.id}' ) sample.sample_groups = new_sample_groups sample.update(new_data) if 'file_info' in new_data: mdt.update_metadata(sample.filename, new_data['file_info']) if filename is not None: os.remove(sample.filename) shutil.copy(filename, sample.filename) os.remove(filename) sample.last_editor = user sample.filename = f'/data/samples/{sample.id}.h5' db.session.commit() return sample raise AuthException( f'User {user.email} is not permitted to modify sample {sample.id}')
def update_workflow(user: User, workflow: Workflow, new_data: Dict[str, Any], filename: str = None) -> Workflow: """ Update workflow metadata. :param user: :param workflow: :param new_data: :parma filename: :return: """ if is_write_permitted(user, workflow): if 'id' in new_data: if workflow.id != int(new_data['id']) and Workflow.query.filter_by( id=new_data['id']) is not None: raise ValueError( f'Workflow with id {new_data["id"]} already exists!') if 'analysis_ids' in new_data: new_analyses = [ get_analysis(user, analysis_id) for analysis_id in new_data['analysis_ids'] ] remove_analyses = [ analysis for analysis in workflow.analyses if analysis.id not in new_data['analysis_ids'] ] for analysis in new_analyses: if not is_write_permitted(user, analysis): raise AuthException( f'User {user.email} is not permitted to attach workflow {workflow.id} to analysis {analysis.id}' ) for analysis in remove_analyses: if not is_write_permitted(user, analysis): raise AuthException( f'User {user.email} is not permitted to detach workflow {workflow.id} from analysis {analysis.id}' ) workflow.analyses = new_analyses workflow.update(new_data) if 'workflow_definition' in new_data: if workflow.file_type == 'json': json.dump(new_data['workflow_definition'], open(workflow.filename, 'w+')) elif workflow.file_type == 'yaml': yaml.dump(new_data['workflow_definition'], open(workflow.filename, 'w+')) else: open(workflow.filename, 'w+').write(new_data['workflow_definition']) if filename is not None: os.remove(workflow.filename) shutil.copy(filename, workflow.filename) os.remove(filename) db.session.commit() return workflow raise AuthException( f'User {user.email} is not permitted to modify workflow {workflow.id}')
def update_external_file(user: User, external_file: ExternalFile, new_data: Dict[str, Any], move_file: bool = False, filename: str = None) -> ExternalFile: """ Update the data in the external file record :param user: :param external_file: :param new_data: :param move_file: :param filename: :return: """ if is_write_permitted(user, external_file): if 'id' in new_data: if external_file.id != int( new_data['id']) and ExternalFile.query.filter_by( id=new_data['id']) is not None: raise ValueError( f'External file with id {new_data["id"]} already exists!') if 'analysis_ids' in new_data: new_analyses = [ get_analysis(user, analysis_id) for analysis_id in new_data['analysis_ids'] ] remove_analyses = [ analysis for analysis in external_file.analyses if analysis.id not in new_data['analysis_ids'] ] for analysis in new_analyses: if not is_write_permitted(user, analysis): raise AuthException( f'User {user.email} is not permitted to attach external file {external_file.id} to analysis {analysis.id}' ) for analysis in remove_analyses: if not is_write_permitted(user, analysis): raise AuthException( f'User {user.email} is not permitted to detach external file {external_file.id} from analysis {analysis.id}' ) external_file.analyses = new_analyses if move_file and 'filename' in new_data: original_filename = external_file.filename shutil.copy(original_filename, new_data['filename']) os.remove(original_filename) if filename is not None: os.remove(external_file.filename) shutil.copy(filename, external_file.filename) os.remove(filename) external_file.update(new_data) external_file.last_editor = user db.session.commit() return external_file raise AuthException( f'User {user.id} is not permitted to modify external file record {external_file.id}' )
def download_external_file(user: User, external_file: ExternalFile) -> Dict[str, str]: if is_read_permitted(user, external_file): return {'filename': os.path.basename(external_file.filename)} raise AuthException( f'User {user.id} is not permitted to access external file {external_file.id}' )
def create_new_label_dataset(user: User, collection: Collection, name: str, data_type: str = 'string') -> Dict[str, str]: if is_write_permitted(user, collection): if re.match('^[^\d\W]\w*$', name): collection.create_label_column(name, data_type) return {'message': f'Created dataset {name} in collection {collection.id}.'} raise ValueError(f'Suggested name {name} is not valid.') raise AuthException(f'User {user.email} not permitted to modify collection {collection.id}.')
def create_collection(user: User, samples: List[Sample], data: Dict[str, Any], sort_by: str = 'base_sample_id') -> Collection: """ Create a new collection by concatenating samples. Collection metadata is set with new_data :param user: :param samples: :param data: Collection attributes :param sort_by: :return: """ data['owner_id'] = user.id data['creator_id'] = user.id if 'id' in data: # cannot create with specified id del data['id'] for sample in samples: if not is_read_permitted(user, sample): raise AuthException(f'User {user.id} is not permitted to access sample {sample.id}') filenames = [sample.filename for sample in samples] new_collection = Collection(owner=user, creator=user, last_editor=user, name=data['name']) db.session.add(new_collection) db.session.commit() new_collection.filename = f'{DATADIR}/collections/{new_collection.id}.h5' db.session.commit() if len(filenames): new_collection.merge_samples(samples, sort_by) else: new_collection.create_empty_file() update_collection(user, new_collection, data) return new_collection
def delete_collection(user: User, collection: Collection) -> Dict[str, str]: if is_write_permitted(user, collection): collection_id = collection.id db.session.delete(collection) db.session.commit() # event will handle file deletion return {'message': f'collection {collection_id} removed'} raise AuthException(f'User {user.email} is not permitted to modify collection {collection.id}')
def attach_collection(user: User, analysis: Analysis, collection: Collection) -> Dict[str, Any]: """ Add a collection to the list of collections belonging to an analysis :param user: :param analysis: :param collection: :return: """ # check read permissions on analysis and collection if is_read_permitted(user, collection) and is_write_permitted( user, analysis): if collection not in analysis.collections: analysis.collections.append(collection) db.session.commit() return { 'message': f'collection {collection.id} attached to analysis {analysis.id}' } return { 'message': f'Collection {collection.id} already attached to analysis {analysis.id}' } raise AuthException( f'User {user.email} is not permitted to attach collection {collection.id} ' f'to analysis {analysis.id}')
def delete_invitation(user: User, invitation: UserInvitation) -> Dict[str, str]: if user.admin: db.session.remove(invitation) db.session.commit() return {'message': f'Invitation {invitation.id} removed.'} raise AuthException( f'User {user.email} is not an administrator and cannot view or edit invitations.' )
def get_included_sample_groups(user: User, sample: Sample) -> List[SampleGroup]: """ Get a list of sample groups that a sample is found in :param user: :param sample: :return: """ if is_read_permitted(user, sample): return get_read_permitted_records(user, sample.sample_groups) raise AuthException(f'User {user.email} not permitted to view sample {sample.id}')
def get_sample_group_members(user: User, sample_group: SampleGroup) -> List[Sample]: """ Get a list of samples which belong to this group. :param user: :param sample_group: :return: """ if is_read_permitted(user, sample_group): return get_read_permitted_records(user, sample_group.samples) raise AuthException(f'User {user.email} not permitted to view sample group {sample_group.id}')
def list_collection_paths(user: User, collection: Collection) -> List[str]: """ List the paths corresponding to datasets in the collection :param user: :param collection: :return: """ if is_read_permitted(user, collection): return mdt.get_dataset_paths(collection.filename) raise AuthException(f'User {user.email} is not permitted to access collection {collection.id}')
def download_collection(user: User, collection: Collection) -> Dict[str, str]: """ If the user is permitted to read this collection, get the path to the collection, else throw. The file is sent via the send_from_directory flask method :param user: :param collection: :return: """ if is_read_permitted(user, collection): return {'filename': os.path.basename(collection.filename)} raise AuthException(f'User {user.email} is not permitted to access collection {collection.id}')
def sample_in_sample_group(user: User, sample: Sample, sample_group: SampleGroup) -> bool: """ Determine if a sample belongs to a sample group. NotFoundException, :param user: :param sample: :param sample_group: :return: """ if is_read_permitted(user, sample_group) and is_read_permitted(user, sample): return sample in sample_group.samples raise AuthException(f'User {user.email} not permitted to check the attachment of {sample.id} to sample group {sample_group.id}')
def validate_login(email: str, password: str) -> User: """ Authenticate a user :param email: :param password: password in plaintext :return: """ user = User.query.filter_by(email=email).first() if user is None or not user.check_password(password): raise AuthException('Invalid username/password.') return user
def list_sample_paths(user: User, sample: Sample) -> List[str]: """ List all the paths to datasets within the sample file :param user: :param sample: :return: """ if is_read_permitted(user, sample): return mdt.get_dataset_paths(sample.filename) raise AuthException( f'User {user.email} is not permitted to access sample {sample.id}')
def download_sample(user: User, sample: Sample) -> Dict[str, str]: """ If the user with user_id is permitted to access sample_id, present the filename for the sample with sample_id :param user: :param sample: :return: """ if is_read_permitted(user, sample): return {'filename': f'{sample.id}.h5'} raise AuthException( f'User {user.email} is not permitted to access sample {sample.id}')
def resume_job(user: User, job: Job) -> Dict[str, Any]: """ Release the hold on a job on the Cromwell job server. :param user: :param job: :return: """ job.refresh() if job.owner == user or user.admin: return job.resume() raise AuthException( f'User {user.email} is not authorized to resume job {job.id}')
def delete_sample_group(user: User, sample_group: SampleGroup) -> Dict[str, str]: """ Delete a sample group. :param user: :param sample_group: :return: """ if is_write_permitted(user, sample_group): db.session.delete(sample_group) db.session.commit() return {'message': f'User group {sample_group.id} deleted'} raise AuthException(f'User {user.email} not permitted to modify sample group {sample_group.id}')
def get_analysis(user: User, analysis_id: int) -> Analysis: """ Get analysis information :param user: :param analysis_id: :return: """ analysis = Analysis.query.filter_by(id=analysis_id).first() if is_read_permitted(user, analysis): return analysis raise AuthException( f'User {user.email} is not permitted to access analysis {analysis_id}')
def cancel_job(user: User, job: Job) -> Dict[str, Any]: """ Abort a running job on the Cromwell job server. :param user: :param job: :return: """ job.refresh() if job.owner == user or user.admin: return job.cancel() raise AuthException( f'User {user.email} is not authorized to cancel job {job.id}')
def get_attached_collections(user: User, analysis: Analysis) -> List[Collection]: """ Get all collections which belong to an analysis :param user: :param analysis: :return: """ if is_read_permitted(user, analysis): return analysis.collections raise AuthException( f'User {user.email} is not permitted to access analysis {analysis.id}')
def get_sample_group(user: User, group_id: int) -> SampleGroup: """ Get a sample group. :param user: :param group_id: :return: """ sample_group = SampleGroup.query.filter_by(id=group_id).first() if sample_group is None: raise NotFoundException(f'Sample group with id {group_id} not found.') if is_read_permitted(user, sample_group): return sample_group raise AuthException(f'User {user.email} is not authorized to view sample group {group_id}')
def update_collection(user: User, collection: Collection, new_data: Dict[str, Any], filename: str = None) -> Collection: """ Update collection attributes :param user: :param collection: :param new_data: :return: """ if is_write_permitted(user, collection): # file attributes and database attributes should be separated if 'id' in new_data: if collection.id != int(new_data['id']) and Collection.query.filter_by(id=new_data['id']) is not None: raise ValueError(f'Collection with id {new_data["id"]} already exists!') # verify write permissions on analyses to attach to or detach from if 'analysis_ids' in new_data: new_analyses = [get_analysis(user, analysis_id) for analysis_id in new_data['analysis_ids']] remove_analyses = [analysis for analysis in collection.analyses if analysis.id not in new_data['analysis_ids']] for analysis in new_analyses: if not is_write_permitted(user, analysis): raise AuthException(f'User {user.email} is not permitted to attach collection {collection.id} ' f'to analysis {analysis.id}') for analysis in remove_analyses: if not is_write_permitted(user, analysis): raise AuthException(f'User {user.email} is not permitted to detach collection {collection.id} ' f'from analysis {analysis.id}') collection.analyses = new_analyses collection.update(new_data) if filename is not None: os.remove(collection.filename) shutil.copy(filename, collection.filename) os.remove(filename) if 'file_info' in new_data: mdt.update_metadata(collection.filename, {key: value for key, value in new_data['file_info'].items()}) collection.last_editor = user db.session.commit() return collection raise AuthException(f'User {user.email} is not permitted to modify collection {collection.id}')
def download_collection_dataset(user: User, collection: Collection, path: str) -> Dict[str, str]: """ If the user is allowed to read a collection, get the contents required to send a file containing a dataset as CSV :param user: :param collection: :param path: :return: """ csv_filename = f'{os.path.basename(os.path.normpath(path))}.csv' if is_read_permitted(user, collection): return {'csv': mdt.get_csv(collection.filename, path), 'cd': f'attachment; filename={csv_filename}'} raise AuthException(f'User {user.email} is not permitted to access collection {collection.id}')
def delete_analysis(user: User, analysis: Analysis) -> Dict[str, str]: """ Remove the record associated with this analysis from the database :param user: :param analysis: :return: """ if is_write_permitted(user, analysis): db.session.delete(analysis) db.session.commit() return {'message': f'Analysis {analysis.id} deleted'} raise AuthException( f'User {user.email} is not permitted to modify analysis {analysis.id}')
def get_attached_analyses(user: User, collection: Collection) -> List[Analysis]: """ Get all analysis that a collection belongs to :param user: :param collection: :return: """ if is_read_permitted(user, collection): return get_all_read_permitted_records(user, collection.analyses) raise AuthException( f'User {user.email} not permitted to access collection {collection.id}' )
def delete_user_group(user: User, user_group: UserGroup) -> Dict[str, str]: """ Delete a user group. :param user: :param user_group: :return: """ if is_user_group_admin(user, user_group) or user.admin: user_group_id = user_group.id db.session.delete(user_group) db.session.commit() return {'message': f'User group {user_group_id} deleted.'} raise AuthException(f'User {user.email} not permitted to modify user group {user_group.id}')
def update_user_attachments(current_user: User, user_group: UserGroup, users: List[User]) -> UserGroup: """ Change which members belong to a user group. :param current_user: :param user_group: :param users: The only users of the user group after this is called. :return: """ if is_user_group_admin(current_user, user_group): user_group.members = users db.session.commit() return user_group raise AuthException(f'User {current_user.id} not authorized to modify group {user_group.id}')
def get_collection(user: User, collection_id: int) -> Collection: """ Get the attributes and dataset information of a collection :param user: :param collection_id: :return: """ collection = Collection.query.filter_by(id=collection_id).first() if collection is None: raise NotFoundException(f'No collection with id {collection_id}') if is_read_permitted(user, collection): return collection raise AuthException(f'User {user.email} is not authorized to view collection {collection.id}')