mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-29 01:28:11 +08:00
annotations CLI and file UX rework (#1049)
* rename config param label-file * annotations rework - CLI params, file naming and backups * lint * improve cli option error checks * enable session cookies * enable session cookies * add session id * name annotations file in multi-dataset and multi-user safe manner * pass data user hash to front-end * add annotation collection name support to front-end * add constant for annotation data collection name * parameterize annotation collection name; make it sticky in the session * clarify comments * hard wire a temporary data collection name for testing * prettier * test comment * package command * set annotations filename dialog * name and hash are visible * wire up data collection capture
This commit is contained in:
+54
-19
@@ -1,8 +1,9 @@
|
||||
from http import HTTPStatus
|
||||
import warnings
|
||||
from os.path import basename
|
||||
from uuid import uuid4
|
||||
import re
|
||||
|
||||
from flask import Blueprint, current_app, jsonify, make_response, request
|
||||
from flask import Blueprint, current_app, jsonify, make_response, request, session
|
||||
from flask_restful import Api, Resource
|
||||
from server import __version__ as cellxgene_version
|
||||
from anndata import __version__ as anndata_version
|
||||
@@ -11,6 +12,8 @@ from server.app.util.constants import (
|
||||
Axis,
|
||||
DiffExpMode,
|
||||
JSON_NaN_to_num_warning_msg,
|
||||
CXGUID,
|
||||
CXG_ANNO_COLLECTION
|
||||
)
|
||||
from server.app.util.errors import (
|
||||
FilterError,
|
||||
@@ -20,23 +23,20 @@ from server.app.util.errors import (
|
||||
DisabledFeatureError,
|
||||
)
|
||||
|
||||
"""
|
||||
Sort order for routes
|
||||
1. Initialize
|
||||
2. Data & Metadata
|
||||
3. Computation
|
||||
"""
|
||||
|
||||
|
||||
class SchemaAPI(Resource):
|
||||
def get(self):
|
||||
cxguid = get_userid(session)
|
||||
anno_collection = get_anno_collection(session)
|
||||
return make_response(
|
||||
jsonify({"schema": current_app.data.get_schema()}), HTTPStatus.OK
|
||||
jsonify({"schema": current_app.data.get_schema(uid=cxguid, collection=anno_collection)}), HTTPStatus.OK
|
||||
)
|
||||
|
||||
|
||||
class ConfigAPI(Resource):
|
||||
def get(self):
|
||||
cxguid = get_userid(session)
|
||||
anno_collection = get_anno_collection(session)
|
||||
config = {
|
||||
"config": {
|
||||
"features": [
|
||||
@@ -69,9 +69,7 @@ class ConfigAPI(Resource):
|
||||
"about-dataset": current_app.config["ABOUT_DATASET"]
|
||||
},
|
||||
"parameters": {
|
||||
"max-category-items": current_app.data.config["max_category_items"],
|
||||
"disable-diffexp": current_app.data.config["disable_diffexp"],
|
||||
"diffexp-may-be-slow": current_app.data.config["diffexp_may_be_slow"]
|
||||
**current_app.data.get_config_parameters(uid=cxguid, collection=anno_collection)
|
||||
},
|
||||
"library_versions": {
|
||||
"cellxgene": cellxgene_version,
|
||||
@@ -80,10 +78,6 @@ class ConfigAPI(Resource):
|
||||
}
|
||||
}
|
||||
|
||||
label_file = current_app.data.config["label_file"]
|
||||
if label_file:
|
||||
config["config"]["parameters"]["label_file"] = basename(label_file)
|
||||
|
||||
return make_response(jsonify(config), HTTPStatus.OK)
|
||||
|
||||
|
||||
@@ -93,9 +87,12 @@ class AnnotationsObsAPI(Resource):
|
||||
preferred_mimetype = request.accept_mimetypes.best_match(
|
||||
["application/octet-stream"]
|
||||
)
|
||||
cxguid = get_userid(session)
|
||||
anno_collection = get_anno_collection(session)
|
||||
try:
|
||||
if preferred_mimetype == "application/octet-stream":
|
||||
return make_response(current_app.data.annotation_to_fbs_matrix("obs", fields),
|
||||
fbs = current_app.data.annotation_to_fbs_matrix("obs", fields, uid=cxguid, collection=anno_collection)
|
||||
return make_response(fbs,
|
||||
HTTPStatus.OK,
|
||||
{"Content-Type": "application/octet-stream"})
|
||||
else:
|
||||
@@ -106,9 +103,18 @@ class AnnotationsObsAPI(Resource):
|
||||
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||
|
||||
def put(self):
|
||||
cxguid = get_userid(session)
|
||||
anno_collection = request.args.get("annotation-collection-name", default=None)
|
||||
if anno_collection is not None:
|
||||
if not is_safe_collection_name(anno_collection):
|
||||
return make_response(f"Error, bad annotation collection name", HTTPStatus.BAD_REQUEST)
|
||||
set_anno_collection(session, anno_collection)
|
||||
else:
|
||||
anno_collection = get_anno_collection(session)
|
||||
|
||||
try:
|
||||
fbs = request.get_data()
|
||||
res = current_app.data.annotation_put_fbs("obs", fbs)
|
||||
res = current_app.data.annotation_put_fbs("obs", fbs, uid=cxguid, collection=anno_collection)
|
||||
return make_response(
|
||||
res, HTTPStatus.OK, {"Content-Type": "application/json"}
|
||||
)
|
||||
@@ -246,6 +252,35 @@ class LayoutObsAPI(Resource):
|
||||
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
def get_userid(ss):
|
||||
if CXGUID not in ss:
|
||||
ss[CXGUID] = uuid4().hex
|
||||
ss.permanent = True
|
||||
return ss[CXGUID]
|
||||
|
||||
|
||||
def get_anno_collection(ss):
|
||||
collection = ss[CXG_ANNO_COLLECTION] if CXG_ANNO_COLLECTION in ss else None
|
||||
return collection
|
||||
|
||||
|
||||
def set_anno_collection(ss, name):
|
||||
ss[CXG_ANNO_COLLECTION] = name
|
||||
ss.permanent = True
|
||||
|
||||
|
||||
def is_safe_collection_name(name):
|
||||
"""
|
||||
return true if this is a safe collection name
|
||||
|
||||
this is ultra convervative. If we want to allow full legal file name syntax,
|
||||
we could look at modules like `pathvalidate`
|
||||
"""
|
||||
if name is None:
|
||||
return False
|
||||
return re.match(r'^\w+$', name) is not None
|
||||
|
||||
|
||||
def get_api_resources():
|
||||
bp = Blueprint("api", __name__, url_prefix="/api/v0.2")
|
||||
api = Api(bp)
|
||||
|
||||
Reference in New Issue
Block a user