Skip to content

Commit

Permalink
serverless
Browse files Browse the repository at this point in the history
  • Loading branch information
cmungall committed Nov 11, 2021
1 parent bb91032 commit c9040b2
Show file tree
Hide file tree
Showing 13 changed files with 463 additions and 17 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
.idea
__pycache__
.serverless
node_modules/
15 changes: 15 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
RUN = poetry run

# serverless/lambda seems to require this be at root level
#APP_PATH = ontology_term_usage.app.main
APP_PATH = main

test:
$(RUN) pytest -s

app:
$(RUN) uvicorn $(APP_PATH):app --reload

# https://adem.sh/blog/tutorial-fastapi-aws-lambda-serverless
deploy:
sls deploy --stage staging
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,31 @@ Demo:
poetry run pytest -s
```

## API

To run the API locally:

```bash
make app
```

* Docs: http://127.0.0.1:8000/docs
* Find usages: http://127.0.0.1:8000/usage/GO:0007588

Deployment on AWS via serverless:

Follow

https://adem.sh/blog/tutorial-fastapi-aws-lambda-serverless

for serverless/lambda deployment

Type

```bash
make deploy
```

## Future plans

It may be better to wrap this into a generic obolibrary python package
Expand Down
34 changes: 34 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# TODO: figure out how to put this in the app/ folder and still use serverless
# This line: `handler: main.handler`
# How do we specify a path here, as per uvicorn?

import os
from enum import Enum
from typing import Optional

from pydantic import BaseModel
from fastapi import FastAPI, Query

# for lambda; see https://adem.sh/blog/tutorial-fastapi-aws-lambda-serverless
from mangum import Mangum


from ontology_term_usage.term_usage import OntologyClient, ResultSet, TermUsage, TERM

stage = os.environ.get('STAGE', None)
openapi_prefix = f"/{stage}" if stage else "/"

client = OntologyClient()

app = FastAPI(title='Ontology Usage API', openapi_prefix=openapi_prefix)

@app.get("/")
async def root():
return {"message": "Hello World"}

@app.get("/usage/{term}", response_model=ResultSet)
async def usage(term: TERM) -> ResultSet:
rs = client.term_usage(term)
return rs

handler = Mangum(app)
2 changes: 1 addition & 1 deletion ontology_term_usage/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__version__ = '0.1.0'

from ontology_term_usage import *
from ontology_term_usage.term_usage import OntologyClient, ResultSet, TermUsage
Empty file.
30 changes: 30 additions & 0 deletions ontology_term_usage/app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import os
from enum import Enum
from typing import Optional

from pydantic import BaseModel
from fastapi import FastAPI, Query

# for lambda; see https://adem.sh/blog/tutorial-fastapi-aws-lambda-serverless
from mangum import Mangum


from ontology_term_usage.term_usage import OntologyClient, ResultSet, TermUsage, TERM

stage = os.environ.get('STAGE', None)
openapi_prefix = f"/{stage}" if stage else "/"

client = OntologyClient()

app = FastAPI(title='Ontology Usage API', openapi_prefix=openapi_prefix)

@app.get("/")
async def root():
return {"message": "Hello World"}

@app.get("/usage/{term}", response_model=ResultSet)
async def usage(term: TERM) -> ResultSet:
rs = client.term_usage(term)
return rs

handler = Mangum(app)
110 changes: 99 additions & 11 deletions ontology_term_usage/term_usage.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,46 @@
import logging
from typing import List
from typing import List, Dict

from SPARQLWrapper import SPARQLWrapper, JSON
from pydantic import BaseModel


TERM = str
URISTR = str
SERVICE = str
CATEGORY = str

class TermUsage(BaseModel):
"""
Info on how a term is used
Most of the the time a curator needs to know the id/uri and label of the term,
but it is also useful to give more context
Note the thing using the term need not be an ontology term - e.g. it could be a uniprot protein
"""
uri: URISTR
label: str = None
category: CATEGORY = None
predicate: URISTR = None
graph: URISTR = None
notes: str = None
axiom_type: str = None
endpoint: str = None

class ResultSet(BaseModel):
term: URISTR = None
limit: int = None
usages: Dict[SERVICE, List[TermUsage]] = {}

ontobee_usage_query_template = """
SELECT ?uri ?label ?predicate WHERE {{
?uri <http://www.w3.org/2000/01/rdf-schema#subClassOf> ?restr .
?uri rdfs:label ?label .
?restr <http://www.w3.org/2002/07/owl#onProperty> ?predicate .
?restr <http://www.w3.org/2002/07/owl#someValuesFrom> <{term_uri}>
GRAPH ?graph {{
?uri <http://www.w3.org/2000/01/rdf-schema#subClassOf> ?restr .
?restr <http://www.w3.org/2002/07/owl#onProperty> ?predicate .
?restr <http://www.w3.org/2002/07/owl#someValuesFrom> <{term_uri}>
}}
?uri rdfs:label ?label
}}
"""

Expand All @@ -40,34 +53,100 @@ class TermUsage(BaseModel):
}}
"""

class OntologyClient:
uniprot_usage_query_template = """
SELECT ?uri ?label WHERE {{
?uri <http://purl.uniprot.org/core/classifiedWith> <{term_uri}> ;
rdfs:label ?label
}}
"""

gocam_usage_query_template = """
SELECT ?uri ?graph WHERE {{
GRAPH ?graph {{
?uri rdf:type <{term_uri}>
}}
}}
"""

config = {
'ontobee':
{
'endpoint': 'http://sparql.hegroup.org/sparql',
'query_template': ontobee_usage_query_template,
'category': 'OntologyTerm'
},
'ubergraph':
{
'endpoint': 'https://stars-app.renci.org/ubergraph/sparql',
'query_template': ubergraph_usage_query_template,
'category': 'OntologyTerm'
},
'uniprot':
{
'endpoint': 'https://sparql.uniprot.org/sparql',
'query_template': uniprot_usage_query_template,
'category': 'Protein'
},
'gocam':
{
'endpoint': 'http://rdf.geneontology.org/sparql',
'query_template': gocam_usage_query_template,
'category': 'Model'
},
}

class OntologyClient(BaseModel):
"""
Wrapper for multiple ontology endpoints
"""

limit: int = 30

def term_to_uri(self, term: TERM) -> URISTR:
if ':/' in term:
return term
[prefix, local_id] = term.split(':')
return f'http://purl.obolibrary.org/obo/{prefix}_{local_id}'

def _term_usage_query(self, term: TERM, endpoint: str, template: str) -> List[TermUsage]:
def _term_usage_query(self, term: TERM, service: str) -> List[TermUsage]:
info = config[service]
endpoint = info['endpoint']
template = info['query_template']
category = info['category']
limit = self.limit
term_uri = self.term_to_uri(term)
sparql = SPARQLWrapper(endpoint)
q = template.format(term_uri=term_uri)
print(q)
q = f'{q}\nLIMIT {limit}'
logging.info(q)
sparql.setQuery(q)
sparql.setReturnFormat(JSON)
results = sparql.query().convert()
usages = []
for result in results["results"]["bindings"]:
d = {k: v['value'] for k, v in result.items()}
u = TermUsage(**d)
print(f'U={u}')
u.endpoint = endpoint
u.category = category
logging.info(f'U={u}')
usages.append(u)
return usages

def term_usage(self, term: TERM, services: List[SERVICE] = None) -> ResultSet:
"""
iterate through all services querying for term usage
:param term:
:param services: if None, queries all
:return:
"""
rs = ResultSet(term=term, limit=self.limit)
if services == None:
services = config.keys()
for service in services:
rs.usages[service] = self._term_usage_query(term, service)
return rs

def term_usage_ontobee(self, term: TERM) -> List[TermUsage]:
"""
Queries ontobee for usage
Expand All @@ -77,7 +156,7 @@ def term_usage_ontobee(self, term: TERM) -> List[TermUsage]:
:param term:
:return:
"""
return self._term_usage_query(term, "http://sparql.hegroup.org/sparql", ontobee_usage_query_template)
return self._term_usage_query(term, 'ontobee')

def term_usage_ubergraph(self, term: TERM) -> List[TermUsage]:
"""
Expand All @@ -87,5 +166,14 @@ def term_usage_ubergraph(self, term: TERM) -> List[TermUsage]:
:param term:
:return:
"""
return self._term_usage_query(term, "https://stars-app.renci.org/ubergraph/sparql", ubergraph_usage_query_template)
return self._term_usage_query(term, 'ubergraph')

def term_usage_uniprot(self, term: TERM) -> List[TermUsage]:
"""
Queries uniprot for usage
:param term:
:return:
"""
return self._term_usage_query(term, 'uniprot')

8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "ontology-term-usage",
"version": "1.0.0",
"author": "Chris Mungall",
"dependencies": {
"serverless-python-requirements": "^5.0.1"
}
}
Loading

0 comments on commit c9040b2

Please sign in to comment.