mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-08 18:08:12 +08:00
Add user-generated annotations tests to the server (#1164)
* Add user-generated annotations tests to the server Partially completes https://github.com/chanzuckerberg/cellxgene/issues/969 * Auto-format python code * @skip_if: passing lambdas > than property strings * Respond to feedback from @bkmartinjr
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
include ../common.mk
|
include ../common.mk
|
||||||
|
|
||||||
DATASET := $(if $(DATASET),$(DATASET),../example-dataset/pbmc3k.h5ad)
|
DATASET := $(if $(DATASET),$(DATASET),../example-dataset/pbmc3k.h5ad)
|
||||||
ANNOTATIONS := $(if $(ANNOTATIONS),$(ANNOTATIONS),../example-dataset/pbmc3k-annotations.csv)
|
ANNOTATIONS := $(if $(ANNOTATIONS),$(ANNOTATIONS),../server/test/test_datasets/pbmc3k-annotations.csv)
|
||||||
ANNOTATIONS_FILENAME := $(shell basename $(ANNOTATIONS))
|
ANNOTATIONS_FILENAME := $(shell basename $(ANNOTATIONS))
|
||||||
|
|
||||||
# Packaging
|
# Packaging
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ exclude = '''
|
|||||||
| buck-out
|
| buck-out
|
||||||
| build
|
| build
|
||||||
| dist
|
| dist
|
||||||
| server/app/util/fbs/NetEncoding
|
| server/data_common/fbs/NetEncoding
|
||||||
)/
|
)/
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|||||||
+6
-9
@@ -82,12 +82,12 @@ def rest_get_data_adaptor(func):
|
|||||||
|
|
||||||
def static_redirect(dataset, therest):
|
def static_redirect(dataset, therest):
|
||||||
""" redirect all static requests to the standard location """
|
""" redirect all static requests to the standard location """
|
||||||
return redirect(f'/static/{therest}', code=301)
|
return redirect(f"/static/{therest}", code=301)
|
||||||
|
|
||||||
|
|
||||||
def favicon_redirect(dataset):
|
def favicon_redirect(dataset):
|
||||||
""" redirect favicon to static dir """
|
""" redirect favicon to static dir """
|
||||||
return redirect('/static/favicon.png', code=301)
|
return redirect("/static/favicon.png", code=301)
|
||||||
|
|
||||||
|
|
||||||
def dataroot_index():
|
def dataroot_index():
|
||||||
@@ -127,20 +127,17 @@ class SchemaAPI(Resource):
|
|||||||
class ConfigAPI(Resource):
|
class ConfigAPI(Resource):
|
||||||
@rest_get_data_adaptor
|
@rest_get_data_adaptor
|
||||||
def get(self, data_adaptor):
|
def get(self, data_adaptor):
|
||||||
return common_rest.config_get(
|
return common_rest.config_get(current_app.app_config, data_adaptor, current_app.annotations)
|
||||||
current_app.app_config, data_adaptor, current_app.annotations)
|
|
||||||
|
|
||||||
|
|
||||||
class AnnotationsObsAPI(Resource):
|
class AnnotationsObsAPI(Resource):
|
||||||
@rest_get_data_adaptor
|
@rest_get_data_adaptor
|
||||||
def get(self, data_adaptor):
|
def get(self, data_adaptor):
|
||||||
return common_rest.annotations_obs_get(
|
return common_rest.annotations_obs_get(request, data_adaptor, current_app.annotations)
|
||||||
request, data_adaptor, current_app.annotations)
|
|
||||||
|
|
||||||
@rest_get_data_adaptor
|
@rest_get_data_adaptor
|
||||||
def put(self, data_adaptor):
|
def put(self, data_adaptor):
|
||||||
return common_rest.annotations_obs_put(
|
return common_rest.annotations_obs_put(request, data_adaptor, current_app.annotations)
|
||||||
request, data_adaptor, current_app.annotations)
|
|
||||||
|
|
||||||
|
|
||||||
class AnnotationsVarAPI(Resource):
|
class AnnotationsVarAPI(Resource):
|
||||||
@@ -216,7 +213,7 @@ class Server:
|
|||||||
bp_api = Blueprint("api_dataset", __name__, url_prefix="/<dataset>" + api_version)
|
bp_api = Blueprint("api_dataset", __name__, url_prefix="/<dataset>" + api_version)
|
||||||
resources = get_api_resources(bp_api)
|
resources = get_api_resources(bp_api)
|
||||||
self.app.register_blueprint(resources.blueprint)
|
self.app.register_blueprint(resources.blueprint)
|
||||||
self.app.add_url_rule("/<dataset>/", 'dataset_index', dataset_index)
|
self.app.add_url_rule("/<dataset>/", "dataset_index", dataset_index)
|
||||||
self.app.add_url_rule("/<dataset>/static/<path:therest>", "static_redirect", static_redirect)
|
self.app.add_url_rule("/<dataset>/static/<path:therest>", "static_redirect", static_redirect)
|
||||||
self.app.add_url_rule("/<dataset>/favicon.png", "favicon_redirect", favicon_redirect)
|
self.app.add_url_rule("/<dataset>/favicon.png", "favicon_redirect", favicon_redirect)
|
||||||
|
|
||||||
|
|||||||
+15
-19
@@ -20,7 +20,7 @@ from server.common.errors import OntologyLoadFailure
|
|||||||
|
|
||||||
# anything bigger than this will generate a special message
|
# anything bigger than this will generate a special message
|
||||||
BIG_FILE_SIZE_THRESHOLD = 100 * 2 ** 20 # 100MB
|
BIG_FILE_SIZE_THRESHOLD = 100 * 2 ** 20 # 100MB
|
||||||
DEFAULT_SERVER_PORT = int(environ.get('CXG_SERVER_PORT', '5005'))
|
DEFAULT_SERVER_PORT = int(environ.get("CXG_SERVER_PORT", "5005"))
|
||||||
|
|
||||||
|
|
||||||
def annotation_args(func):
|
def annotation_args(func):
|
||||||
@@ -54,14 +54,14 @@ def annotation_args(func):
|
|||||||
is_flag=True,
|
is_flag=True,
|
||||||
default=False,
|
default=False,
|
||||||
show_default=True,
|
show_default=True,
|
||||||
help="When creating annotations, optionally autocomplete names from ontology terms."
|
help="When creating annotations, optionally autocomplete names from ontology terms.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--experimental-annotations-ontology-obo",
|
"--experimental-annotations-ontology-obo",
|
||||||
default=None,
|
default=None,
|
||||||
show_default=True,
|
show_default=True,
|
||||||
metavar="<path or url>",
|
metavar="<path or url>",
|
||||||
help="Location of OBO file defining cell annotation autosuggest terms."
|
help="Location of OBO file defining cell annotation autosuggest terms.",
|
||||||
)
|
)
|
||||||
@functools.wraps(func)
|
@functools.wraps(func)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
@@ -110,7 +110,6 @@ def config_args(func):
|
|||||||
|
|
||||||
|
|
||||||
def dataset_args(func):
|
def dataset_args(func):
|
||||||
|
|
||||||
@click.option(
|
@click.option(
|
||||||
"--obs-names",
|
"--obs-names",
|
||||||
"-obs",
|
"-obs",
|
||||||
@@ -131,15 +130,9 @@ def dataset_args(func):
|
|||||||
is_flag=True,
|
is_flag=True,
|
||||||
default=False,
|
default=False,
|
||||||
show_default=False,
|
show_default=False,
|
||||||
help="Load anndata in file-backed mode. "
|
help="Load anndata in file-backed mode. " "This may save memory, but may result in slower overall performance.",
|
||||||
"This may save memory, but may result in slower overall performance.",
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
"--title",
|
|
||||||
"-t",
|
|
||||||
metavar="<text>",
|
|
||||||
help="Title to display. If omitted will use file name."
|
|
||||||
)
|
)
|
||||||
|
@click.option("--title", "-t", metavar="<text>", help="Title to display. If omitted will use file name.")
|
||||||
@click.option(
|
@click.option(
|
||||||
"--about",
|
"--about",
|
||||||
metavar="<URL>",
|
metavar="<URL>",
|
||||||
@@ -212,8 +205,9 @@ def launch_args(func):
|
|||||||
default=None,
|
default=None,
|
||||||
metavar="<data directory>",
|
metavar="<data directory>",
|
||||||
help="Enable cellxgene to serve multiple files. Supply path (local directory or URL)"
|
help="Enable cellxgene to serve multiple files. Supply path (local directory or URL)"
|
||||||
" to folder containing H5AD and/or CXG datasets.",
|
" to folder containing H5AD and/or CXG datasets.",
|
||||||
hidden=True) # TODO, unhide when dataroot is supported)
|
hidden=True,
|
||||||
|
) # TODO, unhide when dataroot is supported)
|
||||||
@click.argument("datapath", required=False, metavar="<path to data file>")
|
@click.argument("datapath", required=False, metavar="<path to data file>")
|
||||||
@click.option(
|
@click.option(
|
||||||
"--open",
|
"--open",
|
||||||
@@ -282,7 +276,7 @@ def launch(
|
|||||||
backed,
|
backed,
|
||||||
disable_diffexp,
|
disable_diffexp,
|
||||||
experimental_annotations_ontology,
|
experimental_annotations_ontology,
|
||||||
experimental_annotations_ontology_obo
|
experimental_annotations_ontology_obo,
|
||||||
):
|
):
|
||||||
"""Launch the cellxgene data viewer.
|
"""Launch the cellxgene data viewer.
|
||||||
This web app lets you explore single-cell expression data.
|
This web app lets you explore single-cell expression data.
|
||||||
@@ -308,7 +302,7 @@ def launch(
|
|||||||
|
|
||||||
if datapath is None and dataroot is None:
|
if datapath is None and dataroot is None:
|
||||||
# TODO: change the error message once dataroot is fully supported
|
# TODO: change the error message once dataroot is fully supported
|
||||||
raise click.ClickException("Missing argument \"<path to data file>.\"")
|
raise click.ClickException('Missing argument "<path to data file>."')
|
||||||
# raise click.ClickException("must supply either <path to data file> or --dataroot")
|
# raise click.ClickException("must supply either <path to data file> or --dataroot")
|
||||||
if datapath is not None and dataroot is not None:
|
if datapath is not None and dataroot is not None:
|
||||||
raise click.ClickException("must supply only one of <path to data file> or --dataroot")
|
raise click.ClickException("must supply only one of <path to data file> or --dataroot")
|
||||||
@@ -376,6 +370,7 @@ def launch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if about:
|
if about:
|
||||||
|
|
||||||
def url_check(url):
|
def url_check(url):
|
||||||
try:
|
try:
|
||||||
result = urlparse(url)
|
result = urlparse(url)
|
||||||
@@ -405,7 +400,8 @@ def launch(
|
|||||||
obs_names=obs_names,
|
obs_names=obs_names,
|
||||||
var_names=var_names,
|
var_names=var_names,
|
||||||
anndata_backed=backed,
|
anndata_backed=backed,
|
||||||
disable_diffexp=disable_diffexp)
|
disable_diffexp=disable_diffexp,
|
||||||
|
)
|
||||||
|
|
||||||
matrix_data_cache_manager = MatrixDataCacheManager()
|
matrix_data_cache_manager = MatrixDataCacheManager()
|
||||||
data_adaptor = None
|
data_adaptor = None
|
||||||
@@ -424,8 +420,7 @@ def launch(
|
|||||||
annotations = None
|
annotations = None
|
||||||
|
|
||||||
if experimental_annotations:
|
if experimental_annotations:
|
||||||
annotations = AnnotationsLocalFile(experimental_annotations_output_dir,
|
annotations = AnnotationsLocalFile(experimental_annotations_output_dir, experimental_annotations_file)
|
||||||
experimental_annotations_file)
|
|
||||||
|
|
||||||
# if the user has specified a fixed label file, go ahead and validate it
|
# if the user has specified a fixed label file, go ahead and validate it
|
||||||
# so that we can remove errors early in the process.
|
# so that we can remove errors early in the process.
|
||||||
@@ -441,6 +436,7 @@ def launch(
|
|||||||
|
|
||||||
# create the server
|
# create the server
|
||||||
from server.app.app import Server
|
from server.app.app import Server
|
||||||
|
|
||||||
server = Server(matrix_data_cache_manager, annotations, app_config)
|
server = Server(matrix_data_cache_manager, annotations, app_config)
|
||||||
|
|
||||||
if not verbose:
|
if not verbose:
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ def log_upgrade_check():
|
|||||||
|
|
||||||
# Get the current latest release
|
# Get the current latest release
|
||||||
try:
|
try:
|
||||||
release_tag_generator = (r['tag_name'] for r in _request_cellxgene_releases())
|
release_tag_generator = (r["tag_name"] for r in _request_cellxgene_releases())
|
||||||
latest_release = next(release_tag_generator, lambda tag_name: validate_version_str(tag_name))
|
latest_release = next(release_tag_generator, lambda tag_name: validate_version_str(tag_name))
|
||||||
if version_gt(latest_release, __version__):
|
if version_gt(latest_release, __version__):
|
||||||
click.echo(f"There's a new version of cellxgene available ({latest_release})!")
|
click.echo(f"There's a new version of cellxgene available ({latest_release})!")
|
||||||
@@ -37,15 +37,16 @@ class RateLimitException(Exception):
|
|||||||
|
|
||||||
def _request_cellxgene_releases():
|
def _request_cellxgene_releases():
|
||||||
def raise_on_rate_limit(response):
|
def raise_on_rate_limit(response):
|
||||||
if response.status_code == 403 and res.headers.get('X-RateLimit-Remaining') == '0':
|
if response.status_code == 403 and res.headers.get("X-RateLimit-Remaining") == "0":
|
||||||
raise RateLimitException
|
raise RateLimitException
|
||||||
|
|
||||||
url = "https://api.github.com/repos/chanzuckerberg/cellxgene/releases"
|
url = "https://api.github.com/repos/chanzuckerberg/cellxgene/releases"
|
||||||
res = requests.get(url)
|
res = requests.get(url)
|
||||||
raise_on_rate_limit(res)
|
raise_on_rate_limit(res)
|
||||||
for release in res.json():
|
for release in res.json():
|
||||||
yield release
|
yield release
|
||||||
while 'next' in res.links.keys():
|
while "next" in res.links.keys():
|
||||||
res = requests.get(res.links['next']['url'])
|
res = requests.get(res.links["next"]["url"])
|
||||||
raise_on_rate_limit(res)
|
raise_on_rate_limit(res)
|
||||||
for release in res.json():
|
for release in res.json():
|
||||||
yield release
|
yield release
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class Annotations(metaclass=ABCMeta):
|
|||||||
raise OntologyLoadFailure(f"Unable to find OBO ontology path: {path}") from e
|
raise OntologyLoadFailure(f"Unable to find OBO ontology path: {path}") from e
|
||||||
|
|
||||||
except SyntaxError as e:
|
except SyntaxError as e:
|
||||||
msg = ''.join(traceback.format_exception_only(SyntaxError, e))
|
msg = "".join(traceback.format_exception_only(SyntaxError, e))
|
||||||
raise OntologyLoadFailure(msg) from e
|
raise OntologyLoadFailure(msg) from e
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
+17
-13
@@ -13,16 +13,12 @@ class AppFeature(object):
|
|||||||
setattr(self, k, v)
|
setattr(self, k, v)
|
||||||
|
|
||||||
def todict(self):
|
def todict(self):
|
||||||
d = dict(
|
d = dict(available=self.available, method=self.method, path=self.path)
|
||||||
available=self.available,
|
|
||||||
method=self.method,
|
|
||||||
path=self.path)
|
|
||||||
d.update(self.extra)
|
d.update(self.extra)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
class AppConfig(object):
|
class AppConfig(object):
|
||||||
|
|
||||||
def __init__(self, **kw):
|
def __init__(self, **kw):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
@@ -47,10 +43,20 @@ class AppConfig(object):
|
|||||||
# parameters
|
# parameters
|
||||||
self.diffexp_may_be_slow = False
|
self.diffexp_may_be_slow = False
|
||||||
|
|
||||||
inputs = ["datapath", "dataroot", "title", "about", "scripts", "layout",
|
inputs = [
|
||||||
"max_category_items", "diffexp_lfc_cutoff",
|
"datapath",
|
||||||
"obs_names", "var_names",
|
"dataroot",
|
||||||
"anndata_backed", "disable_diffexp"]
|
"title",
|
||||||
|
"about",
|
||||||
|
"scripts",
|
||||||
|
"layout",
|
||||||
|
"max_category_items",
|
||||||
|
"diffexp_lfc_cutoff",
|
||||||
|
"obs_names",
|
||||||
|
"var_names",
|
||||||
|
"anndata_backed",
|
||||||
|
"disable_diffexp",
|
||||||
|
]
|
||||||
|
|
||||||
self.update(inputs, kw)
|
self.update(inputs, kw)
|
||||||
|
|
||||||
@@ -80,9 +86,7 @@ class AppConfig(object):
|
|||||||
title = self.get_title(data_adaptor)
|
title = self.get_title(data_adaptor)
|
||||||
about = self.get_about(data_adaptor)
|
about = self.get_about(data_adaptor)
|
||||||
|
|
||||||
display_names = dict(
|
display_names = dict(engine=data_adaptor.get_name(), dataset=title)
|
||||||
engine=data_adaptor.get_name(),
|
|
||||||
dataset=title)
|
|
||||||
|
|
||||||
# library_versions
|
# library_versions
|
||||||
library_versions = {}
|
library_versions = {}
|
||||||
@@ -90,7 +94,7 @@ class AppConfig(object):
|
|||||||
library_versions["cellxgene"] = cellxgene_version
|
library_versions["cellxgene"] = cellxgene_version
|
||||||
|
|
||||||
# links
|
# links
|
||||||
links = {"about-dataset" : about}
|
links = {"about-dataset": about}
|
||||||
|
|
||||||
# parameters
|
# parameters
|
||||||
parameters = {
|
parameters = {
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class DataLocator:
|
|||||||
# and clean it up when done. If the path has a suffix/extension,
|
# and clean it up when done. If the path has a suffix/extension,
|
||||||
# do our best to create a file with the same.
|
# do our best to create a file with the same.
|
||||||
ext = os.path.splitext(self.path)
|
ext = os.path.splitext(self.path)
|
||||||
suffix = None if ext[1] == '' else ext[1]
|
suffix = None if ext[1] == "" else ext[1]
|
||||||
with self.open() as src, tempfile.NamedTemporaryFile(prefix="cellxgene_", suffix=suffix, delete=False) as tmp:
|
with self.open() as src, tempfile.NamedTemporaryFile(prefix="cellxgene_", suffix=suffix, delete=False) as tmp:
|
||||||
tmp.write(src.read())
|
tmp.write(src.read())
|
||||||
tmp.close()
|
tmp.close()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ class FilterError(Exception):
|
|||||||
"""
|
"""
|
||||||
Raised when filter is malformed
|
Raised when filter is malformed
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -9,6 +10,7 @@ class JSONEncodingValueError(Exception):
|
|||||||
"""
|
"""
|
||||||
Raised when data cannot be encoded into json
|
Raised when data cannot be encoded into json
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -16,6 +18,7 @@ class MimeTypeError(Exception):
|
|||||||
"""
|
"""
|
||||||
Raised when incompatible MIME type selected
|
Raised when incompatible MIME type selected
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -23,6 +26,7 @@ class PrepareError(Exception):
|
|||||||
"""
|
"""
|
||||||
Raised when data is misprepared
|
Raised when data is misprepared
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -30,6 +34,7 @@ class DatasetAccessError(Exception):
|
|||||||
"""
|
"""
|
||||||
Raised when file loaded into a DataAdaptor is misformatted
|
Raised when file loaded into a DataAdaptor is misformatted
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -37,6 +42,7 @@ class DisabledFeatureError(Exception):
|
|||||||
"""
|
"""
|
||||||
Raised when an attempt to use a disabled feature occurs
|
Raised when an attempt to use a disabled feature occurs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -44,6 +50,7 @@ class AnnotationsError(Exception):
|
|||||||
"""
|
"""
|
||||||
Raised when an attempt to use the annotations feature fails
|
Raised when an attempt to use the annotations feature fails
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -51,4 +58,5 @@ class OntologyLoadFailure(Exception):
|
|||||||
"""
|
"""
|
||||||
Raised when reading the ontology file fails
|
Raised when reading the ontology file fails
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -29,9 +29,7 @@ def schema_get_helper(data_adaptor, annotations):
|
|||||||
|
|
||||||
def schema_get(data_adaptor, annotations):
|
def schema_get(data_adaptor, annotations):
|
||||||
schema = schema_get_helper(data_adaptor, annotations)
|
schema = schema_get_helper(data_adaptor, annotations)
|
||||||
return make_response(
|
return make_response(jsonify({"schema": schema}), HTTPStatus.OK)
|
||||||
jsonify({"schema": schema}), HTTPStatus.OK
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def config_get(app_config, data_adaptor, annotations):
|
def config_get(app_config, data_adaptor, annotations):
|
||||||
|
|||||||
@@ -92,20 +92,18 @@ def jsonify_numpy(data):
|
|||||||
def dtype_to_schema(dtype):
|
def dtype_to_schema(dtype):
|
||||||
schema = {}
|
schema = {}
|
||||||
if dtype == np.float32:
|
if dtype == np.float32:
|
||||||
schema['type'] = 'float32'
|
schema["type"] = "float32"
|
||||||
elif dtype == np.int32:
|
elif dtype == np.int32:
|
||||||
schema['type'] = 'int32'
|
schema["type"] = "int32"
|
||||||
elif dtype == np.bool_:
|
elif dtype == np.bool_:
|
||||||
schema['type'] = 'boolean'
|
schema["type"] = "boolean"
|
||||||
elif dtype == np.str:
|
elif dtype == np.str:
|
||||||
schema['type'] = 'string'
|
schema["type"] = "string"
|
||||||
elif dtype == "category":
|
elif dtype == "category":
|
||||||
schema["type"] = "categorical"
|
schema["type"] = "categorical"
|
||||||
schema["categories"] = dtype.categories.tolist()
|
schema["categories"] = dtype.categories.tolist()
|
||||||
else:
|
else:
|
||||||
raise TypeError(
|
raise TypeError(f"Annotations of type {dtype} are unsupported.")
|
||||||
f"Annotations of type {dtype} are unsupported."
|
|
||||||
)
|
|
||||||
return schema
|
return schema
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -287,7 +287,7 @@ def create_emb(e_name, emb):
|
|||||||
* large tile size (1000)
|
* large tile size (1000)
|
||||||
* default compression level
|
* default compression level
|
||||||
"""
|
"""
|
||||||
filters = tiledb.FilterList([tiledb.ZstdFilter(), ])
|
filters = tiledb.FilterList([tiledb.ZstdFilter()])
|
||||||
attrs = [tiledb.Attr(dtype=emb.dtype, filters=filters)]
|
attrs = [tiledb.Attr(dtype=emb.dtype, filters=filters)]
|
||||||
dims = []
|
dims = []
|
||||||
for d in range(emb.ndim):
|
for d in range(emb.ndim):
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ def anndata_version_is_pre_070():
|
|||||||
|
|
||||||
|
|
||||||
class AnndataAdaptor(DataAdaptor):
|
class AnndataAdaptor(DataAdaptor):
|
||||||
|
|
||||||
def __init__(self, data_locator, config=None):
|
def __init__(self, data_locator, config=None):
|
||||||
super().__init__(config)
|
super().__init__(config)
|
||||||
self.data = None
|
self.data = None
|
||||||
@@ -127,7 +126,6 @@ class AnndataAdaptor(DataAdaptor):
|
|||||||
"dataframe": {"nObs": self.cell_count, "nVar": self.gene_count, "type": str(self.data.X.dtype)},
|
"dataframe": {"nObs": self.cell_count, "nVar": self.gene_count, "type": str(self.data.X.dtype)},
|
||||||
"annotations": {
|
"annotations": {
|
||||||
"obs": {"index": self.parameters.get("obs_names"), "columns": []},
|
"obs": {"index": self.parameters.get("obs_names"), "columns": []},
|
||||||
|
|
||||||
"var": {"index": self.parameters.get("var_names"), "columns": []},
|
"var": {"index": self.parameters.get("var_names"), "columns": []},
|
||||||
},
|
},
|
||||||
"layout": {"obs": []},
|
"layout": {"obs": []},
|
||||||
@@ -177,8 +175,10 @@ class AnndataAdaptor(DataAdaptor):
|
|||||||
|
|
||||||
def _validate_and_initialize(self):
|
def _validate_and_initialize(self):
|
||||||
if anndata_version_is_pre_070() and self.config.anndata_backed:
|
if anndata_version_is_pre_070() and self.config.anndata_backed:
|
||||||
warnings.warn(f"Use of --backed mode with anndata versions older than 0.7 will have serious "
|
warnings.warn(
|
||||||
"performance issues. Please update to at least anndata 0.7 or later.")
|
f"Use of --backed mode with anndata versions older than 0.7 will have serious "
|
||||||
|
"performance issues. Please update to at least anndata 0.7 or later."
|
||||||
|
)
|
||||||
|
|
||||||
# var and obs column names must be unique
|
# var and obs column names must be unique
|
||||||
if not self.data.obs.columns.is_unique or not self.data.var.columns.is_unique:
|
if not self.data.obs.columns.is_unique or not self.data.var.columns.is_unique:
|
||||||
|
|||||||
@@ -132,16 +132,14 @@ class DataAdaptor(metaclass=ABCMeta):
|
|||||||
|
|
||||||
if self.get_embedding_names():
|
if self.get_embedding_names():
|
||||||
# TODO handle "var" when gene layout becomes available
|
# TODO handle "var" when gene layout becomes available
|
||||||
features["layout_obs"] = AppFeature(
|
features["layout_obs"] = AppFeature("/layout/obs", available=True)
|
||||||
"/layout/obs", available=True)
|
|
||||||
else:
|
else:
|
||||||
features["layout_obs"] = AppFeature("/layout/obs")
|
features["layout_obs"] = AppFeature("/layout/obs")
|
||||||
|
|
||||||
if self.config.disable_diffexp:
|
if self.config.disable_diffexp:
|
||||||
features["diffexp"] = AppFeature("/diffexp/")
|
features["diffexp"] = AppFeature("/diffexp/")
|
||||||
else:
|
else:
|
||||||
features["diffexp"] = AppFeature(
|
features["diffexp"] = AppFeature("/diffexp/", available=True)
|
||||||
"/diffexp/", available=True)
|
|
||||||
|
|
||||||
return features
|
return features
|
||||||
|
|
||||||
@@ -152,17 +150,17 @@ class DataAdaptor(metaclass=ABCMeta):
|
|||||||
mask = np.zeros((count,), dtype=np.bool)
|
mask = np.zeros((count,), dtype=np.bool)
|
||||||
for i in filter:
|
for i in filter:
|
||||||
if type(i) == list:
|
if type(i) == list:
|
||||||
mask[i[0]: i[1]] = True
|
mask[i[0] : i[1]] = True
|
||||||
else:
|
else:
|
||||||
mask[i] = True
|
mask[i] = True
|
||||||
return mask
|
return mask
|
||||||
|
|
||||||
def _axis_filter_to_mask(self, axis, filter, count):
|
def _axis_filter_to_mask(self, axis, filter, count):
|
||||||
mask = np.ones((count, ), dtype=np.bool)
|
mask = np.ones((count,), dtype=np.bool)
|
||||||
if 'index' in filter:
|
if "index" in filter:
|
||||||
mask = np.logical_and(mask, self._index_filter_to_mask(filter['index'], count))
|
mask = np.logical_and(mask, self._index_filter_to_mask(filter["index"], count))
|
||||||
if 'annotation_value' in filter:
|
if "annotation_value" in filter:
|
||||||
mask = np.logical_and(mask, self._annotation_filter_to_mask(axis, filter['annotation_value'], count))
|
mask = np.logical_and(mask, self._annotation_filter_to_mask(axis, filter["annotation_value"], count))
|
||||||
|
|
||||||
return mask
|
return mask
|
||||||
|
|
||||||
@@ -176,7 +174,7 @@ class DataAdaptor(metaclass=ABCMeta):
|
|||||||
anno_data = self.query_obs_array(name)
|
anno_data = self.query_obs_array(name)
|
||||||
|
|
||||||
if anno_data.dtype.name in ["boolean", "category", "object"]:
|
if anno_data.dtype.name in ["boolean", "category", "object"]:
|
||||||
values = v.get('values', [])
|
values = v.get("values", [])
|
||||||
key_idx = np.in1d(anno_data, values)
|
key_idx = np.in1d(anno_data, values)
|
||||||
mask = np.logical_and(mask, key_idx)
|
mask = np.logical_and(mask, key_idx)
|
||||||
|
|
||||||
@@ -202,10 +200,10 @@ class DataAdaptor(metaclass=ABCMeta):
|
|||||||
obs_selector = None
|
obs_selector = None
|
||||||
if filter is not None:
|
if filter is not None:
|
||||||
if Axis.OBS in filter:
|
if Axis.OBS in filter:
|
||||||
obs_selector = self._axis_filter_to_mask(Axis.OBS, filter['obs'], shape[0])
|
obs_selector = self._axis_filter_to_mask(Axis.OBS, filter["obs"], shape[0])
|
||||||
|
|
||||||
if Axis.VAR in filter:
|
if Axis.VAR in filter:
|
||||||
var_selector = self._axis_filter_to_mask(Axis.VAR, filter['var'], shape[1])
|
var_selector = self._axis_filter_to_mask(Axis.VAR, filter["var"], shape[1])
|
||||||
|
|
||||||
return (obs_selector, var_selector)
|
return (obs_selector, var_selector)
|
||||||
|
|
||||||
@@ -309,7 +307,7 @@ class DataAdaptor(metaclass=ABCMeta):
|
|||||||
|
|
||||||
embeddings = self.get_embedding_names()
|
embeddings = self.get_embedding_names()
|
||||||
layout_data = []
|
layout_data = []
|
||||||
with ServerTiming.time(f'layout.query'):
|
with ServerTiming.time(f"layout.query"):
|
||||||
for ename in embeddings:
|
for ename in embeddings:
|
||||||
embedding = self.get_embedding_array(ename, 2)
|
embedding = self.get_embedding_array(ename, 2)
|
||||||
|
|
||||||
@@ -326,7 +324,7 @@ class DataAdaptor(metaclass=ABCMeta):
|
|||||||
normalized_layout = normalized_layout.astype(dtype=np.float32)
|
normalized_layout = normalized_layout.astype(dtype=np.float32)
|
||||||
layout_data.append(pd.DataFrame(normalized_layout, columns=[f"{ename}_0", f"{ename}_1"]))
|
layout_data.append(pd.DataFrame(normalized_layout, columns=[f"{ename}_0", f"{ename}_1"]))
|
||||||
|
|
||||||
with ServerTiming.time(f'layout.encode'):
|
with ServerTiming.time(f"layout.encode"):
|
||||||
if layout_data:
|
if layout_data:
|
||||||
df = pd.concat(layout_data, axis=1, copy=False)
|
df = pd.concat(layout_data, axis=1, copy=False)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -135,7 +135,6 @@ class MatrixDataType(Enum):
|
|||||||
|
|
||||||
|
|
||||||
class MatrixDataLoader(object):
|
class MatrixDataLoader(object):
|
||||||
|
|
||||||
def __init__(self, location, etype=None):
|
def __init__(self, location, etype=None):
|
||||||
self.location = location
|
self.location = location
|
||||||
if etype is None:
|
if etype is None:
|
||||||
@@ -145,9 +144,11 @@ class MatrixDataLoader(object):
|
|||||||
self.matrix_type = None
|
self.matrix_type = None
|
||||||
if self.etype == MatrixDataType.H5AD:
|
if self.etype == MatrixDataType.H5AD:
|
||||||
from server.data_anndata.anndata_adaptor import AnndataAdaptor
|
from server.data_anndata.anndata_adaptor import AnndataAdaptor
|
||||||
|
|
||||||
self.matrix_type = AnndataAdaptor
|
self.matrix_type = AnndataAdaptor
|
||||||
elif self.etype == MatrixDataType.CXG:
|
elif self.etype == MatrixDataType.CXG:
|
||||||
from server.data_cxg.cxg_adaptor import CxgAdaptor
|
from server.data_cxg.cxg_adaptor import CxgAdaptor
|
||||||
|
|
||||||
self.matrix_type = CxgAdaptor
|
self.matrix_type = CxgAdaptor
|
||||||
|
|
||||||
def matrix_data_type(self):
|
def matrix_data_type(self):
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from threading import Lock
|
|||||||
# _______________________________________________________________________
|
# _______________________________________________________________________
|
||||||
# Class
|
# Class
|
||||||
|
|
||||||
|
|
||||||
class RWLock(object):
|
class RWLock(object):
|
||||||
""" RWLock class; this is meant to allow an object to be read from by
|
""" RWLock class; this is meant to allow an object to be read from by
|
||||||
multiple threads, but only written to by a single thread at a time. See:
|
multiple threads, but only written to by a single thread at a time. See:
|
||||||
|
|||||||
@@ -16,10 +16,7 @@ import threading
|
|||||||
class CxgAdaptor(DataAdaptor):
|
class CxgAdaptor(DataAdaptor):
|
||||||
|
|
||||||
# TODO: The tiledb context parameters should be a configuration option
|
# TODO: The tiledb context parameters should be a configuration option
|
||||||
tiledb_ctx = tiledb.Ctx({
|
tiledb_ctx = tiledb.Ctx({"sm.tile_cache_size": 8 * 1024 * 1024 * 1024, "sm.num_reader_threads": 32})
|
||||||
'sm.tile_cache_size': 8 * 1024 * 1024 * 1024,
|
|
||||||
'sm.num_reader_threads': 32,
|
|
||||||
})
|
|
||||||
|
|
||||||
def __init__(self, location, config=None):
|
def __init__(self, location, config=None):
|
||||||
super().__init__(config)
|
super().__init__(config)
|
||||||
@@ -28,8 +25,8 @@ class CxgAdaptor(DataAdaptor):
|
|||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
self.url = location
|
self.url = location
|
||||||
if self.url[-1] != '/':
|
if self.url[-1] != "/":
|
||||||
self.url += '/'
|
self.url += "/"
|
||||||
|
|
||||||
self._validate_and_initialize()
|
self._validate_and_initialize()
|
||||||
|
|
||||||
@@ -79,19 +76,18 @@ class CxgAdaptor(DataAdaptor):
|
|||||||
returns list of (absolute paths, type) *without* trailing slash
|
returns list of (absolute paths, type) *without* trailing slash
|
||||||
in the path.
|
in the path.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _cleanpath(p):
|
def _cleanpath(p):
|
||||||
if p[-1] == '/':
|
if p[-1] == "/":
|
||||||
return p[:-1]
|
return p[:-1]
|
||||||
else:
|
else:
|
||||||
return p
|
return p
|
||||||
|
|
||||||
if uri[-1] != '/':
|
if uri[-1] != "/":
|
||||||
uri += '/'
|
uri += "/"
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
tiledb.ls(uri,
|
tiledb.ls(uri, lambda path, type: result.append((_cleanpath(path), type)), ctx=self.tiledb_ctx)
|
||||||
lambda path, type: result.append((_cleanpath(path), type)),
|
|
||||||
ctx=self.tiledb_ctx)
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -135,13 +131,13 @@ class CxgAdaptor(DataAdaptor):
|
|||||||
elif a_type == "array":
|
elif a_type == "array":
|
||||||
# version >0
|
# version >0
|
||||||
gmd = self.open_array("cxg_group_metadata")
|
gmd = self.open_array("cxg_group_metadata")
|
||||||
cxg_version = gmd.meta['cxg_version']
|
cxg_version = gmd.meta["cxg_version"]
|
||||||
if cxg_version == "0.1":
|
if cxg_version == "0.1":
|
||||||
cxg_properties = json.loads(gmd.meta['cxg_properties'])
|
cxg_properties = json.loads(gmd.meta["cxg_properties"])
|
||||||
title = cxg_properties.get('title', None)
|
title = cxg_properties.get("title", None)
|
||||||
about = cxg_properties.get('about', None)
|
about = cxg_properties.get("about", None)
|
||||||
|
|
||||||
if cxg_version not in ['0.0', '0.1']:
|
if cxg_version not in ["0.0", "0.1"]:
|
||||||
raise DatasetAccessError(f"cxg matrix is not valid: {self.url}")
|
raise DatasetAccessError(f"cxg matrix is not valid: {self.url}")
|
||||||
|
|
||||||
self.title = title
|
self.title = title
|
||||||
@@ -175,7 +171,7 @@ class CxgAdaptor(DataAdaptor):
|
|||||||
if obs_items == slice(None) and var_items == slice(None):
|
if obs_items == slice(None) and var_items == slice(None):
|
||||||
data = X[:, :]
|
data = X[:, :]
|
||||||
else:
|
else:
|
||||||
data = X.multi_index[obs_items, var_items]['']
|
data = X.multi_index[obs_items, var_items][""]
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def get_shape(self):
|
def get_shape(self):
|
||||||
@@ -222,11 +218,9 @@ class CxgAdaptor(DataAdaptor):
|
|||||||
# function to get the embedding
|
# function to get the embedding
|
||||||
# this function to iterate through embeddings.
|
# this function to iterate through embeddings.
|
||||||
def get_embedding_names(self):
|
def get_embedding_names(self):
|
||||||
with ServerTiming.time(f'layout.lsuri'):
|
with ServerTiming.time(f"layout.lsuri"):
|
||||||
pemb = self.get_path("emb")
|
pemb = self.get_path("emb")
|
||||||
embeddings = [
|
embeddings = [os.path.basename(p) for (p, t) in self.lsuri(pemb) if t == "array"]
|
||||||
os.path.basename(p) for (p, t) in self.lsuri(pemb) if t == 'array'
|
|
||||||
]
|
|
||||||
return embeddings
|
return embeddings
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -235,82 +229,68 @@ class CxgAdaptor(DataAdaptor):
|
|||||||
dtype = attr.dtype
|
dtype = attr.dtype
|
||||||
schema = {}
|
schema = {}
|
||||||
# type hints take precedence
|
# type hints take precedence
|
||||||
if 'type' in type_hint:
|
if "type" in type_hint:
|
||||||
schema['type'] = type_hint['type']
|
schema["type"] = type_hint["type"]
|
||||||
elif dtype == np.float32:
|
elif dtype == np.float32:
|
||||||
schema['type'] = 'float32'
|
schema["type"] = "float32"
|
||||||
elif dtype == np.int32:
|
elif dtype == np.int32:
|
||||||
schema['type'] = 'int32'
|
schema["type"] = "int32"
|
||||||
elif dtype == np.bool_:
|
elif dtype == np.bool_:
|
||||||
schema['type'] = 'boolean'
|
schema["type"] = "boolean"
|
||||||
elif dtype == np.str:
|
elif dtype == np.str:
|
||||||
schema['type'] = 'string'
|
schema["type"] = "string"
|
||||||
elif dtype == "category":
|
elif dtype == "category":
|
||||||
schema["type"] = "categorical"
|
schema["type"] = "categorical"
|
||||||
schema["categories"] = dtype.categories.tolist()
|
schema["categories"] = dtype.categories.tolist()
|
||||||
else:
|
else:
|
||||||
raise TypeError(
|
raise TypeError(f"Annotations of type {dtype} are unsupported.")
|
||||||
f"Annotations of type {dtype} are unsupported."
|
|
||||||
)
|
|
||||||
|
|
||||||
if schema['type'] == 'categorical' and 'categories' in schema_hints:
|
if schema["type"] == "categorical" and "categories" in schema_hints:
|
||||||
schema['categories'] = schema_hints['categories']
|
schema["categories"] = schema_hints["categories"]
|
||||||
return schema
|
return schema
|
||||||
|
|
||||||
def get_schema(self):
|
def get_schema(self):
|
||||||
shape = self.get_shape()
|
shape = self.get_shape()
|
||||||
dtype = self.get_X_array_dtype()
|
dtype = self.get_X_array_dtype()
|
||||||
|
|
||||||
dataframe = {
|
dataframe = {"nObs": shape[0], "nVar": shape[1], "type": dtype.name}
|
||||||
'nObs': shape[0],
|
|
||||||
'nVar': shape[1],
|
|
||||||
'type': dtype.name
|
|
||||||
}
|
|
||||||
|
|
||||||
annotations = {}
|
annotations = {}
|
||||||
for ax in ('obs', 'var'):
|
for ax in ("obs", "var"):
|
||||||
A = self.open_array(ax)
|
A = self.open_array(ax)
|
||||||
schema_hints = json.loads(A.meta['cxg_schema']) if 'cxg_schema' in A.meta else {}
|
schema_hints = json.loads(A.meta["cxg_schema"]) if "cxg_schema" in A.meta else {}
|
||||||
if type(schema_hints) is not dict:
|
if type(schema_hints) is not dict:
|
||||||
raise TypeError(f'Array schema was malformed.')
|
raise TypeError(f"Array schema was malformed.")
|
||||||
|
|
||||||
cols = []
|
cols = []
|
||||||
for attr in A.schema:
|
for attr in A.schema:
|
||||||
schema = dict(name=attr.name, writable=False)
|
schema = dict(name=attr.name, writable=False)
|
||||||
type_hint = schema_hints.get(attr.name, {})
|
type_hint = schema_hints.get(attr.name, {})
|
||||||
# type hints take precedence
|
# type hints take precedence
|
||||||
if 'type' in type_hint:
|
if "type" in type_hint:
|
||||||
schema['type'] = type_hint['type']
|
schema["type"] = type_hint["type"]
|
||||||
if schema['type'] == 'categorical' and 'categories' in type_hint:
|
if schema["type"] == "categorical" and "categories" in type_hint:
|
||||||
schema['categories'] = type_hint['categories']
|
schema["categories"] = type_hint["categories"]
|
||||||
else:
|
else:
|
||||||
schema.update(dtype_to_schema(attr.dtype))
|
schema.update(dtype_to_schema(attr.dtype))
|
||||||
cols.append(schema)
|
cols.append(schema)
|
||||||
|
|
||||||
annotations[ax] = dict(columns=cols)
|
annotations[ax] = dict(columns=cols)
|
||||||
|
|
||||||
if 'index' in schema_hints:
|
if "index" in schema_hints:
|
||||||
annotations[ax].update({'index': schema_hints['index']})
|
annotations[ax].update({"index": schema_hints["index"]})
|
||||||
|
|
||||||
obs_layout = []
|
obs_layout = []
|
||||||
embeddings = self.get_embedding_names()
|
embeddings = self.get_embedding_names()
|
||||||
for ename in embeddings:
|
for ename in embeddings:
|
||||||
A = self.open_array(f"emb/{ename}")
|
A = self.open_array(f"emb/{ename}")
|
||||||
obs_layout.append({
|
obs_layout.append({"name": ename, "type": A.dtype.name, "dims": [f"{ename}_{d}" for d in range(0, A.ndim)]})
|
||||||
'name': ename,
|
|
||||||
'type': A.dtype.name,
|
|
||||||
'dims': [f'{ename}_{d}' for d in range(0, A.ndim)]
|
|
||||||
})
|
|
||||||
|
|
||||||
schema = {
|
schema = {"dataframe": dataframe, "annotations": annotations, "layout": {"obs": obs_layout}}
|
||||||
'dataframe': dataframe,
|
|
||||||
'annotations': annotations,
|
|
||||||
'layout': {'obs': obs_layout}
|
|
||||||
}
|
|
||||||
return schema
|
return schema
|
||||||
|
|
||||||
def annotation_to_fbs_matrix(self, axis, fields=None, labels=None):
|
def annotation_to_fbs_matrix(self, axis, fields=None, labels=None):
|
||||||
with ServerTiming.time(f'annotations.{axis}.query'):
|
with ServerTiming.time(f"annotations.{axis}.query"):
|
||||||
A = self.open_array(str(axis))
|
A = self.open_array(str(axis))
|
||||||
if fields is not None and len(fields) > 0:
|
if fields is not None and len(fields) > 0:
|
||||||
try:
|
try:
|
||||||
@@ -326,7 +306,7 @@ class CxgAdaptor(DataAdaptor):
|
|||||||
obs_names = self.get_obs_names()
|
obs_names = self.get_obs_names()
|
||||||
df = df.join(labels, obs_names)
|
df = df.join(labels, obs_names)
|
||||||
|
|
||||||
with ServerTiming.time(f'annotations.{axis}.encode'):
|
with ServerTiming.time(f"annotations.{axis}.encode"):
|
||||||
fbs = encode_matrix_fbs(df, col_idx=df.columns)
|
fbs = encode_matrix_fbs(df, col_idx=df.columns)
|
||||||
|
|
||||||
return fbs
|
return fbs
|
||||||
@@ -337,7 +317,7 @@ class CxgAdaptor(DataAdaptor):
|
|||||||
if boolarray is None:
|
if boolarray is None:
|
||||||
return slice(None)
|
return slice(None)
|
||||||
assert type(boolarray) == np.ndarray
|
assert type(boolarray) == np.ndarray
|
||||||
assert(boolarray.dtype) == bool
|
assert (boolarray.dtype) == bool
|
||||||
|
|
||||||
selector = np.nonzero(boolarray)[0]
|
selector = np.nonzero(boolarray)[0]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
from os import path
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from server.common.annotations import AnnotationsLocalFile
|
||||||
|
from server.common.data_locator import DataLocator
|
||||||
|
from server.data_common.fbs.matrix import encode_matrix_fbs
|
||||||
|
from server.data_common.matrix_loader import MatrixDataLoader, MatrixDataType
|
||||||
|
|
||||||
|
|
||||||
|
def data_with_tmp_annotations(ext: MatrixDataType, annotations_fixture=False):
|
||||||
|
tmp_dir = tempfile.mkdtemp()
|
||||||
|
annotations_file = path.join(tmp_dir, "test_annotations.csv")
|
||||||
|
if annotations_fixture:
|
||||||
|
shutil.copyfile(f"test/test_datasets/pbmc3k-annotations.csv", annotations_file)
|
||||||
|
args = {
|
||||||
|
"layout": ["umap"],
|
||||||
|
"max_category_items": 100,
|
||||||
|
"obs_names": None,
|
||||||
|
"var_names": None,
|
||||||
|
"diffexp_lfc_cutoff": 0.01,
|
||||||
|
}
|
||||||
|
fname = {
|
||||||
|
MatrixDataType.H5AD: "../example-dataset/pbmc3k.h5ad",
|
||||||
|
MatrixDataType.CXG: "test/test_datasets/pbmc3k.cxg",
|
||||||
|
}[ext]
|
||||||
|
data_locator = DataLocator(fname)
|
||||||
|
data = MatrixDataLoader(data_locator.abspath()).open(args)
|
||||||
|
annotations = AnnotationsLocalFile(None, annotations_file)
|
||||||
|
return data, tmp_dir, annotations
|
||||||
|
|
||||||
|
|
||||||
|
def make_fbs(data):
|
||||||
|
df = pd.DataFrame(data)
|
||||||
|
return encode_matrix_fbs(matrix=df, row_idx=None, col_idx=df.columns)
|
||||||
|
|
||||||
|
|
||||||
|
def skip_if(condition, reason: str):
|
||||||
|
def decorator(f):
|
||||||
|
def wraps(self, *args, **kwargs):
|
||||||
|
if condition(self):
|
||||||
|
self.skipTest(reason)
|
||||||
|
else:
|
||||||
|
f(self, *args, **kwargs)
|
||||||
|
|
||||||
|
return wraps
|
||||||
|
|
||||||
|
return decorator
|
||||||
@@ -3,7 +3,7 @@ from os import path
|
|||||||
import pytest
|
import pytest
|
||||||
import time
|
import time
|
||||||
import unittest
|
import unittest
|
||||||
import decode_fbs
|
import server.test.decode_fbs as decode_fbs
|
||||||
from parameterized import parameterized_class
|
from parameterized import parameterized_class
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|||||||
+181
-40
@@ -1,17 +1,24 @@
|
|||||||
|
import shutil
|
||||||
|
import time
|
||||||
|
import unittest
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from subprocess import Popen
|
from subprocess import Popen
|
||||||
import unittest
|
|
||||||
import time
|
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
import decode_fbs
|
import server.test.decode_fbs as decode_fbs
|
||||||
|
from server.test import skip_if, data_with_tmp_annotations, make_fbs
|
||||||
|
from server.data_common.matrix_loader import MatrixDataType
|
||||||
|
|
||||||
BAD_FILTER = {"filter": {"obs": {"annotation_value": [{"name": "xyz"}]}}}
|
BAD_FILTER = {"filter": {"obs": {"annotation_value": [{"name": "xyz"}]}}}
|
||||||
|
|
||||||
|
# TODO (mweiden): remove ANNOTATIONS_ENABLED and Annotation subclasses when annotations are no longer experimental
|
||||||
|
# TODO (mweiden): remove MATRIX_DATA_TYPE and skip_if when user annotations for the CXG format is complete
|
||||||
|
|
||||||
|
|
||||||
class EndPoints(object):
|
class EndPoints(object):
|
||||||
|
ANNOTATIONS_ENABLED = False
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.session = requests.Session()
|
self.session = requests.Session()
|
||||||
@@ -25,7 +32,9 @@ class EndPoints(object):
|
|||||||
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"]), 2)
|
self.assertEqual(len(result_data["schema"]["annotations"]["obs"]), 2)
|
||||||
self.assertEqual(len(result_data["schema"]["annotations"]["obs"]["columns"]), 5)
|
self.assertEqual(
|
||||||
|
len(result_data["schema"]["annotations"]["obs"]["columns"]), 6 if self.ANNOTATIONS_ENABLED else 5
|
||||||
|
)
|
||||||
|
|
||||||
def test_config(self):
|
def test_config(self):
|
||||||
endpoint = "config"
|
endpoint = "config"
|
||||||
@@ -51,7 +60,7 @@ class EndPoints(object):
|
|||||||
self.assertIsNotNone(df["columns"])
|
self.assertIsNotNone(df["columns"])
|
||||||
self.assertSetEqual(
|
self.assertSetEqual(
|
||||||
set(df["col_idx"]),
|
set(df["col_idx"]),
|
||||||
set(["pca_0", "pca_1", "tsne_0", "tsne_1", "umap_0", "umap_1", "draw_graph_fr_0", "draw_graph_fr_1"]),
|
{"pca_0", "pca_1", "tsne_0", "tsne_1", "umap_0", "umap_1", "draw_graph_fr_0", "draw_graph_fr_1"},
|
||||||
)
|
)
|
||||||
self.assertIsNone(df["row_idx"])
|
self.assertIsNone(df["row_idx"])
|
||||||
self.assertEqual(len(df["columns"]), df["n_cols"])
|
self.assertEqual(len(df["columns"]), df["n_cols"])
|
||||||
@@ -71,14 +80,21 @@ class EndPoints(object):
|
|||||||
self.assertEqual(result.headers["Content-Type"], "application/octet-stream")
|
self.assertEqual(result.headers["Content-Type"], "application/octet-stream")
|
||||||
df = decode_fbs.decode_matrix_FBS(result.content)
|
df = decode_fbs.decode_matrix_FBS(result.content)
|
||||||
self.assertEqual(df["n_rows"], 2638)
|
self.assertEqual(df["n_rows"], 2638)
|
||||||
self.assertEqual(df["n_cols"], 5)
|
self.assertEqual(df["n_cols"], 6 if self.ANNOTATIONS_ENABLED else 5)
|
||||||
self.assertIsNotNone(df["columns"])
|
self.assertIsNotNone(df["columns"])
|
||||||
self.assertIsNotNone(df["col_idx"])
|
|
||||||
self.assertIsNone(df["row_idx"])
|
self.assertIsNone(df["row_idx"])
|
||||||
self.assertEqual(len(df["columns"]), df["n_cols"])
|
self.assertEqual(len(df["columns"]), df["n_cols"])
|
||||||
obs_index_col_name = self.schema["schema"]["annotations"]["obs"]["index"]
|
obs_index_col_name = self.schema["schema"]["annotations"]["obs"]["index"]
|
||||||
self.assertListEqual(df["col_idx"], [obs_index_col_name, "n_genes", "percent_mito", "n_counts", "louvain"])
|
self.assertListEqual(
|
||||||
|
df["col_idx"],
|
||||||
|
[obs_index_col_name, "n_genes", "percent_mito", "n_counts", "louvain"]
|
||||||
|
+ (["cluster-test"] if self.ANNOTATIONS_ENABLED else []),
|
||||||
|
)
|
||||||
|
|
||||||
|
@skip_if(
|
||||||
|
lambda slf: hasattr(slf, "MATRIX_DATA_TYPE") and slf.MATRIX_DATA_TYPE == MatrixDataType.CXG,
|
||||||
|
"CXG file annotations are not feature-complete!",
|
||||||
|
)
|
||||||
def test_get_annotations_obs_keys_fbs(self):
|
def test_get_annotations_obs_keys_fbs(self):
|
||||||
endpoint = "annotations/obs"
|
endpoint = "annotations/obs"
|
||||||
query = "annotation-name=n_genes&annotation-name=percent_mito"
|
query = "annotation-name=n_genes&annotation-name=percent_mito"
|
||||||
@@ -91,7 +107,6 @@ class EndPoints(object):
|
|||||||
self.assertEqual(df["n_rows"], 2638)
|
self.assertEqual(df["n_rows"], 2638)
|
||||||
self.assertEqual(df["n_cols"], 2)
|
self.assertEqual(df["n_cols"], 2)
|
||||||
self.assertIsNotNone(df["columns"])
|
self.assertIsNotNone(df["columns"])
|
||||||
self.assertIsNotNone(df["col_idx"])
|
|
||||||
self.assertIsNone(df["row_idx"])
|
self.assertIsNone(df["row_idx"])
|
||||||
self.assertEqual(len(df["columns"]), df["n_cols"])
|
self.assertEqual(len(df["columns"]), df["n_cols"])
|
||||||
self.assertListEqual(df["col_idx"], ["n_genes", "percent_mito"])
|
self.assertListEqual(df["col_idx"], ["n_genes", "percent_mito"])
|
||||||
@@ -144,7 +159,6 @@ class EndPoints(object):
|
|||||||
self.assertEqual(df["n_rows"], 1838)
|
self.assertEqual(df["n_rows"], 1838)
|
||||||
self.assertEqual(df["n_cols"], 2)
|
self.assertEqual(df["n_cols"], 2)
|
||||||
self.assertIsNotNone(df["columns"])
|
self.assertIsNotNone(df["columns"])
|
||||||
self.assertIsNotNone(df["col_idx"])
|
|
||||||
self.assertIsNone(df["row_idx"])
|
self.assertIsNone(df["row_idx"])
|
||||||
self.assertEqual(len(df["columns"]), df["n_cols"])
|
self.assertEqual(len(df["columns"]), df["n_cols"])
|
||||||
var_index_col_name = self.schema["schema"]["annotations"]["var"]["index"]
|
var_index_col_name = self.schema["schema"]["annotations"]["var"]["index"]
|
||||||
@@ -162,7 +176,6 @@ class EndPoints(object):
|
|||||||
self.assertEqual(df["n_rows"], 1838)
|
self.assertEqual(df["n_rows"], 1838)
|
||||||
self.assertEqual(df["n_cols"], 1)
|
self.assertEqual(df["n_cols"], 1)
|
||||||
self.assertIsNotNone(df["columns"])
|
self.assertIsNotNone(df["columns"])
|
||||||
self.assertIsNotNone(df["col_idx"])
|
|
||||||
self.assertIsNone(df["row_idx"])
|
self.assertIsNone(df["row_idx"])
|
||||||
self.assertEqual(len(df["columns"]), df["n_cols"])
|
self.assertEqual(len(df["columns"]), df["n_cols"])
|
||||||
self.assertListEqual(df["col_idx"], ["n_cells"])
|
self.assertListEqual(df["col_idx"], ["n_cells"])
|
||||||
@@ -215,7 +228,6 @@ class EndPoints(object):
|
|||||||
self.assertEqual(df["n_rows"], 2638)
|
self.assertEqual(df["n_rows"], 2638)
|
||||||
self.assertEqual(df["n_cols"], 3)
|
self.assertEqual(df["n_cols"], 3)
|
||||||
self.assertIsNotNone(df["columns"])
|
self.assertIsNotNone(df["columns"])
|
||||||
self.assertIsNotNone(df["col_idx"])
|
|
||||||
self.assertIsNone(df["row_idx"])
|
self.assertIsNone(df["row_idx"])
|
||||||
self.assertEqual(len(df["columns"]), df["n_cols"])
|
self.assertEqual(len(df["columns"]), df["n_cols"])
|
||||||
self.assertListEqual(df["col_idx"].tolist(), [0, 1, 4])
|
self.assertListEqual(df["col_idx"].tolist(), [0, 1, 4])
|
||||||
@@ -240,6 +252,77 @@ class EndPoints(object):
|
|||||||
result = self.session.get(url)
|
result = self.session.get(url)
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
self.assertEqual(result.status_code, HTTPStatus.OK)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _setUpClass(child_class, start_command):
|
||||||
|
child_class.ps = Popen(start_command)
|
||||||
|
child_class.session = requests.Session()
|
||||||
|
for i in range(90):
|
||||||
|
try:
|
||||||
|
result = child_class.session.get(f"{child_class.URL_BASE}schema")
|
||||||
|
child_class.schema = result.json()
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _tearDownClass(child_class):
|
||||||
|
try:
|
||||||
|
child_class.ps.terminate()
|
||||||
|
except ProcessLookupError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EndPointsAnnotations(EndPoints):
|
||||||
|
def test_get_schema_existing_writable(self):
|
||||||
|
self._test_get_schema_writable("cluster-test")
|
||||||
|
|
||||||
|
@skip_if(lambda slf: slf.MATRIX_DATA_TYPE == MatrixDataType.CXG, "CXG file annotations are not feature-complete!")
|
||||||
|
def test_get_user_annotations_existing_obs_keys_fbs(self):
|
||||||
|
self._test_get_user_annotations_obs_keys_fbs(
|
||||||
|
"cluster-test", {"unassigned", "one", "two", "three", "four", "five"},
|
||||||
|
)
|
||||||
|
|
||||||
|
@skip_if(lambda slf: slf.MATRIX_DATA_TYPE == MatrixDataType.CXG, "CXG file annotations are not feature-complete!")
|
||||||
|
def test_put_user_annotations_obs_fbs(self):
|
||||||
|
endpoint = "annotations/obs"
|
||||||
|
query = "annotation-collection-name=test_annotations"
|
||||||
|
url = f"{self.URL_BASE}{endpoint}?{query}"
|
||||||
|
n_rows = self.data.get_shape()[0]
|
||||||
|
fbs = make_fbs({"cat_A": pd.Series(["label_A" for l in range(0, n_rows)], dtype="category")})
|
||||||
|
result = self.session.put(url, data=fbs)
|
||||||
|
self.assertEqual(result.status_code, HTTPStatus.OK)
|
||||||
|
self.assertEqual(result.headers["Content-Type"], "application/json")
|
||||||
|
self.assertEqual(result.json(), {"status": "OK"})
|
||||||
|
self._test_get_schema_writable("cat_A")
|
||||||
|
self._test_get_user_annotations_obs_keys_fbs("cat_A", {"label_A"})
|
||||||
|
|
||||||
|
def _test_get_user_annotations_obs_keys_fbs(self, annotation_name, columns):
|
||||||
|
endpoint = "annotations/obs"
|
||||||
|
query = f"annotation-name={annotation_name}"
|
||||||
|
url = f"{self.URL_BASE}{endpoint}?{query}"
|
||||||
|
header = {"Accept": "application/octet-stream"}
|
||||||
|
result = self.session.get(url, headers=header)
|
||||||
|
self.assertEqual(result.status_code, HTTPStatus.OK)
|
||||||
|
self.assertEqual(result.headers["Content-Type"], "application/octet-stream")
|
||||||
|
df = decode_fbs.decode_matrix_FBS(result.content)
|
||||||
|
self.assertEqual(df["n_rows"], 2638)
|
||||||
|
self.assertEqual(df["n_cols"], 1)
|
||||||
|
self.assertListEqual(df["col_idx"], [annotation_name])
|
||||||
|
self.assertEqual(set(df["columns"][0]), columns)
|
||||||
|
self.assertIsNone(df["row_idx"])
|
||||||
|
self.assertEqual(len(df["columns"]), df["n_cols"])
|
||||||
|
|
||||||
|
def _test_get_schema_writable(self, cluster_name):
|
||||||
|
endpoint = "schema"
|
||||||
|
url = f"{self.URL_BASE}{endpoint}"
|
||||||
|
result = self.session.get(url)
|
||||||
|
self.assertEqual(result.status_code, HTTPStatus.OK)
|
||||||
|
self.assertEqual(result.headers["Content-Type"], "application/json")
|
||||||
|
result_data = result.json()
|
||||||
|
columns = result_data["schema"]["annotations"]["obs"]["columns"]
|
||||||
|
matching_columns = [c for c in columns if c["name"] == cluster_name]
|
||||||
|
self.assertEqual(len(matching_columns), 1)
|
||||||
|
self.assertTrue(matching_columns[0]["writable"])
|
||||||
|
|
||||||
|
|
||||||
class EndPointsAnndata(unittest.TestCase, EndPoints):
|
class EndPointsAnndata(unittest.TestCase, EndPoints):
|
||||||
"""Test Case for endpoints"""
|
"""Test Case for endpoints"""
|
||||||
@@ -251,7 +334,8 @@ class EndPointsAnndata(unittest.TestCase, EndPoints):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
cls.ps = Popen(
|
cls._setUpClass(
|
||||||
|
cls,
|
||||||
[
|
[
|
||||||
"cellxgene",
|
"cellxgene",
|
||||||
"--no-upgrade-check",
|
"--no-upgrade-check",
|
||||||
@@ -260,22 +344,16 @@ class EndPointsAnndata(unittest.TestCase, EndPoints):
|
|||||||
"--verbose",
|
"--verbose",
|
||||||
"--port",
|
"--port",
|
||||||
str(cls.PORT),
|
str(cls.PORT),
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
cls.session = requests.Session()
|
|
||||||
for i in range(90):
|
|
||||||
try:
|
|
||||||
result = cls.session.get(f"{cls.URL_BASE}schema")
|
|
||||||
cls.schema = result.json()
|
|
||||||
except requests.exceptions.ConnectionError:
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(cls):
|
def tearDownClass(cls):
|
||||||
try:
|
cls._tearDownClass(cls)
|
||||||
cls.ps.terminate()
|
|
||||||
except ProcessLookupError:
|
@property
|
||||||
pass
|
def annotations_enabled(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class EndPointsCxg(unittest.TestCase, EndPoints):
|
class EndPointsCxg(unittest.TestCase, EndPoints):
|
||||||
@@ -288,28 +366,91 @@ class EndPointsCxg(unittest.TestCase, EndPoints):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
cls.ps = Popen(
|
cls._setUpClass(
|
||||||
|
cls,
|
||||||
[
|
[
|
||||||
"cellxgene",
|
"cellxgene",
|
||||||
"--no-upgrade-check",
|
"--no-upgrade-check",
|
||||||
"launch",
|
"launch",
|
||||||
"../example-dataset/pbmc3k.cxg",
|
"test/test_datasets/pbmc3k.cxg",
|
||||||
"--verbose",
|
"--verbose",
|
||||||
"--port",
|
"--port",
|
||||||
str(cls.PORT),
|
str(cls.PORT),
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
cls.session = requests.Session()
|
|
||||||
for i in range(90):
|
|
||||||
try:
|
|
||||||
result = cls.session.get(f"{cls.URL_BASE}schema")
|
|
||||||
cls.schema = result.json()
|
|
||||||
except requests.exceptions.ConnectionError:
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(cls):
|
def tearDownClass(cls):
|
||||||
try:
|
cls._tearDownClass(cls)
|
||||||
cls.ps.terminate()
|
|
||||||
except ProcessLookupError:
|
|
||||||
pass
|
class EndPointsAnndataAnnotations(unittest.TestCase, EndPointsAnnotations):
|
||||||
|
"""Test Case for endpoints"""
|
||||||
|
|
||||||
|
PORT = 5012
|
||||||
|
LOCAL_URL = f"http://127.0.0.1:{PORT}/"
|
||||||
|
VERSION = "v0.2"
|
||||||
|
URL_BASE = f"{LOCAL_URL}api/{VERSION}/"
|
||||||
|
ANNOTATIONS_ENABLED = True
|
||||||
|
MATRIX_DATA_TYPE = MatrixDataType.H5AD
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.data, cls.tmp_dir, cls.annotations = data_with_tmp_annotations(
|
||||||
|
MatrixDataType.H5AD, annotations_fixture=True
|
||||||
|
)
|
||||||
|
cls._setUpClass(
|
||||||
|
cls,
|
||||||
|
[
|
||||||
|
"cellxgene",
|
||||||
|
"--no-upgrade-check",
|
||||||
|
"launch",
|
||||||
|
"--experimental-annotations",
|
||||||
|
"--experimental-annotations-file",
|
||||||
|
cls.annotations.output_file,
|
||||||
|
"--verbose",
|
||||||
|
"--port",
|
||||||
|
str(cls.PORT),
|
||||||
|
cls.data.get_location(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
shutil.rmtree(cls.tmp_dir)
|
||||||
|
cls._tearDownClass(cls)
|
||||||
|
|
||||||
|
|
||||||
|
class EndPointsCxgAnnotations(unittest.TestCase, EndPointsAnnotations):
|
||||||
|
"""Test Case for endpoints"""
|
||||||
|
|
||||||
|
PORT = 5013
|
||||||
|
LOCAL_URL = f"http://127.0.0.1:{PORT}/"
|
||||||
|
VERSION = "v0.2"
|
||||||
|
URL_BASE = f"{LOCAL_URL}api/{VERSION}/"
|
||||||
|
ANNOTATIONS_ENABLED = True
|
||||||
|
MATRIX_DATA_TYPE = MatrixDataType.CXG
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.data, cls.tmp_dir, cls.annotations = data_with_tmp_annotations(MatrixDataType.CXG, annotations_fixture=True)
|
||||||
|
cls._setUpClass(
|
||||||
|
cls,
|
||||||
|
[
|
||||||
|
"cellxgene",
|
||||||
|
"--no-upgrade-check",
|
||||||
|
"launch",
|
||||||
|
"--experimental-annotations",
|
||||||
|
"--experimental-annotations-file",
|
||||||
|
cls.annotations.output_file,
|
||||||
|
"--verbose",
|
||||||
|
"--port",
|
||||||
|
str(cls.PORT),
|
||||||
|
cls.data.get_location(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
shutil.rmtree(cls.tmp_dir)
|
||||||
|
cls._tearDownClass(cls)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import pandas as pd
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from scipy import sparse
|
from scipy import sparse
|
||||||
|
|
||||||
import decode_fbs
|
import server.test.decode_fbs as decode_fbs
|
||||||
from server.data_common.fbs.matrix import encode_matrix_fbs, decode_matrix_fbs
|
from server.data_common.fbs.matrix import encode_matrix_fbs, decode_matrix_fbs
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import unittest
|
|||||||
import warnings
|
import warnings
|
||||||
import math
|
import math
|
||||||
|
|
||||||
import decode_fbs
|
import server.test.decode_fbs as decode_fbs
|
||||||
|
|
||||||
from server.data_anndata.anndata_adaptor import AnndataAdaptor
|
from server.data_anndata.anndata_adaptor import AnndataAdaptor
|
||||||
from server.common.errors import FilterError
|
from server.common.errors import FilterError
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import unittest
|
|||||||
import time
|
import time
|
||||||
import math
|
import math
|
||||||
|
|
||||||
import decode_fbs
|
import server.test.decode_fbs as decode_fbs
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +1,23 @@
|
|||||||
import json
|
import json
|
||||||
from os import path, listdir
|
from os import path, listdir
|
||||||
import unittest
|
import unittest
|
||||||
import decode_fbs
|
import server.test.decode_fbs as decode_fbs
|
||||||
import tempfile
|
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from server.data_anndata.anndata_adaptor import AnndataAdaptor
|
|
||||||
from server.data_common.fbs.matrix import encode_matrix_fbs
|
|
||||||
from server.common.data_locator import DataLocator
|
|
||||||
from server.common.annotations import AnnotationsLocalFile
|
|
||||||
from server.common.rest import schema_get_helper, annotations_put_fbs_helper
|
from server.common.rest import schema_get_helper, annotations_put_fbs_helper
|
||||||
|
from server.test import data_with_tmp_annotations, make_fbs
|
||||||
|
from server.data_common.matrix_loader import MatrixDataType
|
||||||
|
|
||||||
|
|
||||||
class WritableAnnotationTest(unittest.TestCase):
|
class WritableAnnotationTest(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.tmpDir = tempfile.mkdtemp()
|
self.data, self.tmp_dir, self.annotations = data_with_tmp_annotations(MatrixDataType.H5AD)
|
||||||
self.annotations_file = path.join(self.tmpDir, "test_annotations.csv")
|
|
||||||
args = {
|
|
||||||
"layout": ["umap"],
|
|
||||||
"max_category_items": 100,
|
|
||||||
"obs_names": None,
|
|
||||||
"var_names": None,
|
|
||||||
"diffexp_lfc_cutoff": 0.01,
|
|
||||||
}
|
|
||||||
fname = "../example-dataset/pbmc3k.h5ad"
|
|
||||||
data_locator = DataLocator(fname)
|
|
||||||
self.data = AnndataAdaptor(data_locator, args)
|
|
||||||
self.annotations = AnnotationsLocalFile(None, self.annotations_file)
|
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
shutil.rmtree(self.tmpDir)
|
shutil.rmtree(self.tmp_dir)
|
||||||
|
|
||||||
def make_fbs(self, data):
|
|
||||||
df = pd.DataFrame(data)
|
|
||||||
return encode_matrix_fbs(matrix=df, row_idx=None, col_idx=df.columns)
|
|
||||||
|
|
||||||
def annotation_put_fbs(self, fbs):
|
def annotation_put_fbs(self, fbs):
|
||||||
annotations_put_fbs_helper(self.data, self.annotations, fbs)
|
annotations_put_fbs_helper(self.data, self.annotations, fbs)
|
||||||
@@ -45,8 +26,8 @@ class WritableAnnotationTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_error_checks(self):
|
def test_error_checks(self):
|
||||||
# verify that the expected errors are generated
|
# verify that the expected errors are generated
|
||||||
n_rows = self.data.data.obs.shape[0]
|
n_rows = self.data.get_shape()[0]
|
||||||
fbs_bad = self.make_fbs({"louvain": pd.Series(["undefined" for l in range(0, n_rows)], dtype="category")})
|
fbs_bad = make_fbs({"louvain": pd.Series(["undefined" for l in range(0, n_rows)], dtype="category")})
|
||||||
|
|
||||||
# ensure we catch attempt to overwrite non-writable data
|
# ensure we catch attempt to overwrite non-writable data
|
||||||
with self.assertRaises(KeyError):
|
with self.assertRaises(KeyError):
|
||||||
@@ -54,8 +35,8 @@ class WritableAnnotationTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_write_to_file(self):
|
def test_write_to_file(self):
|
||||||
# verify the file is written as expected
|
# verify the file is written as expected
|
||||||
n_rows = self.data.data.obs.shape[0]
|
n_rows = self.data.get_shape()[0]
|
||||||
fbs = self.make_fbs(
|
fbs = make_fbs(
|
||||||
{
|
{
|
||||||
"cat_A": pd.Series(["label_A" for l in range(0, n_rows)], dtype="category"),
|
"cat_A": pd.Series(["label_A" for l in range(0, n_rows)], dtype="category"),
|
||||||
"cat_B": pd.Series(["label_B" for l in range(0, n_rows)], dtype="category"),
|
"cat_B": pd.Series(["label_B" for l in range(0, n_rows)], dtype="category"),
|
||||||
@@ -63,8 +44,8 @@ class WritableAnnotationTest(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
res = self.annotation_put_fbs(fbs)
|
res = self.annotation_put_fbs(fbs)
|
||||||
self.assertEqual(res, json.dumps({"status": "OK"}))
|
self.assertEqual(res, json.dumps({"status": "OK"}))
|
||||||
self.assertTrue(path.exists(self.annotations_file))
|
self.assertTrue(path.exists(self.annotations.output_file))
|
||||||
df = pd.read_csv(self.annotations_file, index_col=0, header=0, comment="#")
|
df = pd.read_csv(self.annotations.output_file, index_col=0, header=0, comment="#")
|
||||||
self.assertEqual(df.shape, (n_rows, 2))
|
self.assertEqual(df.shape, (n_rows, 2))
|
||||||
self.assertEqual(set(df.columns), {"cat_A", "cat_B"})
|
self.assertEqual(set(df.columns), {"cat_A", "cat_B"})
|
||||||
self.assertTrue(self.data.original_obs_index.equals(df.index))
|
self.assertTrue(self.data.original_obs_index.equals(df.index))
|
||||||
@@ -72,7 +53,7 @@ class WritableAnnotationTest(unittest.TestCase):
|
|||||||
self.assertTrue(np.all(df["cat_B"] == ["label_B" for l in range(0, n_rows)]))
|
self.assertTrue(np.all(df["cat_B"] == ["label_B" for l in range(0, n_rows)]))
|
||||||
|
|
||||||
# verify complete overwrite on second attempt, AND rotation occurs
|
# verify complete overwrite on second attempt, AND rotation occurs
|
||||||
fbs = self.make_fbs(
|
fbs = make_fbs(
|
||||||
{
|
{
|
||||||
"cat_A": pd.Series(["label_A1" for l in range(0, n_rows)], dtype="category"),
|
"cat_A": pd.Series(["label_A1" for l in range(0, n_rows)], dtype="category"),
|
||||||
"cat_C": pd.Series(["label_C" for l in range(0, n_rows)], dtype="category"),
|
"cat_C": pd.Series(["label_C" for l in range(0, n_rows)], dtype="category"),
|
||||||
@@ -80,14 +61,14 @@ class WritableAnnotationTest(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
res = self.annotation_put_fbs(fbs)
|
res = self.annotation_put_fbs(fbs)
|
||||||
self.assertEqual(res, json.dumps({"status": "OK"}))
|
self.assertEqual(res, json.dumps({"status": "OK"}))
|
||||||
self.assertTrue(path.exists(self.annotations_file))
|
self.assertTrue(path.exists(self.annotations.output_file))
|
||||||
df = pd.read_csv(self.annotations_file, index_col=0, header=0, comment="#")
|
df = pd.read_csv(self.annotations.output_file, index_col=0, header=0, comment="#")
|
||||||
self.assertEqual(set(df.columns), {"cat_A", "cat_C"})
|
self.assertEqual(set(df.columns), {"cat_A", "cat_C"})
|
||||||
self.assertTrue(np.all(df["cat_A"] == ["label_A1" for l in range(0, n_rows)]))
|
self.assertTrue(np.all(df["cat_A"] == ["label_A1" for l in range(0, n_rows)]))
|
||||||
self.assertTrue(np.all(df["cat_C"] == ["label_C" for l in range(0, n_rows)]))
|
self.assertTrue(np.all(df["cat_C"] == ["label_C" for l in range(0, n_rows)]))
|
||||||
|
|
||||||
# rotation
|
# rotation
|
||||||
name, ext = path.splitext(self.annotations_file)
|
name, ext = path.splitext(self.annotations.output_file)
|
||||||
backup_dir = f"{name}-backups"
|
backup_dir = f"{name}-backups"
|
||||||
self.assertTrue(path.isdir(backup_dir))
|
self.assertTrue(path.isdir(backup_dir))
|
||||||
found_files = listdir(backup_dir)
|
found_files = listdir(backup_dir)
|
||||||
@@ -95,8 +76,8 @@ class WritableAnnotationTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_file_rotation_to_max_9(self):
|
def test_file_rotation_to_max_9(self):
|
||||||
# verify we stop rotation at 9
|
# verify we stop rotation at 9
|
||||||
n_rows = self.data.data.obs.shape[0]
|
n_rows = self.data.get_shape()[0]
|
||||||
fbs = self.make_fbs(
|
fbs = make_fbs(
|
||||||
{
|
{
|
||||||
"cat_A": pd.Series(["label_A" for l in range(0, n_rows)], dtype="category"),
|
"cat_A": pd.Series(["label_A" for l in range(0, n_rows)], dtype="category"),
|
||||||
"cat_B": pd.Series(["label_B" for l in range(0, n_rows)], dtype="category"),
|
"cat_B": pd.Series(["label_B" for l in range(0, n_rows)], dtype="category"),
|
||||||
@@ -106,7 +87,7 @@ class WritableAnnotationTest(unittest.TestCase):
|
|||||||
res = self.annotation_put_fbs(fbs)
|
res = self.annotation_put_fbs(fbs)
|
||||||
self.assertEqual(res, json.dumps({"status": "OK"}))
|
self.assertEqual(res, json.dumps({"status": "OK"}))
|
||||||
|
|
||||||
name, ext = path.splitext(self.annotations_file)
|
name, ext = path.splitext(self.annotations.output_file)
|
||||||
backup_dir = f"{name}-backups"
|
backup_dir = f"{name}-backups"
|
||||||
self.assertTrue(path.isdir(backup_dir))
|
self.assertTrue(path.isdir(backup_dir))
|
||||||
found_files = listdir(backup_dir)
|
found_files = listdir(backup_dir)
|
||||||
@@ -116,8 +97,8 @@ class WritableAnnotationTest(unittest.TestCase):
|
|||||||
# verify that OBS PUTs (annotation_put_fbs) are accessible via
|
# verify that OBS PUTs (annotation_put_fbs) are accessible via
|
||||||
# GET (annotation_to_fbs_matrix)
|
# GET (annotation_to_fbs_matrix)
|
||||||
|
|
||||||
n_rows = self.data.data.obs.shape[0]
|
n_rows = self.data.get_shape()[0]
|
||||||
fbs = self.make_fbs(
|
fbs = make_fbs(
|
||||||
{
|
{
|
||||||
"cat_A": pd.Series(["label_A" for l in range(0, n_rows)], dtype="category"),
|
"cat_A": pd.Series(["label_A" for l in range(0, n_rows)], dtype="category"),
|
||||||
"cat_B": pd.Series(["label_B" for l in range(0, n_rows)], dtype="category"),
|
"cat_B": pd.Series(["label_B" for l in range(0, n_rows)], dtype="category"),
|
||||||
|
|||||||
Reference in New Issue
Block a user