mirror of
https://github.com/Novartis/cellxgene-gateway.git
synced 2026-10-09 06:38:12 +08:00
Merge pull request #99 from andynu/gunicorn-support
Gunicorn support for now. Will revisit populating item sources on module load to improve reusability of module (specifically in tests for now).
This commit is contained in:
@@ -55,6 +55,7 @@ jobs:
|
|||||||
coverage xml -i
|
coverage xml -i
|
||||||
|
|
||||||
- name: "Upload coverage to Codecov"
|
- name: "Upload coverage to Codecov"
|
||||||
|
if: ${{ github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||||
uses: codecov/codecov-action@v1
|
uses: codecov/codecov-action@v1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ class CacheEntry:
|
|||||||
headers = {}
|
headers = {}
|
||||||
copy_headers = [
|
copy_headers = [
|
||||||
"accept",
|
"accept",
|
||||||
"accept-encoding",
|
# "accept-encoding" - removed: let requests library handle compression/decompression
|
||||||
"accept-language",
|
"accept-language",
|
||||||
"cache-control",
|
"cache-control",
|
||||||
"connection",
|
"connection",
|
||||||
@@ -168,9 +168,8 @@ class CacheEntry:
|
|||||||
headers[h] = request.headers[h]
|
headers[h] = request.headers[h]
|
||||||
|
|
||||||
full_path = self.cellxgene_basepath() + subpath + querystring()
|
full_path = self.cellxgene_basepath() + subpath + querystring()
|
||||||
|
cellxgene_response = None
|
||||||
try:
|
try:
|
||||||
cellxgene_response = None
|
|
||||||
if request.method in ["GET", "HEAD", "OPTIONS"]:
|
if request.method in ["GET", "HEAD", "OPTIONS"]:
|
||||||
cellxgene_response = get(full_path, headers=headers)
|
cellxgene_response = get(full_path, headers=headers)
|
||||||
elif request.method == "PUT":
|
elif request.method == "PUT":
|
||||||
|
|||||||
@@ -78,6 +78,44 @@ if (
|
|||||||
cache = BackendCache()
|
cache = BackendCache()
|
||||||
|
|
||||||
|
|
||||||
|
# Initialize data sources - this is defined later in the file but called here
|
||||||
|
# to ensure initialization happens when WSGI servers (Gunicorn) import the module
|
||||||
|
def initialize_data_sources():
|
||||||
|
"""Initialize data sources from environment variables.
|
||||||
|
Called at module import time for WSGI server compatibility (Gunicorn).
|
||||||
|
Uses a guard flag to prevent double initialization within a process."""
|
||||||
|
global default_item_source
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=env.log_level,
|
||||||
|
format="%(asctime)s:%(name)s:%(levelname)s:%(message)s",
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
cellxgene_data = os.environ.get("CELLXGENE_DATA", None)
|
||||||
|
cellxgene_bucket = os.environ.get("CELLXGENE_BUCKET", None)
|
||||||
|
|
||||||
|
if cellxgene_bucket is not None:
|
||||||
|
from cellxgene_gateway.items.s3.s3item_source import S3ItemSource
|
||||||
|
|
||||||
|
s3_source = S3ItemSource(cellxgene_bucket, name="s3")
|
||||||
|
item_sources.append(s3_source)
|
||||||
|
default_item_source = s3_source
|
||||||
|
logger.info("Initialized S3 data source")
|
||||||
|
logger.debug(f"S3 bucket: {cellxgene_bucket}")
|
||||||
|
if cellxgene_data is not None:
|
||||||
|
from cellxgene_gateway.items.file.fileitem_source import FileItemSource
|
||||||
|
|
||||||
|
file_source = FileItemSource(cellxgene_data, name="local")
|
||||||
|
item_sources.append(file_source)
|
||||||
|
default_item_source = file_source
|
||||||
|
logger.info("Initialized local file data source")
|
||||||
|
logger.debug(f"Data directory: {cellxgene_data}")
|
||||||
|
if len(item_sources) == 0:
|
||||||
|
raise Exception("Please specify CELLXGENE_DATA or CELLXGENE_BUCKET")
|
||||||
|
flask_util.include_source_in_url = len(item_sources) > 1
|
||||||
|
|
||||||
|
|
||||||
@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}"
|
||||||
@@ -295,28 +333,14 @@ def launch():
|
|||||||
app.run(host="0.0.0.0", port=env.gateway_port, debug=False)
|
app.run(host="0.0.0.0", port=env.gateway_port, debug=False)
|
||||||
|
|
||||||
|
|
||||||
|
# When using servers like Gunicorn or uWSGI, this file is imported rather than run directly.
|
||||||
|
# As a result, the main() function is never called automatically.
|
||||||
|
# Therefore, we must initialize the data sources at import time to ensure they are available.
|
||||||
|
initialize_data_sources()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
logging.basicConfig(
|
"""CLI entry point for Flask development server."""
|
||||||
level=env.log_level,
|
|
||||||
format="%(asctime)s:%(name)s:%(levelname)s:%(message)s",
|
|
||||||
)
|
|
||||||
cellxgene_data = os.environ.get("CELLXGENE_DATA", None)
|
|
||||||
cellxgene_bucket = os.environ.get("CELLXGENE_BUCKET", None)
|
|
||||||
|
|
||||||
if cellxgene_bucket is not None:
|
|
||||||
from cellxgene_gateway.items.s3.s3item_source import S3ItemSource
|
|
||||||
|
|
||||||
item_sources.append(S3ItemSource(cellxgene_bucket, name="s3"))
|
|
||||||
default_item_source = "s3"
|
|
||||||
if cellxgene_data is not None:
|
|
||||||
from cellxgene_gateway.items.file.fileitem_source import FileItemSource
|
|
||||||
|
|
||||||
item_sources.append(FileItemSource(cellxgene_data, name="local"))
|
|
||||||
default_item_source = "local"
|
|
||||||
if len(item_sources) == 0:
|
|
||||||
raise Exception("Please specify CELLXGENE_DATA or CELLXGENE_BUCKET")
|
|
||||||
flask_util.include_source_in_url = len(item_sources) > 1
|
|
||||||
|
|
||||||
launch()
|
launch()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,32 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
|
||||||
from unittest.mock import MagicMock, Mock, patch
|
from unittest.mock import MagicMock, Mock, patch
|
||||||
|
|
||||||
from cellxgene_gateway.gateway import app
|
|
||||||
from cellxgene_gateway.items.item import ItemType
|
from cellxgene_gateway.items.item import ItemType
|
||||||
from cellxgene_gateway.items.s3.s3item import S3Item
|
from cellxgene_gateway.items.s3.s3item import S3Item
|
||||||
from cellxgene_gateway.items.s3.s3item_source import S3ItemSource
|
from cellxgene_gateway.items.s3.s3item_source import S3ItemSource
|
||||||
|
|
||||||
|
|
||||||
class TestScanDirectory(unittest.TestCase):
|
class TestScanDirectory(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self._tmpdir = tempfile.mkdtemp()
|
||||||
|
self._cellxgene_data = os.environ.get("CELLXGENE_DATA", "")
|
||||||
|
os.environ["CELLXGENE_DATA"] = self._tmpdir
|
||||||
|
|
||||||
|
from cellxgene_gateway.gateway import app
|
||||||
|
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
if self._cellxgene_data:
|
||||||
|
os.environ["CELLXGENE_DATA"] = self._cellxgene_data
|
||||||
|
else:
|
||||||
|
del os.environ["CELLXGENE_DATA"]
|
||||||
|
shutil.rmtree(self._tmpdir)
|
||||||
|
|
||||||
@patch("s3fs.S3FileSystem")
|
@patch("s3fs.S3FileSystem")
|
||||||
def test_GIVEN_invalid_bucket_THEN_throws_error(self, s3func):
|
def test_GIVEN_invalid_bucket_THEN_throws_error(self, s3func):
|
||||||
class S3Mock:
|
class S3Mock:
|
||||||
@@ -26,6 +45,7 @@ class TestScanDirectory(unittest.TestCase):
|
|||||||
|
|
||||||
@patch("s3fs.S3FileSystem")
|
@patch("s3fs.S3FileSystem")
|
||||||
def test__GIVEN_multilevel_bucket_THEN_properly_recurses_suburls(self, s3func):
|
def test__GIVEN_multilevel_bucket_THEN_properly_recurses_suburls(self, s3func):
|
||||||
|
|
||||||
class S3Mock:
|
class S3Mock:
|
||||||
def exists(path):
|
def exists(path):
|
||||||
if path in [
|
if path in [
|
||||||
@@ -82,7 +102,7 @@ class TestScanDirectory(unittest.TestCase):
|
|||||||
|
|
||||||
s3func.return_value = S3Mock
|
s3func.return_value = S3Mock
|
||||||
source = S3ItemSource("my-bucket")
|
source = S3ItemSource("my-bucket")
|
||||||
with app.test_request_context(query_string="refresh=true") as test_context:
|
with self.app.test_request_context(query_string="refresh=true") as test_context:
|
||||||
tree = source.scan_directory()
|
tree = source.scan_directory()
|
||||||
|
|
||||||
def s3item_compare(i1, i2, msg=""):
|
def s3item_compare(i1, i2, msg=""):
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
from cellxgene_gateway import flask_util
|
from cellxgene_gateway import flask_util
|
||||||
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
|
||||||
from cellxgene_gateway.gateway import app
|
from cellxgene_gateway.items.item import ItemType
|
||||||
from cellxgene_gateway.items.file.fileitem import FileItem
|
from cellxgene_gateway.items.file.fileitem import FileItem
|
||||||
from cellxgene_gateway.items.file.fileitem_source import FileItemSource
|
from cellxgene_gateway.items.file.fileitem_source import FileItemSource
|
||||||
from cellxgene_gateway.items.item import ItemType
|
|
||||||
|
|
||||||
key = CacheKey(
|
key = CacheKey(
|
||||||
FileItem("/czi/", name="pbmc3k.h5ad", type=ItemType.h5ad),
|
FileItem("/czi/", name="pbmc3k.h5ad", type=ItemType.h5ad),
|
||||||
@@ -18,11 +19,25 @@ key = CacheKey(
|
|||||||
|
|
||||||
class TestRenderEntry(unittest.TestCase):
|
class TestRenderEntry(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
self._tmpdir = tempfile.mkdtemp()
|
||||||
|
self._cellxgene_data = os.environ.get("CELLXGENE_DATA", "")
|
||||||
|
os.environ["CELLXGENE_DATA"] = self._tmpdir
|
||||||
|
|
||||||
|
from cellxgene_gateway.gateway import app
|
||||||
|
|
||||||
self.app = app
|
self.app = app
|
||||||
self.app_context = self.app.test_request_context()
|
self.app_context = self.app.test_request_context()
|
||||||
self.app_context.push()
|
self.app_context.push()
|
||||||
self.client = self.app.test_client()
|
self.client = self.app.test_client()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.app_context.pop()
|
||||||
|
if self._cellxgene_data:
|
||||||
|
os.environ["CELLXGENE_DATA"] = self._cellxgene_data
|
||||||
|
else:
|
||||||
|
del os.environ["CELLXGENE_DATA"]
|
||||||
|
shutil.rmtree(self._tmpdir)
|
||||||
|
|
||||||
def test_GIVEN_key_and_port_THEN_returns_loading_CacheEntry(self):
|
def test_GIVEN_key_and_port_THEN_returns_loading_CacheEntry(self):
|
||||||
entry = CacheEntry.for_key("some-key", 1)
|
entry = CacheEntry.for_key("some-key", 1)
|
||||||
self.assertEqual(entry.status, CacheEntryStatus.loading)
|
self.assertEqual(entry.status, CacheEntryStatus.loading)
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
@@ -25,6 +28,25 @@ def make_entry(subpath="somepath", annotations=None):
|
|||||||
|
|
||||||
|
|
||||||
class TestRenderEntry(unittest.TestCase):
|
class TestRenderEntry(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self._tmpdir = tempfile.mkdtemp()
|
||||||
|
self._cellxgene_data = os.environ.get("CELLXGENE_DATA", "")
|
||||||
|
os.environ["CELLXGENE_DATA"] = self._tmpdir
|
||||||
|
|
||||||
|
from cellxgene_gateway.gateway import app
|
||||||
|
|
||||||
|
self.app = app
|
||||||
|
self.app_context = self.app.test_request_context()
|
||||||
|
self.app_context.push()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.app_context.pop()
|
||||||
|
if self._cellxgene_data:
|
||||||
|
os.environ["CELLXGENE_DATA"] = self._cellxgene_data
|
||||||
|
else:
|
||||||
|
del os.environ["CELLXGENE_DATA"]
|
||||||
|
shutil.rmtree(self._tmpdir)
|
||||||
|
|
||||||
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 = make_entry(subpath="/somepath/")
|
entry = make_entry(subpath="/somepath/")
|
||||||
rendered = render_item(entry, source)
|
rendered = render_item(entry, source)
|
||||||
@@ -47,6 +69,26 @@ class TestRenderEntry(unittest.TestCase):
|
|||||||
|
|
||||||
|
|
||||||
class TestRenderAnnotation(unittest.TestCase):
|
class TestRenderAnnotation(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self._tmpdir = tempfile.mkdtemp()
|
||||||
|
self._cellxgene_data = os.environ.get("CELLXGENE_DATA", "")
|
||||||
|
os.environ["CELLXGENE_DATA"] = self._tmpdir
|
||||||
|
|
||||||
|
from cellxgene_gateway.gateway import app
|
||||||
|
|
||||||
|
self.app = app
|
||||||
|
self.app_context = self.app.test_request_context()
|
||||||
|
self.app_context.push()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.app_context.pop()
|
||||||
|
if self._cellxgene_data:
|
||||||
|
os.environ["CELLXGENE_DATA"] = self._cellxgene_data
|
||||||
|
else:
|
||||||
|
del os.environ["CELLXGENE_DATA"]
|
||||||
|
shutil.rmtree(self._tmpdir)
|
||||||
|
|
||||||
@patch("cellxgene_gateway.filecrawl.enable_annotations", new=True)
|
@patch("cellxgene_gateway.filecrawl.enable_annotations", new=True)
|
||||||
def test_GIVEN_no_annotation_THEN_new_alone(self):
|
def test_GIVEN_no_annotation_THEN_new_alone(self):
|
||||||
entry = make_entry(annotations=None)
|
entry = make_entry(annotations=None)
|
||||||
@@ -103,12 +145,24 @@ class TestRenderItemSource(unittest.TestCase):
|
|||||||
|
|
||||||
class TestRenderItemTree(unittest.TestCase):
|
class TestRenderItemTree(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
self._tmpdir = tempfile.mkdtemp()
|
||||||
|
self._cellxgene_data = os.environ.get("CELLXGENE_DATA", "")
|
||||||
|
os.environ["CELLXGENE_DATA"] = self._tmpdir
|
||||||
|
|
||||||
from cellxgene_gateway.gateway import app
|
from cellxgene_gateway.gateway import app
|
||||||
|
|
||||||
self.app = app
|
self.app = app
|
||||||
self.app_context = self.app.test_request_context()
|
self.app_context = self.app.test_request_context()
|
||||||
self.app_context.push()
|
self.app_context.push()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.app_context.pop()
|
||||||
|
if self._cellxgene_data:
|
||||||
|
os.environ["CELLXGENE_DATA"] = self._cellxgene_data
|
||||||
|
else:
|
||||||
|
del os.environ["CELLXGENE_DATA"]
|
||||||
|
shutil.rmtree(self._tmpdir)
|
||||||
|
|
||||||
@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"
|
||||||
|
|||||||
Reference in New Issue
Block a user