mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-26 07:18:11 +08:00
Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f48d06fb90 | ||
|
|
84563291a0 | ||
|
|
ddb601c103 | ||
|
|
450261f109 | ||
|
|
c0f4ad6dfa | ||
|
|
ff5da77372 | ||
|
|
a9044b01df | ||
|
|
11b948369f | ||
|
|
a2dc0c25b1 | ||
|
|
a9ef01a6f9 | ||
|
|
03d9e8e6aa | ||
|
|
30e19e47c6 | ||
|
|
d2b20129f7 | ||
|
|
6c86216f6b | ||
|
|
4df50a7677 | ||
|
|
69a6d52240 | ||
|
|
06da05eb9f | ||
|
|
d753441acc | ||
|
|
8b4c1e418e | ||
|
|
f2bd6ebce1 | ||
|
|
de44739f8b | ||
|
|
002c9a5c7f | ||
|
|
e6a0351079 | ||
|
|
38fdc649f5 | ||
|
|
69a5d5e837 | ||
|
|
b854c58eea | ||
|
|
2d650ba50d | ||
|
|
8bac98f25c | ||
|
|
ceb0cc6f27 |
+1
-1
@@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 1.0.0
|
||||
current_version = 1.1.1
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<prerel>rc)\.(?P<prerelversion>\d+))?
|
||||
serialize =
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Build docker image
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Cache env vars
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
- run: |
|
||||
git fetch --depth=1 origin +${{github.base_ref}}
|
||||
- name: Set up Python 3.7
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.7
|
||||
- name: Node cache
|
||||
@@ -46,10 +46,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.7
|
||||
uses: actions/setup-python@v1
|
||||
- name: Set up Python 3.7 (pyenv) # pyenv needed for mlflow in cli annotate tests
|
||||
uses: gabrielfalcao/pyenv-action@v9
|
||||
with:
|
||||
python-version: 3.7
|
||||
default: 3.7
|
||||
command: pip install -U pip # upgrade pip after installing python
|
||||
- run: pip install virtualenv # virtualenv needed for mlflow in cli annotate tests
|
||||
- name: Python cache
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
@@ -78,7 +80,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.7
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.7
|
||||
- name: Python cache
|
||||
@@ -102,32 +104,33 @@ jobs:
|
||||
cd client && make smoke-test
|
||||
./node_modules/codecov/bin/codecov --yml=../.codecov.yml --root=../ --gcov-root=../ -C -F frontend,javascript,smokeTest
|
||||
|
||||
smoke-tests-annotations:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.7
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.7
|
||||
- name: Python cache
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Node cache
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
- name: Install dependencies
|
||||
run: make pydist install-dist
|
||||
- name: Smoke tests (with annotations feature)
|
||||
run: |
|
||||
cd client && make smoke-test-annotations
|
||||
./node_modules/codecov/bin/codecov --yml=../.codecov.yml --root=../ --gcov-root=../ -C -F frontend,javascript,smokeTestAnnotations
|
||||
# TODO: reinstate: https://github.com/chanzuckerberg/cellxgene/issues/2544
|
||||
# smoke-tests-annotations:
|
||||
# runs-on: ubuntu-latest
|
||||
# timeout-minutes: 20
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
# - name: Set up Python 3.7
|
||||
# uses: actions/setup-python@v4
|
||||
# with:
|
||||
# python-version: 3.7
|
||||
# - name: Python cache
|
||||
# uses: actions/cache@v1
|
||||
# with:
|
||||
# path: ~/.cache/pip
|
||||
# key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-pip-
|
||||
# - name: Node cache
|
||||
# uses: actions/cache@v1
|
||||
# with:
|
||||
# path: ~/.npm
|
||||
# key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-node-
|
||||
# - name: Install dependencies
|
||||
# run: make pydist install-dist
|
||||
# - name: Smoke tests (with annotations feature)
|
||||
# run: |
|
||||
# cd client && make smoke-test-annotations
|
||||
# ./node_modules/codecov/bin/codecov --yml=../.codecov.yml --root=../ --gcov-root=../ -C -F frontend,javascript,smokeTestAnnotations
|
||||
|
||||
@@ -54,3 +54,6 @@ client/.eslintcache
|
||||
|
||||
# E2E Testing
|
||||
ignoreE2E*
|
||||
|
||||
# annotate subcmd
|
||||
.models_cache
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017-2021 Chan Zuckerberg Initiative
|
||||
Copyright (c) 2017-2022 Chan Zuckerberg Initiative
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
|
||||
@@ -3,5 +3,6 @@ recursive-include server/common/web/static *
|
||||
|
||||
include server/requirements.txt
|
||||
include server/requirements-prepare.txt
|
||||
include server/requirements-annotate.txt
|
||||
include server/converters/schema/hgnc_complete_set.txt.gz
|
||||
include server/converters/schema/schema_definitions/*
|
||||
|
||||
@@ -7,27 +7,27 @@ _an interactive explorer for single-cell transcriptomics data_
|
||||
[](https://github.com/chanzuckerberg/cellxgene/actions?query=workflow%3A%22Compatibility+Tests%22)
|
||||

|
||||
|
||||
cellxgene Desktop (pronounced "cell-by-gene") is an interactive data explorer for single-cell datasets, such as those coming from the [Human Cell Atlas](https://humancellatlas.org). Leveraging modern web development techniques to enable fast visualizations of at least 1 million cells, we hope to enable biologists and computational researchers to explore their data.
|
||||
CZ CELLxGENE Annotate (pronounced "cell-by-gene") is an interactive data explorer for single-cell datasets, such as those coming from the [Human Cell Atlas](https://humancellatlas.org). Leveraging modern web development techniques to enable fast visualizations of at least 1 million cells, we hope to enable biologists and computational researchers to explore their data.
|
||||
|
||||
Whether you need to visualize one thousand cells or one million, cellxgene Desktop helps you gain insight into your single-cell data.
|
||||
Whether you need to visualize one thousand cells or one million, CELLxGENE Annotate helps you gain insight into your single-cell data.
|
||||
|
||||
<img src="https://github.com/chanzuckerberg/cellxgene/raw/main/docs/images/crossfilter.gif" width="350" height="200" hspace="30"><img src="https://github.com/chanzuckerberg/cellxgene/raw/main/docs/images/category-breakdown.gif" width="350" height="200" hspace="30">
|
||||
|
||||
# Getting started
|
||||
|
||||
### The comprehensive guide to cellxgene Desktop
|
||||
### The comprehensive guide to CZ CELLxGENE Annotate
|
||||
|
||||
[The cellxgene documentation is your one-stop-shop for information about cellxgene Desktop](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/README.md)! You may be particularly interested in:
|
||||
[The CZ CELLxGENE Annotate documentation is your one-stop-shop for information about CELLxGENE Annotate](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/README.md)! You may be particularly interested in:
|
||||
|
||||
- Seeing [what cellxgene Desktop can do](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/explore-data/explorer-tutorials.md)
|
||||
- Learning more about cellxgene [installation](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/desktop/install.md) and [usage](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/desktop/quick-start.md#quick-start-1)
|
||||
- [Preparing your own data](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/desktop/data-reqs.md) for use in cellxgene Desktop
|
||||
- Seeing [what Annotate can do](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/explore-data/explorer-tutorials.md)
|
||||
- Learning more about Annotate [installation](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/desktop/install.md) and [usage](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/desktop/quick-start.md#quick-start-1)
|
||||
- [Preparing your own data](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/desktop/data-reqs.md) for use in Annotate
|
||||
- Checking out [our roadmap](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/roadmap.md) for future development
|
||||
- [Contributing](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/contribute.md) to cellxgene Desktop
|
||||
- [Contributing](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/contribute.md) to Annotate
|
||||
|
||||
### Quick start
|
||||
|
||||
To install cellxgene Desktop you need Python 3.6+. We recommend [installing cellxgene Desktop into a conda or virtual environment.](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/desktop/install.md)
|
||||
To install CELLxGENE Annotate you need Python 3.6+. We recommend [installing Annotate into a conda or virtual environment.](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/desktop/install.md)
|
||||
|
||||
Install the package.
|
||||
|
||||
@@ -35,19 +35,19 @@ Install the package.
|
||||
pip install cellxgene
|
||||
```
|
||||
|
||||
Launch cellxgene Desktop with an example [anndata](https://anndata.readthedocs.io/en/latest/) file
|
||||
Launch Annotate with an example [anndata](https://anndata.readthedocs.io/en/latest/) file
|
||||
|
||||
```bash
|
||||
cellxgene launch https://cellxgene-example-data.czi.technology/pbmc3k.h5ad
|
||||
```
|
||||
|
||||
To explore more datasets already formatted for cellxgene Desktop, check out the [Demo data](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/desktop/quick-start.md#example-datasets) or
|
||||
To explore more datasets already formatted for Annotate, check out the [Demo data](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/desktop/quick-start.md#example-datasets) or
|
||||
see [Preparing your data](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/desktop/data-reqs.md) to learn more about formatting your own
|
||||
data for cellxgene Desktop.
|
||||
data for CELLxGENE Annotate.
|
||||
|
||||
### Supported browsers
|
||||
|
||||
cellxgene Desktop currently supports the following browsers:
|
||||
CELLxGENE Annotate currently supports the following browsers:
|
||||
|
||||
- Google Chrome 61+
|
||||
- Edge 15+
|
||||
@@ -62,11 +62,11 @@ For questions, suggestions, or accolades, [join the `#cellxgene-users` channel o
|
||||
|
||||
For any errors, [report bugs on Github](https://github.com/chanzuckerberg/cellxgene/issues).
|
||||
|
||||
# Developing with cellxgene Desktop
|
||||
# Developing with CZ CELLxGENE Annotate
|
||||
|
||||
### Contributing
|
||||
|
||||
We warmly welcome contributions from the community! Please see our [contributing guide](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/contribute.md) and don't hesitate to open an issue or send a pull request to improve cellxgene Desktop. Please see the [dev_docs](https://github.com/chanzuckerberg/cellxgene/tree/main/dev_docs) for pull request suggestions, unit test details, local documentation preview, and other development specifics.
|
||||
We warmly welcome contributions from the community! Please see our [contributing guide](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/contribute.md) and don't hesitate to open an issue or send a pull request to improve CELLxGENE Annotate. Please see the [dev_docs](https://github.com/chanzuckerberg/cellxgene/tree/main/dev_docs) for pull request suggestions, unit test details, local documentation preview, and other development specifics.
|
||||
|
||||
This project adheres to the Contributor Covenant [code of conduct](https://github.com/chanzuckerberg/.github/blob/master/CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to opensource@chanzuckerberg.com.
|
||||
|
||||
@@ -77,11 +77,11 @@ As such, we encourage other scientific tool builders in academia or industry to
|
||||
this project. All code is freely available for reuse under the [MIT license](https://opensource.org/licenses/MIT).
|
||||
|
||||
|
||||
Before extending cellxgene, we encourage you to reach out to us with ideas or questions. It might be possible that an
|
||||
Before extending CELLxGENE Annotate, we encourage you to reach out to us with ideas or questions. It might be possible that an
|
||||
extension could be directly contributed, which would make it available for a wider audience, or that it's on our
|
||||
[roadmap](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/roadmap.md) and under active development.
|
||||
|
||||
See the [cellxgene extensions](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/community-extensions.md) section of our documentation for examples of community use and cellxgene extensions.
|
||||
See the [CELLxGENE extensions](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/community-extensions.md) section of our documentation for examples of community use and CELLxGENE extensions.
|
||||
|
||||
### Security
|
||||
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
exports[`did launch page launched 1`] = `"<span style=\\"max-width: 155px; display: flex; overflow: hidden; justify-content: flex-start; width: 100%; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">pbm</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">c3k</span><span style=\\"position: absolute; right: 0px; color: inherit;\\">c3k</span></span></span>"`;
|
||||
|
||||
exports[`metadata loads categories and values from dataset appear 1`] = `"<div style=\\"display: flex; justify-content: space-between; align-items: baseline;\\"><div style=\\"display: flex; justify-content: flex-start; align-items: flex-start;\\"><label class=\\"bp3-control bp3-checkbox\\" for=\\"category-select-louvain\\"><input id=\\"category-select-louvain\\" data-testclass=\\"category-select\\" data-testid=\\"louvain:category-select\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span role=\\"menuitem\\" tabindex=\\"0\\" data-testclass=\\"category-expand\\" data-testid=\\"louvain:category-expand\\" style=\\"cursor: pointer;\\"><span aria-haspopup=\\"true\\" class=\\"bp3-popover2-target\\"><span data-testid=\\"louvain:category-label\\" tabindex=\\"-1\\" aria-label=\\"louvain\\" class=\\"\\" style=\\"max-width: 265px;\\"><span style=\\"max-width: 265px; display: flex; overflow: hidden; justify-content: flex-start; width: 100%; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">lou</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">vain</span><span style=\\"position: absolute; right: 0px; color: inherit;\\">vain</span></span></span></span></span><svg stroke=\\"currentColor\\" fill=\\"currentColor\\" stroke-width=\\"0\\" viewBox=\\"0 0 320 512\\" data-testclass=\\"category-expand-is-not-expanded\\" height=\\"1em\\" width=\\"1em\\" xmlns=\\"http://www.w3.org/2000/svg\\" style=\\"font-size: 10px; margin-left: 5px;\\"><path d=\\"M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z\\"></path></svg></span></div><div><span class=\\"bp3-popover-wrapper\\"><span aria-haspopup=\\"true\\" class=\\"bp3-popover-target\\"><a role=\\"button\\" data-testclass=\\"colorby\\" data-testid=\\"colorby-louvain\\" class=\\"bp3-button\\" tabindex=\\"0\\"><span icon=\\"tint\\" class=\\"bp3-icon bp3-icon-tint\\"><svg data-icon=\\"tint\\" width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\"><desc>tint</desc><path d=\\"M7.88 1s-4.9 6.28-4.9 8.9c.01 2.82 2.34 5.1 4.99 5.1 2.65-.01 5.03-2.3 5.03-5.13C12.99 7.17 7.88 1 7.88 1z\\" fill-rule=\\"evenodd\\"></path></svg></span></a></span></span></div></div><div style=\\"margin-left: 26px;\\"></div>"`;
|
||||
exports[`metadata loads categories and values from dataset appear 1`] = `"<div style=\\"display: flex; justify-content: space-between; align-items: baseline;\\"><div style=\\"display: flex; justify-content: flex-start; align-items: flex-start;\\"><label class=\\"bp3-control bp3-checkbox\\" for=\\"category-select-louvain\\"><input id=\\"category-select-louvain\\" data-testclass=\\"category-select\\" data-testid=\\"louvain:category-select\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span role=\\"menuitem\\" tabindex=\\"0\\" data-testclass=\\"category-expand\\" data-testid=\\"louvain:category-expand\\" style=\\"cursor: pointer;\\"><span aria-haspopup=\\"true\\" class=\\"bp3-popover2-target\\"><span data-testid=\\"louvain:category-label\\" tabindex=\\"-1\\" aria-label=\\"louvain\\" class=\\"\\" style=\\"max-width: 265px;\\"><span style=\\"max-width: 265px; display: flex; overflow: hidden; justify-content: flex-start; width: 100%; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">lou</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">vain</span><span style=\\"position: absolute; right: 0px; color: inherit;\\">vain</span></span></span></span></span><svg stroke=\\"currentColor\\" fill=\\"currentColor\\" stroke-width=\\"0\\" viewBox=\\"0 0 320 512\\" data-testclass=\\"category-expand-is-not-expanded\\" height=\\"1em\\" width=\\"1em\\" xmlns=\\"http://www.w3.org/2000/svg\\" style=\\"font-size: 10px; margin-left: 5px;\\"><path d=\\"M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z\\"></path></svg></span></div><div><span class=\\"bp3-popover-wrapper\\"><span aria-haspopup=\\"true\\" class=\\"bp3-popover-target\\"><a role=\\"button\\" data-testclass=\\"colorby\\" data-testid=\\"colorby-louvain\\" class=\\"bp3-button\\" tabindex=\\"0\\"><span icon=\\"tint\\" aria-hidden=\\"true\\" tabindex=\\"0\\" class=\\"bp3-icon bp3-icon-tint\\"><svg data-icon=\\"tint\\" width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\"><path d=\\"M7.88 1s-4.9 6.28-4.9 8.9c.01 2.82 2.34 5.1 4.99 5.1 2.65-.01 5.03-2.3 5.03-5.13C12.99 7.17 7.88 1 7.88 1z\\" fill-rule=\\"evenodd\\"></path></svg></span></a></span></span></div></div><div style=\\"margin-left: 26px;\\"></div>"`;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,8 +1,6 @@
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const FaviconsWebpackPlugin = require("favicons-webpack-plugin");
|
||||
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
|
||||
const { merge } = require("webpack-merge");
|
||||
@@ -11,6 +9,7 @@ const sharedConfig = require("./webpack.config.shared");
|
||||
const babelOptions = require("../babel/babel.dev");
|
||||
|
||||
const fonts = path.resolve("src/fonts");
|
||||
const images = path.resolve("src/images");
|
||||
const nodeModules = path.resolve("node_modules");
|
||||
|
||||
const devConfig = {
|
||||
@@ -30,11 +29,11 @@ const devConfig = {
|
||||
{
|
||||
test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2|otf)$/i,
|
||||
loader: "file-loader",
|
||||
include: [nodeModules, fonts],
|
||||
include: [nodeModules, fonts, images],
|
||||
options: {
|
||||
name: "static/assets/[name].[ext]",
|
||||
// (thuang): This is needed to make sure @font url path is '/static/assets/'
|
||||
publicPath: "/",
|
||||
publicPath: "..",
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -44,21 +43,6 @@ const devConfig = {
|
||||
inject: true,
|
||||
template: path.resolve("index.html"),
|
||||
}),
|
||||
new FaviconsWebpackPlugin({
|
||||
logo: "./favicon.png",
|
||||
prefix: "static/img/",
|
||||
favicons: {
|
||||
icons: {
|
||||
android: false,
|
||||
appleIcon: false,
|
||||
appleStartup: false,
|
||||
coast: false,
|
||||
firefox: false,
|
||||
windows: false,
|
||||
yandex: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "static/[name].css",
|
||||
}),
|
||||
@@ -73,9 +57,6 @@ const devConfig = {
|
||||
CXG_SERVER_PORT: process.env.CXG_SERVER_PORT || "5005",
|
||||
}),
|
||||
}),
|
||||
new ScriptExtHtmlWebpackPlugin({
|
||||
async: "obsolete",
|
||||
}),
|
||||
],
|
||||
infrastructureLogging: {
|
||||
level: "warn",
|
||||
|
||||
@@ -3,9 +3,7 @@ const webpack = require("webpack");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
|
||||
const TerserJSPlugin = require("terser-webpack-plugin");
|
||||
const CleanCss = require("clean-css");
|
||||
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
|
||||
const FaviconsWebpackPlugin = require("favicons-webpack-plugin");
|
||||
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
|
||||
const { merge } = require("webpack-merge");
|
||||
@@ -16,6 +14,7 @@ const CspHashPlugin = require("./cspHashPlugin");
|
||||
const sharedConfig = require("./webpack.config.shared");
|
||||
|
||||
const fonts = path.resolve("src/fonts");
|
||||
const images = path.resolve("src/images");
|
||||
const nodeModules = path.resolve("node_modules");
|
||||
|
||||
const prodConfig = {
|
||||
@@ -29,8 +28,8 @@ const prodConfig = {
|
||||
minimize: true,
|
||||
minimizer: [
|
||||
new TerserJSPlugin({}),
|
||||
new OptimizeCSSAssetsPlugin({
|
||||
cssProcessor: CleanCss,
|
||||
new CssMinimizerPlugin({
|
||||
minify: CssMinimizerPlugin.cleanCssMinify,
|
||||
}),
|
||||
],
|
||||
},
|
||||
@@ -45,11 +44,11 @@ const prodConfig = {
|
||||
{
|
||||
test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2|otf)$/i,
|
||||
loader: "file-loader",
|
||||
include: [nodeModules, fonts],
|
||||
include: [nodeModules, fonts, images],
|
||||
options: {
|
||||
name: "static/assets/[name]-[contenthash].[ext]",
|
||||
// (thuang): This is needed to make sure @font url path is '../static/assets/'
|
||||
publicPath: "static/",
|
||||
publicPath: "..",
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -66,21 +65,6 @@ const prodConfig = {
|
||||
protectWebpackAssets: false,
|
||||
cleanAfterEveryBuildPatterns: ["main.js", "main.css"],
|
||||
}),
|
||||
new FaviconsWebpackPlugin({
|
||||
logo: "./favicon.png",
|
||||
prefix: "static/assets/",
|
||||
favicons: {
|
||||
icons: {
|
||||
android: false,
|
||||
appleIcon: false,
|
||||
appleStartup: false,
|
||||
coast: false,
|
||||
firefox: false,
|
||||
windows: false,
|
||||
yandex: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "static/[name]-[contenthash].css",
|
||||
}),
|
||||
|
||||
@@ -2,8 +2,6 @@ const path = require("path");
|
||||
const fs = require("fs");
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
const ObsoleteWebpackPlugin = require("obsolete-webpack-plugin");
|
||||
// eslint-disable-next-line @blueprintjs/classes-constants -- incorrect match
|
||||
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");
|
||||
|
||||
const src = path.resolve("src");
|
||||
const nodeModules = path.resolve("node_modules");
|
||||
@@ -67,8 +65,5 @@ module.exports = {
|
||||
template: obsoleteHTMLTemplate,
|
||||
promptOnNonTargetBrowser: false,
|
||||
}),
|
||||
new ScriptExtHtmlWebpackPlugin({
|
||||
async: "obsolete",
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>cell×gene</title>
|
||||
<title>CELL×GENE | Annotate</title>
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>cell×gene</title>
|
||||
<title>CELL×GENE | Annotate</title>
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
|
||||
Generated
+7108
-19010
File diff suppressed because it is too large
Load Diff
+2
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cellxgene",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.1",
|
||||
"license": "MIT",
|
||||
"description": "cellxgene is a web application for the interactive exploration of single cell sequence data.",
|
||||
"repository": "https://github.com/chanzuckerberg/cellxgene",
|
||||
@@ -101,6 +101,7 @@
|
||||
"clean-webpack-plugin": "^4.0.0-alpha.0",
|
||||
"codecov": "^3.7.1",
|
||||
"css-loader": "^5.2.4",
|
||||
"css-minimizer-webpack-plugin": "^4.0.0",
|
||||
"eslint": "^7.24.0",
|
||||
"eslint-config-airbnb": "^18.2.0",
|
||||
"eslint-config-prettier": "^8.2.0",
|
||||
@@ -114,8 +115,6 @@
|
||||
"eslint-plugin-react-hooks": "^4.0.8",
|
||||
"expect-puppeteer": "^5.0.0",
|
||||
"express": "^4.17.1",
|
||||
"favicons": "^6.2.2",
|
||||
"favicons-webpack-plugin": "^5.0.2",
|
||||
"file-loader": "^6.0.0",
|
||||
"html-webpack-plugin": "^5.3.1",
|
||||
"husky": "^7.0.2",
|
||||
@@ -134,11 +133,9 @@
|
||||
"lodash.zip": "^4.2.0",
|
||||
"mini-css-extract-plugin": "^1.5.0",
|
||||
"obsolete-webpack-plugin": "^0.5.6",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
||||
"prettier": "^2.0.5",
|
||||
"puppeteer": "^8.0.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"script-ext-html-webpack-plugin": "^2.1.4",
|
||||
"serve-favicon": "^2.5.0",
|
||||
"terser-webpack-plugin": "^5.1.1",
|
||||
"webpack": "^5.34.0",
|
||||
|
||||
@@ -58,10 +58,11 @@ function _maskToList(mask) {
|
||||
if (!mask) {
|
||||
return null;
|
||||
}
|
||||
const list = new Int32Array(mask.length);
|
||||
const [...m] = mask;
|
||||
const list = new Int32Array(m.length);
|
||||
let elems = 0;
|
||||
for (let i = 0, l = mask.length; i < l; i += 1) {
|
||||
if (mask[i]) {
|
||||
for (let i = 0, l = m.length; i < l; i += 1) {
|
||||
if (m[i]) {
|
||||
list[elems] = i;
|
||||
elems += 1;
|
||||
}
|
||||
|
||||
@@ -91,12 +91,9 @@ export function _whereCacheCreate(field, query, columnLabels) {
|
||||
*/
|
||||
if (typeof query !== "object") return null;
|
||||
|
||||
if (query.where) {
|
||||
const {
|
||||
field: queryField,
|
||||
column: queryColumn,
|
||||
value: queryValue,
|
||||
} = query.where;
|
||||
const { where, summarize } = query;
|
||||
if (where) {
|
||||
const { field: queryField, column: queryColumn, value: queryValue } = where;
|
||||
return {
|
||||
where: {
|
||||
[field]: {
|
||||
@@ -107,13 +104,13 @@ export function _whereCacheCreate(field, query, columnLabels) {
|
||||
},
|
||||
};
|
||||
}
|
||||
if (query.summarize) {
|
||||
if (summarize) {
|
||||
const {
|
||||
method,
|
||||
field: queryField,
|
||||
column: queryColumn,
|
||||
values: queryValues,
|
||||
} = query.summarize;
|
||||
} = summarize;
|
||||
const queryValueHash = _hashStringValues(queryValues);
|
||||
return {
|
||||
summarize: {
|
||||
|
||||
@@ -41,7 +41,7 @@ class App extends React.Component {
|
||||
const { loading, error, graphRenderCounter } = this.props;
|
||||
return (
|
||||
<Container>
|
||||
<Helmet title="cellxgene" />
|
||||
<Helmet title="CELL×GENE | Annotate" />
|
||||
{loading ? (
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import React from "react";
|
||||
import * as globals from "../../globals";
|
||||
import icon from "../../images/icon.png";
|
||||
|
||||
const Logo = (props) => {
|
||||
const { size } = props;
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 48 48" fill="none">
|
||||
<rect width="48" height="48" fill="white" />
|
||||
<rect width="48" height="48" fill={globals.logoColor} />
|
||||
<rect x="19" y="19" width="22" height="22" fill="white" />
|
||||
<rect x="24" y="24" width="12" height="12" fill={globals.logoColor} />
|
||||
<rect x="7" y="19" width="7" height="22" fill="white" />
|
||||
<rect x="19" y="7" width="22" height="7" fill="white" />
|
||||
</svg>
|
||||
<img
|
||||
src={icon}
|
||||
height={size}
|
||||
width={size}
|
||||
alt="CELLxGENE Annotate Logo"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
@@ -137,10 +137,13 @@ function _getEmbeddingRowOffsets(baseRowIndex, embeddingDf) {
|
||||
- if the embedding contains NaN coordinates, return a rowIndex
|
||||
that contains only the rows with discrete valued coordinates.
|
||||
|
||||
Currently assumes that there will be onl two dimensions in the embedding.
|
||||
Currently assumes that there will be only two dimensions in the embedding.
|
||||
*/
|
||||
// eslint-disable-next-line react/destructuring-assignment -- destructuring fails
|
||||
const X = embeddingDf.icol(0).asArray();
|
||||
// eslint-disable-next-line react/destructuring-assignment -- destructuring fails
|
||||
const Y = embeddingDf.icol(1).asArray();
|
||||
|
||||
const offsets = new Int32Array(X.length);
|
||||
let numOffsets = 0;
|
||||
|
||||
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/expect -f
|
||||
|
||||
# Mac only! (depends upon `open` command)
|
||||
|
||||
set h5ad [lindex $argv 0]
|
||||
puts "$h5ad"
|
||||
|
||||
spawn cellxgene launch $h5ad
|
||||
|
||||
set timeout 10
|
||||
expect -indices -re "Please go to (http:\/\/localhost:\[0-9\]+)" {
|
||||
set url $expect_out(1,string)
|
||||
exec >@stdout 2>@stderr open $url
|
||||
}
|
||||
|
||||
interact
|
||||
+1
-1
@@ -2,7 +2,7 @@ import logging
|
||||
import sys
|
||||
from server.common.utils.utils import import_plugins
|
||||
|
||||
__version__ = "1.0.0"
|
||||
__version__ = "1.1.1"
|
||||
display_version = "cellxgene v" + __version__
|
||||
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class AnnotationType(Enum):
|
||||
CELL_TYPE = "cell_type"
|
||||
@@ -0,0 +1,242 @@
|
||||
import functools
|
||||
import json
|
||||
import os.path
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
from os.path import isfile
|
||||
from subprocess import STDOUT, PIPE
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
import click
|
||||
import pandas as pd
|
||||
from click import BadParameter
|
||||
|
||||
from server.annotate.annotation_types import AnnotationType
|
||||
from server.common.utils.data_locator import DataLocator
|
||||
from server.common.utils.utils import sort_options
|
||||
|
||||
|
||||
def annotate_args(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@sort_options
|
||||
@click.command(
|
||||
options_metavar="<options>"
|
||||
)
|
||||
@click.argument(
|
||||
"input_h5ad_file",
|
||||
type=click.Path(exists=True, dir_okay=False, readable=True),
|
||||
nargs=1,
|
||||
metavar="<path to H5AD input file>",
|
||||
required=True,
|
||||
)
|
||||
@click.option(
|
||||
"-m",
|
||||
"--model-url",
|
||||
# Making this a required "option", rather than an "argument", since we support automatic model selection in the
|
||||
# future, in which case the user would not need to specify this option at all and we can make it optional at
|
||||
# that time.
|
||||
required=True,
|
||||
help="The URL of the model used to prediction annotated labels. May be a local filesystem directory "
|
||||
"or S3 path (s3://)",
|
||||
)
|
||||
@click.option(
|
||||
"-o",
|
||||
"--output-h5ad-file",
|
||||
default="",
|
||||
help="The output H5AD file that will contain the generated annotation values. If this option is not provided, "
|
||||
"the input file will be overwritten to include the new annotations; in this case you must specify "
|
||||
"--overwrite.",
|
||||
metavar="<filename>",
|
||||
)
|
||||
@click.option(
|
||||
"--overwrite",
|
||||
default=False,
|
||||
is_flag=True,
|
||||
help="Allow overwriting of the specified H5AD output file, if it exists. For safety, you must specify this "
|
||||
"flag if the specified output file already exists or if the --output-h5ad-file option is not provided.",
|
||||
show_default=True,
|
||||
)
|
||||
@click.option(
|
||||
"-l",
|
||||
"--counts-layer",
|
||||
help="If specified, raw counts will be read from the AnnData layer of the specified name. If unspecified, "
|
||||
"raw counts will be read from `X` matrix, unless 'raw.X' exists, in which case that will be used.",
|
||||
)
|
||||
@click.option(
|
||||
"-g",
|
||||
"--gene-column-name",
|
||||
help="The name of the `var` column that contains gene names. The values in this column will be used to match "
|
||||
"genes between the query and reference datasets. If not specified, the gene names are expected to exist "
|
||||
"in `var.index`.",
|
||||
)
|
||||
# TODO: Useful if we want to support discoverability of models
|
||||
# @click.option(
|
||||
# "-r",
|
||||
# "--model-repository",
|
||||
# help="The base URL of the model repository. Maybe a local filesystem directory or S3 path (s3://)"
|
||||
# )
|
||||
# TODO: Useful if we want to support other, future annotation types, beyond "Cell Type". Currently hidden
|
||||
@click.option(
|
||||
"-a",
|
||||
"--annotation-type",
|
||||
type=click.Choice([t.value for t in AnnotationType]),
|
||||
default=AnnotationType.CELL_TYPE.value,
|
||||
show_default=True,
|
||||
hidden=True, # Remove if we add support for more annotation types
|
||||
help="The type of annotation to perform. This model to be used will be inferred from the annotation type.",
|
||||
)
|
||||
@click.option(
|
||||
"-c",
|
||||
"--annotation-prefix",
|
||||
type=str,
|
||||
default="cxg",
|
||||
show_default=True,
|
||||
help="An optional prefix used to form the names of: 1) new `obs` annotation columns that will store the predicted "
|
||||
"annotation values and confidence scores, 2) `obsm` embeddings (reference and umap embedding), and "
|
||||
"3) `uns` metadata for the prediction operation",
|
||||
)
|
||||
@click.option(
|
||||
"-n",
|
||||
"--run-name",
|
||||
type=str,
|
||||
help="An optional run name that will be used as a suffix to form the names of new `obs` annotation columns that "
|
||||
"will store the predicted annotation values and confidence scores. This can be used to allow multiple "
|
||||
"annotation predictions to be run on a single AnnData object.",
|
||||
)
|
||||
@click.option("--use-model-cache/--no-use-model-cache", default=True)
|
||||
@click.option(
|
||||
"--use-gpu/--no-use-gpu",
|
||||
default=True,
|
||||
help="Whether to use a GPU for annotation operations (highly recommended, if available).",
|
||||
)
|
||||
# TODO: This is a cell type model-specific arg, so not ideal to specify here as a hardcoded option
|
||||
@click.option(
|
||||
"--classifier",
|
||||
default="default",
|
||||
help="For cell type annotation, the classifier level to use. The classifier is model-dependent, so refer to "
|
||||
"documentation for the specified model for valid values.",
|
||||
)
|
||||
# TODO: This is a cell type model-specific arg, so not ideal to specify here as a hardcoded option
|
||||
@click.option(
|
||||
"--organism",
|
||||
type=click.Choice(["Homo sapiens", "Mus musculus"], case_sensitive=True),
|
||||
default="Homo sapiens",
|
||||
help="For cell type annotation, the organism of the dataset. Used to normalize gene names to HGLC conventions when "
|
||||
"an annotation model has been trained using data from different organism.",
|
||||
)
|
||||
@click.option(
|
||||
"--model-cache-dir",
|
||||
default=".models_cache",
|
||||
help="Local directory used to store model files that are retrieved from a remote location. Model files will "
|
||||
"be read from this directory first, if they exist, to avoid repeating large downloads.",
|
||||
)
|
||||
@click.option(
|
||||
"--mlflow-env-manager",
|
||||
type=click.Choice(["virtualenv", "conda", "local"]),
|
||||
default="virtualenv",
|
||||
help="Annotation model prediction will be installed and executed in the specified type of environment. MacOS users "
|
||||
"on Apple Silicon (arm64, M1, M2, etc.) are recommended to use 'conda' to avoid Python package installation "
|
||||
"errors. If 'conda' is specified then cellxgene must also have been installed within a conda environment",
|
||||
)
|
||||
@click.help_option("--help", "-h", help="Show this message and exit.")
|
||||
def annotate(**cli_args):
|
||||
"""
|
||||
Add predicted annotations to an H5AD file. Run `cellxgene annotate --help` for more information.
|
||||
"""
|
||||
_validate_options(cli_args)
|
||||
|
||||
print(f"Reading query dataset {cli_args['input_h5ad_file']}...")
|
||||
|
||||
annotation_prefix = "_".join(
|
||||
filter(None, [cli_args.get("annotation_prefix"), cli_args.get("annotation_type"), cli_args.get("run_name")])
|
||||
)
|
||||
|
||||
output_h5ad_file = (
|
||||
cli_args["input_h5ad_file"]
|
||||
if cli_args["overwrite"] and not cli_args["output_h5ad_file"]
|
||||
else cli_args["output_h5ad_file"]
|
||||
)
|
||||
|
||||
model_url = cli_args.get("model_url")
|
||||
local_model_path = _retrieve_model(cli_args.get("model_cache_dir"), model_url, cli_args.get("use_model_cache"))
|
||||
|
||||
print(f"Annotating {cli_args.get('input_h5ad_file')} with {cli_args.get('annotation_type')}...")
|
||||
|
||||
if cli_args["annotation_type"] == AnnotationType.CELL_TYPE.value:
|
||||
predict_args = dict(
|
||||
query_dataset_h5ad_path=cli_args.get("input_h5ad_file"),
|
||||
output_h5ad_path=output_h5ad_file,
|
||||
annotation_prefix=annotation_prefix,
|
||||
counts_layer=cli_args.get("counts_layer"),
|
||||
gene_column_name=cli_args.get("gene_column_name"),
|
||||
classifier=cli_args.get("classifier"),
|
||||
organism=cli_args.get("organism"),
|
||||
use_gpu=cli_args.get("use_gpu"),
|
||||
)
|
||||
# Drop args that have values of `None` as these will cause problems when passing into MLflow predict, since it
|
||||
# ultimately gets converted into 1-row Pandas DataFrame (None is interpreted as a float type column!)
|
||||
predict_args = dict([(k, v) for k, v in predict_args.items() if v is not None])
|
||||
|
||||
# Invoke prediction using MLflow cli, as a separate process.
|
||||
# This fully prepares the Python environment that is needed for executing the model.
|
||||
# The Python environment will be reused after it is setup once.
|
||||
with NamedTemporaryFile(buffering=0) as predict_args_file:
|
||||
# write the mlflow predict arguments to a csv file, which will be passed to mlflow cmd
|
||||
pd.DataFrame([json.dumps(predict_args)]).to_csv(predict_args_file, index=None)
|
||||
predict_args_file.seek(0)
|
||||
|
||||
# run mlflow prediction in subprocess
|
||||
predict_cmd = (
|
||||
f"mlflow models predict "
|
||||
f"--env-manager {cli_args['mlflow_env_manager']} "
|
||||
f"--model-uri {local_model_path} "
|
||||
f"--content-type csv --input-path {predict_args_file.name}"
|
||||
)
|
||||
p = subprocess.Popen(
|
||||
args=shlex.split(predict_cmd), stdin=predict_args_file, text=True, bufsize=0, stdout=PIPE, stderr=STDOUT
|
||||
)
|
||||
|
||||
# display mlflow process output as it runs
|
||||
for line in p.stdout:
|
||||
print(line.rstrip())
|
||||
|
||||
p.wait()
|
||||
if p.returncode == 0:
|
||||
print(f"Wrote annotations to {output_h5ad_file}")
|
||||
else:
|
||||
print("Annotation failed!")
|
||||
else:
|
||||
raise BadParameter(f"unknown annotation type {cli_args['annotation_type']}")
|
||||
|
||||
|
||||
def _retrieve_model(model_cache_dir, model_url, use_cache=True):
|
||||
local_cache_model_path = os.path.join(model_cache_dir, os.path.splitext(os.path.basename(model_url))[0])
|
||||
if not os.path.exists(local_cache_model_path) or not use_cache:
|
||||
print(f"Retrieving model from {model_url}")
|
||||
# download from remote source
|
||||
with DataLocator(model_url).local_handle() as model_archive_local_path:
|
||||
# unpack archive to local cache dir
|
||||
shutil.unpack_archive(model_archive_local_path, local_cache_model_path)
|
||||
else:
|
||||
print(f"Using cached model at {local_cache_model_path}")
|
||||
|
||||
return local_cache_model_path
|
||||
|
||||
|
||||
def _validate_options(cli_args):
|
||||
output = cli_args["output_h5ad_file"]
|
||||
overwrite = cli_args["overwrite"]
|
||||
|
||||
if isfile(output) and not overwrite:
|
||||
raise click.UsageError(f"Cannot overwrite existing file {output}, try using the flag --overwrite")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
annotate()
|
||||
@@ -1,5 +1,6 @@
|
||||
import click
|
||||
|
||||
from .annotate import annotate
|
||||
from .launch import launch
|
||||
from .prepare import prepare
|
||||
from .upgrade import log_upgrade_check
|
||||
@@ -31,4 +32,5 @@ def cli(upgrade_check):
|
||||
|
||||
|
||||
cli.add_command(launch)
|
||||
cli.add_command(annotate)
|
||||
cli.add_command(prepare)
|
||||
|
||||
@@ -57,7 +57,7 @@ class Annotations(metaclass=ABCMeta):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def write_gene_sets(self, gs, data_adaptor):
|
||||
def write_gene_sets(self, gs, tid, data_adaptor):
|
||||
"""Write the gene sets (gs) to a persistent storage such that it can later be read"""
|
||||
pass
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from hashlib import blake2b
|
||||
|
||||
import pandas as pd
|
||||
from flask import session
|
||||
from fsspec import AbstractFileSystem
|
||||
|
||||
from server import __version__ as cellxgene_version
|
||||
from server.app.session import get_user_id
|
||||
@@ -62,21 +63,27 @@ class AnnotationsLocalFile(Annotations):
|
||||
self.check_user_annotations_enabled() # raises
|
||||
|
||||
fname = self._get_celllabels_filename(data_adaptor)
|
||||
empty_labels = pd.DataFrame()
|
||||
if fname is None:
|
||||
return empty_labels
|
||||
|
||||
with self.label_lock:
|
||||
if fname is not None and os.path.exists(fname) and os.path.getsize(fname) > 0:
|
||||
# returned the cached labels if possible, otherwise read them from the file
|
||||
if fname == self.last_label_fname:
|
||||
return self.last_labels
|
||||
else:
|
||||
labels = pd.read_csv(
|
||||
fname, dtype="category", index_col=0, header=0, comment="#", keep_default_na=False
|
||||
)
|
||||
# update the cache
|
||||
self.last_label_fname = fname
|
||||
self.last_labels = labels
|
||||
return labels
|
||||
else:
|
||||
return pd.DataFrame()
|
||||
locator = DataLocator(fname)
|
||||
if not locator.exists() or locator.size() == 0:
|
||||
return empty_labels
|
||||
|
||||
# return the cached labels if possible
|
||||
if fname == self.last_label_fname:
|
||||
return self.last_labels
|
||||
|
||||
# otherwise, read labels from file
|
||||
with locator.open() as f:
|
||||
labels = pd.read_csv(f, dtype="category", index_col=0, header=0, comment="#", keep_default_na=False)
|
||||
|
||||
# update the cache
|
||||
self.last_label_fname = fname
|
||||
self.last_labels = labels
|
||||
return labels
|
||||
|
||||
def write_labels(self, df, data_adaptor):
|
||||
self.check_user_annotations_enabled() # raises
|
||||
@@ -95,13 +102,12 @@ class AnnotationsLocalFile(Annotations):
|
||||
|
||||
fname = self._get_celllabels_filename(data_adaptor)
|
||||
self._backup(fname)
|
||||
if not df.empty:
|
||||
with open(fname, "w", newline="") as f:
|
||||
locator = DataLocator(fname)
|
||||
with locator.open("w") as f:
|
||||
if not df.empty:
|
||||
if header is not None:
|
||||
f.write(header)
|
||||
df.to_csv(f)
|
||||
else:
|
||||
open(fname, "w").close()
|
||||
|
||||
# update the cache
|
||||
self.last_label_fname = fname
|
||||
@@ -109,26 +115,32 @@ class AnnotationsLocalFile(Annotations):
|
||||
|
||||
def read_gene_sets(self, data_adaptor, context=None):
|
||||
fname = self._get_genesets_filename(data_adaptor)
|
||||
gene_sets = {}
|
||||
tid = None
|
||||
empty_gene_sets = {}
|
||||
|
||||
with self.gene_sets_lock:
|
||||
tid = self.last_geneset_tid # inside the critical section
|
||||
if fname is not None and os.path.exists(fname) and os.path.getsize(fname) > 0:
|
||||
# return the cached genesets if possible, otherwise read from file and validate them
|
||||
if fname == self.last_geneset_fname:
|
||||
gene_sets = self.last_geneset
|
||||
else:
|
||||
# read
|
||||
gene_sets = read_gene_sets_tidycsv(DataLocator(fname), context)
|
||||
if fname is None:
|
||||
return (empty_gene_sets, tid)
|
||||
|
||||
# validate
|
||||
gene_sets = data_adaptor.check_new_gene_sets(gene_sets, context)
|
||||
locator = DataLocator(fname)
|
||||
if not locator.exists() or locator.size() == 0:
|
||||
return (empty_gene_sets, tid)
|
||||
|
||||
# update cache
|
||||
self.last_geneset_fname = fname
|
||||
self.last_geneset = gene_sets
|
||||
# return the cached genesets if possible, otherwise read from file and validate them
|
||||
if fname == self.last_geneset_fname:
|
||||
return (self.last_geneset, tid)
|
||||
|
||||
return (gene_sets, tid)
|
||||
# read
|
||||
gene_sets = read_gene_sets_tidycsv(locator, context)
|
||||
|
||||
# validate
|
||||
gene_sets = data_adaptor.check_new_gene_sets(gene_sets, context)
|
||||
|
||||
# update cache
|
||||
self.last_geneset_fname = fname
|
||||
self.last_geneset = gene_sets
|
||||
|
||||
return (gene_sets, tid)
|
||||
|
||||
def write_gene_sets(self, gene_sets, tid, data_adaptor):
|
||||
self.check_gene_sets_save_enabled() # raises
|
||||
@@ -157,9 +169,9 @@ class AnnotationsLocalFile(Annotations):
|
||||
|
||||
fname = self._get_genesets_filename(data_adaptor)
|
||||
self._backup(fname)
|
||||
with open(fname, "w", newline="") as f:
|
||||
f.write(header)
|
||||
f.write(self.gene_sets_to_csv(gene_sets))
|
||||
locator = DataLocator(fname)
|
||||
with locator.open("w", newline="") as f:
|
||||
f.write(header + self.gene_sets_to_csv(gene_sets))
|
||||
|
||||
# update the cache
|
||||
self.last_geneset_fname = fname
|
||||
@@ -181,7 +193,7 @@ class AnnotationsLocalFile(Annotations):
|
||||
|
||||
output_file = self.label_output_file or self.gene_sets_output_file
|
||||
if output_file:
|
||||
return os.path.dirname(os.path.abspath(output_file))
|
||||
return os.path.dirname(DataLocator(output_file).abspath())
|
||||
|
||||
return os.getcwd()
|
||||
|
||||
@@ -220,34 +232,37 @@ class AnnotationsLocalFile(Annotations):
|
||||
1. fname -> backup_dir/fname-TIME
|
||||
2. delete excess files in backup_dir
|
||||
"""
|
||||
root, ext = os.path.splitext(fname)
|
||||
backup_dir = f"{root}-backups"
|
||||
locator = DataLocator(fname)
|
||||
fs: AbstractFileSystem = locator.fs # Handle to underlying fsspec file system
|
||||
|
||||
# Make sure there is work to do
|
||||
if not os.path.exists(fname):
|
||||
if not locator.exists():
|
||||
return
|
||||
|
||||
root, ext = os.path.splitext(locator.abspath())
|
||||
backup_dir = f"{root}-backups"
|
||||
|
||||
# Ensure backup_dir exists
|
||||
if not os.path.exists(backup_dir):
|
||||
os.mkdir(backup_dir)
|
||||
fs.mkdirs(backup_dir, exist_ok=True)
|
||||
|
||||
# Save current file to backup_dir
|
||||
fname_base = os.path.basename(fname)
|
||||
fname_base_root, fname_base_ext = os.path.splitext(fname_base)
|
||||
# don't use ISO standard time format, as it contains characters illegal on some filesytems.
|
||||
# don't use ISO standard time format, as it contains characters illegal on some filesystems.
|
||||
nowish = datetime.now().strftime("%Y-%m-%dT%H-%M-%S")
|
||||
backup_fname = os.path.join(backup_dir, f"{fname_base_root}-{nowish}{fname_base_ext}")
|
||||
if os.path.exists(backup_fname):
|
||||
os.remove(backup_fname)
|
||||
os.rename(fname, backup_fname)
|
||||
if fs.exists(backup_fname):
|
||||
fs.delete(backup_fname)
|
||||
fs.rename(fname, backup_fname)
|
||||
|
||||
# prune the backup_dir to max number of backup files, keeping the most recent backups
|
||||
backups = list(filter(lambda s: s.startswith(fname_base_root), os.listdir(backup_dir)))
|
||||
excess_count = len(backups) - max_backups
|
||||
if excess_count > 0:
|
||||
backups.sort()
|
||||
for bu in backups[0:excess_count]:
|
||||
os.remove(os.path.join(backup_dir, bu))
|
||||
backup_path_prefix = DataLocator.strip_protocol(os.path.join(backup_dir, fname_base_root + "-"))
|
||||
backups = list(filter(lambda s: s.startswith(backup_path_prefix), fs.ls(backup_dir)))
|
||||
|
||||
# sorting to drop the oldest
|
||||
excess_backups = list(sorted(backups, reverse=True))[max_backups:]
|
||||
for bu in excess_backups:
|
||||
fs.delete(bu)
|
||||
|
||||
def update_parameters(self, parameters, data_adaptor):
|
||||
params = {}
|
||||
|
||||
@@ -4,6 +4,7 @@ from os.path import splitext, isdir
|
||||
from server.common.annotations.local_file_csv import AnnotationsLocalFile
|
||||
from server.common.config.base_config import BaseConfig
|
||||
from server.common.errors import ConfigurationError, AnnotationsError
|
||||
from server.common.utils.data_locator import DataLocator
|
||||
from server.data_common.matrix_loader import MatrixDataLoader
|
||||
|
||||
|
||||
@@ -127,11 +128,15 @@ class DatasetConfig(BaseConfig):
|
||||
if lf_ext and lf_ext != ".csv":
|
||||
raise ConfigurationError(f"genesets file type must be .csv: {genesets_filename}")
|
||||
|
||||
if dirname is not None and not isdir(dirname):
|
||||
try:
|
||||
os.mkdir(dirname)
|
||||
except OSError:
|
||||
raise ConfigurationError("Unable to create directory specified by --user-generated-data-dir")
|
||||
if dirname is not None:
|
||||
if not DataLocator(dirname).islocal():
|
||||
# remote object stores only support objects but not directories, do nothing
|
||||
pass
|
||||
elif not isdir(dirname):
|
||||
try:
|
||||
os.mkdir(dirname)
|
||||
except OSError:
|
||||
raise ConfigurationError("Unable to create directory specified by --user-generated-data-dir")
|
||||
|
||||
anno_config = {
|
||||
"user-annotations": self.user_annotations__enable,
|
||||
|
||||
@@ -52,8 +52,10 @@ class DataLocator:
|
||||
self.fs = fsspec.filesystem(self.protocol)
|
||||
|
||||
def __repr__(self):
|
||||
return f"DataLocator(protocol={self.protocol}, cname={self.cname}, "
|
||||
f"path={self.path}, uri_or_path={self.uri_or_path})"
|
||||
return (
|
||||
f"DataLocator(protocol={self.protocol}, cname={self.cname}, "
|
||||
f"path={self.path}, uri_or_path={self.uri_or_path})"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_protocol_and_path(uri_or_path):
|
||||
@@ -65,6 +67,10 @@ class DataLocator:
|
||||
return protocol, path
|
||||
return None, uri_or_path
|
||||
|
||||
@staticmethod
|
||||
def strip_protocol(uri_or_path):
|
||||
return DataLocator._get_protocol_and_path(uri_or_path)[1]
|
||||
|
||||
def exists(self):
|
||||
return self.fs.exists(self.cname)
|
||||
|
||||
@@ -72,7 +78,7 @@ class DataLocator:
|
||||
return self.fs.size(self.cname)
|
||||
|
||||
def lastmodtime(self):
|
||||
""" return datetime object representing last modification time, or None if unavailable """
|
||||
"""return datetime object representing last modification time, or None if unavailable"""
|
||||
info = self.fs.info(self.cname)
|
||||
if self.islocal() and info is not None:
|
||||
return datetime.fromtimestamp(info["mtime"])
|
||||
@@ -92,8 +98,8 @@ class DataLocator:
|
||||
def isfile(self):
|
||||
return self.fs.isfile(self.cname)
|
||||
|
||||
def open(self, *args):
|
||||
return self.fs.open(self.uri_or_path, *args)
|
||||
def open(self, *args, **kwargs):
|
||||
return self.fs.open(self.uri_or_path, *args, **kwargs)
|
||||
|
||||
def islocal(self):
|
||||
return self.protocol is None or self.protocol == "file"
|
||||
@@ -107,10 +113,9 @@ class DataLocator:
|
||||
# do our best to create a file with the same.
|
||||
ext = os.path.splitext(self.path)
|
||||
suffix = None if ext[1] == "" else ext[1]
|
||||
with self.open() as src, tempfile.NamedTemporaryFile(prefix="cellxgene_", suffix=suffix, delete=False) as tmp:
|
||||
tmp.write(src.read())
|
||||
with tempfile.NamedTemporaryFile(prefix="cellxgene_", suffix=suffix, delete=False) as tmp:
|
||||
self.fs.download(self.uri_or_path, tmp.name)
|
||||
tmp.close()
|
||||
src.close()
|
||||
tmp_path = tmp.name
|
||||
return LocalFilePath(tmp_path, delete=True)
|
||||
|
||||
|
||||
@@ -98,8 +98,7 @@ def custom_format_warning(msg, *args, **kwargs):
|
||||
|
||||
|
||||
def jsonify_strict(data):
|
||||
return json.dumps(data, cls=StrictJSONEncoder, allow_nan=False)
|
||||
|
||||
return StrictJSONEncoder().encode(data)
|
||||
|
||||
def import_plugins(plugin_module):
|
||||
"""
|
||||
|
||||
@@ -174,10 +174,14 @@ class AnndataAdaptor(DataAdaptor):
|
||||
except MemoryError:
|
||||
raise DatasetAccessError("Out of memory - file is too large for available memory.")
|
||||
except Exception:
|
||||
raise DatasetAccessError(
|
||||
import traceback
|
||||
message = (
|
||||
"File not found or is inaccessible. File must be an .h5ad object. "
|
||||
"Please check your input and try again."
|
||||
)
|
||||
)
|
||||
if self.server_config.app__verbose:
|
||||
message += f"\n{traceback.format_exc()}"
|
||||
raise DatasetAccessError(message)
|
||||
|
||||
def _validate_and_initialize(self):
|
||||
if anndata_version_is_pre_070():
|
||||
@@ -236,6 +240,14 @@ class AnndataAdaptor(DataAdaptor):
|
||||
warnings.warn(
|
||||
f"Anndata data matrix is in {self.data.X.dtype} format not float32. " f"Precision may be truncated."
|
||||
)
|
||||
if self.data.X.dtype < np.float32:
|
||||
if self.data.isbacked:
|
||||
raise DatasetAccessError(f"Data matrix in {self.data.X.dtype} format is not supported in backed mode."
|
||||
" Please reload without --backed, or convert matrix to float32")
|
||||
warnings.warn(
|
||||
f"Anndata data matrix is in unsupported {self.data.X.dtype} format -- will be cast to float32"
|
||||
)
|
||||
self.data.X = self.data.X.astype(np.float32)
|
||||
for ax in Axis:
|
||||
curr_axis = getattr(self.data, str(ax))
|
||||
for ann in curr_axis:
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
mlflow
|
||||
scanpy
|
||||
@@ -2,9 +2,9 @@ black
|
||||
bumpversion>=0.5
|
||||
codecov>=2.0.15
|
||||
parameterized>=0.7.0
|
||||
psycopg2-binary>=2.8.5
|
||||
pytest>=3.6.3
|
||||
python-jose>=3.2.0
|
||||
twine>=1.12.1
|
||||
-r requirements.txt
|
||||
-r requirements-prepare.txt
|
||||
-r requirements-annotate.txt
|
||||
|
||||
@@ -15,7 +15,7 @@ fsspec>=0.4.4,<0.8.0
|
||||
gunicorn>=20.0.4
|
||||
h5py>=3.0.0
|
||||
numba>=0.51.2
|
||||
numpy>=1.17.5
|
||||
numpy>=1.17.5,<=1.22
|
||||
packaging>=20.0
|
||||
pandas>=1.0,!=1.1 # pandas 1.1 breaks tests, https://github.com/pandas-dev/pandas/issues/35446
|
||||
PyYAML>=5.4 # CVE-2020-14343
|
||||
|
||||
@@ -9,9 +9,12 @@ with open("server/requirements.txt") as fh:
|
||||
with open("server/requirements-prepare.txt") as fh:
|
||||
requirements_prepare = fh.read().splitlines()
|
||||
|
||||
with open("server/requirements-annotate.txt") as fh:
|
||||
requirements_annotate = fh.read().splitlines()
|
||||
|
||||
setup(
|
||||
name="cellxgene",
|
||||
version="1.0.0",
|
||||
version="1.1.1",
|
||||
packages=find_packages(),
|
||||
url="https://github.com/chanzuckerberg/cellxgene",
|
||||
license="MIT",
|
||||
@@ -40,5 +43,5 @@ setup(
|
||||
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
||||
],
|
||||
entry_points={"console_scripts": ["cellxgene = server.cli.cli:cli"]},
|
||||
extras_require=dict(prepare=requirements_prepare),
|
||||
extras_require=dict(prepare=requirements_prepare, annotate=requirements_annotate),
|
||||
)
|
||||
|
||||
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
from .mlflow_model_fixture import FakeModel
|
||||
|
||||
|
||||
def _load_pyfunc(data_path):
|
||||
return FakeModel()
|
||||
@@ -0,0 +1,11 @@
|
||||
import mlflow
|
||||
|
||||
|
||||
class FakeModel(mlflow.pyfunc.PythonModel):
|
||||
def __init__(self, input_to_output: dict = {}):
|
||||
self.input_to_output = input_to_output
|
||||
|
||||
def predict(self, model_input) -> None:
|
||||
# this stdout output is useful for validating the input in a test, noting that this model will be invoked in a
|
||||
# subprocess, so stdout is one means of communicating information back to the test code
|
||||
print(f"__MODEL_INPUT__={model_input.iloc[0][0]}")
|
||||
@@ -0,0 +1,130 @@
|
||||
import os
|
||||
import shutil
|
||||
import unittest
|
||||
from tempfile import mkstemp, TemporaryDirectory, NamedTemporaryFile
|
||||
|
||||
import mlflow
|
||||
from click.testing import CliRunner
|
||||
|
||||
from server.cli.annotate import annotate
|
||||
from test.unit.cli.fixtures.mlflow_model_fixture import FakeModel
|
||||
|
||||
|
||||
def write_model(model) -> str:
|
||||
with TemporaryDirectory() as mlflow_model_dir:
|
||||
fixtures_path = os.path.join(os.path.dirname(__file__), "fixtures")
|
||||
mlflow.pyfunc.save_model(mlflow_model_dir, loader_module="fixtures", code_path=[fixtures_path])
|
||||
return shutil.make_archive(mkstemp()[1], "zip", mlflow_model_dir)
|
||||
|
||||
|
||||
class TestCliAnnotate(unittest.TestCase):
|
||||
def test__annotate__loads_and_runs(self):
|
||||
"""
|
||||
Invokes the `annotate` subcommand of cellxgene CLI, using a CliRunner() programmatic invocation.
|
||||
|
||||
This tests the happy path case:
|
||||
1) Command line options are parsed;
|
||||
2) An MLflow model zip archive can be read in (from local disk), unpacked, and invoked;
|
||||
3) The correct options are passed to the MLflow model.
|
||||
4) The annotate subcommand exits successfully.
|
||||
|
||||
This does not verify model output or predictions (it's a fake MLflow model, after all); it's up to the real model
|
||||
to output its predictions as it wants, but this is specific to the model and so not tested here.
|
||||
|
||||
The CliRunner() invokes the subcommand in a subprocess, and the annotate subcommand itself invokes the MLflow
|
||||
model in yet another subprocess. So while this test can help determine if everything is working, it is not a
|
||||
simple matter to debug in the case of a failure. However, the stdout/stderr of the MLflow process is captured
|
||||
by the CliRunner() subprocess, so errors can be inspected in result.stdout when debugging this test. Hope this
|
||||
helps!
|
||||
"""
|
||||
|
||||
_, query_dataset_file_path = mkstemp()
|
||||
model_file_path = write_model(FakeModel())
|
||||
|
||||
result = CliRunner().invoke(
|
||||
annotate,
|
||||
[
|
||||
query_dataset_file_path,
|
||||
"--model-url",
|
||||
model_file_path,
|
||||
"--output-h5ad-file",
|
||||
f"{query_dataset_file_path}.output",
|
||||
# avoid having mflow create conda env or virtualenv when in test env;
|
||||
# this avoids making pip remote requests and is also faster
|
||||
"--mlflow-env-manager",
|
||||
"local",
|
||||
],
|
||||
)
|
||||
|
||||
# to help debugging, show the output from the CliRunner and MLflow stdout
|
||||
if result.exit_code:
|
||||
print(result.stdout)
|
||||
|
||||
self.assertEqual(0, result.exit_code, "runs successfully")
|
||||
|
||||
# The FakeModel will print it inputs to stdout, as "__MODEL_INPUT__={...}", allowing us to assert that it received valid inputs.
|
||||
self.assertIn(
|
||||
"__MODEL_INPUT__={"
|
||||
f'"query_dataset_h5ad_path": "{query_dataset_file_path}", '
|
||||
f'"output_h5ad_path": "{query_dataset_file_path}.output", '
|
||||
'"annotation_prefix": "cxg_cell_type", "classifier": "default", '
|
||||
'"organism": "Homo sapiens", "use_gpu": true}',
|
||||
result.stdout,
|
||||
"inputs passed correctly",
|
||||
)
|
||||
self.assertIn(
|
||||
f"Wrote annotations to {query_dataset_file_path}.output",
|
||||
result.stdout,
|
||||
"success message is correct",
|
||||
)
|
||||
|
||||
def test__annotate__requires_overwrite_option_when_output_file_exists(self):
|
||||
|
||||
with NamedTemporaryFile() as input_h5ad, NamedTemporaryFile() as existing_file:
|
||||
required_options = [input_h5ad.name, "--output-h5ad-file", existing_file.name, "--model-url", "some_url"]
|
||||
result = CliRunner().invoke(
|
||||
annotate,
|
||||
required_options + [],
|
||||
)
|
||||
|
||||
self.assertNotEqual(0, result.exit_code, "aborts with non-success code")
|
||||
self.assertIn(
|
||||
"try using the flag --overwrite",
|
||||
result.stdout,
|
||||
"error message displayed",
|
||||
)
|
||||
|
||||
def test__annotate__overwrite_option_allows_overwrite_of_existing_output_file(self):
|
||||
model_file_path = write_model(FakeModel())
|
||||
|
||||
with NamedTemporaryFile() as existing_file:
|
||||
required_options = [
|
||||
existing_file.name,
|
||||
"--output-h5ad-file",
|
||||
existing_file.name,
|
||||
"--overwrite",
|
||||
"--model-url",
|
||||
model_file_path,
|
||||
]
|
||||
result = CliRunner().invoke(
|
||||
annotate,
|
||||
required_options + [],
|
||||
)
|
||||
|
||||
print(result.stdout)
|
||||
self.assertNotEqual(1, result.exit_code, "aborts with non-success code")
|
||||
self.assertIn(
|
||||
f"Wrote annotations to {existing_file.name}",
|
||||
result.stdout,
|
||||
"success message is correct on output file overwrite",
|
||||
)
|
||||
|
||||
|
||||
# TODO:
|
||||
# Test annotate cli args more comprehensively
|
||||
# Test server.cli.annotate._validate_options
|
||||
# Test model caching feature works
|
||||
# Test model loading from s3 works (maybe w/just a real model)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -290,6 +290,7 @@ class EndPoints(object):
|
||||
result_data = result.json()
|
||||
self.assertEqual(result_data, pbmc3k_colors)
|
||||
|
||||
@unittest.skip('needs fix: https://github.com/chanzuckerberg/cellxgene/issues/2542')
|
||||
def test_static(self):
|
||||
endpoint = "static"
|
||||
file = "assets/favicon.ico"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
import unittest
|
||||
|
||||
@@ -37,6 +36,7 @@ Test the anndata adaptor using the pbmc3k data set.
|
||||
(f"{FIXTURES_ROOT}/pbmc3k-CSC-gz.h5ad", True, "normal"),
|
||||
(f"{FIXTURES_ROOT}/pbmc3k-CSR-gz.h5ad", True, "normal"),
|
||||
(f"{FIXTURES_ROOT}/pbmc3k_64.h5ad", False, "auto"), # 64 bit conversion tests
|
||||
(f"{FIXTURES_ROOT}/pbmc3k_16.h5ad", False, "auto"), # 16 bit conversion tests
|
||||
],
|
||||
)
|
||||
class AdaptorTest(unittest.TestCase):
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import unittest
|
||||
|
||||
from parameterized import parameterized_class
|
||||
|
||||
from server.common.errors import DatasetAccessError
|
||||
from test import FIXTURES_ROOT
|
||||
from test.unit import app_config
|
||||
|
||||
|
||||
@parameterized_class(
|
||||
("data_locator", "backed", "X_approximate_distribution"),
|
||||
[
|
||||
(f"{FIXTURES_ROOT}/pbmc3k_16.h5ad", True, "auto"), # 16 bit conversion tests
|
||||
],
|
||||
)
|
||||
class AdaptorLoadErrorTest(unittest.TestCase):
|
||||
def test_float16_backed_raises_err(self):
|
||||
with self.assertRaises(DatasetAccessError):
|
||||
config = app_config(
|
||||
self.data_locator,
|
||||
backed=self.backed,
|
||||
extra_dataset_config=dict(X_approximate_distribution=self.X_approximate_distribution),
|
||||
)
|
||||
@@ -54,5 +54,5 @@ class TestJsonifyStrict(unittest.TestCase):
|
||||
# the actual test!
|
||||
self.assertEqual(
|
||||
jsonify_strict(values),
|
||||
'{"floating": [100.0, 101.0, 102.0], "integer": [0, 1, 2, 3, 4, 5, 6, 7]}',
|
||||
'{"integer": [0, 1, 2, 3, 4, 5, 6, 7], "floating": [100.0, 101.0, 102.0]}',
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user