mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-16 21:37:59 +08:00
This splits the backend into two parts: the local backend for desktop cellxgene and the AWS backend for hosted cellxgene. The local backend is in local_server while the hosted remains in server. The general idea is to copy everything from server to local_server, pull unneeded stuff out of local_server, and keep server as-is for this PR. Not touching server means all the infra and deployment code will continue working just as it did before so we can make those changes incrementally.
119 lines
3.8 KiB
Python
119 lines
3.8 KiB
Python
import contextlib
|
|
import errno
|
|
import importlib.util
|
|
import logging
|
|
import os
|
|
import pkgutil
|
|
import socket
|
|
from urllib.parse import urlsplit, urljoin
|
|
|
|
import numpy as np
|
|
from flask import json
|
|
|
|
from local_server.common.errors import ConfigurationError
|
|
|
|
|
|
def find_available_port(host, port=5005):
|
|
"""
|
|
Helper method to find open port on host. Tries 5000 ports incremented from the specified port
|
|
"""
|
|
# Takes approx 2 seconds to do a scan of 5000 ports on my laptop
|
|
num_ports_to_try = 5000
|
|
for port_to_try in range(port, port + num_ports_to_try):
|
|
if is_port_available(host, port_to_try):
|
|
return port_to_try
|
|
raise socket.error(errno.EADDRINUSE, f"No port in range {port} - {port + num_ports_to_try - 1} available.")
|
|
|
|
|
|
def is_port_available(host, port):
|
|
is_available = False
|
|
with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
|
|
try:
|
|
s.bind((host, port))
|
|
is_available = True
|
|
except socket.error:
|
|
pass
|
|
return is_available
|
|
|
|
|
|
def sort_options(command):
|
|
"""
|
|
Helper for the click options - will sort options in a command, and can
|
|
be used as a decorator.
|
|
"""
|
|
command.params.sort(key=lambda p: p.name)
|
|
return command
|
|
|
|
|
|
def path_join(base, *urls):
|
|
"""
|
|
this is like urllib.parse.urljoin, except it works around the scheme-specific
|
|
cleverness in the aforementioned code, ignores anything in the url except the path,
|
|
and accepts more than one url.
|
|
"""
|
|
if not base.endswith("/"):
|
|
base += "/"
|
|
btpl = urlsplit(base)
|
|
path = btpl.path
|
|
for url in urls:
|
|
utpl = urlsplit(url)
|
|
if btpl.scheme == "":
|
|
path = os.path.join(path, utpl.path)
|
|
path = os.path.normpath(path)
|
|
else:
|
|
path = urljoin(path, utpl.path)
|
|
return btpl._replace(path=path).geturl()
|
|
|
|
|
|
class Float32JSONEncoder(json.JSONEncoder):
|
|
def __init__(self, *args, **kwargs):
|
|
"""
|
|
NaN/Infinities are illegal in standard JSON. Python extends JSON with
|
|
non-standard symbols that most JavaScript JSON parsers do not understand.
|
|
The `allow_nan` parameter will force Python simplejson to throw an ValueError
|
|
if it runs into non-finite floating point values which are unsupported by
|
|
standard JSON.
|
|
"""
|
|
kwargs["allow_nan"] = False
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def default(self, obj):
|
|
if isinstance(obj, np.float32):
|
|
return float(obj)
|
|
elif isinstance(obj, np.integer):
|
|
return int(obj)
|
|
return json.JSONEncoder.default(self, obj)
|
|
|
|
|
|
def custom_format_warning(msg, *args, **kwargs):
|
|
return f"[cellxgene] Warning: {msg} \n"
|
|
|
|
|
|
def jsonify_numpy(data):
|
|
return json.dumps(data, cls=Float32JSONEncoder, allow_nan=False)
|
|
|
|
|
|
def import_plugins(plugin_module):
|
|
"""
|
|
Load optional plugin modules from local_server.common.plugins
|
|
|
|
If you would like to customize cellxgene, you can add submodules to server.common.plugins before running the app.
|
|
This code will import each, loading the code in each. If no plugins are defined, initializing the app continues as
|
|
normal.
|
|
"""
|
|
loaded_modules = []
|
|
try:
|
|
pkg = importlib.import_module(plugin_module)
|
|
for loader, name, is_pkg in pkgutil.walk_packages(pkg.__path__):
|
|
full_name = f"{plugin_module}.{name}"
|
|
try:
|
|
module = importlib.import_module(full_name)
|
|
except Exception as e:
|
|
raise ConfigurationError(f"Unexpected error while importing plugin: {plugin_module}.{name}: {str(e)}")
|
|
loaded_modules.append(module)
|
|
except ModuleNotFoundError as e:
|
|
# This exception occurs when the plugin_module does not exist (not an error).
|
|
logging.debug(f"No plugins found in module: {plugin_module}: {str(e)}")
|
|
|
|
return loaded_modules
|