mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-16 21:37:59 +08:00
* split out config * add tests for base and app config, refactor client config out of app config * refactor default config retrieval * create config test class and helper functions * move default_config into server to fix import issue
156 lines
6.1 KiB
Python
156 lines
6.1 KiB
Python
import json
|
|
import os
|
|
import re
|
|
import time
|
|
|
|
import pandas as pd
|
|
import tiledb
|
|
from flask import current_app
|
|
|
|
from server.common.annotations.annotations import Annotations
|
|
from server.common.errors import AnnotationCategoryNameError
|
|
from server.common.utils.sanitization_utils import sanitize_values_in_list
|
|
from server.common.utils.type_conversion_utils import get_dtypes_and_schemas_of_dataframe, get_dtype_of_array
|
|
from server.db.cellxgene_orm import Annotation
|
|
|
|
|
|
class AnnotationsHostedTileDB(Annotations):
|
|
CXG_ANNO_COLLECTION = "cxg_anno_collection"
|
|
|
|
def __init__(self, directory_path, db):
|
|
super().__init__()
|
|
self.db = db
|
|
if directory_path[-1] == "/":
|
|
self.directory_path = directory_path
|
|
else:
|
|
self.directory_path = directory_path + "/"
|
|
|
|
def check_category_names(self, df):
|
|
original_category_names = df.keys().to_list()
|
|
sanitized_category_names = set(sanitize_values_in_list(original_category_names).values())
|
|
unsanitary_original_category_names = set(original_category_names).difference(sanitized_category_names)
|
|
if unsanitary_original_category_names:
|
|
raise AnnotationCategoryNameError(
|
|
f"{unsanitary_original_category_names} are not valid category names, please resubmit"
|
|
)
|
|
|
|
def is_safe_collection_name(self, name):
|
|
"""
|
|
return true if this is a safe collection name
|
|
this is ultra conservative. 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 set_collection(self, name):
|
|
self.CXG_ANNO_COLLECTION = name
|
|
|
|
def read_labels(self, data_adaptor):
|
|
user_id = current_app.auth.get_user_id()
|
|
if user_id is None:
|
|
return
|
|
dataset_name = data_adaptor.get_location()
|
|
dataset_id = self.db.get_or_create_dataset(dataset_name)
|
|
|
|
annotation_object = self.db.query_for_most_recent(
|
|
Annotation, [Annotation.user_id == user_id, Annotation.dataset_id == dataset_id]
|
|
)
|
|
if annotation_object:
|
|
df = tiledb.open(annotation_object.tiledb_uri)
|
|
pandas_df = self.convert_to_pandas_df(df, annotation_object.schema_hints)
|
|
return pandas_df
|
|
else:
|
|
return None
|
|
|
|
def convert_to_pandas_df(self, tileDBArray, schema_hints):
|
|
repr_meta = None
|
|
index_dims = None
|
|
schema_hints = json.loads(schema_hints)
|
|
|
|
if "__pandas_attribute_repr" in tileDBArray.meta:
|
|
# backwards compatibility... unsure if necessary at this point
|
|
repr_meta = json.loads(tileDBArray.meta["__pandas_attribute_repr"])
|
|
if "__pandas_index_dims" in tileDBArray.meta:
|
|
index_dims = json.loads(tileDBArray.meta["__pandas_index_dims"])
|
|
|
|
data = tileDBArray[:]
|
|
indexes = list()
|
|
|
|
for col_name, col_val in data.items():
|
|
# If the column values are byte literals, decode them
|
|
if isinstance(col_val[0], bytes):
|
|
col_val = [value.decode("utf-8") for value in col_val]
|
|
|
|
if schema_hints and col_name in schema_hints:
|
|
type = schema_hints.get(col_name).get("type")
|
|
if type and type == "categorical":
|
|
new_col = pd.Series(col_val, dtype="category")
|
|
data[col_name] = new_col
|
|
elif repr_meta and col_name in repr_meta:
|
|
new_col = pd.Series(col_val, dtype=repr_meta[col_name])
|
|
data[col_name] = new_col
|
|
elif index_dims and col_name in index_dims:
|
|
new_col = pd.Series(col_val, dtype=index_dims[col_name])
|
|
data[col_name] = new_col
|
|
indexes.append(col_name)
|
|
|
|
new_df = pd.DataFrame.from_dict(data)
|
|
if len(indexes) > 0:
|
|
new_df.set_index(indexes, inplace=True)
|
|
|
|
return new_df
|
|
|
|
def write_labels(self, df, data_adaptor):
|
|
auth_user_id = current_app.auth.get_user_id()
|
|
user_name = current_app.auth.get_user_name()
|
|
timestamp = time.time()
|
|
dataset_location = data_adaptor.get_location()
|
|
dataset_id = self.db.get_or_create_dataset(dataset_location)
|
|
dataset_name = data_adaptor.get_title()
|
|
user_id = self.db.get_or_create_user(auth_user_id)
|
|
"""
|
|
NOTE: The uri contains the dataset name, user name and a timestamp as a convenience for debugging purposes.
|
|
People may have the same name and time.time() can be server dependent.
|
|
See - https://docs.python.org/2/library/time.html#time.time
|
|
|
|
The annotations objects in the database should be used as the source of truth about who an annotation belongs
|
|
to (for authorization purposes) and what time it was created (for garbage collection).
|
|
"""
|
|
uri = f"{self.directory_path}{dataset_name}/{user_name}/{timestamp}"
|
|
if uri.startswith("s3://"):
|
|
pass
|
|
else:
|
|
os.makedirs(uri, exist_ok=True)
|
|
_, dataframe_schema_type_hints = get_dtypes_and_schemas_of_dataframe(df)
|
|
annotation = Annotation(
|
|
tiledb_uri=uri,
|
|
user_id=user_id,
|
|
dataset_id=str(dataset_id),
|
|
schema_hints=json.dumps(dataframe_schema_type_hints),
|
|
)
|
|
if not df.empty:
|
|
self.check_category_names(df)
|
|
# convert to tiledb datatypes
|
|
|
|
for col in df:
|
|
df[col] = df[col].astype(get_dtype_of_array(df[col]))
|
|
tiledb.from_pandas(uri, df)
|
|
|
|
self.db.session.add(annotation)
|
|
self.db.session.commit()
|
|
|
|
def update_parameters(self, parameters, data_adaptor):
|
|
params = {}
|
|
params["annotations"] = True
|
|
params["user_annotation_collection_name_enabled"] = False
|
|
|
|
if self.ontology_data:
|
|
params["annotations_cell_ontology_enabled"] = True
|
|
params["annotations_cell_ontology_terms"] = self.ontology_data
|
|
else:
|
|
params["annotations_cell_ontology_enabled"] = False
|
|
|
|
parameters.update(params)
|