10 Commits

Author SHA1 Message Date
Alok Saldanha
f296efcc55 #79 added cellxgene-data directory so it actually works 2022-12-21 16:02:50 -05:00
Alok Saldanha
facfb27d5c #79 add simple example to customize cellxgene-gateway ui 2022-12-21 15:42:13 -05:00
Alok Saldanha
390fe24ea4 prepare for 0.3.10 release 2022-06-20 21:50:42 -04:00
Alok Saldanha
590565bea2 Merge pull request #69 from Novartis/68_read_from_subprocess
68 read from subprocess
2022-06-20 21:50:28 -04:00
Alok Saldanha
d32a31e855 #68 read process output until it exits 2022-06-20 21:46:19 -04:00
Alok Saldanha
0cd551382e #65 add environment variable to control how long cellxgene processes can remain idle 2022-06-20 21:46:19 -04:00
Alok Saldanha
36c0a4d3d7 #68 add param to set log level 2022-06-20 21:29:19 -04:00
Alok Saldanha
8d8a0a3483 #68 close responses 2022-06-20 21:29:19 -04:00
Alok Saldanha
eaa157079c Merge pull request #67 from Novartis/docker
Remove version pins to upgrade Flask
2022-06-07 12:06:49 -04:00
Alok Saldanha
c5f3c68740 Merge pull request #66 from romanhaa/docker
Dockerise cellxgene-gateway
2022-06-07 06:04:13 -04:00
12 changed files with 96 additions and 44 deletions

View File

@@ -1,3 +1,10 @@
# 0.3.10
* #65 Added GATEWAY_EXPIRE_SECONDS to set how long cellxgene servers can remain idle before being terminated.
* Added GATEWAY_LOG_LEVEL to set the log level
* #68 Close connections after reading response
* #68 Background thread reads from output of cellxgene process until it exits
# 0.3.9 # 0.3.9
* Added S3_ENABLE_LISTINGS_CACHE variable (See README.md) * Added S3_ENABLE_LISTINGS_CACHE variable (See README.md)

View File

@@ -73,9 +73,11 @@ Optional environment variables:
* `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())`
* `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_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.
* `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.
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.

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.9" __version__ = "0.3.10"

View File

@@ -9,7 +9,6 @@
import datetime import datetime
import logging import logging
import re import re
import urllib.parse
from enum import Enum from enum import Enum
import psutil import psutil
@@ -22,6 +21,8 @@ from cellxgene_gateway.cellxgene_exception import CellxgeneException
from cellxgene_gateway.flask_util import querystring from cellxgene_gateway.flask_util import querystring
from cellxgene_gateway.util import current_time_stamp from cellxgene_gateway.util import current_time_stamp
logger = logging.getLogger(__name__)
class CacheEntryStatus(Enum): class CacheEntryStatus(Enum):
loaded = "loaded" loaded = "loaded"
@@ -111,7 +112,7 @@ class CacheEntry:
except psutil.NoSuchProcess: except psutil.NoSuchProcess:
pass pass
logging.getLogger("cellxgene_gateway").info(f"terminated {terminated}") logger.info(f"terminated {terminated}")
self.status = CacheEntryStatus.terminated self.status = CacheEntryStatus.terminated
def rewrite_text_content(self, cellxgene_content): def rewrite_text_content(self, cellxgene_content):
@@ -169,38 +170,43 @@ class CacheEntry:
full_path = self.cellxgene_basepath() + subpath + querystring() full_path = self.cellxgene_basepath() + subpath + querystring()
if request.method in ["GET", "HEAD", "OPTIONS"]: try:
cellxgene_response = get(full_path, headers=headers) cellxgene_response = None
elif request.method == "PUT": if request.method in ["GET", "HEAD", "OPTIONS"]:
cellxgene_response = put( cellxgene_response = get(full_path, headers=headers)
full_path, elif request.method == "PUT":
headers=headers, cellxgene_response = put(
data=request.data, full_path,
) headers=headers,
elif request.method == "POST": data=request.data,
cellxgene_response = post( )
full_path, elif request.method == "POST":
headers=headers, cellxgene_response = post(
data=request.data, full_path,
) headers=headers,
else: data=request.data,
raise CellxgeneException(f"Unexpected method {request.method}", 400) )
content_type = cellxgene_response.headers["content-type"] else:
if "text" in content_type: raise CellxgeneException(f"Unexpected method {request.method}", 400)
gateway_content = self.rewrite_text_content( content_type = cellxgene_response.headers["content-type"]
cellxgene_response.content.decode() if "text" in content_type:
) gateway_content = self.rewrite_text_content(
else: cellxgene_response.content.decode()
gateway_content = cellxgene_response.content )
else:
gateway_content = cellxgene_response.content
resp_headers = {} resp_headers = {}
for h in copy_headers: for h in copy_headers:
if h in cellxgene_response.headers: if h in cellxgene_response.headers:
resp_headers[h] = cellxgene_response.headers[h] resp_headers[h] = cellxgene_response.headers[h]
gateway_response = make_response( gateway_response = make_response(
gateway_content, gateway_content,
cellxgene_response.status_code, cellxgene_response.status_code,
resp_headers, resp_headers,
) )
finally:
if cellxgene_response is not None:
cellxgene_response.close()
return gateway_response return gateway_response

View File

