mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-26 03:38:12 +08:00
* Introduce a config file to cellxgene The config file format is in yaml. The default config is located in server/common/default_config.py. A user may create a yaml file that contains a subset of these fields. It can be used during cellxgene launch, or for hosted cellxgene. The code has also been refactored. Much of the logic to check arguments has moved from launch to app config. It is now possible to set the tiledb context parameters using the config file. Other feature will soon be handled in a similar way.
245 lines
8.3 KiB
Python
245 lines
8.3 KiB
Python
import os
|
|
import datetime
|
|
|
|
from flask import Flask, redirect, current_app, make_response, render_template, abort
|
|
from flask import Blueprint, request, send_from_directory
|
|
from flask_caching import Cache
|
|
from flask_compress import Compress
|
|
from flask_cors import CORS
|
|
from flask_restful import Api, Resource
|
|
|
|
from http import HTTPStatus
|
|
|
|
import server.common.rest as common_rest
|
|
from server.common.errors import DatasetAccessError
|
|
from server.common.utils import path_join, Float32JSONEncoder
|
|
from server.common.data_locator import DataLocator
|
|
from server.common.health import health_check
|
|
from server.data_common.matrix_loader import MatrixDataLoader
|
|
|
|
from functools import wraps
|
|
|
|
webbp = Blueprint("webapp", "server.common.web", template_folder="templates")
|
|
|
|
|
|
@webbp.route("/", methods=["GET"])
|
|
def dataset_index(dataset=None):
|
|
config = current_app.app_config
|
|
if dataset is None:
|
|
if config.single_dataset__datapath:
|
|
location = config.single_dataset__datapath
|
|
else:
|
|
return dataroot_index()
|
|
else:
|
|
location = path_join(config.multi_dataset__dataroot, dataset)
|
|
|
|
scripts = config.server__scripts
|
|
|
|
try:
|
|
cache_manager = current_app.matrix_data_cache_manager
|
|
with cache_manager.data_adaptor(location, config) as data_adaptor:
|
|
dataset_title = config.get_title(data_adaptor)
|
|
return render_template("index.html", datasetTitle=dataset_title, SCRIPTS=scripts)
|
|
except DatasetAccessError as e:
|
|
return make_response(f"Invalid dataset {dataset}: {str(e)}", HTTPStatus.BAD_REQUEST)
|
|
|
|
|
|
@webbp.route("/favicon.png", methods=["GET"])
|
|
def favicon():
|
|
return send_from_directory(os.path.join(webbp.root_path, "static/img/"), "favicon.png")
|
|
|
|
|
|
@webbp.route("/health", methods=["GET"])
|
|
def health():
|
|
config = current_app.app_config
|
|
return health_check(config)
|
|
|
|
|
|
def get_data_adaptor(dataset=None):
|
|
config = current_app.app_config
|
|
|
|
if dataset is None:
|
|
datapath = config.single_dataset__datapath
|
|
else:
|
|
datapath = path_join(config.multi_dataset__dataroot, dataset)
|
|
# path_join returns a normalized path. Therefore it is
|
|
# sufficient to check that the datapath starts with the
|
|
# dataroot to determine that the datapath is under the dataroot.
|
|
if not datapath.startswith(config.multi_dataset__dataroot):
|
|
raise DatasetAccessError("Invalid dataset {dataset}")
|
|
|
|
if datapath is None:
|
|
return make_response("Dataset must be supplied", HTTPStatus.BAD_REQUEST)
|
|
|
|
cache_manager = current_app.matrix_data_cache_manager
|
|
return cache_manager.data_adaptor(datapath, config)
|
|
|
|
|
|
def rest_get_data_adaptor(func):
|
|
@wraps(func)
|
|
def wrapped_function(self, dataset=None):
|
|
try:
|
|
with get_data_adaptor(dataset) as data_adaptor:
|
|
return func(self, data_adaptor)
|
|
except DatasetAccessError as e:
|
|
return make_response(f"Invalid dataset {dataset}: {str(e)}", HTTPStatus.BAD_REQUEST)
|
|
|
|
return wrapped_function
|
|
|
|
|
|
def static_redirect(dataset, therest):
|
|
""" redirect all static requests to the standard location """
|
|
return redirect(f"/static/{therest}", code=301)
|
|
|
|
|
|
def dataroot_test_index():
|
|
# the following index page is meant for testing/debugging purposes
|
|
data = '<!doctype html><html lang="en">'
|
|
data += "<head><title>Hosted Cellxgene</title></head>"
|
|
data += "<body><H1>Welcome to cellxgene</H1>"
|
|
|
|
try:
|
|
config = current_app.app_config
|
|
locator = DataLocator(config.multi_dataset__dataroot)
|
|
datasets = []
|
|
for fname in locator.ls():
|
|
location = path_join(config.multi_dataset__dataroot, fname)
|
|
try:
|
|
MatrixDataLoader(location, app_config=config)
|
|
datasets.append(fname)
|
|
except DatasetAccessError:
|
|
# skip over invalid datasets
|
|
pass
|
|
|
|
data += "<br/>Select one of these datasets...<br/>"
|
|
data += "<ul>"
|
|
datasets.sort()
|
|
for dataset in datasets:
|
|
data += f"<li><a href={dataset}>{dataset}</a></li>"
|
|
data += "</ul>"
|
|
except Exception as e:
|
|
data += f'<br/>Unable to locate datasets from {config.multi_dataset__dataroot}: {str(e)}'
|
|
|
|
data += "</body></html>"
|
|
return make_response(data)
|
|
|
|
|
|
def dataroot_index():
|
|
# Handle the base url for the cellxgene server when running in multi dataset mode
|
|
config = current_app.app_config
|
|
if not config.multi_dataset__index:
|
|
abort(404)
|
|
elif config.multi_dataset__index is True:
|
|
return dataroot_test_index()
|
|
else:
|
|
return redirect(config.multi_dataset__index)
|
|
|
|
|
|
class SchemaAPI(Resource):
|
|
@rest_get_data_adaptor
|
|
def get(self, data_adaptor):
|
|
return common_rest.schema_get(data_adaptor, current_app.annotations)
|
|
|
|
|
|
class ConfigAPI(Resource):
|
|
@rest_get_data_adaptor
|
|
def get(self, data_adaptor):
|
|
return common_rest.config_get(current_app.app_config, data_adaptor, current_app.annotations)
|
|
|
|
|
|
class AnnotationsObsAPI(Resource):
|
|
@rest_get_data_adaptor
|
|
def get(self, data_adaptor):
|
|
return common_rest.annotations_obs_get(request, data_adaptor, current_app.annotations)
|
|
|
|
@rest_get_data_adaptor
|
|
def put(self, data_adaptor):
|
|
return common_rest.annotations_obs_put(request, data_adaptor, current_app.annotations)
|
|
|
|
|
|
class AnnotationsVarAPI(Resource):
|
|
@rest_get_data_adaptor
|
|
def get(self, data_adaptor):
|
|
return common_rest.annotations_var_get(request, data_adaptor, current_app.annotations)
|
|
|
|
|
|
class DataVarAPI(Resource):
|
|
@rest_get_data_adaptor
|
|
def put(self, data_adaptor):
|
|
return common_rest.data_var_put(request, data_adaptor)
|
|
|
|
|
|
class DiffExpObsAPI(Resource):
|
|
@rest_get_data_adaptor
|
|
def post(self, data_adaptor):
|
|
return common_rest.diffexp_obs_post(request, data_adaptor)
|
|
|
|
|
|
class LayoutObsAPI(Resource):
|
|
@rest_get_data_adaptor
|
|
def get(self, data_adaptor):
|
|
return common_rest.layout_obs_get(request, data_adaptor)
|
|
|
|
@rest_get_data_adaptor
|
|
def put(self, data_adaptor):
|
|
return common_rest.layout_obs_put(request, data_adaptor)
|
|
|
|
|
|
def get_api_resources(bp_api):
|
|
api = Api(bp_api)
|
|
# Initialization routes
|
|
api.add_resource(SchemaAPI, "/schema")
|
|
api.add_resource(ConfigAPI, "/config")
|
|
# Data routes
|
|
api.add_resource(AnnotationsObsAPI, "/annotations/obs")
|
|
api.add_resource(AnnotationsVarAPI, "/annotations/var")
|
|
api.add_resource(DataVarAPI, "/data/var")
|
|
# Computation routes
|
|
api.add_resource(DiffExpObsAPI, "/diffexp/obs")
|
|
api.add_resource(LayoutObsAPI, "/layout/obs")
|
|
return api
|
|
|
|
|
|
class Server:
|
|
def __init__(self, matrix_data_cache_manager, annotations, app_config):
|
|
|
|
self.app = Flask(__name__, static_folder="../common/web/static")
|
|
self.app.json_encoder = Float32JSONEncoder
|
|
|
|
self.cache = Cache(config={"CACHE_TYPE": "simple", "CACHE_DEFAULT_TIMEOUT": 860_000})
|
|
self.cache.init_app(self.app)
|
|
|
|
Compress(self.app)
|
|
CORS(self.app, supports_credentials=True)
|
|
|
|
# enable session data
|
|
self.app.permanent_session_lifetime = datetime.timedelta(days=50 * 365)
|
|
|
|
# Config
|
|
SECRET_KEY = os.environ.get("CXG_SECRET_KEY", default="SparkleAndShine")
|
|
self.app.config.update(SECRET_KEY=SECRET_KEY)
|
|
|
|
self.app.register_blueprint(webbp)
|
|
|
|
api_version = "/api/v0.2"
|
|
if app_config.single_dataset__datapath:
|
|
bp_api = Blueprint("api", __name__, url_prefix=api_version)
|
|
resources = get_api_resources(bp_api)
|
|
self.app.register_blueprint(resources.blueprint)
|
|
|
|
else:
|
|
# NOTE: These routes only allow the dataset to be in the directory
|
|
# of the dataroot, and not a subdirectory. We may want to change
|
|
# the route format at some point
|
|
bp_api = Blueprint("api_dataset", __name__, url_prefix="/<dataset>" + api_version)
|
|
resources = get_api_resources(bp_api)
|
|
self.app.register_blueprint(resources.blueprint)
|
|
self.app.add_url_rule("/<dataset>/", "dataset_index", dataset_index, methods=["GET"])
|
|
self.app.add_url_rule(
|
|
"/<dataset>/static/<path:therest>", "static_redirect", static_redirect, methods=["GET"]
|
|
)
|
|
|
|
self.app.matrix_data_cache_manager = matrix_data_cache_manager
|
|
self.app.annotations = annotations
|
|
self.app.app_config = app_config
|