Skip to content
Snippets Groups Projects
pybis.py 180 KiB
Newer Older
  • Learn to ignore specific revisions
  •             self.__dict__[name] = value
            else:
                super(DataSet, self).__setattr__(name, value)
    
    
        @property
        def props(self):
            return self.__dict__['p']
    
    
        @property
        def type(self):
            return self.__dict__['type']
    
        @type.setter
        def type(self, type_name):
    
            dataset_type = self.openbis.get_dataset_type(type_name.upper())
            self.p.__dict__['_type'] = dataset_type
            self.a.__dict__['_type'] = dataset_type
    
        @property
        def physicalData(self):
            if 'physicalData' in self.data:
                return PhysicalData(self.data['physicalData'])
    
    
        @property
        def linkedData(self):
            if 'linkedData' in self.data:
                return LinkedData(self.data['linkedData'])
    
    
        @property
        def status(self):
            ds = self.openbis.get_dataset(self.permId)
            self.data['physicalData'] = ds.data['physicalData']
            try:
                return self.data['physicalData']['status']
            except Exception:
                return None
    
        def archive(self, remove_from_data_store=True):
    
                "removeFromDataStore": remove_from_data_store,
    
                "@type": "as.dto.dataset.archive.DataSetArchiveOptions"
    
            }
            self.archive_unarchive('archiveDataSets', fetchopts)
    
            if VERBOSE: print("DataSet {} archived".format(self.permId))
    
            fetchopts = {
                "@type": "as.dto.dataset.unarchive.DataSetUnarchiveOptions"
    
            }
            self.archive_unarchive('unarchiveDataSets', fetchopts)
    
            if VERBOSE: print("DataSet {} unarchived".format(self.permId))
    
    
        def archive_unarchive(self, method, fetchopts):
            dss = self.get_datastore
            payload = {}
    
            request = {
    
                "method": method,
                "params": [
                    self.openbis.token,
                    [{
                        "permId": self.permId,
                        "@type": "as.dto.dataset.id.DataSetPermId"
                    }],
                    dict(fetchopts)
                ],
    
            }
            resp = self.openbis._post_request(self._openbis.as_v3, request)
            return
    
    
        def set_properties(self, properties):
            self.openbis.update_dataset(self.permId, properties=properties)
    
        def download(self, files=None, destination=None, wait_until_finished=True, workers=10):
    
            """ download the actual files and put them by default in the following folder:
    
            __current_dir__/destination/dataset_permId/
    
            If no files are specified, all files of a given dataset are downloaded.
    
            If no destination is specified, the hostname is chosen instead.
    
            Files are usually downloaded in parallel, using 10 workers by default. If you want to wait until
            all the files are downloaded, set the wait_until_finished option to True.
    
                files = self.file_list
    
            if destination is None:
                destination = self.openbis.hostname
    
    
            base_url = self.data['dataStore']['downloadUrl'] + '/datastore_server/' + self.permId + '/'
    
            queue = DataSetDownloadQueue(workers=workers)
    
            # get file list and start download
    
                file_info = self.get_file_list(start_folder=filename)
                file_size = file_info[0]['fileSize']
    
                download_url = base_url + filename + '?sessionID=' + self.openbis.token
    
                filename_dest = os.path.join(destination, self.permId, filename)
                queue.put([download_url, filename_dest, file_size, self.openbis.verify_certificates])
    
    
            # wait until all files have downloaded
            if wait_until_finished:
                queue.join()
    
    
            if VERBOSE: print("Files downloaded to: %s" % os.path.join(destination, self.permId))
    
        @property
        def folder(self):
            return self.__dict__['folder']
    
    
    Swen Vermeul's avatar
    Swen Vermeul committed
        @property
    
            """returns the list of files including their directories as an array of strings. Just folders are not
            listed.
            """
    
            files = []
            for file in self.get_file_list(recursive=True):
                if file['isDirectory']:
                    pass
                else:
                    files.append(file['pathInDataSet'])
            return files
    
    
        def get_files(self, start_folder='/'):
    
            """Returns a DataFrame of all files in this dataset
    
    Swen Vermeul's avatar
    Swen Vermeul committed
            """
    
            def createRelativePath(pathInDataSet):
                if self.shareId is None:
                    return ''
                else:
                    return os.path.join(self.shareId, self.location, pathInDataSet)
    
    
            def signed_to_unsigned(sig_int):
                """openBIS delivers crc32 checksums as signed integers.
                If the number is negative, we just have to add 2**32
                We display the hex number to match with the classic UI
                """
                if sig_int < 0:
    
                    sig_int += 2 ** 32
                return "%x" % (sig_int & 0xFFFFFFFF)
    
    
            files = self.get_file_list(start_folder=start_folder)
    
    Swen Vermeul's avatar
    Swen Vermeul committed
            df = DataFrame(files)
            df['relativePath'] = df['pathInDataSet'].map(createRelativePath)
    
            df['crc32Checksum'] = df['crc32Checksum'].fillna(0.0).astype(int).map(signed_to_unsigned)
    
    Swen Vermeul's avatar
    Swen Vermeul committed
            return df[['isDirectory', 'pathInDataSet', 'fileSize', 'crc32Checksum']]
    
        def get_file_list(self, recursive=True, start_folder="/"):
    
            """Lists all files of a given dataset. You can specifiy a start_folder other than "/".
    
            By default, all directories and their containing files are listed recursively. You can
            turn off this option by setting recursive=False.
            """
    
                "method": "listFilesForDataSet",
                "params": [
    
                    self.openbis.token,
    
                    start_folder,
    
            resp = requests.post(
    
                self.data["dataStore"]["downloadUrl"] + '/datastore_server/rmi-dss-api-v1.json',
    
                verify=self.openbis.verify_certificates
            )
    
                    raise ValueError('Error from openBIS: ' + data['error']['message'])
    
                elif 'result' in data:
                    return data['result']
    
                    raise ValueError('request to openBIS did not return either result nor error')
    
                raise ValueError('internal error while performing post request')
    
    
        def _generate_plugin_request(self, dss):
    
            """generates a request to activate the dataset-uploader ingestion plugin to
    
            register our files as a new dataset
            """
    
            sample_identifier = None
            if self.sample is not None:
                sample_identifier = self.sample.identifier
    
            experiment_identifier = None
            if self.experiment is not None:
                experiment_identifier = self.experiment.identifier
    
    
            dataset_type = self.type.code
    
            properties = self.props.all_nonempty()
    
                "method": "createReportFromAggregationService",
                "params": [
    
                    self.openbis.token,
                    dss,
    
                        "method" : "insertDataSet",
                        "sampleIdentifier" : sample_identifier,
                        "experimentIdentifier" : experiment_identifier,
                        "dataSetType" : dataset_type,
                        "folderName" : self.folder,
                        "fileNames" : self.files,
                        "isZipDirectoryUpload" : False,
                        "properties" : properties,
                        "parentIdentifiers": parentIds
    
        def save(self):
            if self.is_new:
    
                if self.files is None or len(self.files) == 0:
                    raise ValueError('Cannot register a dataset without a file. Please provide at least one file')
    
                if self.sample is None and self.experiment is None:
                    raise ValueError('A DataSet must be either connected to a Sample or an Experiment')
    
                # upload the data to the user session workspace
                datastores = self.openbis.get_datastores()
    
                self.openbis.upload_files(
                    datastore_url= datastores['downloadUrl'][0],
                    files=self.files,
                    folder='',
                    wait_until_finished=True
                )
    
                # activate the ingestion plugin, as soon as the data is uploaded
                request = self._generate_plugin_request(dss=datastores['code'][0])
    
                resp = self.openbis._post_request(self.openbis.reg_v1, request)
    
                if resp['rows'][0][0]['value'] == 'OK':
    
                    permId = resp['rows'][0][2]['value']
                    if permId is None or permId == '': 
                        self.__dict__['is_new'] = False
    
                        if VERBOSE: print("DataSet successfully created. Because you connected to an openBIS version older than 16.05.04, you cannot update the object.")
    
                    else:
                        new_dataset_data = self.openbis.get_dataset(permId, only_data=True)
                        self._set_data(new_dataset_data)
    
                        if VERBOSE: print("DataSet successfully created.")
    
                else:
                    raise ValueError('Error while creating the DataSet: ' + resp['rows'][0][1]['value'])
    
                
    
            else:
                request = self._up_attrs()
                props = self.p._all_props()
                request["params"][1][0]["properties"] = props
                request["params"][1][0].pop('parentIds')
                request["params"][1][0].pop('childIds')
    
                self.openbis._post_request(self.openbis.as_v3, request)
    
                if VERBOSE: print("DataSet successfully updated.")
    
        """ General class for both samples and experiments that hold all common attributes, such as:
        - space
    
        - parents (sample, dataset)
        - children (sample, dataset)
    
        def __init__(self, openbis_obj, entity, type=None):
    
            self.__dict__['_openbis'] = openbis_obj
    
            self.__dict__['_entity'] = entity
    
            if type is not None:
    
                self.__dict__['_type'] = type.data
    
            self.__dict__['_allowed_attrs'] = _definitions(entity)['attrs']
    
            self.__dict__['_identifier'] = None
            self.__dict__['_is_new'] = True
    
            self.__dict__['_tags'] = []
    
            """This internal method is invoked when an existing object is loaded.
            Instead of invoking a special method we «call» the object with the data
               self(data)
            which automatically invokes this method.
            Since the data comes from openBIS, we do not have to check it (hence the
            self.__dict__ statements to prevent invoking the __setattr__ method)
            Internally data is stored with an underscore, e.g.
    
                sample._space = { 
                    '@type': 'as.dto.space.id.SpacePermId',
                    'permId': 'MATERIALS' 
                }
    
            but when fetching the attribute without the underscore, we only return
            the relevant data for the user:
    
            # entity is read from openBIS, so it is not new anymore
    
                if attr in ["code", "permId", "identifier",
                            "type", "container", "components"]:
                    self.__dict__['_' + attr] = data.get(attr, None)
    
                    # remove the @id attribute
                    if isinstance(self.__dict__['_' + attr], dict):
                        self.__dict__['_' + attr].pop('@id')
    
                elif attr in ["space"]:
    
                elif attr in ["sample", "experiment", "project"]:
    
                elif attr in ["parents", "children", "samples"]:
                    self.__dict__['_' + attr] = []
    
                        if 'identifier' in item:
    
                            self.__dict__['_' + attr].append(item['identifier'])
    
                        elif 'permId' in item:
    
                            self.__dict__['_' + attr].append(item['permId'])
    
                elif attr in ["tags"]:
    
                            "code": item['code'],
                            "@type": "as.dto.tag.id.TagCode"
                        })
    
                    self.__dict__['_tags'] = tags
                    self.__dict__['_prev_tags'] = copy.deepcopy(tags)
    
                    self.__dict__['_' + attr] = data.get(attr, None)
    
        def _new_attrs(self, method_name=None):
    
            """Returns the Python-equivalent JSON request when a new object is created.
            It is used internally by the save() method of a newly created object.
            """
    
            defs = _definitions(self.entity)
            attr2ids = _definitions('attr2ids')
    
            new_obj = {
    
                "@type": "as.dto.{}.create.{}Creation".format(self.entity.lower(), self.entity)
            }
    
    
            for attr in defs['attrs_new']:
                items = None
    
    
                if attr == 'type':
                    new_obj['typeId'] = self._type['permId']
                    continue
    
                elif attr == 'attachments':
    
                    attachments = getattr(self, '_new_attachments')
    
                    if attachments is None:
                        continue
    
                    atts_data = [attachment.get_data() for attachment in attachments]
    
                    items = atts_data
    
                elif attr in defs['multi']:
    
                    # parents, children, components, container, tags, attachments
    
                    items = getattr(self, '_' + attr)
    
                    if items is None:
                        items = []
    
    
                elif attr == 'userIds':
                    if '_changed_users' not in self.__dict__:
                        continue
    
                    new_obj[attr]=[]
                    for userId in self.__dict__['_changed_users']:
                        if self.__dict__['_changed_users'][userId]['action'] == 'Add':
                            new_obj[attr].append({
                                "permId": userId,
                                "@type": "as.dto.person.id.PersonPermId"
                            })
    
                elif attr == 'description':
                    new_obj[attr] = self.__dict__['_description'].get('value')
    
    
                    items = getattr(self, '_' + attr)
    
                    key = None
                    if attr in attr2ids:
                        # translate parents into parentIds, children into childIds etc.
                        key = attr2ids[attr]
                    else:
                        key = attr
    
            # guess the method name for creating a new entity and build the request
    
            if method_name is None:
                method_name = "create{}s".format(self.entity)
    
                "method": method_name,
    
                "params": [
                    self.openbis.token,
    
        def _up_attrs(self, method_name=None):
    
            """Returns the Python-equivalent JSON request when a new object is updated.
            It is used internally by the save() method of an object to be updated.
            """
    
            defs = _definitions(self._entity)
    
                "@type": "as.dto.{}.update.{}Update".format(self.entity.lower(), self.entity),
                defs["identifier"]: self._permId
            }
    
    Swen Vermeul's avatar
    Swen Vermeul committed
            # look at all attributes available for that entity
    
            # that can be updated
            for attr in defs['attrs_up']:
                items = None
    
    Swen Vermeul's avatar
    Swen Vermeul committed
    
                if attr == 'attachments':
                    # v3 API currently only supports adding attachments
                    attachments = self.__dict__.get('_new_attachments', None)
                    if attachments is None:
                        continue
    
                    atts_data = [attachment.get_data() for attachment in attachments]
    
    Swen Vermeul's avatar
    Swen Vermeul committed
    
    
                    up_obj['attachments'] = {
                        "actions": [{
                            "items": atts_data,
                            "@type": "as.dto.common.update.ListUpdateActionAdd"
                        }],
                        "@type": "as.dto.attachment.update.AttachmentListUpdateValue"
                    }
    
    Swen Vermeul's avatar
    Swen Vermeul committed
    
    
                    # look which tags/users have been added or removed and update them
    
                    if getattr(self, '_prev_'+attr) is None:
                        self.__dict__['_prev_'+attr] = []
    
                    for id in self.get('_prev_'+attr):
                        if id not in self.get('_'+attr):
    
                                "@type": "as.dto.common.update.ListUpdateActionRemove"
                            })
    
    
                    for id in self.get('_'+attr):
                        if id not in self.get('_prev_'+attr):
    
                                "@type": "as.dto.common.update.ListUpdateActionAdd"
                            })
    
                        "@type": "as.dto.common.update.IdListUpdateValue",
                        "actions": actions
                    }
    
    
                elif attr == 'userIds':
                    actions = []
                    if '_changed_users' not in self.__dict__:
                        continue
                    for userId in self.__dict__['_changed_users']:
                        actions.append({
    		        "items": [
                                {
                                    "permId": userId,
                                    "@type": "as.dto.person.id.PersonPermId"
    			    }
                            ],
    		        "@type": "as.dto.common.update.ListUpdateAction{}".format(
                                self.__dict__['_changed_users'][userId]['action']
                            )
    		    })
    
                    up_obj['userIds'] = {
                        "actions": actions,
                        "@type": "as.dto.common.update.IdListUpdateValue" 
                    }
    
    
                elif '_' + attr in self.__dict__:
    
                    # handle multivalue attributes (parents, children, tags etc.)
                    # we only cover the Set mechanism, which means we always update 
                    # all items in a list
                    if attr in defs['multi']:
    
                        items = self.__dict__.get('_' + attr, [])
    
                        if items == None:
                            items = []
                        up_obj[attr2ids[attr]] = {
                            "actions": [
                                {
                                    "items": items,
                                    "@type": "as.dto.common.update.ListUpdateActionSet",
                                }
                            ],
                            "@type": "as.dto.common.update.IdListUpdateValue"
                        }
    
                        # handle single attributes (space, experiment, project, container, etc.)
    
                        value = self.__dict__.get('_' + attr, {})
    
                        if value is None:
                            pass
    
                        elif len(value) == 0:
                            # value is {}: it means that we want this attribute to be
                            # deleted, not updated.
                            up_obj[attr2ids[attr]] = {
                                "@type": "as.dto.common.update.FieldUpdateValue",
                                "isModified": True,
                            }
                        elif 'isModified' in value and value['isModified'] == True:
                            val = {}
                            for x in ['identifier','permId','@type']:
                                if x in value:
                                    val[x] = value[x]
    
                            if attr in ['description']:
                                val = value['value']
    
                            up_obj[attr2ids[attr]] = {
    
                                "@type": "as.dto.common.update.FieldUpdateValue",
    
            # update an existing entity
            if method_name is None:
                method_name = "update{}s".format(self.entity)
    
                "method": method_name,
    
                "params": [
                    self.openbis.token,
    
            """ handles all attribute requests dynamically.
            Values are returned in a sensible way, for example:
                the identifiers of parents, children and components are returned as an
                array of values, whereas attachments, users (of groups) and
                roleAssignments are returned as an array of dictionaries.
    
            name_map = {
                'group': 'authorizationGroup',
                'roles': 'roleAssignments'
            }
            if name in name_map:
                name = name_map[name]
    
            int_name = '_' + name
            if int_name in self.__dict__:
    
                if int_name == '_attachments':
                    attachments = []
                    for att in self._attachments:
                        attachments.append({
                            "fileName":    att.get('fileName'),
                            "title":       att.get('title'),
                            "description": att.get('description'),
                            "version":     att.get('version'),
                        })
                    return attachments
    
                elif int_name == '_users':
                    users = []
                    for user in self._users:
                        users.append({
                            "firstName": user.get('firstName'),
                            "lastName" : user.get('lastName'),
                            "email"    : user.get('email'),
                            "userId"   : user.get('userId'),
    
                            "space"    : user.get('space').get('code') if user.get('space') is not None else None,
    
                        })
                    return users
    
                elif int_name == '_roleAssignments':
                    ras = []
                    for ra in self._roleAssignments:
                        ras.append({
    
                            "role":      ra.get('role'),
                            "roleLevel": ra.get('roleLevel'),
                            "space":     ra.get('space').get('code'),
                            "project":   ra.get('role'),
                        })
                    return ras
    
                elif int_name in ['_registrator', '_modifier', '_dataProducer']:
    
                    return self.__dict__[int_name].get('userId', None)
    
                elif int_name in ['_registrationDate', '_modificationDate', '_accessDate', '_dataProductionDate']:
    
                    return format_timestamp(self.__dict__[int_name])
    
                # if the attribute contains a list, 
                # return a list of either identifiers, codes or
                # permIds (whatever is available first)
    
                elif isinstance(self.__dict__[int_name], list):
                    values = []
                    for item in self.__dict__[int_name]:
    
                        if "identifier" in item:
                            values.append(item['identifier'])
                        elif "code" in item:
                            values.append(item['code'])
                        elif "permId" in item:
                            values.append(item['permId'])
                        else:
                            pass
    
                # attribute contains a dictionary: same procedure as above.
    
                elif isinstance(self.__dict__[int_name], dict):
    
                    if "identifier" in self.__dict__[int_name]:
                        return self.__dict__[int_name]['identifier']
                    elif "code" in self.__dict__[int_name]:
                        return self.__dict__[int_name]['code']
                    elif "permId" in self.__dict__[int_name]:
                        return self.__dict__[int_name]['permId']
    
                    elif "id" in self.__dict__[int_name]:
                        return self.__dict__[int_name]['id']
    
                else:
                    return self.__dict__[int_name]
            else:
                return None
    
            """This method is always invoked whenever we assign an attribute to an
            object, e.g.
                new_sample.space = 'MATERIALS'
                new_sample.parents = ['/MATERIALS/YEAST747']
            """
    
            #allowed_attrs = []
            #if self.is_new:
            #    allowed_attrs = _definitions(self.entity)['attrs_new']
            #else:
            #    allowed_attrs = _definitions(self.entity)['attrs_up']
    
            #if name not in allowed_attrs:
            #    raise ValueError("{} is not in the list of changeable attributes ({})".format(name, ", ".join(allowed_attrs) ) )
    
    
            if name in ["parents", "parent", "children", "child", "components"]:
                if name == "parent":
                    name = "parents"
                if name == "child":
                    name = "children"
    
    
                if not isinstance(value, list):
                    value = [value]
                objs = []
                for val in value:
    
                    if isinstance(val, str):
                        # fetch objects in openBIS, make sure they actually exists
                        obj = getattr(self._openbis, 'get_' + self._entity.lower())(val)
                        objs.append(obj)
                    elif getattr(val, '_permId'):
                        # we got an existing object
                        objs.append(val)
    
                permids = []
                for item in objs:
                    permid = item._permId
                    # remove any existing @id keys to prevent jackson parser errors
    
                    if '@id' in permid: permid.pop('@id')
    
                    permids.append(permid)
    
                self.__dict__['_' + name] = permids
    
            elif name in ["attachments"]:
                if isinstance(value, list):
                    for item in value:
                        if isinstance(item, dict):
                            self.add_attachment(**item)
                        else:
                            self.add_attachment(item)
    
                else:
                    self.add_attachment(value)
    
                obj = None
    
                if value is None:
                    self.__dict__['_'+name] = None
                    return
    
    
                if isinstance(value, str):
    
                    # fetch object in openBIS, make sure it actually exists
    
                    obj = getattr(self._openbis, "get_" + name)(value)
    
                else:
                    obj = value
    
                self.__dict__['_' + name] = obj.data['permId']
    
                # mark attribute as modified, if it's an existing entity
    
                if self.is_new:
    
                    self.__dict__['_' + name]['isModified'] = True
    
            elif name in ["sample", "experiment", "project"]:
                obj = None
                if isinstance(value, str):
                    # fetch object in openBIS, make sure it actually exists
                    obj = getattr(self._openbis, "get_" + name)(value)
                elif value is None:
                    self.__dict__['_'+name] = {}
                    return
                else:
                    obj = value
    
                self.__dict__['_' + name] = obj.data['identifier']
    
                # mark attribute as modified, if it's an existing entity
                if self.is_new:
                    pass
                else:
                    self.__dict__['_' + name]['isModified'] = True
    
    
            elif name in ["identifier"]:
    
                raise KeyError("you can not modify the {}".format(name))
            elif name == "code":
    
                    if self._type['autoGeneratedCode']:
                        raise KeyError("This {}Type has auto-generated code. You cannot set a code".format(self.entity))
    
    Swen Vermeul's avatar
    Swen Vermeul committed
                except KeyError:
    
                except TypeError:
                    pass
    
                self.__dict__['_code'] = value
    
    
            elif name in [ "description" ]:
                self.__dict__['_'+name] = {
                    "value": value,
                }
                if not self.is_new:
                    self.__dict__['_' + name]['isModified'] = True
                    
            elif name in ["userId"]:
                # values that are directly assigned
                self.__dict__['_' + name] = value
    
            elif name in ["userIds"]:
                self.add_users(value)
    
    
            else:
                raise KeyError("no such attribute: {}".format(name))
    
        def get_type(self):
    
            return self._type
    
        def _ident_for_whatever(self, whatever):
            if isinstance(whatever, str):
                # fetch parent in openBIS, we are given an identifier
                obj = getattr(self._openbis, 'get_'+self._entity.lower())(whatever)
            else:
                # we assume we got an object
                obj = whatever
    
            ident = None
            if getattr(obj, '_identifier'):
                ident = obj._identifier
            elif getattr(obj, '_permId'):
                ident = obj._permId
    
            if '@id' in ident: ident.pop('@id')
            return ident
    
    
    Swen Vermeul's avatar
    Swen Vermeul committed
        def get_parents(self, **kwargs):
    
            if identifier is None:
                identifier = self.permId
    
            if identifier is None:
                # TODO: if this is a new object, return the list of the parents which have been assigned
                pass
            else:
                return getattr(self._openbis, 'get_' + self._entity.lower() + 's')(withChildren=identifier, **kwargs)
    
        def add_parents(self, parents):
            if getattr(self, '_parents') is None:
                self.__dict__['_parents'] = []
            if not isinstance(parents, list):
                parents = [parents]
            for parent in parents:
                self.__dict__['_parents'].append(self._ident_for_whatever(parent))
    
        def del_parents(self, parents):
            if getattr(self, '_parents') is None:
                return
            if not isinstance(parents, list):
                parents = [parents]
            for parent in parents:
                ident = self._ident_for_whatever(parent)
                for i, item in enumerate(self.__dict__['_parents']):
                    if 'identifier' in ident and 'identifier' in item and ident['identifier'] == item['identifier']:
                        self.__dict__['_parents'].pop(i)
                    elif 'permId' in ident and 'permId' in item and ident['permId'] == item['permId']:
                        self.__dict__['_parents'].pop(i)
    
    
    Swen Vermeul's avatar
    Swen Vermeul committed
        def get_children(self, **kwargs):
    
            if identifier is None:
                identifier = self.permId
    
            if identifier is None:
                # TODO: if this is a new object, return the list of the children which have been assigned
                pass
            else:
                # e.g. self._openbis.get_samples(withParents=self.identifier)
                return getattr(self._openbis, 'get_' + self._entity.lower() + 's')(withParents=identifier, **kwargs)
    
        def add_children(self, children):
            if getattr(self, '_children') is None:
                self.__dict__['_children'] = []
            if not isinstance(children, list):
                children = [children]
            for child in children:
                self.__dict__['_children'].append(self._ident_for_whatever(child))
    
        def del_children(self, children):
            if getattr(self, '_children') is None:
                return
            if not isinstance(children, list):
                children = [children]
            for child in children:
                ident = self._ident_for_whatever(child)
                for i, item in enumerate(self.__dict__['_children']):
                    if 'identifier' in ident and 'identifier' in item and ident['identifier'] == item['identifier']:
                        self.__dict__['_children'].pop(i)
                    elif 'permId' in ident and 'permId' in item and ident['permId'] == item['permId']:
                        self.__dict__['_children'].pop(i)
    
    
        @property
        def tags(self):
            if getattr(self, '_tags') is not None:
    
                return [x['code'] for x in self._tags]
    
            if getattr(self, '_tags') is None:
                self.__dict__['_tags'] = []
    
            tagIds = _create_tagIds(tags)
    
            # remove tags that are not in the new tags list
            for tagId in self.__dict__['_tags']:
                if tagId not in tagIds:
                    self.__dict__['_tags'].remove(tagId)
    
            # add all new tags that are not in the list yet
            for tagId in tagIds:
                if tagId not in self.__dict__['_tags']:
    
                    self.__dict__['_tags'].append(tagId)
    
        def set_users(self, userIds):
            if userIds is None:
                return
            if getattr(self, '_userIds') is None:
                self.__dict__['_userIds'] = []
            if not isinstance(userIds, list):
                userIds = [userIds]
            for userId in userIds:
    
                person = self.openbis.get_person(userId=user, only_data=True)
    
                self.__dict__['_userIds'].append({
                    "permId": userId,
                    "@type": "as.dto.person.id.PersonPermId"
                })
    
            
        def add_users(self, userIds):
            if userIds is None:
                return
            if getattr(self, '_changed_users') is None:
                self.__dict__['_changed_users'] = {}
    
            if not isinstance(userIds, list):
                userIds = [userIds]
    
                self.__dict__['_changed_users'][userId] = {
                    "action": "Add"
                }
    
        def del_users(self, userIds):
            if userIds is None:
                return
            if getattr(self, '_changed_users') is None:
                self.__dict__['_changed_users'] = {}
    
            if not isinstance(userIds, list):
                userIds = [userIds]
    
                self.__dict__['_changed_users'][userId] = {
                    "action": "Remove"
                }
    
            if getattr(self, '_tags') is None:
                self.__dict__['_tags'] = []
    
            # add the new tags to the _tags and _new_tags list,
            # if not listed yet
            tagIds = _create_tagIds(tags)
            for tagId in tagIds:
                if not tagId in self.__dict__['_tags']:
    
                    self.__dict__['_tags'].append(tagId)
    
            if getattr(self, '_tags') is None:
                self.__dict__['_tags'] = []
    
            # remove the tags from the _tags and _del_tags list,
            # if listed there
            tagIds = _create_tagIds(tags)
            for tagId in tagIds:
                if tagId in self.__dict__['_tags']:
    
                    self.__dict__['_tags'].remove(tagId)
    
    Swen Vermeul's avatar
    Swen Vermeul committed
        def get_attachments(self):
            if getattr(self, '_attachments') is None:
                return None
            else:
    
                return DataFrame(self._attachments)[['fileName', 'title', 'description', 'version']]
    
    Swen Vermeul's avatar
    Swen Vermeul committed
    
    
        def add_attachment(self, fileName, title=None, description=None):
            att = Attachment(filename=fileName, title=title, description=description)
    
    Swen Vermeul's avatar
    Swen Vermeul committed
            if getattr(self, '_attachments') is None:
                self.__dict__['_attachments'] = []
            self._attachments.append(att.get_data_short())
    
            if getattr(self, '_new_attachments') is None:
                self.__dict__['_new_attachments'] = []
            self._new_attachments.append(att)
    
        def download_attachments(self):
    
            method = 'get' + self.entity + 's'
    
    Swen Vermeul's avatar
    Swen Vermeul committed
            entity = self.entity.lower()
            request = {
                "method": method,
    
                "params": [
                    self._openbis.token,
                    [self._permId],
                    dict(
                        attachments=fetch_option['attachmentsWithContent'],
                        **fetch_option[entity]
                    )
                ]
    
    Swen Vermeul's avatar
    Swen Vermeul committed
            }
            resp = self._openbis._post_request(self._openbis.as_v3, request)
            attachments = resp[self.permId]['attachments']
            file_list = []
            for attachment in attachments:
                filename = os.path.join(
    
    Swen Vermeul's avatar
    Swen Vermeul committed
                    self.permId,
                    attachment['fileName']
                )
                os.makedirs(os.path.dirname(filename), exist_ok=True)
                with open(filename, 'wb') as att:
                    content = base64.b64decode(attachment['content'])
    
    Swen Vermeul's avatar
    Swen Vermeul committed
                file_list.append(filename)
            return file_list
    
    
            def nvl(val, string=''):
                if val is None:
                    return string
                return val
    
    
            html = """
                <table border="1" class="dataframe">