Skip to content

Commit

Permalink
more refactoring, and drs endpoint updates
Browse files Browse the repository at this point in the history
  • Loading branch information
nsheff committed Nov 7, 2023
1 parent 5e0f554 commit d73968b
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 120 deletions.
3 changes: 3 additions & 0 deletions bedhost/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import logging
import logmuse

from .const import PKG_NAME

_LOGGER = logmuse.init_logger(PKG_NAME)

logging.getLogger("bbconf").setLevel(logging.DEBUG)
51 changes: 2 additions & 49 deletions bedhost/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def serve_file(self, path: str, remote: bool = None):
:param bool remote: whether to redirect to a remote source or serve local
:exception FileNotFoundError: if file not found
"""
remote = remote or self.is_remote
remote = remote or True
if remote:
_LOGGER.info(f"Redirecting to: {path}")
return RedirectResponse(path)
Expand Down Expand Up @@ -208,7 +208,7 @@ def attach_routers(app):
return app


def configure(bbconf_file_path):
def configure(bbconf_file_path, app):
try:
# bbconf_file_path = os.environ.get("BEDBASE_CONFIG") or None
_LOGGER.info(f"Loading config: '{bbconf_file_path}'")
Expand Down Expand Up @@ -236,50 +236,3 @@ def configure(bbconf_file_path):
f"Using remote files for serving. Prefix: {bbc.config[CFG_REMOTE_KEY]['http']['prefix']}"
)
return bbc


# def get_id_map(bbc, table_name, file_type):
# """
# Get a dict for avalible file/figure ids
#
# :param str table_name: table name to query
# :param st file_type: "file" or "image"
# :return dict
# """
#
# id_map = {}
#
# schema = serve_schema_for_table(bbc=bbc, table_name=table_name)
# # This is basically just doing this:
# # if table_name == BED_TABLE:
# # schema = bbc.bed.schema
# # if table_name == BEDSET_TABLE:
# # schema = bbc.bedset.schema
# # TODO: Eliminate the need for bedhost to be aware of table names; this should be abstracted away by bbconf/pipestat
# for key, value in schema.sample_level_data.items():
# if value["type"] == file_type:
# id_map[value["label"]] = key
#
# return id_map


# def get_enum_map(bbc, table_name, file_type):
# """
# Get a dict of file/figure labels

# :param str table_name: table name to query
# :param st file_type: "file" or "image"
# :return dict
# """

# enum_map = {}
# _LOGGER.debug(f"Getting enum map for {file_type} in {table_name}")

# # TO FIX: I think we need a different way to get the schema
# schema = serve_schema_for_table(bbc=bbc, table_name=table_name)

# for key, value in schema.sample_level_data.items():
# if value["type"] == file_type:
# enum_map[value["label"]] = value["label"]

# return enum_map
193 changes: 158 additions & 35 deletions bedhost/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@
import sys
import uvicorn

from fastapi import FastAPI
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from typing import Dict
from urllib.parse import urlparse
from fastapi import Response, HTTPException

from bbconf.exceptions import *
from pipestat.exceptions import RecordNotFoundError, ColumnNotFoundError


from . import _LOGGER
from .helpers import FileResponse, configure, attach_routers, get_openapi_version
Expand Down Expand Up @@ -43,62 +50,178 @@
)


@app.get("/")
@app.get("/", summary="API intro page", tags=["General endpoints"])
async def index():
"""
Display the dummy index UI page
Display the index UI page
"""
return FileResponse(os.path.join(STATIC_PATH, "index.html"))


@app.get("/versions", response_model=Dict[str, str])
async def get_version_info():
"""
Returns app version information
"""
versions = ALL_VERSIONS
versions.update({"openapi_version": get_openapi_version(app)})
return versions


@app.get("/objects/{object_id}")
async def get_object_metadata(object_id: str):
"""
Returns metadata for a given object
"""
return {
"id": object_id
}

return bbc.get_bed_drs_metadata(object_id)

@app.get("/objects/{object_id}/access/{access_id}")
async def get_object_bytes_url(object_id: str, access_id: str):
"""
Returns a URL for a given object
"""
return bbc.get_bed_url(object_id, access_id)

@app.get("/service-info", summary="GA4GH service info", tags=["General endpoints"])
async def service_info():
"""
Returns information about this service, such as versions, name, etc.
"""
all_versions = ALL_VERSIONS
service_version = all_versions["apiserver_version"]
all_versions.update({"openapi_version": get_openapi_version(app)})
del all_versions["apiserver_version"]
ret = {
"id": "org.bedbase.api",
"name": "BEDbase API",
"type": {
"group": "org.databio",
"artifact": "bedbase",
"version": ALL_VERSIONS["apiserver_version"],
"version": service_version,
},
"description": "An API providing genomic interval data and metadata",
"organization": {"name": "Databio Lab", "url": "https://databio.org"},
"contactUrl": "https://github.com/databio/bedbase/issues",
"documentationUrl": "https://bedbase.org",
"updatedAt": "2023-10-25T00:00:00Z",
"environment": "dev",
"version": ALL_VERSIONS["apiserver_version"]
"version": service_version,
"component_versions": all_versions,
}
return JSONResponse(content=ret)


