upgrade common wakatime package to v0.5.0

This commit is contained in:
Alan Hamlett 2013-12-13 15:35:49 +01:00
parent 805e2fe222
commit d9cc911595
12 changed files with 250 additions and 116 deletions

14
packages/wakatime/AUTHORS Normal file
View file

@ -0,0 +1,14 @@
WakaTime is written and maintained by Alan Hamlett and
various contributors:
Development Lead
----------------
- Alan Hamlett <alan.hamlett@gmail.com>
Patches and Suggestions
-----------------------
- 3onyc <3onyc@x3tech.com>

View file

@ -3,6 +3,13 @@ History
------- -------
0.5.0 (2013-12-13)
++++++++++++++++++
- Convert ~/.wakatime.conf to ~/.wakatime.cfg and use configparser format
- new [projectmap] section in cfg file for naming projects based on folders
0.4.10 (2013-11-13) 0.4.10 (2013-11-13)
+++++++++++++++++++ +++++++++++++++++++

View file

@ -1,9 +1,10 @@
WakaTime WakaTime
======== ========
Automatic time tracking for your text editor. This is the command line Automatic time tracking for your text editor. This is the common interface
event appender for the WakaTime api. You shouldn't need to directly for the WakaTime api. You shouldn't need to directly use this package.
use this outside of a text editor plugin.
Go to http://wakatime.com to install the plugin for your text editor.
Installation Installation

View file

@ -3,7 +3,7 @@
wakatime-cli wakatime-cli
~~~~~~~~~~~~ ~~~~~~~~~~~~
Action event appender for Wakati.Me, auto time tracking for text editors. Command-line entry point.
:copyright: (c) 2013 Alan Hamlett. :copyright: (c) 2013 Alan Hamlett.
:license: BSD, see LICENSE for more details. :license: BSD, see LICENSE for more details.

View file

