mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-13 15:28:27 +01:00
analytics: send user events
Fixes #693 Signed-off-by: Ruslan Kuprieiev <[email protected]>
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
import os
|
||||
import json
|
||||
import errno
|
||||
|
||||
from dvc import VERSION
|
||||
from dvc.utils import is_binary
|
||||
from dvc.lock import Lock, LockError
|
||||
from dvc.logger import Logger
|
||||
|
||||
|
||||
class Analytics(object):
|
||||
URL = 'https://dddc07bh9f.execute-api.us-east-2.amazonaws.com/production'
|
||||
TIMEOUT_POST = 10
|
||||
|
||||
USER_ID_FILE = 'user_id'
|
||||
|
||||
PARAM_DVC_VERSION = 'dvc_version'
|
||||
PARAM_USER_ID = 'user_id'
|
||||
PARAM_SYSTEM_INFO = 'system_info'
|
||||
|
||||
PARAM_OS = 'os'
|
||||
|
||||
PARAM_WINDOWS_VERSION_MAJOR = 'windows_version_major'
|
||||
PARAM_WINDOWS_VERSION_MINOR = 'windows_version_minor'
|
||||
PARAM_WINDOWS_VERSION_BUILD = 'windows_version_build'
|
||||
PARAM_WINDOWS_VERSION_SERVICE_PACK = 'windows_version_service_pack'
|
||||
|
||||
PARAM_MAC_VERSION = 'mac_version'
|
||||
|
||||
PARAM_LINUX_DISTRO = 'linux_distro'
|
||||
PARAM_LINUX_DISTRO_VERSION = 'linux_distro_version'
|
||||
PARAM_LINUX_DISTRO_LIKE = 'linux_distro_like'
|
||||
|
||||
PARAM_SCM_CLASS = 'scm_class'
|
||||
PARAM_IS_BINARY = 'is_binary'
|
||||
PARAM_CMD_CLASS = 'cmd_class'
|
||||
PARAM_CMD_RETURN_CODE = 'cmd_return_code'
|
||||
|
||||
def __init__(self, info={}):
|
||||
from dvc.config import Config
|
||||
|
||||
self.info = info
|
||||
|
||||
cdir = Config.get_global_config_dir()
|
||||
try:
|
||||
os.makedirs(cdir)
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
raise
|
||||
|
||||
self.user_id_file = os.path.join(cdir, self.USER_ID_FILE)
|
||||
self.user_id_file_lock = Lock(cdir, self.USER_ID_FILE + '.lock')
|
||||
|
||||
@staticmethod
|
||||
def load(path):
|
||||
with open(path, 'r') as fobj:
|
||||
a = Analytics(info=json.load(fobj))
|
||||
os.unlink(path)
|
||||
return a
|
||||
|
||||
def _write_user_id(self):
|
||||
import uuid
|
||||
|
||||
with open(self.user_id_file, 'w+') as fobj:
|
||||
user_id = str(uuid.uuid4())
|
||||
info = {self.PARAM_USER_ID: user_id}
|
||||
json.dump(info, fobj)
|
||||
return user_id
|
||||
|
||||
def _read_user_id(self):
|
||||
if not os.path.exists(self.user_id_file):
|
||||
return None
|
||||
|
||||
with open(self.user_id_file, 'r') as fobj:
|
||||
try:
|
||||
info = json.load(fobj)
|
||||
return info[self.PARAM_USER_ID]
|
||||
except json.JSONDecodeError as exc:
|
||||
Logger.debug("Failed to load user_id: {}".format(exc))
|
||||
return None
|
||||
|
||||
def _get_user_id(self):
|
||||
try:
|
||||
with self.user_id_file_lock:
|
||||
user_id = self._read_user_id()
|
||||
if user_id is None:
|
||||
user_id = self._write_user_id()
|
||||
return user_id
|
||||
except LockError:
|
||||
msg = "Failed to acquire '{}'"
|
||||
Logger.debug(msg.format(self.user_id_file_lock.lock_file))
|
||||
|
||||
def _collect_windows(self):
|
||||
import sys
|
||||
version = sys.getwindowsversion()
|
||||
info = {}
|
||||
info[self.PARAM_OS] = 'windows'
|
||||
info[self.PARAM_WINDOWS_VERSION_MAJOR] = version.major
|
||||
info[self.PARAM_WINDOWS_VERSION_MINOR] = version.minor
|
||||
info[self.PARAM_WINDOWS_VERSION_BUILD] = version.build
|
||||
info[self.PARAM_WINDOWS_VERSION_SERVICE_PACK] = version.service_pack
|
||||
return info
|
||||
|
||||
def _collect_darwin(self):
|
||||
import platform
|
||||
info = {}
|
||||
info[self.PARAM_OS] = 'mac'
|
||||
info[self.PARAM_MAC_VERSION] = platform.mac_ver()[0]
|
||||
return info
|
||||
|
||||
def _collect_linux(self):
|
||||
import distro
|
||||
info = {}
|
||||
info[self.PARAM_OS] = 'linux'
|
||||
info[self.PARAM_LINUX_DISTRO] = distro.id()
|
||||
info[self.PARAM_LINUX_DISTRO_VERSION] = distro.version()
|
||||
info[self.PARAM_LINUX_DISTRO_LIKE] = distro.like()
|
||||
return info
|
||||
|
||||
def _collect_system_info(self):
|
||||
import platform
|
||||
system = platform.system()
|
||||
if system == 'Windows':
|
||||
return self._collect_windows()
|
||||
elif system == 'Darwin':
|
||||
return self._collect_darwin()
|
||||
elif system == 'Linux':
|
||||
return self._collect_linux()
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
def _collect(self, args=None, ret=None):
|
||||
from dvc.scm import SCM
|
||||
from dvc.project import Project, NotDvcProjectError
|
||||
from dvc.command.daemon import CmdDaemonAnalytics
|
||||
|
||||
self.info[self.PARAM_DVC_VERSION] = VERSION
|
||||
self.info[self.PARAM_IS_BINARY] = is_binary()
|
||||
self.info[self.PARAM_USER_ID] = self._get_user_id()
|
||||
|
||||
self.info[self.PARAM_SYSTEM_INFO] = self._collect_system_info()
|
||||
|
||||
try:
|
||||
scm = SCM(root_dir=Project._find_root())
|
||||
self.info[self.PARAM_SCM_CLASS] = type(scm).__name__
|
||||
except NotDvcProjectError:
|
||||
pass
|
||||
|
||||
if ret is not None:
|
||||
self.info[self.PARAM_CMD_RETURN_CODE] = ret
|
||||
|
||||
if args is not None and hasattr(args, 'func'):
|
||||
assert args.func != CmdDaemonAnalytics
|
||||
self.info[self.PARAM_CMD_CLASS] = args.func.__name__
|
||||
|
||||
def dump(self):
|
||||
import tempfile
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=False, mode='w') as fobj:
|
||||
json.dump(self.info, fobj)
|
||||
return fobj.name
|
||||
|
||||
@staticmethod
|
||||
def _is_enabled(cmd):
|
||||
from dvc.config import Config
|
||||
from dvc.project import Project, NotDvcProjectError
|
||||
from dvc.command.daemon import CmdDaemonBase
|
||||
|
||||
if os.getenv('DVC_TEST'):
|
||||
return False
|
||||
|
||||
if isinstance(cmd, CmdDaemonBase):
|
||||
return False
|
||||
|
||||
if cmd is None or not hasattr(cmd, 'config'):
|
||||
try:
|
||||
dvc_dir = Project._find_dvc_dir()
|
||||
config = Config(dvc_dir)
|
||||
assert config is not None
|
||||
except NotDvcProjectError:
|
||||
config = Config(validate=False)
|
||||
assert config is not None
|
||||
else:
|
||||
config = cmd.config
|
||||
assert config is not None
|
||||
|
||||
core = config._config.get(Config.SECTION_CORE, {})
|
||||
enabled = core.get(Config.SECTION_CORE_ANALYTICS, True)
|
||||
Logger.debug("Analytics is {}abled."
|
||||
.format('en' if enabled else 'dis'))
|
||||
return enabled
|
||||
|
||||
def send(self, detach=True, cmd=None, args=None, ret=None):
|
||||
import requests
|
||||
from dvc.daemon import Daemon
|
||||
|
||||
if not self._is_enabled(cmd):
|
||||
return
|
||||
|
||||
if detach: # pragma: no cover
|
||||
Daemon()(['analytics', self.dump()])
|
||||
return
|
||||
|
||||
self._collect(args=args, ret=ret)
|
||||
|
||||
Logger.debug("Sending analytics: {}".format(self.info))
|
||||
|
||||
try:
|
||||
requests.post(self.URL, json=self.info, timeout=self.TIMEOUT_POST)
|
||||
except requests.exceptions.RequestException as exc:
|
||||
Logger.debug("Failed to send analytics: {}".format(str(exc)))
|
||||
+19
-2
@@ -26,7 +26,8 @@ from dvc.command.install import CmdInstall
|
||||
from dvc.command.root import CmdRoot
|
||||
from dvc.command.lock import CmdLock, CmdUnlock
|
||||
from dvc.command.pipeline import CmdPipelineShow, CmdPipelineList
|
||||
from dvc.command.daemon import CmdDaemonUpdater
|
||||
from dvc.command.daemon import CmdDaemonUpdater, CmdDaemonAnalytics
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.logger import Logger
|
||||
from dvc import VERSION
|
||||
|
||||
@@ -41,11 +42,16 @@ def _fix_subparsers(subparsers):
|
||||
subparsers.dest = 'cmd'
|
||||
|
||||
|
||||
class DvcParserError(DvcException):
|
||||
def __init__(self):
|
||||
super(DvcException, self).__init__("Parser error")
|
||||
|
||||
|
||||
class DvcParser(argparse.ArgumentParser):
|
||||
def error(self, message):
|
||||
sys.stderr.write('{}{}\n'.format(Logger.error_prefix(), message))
|
||||
self.print_help()
|
||||
sys.exit(2)
|
||||
raise DvcParserError()
|
||||
|
||||
|
||||
class VersionAction(argparse.Action): # pragma: no cover
|
||||
@@ -1037,6 +1043,17 @@ def parse_args(argv=None):
|
||||
help=DAEMON_UPDATER_HELP)
|
||||
daemon_updater_parser.set_defaults(func=CmdDaemonUpdater)
|
||||
|
||||
DAEMON_ANALYTICS_HELP = 'Send dvc usage analytics.'
|
||||
daemon_analytics_parser = daemon_subparsers.add_parser(
|
||||
'analytics',
|
||||
parents=[parent_parser],
|
||||
description=DAEMON_ANALYTICS_HELP,
|
||||
help=DAEMON_ANALYTICS_HELP)
|
||||
daemon_analytics_parser.add_argument(
|
||||
'target',
|
||||
help="Analytics file.")
|
||||
daemon_analytics_parser.set_defaults(func=CmdDaemonAnalytics)
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if (issubclass(args.func, CmdRepro)
|
||||
|
||||
+2
-18
@@ -1,7 +1,4 @@
|
||||
import os
|
||||
|
||||
from dvc.logger import Logger
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.lock import LockError
|
||||
|
||||
|
||||
@@ -9,7 +6,8 @@ class CmdBase(object):
|
||||
def __init__(self, args):
|
||||
from dvc.project import Project
|
||||
|
||||
self.project = Project(self._find_root())
|
||||
self.project = Project()
|
||||
self.config = self.project.config
|
||||
self.args = args
|
||||
self._set_loglevel(args)
|
||||
|
||||
@@ -21,20 +19,6 @@ class CmdBase(object):
|
||||
elif args.verbose:
|
||||
Logger.be_verbose()
|
||||
|
||||
def _find_root(self):
|
||||
from dvc.project import Project
|
||||
|
||||
root = os.getcwd()
|
||||
while True:
|
||||
dvc_dir = os.path.join(root, Project.DVC_DIR)
|
||||
if os.path.isdir(dvc_dir):
|
||||
return root
|
||||
if os.path.ismount(root):
|
||||
break
|
||||
root = os.path.dirname(root)
|
||||
msg = "Not a dvc repository (checked up to mount point {})"
|
||||
raise DvcException(msg.format(root))
|
||||
|
||||
def run_cmd(self):
|
||||
try:
|
||||
with self.project.lock:
|
||||
|
||||
+14
-3
@@ -8,18 +8,29 @@ from dvc.exceptions import DvcException
|
||||
|
||||
class CmdConfig(CmdBase):
|
||||
def __init__(self, args):
|
||||
from dvc.project import Project
|
||||
from dvc.project import Project, NotDvcProjectError
|
||||
|
||||
self.args = args
|
||||
dvc_dir = os.path.join(self._find_root(), Project.DVC_DIR)
|
||||
self.config = Config(dvc_dir)
|
||||
|
||||
try:
|
||||
dvc_dir = os.path.join(Project._find_root(), Project.DVC_DIR)
|
||||
saved_exc = None
|
||||
except NotDvcProjectError as exc:
|
||||
dvc_dir = None
|
||||
saved_exc = exc
|
||||
|
||||
self.config = Config(dvc_dir, validate=False)
|
||||
if self.args.system:
|
||||
self.configobj = self.config._system_config
|
||||
elif self.args.glob:
|
||||
self.configobj = self.config._global_config
|
||||
elif self.args.local:
|
||||
if dvc_dir is None:
|
||||
raise saved_exc
|
||||
self.configobj = self.config._local_config
|
||||
else:
|
||||
if dvc_dir is None:
|
||||
raise saved_exc
|
||||
self.configobj = self.config._project_config
|
||||
|
||||
def run_cmd(self):
|
||||
|
||||
+19
-3
@@ -1,19 +1,35 @@
|
||||
from dvc.command.base import CmdBase
|
||||
|
||||
|
||||
class CmdDaemonUpdater(CmdBase):
|
||||
class CmdDaemonBase(CmdBase):
|
||||
def __init__(self, args):
|
||||
pass
|
||||
self.args = args
|
||||
self.config = None
|
||||
self._set_loglevel(args)
|
||||
|
||||
def run_cmd(self):
|
||||
return self.run()
|
||||
|
||||
|
||||
class CmdDaemonUpdater(CmdDaemonBase):
|
||||
def run(self):
|
||||
import os
|
||||
from dvc.project import Project
|
||||
from dvc.updater import Updater
|
||||
|
||||
root_dir = self._find_root()
|
||||
root_dir = Project._find_root()
|
||||
dvc_dir = os.path.join(root_dir, Project.DVC_DIR)
|
||||
updater = Updater(dvc_dir)
|
||||
updater.fetch(detach=False)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
class CmdDaemonAnalytics(CmdDaemonBase):
|
||||
def run(self):
|
||||
from dvc.analytics import Analytics
|
||||
|
||||
analytics = Analytics.load(self.args.target)
|
||||
analytics.send(detach=False)
|
||||
|
||||
return 0
|
||||
|
||||
+4
-1
@@ -9,7 +9,10 @@ class CmdInit(object):
|
||||
from dvc.project import Project, InitError
|
||||
|
||||
try:
|
||||
Project.init('.', no_scm=self.args.no_scm, force=self.args.force)
|
||||
self.project = Project.init('.',
|
||||
no_scm=self.args.no_scm,
|
||||
force=self.args.force)
|
||||
self.config = self.project.config
|
||||
except InitError as e:
|
||||
Logger.error('Failed to initiate dvc', e)
|
||||
return 1
|
||||
|
||||
+42
-19
@@ -68,6 +68,8 @@ class Config(object):
|
||||
SECTION_CORE_REMOTE = 'remote'
|
||||
SECTION_CORE_INTERACTIVE_SCHEMA = And(str, is_bool, Use(to_bool))
|
||||
SECTION_CORE_INTERACTIVE = 'interactive'
|
||||
SECTION_CORE_ANALYTICS = 'analytics'
|
||||
SECTION_CORE_ANALYTICS_SCHEMA = And(str, is_bool, Use(to_bool))
|
||||
|
||||
SECTION_CACHE = 'cache'
|
||||
SECTION_CACHE_DIR = 'dir'
|
||||
@@ -106,6 +108,8 @@ class Config(object):
|
||||
Optional(SECTION_CORE_REMOTE, default=''): And(str, Use(str.lower)),
|
||||
Optional(SECTION_CORE_INTERACTIVE,
|
||||
default=False): SECTION_CORE_INTERACTIVE_SCHEMA,
|
||||
Optional(SECTION_CORE_ANALYTICS,
|
||||
default=True): SECTION_CORE_ANALYTICS_SCHEMA,
|
||||
|
||||
# backward compatibility
|
||||
Optional(SECTION_CORE_CLOUD, default=''): SECTION_CORE_CLOUD_SCHEMA,
|
||||
@@ -190,28 +194,34 @@ class Config(object):
|
||||
Optional(SECTION_LOCAL, default={}): SECTION_LOCAL_SCHEMA,
|
||||
}
|
||||
|
||||
def __init__(self, dvc_dir):
|
||||
self.dvc_dir = os.path.abspath(os.path.realpath(dvc_dir))
|
||||
self.system_config_file = os.path.join(self.system_config_dir,
|
||||
def __init__(self, dvc_dir=None, validate=True):
|
||||
self.system_config_file = os.path.join(self.get_system_config_dir(),
|
||||
self.CONFIG)
|
||||
self.global_config_file = os.path.join(self.global_config_dir,
|
||||
self.global_config_file = os.path.join(self.get_global_config_dir(),
|
||||
self.CONFIG)
|
||||
self.config_file = os.path.join(dvc_dir, self.CONFIG)
|
||||
self.config_local_file = os.path.join(dvc_dir, self.CONFIG_LOCAL)
|
||||
|
||||
self._load()
|
||||
if dvc_dir is not None:
|
||||
self.dvc_dir = os.path.abspath(os.path.realpath(dvc_dir))
|
||||
self.config_file = os.path.join(dvc_dir, self.CONFIG)
|
||||
self.config_local_file = os.path.join(dvc_dir, self.CONFIG_LOCAL)
|
||||
else:
|
||||
self.dvc_dir = None
|
||||
self.config_file = None
|
||||
self.config_local_file = None
|
||||
|
||||
@property
|
||||
def global_config_dir(self):
|
||||
self.load(validate=validate)
|
||||
|
||||
@staticmethod
|
||||
def get_global_config_dir():
|
||||
from appdirs import user_config_dir
|
||||
return user_config_dir(appname=self.APPNAME,
|
||||
appauthor=self.APPAUTHOR)
|
||||
return user_config_dir(appname=Config.APPNAME,
|
||||
appauthor=Config.APPAUTHOR)
|
||||
|
||||
@property
|
||||
def system_config_dir(self):
|
||||
@staticmethod
|
||||
def get_system_config_dir():
|
||||
from appdirs import site_config_dir
|
||||
return site_config_dir(appname=self.APPNAME,
|
||||
appauthor=self.APPAUTHOR)
|
||||
return site_config_dir(appname=Config.APPNAME,
|
||||
appauthor=Config.APPAUTHOR)
|
||||
|
||||
@staticmethod
|
||||
def init(dvc_dir):
|
||||
@@ -222,11 +232,20 @@ class Config(object):
|
||||
def _load(self):
|
||||
self._system_config = configobj.ConfigObj(self.system_config_file)
|
||||
self._global_config = configobj.ConfigObj(self.global_config_file)
|
||||
self._project_config = configobj.ConfigObj(self.config_file)
|
||||
self._local_config = configobj.ConfigObj(self.config_local_file)
|
||||
|
||||
if self.config_file is not None:
|
||||
self._project_config = configobj.ConfigObj(self.config_file)
|
||||
else:
|
||||
self._project_config = configobj.ConfigObj()
|
||||
|
||||
if self.config_local_file is not None:
|
||||
self._local_config = configobj.ConfigObj(self.config_local_file)
|
||||
else:
|
||||
self._local_config = configobj.ConfigObj()
|
||||
|
||||
self._config = None
|
||||
|
||||
def load(self):
|
||||
def load(self, validate=True):
|
||||
self._load()
|
||||
try:
|
||||
self._config = configobj.ConfigObj(self.system_config_file)
|
||||
@@ -241,7 +260,8 @@ class Config(object):
|
||||
c = self._lower(c)
|
||||
self._config = self._merge(self._config, c)
|
||||
|
||||
self._config = Schema(self.SCHEMA).validate(self._config)
|
||||
if validate:
|
||||
self._config = Schema(self.SCHEMA).validate(self._config)
|
||||
|
||||
# NOTE: now converting back to ConfigObj
|
||||
self._config = configobj.ConfigObj(self._config,
|
||||
@@ -271,6 +291,9 @@ class Config(object):
|
||||
self._local_config]
|
||||
|
||||
for conf in clist:
|
||||
if conf.filename is None:
|
||||
continue
|
||||
|
||||
try:
|
||||
Logger.debug("Writing '{}'.".format(conf.filename))
|
||||
dname = os.path.dirname(os.path.abspath(conf.filename))
|
||||
|
||||
+2
-2
@@ -42,13 +42,13 @@ class Daemon(object): # pragma: no cover
|
||||
close_fds=True,
|
||||
shell=False)
|
||||
|
||||
def __call__(self, name):
|
||||
def __call__(self, args):
|
||||
from dvc.utils import is_binary
|
||||
|
||||
cmd = [sys.executable]
|
||||
if not is_binary():
|
||||
cmd += ['-m', 'dvc']
|
||||
cmd += ['daemon', name, '-q']
|
||||
cmd += ['daemon', '-q'] + args
|
||||
|
||||
Logger.debug("Trying to spawn '{}'".format(cmd))
|
||||
|
||||
|
||||
+19
-11
@@ -1,27 +1,35 @@
|
||||
from dvc.logger import Logger
|
||||
from dvc.cli import parse_args
|
||||
from dvc.command.base import CmdBase
|
||||
from dvc.analytics import Analytics
|
||||
from dvc.cli import DvcParserError
|
||||
from dvc.project import NotDvcProjectError
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
Logger.init()
|
||||
|
||||
args = parse_args(argv)
|
||||
|
||||
# Init loglevel early in case we'll run
|
||||
# into errors before setting it properly
|
||||
CmdBase._set_loglevel(args)
|
||||
|
||||
args = None
|
||||
cmd = None
|
||||
try:
|
||||
args = parse_args(argv)
|
||||
|
||||
# Init loglevel early in case we'll run
|
||||
# into errors before setting it properly
|
||||
CmdBase._set_loglevel(args)
|
||||
|
||||
cmd = args.func(args)
|
||||
except Exception as ex:
|
||||
Logger.error('Initialization error', ex)
|
||||
return 255
|
||||
|
||||
try:
|
||||
ret = cmd.run_cmd()
|
||||
except NotDvcProjectError as ex:
|
||||
Logger.error(str(ex))
|
||||
ret = 253
|
||||
except DvcParserError:
|
||||
ret = 254
|
||||
except Exception as ex:
|
||||
Logger.error('Unexpected error', ex)
|
||||
return 254
|
||||
ret = 255
|
||||
|
||||
Analytics().send(detach=True, cmd=cmd, args=args, ret=ret)
|
||||
|
||||
return ret
|
||||
|
||||
+30
-2
@@ -10,6 +10,12 @@ class InitError(DvcException):
|
||||
super(InitError, self).__init__(msg)
|
||||
|
||||
|
||||
class NotDvcProjectError(DvcException):
|
||||
def __init__(self, root):
|
||||
msg = "Not a dvc repository (checked up to mount point {})"
|
||||
super(NotDvcProjectError, self).__init__(msg.format(root))
|
||||
|
||||
|
||||
class ReproductionError(DvcException):
|
||||
def __init__(self, dvc_file_name, ex):
|
||||
self.path = dvc_file_name
|
||||
@@ -20,7 +26,7 @@ class ReproductionError(DvcException):
|
||||
class Project(object):
|
||||
DVC_DIR = '.dvc'
|
||||
|
||||
def __init__(self, root_dir):
|
||||
def __init__(self, root_dir=None):
|
||||
from dvc.logger import Logger
|
||||
from dvc.config import Config
|
||||
from dvc.state import State
|
||||
@@ -31,11 +37,12 @@ class Project(object):
|
||||
from dvc.updater import Updater
|
||||
from dvc.prompt import Prompt
|
||||
|
||||
root_dir = self._find_root(root_dir)
|
||||
|
||||
self.root_dir = os.path.abspath(os.path.realpath(root_dir))
|
||||
self.dvc_dir = os.path.join(self.root_dir, self.DVC_DIR)
|
||||
|
||||
self.config = Config(self.dvc_dir)
|
||||
self.config.load()
|
||||
|
||||
self.scm = SCM(self.root_dir, project=self)
|
||||
self.lock = Lock(self.dvc_dir)
|
||||
@@ -57,6 +64,27 @@ class Project(object):
|
||||
|
||||
self.updater.check()
|
||||
|
||||
@staticmethod
|
||||
def _find_root(root=None):
|
||||
if root is None:
|
||||
root = os.getcwd()
|
||||
else:
|
||||
root = os.path.abspath(os.path.realpath(root))
|
||||
|
||||
while True:
|
||||
dvc_dir = os.path.join(root, Project.DVC_DIR)
|
||||
if os.path.isdir(dvc_dir):
|
||||
return root
|
||||
if os.path.ismount(root):
|
||||
break
|
||||
root = os.path.dirname(root)
|
||||
raise NotDvcProjectError(root)
|
||||
|
||||
@staticmethod
|
||||
def _find_dvc_dir(root=None):
|
||||
root_dir = Project._find_root(root)
|
||||
return os.path.join(root_dir, Project.DVC_DIR)
|
||||
|
||||
def _remind_to_git_add(self):
|
||||
if len(self._files_to_git_add) == 0:
|
||||
return
|
||||
|
||||
+1
-2
@@ -68,8 +68,7 @@ class Updater(object): # pragma: no cover
|
||||
from dvc.daemon import Daemon
|
||||
|
||||
if detach:
|
||||
d = Daemon()
|
||||
d('updater')
|
||||
Daemon()(['updater'])
|
||||
return
|
||||
|
||||
self._with_lock(self._get_latest_version, 'fetching')
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import os
|
||||
import mock
|
||||
import requests
|
||||
import tempfile
|
||||
|
||||
from dvc.main import main
|
||||
from dvc.analytics import Analytics
|
||||
from tests.basic_env import TestDvc, TestGit, TestDir
|
||||
|
||||
|
||||
def _clean_getenv(key, default=None):
|
||||
"""
|
||||
Remove env vars that affect dvc behavior in tests
|
||||
"""
|
||||
if key in ['DVC_TEST', 'CI']:
|
||||
return None
|
||||
return os.environ.get(key, default)
|
||||
|
||||
|
||||
class TestAnalytics(TestDir):
|
||||
def test(self):
|
||||
a = Analytics()
|
||||
a._collect()
|
||||
self.assertTrue(isinstance(a.info, dict))
|
||||
self.assertNotEqual(a.info, {})
|
||||
self.assertTrue(a.PARAM_USER_ID in a.info.keys())
|
||||
self.assertTrue(a.PARAM_SYSTEM_INFO in a.info.keys())
|
||||
self.assertNotEqual(a.info[a.PARAM_SYSTEM_INFO], {})
|
||||
|
||||
@mock.patch.object(os, 'getenv', new=_clean_getenv)
|
||||
@mock.patch('requests.post')
|
||||
def test_send(self, mockpost):
|
||||
ret = main(['daemon', 'analytics', Analytics().dump(), '-v'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
self.assertTrue(mockpost.called)
|
||||
|
||||
@mock.patch.object(os, 'getenv', new=_clean_getenv)
|
||||
@mock.patch.object(requests, 'post',
|
||||
side_effect=requests.exceptions.RequestException())
|
||||
def test_send_failed(self, mockpost):
|
||||
ret = main(['daemon', 'analytics', Analytics().dump(), '-v'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
self.assertTrue(mockpost.called)
|
||||
|
||||
|
||||
class TestAnalyticsGit(TestAnalytics, TestGit):
|
||||
pass
|
||||
|
||||
|
||||
class TestAnalyticsDvc(TestAnalytics, TestDvc):
|
||||
@mock.patch('requests.post')
|
||||
def test_send_disabled(self, mockpost):
|
||||
ret = main(['config', 'core.analytics', 'false'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
with mock.patch.object(os, 'getenv', new=_clean_getenv):
|
||||
ret = main(['daemon', 'analytics', Analytics().dump(), '-v'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
self.assertFalse(mockpost.called)
|
||||
Reference in New Issue
Block a user