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)
+++++++++++++++++++

View File

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

View File

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

View File

@ -3,7 +3,9 @@
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.
:license: BSD, see LICENSE for more details.
@ -12,7 +14,7 @@
from __future__ import print_function
__title__ = 'wakatime'
__version__ = '0.4.10'
__version__ = '0.5.0'
__author__ = 'Alan Hamlett'
__license__ = 'BSD'
__copyright__ = 'Copyright 2013 Alan Hamlett'
@ -26,6 +28,15 @@ import re
import sys
import time
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.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 simplejson as json
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__)
@ -52,46 +58,82 @@ class FileAction(argparse.Action):
setattr(namespace, self.dest, values)
def parseConfigFile(configFile):
if not configFile:
configFile = os.path.join(os.path.expanduser('~'), '.wakatime.conf')
def upgradeConfigFile(configFile):
"""For backwards-compatibility, upgrade the existing config file
to work with configparser and rename from .wakatime.conf to .wakatime.cfg.
"""
# define default config values
configs = {
'api_key': None,
'ignore': [],
'verbose': False,
}
if not os.path.isfile(configFile):
return configs
if os.path.isfile(configFile):
# if upgraded cfg file already exists, don't overwrite it
return
oldConfig = os.path.join(os.path.expanduser('~'), '.wakatime.conf')
try:
with open(configFile) as fh:
for line in fh:
configs = {
'ignore': [],
}
with open(oldConfig) as fh:
for line in fh.readlines():
line = line.split('=', 1)
if len(line) == 2 and line[0].strip() and line[1].strip():
line[0] = line[0].strip()
line[1] = 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]
if line[0].strip() == 'ignore':
configs['ignore'].append(line[1].strip())
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:
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
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:
sys.argv
except AttributeError:
sys.argv = argv
# define supported command line arguments
parser = argparse.ArgumentParser(
description='Wakati.Me event api appender')
parser.add_argument('--file', dest='targetFile', metavar='file',
@ -119,27 +161,45 @@ def parseArguments(argv):
parser.add_argument('--verbose', dest='verbose', action='store_true',
help='turns on debug messages in log file')
parser.add_argument('--version', action='version', version=__version__)
# parse command line arguments
args = parser.parse_args(args=argv[1:])
# use current unix epoch timestamp by default
if not args.timestamp:
args.timestamp = time.time()
# set arguments from config file
# parse ~/.wakatime.cfg file
configs = parseConfigFile(args.config)
if configs is None:
return args, configs
# update args from configs
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:
args.key = default_key
else:
parser.error('Missing api key')
for pattern in configs.get('ignore', []):
if not args.ignore:
args.ignore = []
args.ignore.append(pattern)
if not args.verbose and 'verbose' in configs:
args.verbose = configs['verbose']
if not args.logfile and 'logfile' in configs:
args.logfile = configs['logfile']
return args
if not args.ignore:
args.ignore = []
if configs.has_option('settings', 'ignore'):
try:
for pattern in configs.get('settings', 'ignore').split("\n"):
if pattern.strip() != '':
args.ignore.append(pattern)
except TypeError:
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):
@ -233,28 +293,39 @@ def send_action(project=None, branch=None, stats={}, key=None, targetFile=None,
def main(argv=None):
if not 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__)
ignore = should_ignore(args.targetFile, args.ignore)
if ignore is not False:
log.debug('File ignored because matches pattern: %s' % ignore)
return 0
if os.path.isfile(args.targetFile):
stats = get_file_stats(args.targetFile)
project = find_project(args.targetFile, configs=configs)
branch = None
name = None
stats = get_file_stats(args.targetFile)
project = find_project(args.targetFile)
if project:
branch = project.branch()
name = project.name()
project_name = project.name()
if send_action(
project=name,
project=project_name,
branch=branch,
stats=stats,
**vars(args)
):
return 0
return 102
return 0 # success
return 102 # api error
else:
log.debug('File does not exist; ignoring this action.')
return 101
return 0

View File

@ -12,25 +12,34 @@
import logging
import os
from .projects.wakatime import WakaTime
from .projects.git import Git
from .projects.mercurial import Mercurial
from .projects.projectmap import ProjectMap
from .projects.subversion import Subversion
from .projects.wakatime import WakaTime
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 = [
WakaTime,
ProjectMap,
Git,
Mercurial,
Subversion,
]
def find_project(path):
def find_project(path, configs=None):
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():
return project
return None

View File

@ -22,18 +22,19 @@ class BaseProject(object):
be found for the current path.
"""
def __init__(self, path):
def __init__(self, path, configs=None):
self.path = path
self._configs = configs
def type(self):
def project_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
project_type = self.__class__.__name__.lower()
if project_type == 'baseproject':
project_type = None
return project_type
def process(self):
""" Processes self.path into a project and

View File

@ -25,35 +25,32 @@ log = logging.getLogger(__name__)
class Git(BaseProject):
def process(self):
self.config = self._find_config(self.path)
if self.config:
return True
return False
self.configFile = self._find_git_config_file(self.path)
return self.configFile is not None
def name(self):
base = self._project_base()
if base:
return os.path.basename(base)
return unicode(os.path.basename(base))
return None
def branch(self):
branch = None
base = self._project_base()
if base:
head = os.path.join(self._project_base(), '.git', 'HEAD')
try:
with open(head) as f:
branch = f.readline().strip().rsplit('/', 1)[-1]
with open(head) as fh:
return unicode(fh.readline().strip().rsplit('/', 1)[-1])
except IOError:
pass
return branch
def _project_base(self):
if self.config:
return os.path.dirname(os.path.dirname(self.config))
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)
if os.path.isfile(path):
path = os.path.split(path)[0]
@ -62,34 +59,4 @@ class Git(BaseProject):
split_path = os.path.split(path)
if split_path[1] == '':
return None
return self._find_config(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
return self._find_git_config_file(split_path[0])

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)
def name(self):
return self.info['Repository Root'].split('/')[-1]
return unicode(self.info['Repository Root'].split('/')[-1])
def branch(self):
branch = None
if self.base:
branch = os.path.basename(self.base)
return branch
unicode(os.path.basename(self.base))
return None
def _get_info(self, path):
info = OrderedDict()

View File

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

View File

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