@ -3,7 +3,9 @@
wakatime wakatime
~~~~~~~~ ~~~~~~~~
Action event appender for Wakati.Me, auto time tracking for text editors. Common interface to WakaTime.com for most text editor plugins.
WakaTime.com is fully automatic time tracking for text editors.
More info at http://wakatime.com
:copyright: (c) 2013 Alan Hamlett. :copyright: (c) 2013 Alan Hamlett.
:license: BSD, see LICENSE for more details. :license: BSD, see LICENSE for more details.
@ -12,7 +14,7 @@
from __future__ import print_function from __future__ import print_function
__title__ = 'wakatime' __title__ = 'wakatime'
__version__ = '0.4.10' __version__ = '0.5.0'
__author__ = 'Alan Hamlett' __author__ = 'Alan Hamlett'
__license__ = 'BSD' __license__ = 'BSD'
__copyright__ = 'Copyright 2013 Alan Hamlett' __copyright__ = 'Copyright 2013 Alan Hamlett'
@ -26,6 +28,15 @@ import re
import sys import sys
import time import time
import traceback import traceback
try:
import ConfigParser as configparser
except ImportError:
import configparser
try:
from urllib2 import HTTPError, Request, urlopen
except ImportError:
from urllib.error import HTTPError
from urllib.request import Request, urlopen
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'packages')) sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'packages'))
@ -35,11 +46,6 @@ from .stats import get_file_stats
from .packages import argparse from .packages import argparse
from .packages import simplejson as json from .packages import simplejson as json
from .packages import tzlocal from .packages import tzlocal
try:
from urllib2 import HTTPError, Request, urlopen
except ImportError:
from urllib.error import HTTPError
from urllib.request import Request, urlopen
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -52,46 +58,82 @@ class FileAction(argparse.Action):
setattr(namespace, self.dest, values) setattr(namespace, self.dest, values)
def parseConfigFile(configFile): def upgradeConfigFile(configFile):
if not configFile: """For backwards-compatibility, upgrade the existing config file
configFile = os.path.join(os.path.expanduser('~'), '.wakatime.conf') to work with configparser and rename from .wakatime.conf to .wakatime.cfg.
"""
# define default config values if os.path.isfile(configFile):
configs = { # if upgraded cfg file already exists, don't overwrite it
'api_key': None, return
'ignore': [],
'verbose': False,
}
if not os.path.isfile(configFile):
return configs
oldConfig = os.path.join(os.path.expanduser('~'), '.wakatime.conf')
try: try:
with open(configFile) as fh: configs = {
for line in fh: 'ignore': [],
}
with open(oldConfig) as fh:
for line in fh.readlines():
line = line.split('=', 1) line = line.split('=', 1)
if len(line) == 2 and line[0].strip() and line[1].strip(): if len(line) == 2 and line[0].strip() and line[1].strip():
line[0] = line[0].strip() if line[0].strip() == 'ignore':
line[1] = line[1].strip() configs['ignore'].append(line[1].strip())
if line[0] in configs:
if isinstance(configs[line[0]], list):
configs[line[0]].append(line[1])
elif isinstance(configs[line[0]], bool):
configs[line[0]] = True if line[1].lower() == 'true' else False
else:
configs[line[0]] = line[1]
else: else:
configs[line[0]] = line[1] configs[line[0].strip()] = line[1].strip()
with open(configFile, 'w') as fh:
fh.write("[settings]\n")
for name, value in configs.items():
if isinstance(value, list):
fh.write("%s=\n" % name)
for item in value:
fh.write(" %s\n" % item)
else:
fh.write("%s = %s\n" % (name, value))
os.remove(oldConfig)
except IOError: except IOError:
print('Error: Could not read from config file ~/.wakatime.conf') pass
def parseConfigFile(configFile):
"""Returns a configparser.SafeConfigParser instance with configs
read from the config file. Default location of the config file is
at ~/.wakatime.cfg.
"""
if not configFile:
configFile = os.path.join(os.path.expanduser('~'), '.wakatime.cfg')
upgradeConfigFile(configFile)
configs = configparser.SafeConfigParser()
try:
with open(configFile) as fh:
try:
configs.readfp(fh)
except configparser.Error:
print(traceback.format_exc())
return None
except IOError:
if not os.path.isfile(configFile):
print('Error: Could not read from config file ~/.wakatime.conf')
return configs return configs
def parseArguments(argv): def parseArguments(argv):
"""Parse command line arguments and configs from ~/.wakatime.cfg.
Command line arguments take precedence over config file settings.
Returns instances of ArgumentParser and SafeConfigParser.
"""
try: try:
sys.argv sys.argv
except AttributeError: except AttributeError:
sys.argv = argv sys.argv = argv
# define supported command line arguments
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='Wakati.Me event api appender') description='Wakati.Me event api appender')
parser.add_argument('--file', dest='targetFile', metavar='file', parser.add_argument('--file', dest='targetFile', metavar='file',
@ -119,27 +161,45 @@ def parseArguments(argv):
parser.add_argument('--verbose', dest='verbose', action='store_true', parser.add_argument('--verbose', dest='verbose', action='store_true',
help='turns on debug messages in log file') help='turns on debug messages in log file')
parser.add_argument('--version', action='version', version=__version__) parser.add_argument('--version', action='version', version=__version__)
# parse command line arguments
args = parser.parse_args(args=argv[1:]) args = parser.parse_args(args=argv[1:])
# use current unix epoch timestamp by default
if not args.timestamp: if not args.timestamp:
args.timestamp = time.time() args.timestamp = time.time()
# set arguments from config file # parse ~/.wakatime.cfg file
configs = parseConfigFile(args.config) configs = parseConfigFile(args.config)
if configs is None:
return args, configs
# update args from configs
if not args.key: if not args.key:
default_key = configs.get('api_key') default_key = None
if configs.has_option('settings', 'api_key'):
default_key = configs.get('settings', 'api_key')
if default_key: if default_key:
args.key = default_key args.key = default_key
else: else:
parser.error('Missing api key') parser.error('Missing api key')
for pattern in configs.get('ignore', []): if not args.ignore:
if not args.ignore: args.ignore = []
args.ignore = [] if configs.has_option('settings', 'ignore'):
args.ignore.append(pattern) try:
if not args.verbose and 'verbose' in configs: for pattern in configs.get('settings', 'ignore').split("\n"):
args.verbose = configs['verbose'] if pattern.strip() != '':
if not args.logfile and 'logfile' in configs: args.ignore.append(pattern)
args.logfile = configs['logfile'] except TypeError:
return args pass
if not args.verbose and configs.has_option('settings', 'verbose'):
args.verbose = configs.getboolean('settings', 'verbose')
if not args.verbose and configs.has_option('settings', 'debug'):
args.verbose = configs.getboolean('settings', 'debug')
if not args.logfile and configs.has_option('settings', 'logfile'):
args.logfile = configs.get('settings', 'logfile')
return args, configs
def should_ignore(fileName, patterns): def should_ignore(fileName, patterns):
@ -233,28 +293,39 @@ def send_action(project=None, branch=None, stats={}, key=None, targetFile=None,
def main(argv=None): def main(argv=None):
if not argv: if not argv:
argv = sys.argv argv = sys.argv
args = parseArguments(argv)
args, configs = parseArguments(argv)
if configs is None:
return 103 # config file parsing error
setup_logging(args, __version__) setup_logging(args, __version__)
ignore = should_ignore(args.targetFile, args.ignore) ignore = should_ignore(args.targetFile, args.ignore)
if ignore is not False: if ignore is not False:
log.debug('File ignored because matches pattern: %s' % ignore) log.debug('File ignored because matches pattern: %s' % ignore)
return 0 return 0
if os.path.isfile(args.targetFile): if os.path.isfile(args.targetFile):
stats = get_file_stats(args.targetFile)
project = find_project(args.targetFile, configs=configs)
branch = None branch = None
name = None name = None
stats = get_file_stats(args.targetFile)
project = find_project(args.targetFile)
if project: if project:
branch = project.branch() branch = project.branch()
name = project.name() project_name = project.name()
if send_action( if send_action(
project=name, project=project_name,
branch=branch, branch=branch,
stats=stats, stats=stats,
**vars(args) **vars(args)
): ):
return 0 return 0 # success
return 102
return 102 # api error
else: else:
log.debug('File does not exist; ignoring this action.') log.debug('File does not exist; ignoring this action.')
return 101 return 0

View file

@ -12,25 +12,34 @@
import logging import logging
import os import os
from .projects.wakatime import WakaTime
from .projects.git import Git from .projects.git import Git
from .projects.mercurial import Mercurial from .projects.mercurial import Mercurial
from .projects.projectmap import ProjectMap
from .projects.subversion import Subversion from .projects.subversion import Subversion
from .projects.wakatime import WakaTime
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# List of plugin classes to find a project for the current file path.
# Project plugins will be processed with priority in the order below.
PLUGINS = [ PLUGINS = [
WakaTime, WakaTime,
ProjectMap,
Git, Git,
Mercurial, Mercurial,
Subversion, Subversion,
] ]
def find_project(path): def find_project(path, configs=None):
for plugin in PLUGINS: for plugin in PLUGINS:
project = plugin(path) plugin_name = plugin.__name__.lower()
plugin_configs = None
if configs and configs.has_section(plugin_name):
plugin_configs = dict(configs.items(plugin_name))
project = plugin(path, configs=plugin_configs)
if project.process(): if project.process():
return project return project
return None return None

View file

@ -22,18 +22,19 @@ class BaseProject(object):
be found for the current path. be found for the current path.
""" """
def __init__(self, path): def __init__(self, path, configs=None):
self.path = path self.path = path
self._configs = configs
def type(self): def project_type(self):
""" Returns None if this is the base class. """ Returns None if this is the base class.
Returns the type of project if this is a Returns the type of project if this is a
valid project. valid project.
""" """
type = self.__class__.__name__.lower() project_type = self.__class__.__name__.lower()
if type == 'baseproject': if project_type == 'baseproject':
type = None project_type = None
return type return project_type
def process(self): def process(self):
""" Processes self.path into a project and """ Processes self.path into a project and

View file

@ -25,35 +25,32 @@ log = logging.getLogger(__name__)
class Git(BaseProject): class Git(BaseProject):
def process(self): def process(self):
self.config = self._find_config(self.path) self.configFile = self._find_git_config_file(self.path)
if self.config: return self.configFile is not None
return True
return False
def name(self): def name(self):
base = self._project_base() base = self._project_base()
if base: if base:
return os.path.basename(base) return unicode(os.path.basename(base))
return None return None
def branch(self): def branch(self):
branch = None
base = self._project_base() base = self._project_base()
if base: if base:
head = os.path.join(self._project_base(), '.git', 'HEAD') head = os.path.join(self._project_base(), '.git', 'HEAD')
try: try:
with open(head) as f: with open(head) as fh:
branch = f.readline().strip().rsplit('/', 1)[-1] return unicode(fh.readline().strip().rsplit('/', 1)[-1])
except IOError: except IOError:
pass pass
return branch
def _project_base(self):
if self.config:
return os.path.dirname(os.path.dirname(self.config))
return None return None
def _find_config(self, path): def _project_base(self):
if self.configFile:
return os.path.dirname(os.path.dirname(self.configFile))
return None
def _find_git_config_file(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]
@ -62,34 +59,4 @@ 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._find_config(split_path[0]) return self._find_git_config_file(split_path[0])
def _parse_config(self):
sections = {}
try:
f = open(self.config, 'r')
except IOError as e:
log.exception("Exception:")
else:
with f:
section = None
for line in f.readlines():
line = line.lstrip()
if len(line) > 0 and line[0] == '[':
section = line[1:].split(']', 1)[0]
temp = section.split(' ', 1)
section = temp[0].lower()
if len(temp) > 1:
section = ' '.join([section, temp[1]])
sections[section] = {}
else:
try:
(setting, value) = line.split('=', 1)
except ValueError:
setting = line.split('#', 1)[0].split(';', 1)[0]
value = 'true'
setting = setting.strip().lower()
value = value.split('#', 1)[0].split(';', 1)[0].strip()
sections[section][setting] = value
f.close()
return sections

View file

@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
"""
wakatime.projects.projectmap
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Use the ~/.wakatime.cfg file to set custom project names by
recursively matching folder paths.
Project maps go under the [projectmap] config section.
For example:
[projectmap]
/home/user/projects/foo = new project name
/home/user/projects/bar = project2
Will result in file `/home/user/projects/foo/src/main.c` to have
project name `new project name`.
:copyright: (c) 2013 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
import logging
import os
from .base import BaseProject
log = logging.getLogger(__name__)
class ProjectMap(BaseProject):
def process(self):
if not self._configs:
return False
self.project = self._find_project(self.path)
return self.project is not None
def _find_project(self, path):
path = os.path.realpath(path)
if os.path.isfile(path):
path = os.path.split(path)[0]
if self._configs.get(path.lower()):
return self._configs.get(path.lower())
if self._configs.get('%s/' % path.lower()):
return self._configs.get('%s/' % path.lower())
if self._configs.get('%s\\' % path.lower()):
return self._configs.get('%s\\' % path.lower())
split_path = os.path.split(path)
if split_path[1] == '':
return None
return self._find_project(split_path[0])
def branch(self):
return None
def name(self):
if self.project:
return unicode(self.project)
return None

View file

@ -30,13 +30,12 @@ class Subversion(BaseProject):
return self._find_project_base(self.path) return self._find_project_base(self.path)
def name(self): def name(self):
return self.info['Repository Root'].split('/')[-1] return unicode(self.info['Repository Root'].split('/')[-1])
def branch(self): def branch(self):
branch = None
if self.base: if self.base:
branch = os.path.basename(self.base) unicode(os.path.basename(self.base))
return branch return None
def _get_info(self, path): def _get_info(self, path):
info = OrderedDict() info = OrderedDict()

View file

@ -28,13 +28,12 @@ class WakaTime(BaseProject):
return False return False
def name(self): def name(self):
project_name = None
try: try:
with open(self.config) as fh: with open(self.config) as fh:
project_name = fh.readline().strip() return unicode(fh.readline().strip())
except IOError as e: except IOError as e:
log.exception("Exception:") log.exception("Exception:")
return project_name return None
def branch(self): def branch(self):
return None return None

View file

@ -28,6 +28,7 @@ EXTENSIONS = {
'j2': 'HTML', 'j2': 'HTML',
'markdown': 'Markdown', 'markdown': 'Markdown',
'md': 'Markdown', 'md': 'Markdown',
'twig': 'Twig',
} }
TRANSLATIONS = { TRANSLATIONS = {
'CSS+Genshi Text': 'CSS', 'CSS+Genshi Text': 'CSS',