Compare commits

...
Author SHA1 Message Date
Alok Saldanha 05a95c2ca9 Prepare 0.4.0 release 2024-03-10 09:33:50 -04:00
Alok Saldanha 5d29153544 #87 remove version pins for markupsafe, flask and werkzeug
also remove dependency on flask-api
2024-03-10 09:31:39 -04:00
Alok Saldanha 6a2bc409db Prepare for 0.3.12 release 2024-03-03 07:45:47 -05:00
Alok Saldanha fa72481b66 Merge remote-tracking branch 'origin/dependabot/pip/werkzeug-2.3.8' 2024-03-03 07:33:03 -05:00
Alok Saldanha 3d0166904b Merge pull request #90 from Novartis/dependabot/pip/flask-2.2.5
Bump flask from 2.2.2 to 2.2.5
2024-03-03 07:31:42 -05:00
Alok Saldanha 4e63ff95a8 #87 blacken 2024-03-02 12:25:21 -05:00
Alok Saldanha c1111e2cb4 #87 patch enable annotations 2024-03-02 12:23:11 -05:00
Alok Saldanha c4b9084286 #87 Fix test 2024-03-02 12:08:57 -05:00
dependabot[bot] 0282da03c8 Bump werkzeug from 2.3.0 to 2.3.8
Bumps [werkzeug](https://github.com/pallets/werkzeug) from 2.3.0 to 2.3.8.
- [Release notes](https://github.com/pallets/werkzeug/releases)
- [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/werkzeug/compare/2.3.0...2.3.8)

---
updated-dependencies:
- dependency-name: werkzeug
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-02 16:56:15 +00:00
dependabot[bot] bec74bec45 Bump flask from 2.2.2 to 2.2.5
Bumps [flask](https://github.com/pallets/flask) from 2.2.2 to 2.2.5.
- [Release notes](https://github.com/pallets/flask/releases)
- [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/flask/compare/2.2.2...2.2.5)

---
updated-dependencies:
- dependency-name: flask
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-02 16:56:15 +00:00
Alok Saldanha 35c8e8180c #87 temporarily pin versions 2024-03-02 11:55:07 -05:00
Alok Saldanha 0000a60eb0 #73 hide annotation links when disabled 2024-03-02 11:51:20 -05:00
Alok Saldanha 1e02e0abb8 Merge pull request #74 from Novartis/73_reorder_filecrawl
#73 moved new annotation link to front
2024-03-02 11:44:49 -05:00
Alok Saldanha 66cca86b51 Merge remote-tracking branch 'ghall/just_gene_sets' 2024-03-02 11:09:00 -05:00
Alok Saldanha f046e7c5d2 Merge pull request #88 from Mye-InfoBank/master
Fix dockerfile installation problems
2024-02-24 09:22:49 -05:00
Nico Trummer a8f6e45f34 Implement pip upgrade to Dockerfile 2024-02-21 09:47:46 +01:00
george-hall-ucl 19caa6cb80 Sorry -- forgot to lint 2023-08-08 16:04:03 +01:00
george-hall-ucl 56bd079024 Save gene sets without cell annotations
This fixes a bug whereby new gene_sets csv files created without
accompanying cell-level annotations could not be detected by the
filecrawler.
2023-08-08 15:49:57 +01:00
Alok Saldanha 9d10932b06 prepare for 0.3.11 release 2023-07-09 19:27:32 -04:00
Alok Saldanha c7c156b4cf Merge pull request #77 from aeisenbarth/filter-empty-folders
Filter directories without h5ad files
2023-07-09 07:04:06 -06:00
Alok Saldanha 624d1f8567 #78 Revert "Rename argument "filter" to "subpath""
This reverts commit fdd6cca297.
2023-07-09 08:25:27 -04:00
Alok Saldanha 79c3f588b6 Merge pull request #80 from Novartis/79_add_docker_example
79 add docker example
2023-07-09 06:05:50 -06:00
Alok Saldanha d8fd07572c Merge pull request #83 from Novartis/81_gene_set_support
gene set support
2023-07-09 06:03:33 -06:00
Alok Saldanha 64d636a1c5 #81 added unit test for gene sets 2023-07-09 07:46:01 -04:00
Alok Saldanha 7b314d4457 #81 switch to latest ubuntu 2023-07-06 17:15:00 -06:00
Alok Saldanha 2754bc1ef1 #81 Combined GATEWAY_ENABLE_ANNOTATIONS and GATEWAY_ENABLE_GENE_SETS flags 2023-07-06 08:53:19 -06:00
Alok Saldanha 5a650334df #81 moved gene set check into fileitem_source 2023-07-06 08:18:50 -06:00
george-hall-ucl 81c8ce4219 #81 Add support for gene sets
This adds the flag `GATEWAY_ENABLE_GENE_SETS` to enable support for gene
sets.  To simplify implementation, activating this flag also activates
`GATEWAY_ENABLE_ANNOTATIONS`.  The gene sets are saved in a file that
has the same name as the annotations `csv` but with `_gene_sets`
appended to the file name (before the extension).  This file is hidden
in filecrawler, and the gene sets are loaded when the associated
annotations file is loaded.

If the annotations file is missing, then an Exception is raised.

I have updated one unit test to make it expect
`--disable-gene-sets-save` in the default case (i.e. if
`GATEWAY_ENABLE_ANNOTATIONS = 0`).  All units tests pass.

I have updated the README to document `GATEWAY_ENABLE_GENE_SETS`.
2023-07-06 07:59:12 -06:00
Andreas Eisenbarth 6607b15085 Exclude directories having no h5ad files 2022-10-12 17:31:22 +02:00
Andreas Eisenbarth 0e92b73347 Add test case for dirs without h5ad 2022-10-12 17:31:22 +02:00
Andreas Eisenbarth 88b9b815c0 Adjust test case for dirs with h5ad 2022-10-12 17:31:22 +02:00
Andreas Eisenbarth 6ef82b36f1 For running individual tests, make sure flask_util.view_url is callable 2022-10-12 16:36:46 +02:00
Andreas Eisenbarth fdd6cca297 Rename argument "filter" to "subpath" 2022-10-12 13:40:39 +02:00
Alok Saldanha a00403c60e #73 moved new annotation link to front 2022-08-21 08:15:09 -04:00
17 changed files with 250 additions and 52 deletions
+2 -3
View File
@@ -6,7 +6,7 @@ on: [push, pull_request]
jobs: jobs:
black: black:
runs-on: ubuntu-18.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
name: Checkout repository name: Checkout repository
@@ -25,7 +25,7 @@ jobs:
black . --check black . --check
# This job is copied over from `deploy.yaml` # This job is copied over from `deploy.yaml`
run-tests: run-tests:
runs-on: ubuntu-18.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -39,7 +39,6 @@ jobs:
conda env create -f environment.yml conda env create -f environment.yml
eval "$(conda shell.bash hook)" eval "$(conda shell.bash hook)"
conda activate cellxgene-gateway conda activate cellxgene-gateway
pip install markupsafe==2.0.1 # temporary workaround for jinja2-2.11.3 calling soft_unicode in markupsafe
python setup.py install python setup.py install
- name: Run tests - name: Run tests
+19
View File
@@ -1,3 +1,22 @@
# 0.4.0
* Removed dependency on flask-api
* Updated dependencies (python 3.11, numpy, unpinned flask, werkzeug)
# 0.3.12
* #81 List gene set annotations when cell annotations not present
* #86 Upgrade pip within docker image
* #73 Moved new link to front
* #87 Temporarily pin versions of werkzeug and flask
# 0.3.11
* #81 added support for gene sets
* #79 added example for cellxgene-gateway customized docker image
* #78 prune directories that do not contain h5ad files
# 0.3.10 # 0.3.10
* #65 Added GATEWAY_EXPIRE_SECONDS to set how long cellxgene servers can remain idle before being terminated. * #65 Added GATEWAY_EXPIRE_SECONDS to set how long cellxgene servers can remain idle before being terminated.
+3 -2
View File
@@ -1,6 +1,7 @@
FROM python:3.9 FROM python:3.11
RUN pip install cellxgene-gateway 'MarkupSafe<2.1' RUN pip install --upgrade pip
RUN pip install "cellxgene-gateway>=0.4"
ENV CELLXGENE_DATA=/cellxgene-data ENV CELLXGENE_DATA=/cellxgene-data
ENV CELLXGENE_LOCATION=/usr/local/bin/cellxgene ENV CELLXGENE_LOCATION=/usr/local/bin/cellxgene
+31 -2
View File
@@ -75,7 +75,7 @@ Optional environment variables:
* `GATEWAY_PORT` - local port that the gateway should bind to, defaults to 5005 * `GATEWAY_PORT` - local port that the gateway should bind to, defaults to 5005
* `GATEWAY_EXPIRE_SECONDS` - time in seconds that a cellxgene process will remain idle before being terminated. Defaults to 3600 (one hour) * `GATEWAY_EXPIRE_SECONDS` - time in seconds that a cellxgene process will remain idle before being terminated. Defaults to 3600 (one hour)
* `GATEWAY_EXTRA_SCRIPTS` - JSON array of script paths, will be embedded into each page and forwarded with `--scripts` to cellxgene server * `GATEWAY_EXTRA_SCRIPTS` - JSON array of script paths, will be embedded into each page and forwarded with `--scripts` to cellxgene server
* `GATEWAY_ENABLE_ANNOTATIONS` - Set to `true` or to `1` to enable cellxgene annotations. * `GATEWAY_ENABLE_ANNOTATIONS` - Set to `true` or to `1` to enable cellxgene annotations and gene sets.
* `GATEWAY_ENABLE_BACKED_MODE` - Set to `true` or to `1` to load AnnData in file-backed mode. This saves memory and speeds up launch time but may reduce overall performance. * `GATEWAY_ENABLE_BACKED_MODE` - Set to `true` or to `1` to load AnnData in file-backed mode. This saves memory and speeds up launch time but may reduce overall performance.
* `GATEWAY_LOG_LEVEL` - default is `INFO`. set to `DEBUG` to increase logging and to `WARNING` to decrease logging. * `GATEWAY_LOG_LEVEL` - default is `INFO`. set to `DEBUG` to increase logging and to `WARNING` to decrease logging.
* `S3_ENABLE_LISTINGS_CACHE` - Set to `true` or to `1` to cache listings of S3 folders for performance. If the cache becomes stale, set `filecrawl.html?refresh=true` query parameter to refresh the cache. * `S3_ENABLE_LISTINGS_CACHE` - Set to `true` or to `1` to cache listings of S3 folders for performance. If the cache becomes stale, set `filecrawl.html?refresh=true` query parameter to refresh the cache.
@@ -110,7 +110,7 @@ Additional environment variables can be provided with the `-e` parameter:
```bash ```bash
docker run -it --rm \ docker run -it --rm \
-v <local_data_dir>:/cellxgene-data \ -v ../cellxgene_data:/cellxgene-data \
-e GATEWAY_PORT=8080 \ -e GATEWAY_PORT=8080 \
-p 8080:8080 \ -p 8080:8080 \
cellxgene-gateway cellxgene-gateway
@@ -191,6 +191,35 @@ black .
If you need help for any reason, please make a github ticket. One of the contributors should help you out. If you need help for any reason, please make a github ticket. One of the contributors should help you out.
# Releasing New Versions
## How to prepare for release
- Update Changelog.md and version number in __init__.py
- Cut a release on github
- Go to your project homepage on GitHub
- On right side, you will see [Releases](https://github.com/Novartis/cellxgene-gateway/releases) link. Click on it.
- Click on Draft a new release
- Fill in all the details
- Tag version should be the version number of your package release
- Release Title can be anything you want, but we use v0.3.11 (the same as the tag to be created on publish)
- Description should be changelog
- Click Publish release at the bottom of the page
- Now under Releases you can view all of your releases.
- Copy the download link (tar.gz) and save it somewhere
## How to publish to PyPI
Make sure your `.pypirc` is set up for testpypi and pypi index servers.
```bash
rm -rf dist
python setup.py sdist bdist_wheel
python -m twine upload --repository testpypi dist/*
python -m twine upload dist/*
```
# Contributors # Contributors
* Niket Patel - https://github.com/NiketPatel9 * Niket Patel - https://github.com/NiketPatel9
+1 -1
View File
@@ -7,4 +7,4 @@
# OR CONDITIONS OF ANY KIND, either express or implied. See the License for # OR CONDITIONS OF ANY KIND, either express or implied. See the License for
# the specific language governing permissions and limitations under the License. # the specific language governing permissions and limitations under the License.
__version__ = "0.3.10" __version__ = "0.4.0"
+3 -4
View File
@@ -8,11 +8,10 @@
# the specific language governing permissions and limitations under the License. # the specific language governing permissions and limitations under the License.
import time import time
from http import HTTPStatus
from threading import Thread from threading import Thread
from typing import List from typing import List
from flask_api import status
from cellxgene_gateway import env from cellxgene_gateway import env
from cellxgene_gateway.cache_entry import CacheEntry, CacheEntryStatus from cellxgene_gateway.cache_entry import CacheEntry, CacheEntryStatus
from cellxgene_gateway.cache_key import CacheKey from cellxgene_gateway.cache_key import CacheKey
@@ -53,7 +52,7 @@ class BackendCache:
return matches[0] return matches[0]
else: else:
raise CellxgeneException( raise CellxgeneException(
status.HTTP_500_INTERNAL_SERVER_ERROR, HTTPStatus.INTERNAL_SERVER_ERROR,
"Found " + str(len(matches)) + " for " + path, "Found " + str(len(matches)) + " for " + path,
) )
@@ -71,7 +70,7 @@ class BackendCache:
return matches[0] return matches[0]
else: else:
raise CellxgeneException( raise CellxgeneException(
status.HTTP_500_INTERNAL_SERVER_ERROR, HTTPStatus.INTERNAL_SERVER_ERROR,
"Found " + str(len(matches)) + " for " + key.dataset, "Found " + str(len(matches)) + " for " + key.dataset,
) )
-1
View File
@@ -58,7 +58,6 @@ class CacheEntry:
@classmethod @classmethod
def for_key(cls, key, port): def for_key(cls, key, port):
return cls( return cls(
None, None,
key, key,
-2
View File
@@ -9,8 +9,6 @@
import os import os
from flask_api import status
from cellxgene_gateway import env from cellxgene_gateway import env
from cellxgene_gateway.cellxgene_exception import CellxgeneException from cellxgene_gateway.cellxgene_exception import CellxgeneException
+13 -12
View File
@@ -7,31 +7,32 @@
# OR CONDITIONS OF ANY KIND, either express or implied. See the License for # OR CONDITIONS OF ANY KIND, either express or implied. See the License for
# the specific language governing permissions and limitations under the License. # the specific language governing permissions and limitations under the License.
import os import html
import urllib.parse import urllib.parse
from cellxgene_gateway import env, flask_util from cellxgene_gateway import flask_util
from cellxgene_gateway.cache_key import CacheKey from cellxgene_gateway.cache_key import CacheKey
from cellxgene_gateway.dir_util import annotations_suffix, make_annotations, make_h5ad from cellxgene_gateway.dir_util import annotations_suffix, make_annotations, make_h5ad
from cellxgene_gateway.env import enable_annotations
def render_annotations(item, item_source): def render_annotations(item, item_source):
if not enable_annotations:
return ""
url = flask_util.view_url( url = flask_util.view_url(
item_source.get_annotations_subpath(item), item_source.name item_source.get_annotations_subpath(item), item_source.name
) )
new_annotation = f"<a class='new' href='{url}'>new</a>" new_annotation = [f"<a class='new' href='{url}'>new</a>"]
annotations = ( annotations = (
", ".join( [
[ f"<a href='{CacheKey(item, item_source, a).view_url}/'>{html.escape(a.name)}</a>"
f"<a href='{CacheKey(item, item_source, a).view_url}/'>{a.name}</a>" for a in item.annotations
for a in item.annotations ]
]
)
+ ", "
if item.annotations if item.annotations
else "" else []
) )
return " | annotations: " + annotations + new_annotation return "| annotations: " + ", ".join(new_annotation + annotations)
def render_item(item, item_source): def render_item(item, item_source):
-2
View File
@@ -80,7 +80,6 @@ cache = BackendCache()
@app.errorhandler(CellxgeneException) @app.errorhandler(CellxgeneException)
def handle_invalid_usage(error): def handle_invalid_usage(error):
message = f"{error.http_status} Error : {error.message}" message = f"{error.http_status} Error : {error.message}"
return ( return (
@@ -95,7 +94,6 @@ def handle_invalid_usage(error):
@app.errorhandler(ProcessException) @app.errorhandler(ProcessException)
def handle_invalid_process(error): def handle_invalid_process(error):
message = [] message = []
message.append(error.message) message.append(error.message)
@@ -24,17 +24,22 @@ class FileItemSource(ItemSource):
h5ad_suffix=dir_util.h5ad_suffix, h5ad_suffix=dir_util.h5ad_suffix,
annotation_dir_suffix=dir_util.annotations_suffix, annotation_dir_suffix=dir_util.annotations_suffix,
annotation_file_suffix=".csv", annotation_file_suffix=".csv",
gene_set_file_suffix="_gene_sets.csv",
): ):
self._name = name self._name = name
self.base_path = base_path self.base_path = base_path
self.h5ad_suffix = h5ad_suffix self.h5ad_suffix = h5ad_suffix
self.annotation_dir_suffix = annotation_dir_suffix self.annotation_dir_suffix = annotation_dir_suffix
self.annotation_file_suffix = annotation_file_suffix self.annotation_file_suffix = annotation_file_suffix
self.gene_set_file_suffix = gene_set_file_suffix
@property @property
def name(self): def name(self):
return self._name or f"Files:{self.base_path}" return self._name or f"Files:{self.base_path}"
def is_gene_set(self, path: str) -> bool:
return path.endswith(self.gene_set_file_suffix)
def is_h5ad_file(self, path: str) -> bool: def is_h5ad_file(self, path: str) -> bool:
return path.endswith(self.h5ad_suffix) and os.path.isfile(path) return path.endswith(self.h5ad_suffix) and os.path.isfile(path)
@@ -63,7 +68,7 @@ class FileItemSource(ItemSource):
return item_tree return item_tree
def scan_directory(self, subpath="") -> dict: def scan_directory(self, subpath: str = "") -> ItemTree:
base_path = os.path.join(self.base_path, subpath) base_path = os.path.join(self.base_path, subpath)
if not os.path.exists(base_path): if not os.path.exists(base_path):
@@ -100,6 +105,11 @@ class FileItemSource(ItemSource):
branches = [ branches = [
self.scan_directory(os.path.join(subpath, subdir)) for subdir in subdirs self.scan_directory(os.path.join(subpath, subdir)) for subdir in subdirs
] ]
# Exclude branches without files as leaves. Since traversal is applied pre-order,
# branch.branches has already been processed and we don't need to check deeper nesting.
branches = [
branch for branch in branches if branch.items or branch.branches
]
return ItemTree(subpath, items, branches) return ItemTree(subpath, items, branches)
@@ -176,11 +186,29 @@ class FileItemSource(ItemSource):
annotations_subpath = self.get_annotations_subpath(item) annotations_subpath = self.get_annotations_subpath(item)
annotations_fullpath = self.full_path(annotations_subpath) annotations_fullpath = self.full_path(annotations_subpath)
if os.path.isdir(annotations_fullpath): if os.path.isdir(annotations_fullpath):
return [ sorted_files = sorted(os.listdir(annotations_fullpath))
annotation_files = [
self.make_fileitem_from_path(annotation, annotations_subpath, True) self.make_fileitem_from_path(annotation, annotations_subpath, True)
for annotation in sorted(os.listdir(annotations_fullpath)) for annotation in sorted_files
if annotation.endswith(self.annotation_file_suffix) if annotation.endswith(self.annotation_file_suffix)
and not self.is_gene_set(annotation)
and os.path.isfile(os.path.join(annotations_fullpath, annotation)) and os.path.isfile(os.path.join(annotations_fullpath, annotation))
] ]
# Catch gene sets without accompanying [annotations].csv
gene_sets_files = [
self.make_fileitem_from_path(
annotation[: -len(self.gene_set_file_suffix)] + ".csv",
annotations_subpath,
True,
)
for annotation in sorted_files
if self.is_gene_set(annotation)
and annotation[: -len(self.gene_set_file_suffix)]
not in [a.name for a in annotation_files]
and os.path.isfile(os.path.join(annotations_fullpath, annotation))
]
return sorted(annotation_files + gene_sets_files, key=lambda x: x.name)
else: else:
return None return None
@@ -116,6 +116,9 @@ class S3ItemSource(ItemSource):
branches = None branches = None
if len(subdir_keys) > 0: if len(subdir_keys) > 0:
branches = [self.scan_directory(key) for key in subdir_keys] branches = [self.scan_directory(key) for key in subdir_keys]
branches = [
branch for branch in branches if branch.items or branch.branches
]
return ItemTree(directory_key, items, branches) return ItemTree(directory_key, items, branches)
+6 -4
View File
@@ -9,8 +9,7 @@
import logging import logging
import subprocess import subprocess
from http import HTTPStatus
from flask_api import status
from cellxgene_gateway.cache_entry import CacheEntryStatus from cellxgene_gateway.cache_entry import CacheEntryStatus
from cellxgene_gateway.dir_util import make_annotations from cellxgene_gateway.dir_util import make_annotations
@@ -30,8 +29,11 @@ class SubprocessBackend:
extra_args = f" --annotations-dir {make_annotations(file_path)}" extra_args = f" --annotations-dir {make_annotations(file_path)}"
else: else:
extra_args = f" --annotations-file {annotation_file_path}" extra_args = f" --annotations-file {annotation_file_path}"
gene_sets_file_path = annotation_file_path[:-4] + "_gene_sets.csv"
extra_args += f" --gene-sets-file {gene_sets_file_path}"
else: else:
extra_args = " --disable-annotations" extra_args = " --disable-annotations"
extra_args += " --disable-gene-sets-save"
if enable_backed_mode: if enable_backed_mode:
extra_args += " --backed" extra_args += " --backed"
if not cellxgene_args is None: if not cellxgene_args is None:
@@ -73,10 +75,10 @@ class SubprocessBackend:
or "Could not open file" in stderr or "Could not open file" in stderr
): ):
message = "File was invalid." message = "File was invalid."
http_status = status.HTTP_400_BAD_REQUEST http_status = HTTPStatus.BAD_REQUEST
else: else:
message = "Cellxgene failed to launch dataset." message = "Cellxgene failed to launch dataset."
http_status = status.HTTP_500_INTERNAL_SERVER_ERROR http_status = HTTPStatus.INTERNAL_SERVER_ERROR
cache_entry.status = CacheEntryStatus.error cache_entry.status = CacheEntryStatus.error
cache_entry.set_error(message, stderr, http_status) cache_entry.set_error(message, stderr, http_status)
+1 -2
View File
@@ -2,7 +2,7 @@ name: cellxgene-gateway
channels: channels:
- conda-forge - conda-forge
dependencies: dependencies:
- python=3.9 - python=3.11
- requests - requests
- flask - flask
- psutil - psutil
@@ -13,6 +13,5 @@ dependencies:
- pip - pip
- pip: - pip:
- pre_commit - pre_commit
- flask-api
- werkzeug - werkzeug
- cellxgene - cellxgene
-1
View File
@@ -1,6 +1,5 @@
cellxgene cellxgene
flask flask
flask-api
werkzeug werkzeug
psutil psutil
requests requests
+100 -11
View File
@@ -1,5 +1,6 @@
import unittest import unittest
from unittest.mock import MagicMock, patch from collections import defaultdict
from unittest.mock import patch
from cellxgene_gateway.filecrawl import ( from cellxgene_gateway.filecrawl import (
render_item, render_item,
@@ -13,26 +14,79 @@ from cellxgene_gateway.items.item import ItemTree, ItemType
source = FileItemSource("/tmp") source = FileItemSource("/tmp")
def make_entry(subpath="somepath", annotations=None):
return FileItem(
subpath=subpath,
name="entry",
ext=".h5ad",
type=ItemType.h5ad,
annotations=annotations,
)
class TestRenderEntry(unittest.TestCase): class TestRenderEntry(unittest.TestCase):
def test_GIVEN_path_both_slash_THEN_view_has_single_slash(self): def test_GIVEN_path_both_slash_THEN_view_has_single_slash(self):
entry = FileItem(subpath="/somepath/", name="entry", type=ItemType.h5ad) entry = make_entry(subpath="/somepath/")
rendered = render_item(entry, source) rendered = render_item(entry, source)
self.assertIn("view/somepath/entry/'", rendered) self.assertIn("view/somepath/entry.h5ad/'", rendered)
def test_GIVEN_path_starts_slash_THEN_view_has_single_slash(self): def test_GIVEN_path_starts_slash_THEN_view_has_single_slash(self):
entry = FileItem(subpath="/somepath", name="entry", type=ItemType.h5ad) entry = make_entry(subpath="/somepath")
rendered = render_item(entry, source) rendered = render_item(entry, source)
self.assertIn("view/somepath/entry/'", rendered) self.assertIn("view/somepath/entry.h5ad/'", rendered)
def test_GIVEN_path_ends_slash_THEN_view_has_single_slash(self): def test_GIVEN_path_ends_slash_THEN_view_has_single_slash(self):
entry = FileItem(subpath="somepath/", name="entry", type=ItemType.h5ad) entry = make_entry(subpath="somepath/")
rendered = render_item(entry, source) rendered = render_item(entry, source)
self.assertIn("view/somepath/entry/'", rendered) self.assertIn("view/somepath/entry.h5ad/'", rendered)
def test_GIVEN_path_no_slash_THEN_view_has_single_slash(self): def test_GIVEN_path_no_slash_THEN_view_has_single_slash(self):
entry = FileItem(subpath="somepath", name="entry", type=ItemType.h5ad) entry = make_entry(subpath="somepath")
rendered = render_item(entry, source) rendered = render_item(entry, source)
self.assertIn("view/somepath/entry/'", rendered) self.assertIn("view/somepath/entry.h5ad/'", rendered)
class TestRenderAnnotation(unittest.TestCase):
@patch("cellxgene_gateway.filecrawl.enable_annotations", new=True)
def test_GIVEN_no_annotation_THEN_new_alone(self):
entry = make_entry(annotations=None)
rendered = render_item(entry, source)
self.assertIn(
"> | annotations: <a class='new' href='/source/Files:/tmp/view/somepath/entry_annotations'>new</a></li>",
rendered,
)
@patch("cellxgene_gateway.filecrawl.enable_annotations", new=True)
def test_GIVEN_annotation_THEN_new_before(self):
annotation = FileItem(
subpath="somepath/entry_annotations",
name="annot",
ext=".csv",
type=ItemType.annotation,
)
entry = make_entry(annotations=[annotation])
rendered = render_item(entry, source)
self.assertIn(
"> | annotations: <a class='new' href='/source/Files:/tmp/view/somepath/entry_annotations'>new</a>,"
" <a href='/source/Files:/tmp/view/somepath/entry_annotations/annot.csv/'>annot</a></li>",
rendered,
)
@patch("cellxgene_gateway.filecrawl.enable_annotations", new=True)
def test_GIVEN_annotation_THEN_escaped(self):
annotation = FileItem(
subpath="somepath/entry_annotations",
name="hot&cold",
ext=".csv",
type=ItemType.annotation,
)
entry = make_entry(annotations=[annotation])
rendered = render_item(entry, source)
self.assertIn(
"> | annotations: <a class='new' href='/source/Files:/tmp/view/somepath/entry_annotations'>new</a>,"
" <a href='/source/Files:/tmp/view/somepath/entry_annotations/hot&cold.csv/'>hot&amp;cold</a></li>",
rendered,
)
class TestRenderItemSource(unittest.TestCase): class TestRenderItemSource(unittest.TestCase):
@@ -48,12 +102,47 @@ class TestRenderItemSource(unittest.TestCase):
class TestRenderItemTree(unittest.TestCase): class TestRenderItemTree(unittest.TestCase):
def setUp(self):
from cellxgene_gateway.gateway import app
self.app = app
self.app_context = self.app.test_request_context()
self.app_context.push()
@patch("cellxgene_gateway.items.file.fileitem_source.FileItemSource") @patch("cellxgene_gateway.items.file.fileitem_source.FileItemSource")
def test_GIVEN_deep_nested_dirs_THEN_includes_dirs_in_output(self, item_source): def test_GIVEN_deep_nested_dirs_THEN_includes_dirs_in_output(self, item_source):
item_source.name = "FakeSource" item_source.name = "FakeSource"
item_tree = ItemTree("foo/bar/baz", [], []) item_source.get_annotations_subpath = lambda _: "FakeAnnotations"
file_item = FileItem(
subpath="foo/bar/baz", name="file.h5ad", type=ItemType.h5ad
)
item_tree = ItemTree("foo/bar/baz", [file_item], [])
rendered = render_item_tree(item_tree, item_source) rendered = render_item_tree(item_tree, item_source)
self.assertEqual( self.assertEqual(
rendered, rendered,
"<li><a href='/filecrawl/foo/bar/baz?source=FakeSource'>baz</a><ul></ul></li>", "<li><a href='/filecrawl/foo/bar/baz?source=FakeSource'>baz</a><ul>"
"<li> <a href='/source/FakeSource/view/foo/bar/baz/file.h5ad/'>file.h5ad</a>"
" </li></ul></li>",
)
@patch(
"os.listdir",
side_effect=lambda parent: defaultdict(
list, {"tmp": ["foo"], "tmp/foo": ["bar"]}
)[parent],
)
@patch("os.path.exists", return_value=True)
def test_GIVEN_dirs_without_h5ad_THEN_excludes_dirs_in_output(
self, listdir, exists
):
# Directories:
# - tmp
# - foo
# - bar (no h5ad files)
item_source = FileItemSource("tmp", name="local")
item_tree = item_source.list_items("foo")
rendered = render_item_tree(item_tree, item_source)
self.assertEqual(
rendered,
"<li><a href='/filecrawl/foo?source=local'>foo</a><ul></ul></li>",
) )
+37 -2
View File
@@ -1,7 +1,6 @@
import unittest import unittest
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from cellxgene_gateway.backend_cache import BackendCache
from cellxgene_gateway.cache_entry import CacheEntry from cellxgene_gateway.cache_entry import CacheEntry
from cellxgene_gateway.cache_key import CacheKey from cellxgene_gateway.cache_key import CacheKey
from cellxgene_gateway.items.file.fileitem import FileItem from cellxgene_gateway.items.file.fileitem import FileItem
@@ -33,10 +32,46 @@ class TestSubprocessBackend(unittest.TestCase):
backend.launch(cellxgene_loc, scripts, entry) backend.launch(cellxgene_loc, scripts, entry)
popen.assert_called_once_with( popen.assert_called_once_with(
[ [
"yes | /some/cellxgene launch /tmp/czi/pbmc3k.h5ad --port 8000 --host 127.0.0.1 --disable-annotations --scripts http://example.com/script.js --scripts http://example.com/script2.js" "yes | /some/cellxgene launch /tmp/czi/pbmc3k.h5ad --port 8000 --host 127.0.0.1 --disable-annotations --disable-gene-sets-save --scripts http://example.com/script.js --scripts http://example.com/script2.js"
], ],
shell=True, shell=True,
stderr=-1, stderr=-1,
stdout=-1, stdout=-1,
) )
self.assertEqual("An unexpected error", context.exception.stderr) self.assertEqual("An unexpected error", context.exception.stderr)
@patch("subprocess.Popen")
def test_launch_GIVEN_annotations_enabled_THEN_set_flags(self, popen):
subprocess = MagicMock()
subprocess.stdout.readline().decode.return_value = (
"[cellxgene] Type CTRL-C at any time to exit.\n"
)
subprocess.stderr.read().decode.return_value = ""
popen.return_value = subprocess
key = CacheKey(
FileItem("/czi/", name="pbmc3k.h5ad", type=ItemType.h5ad),
FileItemSource("/tmp", "local"),
FileItem(
"/czi/pbmc3k_annotations/", name="foo.csv", type=ItemType.annotation
),
)
entry = CacheEntry.for_key(key, 8000)
import cellxgene_gateway.subprocess_backend
cellxgene_gateway.subprocess_backend.enable_annotations = True
try:
backend = cellxgene_gateway.subprocess_backend.SubprocessBackend()
cellxgene_loc = "/some/cellxgene"
backend.launch(cellxgene_loc, [], entry)
finally:
cellxgene_gateway.subprocess_backend.enable_annotations = False
popen.assert_called_once_with(
[
"yes | /some/cellxgene launch /tmp/czi/pbmc3k.h5ad --port 8000 --host 127.0.0.1 --annotations-file /tmp/czi/pbmc3k_annotations/foo.csv --gene-sets-file /tmp/czi/pbmc3k_annotations/foo_gene_sets.csv"
],
shell=True,
stderr=-1,
stdout=-1,
)