From 11ef5ae51bc8311ce8e6a7b0b56ecf9c11f4483f Mon Sep 17 00:00:00 2001 From: Charlotte Weaver Date: Mon, 8 Oct 2018 14:17:56 -0700 Subject: [PATCH] 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 --- docs/REST_API.md | 5 +- server/app/rest_api/rest.py | 166 ++++++++++++++++++++++++------------ server/app/util/utils.py | 21 +++++ server/test/test_api.py | 82 ++++++++++++------ 4 files changed, 193 insertions(+), 81 deletions(-) diff --git a/docs/REST_API.md b/docs/REST_API.md index 24bc3b46..3a95fbc8 100644 --- a/docs/REST_API.md +++ b/docs/REST_API.md @@ -300,7 +300,7 @@ Observations and variables are guaranteed to have a `name` annotation, which sho **Response code:** - 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. @@ -342,8 +342,7 @@ Same as the `GET /annotations` routes, with additional ability to filter by obse **Response code:** - 200 Success -- 400 Bad Request - malformed filter -- 404 Not Found - one or more of the annotation-name identifiers were not associated with an annotation name. +- 400 Bad Request - malformed filter or one or more of the annotation-name identifiers were not associated with an annotation name. **Response body:** diff --git a/server/app/rest_api/rest.py b/server/app/rest_api/rest.py index 5bffafa6..cf229fef 100644 --- a/server/app/rest_api/rest.py +++ b/server/app/rest_api/rest.py @@ -10,6 +10,7 @@ from werkzeug.datastructures import ImmutableMultiDict from server.app.util.constants import Axis, DiffExpMode from server.app.util.filter import parse_filter, QueryStringError from server.app.util.models import FilterModel +from server.app.util.utils import MimeTypeError, get_mime_type class SchemaAPI(Resource): @@ -54,7 +55,7 @@ class SchemaAPI(Resource): }) 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): @@ -105,7 +106,7 @@ class ConfigAPI(Resource): } } } - return make_response(jsonify(config), 200) + return make_response(jsonify(config), HTTPStatus.OK) class LayoutObsAPI(Resource): @@ -131,7 +132,7 @@ class LayoutObsAPI(Resource): } }) 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({ "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): - df = current_app.data.filter_dataframe(request.get_json()["filter"]) - return make_response((jsonify({"layout": current_app.data.layout(df)}))) + try: + 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): @@ -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: annotation_response = current_app.data.annotation(current_app.data.data, "obs", fields) except KeyError: - return make_response(f"Error bad key in {fields}", 404) - return make_response(jsonify(annotation_response)) + return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST) + return make_response(jsonify(annotation_response), HTTPStatus.OK) @swagger.doc({ "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): 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: annotation_response = current_app.data.annotation(df, "obs", fields) except KeyError: - return make_response(f"Error bad key in {fields}", 404) - return make_response(jsonify(annotation_response)) + return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST) + return make_response(jsonify(annotation_response), HTTPStatus.OK) 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: annotation_response = current_app.data.annotation(current_app.data.data, "var", fields) except KeyError: - return make_response(f"Error bad key in {fields}", 404) - return make_response(jsonify(annotation_response)) + return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST) + return make_response(jsonify(annotation_response), HTTPStatus.OK) @swagger.doc({ "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): 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: annotation_response = current_app.data.annotation(df, "var", fields) except KeyError: - return make_response(f"Error bad key in {fields}", 404) - return make_response(jsonify(annotation_response)) + return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST) + return make_response(jsonify(annotation_response), HTTPStatus.OK) class DiffExpObsAPI(Resource): @@ -383,6 +417,15 @@ class DiffExpObsAPI(Resource): [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: mode = DiffExpMode(args["mode"]) except KeyError: - return make_response("Error: mode is required", 400) + return make_response("Error: mode is required", HTTPStatus.BAD_REQUEST) 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 if mode == DiffExpMode.VAR_FILTER: 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"]: - 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: - return make_response("set1 is required.", 400) + 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", 400) + 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", 501) + 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", 400) + return make_response("Var filter not allowed for set2", HTTPStatus.BAD_REQUEST) set1_filter = args["set1"]["filter"] set2_filter = args.get("set2", {"filter": {}})["filter"] if "varFilter" in args: @@ -420,14 +463,14 @@ class DiffExpObsAPI(Resource): df2 = current_app.data.filter_dataframe(set2_filter, include_uns=False) # exceeds size limit 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 count = args.get("count", None) try: diffexp = current_app.data.diffexp(df1, df2, count) except ValueError as ve: - return make_response(ve.message, 400) - return make_response(jsonify(diffexp)) + return make_response(ve.message, HTTPStatus.BAD_REQUEST) + return make_response(jsonify(diffexp), HTTPStatus.OK) class DataObsAPI(Resource): @@ -469,19 +512,26 @@ class DataObsAPI(Resource): } }) def get(self): + accept_type = request.args.get("accept-type", None) # request.args is immutable args = dict(request.args) - accept_type = args.pop("accept-type", None) + args.pop("accept-type", None) try: filter_ = parse_filter(ImmutableMultiDict(args), current_app.data.schema['annotations']) except QueryStringError as e: return make_response(e.message, HTTPStatus.BAD_REQUEST) - df = current_app.data.filter_dataframe(filter_, include_uns=False) - if accept_type and accept_type[0] == "application/json": - return make_response((jsonify(current_app.data.data_frame(df, axis=Axis.OBS)))) + try: + df = current_app.data.filter_dataframe(filter_, include_uns=False) + except KeyError: + return make_response("malformed filter", HTTPStatus.BAD_REQUEST) # TODO support CSV - else: - return make_response(f"Unsupported accept-type: {accept_type}", HTTPStatus.NOT_ACCEPTABLE) + try: + # 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({ "summary": "Get data (expression values) from the dataframe.", @@ -517,13 +567,15 @@ class DataObsAPI(Resource): def put(self): if not request.accept_mimetypes.best_match(["application/json", "text/csv"]): return make_response(f"Unsupported MIME type '{request.accept_mimetypes}'", HTTPStatus.NOT_ACCEPTABLE) - # TODO catch error for bad filter - df = current_app.data.filter_dataframe(request.get_json()["filter"], include_uns=False) - if request.accept_mimetypes.best_match(['application/json']): - return make_response((jsonify(current_app.data.data_frame(df, axis=Axis.OBS)))) - # TODO support CSV - else: - return make_response(f"Unsupported MIME type '{request.accept_mimetypes}'", HTTPStatus.NOT_ACCEPTABLE) + 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: + 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.OBS))), HTTPStatus.OK) class DataVarAPI(Resource): @@ -565,19 +617,24 @@ class DataVarAPI(Resource): } }) def get(self): + accept_type = request.args.get("accept-type", None) # request.args is immutable args = dict(request.args) - accept_type = args.pop("accept-type", None) + args.pop("accept-type", None) try: filter_ = parse_filter(ImmutableMultiDict(args), current_app.data.schema['annotations']) except QueryStringError as e: return make_response(e.message, HTTPStatus.BAD_REQUEST) - df = current_app.data.filter_dataframe(filter_, include_uns=False) - if accept_type and accept_type[0] == "application/json": - return make_response((jsonify(current_app.data.data_frame(df, axis=Axis.VAR)))) - # TODO support CSV - else: - return make_response(f"Unsupported accept-type: {accept_type}", HTTPStatus.NOT_ACCEPTABLE) + try: + df = current_app.data.filter_dataframe(filter_, include_uns=False) + except KeyError: + return make_response("malformed filter", HTTPStatus.BAD_REQUEST) + try: + 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({ "summary": "Get data (expression values) from the dataframe.", @@ -613,13 +670,16 @@ class DataVarAPI(Resource): def put(self): if not request.accept_mimetypes.best_match(["application/json", "text/csv"]): return make_response(f"Unsupported MIME type '{request.accept_mimetypes}'", HTTPStatus.NOT_ACCEPTABLE) - # TODO catch error for bad filter - df = current_app.data.filter_dataframe(request.get_json()["filter"], include_uns=False) - if request.accept_mimetypes.best_match(['application/json']): - return make_response((jsonify(current_app.data.data_frame(df, axis=Axis.VAR)))) + try: + df = current_app.data.filter_dataframe(request.get_json()["filter"], include_uns=False) + except KeyError: + return make_response("malformed filter", HTTPStatus.BAD_REQUEST) # TODO support CSV - else: - return make_response(f"Unsupported MIME type '{request.accept_mimetypes}'", HTTPStatus.NOT_ACCEPTABLE) + try: + 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(): diff --git a/server/app/util/utils.py b/server/app/util/utils.py index 68b29995..7c4419ff 100644 --- a/server/app/util/utils.py +++ b/server/app/util/utils.py @@ -10,3 +10,24 @@ class Float32JSONEncoder(json.JSONEncoder): elif isinstance(obj, integer): return int(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 diff --git a/server/test/test_api.py b/server/test/test_api.py index 63253cc2..e5c551f4 100644 --- a/server/test/test_api.py +++ b/server/test/test_api.py @@ -1,12 +1,24 @@ -import requests +from http import HTTPStatus from subprocess import Popen import unittest import time +import requests + LOCAL_URL = "http://127.0.0.1:5005/" VERSION = "v0.2" URL_BASE = f"{LOCAL_URL}api/{VERSION}/" +BAD_FILTER = { + "filter": { + "obs": { + "annotation_value": [ + {"name": "xyz"}, + ], + } + } + } + class EndPoints(unittest.TestCase): """Test Case for endpoints""" @@ -35,7 +47,7 @@ class EndPoints(unittest.TestCase): endpoint = "schema" url = f"{URL_BASE}{endpoint}" result = self.session.get(url) - self.assertEqual(result.status_code, 200) + self.assertEqual(result.status_code, HTTPStatus.OK) result_data = result.json() self.assertEqual(result_data["schema"]["dataframe"]["nObs"], 2638) self.assertEqual(len(result_data["schema"]["annotations"]["obs"]), 5) @@ -44,7 +56,7 @@ class EndPoints(unittest.TestCase): endpoint = "config" url = f"{URL_BASE}{endpoint}" result = self.session.get(url) - self.assertEqual(result.status_code, 200) + self.assertEqual(result.status_code, HTTPStatus.OK) result_data = result.json() self.assertEqual(result_data["config"]["displayNames"]["dataset"], "example-dataset") self.assertEqual(len(result_data["config"]["features"]), 4) @@ -53,7 +65,7 @@ class EndPoints(unittest.TestCase): endpoint = "layout/obs" url = f"{URL_BASE}{endpoint}" result = self.session.get(url) - self.assertEqual(result.status_code, 200) + self.assertEqual(result.status_code, HTTPStatus.OK) result_data = result.json() self.assertEqual(result_data["layout"]["ndims"], 2) self.assertEqual(len(result_data["layout"]["coordinates"]), 2638) @@ -73,15 +85,28 @@ class EndPoints(unittest.TestCase): } } 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() 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): endpoint = "annotations/obs" url = f"{URL_BASE}{endpoint}" result = self.session.get(url) - self.assertEqual(result.status_code, 200) + self.assertEqual(result.status_code, HTTPStatus.OK) result_data = result.json() self.assertEqual(result_data["names"], ["n_genes", "percent_mito", "n_counts", "louvain", "name"]) self.assertEqual(len(result_data["data"]), 2638) @@ -92,7 +117,7 @@ class EndPoints(unittest.TestCase): query = "annotation-name=n_genes&annotation-name=percent_mito" url = f"{URL_BASE}{endpoint}?{query}" result = self.session.get(url) - self.assertEqual(result.status_code, 200) + self.assertEqual(result.status_code, HTTPStatus.OK) result_data = result.json() self.assertEqual(result_data["names"], ["n_genes", "percent_mito"]) self.assertEqual(len(result_data["data"][0]), 3) @@ -102,7 +127,7 @@ class EndPoints(unittest.TestCase): query = "annotation-name=notakey" url = f"{URL_BASE}{endpoint}?{query}" 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): endpoint = "annotations/obs" @@ -119,7 +144,7 @@ class EndPoints(unittest.TestCase): } } 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() self.assertEqual(result_data["names"], ["n_genes", "percent_mito", "n_counts", "louvain", "name"]) self.assertEqual(len(result_data["data"]), 15) @@ -140,7 +165,7 @@ class EndPoints(unittest.TestCase): } } 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() self.assertEqual(result_data["names"], ["n_genes", "percent_mito"]) self.assertEqual(len(result_data["data"][0]), 3) @@ -170,7 +195,7 @@ class EndPoints(unittest.TestCase): "count": 7 } result = self.session.post(url, json=params) - self.assertEqual(result.status_code, 200) + self.assertEqual(result.status_code, HTTPStatus.OK) result_data = result.json() self.assertEqual(len(result_data), 7) @@ -195,7 +220,7 @@ class EndPoints(unittest.TestCase): } } result = self.session.post(url, json=params) - self.assertEqual(result.status_code, 200) + self.assertEqual(result.status_code, HTTPStatus.OK) result_data = result.json() self.assertEqual(len(result_data), 10) @@ -203,7 +228,7 @@ class EndPoints(unittest.TestCase): endpoint = "annotations/var" url = f"{URL_BASE}{endpoint}" result = self.session.get(url) - self.assertEqual(result.status_code, 200) + self.assertEqual(result.status_code, HTTPStatus.OK) result_data = result.json() self.assertEqual(result_data["names"], ["n_cells", "name"]) self.assertEqual(len(result_data["data"]), 1838) @@ -214,7 +239,7 @@ class EndPoints(unittest.TestCase): query = "annotation-name=n_cells" url = f"{URL_BASE}{endpoint}?{query}" result = self.session.get(url) - self.assertEqual(result.status_code, 200) + self.assertEqual(result.status_code, HTTPStatus.OK) result_data = result.json() self.assertEqual(result_data["names"], ["n_cells"]) self.assertEqual(len(result_data["data"][0]), 2) @@ -224,7 +249,7 @@ class EndPoints(unittest.TestCase): query = "annotation-name=notakey" url = f"{URL_BASE}{endpoint}?{query}" 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): endpoint = "annotations/var" @@ -239,7 +264,7 @@ class EndPoints(unittest.TestCase): } } 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() self.assertEqual(result_data["names"], ["n_cells", "name"]) self.assertEqual(len(result_data["data"]), 2) @@ -258,7 +283,7 @@ class EndPoints(unittest.TestCase): } } 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() self.assertEqual(result_data["names"], ["n_cells"]) self.assertEqual(len(result_data["data"][0]), 2) @@ -270,7 +295,7 @@ class EndPoints(unittest.TestCase): query = "accept-type=application/json" url = f"{URL_BASE}{endpoint}?{query}" result = self.session.get(url) - self.assertEqual(result.status_code, 200) + self.assertEqual(result.status_code, HTTPStatus.OK) result_data = result.json() self.assertEqual(len(result_data["obs"]), 2638) @@ -280,11 +305,18 @@ class EndPoints(unittest.TestCase): query = "accept-type=xxx" url = f"{URL_BASE}{endpoint}?{query}" result = self.session.get(url) - self.assertEqual(result.status_code, 406) - # no accept type + self.assertEqual(result.status_code, HTTPStatus.NOT_ACCEPTABLE) + 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}" result = self.session.get(url) - self.assertEqual(result.status_code, 406) + self.assertEqual(result.status_code, HTTPStatus.OK) def test_data_filter(self): 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,*" url = f"{URL_BASE}{endpoint}?{query}" result = self.session.get(url) - self.assertEqual(result.status_code, 200) + self.assertEqual(result.status_code, HTTPStatus.OK) result_data = result.json() 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) - self.assertEqual(result.status_code, 200) + self.assertEqual(result.status_code, HTTPStatus.OK) result_data = result.json() 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) - self.assertEqual(result.status_code, 200) + self.assertEqual(result.status_code, HTTPStatus.OK) result_data = result.json() if axis == "obs": self.assertEqual(len(result_data["obs"][0]), 2) @@ -346,4 +378,4 @@ class EndPoints(unittest.TestCase): file = "js/service-worker.js" url = f"{LOCAL_URL}{endpoint}/{file}" result = self.session.get(url) - self.assertEqual(result.status_code, 200) + self.assertEqual(result.status_code, HTTPStatus.OK)