mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-15 20:57:56 +08:00
* move common code into server, update tests and makefile remove backend directory, refactor update smoke tests
268 lines
8.1 KiB
Python
268 lines
8.1 KiB
Python
import datetime
|
|
import logging
|
|
from functools import wraps
|
|
|
|
from flask import (
|
|
Flask,
|
|
current_app,
|
|
make_response,
|
|
render_template,
|
|
Blueprint,
|
|
request,
|
|
send_from_directory,
|
|
)
|
|
from flask_restful import Api, Resource
|
|
|
|
import server.common.rest as common_rest
|
|
from server.common.errors import DatasetAccessError, RequestException
|
|
from server.common.health import health_check
|
|
from server.common.utils.utils import StrictJSONEncoder
|
|
|
|
webbp = Blueprint("webapp", "server.common.web", template_folder="templates")
|
|
|
|
ONE_WEEK = 7 * 24 * 60 * 60
|
|
|
|
|
|
def _cache_control(always, **cache_kwargs):
|
|
"""
|
|
Used to easily manage cache control headers on responses.
|
|
See Werkzeug for attributes that can be set, eg, no_cache, private, max_age, etc.
|
|
https://werkzeug.palletsprojects.com/en/1.0.x/datastructures/#werkzeug.datastructures.ResponseCacheControl
|
|
"""
|
|
|
|
def inner_cache_control(f):
|
|
@wraps(f)
|
|
def wrapper(*args, **kwargs):
|
|
response = make_response(f(*args, **kwargs))
|
|
if not always and not current_app.app_config.server_config.app__generate_cache_control_headers:
|
|
return response
|
|
if response.status_code >= 400:
|
|
return response
|
|
for k, v in cache_kwargs.items():
|
|
setattr(response.cache_control, k, v)
|
|
return response
|
|
|
|
return wrapper
|
|
|
|
return inner_cache_control
|
|
|
|
|
|
def cache_control(**cache_kwargs):
|
|
""" config driven """
|
|
return _cache_control(False, **cache_kwargs)
|
|
|
|
|
|
def cache_control_always(**cache_kwargs):
|
|
""" always generate headers, regardless of the config """
|
|
return _cache_control(True, **cache_kwargs)
|
|
|
|
|
|
@webbp.route("/", methods=["GET"])
|
|
def dataset_index():
|
|
app_config = current_app.app_config
|
|
|
|
dataset_config = app_config.get_dataset_config()
|
|
scripts = dataset_config.app__scripts
|
|
inline_scripts = dataset_config.app__inline_scripts
|
|
|
|
try:
|
|
args = {"SCRIPTS": scripts, "INLINE_SCRIPTS": inline_scripts}
|
|
return render_template("index.html", **args)
|
|
|
|
except DatasetAccessError as e:
|
|
return common_rest.abort_and_log(
|
|
e.status_code, f"Invalid dataset: {e.message}", loglevel=logging.INFO, include_exc_info=True
|
|
)
|
|
|
|
|
|
@webbp.errorhandler(RequestException)
|
|
def handle_request_exception(error):
|
|
return common_rest.abort_and_log(error.status_code, error.message, loglevel=logging.INFO, include_exc_info=True)
|
|
|
|
|
|
def rest_get_data_adaptor(func):
|
|
@wraps(func)
|
|
def wrapped_function(self):
|
|
try:
|
|
return func(self, current_app.data_adaptor)
|
|
except DatasetAccessError as e:
|
|
return common_rest.abort_and_log(
|
|
e.status_code, f"Invalid dataset: {e.message}", loglevel=logging.INFO, include_exc_info=True
|
|
)
|
|
|
|
return wrapped_function
|
|
|
|
|
|
class HealthAPI(Resource):
|
|
@cache_control_always(no_store=True)
|
|
def get(self):
|
|
config = current_app.app_config
|
|
return health_check(config)
|
|
|
|
|
|
class SchemaAPI(Resource):
|
|
# TODO @mdunitz separate dataset schema and user schema
|
|
@cache_control(public=True, max_age=ONE_WEEK)
|
|
@rest_get_data_adaptor
|
|
def get(self, data_adaptor):
|
|
return common_rest.schema_get(data_adaptor)
|
|
|
|
|
|
class ConfigAPI(Resource):
|
|
@cache_control(public=True, max_age=ONE_WEEK)
|
|
@rest_get_data_adaptor
|
|
def get(self, data_adaptor):
|
|
return common_rest.config_get(current_app.app_config, data_adaptor)
|
|
|
|
|
|
class AnnotationsObsAPI(Resource):
|
|
@cache_control(public=True, max_age=ONE_WEEK)
|
|
@rest_get_data_adaptor
|
|
def get(self, data_adaptor):
|
|
return common_rest.annotations_obs_get(request, data_adaptor)
|
|
|
|
@cache_control(no_store=True)
|
|
@rest_get_data_adaptor
|
|
def put(self, data_adaptor):
|
|
return common_rest.annotations_obs_put(request, data_adaptor)
|
|
|
|
|
|
class AnnotationsVarAPI(Resource):
|
|
@cache_control(public=True, max_age=ONE_WEEK)
|
|
@rest_get_data_adaptor
|
|
def get(self, data_adaptor):
|
|
return common_rest.annotations_var_get(request, data_adaptor)
|
|
|
|
|
|
class DataVarAPI(Resource):
|
|
@cache_control(no_store=True)
|
|
@rest_get_data_adaptor
|
|
def put(self, data_adaptor):
|
|
return common_rest.data_var_put(request, data_adaptor)
|
|
|
|
@cache_control(public=True, max_age=ONE_WEEK)
|
|
@rest_get_data_adaptor
|
|
def get(self, data_adaptor):
|
|
return common_rest.data_var_get(request, data_adaptor)
|
|
|
|
|
|
class ColorsAPI(Resource):
|
|
@cache_control(public=True, max_age=ONE_WEEK)
|
|
@rest_get_data_adaptor
|
|
def get(self, data_adaptor):
|
|
return common_rest.colors_get(data_adaptor)
|
|
|
|
|
|
class DiffExpObsAPI(Resource):
|
|
@cache_control(no_store=True)
|
|
@rest_get_data_adaptor
|
|
def post(self, data_adaptor):
|
|
return common_rest.diffexp_obs_post(request, data_adaptor)
|
|
|
|
|
|
class LayoutObsAPI(Resource):
|
|
@cache_control(public=True, max_age=ONE_WEEK)
|
|
@rest_get_data_adaptor
|
|
def get(self, data_adaptor):
|
|
return common_rest.layout_obs_get(request, data_adaptor)
|
|
|
|
|
|
class GenesetsAPI(Resource):
|
|
@cache_control(public=True, max_age=ONE_WEEK)
|
|
@rest_get_data_adaptor
|
|
def get(self, data_adaptor):
|
|
return common_rest.genesets_get(request, data_adaptor)
|
|
|
|
@cache_control(no_store=True)
|
|
@rest_get_data_adaptor
|
|
def put(self, data_adaptor):
|
|
return common_rest.genesets_put(request, data_adaptor)
|
|
|
|
|
|
class SummarizeVarAPI(Resource):
|
|
@rest_get_data_adaptor
|
|
@cache_control(public=True, max_age=ONE_WEEK)
|
|
def get(self, data_adaptor):
|
|
return common_rest.summarize_var_get(request, data_adaptor)
|
|
|
|
@rest_get_data_adaptor
|
|
@cache_control(no_store=True)
|
|
def post(self, data_adaptor):
|
|
return common_rest.summarize_var_post(request, data_adaptor)
|
|
|
|
|
|
def get_api_base_resources(bp_base):
|
|
"""Add resources that are accessed from the api url"""
|
|
api = Api(bp_base)
|
|
|
|
# Diagnostics routes
|
|
api.add_resource(HealthAPI, "/health")
|
|
return api
|
|
|
|
|
|
def get_api_dataroot_resources(bp_dataroot):
|
|
"""Add resources that refer to a dataset"""
|
|
api = Api(bp_dataroot)
|
|
|
|
def add_resource(resource, url):
|
|
"""convenience function to make the outer function less verbose"""
|
|
api.add_resource(resource, url)
|
|
|
|
# Initialization routes
|
|
add_resource(SchemaAPI, "/schema")
|
|
add_resource(ConfigAPI, "/config")
|
|
# Data routes
|
|
add_resource(AnnotationsObsAPI, "/annotations/obs")
|
|
add_resource(AnnotationsVarAPI, "/annotations/var")
|
|
add_resource(DataVarAPI, "/data/var")
|
|
add_resource(GenesetsAPI, "/genesets")
|
|
add_resource(SummarizeVarAPI, "/summarize/var")
|
|
# Display routes
|
|
add_resource(ColorsAPI, "/colors")
|
|
# Computation routes
|
|
add_resource(DiffExpObsAPI, "/diffexp/obs")
|
|
add_resource(LayoutObsAPI, "/layout/obs")
|
|
return api
|
|
|
|
|
|
class Server:
|
|
@staticmethod
|
|
def _before_adding_routes(app, app_config):
|
|
""" will be called before routes are added, during __init__. Subclass protocol """
|
|
pass
|
|
|
|
def __init__(self, app_config):
|
|
self.app = Flask(__name__, static_folder=None)
|
|
self._before_adding_routes(self.app, app_config)
|
|
self.app.json_encoder = StrictJSONEncoder
|
|
server_config = app_config.server_config
|
|
|
|
# enable session data
|
|
self.app.permanent_session_lifetime = datetime.timedelta(days=50 * 365)
|
|
|
|
# Config
|
|
secret_key = server_config.app__flask_secret_key
|
|
self.app.config.update(SECRET_KEY=secret_key)
|
|
|
|
self.app.register_blueprint(webbp)
|
|
|
|
api_version = "/api/v0.2"
|
|
api_path = "/"
|
|
|
|
bp_base = Blueprint("bp_base", __name__, url_prefix=api_path)
|
|
base_resources = get_api_base_resources(bp_base)
|
|
self.app.register_blueprint(base_resources.blueprint)
|
|
|
|
bp_api = Blueprint("api", __name__, url_prefix=f"{api_path}{api_version}")
|
|
resources = get_api_dataroot_resources(bp_api)
|
|
self.app.register_blueprint(resources.blueprint)
|
|
self.app.add_url_rule(
|
|
"/static/<path:filename>",
|
|
"static_assets",
|
|
view_func=lambda filename: send_from_directory("../common/web/static", filename),
|
|
methods=["GET"],
|
|
)
|
|
|
|
self.app.data_adaptor = server_config.data_adaptor
|
|
self.app.app_config = app_config
|