From a9e0bdb3fec082eeec79b2934e3a8acced1344dc Mon Sep 17 00:00:00 2001 From: Alan Hamlett Date: Wed, 10 Jul 2013 00:14:44 -0700 Subject: [PATCH] updated wakatime.py package. using new usage logic for better actions accuracy. --- README.md | 2 +- packages/wakatime/log.py | 8 +- packages/wakatime/project.py | 2 +- packages/wakatime/projects/base.py | 39 +++++---- packages/wakatime/projects/git.py | 52 ++++++++--- packages/wakatime/projects/mercurial.py | 10 ++- packages/wakatime/projects/subversion.py | 39 ++++++++- packages/wakatime/wakatime.py | 6 +- sublime-wakatime.py | 105 +++++++++++++++-------- 9 files changed, 180 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index d1185ea..2a895d3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -sublime-wakatime 0.1.0 +sublime-wakatime ================ automatic time tracking for Sublime Text 2 & 3 using https://wakati.me diff --git a/packages/wakatime/log.py b/packages/wakatime/log.py index 27adbc2..4e108bf 100644 --- a/packages/wakatime/log.py +++ b/packages/wakatime/log.py @@ -27,15 +27,13 @@ class CustomEncoder(json.JSONEncoder): class JsonFormatter(logging.Formatter): - def __init__(self, timestamp, endtime, isWrite, targetFile, version, - plugin, datefmt=None): + def setup(self, timestamp, endtime, isWrite, targetFile, version, plugin): self.timestamp = timestamp self.endtime = endtime self.isWrite = isWrite self.targetFile = targetFile self.version = version self.plugin = plugin - super(JsonFormatter, self).__init__(datefmt=datefmt) def format(self, record): data = OrderedDict([ @@ -66,14 +64,14 @@ def setup_logging(args, version): if not logfile: logfile = '~/.wakatime.log' handler = logging.FileHandler(os.path.expanduser(logfile)) - formatter = JsonFormatter( + formatter = JsonFormatter(datefmt='%Y-%m-%dT%H:%M:%SZ') + formatter.setup( timestamp=args.timestamp, endtime=args.endtime, isWrite=args.isWrite, targetFile=args.targetFile, version=version, plugin=args.plugin, - datefmt='%Y-%m-%dT%H:%M:%SZ', ) handler.setFormatter(formatter) logger = logging.getLogger() diff --git a/packages/wakatime/project.py b/packages/wakatime/project.py index 0dd23cd..4d33635 100644 --- a/packages/wakatime/project.py +++ b/packages/wakatime/project.py @@ -30,6 +30,6 @@ PLUGINS = [ def find_project(path): for plugin in PLUGINS: project = plugin(path) - if project.config: + if project.process(): return project return BaseProject(path) diff --git a/packages/wakatime/projects/base.py b/packages/wakatime/projects/base.py index bf2f6ef..728a7f8 100644 --- a/packages/wakatime/projects/base.py +++ b/packages/wakatime/projects/base.py @@ -16,32 +16,39 @@ import os log = logging.getLogger(__name__) -class BaseProject(): +class BaseProject(object): + """ Parent project class only + used when no valid project can + be found for the current path. + """ def __init__(self, path): self.path = path - self.config = self.findConfig(path) - - def name(self): - base = self.base() - if base: - return os.path.basename(base) - return None def type(self): + """ Returns None if this is the base class. + Returns the type of project if this is a + valid project. + """ type = self.__class__.__name__.lower() if type == 'baseproject': type = None return type - def base(self): - if self.config: - return os.path.dirname(self.config) + def process(self): + """ Processes self.path into a project and + returns True if project is valid, otherwise + returns False. + """ + return False + + def name(self): + """ Returns the project's name. + """ return None def tags(self): - tags = [] - return tags - - def findConfig(self, path): - return '' + """ Returns an array of tag strings for the + path and/or project. + """ + return [] diff --git a/packages/wakatime/projects/git.py b/packages/wakatime/projects/git.py index b1c6519..00cf1f4 100644 --- a/packages/wakatime/projects/git.py +++ b/packages/wakatime/projects/git.py @@ -11,13 +11,10 @@ import logging import os +from subprocess import Popen, PIPE from .base import BaseProject - -try: - from collections import OrderedDict -except ImportError: - from ..packages.ordereddict import OrderedDict +from ..packages.ordereddict import OrderedDict log = logging.getLogger(__name__) @@ -25,21 +22,54 @@ log = logging.getLogger(__name__) class Git(BaseProject): - def base(self): + def process(self): + self.config = self._find_config(self.path) if self.config: - return os.path.dirname(os.path.dirname(self.config)) + return True + return False + + def name(self): + base = self._project_base() + if base: + return os.path.basename(base) return None def tags(self): tags = [] if self.config: - sections = self.parseConfig() + base = self._project_base() + if base: + tags.append(base) + sections = self._parse_config() for section in sections: if section.split(' ', 1)[0] == 'remote' and 'url' in sections[section]: tags.append(sections[section]['url']) + branch = self._current_branch() + if branch is not None: + tags.append(branch) return tags - def findConfig(self, path): + def _project_base(self): + if self.config: + return os.path.dirname(os.path.dirname(self.config)) + return None + + def _current_branch(self): + stdout = None + try: + stdout, stderr = Popen([ + 'git', 'branch', '--no-color', '--list' + ], stdout=PIPE, cwd=self._project_base()).communicate() + except OSError: + pass + if stdout: + for line in stdout.splitlines(): + line = line.split(' ', 1) + if line[0] == '*': + return line[1] + return None + + def _find_config(self, path): path = os.path.realpath(path) if os.path.isfile(path): path = os.path.split(path)[0] @@ -48,9 +78,9 @@ class Git(BaseProject): split_path = os.path.split(path) if split_path[1] == '': return None - return self.findConfig(split_path[0]) + return self._find_config(split_path[0]) - def parseConfig(self): + def _parse_config(self): sections = {} try: f = open(self.config, 'r') diff --git a/packages/wakatime/projects/mercurial.py b/packages/wakatime/projects/mercurial.py index 6be3030..7d9b2ae 100644 --- a/packages/wakatime/projects/mercurial.py +++ b/packages/wakatime/projects/mercurial.py @@ -20,9 +20,11 @@ log = logging.getLogger(__name__) class Mercurial(BaseProject): - def base(self): - return super(Mercurial, self).base() + def process(self): + return False + + def name(self): + return None def tags(self): - tags = [] - return tags + return [] diff --git a/packages/wakatime/projects/subversion.py b/packages/wakatime/projects/subversion.py index a876885..35d834d 100644 --- a/packages/wakatime/projects/subversion.py +++ b/packages/wakatime/projects/subversion.py @@ -11,8 +11,10 @@ import logging import os +from subprocess import Popen, PIPE from .base import BaseProject +from ..packages.ordereddict import OrderedDict log = logging.getLogger(__name__) @@ -20,9 +22,42 @@ log = logging.getLogger(__name__) class Subversion(BaseProject): - def base(self): - return super(Subversion, self).base() + def process(self): + self.info = self._get_info() + if 'Repository Root' in self.info: + return True + return False + + def name(self): + return self.info['Repository Root'].split('/')[-1] + + def _get_info(self): + info = OrderedDict() + stdout = None + try: + stdout, stderr = Popen([ + 'svn', 'info', os.path.realpath(self.path) + ], stdout=PIPE).communicate() + except OSError: + pass + else: + if stdout: + interesting = [ + 'Repository Root', + 'Repository UUID', + 'URL', + ] + for line in stdout.splitlines(): + line = line.split(': ', 1) + if line[0] in interesting: + info[line[0]] = line[1] + return info def tags(self): tags = [] + for key in self.info: + if key == 'Repository UUID': + tags.append(self.info[key]) + if key == 'URL': + tags.append(os.path.dirname(self.info[key])) return tags diff --git a/packages/wakatime/wakatime.py b/packages/wakatime/wakatime.py index 320ba8f..9a3d110 100644 --- a/packages/wakatime/wakatime.py +++ b/packages/wakatime/wakatime.py @@ -12,7 +12,7 @@ from __future__ import print_function __title__ = 'wakatime' -__version__ = '0.1.1' +__version__ = '0.1.2' __author__ = 'Alan Hamlett' __license__ = 'BSD' __copyright__ = 'Copyright 2013 Alan Hamlett' @@ -97,10 +97,6 @@ def parseArguments(): args.key = default_key else: parser.error('Missing api key') - if args.endtime and args.endtime < args.timestamp: - tmp = args.timestamp - args.timestamp = args.endtime - args.endtime = tmp return args diff --git a/sublime-wakatime.py b/sublime-wakatime.py index ab2d5b8..991e924 100644 --- a/sublime-wakatime.py +++ b/sublime-wakatime.py @@ -16,15 +16,16 @@ import sublime import sublime_plugin -# Prompt user if no activity for this many minutes -AWAY_MINUTES = 5 - # globals +AWAY_MINUTES = 10 +ACTION_FREQUENCY = 5 PLUGIN_DIR = dirname(realpath(__file__)) API_CLIENT = '%s/packages/wakatime/wakatime.py' % PLUGIN_DIR LAST_ACTION = 0 +LAST_USAGE = 0 LAST_FILE = None + # To be backwards compatible, rename config file if isfile(expanduser('~/.wakatime')): call([ @@ -35,7 +36,7 @@ if isfile(expanduser('~/.wakatime')): def api(targetFile, timestamp, isWrite=False, endtime=None): - global LAST_ACTION, LAST_FILE + global LAST_ACTION, LAST_USAGE, LAST_FILE if not targetFile: targetFile = LAST_FILE if targetFile: @@ -43,6 +44,7 @@ def api(targetFile, timestamp, isWrite=False, endtime=None): '--file', targetFile, '--time', str('%f' % timestamp), '--plugin', 'sublime-wakatime/%s' % __version__, + #'--verbose', ] if isWrite: cmd.append('--write') @@ -53,53 +55,80 @@ def api(targetFile, timestamp, isWrite=False, endtime=None): if endtime and endtime > LAST_ACTION: LAST_ACTION = endtime LAST_FILE = targetFile + LAST_USAGE = LAST_ACTION def away(now): - if LAST_ACTION == 0: - return False - duration = now - LAST_ACTION - if duration > AWAY_MINUTES * 60: - duration = int(duration) - units = 'seconds' - if duration > 59: - duration = int(duration / 60.0) - units = 'minutes' - if duration > 59: - duration = int(duration / 60.0) - units = 'hours' - if duration > 24: - duration = int(duration / 24.0) - units = 'days' - return sublime\ - .ok_cancel_dialog("You were away %d %s. Add time to current file?"\ - % (duration, units), 'Yes, log this time') + duration = now - LAST_USAGE + units = 'seconds' + if duration > 59: + duration = int(duration / 60) + units = 'minutes' + if duration > 59: + duration = int(duration / 60) + units = 'hours' + if duration > 24: + duration = int(duration / 24) + units = 'days' + return sublime\ + .ok_cancel_dialog("You were away %d %s. Add time to current file?"\ + % (duration, units), 'Yes, log this time') def enough_time_passed(now): - return (now - LAST_ACTION >= 299) + if now - LAST_ACTION > ACTION_FREQUENCY * 60: + return True + return False + + +def should_prompt_user(now): + if not LAST_USAGE: + return False + duration = now - LAST_USAGE + if duration > AWAY_MINUTES * 60: + return True + return False + + +def handle_write_action(view): + now = time.time() + targetFile = view.file_name() + if enough_time_passed(now) or targetFile != LAST_FILE: + if should_prompt_user(now): + if away(now): + api(targetFile, now, endtime=LAST_ACTION, isWrite=True) + else: + api(targetFile, now, isWrite=True) + else: + api(targetFile, now, endtime=LAST_ACTION, isWrite=True) + else: + api(targetFile, now, isWrite=True) + + +def handle_normal_action(view): + global LAST_USAGE + now = time.time() + targetFile = view.file_name() + if enough_time_passed(now) or targetFile != LAST_FILE: + if should_prompt_user(now): + if away(now): + api(targetFile, now, endtime=LAST_ACTION) + else: + api(targetFile, now) + else: + api(targetFile, now, endtime=LAST_ACTION) + else: + LAST_USAGE = now class WakatimeListener(sublime_plugin.EventListener): def on_post_save(self, view): - api(view.file_name(), time.time(), isWrite=True) + handle_write_action(view) def on_activated(self, view): - now = time.time() - targetFile = view.file_name() - if enough_time_passed(now) or targetFile != LAST_FILE: - if away(now): - api(targetFile, LAST_ACTION, endtime=now) - else: - api(targetFile, now) + handle_normal_action(view) def on_selection_modified(self, view): - now = time.time() - targetFile = view.file_name() - if enough_time_passed(now) or targetFile != LAST_FILE: - if away(now): - api(targetFile, LAST_ACTION, endtime=now) - else: - api(targetFile, now) + handle_normal_action(view)