mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-29 03:28:12 +08:00
Restv2 feature branch merge to master (#284)
Move to new REST v0.2 communication between front and back-end. This is a first cut implementation which is functional, but will need follow-up enhancements for performance, error checking, etc. Protocol spec is in docs directory. * Add filtering via indexing * Using new filter specs Indexing working * Added filtering by annotation value * factor out common methods * Documentation * create enum for axis (obs/var) * Better description for filter's return * Add boolean to enumerated types * Augmented enum for scanpy axis * Create schema for annotations Based on datatype within scanpy/anndata + tests * remove obsolete schema parse script * Update rest api to remove old routes and add schema route * Separate development requirements * Warning for unsupported datatypes * include -r requirements.txt in dev * Merged downcast warnings * Fixed bug where names were NaNs Needed to include the index too when creating the series * Add config endpoint * Generate app features from CLI selections * Move features to driver * Add tests for schema * Clearer version wording * python3 version of super * version from engine to package level * move features to driver * Revise layout function to match the new spec * GET for layout/obs * PUT Layout (#211) * PUT Layout * Csweaver/annotations (#212) * Update scanpy engine to support the rest v0.2 annotation requests * GET endpoint for obs annotations + tests * Documentation * Test annotations in scanpy engine * Description for annotation-keys param * annotation->annotations * clarified return for annotations * Use URL query list for annotations fields * parse_filter parses v0.2 GET filters (#215) * parse_filter parses v0.2 GET filters * Don't allow index filters from query params * Better variable conversion * Parse filter improvements - uses default dict - renamed filter -> query_filter * Cleanup Tasks (#216) * Add test_api back into travis build * Do custom JSON encoding the correct way * Run cellxgene server in test setup * Cleanup new tests too * Option to bind to all interfaces (#225) app.run("0.0.0.0") instead of app.run("127.0.0.1") binds to all interfaces. Note: There are comments on the internet that says that the flask server is not up to the task of production serving. I don't think that such scalability concerns apply here, but I was able to get cellxgene working with twistd relatively easily, and we could switch to that if there are scalability concerns. Test plan: browsed to <ip>:5005/api/v0.2/config on a different host. * Add filtering via indexing * Using new filter specs Indexing working * Added filtering by annotation value * factor out common methods * Documentation * create enum for axis (obs/var) * Better description for filter's return * Add boolean to enumerated types * Augmented enum for scanpy axis * Create schema for annotations Based on datatype within scanpy/anndata + tests * remove obsolete schema parse script * Update rest api to remove old routes and add schema route * Separate development requirements * Warning for unsupported datatypes * include -r requirements.txt in dev * Merged downcast warnings * Fixed bug where names were NaNs Needed to include the index too when creating the series * Add config endpoint * Generate app features from CLI selections * Move features to driver * Add tests for schema * Clearer version wording * python3 version of super * version from engine to package level * move features to driver * Revise layout function to match the new spec * GET for layout/obs * PUT Layout (#211) * PUT Layout * Csweaver/annotations (#212) * Update scanpy engine to support the rest v0.2 annotation requests * GET endpoint for obs annotations + tests * Documentation * Test annotations in scanpy engine * Description for annotation-keys param * annotation->annotations * clarified return for annotations * Use URL query list for annotations fields * parse_filter parses v0.2 GET filters (#215) * parse_filter parses v0.2 GET filters * Don't allow index filters from query params * Better variable conversion * Parse filter improvements - uses default dict - renamed filter -> query_filter * Cleanup Tasks (#216) * Add test_api back into travis build * Do custom JSON encoding the correct way * Run cellxgene server in test setup * Cleanup new tests too * Option to bind to all interfaces (#225) app.run("0.0.0.0") instead of app.run("127.0.0.1") binds to all interfaces. Note: There are comments on the internet that says that the flask server is not up to the task of production serving. I don't think that such scalability concerns apply here, but I was able to get cellxgene working with twistd relatively easily, and we could switch to that if there are scalability concerns. Test plan: browsed to <ip>:5005/api/v0.2/config on a different host. * Fix merge errors - import warnings was improperly deleted - scanpy engine tests were totally wrong * Fix merge error with driver * PUT /annotations (#235) * Add query param for annotation name * fix descriptions, eliminate else clause * first cut at initial data load on rest 0.2 api * Annotation var (#248) * Fix bug strings are always objects in pandas * Add axis to annotation method * Add /annotation/var to REST api * Csweaver/expressiondata (#242) * Refactor expression method for REST v2 * Add message to QueryStringError * Fix range filters * Add GET route for /data * /data PUT route * rename expression to data_frame * clarification of error * Improve accept type handling * support all schema types for 0.2 REST API * remove REST 0.1 code; connect var annotations loading * config reducer; use config to set data set title; remove obsolete templating code for data set title * REST 0.2 expression conversion support * partial port of expression to REST 0.2 * diffexp (#273) * Add diffexp method to scanpy and test * Minor tweaks to diffexp Get a minimal working version to unblock FE development * Fixing things git deleted * cleanup print statements * Add index test * additional, partial REST 0.2 bring up of diffexp * Ignore unstructured annotations for data (#275) This is a temp hack, need to figure out how to include data.uns if there is only one gene * diffexp REST 0.2 port finish * ignore unstructured annotaitons on all routes except layout * correctly use varDataCache; maintain state during world rebuild * correct varDataCache use * temporarily disable all memoization * refinements to expression data caching * clear cell sets upon regraph/reset * update version of REST to 0.2 * Travis build fixes - comment out cache import - fix duplicate test name * Remove dependency from travis * clarify semantics of config variables * move generic action helpers into util
This commit is contained in:
+320
-41
@@ -1,60 +1,339 @@
|
||||
import unittest
|
||||
import requests
|
||||
import json
|
||||
from subprocess import Popen
|
||||
import unittest
|
||||
import time
|
||||
|
||||
LOCAL_URL = "http://127.0.0.1:5005/"
|
||||
VERSION = "v0.2"
|
||||
URL_BASE = f"{LOCAL_URL}api/{VERSION}/"
|
||||
|
||||
|
||||
class EndPoints(unittest.TestCase):
|
||||
"""Test Case for endpoints"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.ps = Popen(["cellxgene", "scanpy", "example-dataset/"])
|
||||
session = requests.Session()
|
||||
for i in range(90):
|
||||
try:
|
||||
session.get(f"{URL_BASE}schema")
|
||||
except requests.exceptions.ConnectionError:
|
||||
time.sleep(1)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
try:
|
||||
cls.ps.terminate()
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
|
||||
def setUp(self):
|
||||
# Local
|
||||
self.local_url = "http://127.0.0.1:5005/"
|
||||
self.version = "v0.1"
|
||||
self.url_base = "{local_url}api/{version}/".format(local_url=self.local_url, version=self.version)
|
||||
self.session = requests.Session()
|
||||
|
||||
def test_cells(self):
|
||||
url = "{base}{endpoint}?{params}".format(base=self.url_base, endpoint="cells", params="&".join(
|
||||
["louvain=B cells"]))
|
||||
result = self.session.get(url)
|
||||
assert result.status_code == 200
|
||||
result_data = result.json()
|
||||
assert "B cells" in result_data["data"]["ranges"]["louvain"]["options"]
|
||||
url = "{base}{endpoint}?{params}".format(base=self.url_base, endpoint="cells", params="&".join(
|
||||
["louvain=B cells", "louvain=Megakaryocytes"]))
|
||||
result = self.session.get(url)
|
||||
assert result.status_code == 200
|
||||
result_data = result.json()
|
||||
assert "Megakaryocytes" in result_data["data"]["ranges"]["louvain"]["options"]
|
||||
|
||||
def test_initialize(self):
|
||||
url = "{base}{endpoint}".format(base=self.url_base, endpoint="initialize")
|
||||
endpoint = "schema"
|
||||
url = f"{URL_BASE}{endpoint}"
|
||||
result = self.session.get(url)
|
||||
assert result.status_code == 200
|
||||
self.assertEqual(result.status_code, 200)
|
||||
result_data = result.json()
|
||||
assert result_data["data"]["cellcount"] == 2638
|
||||
assert len(result_data["data"]['ranges']['CellName']['options']) == 2638
|
||||
self.assertEqual(result_data["schema"]["dataframe"]["nObs"], 2638)
|
||||
self.assertEqual(len(result_data["schema"]["annotations"]["obs"]), 5)
|
||||
|
||||
|
||||
def test_expression_get(self):
|
||||
url = "{base}{endpoint}".format(base=self.url_base, endpoint="expression")
|
||||
def test_config(self):
|
||||
endpoint = "config"
|
||||
url = f"{URL_BASE}{endpoint}"
|
||||
result = self.session.get(url)
|
||||
assert result.status_code == 200
|
||||
|
||||
def test_expression_post(self):
|
||||
url = "{base}{endpoint}".format(base=self.url_base, endpoint="expression")
|
||||
result = self.session.post(url, data=json.dumps({"celllist": ["AAACATACAACCAC-1", "AACCGATGGTCATG-1"], "genelist": ["BACH1", "MIS18A", "ATP5O"]}), headers={'content-type': 'application/json'})
|
||||
assert result.status_code == 200
|
||||
self.assertEqual(result.status_code, 200)
|
||||
result_data = result.json()
|
||||
assert len(result_data["data"]["cells"]) == 2
|
||||
assert len(result_data["data"]["cells"][0]['e']) == 3
|
||||
self.assertEqual(result_data["config"]["displayNames"]["dataset"], "example-dataset")
|
||||
self.assertEqual(len(result_data["config"]["features"]), 4)
|
||||
|
||||
def test_diffexp(self):
|
||||
url = "{base}{endpoint}".format(base=self.url_base, endpoint="diffexpression")
|
||||
result = self.session.post(url, data=json.dumps({"celllist1": ["AAACATACAACCAC-1", "AACCGATGGTCATG-1"], "celllist2": ["CCGATAGACCTAAG-1", "GGTGGAGAAGTAGA-1"]}), headers={'content-type': 'application/json'})
|
||||
assert result.status_code == 200
|
||||
def test_get_layout(self):
|
||||
endpoint = "layout/obs"
|
||||
url = f"{URL_BASE}{endpoint}"
|
||||
result = self.session.get(url)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
result_data = result.json()
|
||||
self.assertEqual(result_data["layout"]["ndims"], 2)
|
||||
self.assertEqual(len(result_data["layout"]["coordinates"]), 2638)
|
||||
|
||||
def test_put_layout(self):
|
||||
endpoint = "layout/obs"
|
||||
url = f"{URL_BASE}{endpoint}"
|
||||
obs_filter = {
|
||||
"filter": {
|
||||
"obs": {
|
||||
"annotation_value": [
|
||||
{"name": "louvain", "values": ["NK cells", "CD8 T cells"]},
|
||||
{"name": "n_counts", "min": 3000},
|
||||
],
|
||||
"index": [1, 99, [1000, 2000]]
|
||||
}
|
||||
}
|
||||
}
|
||||
result = self.session.put(url, json=obs_filter)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
result_data = result.json()
|
||||
self.assertEqual(len(result_data["layout"]["coordinates"]), 15)
|
||||
|
||||
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)
|
||||
result_data = result.json()
|
||||
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"][0]), 6)
|
||||
|
||||
def test_get_annotations_obs_keys(self):
|
||||
endpoint = "annotations/obs"
|
||||
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)
|
||||
result_data = result.json()
|
||||
self.assertEqual(result_data["names"], ["n_genes", "percent_mito"])
|
||||
self.assertEqual(len(result_data["data"][0]), 3)
|
||||
|
||||
def test_get_annotations_obs_error(self):
|
||||
endpoint = "annotations/obs"
|
||||
query = "annotation-name=notakey"
|
||||
url = f"{URL_BASE}{endpoint}?{query}"
|
||||
result = self.session.get(url)
|
||||
self.assertEqual(result.status_code, 404)
|
||||
|
||||
def test_put_annotations_obs(self):
|
||||
endpoint = "annotations/obs"
|
||||
url = f"{URL_BASE}{endpoint}"
|
||||
obs_filter = {
|
||||
"filter": {
|
||||
"obs": {
|
||||
"annotation_value": [
|
||||
{"name": "louvain", "values": ["NK cells", "CD8 T cells"]},
|
||||
{"name": "n_counts", "min": 3000},
|
||||
],
|
||||
"index": [1, 99, [1000, 2000]]
|
||||
}
|
||||
}
|
||||
}
|
||||
result = self.session.put(url, json=obs_filter)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
result_data = result.json()
|
||||
self.assertEqual(result_data["names"], ["n_genes", "percent_mito", "n_counts", "louvain", "name"])
|
||||
self.assertEqual(len(result_data["data"]), 15)
|
||||
|
||||
def test_filter_put_annotations_obs(self):
|
||||
endpoint = "annotations/obs"
|
||||
query = "annotation-name=n_genes&annotation-name=percent_mito"
|
||||
url = f"{URL_BASE}{endpoint}?{query}"
|
||||
obs_filter = {
|
||||
"filter": {
|
||||
"obs": {
|
||||
"annotation_value": [
|
||||
{"name": "louvain", "values": ["NK cells", "CD8 T cells"]},
|
||||
{"name": "n_counts", "min": 3000},
|
||||
],
|
||||
"index": [1, 99, [1000, 2000]]
|
||||
}
|
||||
}
|
||||
}
|
||||
result = self.session.put(url, json=obs_filter)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
result_data = result.json()
|
||||
self.assertEqual(result_data["names"], ["n_genes", "percent_mito"])
|
||||
self.assertEqual(len(result_data["data"][0]), 3)
|
||||
self.assertEqual(len(result_data["data"]), 15)
|
||||
|
||||
def test_diff_exp(self):
|
||||
endpoint = "diffexp/obs"
|
||||
url = f"{URL_BASE}{endpoint}"
|
||||
params = {
|
||||
"mode": "topN",
|
||||
"set1": {
|
||||
"filter": {
|
||||
"obs": {"annotation_value": [
|
||||
{"name": "louvain", "values": ["NK cells"]}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"set2": {
|
||||
"filter": {
|
||||
"obs": {"annotation_value": [
|
||||
{"name": "louvain", "values": ["CD8 T cells"]}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"count": 7
|
||||
}
|
||||
result = self.session.post(url, json=params)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
result_data = result.json()
|
||||
self.assertEqual(len(result_data), 7)
|
||||
|
||||
def test_diff_exp_indices(self):
|
||||
endpoint = "diffexp/obs"
|
||||
url = f"{URL_BASE}{endpoint}"
|
||||
params = {
|
||||
"mode": "topN",
|
||||
"set1": {
|
||||
"filter": {
|
||||
"obs": {
|
||||
"index": [[0, 500]]
|
||||
}
|
||||
}
|
||||
},
|
||||
"set2": {
|
||||
"filter": {
|
||||
"obs": {
|
||||
"index": [[500, 1000]]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result = self.session.post(url, json=params)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
result_data = result.json()
|
||||
self.assertEqual(len(result_data), 10)
|
||||
|
||||
def test_get_annotations_var(self):
|
||||
endpoint = "annotations/var"
|
||||
url = f"{URL_BASE}{endpoint}"
|
||||
result = self.session.get(url)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
result_data = result.json()
|
||||
self.assertEqual(result_data["names"], ["n_cells", "name"])
|
||||
self.assertEqual(len(result_data["data"]), 1838)
|
||||
self.assertEqual(len(result_data["data"][0]), 3)
|
||||
|
||||
def test_get_annotations_var_keys(self):
|
||||
endpoint = "annotations/var"
|
||||
query = "annotation-name=n_cells"
|
||||
url = f"{URL_BASE}{endpoint}?{query}"
|
||||
result = self.session.get(url)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
result_data = result.json()
|
||||
self.assertEqual(result_data["names"], ["n_cells"])
|
||||
self.assertEqual(len(result_data["data"][0]), 2)
|
||||
|
||||
def test_get_annotations_var_error(self):
|
||||
endpoint = "annotations/var"
|
||||
query = "annotation-name=notakey"
|
||||
url = f"{URL_BASE}{endpoint}?{query}"
|
||||
result = self.session.get(url)
|
||||
self.assertEqual(result.status_code, 404)
|
||||
|
||||
def test_put_annotations_var(self):
|
||||
endpoint = "annotations/var"
|
||||
url = f"{URL_BASE}{endpoint}"
|
||||
var_filter = {
|
||||
"filter": {
|
||||
"var": {
|
||||
"annotation_value": [
|
||||
{"name": "name", "values": ["ATAD3C", "RER1"]},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
result = self.session.put(url, json=var_filter)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
result_data = result.json()
|
||||
self.assertEqual(result_data["names"], ["n_cells", "name"])
|
||||
self.assertEqual(len(result_data["data"]), 2)
|
||||
|
||||
def test_filter_put_annotations_var(self):
|
||||
endpoint = "annotations/var"
|
||||
query = "annotation-name=n_cells"
|
||||
url = f"{URL_BASE}{endpoint}?{query}"
|
||||
var_filter = {
|
||||
"filter": {
|
||||
"var": {
|
||||
"annotation_value": [
|
||||
{"name": "name", "values": ["ATAD3C", "RER1"]},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
result = self.session.put(url, json=var_filter)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
result_data = result.json()
|
||||
self.assertEqual(result_data["names"], ["n_cells"])
|
||||
self.assertEqual(len(result_data["data"][0]), 2)
|
||||
self.assertEqual(len(result_data["data"]), 2)
|
||||
|
||||
def test_get_data(self):
|
||||
endpoint = "data/obs"
|
||||
query = "accept-type=application/json"
|
||||
url = f"{URL_BASE}{endpoint}?{query}"
|
||||
result = self.session.get(url)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
result_data = result.json()
|
||||
self.assertEqual(len(result_data["obs"]), 2638)
|
||||
|
||||
def test_data_mimetype_error(self):
|
||||
endpoint = "data/obs"
|
||||
query = "accept-type=xxx"
|
||||
url = f"{URL_BASE}{endpoint}?{query}"
|
||||
result = self.session.get(url)
|
||||
self.assertEqual(result.status_code, 406)
|
||||
# no accept type
|
||||
url = f"{URL_BASE}{endpoint}"
|
||||
result = self.session.get(url)
|
||||
self.assertEqual(result.status_code, 406)
|
||||
|
||||
def test_data_filter(self):
|
||||
endpoint = "data/obs"
|
||||
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)
|
||||
result_data = result.json()
|
||||
self.assertEqual(len(result_data["obs"]), 38)
|
||||
|
||||
def test_data_put(self):
|
||||
endpoint = "data/obs"
|
||||
url = f"{URL_BASE}{endpoint}"
|
||||
header = {"Accept": "application/json"}
|
||||
obs_filter = {
|
||||
"filter": {
|
||||
"obs": {
|
||||
"annotation_value": [
|
||||
{"name": "louvain", "values": ["NK cells", "CD8 T cells"]},
|
||||
{"name": "n_counts", "min": 3000},
|
||||
],
|
||||
"index": [1, 99, [1000, 2000]]
|
||||
}
|
||||
}
|
||||
}
|
||||
result = self.session.put(url, headers=header, json=obs_filter)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
result_data = result.json()
|
||||
self.assertEqual(len(result_data["obs"]), 15)
|
||||
|
||||
def test_data_put_single_var(self):
|
||||
endpoint = "data/obs"
|
||||
url = f"{URL_BASE}{endpoint}"
|
||||
header = {"Accept": "application/json"}
|
||||
var_filter = {
|
||||
"filter": {
|
||||
"var": {
|
||||
"annotation_value": [
|
||||
{"name": "name", "values": ["RER1"]},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
result = self.session.put(url, headers=header, json=var_filter)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
result_data = result.json()
|
||||
self.assertEqual(len(result_data["obs"][0]), 2)
|
||||
|
||||
def test_static(self):
|
||||
url = "{url}{endpoint}/{file}".format(url=self.local_url, endpoint="static", file="js/service-worker.js")
|
||||
endpoint = "static"
|
||||
file = "js/service-worker.js"
|
||||
url = f"{LOCAL_URL}{endpoint}/{file}"
|
||||
result = self.session.get(url)
|
||||
assert result.status_code == 200
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
Reference in New Issue
Block a user