diff --git a/docs/REST_API.md b/docs/REST_API.md index 3a95fbc8..d932e0b9 100644 --- a/docs/REST_API.md +++ b/docs/REST_API.md @@ -461,7 +461,7 @@ Get the _default_ layout for all observations or (_future_) all variables. Retur } ``` -### POST /layout/obs, (_future_) POST /layout/var +### PUT /layout/obs, (_future_) PUT /layout/var Generate layout for the caller-specified subset of data, as indicated by the filter. This operation implicitly requests a re-layout operation to be performed on the specified data. diff --git a/server/app/rest_api/rest.py b/server/app/rest_api/rest.py index f1316c7b..1df5417a 100644 --- a/server/app/rest_api/rest.py +++ b/server/app/rest_api/rest.py @@ -670,17 +670,12 @@ class LayoutObsAPI(Resource): }, } }) - def post(self): + def put(self): try: - return make_response( - jsonify({ - "layout": current_app.data.layout( - request.get_json()["filter"], - interactive_limit=current_app.data.features["layout"]["obs"]["interactiveLimit"] - ) - }), - HTTPStatus.OK - ) + filter = request.get_json()["filter"] + interactive_limit = current_app.data.features["layout"]["obs"]["interactiveLimit"] + layout = current_app.data.layout(filter, interactive_limit=interactive_limit) + return make_response(jsonify({"layout": layout}), HTTPStatus.OK) except FilterError as e: return make_response(e.message, HTTPStatus.BAD_REQUEST) except InteractiveError: diff --git a/server/app/scanpy_engine/scanpy_engine.py b/server/app/scanpy_engine/scanpy_engine.py index c58cbd0e..cfac5dff 100644 --- a/server/app/scanpy_engine/scanpy_engine.py +++ b/server/app/scanpy_engine/scanpy_engine.py @@ -6,8 +6,7 @@ from pandas import DataFrame, Series import scanpy.api as sc from scipy import stats -# TODO fix memoization so that it correctly identifies the same request -# from server.app.app import cache +from server.app.app import cache from server.app.driver.driver import CXGDriver from server.app.util.constants import Axis, DEFAULT_TOP_N, DiffExpMode from server.app.util.utils import FilterError, InteractiveError @@ -201,7 +200,7 @@ class ScanpyEngine(CXGDriver): index = np.logical_and(index, key_idx) return index - # @cache.memoize() + @cache.memoize() def annotation(self, filter, axis, fields=None): """ Gets annotation value for each observation @@ -224,7 +223,7 @@ class ScanpyEngine(CXGDriver): "data": annotations.reset_index().values.tolist() } - # @cache.memoize() + @cache.memoize() def data_frame(self, filter, axis): """ Retrieves data for each variable for observations in data frame @@ -261,7 +260,6 @@ class ScanpyEngine(CXGDriver): } return result - # @cache.memoize() def diffexp(self, filter1, filter2, top_n=None, interactive_limit=None): """ Computes the top differentially expressed variables between two observation sets. If dataframes @@ -321,7 +319,7 @@ class ScanpyEngine(CXGDriver): # Results need to be returned in var index order return sorted(result, key=lambda gene: gene[0]) - # @cache.memoize() + @cache.memoize() def layout(self, filter, interactive_limit=None): """ Computes a n-d layout for cells through dimensionality reduction. diff --git a/server/test/test_api.py b/server/test/test_api.py index e5c551f4..9a22c64d 100644 --- a/server/test/test_api.py +++ b/server/test/test_api.py @@ -84,22 +84,16 @@ class EndPoints(unittest.TestCase): } } } - result = self.session.post(url, json=obs_filter) + result = self.session.put(url, json=obs_filter) 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(): + endpoints = ["layout/obs", "annotations/obs", "annotations/var", "data/obs", "data/var"] + for endpoint in endpoints: url = f"{URL_BASE}{endpoint}" - result = getattr(self.session, method)(url, json=BAD_FILTER) + result = self.session.put(url, json=BAD_FILTER) self.assertEqual(result.status_code, HTTPStatus.BAD_REQUEST) def test_get_annotations_obs(self): @@ -373,6 +367,39 @@ class EndPoints(unittest.TestCase): self.assertEqual(len(result_data["obs"]), 2638) self.assertEqual(len(result_data["var"][0]), 2639) + def test_cache(self): + endpoint = "annotations/var" + url = f"{URL_BASE}{endpoint}" + f1 = {"filter": {"var": {"annotation_value": [{"name": "name", + "values": ["HLA-DRB1", "HLA-DQA1", "HLA-DQB1", "HLA-DPA1", + "HLA-DPB1", "MS4A1", "IL32", "CCL5", "CD79B", + "CD79A"]}]}}} + result = self.session.put(url, json=f1) + self.assertEqual(result.status_code, HTTPStatus.OK) + result_data1 = result.json() + f2 = {"filter": {"var": {"annotation_value": [{"name": "name", + "values": ["FGFBP2", "GZMA", "LTB", "PRF1", "CTSW", "GZMH", + "CCL5", "CCL4", "CST7", "NKG7"]}]}}} + result = self.session.put(url, json=f2) + self.assertEqual(result.status_code, HTTPStatus.OK) + result_data2 = result.json() + self.assertNotEqual(result_data1, result_data2) + + def test_cache_nofilter(self): + endpoint = "annotations/var" + url = f"{URL_BASE}{endpoint}" + f1 = {"filter": {}} + result = self.session.put(url, json=f1) + self.assertEqual(result.status_code, HTTPStatus.OK) + result_data1 = result.json() + f2 = {"filter": {"var": {"annotation_value": [{"name": "name", + "values": ["FGFBP2", "GZMA", "LTB", "PRF1", "CTSW", "GZMH", + "CCL5", "CCL4", "CST7", "NKG7"]}]}}} + result = self.session.put(url, json=f2) + self.assertEqual(result.status_code, HTTPStatus.OK) + result_data2 = result.json() + self.assertNotEqual(result_data1, result_data2) + def test_static(self): endpoint = "static" file = "js/service-worker.js"