updated wakatime.py package. using new usage logic for better actions accuracy.

This commit is contained in:
Alan Hamlett 2013-07-10 00:14:44 -07:00
parent eb1e3f72db
commit a9e0bdb3fe
9 changed files with 180 additions and 83 deletions

View file

@ -1,4 +1,4 @@
sublime-wakatime 0.1.0 sublime-wakatime
================ ================
automatic time tracking for Sublime Text 2 & 3 using https://wakati.me automatic time tracking for Sublime Text 2 & 3 using https://wakati.me

View file

@ -27,15 +27,13 @@ class CustomEncoder(json.JSONEncoder):
class JsonFormatter(logging.Formatter): class JsonFormatter(logging.Formatter):
def __init__(self, timestamp, endtime, isWrite, targetFile, version, def setup(self, timestamp, endtime, isWrite, targetFile, version, plugin):
plugin, datefmt=None):
self.timestamp = timestamp self.timestamp = timestamp
self.endtime = endtime self.endtime = endtime
self.isWrite = isWrite self.isWrite = isWrite
self.targetFile = targetFile self.targetFile = targetFile
self.version = version self.version = version
self.plugin = plugin self.plugin = plugin
super(JsonFormatter, self).__init__(datefmt=datefmt)
def format(self, record): def format(self, record):
data = OrderedDict([ data = OrderedDict([
@ -66,14 +64,14 @@ def setup_logging(args, version):
if not logfile: if not logfile:
logfile = '~/.wakatime.log' logfile = '~/.wakatime.log'
handler = logging.FileHandler(os.path.expanduser(logfile)) handler = logging.FileHandler(os.path.expanduser(logfile))
formatter = JsonFormatter( formatter = JsonFormatter(datefmt='%Y-%m-%dT%H:%M:%SZ')
formatter.setup(
timestamp=args.timestamp, timestamp=args.timestamp,
endtime=args.endtime, endtime=args.endtime,
isWrite=args.isWrite, isWrite=args.isWrite,
targetFile=args.targetFile, targetFile=args.targetFile,
version=version, version=version,
plugin=args.plugin, plugin=args.plugin,
datefmt='%Y-%m-%dT%H:%M:%SZ',
) )
handler.setFormatter(formatter) handler.setFormatter(formatter)
logger = logging.getLogger() logger = logging.getLogger()

View file

@ -30,6 +30,6 @@ PLUGINS = [
def find_project(path): def find_project(path):
for plugin in PLUGINS: for plugin in PLUGINS:
project = plugin(path) project = plugin(path)
if project.config: if project.process():
return project return project
return BaseProject(path) return BaseProject(path)

View file

@ -16,32 +16,39 @@ import os
log = logging.getLogger(__name__) 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): def __init__(self, path):
self.path = 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): 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() type = self.__class__.__name__.lower()
if type == 'baseproject': if type == 'baseproject':
type = None type = None
return type return type
def base(self): def process(self):
if self.config: """ Processes self.path into a project and
return os.path.dirname(self.config) returns True if project is valid, otherwise
returns False.
"""
return False
def name(self):
""" Returns the project's name.
"""
return None return None
def tags(self): def tags(self):
tags = [] """ Returns an array of tag strings for the
return tags path and/or project.
"""
def findConfig(self, path): return []
return ''

View file

@ -11,13 +11,10 @@
import logging import logging
import os import os
from subprocess import Popen, PIPE
from .base import BaseProject from .base import BaseProject
from ..packages.ordereddict import OrderedDict
try:
from collections import OrderedDict
except ImportError:
from ..packages.ordereddict import OrderedDict
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -25,21 +22,54 @@ log = logging.getLogger(__name__)
class Git(BaseProject): class Git(BaseProject):
def base(self): def process(self):
self.config = self._find_config(self.path)
if self.config: 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 return None
def tags(self): def tags(self):
tags = [] tags = []
if self.config: if self.config:
sections = self.parseConfig() base = self._project_base()
if base:
tags.append(base)
sections = self._parse_config()
for section in sections: for section in sections:
if section.split(' ', 1)[0] == 'remote' and 'url' in sections[section]: if section.split(' ', 1)[0] == 'remote' and 'url' in sections[section]:
tags.append(sections[section]['url']) tags.append(sections[section]['url'])
branch = self._current_branch()
if branch is not None:
tags.append(branch)
return tags 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) path = os.path.realpath(path)
if os.path.isfile(path): if os.path.isfile(path):
path = os.path.split(path)[0] path = os.path.split(path)[0]
@ -48,9 +78,9 @@ class Git(BaseProject):
split_path = os.path.split(path) split_path = os.path.split(path)
if split_path[1] == '': if split_path[1] == '':
return None return None
return self.findConfig(split_path[0]) return self._find_config(split_path[0])
def parseConfig(self): def _parse_config(self):
sections = {} sections = {}
try: try:
f = open(self.config, 'r') f = open(self.config, 'r')

View file

@ -20,9 +20,11 @@ log = logging.getLogger(__name__)
class Mercurial(BaseProject): class Mercurial(BaseProject):
def base(self): def process(self):
return super(Mercurial, self).base() return False
def name(self):
return None
def tags(self): def tags(self):
tags = [] return []
return tags

View file

@ -11,8 +11,10 @@
import logging import logging
import os import os
from subprocess import Popen, PIPE
from .base import BaseProject from .base import BaseProject
from ..packages.ordereddict import OrderedDict
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -20,9 +22,42 @@ log = logging.getLogger(__name__)
class Subversion(BaseProject): class Subversion(BaseProject):
def base(self): def process(self):
return super(Subversion, self).base() 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): def tags(self):
tags = [] 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 return tags

View file

@ -12,7 +12,7 @@
from __future__ import print_function from __future__ import print_function
__title__ = 'wakatime' __title__ = 'wakatime'
__version__ = '0.1.1' __version__ = '0.1.2'
__author__ = 'Alan Hamlett' __author__ = 'Alan Hamlett'
__license__ = 'BSD' __license__ = 'BSD'
__copyright__ = 'Copyright 2013 Alan Hamlett' __copyright__ = 'Copyright 2013 Alan Hamlett'
@ -97,10 +97,6 @@ def parseArguments():
args.key = default_key args.key = default_key
else: else:
parser.error('Missing api key') 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 return args

View file

@ -16,15 +16,16 @@ import sublime
import sublime_plugin import sublime_plugin
# Prompt user if no activity for this many minutes
AWAY_MINUTES = 5
# globals # globals
AWAY_MINUTES = 10
ACTION_FREQUENCY = 5
PLUGIN_DIR = dirname(realpath(__file__)) PLUGIN_DIR = dirname(realpath(__file__))
API_CLIENT = '%s/packages/wakatime/wakatime.py' % PLUGIN_DIR API_CLIENT = '%s/packages/wakatime/wakatime.py' % PLUGIN_DIR
LAST_ACTION = 0 LAST_ACTION = 0
LAST_USAGE = 0
LAST_FILE = None LAST_FILE = None
# To be backwards compatible, rename config file # To be backwards compatible, rename config file
if isfile(expanduser('~/.wakatime')): if isfile(expanduser('~/.wakatime')):
call([ call([
@ -35,7 +36,7 @@ if isfile(expanduser('~/.wakatime')):
def api(targetFile, timestamp, isWrite=False, endtime=None): def api(targetFile, timestamp, isWrite=False, endtime=None):
global LAST_ACTION, LAST_FILE global LAST_ACTION, LAST_USAGE, LAST_FILE
if not targetFile: if not targetFile:
targetFile = LAST_FILE targetFile = LAST_FILE
if targetFile: if targetFile:
@ -43,6 +44,7 @@ def api(targetFile, timestamp, isWrite=False, endtime=None):
'--file', targetFile, '--file', targetFile,
'--time', str('%f' % timestamp), '--time', str('%f' % timestamp),
'--plugin', 'sublime-wakatime/%s' % __version__, '--plugin', 'sublime-wakatime/%s' % __version__,
#'--verbose',
] ]
if isWrite: if isWrite:
cmd.append('--write') cmd.append('--write')
@ -53,53 +55,80 @@ def api(targetFile, timestamp, isWrite=False, endtime=None):
if endtime and endtime > LAST_ACTION: if endtime and endtime > LAST_ACTION:
LAST_ACTION = endtime LAST_ACTION = endtime
LAST_FILE = targetFile LAST_FILE = targetFile
LAST_USAGE = LAST_ACTION
def away(now): def away(now):
if LAST_ACTION == 0: duration = now - LAST_USAGE
return False units = 'seconds'
duration = now - LAST_ACTION if duration > 59:
if duration > AWAY_MINUTES * 60: duration = int(duration / 60)
duration = int(duration) units = 'minutes'
units = 'seconds' if duration > 59:
if duration > 59: duration = int(duration / 60)
duration = int(duration / 60.0) units = 'hours'
units = 'minutes' if duration > 24:
if duration > 59: duration = int(duration / 24)
duration = int(duration / 60.0) units = 'days'
units = 'hours' return sublime\
if duration > 24: .ok_cancel_dialog("You were away %d %s. Add time to current file?"\
duration = int(duration / 24.0) % (duration, units), 'Yes, log this time')
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): 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): class WakatimeListener(sublime_plugin.EventListener):
def on_post_save(self, view): def on_post_save(self, view):
api(view.file_name(), time.time(), isWrite=True) handle_write_action(view)
def on_activated(self, view): def on_activated(self, view):
now = time.time() handle_normal_action(view)
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)
def on_selection_modified(self, view): def on_selection_modified(self, view):
now = time.time() handle_normal_action(view)
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)