DRS_ENDPOINTS_LABEL = "objects -- download files via DRS"


@app.get(
"/objects/{object_id}",
summary="Get DRS object metadata",
tags=[DRS_ENDPOINTS_LABEL],
)
async def get_drs_object_metadata(object_id: str, req: Request):
"""
Returns metadata about a DrsObject.
"""
ids = parse_bedbase_drs_object_id(object_id)
base_uri = urlparse(str(req.url)).netloc
return bbc.get_drs_metadata(
ids["record_type"], ids["record_id"], ids["result_id"], base_uri
)


@app.get(
"/objects/{object_id}/access/{access_id}",
summary="Get URL where you can retrive files",
tags=[DRS_ENDPOINTS_LABEL],
)
async def get_object_bytes_url(object_id: str, access_id: str):
"""
Returns a URL that can be used to fetch the bytes of a DrsObject.
"""
ids = parse_bedbase_drs_object_id(object_id)
return bbc.get_object_uri(
ids["record_type"], ids["record_id"], ids["result_id"], access_id
)


@app.head(
"/objects/{object_id}/access/{access_id}/bytes", include_in_schema=False
) # Required by UCSC track hubs
@app.get(
"/objects/{object_id}/access/{access_id}/bytes",
summary="Download actual files",
tags=[DRS_ENDPOINTS_LABEL],
)
async def get_object_bytes(object_id: str, access_id: str):
"""
Returns the bytes of a DrsObject.
"""
ids = parse_bedbase_drs_object_id(object_id)
return bbc.serve_file(
bbc.get_object_uri(
ids["record_type"], ids["record_id"], ids["result_id"], access_id
)
)


@app.get(
"/objects/{object_id}/access/{access_id}/thumbnail",
summary="Download thumbnail",
tags=[DRS_ENDPOINTS_LABEL],
)
async def get_object_thumbnail(object_id: str, access_id: str):
"""
Returns the bytes of a thumbnail of a DrsObject
"""
ids = parse_bedbase_drs_object_id(object_id)
return bbc.serve_file(
bbc.get_thumbnail_uri(
ids["record_type"], ids["record_id"], ids["result_id"], access_id
)
)


# DRS-compatible API.
# Requires using `object_id` which has the form: `<record_type>.<record_id>.<object_class>`
# for example: `bed.326d5d77c7decf067bd4c7b42340c9a8.bedfile`
# or: `bed.421d2128e183424fcc6a74269bae7934.bedfile`
# bed.326d5d77c7decf067bd4c7b42340c9a8.bedfile
# bed.326d5d77c7decf067bd4c7b42340c9a8.bigbed
def parse_bedbase_drs_object_id(object_id: str):
"""
Parse bedbase object id into its components
"""
record_type, record_id, result_id = object_id.split(".")
if record_type not in ["bed", "bedset"]:
raise HTTPException(
status_code=400, detail=f"Object type {record_type} is incorrect"
)
return {
"record_type": record_type,
"record_id": record_id,
"result_id": result_id,
}


# General-purpose exception handlers (so we don't have to write try/catch blocks in every endpoint)

@app.exception_handler(MissingThumbnailError)
async def exception_handler_MissingThumbnailError(
request: Request, exc: MissingThumbnailError
):
return JSONResponse(
status_code=404,
content={"msg": "No thumbnail for this object.", "status_code": 404},
)


@app.exception_handler(IncorrectAccessMethodError)
async def exception_handler_IncorrectAccessMethodError(
request: Request, exc: IncorrectAccessMethodError
):
return JSONResponse(
status_code=404,
content={"msg": "Requested access URL was not found.", "status_code": 404},
)


@app.exception_handler(ColumnNotFoundError)
async def exception_handler_ColumnNotFoundError(
request: Request, exc: ColumnNotFoundError
):
return JSONResponse(
status_code=404,
content={"msg": "Malformed result identifier.", "status_code": 404},
)


@app.exception_handler(RecordNotFoundError)
async def exception_handler_RecordNotFoundError(
request: Request, exc: RecordNotFoundError
):
return JSONResponse(
status_code=404,
content={"msg": "Record not found.", "status_code": 404},
)


def main():
parser = build_parser()
args = parser.parse_args()
Expand All @@ -111,7 +234,7 @@ def main():
_LOGGER.info(f"Running {PKG_NAME} app...")
bbconf_file_path = args.config or os.environ.get("BEDBASE_CONFIG") or None
global bbc
bbc = configure(bbconf_file_path)
bbc = configure(bbconf_file_path, app)
attach_routers(app)
uvicorn.run(
app,
Expand All @@ -125,7 +248,7 @@ def main():
bbconf_file_path = os.environ.get("BEDBASE_CONFIG") or None
# must be configured before attaching routers to avoid circular imports
global bbc
bbc = configure(bbconf_file_path)
bbc = configure(bbconf_file_path, app)
attach_routers(app)
else:
raise EnvironmentError(
Expand Down
Loading

0 comments on commit d73968b

Please sign in to comment.