tighten up error reporting (#1269)

* black reformat

* tighten up error reporting

* lint

* fine tuning

* additional improvements in exception handling

* lint

* include exception and traceback in log

* fix typo
This commit is contained in:
Bruce Martin
2020-03-22 09:55:47 -07:00
committed by GitHub
parent 8180be83b8
commit db7a485796
9 changed files with 124 additions and 129 deletions
+6 -7
View File
@@ -11,7 +11,6 @@ from server.common.errors import AnnotationsError, OntologyLoadFailure
from server.common.utils import series_to_schema
import fsspec
import fastobo
import traceback # use built-in formatter for SyntaxError
from flask import session
from abc import ABCMeta, abstractmethod
@@ -39,14 +38,13 @@ class Annotations(metaclass=ABCMeta):
self.ontology_data = names
except FileNotFoundError as e:
raise OntologyLoadFailure(f"Unable to find OBO ontology path: {path}") from e
raise OntologyLoadFailure("Unable to find OBO ontology path") from e
except SyntaxError as e:
msg = "".join(traceback.format_exception_only(SyntaxError, e))
raise OntologyLoadFailure(msg) from e
raise OntologyLoadFailure("Syntax error loading OBO ontology") from e
except Exception as e:
raise OntologyLoadFailure(f"Error loading OBO file {path}") from e
raise OntologyLoadFailure("Error loading OBO file") from e
def get_schema(self, data_adaptor):
labels = self.read_labels(data_adaptor)
@@ -124,8 +122,9 @@ class AnnotationsLocalFile(Annotations):
if fname == self.last_fname:
return self.last_labels
else:
labels = pd.read_csv(fname, dtype="category",
index_col=0, header=0, comment="#", keep_default_na=False)
labels = pd.read_csv(
fname, dtype="category", index_col=0, header=0, comment="#", keep_default_na=False
)
# update the cache
self.last_fname = fname
self.last_labels = labels
+63 -69
View File
@@ -1,7 +1,8 @@
import sys
from http import HTTPStatus
import warnings
import copy
from flask import make_response, jsonify
import logging
from flask import make_response, jsonify, current_app, abort
from server.common.constants import Axis, DiffExpMode, JSON_NaN_to_num_warning_msg
from server.common.errors import (
FilterError,
@@ -14,6 +15,20 @@ import json
from server.data_common.fbs.matrix import decode_matrix_fbs
def abort_and_log(code, logmsg, loglevel=logging.DEBUG, include_exc_info=False):
"""
Log the message, then abort with HTTP code. If include_exc_info is true,
also include current exception via sys.exc_info().
"""
if include_exc_info:
exc_info = sys.exc_info()
else:
exc_info = False
current_app.logger.log(loglevel, logmsg, exc_info=exc_info)
# Do NOT send log message to HTTP response.
return abort(code)
def schema_get_helper(data_adaptor, annotations):
"""helper function to gather the schema from the data source and annotations"""
schema = data_adaptor.get_schema()
@@ -41,19 +56,15 @@ def annotations_obs_get(request, data_adaptor, annotations):
fields = request.args.getlist("annotation-name", None)
preferred_mimetype = request.accept_mimetypes.best_match(["application/octet-stream"])
if preferred_mimetype != "application/octet-stream":
return make_response(f"Unsupported MIME type '{request.accept_mimetypes}'", HTTPStatus.NOT_ACCEPTABLE)
return abort(HTTPStatus.NOT_ACCEPTABLE)
try:
labels = None
if annotations:
labels = annotations.read_labels(data_adaptor)
fbs = data_adaptor.annotation_to_fbs_matrix(Axis.OBS, fields, labels)
return make_response(fbs, HTTPStatus.OK, {"Content-Type": "application/octet-stream"})
except KeyError:
return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST)
except ValueError as e:
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
except Exception as e:
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
except KeyError as e:
return abort_and_log(HTTPStatus.BAD_REQUEST, str(e), include_exc_info=True)
def annotations_put_fbs_helper(data_adaptor, annotations, fbs):
@@ -69,14 +80,14 @@ def annotations_put_fbs_helper(data_adaptor, annotations, fbs):
def annotations_obs_put(request, data_adaptor, annotations):
if annotations is None:
return make_response("Error, annotations are not configured", HTTPStatus.BAD_REQUEST)
return abort(HTTPStatus.NOT_IMPLEMENTED)
anno_collection = request.args.get("annotation-collection-name", default=None)
fbs = request.get_data()
if anno_collection is not None:
if not annotations.is_safe_collection_name(anno_collection):
return make_response(f"Error, bad annotation collection name", HTTPStatus.BAD_REQUEST)
return abort(HTTPStatus.BAD_REQUEST, "Bad annotation collection name")
annotations.set_collection(anno_collection)
try:
@@ -84,16 +95,14 @@ def annotations_obs_put(request, data_adaptor, annotations):
res = json.dumps({"status": "OK"})
return make_response(res, HTTPStatus.OK, {"Content-Type": "application/json"})
except (ValueError, DisabledFeatureError, KeyError) as e:
return make_response(str(e), HTTPStatus.BAD_REQUEST)
except Exception as e:
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
return abort_and_log(HTTPStatus.BAD_REQUEST, str(e), include_exc_info=True)
def annotations_var_get(request, data_adaptor, annotations):
fields = request.args.getlist("annotation-name", None)
preferred_mimetype = request.accept_mimetypes.best_match(["application/octet-stream"])
if preferred_mimetype != "application/octet-stream":
return make_response(f"Unsupported MIME type '{request.accept_mimetypes}'", HTTPStatus.NOT_ACCEPTABLE)
return abort(HTTPStatus.NOT_ACCEPTABLE)
try:
labels = None
if annotations is not None:
@@ -103,18 +112,14 @@ def annotations_var_get(request, data_adaptor, annotations):
HTTPStatus.OK,
{"Content-Type": "application/octet-stream"},
)
except KeyError:
return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST)
except ValueError as e:
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
except Exception as e:
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
except KeyError as e:
return abort_and_log(HTTPStatus.BAD_REQUEST, str(e), include_exc_info=True)
def data_var_put(request, data_adaptor):
preferred_mimetype = request.accept_mimetypes.best_match(["application/octet-stream"])
if preferred_mimetype != "application/octet-stream":
return make_response(f"Unsupported MIME type '{request.accept_mimetypes}'", HTTPStatus.NOT_ACCEPTABLE)
return abort(HTTPStatus.NOT_ACCEPTABLE)
filter_json = request.get_json()
filter = filter_json["filter"] if filter_json else None
@@ -125,58 +130,43 @@ def data_var_put(request, data_adaptor):
{"Content-Type": "application/octet-stream"},
)
except FilterError as e:
return make_response(str(e), HTTPStatus.BAD_REQUEST)
except ValueError as e:
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
return abort_and_log(HTTPStatus.BAD_REQUEST, str(e), include_exc_info=True)
def diffexp_obs_post(request, data_adaptor):
if not data_adaptor.config.diffexp__enable:
return make_response(f"diffexp not supported.", HTTPStatus.BAD_REQUEST)
return abort(HTTPStatus.NOT_IMPLEMENTED)
args = request.get_json()
# confirm mode is present and legal
try:
# TODO: implement varfilter mode
mode = DiffExpMode(args["mode"])
except (KeyError, TypeError):
return make_response("Error: mode is required", HTTPStatus.BAD_REQUEST)
except ValueError:
return make_response(f"Error: invalid mode option {args['mode']}", HTTPStatus.BAD_REQUEST)
# Validate filters
if mode == DiffExpMode.VAR_FILTER or "varFilter" in args:
# not NOT_IMPLEMENTED
return make_response("mode=varfilter not implemented", HTTPStatus.NOT_IMPLEMENTED)
if mode == DiffExpMode.TOP_N and "count" not in args:
return make_response("mode=topN requires a count parameter", HTTPStatus.BAD_REQUEST)
if "set1" not in args:
return make_response("set1 is required.", HTTPStatus.BAD_REQUEST)
if Axis.VAR in args["set1"]["filter"]:
return make_response("Var filter not allowed for set1", HTTPStatus.BAD_REQUEST)
# set2
if "set2" not in args:
return make_response("Set2 as inverse of set1 is not implemented", HTTPStatus.NOT_IMPLEMENTED)
if Axis.VAR in args["set2"]["filter"]:
return make_response("Var filter not allowed for set2", HTTPStatus.BAD_REQUEST)
if mode == DiffExpMode.VAR_FILTER or "varFilter" in args:
return abort_and_log(HTTPStatus.NOT_IMPLEMENTED, "varFilter not enabled")
set1_filter = args["set1"]["filter"]
set2_filter = args.get("set2", {"filter": {}})["filter"]
set1_filter = args.get("set1", {"filter": {}})["filter"]
set2_filter = args.get("set2", {"filter": {}})["filter"]
count = args.get("count", None)
# TODO: implement varfilter mode
if set1_filter is None or set2_filter is None or count is None:
return abort_and_log(HTTPStatus.BAD_REQUEST, "missing required parameter")
if Axis.VAR in set1_filter or Axis.VAR in set2_filter:
return abort_and_log(HTTPStatus.BAD_REQUEST, "var axis filter not enabled")
except (KeyError, TypeError) as e:
return abort_and_log(HTTPStatus.BAD_REQUEST, str(e), include_exc_info=True)
# mode=topN
count = args.get("count", None)
try:
diffexp = data_adaptor.diffexp_topN(set1_filter, set2_filter, count)
return make_response(diffexp, HTTPStatus.OK, {"Content-Type": "application/json"})
except (ValueError, DisabledFeatureError, FilterError) as e:
return make_response(str(e), HTTPStatus.BAD_REQUEST)
except JSONEncodingValueError as e:
# JSON encoding failure, usually due to bad data
warnings.warn(JSON_NaN_to_num_warning_msg)
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
except ValueError as e:
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
return abort_and_log(HTTPStatus.BAD_REQUEST, str(e), include_exc_info=True)
except JSONEncodingValueError:
# JSON encoding failure, usually due to bad data. Just let it ripple up
# to default exception handler.
current_app.logger.warning(JSON_NaN_to_num_warning_msg)
raise
def layout_obs_get(request, data_adaptor):
@@ -187,24 +177,28 @@ def layout_obs_get(request, data_adaptor):
data_adaptor.layout_to_fbs_matrix(), HTTPStatus.OK, {"Content-Type": "application/octet-stream"}
)
else:
return make_response(f"Unsupported MIME type '{request.accept_mimetypes}'", HTTPStatus.NOT_ACCEPTABLE)
except PrepareError as e:
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
except ValueError as e:
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
return abort(HTTPStatus.NOT_ACCEPTABLE)
except PrepareError:
return abort_and_log(
HTTPStatus.NOT_IMPLEMENTED,
f"No embedding available {request.path}",
loglevel=logging.ERROR,
include_exc_info=True,
)
def layout_obs_put(request, data_adaptor):
if not data_adaptor.config.embedding__enable_reembedding:
return abort(HTTPStatus.NOT_IMPLEMENTED)
preferred_mimetype = request.accept_mimetypes.best_match(["application/octet-stream"])
if preferred_mimetype != "application/octet-stream":
return make_response(f"Unsupported MIME type '{request.accept_mimetypes}'", HTTPStatus.NOT_ACCEPTABLE)
if not data_adaptor.config.embedding__enable_reembedding:
return make_response(f"Computed embedding not supported.", HTTPStatus.BAD_REQUEST)
return abort(HTTPStatus.NOT_ACCEPTABLE)
args = request.get_json()
filter = args["filter"] if args else None
if not filter:
return make_response("Error: obs filter is required", HTTPStatus.BAD_REQUEST)
return abort_and_log(HTTPStatus.BAD_REQUEST, "obs filter is required")
method = args["method"] if args else "umap"
try:
@@ -219,6 +213,6 @@ def layout_obs_put(request, data_adaptor):
},
)
except NotImplementedError as e:
return make_response(str(e), HTTPStatus.NOT_IMPLEMENTED)
return abort_and_log(HTTPStatus.NOT_IMPLEMENTED, str(e), include_exc_info=True)
except (ValueError, DisabledFeatureError, FilterError) as e:
return make_response(str(e), HTTPStatus.BAD_REQUEST)
return abort_and_log(HTTPStatus.BAD_REQUEST, str(e), include_exc_info=True)