diff --git a/src/python/PyBis/pybis/pybis.py b/src/python/PyBis/pybis/pybis.py index 99f8ac05c2778c0239b5e60e0ba98758ab12153c..a963cc0a02c449d468ebc33cb543821015daa48a 100644 --- a/src/python/PyBis/pybis/pybis.py +++ b/src/python/PyBis/pybis/pybis.py @@ -44,6 +44,15 @@ from datetime import datetime PYBIS_PLUGIN = "dataset-uploader-api" +# display messages when in a interactive context (IPython or Jupyter) +try: + get_ipython() +except Exception: + VERBOSE = False +else: + VERBOSE = True + + def _definitions(entity): entities = { @@ -162,7 +171,7 @@ def _definitions(entity): return entities[entity] -def get_search_type_for_entity(entity): +def get_search_type_for_entity(entity, operator=None): """ Returns a dictionary containing the correct search criteria type for a given entity. @@ -190,15 +199,18 @@ def get_search_type_for_entity(entity): "vocabulary_term": "as.dto.vocabulary.search.VocabularyTermSearchCriteria", "tag": "as.dto.tag.search.TagSearchCriteria", "authorizationGroup": "as.dto.authorizationgroup.search.AuthorizationGroupSearchCriteria", - "role_assignment": "as.dto.roleassignment.search.RoleAssignmentSearchCriteria", + "roleAssignment": "as.dto.roleassignment.search.RoleAssignmentSearchCriteria", "person": "as.dto.person.search.PersonSearchCriteria", "code": "as.dto.common.search.CodeSearchCriteria", "sample_type": "as.dto.sample.search.SampleTypeSearchCriteria", "global": "as.dto.global.GlobalSearchObject", } - return { - "@type": search_criteria[entity] - } + + sc = { "@type": search_criteria[entity] } + if operator is not None: + sc["operator"] = operator + + return sc def get_attrs_for_entity(entity): """ For a given entity this method returns an iterator for all searchable @@ -216,6 +228,8 @@ fetch_option = { "project": {"@type": "as.dto.project.fetchoptions.ProjectFetchOptions"}, "person": {"@type": "as.dto.person.fetchoptions.PersonFetchOptions"}, "users": {"@type": "as.dto.person.fetchoptions.PersonFetchOptions" }, + "user": {"@type": "as.dto.person.fetchoptions.PersonFetchOptions" }, + "authorizationGroup": {"@type": "as.dto.authorizationgroup.fetchoptions.AuthorizationGroupFetchOptions"}, "experiment": { "@type": "as.dto.experiment.fetchoptions.ExperimentFetchOptions", "type": {"@type": "as.dto.experiment.fetchoptions.ExperimentTypeFetchOptions"} @@ -266,7 +280,6 @@ fetch_option = { }, "history": {"@type": "as.dto.history.fetchoptions.HistoryEntryFetchOptions"}, "dataStore": {"@type": "as.dto.datastore.fetchoptions.DataStoreFetchOptions"}, - "authorizationGroup": {"@type": "as.dto.authorizationgroup.fetchoptions.AuthorizationGroupFetchOptions"}, } @@ -368,10 +381,30 @@ def extract_person(person): return str(person) return person['userId'] - -def extract_users(users): - if not isinstance(users, dict): - return str(users) +def extract_person_details(person): + if not isinstance(person, dict): + return str(person) + return "{} {} <{}>".format( + person['firstName'], + person['lastName'], + person['email'] + ) + +def extract_id(id): + if not isinstance(id, dict): + return str(id) + else: + return id['techId'] + +def extract_userId(user): + if isinstance(user, list): + return ", ".join([ + u['userId'] for u in user + ]) + elif isinstance(user, dict): + return user['userId'] + else: + return str(user) def crc32(fileName): @@ -502,6 +535,23 @@ def _criteria_for_code(code): "@type": "as.dto.common.search.CodeSearchCriteria" } +def _subcriteria_for_userId(userId): + return { + "criteria": [ + { + "fieldName": "userId", + "fieldType": "ATTRIBUTE", + "fieldValue": { + "value": userId, + "@type": "as.dto.common.search.StringEqualToValue" + }, + "@type": "as.dto.person.search.UserIdSearchCriteria" + } + ], + "@type": "as.dto.person.search.PersonSearchCriteria", + "operator": "AND" + } + def _subcriteria_for_type(code, entity): return { @@ -631,7 +681,7 @@ def _subcriteria_for_properties(prop, val): } -def _subcriteria_for_permid(permids, entity, parents_or_children=''): +def _subcriteria_for_permid(permids, entity, parents_or_children='', operator='AND'): if not isinstance(permids, list): permids = [permids] @@ -652,7 +702,7 @@ def _subcriteria_for_permid(permids, entity, parents_or_children=''): "@type": "as.dto.{}.search.{}{}SearchCriteria".format( entity.lower(), entity, parents_or_children ), - "operator": "OR" + "operator": operator } return criteria @@ -721,6 +771,7 @@ class Openbis: if url_obj.hostname is None: raise ValueError("hostname is missing") + self.url = url_obj.geturl() self.port = url_obj.port self.hostname = url_obj.hostname @@ -777,6 +828,7 @@ class Openbis: "get_person(userId)", "get_groups()", "get_group(code)", + "get_assigned_roles()", "new_group(code, description, userIds)", 'new_space(name, description)', 'new_project(space, code, description, attachments)', @@ -882,7 +934,7 @@ class Openbis: if resp.ok: resp = resp.json() if 'error' in resp: - print(json.dumps(request)) + if VERBOSE: print(json.dumps(request)) raise ValueError(resp['error']['message']) elif 'result' in resp: return resp['result'] @@ -1013,8 +1065,79 @@ class Openbis: return group else: return Group(self, data=group) - + def get_assigned_roles(self, **search_args): + """ Get the assigned roles for a given group, person or space + """ + search_criteria = get_search_type_for_entity('roleAssignment', 'AND') + allowed_search_attrs = ['role', 'roleLevel', 'user', 'group', 'space'] + + sub_crit = [] + for attr in search_args: + if attr in allowed_search_attrs: + if attr == 'space': + sub_crit.append( + _subcriteria_for_code(search_args[attr], 'space') + ) + elif attr == 'user': + userId = '' + if isinstance(search_args[attr], str): + userId = search_args[attr] + else: + userId = search_args[attr].userId + + sub_crit.append( + _subcriteria_for_userId(userId) + ) + elif attr == 'group': + sub_crit.append( + _subcriteria_for_permid(search_args[attr], 'AuthorizationGroup') + ) + elif attr == 'role': + # TODO + raise ValueError("not yet implemented") + elif attr == 'roleLevel': + # TODO + raise ValueError("not yet implemented") + else: + pass + else: + raise ValueError("unknown search argument {}".format(search_arg)) + + search_criteria['criteria'] = sub_crit + + fetchopts = {} + for option in ['roleAssignments', 'space', 'user', 'authorizationGroup','registrator']: + fetchopts[option] = fetch_option[option] + + request = { + "method": "searchRoleAssignments", + "params": [ + self.token, + search_criteria, + fetchopts + ] + } + + resp = self._post_request(self.as_v3, request) + if len(resp['objects']) == 0: + raise ValueError("No assigned roles found!") + + objects = resp['objects'] + parse_jackson(objects) + roles = DataFrame(objects) + + roles['id'] = roles['id'].map(extract_id) + roles['user'] = roles['user'].map(extract_userId) + roles['group'] = roles['authorizationGroup'].map(extract_code) + roles['space'] = roles['space'].map(extract_code) + p = Things( + self, entity='role', + df=roles[['id', 'role', 'roleLevel', 'user', 'group', 'space']], + identifier_name='permId' + ) + return p + def get_groups(self, **search_args): """ Get openBIS AuthorizationGroups. Returns a «Things» object. @@ -1040,6 +1163,7 @@ class Openbis: search_criteria = get_search_type_for_entity('authorizationGroup') search_criteria['criteria'] = criteria + search_criteria['operator'] = 'AND' fetchopts = fetch_option['authorizationGroup'] for option in ['roleAssignments', 'registrator', 'users']: @@ -1059,11 +1183,11 @@ class Openbis: objects = resp['objects'] parse_jackson(objects) - groups = DataFrame(resp['objects']) + groups = DataFrame(objects) groups['permId'] = groups['permId'].map(extract_permid) groups['registrator'] = groups['registrator'].map(extract_person) - groups['users'] = groups['users'].map(extract_users) + groups['users'] = groups['users'].map(extract_userId) groups['registrationDate'] = groups['registrationDate'].map(format_timestamp) groups['modificationDate'] = groups['modificationDate'].map(format_timestamp) p = Things( @@ -2772,14 +2896,14 @@ class DataSet(OpenBisObject): "@type": "as.dto.dataset.archive.DataSetArchiveOptions" } self.archive_unarchive('archiveDataSets', fetchopts) - print("DataSet {} archived".format(self.permId)) + if VERBOSE: print("DataSet {} archived".format(self.permId)) def unarchive(self): fetchopts = { "@type": "as.dto.dataset.unarchive.DataSetUnarchiveOptions" } self.archive_unarchive('unarchiveDataSets', fetchopts) - print("DataSet {} unarchived".format(self.permId)) + if VERBOSE: print("DataSet {} unarchived".format(self.permId)) def archive_unarchive(self, method, fetchopts): dss = self.get_datastore @@ -2835,7 +2959,7 @@ class DataSet(OpenBisObject): if wait_until_finished: queue.join() - print("Files downloaded to: %s" % os.path.join(destination, self.permId)) + if VERBOSE: print("Files downloaded to: %s" % os.path.join(destination, self.permId)) @property def folder(self): @@ -2980,11 +3104,11 @@ class DataSet(OpenBisObject): permId = resp['rows'][0][2]['value'] if permId is None or permId == '': self.__dict__['is_new'] = False - print("DataSet successfully created. Because you connected to an openBIS version older than 16.05.04, you cannot update the object.") + 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) - print("DataSet successfully created.") + if VERBOSE: print("DataSet successfully created.") else: raise ValueError('Error while creating the DataSet: ' + resp['rows'][0][1]['value']) @@ -2999,7 +3123,7 @@ class DataSet(OpenBisObject): request["params"][1][0].pop('childIds') self.openbis._post_request(self.openbis.as_v3, request) - print("DataSet successfully updated.") + if VERBOSE: print("DataSet successfully updated.") class AttrHolder(): @@ -3245,25 +3369,54 @@ class AttrHolder(): def __getattr__(self, name): - """ 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. + """ 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. """ int_name = '_' + name if int_name in self.__dict__: - if int_name in ['_attachments']: - return [ - { - "fileName": x['fileName'], - "title": x['title'], - "description": x['description'] - } for x in self._attachments - ] - if int_name in ['_registrator', '_modifier', '_dataProducer']: + 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'), + }) + 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) @@ -3279,6 +3432,7 @@ class AttrHolder(): else: pass return values + # attribute contains a dictionary: same procedure as above. elif isinstance(self.__dict__[int_name], dict): if "identifier" in self.__dict__[int_name]: @@ -3287,6 +3441,7 @@ class AttrHolder(): return self.__dict__[int_name]['code'] elif "permId" in self.__dict__[int_name]: return self.__dict__[int_name]['permId'] + else: return self.__dict__[int_name] else: @@ -3608,16 +3763,41 @@ class AttrHolder(): return html def __repr__(self): + """ When using iPython, this method displays a nice table + of all attributes and their values when the object is printed. + """ headers = ['attribute', 'value'] lines = [] for attr in self._allowed_attrs: if attr == 'attachments': continue - lines.append([ - attr, - nvl(getattr(self, attr, '')) - ]) + elif attr == 'users': + lines.append([ + attr, + ", ".join(att['userId'] for att in self._users) + ]) + elif attr == 'roleAssignments': + roles = [] + for role in self._roleAssignments: + if role.get('space') is not None: + roles.append("{} ({})".format( + role.get('role'), + role.get('space').get('code') + )) + else: + roles.append(role.get('role')) + + lines.append([ + attr, + ", ".join(roles) + ]) + + else: + lines.append([ + attr, + nvl(getattr(self, attr, '')) + ]) return tabulate(lines, headers=headers) @@ -3703,7 +3883,7 @@ class Sample(): request["params"][1][0]["properties"] = props resp = self.openbis._post_request(self.openbis.as_v3, request) - print("Sample successfully created.") + if VERBOSE: print("Sample successfully created.") new_sample_data = self.openbis.get_sample(resp[0]['permId'], only_data=True) self._set_data(new_sample_data) return self @@ -3712,7 +3892,7 @@ class Sample(): request = self._up_attrs() request["params"][1][0]["properties"] = props self.openbis._post_request(self.openbis.as_v3, request) - print("Sample successfully updated.") + if VERBOSE: print("Sample successfully updated.") new_sample_data = self.openbis.get_sample(self.permId, only_data=True) self._set_data(new_sample_data) @@ -3762,9 +3942,27 @@ class Person(OpenBisObject): """ return [ 'permId', 'userId', 'firstName', 'lastName', 'email', - 'registrator', 'registrationDate','roleAssignments','space' + 'registrator', 'registrationDate','roleAssignments','space', + 'get_roles()', 'assign_role()', 'revoke_role()', ] + def get_roles(self): + return self.openbis.get_roles(person=self) + + def assign_role(self, role): + self.openbis.assign_role(person=self, role=role) + if VERBOSE: + print( + "Role {} successfully assigned to person {}".format(role, self.userId) + ) + + def revoke_role(self, role): + self.openbis.revoke_role(person=self, role=role) + if VERBOSE: + print( + "Role {} successfully revoked from person {}".format(role, self.userId) + ) + def __str__(self): return "{} {}".format(self.get('firstName'), self.get('lastName')) @@ -3781,7 +3979,7 @@ class Person(OpenBisObject): request['params'][1][0]['homeSpaceId'] = request['params'][1][0]['spaceId'] del(request['params'][1][0]['spaceId']) resp = self.openbis._post_request(self.openbis.as_v3, request) - print("Person successfully created.") + if VERBOSE: print("Person successfully created.") new_person_data = self.openbis.get_person(resp[0]['permId'], only_data=True) self._set_data(new_person_data) return self @@ -3797,7 +3995,7 @@ class Person(OpenBisObject): return json.dumps(request) self.openbis._post_request(self.openbis.as_v3, request) - print("Person successfully updated.") + if VERBOSE: print("Person successfully updated.") new_person_data = self.openbis.get_person(self.permId, only_data=True) self._set_data(new_person_data) @@ -3820,11 +4018,31 @@ class Group(OpenBisObject): def __dir__(self): return [ - 'get_users()', 'add_users()', 'del_users()', - 'get_roles()', 'add_roles()', 'del_roles()' + 'code','description','users','roleAssignments', + 'get_users()', 'set_users()', 'add_users()', 'del_users()', + 'get_roles()', 'assgin_role()', 'revoke_role()' ] + def get_roles(self): + return self.openbis.get_roles(group=self) + + def assign_role(self, role): + self.openbis.assign_role(group=self, role=role) + if VERBOSE: + print( + "Role {} successfully assigned to group {}".format(role, self.code) + ) + + def revoke_role(self, role): + self.openbis.revoke_role(group=self, role=role) + if VERBOSE: + print( + "Role {} successfully revoked from group {}".format(role, self.code) + ) + def _repr_html_(self): + """ creates a nice table in Jupyter notebooks when the object itself displayed + """ def nvl(val, string=''): if val is None: return string @@ -3889,7 +4107,7 @@ class Group(OpenBisObject): if self.is_new: request = self._new_attrs() resp = self.openbis._post_request(self.openbis.as_v3, request) - print("Group successfully created.") + if VERBOSE: print("Group successfully created.") # re-fetch group from openBIS new_data = self.openbis.get_person(resp[0]['permId'], only_data=True) self._set_data(new_data) @@ -3898,7 +4116,7 @@ class Group(OpenBisObject): else: request = self._up_attrs() self.openbis._post_request(self.openbis.as_v3, request) - print("Group successfully updated.") + if VERBOSE: print("Group successfully updated.") # re-fetch group from openBIS new_data = self.openbis.get_group(self.permId, only_data=True) self._set_data(new_data) @@ -3959,13 +4177,13 @@ class Space(OpenBisObject): def delete(self, reason): self.openbis.delete_entity('Space', self.permId, reason) - print("Space {} has been sucessfully deleted.".format(self.permId)) + if VERBOSE: print("Space {} has been sucessfully deleted.".format(self.permId)) def save(self): if self.is_new: request = self._new_attrs() resp = self.openbis._post_request(self.openbis.as_v3, request) - print("Space successfully created.") + if VERBOSE: print("Space successfully created.") new_space_data = self.openbis.get_space(resp[0]['permId'], only_data=True) self._set_data(new_space_data) return self @@ -3973,7 +4191,7 @@ class Space(OpenBisObject): else: request = self._up_attrs() self.openbis._post_request(self.openbis.as_v3, request) - print("Space successfully updated.") + if VERBOSE: print("Space successfully updated.") new_space_data = self.openbis.get_space(self.permId, only_data=True) self._set_data(new_space_data) @@ -4225,7 +4443,7 @@ class Experiment(OpenBisObject): request["params"][1][0]["properties"] = props resp = self.openbis._post_request(self.openbis.as_v3, request) - print("Experiment successfully created.") + if VERBOSE: print("Experiment successfully created.") new_exp_data = self.openbis.get_experiment(resp[0]['permId'], only_data=True) self._set_data(new_exp_data) return self @@ -4234,7 +4452,7 @@ class Experiment(OpenBisObject): props = self.p._all_props() request["params"][1][0]["properties"] = props self.openbis._post_request(self.openbis.as_v3, request) - print("Experiment successfully updated.") + if VERBOSE: print("Experiment successfully updated.") new_exp_data = self.openbis.get_experiment(resp[0]['permId'], only_data=True) self._set_data(new_exp_data) @@ -4385,14 +4603,14 @@ class Project(OpenBisObject): request = self._new_attrs() resp = self.openbis._post_request(self.openbis.as_v3, request) self.a.__dict__['_is_new'] = False - print("Project successfully created.") + if VERBOSE: print("Project successfully created.") new_project_data = self.openbis.get_project(resp[0]['permId'], only_data=True) self._set_data(new_project_data) return self else: request = self._up_attrs() self.openbis._post_request(self.openbis.as_v3, request) - print("Project successfully updated.") + if VERBOSE: print("Project successfully updated.") class SemanticAnnotation(): @@ -4472,7 +4690,7 @@ class SemanticAnnotation(): self._openbis._post_request(self._openbis.as_v3, request) self._isNew = False - print("Semantic annotation successfully created.") + if VERBOSE: print("Semantic annotation successfully created.") def _update(self): @@ -4500,8 +4718,8 @@ class SemanticAnnotation(): } self._openbis._post_request(self._openbis.as_v3, request) - print("Semantic annotation successfully updated.") + if VERBOSE: print("Semantic annotation successfully updated.") def delete(self, reason): self._openbis.delete_entity('SemanticAnnotation', self.permId, reason, False) - print("Semantic annotation successfully deleted.") + if VERBOSE: print("Semantic annotation successfully deleted.") diff --git a/src/python/PyBis/pybis/utils.py b/src/python/PyBis/pybis/utils.py index e992594c9e2a855ae78bb6777755ae4018e8871a..31cdfd7e12fbc79cfe2b7b6c39f57a7e2939e5eb 100644 --- a/src/python/PyBis/pybis/utils.py +++ b/src/python/PyBis/pybis/utils.py @@ -10,7 +10,7 @@ def parse_jackson(input_json): interesting=['tags', 'registrator', 'modifier', 'type', 'parents', 'children', 'containers', 'properties', 'experiment', 'sample', 'project', 'space', 'propertyType', 'entityType', 'propertyType', 'propertyAssignment', - 'externalDms', 'roleAssignments' + 'externalDms', 'roleAssignments', 'user', 'authorizationGroup' ] found = {} def build_cache(graph):