REST Error handling (#299)

* Use HTTPStatus for all responses

More informative than just the code as an int

* Fill out REST error handling

* Test error routes

* Use HTTP Status for tests too

* factor mime type request into function

* Better mimetype errors

* 404 -> 400 error for bad key
This commit is contained in:
Charlotte Weaver
2018-10-08 14:17:56 -07:00
committed by GitHub
parent 8be0833d23
commit 11ef5ae51b
4 changed files with 193 additions and 81 deletions
+2 -3
View File
@@ -300,7 +300,7 @@ Observations and variables are guaranteed to have a `name` annotation, which sho
**Response code:** **Response code:**
- 200 Success - 200 Success
- 404 Not Found - one or more of the names specified with `annotation-name` are not associated with an annotation. - 400 Bad Request - one or more of the names specified with `annotation-name` are not associated with an annotation.
**Response body:** annotation description and values. Values conform to the schema returned by `/schema` and each record begins with the observation or variable index. Where the specific value is not defined, `null`will be returned. Values will be sorted by index. **Response body:** annotation description and values. Values conform to the schema returned by `/schema` and each record begins with the observation or variable index. Where the specific value is not defined, `null`will be returned. Values will be sorted by index.
@@ -342,8 +342,7 @@ Same as the `GET /annotations` routes, with additional ability to filter by obse
**Response code:** **Response code:**
- 200 Success - 200 Success
- 400 Bad Request - malformed filter - 400 Bad Request - malformed filter or one or more of the annotation-name identifiers were not associated with an annotation name.
- 404 Not Found - one or more of the annotation-name identifiers were not associated with an annotation name.
**Response body:** **Response body:**
+113 -53
View File
@@ -10,6 +10,7 @@ from werkzeug.datastructures import ImmutableMultiDict
from server.app.util.constants import Axis, DiffExpMode from server.app.util.constants import Axis, DiffExpMode
from server.app.util.filter import parse_filter, QueryStringError from server.app.util.filter import parse_filter, QueryStringError
from server.app.util.models import FilterModel from server.app.util.models import FilterModel
from server.app.util.utils import MimeTypeError, get_mime_type
class SchemaAPI(Resource): class SchemaAPI(Resource):
@@ -54,7 +55,7 @@ class SchemaAPI(Resource):
}) })
def get(self): def get(self):
return make_response(jsonify({"schema": current_app.data.schema}), 200) return make_response(jsonify({"schema": current_app.data.schema}), HTTPStatus.OK)
class ConfigAPI(Resource): class ConfigAPI(Resource):
@@ -105,7 +106,7 @@ class ConfigAPI(Resource):
} }
} }
} }
return make_response(jsonify(config), 200) return make_response(jsonify(config), HTTPStatus.OK)
class LayoutObsAPI(Resource): class LayoutObsAPI(Resource):
@@ -131,7 +132,7 @@ class LayoutObsAPI(Resource):
} }
}) })
def get(self): def get(self):
return make_response((jsonify({"layout": current_app.data.layout(current_app.data.data)}))) return make_response((jsonify({"layout": current_app.data.layout(current_app.data.data)})), HTTPStatus.OK)
@swagger.doc({ @swagger.doc({
"summary": "Observation layout for filtered subset.", "summary": "Observation layout for filtered subset.",
@@ -158,12 +159,23 @@ class LayoutObsAPI(Resource):
} }
} }
} }
} },
"400": {
"description": "Malformed filter"
},
"403": {
"description": "Non-interactive request"
},
} }
}) })
def post(self): def post(self):
df = current_app.data.filter_dataframe(request.get_json()["filter"]) try:
return make_response((jsonify({"layout": current_app.data.layout(df)}))) df = current_app.data.filter_dataframe(request.get_json()["filter"])
except KeyError:
return make_response("Malformed filter", HTTPStatus.BAD_REQUEST)
if len(df.obs.index) > current_app.data.features["layout"]["obs"]["interactiveLimit"]:
return make_response("Non-interactive request", HTTPStatus.FORBIDDEN)
return make_response((jsonify({"layout": current_app.data.layout(df)})), HTTPStatus.OK)
class AnnotationsObsAPI(Resource): class AnnotationsObsAPI(Resource):
@@ -193,6 +205,10 @@ class AnnotationsObsAPI(Resource):
} }
} }
},
"400": {
"description": "one or more of the annotation-name identifiers were not associated with an "
"annotation name"
} }
} }
}) })
@@ -201,8 +217,8 @@ class AnnotationsObsAPI(Resource):
try: try:
annotation_response = current_app.data.annotation(current_app.data.data, "obs", fields) annotation_response = current_app.data.annotation(current_app.data.data, "obs", fields)
except KeyError: except KeyError:
return make_response(f"Error bad key in {fields}", 404) return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST)
return make_response(jsonify(annotation_response)) return make_response(jsonify(annotation_response), HTTPStatus.OK)
@swagger.doc({ @swagger.doc({
"summary": "Fetch annotations (metadata) for filtered subset of observations.", "summary": "Fetch annotations (metadata) for filtered subset of observations.",
@@ -238,17 +254,24 @@ class AnnotationsObsAPI(Resource):
} }
} }
},
"400": {
"description": "malformed filter or one or more of the annotation-name identifiers were"
"not associated with an annotation name"
} }
} }
}) })
def put(self): def put(self):
fields = request.args.getlist("annotation-name", None) fields = request.args.getlist("annotation-name", None)
df = current_app.data.filter_dataframe(request.get_json()["filter"], include_uns=False) try:
df = current_app.data.filter_dataframe(request.get_json()["filter"], include_uns=False)
except KeyError:
return make_response("Malformed filter", HTTPStatus.BAD_REQUEST)
try: try:
annotation_response = current_app.data.annotation(df, "obs", fields) annotation_response = current_app.data.annotation(df, "obs", fields)
except KeyError: except KeyError:
return make_response(f"Error bad key in {fields}", 404) return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST)
return make_response(jsonify(annotation_response)) return make_response(jsonify(annotation_response), HTTPStatus.OK)
class AnnotationsVarAPI(Resource): class AnnotationsVarAPI(Resource):
@@ -277,6 +300,10 @@ class AnnotationsVarAPI(Resource):
} }
} }
},
"400": {
"description": "one or more of the annotation-name identifiers were not associated with an"
" annotation name"
} }
} }
}) })
@@ -285,8 +312,8 @@ class AnnotationsVarAPI(Resource):
try: try:
annotation_response = current_app.data.annotation(current_app.data.data, "var", fields) annotation_response = current_app.data.annotation(current_app.data.data, "var", fields)
except KeyError: except KeyError:
return make_response(f"Error bad key in {fields}", 404) return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST)
return make_response(jsonify(annotation_response)) return make_response(jsonify(annotation_response), HTTPStatus.OK)
@swagger.doc({ @swagger.doc({
"summary": "Fetch annotations (metadata) for filtered subset of variables.", "summary": "Fetch annotations (metadata) for filtered subset of variables.",
@@ -320,17 +347,24 @@ class AnnotationsVarAPI(Resource):
] ]
} }
} }
},
"400": {
"description": "malformed filter or one or more of the annotation-name identifiers were"
"not associated with an annotation name"
} }
} }
}) })
def put(self): def put(self):
fields = request.args.getlist("annotation-name", None) fields = request.args.getlist("annotation-name", None)
df = current_app.data.filter_dataframe(request.get_json()["filter"], include_uns=False) try:
df = current_app.data.filter_dataframe(request.get_json()["filter"], include_uns=False)
except KeyError:
return make_response("Malformed filter", HTTPStatus.BAD_REQUEST)
try: try:
annotation_response = current_app.data.annotation(df, "var", fields) annotation_response = current_app.data.annotation(df, "var", fields)
except KeyError: except KeyError:
return make_response(f"Error bad key in {fields}", 404) return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST)
return make_response(jsonify(annotation_response)) return make_response(jsonify(annotation_response), HTTPStatus.OK)
class DiffExpObsAPI(Resource): class DiffExpObsAPI(Resource):
@@ -383,6 +417,15 @@ class DiffExpObsAPI(Resource):
[1250, -2.569489, 2.655706e-63, 3.642036e-57, 383.393, 583.9], [1250, -2.569489, 2.655706e-63, 3.642036e-57, 383.393, 583.9],
] ]
} }
},
"400": {
"description": "malformed filter"
},
"403": {
"description": "non-interactive request"
},
"501": {
"description": "diffexp is not implemented"
} }
} }
}) })
@@ -392,24 +435,24 @@ class DiffExpObsAPI(Resource):
try: try:
mode = DiffExpMode(args["mode"]) mode = DiffExpMode(args["mode"])
except KeyError: except KeyError:
return make_response("Error: mode is required", 400) return make_response("Error: mode is required", HTTPStatus.BAD_REQUEST)
except ValueError: except ValueError:
return make_response(f"Error: invalid mode option {args['mode']}", 400) return make_response(f"Error: invalid mode option {args['mode']}", HTTPStatus.BAD_REQUEST)
# Validate filters # Validate filters
if mode == DiffExpMode.VAR_FILTER: if mode == DiffExpMode.VAR_FILTER:
if "varFilter" not in args: if "varFilter" not in args:
return make_response("varFilter is required when mode is set to varFilter ", 400) return make_response("varFilter is required when mode is set to varFilter ", HTTPStatus.BAD_REQUEST)
if Axis.OBS in args["varFilter"]["filter"]: if Axis.OBS in args["varFilter"]["filter"]:
return make_response("Obs filter not allowed in varFilter", 400) return make_response("Obs filter not allowed in varFilter", HTTPStatus.BAD_REQUEST)
if "set1" not in args: if "set1" not in args:
return make_response("set1 is required.", 400) return make_response("set1 is required.", HTTPStatus.BAD_REQUEST)
if Axis.VAR in args["set1"]["filter"]: if Axis.VAR in args["set1"]["filter"]:
return make_response("Var filter not allowed for set1", 400) return make_response("Var filter not allowed for set1", HTTPStatus.BAD_REQUEST)
# set2 # set2
if "set2" not in args: if "set2" not in args:
return make_response("Set2 as inverse of set1 is not implemented", 501) return make_response("Set2 as inverse of set1 is not implemented", HTTPStatus.NOT_IMPLEMENTED)
if Axis.VAR in args["set2"]["filter"]: if Axis.VAR in args["set2"]["filter"]:
return make_response("Var filter not allowed for set2", 400) return make_response("Var filter not allowed for set2", HTTPStatus.BAD_REQUEST)
set1_filter = args["set1"]["filter"] set1_filter = args["set1"]["filter"]
set2_filter = args.get("set2", {"filter": {}})["filter"] set2_filter = args.get("set2", {"filter": {}})["filter"]
if "varFilter" in args: if "varFilter" in args:
@@ -420,14 +463,14 @@ class DiffExpObsAPI(Resource):
df2 = current_app.data.filter_dataframe(set2_filter, include_uns=False) df2 = current_app.data.filter_dataframe(set2_filter, include_uns=False)
# exceeds size limit # exceeds size limit
if df1.shape[0] + df2.shape[0] > current_app.data.features["diffexp"]["interactiveLimit"]: if df1.shape[0] + df2.shape[0] > current_app.data.features["diffexp"]["interactiveLimit"]:
return make_response("Non-interactive request", 403) return make_response("Non-interactive request", HTTPStatus.FORBIDDEN)
# mode # mode
count = args.get("count", None) count = args.get("count", None)
try: try:
diffexp = current_app.data.diffexp(df1, df2, count) diffexp = current_app.data.diffexp(df1, df2, count)
except ValueError as ve: except ValueError as ve:
return make_response(ve.message, 400) return make_response(ve.message, HTTPStatus.BAD_REQUEST)
return make_response(jsonify(diffexp)) return make_response(jsonify(diffexp), HTTPStatus.OK)
class DataObsAPI(Resource): class DataObsAPI(Resource):
@@ -469,19 +512,26 @@ class DataObsAPI(Resource):
} }
}) })
def get(self): def get(self):
accept_type = request.args.get("accept-type", None)
# request.args is immutable # request.args is immutable
args = dict(request.args) args = dict(request.args)
accept_type = args.pop("accept-type", None) args.pop("accept-type", None)
try: try:
filter_ = parse_filter(ImmutableMultiDict(args), current_app.data.schema['annotations']) filter_ = parse_filter(ImmutableMultiDict(args), current_app.data.schema['annotations'])
except QueryStringError as e: except QueryStringError as e:
return make_response(e.message, HTTPStatus.BAD_REQUEST) return make_response(e.message, HTTPStatus.BAD_REQUEST)
df = current_app.data.filter_dataframe(filter_, include_uns=False) try:
if accept_type and accept_type[0] == "application/json": df = current_app.data.filter_dataframe(filter_, include_uns=False)
return make_response((jsonify(current_app.data.data_frame(df, axis=Axis.OBS)))) except KeyError:
return make_response("malformed filter", HTTPStatus.BAD_REQUEST)
# TODO support CSV # TODO support CSV
else: try:
return make_response(f"Unsupported accept-type: {accept_type}", HTTPStatus.NOT_ACCEPTABLE) # TODO store mime_type when more than one is supported
get_mime_type(acceptable_types=["application/json"], query_param=accept_type,
header=request.accept_mimetypes)
except MimeTypeError as e:
return make_response(e.message, HTTPStatus.NOT_ACCEPTABLE)
return make_response((jsonify(current_app.data.data_frame(df, axis=Axis.OBS))), HTTPStatus.OK)
@swagger.doc({ @swagger.doc({
"summary": "Get data (expression values) from the dataframe.", "summary": "Get data (expression values) from the dataframe.",
@@ -517,13 +567,15 @@ class DataObsAPI(Resource):
def put(self): def put(self):
if not request.accept_mimetypes.best_match(["application/json", "text/csv"]): if not request.accept_mimetypes.best_match(["application/json", "text/csv"]):
return make_response(f"Unsupported MIME type '{request.accept_mimetypes}'", HTTPStatus.NOT_ACCEPTABLE) return make_response(f"Unsupported MIME type '{request.accept_mimetypes}'", HTTPStatus.NOT_ACCEPTABLE)
# TODO catch error for bad filter try:
df = current_app.data.filter_dataframe(request.get_json()["filter"], include_uns=False) df = current_app.data.filter_dataframe(request.get_json()["filter"], include_uns=False)
if request.accept_mimetypes.best_match(['application/json']): except KeyError:
return make_response((jsonify(current_app.data.data_frame(df, axis=Axis.OBS)))) return make_response("malformed filter", HTTPStatus.BAD_REQUEST)
# TODO support CSV try:
else: get_mime_type(acceptable_types=["application/json"], header=request.accept_mimetypes)
return make_response(f"Unsupported MIME type '{request.accept_mimetypes}'", HTTPStatus.NOT_ACCEPTABLE) except MimeTypeError as e:
return make_response(e.message, HTTPStatus.NOT_ACCEPTABLE)
return make_response((jsonify(current_app.data.data_frame(df, axis=Axis.OBS))), HTTPStatus.OK)
class DataVarAPI(Resource): class DataVarAPI(Resource):
@@ -565,19 +617,24 @@ class DataVarAPI(Resource):
} }
}) })
def get(self): def get(self):
accept_type = request.args.get("accept-type", None)
# request.args is immutable # request.args is immutable
args = dict(request.args) args = dict(request.args)
accept_type = args.pop("accept-type", None) args.pop("accept-type", None)
try: try:
filter_ = parse_filter(ImmutableMultiDict(args), current_app.data.schema['annotations']) filter_ = parse_filter(ImmutableMultiDict(args), current_app.data.schema['annotations'])
except QueryStringError as e: except QueryStringError as e:
return make_response(e.message, HTTPStatus.BAD_REQUEST) return make_response(e.message, HTTPStatus.BAD_REQUEST)
df = current_app.data.filter_dataframe(filter_, include_uns=False) try:
if accept_type and accept_type[0] == "application/json": df = current_app.data.filter_dataframe(filter_, include_uns=False)
return make_response((jsonify(current_app.data.data_frame(df, axis=Axis.VAR)))) except KeyError:
# TODO support CSV return make_response("malformed filter", HTTPStatus.BAD_REQUEST)
else: try:
return make_response(f"Unsupported accept-type: {accept_type}", HTTPStatus.NOT_ACCEPTABLE) get_mime_type(acceptable_types=["application/json"], query_param=accept_type,
header=request.accept_mimetypes)
except MimeTypeError as e:
return make_response(e.message, HTTPStatus.NOT_ACCEPTABLE)
return make_response((jsonify(current_app.data.data_frame(df, axis=Axis.VAR))), HTTPStatus.OK)
@swagger.doc({ @swagger.doc({
"summary": "Get data (expression values) from the dataframe.", "summary": "Get data (expression values) from the dataframe.",
@@ -613,13 +670,16 @@ class DataVarAPI(Resource):
def put(self): def put(self):
if not request.accept_mimetypes.best_match(["application/json", "text/csv"]): if not request.accept_mimetypes.best_match(["application/json", "text/csv"]):
return make_response(f"Unsupported MIME type '{request.accept_mimetypes}'", HTTPStatus.NOT_ACCEPTABLE) return make_response(f"Unsupported MIME type '{request.accept_mimetypes}'", HTTPStatus.NOT_ACCEPTABLE)
# TODO catch error for bad filter try:
df = current_app.data.filter_dataframe(request.get_json()["filter"], include_uns=False) df = current_app.data.filter_dataframe(request.get_json()["filter"], include_uns=False)
if request.accept_mimetypes.best_match(['application/json']): except KeyError:
return make_response((jsonify(current_app.data.data_frame(df, axis=Axis.VAR)))) return make_response("malformed filter", HTTPStatus.BAD_REQUEST)
# TODO support CSV # TODO support CSV
else: try:
return make_response(f"Unsupported MIME type '{request.accept_mimetypes}'", HTTPStatus.NOT_ACCEPTABLE) get_mime_type(acceptable_types=["application/json"], header=request.accept_mimetypes)
except MimeTypeError as e:
return make_response(e.message, HTTPStatus.NOT_ACCEPTABLE)
return make_response((jsonify(current_app.data.data_frame(df, axis=Axis.VAR))), HTTPStatus.OK)
def get_api_resources(): def get_api_resources():
+21
View File
@@ -10,3 +10,24 @@ class Float32JSONEncoder(json.JSONEncoder):
elif isinstance(obj, integer): elif isinstance(obj, integer):
return int(obj) return int(obj)
return json.JSONEncoder.default(self, obj) return json.JSONEncoder.default(self, obj)
class MimeTypeError(Exception):
def __init__(self, message):
self.message = message
def get_mime_type(default="application/json", acceptable_types=["application/json", "text/csv"], query_param=None,
header=None):
mime_type = default
if query_param:
if query_param in acceptable_types:
mime_type = query_param
else:
raise MimeTypeError(f"Unsupported mime type {query_param} specified in query parameter 'accept-type'")
elif len(header):
mime_type = header.best_match(acceptable_types)
if not mime_type:
raise MimeTypeError(f"Unsupported mime type(s) {header} in HTTP Accept header")
return mime_type
+57 -25
View File
@@ -1,12 +1,24 @@
import requests from http import HTTPStatus
from subprocess import Popen from subprocess import Popen
import unittest import unittest
import time import time
import requests
LOCAL_URL = "http://127.0.0.1:5005/" LOCAL_URL = "http://127.0.0.1:5005/"
VERSION = "v0.2" VERSION = "v0.2"
URL_BASE = f"{LOCAL_URL}api/{VERSION}/" URL_BASE = f"{LOCAL_URL}api/{VERSION}/"
BAD_FILTER = {
"filter": {
"obs": {
"annotation_value": [
{"name": "xyz"},
],
}
}
}
class EndPoints(unittest.TestCase): class EndPoints(unittest.TestCase):
"""Test Case for endpoints""" """Test Case for endpoints"""
@@ -35,7 +47,7 @@ class EndPoints(unittest.TestCase):
endpoint = "schema" endpoint = "schema"
url = f"{URL_BASE}{endpoint}" url = f"{URL_BASE}{endpoint}"
result = self.session.get(url) result = self.session.get(url)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, HTTPStatus.OK)
result_data = result.json() result_data = result.json()
self.assertEqual(result_data["schema"]["dataframe"]["nObs"], 2638) self.assertEqual(result_data["schema"]["dataframe"]["nObs"], 2638)
self.assertEqual(len(result_data["schema"]["annotations"]["obs"]), 5) self.assertEqual(len(result_data["schema"]["annotations"]["obs"]), 5)
@@ -44,7 +56,7 @@ class EndPoints(unittest.TestCase):
endpoint = "config" endpoint = "config"
url = f"{URL_BASE}{endpoint}" url = f"{URL_BASE}{endpoint}"
result = self.session.get(url) result = self.session.get(url)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, HTTPStatus.OK)
result_data = result.json() result_data = result.json()
self.assertEqual(result_data["config"]["displayNames"]["dataset"], "example-dataset") self.assertEqual(result_data["config"]["displayNames"]["dataset"], "example-dataset")
self.assertEqual(len(result_data["config"]["features"]), 4) self.assertEqual(len(result_data["config"]["features"]), 4)
@@ -53,7 +65,7 @@ class EndPoints(unittest.TestCase):
endpoint = "layout/obs" endpoint = "layout/obs"
url = f"{URL_BASE}{endpoint}" url = f"{URL_BASE}{endpoint}"
result = self.session.get(url) result = self.session.get(url)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, HTTPStatus.OK)
result_data = result.json() result_data = result.json()
self.assertEqual(result_data["layout"]["ndims"], 2) self.assertEqual(result_data["layout"]["ndims"], 2)
self.assertEqual(len(result_data["layout"]["coordinates"]), 2638) self.assertEqual(len(result_data["layout"]["coordinates"]), 2638)
@@ -73,15 +85,28 @@ class EndPoints(unittest.TestCase):
} }
} }
result = self.session.post(url, json=obs_filter) result = self.session.post(url, json=obs_filter)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, HTTPStatus.OK)
result_data = result.json() result_data = result.json()
self.assertEqual(len(result_data["layout"]["coordinates"]), 15) self.assertEqual(len(result_data["layout"]["coordinates"]), 15)
def test_bad_filter(self):
endpoints = {
"layout/obs": "post",
"annotations/obs": "put",
"annotations/var": "put",
"data/obs": "put",
"data/var": "put"
}
for endpoint, method in endpoints.items():
url = f"{URL_BASE}{endpoint}"
result = getattr(self.session, method)(url, json=BAD_FILTER)
self.assertEqual(result.status_code, HTTPStatus.BAD_REQUEST)
def test_get_annotations_obs(self): def test_get_annotations_obs(self):
endpoint = "annotations/obs" endpoint = "annotations/obs"
url = f"{URL_BASE}{endpoint}" url = f"{URL_BASE}{endpoint}"
result = self.session.get(url) result = self.session.get(url)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, HTTPStatus.OK)
result_data = result.json() result_data = result.json()
self.assertEqual(result_data["names"], ["n_genes", "percent_mito", "n_counts", "louvain", "name"]) self.assertEqual(result_data["names"], ["n_genes", "percent_mito", "n_counts", "louvain", "name"])
self.assertEqual(len(result_data["data"]), 2638) self.assertEqual(len(result_data["data"]), 2638)
@@ -92,7 +117,7 @@ class EndPoints(unittest.TestCase):
query = "annotation-name=n_genes&annotation-name=percent_mito" query = "annotation-name=n_genes&annotation-name=percent_mito"
url = f"{URL_BASE}{endpoint}?{query}" url = f"{URL_BASE}{endpoint}?{query}"
result = self.session.get(url) result = self.session.get(url)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, HTTPStatus.OK)
result_data = result.json() result_data = result.json()
self.assertEqual(result_data["names"], ["n_genes", "percent_mito"]) self.assertEqual(result_data["names"], ["n_genes", "percent_mito"])
self.assertEqual(len(result_data["data"][0]), 3) self.assertEqual(len(result_data["data"][0]), 3)
@@ -102,7 +127,7 @@ class EndPoints(unittest.TestCase):
query = "annotation-name=notakey" query = "annotation-name=notakey"
url = f"{URL_BASE}{endpoint}?{query}" url = f"{URL_BASE}{endpoint}?{query}"
result = self.session.get(url) result = self.session.get(url)
self.assertEqual(result.status_code, 404) self.assertEqual(result.status_code, HTTPStatus.BAD_REQUEST)
def test_put_annotations_obs(self): def test_put_annotations_obs(self):
endpoint = "annotations/obs" endpoint = "annotations/obs"
@@ -119,7 +144,7 @@ class EndPoints(unittest.TestCase):
} }
} }
result = self.session.put(url, json=obs_filter) result = self.session.put(url, json=obs_filter)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, HTTPStatus.OK)
result_data = result.json() result_data = result.json()
self.assertEqual(result_data["names"], ["n_genes", "percent_mito", "n_counts", "louvain", "name"]) self.assertEqual(result_data["names"], ["n_genes", "percent_mito", "n_counts", "louvain", "name"])
self.assertEqual(len(result_data["data"]), 15) self.assertEqual(len(result_data["data"]), 15)
@@ -140,7 +165,7 @@ class EndPoints(unittest.TestCase):
} }
} }
result = self.session.put(url, json=obs_filter) result = self.session.put(url, json=obs_filter)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, HTTPStatus.OK)
result_data = result.json() result_data = result.json()
self.assertEqual(result_data["names"], ["n_genes", "percent_mito"]) self.assertEqual(result_data["names"], ["n_genes", "percent_mito"])
self.assertEqual(len(result_data["data"][0]), 3) self.assertEqual(len(result_data["data"][0]), 3)
@@ -170,7 +195,7 @@ class EndPoints(unittest.TestCase):
"count": 7 "count": 7
} }
result = self.session.post(url, json=params) result = self.session.post(url, json=params)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, HTTPStatus.OK)
result_data = result.json() result_data = result.json()
self.assertEqual(len(result_data), 7) self.assertEqual(len(result_data), 7)
@@ -195,7 +220,7 @@ class EndPoints(unittest.TestCase):
} }
} }
result = self.session.post(url, json=params) result = self.session.post(url, json=params)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, HTTPStatus.OK)
result_data = result.json() result_data = result.json()
self.assertEqual(len(result_data), 10) self.assertEqual(len(result_data), 10)
@@ -203,7 +228,7 @@ class EndPoints(unittest.TestCase):
endpoint = "annotations/var" endpoint = "annotations/var"
url = f"{URL_BASE}{endpoint}" url = f"{URL_BASE}{endpoint}"
result = self.session.get(url) result = self.session.get(url)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, HTTPStatus.OK)
result_data = result.json() result_data = result.json()
self.assertEqual(result_data["names"], ["n_cells", "name"]) self.assertEqual(result_data["names"], ["n_cells", "name"])
self.assertEqual(len(result_data["data"]), 1838) self.assertEqual(len(result_data["data"]), 1838)
@@ -214,7 +239,7 @@ class EndPoints(unittest.TestCase):
query = "annotation-name=n_cells" query = "annotation-name=n_cells"
url = f"{URL_BASE}{endpoint}?{query}" url = f"{URL_BASE}{endpoint}?{query}"
result = self.session.get(url) result = self.session.get(url)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, HTTPStatus.OK)
result_data = result.json() result_data = result.json()
self.assertEqual(result_data["names"], ["n_cells"]) self.assertEqual(result_data["names"], ["n_cells"])
self.assertEqual(len(result_data["data"][0]), 2) self.assertEqual(len(result_data["data"][0]), 2)
@@ -224,7 +249,7 @@ class EndPoints(unittest.TestCase):
query = "annotation-name=notakey" query = "annotation-name=notakey"
url = f"{URL_BASE}{endpoint}?{query}" url = f"{URL_BASE}{endpoint}?{query}"
result = self.session.get(url) result = self.session.get(url)
self.assertEqual(result.status_code, 404) self.assertEqual(result.status_code, HTTPStatus.BAD_REQUEST)
def test_put_annotations_var(self): def test_put_annotations_var(self):
endpoint = "annotations/var" endpoint = "annotations/var"
@@ -239,7 +264,7 @@ class EndPoints(unittest.TestCase):
} }
} }
result = self.session.put(url, json=var_filter) result = self.session.put(url, json=var_filter)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, HTTPStatus.OK)
result_data = result.json() result_data = result.json()
self.assertEqual(result_data["names"], ["n_cells", "name"]) self.assertEqual(result_data["names"], ["n_cells", "name"])
self.assertEqual(len(result_data["data"]), 2) self.assertEqual(len(result_data["data"]), 2)
@@ -258,7 +283,7 @@ class EndPoints(unittest.TestCase):
} }
} }
result = self.session.put(url, json=var_filter) result = self.session.put(url, json=var_filter)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, HTTPStatus.OK)
result_data = result.json() result_data = result.json()
self.assertEqual(result_data["names"], ["n_cells"]) self.assertEqual(result_data["names"], ["n_cells"])
self.assertEqual(len(result_data["data"][0]), 2) self.assertEqual(len(result_data["data"][0]), 2)
@@ -270,7 +295,7 @@ class EndPoints(unittest.TestCase):
query = "accept-type=application/json" query = "accept-type=application/json"
url = f"{URL_BASE}{endpoint}?{query}" url = f"{URL_BASE}{endpoint}?{query}"
result = self.session.get(url) result = self.session.get(url)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, HTTPStatus.OK)
result_data = result.json() result_data = result.json()
self.assertEqual(len(result_data["obs"]), 2638) self.assertEqual(len(result_data["obs"]), 2638)
@@ -280,11 +305,18 @@ class EndPoints(unittest.TestCase):
query = "accept-type=xxx" query = "accept-type=xxx"
url = f"{URL_BASE}{endpoint}?{query}" url = f"{URL_BASE}{endpoint}?{query}"
result = self.session.get(url) result = self.session.get(url)
self.assertEqual(result.status_code, 406) self.assertEqual(result.status_code, HTTPStatus.NOT_ACCEPTABLE)
# no accept type url = f"{URL_BASE}{endpoint}"
header = {"Accept": "sdkljfa;dsjalkj"}
result = self.session.get(url, headers=header)
self.assertEqual(result.status_code, HTTPStatus.NOT_ACCEPTABLE)
def test_json_default(self):
for axis in ["obs", "var"]:
endpoint = f"data/{axis}"
url = f"{URL_BASE}{endpoint}" url = f"{URL_BASE}{endpoint}"
result = self.session.get(url) result = self.session.get(url)
self.assertEqual(result.status_code, 406) self.assertEqual(result.status_code, HTTPStatus.OK)
def test_data_filter(self): def test_data_filter(self):
for axis in ["obs", "var"]: for axis in ["obs", "var"]:
@@ -292,7 +324,7 @@ class EndPoints(unittest.TestCase):
query = "accept-type=application/json&obs:louvain=NK cells&obs:louvain=CD8 T cells&obs:n_counts=3000,*" query = "accept-type=application/json&obs:louvain=NK cells&obs:louvain=CD8 T cells&obs:n_counts=3000,*"
url = f"{URL_BASE}{endpoint}?{query}" url = f"{URL_BASE}{endpoint}?{query}"
result = self.session.get(url) result = self.session.get(url)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, HTTPStatus.OK)
result_data = result.json() result_data = result.json()
self.assertEqual(len(result_data["obs"]), 38) self.assertEqual(len(result_data["obs"]), 38)
@@ -313,7 +345,7 @@ class EndPoints(unittest.TestCase):
} }
} }
result = self.session.put(url, headers=header, json=obs_filter) result = self.session.put(url, headers=header, json=obs_filter)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, HTTPStatus.OK)
result_data = result.json() result_data = result.json()
self.assertEqual(len(result_data["obs"]), 15) self.assertEqual(len(result_data["obs"]), 15)
@@ -332,7 +364,7 @@ class EndPoints(unittest.TestCase):
} }
} }
result = self.session.put(url, headers=header, json=var_filter) result = self.session.put(url, headers=header, json=var_filter)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, HTTPStatus.OK)
result_data = result.json() result_data = result.json()
if axis == "obs": if axis == "obs":
self.assertEqual(len(result_data["obs"][0]), 2) self.assertEqual(len(result_data["obs"][0]), 2)
@@ -346,4 +378,4 @@ class EndPoints(unittest.TestCase):
file = "js/service-worker.js" file = "js/service-worker.js"
url = f"{LOCAL_URL}{endpoint}/{file}" url = f"{LOCAL_URL}{endpoint}/{file}"
result = self.session.get(url) result = self.session.get(url)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, HTTPStatus.OK)