Signed-off-by: Ruslan Kuprieiev <[email protected]>
This commit is contained in:
Ruslan Kuprieiev
2017-12-23 09:37:40 +02:00
parent 214e30e15c
commit a779424a36
45 changed files with 987 additions and 1601 deletions
+25
View File
@@ -0,0 +1,25 @@
import os
class Cache(object):
CACHE_DIR = 'cache'
def __init__(self, dvc_dir):
self.cache_dir = os.path.join(dvc_dir, self.CACHE_DIR)
@staticmethod
def init(dvc_dir):
cache_dir = os.path.join(dvc_dir, Cache.CACHE_DIR)
os.mkdir(cache_dir)
return Cache(dvc_dir)
def all(self):
clist = []
for cache in os.listdir(self.cache_dir):
path = os.path.join(self.cache_dir, cache)
if os.path.isfile(path):
clist.append(path)
return clist
def get(self, md5):
return os.path.join(self.cache_dir, md5)
+19 -46
View File
@@ -18,9 +18,8 @@ from dvc.command.show_workflow import CmdShowWorkflow
from dvc.command.instance_create import CmdInstanceCreate
from dvc.command.config import CmdConfig
from dvc.command.show_pipeline import CmdShowPipeline
from dvc.command.merge import CmdMerge
from dvc.command.checkout import CmdCheckout
from dvc.state_file import StateFile
from dvc.stage import Stage
from dvc import VERSION
@@ -45,6 +44,7 @@ def parse_args(argv=None):
action='store_true',
default=False,
help='Skip all git actions including reproducibility check and commits.')
parent_parser.add_argument(
'-b',
'--branch',
@@ -84,10 +84,6 @@ def parse_args(argv=None):
'init',
parents=[parent_parser],
help='Initialize dvc over a directory (should already be a git dir).')
init_parser.add_argument(
'--data-dir',
default='data',
help='Data directory.')
init_parser.set_defaults(func=CmdInit)
# Run
@@ -98,15 +94,20 @@ def parse_args(argv=None):
run_parser.add_argument('-d',
'--deps',
action='append',
default = [],
default=[],
help='Declare dependencies for reproducible cmd.')
run_parser.add_argument('-D',
'--deps-no-cache',
action='append',
default=[],
help='Declare dependencies that should not be cached for reproducible cmd.')
run_parser.add_argument('-o',
'--out',
'--outs',
action='append',
default=[],
help='Declare output data file (sync to cloud) for reproducible cmd.')
run_parser.add_argument('-g',
'--out-git',
run_parser.add_argument('-O',
'--outs-no-cache',
action='append',
default=[],
help='Declare output regular file (sync to Git) for reproducible cmd.')
@@ -117,6 +118,7 @@ def parse_args(argv=None):
help='Lock data item - disable reproduction.')
run_parser.add_argument('-f',
'--file',
default=Stage.STAGE_FILE,
help='Specify name of the state file')
run_parser.add_argument('-c',
'--cwd',
@@ -132,10 +134,6 @@ def parse_args(argv=None):
parent_sync_parser = argparse.ArgumentParser(
add_help=False,
parents=[parent_parser])
parent_sync_parser.add_argument(
'targets',
nargs='+',
help='File or directory to sync.')
parent_sync_parser.add_argument('-j',
'--jobs',
type=int,
@@ -171,7 +169,7 @@ def parse_args(argv=None):
repro_parser.add_argument(
'targets',
nargs='*',
default=[StateFile.DVCFILE_NAME],
default=[Stage.STAGE_FILE],
help='Data items or stages to reproduce.')
repro_parser.add_argument('-f',
'--force',
@@ -190,23 +188,9 @@ def parse_args(argv=None):
'remove',
parents=[parent_parser],
help='Remove data item from data directory.')
remove_parser.add_argument('target',
nargs='*',
remove_parser.add_argument('targets',
nargs='+',
help='Target to remove - file or directory.')
remove_parser.add_argument('-l',
'--keep-in-cloud',
action='store_true',
default=False,
help='Do not remove data from cloud.')
remove_parser.add_argument('-r',
'--recursive',
action='store_true',
help='Remove directory recursively.')
remove_parser.add_argument('-c',
'--keep-in-cache',
action='store_true',
default=False,
help='Do not remove data from cache.')
remove_parser.set_defaults(func=CmdRemove)
# Add
@@ -215,7 +199,7 @@ def parse_args(argv=None):
parents=[parent_parser],
help='Add files/directories to dvc')
import_parser.add_argument(
'input',
'targets',
nargs='+',
help='Input files/directories')
import_parser.set_defaults(func=CmdAdd)
@@ -225,20 +209,16 @@ def parse_args(argv=None):
'lock',
parents=[parent_parser],
help='Lock')
lock_parser.add_argument('-l',
'--lock',
action='store_true',
default=False,
help='Lock data item - disable reproduction.')
lock_parser.add_argument('-u',
'--unlock',
action='store_true',
default=False,
help='Unlock data item - enable reproduction.')
help='Unlock stage - enable reproduction.')
lock_parser.add_argument(
'files',
nargs='*',
help='Data items to lock or unlock.')
default=[Stage.STAGE_FILE],
help='Stages to lock or unlock.')
lock_parser.set_defaults(func=CmdLock)
# Garbage collector
@@ -406,13 +386,6 @@ def parse_args(argv=None):
workflow_parser.set_defaults(func=CmdShowWorkflow)
# Merge
merge_parser = subparsers.add_parser(
'merge',
parents=[parent_parser],
help='Merge')
merge_parser.set_defaults(func=CmdMerge)
# Checkout
checkout_parser = subparsers.add_parser(
'checkout',
+1 -2
View File
@@ -3,8 +3,7 @@ from dvc.logger import Logger
class CloudSettings(object):
def __init__(self, path_factory, global_storage_path, cloud_config):
self.path_factory = path_factory
def __init__(self, global_storage_path, cloud_config):
self.cloud_config = cloud_config
self.global_storage_path = global_storage_path
+3 -55
View File
@@ -1,60 +1,8 @@
import os
from dvc.command.common.base import CmdBase
from dvc.logger import Logger
from dvc.state_file import StateFile
from dvc.path.data_item import DataItem
class CmdAdd(CmdBase):
def __init__(self, settings):
super(CmdAdd, self).__init__(settings)
def collect_file(self, fname):
return [self.settings.path_factory.data_item(fname)]
def collect_dir(self, dname):
targets = []
for root, dirs, files in os.walk(dname):
for fname in files:
targets += self.collect_file(os.path.join(root, fname))
return targets
def collect_targets(self, inputs):
targets = []
for i in inputs:
if not os.path.isdir(i):
targets += self.collect_file(i)
else:
targets += self.collect_dir(i)
return targets
def add_files(self, targets):
for data_item in targets:
data_item.move_data_to_cache()
def create_state_files(self, targets):
"""
Create state files for all targets.
"""
for data_item in targets:
Logger.debug('Creating state file for {}'.format(data_item.data.relative))
fname = os.path.basename(data_item.data.relative + StateFile.STATE_FILE_SUFFIX)
out = StateFile.parse_deps_state(self.settings, [data_item.data.relative],
currdir=os.path.curdir)
state_file = StateFile(fname=fname,
cmd=None,
out=out,
out_git=[],
deps=[],
locked=True)
state_file.save()
Logger.debug('State file "{}" was created'.format(data_item.state.relative))
def run(self):
targets = self.collect_targets(self.parsed_args.input)
self.add_files(targets)
self.create_state_files(targets)
msg = 'DVC add: {}'.format(str(self.parsed_args.input))
self.commit_if_needed(msg)
for target in self.args.targets:
self.project.add(target)
return 0
+1 -53
View File
@@ -1,59 +1,7 @@
import os
from dvc.command.common.base import CmdBase
from dvc.command.common.cache_dir import CacheDir
from dvc.config import ConfigI
from dvc.logger import Logger
from dvc.system import System
class CmdCheckout(CmdBase):
def __init__(self, settings):
super(CmdCheckout, self).__init__(settings)
@staticmethod
def cache_ok(item):
data = item.data.relative
cache = item.cache.relative
if not os.path.isfile(data) or not os.path.isfile(cache):
return False
if not System.samefile(data, cache):
return False
return True
@staticmethod
def checkout(items):
for item in items:
if CmdCheckout.cache_ok(item):
continue
if os.path.isfile(item.data.relative):
os.remove(item.data.relative)
System.hardlink(item.cache.relative, item.data.relative)
Logger.info('Checkout \'{}\''.format(item.data.relative))
def run(self):
self.remove_not_tracked_hardlinks()
items = self.settings.path_factory.all_existing_data_items()
self.checkout(items)
self.project.checkout()
return 0
def remove_not_tracked_hardlinks(self):
untracked_files = self.git.all_untracked_files()
cache_dir = os.path.join(self.git.git_dir_abs, ConfigI.CACHE_DIR)
cached_files = CacheDir(cache_dir).find_caches(untracked_files)
for file in cached_files:
Logger.info(u'Remove \'{}\''.format(file))
os.remove(file)
dir = os.path.dirname(file)
if not os.listdir(dir):
Logger.info(u'Remove empty directory \'{}\''.format(dir))
os.removedirs(dir)
pass
+10 -66
View File
@@ -1,76 +1,20 @@
from dvc.logger import Logger
from dvc.command.common.branch_changer import BranchChanger
from dvc.command.common.dvc_lock import DvcLock
from dvc.project import Project
class CmdBase(object):
def __init__(self, settings):
self._settings = settings
def __init__(self, args):
self.project = Project('.')
self.args = args
if settings._parsed_args.quiet and not settings._parsed_args.verbose:
Logger.be_quiet()
elif not settings._parsed_args.quiet and settings._parsed_args.verbose:
Logger.be_verbose()
@property
def settings(self):
return self._settings
#NOTE: this name is really confusing. It should really be called "command" or smth,
# because it is only used for "command" argument from CmdRun.
@property
def args(self):
return self._settings.args
@property
def cloud(self):
return self._settings.cloud
@property
def parsed_args(self):
return self._settings._parsed_args
@property
def config(self):
return self._settings.config
@property
def git(self):
return self._settings.git
@property
def no_git_actions(self):
return self.parsed_args.no_git_actions
def set_git_action(self, value):
self.parsed_args.no_git_actions = not value
def set_locker(self, value):
self.parsed_args.no_lock = value
def commit_if_needed(self, message, error=False):
if error or self.no_git_actions:
self.not_committed_changes_warning()
return 1
else:
self.git.commit_all_changes_and_log_status(message)
return 0
@staticmethod
def not_committed_changes_warning():
Logger.warn('changes were not committed to git')
if args.quiet and not args.verbose:
self.project.logger.be_quiet()
elif not args.quiet and args.verbose:
self.project.logger.be_verbose()
def run_cmd(self):
with DvcLock(self.is_locker, self.git):
with BranchChanger(self.parsed_args.branch, self.parsed_args.new_branch, self.git):
with self.project.lock:
with self.project.scm.brancher(self.args.branch, self.args.new_branch):
return self.run()
# Abstract methods that have to be implemented by any inheritance class
def run(self):
pass
@property
def is_locker(self):
if 'no_lock' in self.parsed_args.__dict__:
return not self.parsed_args.no_lock
return True
+4 -4
View File
@@ -7,7 +7,7 @@ class BranchChangerError(CmdCommonError):
class BranchChanger(object):
def __init__(self, branch, new_branch, git):
def __init__(self, scm, branch, new_branch):
if branch and new_branch:
raise BranchChangerError("Commands conflict: --branch and --new-branch cannot be used at the same command")
@@ -15,17 +15,17 @@ class BranchChanger(object):
self.branch = branch if branch else new_branch
self.create_new = new_branch is not None
self.git = git
self.scm = scm
def __enter__(self):
if self.perform_action:
code, _, err = self.git.checkout(self.branch, self.create_new)
code, _, err = self.scm.checkout(self.branch, self.create_new)
if code != 0:
raise BranchChangerError(self.branch, err)
return self
def __exit__(self, type, value, traceback):
if self.perform_action:
code, _, err = self.git.checkout_previous()
code, _, err = self.scm.checkout('-')
if code != 0:
raise BranchChangerError(self.branch, err)
-28
View File
@@ -1,28 +0,0 @@
import fasteners
from dvc.command.common.common_error import CmdCommonError
class DvcLockerError(CmdCommonError):
def __init__(self, msg):
super(DvcLockerError, self).__init__('DVC locker error: {}'.format(msg))
class DvcLock(object):
TIMEOUT = 5
def __init__(self, is_locker, git):
self.is_locker = is_locker
self.git = git
self.lock = None
def __enter__(self):
if self.is_locker:
self.lock = fasteners.InterProcessLock(self.git.lock_file)
if not self.lock.acquire(timeout=self.TIMEOUT):
raise DvcLockerError('Cannot perform the cmd since DVC is busy and locked. Please retry the cmd later.')
return self.lock
def __exit__(self, type, value, traceback):
if self.is_locker:
self.lock.release()
+12 -19
View File
@@ -7,17 +7,6 @@ from dvc.config import Config
class CmdConfig(CmdBase):
def __init__(self, settings):
super(CmdConfig, self).__init__(settings)
# Using configobj because it doesn't
# drop comments like configparser does.
self.configobj = configobj.ConfigObj(self._config_path, write_empty_values=True)
@property
def _config_path(self):
return os.path.join(self.git.git_dir, Config.CONFIG_DIR, Config.CONFIG)
def _get_key(self, d, name, add=False):
for k in d.keys():
if k.lower() == name.lower():
@@ -34,7 +23,7 @@ class CmdConfig(CmdBase):
del self.configobj[self.section][self.opt]
self.configobj.write()
except Exception as exc:
Logger.error('Failed to unset \'{}\': {}'.format(self.parsed_args.name, exc))
Logger.error('Failed to unset \'{}\': {}'.format(self.args.name, exc))
return 1
return 0
@@ -45,19 +34,19 @@ class CmdConfig(CmdBase):
def set(self):
try:
self.configobj[self.section][self.opt] = self.parsed_args.value
self.configobj[self.section][self.opt] = self.args.value
self.configobj.write()
except Exception as exc:
Logger.error('Failed to set \'{}\' to \'{}\': {}'.format(self.parsed_args.name,
self.parsed_args.value,
Logger.error('Failed to set \'{}\' to \'{}\': {}'.format(self.args.name,
self.args.value,
exc))
return 1
return 0
def check_opt(self):
_section, _opt = self.parsed_args.name.strip().split('.', 1)
add = (self.parsed_args.value != None and self.parsed_args.unset == False)
_section, _opt = self.args.name.strip().split('.', 1)
add = (self.args.value != None and self.args.unset == False)
section = self._get_key(self.configobj, _section, add)
@@ -76,13 +65,17 @@ class CmdConfig(CmdBase):
return 0
def run(self):
# Using configobj because it doesn't
# drop comments like configparser does.
self.configobj = configobj.ConfigObj(self.project.config.config_file, write_empty_values=True)
if self.check_opt() != 0:
return 1
if self.parsed_args.unset:
if self.args.unset:
return self.unset()
if self.parsed_args.value is None:
if self.args.value is None:
return self.show()
return self.set()
+8 -22
View File
@@ -1,31 +1,17 @@
from dvc.command.common.base import CmdBase
from dvc.logger import Logger
import dvc.data_cloud as cloud
from dvc.command.common.base import CmdBase
class CmdDataBase(CmdBase):
def __init__(self, settings, cmd):
super(CmdDataBase, self).__init__(settings)
self.cmd = cmd
class CmdDataPull(CmdBase):
def run(self):
getattr(self.cloud, self.cmd)(self.parsed_args.targets, self.parsed_args.jobs)
self.project.pull(self.args.jobs)
class CmdDataPull(CmdDataBase):
def __init__(self, settings):
super(CmdDataPull, self).__init__(settings, 'pull')
class CmdDataPush(CmdDataBase):
def __init__(self, settings):
super(CmdDataPush, self).__init__(settings, 'push')
class CmdDataPush(CmdBase):
def run(self):
self.project.push(self.args.jobs)
class CmdDataStatus(CmdBase):
def __init__(self, settings):
super(CmdDataStatus, self).__init__(settings)
def _show(self, status):
for s in status:
target, ret = s
@@ -39,9 +25,9 @@ class CmdDataStatus(CmdBase):
cloud.STATUS_NEW : 'new file:',
}
Logger.info('\t{}\t{}'.format(prefix_map[ret], target.data.dvc))
self.project.logger.info('\t{}\t{}'.format(prefix_map[ret], target))
def run(self):
status = self.cloud.status(self.parsed_args.targets, self.parsed_args.jobs)
status = self.project.status(self.args.jobs)
self._show(status)
return 0
+1 -17
View File
@@ -1,23 +1,7 @@
import os
from dvc.command.common.base import CmdBase
from dvc.logger import Logger
from dvc.config import ConfigI
from dvc.state_file import StateFile
class CmdGC(CmdBase):
def __init__(self, settings):
super(CmdGC, self).__init__(settings)
def run(self):
clist = [str(x) for x in StateFile.find_all_cache_files(self.git)]
for cache in os.listdir(ConfigI.CACHE_DIR):
fname = os.path.join(ConfigI.CACHE_DIR, cache)
if os.path.basename(fname) in clist:
continue
os.remove(fname)
Logger.info('Cache \'{}\' was removed'.format(fname))
self.project.gc()
return 0
+7 -110
View File
@@ -1,112 +1,9 @@
import os
from pathlib import Path
from dvc.project import Project
from dvc.command.common.base import CmdBase
from dvc.logger import Logger
from dvc.config import Config
from dvc.exceptions import DvcException
from dvc.path.data_item import DataItem
class CmdInit(object):
def __init__(self, args):
self.args = args
class InitError(DvcException):
def __init__(self, msg):
DvcException.__init__(self, 'Init error: {}'.format(msg))
class CmdInit(CmdBase):
CONFIG_TEMPLATE = '''[Global]
# Default target
Target =
# Supported clouds: AWS, GCP
Cloud = AWS
# This global storage path is going to be deprecated in the next version.
# Please use StoragePath from a specific cloud instead.
#StoragePath =
# Log levels: Debug, Info, Warning and Error
LogLevel = Info
[AWS]
CredentialPath = ~/.aws/credentials
CredentialSection = default
StoragePath = dvc/tutorial
# Default settings for AWS instances:
Type = t2.nano
Image = ami-2d39803a
SpotPrice =
SpotTimeout = 300
KeyPairName = dvc-key
KeyPairDir = ~/.ssh
SecurityGroup = dvc-sg
Region = us-east-1
Zone = us-east-1a
SubnetId =
Volume = my-100gb-drive-io
Monitoring = false
EbsOptimized = false
AllDisksAsRAID0 = false
[GCP]
StoragePath =
ProjectName =
'''
def __init__(self, settings):
super(CmdInit, self).__init__(settings)
@property
def is_locker(self):
return False
def get_not_existing_path(self, *args):
path = Path(os.path.join(self.git.git_dir, *args))
if path.exists():
raise InitError('Path "{}" already exist'.format(path.name))
return path
def get_not_existing_conf_file_name(self):
file_name = os.path.join(self.git.git_dir, Config.CONFIG_DIR, Config.CONFIG)
if os.path.exists(file_name):
raise InitError('Configuration file "{}" already exist'.format(file_name))
return file_name
def run(self):
if not self.no_git_actions and not self.git.is_ready_to_go():
return 1
if os.path.realpath(os.path.curdir) != self.settings.git.git_dir_abs:
Logger.error('DVC error: initialization could be done only from git root directory {}'.format(
self.settings.git.git_dir_abs
))
return 1
config_dir_path = self.get_not_existing_path(Config.CONFIG_DIR)
cache_dir_path = self.get_not_existing_path(Config.CONFIG_DIR, Config.CACHE_DIR_NAME)
conf_file_name = self.get_not_existing_conf_file_name()
config_dir_path.mkdir()
cache_dir_path.mkdir()
Logger.info('Directories {}/, {}/ were created'.format(
config_dir_path.name,
os.path.join(config_dir_path.name, cache_dir_path.name)))
conf_file = open(conf_file_name, 'wt')
conf_file.write(self.CONFIG_TEMPLATE)
conf_file.close()
self.git.modify_gitignore([os.path.join(config_dir_path.name, cache_dir_path.name),
os.path.join(config_dir_path.name, os.path.basename(self.git.lock_file)),
'*' + DataItem.LOCAL_STATE_FILE_SUFFIX])
message = 'DVC init. cache dir {}'.format(cache_dir_path.name)
return self.commit_if_needed(message)
def run_cmd(self):
Project.init('.')
return 0
+17 -31
View File
@@ -1,41 +1,27 @@
from dvc.command.common.base import CmdBase
from dvc.logger import Logger
from dvc.state_file import StateFile
from dvc.stage import Stage
class CmdLock(CmdBase):
def __init__(self, settings):
super(CmdLock, self).__init__(settings)
def run(self):
return self.lock_files(self.parsed_args.files, not self.parsed_args.unlock)
def lock_files(self, files, target):
cmd = 'lock' if target else 'unlock'
error = 0
for file in files:
lock = not self.args.unlock
cmd = 'lock' if lock else 'unlock'
ret = 0
for file in self.args.files:
try:
data_item = self.settings.path_factory.existing_data_item(file)
state = StateFile.load(data_item, self.settings)
stage = Stage.load(self.project, file)
if state.locked and target:
Logger.warn('Data item {} is already locked'.format(data_item.data.relative))
elif not state.locked and not target:
Logger.warn('Data item {} is already unlocked'.format(data_item.data.relative))
if stage.locked and lock:
self.project.logger.warn('Stage {} is already locked'.format(file))
elif not stage.locked and not lock:
self.project.logger.warn('Stage {} is already unlocked'.format(file))
else:
state.locked = target
Logger.debug('Saving status file for data item {}'.format(data_item.data.relative))
state.save()
Logger.info('Data item {} was {}ed'.format(data_item.data.relative, cmd))
stage.locked = lock
self.project.logger.debug('Saving stage file {}'.format(file))
stage.dump()
self.project.logger.info('Stage {} was {}ed'.format(file, cmd))
except Exception as ex:
error += 1
Logger.error('Unable to {} {}: {}'.format(cmd, file, ex))
ret = 1
self.project.logger.error('Unable to {} {}: {}'.format(cmd, file, ex))
if error > 0 and not self.no_git_actions:
Logger.error('Errors occurred. One or more repro cmd was not successful.')
self.not_committed_changes_warning()
else:
self.commit_if_needed('DVC lock: {}'.format(' '.join(self.args)))
return 0
return ret
-56
View File
@@ -1,56 +0,0 @@
import os
from dvc.command.common.base import CmdBase
from dvc.config import ConfigI
from dvc.logger import Logger
from dvc.state_file import StateFile
from dvc.path.data_item import DataItem
from dvc.system import System
from dvc.command.checkout import CmdCheckout
class CmdMerge(CmdBase):
def __init__(self, settings):
super(CmdMerge, self).__init__(settings)
def print_info(self, targets):
for fname in targets:
Logger.info('Restored original data after merge:')
Logger.info(' {}'.format(fname))
def collect_targets(self):
targets = []
for fname in self.git.get_last_merge_changed_files():
if not StateFile._is_state_file(fname):
continue
state = StateFile.load(fname)
if not state.cmd and state.locked:
targets.append(fname)
return targets
def checkout_targets(self, targets):
items = []
for fname in targets:
self.git.checkout_file_before_last_merge(fname)
state = StateFile.load(fname)
for out in state.out:
item = self.settings.path_factory.data_item(os.path.join(state.cwd, out))
items.append(item)
CmdCheckout.checkout(items)
msg = 'DVC merge files: {}'.format(' '.join(targets))
self.commit_if_needed(msg)
def run(self):
targets = self.collect_targets()
if not targets:
return 1
self.checkout_targets(targets)
self.print_info(targets)
return 0
+5 -72
View File
@@ -1,75 +1,8 @@
import os
from dvc.command.common.traverse import Traverse
from dvc.logger import Logger
from dvc.command.common.base import CmdBase
class CmdRemove(Traverse):
def __init__(self, settings):
super(CmdRemove, self).__init__(settings, "remove")
class CmdRemove(CmdBase):
def run(self):
if not self.parsed_args.target:
Logger.error('Nothing specified, nothing removed.')
return 1
return super(CmdRemove, self).run()
def process_file(self, target):
Logger.debug(u'[Cmd-Remove] Remove file {}.'.format(target))
data_item = self._get_data_item(target)
self._remove_cache_file(data_item)
self._remove_cloud_cache(data_item)
self._remove_state_file(data_item)
os.remove(data_item.data.relative)
Logger.debug(u'[Cmd-Remove] Remove data item {}. Success.'.format(data_item.data.relative))
pass
def _remove_state_file(self, data_item):
if os.path.isfile(data_item.state.relative):
self._remove_dvc_path(data_item.state, 'state')
else:
Logger.warn(u'[Cmd-Remove] State file {} for data instance {} does not exist'.format(
data_item.state.relative, data_item.data.relative))
def _remove_cache_file(self, data_item):
if not self.parsed_args.keep_in_cache and os.path.isfile(data_item.cache.relative):
self._remove_dvc_path(data_item.cache, 'cache')
else:
if not self.parsed_args.keep_in_cache:
msg = u'[Cmd-Remove] Unable to find cache file {} for data item {}'
Logger.warn(msg.format(data_item.cache.relative, data_item.data.relative))
pass
def _remove_dvc_path(self, dvc_path, name):
Logger.debug(u'[Cmd-Remove] Remove {} {}.'.format(name, dvc_path.relative))
os.remove(dvc_path.relative)
self.remove_dir_if_empty(dvc_path.relative)
Logger.debug(u'[Cmd-Remove] Remove {}. Success.'.format(name))
@staticmethod
def remove_dir_if_empty(file):
dir = os.path.dirname(file)
if dir != '' and not os.listdir(dir):
Logger.debug(u'[Cmd-Remove] Empty directory was removed {}.'.format(dir))
os.rmdir(dir)
pass
def traverse_dir_finalize(self, target):
os.rmdir(target)
def is_recursive(self):
return self.parsed_args.recursive
# Renaming
def remove_dir(self, target):
return self._traverse_dir(target)
def remove_file(self, target):
self.process_file(target)
def remove_all(self):
return self._traverse_all()
for target in self.args.targets:
self.project.remove(target)
return 0
+6 -153
View File
@@ -1,155 +1,8 @@
import os
import copy
from dvc.command.run import CmdRun
from dvc.logger import Logger
from dvc.exceptions import DvcException
from dvc.state_file import StateFile
from dvc.system import System
from dvc.data_cloud import file_md5
class ReproError(DvcException):
def __init__(self, msg):
DvcException.__init__(self, 'Run error: {}'.format(msg))
class CmdRepro(CmdRun):
def __init__(self, settings):
super(CmdRepro, self).__init__(settings)
from dvc.command.common.base import CmdBase
class CmdRepro(CmdBase):
def run(self):
recursive = not self.parsed_args.single_item
stages = []
for target in self.parsed_args.targets:
if StateFile._is_state_file(target):
stage = StateFile.load(target)
else:
stage = StateFile.find_by_output(self.settings, target)
if stage:
stages.append(stage)
self.repro_stages(stages, recursive, self.parsed_args.force)
names = [os.path.relpath(stage.path) for stage in stages]
return self.commit_if_needed('DVC repro: {}'.format(names))
def repro_stages(self, stages, recursive, force):
error = False
changed = False
for stage in stages:
try:
change = ReproStage(self.settings, stage, recursive, force)
if change.reproduce():
changed = True
Logger.info(u'Stage "{}" was reproduced.'.format(stage.path))
else:
Logger.info(u'Reproduction is not required for stage "{}".'.format(stage.path))
except ReproError as err:
Logger.error('Error in reproducing stage {}: {}'.format(stage.path, str(err)))
error = True
break
if error and not self.no_git_actions:
Logger.error('Errors occurred. One or more repro cmd was not successful.')
self.not_committed_changes_warning()
return changed and not error
class ReproStage(object):
def __init__(self, settings, stage, recursive, force):
self.git = settings.git
self.settings = settings
self._recursive = recursive
self._force = force
self.stage = stage
if not self.stage.cmd and not self.stage.locked:
msg = 'Error: stage "{}" is not locked, but has no command for reproduction'
raise ReproError(msg.format(stage.path))
def is_cache_exists(self):
for out in self.stage.out:
path = os.path.join(self.stage.cwd, out)
if not os.path.exists(path):
return False
return True
def remove_output_files(self):
for out in self.stage.out:
path = os.path.join(self.stage.cwd, out)
Logger.debug('Removing output file {} before reproduction.'.format(path))
try:
os.remove(path)
except Exception as ex:
msg = 'Output file {} cannot be removed before reproduction: {}'
Logger.debug(msg.format(path, ex))
def reproduce_run(self):
Logger.info('Reproducing run command for stage {}. Args: {}'.format(
self.stage.path, self.stage.cmd))
CmdRun.run_command(self.settings, self.stage)
def reproduce_stage(self):
Logger.debug('Reproducing stage {}.'.format(self.stage.path))
self.remove_output_files()
self.reproduce_run()
def is_repro_required(self):
deps_changed = self.reproduce_deps()
if deps_changed or self._force or not self.is_cache_exists():
return True
return False
def reproduce(self):
Logger.debug('Reproduce stage {}. recursive={}, force={}'.format(
self.stage.path, self._recursive, self._force))
if self.stage.locked:
Logger.debug('Stage {} is not reproducible'.format(self.stage.path))
return False
if not self.is_repro_required():
Logger.debug('Stage {} is up to date'.format(self.stage.path))
return False
Logger.debug('Stage {} is going to be reproduced'.format(self.stage.path))
self.reproduce_stage()
return True
def reproduce_dep(self, path, md5, recursive):
if not self.settings.path_factory.is_data_item(path):
if md5 != file_md5(os.path.join(self.git.git_dir_abs, path))[0]:
self.log_repro_reason('source {} was changed'.format(path))
return True
return False
stage = StateFile.find_by_output(self.settings, path)
if recursive:
ReproStage(self.settings, stage, self._recursive, self._force).reproduce()
stage = StateFile.load(stage.path)
if md5 != stage.out[os.path.relpath(path, stage.cwd)]:
self.log_repro_reason('data item {} was changed - md5 sum doesn\'t match'.format(path))
return True
return False
def reproduce_deps(self):
result = False
for name,md5 in self.stage.deps.items():
path = os.path.join(self.stage.cwd, name)
if self.reproduce_dep(path, md5, self._recursive):
result = True
return result
def log_repro_reason(self, reason):
msg = u'Repro is required for stage {} because of {}'
Logger.debug(msg.format(self.stage.path, reason))
recursive = not self.args.single_item
self.project.reproduce(self.args.targets,
recursive=recursive,
force=self.args.force)
+9 -67
View File
@@ -1,72 +1,14 @@
import os
from dvc.command.common.base import CmdBase
from dvc.exceptions import DvcException
from dvc.logger import Logger
from dvc.state_file import StateFile
from dvc.executor import Executor
class RunError(DvcException):
def __init__(self, msg):
DvcException.__init__(self, 'Run error: {}'.format(msg))
class CmdRun(CmdBase):
def __init__(self, settings):
super(CmdRun, self).__init__(settings)
def run(self):
cmd = ' '.join(self.parsed_args.command)
stage_file = self.get_stage_file()
if os.path.isfile(stage_file):
Logger.error("Stage file {} already exists".format(stage_file))
return 1
state = StateFile(fname=os.path.join(self.parsed_args.cwd, stage_file),
cmd=cmd,
out=self.parsed_args.out,
out_git=self.parsed_args.out_git,
deps=self.parsed_args.deps,
locked=self.parsed_args.lock,
cwd=self.parsed_args.cwd)
self.run_command(self.settings, state)
return self.commit_if_needed('DVC run: {}'.format(state.cmd))
def get_stage_file(self):
if self.parsed_args.file:
return self.parsed_args.file
if self.parsed_args.out or self.parsed_args.out_git:
result = self.parsed_args.out[0] if self.parsed_args.out else self.parsed_args.out_git[0]
result = os.path.basename(result+StateFile.STATE_FILE_SUFFIX)
Logger.info(u'Using \'{}\' as a stage file'.format(result))
return result
Logger.info(u'Using \'{}\' as stage file'.format(StateFile.DVCFILE_NAME))
return StateFile.DVCFILE_NAME
@staticmethod
def run_command(settings, state):
Executor.exec_cmd_only_success(state.cmd, cwd=state.cwd, shell=True)
CmdRun.move_output_to_cache(settings, state)
CmdRun.update_state_file(settings, state)
@staticmethod
def update_state_file(settings, state):
Logger.debug('Update state file "{}"'.format(state.path))
state.out = StateFile.parse_deps_state(settings, state.out, currdir=state.cwd)
state.out_git = StateFile.parse_deps_state(settings, state.out_git, currdir=state.cwd)
state.deps = StateFile.parse_deps_state(settings, state.deps, currdir=state.cwd)
state.save()
@staticmethod
def move_output_to_cache(settings, state):
items = settings.path_factory.to_data_items(state.out)[0]
for item in items:
Logger.debug('Move output file "{}" to cache dir "{}" and create a hardlink'.format(
item.data.relative, item.cache_dir_abs))
item.move_data_to_cache()
self.project.run(cmd=' '.join(self.args.command),
outs=self.args.outs,
outs_no_cache=self.args.outs_no_cache,
deps=self.args.deps,
deps_no_cache=self.args.deps_no_cache,
locked=self.args.lock,
fname=self.args.file,
cwd=self.args.cwd)
return 0
+17 -33
View File
@@ -1,17 +1,10 @@
import os
import networkx as nx
from dvc.command.common.traverse import Traverse
from dvc.logger import Logger
from dvc.state_file import StateFile
from dvc.command.common.base import CmdBase
class CmdShowPipeline(Traverse):
def __init__(self, settings):
super(CmdShowPipeline, self).__init__(settings, "collect", do_not_start_from_root=False)
self.g = nx.DiGraph()
self.subs = []
class CmdShowPipeline(CmdBase):
def draw(self, g, target, fname_suffix):
fname = 'pipeline_' + fname_suffix
try:
@@ -36,7 +29,7 @@ class CmdShowPipeline(Traverse):
target = '.'
return self.draw(self.g, target, target)
for t in self.parsed_args.target:
for t in self.args.target:
fname_suffix = os.path.basename(os.path.normpath(t))
s = self.find_sub(t)
@@ -47,16 +40,16 @@ class CmdShowPipeline(Traverse):
return 0
def run(self):
saved_targets = self.settings.parsed_args.target
self.settings.parsed_args.target = ['.']
self.g = nx.DiGraph()
self.subs = []
saved_targets = self.args.target
self.args.target = ['.']
ret = super(CmdShowPipeline, self).run()
for stage in self.project.stages():
self.collect_stage(stage)
self.settings.parsed_args.target = saved_targets
if ret != 0:
Logger.error('Failed to build dependency graph for the project')
return 1
self.args.target = saved_targets
# Try to find independent clusters which might occure
# when a bunch of data items were used independently.
@@ -64,27 +57,18 @@ class CmdShowPipeline(Traverse):
return self.draw_targets(saved_targets)
def process_file(self, target):
data_item = self._get_data_item(target)
name = data_item.data.relative
def collect_stage(self, stage):
name = os.path.relpath(stage.path, self.project.root_dir)
state = StateFile.load(data_item, self.git)
self.g.add_node(name)
for i in state.input_files:
for dep in stage.deps:
i = os.path.relpath(dep.path, self.project.root_dir)
self.g.add_node(i)
self.g.add_edge(i, name)
for o in state.output_files:
if o == name:
continue
for out in state.outs:
o = os.path.relpath(out.path, self.project.root_dir)
self.g.add_node(o)
self.g.add_edge(name, o)
@property
def no_git_actions(self):
return True
@staticmethod
def not_committed_changes_warning():
pass
+8 -19
View File
@@ -1,27 +1,16 @@
from dvc.command.common.base import CmdBase
from dvc.logger import Logger
from dvc.git_wrapper import GitWrapper
class CmdShowWorkflow(CmdBase):
def __init__(self, settings):
super(CmdShowWorkflow, self).__init__(settings)
def run(self):
target = self.settings.parsed_args.target
target = self.args.target
if not target:
target = self._settings.config._config['Global'].get('Target', '')
Logger.debug(u'Set show workflow target as {}'.format(target))
target = self.project.config._config['Global'].get('Target', '')
self.project.logger.debug(u'Set show workflow target as {}'.format(target))
wf = self.git.get_all_commits(target, self.settings)
wf.build_graph(self.settings.parsed_args.dvc_commits,
self.settings.parsed_args.all_commits,
self.settings.parsed_args.max_commits)
wf = GitWrapper.get_all_commits(target, self.settings)
wf.build_graph(self.args.dvc_commits,
self.args.all_commits,
self.args.max_commits)
return 0
@property
def no_git_actions(self):
return True
@staticmethod
def not_committed_changes_warning():
pass
+45 -54
View File
@@ -14,67 +14,58 @@ class ConfigError(DvcException):
DvcException.__init__(self, 'Config file error: {}'.format(msg))
class ConfigI(object):
""" Basic config instance """
CONFIG_DIR = '.dvc'
TARGET_FILE_DEFAULT = 'target'
class Config(object):
CONFIG = 'config'
CACHE_DIR_NAME = 'cache'
CONFIG_TEMPLATE = '''
[Global]
# Supported clouds: AWS, GCP
Cloud = AWS
CACHE_DIR = os.path.join(CONFIG_DIR, CACHE_DIR_NAME)
# Log levels: Debug, Info, Warning and Error
LogLevel = Info
def __init__(self, cloud=None, conf_parser=None):
self._conf_parser = None
self._cloud = None
self.set(cloud, conf_parser)
[AWS]
CredentialPath = ~/.aws/credentials
CredentialSection = default
def set(self, cloud=None, conf_parser=None):
""" Set config params """
self._cloud = cloud
self._conf_parser = conf_parser
StoragePath = dvc/tutorial
@property
def cache_dir(self):
""" Directory with cached data files """
return self.CACHE_DIR
@property
def cloud(self):
""" Cloud config """
return self._cloud
# Default settings for AWS instances:
Type = t2.nano
Image = ami-2d39803a
@property
def conf_parser(self):
return self._conf_parser
SpotPrice =
SpotTimeout = 300
KeyPairName = dvc-key
KeyPairDir = ~/.ssh
SecurityGroup = dvc-sg
class Config(ConfigI):
""" Parsed config object """
def __init__(self, conf_file=ConfigI.CONFIG, conf_pseudo_file=None):
"""
Params:
conf_file (String): configuration file
conf_pseudo_file (String): for unit testing, something that supports readline;
supersedes conf_file
"""
self._conf_file = conf_file
Region = us-east-1
Zone = us-east-1a
SubnetId =
Volume = my-100gb-drive-io
Monitoring = false
EbsOptimized = false
AllDisksAsRAID0 = false
[GCP]
StoragePath =
ProjectName =
'''
def __init__(self, dvc_dir):
self.dvc_dir = dvc_dir
self.config_file = os.path.join(dvc_dir, self.CONFIG)
self._config = configparser.SafeConfigParser()
self._config.read(self.config_file)
Logger.set_level(self._config['Global']['LogLevel'])
if conf_pseudo_file is not None:
self._config.readfp(conf_pseudo_file)
else:
fname = os.path.join(self.CONFIG_DIR, conf_file)
if not os.path.isfile(fname):
raise ConfigError('Config file "{}" does not exist {}'.format(fname, os.getcwd()))
self._config.read(fname)
level = self._config['Global']['LogLevel']
Logger.set_level(level)
super(Config, self).__init__(self._config['Global']['Cloud'],
self._config)
@property
def file(self):
""" Config file object """
return self._conf_file
@staticmethod
def init(dvc_dir):
config_file = os.path.join(dvc_dir, Config.CONFIG)
open(config_file, 'w').write(Config.CONFIG_TEMPLATE)
return Config(dvc_dir)
+52 -157
View File
@@ -141,7 +141,7 @@ class DataCloudBase(object):
def cache_file_key(self, fname):
""" Key of a file within the bucket """
return '{}/{}'.format(self.storage_prefix, fname).strip('/')
return '{}/{}'.format(self.storage_prefix, os.path.basename(fname)).strip('/')
@staticmethod
def tmp_file(fname):
@@ -154,57 +154,39 @@ class DataCloudBase(object):
"""
pass
def _import(self, bucket, path, item):
def _import(self, bucket, fin, fout):
"""
Cloud-specific method for importing data file.
"""
pass
def push(self, item):
def push(self, path):
""" Cloud-specific method for pushing data """
pass
def pull(self, item):
def pull(self, path):
""" Generic method for pulling data from the cloud """
fname = item.cache.dvc
key_name = self.cache_file_key(fname)
return self._import(self.storage_bucket, key_name, item)
key_name = self.cache_file_key(path)
return self._import(self.storage_bucket, key_name, path)
def import_data(self, url, item):
""" Generic method for importing data """
parsed = urlparse(url)
return self._import(parsed.netloc, parsed.path, item)
def sync(self, fname):
"""
Generic method for syncing data. It will decide on its own whether it needs
to push or pull data, depending on existance of local cache file.
"""
item = self._cloud_settings.path_factory.data_item(fname)
if os.path.isfile(item.cache.dvc):
return self.push(item)
else:
return self.pull(item)
def remove(self, item):
def remove(self, path):
"""
Cloud-specific method for removing data item from the cloud.
"""
pass
def _status(self, item):
def _status(self, path):
"""
Cloud-specific method for checking data item status.
"""
pass
def status(self, item):
def status(self, path):
"""
Generic method for checking data item status.
"""
return STATUS_MAP.get(self._status(item), STATUS_UNKNOWN)
return STATUS_MAP.get(self._status(path), STATUS_UNKNOWN)
class DataCloudLOCAL(DataCloudBase):
"""
@@ -373,7 +355,7 @@ class DataCloudHTTP(DataCloudBase):
class DataCloudAWS(DataCloudBase):
""" DataCloud class for Amazon Web Services """
def __init__(self, cloud_settings): # settings, config, cloud_config):
def __init__(self, cloud_settings):
super(DataCloudAWS, self).__init__(cloud_settings)
self._aws_creds = AWSCredentials(cloud_settings.cloud_config)
@@ -455,11 +437,10 @@ class DataCloudAWS(DataCloudBase):
"""
return fname + '.download'
def _import(self, bucket_name, key_name, data_item):
def _import(self, bucket_name, key_name, fname):
bucket = self._get_bucket_aws(bucket_name)
fname = data_item.data.dvc
tmp_file = self.tmp_file(fname)
name = os.path.basename(fname)
key = bucket.get_key(key_name)
@@ -469,7 +450,7 @@ class DataCloudAWS(DataCloudBase):
if self._cmp_checksum(key, fname):
Logger.debug('File "{}" matches with "{}".'.format(fname, key_name))
return data_item
return fname
Logger.debug('Downloading cache file from S3 "{}/{}" to "{}"'.format(bucket.name,
key_name,
@@ -484,22 +465,11 @@ class DataCloudAWS(DataCloudBase):
return None
os.rename(tmp_file, fname)
data_item.move_data_to_cache()
progress.finish_target(name)
Logger.debug('Downloading completed')
return data_item
def _read_upload_tracker(self, fname):
"""
Try reading upload tracker if present.
"""
try:
return open(self._upload_tracker(fname), 'r').read()
except Exception as exc:
Logger.debug("Failed to read upload tracker file for {}: {}".format(fname, exc))
return None
return fname
def _write_upload_tracker(self, fname, mp_id):
"""
@@ -604,48 +574,48 @@ class DataCloudAWS(DataCloudBase):
multipart.complete_upload()
self._unlink_upload_tracker(fname)
def push(self, data_item):
def push(self, path):
""" push, aws version """
aws_key = self.cache_file_key(data_item.cache.dvc)
aws_key = self.cache_file_key(path)
bucket = self._get_bucket_aws(self.storage_bucket)
key = bucket.get_key(aws_key)
if key:
Logger.debug('File already uploaded to the cloud. Checksum validation...')
if self._cmp_checksum(key, data_item.cache.dvc):
if self._cmp_checksum(key, path):
Logger.debug('File checksum matches. No uploading is needed.')
return data_item
return path
Logger.debug('Checksum miss-match. Re-uploading is required.')
key = bucket.new_key(aws_key)
try:
self._push_multipart(key, data_item.cache.relative)
self._push_multipart(key, path)
except Exception as exc:
Logger.error('Failed to upload "{}": {}'.format(data_item.cache.relative, exc))
Logger.error('Failed to upload "{}": {}'.format(path, exc))
return None
progress.finish_target(os.path.basename(data_item.cache.relative))
progress.finish_target(os.path.basename(path))
return data_item
return path
def _status(self, data_item):
aws_key = self.cache_file_key(data_item.cache.dvc)
def _status(self, path):
aws_key = self.cache_file_key(path)
bucket = self._get_bucket_aws(self.storage_bucket)
key = bucket.get_key(aws_key)
remote_exists = key is not None
local_exists = os.path.exists(data_item.cache.relative)
local_exists = os.path.exists(path)
diff = None
if remote_exists and local_exists:
diff = self._cmp_checksum(key, data_item.cache.dvc)
diff = self._cmp_checksum(key, path)
return (local_exists, remote_exists, diff)
def remove(self, data_item):
aws_file_name = self.cache_file_key(data_item.cache.dvc)
def remove(self, path):
aws_file_name = self.cache_file_key(path)
Logger.debug(u'[Cmd-Remove] Remove from cloud {}.'.format(aws_file_name))
@@ -702,11 +672,10 @@ class DataCloudGCP(DataCloudBase):
return False
def _import(self, bucket_name, key, data_item):
def _import(self, bucket_name, key, fname):
bucket = self._get_bucket_gc(bucket_name)
fname = data_item.data.dvc
name = os.path.basename(fname)
tmp_file = self.tmp_file(fname)
@@ -717,7 +686,7 @@ class DataCloudGCP(DataCloudBase):
if self._cmp_checksum(blob, fname):
Logger.debug('File "{}" matches with "{}".'.format(fname, key))
return data_item
return fname
Logger.debug('Downloading cache file from gc "{}/{}"'.format(bucket.name, key))
@@ -732,51 +701,50 @@ class DataCloudGCP(DataCloudBase):
return None
os.rename(tmp_file, fname)
data_item.move_data_to_cache()
progress.finish_target(name)
Logger.debug('Downloading completed')
return data_item
return fname
def push(self, data_item):
def push(self, path):
""" push, gcp version """
bucket = self._get_bucket_gc(self.storage_bucket)
blob_name = self.cache_file_key(data_item.cache.dvc)
name = os.path.basename(data_item.cache.dvc)
blob_name = self.cache_file_key(path)
name = os.path.basename(path)
blob = bucket.get_blob(blob_name)
if blob is not None and blob.exists():
if self._cmp_checksum(blob, data_item.cache.dvc):
Logger.debug('checksum %s matches. Skipping upload' % data_item.cache.relative)
return data_item
Logger.debug('checksum %s mismatch. re-uploading' % data_item.cache.relative)
if self._cmp_checksum(blob, path):
Logger.debug('checksum %s matches. Skipping upload' % path)
return path
Logger.debug('checksum %s mismatch. re-uploading' % path)
# same as in _import
progress.update_target(name, 0, None)
blob = bucket.blob(blob_name)
blob.upload_from_filename(data_item.cache.relative)
blob.upload_from_filename(path)
progress.finish_target(name)
Logger.debug('uploading %s completed' % data_item.cache.relative)
Logger.debug('uploading %s completed' % path)
return data_item
return path
def _status(self, data_item):
def _status(self, path):
""" status, gcp version """
bucket = self._get_bucket_gc(self.storage_bucket)
blob_name = self.cache_file_key(data_item.cache.dvc)
blob_name = self.cache_file_key(path)
blob = bucket.get_blob(blob_name)
remote_exists = blob is not None and blob.exists()
local_exists = os.path.exists(data_item.cache.relative)
local_exists = os.path.exists(path)
diff = None
if remote_exists and local_exists:
diff = self._cmp_checksum(blob, data_item.cache.dvc)
diff = self._cmp_checksum(blob, path)
return (local_exists, remote_exists, diff)
@@ -803,17 +771,8 @@ class DataCloud(object):
'' : 'LOCAL',
}
def __init__(self, settings):
assert isinstance(settings, dvc.settings.Settings)
#To handle ConfigI case
if not hasattr(settings.config, '_config'):
self._settings = settings
self._cloud = DataCloudBase(None)
return
self._settings = settings
self._config = self._settings.config._config
def __init__(self, config):
self._config = config
cloud_type = self._config['Global'].get('Cloud', '').strip().upper()
if cloud_type not in self.CLOUD_MAP.keys():
@@ -823,8 +782,7 @@ class DataCloud(object):
raise ConfigError('Can\'t find cloud section \'[%s]\' in config' % cloud_type)
cloud_settings = self.get_cloud_settings(self._config,
cloud_type,
self._settings.path_factory)
cloud_type)
self.typ = cloud_type
self._cloud = self.CLOUD_MAP[cloud_type](cloud_settings)
@@ -832,7 +790,7 @@ class DataCloud(object):
self.sanity_check()
@staticmethod
def get_cloud_settings(config, cloud_type, path_factory):
def get_cloud_settings(config, cloud_type):
"""
Obtain cloud settings from config.
"""
@@ -841,7 +799,7 @@ class DataCloud(object):
else:
cloud_config = config[cloud_type]
global_storage_path = config['Global'].get('StoragePath', None)
cloud_settings = CloudSettings(path_factory, global_storage_path, cloud_config)
cloud_settings = CloudSettings(global_storage_path, cloud_config)
return cloud_settings
def sanity_check(self):
@@ -867,62 +825,11 @@ class DataCloud(object):
self._cloud.sanity_check()
def _collect_target(self, target):
"""
Collect target as a file or directory.
"""
if self._settings.path_factory.is_data_item(target):
item = self._settings.path_factory.existing_data_item(target)
return [item]
elif os.path.isdir(target):
return self._settings.path_factory.all_existing_data_items(target)
Logger.warn('Target "{}" does not exist'.format(target))
return []
def _collect_targets(self, targets):
"""
Collect every target as a data item.
"""
collected = []
for target in targets:
collected += self._collect_target(target)
return collected
def _map_targets(self, func, targets, jobs):
"""
Process targets as data items in parallel.
"""
collected = self._collect_targets(targets)
return map_progress(func, collected, jobs)
def _import(self, target):
"""
Generic method for importing targets in a cloud-agnostic way.
"""
url, item = target
parsed = urlparse(url)
typ = self.SCHEME_MAP.get(parsed.scheme, None)
if typ == None:
Logger.error('Not supported scheme \'{}\''.format(parsed.scheme))
return None
#To handle ConfigI case
if not hasattr(self._settings.config, '_config'):
self._config = None
cloud_settings = None
else:
self._config = self._settings.config._config
cloud_settings = self.get_cloud_settings(self._config, typ, self._settings.path_factory)
cloud = self.CLOUD_MAP[typ](cloud_settings)
return cloud.import_data(url, item)
return map_progress(func, targets, jobs)
def sync(self, targets, jobs=1):
"""
@@ -942,18 +849,6 @@ class DataCloud(object):
"""
return self._map_targets(self._cloud.pull, targets, jobs)
def import_data(self, targets, jobs=1):
"""
Import data items in a cloud-agnostic way.
"""
return map_progress(self._import, targets, jobs)
def remove(self, item):
"""
Remove data items is a cloud-agnostic way.
"""
return self._cloud.remove(item)
def status(self, targets, jobs=1):
"""
Check status of data items in a cloud-agnostic way.
+1 -2
View File
@@ -1,3 +1,2 @@
class DvcException(Exception):
def __init__(self, msg):
Exception.__init__(self, msg)
pass
-1
View File
@@ -7,7 +7,6 @@ from dvc.exceptions import DvcException
from dvc.logger import Logger
from dvc.config import Config
from dvc.executor import Executor, ExecutorError
from dvc.path.data_item import DataItem
from dvc.system import System
from dvc.graph.workflow import Workflow
from dvc.graph.commit import Commit
+26
View File
@@ -0,0 +1,26 @@
import os
import fasteners
class Lock(object):
LOCK_FILE = 'lock'
TIMEOUT = 5
def __init__(self, dvc_dir):
self.lock_file = os.path.join(dvc_dir, self.LOCK_FILE)
self._lock = fasteners.InterProcessLock(self.lock_file)
@staticmethod
def init(dvc_dir):
return Lock(dvc_dir)
def lock(self):
self._lock.acquire(timeout=self.TIMEOUT)
def unlock(self):
self._lock.release()
def __enter__(self):
self.lock()
def __exit__(self, type, value, tb):
self.unlock()
+3 -25
View File
@@ -1,27 +1,5 @@
"""
main entry point / argument parsing for dvc
"""
import sys
from dvc.settings import Settings
from dvc.logger import Logger
from dvc.config import ConfigError
from dvc.cli import parse_args
def main():
try:
settings = Settings(sys.argv[1:])
instance = settings._parsed_args.func(settings)
except Exception as e:
# In case we didn't even manage to parse options
exc_info = '-v' in sys.argv or '--verbose' in sys.argv
Logger.error("Settings error: {}".format(e), exc_info=exc_info)
return 255
try:
ret = instance.run_cmd()
except Exception as e:
exc_info = settings.parsed_args.verbose
Logger.error("{} error: {}".format(instance.__class__.__name__, e), exc_info=exc_info)
return 254
return ret
args = parse_args()
return args.func(args).run_cmd()
View File
-114
View File
@@ -1,114 +0,0 @@
import os
import stat
from dvc.config import ConfigI
from dvc.path.path import Path
from dvc.exceptions import DvcException
from dvc.system import System
from dvc.utils import cached_property
from dvc.data_cloud import file_md5
from dvc.state_file import StateFile, LocalStateFile
class DataItemError(DvcException):
def __init__(self, msg):
super(DataItemError, self).__init__('Data item error: {}'.format(msg))
class DataDirError(DvcException):
def __init__(self, msg):
super(DataDirError, self).__init__(msg)
class DataItemInConfigDirError(DataDirError):
def __init__(self, file):
msg = 'File "{}" is in config directory'.format(file)
super(DataDirError, self).__init__(msg)
class NotInGitDirError(DataDirError):
def __init__(self, file, git_dir):
msg = 'File "{}" is not in git directory "{}"'.format(file, git_dir)
super(NotInGitDirError, self).__init__(msg)
class DataItem(object):
LOCAL_STATE_FILE_PREFIX = '.'
LOCAL_STATE_FILE_SUFFIX = '.dvc_local_state'
CACHE_FILE_SEP = '_'
def __init__(self, data_file, git, config, cache_file=None):
self._git = git
self._config = config
self._cache_file = cache_file
self._data = Path(data_file, git)
if not self._data.abs.startswith(self._git.git_dir_abs):
raise NotInGitDirError(data_file, self._git.git_dir_abs)
if self._data.abs.startswith(os.path.join(self._git.git_dir_abs, self._config.CONFIG_DIR)):
raise DataItemInConfigDirError(data_file)
def copy(self, cache_file=None):
if not cache_file:
cache_file = self._cache_file
return DataItem(self._data.abs, self._git, self._config, cache_file)
def __hash__(self):
return self.data.dvc.__hash__()
def __eq__(self, other):
if other == None:
return False
return self.data.dvc == other.data.dvc
@property
def data(self):
return self._data
def _state(self, prefix, suffix):
state_file = os.path.join(self._git.git_dir_abs,
os.path.dirname(self.data.dvc),
prefix + os.path.basename(self.data.dvc) + suffix)
return Path(state_file, self._git)
@cached_property
def state(self):
return Path(StateFile.find(self).path, self._git)
@cached_property
def cache_dir_abs(self):
return os.path.join(self._git.git_dir_abs, ConfigI.CACHE_DIR)
@cached_property
def local_state(self):
return self._state(self.LOCAL_STATE_FILE_PREFIX, self.LOCAL_STATE_FILE_SUFFIX)
@cached_property
def cache_dir(self):
return os.path.join(self._git.git_dir_abs, self._config.cache_dir)
@property
def cache(self):
cache_dir = self.cache_dir_abs
if self._cache_file:
file_name = os.path.relpath(os.path.realpath(self._cache_file), cache_dir)
else:
file_name = str(StateFile.find_md5(self))
cache_file = os.path.join(cache_dir, file_name)
return Path(cache_file, self._git)
def move_data_to_cache(self):
md5 = file_md5(self.data.relative)[0]
self._cache_file = os.path.join(self.cache_dir_abs, md5)
self._git.modify_gitignore([self.data.relative])
if not os.path.isfile(self.cache.relative):
System.hardlink(self.data.relative, self.cache.relative)
os.chmod(self.data.relative, stat.S_IREAD)
LocalStateFile(self).save()
-59
View File
@@ -1,59 +0,0 @@
import os
import re
from dvc.config import ConfigI
from dvc.path.data_item import DataItem, DataItemError, DataDirError
from dvc.path.path import Path
from dvc.path.stated_data_item import StatedDataItem
from dvc.system import System
from dvc.state_file import StateFile
class PathFactory(object):
def __init__(self, settings):
self._git = settings.git
self._config = settings.config
self._settings = settings
self._curr_dir_abs = System.realpath(os.curdir)
def path(self, relative_raw):
return Path(relative_raw, self._git)
def data_item(self, data_file, cache_file=None):
return DataItem(data_file, self._git, self._config, cache_file)
def stated_data_item(self, state, data_file):
return StatedDataItem(state, data_file, self._git, self._config)
def data_item_from_dvc_path(self, dvc_path, existing=True):
path = Path.from_dvc_path(dvc_path, self._git)
if existing:
return self.existing_data_item(path.relative)
else:
return self.data_item(path.relative)
def is_data_item(self, fname):
return StateFile.find_by_output(self._settings, fname) != None
def existing_data_item(self, fname):
if not self.is_data_item(fname):
raise DataItemError(u'No state file found for data file {}'.format(fname))
return DataItem(fname, self._git, self._config)
def to_data_items(self, files):
result = []
externally_created_files = []
for file in files:
try:
data_item = DataItem(file, self._git, self._config)
result.append(data_item)
except DataDirError:
externally_created_files.append(file)
return result, externally_created_files
def all_existing_data_items(self, subdir='.', cache_exists=True):
files = StateFile.find_all_data_files(self._git, subdir)
return self.to_data_items(files)[0]
-40
View File
@@ -1,40 +0,0 @@
import os
from dvc.system import System
class Path(object):
def __init__(self, path, git):
if not os.path.isabs(path):
pwd = System.get_cwd()
path = os.path.normpath(os.path.join(pwd, path))
self._abs = path
self._dvc = os.path.relpath(self.abs, git.git_dir_abs)
self._relative = os.path.relpath(self._abs, git.curr_dir_abs)
self._filename = os.path.basename(self._abs)
self._dirname = os.path.dirname(self._abs)
@staticmethod
def from_dvc_path(dvc_path, git):
return Path(os.path.join(git.git_dir_abs, dvc_path), git)
@property
def dvc(self):
return self._dvc
@property
def abs(self):
return self._abs
@property
def relative(self):
return self._relative
@property
def filename(self):
return self._filename
@property
def dirname(self):
return self._dirname
-39
View File
@@ -1,39 +0,0 @@
from dvc.path.data_item import DataItem
class StatedDataItem(DataItem):
STATUS_UNTRACKED = '?'
STATUS_DELETE = 'D'
STATUS_MODIFIED = 'M'
STATUS_TYPE_CHANGED = 'T'
def __init__(self, state, data_file, git, config, cache_file=None):
super(StatedDataItem, self).__init__(data_file, git, config, cache_file)
self._status = state
@property
def status(self):
return self._status
def _check_status(self, status):
return self._status.find(status) >= 0
@property
def is_removed(self):
return self._check_status(self.STATUS_DELETE)
@property
def is_modified(self):
return self._check_status(self.STATUS_MODIFIED) \
or self._check_status(self.STATUS_TYPE_CHANGED)
@property
def is_new(self):
return self._check_status(self.STATUS_UNTRACKED)
@property
def is_unusual(self):
return self.is_new or self.is_modified or self.is_removed
def __repr__(self):
return u'({}, {})'.format(self.state, self.data.dvc)
+227
View File
@@ -0,0 +1,227 @@
import os
import itertools
import networkx as nx
from dvc.logger import Logger
from dvc.exceptions import DvcException
from dvc.stage import Stage, Output, Dependency
from dvc.config import Config
from dvc.state import State
from dvc.lock import Lock
from dvc.scm import SCM
from dvc.cache import Cache
from dvc.data_cloud import DataCloud
class PipelineError(DvcException):
pass
class StageNotInPipelineError(PipelineError):
pass
class StageNotFoundError(DvcException):
pass
class Pipeline(object):
def __init__(self, project, G):
self.project = project
self.G = G
def graph(self):
return self.G
def stages(self):
return nx.get_node_attributes(self.G, 'stage')
def changed(self, stage):
for node in nx.dfs_postorder_nodes(G, stage.path.relative_to(self.project.root_dir)):
if self.stages[node].changed():
return True
return False
def reproduce(self, stage):
if stage not in self.stages():
raise StageNotInPipelineError()
if not self.changed(stage):
raise PipelineNotChangedError()
for node in nx.dfs_postorder_nodes(G, stage.path.relative_to(self.project.root_dir)):
self.stages[node].reproduce()
stage.reproduce()
class Project(object):
DVC_DIR = '.dvc'
def __init__(self, root_dir):
self.root_dir = os.path.abspath(root_dir)
self.dvc_dir = os.path.join(self.root_dir, self.DVC_DIR)
self.scm = SCM(self.root_dir)
self.lock = Lock(self.dvc_dir)
self.cache = Cache(self.dvc_dir)
self.state = State(self.root_dir, self.dvc_dir)
self.config = Config(self.dvc_dir)
self.logger = Logger()
self.cloud = DataCloud(self.config._config)
@staticmethod
def init(root_dir):
root_dir = os.path.abspath(root_dir)
dvc_dir = os.path.join(root_dir, Project.DVC_DIR)
os.mkdir(dvc_dir)
config = Config.init(dvc_dir)
cache = Cache.init(dvc_dir)
state = State.init(root_dir, dvc_dir)
lock = Lock(dvc_dir)
scm = SCM(root_dir)
scm.ignore_list([cache.cache_dir,
state.state_file,
lock.lock_file])
scm.commit('DVC init')
return Project(root_dir)
def add(self, fname):
path = os.path.abspath(fname) + Stage.STAGE_FILE_SUFFIX
cwd = os.path.dirname(path)
outputs = [Output.loads(self, os.path.basename(fname), use_cache=True, cwd=cwd)]
stage = Stage(project=self,
path=path,
cmd=None,
cwd=cwd,
outs=outputs,
deps=[],
locked=True)
stage.save()
stage.dump()
def remove(self, fname):
path = os.path.abspath(fname)
for out in self.outs():
if out.path == path:
out.stage().remove()
def _add_orphans(self, deps):
outs = [out.path for out in self.outs()]
for dep in deps:
if not dep.use_cache or dep.path in outs:
continue
self.add(dep.path)
def run(self, cmd, deps, deps_no_cache, outs, outs_no_cache, locked, fname, cwd):
cwd = os.path.abspath(cwd)
path = os.path.join(cwd, fname)
outputs = Output.loads_from(self, outs, use_cache=True, cwd=cwd)
outputs += Output.loads_from(self, outs_no_cache, use_cache=False, cwd=cwd)
deps = Dependency.loads_from(self, deps, use_cache=True, cwd=cwd)
deps += Dependency.loads_from(self, deps_no_cache, use_cache=False, cwd=cwd)
# Add files that were specified as cached dependencies and weren't added before
self._add_orphans(deps)
stage = Stage(project=self,
path=path,
cmd=cmd,
cwd=cwd,
outs=outputs,
deps=deps,
locked=locked)
stage.run()
stage.dump()
def reproduce(self, targets, recursive=False, force=False):
stages = nx.get_node_attributes(self.graph(), 'stage')
for target in targets:
node = os.path.relpath(os.path.abspath(target), self.root_dir)
if node not in stages:
raise StageNotFoundError(target)
if recursive:
for node in nx.dfs_postorder_nodes(self.graph(), node):
stages[node].reproduce(force=force)
stages[node].reproduce(force=force)
def checkout(self):
for stage in self.stages():
stage.checkout()
def _used_cache(self):
clist = []
for stage in self.stages():
for entry in itertools.chain(stage.outs, stage.deps):
if not entry.use_cache:
continue
if entry.cache not in clist:
clist.append(entry.cache)
return clist
def gc(self):
clist = self._used_cache()
for cache in self.cache.all():
if cache in clist:
continue
os.unlink(cache)
self.logger.info('\'{}\' was removed'.format(cache))
def push(self, jobs=1):
self.cloud.push(self._used_cache(), jobs)
def pull(self, jobs=1):
self.cloud.pull(self._used_cache(), jobs)
for stage in self.stages():
for entry in itertools.chain(stage.outs, stage.deps):
if entry.use_cache:
entry.link()
def status(self, jobs=1):
return self.cloud.status(self._used_cache(), jobs)
def graph(self):
G = nx.DiGraph()
for stage in self.stages():
node = os.path.relpath(stage.path, self.root_dir)
G.add_node(node, stage=stage)
for dep in stage.deps:
dep_stage = dep.stage()
if not dep_stage:
continue
dep_node = os.path.relpath(dep_stage.path, self.root_dir)
G.add_node(dep_node, stage=dep_stage)
G.add_edge(node, dep_node)
return G
def stages(self):
stages = []
for root, dirs, files in os.walk(self.root_dir):
for fname in files:
path = os.path.join(root, fname)
if not Stage.is_stage_file(path):
continue
stages.append(Stage.load(self, path))
return stages
def outs(self):
outs = []
for stage in self.stages():
outs += stage.outs
return outs
def pipelines(self):
pipelines = []
for G in nx.weakly_connected_component_subgraphs(self.graph()):
pipeline = Pipeline(self, G)
pipelines.append(pipeline)
return pipelines
+87
View File
@@ -0,0 +1,87 @@
import os
import git
from dvc.exceptions import DvcException
from dvc.command.common.branch_changer import BranchChanger
class FileNotInRepoError(DvcException):
pass
class Base(object):
@staticmethod
def is_repo(root_dir):
return True
def ignore(self, path):
pass
def ignore_list(self, p_list):
return [self.ignore(path) for path in p_list]
def add(self, path):
pass
def add_list(self, p_list):
return [self.add(path) for path in p_list]
def commit(self, msg):
pass
def checkout(self, branch):
pass
def branch(self, branch):
pass
def brancher(self, branch, new_branch):
return BranchChanger(self, branch, new_branch)
class Git(Base):
GITIGNORE = '.gitignore'
GIT_DIR = '.git'
def __init__(self, root_dir):
self.root_dir = root_dir
self.repo = git.Repo(root_dir)
@staticmethod
def is_repo(root_dir):
git_dir = os.path.join(root_dir, Git.GIT_DIR)
return os.path.isdir(git_dir)
def ignore(self, path):
entry = os.path.basename(path)
gitignore = os.path.join(os.path.dirname(path), self.GITIGNORE)
if not gitignore.startswith(self.root_dir):
raise FileNotInRepoError()
if os.path.exists(gitignore) and entry in open(gitignore, 'r').read():
return
open(gitignore, 'a').write('\n' + entry)
def add(self, path):
self.repo.index.add(path)
def commit(self, msg):
self.repo.index.commit(msg)
def checkout(self, branch, create_new=False):
if create_new:
self.repo.git.checkout('HEAD', b=branch)
else:
self.repo.git.checkout(branch)
def branch(self, branch):
self.repo.git.branch(branch)
def SCM(root_dir):
if Git.is_repo(root_dir):
return Git(root_dir)
return Base(root_dir)
+249
View File
@@ -0,0 +1,249 @@
import os
import stat
import yaml
import itertools
from dvc.system import System
from dvc.data_cloud import file_md5
from dvc.exceptions import DvcException
from dvc.executor import Executor
class OutputNoCacheError(DvcException):
pass
class Output(object):
PARAM_PATH = 'path'
PARAM_MD5 = 'md5'
PARAM_CACHE = 'cache'
def __init__(self, project, path, md5=None, use_cache=False):
self.project = project
self.path = path
self.md5 = md5
self.use_cache = use_cache
def _changed_md5(self):
return self.md5 != file_md5(self.path)[0]
def changed(self):
if not self.use_cache:
changed = self._changed_md5()
else:
changed = not os.path.samefile(self.path, self.cache)
if changed:
self.project.logger.debug('{} changed'.format(self.path))
return changed
@property
def cache(self):
return self.project.cache.get(self.md5)
def link(self, checkout=False):
if not self.use_cache:
raise OutputNoCacheError()
if not os.path.exists(self.path) and not os.path.exists(self.cache):
raise OutputNoCacheError()
if os.path.exists(self.path) and os.path.exists(self.cache) and os.path.samefile(self.path, self.cache):
return
if os.path.exists(self.cache):
if os.path.exists(self.path):
# This means that we already have cache for this data.
# We remove data and link it to existing cache to save
# some space.
os.unlink(self.path)
src = self.cache
link = self.path
elif not checkout:
src = self.path
link = self.cache
else:
raise OutputNoCacheError()
System.hardlink(src, link)
def checkout(self):
if not self.use_cache:
return
self.link(checkout=True)
def mtime(self):
return os.path.getmtime(self.path)
def update(self, md5=None):
self.md5 = md5
if not self.md5:
self.md5 = file_md5(self.path)[0]
self.project.state.update(self.path, self.md5, self.mtime())
def save(self):
if not self.use_cache:
return
self.project.scm.ignore(self.path)
self.link()
os.chmod(self.path, stat.S_IREAD)
def dumpd(self, cwd):
return {
Output.PARAM_PATH: os.path.relpath(self.path, cwd),
Output.PARAM_MD5: self.md5,
Output.PARAM_CACHE: self.use_cache
}
@classmethod
def loadd(cls, project, d, cwd='.'):
path = os.path.join(cwd, d[Output.PARAM_PATH])
md5 = d[Output.PARAM_MD5]
use_cache = d[Output.PARAM_CACHE]
return cls(project, path, md5, use_cache=use_cache)
@classmethod
def loadd_from(cls, project, d_list, cwd='.'):
return [cls.loadd(project, x, cwd=cwd) for x in d_list]
@classmethod
def loads(cls, project, s, use_cache=False, cwd='.'):
return cls(project, os.path.join(cwd, s), None, use_cache=use_cache)
@classmethod
def loads_from(cls, project, s_list, use_cache=False, cwd='.'):
return [cls.loads(project, x, use_cache, cwd=cwd) for x in s_list]
def stage(self):
for stage in self.project.stages():
for out in stage.outs:
if self.path == out.path:
return stage
return None
class Dependency(Output):
def update(self):
md5 = None
state = self.project.state.get(self.path)
if state and state.mtime == self.mtime():
md5 = state.md5
msg = '{} using md5 from state file for dependency'
self.project.logger.debug(msg.format(self.path))
super(Dependency, self).update(md5=md5)
class Stage(object):
STAGE_FILE = 'Dvcfile'
STAGE_FILE_SUFFIX = '.dvc'
PARAM_CMD = 'cmd'
PARAM_DEPS = 'deps'
PARAM_OUTS = 'outs'
PARAM_LOCKED = 'locked'
def __init__(self, project, path=None, cmd=None, cwd=None, deps=[], outs=[], locked=False):
self.project = project
self.path = path
self.cmd = cmd
self.cwd = cwd
self.outs = outs
self.deps = deps
self.locked = locked
@staticmethod
def is_stage_file(path):
if not os.path.isfile(path):
return False
if not path.endswith(Stage.STAGE_FILE_SUFFIX) and os.path.basename(path) != Stage.STAGE_FILE:
return False
return True
def changed(self):
for entry in itertools.chain(self.outs, self.deps):
if entry.changed():
self.project.logger.debug("{} changed".format(self.path))
return True
return False
def remove_outs(self):
for out in self.outs:
self.project.logger.debug("Removing '{}'".format(out.path))
os.unlink(out.path)
def remove(self):
self.remove_outs()
os.unlink(self.path)
def reproduce(self, force=False):
if not self.changed() and not force:
return
if self.cmd:
# Removing outputs only if we actually have command to reproduce
self.remove_outs()
self.run()
self.project.logger.debug("{} reproduced".format(self.path))
@staticmethod
def loadd(project, d, path):
path = os.path.abspath(path)
cwd = os.path.dirname(path)
cmd = d[Stage.PARAM_CMD]
deps = Dependency.loadd_from(project, d[Stage.PARAM_DEPS], cwd=cwd)
outs = Output.loadd_from(project, d[Stage.PARAM_OUTS], cwd=cwd)
locked = d[Stage.PARAM_LOCKED]
return Stage(project=project,
path=path,
cmd=cmd,
cwd=cwd,
deps=deps,
outs=outs,
locked=locked)
@staticmethod
def load(project, fname):
with open(fname, 'r') as fd:
return Stage.loadd(project, yaml.load(fd), fname)
def dumpd(self):
deps = [x.dumpd(self.cwd) for x in self.deps]
outs = [x.dumpd(self.cwd) for x in self.outs]
return {
Stage.PARAM_CMD: self.cmd,
Stage.PARAM_DEPS: deps,
Stage.PARAM_OUTS: outs,
Stage.PARAM_LOCKED: self.locked
}
def dump(self, fname=None):
if not fname:
fname = self.path
with open(fname, 'w') as fd:
yaml.dump(self.dumpd(), fd, default_flow_style=False)
def save(self):
for dep in self.deps:
dep.update()
dep.save()
for out in self.outs:
out.update()
out.save()
def run(self):
if self.cmd:
Executor.exec_cmd_only_success(self.cmd, cwd=str(self.cwd), shell=True)
self.save()
def checkout(self):
for entry in itertools.chain(self.outs, self.deps):
entry.checkout()
+79
View File
@@ -0,0 +1,79 @@
import os
import tinydb
from dvc.exceptions import DvcException
class StateEntry(object):
PARAM_PATH = 'path'
PARAM_MTIME = 'mtime'
PARAM_MD5 = 'md5'
def __init__(self, root_dir, path, md5, mtime):
self.root_dir = root_dir
self.path = path
self.mtime = mtime
self.md5 = md5
self.dvc_path = os.path.relpath(self.path, self.root_dir)
def update(self, md5, mtime):
self.mtime = mtime
self.md5 = md5
@staticmethod
def loadd(root_dir, d):
path = os.path.join(root_dir, d[StateEntry.PARAM_PATH])
mtime = d[StateEntry.PARAM_MTIME]
md5 = d[StateEntry.PARAM_MD5]
return StateEntry(root_dir, path, md5, mtime)
def dumpd(self):
return {
self.PARAM_PATH: self.dvc_path,
self.PARAM_MD5: self.md5,
self.PARAM_MTIME: self.mtime
}
class StateDuplicateError(DvcException):
pass
class State(object):
STATE_FILE = 'state'
def __init__(self, root_dir, dvc_dir):
self.root_dir = root_dir
self.dvc_dir = dvc_dir
self.state_file = os.path.join(dvc_dir, self.STATE_FILE)
self._db = tinydb.TinyDB(self.state_file)
self._q = tinydb.Query()
@staticmethod
def init(root_dir, dvc_dir):
return State(root_dir, dvc_dir)
def update(self, path, md5, mtime):
existing = self.get(path)
if not existing:
return self.add(path, md5, mtime)
state = StateEntry(self.root_dir, path, md5, mtime)
self._db.update(state.dumpd(), self._q.path == state.dvc_path)
return state
def add(self, path, md5, mtime):
entry = StateEntry(self.root_dir, path, md5, mtime)
self._db.insert(entry.dumpd())
return entry
def get(self, path):
d_list = self._db.search(self._q.path == os.path.relpath(path, self.root_dir))
if not len(d_list):
return None
if len(d_list) > 1:
raise StateDuplicateError()
return StateEntry.loadd(self.root_dir, d_list[0])
-223
View File
@@ -1,223 +0,0 @@
import os
import yaml
from dvc.exceptions import DvcException
from dvc.data_cloud import file_md5
from dvc.path.path import Path
class StateFileError(DvcException):
def __init__(self, msg):
DvcException.__init__(self, 'State file error: {}'.format(msg))
class StateFileBase(object):
@staticmethod
def _save(fname, data):
with open(fname, 'w') as fd:
yaml.dump(data, fd, default_flow_style=False)
@staticmethod
def _load(fname, state_class, *args):
with open(fname, 'r') as fd:
data = yaml.load(fd)
return state_class.loadd(data, *args)
class StateFile(StateFileBase):
DVCFILE_NAME = 'Dvcfile'
STATE_FILE_SUFFIX = '.dvc'
PARAM_CMD = 'cmd'
PARAM_DEPS = 'deps'
PARAM_LOCKED = 'locked'
PARAM_OUT = 'out'
PARAM_OUT_GIT = 'out-git'
def __init__(self,
fname=None,
cmd=None,
out=None,
out_git=None,
deps=None,
locked=None,
cwd=''):
super(StateFile, self).__init__()
self.cmd = cmd
self.out = out
self.out_git = out_git
self.deps = deps
self.locked = locked
self.path = os.path.abspath(fname) if fname else None
self.cwd = cwd
@staticmethod
def parse_deps_state(settings, deps, currdir=None):
state = {}
for dep in deps:
if settings.path_factory.is_data_item(dep):
item = settings.path_factory.data_item(dep)
md5 = StateFile.find_md5(item)
else:
md5 = file_md5(os.path.join(settings.git.git_dir_abs, dep))[0]
if currdir:
name = os.path.relpath(dep, currdir)
else:
name = dep
state[name] = md5
return state
@staticmethod
def loadd(data, fname=None):
return StateFile(fname=fname,
cmd=data.get(StateFile.PARAM_CMD, None),
out=data.get(StateFile.PARAM_OUT, None),
out_git=data.get(StateFile.PARAM_OUT_GIT, None),
deps=data.get(StateFile.PARAM_DEPS, []),
locked=data.get(StateFile.PARAM_LOCKED, None))
@staticmethod
def load(fname):
return StateFile._load(fname, StateFile, fname)
@staticmethod
def _is_state_file(path):
return (path.endswith(StateFile.STATE_FILE_SUFFIX) or \
os.path.basename(path) == StateFile.DVCFILE_NAME) and \
os.path.isfile(path)
@staticmethod
def _find_state(fname, dname):
name = os.path.relpath(fname, dname)
for entry in os.listdir(dname):
state_file = os.path.join(dname, entry)
if not StateFile._is_state_file(state_file):
continue
state = StateFile.load(os.path.join(dname, state_file))
if name in state.out:
return state
return None
@staticmethod
def _find(name, start, finish):
dname = start
fname = name
while True:
state = StateFile._find_state(fname, dname)
if state:
return state
if dname == finish:
break
dname = os.path.dirname(dname)
return None
@staticmethod
def find(data_item):
return StateFile._find(data_item.data.abs, data_item.data.dirname, data_item._git.git_dir_abs)
@staticmethod
def find_by_output(settings, output):
path = os.path.abspath(output)
return StateFile._find(path, os.path.dirname(path), settings.git.git_dir_abs)
@staticmethod
def find_all_state_files(git, subdir='.'):
states = []
for root, dirs, files in os.walk(os.path.join(git.git_dir_abs, subdir)):
for fname in files:
path = os.path.join(root, fname)
if not StateFile._is_state_file(path):
continue
states.append(path)
return states
@staticmethod
def find_all_states(git, subdir='.'):
state_files = StateFile.find_all_state_files(git, subdir)
return [StateFile.load(state_file) for state_file in state_files]
@staticmethod
def find_all_data_files(git, subdir='.'):
states = StateFile.find_all_states(git, subdir)
files = []
for state in states:
for out in state.out:
files.append(os.path.join(state.cwd, out))
return files
@staticmethod
def find_all_cache_files(git, subdir='.'):
states = StateFile.find_all_states(git, subdir)
cache_files = []
for state in states:
for out,md5 in state.out.items():
cache_files.append(md5)
return cache_files
@staticmethod
def find_md5(data_item):
state = StateFile.find(data_item)
name = os.path.relpath(data_item.data.abs, os.path.dirname(state.path))
return state.out[name]
@staticmethod
def loads(content):
data = yaml.load(content)
return StateFile.loadd(data)
def save(self):
res = {
self.PARAM_CMD: self.cmd,
self.PARAM_OUT: self.out,
self.PARAM_OUT_GIT: self.out_git,
self.PARAM_DEPS: self.deps,
self.PARAM_LOCKED: self.locked
}
self._save(self.path, res)
class LocalStateFile(StateFileBase):
MAGIC = 'DVC-Local-State'
VERSION = '0.1'
PARAM_DATA_TIMESTAMP = 'DataTimestamp'
PARAM_CACHE_TIMESTAMP = 'CacheTimestamp'
def __init__(self, data_item, data_timestamp=None, cache_timestamp=None):
super(LocalStateFile, self).__init__()
self.data_item = data_item
self.data_timestamp = data_timestamp
self.cache_timestamp = cache_timestamp
if not data_timestamp:
self.data_timestamp = os.path.getmtime(self.data_item.data.relative)
if not cache_timestamp:
self.cache_timestamp = os.path.getmtime(self.data_item.cache.relative)
@staticmethod
def loadd(data):
return LocalStateFile(None,
data.get(LocalStateFile.PARAM_DATA_TIMESTAMP, None),
data.get(LocalStateFile.PARAM_CACHE_TIMESTAMP, None))
@staticmethod
def load(data_item):
return LocalStateFile._load(data_item.local_state.relative, LocalStateFile)
def save(self):
res = {
self.PARAM_DATA_TIMESTAMP : self.data_timestamp,
self.PARAM_CACHE_TIMESTAMP : self.cache_timestamp
}
self._save(self.data_item.local_state.relative, res)
-1
View File
@@ -132,7 +132,6 @@ function dvc_create_repo() {
mkdir data
cp $DATA_CACHE/* data
dvc add data
}
function dvc_clean_cloud_aws() {
+4 -3
View File
@@ -5,12 +5,13 @@ set -e
source common.sh
TESTS+=" test-init.sh"
TESTS+=" test-add.sh"
TESTS+=" test-add-remove.sh"
TESTS+=" test-sync.sh"
TESTS+=" test-repro-code.sh"
TESTS+=" test-merge.sh"
#TESTS+=" test-merge.sh"
TESTS+=" test-checkout.sh"
TESTS+=" test-gc.sh"
TESTS+=" test-lock.sh"
for t in $TESTS; do
rm -rf $TEST_REPO
@@ -18,4 +19,4 @@ for t in $TESTS; do
./$t
done
rm -rf $TEST_REPO
#rm -rf $TEST_REPO
+22
View File
@@ -0,0 +1,22 @@
#!/bin/bash
set -e
source common.sh
dvc_create_repo
dvc add data/foo
dvc_check_files data/foo.dvc data/foo
dvc remove data/foo
if [ -f "data/foo" ]; then
echo "data/foo was not removed"
dvc_fail
fi
if [ -f "data/foo.dvc" ]; then
echo "data/foo.dvc was not removed"
dvc_fail
fi
dvc_pass
-13
View File
@@ -1,13 +0,0 @@
#!/bin/bash
set -e
source common.sh
dvc_create_repo
dvc_info "Add file"
cp $DATA_CACHE/foo data/local
dvc add data/local
dvc_check_files "data/local data/local.dvc data/.local.dvc_local_state"
dvc_pass
+3 -1
View File
@@ -6,7 +6,9 @@ source common.sh
dvc_create_repo
dvc run -D code/code.sh -d data/foo -o data/foo1 bash code/code.sh data/foo data/foo1
rm -f data/foo
dvc checkout
dvc_check_files data/foo data/foo.dvc
dvc_check_files data/foo
dvc_pass
+15
View File
@@ -0,0 +1,15 @@
#!/bin/bash
set -e
source common.sh
dvc_create_repo
dvc run -D code/code.sh -d data/foo -o data/foo1 bash code/code.sh data/foo data/foo1
cat Dvcfile | grep 'locked' | grep 'false' || dvc_fatal "stage locked after 'dvc run'"
dvc lock
cat Dvcfile | grep 'locked' | grep 'true' || dvc_fatal "stage not locked after 'dvc lock'"
dvc lock --unlock
cat Dvcfile | grep 'locked' | grep 'false' || dvc_fatal "stage locked after 'dvc lock --unlock'"
dvc_pass
+9 -10
View File
@@ -10,23 +10,22 @@ function test_generic() {
dvc_create_repo
dvc_info 'Copy foo into foo1'
dvc run -d code/code.sh -d data/foo -o data/foo1 bash code/code.sh data/foo data/foo1
dvc run -D code/code.sh -d data/foo -o data/foo1 bash code/code.sh data/foo data/foo1
dvc_info 'Modify code'
echo " " >> code/code.sh
git commit -am 'Change code'
dvc_info 'Reproduce foo1'
dvc repro data/foo1
dvc repro
dvc_check_files data/foo1 data/foo1.dvc
if [ "$(cat data/foo1)" != "foo" ]; then
dvc_fail
fi
dvc_info 'Modify foo'
dvc remove data/foo
rm -f data/foo
cp $DATA_CACHE/bar data/foo
dvc add data/foo
dvc_info 'Reproduce foo1 as default target'
dvc repro
@@ -48,9 +47,9 @@ function test_partial() {
git commit -m "copy code"
dvc_info "Create repro chain foo -> foo1 -> foo2 -> foo3"
dvc run -f copy_foo_foo1.dvc -d code/code1.sh -d data/foo -o data/foo1 cp data/foo data/foo1
dvc run -f copy_foo1_foo2.dvc -d code/code2.sh -d data/foo1 -o data/foo2 cp data/foo1 data/foo2
dvc run -f copy_foo2_foo3.dvc -d code/code3.sh -d data/foo2 -o data/foo3 cp data/foo2 data/foo3
dvc run -f copy_foo_foo1.dvc -D code/code1.sh -d data/foo -o data/foo1 cp data/foo data/foo1
dvc run -f copy_foo1_foo2.dvc -D code/code2.sh -d data/foo1 -o data/foo2 cp data/foo1 data/foo2
dvc run -f copy_foo2_foo3.dvc -D code/code3.sh -d data/foo2 -o data/foo3 cp data/foo2 data/foo3
dvc_info "Save original timestamps"
FOO_TS=$(dvc_timestamp data/foo)
@@ -64,7 +63,7 @@ function test_partial() {
git commit -m "modify code1.sh"
dvc_info "Reproduce data/foo3"
dvc repro data/foo3
dvc repro copy_foo2_foo3.dvc
dvc_info "Check timestamps"
if [ "$FOO_TS" != "$(dvc_timestamp data/foo)" ]; then
@@ -72,8 +71,8 @@ function test_partial() {
dvc_fail
fi
if [ "$FOO1_TS" == "$(dvc_timestamp data/foo1)" ]; then
dvc_error "data/foo1 timestamp did not change"
if [ "$FOO1_TS" != "$(dvc_timestamp data/foo1)" ]; then
dvc_error "data/foo1 timestamp changed"
dvc_fail
fi
+8 -6
View File
@@ -5,28 +5,30 @@ set -e
source common.sh
function test_sync() {
dvc run -D code/code.sh -d data/foo -o data/foo1 bash code/code.sh data/foo data/foo1
dvc_info "Checking status"
dvc status data/ | grep "new file" || dvc_fail
dvc status | grep "new file" || dvc_fail
dvc_info "Pushing data"
dvc push data/
dvc push
dvc_info "Checking status"
dvc status data/
dvc status
dvc_info "Removing all cache"
rm -rf .dvc/cache/*
rm -rf data/foo data/bar
dvc_info "Checking status"
dvc status data/ | grep "deleted" || dvc_fail
dvc status | grep "deleted" || dvc_fail
dvc_info "Pulling data"
dvc pull data/
dvc pull
dvc_check_files data/foo data/bar
dvc_info "Checking status"
dvc status data/
dvc status
}
function test_aws() {
+2
View File
@@ -91,3 +91,5 @@ networkx==1.11
gcs-oauth2-boto-plugin==1.14
xmltodict==0.11.0
pyyaml==3.12
tinydb==3.7.0
gitpython==2.1.8
+2
View File
@@ -96,6 +96,8 @@ install_requires = [
'networkx==1.11',
'gcs-oauth2-boto-plugin==1.14',
'pyyaml==3.12',
'tinydb==3.7.0',
'gitpython==2.1.8',
]
if platform.system() == 'Darwin':