def data_with_tmp_annotations(ext: MatrixDataType, annotations_fixture=False): tmp_dir = tempfile.mkdtemp() annotations_file = path.join(tmp_dir, "test_annotations.csv") if annotations_fixture: shutil.copyfile( f"{PROJECT_ROOT}/local_server/test/fixtures/pbmc3k-annotations.csv", annotations_file) fname = { MatrixDataType.H5AD: f"{PROJECT_ROOT}/example-dataset/pbmc3k.h5ad", }[ext] data_locator = DataLocator(fname) config = AppConfig() config.update_server_config( app__flask_secret_key="secret", single_dataset__obs_names=None, single_dataset__var_names=None, single_dataset__datapath=data_locator.path, ) config.update_dataset_config( embeddings__names=["umap"], presentation__max_categories=100, diffexp__lfc_cutoff=0.01, ) config.complete_config() data = MatrixDataLoader(data_locator.abspath()).open(config) annotations = AnnotationsLocalFile(None, annotations_file) return data, tmp_dir, annotations
def setUp(self): self.data_file = DataLocator(f"{PROJECT_ROOT}/example-dataset/pbmc3k.h5ad") config = AppConfig() config.update_server_config(single_dataset__datapath=self.data_file.path) config.update_server_config(app__flask_secret_key="secret") config.complete_config() self.data = AnndataAdaptor(self.data_file, config)
def test_simple_update_single_config_from_path_and_value(self): """Update a simple config parameter""" config = AppConfig() config.server_config.single_dataset__datapath = "my/data/path" # test simple value in server config.update_single_config_from_path_and_value(["server", "app", "flask_secret_key"], "mysecret") self.assertEqual(config.server_config.app__flask_secret_key, "mysecret") # test simple value in default dataset config.update_single_config_from_path_and_value( ["dataset", "user_annotations", "ontology", "obo_location"], "dummy_location", ) self.assertEqual(config.dataset_config.user_annotations__ontology__obo_location, "dummy_location") # error checking bad_paths = [ ( ["dataset", "does", "not", "exist"], "unknown config parameter at path: '['dataset', 'does', 'not', 'exist']'", ), (["does", "not", "exist"], "path must start with 'server', or 'dataset'"), ([], "path must start with 'server', or 'dataset'"), ([1, 2, 3], "path must be a list of strings, got '[1, 2, 3]'"), ("string", "path must be a list of strings, got 'string'"), ] for bad_path, error_message in bad_paths: with self.assertRaises(ConfigurationError) as config_error: config.update_single_config_from_path_and_value(bad_path, "value") self.assertEqual(config_error.exception.message, error_message)
def test_configfile_with_specialization(self): # test that per_dataset_config config load the default config, then the specialized config with tempfile.TemporaryDirectory() as tempdir: configfile = os.path.join(tempdir, "config.yaml") with open(configfile, "w") as fconfig: config = """ server: single_dataset: datapath: fake_datapath dataset: user_annotations: enable: false type: local_file_csv local_file_csv: file: fake_file directory: fake_dir """ fconfig.write(config) app_config = AppConfig() app_config.update_from_config_file(configfile) test_config = app_config.dataset_config # test config from default self.assertEqual(test_config.user_annotations__type, "local_file_csv") self.assertEqual( test_config.user_annotations__local_file_csv__file, "fake_file")
def get_config(self, **kwargs): file_name = self.custom_app_config( dataset_datapath=H5AD_FIXTURE, config_file_name=self.config_file_name, **kwargs ) config = AppConfig() config.update_from_config_file(file_name) return config
def test_get_dataset_config_returns_dataset_config_for_single_datasets(self): datapath = f"{FIXTURES_ROOT}/1e4dfec4-c0b2-46ad-a04e-ff3ffb3c0a8f.h5ad" file_name = self.custom_app_config(dataset_datapath=datapath, config_file_name=self.config_file_name) config = AppConfig() config.update_from_config_file(file_name) self.assertEqual(config.get_dataset_config(), config.dataset_config)
def test_aws_secrets_manager(self, mock_get_secret_key): mock_get_secret_key.return_value = { "flask_secret_key": "mock_flask_secret_key", } configfile = self.custom_external_config( aws_secrets_manager_region="us-west-2", aws_secrets_manager_secrets=[ dict( name="my_secret", values=[ dict(key="flask_secret_key", path=["server", "app", "flask_secret_key"], required=True), ], ) ], config_file_name="secret_external_config.yaml", ) app_config = AppConfig() app_config.update_from_config_file(configfile) app_config.server_config.single_dataset__datapath = f"{FIXTURES_ROOT}/pbmc3k-CSC-gz.h5ad" app_config.complete_config() self.assertEqual(app_config.server_config.app__flask_secret_key, "mock_flask_secret_key")
def test_config_for_single_dataset(self): file_name = self.custom_app_config( config_file_name="single_dataset.yml", dataset_datapath=f"{H5AD_FIXTURE}") config = AppConfig() config.update_from_config_file(file_name) config.server_config.handle_single_dataset(self.context) file_name = self.custom_app_config( config_file_name="single_dataset_with_about.yml", about="www.cziscience.com", dataset_datapath=f"{H5AD_FIXTURE}", ) config = AppConfig() config.update_from_config_file(file_name) with self.assertRaises(ConfigurationError): config.server_config.handle_single_dataset(self.context)
def test_environment_variable_errors(self): # no name app_config = AppConfig() app_config.external_config.environment = [ dict(required=True, path=["this", "is", "a", "path"]) ] with self.assertRaises(ConfigurationError) as config_error: app_config.complete_config() self.assertEqual(config_error.exception.message, "environment: 'name' is missing") # required has wrong type app_config = AppConfig() app_config.external_config.environment = [ dict(name="myenvar", required="optional", path=["this", "is", "a", "path"]) ] with self.assertRaises(ConfigurationError) as config_error: app_config.complete_config() self.assertEqual(config_error.exception.message, "environment: 'required' must be a bool") # no path app_config = AppConfig() app_config.external_config.environment = [ dict(name="myenvar", required=True) ] with self.assertRaises(ConfigurationError) as config_error: app_config.complete_config() self.assertEqual(config_error.exception.message, "environment: 'path' is missing") # required environment variable is not set app_config = AppConfig() app_config.external_config.environment = [ dict(name="THIS_ENV_IS_NOT_SET", required=True, path=["this", "is", "a", "path"]) ] with self.assertRaises(ConfigurationError) as config_error: app_config.complete_config() self.assertEqual( config_error.exception.message, "required environment variable 'THIS_ENV_IS_NOT_SET' not set")
def test_init_datatset_config_sets_vars_from_config(self): config = AppConfig() self.assertEqual(config.dataset_config.presentation__max_categories, 1000) self.assertEqual(config.dataset_config.user_annotations__type, "local_file_csv") self.assertEqual(config.dataset_config.diffexp__lfc_cutoff, 0.01) self.assertIsNone( config.dataset_config.user_annotations__ontology__obo_location)
def get_basic_config(self): config = AppConfig() config.update_server_config( single_dataset__obs_names=None, single_dataset__var_names=None, ) config.update_server_config(app__flask_secret_key="secret") config.update_dataset_config( embeddings__names=["umap"], presentation__max_categories=100, diffexp__lfc_cutoff=0.01, ) return config
def test_handle_app___can_use_envar_port(self): config = self.get_config(port=24) self.assertEqual(config.server_config.app__port, 24) # Note if the port is set in the config file it will NOT be overwritten by a different envvar os.environ["CXG_SERVER_PORT"] = "4008" self.config = AppConfig() self.config.update_server_config(app__flask_secret_key="secret") self.config.server_config.handle_app(self.context) self.assertEqual(self.config.server_config.app__port, 4008) del os.environ["CXG_SERVER_PORT"]
def test_update_app_config(self): config = AppConfig() config.update_server_config(app__verbose=True, single_dataset__datapath="datapath") vars = config.server_config.changes_from_default() self.assertCountEqual(vars, [("app__verbose", True, False), ("single_dataset__datapath", "datapath", None)]) config = AppConfig() config.update_dataset_config(app__scripts=(), app__inline_scripts=()) vars = config.server_config.changes_from_default() self.assertCountEqual(vars, []) config = AppConfig() config.update_dataset_config(app__scripts=[], app__inline_scripts=[]) vars = config.dataset_config.changes_from_default() self.assertCountEqual(vars, []) config = AppConfig() config.update_dataset_config(app__scripts=("a", "b"), app__inline_scripts=["c", "d"]) vars = config.dataset_config.changes_from_default() self.assertCountEqual(vars, [("app__scripts", ["a", "b"], []), ("app__inline_scripts", ["c", "d"], [])])
def test_handle_embeddings__checks_data_file_types(self): file_name = self.custom_app_config( embedding_names=["name1", "name2"], enable_reembedding="true", dataset_datapath=f"{FIXTURES_ROOT}/pbmc3k-CSC-gz.h5ad", anndata_backed="true", config_file_name=self.config_file_name, ) config = AppConfig() config.update_from_config_file(file_name) config.server_config.complete_config(self.context) with self.assertRaises(ConfigurationError): config.dataset_config.handle_embeddings()
def test_get_default_config_correctly_reads_default_config_file(self): app_default_config = AppConfig().default_config expected_config = yaml.load(default_config, Loader=yaml.Loader) server_config = app_default_config["server"] dataset_config = app_default_config["dataset"] expected_server_config = expected_config["server"] expected_dataset_config = expected_config["dataset"] self.assertDictEqual(app_default_config, expected_config) self.assertDictEqual(server_config, expected_server_config) self.assertDictEqual(dataset_config, expected_dataset_config)
def setUp(self): self.config_file_name = f"{unittest.TestCase.id(self).split('.')[-1]}.yml" self.config = AppConfig() self.config.update_server_config(app__flask_secret_key="secret") self.config.update_server_config(single_dataset__datapath=H5AD_FIXTURE) self.dataset_config = self.config.dataset_config self.config.complete_config() message_list = [] def noop(message): message_list.append(message) messagefn = noop self.context = dict(messagefn=messagefn, messages=message_list)
def test_configfile_no_server_section(self): # test a config file without a dataset section with tempfile.TemporaryDirectory() as tempdir: configfile = os.path.join(tempdir, "config.yaml") with open(configfile, "w") as fconfig: config = """ dataset: user_annotations: enable: false """ fconfig.write(config) app_config = AppConfig() app_config.update_from_config_file(configfile) server_changes = app_config.server_config.changes_from_default() dataset_changes = app_config.dataset_config.changes_from_default() self.assertEqual(server_changes, []) self.assertEqual(dataset_changes, [("user_annotations__enable", False, True)])
def test_handle_data_locator_works_for_default_types( self, mock_discover_region_name): mock_discover_region_name.return_value = None # Default config self.assertEqual( self.config.server_config.data_locator__s3__region_name, None) # hard coded config = self.get_config() self.assertEqual(config.server_config.data_locator__s3__region_name, "us-east-1") # incorrectly formatted datapath = "s3://shouldnt/work" file_name = self.custom_app_config( dataset_datapath=datapath, config_file_name=self.config_file_name, data_locater_region_name="true") config = AppConfig() config.update_from_config_file(file_name) with self.assertRaises(ConfigurationError): config.server_config.handle_data_locator()
def app_config(data_locator, backed=False, extra_server_config={}, extra_dataset_config={}): config = AppConfig() config.update_server_config( app__flask_secret_key="secret", single_dataset__obs_names=None, single_dataset__var_names=None, adaptor__anndata_adaptor__backed=backed, single_dataset__datapath=data_locator, limits__diffexp_cellcount_max=None, limits__column_request_max=None, ) config.update_dataset_config(embeddings__names=["umap", "tsne", "pca"], presentation__max_categories=100, diffexp__lfc_cutoff=0.01) config.update_server_config(**extra_server_config) config.update_dataset_config(**extra_dataset_config) config.complete_config() return config
def test_mapping_creation_returns_map_of_server_and_dataset_config(self): config = AppConfig() mapping = config.dataset_config.create_mapping(config.default_config) self.assertIsNotNone(mapping["server__app__verbose"]) self.assertIsNotNone(mapping["dataset__presentation__max_categories"]) self.assertIsNotNone(mapping["dataset__user_annotations__ontology__obo_location"])
def launch( datapath, verbose, debug, open_browser, port, host, embedding, obs_names, var_names, max_category_items, disable_custom_colors, diffexp_lfc_cutoff, title, scripts, about, disable_annotations, annotations_file, annotations_dir, backed, disable_diffexp, experimental_annotations_ontology, experimental_annotations_ontology_obo, experimental_enable_reembedding, config_file, dump_default_config, ): """Launch the cellxgene data viewer. This web app lets you explore single-cell expression data. Data must be in a format that cellxgene expects. Read the "getting started" guide to learn more: https://chanzuckerberg.github.io/cellxgene/getting-started.html Examples: > cellxgene launch example-dataset/pbmc3k.h5ad --title pbmc3k > cellxgene launch <your data file> --title <your title> > cellxgene launch <url>""" if dump_default_config: print(default_config) sys.exit(0) # Startup message click.echo("[cellxgene] Starting the CLI...") # app config app_config = AppConfig() server_config = app_config.server_config try: if config_file: app_config.update_from_config_file(config_file) # Determine which config options were give on the command line. # Those will override the ones provided in the config file (if provided). cli_config = AppConfig() cli_config.update_server_config( app__verbose=verbose, app__debug=debug, app__host=host, app__port=port, app__open_browser=open_browser, single_dataset__datapath=datapath, single_dataset__title=title, single_dataset__about=about, single_dataset__obs_names=obs_names, single_dataset__var_names=var_names, adaptor__anndata_adaptor__backed=backed, ) cli_config.update_dataset_config( app__scripts=scripts, user_annotations__enable=not disable_annotations, user_annotations__local_file_csv__file=annotations_file, user_annotations__local_file_csv__directory=annotations_dir, user_annotations__ontology__enable= experimental_annotations_ontology, user_annotations__ontology__obo_location= experimental_annotations_ontology_obo, presentation__max_categories=max_category_items, presentation__custom_colors=not disable_custom_colors, embeddings__names=embedding, embeddings__enable_reembedding=experimental_enable_reembedding, diffexp__enable=not disable_diffexp, diffexp__lfc_cutoff=diffexp_lfc_cutoff, ) diff = cli_config.server_config.changes_from_default() changes = {key: val for key, val, _ in diff} app_config.update_server_config(**changes) diff = cli_config.dataset_config.changes_from_default() changes = {key: val for key, val, _ in diff} app_config.update_dataset_config(**changes) # process the configuration # any errors will be thrown as an exception. # any info messages will be passed to the messagefn function. def messagefn(message): click.echo("[cellxgene] " + message) # Use a default secret if one is not provided if not server_config.app__flask_secret_key: app_config.update_server_config( app__flask_secret_key="SparkleAndShine") app_config.complete_config(messagefn) except (ConfigurationError, DatasetAccessError) as e: raise click.ClickException(e) handle_scripts(scripts) # create the server server = CliLaunchServer(app_config) if not server_config.app__verbose: log = logging.getLogger("werkzeug") log.setLevel(logging.ERROR) cellxgene_url = f"http://{app_config.server_config.app__host}:{app_config.server_config.app__port}" if server_config.app__open_browser: click.echo( f"[cellxgene] Launching! Opening your browser to {cellxgene_url} now." ) webbrowser.open(cellxgene_url) else: click.echo( f"[cellxgene] Launching! Please go to {cellxgene_url} in your browser." ) click.echo("[cellxgene] Type CTRL-C at any time to exit.") if not server_config.app__verbose: f = open(os.devnull, "w") sys.stdout = f try: server.app.run( host=server_config.app__host, debug=server_config.app__debug, port=server_config.app__port, threaded=not server_config.app__debug, use_debugger=False, use_reloader=False, ) except OSError as e: if e.errno == errno.EADDRINUSE: raise click.ClickException( "Port is in use, please specify an open port using the --port flag." ) from e raise
def main(): parser = argparse.ArgumentParser("A command to test diffexp") parser.add_argument("dataset", help="name of a dataset to load") parser.add_argument("-na", "--numA", type=int, help="number of rows in group A") parser.add_argument("-nb", "--numB", type=int, help="number of rows in group B") parser.add_argument("-va", "--varA", help="obs variable:value to use for group A") parser.add_argument("-vb", "--varB", help="obs variable:value to use for group B") parser.add_argument("-t", "--trials", default=1, type=int, help="number of trials") parser.add_argument("-a", "--alg", choices=("default", "generic"), default="default", help="algorithm to use") parser.add_argument("-s", "--show", default=False, action="store_true", help="show the results") parser.add_argument("-n", "--new-selection", default=False, action="store_true", help="change the selection between each trial") parser.add_argument("--seed", default=1, type=int, help="set the random seed") args = parser.parse_args() app_config = AppConfig() app_config.update_server_config(single_dataset__datapath=args.dataset) app_config.update_server_config(app__verbose=True) app_config.complete_config() loader = MatrixDataLoader(args.dataset) adaptor = loader.open(app_config) random.seed(args.seed) np.random.seed(args.seed) rows = adaptor.get_shape()[0] if args.numA: filterA = random.sample(range(rows), args.numA) elif args.varA: vname, vval = args.varA.split(":") filterA = get_filter_from_obs(adaptor, vname, vval) else: print("must supply numA or varA") sys.exit(1) if args.numB: filterB = random.sample(range(rows), args.numB) elif args.varB: vname, vval = args.varB.split(":") filterB = get_filter_from_obs(adaptor, vname, vval) else: print("must supply numB or varB") sys.exit(1) for i in range(args.trials): if args.new_selection: if args.numA: filterA = random.sample(range(rows), args.numA) if args.numB: filterB = random.sample(range(rows), args.numB) maskA = np.zeros(rows, dtype=bool) maskA[filterA] = True maskB = np.zeros(rows, dtype=bool) maskB[filterB] = True t1 = time.time() if args.alg == "default": results = adaptor.compute_diffexp_ttest(maskA, maskB) elif args.alg == "generic": results = diffexp_generic.diffexp_ttest(adaptor, maskA, maskB) t2 = time.time() print("TIME=", t2 - t1) if args.show: for res in results: print(res)
import functools import logging import sys import webbrowser import os import click from flask_compress import Compress from flask_cors import CORS from local_server.default_config import default_config from local_server.app.app import Server from local_server.common.config.app_config import AppConfig from local_server.common.errors import DatasetAccessError, ConfigurationError from local_server.common.utils.utils import sort_options DEFAULT_CONFIG = AppConfig() def annotation_args(func): @click.option( "--disable-annotations", is_flag=True, default=not DEFAULT_CONFIG.dataset_config.user_annotations__enable, show_default=True, help="Disable user annotation of data.", ) @click.option( "--annotations-file", default=DEFAULT_CONFIG.dataset_config. user_annotations__local_file_csv__file, show_default=True,
def test_aws_secrets_manager_error(self, mock_get_secret_key): mock_get_secret_key.return_value = { "db_uri": "mock_db_uri", } # no region app_config = AppConfig() app_config.external_config.aws_secrets_manager__region = None app_config.external_config.aws_secrets_manager__secrets = [ dict(name="secret1", values=[ dict(key="key1", required=True, path=["this", "is", "my", "path"]) ]) ] with self.assertRaises(ConfigurationError) as config_error: app_config.complete_config() self.assertEqual( config_error.exception.message, "Invalid type for attribute: aws_secrets_manager__region, expected type str, got NoneType", ) # missing secret name app_config = AppConfig() app_config.external_config.aws_secrets_manager__region = "us-west-2" app_config.external_config.aws_secrets_manager__secrets = [ dict(values=[ dict(key="db_uri", required=True, path=["this", "is", "my", "path"]) ]) ] with self.assertRaises(ConfigurationError) as config_error: app_config.complete_config() self.assertEqual(config_error.exception.message, "aws_secrets_manager: 'name' is missing") # secret name wrong type app_config = AppConfig() app_config.external_config.aws_secrets_manager__region = "us-west-2" app_config.external_config.aws_secrets_manager__secrets = [ dict(name=1, values=[ dict(key="db_uri", required=True, path=["this", "is", "my", "path"]) ]) ] with self.assertRaises(ConfigurationError) as config_error: app_config.complete_config() self.assertEqual(config_error.exception.message, "aws_secrets_manager: 'name' must be a string") # missing values name app_config = AppConfig() app_config.external_config.aws_secrets_manager__region = "us-west-2" app_config.external_config.aws_secrets_manager__secrets = [ dict(name="mysecret") ] with self.assertRaises(ConfigurationError) as config_error: app_config.complete_config() self.assertEqual(config_error.exception.message, "aws_secrets_manager: 'values' is missing") # values wrong type app_config = AppConfig() app_config.external_config.aws_secrets_manager__region = "us-west-2" app_config.external_config.aws_secrets_manager__secrets = [ dict(name="mysecret", values=dict(key="db_uri", required=True, path=["this", "is", "my", "path"])) ] with self.assertRaises(ConfigurationError) as config_error: app_config.complete_config() self.assertEqual(config_error.exception.message, "aws_secrets_manager: 'values' must be a list") # entry missing key app_config = AppConfig() app_config.external_config.aws_secrets_manager__region = "us-west-2" app_config.external_config.aws_secrets_manager__secrets = [ dict(name="mysecret", values=[ dict(required=True, path=["this", "is", "my", "path"]) ]) ] with self.assertRaises(ConfigurationError) as config_error: app_config.complete_config() self.assertEqual(config_error.exception.message, "missing 'key' in secret values: mysecret") # entry required is wrong type app_config = AppConfig() app_config.external_config.aws_secrets_manager__region = "us-west-2" app_config.external_config.aws_secrets_manager__secrets = [ dict(name="mysecret", values=[ dict(key="db_uri", required="optional", path=["this", "is", "my", "path"]) ]) ] with self.assertRaises(ConfigurationError) as config_error: app_config.complete_config() self.assertEqual( config_error.exception.message, "wrong type for 'required' in secret values: mysecret") # entry missing path app_config = AppConfig() app_config.external_config.aws_secrets_manager__region = "us-west-2" app_config.external_config.aws_secrets_manager__secrets = [ dict(name="mysecret", values=[dict(key="db_uri", required=True)]) ] with self.assertRaises(ConfigurationError) as config_error: app_config.complete_config() self.assertEqual(config_error.exception.message, "missing 'path' in secret values: mysecret") # secret missing required key app_config = AppConfig() app_config.external_config.aws_secrets_manager__region = "us-west-2" app_config.external_config.aws_secrets_manager__secrets = [ dict( name="mysecret", values=[ dict(key="KEY_DOES_NOT_EXIST", required=True, path=["this", "is", "a", "path"]) ], ) ] with self.assertRaises(ConfigurationError) as config_error: app_config.complete_config() self.assertEqual( config_error.exception.message, "required secret 'mysecret:KEY_DOES_NOT_EXIST' not set")