Split out the local backend (#2052)

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.
This commit is contained in:
Marcus Kinsella
2021-02-18 12:58:22 -08:00
committed by GitHub
parent 036b5f8c0f
commit fb61bd6e9c
153 changed files with 14027 additions and 46 deletions
+154
View File
@@ -0,0 +1,154 @@
import os
import tempfile
import fsspec
from datetime import datetime
import boto3
import botocore
from urllib.parse import urlparse
class DataLocator:
"""
DataLocator is a simple wrapper around fsspec functionality, and provides a
set of functions to encapsulate a data location (URI or path), interogate
metadata about the object at that location (size, existance, etc) and
access the underlying data.
https://filesystem-spec.readthedocs.io/en/latest/index.html
Example:
dl = DataLocator("/tmp/foo.h5ad")
if dl.exists():
print(dl.size())
with dl.open() as f:
thecontents = f.read()
DataLocator will accept a URI or native path. Error handling is as defined
in fsspec.
"""
def __init__(self, uri_or_path, region_name=None):
if isinstance(uri_or_path, DataLocator):
locator = uri_or_path
self.uri_or_path = locator.uri_or_path
self.protocol = locator.protocol
self.path = locator.path
self.cname = locator.cname
else:
self.uri_or_path = uri_or_path
self.protocol, self.path = DataLocator._get_protocol_and_path(uri_or_path)
# work-around for LocalFileSystem not treating file: and None as the same scheme/protocol
self.cname = self.path if self.protocol == "file" else self.uri_or_path
# fsspec.filesystem will throw RuntimeError if the protocol is unsupported
if self.protocol == "s3":
if region_name:
config_kwargs = dict(region_name=region_name)
self.fs = fsspec.filesystem(self.protocol, listings_expiry_time=30, config_kwargs=config_kwargs)
else:
self.fs = fsspec.filesystem(self.protocol, listings_expiry_time=30)
else:
self.fs = fsspec.filesystem(self.protocol)
def __repr__(self):
return f"DataLocator(protocol={self.protocol}, cname={self.cname}, "
f"path={self.path}, uri_or_path={self.uri_or_path})"
@staticmethod
def _get_protocol_and_path(uri_or_path):
if "://" in uri_or_path:
protocol, path = uri_or_path.split("://", 1)
# windows!!! Ignore single letter drive identifiers,
# eg, G:\foo.txt
if len(protocol) > 1:
return protocol, path
return None, uri_or_path
def exists(self):
return self.fs.exists(self.cname)
def size(self):
return self.fs.size(self.cname)
def lastmodtime(self):
""" return datetime object representing last modification time, or None if unavailable """
info = self.fs.info(self.cname)
if self.islocal() and info is not None:
return datetime.fromtimestamp(info["mtime"])
else:
return getattr(info, "LastModified", None)
def abspath(self):
"""
return the absolute path for the locator - only really does something
for file: protocol, as all others are already absolute
"""
if self.islocal():
return os.path.abspath(self.path)
else:
return self.uri_or_path
def isfile(self):
return self.fs.isfile(self.cname)
def open(self, *args):
return self.fs.open(self.uri_or_path, *args)
def islocal(self):
return self.protocol is None or self.protocol == "file"
def local_handle(self):
if self.islocal():
return LocalFilePath(self.path)
# if not local, create a tmp file system object to contain the data,
# and clean it up when done. If the path has a suffix/extension,
# do our best to create a file with the same.
ext = os.path.splitext(self.path)
suffix = None if ext[1] == "" else ext[1]
with self.open() as src, tempfile.NamedTemporaryFile(prefix="cellxgene_", suffix=suffix, delete=False) as tmp:
tmp.write(src.read())
tmp.close()
src.close()
tmp_path = tmp.name
return LocalFilePath(tmp_path, delete=True)
def ls(self):
paths = self.fs.ls(self.uri_or_path)
return [os.path.basename(p) for p in paths]
class LocalFilePath:
def __init__(self, tmp_path, delete=False):
self.tmp_path = tmp_path
self.delete = delete
def __enter__(self):
return self.tmp_path
def __exit__(self, *args):
if self.delete:
os.unlink(self.tmp_path)
def discover_s3_region_name(uri):
"""If this is an s3 protocol, discover and return the (aws) region name.
If a return name could not be discovered, or if the uri is not an s3 protocol, return None."""
protocol, _ = DataLocator._get_protocol_and_path(uri)
if protocol == "s3":
bucket = urlparse(uri).netloc
client = boto3.client("s3")
try:
res = client.head_bucket(Bucket=bucket)
except botocore.exceptions.ClientError:
return None
region = res.get("ResponseMetadata", {}).get("HTTPHeaders", {}).get("x-amz-bucket-region")
if region:
return region
else:
return None
return None