mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-16 21:37:59 +08:00
381 lines
19 KiB
Python
381 lines
19 KiB
Python
import os
|
|
import sys
|
|
import warnings
|
|
from os.path import basename
|
|
from urllib.parse import urlparse, quote_plus
|
|
|
|
from server.auth.auth import AuthTypeFactory
|
|
from server.common.config.base_config import BaseConfig
|
|
from server.common.config import DEFAULT_SERVER_PORT, BIG_FILE_SIZE_THRESHOLD
|
|
from server.common.errors import ConfigurationError, DatasetAccessError
|
|
from server.common.data_locator import discover_s3_region_name
|
|
from server.common.utils.utils import is_port_available, find_available_port, custom_format_warning
|
|
from server.compute import diffexp_cxg as diffexp_tiledb
|
|
from server.data_common.matrix_loader import MatrixDataCacheManager, MatrixDataLoader, MatrixDataType
|
|
|
|
|
|
class ServerConfig(BaseConfig):
|
|
"""Manages the config attribute associated with the server."""
|
|
|
|
def __init__(self, app_config, default_config):
|
|
dictval_cases = [
|
|
("app", "csp_directives"),
|
|
("authentication", "params_oauth", "cookie"),
|
|
("authentication", "params_oauth", "jwt_decode_options"),
|
|
("adaptor", "cxg_adaptor", "tiledb_ctx"),
|
|
("multi_dataset", "dataroot"),
|
|
]
|
|
super().__init__(app_config, default_config, dictval_cases)
|
|
|
|
try:
|
|
self.app__verbose = default_config["app"]["verbose"]
|
|
self.app__debug = default_config["app"]["debug"]
|
|
self.app__host = default_config["app"]["host"]
|
|
self.app__port = default_config["app"]["port"]
|
|
self.app__open_browser = default_config["app"]["open_browser"]
|
|
self.app__force_https = default_config["app"]["force_https"]
|
|
self.app__flask_secret_key = default_config["app"]["flask_secret_key"]
|
|
self.app__generate_cache_control_headers = default_config["app"]["generate_cache_control_headers"]
|
|
self.app__server_timing_headers = default_config["app"]["server_timing_headers"]
|
|
self.app__csp_directives = default_config["app"]["csp_directives"]
|
|
self.app__api_base_url = default_config["app"]["api_base_url"]
|
|
self.app__web_base_url = default_config["app"]["web_base_url"]
|
|
|
|
self.authentication__type = default_config["authentication"]["type"]
|
|
self.authentication__params_oauth__oauth_api_base_url = default_config["authentication"]["params_oauth"][
|
|
"oauth_api_base_url"
|
|
]
|
|
self.authentication__params_oauth__client_id = default_config["authentication"]["params_oauth"]["client_id"]
|
|
self.authentication__params_oauth__client_secret = default_config["authentication"]["params_oauth"][
|
|
"client_secret"
|
|
]
|
|
self.authentication__params_oauth__jwt_decode_options = default_config["authentication"]["params_oauth"][
|
|
"jwt_decode_options"
|
|
]
|
|
self.authentication__params_oauth__session_cookie = default_config["authentication"]["params_oauth"][
|
|
"session_cookie"
|
|
]
|
|
self.authentication__params_oauth__cookie = default_config["authentication"]["params_oauth"]["cookie"]
|
|
|
|
self.multi_dataset__dataroot = default_config["multi_dataset"]["dataroot"]
|
|
self.multi_dataset__index = default_config["multi_dataset"]["index"]
|
|
self.multi_dataset__allowed_matrix_types = default_config["multi_dataset"]["allowed_matrix_types"]
|
|
self.multi_dataset__matrix_cache__max_datasets = default_config["multi_dataset"]["matrix_cache"][
|
|
"max_datasets"
|
|
]
|
|
self.multi_dataset__matrix_cache__timelimit_s = default_config["multi_dataset"]["matrix_cache"][
|
|
"timelimit_s"
|
|
]
|
|
|
|
self.single_dataset__datapath = default_config["single_dataset"]["datapath"]
|
|
self.single_dataset__obs_names = default_config["single_dataset"]["obs_names"]
|
|
self.single_dataset__var_names = default_config["single_dataset"]["var_names"]
|
|
self.single_dataset__about = default_config["single_dataset"]["about"]
|
|
self.single_dataset__title = default_config["single_dataset"]["title"]
|
|
|
|
self.diffexp__alg_cxg__max_workers = default_config["diffexp"]["alg_cxg"]["max_workers"]
|
|
self.diffexp__alg_cxg__cpu_multiplier = default_config["diffexp"]["alg_cxg"]["cpu_multiplier"]
|
|
self.diffexp__alg_cxg__target_workunit = default_config["diffexp"]["alg_cxg"]["target_workunit"]
|
|
|
|
self.data_locator__s3__region_name = default_config["data_locator"]["s3"]["region_name"]
|
|
|
|
self.adaptor__cxg_adaptor__tiledb_ctx = default_config["adaptor"]["cxg_adaptor"]["tiledb_ctx"]
|
|
self.adaptor__anndata_adaptor__backed = default_config["adaptor"]["anndata_adaptor"]["backed"]
|
|
|
|
self.limits__diffexp_cellcount_max = default_config["limits"]["diffexp_cellcount_max"]
|
|
self.limits__column_request_max = default_config["limits"]["column_request_max"]
|
|
|
|
except KeyError as e:
|
|
raise ConfigurationError(f"Unexpected config: {str(e)}")
|
|
|
|
# The matrix data cache manager is created during the complete_config and stored here.
|
|
self.matrix_data_cache_manager = None
|
|
|
|
# The authentication object
|
|
self.auth = None
|
|
|
|
def complete_config(self, context):
|
|
self.handle_app(context)
|
|
self.handle_data_source()
|
|
self.handle_authentication()
|
|
self.handle_data_locator()
|
|
self.handle_adaptor() # may depend on data_locator
|
|
self.handle_single_dataset(context) # may depend on adaptor
|
|
self.handle_multi_dataset() # may depend on adaptor
|
|
self.handle_diffexp()
|
|
self.handle_limits()
|
|
|
|
self.check_config()
|
|
|
|
def handle_app(self, context):
|
|
self.validate_correct_type_of_configuration_attribute("app__verbose", bool)
|
|
self.validate_correct_type_of_configuration_attribute("app__debug", bool)
|
|
self.validate_correct_type_of_configuration_attribute("app__host", str)
|
|
self.validate_correct_type_of_configuration_attribute("app__port", (type(None), int))
|
|
self.validate_correct_type_of_configuration_attribute("app__open_browser", bool)
|
|
self.validate_correct_type_of_configuration_attribute("app__force_https", bool)
|
|
self.validate_correct_type_of_configuration_attribute("app__flask_secret_key", str)
|
|
self.validate_correct_type_of_configuration_attribute("app__generate_cache_control_headers", bool)
|
|
self.validate_correct_type_of_configuration_attribute("app__server_timing_headers", bool)
|
|
self.validate_correct_type_of_configuration_attribute("app__csp_directives", (type(None), dict))
|
|
self.validate_correct_type_of_configuration_attribute("app__api_base_url", (type(None), str))
|
|
self.validate_correct_type_of_configuration_attribute("app__web_base_url", (type(None), str))
|
|
|
|
if self.app__port:
|
|
try:
|
|
if not is_port_available(self.app__host, self.app__port):
|
|
raise ConfigurationError(
|
|
f"The port selected {self.app__port} is in use, please configure an open port."
|
|
)
|
|
except OverflowError:
|
|
raise ConfigurationError(f"Invalid port: {self.app__port}")
|
|
else:
|
|
try:
|
|
default_server_port = int(os.environ.get("CXG_SERVER_PORT", DEFAULT_SERVER_PORT))
|
|
except ValueError:
|
|
raise ConfigurationError(
|
|
"Invalid port from environment variable CXG_SERVER_PORT: " + os.environ.get("CXG_SERVER_PORT")
|
|
)
|
|
try:
|
|
self.app__port = find_available_port(self.app__host, default_server_port)
|
|
except OverflowError:
|
|
raise ConfigurationError(f"Invalid port: {default_server_port}")
|
|
|
|
if self.app__debug:
|
|
context["messagefn"]("in debug mode, setting verbose=True and open_browser=False")
|
|
self.app__verbose = True
|
|
self.app__open_browser = False
|
|
else:
|
|
warnings.formatwarning = custom_format_warning
|
|
|
|
if not self.app__verbose:
|
|
sys.tracebacklimit = 0
|
|
|
|
# CSP Directives are a dict of string: list(string) or string: string
|
|
if self.app__csp_directives is not None:
|
|
for k, v in self.app__csp_directives.items():
|
|
if not isinstance(k, str):
|
|
raise ConfigurationError("CSP directive names must be a string.")
|
|
if isinstance(v, list):
|
|
for policy in v:
|
|
if not isinstance(policy, str):
|
|
raise ConfigurationError("CSP directive value must be a string or list of strings.")
|
|
elif not isinstance(v, str):
|
|
raise ConfigurationError("CSP directive value must be a string or list of strings.")
|
|
|
|
if self.app__web_base_url is None:
|
|
self.app__web_base_url = self.app__api_base_url
|
|
|
|
def handle_authentication(self):
|
|
self.validate_correct_type_of_configuration_attribute("authentication__type", (type(None), str))
|
|
|
|
# oauth
|
|
ptypes = str if self.authentication__type == "oauth" else (type(None), str)
|
|
self.validate_correct_type_of_configuration_attribute(
|
|
"authentication__params_oauth__oauth_api_base_url", ptypes
|
|
)
|
|
self.validate_correct_type_of_configuration_attribute("authentication__params_oauth__client_id", ptypes)
|
|
self.validate_correct_type_of_configuration_attribute("authentication__params_oauth__client_secret", ptypes)
|
|
self.validate_correct_type_of_configuration_attribute(
|
|
"authentication__params_oauth__jwt_decode_options", (type(None), dict)
|
|
)
|
|
self.validate_correct_type_of_configuration_attribute("authentication__params_oauth__session_cookie", bool)
|
|
|
|
if self.authentication__params_oauth__session_cookie:
|
|
self.validate_correct_type_of_configuration_attribute(
|
|
"authentication__params_oauth__cookie", (type(None), dict)
|
|
)
|
|
else:
|
|
self.validate_correct_type_of_configuration_attribute("authentication__params_oauth__cookie", dict)
|
|
|
|
self.auth = AuthTypeFactory.create(self.authentication__type, self)
|
|
if self.auth is None:
|
|
raise ConfigurationError(f"Unknown authentication type: {self.authentication__type}")
|
|
|
|
def handle_data_locator(self):
|
|
self.validate_correct_type_of_configuration_attribute("data_locator__s3__region_name", (type(None), bool, str))
|
|
if self.data_locator__s3__region_name is True:
|
|
path = self.single_dataset__datapath or self.multi_dataset__dataroot
|
|
|
|
if type(path) == dict:
|
|
# if multi_dataset__dataroot is a dict, then use the first key
|
|
# that is in s3. NOTE: it is not supported to have dataroots
|
|
# in different regions.
|
|
paths = [val.get("dataroot") for val in path.values()]
|
|
for path in paths:
|
|
if path.startswith("s3://"):
|
|
break
|
|
if path.startswith("s3://"):
|
|
region_name = discover_s3_region_name(path)
|
|
if region_name is None:
|
|
raise ConfigurationError(f"Unable to discover s3 region name from {path}")
|
|
else:
|
|
region_name = None
|
|
self.data_locator__s3__region_name = region_name
|
|
|
|
def handle_data_source(self):
|
|
self.validate_correct_type_of_configuration_attribute("single_dataset__datapath", (str, type(None)))
|
|
self.validate_correct_type_of_configuration_attribute("multi_dataset__dataroot", (type(None), dict, str))
|
|
|
|
if self.single_dataset__datapath and self.multi_dataset__dataroot:
|
|
raise ConfigurationError(
|
|
"You must supply either a datapath (for single datasets) or a dataroot (for multidatasets). Not both"
|
|
)
|
|
if self.single_dataset__datapath is None and self.multi_dataset__dataroot is None:
|
|
raise ConfigurationError("You must specify a datapath for a single dataset or a dataroot for multidatasets")
|
|
|
|
def handle_single_dataset(self, context):
|
|
self.validate_correct_type_of_configuration_attribute("single_dataset__datapath", (str, type(None)))
|
|
self.validate_correct_type_of_configuration_attribute("single_dataset__title", (str, type(None)))
|
|
self.validate_correct_type_of_configuration_attribute("single_dataset__about", (str, type(None)))
|
|
self.validate_correct_type_of_configuration_attribute("single_dataset__obs_names", (str, type(None)))
|
|
self.validate_correct_type_of_configuration_attribute("single_dataset__var_names", (str, type(None)))
|
|
|
|
if self.single_dataset__datapath is None:
|
|
return
|
|
|
|
# create the matrix data cache manager:
|
|
if self.matrix_data_cache_manager is None:
|
|
self.matrix_data_cache_manager = MatrixDataCacheManager(max_cached=1, timelimit_s=None)
|
|
|
|
# preload this data set
|
|
matrix_data_loader = MatrixDataLoader(self.single_dataset__datapath, app_config=self.app_config)
|
|
try:
|
|
matrix_data_loader.pre_load_validation()
|
|
except DatasetAccessError as e:
|
|
raise ConfigurationError(str(e))
|
|
|
|
file_size = matrix_data_loader.file_size()
|
|
file_basename = basename(self.single_dataset__datapath)
|
|
if file_size > BIG_FILE_SIZE_THRESHOLD:
|
|
context["messagefn"](f"Loading data from {file_basename}, this may take a while...")
|
|
else:
|
|
context["messagefn"](f"Loading data from {file_basename}.")
|
|
|
|
if self.single_dataset__about:
|
|
|
|
def url_check(url):
|
|
try:
|
|
result = urlparse(url)
|
|
if all([result.scheme, result.netloc]):
|
|
return True
|
|
else:
|
|
return False
|
|
except ValueError:
|
|
return False
|
|
|
|
if not url_check(self.single_dataset__about):
|
|
raise ConfigurationError(
|
|
"Must provide an absolute URL for --about. (Example format: http://example.com)"
|
|
)
|
|
|
|
def handle_multi_dataset(self):
|
|
self.validate_correct_type_of_configuration_attribute("multi_dataset__dataroot", (type(None), dict, str))
|
|
self.validate_correct_type_of_configuration_attribute("multi_dataset__index", (type(None), bool, str))
|
|
self.validate_correct_type_of_configuration_attribute("multi_dataset__allowed_matrix_types", list)
|
|
self.validate_correct_type_of_configuration_attribute("multi_dataset__matrix_cache__max_datasets", int)
|
|
self.validate_correct_type_of_configuration_attribute(
|
|
"multi_dataset__matrix_cache__timelimit_s", (type(None), int, float)
|
|
)
|
|
|
|
if self.multi_dataset__dataroot is None:
|
|
return
|
|
|
|
if type(self.multi_dataset__dataroot) == str:
|
|
default_dict = dict(base_url="d", dataroot=self.multi_dataset__dataroot)
|
|
self.multi_dataset__dataroot = dict(d=default_dict)
|
|
|
|
for tag, dataroot_dict in self.multi_dataset__dataroot.items():
|
|
if "base_url" not in dataroot_dict:
|
|
raise ConfigurationError(f"error in multi_dataset__dataroot: missing base_url for tag {tag}")
|
|
if "dataroot" not in dataroot_dict:
|
|
raise ConfigurationError(f"error in multi_dataset__dataroot: missing dataroot, for tag {tag}")
|
|
|
|
base_url = dataroot_dict["base_url"]
|
|
|
|
# sanity check for well formed base urls
|
|
bad = False
|
|
if type(base_url) != str:
|
|
bad = True
|
|
elif os.path.normpath(base_url) != base_url:
|
|
bad = True
|
|
else:
|
|
base_url_parts = base_url.split("/")
|
|
if [quote_plus(part) for part in base_url_parts] != base_url_parts:
|
|
bad = True
|
|
if ".." in base_url_parts:
|
|
bad = True
|
|
if bad:
|
|
raise ConfigurationError(f"error in multi_dataset__dataroot base_url {base_url} for tag {tag}")
|
|
|
|
# verify all the base_urls are unique
|
|
base_urls = [d["base_url"] for d in self.multi_dataset__dataroot.values()]
|
|
if len(base_urls) > len(set(base_urls)):
|
|
raise ConfigurationError("error in multi_dataset__dataroot: base_urls must be unique")
|
|
|
|
# error checking
|
|
for mtype in self.multi_dataset__allowed_matrix_types:
|
|
try:
|
|
MatrixDataType(mtype)
|
|
except ValueError:
|
|
raise ConfigurationError(f'Invalid matrix type in "allowed_matrix_types": {mtype}')
|
|
|
|
# create the matrix data cache manager:
|
|
if self.matrix_data_cache_manager is None:
|
|
self.matrix_data_cache_manager = MatrixDataCacheManager(
|
|
max_cached=self.multi_dataset__matrix_cache__max_datasets,
|
|
timelimit_s=self.multi_dataset__matrix_cache__timelimit_s,
|
|
)
|
|
|
|
def handle_diffexp(self):
|
|
self.validate_correct_type_of_configuration_attribute("diffexp__alg_cxg__max_workers", (str, int))
|
|
self.validate_correct_type_of_configuration_attribute("diffexp__alg_cxg__cpu_multiplier", int)
|
|
self.validate_correct_type_of_configuration_attribute("diffexp__alg_cxg__target_workunit", int)
|
|
|
|
max_workers = self.diffexp__alg_cxg__max_workers
|
|
cpu_multiplier = self.diffexp__alg_cxg__cpu_multiplier
|
|
cpu_count = os.cpu_count()
|
|
max_workers = min(max_workers, cpu_multiplier * cpu_count)
|
|
diffexp_tiledb.set_config(max_workers, self.diffexp__alg_cxg__target_workunit)
|
|
|
|
def handle_adaptor(self):
|
|
# cxg
|
|
self.validate_correct_type_of_configuration_attribute("adaptor__cxg_adaptor__tiledb_ctx", dict)
|
|
regionkey = "vfs.s3.region"
|
|
if regionkey not in self.adaptor__cxg_adaptor__tiledb_ctx:
|
|
if type(self.data_locator__s3__region_name) == str:
|
|
self.adaptor__cxg_adaptor__tiledb_ctx[regionkey] = self.data_locator__s3__region_name
|
|
|
|
from server.data_cxg.cxg_adaptor import CxgAdaptor
|
|
|
|
CxgAdaptor.set_tiledb_context(self.adaptor__cxg_adaptor__tiledb_ctx)
|
|
|
|
# anndata
|
|
self.validate_correct_type_of_configuration_attribute("adaptor__anndata_adaptor__backed", bool)
|
|
|
|
def handle_limits(self):
|
|
self.validate_correct_type_of_configuration_attribute("limits__diffexp_cellcount_max", (type(None), int))
|
|
self.validate_correct_type_of_configuration_attribute("limits__column_request_max", (type(None), int))
|
|
|
|
def exceeds_limit(self, limit_name, value):
|
|
limit_value = getattr(self, "limits__" + limit_name, None)
|
|
if limit_value is None: # disabled
|
|
return False
|
|
return value > limit_value
|
|
|
|
def get_api_base_url(self):
|
|
if self.app__api_base_url == "local":
|
|
return f"http://{self.app__host}:{self.app__port}"
|
|
if self.app__api_base_url and self.app__api_base_url.endswith("/"):
|
|
return self.app__api_base_url[:-1]
|
|
return self.app__api_base_url
|
|
|
|
def get_web_base_url(self):
|
|
if self.app__web_base_url == "local":
|
|
return f"http://{self.app__host}:{self.app__port}"
|
|
if self.app__web_base_url is None:
|
|
return self.get_api_base_url()
|
|
if self.app__web_base_url.endswith("/"):
|
|
return self.app__web_base_url[:-1]
|
|
return self.app__web_base_url
|