Compare commits

...
9 Commits
6 changed files with 32 additions and 9 deletions
+1
View File
@@ -39,6 +39,7 @@ 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
+1
View File
@@ -76,6 +76,7 @@ Optional environment variables:
* `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.
* `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.
* `S3_ENABLE_LISTINGS_CACHE` - Set to `true` or to `1` to cache listings of S3 folders for performance. Can be overridden by setting `filecrawl.html?refresh=true` query parameter.
If any of the following optional variables are set, [ProxyFix](https://werkzeug.palletsprojects.com/en/1.0.x/middleware/proxy_fix/) will be used. If any of the following optional variables are set, [ProxyFix](https://werkzeug.palletsprojects.com/en/1.0.x/middleware/proxy_fix/) will be used.
* `PROXY_FIX_FOR` - Number of upstream proxies setting X-Forwarded-For * `PROXY_FIX_FOR` - Number of upstream proxies setting X-Forwarded-For
-2
View File
@@ -22,9 +22,7 @@ from flask import (
send_from_directory, send_from_directory,
url_for, url_for,
) )
from flask_api import status
from werkzeug.middleware.proxy_fix import ProxyFix from werkzeug.middleware.proxy_fix import ProxyFix
from werkzeug.utils import secure_filename
from cellxgene_gateway import env, flask_util from cellxgene_gateway import env, flask_util
from cellxgene_gateway.backend_cache import BackendCache from cellxgene_gateway.backend_cache import BackendCache
+22 -4
View File
@@ -7,9 +7,11 @@
# 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.
from os.path import basename, dirname, join import os
from os.path import basename, dirname
from typing import List from typing import List
import flask
import s3fs import s3fs
from cellxgene_gateway import dir_util from cellxgene_gateway import dir_util
@@ -18,6 +20,10 @@ from cellxgene_gateway.items.item_source import ItemSource, LookupResult
from cellxgene_gateway.items.s3.s3item import S3Item from cellxgene_gateway.items.s3.s3item import S3Item
def truthy(val: str):
return val.lower() in ["true", "1"]
class S3ItemSource(ItemSource): class S3ItemSource(ItemSource):
def __init__( def __init__(
self, self,
@@ -28,7 +34,10 @@ class S3ItemSource(ItemSource):
annotation_file_suffix=".csv", annotation_file_suffix=".csv",
): ):
self._name = name self._name = name
self.s3 = s3fs.S3FileSystem() enable_cache = os.environ.get("S3_ENABLE_LISTINGS_CACHE", "false").lower()
assert enable_cache in ["0", "1", "false", "true"]
self.use_listings_cache = truthy(enable_cache)
self.s3 = s3fs.S3FileSystem(use_listings_cache=self.use_listings_cache)
if bucket.startswith("s3://"): if bucket.startswith("s3://"):
raise Exception( raise Exception(
f"Bucket name should not include s3:// prefix, got {bucket}" f"Bucket name should not include s3:// prefix, got {bucket}"
@@ -67,6 +76,13 @@ class S3ItemSource(ItemSource):
item_tree = self.scan_directory("" if filter is None else filter) item_tree = self.scan_directory("" if filter is None else filter)
return item_tree return item_tree
@property
def refresh(self):
return (
truthy(flask.request.args.get("refresh", default="false"))
or not self.use_listings_cache
)
def scan_directory(self, directory_key="") -> dict: def scan_directory(self, directory_key="") -> dict:
url = self.url(directory_key) url = self.url(directory_key)
@@ -75,7 +91,7 @@ class S3ItemSource(ItemSource):
s3key_map = dict( s3key_map = dict(
(self.remove_bucket(filepath), "s3://" + filepath) (self.remove_bucket(filepath), "s3://" + filepath)
for filepath in sorted(self.s3.ls(url)) for filepath in sorted(self.s3.ls(url, refresh=self.refresh))
) )
def is_annotation_dir(dir_s3key): def is_annotation_dir(dir_s3key):
@@ -166,7 +182,9 @@ class S3ItemSource(ItemSource):
self.make_s3item_from_key( self.make_s3item_from_key(
basename(annotation), self.remove_bucket(annotation), True basename(annotation), self.remove_bucket(annotation), True
) )
for annotation in sorted(self.s3.ls(annotations_fullpath)) for annotation in sorted(
self.s3.ls(annotations_fullpath, refresh=self.refresh)
)
if annotation.endswith(self.annotation_file_suffix) if annotation.endswith(self.annotation_file_suffix)
and self.s3.isfile("s3://" + annotation) and self.s3.isfile("s3://" + annotation)
] ]
+7 -2
View File
@@ -24,7 +24,10 @@ class TestScanDirectory(unittest.TestCase):
) )
@patch("s3fs.S3FileSystem") @patch("s3fs.S3FileSystem")
def test__GIVEN_multilevel_bucket_THEN_properly_recurses_suburls(self, s3func): @patch("flask.request")
def test__GIVEN_multilevel_bucket_THEN_properly_recurses_suburls(
self, requestMock, s3func
):
class S3Mock: class S3Mock:
def exists(path): def exists(path):
if path in [ if path in [
@@ -38,7 +41,8 @@ class TestScanDirectory(unittest.TestCase):
return True return True
raise Exception("exists called with " + path) raise Exception("exists called with " + path)
def ls(path): def ls(path, refresh):
assert refresh == True
if path == "s3://my-bucket/": if path == "s3://my-bucket/":
return [ return [
"my-bucket/lvl1", "my-bucket/lvl1",
@@ -79,6 +83,7 @@ class TestScanDirectory(unittest.TestCase):
raise Exception("isfile called with " + path) raise Exception("isfile called with " + path)
s3func.return_value = S3Mock s3func.return_value = S3Mock
requestMock.args.get.return_value = "true"
source = S3ItemSource("my-bucket") source = S3ItemSource("my-bucket")
tree = source.scan_directory() tree = source.scan_directory()
+1 -1
View File
@@ -33,7 +33,7 @@ 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 --disable-gene-sets-save --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 --scripts http://example.com/script.js --scripts http://example.com/script2.js"
], ],
shell=True, shell=True,
stderr=-1, stderr=-1,