def add_relationship(self, entity1_ilx: str, relationship_ilx: str, entity2_ilx: str, real_server_resp: bool = False) -> dict: """ Adds relationship connection in Interlex A relationship exists as 3 different parts: 1. entity with type term, cde, fde, or pde 2. entity with type relationship that connects entity1 to entity2 -> Has its' own meta data, so no value needed 3. entity with type term, cde, fde, or pde """ entity1_ilx = self.get_ilx_fragment(entity1_ilx) relationship_ilx = self.get_ilx_fragment(relationship_ilx) entity2_ilx = self.get_ilx_fragment(entity2_ilx) data = { 'term1_id': entity1_ilx, 'relationship_tid': relationship_ilx, 'term2_id': entity2_ilx } # resp = self._post('term/add-relationship', data={**data, **{'batch-elastic': 'true'}}) resp = self._post('term/add-relationship', data=data) if resp.status_code == 200: log.warning( f"Relationship:" f" [{data['term1_id']}] -> [{data['relationship_tid']}] -> [{data['term2_id']}]" f" has already been added") # if real_server_resp: data = resp.json()['data'] return data
def withdraw_relationship(self, entity1_ilx: str, relationship_ilx: str, entity2_ilx: str) -> dict: """ Adds relationship connection in Interlex A relationship exists as 3 different parts: 1. entity with type term, cde, fde, or pde 2. entity with type relationship that connects entity1 to entity2 -> Has its' own meta data, so no value needed 3. entity with type term, cde, fde, or pde """ entity1_data = self.get_entity(entity1_ilx) if not entity1_data['id']: raise self.EntityDoesNotExistError( f'entity1_ilx: {entity1_data} does not exist') relationship_data = self.get_entity(relationship_ilx) if not relationship_data['id']: raise self.EntityDoesNotExistError( f'relationship_ilx: {relationship_ilx} does not exist') entity2_data = self.get_entity(entity2_ilx) if not entity2_data['id']: raise self.EntityDoesNotExistError( f'entity2_ilx: {entity2_data} does not exist') data = { 'term1_id': entity1_data['id'], # entity1_data['id'], 'relationship_tid': relationship_data['id'], # relationship_data['id'], 'term2_id': entity2_data['id'], # entity2_data['id'], 'term1_version': entity1_data['version'], 'term2_version': entity2_data['version'], 'relationship_term_version': relationship_data['version'], 'withdrawn': '1', # 'orig_uid': self.user_id, # BUG: php lacks orig_uid update } entity_relationships = self.get_relationship_via_tid( entity1_data['id']) # TODO: parse through entity_relationships to see if we have a match; else print warning and return None relationship_id = None for relationship in entity_relationships: if str(relationship['term1_id']) == str(entity1_data['id']): if str(relationship['term2_id']) == str(entity2_data['id']): if str(relationship['relationship_tid']) == str( relationship_data['id']): relationship_id = relationship['id'] break if not relationship_id: log.warning('Relationship you wanted to delete does not exist') return {} output = self._post(f'term/edit-relationship/{relationship_id}', data={ **data, **{ 'batch-elastic': 'true' } }).json()['data'] return output
def setup(self, **kwargs): oq.OntCuries({'TMP': 'http://uri.interlex.org/base/tmp_'}) if self.apiEndpoint is not None: try: self.ilx_cli = InterLexClient(base_url=self.apiEndpoint) except exc.NoApiKeyError: if not self.readonly: # expect attribute errors for ilx_cli log.warning( 'You have not set an API key for the SciCrunch API! ' 'InterLexRemote will error if you try to use it.') super().setup(**kwargs)
def add_annotation(self, term_ilx_id: str, annotation_type_ilx_id: str, annotation_value: str, real_server_resp: bool = False) -> dict: """ Adding an annotation value to a prexisting entity An annotation exists as 3 different parts ([1] -> [2] -> [3]): 1. entity with type term, TermSet, cde, fde, or pde 2. entity with type annotation 3. string value of the annotation :param term_ilx_id: Term ILX ID :param annotation_type_ilx_id: Annototation ILX ID :param annotation_value: Annotation value :param real_server_resp: Will return term IDs and versions, not InterLex IDs. :return: Annotation Record >>> self.add_annotation( term_ilx_id='ilx_0101431', # brain ILX ID annotation_type_ilx_id='ilx_0381360', # hasDbXref ILX ID annotation_value='http://neurolex.org/wiki/birnlex_796' # any string value ) """ tid = self.get_ilx_fragment(term_ilx_id) annotation_tid = self.get_ilx_fragment(annotation_type_ilx_id) data = { 'tid': tid, 'annotation_tid': annotation_tid, 'value': annotation_value } resp = self._post('term/add-annotation', data={ **data, **{ 'batch-elastic': 'true' } }) if resp.status_code == 200: log.warning( f"Annotation: " f"[{data['tid']}] -> [{data['annotation_tid']}] -> [{data['value']}]" f"has already been added") if real_server_resp: data = resp.json()['data'] return data
def withdraw_annotation(self, term_ilx_id: str, annotation_type_ilx_id: str, annotation_value: str) -> Optional[dict]: """ If annotation doesnt exist, add it :param term_ilx_id: Term ILX ID :param annotation_type_ilx_id: Annototation ILX ID :param annotation_value: Annotation value :return: Empty Annotation Record """ term_data = self.get_entity(term_ilx_id) if not term_data['id']: raise self.EntityDoesNotExistError('term_ilx_id: ' + term_ilx_id + ' does not exist') anno_data = self.get_entity(annotation_type_ilx_id) if not anno_data['id']: raise self.EntityDoesNotExistError('annotation_type_ilx_id: ' + annotation_type_ilx_id + ' does not exist') entity_annotations = self.get_annotation_via_tid(term_data['id']) annotation_id = '' for annotation in entity_annotations: if str(annotation['tid']) == str(term_data['id']): if str(annotation['annotation_tid']) == str(anno_data['id']): if str(annotation['value']) == str(annotation_value): annotation_id = annotation['id'] break if not annotation_id: log.warning('Annotation you wanted to delete does not exist') return None data = { 'tid': term_data['id'], # for delete 'annotation_tid': anno_data['id'], # for delete 'value': annotation_value, # for delete 'term_version': term_data['version'], 'annotation_term_version': anno_data['version'], 'withdrawn': '1', } output = self._post(f"term/edit-annotation/{annotation_id}", data=data).json()['data'] return output
def add_entity(self, label: str, type: str, cid: str = None, definition: str = None, comment: str = None, superclass: str = None, synonyms: list = None, existing_ids: list = None, force: bool = False, **kwargs) -> dict: """ Add Interlex entity into SciCrunch. Loosely structured ontological data based on the source ontologies for readability. :param label: Preferred name of entity. :param type: Any of the following: term, TermSet, cde, pde, fde, relationship, annotation. # todo add description of each type here :param cid: Community ID :param definition: Entities official definition. :param comment: A foot note regarding either the interpretation of the data or the data itself :param superclass: The ilx_id of the parent of this entity. Example: Organ is a superclass to Brain :param synonyms: Alternate names of the label. :param existing_ids: Alternate/source ontological iri/curies. Can only be one preferred ID. :param force: If entity is different from existing entity. This will add it if you have admin privileges. :param kwargs: a net for extra unneeded paramters. :return: requests.Response of insert or query from existing. >>> self.add_entity( \ label='Brain', \ type='term', # options: term, pde, fde, cde, annotation, or relationship \ definition='Official definition for entity.', \ comment='Additional casual notes for the next person.', \ superclass='ilx_1234567', \ synonyms=[{ \ 'literal': 'Brains', # label of synonym \ 'type': 'obo:hasExactSynonym', # Often predicate defined in ref ontology. \ }], \ existing_ids=[{ \ 'iri': 'http://purl.obolibrary.org/obo/UBERON_0000955', \ 'curie': 'UBERON:0000955', # Obeys prefix:id structure. \ 'preferred': '1', # Can be 0 or 1 with a type of either str or int. \ }], \ ) """ synonyms = synonyms or [] existing_ids = existing_ids or [] entity = { 'label': self._process_field(label, accepted_types=(str, )), 'type': self._process_field(type, accepted_values=self.entity_types, accepted_types=(str, )), 'cid': self._process_field(cid, accepted_types=(str, int)), 'definition': self._process_field(definition, accepted_types=(str, )), 'comment': self._process_field(comment, accepted_types=(str, )), 'superclasses': self._process_superclass(superclass), 'synonyms': self._process_synonyms(synonyms), 'existing_ids': self._process_existing_ids(existing_ids), 'force': force, } #entity['batch-elastic'] = 'true' resp = self._post('term/add', data=deepcopy(entity)) entity = resp.json()['data'] if resp.status_code == 200: log.warning( f"You already added {entity['label']} with InterLex ID {entity['ilx']}" ) # Backend handles more than one. User doesn't need to know. entity['superclass'] = entity.pop('superclasses') if entity['superclass']: entity['superclass'] = 'http://uri.interlex.org/base/' + entity[ 'superclass'][0]['ilx'] # todo: match structure of input # todo: compare if identical; for now test_interlex_client will do return entity
def _scicrunch_api_query(self, kwargs, iri, curie, label, term, predicates, limit): resp = None if iri: try: resp: dict = self.ilx_cli.get_entity(iri) except: pass if resp is None and curie: try: resp: dict = self.ilx_cli.get_entity_from_curie(curie) except (requests.exceptions.HTTPError, self.ilx_cli.Error) as e: log.debug(e) resp = None if resp is None or resp[ 'id'] is None: # FIXME should error before we have to check this # sometimes a remote curie does not match ours try: resp: dict = self.ilx_cli.get_entity_from_curie( self.OntId(iri).curie) except (requests.exceptions.HTTPError, self.ilx_cli.Error) as e: log.debug(e) return if resp['id'] is None: return elif label: try: resp: list = self.ilx_cli.query_elastic(label=label, size=limit) except (requests.exceptions.HTTPError, self.ilx_cli.Error) as e: log.debug(e) resp = None elif term: try: resp: list = self.ilx_cli.query_elastic(term=term, size=limit) except (requests.exceptions.HTTPError, self.ilx_cli.Error) as e: log.debug(e) resp = None else: pass # querying on iri or curie through ilx cli is ok if not resp: return resps = [resp] if isinstance(resp, dict) else resp # FIXME this is really a temp hack until we can get the # next version of the alt resolver up and running with # since iirc it can resolve curies for resp in resps: _frag = resp['ilx'] if _frag is None: log.warning(resp) continue _ilx = 'http://uri.interlex.org/base/' + _frag if iri is None: _iri = _ilx else: _iri = iri if curie is None: _curie = self._fix_fragment(resp['ilx']) else: _curie = curie # TODO if predicates is not None then need calls to get annotations and relations predicates = self._proc_api(resp) yield self.QueryResult( query_args=kwargs, iri=_iri, curie=_curie, label=resp['label'], labels=tuple(), # abbrev=None, # TODO # acronym=None, # TODO definition=resp['definition'], synonyms=tuple(resp['synonyms']), # deprecated=None, # prefix=None, # category=None, predicates=predicates, # _graph=None, _blob=resp, source=self, )
def _rcall__( self, term=None, # put this first so that the happy path query('brain') can be used, matches synonyms prefix=tuple(), # limit search within these prefixes category=None, # like prefix but works on predefined categories of things like 'anatomical entity' or 'species' label=None, # exact matches only abbrev=None, # alternately `abbr` as you have search=None, # hits a lucene index, not very high quality suffix=None, # suffix is 1234567 in PREFIX:1234567 curie=None, # if you are querying you can probably just use OntTerm directly and it will error when it tries to look up iri=None, # the most important one predicates=tuple( ), # provided with an iri or a curie to extract more specific triple information exclude_prefix=tuple(), depth=1, direction='OUTGOING', limit=10, include_deprecated=False, include_supers=False, include_all_services=False, raw=False, ): prefix = one_or_many(prefix) + self._prefix category = one_or_many(category) + self._category qualifiers = cullNone( prefix=prefix if prefix else None, exclude_prefix=exclude_prefix if exclude_prefix else None, category=category if category else None) queries = cullNone(abbrev=abbrev, label=label, term=term, search=search) graph_queries = cullNone( predicates=tuple( self._OntId(p) if ':' in p else p for p in predicates), # size must be known no generator depth=depth, direction=direction) identifiers = cullNone(suffix=suffix, curie=curie, iri=iri) control = dict(include_deprecated=include_deprecated, include_supers=include_supers, limit=limit) if queries and identifiers: log.warning( f'\x1b[91mWARNING: An identifier ({list(identifiers)}) was supplied. Ignoring other query parameters {list(queries)}.\x1b[0m' ) queries = {} if 'suffix' in identifiers and 'prefix' not in qualifiers: raise ValueError( 'Queries using suffix= must also include an explicit prefix.') if len(queries) > 1: raise ValueError( 'Queries only accept a single non-qualifier argument. ' 'Qualifiers are prefix=, category=.') # TODO more conditions here... # TODO? this is one place we could normalize queries as well instead of having # to do it for every single OntService kwargs = { **qualifiers, **queries, **graph_queries, **identifiers, **control } for j, service in enumerate(self.services): # TODO query keyword precedence if there is more than one #print(red.format(str(kwargs))) # TODO don't pass empty kwargs to services that can't handle them? for i, result in enumerate(service.query(**kwargs)): #print(red.format('AAAAAAAAAA'), result) if result: yield result if raw else result.asTerm() if search is None and term is None and result.label and not include_all_services: return # FIXME order services based on which you want first for now, will work on merging later
def _graphQuery(self, subject, predicate, depth=1, direction='OUTGOING', entail=True, inverse=False, include_supers=False, done=None): # TODO need predicate mapping... also subClassOf inverse?? hasSubClass?? # TODO how to handle depth? this is not an obvious or friendly way to do this if entail and inverse: raise NotImplementedError( 'Currently cannot handle inverse and entail at the same time.') if include_supers: if done is None: done = set() done.add(subject) for _, sup in self._graphQuery(subject, 'subClassOf', depth=40): if sup not in done: done.add(sup) for p, o in self._graphQuery(sup, predicate, depth=depth, direction=direction, entail=entail, inverse=inverse, done=done): if o not in done: done.add(o) yield p, o for p, o in self._graphQuery(subject, predicate, depth=depth, direction=direction, entail=entail, inverse=inverse, done=done): if o not in done: yield p, o done.add(o) yield from self._graphQuery(o, predicate, depth=depth, direction=direction, entail=entail, inverse=inverse, include_supers=include_supers, done=done) return d_nodes_edges = self.sgg.getNeighbors(subject, relationshipType=predicate, depth=depth, direction=direction, entail=entail) # TODO if d_nodes_edges: edges = d_nodes_edges['edges'] else: if inverse: # it is probably a bad idea to try to be clever here AND INDEED IT HAS BEEN predicate = self.inverses[predicate] _p = (predicate.curie if hasattr(predicate, 'curie') and predicate.curie is not None else predicate) log.warning(f'{subject.curie} has no edges with predicate {_p} ') return s, o = 'sub', 'obj' if inverse: s, o = o, s def properPredicate(e): if ':' in e['pred']: p = self.OntId(e['pred']) if inverse: # FIXME p == predicate ? no it is worse ... p = self.inverses[p] p = p.curie else: p = e['pred'] return p if depth > 1: #subjects = set(subject.curie) seen = {((predicate.curie if isinstance(predicate, self.OntId) else predicate), subject.curie)} for i, e in enumerate( self.sgg.ordered(subject.curie, edges, inverse=inverse)): if [ v for k, v in e.items() if k != 'meta' and v.startswith('_:') ]: # FIXME warn on these ? bnode getting pulled in ... # argh ... this is annoying to deal with continue #print('record number:', i) # FIXME # FIXME need to actually get the transitive closure, this doesn't actually work #if e[s] in subjects: #subjects.add(object.curie) object = e[o] p = e['pred'] # required if entail == True if (p, object) not in seen: # to make OntTerm(object) work we need to be able to use the 'meta' section... # and would have to fetch the object directly anyway since OntTerm requires # direct atestation ... which suggests that we probably need/want a bulk constructor seen.add((p, object)) yield ( properPredicate(e), self.OntId(object) ) # FIXME TODO this is _very_ inefficient for multiple lookups... else: _has_part_list = ['http://purl.obolibrary.org/obo/BFO_0000051'] _disjoint_with_list = ['disjointWith'] scurie = self._remote_curies.qname(subject) pred_objects = ( (properPredicate(e), self.OntId(e[o])) for e in edges if e[s] == scurie #and not print(predicate, scurie, e['pred'], e[o]) and not [ v for k, v in e.items() if k != 'meta' and v.startswith('_:') ] and ('owlType' not in e['meta'] or (e['meta']['owlType'] != _has_part_list and e['meta']['owlType'] != _disjoint_with_list))) yield from pred_objects