From dce29113f3066e810f06075bae2c32244eb7de4f Mon Sep 17 00:00:00 2001
From: Swen Vermeul <swen@ethz.ch>
Date: Fri, 7 Oct 2016 01:51:59 +0200
Subject: [PATCH] added parse_jackson and many more methods to fetch and update
 experiments

---
 src/python/PyBis/pybis/pybis.py | 858 ++++++++++++++++++++++----------
 1 file changed, 595 insertions(+), 263 deletions(-)

diff --git a/src/python/PyBis/pybis/pybis.py b/src/python/PyBis/pybis/pybis.py
index 47d5df5f923..42da65c1701 100644
--- a/src/python/PyBis/pybis/pybis.py
+++ b/src/python/PyBis/pybis/pybis.py
@@ -20,6 +20,7 @@ import re
 from urllib.parse import urlparse
 import zlib
 
+
 import pandas as pd
 from pandas import DataFrame, Series
 
@@ -42,11 +43,10 @@ fetch_option = {
     "experiment":   { "@type": "as.dto.experiment.fetchoptions.ExperimentFetchOptions" },
     "sample":       { "@type": "as.dto.sample.fetchoptions.SampleFetchOptions" },
     "dataset":      { "@type": "as.dto.dataset.fetchoptions.DataSetFetchOptions" },
-
+    "propertyAssignments" : { "@type" : "as.dto.property.fetchoptions.PropertyAssignmentFetchOptions" }, 
     "physicalData": { "@type": "as.dto.dataset.fetchoptions.PhysicalDataFetchOptions" },
     "linkedData":   { "@type": "as.dto.dataset.fetchoptions.LinkedDataFetchOptions" },
 
-    "type":         { "@type": "as.dto.dataset.fetchoptions.DataSetTypeFetchOptions" },
 
     "properties":   { "@type": "as.dto.property.fetchoptions.PropertyFetchOptions" },
     "tags":         { "@type": "as.dto.tag.fetchoptions.TagFetchOptions" },
@@ -61,17 +61,114 @@ fetch_option = {
 }
 
 
+def parse_jackson(input_json):
+    """openBIS uses a library called 芦jackson禄 to automatically generate the JSON RPC output.
+       Objects that are found the first time are added an attribute 芦@id禄.
+       Any further findings only carry this reference id.
+       This function is used to dereference the output.
+    """
+    interesting=['tags', 'registrator', 'modifier', 'type', 'parents', 
+        'children', 'containers', 'properties', 'experiment', 'sample',
+        'project', 'space', 'propertyType'
+    ]
+    found = {} 
+    def build_cache(graph):
+        if isinstance(graph, list):
+            for item in graph:
+                build_cache(item)
+        elif isinstance(graph, dict) and len(graph) > 0:
+            for key, value in graph.items():
+                if key in interesting:
+                    if isinstance(value, dict):
+                        if '@id' in value:
+                            found[value['@id']] = value
+                        build_cache(value)
+                    elif isinstance(value, list):
+                        for item in value:
+                            if isinstance(item, dict):
+                                if '@id' in item:
+                                    found[item['@id']] = item
+                                build_cache(item)
+                elif isinstance(value, dict):
+                    build_cache(value)
+                elif isinstance(value, list):
+                    build_cache(value)
+                    
+    def deref_graph(graph):            
+        if isinstance(graph, list):
+            for item in graph:
+                deref_graph(item)
+        elif isinstance(graph, dict) and len(graph) > 0:
+            for key, value in graph.items():
+                if key in interesting:
+                    if isinstance(value, dict):
+                        deref_graph(value)
+                    elif isinstance(value, int):
+                        graph[key] = found[value]
+                    elif isinstance(value, list):
+                        for i, list_item in enumerate(value):
+                            if isinstance(list_item, int):
+                                value[i] = found[list_item]
+                elif isinstance(value, dict):
+                    deref_graph(value)
+                elif isinstance(value, list):
+                    deref_graph(value)
+
+    build_cache(input_json)
+    deref_graph(input_json)
+
+def search_request_for_identifier(identifier, entity_type):
+    
+        search_request = {}
+        # assume we got a sample identifier e.g. /TEST/TEST-SAMPLE
+        match = re.match('/', identifier)
+        if match:
+            search_request = {
+                "identifier": identifier.upper(),
+                "@type": "as.dto.{}.id.{}Identifier".format(entity_type.lower(), entity_type.capitalize())
+            }
+        else:
+            search_request = {
+                "permId": identifier,
+                "@type": "as.dto.{}.id.{}PermId".format(entity_type.lower(), entity_type.capitalize())
+            }
+        return search_request
+
+def table_for_attributes(attributes):
+    table = '<table border="1" class="dataframe"><thead><tr style="text-align: right;"> <th>attribute</th> <th>value</th> </tr> </thead><tbody>'
+
+    for key, val in attributes.items():
+        table += '<tr><th>{}</th><td>{}</td></tr>'.format(key, val)
+
+    table += '</tbody></table>'
+    return table
+
 def format_timestamp(ts):
     return datetime.fromtimestamp(round(ts/1000)).strftime('%Y-%m-%d %H:%M:%S')
 
 def extract_code(obj):
     return obj['code']
 
+def extract_deletion(obj):
+    del_objs = []
+    for deleted_object in obj['deletedObjects']:
+        del_objs.append({
+            "reason": obj['reason'],
+            "permId": deleted_object["id"]["permId"],
+            "type": deleted_object["id"]["@type"]
+        })
+    return del_objs
+
 def extract_identifier(ident):
     if not isinstance(ident, dict): 
         return str(ident)
     return ident['identifier']
 
+def extract_nested_identifier(ident):
+    if not isinstance(ident, dict): 
+        return str(ident)
+    return ident['identifier']['identifier']
+
 def extract_permid(permid):
     if not isinstance(permid, dict):
         return str(permid)
@@ -82,10 +179,15 @@ def extract_nested_permid(permid):
         return str(permid)
     return permid['permId']['permId']
 
-def extract_nested_identifier(ident):
-    if not isinstance(ident, dict): 
-        return str(ident)
-    return ident['identifier']['identifier']
+def extract_property_assignments(pas):
+    pa_strings = []
+    for pa in pas:
+        if not isinstance(pa['propertyType'], dict):
+            pa_strings.append(pa['propertyType'])
+        else:
+            pa_strings.append(pa['propertyType']['label'])
+    return pa_strings
+
 
 def extract_person(person):
     if not isinstance(person, dict):
@@ -103,6 +205,20 @@ def extract_properties(prop):
             props.append("%s: %s" % (key, prop[key]))
         return newline.join(props)
 
+def extract_tags(tags):
+    if isinstance(tags, dict):
+        tags = [tags]
+    new_tags = []
+    for tag in tags:
+        new_tags.append(tag["code"])
+    return new_tags
+
+def extract_attachments(attachments):
+    att = []
+    for attachment in attachments:
+        att.append(attachment['fileName'])
+    return att
+
 def crc32(fileName):
     prev = 0
     for eachLine in open(fileName,"rb"):
@@ -112,14 +228,37 @@ def crc32(fileName):
     #return "%X"%(prev & 0xFFFFFFFF)
 
 def _create_tagIds(tags=None):
-
     if tags is None:
         return None
-
     tagIds = []
     for tag in tags:
         tagIds.append({ "code": tag, "@type": "as.dto.tag.id.TagCode" })
+    return tagIds
 
+def _tagIds_for_tags(tags=None, action='Add'):
+    """creates an action item to add or remove tags. Action is either 'Add', 'Remove' or 'Set'
+    """
+    if tags is None:
+        return
+    if not isinstance(tags, list):
+        tags = [tags]
+
+    items = []
+    for tag in tags:
+        items.append({
+            "code": tag,
+            "@type": "as.dto.tag.id.TagCode"
+        })
+
+    tagIds = {
+        "actions": [
+            {
+                "items": items,
+                "@type": "as.dto.common.update.ListUpdateAction{}".format(action.capitalize())
+            }
+        ],
+        "@type": "as.dto.common.update.IdListUpdateValue"
+    }
     return tagIds
 
 
@@ -146,16 +285,97 @@ def _create_projectId(ident):
 
 def _criteria_for_code(code):
     return {
-        "fieldName": "code",
-        "fieldType": "ATTRIBUTE",
         "fieldValue": {
             "value": code,
             "@type": "as.dto.common.search.StringEqualToValue"
         },
         "@type": "as.dto.common.search.CodeSearchCriteria"
     }
-    
 
+def _subcriteria_for_type(code, entity_type):
+
+    return {
+        "@type": "as.dto.{}.search.{}TypeSearchCriteria".format(entity_type.lower(), entity_type),
+          "criteria": [
+            {
+              "@type": "as.dto.common.search.CodeSearchCriteria",
+              "fieldValue": {
+                "value": code.upper(),
+                "@type": "as.dto.common.search.StringEqualToValue"
+              }
+            }
+          ]
+    }
+
+def _subcriteria_for_tags(tags):
+    if not isinstance(tags, list):
+        tags = [tags]
+
+    criterias = []
+    for tag in tags:
+        criterias.append({
+            "fieldName": "code",
+            "fieldType": "ATTRIBUTE",
+            "fieldValue": {
+                "value": tag,
+                "@type": "as.dto.common.search.StringEqualToValue"
+            },
+            "@type": "as.dto.common.search.CodeSearchCriteria"
+        })
+
+    return {
+        "@type": "as.dto.tag.search.TagSearchCriteria",
+        "operator": "AND",
+        "criteria": criterias
+    }
+
+def _subcriteria_for_is_finished(is_finished):
+    return {
+        "@type": "as.dto.common.search.StringPropertySearchCriteria",
+        "fieldName": "FINISHED_FLAG",
+        "fieldType": "PROPERTY",
+        "fieldValue": {
+            "value": is_finished,
+            "@type": "as.dto.common.search.StringEqualToValue"
+        }
+    }
+
+def _subcriteria_for_properties(prop, val):
+    return {
+        "@type": "as.dto.common.search.StringPropertySearchCriteria",
+        "fieldName": prop.upper(),
+        "fieldType": "PROPERTY",
+        "fieldValue": {
+            "value": val,
+            "@type": "as.dto.common.search.StringEqualToValue"
+        }
+    }
+
+def _subcriteria_for_permid(permids, entity_type, parents_or_children='Parents'):
+
+    if not isinstance(permids, list):
+        permids = [permids]
+
+    criterias = []
+    for permid in permids:
+        criterias.append( {
+            "@type": "as.dto.common.search.PermIdSearchCriteria",
+            "fieldValue": {
+                "value": permid,
+                "@type": "as.dto.common.search.StringEqualToValue"
+            },
+            "fieldType": "ATTRIBUTE",
+            "fieldName": "code"
+        } )
+
+    criteria = {
+        "criteria": criterias,
+        "@type": "as.dto.sample.search.{}{}SearchCriteria".format(
+            entity_type, parents_or_children
+        ),
+        "operator": "OR"
+    }
+    return criteria
 
 def _subcriteria_for_code(code, object_type):
     criteria = {
@@ -176,8 +396,8 @@ def _subcriteria_for_code(code, object_type):
     return criteria
 
 class Openbis:
-    """Interface for communicating with openBIS. A current version of openBIS is needed (at least version 16.05).
-
+    """Interface for communicating with openBIS. A current version of openBIS is needed.
+    (minimum version 16.05).
     """
 
     def __init__(self, url='https://localhost:8443', verify_certificates=True, token=None):
@@ -431,7 +651,8 @@ class Openbis:
         return Space(self, resp[spaceId])
 
 
-    def get_samples(self, space=None, project=None, experiment=None, sample_type=None):
+    def get_samples(self, code=None, space=None, project=None, experiment=None, type=None,
+                    withParents=None, withChildren=None):
         """ Get a list of all samples for a given space/project/experiment (or any combination)
         """
 
@@ -441,8 +662,6 @@ class Openbis:
             project = self.default_project
         if experiment is None:
             experiment = self.default_experiment
-        if sample_type is None:
-            sample_type = self.default_sample_type
 
         sub_criteria = []
         if space:
@@ -455,8 +674,15 @@ class Openbis:
             sub_criteria.append(exp_crit)
         if experiment:
             sub_criteria.append(_subcriteria_for_code(experiment, 'experiment'))
-        if sample_type:
-            sub_criteria.append(_subcriteria_for_code(sample_type, 'sample_type'))
+        if type:
+            sub_criteria.append(_subcriteria_for_code(type, 'sample_type'))
+        if code:
+            sub_criteria.append(_criteria_for_code(code))
+        if withParents:
+            sub_criteria.append(_subcriteria_for_permid(withParents, 'Sample', 'Parents'))
+        if withChildren:
+            sub_criteria.append(_subcriteria_for_permid(withChildren, 'Sample', 'Children'))
+
 
         criteria = {
             "criteria": sub_criteria,
@@ -465,24 +691,12 @@ class Openbis:
         }
 
         options = {
-            "properties": {
-                "@type": "as.dto.property.fetchoptions.PropertyFetchOptions"
-            },
-            "tags": {
-                "@type": "as.dto.tag.fetchoptions.TagFetchOptions"
-            },
-            "registrator": {
-                "@type": "as.dto.person.fetchoptions.PersonFetchOptions"
-            },
-            "modifier": {
-                "@type": "as.dto.person.fetchoptions.PersonFetchOptions"
-            },
-            "experiment": {
-                "@type": "as.dto.experiment.fetchoptions.ExperimentFetchOptions"
-            },
-            "type": {
-                "@type": "as.dto.sample.fetchoptions.SampleTypeFetchOptions"
-            },
+            "properties": { "@type": "as.dto.property.fetchoptions.PropertyFetchOptions" },
+            "tags": { "@type": "as.dto.tag.fetchoptions.TagFetchOptions" },
+            "registrator": { "@type": "as.dto.person.fetchoptions.PersonFetchOptions" },
+            "modifier": { "@type": "as.dto.person.fetchoptions.PersonFetchOptions" },
+            "experiment": { "@type": "as.dto.experiment.fetchoptions.ExperimentFetchOptions" },
+            "type": { "@type": "as.dto.sample.fetchoptions.SampleTypeFetchOptions" },
             "@type": "as.dto.sample.fetchoptions.SampleFetchOptions",
         }
 
@@ -497,15 +711,7 @@ class Openbis:
         resp = self._post_request(self.as_v3, request)
         if resp is not None:
             objects = resp['objects']
-            cache = {}
-            for obj in objects:
-                for key in obj.keys():
-                    if key in ('modifier','registrator','project','experiment','space','type'):
-                        if isinstance(obj[key], dict):
-                            cache[ obj[key]['@id'] ] = obj[key]
-                        else:
-                            if obj[key] in cache:
-                                obj[key] = cache[ obj[key] ]
+            parse_jackson(objects)
 
             samples = DataFrame(objects)
             if len(samples) is 0:
@@ -519,11 +725,12 @@ class Openbis:
             samples['experiment'] = samples['experiment'].map(extract_nested_identifier)
             samples['sample_type'] = samples['type'].map(extract_nested_permid)
 
-            return samples[['code', 'identifier', 'experiment', 'sample_type', 'registrator', 'registrationDate', 'modifier', 'modificationDate']]
+            ss = samples[['code', 'identifier', 'experiment', 'sample_type', 'registrator', 'registrationDate', 'modifier', 'modificationDate']]
+            return Things(self, 'sample', ss, 'identifier')
         else:
             raise ValueError("No samples found!")
 
-    def get_experiments(self, code=None, space=None, project=None):
+    def get_experiments(self, code=None, type=None, space=None, project=None, tags=None, is_finished=None, **properties):
         """ Get a list of all experiment for a given space or project (or any combination)
         """
 
@@ -539,6 +746,15 @@ class Openbis:
             sub_criteria.append(_subcriteria_for_code(project, 'project'))
         if code:
             sub_criteria.append(_criteria_for_code(code))
+        if type:
+            sub_criteria.append(_subcriteria_for_type(type, 'Experiment'))
+        if tags:
+            sub_criteria.append(_subcriteria_for_tags(tags))
+        if is_finished is not None:
+            sub_criteria.append(_subcriteria_for_is_finished(is_finished))
+        if properties is not None:
+            for prop in properties:
+                sub_criteria.append(_subcriteria_for_properties(prop, properties[prop]))
 
         criteria = {
             "criteria": sub_criteria,
@@ -551,6 +767,7 @@ class Openbis:
             "registrator": { "@type": "as.dto.person.fetchoptions.PersonFetchOptions" },
             "modifier": { "@type": "as.dto.person.fetchoptions.PersonFetchOptions" },
             "project": { "@type": "as.dto.project.fetchoptions.ProjectFetchOptions" },
+            "type": { "@type": "as.dto.experiment.fetchoptions.ExperimentTypeFetchOptions" },
             "@type": "as.dto.experiment.fetchoptions.ExperimentFetchOptions" 
         }
 
@@ -562,53 +779,97 @@ class Openbis:
             ],
         }
         resp = self._post_request(self.as_v3, request)
+        if len(resp['objects']) == 0:
+            raise ValueError("No experiments found!")
+
+        objects = resp['objects']
+        parse_jackson(objects)
+
+        experiments = DataFrame(objects)
+        experiments['registrationDate']= experiments['registrationDate'].map(format_timestamp)
+        experiments['modificationDate']= experiments['modificationDate'].map(format_timestamp)
+        experiments['project']= experiments['project'].map(extract_code)
+        experiments['registrator'] = experiments['registrator'].map(extract_person)
+        experiments['modifier'] = experiments['modifier'].map(extract_person)
+        experiments['identifier'] = experiments['identifier'].map(extract_identifier)
+        experiments['type'] = experiments['type'].map(extract_code)
+
+        exps = experiments[['code', 'identifier', 'project', 'type', 'registrator', 
+            'registrationDate', 'modifier', 'modificationDate']]
+        return Things(self, 'experiment', exps, 'identifier')
+
+
+    def get_datasets(self, code=None, type=None, withParents=None, withChildren=None):
+
+        sub_criteria = []
+
+        if code:
+            sub_criteria.append(_criteria_for_code(code))
+        if type:
+            sub_criteria.append(_subcriteria_for_type(type, 'DataSet'))
+        if withParents:
+            sub_criteria.append(_subcriteria_for_permid(withParents, 'DataSet', 'Parents'))
+        if withChildren:
+            sub_criteria.append(_subcriteria_for_permid(withChildren, 'DataSet', 'Children'))
+
+        criteria = {
+            "criteria": sub_criteria,
+            "@type": "as.dto.dataset.search.DataSetSearchCriteria",
+            "operator": "AND"
+        }
+
+        fetchopts = {
+#            "parents":      { "@type": "as.dto.dataset.fetchoptions.DataSetFetchOptions" },
+#            "children":     { "@type": "as.dto.dataset.fetchoptions.DataSetFetchOptions" },
+            "containers":   { "@type": "as.dto.dataset.fetchoptions.DataSetFetchOptions" },
+            "type":         { "@type": "as.dto.dataset.fetchoptions.DataSetTypeFetchOptions" }
+        }
+
+        for option in ['tags', 'properties', 'sample']:
+            fetchopts[option] = fetch_option[option]
+
+        request = {
+            "method": "searchDataSets",
+            "params": [ self.token, 
+                criteria,
+                fetchopts,
+            ],
+        }
+        resp = self._post_request(self.as_v3, request)
         if resp is not None:
             objects = resp['objects']
-            cache = {}
-            for obj in objects:
-                for key in obj.keys():
-                    if key in ('modifier','registrator','project','experiement','space'):
-                        if isinstance(obj[key], dict):
-                            cache[ obj[key]['@id'] ] = obj[key]
-                        else:
-                            if obj[key] in cache:
-                                obj[key] = cache[ obj[key] ]
-
-            experiments = DataFrame(objects)
-            experiments['registrationDate']= experiments['registrationDate'].map(format_timestamp)
-            experiments['modificationDate']= experiments['modificationDate'].map(format_timestamp)
-            experiments['project']= experiments['project'].map(extract_code)
-            experiments['registrator'] = experiments['registrator'].map(extract_person)
-            experiments['modifier'] = experiments['modifier'].map(extract_person)
-            experiments['identifier'] = experiments['identifier'].map(extract_identifier)
-
-            exps = experiments[['code', 'identifier', 'project', 'registrator', 'registrationDate', 'modifier', 'modificationDate']]
-            return Things(self, 'experiment', exps, 'identifier')
-        else:
-            raise ValueError("No experiments found!")
+            parse_jackson(objects)
+            datasets = DataFrame(objects)
+            datasets['registrationDate']= datasets['registrationDate'].map(format_timestamp)
+            datasets['modificationDate']= datasets['modificationDate'].map(format_timestamp)
+            datasets['sample']= datasets['sample'].map(extract_nested_identifier)
+            datasets['type']= datasets['type'].map(extract_code)
+            ds = Things(
+                self,
+                'dataset',
+                datasets[['code', 'properties', 'type', 'sample', 'registrationDate', 'modificationDate']]
+            )
+            return ds
 
 
     def get_experiment(self, expId):
         """ Returns an experiment object for a given identifier (expId).
         """
 
-        fo = {
+        fetchopts = {
             "@type": "as.dto.experiment.fetchoptions.ExperimentFetchOptions"
         }
-        for option in ['tags', 'properties']:
-            fo[option] = fetch_option[option]
 
+        search_request = search_request_for_identifier(expId, 'experiment')
+        for option in ['tags', 'properties', 'attachments', 'project']:
+            fetchopts[option] = fetch_option[option]
 
-        expId = str(expId).upper()
         request = {
         "method": "getExperiments",
             "params": [ 
-            self.token,
-            [{ 
-                "identifier": expId,
-                "@type": "as.dto.experiment.id.ExperimentIdentifier"
-            }],
-            fo
+                self.token,
+                [ search_request ],
+                fetchopts
             ],
         } 
         resp = self._post_request(self.as_v3, request)
@@ -643,7 +904,82 @@ class Openbis:
             ],
         }
         resp = self._post_request(self.as_v3, request)
-        return resp[0]['permId']
+        return self.get_experiment(resp[0]['permId'])
+
+
+    def update_experiment(self, experimentId, properties=None, tagIds=None, attachments=None):
+        params = {
+            "experimentId": {
+                "permId": experimentId,
+                "@type": "as.dto.experiment.id.ExperimentPermId"
+            },
+            "@type": "as.dto.experiment.update.ExperimentUpdate"
+        }
+        if properties is not None:
+            params["properties"]= properties
+        if tagIds is not None:
+            params["tagIds"] = tagIds
+        if attachments is not None:
+            params["attachments"] = attachments
+
+        request = {
+            "method": "updateExperiments",
+            "params": [
+                self.token,
+                [ params ]
+            ]
+        }
+        self._post_request(self.as_v3, request)
+
+
+    def delete_entity(self, what, permid, reason):
+        """Deletes Spaces, Projects, Experiments, Samples and DataSets
+        """
+
+        entity_type = "as.dto.{}.id.{}PermId".format(what.lower(), what.capitalize())
+        request = {
+            "method": "delete" + what.capitalize()  + 's',
+            "params": [
+                self.token,
+                [
+                    {
+                        "permId": permid,
+                        "@type": entity_type
+                    }
+                ],
+                {
+                    "reason": reason,
+                    "@type": "as.dto.{}.delete.{}DeletionOptions".format(what.lower(), what.capitalize())
+                }
+            ]
+        }
+        self._post_request(self.as_v3, request)
+
+
+    def get_deletions(self):
+        request = {
+            "method": "searchDeletions",
+            "params": [
+                self.token,
+                {},
+                {
+                    "deletedObjects": {
+                        "@type": "as.dto.deletion.fetchoptions.DeletedObjectFetchOptions"
+                    }
+                }
+            ]
+        }
+        resp = self._post_request(self.as_v3, request)
+        objects = resp['objects']
+        parse_jackson(objects)
+
+        new_objs = [] 
+        for value in objects:
+            del_objs = extract_deletion(value)
+            if len(del_objs) > 0:
+                new_objs.append(*del_objs)
+
+        return DataFrame(new_objs)
 
 
     def get_projects(self, space=None):
@@ -682,15 +1018,7 @@ class Openbis:
         resp = self._post_request(self.as_v3, request)
         if resp is not None:
             objects = resp['objects']
-            cache = {}
-            for obj in objects:
-                for key in obj.keys():
-                    if key in ('registrator','modifier', 'experiment','space'):
-                        if isinstance(obj[key], dict):
-                            cache[ obj[key]['@id'] ] = obj[key]
-                        else:
-                            if obj[key] in cache:
-                                obj[key] = cache[ obj[key] ]
+            parse_jackson(objects)
 
             projects = DataFrame(objects)
             if len(projects) is 0:
@@ -778,48 +1106,86 @@ class Openbis:
     def get_sample_types(self):
         """ Returns a list of all available sample types
         """
-        return self.get_types_of("searchSampleTypes", ["generatedCodePrefix"])
+        return self._get_types_of("searchSampleTypes", "Sample", ["generatedCodePrefix"])
 
 
     def get_experiment_types(self):
         """ Returns a list of all available experiment types
         """
-        return self.get_types_of("searchExperimentTypes")
+        return self._get_types_of("searchExperimentTypes", "Experiment")
 
 
     def get_material_types(self):
         """ Returns a list of all available material types
         """
-        return self.get_types_of("searchMaterialTypes")
+        return self._get_types_of("searchMaterialTypes", "Material")
 
 
     def get_dataset_types(self):
         """ Returns a list (DataFrame object) of all currently available dataset types
         """
-        return self.get_types_of("searchDataSetTypes")
+        return self._get_types_of("searchDataSetTypes", "DataSet")
 
 
-    def get_file_types(self):
-        """ Returns a list (DataFrame object) of all currently available file types
+    def get_vocabulary(self):
+        """ Returns a DataFrame of all vocabulary terms available
         """
-        pass
-        #return self.get_types_of("searchFileTypes")
 
+        fetch_options = {
+            "vocabulary" : { "@type" : "as.dto.vocabulary.fetchoptions.VocabularyFetchOptions" },
+            "@type": "as.dto.vocabulary.fetchoptions.VocabularyTermFetchOptions"
+        }
 
-    def get_types_of(self, method_name, additional_attributes=[]):
+        request = {
+            "method": "searchVocabularyTerms",
+            "params": [ self.token, {}, fetch_options ]
+        }
+        resp = self._post_request(self.as_v3, request)
+        objects = DataFrame(resp['objects'])
+        objects['registrationDate'] = objects['registrationDate'].map(format_timestamp)
+        objects['modificationDate'] = objects['modificationDate'].map(format_timestamp)
+        return objects[['code', 'description', 'registrationDate', 'modificationDate']]
+
+
+    def get_tags(self):
+        """ Returns a DataFrame of all 
+        """
+        request = {
+            "method": "searchTags",
+            "params": [ self.token, {}, {} ]
+        }
+        resp = self._post_request(self.as_v3, request)
+        objects = DataFrame(resp['objects'])
+        objects['registrationDate'] = objects['registrationDate'].map(format_timestamp)
+        return objects[['code', 'registrationDate']]
+
+
+    def _get_types_of(self, method_name, entity_type, additional_attributes=[]):
         """ Returns a list of all available experiment types
         """
 
-        attributes = ['code', 'description', 'modificationDate', *additional_attributes]
+        attributes = ['code', 'description', *additional_attributes]
 
+        fetch_options = {}
+        if entity_type is not None:
+            fetch_options = {
+                "propertyAssignments" : {
+                    "@type" : "as.dto.property.fetchoptions.PropertyAssignmentFetchOptions"
+                },
+                "@type": "as.dto.{}.fetchoptions.{}TypeFetchOptions".format(entity_type.lower(), entity_type)
+            }
+            attributes.append('propertyAssignments')
+        
         request = {
             "method": method_name,
-            "params": [ self.token, {}, {} ],
+            "params": [ self.token, {}, fetch_options ],
         }
         resp = self._post_request(self.as_v3, request)
+        parse_jackson(resp)
         if len(resp['objects']) >= 1:
             types = DataFrame(resp['objects'])
-            types['modificationDate']= types['modificationDate'].map(format_timestamp)
+            if 'propertyAssignments' in fetch_options:
+                types['propertyAssignments'] = types['propertyAssignments'].map(extract_property_assignments)
             return types[attributes]
         else:
             raise ValueError("Nothing found!")
@@ -864,60 +1230,34 @@ class Openbis:
         :return: a DataSet object
         """
 
-        dataset_request = {
+        criteria = [{
+            "permId": permid,
+            "@type": "as.dto.dataset.id.DataSetPermId"
+        }]
+
+        fetchopts = {
+            "parents":      { "@type": "as.dto.dataset.fetchoptions.DataSetFetchOptions" },
+            "children":     { "@type": "as.dto.dataset.fetchoptions.DataSetFetchOptions" },
+            "containers":   { "@type": "as.dto.dataset.fetchoptions.DataSetFetchOptions" },
+            "@type":        "as.dto.dataset.fetchoptions.DataSetFetchOptions",
+        }
+
+        for option in ['tags', 'properties', 'dataStore', 'physicalData', 'linkedData', 
+                       'experiment', 'sample']:
+            fetchopts[option] = fetch_option[option]
+
+        request = {
             "method": "getDataSets",
-            "params": [
-                self.token,
-                [
-                    {
-                        "permId": permid,
-                        "@type": "as.dto.dataset.id.DataSetPermId"
-                    }
-                ],
-                {
-                "parents": {
-                    "@type": "as.dto.dataset.fetchoptions.DataSetFetchOptions"
-                },
-                "type": {
-                    "@type": "as.dto.dataset.fetchoptions.DataSetTypeFetchOptions"
-                },
-                "children": {
-                  "@type": "as.dto.dataset.fetchoptions.DataSetFetchOptions"
-                },
-                "containers": {
-                    "@type": "as.dto.dataset.fetchoptions.DataSetFetchOptions"
-                },
-                "physicalData": {
-                    "@type": "as.dto.dataset.fetchoptions.PhysicalDataFetchOptions"
-                },
-                "linkedData": {
-                    "@type": "as.dto.dataset.fetchoptions.LinkedDataFetchOptions",
-                },
-                "dataStore": {
-                    "@type": "as.dto.datastore.fetchoptions.DataStoreFetchOptions",
-                },
-                "experiment": {
-                    "@type": "as.dto.experiment.fetchoptions.ExperimentFetchOptions",
-                    "project": {
-                        "@type": "as.dto.project.fetchoptions.ProjectFetchOptions"
-                    },
-                },
-                "sample": {
-                    "@type": "as.dto.sample.fetchoptions.SampleFetchOptions"
-                },
-                "properties": {
-                    "@type": "as.dto.property.fetchoptions.PropertyFetchOptions"
-                },
-                "@type": "as.dto.dataset.fetchoptions.DataSetFetchOptions"
-                }
+            "params": [ self.token, 
+                criteria,
+                fetchopts,
             ],
         }
 
-        resp = self._post_request(self.as_v3, dataset_request)
+        resp = self._post_request(self.as_v3, request)
         if resp is not None:
             for permid in resp:
                 return DataSet(self, resp[permid])
-                #return DataSet(self, permid, resp[permid])
 
 
     def get_sample(self, sample_ident):
@@ -927,30 +1267,7 @@ class Openbis:
         :param sample_identifiers: A list of sample identifiers to retrieve.
         """
 
-        if self.token is None:
-            raise ValueError("Please login first")
-
-        search_request = None
-
-        # assume we got a sample identifier e.g. /TEST/TEST-SAMPLE
-        match = re.match('/', sample_ident)
-        if match:
-            search_request = {
-                "identifier": sample_ident,
-                "@type": "as.dto.sample.id.SampleIdentifier"
-            }
-        else:
-            # look if we got a PermID eg. 234567654345-123
-            match = re.match('\d+\-\d+', sample_ident)
-            if match:
-                search_request = {
-                    "permId": sample_ident,
-                    "@type": "as.dto.sample.id.SamplePermId"
-                }
-            else:
-                raise ValueError(
-                    '"' + sample_ident + '" is neither a Sample Identifier nor a PermID'
-                )
+        search_request = search_request_for_identifier(sample_ident, 'sample')
         fetch_options = {
             "type": {
                 "@type": "as.dto.sample.fetchoptions.SampleTypeFetchOptions"
@@ -985,14 +1302,14 @@ class Openbis:
             "method": "getSamples",
             "params": [
                 self.token,
-                [
-                    search_request, 
-                ],
+                [ search_request ],
                 fetch_options
             ],
         }
 
         resp = self._post_request(self.as_v3, sample_request)
+        parse_jackson(resp)
+
         if resp is None or len(resp) == 0:
             raise ValueError('no such sample found: '+sample_ident)
         else:
@@ -1000,29 +1317,6 @@ class Openbis:
                 return Sample(self, resp[sample_ident])
 
 
-    def delete_sample(self, permid, reason):
-        """ Deletes a given sample.
-        """
-        sample_delete_request = {
-            "method": "deleteSamples",
-            "params": [
-                self.token,
-                [
-                    {
-                        "permId": permid,
-                        "@type": "as.dto.sample.id.SamplePermId"
-                    }
-                ],
-                {
-                    "reason": reason,
-                    "@type": "as.dto.sample.delete.SampleDeletionOptions"
-                }
-            ],
-        }
-        resp = self._post_request(self.as_v3, sample_delete_request)
-        return
-
-
     def new_space(self, name, description=None):
         """ Creates a new space in the openBIS instance. Returns a list of all spaces
         """
@@ -1328,22 +1622,39 @@ class DataSetDownloadQueue:
             self.download_queue.task_done()
 
 
-class DataSet(dict):
+class DataSet():
     """ DataSet are openBIS objects that contain the actual files.
     """
 
-    def __init__(self, openbis_obj, *args, **kwargs):
-        super(DataSet, self).__init__(*args, **kwargs)
-        self.__dict__ = self
-        self.permid = self["permId"]["permId"]
+    def __init__(self, openbis_obj, data):
+        self.data = data
+        self.permid = data["code"]
+        self.permId = data["code"]
         self.openbis = openbis_obj
-        if self['physicalData'] is None:
+        if data['physicalData'] is None:
             self.shareId = None
             self.location = None
         else:
-            self.shareId = self['physicalData']['shareId']
-            self.location = self['physicalData']['location']
+            self.shareId = data['physicalData']['shareId']
+            self.location = data['physicalData']['location']
 
+    def _repr_html_(self):
+        html = """
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th>attribute</th>
+      <th>value</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr> <th>permId</th> <td>{}</td> </tr>
+    <tr> <th>properties</th> <td>{}</td> </tr>
+    <tr> <th>tags</th> <td>{}</td> </tr>
+  </tbody>
+</table>
+        """
+        return html.format(self.permid, self.data['properties'], self.data['tags'])
 
     def download(self, files=None, wait_until_finished=True, workers=10):
         """ download the actual files and put them by default in the following folder:
@@ -1358,7 +1669,7 @@ class DataSet(dict):
         elif isinstance(files, str):
             files = [files]
 
-        base_url = self['dataStore']['downloadUrl'] + '/datastore_server/' + self.permid + '/'
+        base_url = self.data['dataStore']['downloadUrl'] + '/datastore_server/' + self.permid + '/'
 
         queue = DataSetDownloadQueue(workers=workers)
 
@@ -1379,27 +1690,10 @@ class DataSet(dict):
 
 
     def get_parents(self):
-        """ Returns an array of the parents of the given dataset. Returns an empty array if no
-        parents were found.
-        """
-        parents = []
-        for item in self['parents']:
-            parent = self.openbis.get_dataset(item['code'])
-            if parent is not None:
-                parents.append(parent)
-        return parents
-
+        return self.openbis.get_datasets(withChildren=self.permid)
 
     def get_children(self):
-        """ Returns an array of the children of the given dataset. Returns an empty array if no
-        children were found.
-        """
-        children = []
-        for item in self['children']:
-            child = self.openbis.get_dataset(item['code'])
-            if child is not None:
-                children.append(child)
-        return children
+        return self.openbis.get_datasets(withParents=self.permid)
 
 
     def file_list(self):
@@ -1446,7 +1740,7 @@ class DataSet(dict):
         }
 
         resp = requests.post(
-            self["dataStore"]["downloadUrl"] + '/datastore_server/rmi-dss-api-v1.json',
+            self.data["dataStore"]["downloadUrl"] + '/datastore_server/rmi-dss-api-v1.json',
             json.dumps(request), 
             verify=self.openbis.verify_certificates
         )
@@ -1463,33 +1757,29 @@ class DataSet(dict):
             raise ValueError('internal error while performing post request')
 
 
-class Sample(dict):
+class Sample():
     """ A Sample is one of the most commonly used objects in openBIS.
     """
 
-    def __init__(self, openbis_obj, *args, **kwargs):
-        super(Sample, self).__init__(*args, **kwargs)
-        self.__dict__ = self
+    def __init__(self, openbis_obj, data):
         self.openbis = openbis_obj
-        self.permid = self.permId['permId']
-        self.ident = self.identifier['identifier']
+        self.data = data
+        self.permid = data['permId']['permId']
+        self.permId = data['permId']['permId']
+        self.identifier  = data['identifier']['identifier']
+        self.ident  = data['identifier']['identifier']
+        self.properties = data['properties']
+        self.tags = extract_tags(data['tags'])
 
 
     def delete(self, reason):
-        self.openbis.delete_sample(self.permid, reason)
+        self.openbis.delete_entity('sample', self.permId, reason)
 
 
     def get_datasets(self):
         objects = self.dataSets
-        cache = {}
-        for obj in objects:
-            for key in obj.keys():
-                if key in ('type'):
-                    if isinstance(obj[key], dict):
-                        cache[ obj[key]['@id'] ] = obj[key]
-                    else:
-                        if obj[key] in cache:
-                            obj[key] = cache[ obj[key] ]
+        parse_jackson(objects)
+
         datasets = DataFrame(objects)
         datasets['registrationDate'] = datasets['registrationDate'].map(format_timestamp)
         datasets['properties'] = datasets['properties'].map(extract_properties)
@@ -1499,21 +1789,10 @@ class Sample(dict):
 
 
     def get_parents(self):
-        parents = []
-        for item in self.parents:
-            parent = self.openbis.get_sample(item['permId']['permId'])
-            if parent is not None:
-                parents.append(parent)
-        return parents
-
+        return self.openbis.get_samples(withChildren=self.permId)
 
     def get_children(self):
-        children = []
-        for item in self.children:
-            child = self.openbis.get_sample(item['permId']['permId'])
-            if child is not None:
-                children.append(child)
-        return children
+        return self.openbis.get_samples(withParents=self.permId)
 
         
 class Space(dict):
@@ -1544,6 +1823,7 @@ class Space(dict):
         return self.openbis.get_experiments(space=self.code)
 
 
+
 class Things():
     """An object that contains a DataFrame object about an entity  available in openBIS.
        
@@ -1564,6 +1844,9 @@ class Things():
             if isinstance(key, int):
                 # get thing by rowid
                 row = self.df.loc[[key]]
+            elif isinstance(key, list):
+                # treat it as a normal dataframe
+                return self.df[key]
             else:
                 # get thing by code
                 row = self.df[self.df[self.identifier_name]==key.upper()]
@@ -1573,19 +1856,68 @@ class Things():
                 return getattr(self.openbis, 'get_'+self.what)(row[self.identifier_name].values[0])
 
 
-class Experiment(dict):
+class Experiment():
     """ managing openBIS experiments
     """
 
-    def __init__(self, openbis_obj, *args, **kwargs):
-        super(Experiment, self).__init__(*args, **kwargs)
-        self.__dict__ = self
+    def __init__(self, openbis_obj, data):
         self.openbis = openbis_obj
+        self.permid = data['permId']['permId']
+        self.permId = data['permId']['permId']
+        self.identifier  = data['identifier']['identifier']
+        self.properties = data['properties']
+        self.tags = extract_tags(data['tags'])
+        self.attachments = extract_attachments(data['attachments'])
+        self.project = data['project']['code']
+        self.data = data
+
+    def __setitem__(self, key, value):
+        self.openbis.update_experiment(self.permid, key, value)
+
+    def set_properties(self, properties):
+        self.openbis.update_experiment(self.permid, properties=properties)
+
+    def set_tags(self, tags):
+        tagIds = _tagIds_for_tags(tags, 'Set')
+        self.openbis.update_experiment(self.permid, tagIds=tagIds)
+
+    def add_tags(self, tags):
+        tagIds = _tagIds_for_tags(tags, 'Add')
+        self.openbis.update_experiment(self.permid, tagIds=tagIds)
+
+    def del_tags(self, tags):
+        tagIds = _tagIds_for_tags(tags, 'Remove')
+        self.openbis.update_experiment(self.permid, tagIds=tagIds)
+
+    def delete(self, reason):
+        self.openbis.delete_entity('experiment', self.permid, reason)
+
+    def _repr_html_(self):
+        html = """
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th>attribute</th>
+      <th>value</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr> <th>permId</th> <td>{}</td> </tr>
+    <tr> <th>identifier</th> <td>{}</td> </tr>
+    <tr> <th>project</th> <td>{}</td> </tr>
+    <tr> <th>properties</th> <td>{}</td> </tr>
+    <tr> <th>tags</th> <td>{}</td> </tr>
+    <tr> <th>attachments</th> <td>{}</td> </tr>
+  </tbody>
+</table>
+        """
+        return html.format(self.permid, self.identifier, self.project, self.properties, self.tags, self.attachments)
 
     def __repr__(self):
         data = {}
-        data["identifier"] = self['identifier']['identifier']
-        data["permId"] = self['permId']['permId']
-        data["properties"] = self['properties']
+        data["identifier"] = self.identifier
+        data["permid"] = self.permid
+        data["properties"] = self.properties
+        data["tags"] = self.tags
         return repr(data)
 
-- 
GitLab