mirror of
https://github.com/Novartis/cellxgene-gateway.git
synced 2026-10-10 06:10:55 +08:00
Moved prometheus monitoring into flask blueprint
This commit is contained in:
committed by
Alok Saldanha
parent
d5008893f7
commit
0f79e1a010
@@ -186,6 +186,21 @@ pip install isort flake8 black
|
|||||||
isort -rc . # rc means recursive, and was deprecated in dev version of isort
|
isort -rc . # rc means recursive, and was deprecated in dev version of isort
|
||||||
black .
|
black .
|
||||||
```
|
```
|
||||||
|
## Dependency management
|
||||||
|
|
||||||
|
We use the following files for dependency management:
|
||||||
|
|
||||||
|
* environment.yml - specifies a conda environment sufficient to run the packages
|
||||||
|
* setup.cfg - lists static dependency information, both minimal and extra dependencies
|
||||||
|
* setup.py - dynamically generated package information
|
||||||
|
* requirements.txt - simple wrapper invoking setup.py
|
||||||
|
|
||||||
|
For more details, see the folloiwng links:
|
||||||
|
|
||||||
|
* https://towardsdatascience.com/setuptools-python-571e7d5500f2
|
||||||
|
* https://towardsdatascience.com/requirements-vs-setuptools-python-ae3ee66e28af
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Getting Help
|
# Getting Help
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ def url(endpoint, descriptor, source_name):
|
|||||||
|
|
||||||
|
|
||||||
def view_url(descriptor, source_name):
|
def view_url(descriptor, source_name):
|
||||||
return url("do_view", descriptor, source_name)
|
return url("gateway_blueprint.do_view", descriptor, source_name)
|
||||||
|
|
||||||
|
|
||||||
def relaunch_url(descriptor, source_name):
|
def relaunch_url(descriptor, source_name):
|
||||||
return url("do_relaunch", descriptor, source_name)
|
return url("gateway_blueprint.do_relaunch", descriptor, source_name)
|
||||||
|
|||||||
+24
-274
@@ -1,50 +1,15 @@
|
|||||||
# Copyright 2019 Novartis Institutes for BioMedical Research Inc. Licensed
|
|
||||||
# under the Apache License, Version 2.0 (the "License"); you may not use
|
|
||||||
# this file except in compliance with the License. You may obtain a copy
|
|
||||||
# of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless
|
|
||||||
# required by applicable law or agreed to in writing, software distributed
|
|
||||||
# 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
|
|
||||||
# the specific language governing permissions and limitations under the License.
|
|
||||||
# import BaseHTTPServer
|
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import urllib.parse
|
|
||||||
from threading import Lock, Thread
|
|
||||||
|
|
||||||
from flask import (
|
import typer
|
||||||
Flask,
|
from flask import Flask
|
||||||
make_response,
|
|
||||||
redirect,
|
|
||||||
render_template,
|
|
||||||
request,
|
|
||||||
send_from_directory,
|
|
||||||
url_for,
|
|
||||||
)
|
|
||||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
from prometheus_flask_exporter import PrometheusMetrics
|
|
||||||
|
|
||||||
from cellxgene_gateway import env, flask_util
|
from cellxgene_gateway import env, flask_util, gateway_blueprint
|
||||||
from cellxgene_gateway.backend_cache import BackendCache
|
|
||||||
from cellxgene_gateway.cache_entry import CacheEntryStatus
|
|
||||||
from cellxgene_gateway.cache_key import CacheKey
|
|
||||||
from cellxgene_gateway.cellxgene_exception import CellxgeneException
|
|
||||||
from cellxgene_gateway.extra_scripts import get_extra_scripts
|
|
||||||
from cellxgene_gateway.filecrawl import render_item_source
|
|
||||||
from cellxgene_gateway.process_exception import ProcessException
|
|
||||||
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 import __version__
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
metrics = PrometheusMetrics(app)
|
|
||||||
metrics.info('app_info', 'Application info', version=__version__)
|
|
||||||
|
|
||||||
item_sources = []
|
|
||||||
default_item_source = None
|
|
||||||
|
|
||||||
|
|
||||||
def _force_https(app):
|
def _force_https(app):
|
||||||
def wrapper(environ, start_response):
|
def wrapper(environ, start_response):
|
||||||
@@ -55,14 +20,6 @@ def _force_https(app):
|
|||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
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.wsgi_app = _force_https(app.wsgi_app)
|
app.wsgi_app = _force_https(app.wsgi_app)
|
||||||
if (
|
if (
|
||||||
env.proxy_fix_for > 0
|
env.proxy_fix_for > 0
|
||||||
@@ -80,229 +37,8 @@ if (
|
|||||||
x_prefix=env.proxy_fix_prefix,
|
x_prefix=env.proxy_fix_prefix,
|
||||||
)
|
)
|
||||||
|
|
||||||
cache = BackendCache()
|
|
||||||
|
|
||||||
|
def main(prometheus: bool = False):
|
||||||
@app.errorhandler(CellxgeneException)
|
|
||||||
def handle_invalid_usage(error):
|
|
||||||
|
|
||||||
message = f"{error.http_status} Error : {error.message}"
|
|
||||||
|
|
||||||
return (
|
|
||||||
render_template(
|
|
||||||
"cellxgene_error.html",
|
|
||||||
extra_scripts=get_extra_scripts(),
|
|
||||||
message=message,
|
|
||||||
),
|
|
||||||
error.http_status,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(ProcessException)
|
|
||||||
def handle_invalid_process(error):
|
|
||||||
|
|
||||||
message = []
|
|
||||||
|
|
||||||
message.append(error.message)
|
|
||||||
message.append(f"{error.http_status} Error.")
|
|
||||||
message.append(f"Stdout: {error.stdout}")
|
|
||||||
message.append(f"Stderr: {error.stderr}")
|
|
||||||
|
|
||||||
return (
|
|
||||||
render_template(
|
|
||||||
"process_error.html",
|
|
||||||
extra_scripts=get_extra_scripts(),
|
|
||||||
message=error.message,
|
|
||||||
http_status=error.http_status,
|
|
||||||
stdout=error.stdout,
|
|
||||||
stderr=error.stderr,
|
|
||||||
relaunch_url=error.key.relaunch_url(),
|
|
||||||
annotation_file=error.key.annotation_descriptor,
|
|
||||||
),
|
|
||||||
error.http_status,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/favicon.ico")
|
|
||||||
def favicon():
|
|
||||||
return send_from_directory(
|
|
||||||
os.path.join(app.root_path, "static"),
|
|
||||||
"nibr.ico",
|
|
||||||
mimetype="image/vnd.microsof.icon",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
|
||||||
def index():
|
|
||||||
return render_template(
|
|
||||||
"index.html",
|
|
||||||
ip=env.ip,
|
|
||||||
cellxgene_data=env.cellxgene_data,
|
|
||||||
extra_scripts=get_extra_scripts(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/filecrawl.html")
|
|
||||||
@app.route("/filecrawl/<path:path>")
|
|
||||||
def filecrawl(path=None):
|
|
||||||
source_name = request.args.get("source")
|
|
||||||
sources = (
|
|
||||||
filter(
|
|
||||||
lambda x: x.name == urllib.parse.unquote_plus(source_name),
|
|
||||||
item_sources,
|
|
||||||
)
|
|
||||||
if source_name
|
|
||||||
else item_sources
|
|
||||||
)
|
|
||||||
# loop all data sources --
|
|
||||||
rendered_sources = [
|
|
||||||
render_item_source(item_source, path) for item_source in sources
|
|
||||||
] # will we need to make this async in the page???
|
|
||||||
rendered_html = "\n".join(rendered_sources)
|
|
||||||
|
|
||||||
resp = make_response(
|
|
||||||
render_template(
|
|
||||||
"filecrawl.html",
|
|
||||||
extra_scripts=get_extra_scripts(),
|
|
||||||
rendered_html=rendered_html,
|
|
||||||
path=path,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
set_no_cache(resp)
|
|
||||||
return resp
|
|
||||||
|
|
||||||
|
|
||||||
entry_lock = Lock()
|
|
||||||
|
|
||||||
|
|
||||||
def matching_source(source_name):
|
|
||||||
if source_name is None:
|
|
||||||
source_name = default_item_source.name
|
|
||||||
matching = [i for i in item_sources if i.name == source_name]
|
|
||||||
if len(matching) != 1:
|
|
||||||
raise Exception(f"Could not find matching item source {source_name}")
|
|
||||||
source = matching[0]
|
|
||||||
return source
|
|
||||||
|
|
||||||
|
|
||||||
@app.route(
|
|
||||||
"/source/<path:source_name>/view/<path:path>",
|
|
||||||
methods=["GET", "PUT", "POST"],
|
|
||||||
)
|
|
||||||
@app.route("/view/<path:path>", methods=["GET", "PUT", "POST"])
|
|
||||||
def do_view(path, source_name=None):
|
|
||||||
source = matching_source(source_name)
|
|
||||||
match = cache.check_path(source, path)
|
|
||||||
|
|
||||||
if match is None:
|
|
||||||
lookup = source.lookup(path)
|
|
||||||
if lookup is None:
|
|
||||||
raise CellxgeneException(
|
|
||||||
f"Could not find item for path {path} in source {source.name}",
|
|
||||||
404,
|
|
||||||
)
|
|
||||||
key = CacheKey.for_lookup(source, lookup)
|
|
||||||
print(
|
|
||||||
f"view path={path}, source_name={source_name}, dataset={key.file_path}, annotation_file= {key.annotation_file_path}, key={key.descriptor}, source={key.source_name}"
|
|
||||||
)
|
|
||||||
with entry_lock:
|
|
||||||
match = cache.check_entry(key)
|
|
||||||
if match is None:
|
|
||||||
uascripts = get_extra_scripts()
|
|
||||||
match = cache.create_entry(key, uascripts)
|
|
||||||
|
|
||||||
match.timestamp = current_time_stamp()
|
|
||||||
|
|
||||||
if (
|
|
||||||
match.status == CacheEntryStatus.loaded
|
|
||||||
or match.status == CacheEntryStatus.loading
|
|
||||||
):
|
|
||||||
if source.is_authorized(match.key.descriptor):
|
|
||||||
return match.serve_content(path)
|
|
||||||
else:
|
|
||||||
raise CellxgeneException("User not authorized to access this data", 403)
|
|
||||||
elif match.status == CacheEntryStatus.error:
|
|
||||||
raise ProcessException.from_cache_entry(match)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/cache_status", methods=["GET"])
|
|
||||||
def do_GET_status():
|
|
||||||
return render_template(
|
|
||||||
"cache_status.html",
|
|
||||||
entry_list=cache.entry_list,
|
|
||||||
extra_scripts=get_extra_scripts(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/cache_status.json", methods=["GET"])
|
|
||||||
def do_GET_status_json():
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"launchtime": app.launchtime,
|
|
||||||
"entry_list": [
|
|
||||||
{
|
|
||||||
"dataset": entry.key.dataset,
|
|
||||||
"annotation_file": entry.key.annotation_file,
|
|
||||||
"launchtime": entry.launchtime,
|
|
||||||
"last_access": entry.timestamp,
|
|
||||||
"status": entry.status,
|
|
||||||
}
|
|
||||||
for entry in cache.entry_list
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/relaunch/<path:path>", methods=["GET"])
|
|
||||||
def do_relaunch(path):
|
|
||||||
source_name = request.args.get("source_name") or default_item_source.name
|
|
||||||
source = matching_source(source_name)
|
|
||||||
key = CacheKey.for_lookup(source, source.lookup(path))
|
|
||||||
match = cache.check_entry(key)
|
|
||||||
if not match is None:
|
|
||||||
match.terminate()
|
|
||||||
return redirect(
|
|
||||||
key.view_url,
|
|
||||||
code=302,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/terminate/<path:path>", methods=["GET"])
|
|
||||||
def do_terminate(path):
|
|
||||||
source_name = request.args.get("source_name") or default_item_source.name
|
|
||||||
source = matching_source(source_name)
|
|
||||||
key = CacheKey.for_lookup(source, source.lookup(path))
|
|
||||||
match = cache.check_entry(key)
|
|
||||||
if not match is None:
|
|
||||||
match.terminate()
|
|
||||||
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 launch():
|
|
||||||
env.validate()
|
|
||||||
if not item_sources or not len(item_sources):
|
|
||||||
raise Exception("No data sources specified for Cellxgene Gateway")
|
|
||||||
|
|
||||||
global default_item_source
|
|
||||||
if default_item_source is None:
|
|
||||||
default_item_source = item_sources[0]
|
|
||||||
|
|
||||||
pruner = PruneProcessCache(cache)
|
|
||||||
|
|
||||||
background_thread = Thread(target=pruner)
|
|
||||||
background_thread.start()
|
|
||||||
|
|
||||||
app.launchtime = current_time_stamp()
|
|
||||||
app.run(host="0.0.0.0", port=env.gateway_port, debug=False)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=env.log_level,
|
level=env.log_level,
|
||||||
format="%(asctime)s:%(name)s:%(levelname)s:%(message)s",
|
format="%(asctime)s:%(name)s:%(levelname)s:%(message)s",
|
||||||
@@ -313,19 +49,33 @@ def main():
|
|||||||
if cellxgene_bucket is not None:
|
if cellxgene_bucket is not None:
|
||||||
from cellxgene_gateway.items.s3.s3item_source import S3ItemSource
|
from cellxgene_gateway.items.s3.s3item_source import S3ItemSource
|
||||||
|
|
||||||
item_sources.append(S3ItemSource(cellxgene_bucket, name="s3"))
|
gateway_blueprint.item_sources.append(S3ItemSource(cellxgene_bucket, name="s3"))
|
||||||
default_item_source = "s3"
|
default_item_source = "s3"
|
||||||
if cellxgene_data is not None:
|
if cellxgene_data is not None:
|
||||||
from cellxgene_gateway.items.file.fileitem_source import FileItemSource
|
from cellxgene_gateway.items.file.fileitem_source import FileItemSource
|
||||||
|
|
||||||
item_sources.append(FileItemSource(cellxgene_data, name="local"))
|
gateway_blueprint.item_sources.append(
|
||||||
|
FileItemSource(cellxgene_data, name="local")
|
||||||
|
)
|
||||||
default_item_source = "local"
|
default_item_source = "local"
|
||||||
if len(item_sources) == 0:
|
if len(gateway_blueprint.item_sources) == 0:
|
||||||
raise Exception("Please specify CELLXGENE_DATA or CELLXGENE_BUCKET")
|
raise Exception("Please specify CELLXGENE_DATA or CELLXGENE_BUCKET")
|
||||||
flask_util.include_source_in_url = len(item_sources) > 1
|
flask_util.include_source_in_url = len(gateway_blueprint.item_sources) > 1
|
||||||
|
|
||||||
launch()
|
if prometheus:
|
||||||
|
from cellxgene_gateway.prometheus import add_metrics
|
||||||
|
|
||||||
|
add_metrics(app)
|
||||||
|
app.register_blueprint(gateway_blueprint.gateway_blueprint)
|
||||||
|
gateway_blueprint.launch()
|
||||||
|
|
||||||
|
app.launchtime = current_time_stamp()
|
||||||
|
app.run(host="0.0.0.0", port=env.gateway_port, debug=False)
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
typer.run(main)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
run()
|
||||||
|
|||||||
@@ -0,0 +1,266 @@
|
|||||||
|
# Copyright 2019 Novartis Institutes for BioMedical Research Inc. Licensed
|
||||||
|
# under the Apache License, Version 2.0 (the "License"); you may not use
|
||||||
|
# this file except in compliance with the License. You may obtain a copy
|
||||||
|
# of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless
|
||||||
|
# required by applicable law or agreed to in writing, software distributed
|
||||||
|
# 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
|
||||||
|
# the specific language governing permissions and limitations under the License.
|
||||||
|
# import BaseHTTPServer
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import urllib.parse
|
||||||
|
from threading import Lock, Thread
|
||||||
|
|
||||||
|
from flask import (
|
||||||
|
Blueprint,
|
||||||
|
current_app,
|
||||||
|
make_response,
|
||||||
|
redirect,
|
||||||
|
render_template,
|
||||||
|
request,
|
||||||
|
send_from_directory,
|
||||||
|
url_for,
|
||||||
|
)
|
||||||
|
|
||||||
|
from cellxgene_gateway import env
|
||||||
|
from cellxgene_gateway.backend_cache import BackendCache
|
||||||
|
from cellxgene_gateway.cache_entry import CacheEntryStatus
|
||||||
|
from cellxgene_gateway.cache_key import CacheKey
|
||||||
|
from cellxgene_gateway.cellxgene_exception import CellxgeneException
|
||||||
|
from cellxgene_gateway.extra_scripts import get_extra_scripts
|
||||||
|
from cellxgene_gateway.filecrawl import render_item_source
|
||||||
|
from cellxgene_gateway.process_exception import ProcessException
|
||||||
|
from cellxgene_gateway.prune_process_cache import PruneProcessCache
|
||||||
|
from cellxgene_gateway.util import current_time_stamp
|
||||||
|
|
||||||
|
gateway_blueprint = Blueprint("gateway_blueprint", __name__)
|
||||||
|
item_sources = []
|
||||||
|
default_item_source = None
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
cache = BackendCache()
|
||||||
|
|
||||||
|
|
||||||
|
@gateway_blueprint.errorhandler(CellxgeneException)
|
||||||
|
def handle_invalid_usage(error):
|
||||||
|
|
||||||
|
message = f"{error.http_status} Error : {error.message}"
|
||||||
|
|
||||||
|
return (
|
||||||
|
render_template(
|
||||||
|
"cellxgene_error.html",
|
||||||
|
extra_scripts=get_extra_scripts(),
|
||||||
|
message=message,
|
||||||
|
),
|
||||||
|
error.http_status,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@gateway_blueprint.errorhandler(ProcessException)
|
||||||
|
def handle_invalid_process(error):
|
||||||
|
|
||||||
|
message = []
|
||||||
|
|
||||||
|
message.append(error.message)
|
||||||
|
message.append(f"{error.http_status} Error.")
|
||||||
|
message.append(f"Stdout: {error.stdout}")
|
||||||
|
message.append(f"Stderr: {error.stderr}")
|
||||||
|
|
||||||
|
return (
|
||||||
|
render_template(
|
||||||
|
"process_error.html",
|
||||||
|
extra_scripts=get_extra_scripts(),
|
||||||
|
message=error.message,
|
||||||
|
http_status=error.http_status,
|
||||||
|
stdout=error.stdout,
|
||||||
|
stderr=error.stderr,
|
||||||
|
relaunch_url=error.key.relaunch_url(),
|
||||||
|
annotation_file=error.key.annotation_descriptor,
|
||||||
|
),
|
||||||
|
error.http_status,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@gateway_blueprint.route("/favicon.ico")
|
||||||
|
def favicon():
|
||||||
|
return send_from_directory(
|
||||||
|
os.path.join(current_app.root_path, "static"),
|
||||||
|
"nibr.ico",
|
||||||
|
mimetype="image/vnd.microsof.icon",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@gateway_blueprint.route("/")
|
||||||
|
def index():
|
||||||
|
return render_template(
|
||||||
|
"index.html",
|
||||||
|
ip=env.ip,
|
||||||
|
cellxgene_data=env.cellxgene_data,
|
||||||
|
extra_scripts=get_extra_scripts(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@gateway_blueprint.route("/filecrawl.html")
|
||||||
|
@gateway_blueprint.route("/filecrawl/<path:path>")
|
||||||
|
def filecrawl(path=None):
|
||||||
|
source_name = request.args.get("source")
|
||||||
|
sources = (
|
||||||
|
filter(
|
||||||
|
lambda x: x.name == urllib.parse.unquote_plus(source_name),
|
||||||
|
item_sources,
|
||||||
|
)
|
||||||
|
if source_name
|
||||||
|
else item_sources
|
||||||
|
)
|
||||||
|
# loop all data sources --
|
||||||
|
rendered_sources = [
|
||||||
|
render_item_source(item_source, path) for item_source in sources
|
||||||
|
] # will we need to make this async in the page???
|
||||||
|
rendered_html = "\n".join(rendered_sources)
|
||||||
|
|
||||||
|
resp = make_response(
|
||||||
|
render_template(
|
||||||
|
"filecrawl.html",
|
||||||
|
extra_scripts=get_extra_scripts(),
|
||||||
|
rendered_html=rendered_html,
|
||||||
|
path=path,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
set_no_cache(resp)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
entry_lock = Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def matching_source(source_name):
|
||||||
|
if source_name is None:
|
||||||
|
source_name = default_item_source.name
|
||||||
|
matching = [i for i in item_sources if i.name == source_name]
|
||||||
|
if len(matching) != 1:
|
||||||
|
raise Exception(f"Could not find matching item source {source_name}")
|
||||||
|
source = matching[0]
|
||||||
|
return source
|
||||||
|
|
||||||
|
|
||||||
|
@gateway_blueprint.route(
|
||||||
|
"/source/<path:source_name>/view/<path:path>",
|
||||||
|
methods=["GET", "PUT", "POST"],
|
||||||
|
)
|
||||||
|
@gateway_blueprint.route("/view/<path:path>", methods=["GET", "PUT", "POST"])
|
||||||
|
def do_view(path, source_name=None):
|
||||||
|
source = matching_source(source_name)
|
||||||
|
match = cache.check_path(source, path)
|
||||||
|
|
||||||
|
if match is None:
|
||||||
|
lookup = source.lookup(path)
|
||||||
|
if lookup is None:
|
||||||
|
raise CellxgeneException(
|
||||||
|
f"Could not find item for path {path} in source {source.name}",
|
||||||
|
404,
|
||||||
|
)
|
||||||
|
key = CacheKey.for_lookup(source, lookup)
|
||||||
|
print(
|
||||||
|
f"view path={path}, source_name={source_name}, dataset={key.file_path}, annotation_file= {key.annotation_file_path}, key={key.descriptor}, source={key.source_name}"
|
||||||
|
)
|
||||||
|
with entry_lock:
|
||||||
|
match = cache.check_entry(key)
|
||||||
|
if match is None:
|
||||||
|
uascripts = get_extra_scripts()
|
||||||
|
match = cache.create_entry(key, uascripts)
|
||||||
|
|
||||||
|
match.timestamp = current_time_stamp()
|
||||||
|
|
||||||
|
if (
|
||||||
|
match.status == CacheEntryStatus.loaded
|
||||||
|
or match.status == CacheEntryStatus.loading
|
||||||
|
):
|
||||||
|
if source.is_authorized(match.key.descriptor):
|
||||||
|
return match.serve_content(path)
|
||||||
|
else:
|
||||||
|
raise CellxgeneException("User not authorized to access this data", 403)
|
||||||
|
elif match.status == CacheEntryStatus.error:
|
||||||
|
raise ProcessException.from_cache_entry(match)
|
||||||
|
|
||||||
|
|
||||||
|
@gateway_blueprint.route("/cache_status", methods=["GET"])
|
||||||
|
def do_GET_status():
|
||||||
|
return render_template(
|
||||||
|
"cache_status.html",
|
||||||
|
entry_list=cache.entry_list,
|
||||||
|
extra_scripts=get_extra_scripts(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@gateway_blueprint.route("/cache_status.json", methods=["GET"])
|
||||||
|
def do_GET_status_json():
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"launchtime": current_app.launchtime,
|
||||||
|
"entry_list": [
|
||||||
|
{
|
||||||
|
"dataset": entry.key.dataset,
|
||||||
|
"annotation_file": entry.key.annotation_file,
|
||||||
|
"launchtime": entry.launchtime,
|
||||||
|
"last_access": entry.timestamp,
|
||||||
|
"status": entry.status,
|
||||||
|
}
|
||||||
|
for entry in cache.entry_list
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@gateway_blueprint.route("/relaunch/<path:path>", methods=["GET"])
|
||||||
|
def do_relaunch(path):
|
||||||
|
source_name = request.args.get("source_name") or default_item_source.name
|
||||||
|
source = matching_source(source_name)
|
||||||
|
key = CacheKey.for_lookup(source, source.lookup(path))
|
||||||
|
match = cache.check_entry(key)
|
||||||
|
if not match is None:
|
||||||
|
match.terminate()
|
||||||
|
return redirect(
|
||||||
|
key.view_url,
|
||||||
|
code=302,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@gateway_blueprint.route("/terminate/<path:path>", methods=["GET"])
|
||||||
|
def do_terminate(path):
|
||||||
|
source_name = request.args.get("source_name") or default_item_source.name
|
||||||
|
source = matching_source(source_name)
|
||||||
|
key = CacheKey.for_lookup(source, source.lookup(path))
|
||||||
|
match = cache.check_entry(key)
|
||||||
|
if not match is None:
|
||||||
|
match.terminate()
|
||||||
|
return redirect(url_for("gateway_blueprint.do_GET_status"), code=302)
|
||||||
|
|
||||||
|
|
||||||
|
@gateway_blueprint.route("/metadata/ip_address", methods=["GET"])
|
||||||
|
def ip_address():
|
||||||
|
resp = make_response(env.ip)
|
||||||
|
return set_no_cache(resp)
|
||||||
|
|
||||||
|
|
||||||
|
def launch():
|
||||||
|
env.validate()
|
||||||
|
if not item_sources or not len(item_sources):
|
||||||
|
raise Exception("No data sources specified for Cellxgene Gateway")
|
||||||
|
|
||||||
|
global default_item_source
|
||||||
|
if default_item_source is None:
|
||||||
|
default_item_source = item_sources[0]
|
||||||
|
|
||||||
|
pruner = PruneProcessCache(cache)
|
||||||
|
|
||||||
|
background_thread = Thread(target=pruner)
|
||||||
|
background_thread.start()
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
from prometheus_flask_exporter import PrometheusMetrics
|
||||||
|
|
||||||
|
from cellxgene_gateway import __version__
|
||||||
|
|
||||||
|
|
||||||
|
def add_metrics(app):
|
||||||
|
metrics = PrometheusMetrics(app)
|
||||||
|
metrics.info("app_info", "Application info", version=__version__)
|
||||||
|
return metrics
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
<td>
|
<td>
|
||||||
{% if entry.status.name == 'loaded' %}
|
{% if entry.status.name == 'loaded' %}
|
||||||
<a
|
<a
|
||||||
href="{{ url_for('do_terminate', path=entry.key.descriptor, source_name=entry.key.source_name) }}">
|
href="{{ url_for('gateway_blueprint.do_terminate', path=entry.key.descriptor, source_name=entry.key.source_name) }}">
|
||||||
terminate </a>
|
terminate </a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -28,11 +28,11 @@
|
|||||||
|
|
||||||
<h4>{{ message }}</h4>
|
<h4>{{ message }}</h4>
|
||||||
|
|
||||||
<a href="{{ url_for('filecrawl') }}">
|
<a href="{{ url_for('gateway_blueprint.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="{{ url_for('index') }}">
|
<a href="{{ url_for('gateway_blueprint.index') }}">
|
||||||
Please click here to return to the homepage.
|
Please click here to return to the homepage.
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,10 +36,10 @@
|
|||||||
Navigation:
|
Navigation:
|
||||||
<ul>
|
<ul>
|
||||||
{% if path %}
|
{% if path %}
|
||||||
<li><a href="{{ url_for('filecrawl') }}">top level</a></li>
|
<li><a href="{{ url_for('gateway_blueprint.filecrawl') }}">top level</a></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="{{ url_for('index') }}">homepage</a></li>
|
<li><a href="{{ url_for('gateway_blueprint.index') }}">homepage</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</p>
|
</p>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -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="{{ url_for('filecrawl') }}" class="list-group-item list-group-item-action">
|
<a href="{{ url_for('gateway_blueprint.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="{{ url_for('do_GET_status') }}" class="list-group-item list-group-item-action">
|
<a href="{{ url_for('gateway_blueprint.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>
|
||||||
|
|
||||||
|
|||||||
@@ -35,11 +35,11 @@
|
|||||||
The page will refresh shortly.
|
The page will refresh shortly.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<a href="{{ url_for('filecrawl') }}">
|
<a href="{{ url_for('gateway_blueprint.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="{{ url_for('index') }}">
|
<a href="{{ url_for('gateway_blueprint.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="{{ relaunch_url }}">
|
<li><a href="{{ relaunch_url }}">
|
||||||
Attempt to relaunch the cellxgene server.
|
Attempt to relaunch the cellxgene server.
|
||||||
</a></li>
|
</a></li>
|
||||||
<li><a href="{{ url_for('filecrawl') }}">
|
<li><a href="{{ url_for('gateway_blueprint.filecrawl') }}">
|
||||||
Return to the file directory.
|
Return to the file directory.
|
||||||
</a></li>
|
</a></li>
|
||||||
<li><a href="{{ url_for('index') }}">
|
<li><a href="{{ url_for('gateway_blueprint.index') }}">
|
||||||
Return to the homepage.
|
Return to the homepage.
|
||||||
</a></li>
|
</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ dependencies:
|
|||||||
- flask
|
- flask
|
||||||
- psutil
|
- psutil
|
||||||
- black
|
- black
|
||||||
|
- typer
|
||||||
- twine
|
- twine
|
||||||
- isort
|
- isort
|
||||||
- coverage
|
- coverage
|
||||||
@@ -16,3 +17,4 @@ dependencies:
|
|||||||
- flask-api
|
- flask-api
|
||||||
- werkzeug
|
- werkzeug
|
||||||
- cellxgene
|
- cellxgene
|
||||||
|
- prometheus-flask-exporter
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ werkzeug
|
|||||||
psutil
|
psutil
|
||||||
requests
|
requests
|
||||||
prometheus-flask-exporter
|
prometheus-flask-exporter
|
||||||
|
typer
|
||||||
@@ -61,9 +61,7 @@ setup(
|
|||||||
},
|
},
|
||||||
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.cli:run"]},
|
||||||
"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",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import MagicMock, Mock, patch
|
from unittest.mock import MagicMock, Mock, patch
|
||||||
|
|
||||||
from cellxgene_gateway.gateway import app
|
from flask import Flask
|
||||||
|
|
||||||
|
from cellxgene_gateway.gateway_blueprint import gateway_blueprint
|
||||||
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
|
||||||
@@ -82,6 +84,8 @@ class TestScanDirectory(unittest.TestCase):
|
|||||||
|
|
||||||
s3func.return_value = S3Mock
|
s3func.return_value = S3Mock
|
||||||
source = S3ItemSource("my-bucket")
|
source = S3ItemSource("my-bucket")
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.register_blueprint(gateway_blueprint)
|
||||||
with app.test_request_context(query_string="refresh=true") as test_context:
|
with app.test_request_context(query_string="refresh=true") as test_context:
|
||||||
tree = source.scan_directory()
|
tree = source.scan_directory()
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ 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.gateway_blueprint import gateway_blueprint
|
||||||
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
|
from cellxgene_gateway.items.item import ItemType
|
||||||
@@ -18,7 +18,8 @@ key = CacheKey(
|
|||||||
|
|
||||||
class TestRenderEntry(unittest.TestCase):
|
class TestRenderEntry(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.app = app
|
self.app = Flask(__name__)
|
||||||
|
self.app.register_blueprint(gateway_blueprint)
|
||||||
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()
|
||||||
|
|||||||
Reference in New Issue
Block a user