diff --git a/dvc/cli.py b/dvc/cli.py index c5ef69039..26ec1ae45 100644 --- a/dvc/cli.py +++ b/dvc/cli.py @@ -4,6 +4,7 @@ import sys import argparse from multiprocessing import cpu_count +from dvc.command.find import CmdFind from dvc.command.init import CmdInit from dvc.command.remove import CmdRemove from dvc.command.run import CmdRun @@ -420,6 +421,33 @@ def parse_args(argv=None): help='Checkout') checkout_parser.set_defaults(func=CmdCheckout) + # Find + find_parser = subparsers.add_parser( + 'find', + parents=[parent_parser], + help='Find branch name') + find_parser.set_defaults(func=CmdFind) + find_parser.add_argument( + 'target', + nargs='?', + help='Target metric file.') + find_parser.add_argument( + '-r', + '--branch-regex', + help='Branch name regexp.') + find_parser.add_argument( + '-c', + '--criteria', + help='Search criteria. By default it finds max.', + choices=['max', 'min', 'all'], + default='max') + find_parser.add_argument( + '-s', + '--show-value', + action='store_true', + default=False, + help='Show metrics value.') + if isinstance(argv, str): argv = argv.split() diff --git a/dvc/command/find.py b/dvc/command/find.py new file mode 100644 index 000000000..0f9091343 --- /dev/null +++ b/dvc/command/find.py @@ -0,0 +1,62 @@ +from dvc import utils +from dvc.command.base import CmdBase, DvcLock +from dvc.executor import ExecutorError +from dvc.logger import Logger + + +class CmdFind(CmdBase): + def __init__(self, settings): + super(CmdFind, self).__init__(settings) + + def run(self): + with DvcLock(self.is_locker, self.git): + fname = self.parsed_args.target + regex = self.parsed_args.branch_regex + + branches = self.git.branches(regex) + metrics = [self.read_metrics(fname, b) for b in branches] + + zipped = zip(metrics, branches) + res = self.filter(zipped, self.parsed_args.criteria) + res = filter(lambda x: x[0] is not None, res) + + for (val, branch) in res: + if self.parsed_args.show_value: + print('{}: {}'.format(branch, val)) + else: + print(branch) + return 0 + + @staticmethod + def filter(zipped, criteria): + if criteria == 'max': + res = [max(zipped, key=lambda t: t[0])] + elif criteria == 'min': + res = [min(zipped, key=lambda t: t[0])] + else: + res = zipped + return res + + def read_metrics(self, fname, branch): + try: + lines = self.git.get_file_content(fname, branch).split('\n') + except ExecutorError as ex: + msg = 'Unable to read metrics file from branch {}: {}' + data_item = self.settings.path_factory.data_item(fname) + + try: + self.git.get_file_content(data_item.state.relative, branch) + Logger.error(msg.format(branch, 'this is data file, not metric file')) + except ExecutorError: + Logger.error(msg.format(branch, 'file does not exist in this branch')) + return None + + return None + + metric = utils.parse_target_metric(lines) + if not metric: + msg = 'Unable to parse metrics from the first line of file {} in branch {}' + Logger.error(msg.format(fname, branch)) + return None + + return metric diff --git a/dvc/git_wrapper.py b/dvc/git_wrapper.py index 05ac13b9f..aca0162b6 100644 --- a/dvc/git_wrapper.py +++ b/dvc/git_wrapper.py @@ -444,3 +444,24 @@ class GitWrapper(GitWrapperI): git_cmd = u'git ls-tree --name-only -r {}'.format(commit) lines = Executor.exec_cmd_only_success(git_cmd.split()).split('\n') return filter(lambda s: s.startswith(ConfigI.STATE_DIR), lines) + + @staticmethod + def branches(regexp): + cmd = 'git branch --format "%(refname)"' + lines = Executor.exec_cmd_only_success(cmd.split()).split('\n') + lines = map(lambda l: l.strip('"'), lines) + + if not regexp or '/' not in regexp: + # remove remotes like 'refs/heads' + lines = map(lambda s: s.split('/')[-1], lines) + + if not regexp: + return lines + + reg = re.compile(regexp) + return filter(reg.search, lines) + + @staticmethod + def get_file_content(fname, branch): + cmd = 'git show {}:{}'.format(branch, fname) + return Executor.exec_cmd_only_success(cmd.split()) diff --git a/dvc/state_file.py b/dvc/state_file.py index 9455cb7b6..e0f3aa887 100644 --- a/dvc/state_file.py +++ b/dvc/state_file.py @@ -4,6 +4,7 @@ import sys import json import re +from dvc import utils from dvc.exceptions import DvcException from dvc.path.data_item import DataDirError from dvc.system import System @@ -18,8 +19,6 @@ class StateFile(object): MAGIC = 'DVC-State' VERSION = '0.1' - FLOATS_FROM_STRING = re.compile(r'[-+]?(?:(?:\d*\.\d+)|(?:\d+\.?))(?:[Ee][+-]?\d+)?') - DVC_PYTHON_FILE_NAME = 'dvc.py' DVC_COMMAND = 'dvc' @@ -110,29 +109,13 @@ class StateFile(object): if target_metric: self.target_metrics[StateFile.TARGET_METRICS_SINGLE_METRIC] = target_metric - @staticmethod - def parse_target_metric(file_name): - fd = open(file_name, 'r') - - try: - lines = fd.readlines(2) - except Exception as e: - raise StateFileError('Unable to parse target file') - - if len(lines) != 1: - raise StateFileError('file contains more then one line') - - # Extract float from string. I.e. from 'AUC: 0.596182' - nums = StateFile.FLOATS_FROM_STRING.findall(lines[0]) - if len(nums) < 1: - raise StateFileError("Unable to parse metrics from the first line") - - return float(nums[0]) - @staticmethod def try_parse_target_metric(file_name): try: - return StateFile.parse_target_metric(file_name) + metric = utils.parse_target_metric_file(file_name) + if not metric: + raise StateFileError('Unable to parse metrics from the first line of file {}'.format(file_name)) + return metric except StateFileError: return None diff --git a/dvc/utils.py b/dvc/utils.py index 6863dd502..3a42db73a 100644 --- a/dvc/utils.py +++ b/dvc/utils.py @@ -2,6 +2,7 @@ Helpers for other modules. """ import os +import re import stat import shutil @@ -13,6 +14,7 @@ from dvc.logger import Logger LOCAL_CHUNK_SIZE = 1024*1024*1024 + def cached_property(func): '''A decorator for caching properties in classes.''' def get(self): @@ -82,3 +84,27 @@ def map_progress(func, targets, n_threads): progress.finish() return zip(targets, ret) + + +def parse_target_metric_file(file_name): + with open(file_name, 'r') as fd: + try: + lines = fd.readlines(2) + except Exception: + return None + return parse_target_metric(lines) + + +FLOATS_FROM_STRING = re.compile(r'[-+]?(?:(?:\d*\.\d+)|(?:\d+\.?))(?:[Ee][+-]?\d+)?') + + +def parse_target_metric(lines): + if len(lines) != 1: + return None + + # Extract float from string. I.e. from 'AUC: 0.596182' + nums = FLOATS_FROM_STRING.findall(lines[0]) + if len(nums) < 1: + return None + + return float(nums[0])