mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-11 15:28:27 +01:00
186 lines
5.3 KiB
Python
186 lines
5.3 KiB
Python
import os
|
|
import yaml
|
|
import itertools
|
|
import subprocess
|
|
|
|
from dvc.exceptions import DvcException
|
|
from dvc.output import Output, Dependency, OutputError
|
|
|
|
|
|
class StageCmdFailedError(DvcException):
|
|
def __init__(self, stage):
|
|
msg = 'Stage {} cmd {} failed'.format(stage.relpath, stage.cmd)
|
|
super(StageCmdFailedError, self).__init__(msg)
|
|
|
|
|
|
class MissingDataSource(OutputError):
|
|
def __init__(self, missing_files):
|
|
assert len(missing_files) > 0
|
|
|
|
source = 'source'
|
|
if len(missing_files) > 1:
|
|
source += 's'
|
|
|
|
msg = u'missing data {}: {}'.format(source, ', '.join(missing_files))
|
|
super(MissingDataSource, self).__init__(msg)
|
|
|
|
|
|
class Stage(object):
|
|
STAGE_FILE = 'Dvcfile'
|
|
STAGE_FILE_SUFFIX = '.dvc'
|
|
|
|
PARAM_CMD = 'cmd'
|
|
PARAM_DEPS = 'deps'
|
|
PARAM_OUTS = 'outs'
|
|
|
|
def __init__(self, project, path=None, cmd=None, cwd=None, deps=[], outs=[]):
|
|
self.project = project
|
|
self.path = path
|
|
self.cmd = cmd
|
|
self.cwd = cwd
|
|
self.outs = outs
|
|
self.deps = deps
|
|
|
|
@property
|
|
def relpath(self):
|
|
return os.path.relpath(self.path)
|
|
|
|
@property
|
|
def dvc_path(self):
|
|
return os.path.relpath(self.path, self.project.root_dir)
|
|
|
|
@property
|
|
def is_data_source(self):
|
|
return self.cmd is None
|
|
|
|
@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
|
|
else:
|
|
self.project.logger.debug("{} didn't change".format(self.path))
|
|
return False
|
|
|
|
def remove_outs(self):
|
|
for out in self.outs:
|
|
out.remove()
|
|
if out.use_cache:
|
|
self.project.scm.ignore_remove(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()
|
|
|
|
@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)
|
|
|
|
return Stage(project=project,
|
|
path=path,
|
|
cmd=cmd,
|
|
cwd=cwd,
|
|
deps=deps,
|
|
outs=outs)
|
|
@staticmethod
|
|
def loads(project=None,
|
|
cmd=None,
|
|
deps=[],
|
|
outs=[],
|
|
outs_no_cache=[],
|
|
fname=None,
|
|
cwd=os.curdir):
|
|
cwd = os.path.abspath(cwd)
|
|
path = os.path.join(cwd, fname)
|
|
outputs = Output.loads_from(project, outs, use_cache=True, cwd=cwd)
|
|
outputs += Output.loads_from(project, outs_no_cache, use_cache=False, cwd=cwd)
|
|
dependencies = Dependency.loads_from(project, deps, cwd=cwd)
|
|
|
|
return Stage(project=project,
|
|
path=path,
|
|
cmd=cmd,
|
|
cwd=cwd,
|
|
outs=outputs,
|
|
deps=dependencies)
|
|
|
|
@staticmethod
|
|
def load(project, fname):
|
|
with open(fname, 'r') as fd:
|
|
return Stage.loadd(project, yaml.safe_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,
|
|
}
|
|
|
|
def dump(self, fname=None):
|
|
if not fname:
|
|
fname = self.path
|
|
|
|
with open(fname, 'w') as fd:
|
|
yaml.safe_dump(self.dumpd(), fd, default_flow_style=False)
|
|
|
|
def save(self):
|
|
for dep in self.deps:
|
|
dep.save()
|
|
|
|
for out in self.outs:
|
|
out.save()
|
|
if out.use_cache:
|
|
self.project.scm.ignore(out.path)
|
|
|
|
def run(self):
|
|
if not self.is_data_source:
|
|
self.project.logger.info(u'Reproducing \'{}\':\n\t{}'.format(self.relpath, self.cmd))
|
|
|
|
p = subprocess.Popen(self.cmd, cwd=self.cwd, shell=True)
|
|
p.communicate()
|
|
if p.returncode != 0:
|
|
raise StageCmdFailedError(self)
|
|
|
|
self.save()
|
|
|
|
self.project.logger.debug(u'\'{}\' was reproduced'.format(self.relpath))
|
|
else:
|
|
self.project.logger.info(u'Verifying data sources in \'{}\''.format(self.relpath))
|
|
self.check_missing_outputs()
|
|
self.save()
|
|
|
|
def check_missing_outputs(self):
|
|
missing_outs = [out.rel_path for out in self.outs if not os.path.exists(out.rel_path)]
|
|
if missing_outs:
|
|
raise MissingDataSource(missing_outs)
|
|
|
|
def checkout(self):
|
|
for out in self.outs:
|
|
out.checkout()
|