remote: continue remote/cache/tree refactoring (#4041)

* remote: move constants into trees

* remote: replace Remote() Cache() helper functions

* output: update for remote changes

* cache/data_cloud: update for remote changes

* dependency: update for remote changes

* stage: update for remote changes

* remote: move remaining non-index methods into tree

* tests: update remote unit tests

* remote: LocalCache needs to lock remote index, not cache index

* fix bugs from moved PARAM_CHECKSUM/PARAM_RELPATH

* tests: repo tree open needs state context

* remote: make Remote.gc a class method

* operates on obj.tree, so that GC can be done with both remote and
  cache instances

* bug fixes

* tests: update test_data_cloud

* bug fixes

* tests: update func tests
This commit is contained in:
Peter Rowlands (변기호)
2020-06-15 16:22:55 +03:00
committed by GitHub
parent a59f90fac9
commit a0c229cf9b
54 changed files with 925 additions and 900 deletions
+8 -4
View File
@@ -28,13 +28,15 @@ def _make_remote_property(name):
"""
def getter(self):
from dvc.remote import Cache as CloudCache
from dvc.remote import get_cloud_tree
from dvc.remote.base import CloudCache
remote = self.config.get(name)
if not remote:
return None
return CloudCache(self.repo, name=remote)
tree = get_cloud_tree(self.repo, name=remote)
return CloudCache(tree)
getter.__name__ = name
return cached_property(getter)
@@ -50,7 +52,8 @@ class Cache:
CACHE_DIR = "cache"
def __init__(self, repo):
from dvc.remote import Cache as CloudCache
from dvc.remote import get_cloud_tree
from dvc.remote.local import LocalCache
self.repo = repo
self.config = config = repo.config["cache"]
@@ -62,7 +65,8 @@ class Cache:
else:
settings = {**config, "url": config["dir"]}
self.local = CloudCache(repo, **settings)
tree = get_cloud_tree(repo, **settings)
self.local = LocalCache(tree)
s3 = _make_remote_property("s3")
gs = _make_remote_property("gs")
+4 -4
View File
@@ -122,12 +122,12 @@ class CmdVersion(CmdBaseNoRepo):
@staticmethod
def get_supported_remotes():
from dvc.remote import REMOTES
from dvc.remote import TREES
supported_remotes = []
for remote in REMOTES:
if not remote.get_missing_deps():
supported_remotes.append(remote.scheme)
for tree_cls in TREES:
if not tree_cls.get_missing_deps():
supported_remotes.append(tree_cls.scheme)
return ", ".join(supported_remotes)
+3 -3
View File
@@ -3,7 +3,7 @@
import logging
from dvc.config import NoRemoteError
from dvc.remote import Remote
from dvc.remote import get_remote
logger = logging.getLogger(__name__)
@@ -45,7 +45,7 @@ class DataCloud:
raise NoRemoteError(error_msg)
def _init_remote(self, name):
return Remote(self.repo, name=name)
return get_remote(self.repo, name=name)
def push(
self, cache, jobs=None, remote=None, show_checksums=False,
@@ -85,7 +85,7 @@ class DataCloud:
cache, jobs=jobs, remote=remote, show_checksums=show_checksums
)
if not remote.verify:
if not remote.tree.verify:
self._save_pulled_checksums(cache)
return downloaded_items_num
+2 -2
View File
@@ -12,7 +12,7 @@ from dvc.dependency.param import ParamsDependency
from dvc.dependency.s3 import S3Dependency
from dvc.dependency.ssh import SSHDependency
from dvc.output.base import BaseOutput
from dvc.remote import Remote
from dvc.remote import get_remote
from dvc.scheme import Schemes
from .repo import RepoDependency
@@ -54,7 +54,7 @@ SCHEMA.update(ParamsDependency.PARAM_SCHEMA)
def _get(stage, p, info):
parsed = urlparse(p) if p else None
if parsed and parsed.scheme == "remote":
remote = Remote(stage.repo, name=parsed.netloc)
remote = get_remote(stage.repo, name=parsed.netloc)
return DEP_MAP[remote.scheme](stage, p, info, remote=remote)
if info and info.get(RepoDependency.PARAM_REPO):
+2 -2
View File
@@ -1,7 +1,7 @@
from dvc.dependency.base import BaseDependency
from dvc.output.base import BaseOutput
from dvc.remote.azure import AzureRemote
from dvc.remote.azure import AzureRemoteTree
class AzureDependency(BaseDependency, BaseOutput):
REMOTE = AzureRemote
TREE_CLS = AzureRemoteTree
+2 -2
View File
@@ -1,7 +1,7 @@
from dvc.dependency.base import BaseDependency
from dvc.output.base import BaseOutput
from dvc.remote.http import HTTPRemote
from dvc.remote.http import HTTPRemoteTree
class HTTPDependency(BaseDependency, BaseOutput):
REMOTE = HTTPRemote
TREE_CLS = HTTPRemoteTree
+2 -2
View File
@@ -1,7 +1,7 @@
from dvc.remote.https import HTTPSRemote
from dvc.remote.https import HTTPSRemoteTree
from .http import HTTPDependency
class HTTPSDependency(HTTPDependency):
REMOTE = HTTPSRemote
TREE_CLS = HTTPSRemoteTree
+8 -8
View File
@@ -10,10 +10,10 @@ from dvc.output.hdfs import HDFSOutput
from dvc.output.local import LocalOutput
from dvc.output.s3 import S3Output
from dvc.output.ssh import SSHOutput
from dvc.remote import Remote
from dvc.remote.hdfs import HDFSRemote
from dvc.remote.local import LocalRemote
from dvc.remote.s3 import S3Remote
from dvc.remote import get_remote
from dvc.remote.hdfs import HDFSRemoteTree
from dvc.remote.local import LocalRemoteTree
from dvc.remote.s3 import S3RemoteTree
from dvc.scheme import Schemes
OUTS = [
@@ -47,9 +47,9 @@ CHECKSUM_SCHEMA = Any(
# so when a few types of outputs share the same name, we only need
# specify it once.
CHECKSUMS_SCHEMA = {
LocalRemote.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
S3Remote.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
HDFSRemote.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
LocalRemoteTree.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
S3RemoteTree.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
HDFSRemoteTree.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
}
SCHEMA = CHECKSUMS_SCHEMA.copy()
@@ -66,7 +66,7 @@ def _get(
parsed = urlparse(p)
if parsed.scheme == "remote":
remote = Remote(stage.repo, name=parsed.netloc)
remote = get_remote(stage.repo, name=parsed.netloc)
return OUTS_MAP[remote.scheme](
stage,
p,
+17 -12
View File
@@ -12,7 +12,7 @@ from dvc.exceptions import (
DvcException,
RemoteCacheRequiredError,
)
from dvc.remote.base import BaseRemote
from dvc.remote.base import BaseRemoteTree, Remote
logger = logging.getLogger(__name__)
@@ -47,7 +47,8 @@ class OutputIsStageFileError(DvcException):
class BaseOutput:
IS_DEPENDENCY = False
REMOTE = BaseRemote
REMOTE_CLS = Remote
TREE_CLS = BaseRemoteTree
PARAM_PATH = "path"
PARAM_CACHE = "cache"
@@ -105,7 +106,11 @@ class BaseOutput:
self.repo = stage.repo if stage else None
self.def_path = path
self.info = info
self.remote = remote or self.REMOTE(self.repo, {})
if remote:
self.remote = remote
else:
tree = self.TREE_CLS(self.repo, {})
self.remote = self.REMOTE_CLS(tree)
self.use_cache = False if self.IS_DEPENDENCY else cache
self.metric = False if self.IS_DEPENDENCY else metric
self.plot = False if self.IS_DEPENDENCY else plot
@@ -119,7 +124,7 @@ class BaseOutput:
if remote:
parsed = urlparse(path)
return remote.path_info / parsed.path.lstrip("/")
return self.REMOTE.TREE_CLS.PATH_CLS(path)
return self.TREE_CLS.PATH_CLS(path)
def __repr__(self):
return "{class_name}: '{def_path}'".format(
@@ -131,7 +136,7 @@ class BaseOutput:
@property
def scheme(self):
return self.REMOTE.scheme
return self.TREE_CLS.scheme
@property
def is_in_repo(self):
@@ -154,7 +159,7 @@ class BaseOutput:
@classmethod
def supported(cls, url):
return cls.REMOTE.supported(url)
return cls.TREE_CLS.supported(url)
@property
def cache_path(self):
@@ -162,15 +167,15 @@ class BaseOutput:
@property
def checksum_type(self):
return self.remote.PARAM_CHECKSUM
return self.remote.tree.PARAM_CHECKSUM
@property
def checksum(self):
return self.info.get(self.remote.PARAM_CHECKSUM)
return self.info.get(self.remote.tree.PARAM_CHECKSUM)
@checksum.setter
def checksum(self, checksum):
self.info[self.remote.PARAM_CHECKSUM] = checksum
self.info[self.remote.tree.PARAM_CHECKSUM] = checksum
def get_checksum(self):
return self.remote.get_checksum(self.path_info)
@@ -357,7 +362,7 @@ class BaseOutput:
def unprotect(self):
if self.exists:
self.remote.unprotect(self.path_info)
self.remote.tree.unprotect(self.path_info)
def get_dir_cache(self, **kwargs):
if not self.is_dir_checksum:
@@ -417,8 +422,8 @@ class BaseOutput:
filter_path = str(filter_info) if filter_info else None
is_win = os.name == "nt"
for entry in self.dir_cache:
checksum = entry[self.remote.PARAM_CHECKSUM]
entry_relpath = entry[self.remote.PARAM_RELPATH]
checksum = entry[self.remote.tree.PARAM_CHECKSUM]
entry_relpath = entry[self.remote.tree.PARAM_RELPATH]
if is_win:
entry_relpath = entry_relpath.replace("/", os.sep)
entry_path = os.path.join(path, entry_relpath)
+2 -2
View File
@@ -1,6 +1,6 @@
from dvc.output.s3 import S3Output
from dvc.remote.gs import GSRemote
from dvc.remote.gs import GSRemoteTree
class GSOutput(S3Output):
REMOTE = GSRemote
TREE_CLS = GSRemoteTree
+2 -2
View File
@@ -1,6 +1,6 @@
from dvc.output.base import BaseOutput
from dvc.remote.hdfs import HDFSRemote
from dvc.remote.hdfs import HDFSRemoteTree
class HDFSOutput(BaseOutput):
REMOTE = HDFSRemote
TREE_CLS = HDFSRemoteTree
+5 -4
View File
@@ -5,7 +5,7 @@ from urllib.parse import urlparse
from dvc.exceptions import DvcException
from dvc.istextfile import istextfile
from dvc.output.base import BaseOutput
from dvc.remote.local import LocalRemote
from dvc.remote.local import LocalRemote, LocalRemoteTree
from dvc.utils import relpath
from dvc.utils.fs import path_isin
@@ -13,7 +13,8 @@ logger = logging.getLogger(__name__)
class LocalOutput(BaseOutput):
REMOTE = LocalRemote
REMOTE_CLS = LocalRemote
TREE_CLS = LocalRemoteTree
sep = os.sep
def __init__(self, stage, path, *args, **kwargs):
@@ -33,12 +34,12 @@ class LocalOutput(BaseOutput):
#
# FIXME: if we have Windows path containing / or posix one with \
# then we have #2059 bug and can't really handle that.
p = self.REMOTE.TREE_CLS.PATH_CLS(path)
p = self.TREE_CLS.PATH_CLS(path)
if not p.is_absolute():
p = self.stage.wdir / p
abs_p = os.path.abspath(os.path.normpath(p))
return self.REMOTE.TREE_CLS.PATH_CLS(abs_p)
return self.TREE_CLS.PATH_CLS(abs_p)
def __str__(self):
if not self.is_in_repo:
+2 -2
View File
@@ -1,6 +1,6 @@
from dvc.output.base import BaseOutput
from dvc.remote.s3 import S3Remote
from dvc.remote.s3 import S3RemoteTree
class S3Output(BaseOutput):
REMOTE = S3Remote
TREE_CLS = S3RemoteTree
+3 -2
View File
@@ -1,6 +1,7 @@
from dvc.output.base import BaseOutput
from dvc.remote.ssh import SSHRemote
from dvc.remote.ssh import SSHRemote, SSHRemoteTree
class SSHOutput(BaseOutput):
REMOTE = SSHRemote
REMOTE_CLS = SSHRemote
TREE_CLS = SSHRemoteTree
+41 -45
View File
@@ -1,45 +1,37 @@
import posixpath
from urllib.parse import urlparse
from dvc.remote.azure import AzureCache, AzureRemote
from dvc.remote.gdrive import GDriveRemote
from dvc.remote.gs import GSCache, GSRemote
from dvc.remote.hdfs import HDFSCache, HDFSRemote
from dvc.remote.http import HTTPRemote
from dvc.remote.https import HTTPSRemote
from dvc.remote.local import LocalCache, LocalRemote
from dvc.remote.oss import OSSRemote
from dvc.remote.s3 import S3Cache, S3Remote
from dvc.remote.ssh import SSHCache, SSHRemote
from dvc.remote.azure import AzureRemoteTree
from dvc.remote.base import Remote
from dvc.remote.gdrive import GDriveRemoteTree
from dvc.remote.gs import GSRemoteTree
from dvc.remote.hdfs import HDFSRemoteTree
from dvc.remote.http import HTTPRemoteTree
from dvc.remote.https import HTTPSRemoteTree
from dvc.remote.local import LocalRemote, LocalRemoteTree
from dvc.remote.oss import OSSRemoteTree
from dvc.remote.s3 import S3RemoteTree
from dvc.remote.ssh import SSHRemote, SSHRemoteTree
CACHES = [
AzureCache,
GSCache,
HDFSCache,
S3Cache,
SSHCache,
# LocalCache is the default
]
REMOTES = [
AzureRemote,
GDriveRemote,
GSRemote,
HDFSRemote,
HTTPRemote,
HTTPSRemote,
S3Remote,
SSHRemote,
OSSRemote,
# NOTE: LocalRemote is the default
TREES = [
AzureRemoteTree,
GDriveRemoteTree,
GSRemoteTree,
HDFSRemoteTree,
HTTPRemoteTree,
HTTPSRemoteTree,
S3RemoteTree,
SSHRemoteTree,
OSSRemoteTree,
# NOTE: LocalRemoteTree is the default
]
def _get(remote_conf, remotes, default):
for remote in remotes:
if remote.supported(remote_conf):
return remote
return default
def _get_tree(remote_conf):
for tree_cls in TREES:
if tree_cls.supported(remote_conf):
return tree_cls
return LocalRemoteTree
def _get_conf(repo, **kwargs):
@@ -51,16 +43,6 @@ def _get_conf(repo, **kwargs):
return _resolve_remote_refs(repo.config, remote_conf)
def Remote(repo, **kwargs):
remote_conf = _get_conf(repo, **kwargs)
return _get(remote_conf, REMOTES, LocalRemote)(repo, remote_conf)
def Cache(repo, **kwargs):
remote_conf = _get_conf(repo, **kwargs)
return _get(remote_conf, CACHES, LocalCache)(repo, remote_conf)
def _resolve_remote_refs(config, remote_conf):
# Support for cross referenced remotes.
# This will merge the settings, shadowing base ref with remote_conf.
@@ -90,3 +72,17 @@ def _resolve_remote_refs(config, remote_conf):
base = config["remote"][parsed.netloc]
url = posixpath.join(base["url"], parsed.path.lstrip("/"))
return {**base, **remote_conf, "url": url}
def get_cloud_tree(repo, **kwargs):
remote_conf = _get_conf(repo, **kwargs)
return _get_tree(remote_conf)(repo, remote_conf)
def get_remote(repo, **kwargs):
tree = get_cloud_tree(repo, **kwargs)
if tree.scheme == "local":
return LocalRemote(tree)
if tree.scheme == "ssh":
return SSHRemote(tree)
return Remote(tree)
+8 -16
View File
@@ -7,17 +7,22 @@ from funcy import cached_property, wrap_prop
from dvc.path_info import CloudURLInfo
from dvc.progress import Tqdm
from dvc.remote.base import BaseRemote, BaseRemoteTree, CacheMixin
from dvc.remote.base import BaseRemoteTree
from dvc.scheme import Schemes
logger = logging.getLogger(__name__)
class AzureRemoteTree(BaseRemoteTree):
scheme = Schemes.AZURE
PATH_CLS = CloudURLInfo
REQUIRES = {"azure-storage-blob": "azure.storage.blob"}
PARAM_CHECKSUM = "etag"
COPY_POLL_SECONDS = 5
LIST_OBJECT_PAGE_SIZE = 5000
def __init__(self, remote, config):
super().__init__(remote, config)
def __init__(self, repo, config):
super().__init__(repo, config)
url = config.get("url", "azure://")
self.path_info = self.PATH_CLS(url)
@@ -132,16 +137,3 @@ class AzureRemoteTree(BaseRemoteTree):
to_file,
progress_callback=pbar.update_to,
)
class AzureRemote(BaseRemote):
scheme = Schemes.AZURE
REQUIRES = {"azure-storage-blob": "azure.storage.blob"}
TREE_CLS = AzureRemoteTree
PARAM_CHECKSUM = "etag"
COPY_POLL_SECONDS = 5
LIST_OBJECT_PAGE_SIZE = 5000
class AzureCache(AzureRemote, CacheMixin):
pass
+384 -331
View File
@@ -73,24 +73,103 @@ class DirCacheError(DvcException):
def index_locked(f):
@wraps(f)
def wrapper(remote_obj, *args, **kwargs):
remote = kwargs.get("remote", remote_obj)
with remote.index:
return f(remote_obj, *args, **kwargs)
def wrapper(obj, named_cache, remote, *args, **kwargs):
if hasattr(remote, "index"):
with remote.index:
return f(obj, named_cache, remote, *args, **kwargs)
return f(obj, named_cache, remote, *args, **kwargs)
return wrapper
class BaseRemoteTree:
SHARED_MODE_MAP = {None: (None, None), "group": (None, None)}
scheme = "base"
REQUIRES = {}
PATH_CLS = URLInfo
JOBS = 4 * cpu_count()
PARAM_RELPATH = "relpath"
CHECKSUM_DIR_SUFFIX = ".dir"
CHECKSUM_JOBS = max(1, min(4, cpu_count() // 2))
DEFAULT_VERIFY = False
LIST_OBJECT_PAGE_SIZE = 1000
TRAVERSE_WEIGHT_MULTIPLIER = 5
TRAVERSE_PREFIX_LEN = 3
TRAVERSE_THRESHOLD_SIZE = 500000
CAN_TRAVERSE = True
CACHE_MODE = None
SHARED_MODE_MAP = {None: (None, None), "group": (None, None)}
CHECKSUM_DIR_SUFFIX = ".dir"
def __init__(self, remote, config):
self.remote = remote
state = StateNoop()
def __init__(self, repo, config):
self.repo = repo
self.config = config
self._check_requires(config)
shared = config.get("shared")
self._file_mode, self._dir_mode = self.SHARED_MODE_MAP[shared]
self.checksum_jobs = (
config.get("checksum_jobs")
or (self.repo and self.repo.config["core"].get("checksum_jobs"))
or self.CHECKSUM_JOBS
)
self.verify = config.get("verify", self.DEFAULT_VERIFY)
@classmethod
def get_missing_deps(cls):
import importlib
missing = []
for package, module in cls.REQUIRES.items():
try:
importlib.import_module(module)
except ImportError:
missing.append(package)
return missing
def _check_requires(self, config):
missing = self.get_missing_deps()
if not missing:
return
url = config.get("url", f"{self.scheme}://")
msg = (
"URL '{}' is supported but requires these missing "
"dependencies: {}. If you have installed dvc using pip, "
"choose one of these options to proceed: \n"
"\n"
" 1) Install specific missing dependencies:\n"
" pip install {}\n"
" 2) Install dvc package that includes those missing "
"dependencies: \n"
" pip install 'dvc[{}]'\n"
" 3) Install dvc package with all possible "
"dependencies included: \n"
" pip install 'dvc[all]'\n"
"\n"
"If you have installed dvc from a binary package and you "
"are still seeing this message, please report it to us "
"using https://github.com/iterative/dvc/issues. Thank you!"
).format(url, missing, " ".join(missing), self.scheme)
raise RemoteMissingDepsError(msg)
@classmethod
def supported(cls, config):
if isinstance(config, (str, bytes)):
url = config
else:
url = config["url"]
# NOTE: silently skipping remote, calling code should handle that
parsed = urlparse(url)
return parsed.scheme == cls.scheme
@property
def file_mode(self):
return self._file_mode
@@ -99,17 +178,9 @@ class BaseRemoteTree:
def dir_mode(self):
return self._dir_mode
@property
def scheme(self):
return self.remote.scheme
@property
def state(self):
return self.remote.state
@property
def cache(self):
return self.remote.cache
return getattr(self.repo.cache, self.scheme)
def open(self, path_info, mode="r", encoding=None):
if hasattr(self, "_generate_download_url"):
@@ -172,6 +243,17 @@ class BaseRemoteTree:
def reflink(self, from_info, to_info):
raise RemoteActionNotImplemented("reflink", self.scheme)
@staticmethod
def protect(path_info):
pass
def is_protected(self, path_info):
return False
@staticmethod
def unprotect(path_info):
pass
@classmethod
def is_dir_checksum(cls, checksum):
if not checksum:
@@ -225,6 +307,24 @@ class BaseRemoteTree:
dir_info = self._collect_dir(path_info, tree, **kwargs)
return self._save_dir_info(dir_info, path_info)
def checksum_to_path_info(self, checksum):
return self.path_info / checksum[0:2] / checksum[2:]
def path_to_checksum(self, path):
parts = self.PATH_CLS(path).parts[-2:]
if not (len(parts) == 2 and parts[0] and len(parts[0]) == 2):
raise ValueError(f"Bad cache file path '{path}'")
return "".join(parts)
def save_info(self, path_info, tree=None, **kwargs):
return {
self.PARAM_CHECKSUM: self.get_checksum(
path_info, tree=tree, **kwargs
)
}
def _calculate_checksums(self, file_infos, tree):
file_infos = list(file_infos)
with Tqdm(
@@ -234,7 +334,7 @@ class BaseRemoteTree:
) as pbar:
worker = pbar.wrap_fn(tree.get_file_checksum)
with ThreadPoolExecutor(
max_workers=self.remote.checksum_jobs
max_workers=self.checksum_jobs
) as executor:
tasks = executor.map(worker, file_infos)
checksums = dict(zip(file_infos, tasks))
@@ -259,7 +359,7 @@ class BaseRemoteTree:
result = [
{
self.remote.PARAM_CHECKSUM: checksums[fi],
self.PARAM_CHECKSUM: checksums[fi],
# NOTE: this is lossy transformation:
# "hey\there" -> "hey/there"
# "hey/there" -> "hey/there"
@@ -268,15 +368,13 @@ class BaseRemoteTree:
#
# Yes, this is a BUG, as long as we permit "/" in
# filenames on Windows and "\" on Unix
self.remote.PARAM_RELPATH: fi.relative_to(
path_info
).as_posix(),
self.PARAM_RELPATH: fi.relative_to(path_info).as_posix(),
}
for fi in file_infos
]
# Sorting the list by path to ensure reproducibility
return sorted(result, key=itemgetter(self.remote.PARAM_RELPATH))
return sorted(result, key=itemgetter(self.PARAM_RELPATH))
def _save_dir_info(self, dir_info, path_info):
checksum, tmp_info = self._get_dir_info_checksum(dir_info)
@@ -284,7 +382,7 @@ class BaseRemoteTree:
if self.cache.changed_cache_file(checksum):
self.cache.tree.makedirs(new_info.parent)
self.cache.tree.move(
tmp_info, new_info, mode=self.remote.CACHE_MODE
tmp_info, new_info, mode=self.cache.CACHE_MODE
)
if self.exists(path_info):
@@ -380,7 +478,7 @@ class BaseRemoteTree:
dir_mode=dir_mode,
)
)
with ThreadPoolExecutor(max_workers=self.remote.JOBS) as executor:
with ThreadPoolExecutor(max_workers=self.JOBS) as executor:
futures = [
executor.submit(download_files, from_info, to_info)
for from_info, to_info in zip(from_infos, to_infos)
@@ -417,161 +515,6 @@ class BaseRemoteTree:
move(tmp_file, to_info, mode=file_mode)
class BaseRemote:
"""Base cloud remote class."""
scheme = "base"
REQUIRES = {}
JOBS = 4 * cpu_count()
INDEX_CLS = RemoteIndex
TREE_CLS = BaseRemoteTree
PARAM_RELPATH = "relpath"
CHECKSUM_DIR_SUFFIX = ".dir"
CHECKSUM_JOBS = max(1, min(4, cpu_count() // 2))
DEFAULT_CACHE_TYPES = ["copy"]
DEFAULT_VERIFY = False
LIST_OBJECT_PAGE_SIZE = 1000
TRAVERSE_WEIGHT_MULTIPLIER = 5
TRAVERSE_PREFIX_LEN = 3
TRAVERSE_THRESHOLD_SIZE = 500000
CAN_TRAVERSE = True
CACHE_MODE = None
state = StateNoop()
def __init__(self, repo, config):
self.repo = repo
self._check_requires(config)
self.checksum_jobs = (
config.get("checksum_jobs")
or (self.repo and self.repo.config["core"].get("checksum_jobs"))
or self.CHECKSUM_JOBS
)
self.verify = config.get("verify", self.DEFAULT_VERIFY)
self._dir_info = {}
self.cache_types = config.get("type") or copy(self.DEFAULT_CACHE_TYPES)
self.cache_type_confirmed = False
url = config.get("url")
if url:
index_name = hashlib.sha256(url.encode("utf-8")).hexdigest()
self.index = self.INDEX_CLS(
self.repo, index_name, dir_suffix=self.CHECKSUM_DIR_SUFFIX
)
else:
self.index = RemoteIndexNoop()
self.tree = self.TREE_CLS(self, config)
@property
def path_info(self):
return self.tree.path_info
@classmethod
def get_missing_deps(cls):
import importlib
missing = []
for package, module in cls.REQUIRES.items():
try:
importlib.import_module(module)
except ImportError:
missing.append(package)
return missing
def _check_requires(self, config):
missing = self.get_missing_deps()
if not missing:
return
url = config.get("url", f"{self.scheme}://")
msg = (
"URL '{}' is supported but requires these missing "
"dependencies: {}. If you have installed dvc using pip, "
"choose one of these options to proceed: \n"
"\n"
" 1) Install specific missing dependencies:\n"
" pip install {}\n"
" 2) Install dvc package that includes those missing "
"dependencies: \n"
" pip install 'dvc[{}]'\n"
" 3) Install dvc package with all possible "
"dependencies included: \n"
" pip install 'dvc[all]'\n"
"\n"
"If you have installed dvc from a binary package and you "
"are still seeing this message, please report it to us "
"using https://github.com/iterative/dvc/issues. Thank you!"
).format(url, missing, " ".join(missing), self.scheme)
raise RemoteMissingDepsError(msg)
def __repr__(self):
return "{class_name}: '{path_info}'".format(
class_name=type(self).__name__,
path_info=self.path_info or "No path",
)
@classmethod
def supported(cls, config):
if isinstance(config, (str, bytes)):
url = config
else:
url = config["url"]
# NOTE: silently skipping remote, calling code should handle that
parsed = urlparse(url)
return parsed.scheme == cls.scheme
@property
def cache(self):
return getattr(self.repo.cache, self.scheme)
@classmethod
def is_dir_checksum(cls, checksum):
return cls.TREE_CLS.is_dir_checksum(checksum)
def get_checksum(self, path_info, **kwargs):
return self.tree.get_checksum(path_info, **kwargs)
def checksum_to_path_info(self, checksum):
return self.path_info / checksum[0:2] / checksum[2:]
def path_to_checksum(self, path):
parts = self.tree.PATH_CLS(path).parts[-2:]
if not (len(parts) == 2 and parts[0] and len(parts[0]) == 2):
raise ValueError(f"Bad cache file path '{path}'")
return "".join(parts)
def save_info(self, path_info, tree=None, **kwargs):
return {
self.PARAM_CHECKSUM: self.tree.get_checksum(
path_info, tree=tree, **kwargs
)
}
def open(self, *args, **kwargs):
return self.tree.open(*args, **kwargs)
@staticmethod
def protect(path_info):
pass
def is_protected(self, path_info):
return False
@staticmethod
def unprotect(path_info):
pass
def list_paths(self, prefix=None, progress_callback=None):
if prefix:
if len(prefix) > 2:
@@ -581,14 +524,14 @@ class BaseRemote:
else:
path_info = self.path_info
if progress_callback:
for file_info in self.tree.walk_files(path_info):
for file_info in self.walk_files(path_info):
progress_callback()
yield file_info.path
else:
yield from self.tree.walk_files(path_info)
yield from self.walk_files(path_info)
def list_checksums(self, prefix=None, progress_callback=None):
"""Iterate over remote checksums.
"""Iterate over checksums in this tree.
If `prefix` is specified, only checksums which begin with `prefix`
will be returned.
@@ -602,7 +545,7 @@ class BaseRemote:
)
def all(self, jobs=None, name=None):
"""Iterate over all checksums in the remote.
"""Iterate over all checksums in this tree.
Checksums will be fetched in parallel threads according to prefix
(except for small remotes) and a progress bar will be displayed.
@@ -616,104 +559,11 @@ class BaseRemote:
if not self.CAN_TRAVERSE:
return self.list_checksums()
remote_size, remote_checksums = self._estimate_remote_size(name=name)
return self._list_checksums_traverse(
remote_size, remote_checksums = self.estimate_remote_size(name=name)
return self.list_checksums_traverse(
remote_size, remote_checksums, jobs, name
)
def checksums_exist(self, checksums, jobs=None, name=None):
"""Check if the given checksums are stored in the remote.
There are two ways of performing this check:
- Traverse method: Get a list of all the files in the remote
(traversing the cache directory) and compare it with
the given checksums. Cache entries will be retrieved in parallel
threads according to prefix (i.e. entries starting with, "00...",
"01...", and so on) and a progress bar will be displayed.
- Exists method: For each given checksum, run the `exists`
method and filter the checksums that aren't on the remote.
This is done in parallel threads.
It also shows a progress bar when performing the check.
The reason for such an odd logic is that most of the remotes
take much shorter time to just retrieve everything they have under
a certain prefix (e.g. s3, gs, ssh, hdfs). Other remotes that can
check if particular file exists much quicker, use their own
implementation of checksums_exist (see ssh, local).
Which method to use will be automatically determined after estimating
the size of the remote cache, and comparing the estimated size with
len(checksums). To estimate the size of the remote cache, we fetch
a small subset of cache entries (i.e. entries starting with "00...").
Based on the number of entries in that subset, the size of the full
cache can be estimated, since the cache is evenly distributed according
to checksum.
Returns:
A list with checksums that were found in the remote
"""
# Remotes which do not use traverse prefix should override
# checksums_exist() (see ssh, local)
assert self.TRAVERSE_PREFIX_LEN >= 2
checksums = set(checksums)
indexed_checksums = set(self.index.intersection(checksums))
checksums -= indexed_checksums
logger.debug(
"Matched '{}' indexed checksums".format(len(indexed_checksums))
)
if not checksums:
return indexed_checksums
if len(checksums) == 1 or not self.CAN_TRAVERSE:
remote_checksums = self._list_checksums_exists(
checksums, jobs, name
)
return list(indexed_checksums) + remote_checksums
# Max remote size allowed for us to use traverse method
remote_size, remote_checksums = self._estimate_remote_size(
checksums, name
)
traverse_pages = remote_size / self.LIST_OBJECT_PAGE_SIZE
# For sufficiently large remotes, traverse must be weighted to account
# for performance overhead from large lists/sets.
# From testing with S3, for remotes with 1M+ files, object_exists is
# faster until len(checksums) is at least 10k~100k
if remote_size > self.TRAVERSE_THRESHOLD_SIZE:
traverse_weight = traverse_pages * self.TRAVERSE_WEIGHT_MULTIPLIER
else:
traverse_weight = traverse_pages
if len(checksums) < traverse_weight:
logger.debug(
"Large remote ('{}' checksums < '{}' traverse weight), "
"using object_exists for remaining checksums".format(
len(checksums), traverse_weight
)
)
return (
list(indexed_checksums)
+ list(checksums & remote_checksums)
+ self._list_checksums_exists(
checksums - remote_checksums, jobs, name
)
)
logger.debug(
"Querying '{}' checksums via traverse".format(len(checksums))
)
remote_checksums = set(
self._list_checksums_traverse(
remote_size, remote_checksums, jobs, name
)
)
return list(indexed_checksums) + list(
checksums & set(remote_checksums)
)
def _checksums_with_limit(
self, limit, prefix=None, progress_callback=None
):
@@ -737,8 +587,8 @@ class BaseRemote:
* self.LIST_OBJECT_PAGE_SIZE,
)
def _estimate_remote_size(self, checksums=None, name=None):
"""Estimate remote cache size based on number of entries beginning with
def estimate_remote_size(self, checksums=None, name=None):
"""Estimate tree size based on number of entries beginning with
"00..." prefix.
"""
prefix = "0" * self.TRAVERSE_PREFIX_LEN
@@ -772,10 +622,10 @@ class BaseRemote:
logger.debug(f"Estimated remote size: {remote_size} files")
return remote_size, remote_checksums
def _list_checksums_traverse(
def list_checksums_traverse(
self, remote_size, remote_checksums, jobs=None, name=None
):
"""Iterate over all checksums in the remote cache.
"""Iterate over all checksums found in this tree.
Checksums are fetched in parallel according to prefix, except in
cases where the remote size is very small.
@@ -827,7 +677,10 @@ class BaseRemote:
in_remote = executor.map(list_with_update, traverse_prefixes,)
yield from itertools.chain.from_iterable(in_remote)
def _list_checksums_exists(self, checksums, jobs=None, name=None):
def list_checksums_exists(self, checksums, jobs=None, name=None):
"""Return list of the specified checksums which exist in this tree.
Checksums will be queried individually.
"""
logger.debug(
"Querying {} checksums via object_exists".format(len(checksums))
)
@@ -839,7 +692,7 @@ class BaseRemote:
) as pbar:
def exists_with_progress(path_info):
ret = self.tree.exists(path_info)
ret = self.exists(path_info)
pbar.update_msg(str(path_info))
return ret
@@ -849,44 +702,243 @@ class BaseRemote:
ret = list(itertools.compress(checksums, in_remote))
return ret
@index_locked
def gc(self, named_cache, jobs=None):
used = set(named_cache.scheme_keys("local"))
if self.scheme != "":
used.update(named_cache.scheme_keys(self.scheme))
removed = False
# checksums must be sorted to ensure we always remove .dir files first
for checksum in sorted(
self.all(jobs, str(self.path_info)),
key=self.is_dir_checksum,
reverse=True,
):
if checksum in used:
continue
path_info = self.checksum_to_path_info(checksum)
if self.is_dir_checksum(checksum):
# backward compatibility
self._remove_unpacked_dir(checksum)
self.tree.remove(path_info)
removed = True
if removed:
self.index.clear()
return removed
def _remove_unpacked_dir(self, checksum):
pass
class CacheMixin:
"""BaseRemote extensions for cache link/checkout operations."""
class Remote:
"""Cloud remote class.
Provides methods for indexing and garbage collecting trees which contain
DVC remotes.
"""
INDEX_CLS = RemoteIndex
def __init__(self, tree):
self.tree = tree
self.repo = tree.repo
config = tree.config
url = config.get("url")
if url:
index_name = hashlib.sha256(url.encode("utf-8")).hexdigest()
self.index = self.INDEX_CLS(
self.repo, index_name, dir_suffix=self.tree.CHECKSUM_DIR_SUFFIX
)
else:
self.index = RemoteIndexNoop()
@property
def path_info(self):
return self.tree.path_info
def __repr__(self):
return "{class_name}: '{path_info}'".format(
class_name=type(self).__name__,
path_info=self.path_info or "No path",
)
@property
def cache(self):
return getattr(self.repo.cache, self.scheme)
@property
def scheme(self):
return self.tree.scheme
def is_dir_checksum(self, checksum):
return self.tree.is_dir_checksum(checksum)
def get_checksum(self, path_info, **kwargs):
return self.tree.get_checksum(path_info, **kwargs)
def checksum_to_path_info(self, checksum):
return self.tree.checksum_to_path_info(checksum)
def path_to_checksum(self, path):
return self.tree.path_to_checksum(path)
def save_info(self, path_info, **kwargs):
return self.tree.save_info(path_info, **kwargs)
def open(self, *args, **kwargs):
return self.tree.open(*args, **kwargs)
def checksums_exist(self, checksums, jobs=None, name=None):
"""Check if the given checksums are stored in the remote.
There are two ways of performing this check:
- Traverse method: Get a list of all the files in the remote
(traversing the cache directory) and compare it with
the given checksums. Cache entries will be retrieved in parallel
threads according to prefix (i.e. entries starting with, "00...",
"01...", and so on) and a progress bar will be displayed.
- Exists method: For each given checksum, run the `exists`
method and filter the checksums that aren't on the remote.
This is done in parallel threads.
It also shows a progress bar when performing the check.
The reason for such an odd logic is that most of the remotes
take much shorter time to just retrieve everything they have under
a certain prefix (e.g. s3, gs, ssh, hdfs). Other remotes that can
check if particular file exists much quicker, use their own
implementation of checksums_exist (see ssh, local).
Which method to use will be automatically determined after estimating
the size of the remote cache, and comparing the estimated size with
len(checksums). To estimate the size of the remote cache, we fetch
a small subset of cache entries (i.e. entries starting with "00...").
Based on the number of entries in that subset, the size of the full
cache can be estimated, since the cache is evenly distributed according
to checksum.
Returns:
A list with checksums that were found in the remote
"""
# Remotes which do not use traverse prefix should override
# checksums_exist() (see ssh, local)
assert self.tree.TRAVERSE_PREFIX_LEN >= 2
checksums = set(checksums)
indexed_checksums = set(self.index.intersection(checksums))
checksums -= indexed_checksums
logger.debug(
"Matched '{}' indexed checksums".format(len(indexed_checksums))
)
if not checksums:
return indexed_checksums
if len(checksums) == 1 or not self.tree.CAN_TRAVERSE:
remote_checksums = self.tree.list_checksums_exists(
checksums, jobs, name
)
return list(indexed_checksums) + remote_checksums
# Max remote size allowed for us to use traverse method
remote_size, remote_checksums = self.tree.estimate_remote_size(
checksums, name
)
traverse_pages = remote_size / self.tree.LIST_OBJECT_PAGE_SIZE
# For sufficiently large remotes, traverse must be weighted to account
# for performance overhead from large lists/sets.
# From testing with S3, for remotes with 1M+ files, object_exists is
# faster until len(checksums) is at least 10k~100k
if remote_size > self.tree.TRAVERSE_THRESHOLD_SIZE:
traverse_weight = (
traverse_pages * self.tree.TRAVERSE_WEIGHT_MULTIPLIER
)
else:
traverse_weight = traverse_pages
if len(checksums) < traverse_weight:
logger.debug(
"Large remote ('{}' checksums < '{}' traverse weight), "
"using object_exists for remaining checksums".format(
len(checksums), traverse_weight
)
)
return (
list(indexed_checksums)
+ list(checksums & remote_checksums)
+ self.tree.list_checksums_exists(
checksums - remote_checksums, jobs, name
)
)
logger.debug(
"Querying '{}' checksums via traverse".format(len(checksums))
)
remote_checksums = set(
self.tree.list_checksums_traverse(
remote_size, remote_checksums, jobs, name
)
)
return list(indexed_checksums) + list(
checksums & set(remote_checksums)
)
@classmethod
@index_locked
def gc(cls, named_cache, remote, jobs=None):
tree = remote.tree
used = set(named_cache.scheme_keys("local"))
if tree.scheme != "":
used.update(named_cache.scheme_keys(tree.scheme))
removed = False
# checksums must be sorted to ensure we always remove .dir files first
for checksum in sorted(
tree.all(jobs, str(tree.path_info)),
key=tree.is_dir_checksum,
reverse=True,
):
if checksum in used:
continue
path_info = tree.checksum_to_path_info(checksum)
if tree.is_dir_checksum(checksum):
# backward compatibility
tree._remove_unpacked_dir(checksum)
tree.remove(path_info)
removed = True
if removed and hasattr(remote, "index"):
remote.index.clear()
return removed
class CloudCache:
"""Cloud cache class."""
DEFAULT_CACHE_TYPES = ["copy"]
CACHE_MODE = BaseRemoteTree.CACHE_MODE
def __init__(self, tree):
self.tree = tree
self.repo = tree.repo
self.cache_types = tree.config.get("type") or copy(
self.DEFAULT_CACHE_TYPES
)
self.cache_type_confirmed = False
self._dir_info = {}
@property
def path_info(self):
return self.tree.path_info
@property
def cache(self):
return getattr(self.repo.cache, self.scheme)
@property
def scheme(self):
return self.tree.scheme
@property
def state(self):
return self.tree.state
def open(self, *args, **kwargs):
return self.tree.open(*args, **kwargs)
def is_dir_checksum(self, checksum):
return self.tree.is_dir_checksum(checksum)
def get_checksum(self, path_info, **kwargs):
return self.tree.get_checksum(path_info, **kwargs)
# Override to return path as a string instead of PathInfo for clouds
# which support string paths (see local)
def checksum_to_path(self, checksum):
return self.checksum_to_path_info(checksum)
def checksum_to_path_info(self, checksum):
return self.tree.checksum_to_path_info(checksum)
def get_dir_cache(self, checksum):
assert checksum
@@ -922,7 +974,8 @@ class CacheMixin:
# only need to convert it for Windows
for info in d:
# NOTE: here is a BUG, see comment to .as_posix() below
info[self.PARAM_RELPATH] = info[self.PARAM_RELPATH].replace(
relpath = info[self.tree.PARAM_RELPATH]
info[self.tree.PARAM_RELPATH] = relpath.replace(
"/", self.tree.PATH_CLS.sep
)
@@ -953,7 +1006,7 @@ class CacheMixin:
logger.debug("'%s' doesn't exist.", path_info)
return True
checksum = checksum_info.get(self.PARAM_CHECKSUM)
checksum = checksum_info.get(self.tree.PARAM_CHECKSUM)
if checksum is None:
logger.debug("hash value for '%s' is missing.", path_info)
return True
@@ -1037,7 +1090,7 @@ class CacheMixin:
path_info
):
# Default relink procedure involves unneeded copy
self.unprotect(path_info)
self.tree.unprotect(path_info)
else:
self.tree.remove(path_info)
self.link(cache_info, path_info)
@@ -1062,7 +1115,7 @@ class CacheMixin:
callback(1)
self.state.save(cache_info, checksum)
return {self.PARAM_CHECKSUM: checksum}
return {self.tree.PARAM_CHECKSUM: checksum}
def _cache_is_copy(self, path_info):
"""Checks whether cache uses copies."""
@@ -1091,8 +1144,8 @@ class CacheMixin:
for entry in Tqdm(
dir_info, desc="Saving " + path_info.name, unit="file"
):
entry_info = path_info / entry[self.PARAM_RELPATH]
entry_checksum = entry[self.PARAM_CHECKSUM]
entry_info = path_info / entry[self.tree.PARAM_RELPATH]
entry_checksum = entry[self.tree.PARAM_CHECKSUM]
self._save_file(
entry_info, tree, entry_checksum, save_link=False, **kwargs
)
@@ -1104,7 +1157,7 @@ class CacheMixin:
cache_info = self.checksum_to_path_info(checksum)
self.state.save(cache_info, checksum)
return {self.PARAM_CHECKSUM: checksum}
return {self.tree.PARAM_CHECKSUM: checksum}
def save(self, path_info, tree, checksum_info, save_link=True, **kwargs):
if path_info.scheme != self.scheme:
@@ -1113,8 +1166,8 @@ class CacheMixin:
)
if not checksum_info:
checksum_info = self.save_info(path_info, tree=tree, **kwargs)
checksum = checksum_info[self.PARAM_CHECKSUM]
checksum_info = self.tree.save_info(path_info, tree=tree, **kwargs)
checksum = checksum_info[self.tree.PARAM_CHECKSUM]
return self._save(path_info, tree, checksum, save_link, **kwargs)
def _save(self, path_info, tree, checksum, save_link=True, **kwargs):
@@ -1141,7 +1194,7 @@ class CacheMixin:
"""
# Prefer string path over PathInfo when possible due to performance
cache_info = self.checksum_to_path(checksum)
if self.is_protected(cache_info):
if self.tree.is_protected(cache_info):
logger.debug(
"Assuming '%s' is unchanged since it is read-only", cache_info
)
@@ -1162,7 +1215,7 @@ class CacheMixin:
if actual.split(".")[0] == checksum.split(".")[0]:
# making cache file read-only so we don't need to check it
# next time
self.protect(cache_info)
self.tree.protect(cache_info)
return False
if self.tree.exists(cache_info):
@@ -1176,10 +1229,10 @@ class CacheMixin:
return True
for entry in self.get_dir_cache(checksum):
entry_checksum = entry[self.PARAM_CHECKSUM]
entry_checksum = entry[self.tree.PARAM_CHECKSUM]
if path_info and filter_info:
entry_info = path_info / entry[self.PARAM_RELPATH]
entry_info = path_info / entry[self.tree.PARAM_RELPATH]
if not entry_info.isin_or_eq(filter_info):
continue
@@ -1258,15 +1311,15 @@ class CacheMixin:
logger.debug("Linking directory '%s'.", path_info)
for entry in dir_info:
relative_path = entry[self.PARAM_RELPATH]
entry_checksum = entry[self.PARAM_CHECKSUM]
relative_path = entry[self.tree.PARAM_RELPATH]
entry_checksum = entry[self.tree.PARAM_CHECKSUM]
entry_cache_info = self.checksum_to_path_info(entry_checksum)
entry_info = path_info / relative_path
if filter_info and not entry_info.isin_or_eq(filter_info):
continue
entry_checksum_info = {self.PARAM_CHECKSUM: entry_checksum}
entry_checksum_info = {self.tree.PARAM_CHECKSUM: entry_checksum}
if relink or self.changed(entry_info, entry_checksum_info):
modified = True
self.safe_remove(entry_info, force=force)
@@ -1290,7 +1343,7 @@ class CacheMixin:
existing_files = set(self.tree.walk_files(path_info))
needed_files = {
path_info / entry[self.PARAM_RELPATH] for entry in dir_info
path_info / entry[self.tree.PARAM_RELPATH] for entry in dir_info
}
redundant_files = existing_files - needed_files
for path in redundant_files:
@@ -1310,7 +1363,7 @@ class CacheMixin:
if path_info.scheme not in ["local", self.scheme]:
raise NotImplementedError
checksum = checksum_info.get(self.PARAM_CHECKSUM)
checksum = checksum_info.get(self.tree.PARAM_CHECKSUM)
failed = None
skip = False
if not checksum:
@@ -1385,6 +1438,6 @@ class CacheMixin:
return len(self.get_dir_cache(checksum))
return ilen(
filter_info.isin_or_eq(path_info / entry[self.PARAM_CHECKSUM])
filter_info.isin_or_eq(path_info / entry[self.tree.PARAM_CHECKSUM])
for entry in self.get_dir_cache(checksum)
)
+11 -16
View File
@@ -14,7 +14,7 @@ from funcy.py3 import cat
from dvc.exceptions import DvcException, FileMissingError
from dvc.path_info import CloudURLInfo
from dvc.progress import Tqdm
from dvc.remote.base import BaseRemote, BaseRemoteTree
from dvc.remote.base import BaseRemoteTree
from dvc.scheme import Schemes
from dvc.utils import format_link, tmp_fname
from dvc.utils.stream import IterStream
@@ -85,7 +85,13 @@ class GDriveURLInfo(CloudURLInfo):
class GDriveRemoteTree(BaseRemoteTree):
scheme = Schemes.GDRIVE
PATH_CLS = GDriveURLInfo
REQUIRES = {"pydrive2": "pydrive2"}
DEFAULT_VERIFY = True
# Always prefer traverse for GDrive since API usage quotas are a concern.
TRAVERSE_WEIGHT_MULTIPLIER = 1
TRAVERSE_PREFIX_LEN = 2
GDRIVE_CREDENTIALS_DATA = "GDRIVE_CREDENTIALS_DATA"
DEFAULT_USER_CREDENTIALS_FILE = "gdrive-user-credentials.json"
@@ -93,8 +99,8 @@ class GDriveRemoteTree(BaseRemoteTree):
DEFAULT_GDRIVE_CLIENT_ID = "710796635688-iivsgbgsb6uv1fap6635dhvuei09o66c.apps.googleusercontent.com" # noqa: E501
DEFAULT_GDRIVE_CLIENT_SECRET = "a1Fz59uTpVNeG_VGuSKDLJXv"
def __init__(self, remote, config):
super().__init__(remote, config)
def __init__(self, repo, config):
super().__init__(repo, config)
self.path_info = self.PATH_CLS(config["url"])
@@ -122,13 +128,12 @@ class GDriveRemoteTree(BaseRemoteTree):
self._client_secret = config.get("gdrive_client_secret")
self._validate_config()
self._gdrive_user_credentials_path = (
tmp_fname(os.path.join(self.remote.repo.tmp_dir, ""))
tmp_fname(os.path.join(self.repo.tmp_dir, ""))
if os.getenv(GDriveRemoteTree.GDRIVE_CREDENTIALS_DATA)
else config.get(
"gdrive_user_credentials_file",
os.path.join(
self.remote.repo.tmp_dir,
self.DEFAULT_USER_CREDENTIALS_FILE,
self.repo.tmp_dir, self.DEFAULT_USER_CREDENTIALS_FILE,
),
)
)
@@ -574,13 +579,3 @@ class GDriveRemoteTree(BaseRemoteTree):
def _download(self, from_info, to_file, name=None, no_progress_bar=False):
item_id = self._get_item_id(from_info)
self._gdrive_download_file(item_id, to_file, name, no_progress_bar)
class GDriveRemote(BaseRemote):
scheme = Schemes.GDRIVE
REQUIRES = {"pydrive2": "pydrive2"}
TREE_CLS = GDriveRemoteTree
DEFAULT_VERIFY = True
# Always prefer traverse for GDrive since API usage quotas are a concern.
TRAVERSE_WEIGHT_MULTIPLIER = 1
TRAVERSE_PREFIX_LEN = 2
+6 -14
View File
@@ -9,7 +9,7 @@ from funcy import cached_property, wrap_prop
from dvc.exceptions import DvcException
from dvc.path_info import CloudURLInfo
from dvc.progress import Tqdm
from dvc.remote.base import BaseRemote, BaseRemoteTree, CacheMixin
from dvc.remote.base import BaseRemoteTree
from dvc.scheme import Schemes
logger = logging.getLogger(__name__)
@@ -65,10 +65,13 @@ def _upload_to_bucket(
class GSRemoteTree(BaseRemoteTree):
scheme = Schemes.GS
PATH_CLS = CloudURLInfo
REQUIRES = {"google-cloud-storage": "google.cloud.storage"}
PARAM_CHECKSUM = "md5"
def __init__(self, remote, config):
super().__init__(remote, config)
def __init__(self, repo, config):
super().__init__(repo, config)
url = config.get("url", "gs:///")
self.path_info = self.PATH_CLS(url)
@@ -193,14 +196,3 @@ class GSRemoteTree(BaseRemoteTree):
disable=no_progress_bar,
) as wrapped:
blob.download_to_file(wrapped)
class GSRemote(BaseRemote):
scheme = Schemes.GS
REQUIRES = {"google-cloud-storage": "google.cloud.storage"}
TREE_CLS = GSRemoteTree
PARAM_CHECKSUM = "md5"
class GSCache(GSRemote, CacheMixin):
pass
+9 -16
View File
@@ -11,15 +11,21 @@ from urllib.parse import urlparse
from dvc.scheme import Schemes
from dvc.utils import fix_env, tmp_fname
from .base import BaseRemote, BaseRemoteTree, CacheMixin, RemoteCmdError
from .base import BaseRemoteTree, RemoteCmdError
from .pool import get_connection
logger = logging.getLogger(__name__)
class HDFSRemoteTree(BaseRemoteTree):
def __init__(self, remote, config):
super().__init__(remote, config)
scheme = Schemes.HDFS
REQUIRES = {"pyarrow": "pyarrow"}
REGEX = r"^hdfs://((?P<user>.*)@)?.*$"
PARAM_CHECKSUM = "checksum"
TRAVERSE_PREFIX_LEN = 2
def __init__(self, repo, config):
super().__init__(repo, config)
self.path_info = None
url = config.get("url")
@@ -173,16 +179,3 @@ class HDFSRemoteTree(BaseRemoteTree):
with self.hdfs(from_info) as hdfs:
with open(to_file, "wb+") as fobj:
hdfs.download(from_info.path, fobj)
class HDFSRemote(BaseRemote):
scheme = Schemes.HDFS
REGEX = r"^hdfs://((?P<user>.*)@)?.*$"
PARAM_CHECKSUM = "checksum"
REQUIRES = {"pyarrow": "pyarrow"}
TREE_CLS = HDFSRemoteTree
TRAVERSE_PREFIX_LEN = 2
class HDFSCache(HDFSRemote, CacheMixin):
pass
+6 -16
View File
@@ -8,7 +8,7 @@ import dvc.prompt as prompt
from dvc.exceptions import DvcException, HTTPError
from dvc.path_info import HTTPURLInfo
from dvc.progress import Tqdm
from dvc.remote.base import BaseRemote, BaseRemoteTree
from dvc.remote.base import BaseRemoteTree
from dvc.scheme import Schemes
logger = logging.getLogger(__name__)
@@ -24,15 +24,18 @@ def ask_password(host, user):
class HTTPRemoteTree(BaseRemoteTree):
scheme = Schemes.HTTP
PATH_CLS = HTTPURLInfo
PARAM_CHECKSUM = "etag"
CAN_TRAVERSE = False
SESSION_RETRIES = 5
SESSION_BACKOFF_FACTOR = 0.1
REQUEST_TIMEOUT = 10
CHUNK_SIZE = 2 ** 16
def __init__(self, remote, config):
super().__init__(remote, config)
def __init__(self, repo, config):
super().__init__(repo, config)
url = config.get("url")
if url:
@@ -185,16 +188,3 @@ class HTTPRemoteTree(BaseRemoteTree):
def _content_length(response):
res = response.headers.get("Content-Length")
return int(res) if res else None
class HTTPRemote(BaseRemote):
scheme = Schemes.HTTP
PARAM_CHECKSUM = "etag"
CAN_TRAVERSE = False
TREE_CLS = HTTPRemoteTree
def list_paths(self, prefix=None, progress_callback=None):
raise NotImplementedError
def gc(self):
raise NotImplementedError
+2 -2
View File
@@ -1,7 +1,7 @@
from dvc.scheme import Schemes
from .http import HTTPRemote
from .http import HTTPRemoteTree
class HTTPSRemote(HTTPRemote):
class HTTPSRemoteTree(HTTPRemoteTree):
scheme = Schemes.HTTPS
+83 -89
View File
@@ -16,9 +16,9 @@ from dvc.remote.base import (
STATUS_MAP,
STATUS_MISSING,
STATUS_NEW,
BaseRemote,
BaseRemoteTree,
CacheMixin,
CloudCache,
Remote,
index_locked,
)
from dvc.remote.index import RemoteIndexNoop
@@ -39,17 +39,24 @@ logger = logging.getLogger(__name__)
class LocalRemoteTree(BaseRemoteTree):
SHARED_MODE_MAP = {None: (0o644, 0o755), "group": (0o664, 0o775)}
scheme = Schemes.LOCAL
PATH_CLS = PathInfo
PARAM_CHECKSUM = "md5"
PARAM_PATH = "path"
TRAVERSE_PREFIX_LEN = 2
UNPACKED_DIR_SUFFIX = ".unpacked"
def __init__(self, remote, config):
super().__init__(remote, config)
CACHE_MODE = 0o444
SHARED_MODE_MAP = {None: (0o644, 0o755), "group": (0o664, 0o775)}
def __init__(self, repo, config):
super().__init__(repo, config)
url = config.get("url")
self.path_info = self.PATH_CLS(url) if url else None
@property
def repo(self):
return self.remote.repo
def state(self):
return self.repo.state
@cached_property
def work_tree(self):
@@ -195,78 +202,6 @@ class LocalRemoteTree(BaseRemoteTree):
os.chmod(tmp_info, self.file_mode)
os.rename(tmp_info, to_info)
def get_file_checksum(self, path_info):
return file_md5(path_info)[0]
@staticmethod
def getsize(path_info):
return os.path.getsize(path_info)
def _upload(
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs
):
makedirs(to_info.parent, exist_ok=True)
tmp_file = tmp_fname(to_info)
copyfile(
from_file, tmp_file, name=name, no_progress_bar=no_progress_bar
)
self.remote.protect(tmp_file)
os.rename(tmp_file, to_info)
@staticmethod
def _download(
from_info, to_file, name=None, no_progress_bar=False, **_kwargs
):
copyfile(
from_info, to_file, no_progress_bar=no_progress_bar, name=name
)
def _log_exceptions(func, operation):
@wraps(func)
def wrapper(from_info, to_info, *args, **kwargs):
try:
func(from_info, to_info, *args, **kwargs)
return 0
except Exception as exc:
# NOTE: this means we ran out of file descriptors and there is no
# reason to try to proceed, as we will hit this error anyways.
if isinstance(exc, OSError) and exc.errno == errno.EMFILE:
raise
logger.exception(
"failed to %s '%s' to '%s'", operation, from_info, to_info
)
return 1
return wrapper
class LocalRemote(BaseRemote):
scheme = Schemes.LOCAL
INDEX_CLS = RemoteIndexNoop
TREE_CLS = LocalRemoteTree
PARAM_CHECKSUM = "md5"
PARAM_PATH = "path"
DEFAULT_CACHE_TYPES = ["reflink", "copy"]
TRAVERSE_PREFIX_LEN = 2
UNPACKED_DIR_SUFFIX = ".unpacked"
CACHE_MODE = 0o444
@property
def state(self):
return self.repo.state
def get(self, md5):
if not md5:
return None
return self.checksum_to_path_info(md5).url
def _unprotect_file(self, path):
if System.is_symlink(path) or System.is_hardlink(path):
logger.debug(f"Unprotecting '{path}'")
@@ -287,7 +222,7 @@ class LocalRemote(BaseRemote):
"a symlink or a hardlink.".format(path)
)
os.chmod(path, self.tree.file_mode)
os.chmod(path, self.file_mode)
def _unprotect_dir(self, path):
assert is_working_tree(self.repo.tree)
@@ -333,14 +268,44 @@ class LocalRemote(BaseRemote):
return stat.S_IMODE(mode) == self.CACHE_MODE
def get_file_checksum(self, path_info):
return file_md5(path_info)[0]
@staticmethod
def getsize(path_info):
return os.path.getsize(path_info)
def _upload(
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs
):
makedirs(to_info.parent, exist_ok=True)
tmp_file = tmp_fname(to_info)
copyfile(
from_file, tmp_file, name=name, no_progress_bar=no_progress_bar
)
self.protect(tmp_file)
os.rename(tmp_file, to_info)
@staticmethod
def _download(
from_info, to_file, name=None, no_progress_bar=False, **_kwargs
):
copyfile(
from_info, to_file, no_progress_bar=no_progress_bar, name=name
)
def list_paths(self, prefix=None, progress_callback=None):
assert self.path_info is not None
if prefix:
path_info = self.path_info / prefix[:2]
if not self.tree.exists(path_info):
if not self.exists(path_info):
return
else:
path_info = self.path_info
# NOTE: use utils.fs walk_files since tree.walk_files will not follow
# symlinks
if progress_callback:
for path in walk_files(path_info):
progress_callback()
@@ -351,13 +316,40 @@ class LocalRemote(BaseRemote):
def _remove_unpacked_dir(self, checksum):
info = self.checksum_to_path_info(checksum)
path_info = info.with_name(info.name + self.UNPACKED_DIR_SUFFIX)
self.tree.remove(path_info)
self.remove(path_info)
class LocalCache(LocalRemote, CacheMixin):
def __init__(self, repo, config):
super().__init__(repo, config)
self.cache_dir = config.get("url")
def _log_exceptions(func, operation):
@wraps(func)
def wrapper(from_info, to_info, *args, **kwargs):
try:
func(from_info, to_info, *args, **kwargs)
return 0
except Exception as exc:
# NOTE: this means we ran out of file descriptors and there is no
# reason to try to proceed, as we will hit this error anyways.
if isinstance(exc, OSError) and exc.errno == errno.EMFILE:
raise
logger.exception(
"failed to %s '%s' to '%s'", operation, from_info, to_info
)
return 1
return wrapper
class LocalRemote(Remote):
INDEX_CLS = RemoteIndexNoop
class LocalCache(CloudCache):
DEFAULT_CACHE_TYPES = ["reflink", "copy"]
CACHE_MODE = LocalRemoteTree.CACHE_MODE
def __init__(self, tree):
super().__init__(tree)
self.cache_dir = tree.config.get("url")
@property
def cache_dir(self):
@@ -513,7 +505,7 @@ class LocalCache(LocalRemote, CacheMixin):
indexed_dir_exists = set()
if indexed_dirs:
indexed_dir_exists.update(
remote._list_checksums_exists(indexed_dirs)
remote.tree.list_checksums_exists(indexed_dirs)
)
missing_dirs = indexed_dirs.difference(indexed_dir_exists)
if missing_dirs:
@@ -525,7 +517,9 @@ class LocalCache(LocalRemote, CacheMixin):
# Check if non-indexed (new) dir checksums exist on remote
dir_exists = dir_md5s.intersection(indexed_dir_exists)
dir_exists.update(remote._list_checksums_exists(dir_md5s - dir_exists))
dir_exists.update(
remote.tree.list_checksums_exists(dir_md5s - dir_exists)
)
# If .dir checksum exists on the remote, assume directory contents
# still exists on the remote
@@ -605,7 +599,7 @@ class LocalCache(LocalRemote, CacheMixin):
desc = "Uploading"
if jobs is None:
jobs = remote.JOBS
jobs = remote.tree.JOBS
dir_status, file_status, dir_contents = self._status(
named_cache,
+24 -28
View File
@@ -6,14 +6,37 @@ from funcy import cached_property, wrap_prop
from dvc.path_info import CloudURLInfo
from dvc.progress import Tqdm
from dvc.remote.base import BaseRemote, BaseRemoteTree
from dvc.remote.base import BaseRemoteTree
from dvc.scheme import Schemes
logger = logging.getLogger(__name__)
class OSSRemoteTree(BaseRemoteTree):
"""
oss2 document:
https://www.alibabacloud.com/help/doc-detail/32026.htm
Examples
----------
$ dvc remote add myremote oss://my-bucket/path
Set key id, key secret and endpoint using modify command
$ dvc remote modify myremote oss_key_id my-key-id
$ dvc remote modify myremote oss_key_secret my-key-secret
$ dvc remote modify myremote oss_endpoint endpoint
or environment variables
$ export OSS_ACCESS_KEY_ID="my-key-id"
$ export OSS_ACCESS_KEY_SECRET="my-key-secret"
$ export OSS_ENDPOINT="endpoint"
"""
scheme = Schemes.OSS
PATH_CLS = CloudURLInfo
REQUIRES = {"oss2": "oss2"}
PARAM_CHECKSUM = "etag"
COPY_POLL_SECONDS = 5
LIST_OBJECT_PAGE_SIZE = 100
def __init__(self, repo, config):
super().__init__(repo, config)
@@ -105,30 +128,3 @@ class OSSRemoteTree(BaseRemoteTree):
self.oss_service.get_object_to_file(
from_info.path, to_file, progress_callback=pbar.update_to
)
class OSSRemote(BaseRemote):
"""
oss2 document:
https://www.alibabacloud.com/help/doc-detail/32026.htm
Examples
----------
$ dvc remote add myremote oss://my-bucket/path
Set key id, key secret and endpoint using modify command
$ dvc remote modify myremote oss_key_id my-key-id
$ dvc remote modify myremote oss_key_secret my-key-secret
$ dvc remote modify myremote oss_endpoint endpoint
or environment variables
$ export OSS_ACCESS_KEY_ID="my-key-id"
$ export OSS_ACCESS_KEY_SECRET="my-key-secret"
$ export OSS_ENDPOINT="endpoint"
"""
scheme = Schemes.OSS
REQUIRES = {"oss2": "oss2"}
PARAM_CHECKSUM = "etag"
COPY_POLL_SECONDS = 5
LIST_OBJECT_PAGE_SIZE = 100
TREE_CLS = OSSRemoteTree
+4 -12
View File
@@ -8,14 +8,17 @@ from dvc.config import ConfigError
from dvc.exceptions import DvcException, ETagMismatchError
from dvc.path_info import CloudURLInfo
from dvc.progress import Tqdm
from dvc.remote.base import BaseRemote, BaseRemoteTree, CacheMixin
from dvc.remote.base import BaseRemoteTree
from dvc.scheme import Schemes
logger = logging.getLogger(__name__)
class S3RemoteTree(BaseRemoteTree):
scheme = Schemes.S3
PATH_CLS = CloudURLInfo
REQUIRES = {"boto3": "boto3"}
PARAM_CHECKSUM = "etag"
def __init__(self, repo, config):
super().__init__(repo, config)
@@ -334,14 +337,3 @@ class S3RemoteTree(BaseRemoteTree):
self.s3.download_file(
from_info.bucket, from_info.path, to_file, Callback=pbar.update
)
class S3Remote(BaseRemote):
scheme = Schemes.S3
REQUIRES = {"boto3": "boto3"}
PARAM_CHECKSUM = "etag"
TREE_CLS = S3RemoteTree
class S3Cache(S3Remote, CacheMixin):
pass
+22 -25
View File
@@ -14,7 +14,7 @@ from funcy import first, memoize, silent, wrap_with
import dvc.prompt as prompt
from dvc.progress import Tqdm
from dvc.remote.base import BaseRemote, BaseRemoteTree, CacheMixin
from dvc.remote.base import BaseRemoteTree, Remote
from dvc.remote.pool import get_connection
from dvc.scheme import Schemes
from dvc.utils import to_chunks
@@ -34,6 +34,18 @@ def ask_password(host, user, port):
class SSHRemoteTree(BaseRemoteTree):
scheme = Schemes.SSH
REQUIRES = {"paramiko": "paramiko"}
JOBS = 4
PARAM_CHECKSUM = "md5"
# At any given time some of the connections will go over network and
# paramiko stuff, so we would ideally have it double of server processors.
# We use conservative setting of 4 instead to not exhaust max sessions.
CHECKSUM_JOBS = 4
DEFAULT_CACHE_TYPES = ["copy"]
TRAVERSE_PREFIX_LEN = 2
DEFAULT_PORT = 22
TIMEOUT = 1800
@@ -256,27 +268,12 @@ class SSHRemoteTree(BaseRemoteTree):
no_progress_bar=no_progress_bar,
)
class SSHRemote(BaseRemote):
scheme = Schemes.SSH
REQUIRES = {"paramiko": "paramiko"}
JOBS = 4
TREE_CLS = SSHRemoteTree
PARAM_CHECKSUM = "md5"
# At any given time some of the connections will go over network and
# paramiko stuff, so we would ideally have it double of server processors.
# We use conservative setting of 4 instead to not exhaust max sessions.
CHECKSUM_JOBS = 4
DEFAULT_CACHE_TYPES = ["copy"]
TRAVERSE_PREFIX_LEN = 2
def list_paths(self, prefix=None, progress_callback=None):
if prefix:
root = posixpath.join(self.path_info.path, prefix[:2])
else:
root = self.path_info.path
with self.tree.ssh(self.path_info) as ssh:
with self.ssh(self.path_info) as ssh:
if prefix and not ssh.exists(root):
return
# If we simply return an iterator then with above closes instantly
@@ -287,6 +284,8 @@ class SSHRemote(BaseRemote):
else:
yield from ssh.walk_files(root)
class SSHRemote(Remote):
def batch_exists(self, path_infos, callback):
def _exists(chunk_and_channel):
chunk, channel = chunk_and_channel
@@ -321,8 +320,8 @@ class SSHRemote(BaseRemote):
faster than current approach (relying on exists(path_info)) applied in
remote/base.
"""
if not self.CAN_TRAVERSE:
return list(set(checksums) & set(self.all()))
if not self.tree.CAN_TRAVERSE:
return list(set(checksums) & set(self.tree.all()))
# possibly prompt for credentials before "Querying" progress output
self.tree.ensure_credentials()
@@ -337,14 +336,12 @@ class SSHRemote(BaseRemote):
def exists_with_progress(chunks):
return self.batch_exists(chunks, callback=pbar.update_msg)
with ThreadPoolExecutor(max_workers=jobs or self.JOBS) as executor:
with ThreadPoolExecutor(
max_workers=jobs or self.tree.JOBS
) as executor:
path_infos = [self.checksum_to_path_info(x) for x in checksums]
chunks = to_chunks(path_infos, num_chunks=self.JOBS)
chunks = to_chunks(path_infos, num_chunks=self.tree.JOBS)
results = executor.map(exists_with_progress, chunks)
in_remote = itertools.chain.from_iterable(results)
ret = list(itertools.compress(checksums, in_remote))
return ret
class SSHCache(SSHRemote, CacheMixin):
pass
+1 -1
View File
@@ -187,7 +187,7 @@ class Repo:
return Repo(root_dir)
def unprotect(self, target):
return self.cache.local.unprotect(PathInfo(target))
return self.cache.local.tree.unprotect(PathInfo(target))
def _ignore(self):
flist = [self.config.files["local"], self.tmp_dir]
+12 -9
View File
@@ -8,8 +8,10 @@ from . import locked
logger = logging.getLogger(__name__)
def _do_gc(typ, func, clist, jobs=None):
removed = func(clist, jobs=jobs)
def _do_gc(typ, remote, clist, jobs=None):
from dvc.remote.base import Remote
removed = Remote.gc(clist, remote, jobs=jobs)
if not removed:
logger.info(f"No unused '{typ}' cache to remove.")
@@ -74,22 +76,23 @@ def gc(
)
)
_do_gc("local", self.cache.local.gc, used, jobs)
# treat caches as remotes for garbage collection
_do_gc("local", self.cache.local, used, jobs)
if self.cache.s3:
_do_gc("s3", self.cache.s3.gc, used, jobs)
_do_gc("s3", self.cache.s3, used, jobs)
if self.cache.gs:
_do_gc("gs", self.cache.gs.gc, used, jobs)
_do_gc("gs", self.cache.gs, used, jobs)
if self.cache.ssh:
_do_gc("ssh", self.cache.ssh.gc, used, jobs)
_do_gc("ssh", self.cache.ssh, used, jobs)
if self.cache.hdfs:
_do_gc("hdfs", self.cache.hdfs.gc, used, jobs)
_do_gc("hdfs", self.cache.hdfs, used, jobs)
if self.cache.azure:
_do_gc("azure", self.cache.azure.gc, used, jobs)
_do_gc("azure", self.cache.azure, used, jobs)
if cloud:
_do_gc("remote", self.cloud.get_remote(remote, "gc -c").gc, used, jobs)
_do_gc("remote", self.cloud.get_remote(remote, "gc -c"), used, jobs)
+3 -3
View File
@@ -48,11 +48,11 @@ class DvcTree(BaseTree):
raise FileNotFoundError
dir_cache = out.get_dir_cache(remote=remote)
for entry in dir_cache:
entry_relpath = entry[out.remote.PARAM_RELPATH]
entry_relpath = entry[out.remote.tree.PARAM_RELPATH]
if os.name == "nt":
entry_relpath = entry_relpath.replace("/", os.sep)
if path == out.path_info / entry_relpath:
return entry[out.remote.PARAM_CHECKSUM]
return entry[out.remote.tree.PARAM_CHECKSUM]
raise FileNotFoundError
def open(self, path, mode="r", encoding="utf-8", remote=None):
@@ -194,7 +194,7 @@ class DvcTree(BaseTree):
download_callback(downloaded)
for entry in dir_cache:
entry_relpath = entry[out.remote.PARAM_RELPATH]
entry_relpath = entry[out.remote.tree.PARAM_RELPATH]
if os.name == "nt":
entry_relpath = entry_relpath.replace("/", os.sep)
path_info = out.path_info / entry_relpath
+4 -3
View File
@@ -8,7 +8,8 @@ from dvc import dependency, output
from dvc.utils.fs import path_isin
from ..dependency import ParamsDependency
from ..remote import LocalRemote, S3Remote
from ..remote.local import LocalRemoteTree
from ..remote.s3 import S3RemoteTree
from ..utils import dict_md5, format_link, relpath
from .exceptions import (
MissingDataSource,
@@ -132,8 +133,8 @@ def stage_dump_eq(stage_cls, old_d, new_d):
new_d.pop(stage_cls.PARAM_MD5, None)
outs = old_d.get(stage_cls.PARAM_OUTS, [])
for out in outs:
out.pop(LocalRemote.PARAM_CHECKSUM, None)
out.pop(S3Remote.PARAM_CHECKSUM, None)
out.pop(LocalRemoteTree.PARAM_CHECKSUM, None)
out.pop(S3RemoteTree.PARAM_CHECKSUM, None)
# outs and deps are lists of dicts. To check equality, we need to make
# them independent of the order, so, we convert them to dicts.
+3 -3
View File
@@ -3,7 +3,7 @@ import os
import pytest
from dvc.exceptions import DownloadError, UploadError
from dvc.remote.base import BaseRemote
from dvc.remote.base import Remote
from dvc.remote.index import RemoteIndex
from dvc.remote.local import LocalRemote, LocalRemoteTree
from dvc.utils.fs import remove
@@ -16,9 +16,9 @@ def remote(tmp_dir, dvc, tmp_path_factory, mocker):
dvc.config["core"]["remote"] = "upstream"
# patch checksums_exist since the LocalRemote normally overrides
# BaseRemote.checksums_exist.
# BaseRemoteTree.checksums_exist.
def checksums_exist(self, *args, **kwargs):
return BaseRemote.checksums_exist(self, *args, **kwargs)
return Remote.checksums_exist(self, *args, **kwargs)
mocker.patch.object(LocalRemote, "checksums_exist", checksums_exist)
+2 -2
View File
@@ -21,7 +21,7 @@ from dvc.exceptions import (
)
from dvc.main import main
from dvc.output.base import OutputAlreadyTrackedError, OutputIsStageFileError
from dvc.remote.local import LocalRemote, LocalRemoteTree
from dvc.remote.local import LocalRemoteTree
from dvc.repo import Repo as DvcRepo
from dvc.stage import Stage
from dvc.system import System
@@ -583,7 +583,7 @@ def test_readding_dir_should_not_unprotect_all(tmp_dir, dvc, mocker):
dvc.add("dir")
tmp_dir.gen("dir/new_file", "new_file_content")
unprotect_spy = mocker.spy(LocalRemote, "unprotect")
unprotect_spy = mocker.spy(LocalRemoteTree, "unprotect")
dvc.add("dir")
assert not unprotect_spy.mock.called
+5 -5
View File
@@ -30,14 +30,14 @@ class TestCache(TestDvc):
self.create(self.cache2, "2")
def test_all(self):
md5_list = list(Cache(self.dvc).local.all())
md5_list = list(Cache(self.dvc).local.tree.all())
self.assertEqual(len(md5_list), 2)
self.assertIn(self.cache1_md5, md5_list)
self.assertIn(self.cache2_md5, md5_list)
def test_get(self):
cache = Cache(self.dvc).local.get(self.cache1_md5)
self.assertEqual(cache, self.cache1)
cache = Cache(self.dvc).local.checksum_to_path_info(self.cache1_md5)
self.assertEqual(os.fspath(cache), self.cache1)
class TestCacheLoadBadDirCache(TestDvc):
@@ -47,13 +47,13 @@ class TestCacheLoadBadDirCache(TestDvc):
def test(self):
checksum = "123.dir"
fname = self.dvc.cache.local.get(checksum)
fname = os.fspath(self.dvc.cache.local.checksum_to_path_info(checksum))
self.create(fname, "<clearly>not,json")
with pytest.raises(DirCacheError):
self.dvc.cache.local.load_dir_cache(checksum)
checksum = "234.dir"
fname = self.dvc.cache.local.get(checksum)
fname = os.fspath(self.dvc.cache.local.checksum_to_path_info(checksum))
self.create(fname, '{"a": "b"}')
self._do_test(self.dvc.cache.local.load_dir_cache(checksum))
+9 -8
View File
@@ -16,8 +16,9 @@ from dvc.exceptions import (
NoOutputOrStageError,
)
from dvc.main import main
from dvc.remote import S3Cache, S3Remote
from dvc.remote.local import LocalRemote
from dvc.remote.base import CloudCache, Remote
from dvc.remote.local import LocalRemoteTree
from dvc.remote.s3 import S3RemoteTree
from dvc.repo import Repo as DvcRepo
from dvc.stage import Stage
from dvc.stage.exceptions import StageFileDoesNotExistError
@@ -99,8 +100,8 @@ class TestCheckoutCorruptedCacheDir(TestDvc):
# NOTE: modifying cache file for one of the files inside the directory
# to check if dvc will detect that the cache is corrupted.
entry = self.dvc.cache.local.load_dir_cache(out.checksum)[0]
checksum = entry[self.dvc.cache.local.PARAM_CHECKSUM]
cache = self.dvc.cache.local.get(checksum)
checksum = entry[self.dvc.cache.local.tree.PARAM_CHECKSUM]
cache = os.fspath(self.dvc.cache.local.checksum_to_path_info(checksum))
os.chmod(cache, 0o644)
with open(cache, "w+") as fobj:
@@ -305,8 +306,8 @@ class TestGitIgnoreWhenCheckout(CheckoutBase):
class TestCheckoutMissingMd5InStageFile(TestRepro):
def test(self):
d = load_yaml(self.file1_stage)
del d[Stage.PARAM_OUTS][0][LocalRemote.PARAM_CHECKSUM]
del d[Stage.PARAM_DEPS][0][LocalRemote.PARAM_CHECKSUM]
del d[Stage.PARAM_OUTS][0][LocalRemoteTree.PARAM_CHECKSUM]
del d[Stage.PARAM_DEPS][0][LocalRemoteTree.PARAM_CHECKSUM]
dump_yaml(self.file1_stage, d)
with pytest.raises(CheckoutError):
@@ -755,9 +756,9 @@ def test_checkout_recursive(tmp_dir, dvc):
not S3.should_test(), reason="Only run with S3 credentials"
)
def test_checkout_for_external_outputs(tmp_dir, dvc):
dvc.cache.s3 = S3Cache(dvc, {"url": S3.get_url()})
dvc.cache.s3 = CloudCache(S3RemoteTree(dvc, {"url": S3.get_url()}))
remote = S3Remote(dvc, {"url": S3.get_url()})
remote = Remote(S3RemoteTree(dvc, {"url": S3.get_url()}))
file_path = remote.path_info / "foo"
remote.tree.s3.put_object(
Bucket=remote.path_info.bucket, Key=file_path.path, Body="foo"
+43 -37
View File
@@ -12,19 +12,16 @@ from dvc.cache import NamedCache
from dvc.data_cloud import DataCloud
from dvc.external_repo import clean_repos
from dvc.main import main
from dvc.remote import (
AzureRemote,
GDriveRemote,
GSRemote,
HDFSRemote,
HTTPRemote,
LocalRemote,
OSSRemote,
S3Remote,
SSHRemote,
)
from dvc.remote.azure import AzureRemoteTree
from dvc.remote.base import STATUS_DELETED, STATUS_NEW, STATUS_OK
from dvc.remote.gdrive import GDriveRemoteTree
from dvc.remote.gs import GSRemoteTree
from dvc.remote.hdfs import HDFSRemoteTree
from dvc.remote.http import HTTPRemoteTree
from dvc.remote.local import LocalRemoteTree
from dvc.remote.oss import OSSRemoteTree
from dvc.remote.s3 import S3RemoteTree
from dvc.remote.ssh import SSHRemoteTree
from dvc.stage.exceptions import StageNotFound
from dvc.utils import file_md5
from dvc.utils.fs import remove
@@ -50,19 +47,19 @@ class TestDataCloud(TestDvc):
def _test_cloud(self, config, cl):
self.dvc.config = config
cloud = DataCloud(self.dvc)
self.assertIsInstance(cloud.get_remote(), cl)
self.assertIsInstance(cloud.get_remote().tree, cl)
def test(self):
config = copy.deepcopy(TEST_CONFIG)
clist = [
("s3://mybucket/", S3Remote),
("gs://mybucket/", GSRemote),
("ssh://user@localhost:/", SSHRemote),
("http://localhost:8000/", HTTPRemote),
("azure://ContainerName=mybucket;conn_string;", AzureRemote),
("oss://mybucket/", OSSRemote),
(TestDvc.mkdtemp(), LocalRemote),
("s3://mybucket/", S3RemoteTree),
("gs://mybucket/", GSRemoteTree),
("ssh://user@localhost:/", SSHRemoteTree),
("http://localhost:8000/", HTTPRemoteTree),
("azure://ContainerName=mybucket;conn_string;", AzureRemoteTree),
("oss://mybucket/", OSSRemoteTree),
(TestDvc.mkdtemp(), LocalRemoteTree),
]
for scheme, cl in clist:
@@ -101,7 +98,9 @@ class TestDataCloudBase(TestDvc):
self.dvc.config = config
self.cloud = DataCloud(self.dvc)
self.assertIsInstance(self.cloud.get_remote(), self._get_cloud_class())
self.assertIsInstance(
self.cloud.get_remote().tree, self._get_cloud_class()
)
def _test_cloud(self):
self._setup_cloud()
@@ -187,7 +186,7 @@ class TestDataCloudBase(TestDvc):
class TestS3Remote(S3, TestDataCloudBase):
def _get_cloud_class(self):
return S3Remote
return S3RemoteTree
class TestGDriveRemote(GDrive, TestDataCloudBase):
@@ -208,10 +207,10 @@ class TestGDriveRemote(GDrive, TestDataCloudBase):
self.dvc.config = config
self.cloud = DataCloud(self.dvc)
remote = self.cloud.get_remote()
self.assertIsInstance(remote, self._get_cloud_class())
self.assertIsInstance(remote.tree, self._get_cloud_class())
def _get_cloud_class(self):
return GDriveRemote
return GDriveRemoteTree
class TestGSRemote(GCP, TestDataCloudBase):
@@ -228,25 +227,27 @@ class TestGSRemote(GCP, TestDataCloudBase):
self.dvc.config = config
self.cloud = DataCloud(self.dvc)
self.assertIsInstance(self.cloud.get_remote(), self._get_cloud_class())
self.assertIsInstance(
self.cloud.get_remote().tree, self._get_cloud_class()
)
def _get_cloud_class(self):
return GSRemote
return GSRemoteTree
class TestAzureRemote(Azure, TestDataCloudBase):
def _get_cloud_class(self):
return AzureRemote
return AzureRemoteTree
class TestOSSRemote(OSS, TestDataCloudBase):
def _get_cloud_class(self):
return OSSRemote
return OSSRemoteTree
class TestLocalRemote(Local, TestDataCloudBase):
def _get_cloud_class(self):
return LocalRemote
return LocalRemoteTree
@pytest.mark.usefixtures("ssh_server")
@@ -271,7 +272,9 @@ class TestSSHRemoteMocked(SSHMocked, TestDataCloudBase):
self.dvc.config = config
self.cloud = DataCloud(self.dvc)
self.assertIsInstance(self.cloud.get_remote(), self._get_cloud_class())
self.assertIsInstance(
self.cloud.get_remote().tree, self._get_cloud_class()
)
def get_url(self):
user = self.ssh_server.test_creds["username"]
@@ -281,12 +284,12 @@ class TestSSHRemoteMocked(SSHMocked, TestDataCloudBase):
return self.ssh_server.test_creds["key_filename"]
def _get_cloud_class(self):
return SSHRemote
return SSHRemoteTree
class TestHDFSRemote(HDFS, TestDataCloudBase):
def _get_cloud_class(self):
return HDFSRemote
return HDFSRemoteTree
@pytest.mark.usefixtures("http_server")
@@ -300,7 +303,7 @@ class TestHTTPRemote(HTTP, TestDataCloudBase):
return super().get_url(self.http_server.server_port)
def _get_cloud_class(self):
return HTTPRemote
return HTTPRemoteTree
class TestDataCloudCLIBase(TestDvc):
@@ -544,7 +547,7 @@ class TestRecursiveSyncOperations(Local, TestDataCloudBase):
self.assertEqual(ret, 0)
def _get_cloud_class(self):
return LocalRemote
return LocalRemoteTree
def _prepare_repo(self):
remote = self.cloud.get_remote()
@@ -577,8 +580,11 @@ class TestRecursiveSyncOperations(Local, TestDataCloudBase):
def _test_recursive_fetch(self, data_md5, data_sub_md5):
self._clear_local_cache()
local_cache_data_path = self.dvc.cache.local.get(data_md5)
local_cache_data_sub_path = self.dvc.cache.local.get(data_sub_md5)
local_cache = self.dvc.cache.local
local_cache_data_path = local_cache.checksum_to_path_info(data_md5)
local_cache_data_sub_path = local_cache.checksum_to_path_info(
data_sub_md5
)
self.assertFalse(os.path.exists(local_cache_data_path))
self.assertFalse(os.path.exists(local_cache_data_sub_path))
@@ -590,8 +596,8 @@ class TestRecursiveSyncOperations(Local, TestDataCloudBase):
def _test_recursive_push(self, data_md5, data_sub_md5):
remote = self.cloud.get_remote()
cloud_data_path = remote.get(data_md5)
cloud_data_sub_path = remote.get(data_sub_md5)
cloud_data_path = remote.checksum_to_path_info(data_md5)
cloud_data_sub_path = remote.checksum_to_path_info(data_sub_md5)
self.assertFalse(os.path.exists(cloud_data_path))
self.assertFalse(os.path.exists(cloud_data_sub_path))
+4 -3
View File
@@ -8,7 +8,7 @@ from git import Repo
from dvc.exceptions import CollectCacheError
from dvc.main import main
from dvc.remote.local import LocalRemote, LocalRemoteTree
from dvc.remote.local import LocalRemoteTree
from dvc.repo import Repo as DvcRepo
from dvc.utils.fs import remove
from tests.basic_env import TestDir, TestDvcGit
@@ -21,7 +21,8 @@ class TestGC(TestDvcGit):
self.dvc.add(self.FOO)
self.dvc.add(self.DATA_DIR)
self.good_cache = [
self.dvc.cache.local.get(md5) for md5 in self.dvc.cache.local.all()
self.dvc.cache.local.checksum_to_path_info(md5)
for md5 in self.dvc.cache.local.tree.all()
]
self.bad_cache = []
@@ -216,7 +217,7 @@ def test_gc_no_unpacked_dir(tmp_dir, dvc):
os.remove("dir.dvc")
unpackeddir = (
dir_stages[0].outs[0].cache_path + LocalRemote.UNPACKED_DIR_SUFFIX
dir_stages[0].outs[0].cache_path + LocalRemoteTree.UNPACKED_DIR_SUFFIX
)
# older (pre 1.0) versions of dvc used to generate this dir
+3 -3
View File
@@ -26,7 +26,7 @@ from dvc.exceptions import (
from dvc.main import main
from dvc.output.base import BaseOutput
from dvc.path_info import URLInfo
from dvc.remote.local import LocalRemote, LocalRemoteTree
from dvc.remote.local import LocalRemoteTree
from dvc.repo import Repo as DvcRepo
from dvc.stage import Stage
from dvc.stage.exceptions import StageFileDoesNotExistError
@@ -782,8 +782,8 @@ class TestReproChangedDirData(SingleStageRun, TestDvc):
class TestReproMissingMd5InStageFile(TestRepro):
def test(self):
d = load_yaml(self.file1_stage)
del d[Stage.PARAM_OUTS][0][LocalRemote.PARAM_CHECKSUM]
del d[Stage.PARAM_DEPS][0][LocalRemote.PARAM_CHECKSUM]
del d[Stage.PARAM_OUTS][0][LocalRemoteTree.PARAM_CHECKSUM]
del d[Stage.PARAM_DEPS][0][LocalRemoteTree.PARAM_CHECKSUM]
dump_yaml(self.file1_stage, d)
stages = self.dvc.reproduce(self.file1_stage)
+6 -4
View File
@@ -5,7 +5,8 @@ import moto.s3.models as s3model
import pytest
from moto import mock_s3
from dvc.remote.s3 import S3Cache, S3Remote, S3RemoteTree
from dvc.remote.base import CloudCache
from dvc.remote.s3 import S3RemoteTree
from tests.remotes import S3
# from https://github.com/spulec/moto/blob/v1.3.5/tests/test_s3/test_s3.py#L40
@@ -54,7 +55,8 @@ def test_copy_singlepart_preserve_etag():
],
)
def test_link_created_on_non_nested_path(base_info, tmp_dir, dvc, scm):
cache = S3Cache(dvc, {"url": str(base_info.parent)})
tree = S3RemoteTree(dvc, {"url": str(base_info.parent)})
cache = CloudCache(tree)
s3 = cache.tree.s3
s3.create_bucket(Bucket=base_info.bucket)
s3.put_object(
@@ -69,8 +71,8 @@ def test_link_created_on_non_nested_path(base_info, tmp_dir, dvc, scm):
@mock_s3
def test_makedirs_doesnot_try_on_top_level_paths(tmp_dir, dvc, scm):
base_info = S3RemoteTree.PATH_CLS("s3://bucket/")
remote = S3Remote(dvc, {"url": str(base_info)})
remote.tree.makedirs(base_info)
tree = S3RemoteTree(dvc, {"url": str(base_info)})
tree.makedirs(base_info)
def _upload_multipart(s3, Bucket, Key):
+3 -3
View File
@@ -6,7 +6,7 @@ import pytest
from dvc.dvcfile import SingleStageFile
from dvc.main import main
from dvc.output.local import LocalOutput
from dvc.remote.local import LocalRemote
from dvc.remote.local import LocalRemoteTree
from dvc.repo import Repo
from dvc.stage import PipelineStage, Stage
from dvc.stage.exceptions import StageFileFormatError
@@ -54,8 +54,8 @@ def test_empty_list():
def test_list():
lst = [
{LocalOutput.PARAM_PATH: "foo", LocalRemote.PARAM_CHECKSUM: "123"},
{LocalOutput.PARAM_PATH: "bar", LocalRemote.PARAM_CHECKSUM: None},
{LocalOutput.PARAM_PATH: "foo", LocalRemoteTree.PARAM_CHECKSUM: "123"},
{LocalOutput.PARAM_PATH: "bar", LocalRemoteTree.PARAM_CHECKSUM: None},
{LocalOutput.PARAM_PATH: "baz"},
]
d = {Stage.PARAM_DEPS: lst}
+1 -1
View File
@@ -192,7 +192,7 @@ def test_repotree_walk_fetch(tmp_dir, dvc, scm, setup_remote):
assert os.path.exists(out.cache_path)
for entry in out.dir_cache:
checksum = entry[out.remote.PARAM_CHECKSUM]
checksum = entry[out.remote.tree.PARAM_CHECKSUM]
assert os.path.exists(dvc.cache.local.checksum_to_path_info(checksum))
+8 -7
View File
@@ -7,9 +7,10 @@ from subprocess import CalledProcessError, Popen, check_output
from moto.s3 import mock_s3
from dvc.remote.gdrive import GDriveRemote, GDriveRemoteTree
from dvc.remote.gs import GSRemote
from dvc.remote.s3 import S3Remote
from dvc.remote.base import Remote
from dvc.remote.gdrive import GDriveRemoteTree
from dvc.remote.gs import GSRemoteTree
from dvc.remote.s3 import S3RemoteTree
from dvc.utils import env2bool
from tests.basic_env import TestDvc
@@ -79,7 +80,7 @@ class S3Mocked(S3):
@contextmanager
def remote(cls, repo):
with mock_s3():
yield S3Remote(repo, {"url": cls.get_url()})
yield Remote(S3RemoteTree(repo, {"url": cls.get_url()}))
@staticmethod
def put_objects(remote, objects):
@@ -127,7 +128,7 @@ class GCP:
@classmethod
@contextmanager
def remote(cls, repo):
yield GSRemote(repo, {"url": cls.get_url()})
yield Remote(GSRemoteTree(repo, {"url": cls.get_url()}))
@staticmethod
def put_objects(remote, objects):
@@ -150,8 +151,8 @@ class GDrive:
"gdrive_service_account_p12_file_path": "test.p12",
"gdrive_use_service_account": True,
}
remote = GDriveRemote(dvc, config)
remote.tree._gdrive_create_dir("root", remote.path_info.path)
tree = GDriveRemoteTree(dvc, config)
tree._gdrive_create_dir("root", tree.path_info.path)
@staticmethod
def get_storagepath():
+22 -22
View File
@@ -5,7 +5,7 @@ import sys
import pytest
from mock import mock_open, patch
from dvc.remote.ssh import SSHRemote, SSHRemoteTree
from dvc.remote.ssh import SSHRemoteTree
from dvc.system import System
from tests.remotes import SSHMocked
@@ -20,21 +20,21 @@ def test_url(dvc):
url = f"ssh://{user}@{host}:{port}{path}"
config = {"url": url}
remote = SSHRemote(dvc, config)
assert remote.path_info == url
tree = SSHRemoteTree(dvc, config)
assert tree.path_info == url
# SCP-like URL ssh://[user@]host.xz:/absolute/path
url = f"ssh://{user}@{host}:{path}"
config = {"url": url}
remote = SSHRemote(dvc, config)
assert remote.tree.path_info == url
tree = SSHRemoteTree(dvc, config)
assert tree.path_info == url
def test_no_path(dvc):
config = {"url": "ssh://127.0.0.1"}
remote = SSHRemote(dvc, config)
assert remote.tree.path_info.path == ""
tree = SSHRemoteTree(dvc, config)
assert tree.path_info.path == ""
mock_ssh_config = """
@@ -67,11 +67,11 @@ else:
def test_ssh_host_override_from_config(
mock_file, mock_exists, dvc, config, expected_host
):
remote = SSHRemote(dvc, config)
tree = SSHRemoteTree(dvc, config)
mock_exists.assert_called_with(SSHRemoteTree.ssh_config_filename())
mock_file.assert_called_with(SSHRemoteTree.ssh_config_filename())
assert remote.tree.path_info.host == expected_host
assert tree.path_info.host == expected_host
@pytest.mark.parametrize(
@@ -95,11 +95,11 @@ def test_ssh_host_override_from_config(
read_data=mock_ssh_config,
)
def test_ssh_user(mock_file, mock_exists, dvc, config, expected_user):
remote = SSHRemote(dvc, config)
tree = SSHRemoteTree(dvc, config)
mock_exists.assert_called_with(SSHRemoteTree.ssh_config_filename())
mock_file.assert_called_with(SSHRemoteTree.ssh_config_filename())
assert remote.tree.path_info.user == expected_user
assert tree.path_info.user == expected_user
@pytest.mark.parametrize(
@@ -120,11 +120,11 @@ def test_ssh_user(mock_file, mock_exists, dvc, config, expected_user):
read_data=mock_ssh_config,
)
def test_ssh_port(mock_file, mock_exists, dvc, config, expected_port):
remote = SSHRemote(dvc, config)
tree = SSHRemoteTree(dvc, config)
mock_exists.assert_called_with(SSHRemoteTree.ssh_config_filename())
mock_file.assert_called_with(SSHRemoteTree.ssh_config_filename())
assert remote.path_info.port == expected_port
assert tree.path_info.port == expected_port
@pytest.mark.parametrize(
@@ -155,11 +155,11 @@ def test_ssh_port(mock_file, mock_exists, dvc, config, expected_port):
read_data=mock_ssh_config,
)
def test_ssh_keyfile(mock_file, mock_exists, dvc, config, expected_keyfile):
remote = SSHRemote(dvc, config)
tree = SSHRemoteTree(dvc, config)
mock_exists.assert_called_with(SSHRemoteTree.ssh_config_filename())
mock_file.assert_called_with(SSHRemoteTree.ssh_config_filename())
assert remote.tree.keyfile == expected_keyfile
assert tree.keyfile == expected_keyfile
@pytest.mark.parametrize(
@@ -177,11 +177,11 @@ def test_ssh_keyfile(mock_file, mock_exists, dvc, config, expected_keyfile):
read_data=mock_ssh_config,
)
def test_ssh_gss_auth(mock_file, mock_exists, dvc, config, expected_gss_auth):
remote = SSHRemote(dvc, config)
tree = SSHRemoteTree(dvc, config)
mock_exists.assert_called_with(SSHRemoteTree.ssh_config_filename())
mock_file.assert_called_with(SSHRemoteTree.ssh_config_filename())
assert remote.tree.gss_auth == expected_gss_auth
assert tree.gss_auth == expected_gss_auth
def test_hardlink_optimization(dvc, tmp_dir, ssh_server):
@@ -194,12 +194,12 @@ def test_hardlink_optimization(dvc, tmp_dir, ssh_server):
"user": user,
"keyfile": ssh_server.test_creds["key_filename"],
}
remote = SSHRemote(dvc, config)
tree = SSHRemoteTree(dvc, config)
from_info = remote.path_info / "empty"
to_info = remote.path_info / "link"
from_info = tree.path_info / "empty"
to_info = tree.path_info / "link"
with remote.open(from_info, "wb"):
with tree.open(from_info, "wb"):
pass
if os.name == "nt":
@@ -207,5 +207,5 @@ def test_hardlink_optimization(dvc, tmp_dir, ssh_server):
else:
link_path = to_info.path
remote.tree.hardlink(from_info, to_info)
tree.hardlink(from_info, to_info)
assert not System.is_hardlink(link_path)
+12 -12
View File
@@ -1,7 +1,7 @@
import pytest
from dvc.path_info import PathInfo
from dvc.remote.azure import AzureRemote
from dvc.remote.azure import AzureRemoteTree
from tests.remotes import Azure
container_name = "container-name"
@@ -18,18 +18,18 @@ def test_init_env_var(monkeypatch, dvc):
monkeypatch.setenv("AZURE_STORAGE_CONNECTION_STRING", connection_string)
config = {"url": "azure://"}
remote = AzureRemote(dvc, config)
assert remote.tree.path_info == "azure://" + container_name
assert remote.tree.connection_string == connection_string
tree = AzureRemoteTree(dvc, config)
assert tree.path_info == "azure://" + container_name
assert tree.connection_string == connection_string
def test_init(dvc):
prefix = "some/prefix"
url = f"azure://{container_name}/{prefix}"
config = {"url": url, "connection_string": connection_string}
remote = AzureRemote(dvc, config)
assert remote.tree.path_info == url
assert remote.tree.connection_string == connection_string
tree = AzureRemoteTree(dvc, config)
assert tree.path_info == url
assert tree.connection_string == connection_string
def test_get_file_checksum(tmp_dir):
@@ -38,11 +38,11 @@ def test_get_file_checksum(tmp_dir):
tmp_dir.gen("foo", "foo")
remote = AzureRemote(None, {})
to_info = remote.tree.PATH_CLS(Azure.get_url())
remote.tree.upload(PathInfo("foo"), to_info)
assert remote.tree.exists(to_info)
checksum = remote.tree.get_file_checksum(to_info)
tree = AzureRemoteTree(None, {})
to_info = tree.PATH_CLS(Azure.get_url())
tree.upload(PathInfo("foo"), to_info)
assert tree.exists(to_info)
checksum = tree.get_file_checksum(to_info)
assert checksum
assert isinstance(checksum, str)
assert checksum.strip("'").strip('"') == checksum
+34 -30
View File
@@ -4,7 +4,12 @@ import mock
import pytest
from dvc.path_info import PathInfo
from dvc.remote.base import BaseRemote, RemoteCmdError, RemoteMissingDepsError
from dvc.remote.base import (
BaseRemoteTree,
Remote,
RemoteCmdError,
RemoteMissingDepsError,
)
class _CallableOrNone:
@@ -15,14 +20,13 @@ class _CallableOrNone:
CallableOrNone = _CallableOrNone()
REMOTE_CLS = BaseRemote
def test_missing_deps(dvc):
requires = {"missing": "missing"}
with mock.patch.object(REMOTE_CLS, "REQUIRES", requires):
with mock.patch.object(BaseRemoteTree, "REQUIRES", requires):
with pytest.raises(RemoteMissingDepsError):
REMOTE_CLS(dvc, {})
BaseRemoteTree(dvc, {})
def test_cmd_error(dvc):
@@ -33,44 +37,44 @@ def test_cmd_error(dvc):
err = "sed: expression #1, char 2: extra characters after command"
with mock.patch.object(
REMOTE_CLS.TREE_CLS,
BaseRemoteTree,
"remove",
side_effect=RemoteCmdError("base", cmd, ret, err),
):
with pytest.raises(RemoteCmdError):
REMOTE_CLS(dvc, config).tree.remove("file")
BaseRemoteTree(dvc, config).remove("file")
@mock.patch.object(BaseRemote, "_list_checksums_traverse")
@mock.patch.object(BaseRemote, "_list_checksums_exists")
@mock.patch.object(BaseRemoteTree, "list_checksums_traverse")
@mock.patch.object(BaseRemoteTree, "list_checksums_exists")
def test_checksums_exist(object_exists, traverse, dvc):
remote = BaseRemote(dvc, {})
remote = Remote(BaseRemoteTree(dvc, {}))
# remote does not support traverse
remote.CAN_TRAVERSE = False
remote.tree.CAN_TRAVERSE = False
with mock.patch.object(
remote, "list_checksums", return_value=list(range(256))
remote.tree, "list_checksums", return_value=list(range(256))
):
checksums = set(range(1000))
remote.checksums_exist(checksums)
object_exists.assert_called_with(checksums, None, None)
traverse.assert_not_called()
remote.CAN_TRAVERSE = True
remote.tree.CAN_TRAVERSE = True
# large remote, small local
object_exists.reset_mock()
traverse.reset_mock()
with mock.patch.object(
remote, "list_checksums", return_value=list(range(256))
remote.tree, "list_checksums", return_value=list(range(256))
):
checksums = list(range(1000))
remote.checksums_exist(checksums)
# verify that _cache_paths_with_max() short circuits
# before returning all 256 remote checksums
max_checksums = math.ceil(
remote._max_estimation_size(checksums)
/ pow(16, remote.TRAVERSE_PREFIX_LEN)
remote.tree._max_estimation_size(checksums)
/ pow(16, remote.tree.TRAVERSE_PREFIX_LEN)
)
assert max_checksums < 256
object_exists.assert_called_with(
@@ -81,15 +85,15 @@ def test_checksums_exist(object_exists, traverse, dvc):
# large remote, large local
object_exists.reset_mock()
traverse.reset_mock()
remote.JOBS = 16
remote.tree.JOBS = 16
with mock.patch.object(
remote, "list_checksums", return_value=list(range(256))
remote.tree, "list_checksums", return_value=list(range(256))
):
checksums = list(range(1000000))
remote.checksums_exist(checksums)
object_exists.assert_not_called()
traverse.assert_called_with(
256 * pow(16, remote.TRAVERSE_PREFIX_LEN),
256 * pow(16, remote.tree.TRAVERSE_PREFIX_LEN),
set(range(256)),
None,
None,
@@ -97,18 +101,18 @@ def test_checksums_exist(object_exists, traverse, dvc):
@mock.patch.object(
BaseRemote, "list_checksums", return_value=[],
BaseRemoteTree, "list_checksums", return_value=[],
)
@mock.patch.object(
BaseRemote, "path_to_checksum", side_effect=lambda x: x,
BaseRemoteTree, "path_to_checksum", side_effect=lambda x: x,
)
def test_list_checksums_traverse(path_to_checksum, list_checksums, dvc):
remote = BaseRemote(dvc, {})
remote.tree.path_info = PathInfo("foo")
tree = BaseRemoteTree(dvc, {})
tree.path_info = PathInfo("foo")
# parallel traverse
size = 256 / remote.JOBS * remote.LIST_OBJECT_PAGE_SIZE
list(remote._list_checksums_traverse(size, {0}))
size = 256 / tree.JOBS * tree.LIST_OBJECT_PAGE_SIZE
list(tree.list_checksums_traverse(size, {0}))
for i in range(1, 16):
list_checksums.assert_any_call(
prefix=f"{i:03x}", progress_callback=CallableOrNone
@@ -121,20 +125,20 @@ def test_list_checksums_traverse(path_to_checksum, list_checksums, dvc):
# default traverse (small remote)
size -= 1
list_checksums.reset_mock()
list(remote._list_checksums_traverse(size - 1, {0}))
list(tree.list_checksums_traverse(size - 1, {0}))
list_checksums.assert_called_with(
prefix=None, progress_callback=CallableOrNone
)
def test_list_checksums(dvc):
remote = BaseRemote(dvc, {})
remote.tree.path_info = PathInfo("foo")
tree = BaseRemoteTree(dvc, {})
tree.path_info = PathInfo("foo")
with mock.patch.object(
remote, "list_paths", return_value=["12/3456", "bar"]
tree, "list_paths", return_value=["12/3456", "bar"]
):
checksums = list(remote.list_checksums())
checksums = list(tree.list_checksums())
assert checksums == ["123456"]
@@ -143,4 +147,4 @@ def test_list_checksums(dvc):
[(None, False), ("", False), ("3456.dir", True), ("3456", False)],
)
def test_is_dir_checksum(checksum, result):
assert BaseRemote.is_dir_checksum(checksum) == result
assert BaseRemoteTree.is_dir_checksum(checksum) == result
+7 -7
View File
@@ -2,7 +2,7 @@ import os
import pytest
from dvc.remote.gdrive import GDriveAuthError, GDriveRemote, GDriveRemoteTree
from dvc.remote.gdrive import GDriveAuthError, GDriveRemoteTree
USER_CREDS_TOKEN_REFRESH_ERROR = '{"access_token": "", "client_id": "", "client_secret": "", "refresh_token": "", "token_expiry": "", "token_uri": "https://oauth2.googleapis.com/token", "user_agent": null, "revoke_uri": "https://oauth2.googleapis.com/revoke", "id_token": null, "id_token_jwt": null, "token_response": {"access_token": "", "expires_in": 3600, "scope": "https://www.googleapis.com/auth/drive.appdata https://www.googleapis.com/auth/drive", "token_type": "Bearer"}, "scopes": ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/drive.appdata"], "token_info_uri": "https://oauth2.googleapis.com/tokeninfo", "invalid": true, "_class": "OAuth2Credentials", "_module": "oauth2client.client"}' # noqa: E501
@@ -17,21 +17,21 @@ class TestRemoteGDrive:
}
def test_init(self, dvc):
remote = GDriveRemote(dvc, self.CONFIG)
assert str(remote.tree.path_info) == self.CONFIG["url"]
tree = GDriveRemoteTree(dvc, self.CONFIG)
assert str(tree.path_info) == self.CONFIG["url"]
def test_drive(self, dvc):
remote = GDriveRemote(dvc, self.CONFIG)
tree = GDriveRemoteTree(dvc, self.CONFIG)
os.environ[
GDriveRemoteTree.GDRIVE_CREDENTIALS_DATA
] = USER_CREDS_TOKEN_REFRESH_ERROR
with pytest.raises(GDriveAuthError):
remote.tree._drive
tree._drive
os.environ[GDriveRemoteTree.GDRIVE_CREDENTIALS_DATA] = ""
remote = GDriveRemote(dvc, self.CONFIG)
tree = GDriveRemoteTree(dvc, self.CONFIG)
os.environ[
GDriveRemoteTree.GDRIVE_CREDENTIALS_DATA
] = USER_CREDS_MISSED_KEY_ERROR
with pytest.raises(GDriveAuthError):
remote.tree._drive
tree._drive
+10 -10
View File
@@ -2,7 +2,7 @@ import mock
import pytest
import requests
from dvc.remote.gs import GSRemote, dynamic_chunk_size
from dvc.remote.gs import GSRemoteTree, dynamic_chunk_size
BUCKET = "bucket"
PREFIX = "prefix"
@@ -17,17 +17,17 @@ CONFIG = {
def test_init(dvc):
remote = GSRemote(dvc, CONFIG)
assert remote.tree.path_info == URL
assert remote.tree.projectname == PROJECT
assert remote.tree.credentialpath == CREDENTIALPATH
tree = GSRemoteTree(dvc, CONFIG)
assert tree.path_info == URL
assert tree.projectname == PROJECT
assert tree.credentialpath == CREDENTIALPATH
@mock.patch("google.cloud.storage.Client.from_service_account_json")
def test_gs(mock_client, dvc):
remote = GSRemote(dvc, CONFIG)
assert remote.tree.credentialpath
remote.tree.gs()
tree = GSRemoteTree(dvc, CONFIG)
assert tree.credentialpath
tree.gs()
mock_client.assert_called_once_with(CREDENTIALPATH)
@@ -35,8 +35,8 @@ def test_gs(mock_client, dvc):
def test_gs_no_credspath(mock_client, dvc):
config = CONFIG.copy()
del config["credentialpath"]
remote = GSRemote(dvc, config)
remote.tree.gs()
tree = GSRemoteTree(dvc, config)
tree.gs()
mock_client.assert_called_with(PROJECT)
+15 -15
View File
@@ -2,7 +2,7 @@ import pytest
from dvc.exceptions import HTTPError
from dvc.path_info import URLInfo
from dvc.remote.http import HTTPRemote
from dvc.remote.http import HTTPRemoteTree
from tests.utils.httpd import StaticFileServer
@@ -11,10 +11,10 @@ def test_download_fails_on_error_code(dvc):
url = f"http://localhost:{httpd.server_port}/"
config = {"url": url}
remote = HTTPRemote(dvc, config)
tree = HTTPRemoteTree(dvc, config)
with pytest.raises(HTTPError):
remote.tree._download(URLInfo(url) / "missing.txt", "missing.txt")
tree._download(URLInfo(url) / "missing.txt", "missing.txt")
def test_public_auth_method(dvc):
@@ -25,9 +25,9 @@ def test_public_auth_method(dvc):
"password": "",
}
remote = HTTPRemote(dvc, config)
tree = HTTPRemoteTree(dvc, config)
assert remote.tree._auth_method() is None
assert tree._auth_method() is None
def test_basic_auth_method(dvc):
@@ -44,10 +44,10 @@ def test_basic_auth_method(dvc):
"password": password,
}
remote = HTTPRemote(dvc, config)
tree = HTTPRemoteTree(dvc, config)
assert remote.tree._auth_method() == auth
assert isinstance(remote.tree._auth_method(), HTTPBasicAuth)
assert tree._auth_method() == auth
assert isinstance(tree._auth_method(), HTTPBasicAuth)
def test_digest_auth_method(dvc):
@@ -64,10 +64,10 @@ def test_digest_auth_method(dvc):
"password": password,
}
remote = HTTPRemote(dvc, config)
tree = HTTPRemoteTree(dvc, config)
assert remote.tree._auth_method() == auth
assert isinstance(remote.tree._auth_method(), HTTPDigestAuth)
assert tree._auth_method() == auth
assert isinstance(tree._auth_method(), HTTPDigestAuth)
def test_custom_auth_method(dvc):
@@ -81,8 +81,8 @@ def test_custom_auth_method(dvc):
"password": password,
}
remote = HTTPRemote(dvc, config)
tree = HTTPRemoteTree(dvc, config)
assert remote.tree._auth_method() is None
assert header in remote.tree.headers
assert remote.tree.headers[header] == password
assert tree._auth_method() is None
assert header in tree.headers
assert tree.headers[header] == password
+20 -18
View File
@@ -5,7 +5,8 @@ import pytest
from dvc.cache import NamedCache
from dvc.path_info import PathInfo
from dvc.remote.local import LocalCache
from dvc.remote.index import RemoteIndexNoop
from dvc.remote.local import LocalCache, LocalRemoteTree
def test_status_download_optimization(mocker, dvc):
@@ -13,7 +14,7 @@ def test_status_download_optimization(mocker, dvc):
And the desired files to fetch are already on the local cache,
Don't check the existence of the desired files on the remote cache
"""
cache = LocalCache(dvc, {})
cache = LocalCache(LocalRemoteTree(dvc, {}))
infos = NamedCache()
infos.add("local", "acbd18db4cc2f85cedef654fccc4a4d8", "foo")
@@ -25,6 +26,7 @@ def test_status_download_optimization(mocker, dvc):
other_remote = mocker.Mock()
other_remote.url = "other_remote"
other_remote.checksums_exist.return_value = []
other_remote.index = RemoteIndexNoop()
cache.status(infos, other_remote, download=True)
@@ -33,8 +35,8 @@ def test_status_download_optimization(mocker, dvc):
@pytest.mark.parametrize("link_name", ["hardlink", "symlink"])
def test_is_protected(tmp_dir, dvc, link_name):
cache = LocalCache(dvc, {})
link_method = getattr(cache.tree, link_name)
tree = LocalRemoteTree(dvc, {})
link_method = getattr(tree, link_name)
(tmp_dir / "foo").write_text("foo")
@@ -43,47 +45,47 @@ def test_is_protected(tmp_dir, dvc, link_name):
link_method(foo, link)
assert not cache.is_protected(foo)
assert not cache.is_protected(link)
assert not tree.is_protected(foo)
assert not tree.is_protected(link)
cache.protect(foo)
tree.protect(foo)
assert cache.is_protected(foo)
assert cache.is_protected(link)
assert tree.is_protected(foo)
assert tree.is_protected(link)
cache.unprotect(link)
tree.unprotect(link)
assert not cache.is_protected(link)
assert not tree.is_protected(link)
if os.name == "nt" and link_name == "hardlink":
# NOTE: NTFS doesn't allow deleting read-only files, which forces us to
# set write perms on the link, which propagates to the source.
assert not cache.is_protected(foo)
assert not tree.is_protected(foo)
else:
assert cache.is_protected(foo)
assert tree.is_protected(foo)
@pytest.mark.parametrize("err", [errno.EPERM, errno.EACCES])
def test_protect_ignore_errors(tmp_dir, mocker, err):
tmp_dir.gen("foo", "foo")
foo = PathInfo("foo")
cache = LocalCache(None, {})
tree = LocalRemoteTree(None, {})
cache.protect(foo)
tree.protect(foo)
mock_chmod = mocker.patch(
"os.chmod", side_effect=OSError(err, "something")
)
cache.protect(foo)
tree.protect(foo)
assert mock_chmod.called
def test_protect_ignore_erofs(tmp_dir, mocker):
tmp_dir.gen("foo", "foo")
foo = PathInfo("foo")
cache = LocalCache(None, {})
tree = LocalRemoteTree(None, {})
mock_chmod = mocker.patch(
"os.chmod", side_effect=OSError(errno.EROFS, "read-only fs")
)
cache.protect(foo)
tree.protect(foo)
assert mock_chmod.called
+6 -6
View File
@@ -1,4 +1,4 @@
from dvc.remote.oss import OSSRemote
from dvc.remote.oss import OSSRemoteTree
bucket_name = "bucket-name"
endpoint = "endpoint"
@@ -15,8 +15,8 @@ def test_init(dvc):
"oss_key_secret": key_secret,
"oss_endpoint": endpoint,
}
remote = OSSRemote(dvc, config)
assert remote.tree.path_info == url
assert remote.tree.endpoint == endpoint
assert remote.tree.key_id == key_id
assert remote.tree.key_secret == key_secret
tree = OSSRemoteTree(dvc, config)
assert tree.path_info == url
assert tree.endpoint == endpoint
assert tree.key_id == key_id
assert tree.key_secret == key_secret
+15 -13
View File
@@ -1,6 +1,8 @@
import pytest
from dvc.remote import GSRemote, Remote, S3Remote
from dvc.remote import get_cloud_tree
from dvc.remote.gs import GSRemoteTree
from dvc.remote.s3 import S3RemoteTree
def test_remote_with_checksum_jobs(dvc):
@@ -10,32 +12,32 @@ def test_remote_with_checksum_jobs(dvc):
}
dvc.config["core"]["checksum_jobs"] = 200
remote = Remote(dvc, name="with_checksum_jobs")
assert remote.checksum_jobs == 100
tree = get_cloud_tree(dvc, name="with_checksum_jobs")
assert tree.checksum_jobs == 100
def test_remote_without_checksum_jobs(dvc):
dvc.config["remote"]["without_checksum_jobs"] = {"url": "s3://bucket/name"}
dvc.config["core"]["checksum_jobs"] = 200
remote = Remote(dvc, name="without_checksum_jobs")
assert remote.checksum_jobs == 200
tree = get_cloud_tree(dvc, name="without_checksum_jobs")
assert tree.checksum_jobs == 200
def test_remote_without_checksum_jobs_default(dvc):
dvc.config["remote"]["without_checksum_jobs"] = {"url": "s3://bucket/name"}
remote = Remote(dvc, name="without_checksum_jobs")
assert remote.checksum_jobs == remote.CHECKSUM_JOBS
tree = get_cloud_tree(dvc, name="without_checksum_jobs")
assert tree.checksum_jobs == tree.CHECKSUM_JOBS
@pytest.mark.parametrize("remote_cls", [GSRemote, S3Remote])
def test_makedirs_not_create_for_top_level_path(remote_cls, dvc, mocker):
url = f"{remote_cls.scheme}://bucket/"
remote = remote_cls(dvc, {"url": url})
@pytest.mark.parametrize("tree_cls", [GSRemoteTree, S3RemoteTree])
def test_makedirs_not_create_for_top_level_path(tree_cls, dvc, mocker):
url = f"{tree_cls.scheme}://bucket/"
tree = tree_cls(dvc, {"url": url})
mocked_client = mocker.PropertyMock()
# we use remote clients with same name as scheme to interact with remote
mocker.patch.object(remote_cls.TREE_CLS, remote.scheme, mocked_client)
mocker.patch.object(tree_cls, tree.scheme, mocked_client)
remote.tree.makedirs(remote.path_info)
tree.makedirs(tree.path_info)
assert not mocked_client.called
+2 -2
View File
@@ -3,7 +3,7 @@ import os
import pytest
from dvc.path_info import PathInfo
from dvc.remote.s3 import S3Remote, S3RemoteTree
from dvc.remote.s3 import S3RemoteTree
from dvc.utils.fs import walk_files
from tests.remotes import GCP, S3Mocked
@@ -91,7 +91,7 @@ def test_copy_preserve_etag_across_buckets(remote, dvc):
s3 = remote.tree.s3
s3.create_bucket(Bucket="another")
another = S3Remote(dvc, {"url": "s3://another", "region": "us-east-1"})
another = S3RemoteTree(dvc, {"url": "s3://another", "region": "us-east-1"})
from_info = remote.path_info / "foo"
to_info = another.path_info / "foo"
+7 -8
View File
@@ -1,7 +1,7 @@
import pytest
from dvc.config import ConfigError
from dvc.remote.s3 import S3Remote
from dvc.remote.s3 import S3RemoteTree
bucket_name = "bucket-name"
prefix = "some/prefix"
@@ -20,9 +20,9 @@ def grants():
def test_init(dvc):
config = {"url": url}
remote = S3Remote(dvc, config)
tree = S3RemoteTree(dvc, config)
assert remote.tree.path_info == url
assert tree.path_info == url
def test_grants(dvc):
@@ -33,8 +33,7 @@ def test_grants(dvc):
"grant_write_acp": "id=write-acp-permission-id",
"grant_full_control": "id=full-control-permission-id",
}
remote = S3Remote(dvc, config)
tree = remote.tree
tree = S3RemoteTree(dvc, config)
assert (
tree.extra_args["GrantRead"]
@@ -52,9 +51,9 @@ def test_grants_mutually_exclusive_acl_error(dvc, grants):
config = {"url": url, "acl": "public-read", grant_option: grant_value}
with pytest.raises(ConfigError):
S3Remote(dvc, config)
S3RemoteTree(dvc, config)
def test_sse_kms_key_id(dvc):
remote = S3Remote(dvc, {"url": url, "sse_kms_key_id": "key"})
assert remote.tree.extra_args["SSEKMSKeyId"] == "key"
tree = S3RemoteTree(dvc, {"url": url, "sse_kms_key_id": "key"})
assert tree.extra_args["SSEKMSKeyId"] == "key"
+3 -2
View File
@@ -22,8 +22,9 @@ def test_open(tmp_dir, dvc):
(tmp_dir / "foo").unlink()
tree = RepoTree(dvc)
with tree.open("foo", "r") as fobj:
assert fobj.read() == "foo"
with dvc.state:
with tree.open("foo", "r") as fobj:
assert fobj.read() == "foo"
def test_open_dirty_hash(tmp_dir, dvc):
+3 -2
View File
@@ -22,8 +22,9 @@ def test_open(tmp_dir, dvc):
(tmp_dir / "foo").unlink()
tree = DvcTree(dvc)
with tree.open("foo", "r") as fobj:
assert fobj.read() == "foo"
with dvc.state:
with tree.open("foo", "r") as fobj:
assert fobj.read() == "foo"
def test_open_dirty_hash(tmp_dir, dvc):