Compare commits

...
43 Commits
Author SHA1 Message Date
Alok Saldanha 6f2b372d32 Preparing for 0.2.3 release 2021-02-28 09:55:26 -05:00
Alokito 25755d3f69 Merge pull request #41 from Novartis/grst_master
Grst master
2021-02-22 08:59:05 -05:00
Alok Saldanha 893092f199 fixed unit tests 2021-02-13 17:30:21 -05:00
Alok Saldanha d3ee04b6e5 blacken 2021-02-13 17:02:55 -05:00
Alok Saldanha f1f4b0c0ca added environment variables for ProxyFix 2021-02-13 16:42:34 -05:00
Alok Saldanha 01dccef7db added back trailing slash to url_for change 2021-02-13 16:41:32 -05:00
Gregor Sturm 9c17e2ff32 Fix redirect in cache_entry 2021-01-18 22:08:25 +01:00
Gregor Sturm fbc18fb636 Add ProxyFix to gateway.py 2021-01-18 20:01:53 +01:00
Gregor Sturm 8c5a635de9 Use url_for in all templates 2021-01-18 19:47:18 +01:00
Gregor Sturm 94062c2d64 apply proxy fix 2021-01-18 18:19:25 +01:00
Gregor Sturm 068e8f7633 Fix url_for 2021-01-18 18:09:45 +01:00
Gregor Sturm 16b54f9409 Use url_for to generate URLs 2021-01-18 16:59:15 +01:00
Alok Saldanha 942410bb44 added instructions on pre-commit installation to README.md 2020-12-31 17:12:11 -05:00
Alokito 0d084a405e Merge pull request #38 from Novartis/feature/action_push
Feature/action push
2020-12-31 16:33:30 -05:00
Alok Saldanha 49d679e779 try evaling the bash hook
per https://github.com/conda/conda/issues/7980
2020-12-31 13:23:25 -05:00
Alok Saldanha 5e6faa4b02 run pr checks on push 2020-12-31 13:06:56 -05:00
Alokito 9fe846c786 Merge pull request #37 from ericmjl/master
Migrated PR checks to GitHub actions
2020-12-28 11:38:26 -05:00
Eric Ma 1c7907aabd Change file extension 2020-12-27 21:26:32 -05:00
Eric Ma 30d2b07b1a Migrated PR checks to GitHub actions 2020-12-27 21:10:07 -05:00
Alokito 21ff56ea8b Merge pull request #35 from dfeinzeig/fix/redirect
add missing trailing slash in effort to avoid whatever is redirecting
2020-10-28 07:39:06 -04:00
David Feinzeig 0b866a46ec add missing trailing slash in effort to avoid whatever is redirecting 2020-10-23 17:41:05 -04:00
Alok Saldanha d5246d4b4b prepared for 0.2.2 release 2020-09-08 21:11:59 -04:00
Alok Saldanha 465373ca7e #30 black formatting 2020-09-08 21:11:59 -04:00
Alok Saldanha fc83523c84 #30 added missing js asset 2020-09-08 21:07:18 -04:00
Alok Saldanha 4e367b0ec9 update travis.yml to new conda env 2020-08-30 13:53:57 -04:00
Alok Saldanha ce3eb0eedf Merge branch 'gmerge' into gmaster
# Conflicts:
#	cellxgene_gateway/backend_cache.py
#	cellxgene_gateway/cache_entry.py
#	cellxgene_gateway/env.py
#	cellxgene_gateway/gateway.py
#	cellxgene_gateway/subprocess_backend.py
#	tests/test_cache_entry.py
#	tests/test_dir_util.py
2020-08-30 13:47:22 -04:00
Alok Saldanha 9219336356 blackened code 2020-08-30 13:38:35 -04:00
Alok Saldanha b689357f29 Applied "Formatting and new enumeration for Cellxgene-gateway" patch 2020-08-30 13:36:39 -04:00
Alok Saldanha 5349df36e8 prepared for 0.2.1 release
updated docs
made GATEWAY_IP optional
2020-08-18 20:02:24 -04:00
Alokito 13802b72b9 Merge pull request #26 from Novartis/feature/25
#25 added CELLXGENE_ARGS environment variable
2020-08-18 19:45:31 -04:00
Alok Saldanha 0f8092e07a #25 added CELLXGENE_ARGS environment variable 2020-08-16 17:08:06 -04:00
Alok Saldanha 61c7c43da7 blackened code 2020-08-16 17:07:23 -04:00
Alokito 0604a0ca9f Merge pull request #27 from Novartis/feature/24
Feature/24
2020-08-16 17:05:49 -04:00
Alok Saldanha 8cdd23a57b #24 switched to absolute paths
relative paths won't necessarily work in css files
2020-08-16 16:17:28 -04:00
Alok Saldanha 486206bd32 #24 make static paths relative 2020-08-16 16:06:34 -04:00
Alokito 4105fc32e1 Update README.md
added link to travis badge
2020-08-10 20:06:31 -04:00
Alok Saldanha 59cdac5f85 move black into environment.yml 2020-08-10 18:01:22 -04:00
Alokito bd1b7e8da2 Merge pull request #23 from Novartis/feature/22_metadata_api
Feature/22 metadata api
2020-08-10 17:57:50 -04:00
Alok Saldanha bfa80f7ef8 #22 blackened code 2020-08-10 17:56:32 -04:00
Alok Saldanha 63cdfe1953 #22 fixed failing tests 2020-08-10 17:56:24 -04:00
Alok Saldanha 578845b133 #22 Added /metadata/ip_address endpoint 2020-08-10 17:49:03 -04:00
Alokito 849c8a58f2 Rename travis.yml to .travis.yml 2020-08-10 16:54:44 -04:00
Alokito 021d981bef Create travis.yml 2020-08-10 16:52:32 -04:00
30 changed files with 663 additions and 217 deletions
+49
View File
@@ -0,0 +1,49 @@
# Tests that run on every PR
name: Pull Request Checks
on: [push, pull_request]
jobs:
black:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
name: Checkout repository
- uses: actions/setup-python@v2
name: Setup Python
with:
python-version: 3.9
- name: Install black
run: |
python -m pip install --upgrade pip
pip install black
- name: Run black
run: |
black -l 79 . --check
# This job is copied over from `deploy.yaml`
run-tests:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
# See: https://github.com/marketplace/actions/setup-conda
- uses: s-weigand/setup-conda@v1
with:
conda-channels: "conda-forge"
- name: Build environment
run: |
conda env create -f environment.yml
eval "$(conda shell.bash hook)"
conda activate cellxgene-gateway
python setup.py install
- name: Run tests
run: |
eval "$(conda shell.bash hook)"
conda activate cellxgene-gateway
python -m unittest discover tests
bash <(curl -s https://codecov.io/bash)
+41
View File
@@ -0,0 +1,41 @@
# This is necessary for nxviz as matplotlib is involved.
# before_script:
# - "export DISPLAY=:99.0"
# - "sh -e /etc/init.d/xvfb start"
# - sleep 5 # give xvfb some time to start
language: python
matrix:
include:
- python: 3.5 # we don't actually use this
env: PYTHON_VERSION=3.7
install:
# We do this conditionally because it saves us some downloading if the
# version is the same.
- wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh;
- bash miniconda.sh -b -p $HOME/miniconda
- export PATH="$HOME/miniconda/bin:$PATH"
- hash -r
- conda config --set always_yes yes --set changeps1 no
- conda update -q conda
- conda config --add channels conda-forge
# Useful for debugging any issues with conda
- conda info -a
# Install Python, py.test, and required packages.
- conda env create -f environment.yml
- source activate cellxgene-gateway
- python setup.py install
script:
# Your test script goes here
- black -l 79 . --check
- python -m unittest discover tests
after_success:
- bash <(curl -s https://codecov.io/bash)
notifications:
email: true
+14
View File
@@ -1,3 +1,17 @@
# 0.2.3
* Added support for ProxyFix
# 0.2.2
* Fixed bug with annotations (missing annotation.js asset)
# 0.2.1
* Minor fixes to enable cellxgene 0.16.0
* Added CELLXGENE_ARGS to enable passing additional arguments to cellxgene
* added metadata/ip_address endpoint
# 0.2.0 # 0.2.0
Incrementing minor version since the changes for 0.15 are breaking, and we may want to release bugfixes from 0.1.0 branch. Incrementing minor version since the changes for 0.15 are breaking, and we may want to release bugfixes from 0.1.0 branch.
+22 -3
View File
@@ -30,7 +30,7 @@ Note: you may need to downgrade h5py with `pip install h5py==2.9.0` due to an [i
### Option 2: Install from PyPI ### Option 2: Install from PyPI
```bash ```bash
# NOT YET DONE, COMING! STAY TUNED pip install cellxgene-gateway
``` ```
## Running cellxgene gateway ## Running cellxgene gateway
@@ -39,7 +39,7 @@ Note: you may need to downgrade h5py with `pip install h5py==2.9.0` due to an [i
```bash ```bash
mkdir ../cellxgene_data mkdir ../cellxgene_data
wget https://github.com/chanzuckerberg/cellxgene/raw/master/example-dataset/pbmc3k.h5ad -O ../cellxgene_data/pbmc3k.h5ad wget https://raw.githubusercontent.com/chanzuckerberg/cellxgene/master/example-dataset/pbmc3k.h5ad -O ../cellxgene_data/pbmc3k.h5ad
``` ```
@@ -60,7 +60,9 @@ Here's what the environment variables mean:
* `CELLXGENE_LOCATION` - the location of the cellxgene executable, e.g. `~/anaconda2/envs/cellxgene/bin/cellxgene` * `CELLXGENE_LOCATION` - the location of the cellxgene executable, e.g. `~/anaconda2/envs/cellxgene/bin/cellxgene`
* `CELLXGENE_DATA` - a directory that can contain subdirectories with `.h5ad` data files, *without* trailing slash, e.g. `/mnt/cellxgene_data` * `CELLXGENE_DATA` - a directory that can contain subdirectories with `.h5ad` data files, *without* trailing slash, e.g. `/mnt/cellxgene_data`
Optional environment variables: Optional environment variables:
* `CELLXGENE_ARGS` - catch-all variable that can be used to pass additional command line args to cellxgene server
* `EXTERNAL_HOST` - the hostname and port from the perspective of the web browser, typically `localhost:5005` if running locally. Defaults to "localhost:{GATEWAY_PORT}" * `EXTERNAL_HOST` - the hostname and port from the perspective of the web browser, typically `localhost:5005` if running locally. Defaults to "localhost:{GATEWAY_PORT}"
* `EXTERNAL_PROTOCOL` - typically http when running locally, can be https when deployed if the gateway is behind a load balancer or reverse proxy that performs https termination. Default value "http" * `EXTERNAL_PROTOCOL` - typically http when running locally, can be https when deployed if the gateway is behind a load balancer or reverse proxy that performs https termination. Default value "http"
* `GATEWAY_IP` - ip addess of instance gateway is running on, mostly used to display SSH instructions. Defaults to `socket.gethostbyname(socket.gethostname())` * `GATEWAY_IP` - ip addess of instance gateway is running on, mostly used to display SSH instructions. Defaults to `socket.gethostbyname(socket.gethostname())`
@@ -68,7 +70,14 @@ 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_UPLOAD` - Set to `true` or `1` to enable HTTP uploads. This is not recommended for a public server. * `GATEWAY_ENABLE_UPLOAD` - Set to `true` or `1` to enable HTTP uploads. This is not recommended for a public 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.
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_PROTO` - Number of upstream proxies setting X-Forwarded-Proto
* `PROXY_FIX_HOST` - Number of upstream proxies setting X-Forwarded-Host
* `PROXY_FIX_PORT` - Number of upstream proxies setting X-Forwarded-Port
* `PROXY_FIX_PREFIX` - Number of upstream proxies setting X-Forwarded-Prefix
The defaults should be fine if you set up a venv and cellxgene_data folder as above. The defaults should be fine if you set up a venv and cellxgene_data folder as above.
@@ -112,8 +121,18 @@ python setup.py develop
For convenience, the code repo includes a `run.sh.example` shell script to run the gateway. For convenience, the code repo includes a `run.sh.example` shell script to run the gateway.
4. Install pre-commit hooks
```bash
conda install -c conda-forge pre-commit
pre-commit install
```
## Running Tests ## Running Tests
[![Build Status](https://travis-ci.org/Novartis/cellxgene-gateway.svg?branch=master)](https://travis-ci.org/Novartis/cellxgene-gateway)
```bash ```bash
python -m unittest discover tests python -m unittest discover tests
``` ```
Executable → Regular
+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.2.0" __version__ = "0.2.3"
+7 -3
View File
@@ -13,7 +13,7 @@ from threading import Thread
from flask_api import status from flask_api import status
from cellxgene_gateway import env from cellxgene_gateway import env
from cellxgene_gateway.cache_entry import CacheEntry from cellxgene_gateway.cache_entry import CacheEntry, CacheEntryStatus
from cellxgene_gateway.cellxgene_exception import CellxgeneException from cellxgene_gateway.cellxgene_exception import CellxgeneException
from cellxgene_gateway.subprocess_backend import SubprocessBackend from cellxgene_gateway.subprocess_backend import SubprocessBackend
@@ -22,8 +22,10 @@ process_backend = SubprocessBackend()
def is_port_in_use(port): def is_port_in_use(port):
import socket import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
return s.connect_ex(('localhost', port)) == 0 return s.connect_ex(("localhost", port)) == 0
class BackendCache: class BackendCache:
def __init__(self): def __init__(self):
@@ -38,7 +40,9 @@ class BackendCache:
matches = [ matches = [
c c
for c in contents for c in contents
if c.key.dataset == key.dataset and c.key.annotation_file == key.annotation_file and c.status != "terminated" if c.key.dataset == key.dataset
and c.key.annotation_file == key.annotation_file
and c.status != CacheEntryStatus.terminated
] ]
if len(matches) == 0: if len(matches) == 0:
+74 -44
View File
@@ -6,17 +6,29 @@
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
# 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 psutil
import logging
import datetime import datetime
import logging
from flask.helpers import url_for
from flask.wrappers import Response
from flask import make_response, request, render_template import psutil
from enum import Enum
from flask import make_response, render_template, request
from requests import get, post, put from requests import get, post, put
import re
from cellxgene_gateway import env from cellxgene_gateway import env
from cellxgene_gateway.cellxgene_exception import CellxgeneException from cellxgene_gateway.cellxgene_exception import CellxgeneException
from cellxgene_gateway.util import current_time_stamp
from cellxgene_gateway.flask_util import querystring from cellxgene_gateway.flask_util import querystring
from cellxgene_gateway.util import current_time_stamp
class CacheEntryStatus(Enum):
loaded = "loaded"
loading = "loading"
error = "error"
terminated = "terminated"
class CacheEntry: class CacheEntry:
def __init__( def __init__(
@@ -26,7 +38,7 @@ class CacheEntry:
port, port,
launchtime, launchtime,
timestamp, timestamp,
status, status: CacheEntryStatus,
message, message,
all_output, all_output,
stderr, stderr,
@@ -52,7 +64,7 @@ class CacheEntry:
port, port,
current_time_stamp(), current_time_stamp(),
current_time_stamp(), current_time_stamp(),
"loading", CacheEntryStatus.loading,
None, None,
None, None,
None, None,
@@ -61,13 +73,13 @@ class CacheEntry:
def set_loaded(self, pid): def set_loaded(self, pid):
self.pid = pid self.pid = pid
self.status = "loaded" self.status = CacheEntryStatus.loaded
def set_error(self, message, stderr, http_status): def set_error(self, message, stderr, http_status):
self.message = message self.message = message
self.stderr = stderr self.stderr = stderr
self.http_status = http_status self.http_status = http_status
self.status = "error" self.status = CacheEntryStatus.error
def append_output(self, output): def append_output(self, output):
if self.all_output == None: if self.all_output == None:
@@ -77,10 +89,12 @@ class CacheEntry:
def terminate(self): def terminate(self):
pid = self.pid pid = self.pid
if pid != None and self.status != "terminated": if pid != None and self.status != CacheEntryStatus.terminated:
terminated = [] terminated = []
def on_terminate(p): def on_terminate(p):
terminated.append(p.pid) terminated.append(p.pid)
p = psutil.Process(pid) p = psutil.Process(pid)
children = p.children() children = p.children()
for child in children: for child in children:
@@ -89,55 +103,72 @@ class CacheEntry:
terminated.append(p.pid) terminated.append(p.pid)
p.terminate() p.terminate()
psutil.wait_procs([p], callback=on_terminate) psutil.wait_procs([p], callback=on_terminate)
logging.getLogger("cellxgene_gateway").info(f"terminated {terminated}") logging.getLogger("cellxgene_gateway").info(
self.status = "terminated" f"terminated {terminated}"
)
self.status = CacheEntryStatus.terminated
def rewrite_text_content(self, cellxgene_content):
# for v0.16.0 compatibility, see issue #24
gateway_content = (
re.sub(
'(="|\()/static/',
f"\\1{self.gateway_basepath()}static/",
cellxgene_content,
)
.replace("http://fonts.gstatic.com", "https://fonts.gstatic.com")
.replace(self.cellxgene_basepath(), self.gateway_basepath())
)
return gateway_content
def gateway_basepath(self):
return url_for("do_view", path=self.key.pathpart) + "/"
def cellxgene_basepath(self):
return f"http://127.0.0.1:{self.port}"
def serve_content(self, path): def serve_content(self, path):
gateway_basepath = ( gateway_basepath = self.gateway_basepath()
f"{env.external_protocol}://{env.external_host}/view/{self.key.pathpart}/"
)
subpath = path[len(self.key.pathpart) :] # noqa: E203 subpath = path[len(self.key.pathpart) :] # noqa: E203
if len(subpath) == 0: if len(subpath) == 0:
r = make_response(f"Redirect to {gateway_basepath}\n", 301) r = make_response(f"Redirect to {gateway_basepath}\n", 302)
r.headers["location"] = gateway_basepath+querystring() r.headers["location"] = gateway_basepath + querystring()
return r return r
elif self.status == "loading": elif self.status == CacheEntryStatus.loading:
launch_time = datetime.datetime.fromtimestamp(self.launchtime) launch_time = datetime.datetime.fromtimestamp(self.launchtime)
return render_template( return render_template(
"loading.html", launchtime=launch_time, all_output=self.all_output "loading.html",
launchtime=launch_time,
all_output=self.all_output,
) )
port = self.port
cellxgene_basepath = f"http://127.0.0.1:{port}"
headers = {} headers = {}
copy_headers = [ copy_headers = [
'accept', "accept",
'accept-encoding', "accept-encoding",
'accept-language', "accept-language",
'cache-control', "cache-control",
'connection', "connection",
'content-length', "content-length",
'content-type', "content-type",
'cookie', "cookie",
'host', "host",
'origin', "origin",
'pragma', "pragma",
'referer', "referer",
'sec-fetch-mode', "sec-fetch-mode",
'sec-fetch-site', "sec-fetch-site",
'user-agent' "user-agent",
] ]
for h in copy_headers: for h in copy_headers:
if h in request.headers: if h in request.headers:
headers[h] = request.headers[h] headers[h] = request.headers[h]
full_path = cellxgene_basepath + subpath + querystring() full_path = self.cellxgene_basepath() + subpath + querystring()
if request.method in ["GET", "HEAD", "OPTIONS"]: if request.method in ["GET", "HEAD", "OPTIONS"]:
cellxgene_response = get( cellxgene_response = get(full_path, headers=headers)
full_path, headers=headers
)
elif request.method == "PUT": elif request.method == "PUT":
cellxgene_response = put( cellxgene_response = put(
full_path, full_path,
@@ -156,10 +187,9 @@ class CacheEntry:
) )
content_type = cellxgene_response.headers["content-type"] content_type = cellxgene_response.headers["content-type"]
if "text" in content_type: if "text" in content_type:
cellxgene_content = cellxgene_response.content.decode() gateway_content = self.rewrite_text_content(
gateway_content = cellxgene_content.replace( cellxgene_response.content.decode()
"http://fonts.gstatic.com", "https://fonts.gstatic.com" )
).replace(cellxgene_basepath, gateway_basepath)
else: else:
gateway_content = cellxgene_response.content gateway_content = cellxgene_response.content
+3 -9
View File
@@ -7,21 +7,15 @@
# 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
from flask_api import status
from cellxgene_gateway import env
from cellxgene_gateway.cellxgene_exception import CellxgeneException
# There are three kinds of CacheKey: # There are three kinds of CacheKey:
# 1) somedir/dataset.h5ad: a dataset # 1) somedir/dataset.h5ad: a dataset
# in this case, pathpart == dataset == 'somedir/dataset.h5ad' # in this case, pathpart == dataset == 'somedir/dataset.h5ad'
# 2) somedir/dataset_annotations/saldaal1-T5HMVBNV.csv : an actual annotaitons file. # 2) somedir/dataset_annotations/my_annotations.csv : an actual annotaitons file.
# in this case, pathpart == 'dataset_annotations/saldaal1-T5HMVBNV.csv', dataset == 'somedir/dataset.h5ad' # in this case, pathpart == 'dataset_annotations/my_annotations.csv', dataset == 'somedir/dataset.h5ad'
# 3) somedir/dataset_annotations: an annotation directory. The corresponding h5ad must exist, but the directory may not. # 3) somedir/dataset_annotations: an annotation directory. The corresponding h5ad must exist, but the directory may not.
# in this case, pathpart == 'dataset_annotations', dataset == 'somedir/dataset.h5ad' # in this case, pathpart == 'dataset_annotations', dataset == 'somedir/dataset.h5ad'
class CacheKey: class CacheKey:
def __init__(self, pathpart, dataset, annotation_file): def __init__(self, pathpart, dataset, annotation_file):
self.pathpart = pathpart self.pathpart = pathpart
+8 -3
View File
@@ -51,8 +51,13 @@ def create_dir(parent_path, dir_name):
else: else:
os.mkdir(full_path) os.mkdir(full_path)
annotations_suffix = '_annotations'
annotations_suffix = "_annotations"
def make_h5ad(el): def make_h5ad(el):
return el[:-len(annotations_suffix)]+'.h5ad' return el[: -len(annotations_suffix)] + ".h5ad"
def make_annotations(el): def make_annotations(el):
return el[:-5]+annotations_suffix return el[:-5] + annotations_suffix
+46 -11
View File
@@ -7,39 +7,71 @@
# 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 logging import logging
import os
import socket import socket
cellxgene_location = os.environ.get("CELLXGENE_LOCATION") cellxgene_location = os.environ.get("CELLXGENE_LOCATION")
cellxgene_data = os.environ.get("CELLXGENE_DATA") cellxgene_data = os.environ.get("CELLXGENE_DATA")
cellxgene_args = os.environ.get("CELLXGENE_ARGS", None)
gateway_port = int(os.environ.get("GATEWAY_PORT", "5005")) gateway_port = int(os.environ.get("GATEWAY_PORT", "5005"))
external_host = os.environ.get("EXTERNAL_HOST", os.environ.get("GATEWAY_HOST", f"localhost:{gateway_port}")) external_host = os.environ.get(
external_protocol = os.environ.get("EXTERNAL_PROTOCOL", os.environ.get("GATEWAY_PROTOCOL", "http")) "EXTERNAL_HOST",
ip = os.environ.get("GATEWAY_IP") os.environ.get("GATEWAY_HOST", f"localhost:{gateway_port}"),
)
external_protocol = os.environ.get(
"EXTERNAL_PROTOCOL", os.environ.get("GATEWAY_PROTOCOL", "http")
)
ip = os.environ.get("GATEWAY_IP", "127.0.0.1")
extra_scripts = os.environ.get("GATEWAY_EXTRA_SCRIPTS") extra_scripts = os.environ.get("GATEWAY_EXTRA_SCRIPTS")
ttl = os.environ.get("GATEWAY_TTL") ttl = os.environ.get("GATEWAY_TTL")
enable_upload = os.environ.get("GATEWAY_ENABLE_UPLOAD", "").lower() in ['true', '1'] enable_upload = os.environ.get("GATEWAY_ENABLE_UPLOAD", "").lower() in [
enable_annotations = os.environ.get("GATEWAY_ENABLE_ANNOTATIONS", "").lower() in ['true', '1'] "true",
enable_backed_mode = os.environ.get("GATEWAY_ENABLE_BACKED_MODE", "").lower() in ['true', '1'] "1",
]
enable_annotations = os.environ.get(
"GATEWAY_ENABLE_ANNOTATIONS", ""
).lower() in [
"true",
"1",
]
enable_backed_mode = os.environ.get(
"GATEWAY_ENABLE_BACKED_MODE", ""
).lower() in [
"true",
"1",
]
env_vars = { env_vars = {
"CELLXGENE_LOCATION": cellxgene_location, "CELLXGENE_LOCATION": cellxgene_location,
"CELLXGENE_DATA": cellxgene_data, "CELLXGENE_DATA": cellxgene_data,
"GATEWAY_IP": ip,
} }
proxy_fix_for = int(os.environ.get("PROXY_FIX_FOR", "0"))
proxy_fix_proto = int(os.environ.get("PROXY_FIX_PROTO", "0"))
proxy_fix_host = int(os.environ.get("PROXY_FIX_HOST", "0"))
proxy_fix_port = int(os.environ.get("PROXY_FIX_PORT", "0"))
proxy_fix_prefix = int(os.environ.get("PROXY_FIX_PREFIX", "0"))
optional_env_vars = { optional_env_vars = {
"EXTERNAL_HOST": external_host, "EXTERNAL_HOST": external_host,
"EXTERNAL_PROTOCOL": external_protocol, "EXTERNAL_PROTOCOL": external_protocol,
"GATEWAY_IP": ip,
"GATEWAY_PORT": gateway_port, "GATEWAY_PORT": gateway_port,
"GATEWAY_EXTRA_SCRIPTS": extra_scripts, "GATEWAY_EXTRA_SCRIPTS": extra_scripts,
"GATEWAY_TTL": ttl, "GATEWAY_TTL": ttl,
"GATEWAY_ENABLE_UPLOAD": enable_upload, "GATEWAY_ENABLE_UPLOAD": enable_upload,
"GATEWAY_ENABLE_ANNOTATIONS": enable_annotations, "GATEWAY_ENABLE_ANNOTATIONS": enable_annotations,
"GATEWAY_ENABLE_BACKED_MODE": enable_backed_mode, "GATEWAY_ENABLE_BACKED_MODE": enable_backed_mode,
"CELLXGENE_ARGS": cellxgene_args,
"PROXY_FIX_FOR": proxy_fix_for,
"PROXY_FIX_PROTO": proxy_fix_proto,
"PROXY_FIX_HOST": proxy_fix_host,
"PROXY_FIX_PORT": proxy_fix_port,
"PROXY_FIX_PREFIX": proxy_fix_prefix,
} }
def validate(): def validate():
if not all(env_vars.values()): if not all(env_vars.values()):
raise ValueError( raise ValueError(
@@ -54,9 +86,12 @@ def validate():
export CELLXGENE_LOCATION=~/anaconda/envs/cellxgene-dev/bin/cellxgene export CELLXGENE_LOCATION=~/anaconda/envs/cellxgene-dev/bin/cellxgene
export CELLXGENE_DATA=../cellxgene_data export CELLXGENE_DATA=../cellxgene_data
export GATEWAY_IP=127.0.0.1
""" """
) )
else: else:
logging.getLogger("cellxgene_gateway").info(f"Got required env: {env_vars}", ) logging.getLogger("cellxgene_gateway").info(
logging.getLogger("cellxgene_gateway").info(f"Got optional env: {optional_env_vars}") f"Got required env: {env_vars}",
)
logging.getLogger("cellxgene_gateway").info(
f"Got optional env: {optional_env_vars}"
)
+2 -1
View File
@@ -7,9 +7,10 @@
# 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 cellxgene_gateway import env
from json import loads from json import loads
from cellxgene_gateway import env
def get_extra_scripts(): def get_extra_scripts():
# can be array of script tags to inject on every page, e.g. for google analytics could be # can be array of script tags to inject on every page, e.g. for google analytics could be
+51 -14
View File
@@ -9,7 +9,13 @@
import os import os
from cellxgene_gateway import env from cellxgene_gateway import env
from cellxgene_gateway.dir_util import make_h5ad, make_annotations, annotations_suffix from cellxgene_gateway.dir_util import (
make_h5ad,
make_annotations,
annotations_suffix,
)
from flask import url_for
def recurse_dir(path): def recurse_dir(path):
if not os.path.exists(path): if not os.path.exists(path):
@@ -18,21 +24,42 @@ def recurse_dir(path):
) )
all_entries = sorted(os.listdir(path)) all_entries = sorted(os.listdir(path))
def is_h5ad(el): def is_h5ad(el):
return el.endswith('.h5ad') and os.path.isfile(os.path.join(path, el)) return el.endswith(".h5ad") and os.path.isfile(os.path.join(path, el))
h5ad_entries = [x for x in all_entries if is_h5ad(x)] h5ad_entries = [x for x in all_entries if is_h5ad(x)]
annotation_dir_entries = [x for x in all_entries if x.endswith(annotations_suffix) and make_h5ad(x) in h5ad_entries] annotation_dir_entries = [
x
for x in all_entries
if x.endswith(annotations_suffix) and make_h5ad(x) in h5ad_entries
]
def list_annotations(el): def list_annotations(el):
full_path = os.path.join(path, el) full_path = os.path.join(path, el)
if not os.path.isdir(full_path): if not os.path.isdir(full_path):
entries = [] entries = []
else: else:
entries = [{ entries = [
"name": x[:-13] if (len(x) > 13 and x[-13] in ['-','_']) else ( {
x[:-4] if x.endswith('.csv') else x), "name": x[:-13]
"path": os.path.join(full_path, x).replace(env.cellxgene_data, ""), if (len(x) > 13 and x[-13] in ["-", "_"])
} for x in sorted(os.listdir(full_path)) if x.endswith('.csv') and os.path.isfile(os.path.join(full_path, x))] else (x[:-4] if x.endswith(".csv") else x),
return [{"name":'new', "class":'new', "path":full_path.replace(env.cellxgene_data, "")}] + entries "path": os.path.join(full_path, x).replace(
env.cellxgene_data, ""
),
}
for x in sorted(os.listdir(full_path))
if x.endswith(".csv")
and os.path.isfile(os.path.join(full_path, x))
]
return [
{
"name": "new",
"class": "new",
"path": full_path.replace(env.cellxgene_data, ""),
}
] + entries
def make_entry(el): def make_entry(el):
full_path = os.path.join(path, el) full_path = os.path.join(path, el)
@@ -63,16 +90,26 @@ def recurse_dir(path):
def render_entries(entries): def render_entries(entries):
return "<ul>" + "\n".join([render_entry(e) for e in entries]) + "</ul>" return "<ul>" + "\n".join([render_entry(e) for e in entries]) + "</ul>"
def get_url(entry): def get_url(entry):
return f"/view/{ entry['path'].lstrip('/') }" return url_for("do_view", path=entry["path"].lstrip("/") + "/")
def get_class(entry): def get_class(entry):
return f" class='{entry['class']}'" if 'class' in entry else '' return f" class='{entry['class']}'" if "class" in entry else ""
def render_annotations(entry): def render_annotations(entry):
if len(entry['annotations']) > 0: if len(entry["annotations"]) > 0:
return ' | annotations: ' + ", ".join([f"<a href='{get_url(a)}'{get_class(a)}>{a['name']}</a>" for a in entry['annotations']]) return " | annotations: " + ", ".join(
[
f"<a href='{get_url(a)}'{get_class(a)}>{a['name']}</a>"
for a in entry["annotations"]
]
)
else: else:
return '' return ""
def render_entry(entry): def render_entry(entry):
if entry["type"] == "file": if entry["type"] == "file":
+2 -1
View File
@@ -9,6 +9,7 @@
from flask import request from flask import request
def querystring(): def querystring():
qs = request.query_string.decode() qs = request.query_string.decode()
return f'?{qs}' if len(qs) > 0 else '' return f"?{qs}" if len(qs) > 0 else ""
+110 -42
View File
@@ -7,16 +7,17 @@
# 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 json
import logging
# import BaseHTTPServer # import BaseHTTPServer
import os import os
import logging from threading import Lock, Thread
from threading import Thread, Lock
import json
from flask import ( from flask import (
Flask, Flask,
redirect,
make_response, make_response,
redirect,
render_template, render_template,
request, request,
send_from_directory, send_from_directory,
@@ -24,29 +25,48 @@ from flask import (
) )
from flask_api import status from flask_api import status
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from werkzeug.middleware.proxy_fix import ProxyFix
from cellxgene_gateway import env from cellxgene_gateway import env
from cellxgene_gateway.backend_cache import BackendCache from cellxgene_gateway.backend_cache import BackendCache
from cellxgene_gateway.cache_entry import CacheEntryStatus
from cellxgene_gateway.cellxgene_exception import CellxgeneException from cellxgene_gateway.cellxgene_exception import CellxgeneException
from cellxgene_gateway.dir_util import create_dir, is_subdir from cellxgene_gateway.dir_util import create_dir, is_subdir
from cellxgene_gateway.filecrawl import recurse_dir, render_entries
from cellxgene_gateway.extra_scripts import get_extra_scripts from cellxgene_gateway.extra_scripts import get_extra_scripts
from cellxgene_gateway.filecrawl import recurse_dir, render_entries
from cellxgene_gateway.path_util import get_key
from cellxgene_gateway.process_exception import ProcessException from cellxgene_gateway.process_exception import ProcessException
from cellxgene_gateway.prune_process_cache import PruneProcessCache from cellxgene_gateway.prune_process_cache import PruneProcessCache
from cellxgene_gateway.util import current_time_stamp from cellxgene_gateway.util import current_time_stamp
from cellxgene_gateway.path_util import get_key
app = Flask(__name__) app = Flask(__name__)
def _force_https(app): def _force_https(app):
def wrapper(environ, start_response): def wrapper(environ, start_response):
environ['wsgi.url_scheme'] = env.external_protocol environ["wsgi.url_scheme"] = env.external_protocol
return app(environ, start_response) return app(environ, start_response)
return wrapper return wrapper
app.wsgi_app = _force_https(app.wsgi_app) app.wsgi_app = _force_https(app.wsgi_app)
if (
env.proxy_fix_for > 0
or env.proxy_fix_proto > 0
or env.proxy_fix_host > 0
or env.proxy_fix_port > 0
or env.proxy_fix_prefix > 0
):
app.wsgi_app = ProxyFix(
app.wsgi_app,
x_for=env.proxy_fix_for,
x_proto=env.proxy_fix_proto,
x_host=env.proxy_fix_host,
x_port=env.proxy_fix_port,
x_prefix=env.proxy_fix_prefix,
)
cache = BackendCache() cache = BackendCache()
location = f"{env.external_protocol}://{env.external_host}"
@app.errorhandler(CellxgeneException) @app.errorhandler(CellxgeneException)
@@ -114,12 +134,13 @@ def index():
enable_upload=env.enable_upload, enable_upload=env.enable_upload,
) )
def make_user(): def make_user():
dir_name = request.form["directory"] dir_name = request.form["directory"]
create_dir(env.cellxgene_data, dir_name) create_dir(env.cellxgene_data, dir_name)
return redirect(location, code=302) return redirect(url_for("index"), code=302)
def make_subdir(): def make_subdir():
@@ -128,22 +149,26 @@ def make_subdir():
create_dir(parent_path, dir_name) create_dir(parent_path, dir_name)
return redirect(location, code=302) return redirect(url_for("index"), code=302)
def upload_file(): def upload_file():
upload_dir = request.form["path"] upload_dir = request.form["path"]
full_upload_path = os.path.join(env.cellxgene_data, upload_dir) full_upload_path = os.path.join(env.cellxgene_data, upload_dir)
if is_subdir(full_upload_path, env.cellxgene_data) and os.path.isdir(full_upload_path): if is_subdir(full_upload_path, env.cellxgene_data) and os.path.isdir(
full_upload_path
):
if request.method == "POST": if request.method == "POST":
if "file" in request.files: if "file" in request.files:
f = request.files["file"] f = request.files["file"]
if f and f.filename.endswith(".h5ad"): if f and f.filename.endswith(".h5ad"):
f.save( f.save(
os.path.join(full_upload_path, secure_filename(f.filename)) os.path.join(
full_upload_path, secure_filename(f.filename)
)
) )
return redirect("/filecrawl.html", code=302) return redirect(url_for("filecrawl"), code=302)
else: else:
raise CellxgeneException( raise CellxgeneException(
"Uploaded file must be in anndata (.h5ad) format.", "Uploaded file must be in anndata (.h5ad) format.",
@@ -159,35 +184,48 @@ def upload_file():
"Invalid directory.", status.HTTP_400_BAD_REQUEST "Invalid directory.", status.HTTP_400_BAD_REQUEST
) )
return redirect(location, code=302) return redirect(url_for("index"), code=302)
if env.enable_upload: if env.enable_upload:
app.add_url_rule('/make_user', 'make_user', make_user, methods=["POST"]) app.add_url_rule("/make_user", "make_user", make_user, methods=["POST"])
app.add_url_rule('/make_subdir', 'make_subdir', make_subdir, methods=["POST"]) app.add_url_rule(
app.add_url_rule('/upload_file', 'upload_file', upload_file, methods=["POST"]) "/make_subdir", "make_subdir", make_subdir, methods=["POST"]
)
app.add_url_rule(
"/upload_file", "upload_file", upload_file, methods=["POST"]
)
def set_no_cache(resp):
resp.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
resp.headers["Pragma"] = "no-cache"
resp.headers["Expires"] = "0"
resp.headers["Cache-Control"] = "public, max-age=0"
return resp
@app.route("/filecrawl.html") @app.route("/filecrawl.html")
def filecrawl(): def filecrawl():
entries = recurse_dir(env.cellxgene_data) entries = recurse_dir(env.cellxgene_data)
rendered_html = render_entries(entries) rendered_html = render_entries(entries)
resp = make_response(render_template( resp = make_response(
"filecrawl.html", render_template(
extra_scripts=get_extra_scripts(), "filecrawl.html",
rendered_html=rendered_html, extra_scripts=get_extra_scripts(),
)) rendered_html=rendered_html,
resp.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" )
resp.headers["Pragma"] = "no-cache" )
resp.headers["Expires"] = "0" return set_no_cache(resp)
resp.headers['Cache-Control'] = 'public, max-age=0'
return resp
@app.route("/filecrawl/<path:path>") @app.route("/filecrawl/<path:path>")
def do_filecrawl(path): def do_filecrawl(path):
filecrawl_path = os.path.join(env.cellxgene_data, path) filecrawl_path = os.path.join(env.cellxgene_data, path)
if not os.path.isdir(filecrawl_path): if not os.path.isdir(filecrawl_path):
raise CellxgeneException( raise CellxgeneException(
"Path is not directory: " + filecrawl_path, status.HTTP_400_BAD_REQUEST "Path is not directory: " + filecrawl_path,
status.HTTP_400_BAD_REQUEST,
) )
entries = recurse_dir(filecrawl_path) entries = recurse_dir(filecrawl_path)
rendered_html = render_entries(entries) rendered_html = render_entries(entries)
@@ -198,11 +236,16 @@ def do_filecrawl(path):
path=path, path=path,
) )
entry_lock = Lock() entry_lock = Lock()
@app.route("/view/<path:path>", methods=["GET", "PUT", "POST"]) @app.route("/view/<path:path>", methods=["GET", "PUT", "POST"])
def do_view(path): def do_view(path):
key = get_key(path) key = get_key(path)
print(f"view path={path}, dataset={key.dataset}, annotation_file= {key.annotation_file}, key={key.pathpart}") print(
f"view path={path}, dataset={key.dataset}, annotation_file= {key.annotation_file}, key={key.pathpart}"
)
with entry_lock: with entry_lock:
match = cache.check_entry(key) match = cache.check_entry(key)
if match is None: if match is None:
@@ -211,9 +254,12 @@ def do_view(path):
match.timestamp = current_time_stamp() match.timestamp = current_time_stamp()
if match.status == "loaded" or match.status == "loading": if (
match.status == CacheEntryStatus.loaded
or match.status == CacheEntryStatus.loading
):
return match.serve_content(path) return match.serve_content(path)
elif match.status == "error": elif match.status == CacheEntryStatus.error:
raise ProcessException.from_cache_entry(match) raise ProcessException.from_cache_entry(match)
@@ -221,16 +267,25 @@ def do_view(path):
def do_GET_status(): def do_GET_status():
return render_template("cache_status.html", entry_list=cache.entry_list) return render_template("cache_status.html", entry_list=cache.entry_list)
@app.route("/cache_status.json", methods=["GET"]) @app.route("/cache_status.json", methods=["GET"])
def do_GET_status_json(): def do_GET_status_json():
return json.dumps({'launchtime':app.launchtime, return json.dumps(
'entry_list':[{ {
'dataset': entry.key.dataset, "launchtime": app.launchtime,
'annotation_file': entry.key.annotation_file, "entry_list": [
'launchtime': entry.launchtime, {
'last_access': entry.timestamp, "dataset": entry.key.dataset,
'status': entry.status "annotation_file": entry.key.annotation_file,
} for entry in cache.entry_list]}) "launchtime": entry.launchtime,
"last_access": entry.timestamp,
"status": entry.status,
}
for entry in cache.entry_list
],
}
)
@app.route("/relaunch/<path:path>", methods=["GET"]) @app.route("/relaunch/<path:path>", methods=["GET"])
def do_relaunch(path): def do_relaunch(path):
@@ -239,7 +294,11 @@ def do_relaunch(path):
if not match is None: if not match is None:
match.terminate() match.terminate()
qs = request.query_string.decode() qs = request.query_string.decode()
return redirect(url_for("do_view", path=path) + (f'?{qs}' if len(qs) > 0 else ''), code=302) return redirect(
url_for("do_view", path=path) + (f"?{qs}" if len(qs) > 0 else ""),
code=302,
)
@app.route("/terminate/<path:path>", methods=["GET"]) @app.route("/terminate/<path:path>", methods=["GET"])
def do_terminate(path): def do_terminate(path):
@@ -250,8 +309,17 @@ def do_terminate(path):
return redirect(url_for("do_GET_status"), code=302) return redirect(url_for("do_GET_status"), code=302)
@app.route("/metadata/ip_address", methods=["GET"])
def ip_address():
resp = make_response(env.ip)
return set_no_cache(resp)
def main(): def main():
logging.basicConfig(level=logging.INFO, format='%(asctime)s:%(name)s:%(levelname)s:%(message)s') logging.basicConfig(
level=logging.INFO,
format="%(asctime)s:%(name)s:%(levelname)s:%(message)s",
)
env.validate() env.validate()
pruner = PruneProcessCache(cache) pruner = PruneProcessCache(cache)
+20 -8
View File
@@ -12,9 +12,10 @@ import os
from flask_api import status from flask_api import status
from cellxgene_gateway import env from cellxgene_gateway import env
from cellxgene_gateway.cache_key import CacheKey
from cellxgene_gateway.cellxgene_exception import CellxgeneException from cellxgene_gateway.cellxgene_exception import CellxgeneException
from cellxgene_gateway.dir_util import make_h5ad from cellxgene_gateway.dir_util import make_h5ad
from cellxgene_gateway.cache_key import CacheKey
def get_key(path): def get_key(path):
if path == "/" or path == "": if path == "/" or path == "":
@@ -25,33 +26,35 @@ def get_key(path):
trimmed = path[:-1] if path[-1] == "/" else path trimmed = path[:-1] if path[-1] == "/" else path
try: try:
# valid paths come in three forms: # valid paths come in three forms:
if trimmed.endswith('.h5ad') and data_file_exists(trimmed): if trimmed.endswith(".h5ad") and data_file_exists(trimmed):
# 1) somedir/dataset.h5ad: a dataset # 1) somedir/dataset.h5ad: a dataset
return CacheKey(trimmed, trimmed, None) return CacheKey(trimmed, trimmed, None)
elif trimmed.endswith('.csv'): elif trimmed.endswith(".csv"):
# 2) somedir/dataset_annotations/saldaal1-T5HMVBNV.csv : an actual annotations file. # 2) somedir/dataset_annotations/my_annotations.csv : an actual annotations file.
annotations_dir = os.path.split(trimmed)[0] annotations_dir = os.path.split(trimmed)[0]
dataset = make_h5ad(annotations_dir) dataset = make_h5ad(annotations_dir)
if data_file_exists(dataset): if data_file_exists(dataset):
data_dir_ensure(annotations_dir) data_dir_ensure(annotations_dir)
return CacheKey(trimmed, dataset, trimmed) return CacheKey(trimmed, dataset, trimmed)
elif trimmed.endswith('_annotations') and data_dir_exists(trimmed): elif trimmed.endswith("_annotations") and data_dir_exists(trimmed):
# 3) somedir/dataset_annotations: an annotation directory. The corresponding h5ad must exist, but the directory may not. # 3) somedir/dataset_annotations: an annotation directory. The corresponding h5ad must exist, but the directory may not.
dataset = make_h5ad(trimmed) dataset = make_h5ad(trimmed)
if data_file_exists(dataset): if data_file_exists(dataset):
return CacheKey(trimmed, dataset, '') return CacheKey(trimmed, dataset, "")
except CellxgeneException: except CellxgeneException:
pass pass
split = os.path.split(trimmed) split = os.path.split(trimmed)
return get_key(split[0]) return get_key(split[0])
def validate_exists(file_path): def validate_exists(file_path):
if not os.path.exists(file_path): if not os.path.exists(file_path):
raise CellxgeneException( raise CellxgeneException(
"File does not exist: " + file_path, status.HTTP_400_BAD_REQUEST "File does not exist: " + file_path, status.HTTP_400_BAD_REQUEST
) )
def validate_is_file(file_path): def validate_is_file(file_path):
validate_exists(file_path) validate_exists(file_path)
if not os.path.isfile(file_path): if not os.path.isfile(file_path):
@@ -59,6 +62,8 @@ def validate_is_file(file_path):
"Path is not file: " + file_path, status.HTTP_400_BAD_REQUEST "Path is not file: " + file_path, status.HTTP_400_BAD_REQUEST
) )
return return
def validate_is_dir(file_path): def validate_is_dir(file_path):
validate_exists(file_path) validate_exists(file_path)
if not os.path.isdir(file_path): if not os.path.isdir(file_path):
@@ -67,29 +72,36 @@ def validate_is_dir(file_path):
) )
return return
def data_file_exists(dataset): def data_file_exists(dataset):
file_path = os.path.join(env.cellxgene_data, dataset) file_path = os.path.join(env.cellxgene_data, dataset)
validate_is_file(file_path) validate_is_file(file_path)
return True return True
def data_dir_exists(dataset): def data_dir_exists(dataset):
file_path = os.path.join(env.cellxgene_data, dataset) file_path = os.path.join(env.cellxgene_data, dataset)
validate_is_dir(file_path) validate_is_dir(file_path)
return True return True
def data_dir_ensure(dataset): def data_dir_ensure(dataset):
file_path = os.path.join(env.cellxgene_data, dataset) file_path = os.path.join(env.cellxgene_data, dataset)
if not os.path.exists(file_path): if not os.path.exists(file_path):
os.makedirs(file_path) os.makedirs(file_path)
def get_file_path(key): def get_file_path(key):
dataset = key.dataset dataset = key.dataset
file_path = os.path.join(env.cellxgene_data, dataset) file_path = os.path.join(env.cellxgene_data, dataset)
validate_is_file(file_path) validate_is_file(file_path)
return file_path return file_path
def get_annotation_file_path(key): def get_annotation_file_path(key):
if key.annotation_file is None: if key.annotation_file is None:
return None return None
if key.annotation_file == '': if key.annotation_file == "":
return '' return ""
file_path = os.path.join(env.cellxgene_data, key.annotation_file) file_path = os.path.join(env.cellxgene_data, key.annotation_file)
return file_path return file_path
+24 -12
View File
@@ -7,17 +7,19 @@
# 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 time
import logging import logging
import time
from cellxgene_gateway.util import current_time_stamp from cellxgene_gateway import env
from cellxgene_gateway.env import ttl from cellxgene_gateway import util
logger = logging.getLogger(__name__)
class PruneProcessCache: class PruneProcessCache:
def __init__(self, cache): def __init__(self, cache):
self.cache = cache self.cache = cache
self.expire_seconds = (3600 if ttl is None else int(ttl)) self.expire_seconds = 3600 if env.ttl is None else int(env.ttl)
def __call__(self): def __call__(self):
while True: while True:
@@ -25,16 +27,26 @@ class PruneProcessCache:
self.prune() self.prune()
def prune(self): def prune(self):
timestamp = current_time_stamp() timestamp = util.current_time_stamp()
cutoff = timestamp - self.expire_seconds cutoff = timestamp - self.expire_seconds
processes_to_delete = [p for p in self.cache.entry_list if p.timestamp < cutoff] processes_to_delete = [
processes_to_keep = [p for p in self.cache.entry_list if not p.timestamp < cutoff] p for p in self.cache.entry_list if p.timestamp < cutoff
logger = logging.getLogger("cellxgene_gateway") ]
logger.debug(f"Cutoff {cutoff} = timestamp {timestamp} - expire seconds {self.expire_seconds} , keeping {processes_to_keep}") processes_to_keep = [
p for p in self.cache.entry_list if not p.timestamp < cutoff
]
logger.debug(
f"Cutoff {cutoff} = timestamp {timestamp} - expire seconds {self.expire_seconds} , keeping {processes_to_keep}, pruning {processes_to_delete}"
)
for process in processes_to_delete: for process in processes_to_delete:
try: try:
logger.info(f"pruning process {process.pid} ({process.key.dataset})") logger.info(
f"pruning process {process.pid} ({process.key.dataset})"
)
self.cache.prune(process) self.cache.prune(process)
except Exception: except Exception:
logger.exception("failed to prune process {process.pid} ({process.dataset})") logger.exception(
"failed to prune process {process.pid} ({process.dataset})"
)
+23 -7
View File
@@ -11,26 +11,38 @@ import logging
import subprocess import subprocess
from flask_api import status from flask_api import status
from cellxgene_gateway.env import enable_annotations, enable_backed_mode from cellxgene_gateway.cache_entry import CacheEntryStatus
from cellxgene_gateway.process_exception import ProcessException
from cellxgene_gateway.dir_util import make_annotations from cellxgene_gateway.dir_util import make_annotations
from cellxgene_gateway.path_util import get_file_path, get_annotation_file_path from cellxgene_gateway.path_util import get_annotation_file_path, get_file_path
from cellxgene_gateway.env import (
enable_annotations,
enable_backed_mode,
cellxgene_args,
)
from cellxgene_gateway.process_exception import ProcessException
class SubprocessBackend: class SubprocessBackend:
def __init__(self): def __init__(self):
pass pass
def create_cmd(self, cellxgene_loc, file_path, port, scripts, annotation_file_path): def create_cmd(
self, cellxgene_loc, file_path, port, scripts, annotation_file_path
):
if enable_annotations and not annotation_file_path is None: if enable_annotations and not annotation_file_path is None:
if annotation_file_path == "": if annotation_file_path == "":
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}"
else: else:
extra_args = " --disable-annotations" extra_args = " --disable-annotations"
if enable_backed_mode: if enable_backed_mode:
extra_args += " --backed" extra_args += " --backed"
if not cellxgene_args is None:
extra_args += f" {cellxgene_args}"
cmd = ( cmd = (
f"yes | {cellxgene_loc} launch {file_path}" f"yes | {cellxgene_loc} launch {file_path}"
+ " --port " + " --port "
@@ -47,7 +59,11 @@ class SubprocessBackend:
def launch(self, cellxgene_loc, scripts, cache_entry): def launch(self, cellxgene_loc, scripts, cache_entry):
cmd = self.create_cmd( cmd = self.create_cmd(
cellxgene_loc, get_file_path(cache_entry.key), cache_entry.port, scripts, get_annotation_file_path(cache_entry.key) cellxgene_loc,
get_file_path(cache_entry.key),
cache_entry.port,
scripts,
get_annotation_file_path(cache_entry.key),
) )
logging.getLogger("cellxgene_gateway").info(f"launching {cmd}") logging.getLogger("cellxgene_gateway").info(f"launching {cmd}")
process = subprocess.Popen( process = subprocess.Popen(
@@ -70,7 +86,7 @@ class SubprocessBackend:
message = "Cellxgene failed to launch dataset." message = "Cellxgene failed to launch dataset."
http_status = status.HTTP_500_INTERNAL_SERVER_ERROR http_status = status.HTTP_500_INTERNAL_SERVER_ERROR
cache_entry.status = "error" cache_entry.status = CacheEntryStatus.error
cache_entry.set_error(message, stderr, http_status) cache_entry.set_error(message, stderr, http_status)
raise ProcessException.from_cache_entry(cache_entry) raise ProcessException.from_cache_entry(cache_entry)
@@ -48,11 +48,11 @@
<td>{{ entry.port }}</td> <td>{{ entry.port }}</td>
<td class="timestamp">{{ entry.launchtime }}</td> <td class="timestamp">{{ entry.launchtime }}</td>
<td class="timestamp">{{ entry.timestamp }}</td> <td class="timestamp">{{ entry.timestamp }}</td>
<td>{{ entry.status }}</td> <td>{{ entry.status.name }}</td>
<td>{{ entry.message }}</td> <td>{{ entry.message }}</td>
<td>{{ entry.http_status }}</td> <td>{{ entry.http_status }}</td>
<td> <td>
{% if entry.status == 'loaded' %} {% if entry.status.name == 'loaded' %}
<a href="{{ url_for('do_terminate', path=entry.key.pathpart) }}"> terminate </a> <a href="{{ url_for('do_terminate', path=entry.key.pathpart) }}"> terminate </a>
{% endif %} {% endif %}
</td> </td>
@@ -28,11 +28,11 @@
<h4>{{ message }}</h4> <h4>{{ message }}</h4>
<a href="/filecrawl.html"> <a href="{{ url_for('filecrawl') }}">
Please click here to be redirected to the file directory. Please click here to be redirected to the file directory.
</a> </a>
<br> <br>
<a href="/"> <a href="{{ url_for('index') }}">
Please click here to return to the homepage. Please click here to return to the homepage.
</a> </a>
</div> </div>
+2 -2
View File
@@ -36,10 +36,10 @@
Navigation: Navigation:
<ul> <ul>
{% if path %} {% if path %}
<li><a href="/filecrawl.html">top level</a></li> <li><a href="{{ url_for('filecrawl') }}">top level</a></li>
{% else %} {% else %}
{% endif %} {% endif %}
<li><a href="/">homepage</a></li> <li><a href="{{ url_for('index') }}">homepage</a></li>
</ul> </ul>
</p> </p>
<script> <script>
+2 -2
View File
@@ -35,12 +35,12 @@
Links: Links:
</h1> </h1>
<div class="list-group" style="width:50%;padding-left:65px"> <div class="list-group" style="width:50%;padding-left:65px">
<a href="/filecrawl.html" class="list-group-item list-group-item-action"> <a href="{{ url_for('filecrawl') }}" class="list-group-item list-group-item-action">
<u>File Crawler: Allows you to view all uploaded data.</u></a> <u>File Crawler: Allows you to view all uploaded data.</u></a>
</div> </div>
<div class="list-group" style="width:50%;padding-left:65px"> <div class="list-group" style="width:50%;padding-left:65px">
<a href="/cache_status" class="list-group-item list-group-item-action"> <a href="{{ url_for('do_GET_status') }}" class="list-group-item list-group-item-action">
<u>Cache Status: view status of launched cellxgene servers.</u></a> <u>Cache Status: view status of launched cellxgene servers.</u></a>
</div> </div>
+2 -2
View File
@@ -35,11 +35,11 @@
The page will refresh shortly. The page will refresh shortly.
</p> </p>
<a href="/filecrawl.html"> <a href="{{ url_for('filecrawl') }}">
Please click here to be redirected to the file directory. Please click here to be redirected to the file directory.
</a> </a>
<br> <br>
<a href="/"> <a href="{{ url_for('index') }}">
Please click here to return to the homepage. Please click here to return to the homepage.
</a> </a>
</div> </div>
@@ -39,10 +39,10 @@
<li><a href="{{url_for('do_relaunch', path=dataset)}}"> <li><a href="{{url_for('do_relaunch', path=dataset)}}">
Attempt to relaunch the cellxgene server. Attempt to relaunch the cellxgene server.
</a></li> </a></li>
<li><a href="/filecrawl.html"> <li><a href="{{ url_for('filecrawl') }}">
Return to the file directory. Return to the file directory.
</a></li> </a></li>
<li><a href="/"> <li><a href="{{ url_for('index') }}">
Return to the homepage. Return to the homepage.
</a></li> </a></li>
</ul> </ul>
+3 -1
View File
@@ -1,4 +1,4 @@
name: cellxgene-dev name: cellxgene-gateway
channels: channels:
- conda-forge - conda-forge
dependencies: dependencies:
@@ -6,6 +6,8 @@ dependencies:
- requests - requests
- flask - flask
- psutil - psutil
- black
- pip
- pip: - pip:
- flask-api - flask-api
- cellxgene>=0.15 - cellxgene>=0.15
+14 -8
View File
@@ -3,22 +3,25 @@ import codecs
from setuptools import find_packages, setup from setuptools import find_packages, setup
import sys import sys
if sys.version_info < (3,6): if sys.version_info < (3, 6):
sys.exit('Sorry, Python < 3.6 is not supported') sys.exit("Sorry, Python < 3.6 is not supported")
def read(rel_path): def read(rel_path):
here = os.path.abspath(os.path.dirname(__file__)) here = os.path.abspath(os.path.dirname(__file__))
with codecs.open(os.path.join(here, rel_path), 'r') as fp: with codecs.open(os.path.join(here, rel_path), "r") as fp:
return fp.read() return fp.read()
def get_version(rel_path): def get_version(rel_path):
for line in read(rel_path).splitlines(): for line in read(rel_path).splitlines():
if line.startswith('__version__'): if line.startswith("__version__"):
delim = '"' if '"' in line else "'" delim = '"' if '"' in line else "'"
return line.split(delim)[1] return line.split(delim)[1]
else: else:
raise RuntimeError("Unable to find version string.") raise RuntimeError("Unable to find version string.")
def parse_requirements(): def parse_requirements():
reqs = [] reqs = []
with open("requirements.txt", "r") as f: with open("requirements.txt", "r") as f:
@@ -26,6 +29,7 @@ def parse_requirements():
reqs.append(l.strip("\n")) reqs.append(l.strip("\n"))
return reqs return reqs
with open("README.md", "r") as fh: with open("README.md", "r") as fh:
long_description = fh.read() long_description = fh.read()
@@ -49,14 +53,16 @@ setup(
package_data={ package_data={
"cellxgene_gateway": [ "cellxgene_gateway": [
"static/css/homepagestyle.css", "static/css/homepagestyle.css",
"static/js/annotation.js",
"static/nibr.ico", "static/nibr.ico",
"templates/*.html" "templates/*.html",
]}, ]
data_files=[('', ['README.md', 'LICENSE'])], },
data_files=[("", ["README.md", "LICENSE"])],
install_requires=install_reqs, install_requires=install_reqs,
entry_points={ entry_points={
"console_scripts": ["cellxgene-gateway=cellxgene_gateway.gateway:main"] "console_scripts": ["cellxgene-gateway=cellxgene_gateway.gateway:main"]
}, },
classifiers=["Topic :: Scientific/Engineering :: Visualization"], classifiers=["Topic :: Scientific/Engineering :: Visualization"],
python_requires='>=3.6', python_requires=">=3.6",
) )
+37
View File
@@ -0,0 +1,37 @@
import unittest
from flask import Flask
from cellxgene_gateway.cache_entry import CacheEntry, CacheEntryStatus
from cellxgene_gateway.cache_key import CacheKey
from cellxgene_gateway.gateway import app
key = CacheKey("czi/pbmc3k.h5ad", "pbmc3k.h5ad", "tmp.csv")
class TestRenderEntry(unittest.TestCase):
def setUp(self):
self.app = app
self.app_context = self.app.test_request_context()
self.app_context.push()
self.client = self.app.test_client()
def test_GIVEN_key_and_port_THEN_returns_loading_CacheEntry(self):
entry = CacheEntry.for_key("some-key", 1)
self.assertEqual(entry.status, CacheEntryStatus.loading)
def test_GIVEN_absolute_static_url_THEN_include_path(self):
actual = CacheEntry.for_key(key, 8000).rewrite_text_content(
"src:url(/static/assets/"
)
expected = "src:url(/view/czi/pbmc3k.h5ad/static/assets/"
self.assertEqual(actual, expected)
def test_GIVEN_absolute_src_THEN_include_path(self):
actual = CacheEntry.for_key(key, 8000).rewrite_text_content(
'<link rel="shortcut icon" href="/static/assets/favicon.ico">'
)
expected = '<link rel="shortcut icon" href="/view/czi/pbmc3k.h5ad/static/assets/favicon.ico">'
self.assertEqual(actual, expected)
if __name__ == "__main__":
unittest.main()
+33 -22
View File
@@ -1,38 +1,49 @@
import unittest import unittest
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from cellxgene_gateway.dir_util import render_entry from cellxgene_gateway.filecrawl import render_entry
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 = { entry = {
"path": "/somepath/", "path": "/somepath/",
"name": "entry", "name": "entry",
"type": "file", "type": "file",
} "annotations": [],
"children": [],
}
rendered = render_entry(entry) rendered = render_entry(entry)
self.assertIn('view/somepath', rendered) self.assertIn("view/somepath", 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 = { entry = {
"path": "/somepath", "path": "/somepath",
"name": "entry", "name": "entry",
"type": "file", "type": "file",
} "annotations": [],
"children": [],
}
rendered = render_entry(entry) rendered = render_entry(entry)
self.assertIn('view/somepath', rendered) self.assertIn("view/somepath", 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 = { entry = {
"path": "somepath/", "path": "somepath/",
"name": "entry", "name": "entry",
"type": "file", "type": "file",
} "annotations": [],
"children": [],
}
rendered = render_entry(entry) rendered = render_entry(entry)
self.assertIn('view/somepath', rendered) self.assertIn("view/somepath", 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 = { entry = {
"path": "somepath", "path": "somepath",
"name": "entry", "name": "entry",
"type": "file", "type": "file",
} "annotations": [],
"children": [],
}
rendered = render_entry(entry) rendered = render_entry(entry)
self.assertIn('view/somepath', rendered) self.assertIn("view/somepath", rendered)
+11 -8
View File
@@ -1,23 +1,26 @@
import unittest import unittest
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from cellxgene_gateway.extra_scripts import get_extra_scripts from cellxgene_gateway.extra_scripts import get_extra_scripts
class TestExtraScripts(unittest.TestCase): class TestExtraScripts(unittest.TestCase):
@patch('cellxgene_gateway.env.extra_scripts', new='["abc","def"]') @patch("cellxgene_gateway.env.extra_scripts", new='["abc","def"]')
def test_GIVEN_two_scripts_THEN_returns_two_strings(self): def test_GIVEN_two_scripts_THEN_returns_two_strings(self):
self.assertEqual(get_extra_scripts(), ['abc', 'def']) self.assertEqual(get_extra_scripts(), ["abc", "def"])
@patch('cellxgene_gateway.env.extra_scripts', new='["abc", "def"]') @patch("cellxgene_gateway.env.extra_scripts", new='["abc", "def"]')
def test_GIVEN_two_scripts_space_THEN_returns_two_strings(self): def test_GIVEN_two_scripts_space_THEN_returns_two_strings(self):
self.assertEqual(get_extra_scripts(), ['abc', 'def']) self.assertEqual(get_extra_scripts(), ["abc", "def"])
@patch('cellxgene_gateway.env.extra_scripts', new=None) @patch("cellxgene_gateway.env.extra_scripts", new=None)
def test_GIVEN_none_THEN_returns_empty_array(self): def test_GIVEN_none_THEN_returns_empty_array(self):
self.assertEqual(get_extra_scripts(), []) self.assertEqual(get_extra_scripts(), [])
@patch('cellxgene_gateway.env.extra_scripts', new='[]') @patch("cellxgene_gateway.env.extra_scripts", new="[]")
def test_GIVEN_empty_string_THEN_returns_empty_array(self): def test_GIVEN_empty_string_THEN_returns_empty_array(self):
self.assertEqual(get_extra_scripts(), []) self.assertEqual(get_extra_scripts(), [])
if __name__ == '__main__':
unittest.main() if __name__ == "__main__":
unittest.main()
+46
View File
@@ -0,0 +1,46 @@
import unittest
from unittest.mock import MagicMock, patch
from cellxgene_gateway.filecrawl import render_entry
class TestRenderEntry(unittest.TestCase):
def test_GIVEN_path_both_slash_THEN_view_has_single_slash(self):
entry = {
"path": "/somepath/",
"name": "entry",
"type": "file",
"annotations": [],
}
rendered = render_entry(entry)
self.assertIn("view/somepath", rendered)
def test_GIVEN_path_starts_slash_THEN_view_has_single_slash(self):
entry = {
"path": "/somepath",
"name": "entry",
"type": "file",
"annotations": [],
}
rendered = render_entry(entry)
self.assertIn("view/somepath", rendered)
def test_GIVEN_path_ends_slash_THEN_view_has_single_slash(self):
entry = {
"path": "somepath/",
"name": "entry",
"type": "file",
"annotations": [],
}
rendered = render_entry(entry)
self.assertIn("view/somepath", rendered)
def test_GIVEN_path_no_slash_THEN_view_has_single_slash(self):
entry = {
"path": "somepath",
"name": "entry",
"type": "file",
"annotations": [],
}
rendered = render_entry(entry)
self.assertIn("view/somepath", rendered)
+10 -7
View File
@@ -1,13 +1,15 @@
import unittest import unittest
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from cellxgene_gateway.cache_entry import CacheEntry
from cellxgene_gateway.backend_cache import BackendCache from cellxgene_gateway.backend_cache import BackendCache
from cellxgene_gateway.cache_entry import CacheEntry
class TestPruneProcessCache(unittest.TestCase): class TestPruneProcessCache(unittest.TestCase):
@patch('cellxgene_gateway.util.current_time_stamp', new=lambda:0) @patch("cellxgene_gateway.util.current_time_stamp", new=lambda: 0)
@patch('cellxgene_gateway.env.ttl', new='10') @patch("cellxgene_gateway.env.ttl", new="10")
@patch('cellxgene_gateway.cache_entry.CacheEntry') @patch("cellxgene_gateway.cache_entry.CacheEntry")
@patch('cellxgene_gateway.cache_entry.CacheEntry') @patch("cellxgene_gateway.cache_entry.CacheEntry")
def test_GIVEN_one_old_one_new_THEN_prune_old(self, old, new): def test_GIVEN_one_old_one_new_THEN_prune_old(self, old, new):
from cellxgene_gateway.prune_process_cache import PruneProcessCache from cellxgene_gateway.prune_process_cache import PruneProcessCache
@@ -22,5 +24,6 @@ class TestPruneProcessCache(unittest.TestCase):
self.assertEqual(len(cache.entry_list), 1) self.assertEqual(len(cache.entry_list), 1)
self.assertEqual(cache.entry_list[0], new) self.assertEqual(cache.entry_list[0], new)
if __name__ == '__main__':
unittest.main() if __name__ == "__main__":
unittest.main()