@@ -9,7 +9,6 @@
import logging import logging
import os import os
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", "")
@@ -24,7 +23,9 @@ external_protocol = os.environ.get(
) )
ip = os.environ.get("GATEWAY_IP") ip = os.environ.get("GATEWAY_IP")
extra_scripts = os.environ.get("GATEWAY_EXTRA_SCRIPTS") extra_scripts = os.environ.get("GATEWAY_EXTRA_SCRIPTS")
ttl = os.environ.get("GATEWAY_TTL") expire_seconds = int(
os.environ.get("GATEWAY_EXPIRE_SECONDS", os.environ.get("GATEWAY_TTL", "3600"))
)
enable_annotations = os.environ.get("GATEWAY_ENABLE_ANNOTATIONS", "").lower() in [ enable_annotations = os.environ.get("GATEWAY_ENABLE_ANNOTATIONS", "").lower() in [
"true", "true",
"1", "1",
@@ -33,6 +34,7 @@ enable_backed_mode = os.environ.get("GATEWAY_ENABLE_BACKED_MODE", "").lower() in
"true", "true",
"1", "1",
] ]
log_level = logging.getLevelName(os.environ.get("GATEWAY_LOG_LEVEL", "INFO"))
env_vars = { env_vars = {
"CELLXGENE_LOCATION": cellxgene_location, "CELLXGENE_LOCATION": cellxgene_location,
@@ -50,9 +52,10 @@ optional_env_vars = {
"GATEWAY_IP": ip, "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_EXPIRE_SECONDS": expire_seconds,
"GATEWAY_ENABLE_ANNOTATIONS": enable_annotations, "GATEWAY_ENABLE_ANNOTATIONS": enable_annotations,
"GATEWAY_ENABLE_BACKED_MODE": enable_backed_mode, "GATEWAY_ENABLE_BACKED_MODE": enable_backed_mode,
"GATEWAY_LOG_LEVEL": log_level,
"CELLXGENE_ARGS": cellxgene_args, "CELLXGENE_ARGS": cellxgene_args,
"CELLXGENE_DATA": cellxgene_data, "CELLXGENE_DATA": cellxgene_data,
"PROXY_FIX_FOR": proxy_fix_for, "PROXY_FIX_FOR": proxy_fix_for,

View File

@@ -299,7 +299,7 @@ def launch():
def main(): def main():
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=env.log_level,
format="%(asctime)s:%(name)s:%(levelname)s:%(message)s", format="%(asctime)s:%(name)s:%(levelname)s:%(message)s",
) )
cellxgene_data = os.environ.get("CELLXGENE_DATA", None) cellxgene_data = os.environ.get("CELLXGENE_DATA", None)

View File

@@ -18,7 +18,7 @@ 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 env.ttl is None else int(env.ttl) self.expire_seconds = env.expire_seconds
def __call__(self): def __call__(self):
while True: while True:

View File

@@ -17,6 +17,8 @@ from cellxgene_gateway.dir_util import make_annotations
from cellxgene_gateway.env import cellxgene_args, enable_annotations, enable_backed_mode from cellxgene_gateway.env import cellxgene_args, enable_annotations, enable_backed_mode
from cellxgene_gateway.process_exception import ProcessException from cellxgene_gateway.process_exception import ProcessException
logger = logging.getLogger(__name__)
class SubprocessBackend: class SubprocessBackend:
def __init__(self): def __init__(self):
@@ -55,7 +57,7 @@ class SubprocessBackend:
scripts, scripts,
cache_entry.key.annotation_file_path, cache_entry.key.annotation_file_path,
) )
logging.getLogger("cellxgene_gateway").info(f"launching {cmd}") logger.info(f"launching {cmd}")
process = subprocess.Popen( process = subprocess.Popen(
[cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True [cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True
) )
@@ -84,5 +86,6 @@ class SubprocessBackend:
cache_entry.append_output(output) cache_entry.append_output(output)
cache_entry.set_loaded(process.pid) cache_entry.set_loaded(process.pid)
for output in process.communicate():
return logger.debug(f"cellxgene:{output}")
logger.info(f"exiting {cmd}")

View File

@@ -0,0 +1,14 @@
FROM python:3.9
RUN pip install cellxgene-gateway 'MarkupSafe<2.1'
COPY customize_ui.sh customize_ui.sh
RUN CELLXGENE_GATEWAY_DIR=/usr/local/lib/python3.9/site-packages/cellxgene_gateway . ./customize_ui.sh
ENV CELLXGENE_DATA=/cellxgene-data
ENV CELLXGENE_LOCATION=/usr/local/bin/cellxgene
EXPOSE 5005
RUN mkdir /cellxgene-data
CMD ["cellxgene-gateway"]

View File

@@ -0,0 +1,14 @@
# Purpose
This is a simple example of how to make a small script to customize the UI of cellxgene-gateway. The script that does the customization is `customize_ui.sh`, it simply makes the main header green using CSS but you could do anything you want there (including adding more script tags, etc).
# Usage
```
docker build -t cellxgene_custom .
CELLXGENE_DATA=`pwd`/../../../cellxgene_data
docker run -p 5005:5005 --mount src=$CELLXGENE_DATA,target=/cellxgene-data,type=bind cellxgene_custom
```
If you now open http://localhost:5005 you should see a green cellxgene gateway header.

View File

@@ -0,0 +1,3 @@
# make the header bright green
find "${CELLXGENE_GATEWAY_DIR}/templates" -name index.html -exec sed -i -e 's/<head>/<head>\
> <style> header h3 {color: #0F0;} <\/style>/g' {} \;

View File

@@ -15,7 +15,7 @@ key = CacheKey(
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.expire_seconds", 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):