import os import sys import warnings from os.path import basename from urllib.parse import urlparse from server.common.config.base_config import BaseConfig from server.common.config import DEFAULT_SERVER_PORT, BIG_FILE_SIZE_THRESHOLD from server.common.utils.data_locator import discover_s3_region_name from server.common.errors import ConfigurationError, DatasetAccessError from server.common.utils.utils import is_port_available, find_available_port, custom_format_warning from server.data_common.matrix_loader import MatrixDataLoader class ServerConfig(BaseConfig): """Manages the config attribute associated with the server.""" def __init__(self, app_config, default_config): super().__init__(app_config, default_config) 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.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.data_locator__s3__region_name = default_config["data_locator"]["s3"]["region_name"] 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)}") self.data_adaptor = None def complete_config(self, context): self.handle_app(context) self.handle_data_source() self.handle_data_locator() self.handle_adaptor() # may depend on data_locator self.handle_single_dataset(context) # may depend on adaptor 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) 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 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 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) 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))) # 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_adaptor(self): 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