mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-15 20:57:56 +08:00
* Add user-generated annotations tests to the server Partially completes https://github.com/chanzuckerberg/cellxgene/issues/969 * Auto-format python code * @skip_if: passing lambdas > than property strings * Respond to feedback from @bkmartinjr
145 lines
4.3 KiB
Python
145 lines
4.3 KiB
Python
import contextlib
|
|
import errno
|
|
import socket
|
|
from urllib.parse import urlsplit, urljoin
|
|
import os
|
|
from flask import json
|
|
import numpy as np
|
|
import pandas as pd
|
|
import warnings
|
|
|
|
|
|
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 dtype_to_schema(dtype):
|
|
schema = {}
|
|
if dtype == np.float32:
|
|
schema["type"] = "float32"
|
|
elif dtype == np.int32:
|
|
schema["type"] = "int32"
|
|
elif dtype == np.bool_:
|
|
schema["type"] = "boolean"
|
|
elif dtype == np.str:
|
|
schema["type"] = "string"
|
|
elif dtype == "category":
|
|
schema["type"] = "categorical"
|
|
schema["categories"] = dtype.categories.tolist()
|
|
else:
|
|
raise TypeError(f"Annotations of type {dtype} are unsupported.")
|
|
return schema
|
|
|
|
|
|
def can_cast_to_float32(array):
|
|
if array.dtype.kind == "f":
|
|
if not np.can_cast(array.dtype, np.float32):
|
|
warnings.warn(f"Annotation {array.name} will be converted to 32 bit float and may lose precision.")
|
|
return True
|
|
return False
|
|
|
|
|
|
def can_cast_to_int32(array):
|
|
if array.dtype.kind in ["i", "u"]:
|
|
if np.can_cast(array.dtype, np.int32):
|
|
return True
|
|
ii32 = np.iinfo(np.int32)
|
|
if array.min() >= ii32.min and array.max() <= ii32.max:
|
|
return True
|
|
return False
|
|
|
|
|
|
def series_to_schema(array):
|
|
assert type(array) == pd.Series
|
|
try:
|
|
return dtype_to_schema(array.dtype)
|
|
except TypeError:
|
|
dtype = array.dtype
|
|
data_kind = dtype.kind
|
|
schema = {}
|
|
if can_cast_to_float32(array):
|
|
schema["type"] = "float32"
|
|
elif can_cast_to_int32(array):
|
|
schema["type"] = "int32"
|
|
elif data_kind == "O" and dtype == "object":
|
|
schema["type"] = "string"
|
|
else:
|
|
raise TypeError(f"Annotations of type {dtype} are unsupported.")
|
|
return schema
|