updated wakatime.py package. using new usage logic for better actions accuracy.
This commit is contained in:
parent
eb1e3f72db
commit
a9e0bdb3fe
9 changed files with 180 additions and 83 deletions
|
@ -1,4 +1,4 @@
|
|||
sublime-wakatime 0.1.0
|
||||
sublime-wakatime
|
||||
================
|
||||
|
||||
automatic time tracking for Sublime Text 2 & 3 using https://wakati.me
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 []
|
||||
|
|
|
@ -11,12 +11,9 @@
|
|||
|
||||
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
|
||||
|
||||
|
||||
|
@ -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')
|
||||
|
|
|
@ -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 []
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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,23 +55,20 @@ 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)
|
||||
duration = now - LAST_USAGE
|
||||
units = 'seconds'
|
||||
if duration > 59:
|
||||
duration = int(duration / 60.0)
|
||||
duration = int(duration / 60)
|
||||
units = 'minutes'
|
||||
if duration > 59:
|
||||
duration = int(duration / 60.0)
|
||||
duration = int(duration / 60)
|
||||
units = 'hours'
|
||||
if duration > 24:
|
||||
duration = int(duration / 24.0)
|
||||
duration = int(duration / 24)
|
||||
units = 'days'
|
||||
return sublime\
|
||||
.ok_cancel_dialog("You were away %d %s. Add time to current file?"\
|
||||
|
@ -77,29 +76,59 @@ def away(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):
|
||||
|
||||
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)
|
||||
|
||||
|
|
Loading…
Reference in a new issue