version 0.2.1. using new actions api scheme.

This commit is contained in:
Alan Hamlett
2013-07-07 18:38:01 -07:00
parent 227b7197d3
commit 2357f1325c
14 changed files with 3056 additions and 223 deletions

View File

85
packages/wakatime/log.py Normal file
View File

@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
"""
wakatime.log
~~~~~~~~~~~~
Provides the configured logger for writing JSON to the log file.
:copyright: (c) 2013 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
import json
import logging
import os
try:
from collections import OrderedDict
except ImportError:
from .packages.ordereddict import OrderedDict
class CustomEncoder(json.JSONEncoder):
def default(self, obj):
return super(CustomEncoder, self).default(obj)
class JsonFormatter(logging.Formatter):
def __init__(self, timestamp, endtime, isWrite, targetFile, version,
plugin, datefmt=None):
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([
('now', self.formatTime(record, self.datefmt)),
('version', self.version),
('plugin', self.plugin),
('time', self.timestamp),
('endtime', self.endtime),
('isWrite', self.isWrite),
('file', self.targetFile),
('level', record.levelname),
('message', record.msg),
])
if not self.endtime:
del data['endtime']
if not self.plugin:
del data['plugin']
if not self.isWrite:
del data['isWrite']
return CustomEncoder().encode(data)
def formatException(self, exc_info):
return exec_info[2].format_exc()
def setup_logging(args, version):
logfile = args.logfile
if not logfile:
logfile = '~/.wakatime.log'
handler = logging.FileHandler(os.path.expanduser(logfile))
formatter = JsonFormatter(
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()
logger.addHandler(handler)
level = logging.INFO
if args.verbose:
level = logging.DEBUG
logger.setLevel(level)
return logger

View File

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,127 @@
# Copyright (c) 2009 Raymond Hettinger
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
from UserDict import DictMixin
class OrderedDict(dict, DictMixin):
def __init__(self, *args, **kwds):
if len(args) > 1:
raise TypeError('expected at most 1 arguments, got %d' % len(args))
try:
self.__end
except AttributeError:
self.clear()
self.update(*args, **kwds)
def clear(self):
self.__end = end = []
end += [None, end, end] # sentinel node for doubly linked list
self.__map = {} # key --> [key, prev, next]
dict.clear(self)
def __setitem__(self, key, value):
if key not in self:
end = self.__end
curr = end[1]
curr[2] = end[1] = self.__map[key] = [key, curr, end]
dict.__setitem__(self, key, value)
def __delitem__(self, key):
dict.__delitem__(self, key)
key, prev, next = self.__map.pop(key)
prev[2] = next
next[1] = prev
def __iter__(self):
end = self.__end
curr = end[2]
while curr is not end:
yield curr[0]
curr = curr[2]
def __reversed__(self):
end = self.__end
curr = end[1]
while curr is not end:
yield curr[0]
curr = curr[1]
def popitem(self, last=True):
if not self:
raise KeyError('dictionary is empty')
if last:
key = reversed(self).next()
else:
key = iter(self).next()
value = self.pop(key)
return key, value
def __reduce__(self):
items = [[k, self[k]] for k in self]
tmp = self.__map, self.__end
del self.__map, self.__end
inst_dict = vars(self).copy()
self.__map, self.__end = tmp
if inst_dict:
return (self.__class__, (items,), inst_dict)
return self.__class__, (items,)
def keys(self):
return list(self)
setdefault = DictMixin.setdefault
update = DictMixin.update
pop = DictMixin.pop
values = DictMixin.values
items = DictMixin.items
iterkeys = DictMixin.iterkeys
itervalues = DictMixin.itervalues
iteritems = DictMixin.iteritems
def __repr__(self):
if not self:
return '%s()' % (self.__class__.__name__,)
return '%s(%r)' % (self.__class__.__name__, self.items())
def copy(self):
return self.__class__(self)
@classmethod
def fromkeys(cls, iterable, value=None):
d = cls()
for key in iterable:
d[key] = value
return d
def __eq__(self, other):
if isinstance(other, OrderedDict):
if len(self) != len(other):
return False
for p, q in zip(self.items(), other.items()):
if p != q:
return False
return True
return dict.__eq__(self, other)
def __ne__(self, other):
return not self == other

View File

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
"""
wakatime.project
~~~~~~~~~~~~~~~~
Returns a project for the given file.
:copyright: (c) 2013 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
import logging
import os
from .projects.base import BaseProject
from .projects.git import Git
from .projects.mercurial import Mercurial
from .projects.subversion import Subversion
log = logging.getLogger(__name__)
PLUGINS = [
Git,
Mercurial,
Subversion,
]
def find_project(path):
for plugin in PLUGINS:
project = plugin(path)
if project.config:
return project
return BaseProject(path)

View File

View File

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
"""
wakatime.projects.base
~~~~~~~~~~~~~~~~~~~~~~
Base project for use when no other project can be found.
:copyright: (c) 2013 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
import logging
import os
log = logging.getLogger(__name__)
class BaseProject():
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):
type = self.__class__.__name__.lower()
if type == 'baseproject':
type = None
return type
def base(self):
if self.config:
return os.path.dirname(self.config)
return None
def tags(self):
tags = []
return tags
def findConfig(self, path):
return ''

View File

@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
"""
wakatime.projects.git
~~~~~~~~~~~~~~~~~~~~~
Information about the git project for a given file.
:copyright: (c) 2013 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
import logging
import os
from .base import BaseProject
try:
from collections import OrderedDict
except ImportError:
from ..packages.ordereddict import OrderedDict
log = logging.getLogger(__name__)
class Git(BaseProject):
def base(self):
if self.config:
return os.path.dirname(os.path.dirname(self.config))
return None
def tags(self):
tags = []
if self.config:
sections = self.parseConfig()
for section in sections:
if section.split(' ', 1)[0] == 'remote' and 'url' in sections[section]:
tags.append(sections[section]['url'])
return tags
def findConfig(self, path):
path = os.path.realpath(path)
if os.path.isfile(path):
path = os.path.split(path)[0]
if os.path.isfile(os.path.join(path, '.git', 'config')):
return os.path.join(path, '.git', 'config')
split_path = os.path.split(path)
if split_path[1] == '':
return None
return self.findConfig(split_path[0])
def parseConfig(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,28 @@
# -*- coding: utf-8 -*-
"""
wakatime.projects.mercurial
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Information about the mercurial project for a given file.
: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 Mercurial(BaseProject):
def base(self):
return super(Mercurial, self).base()
def tags(self):
tags = []
return tags

View File

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
"""
wakatime.projects.subversion
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Information about the svn project for a given file.
: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 Subversion(BaseProject):
def base(self):
return super(Subversion, self).base()
def tags(self):
tags = []
return tags

View File

@ -0,0 +1,199 @@
# -*- coding: utf-8 -*-
""" wakatime
~~~~~~~~
Action event appender for Wakati.Me, a time tracking api for text editors.
:copyright: (c) 2013 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
from __future__ import print_function
__title__ = 'wakatime'
__version__ = '0.1.1'
__author__ = 'Alan Hamlett'
__license__ = 'BSD'
__copyright__ = 'Copyright 2013 Alan Hamlett'
# allow running script directly
if __name__ == '__main__' and __package__ is None:
import os, sys
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, parent_dir)
import wakatime
__package__ = 'wakatime'
del os, sys
import base64
import json
import logging
import os
import platform
import re
import sys
import time
import traceback
import urllib2
from .log import setup_logging
from .project import find_project
try:
import argparse
except ImportError:
from .packages import argparse
log = logging.getLogger(__name__)
class FileAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
values = os.path.realpath(values)
setattr(namespace, self.dest, values)
def parseArguments():
parser = argparse.ArgumentParser(
description='Wakati.Me event api appender')
parser.add_argument('--file', dest='targetFile', metavar='file',
action=FileAction, required=True,
help='absolute path to file for current action')
parser.add_argument('--time', dest='timestamp', metavar='time',
type=float,
help='optional floating-point unix epoch timestamp; '+
'uses current time by default')
parser.add_argument('--endtime', dest='endtime',
help='optional end timestamp turning this action into '+
'a duration; if a non-duration action occurs within a '+
'duration, the duration is ignored')
parser.add_argument('--write', dest='isWrite',
action='store_true',
help='note action was triggered from writing to a file')
parser.add_argument('--plugin', dest='plugin',
help='optional text editor plugin name and version '+
'for User-Agent header')
parser.add_argument('--key', dest='key',
help='your wakati.me api key; uses api_key from '+
'~/.wakatime.conf by default')
parser.add_argument('--logfile', dest='logfile',
help='defaults to ~/.wakatime.log')
parser.add_argument('--config', dest='config',
help='defaults to ~/.wakatime.conf')
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__)
args = parser.parse_args()
if not args.timestamp:
args.timestamp = time.time()
if not args.key:
default_key = get_api_key(args.config)
if default_key:
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
def get_api_key(configFile):
if not configFile:
configFile = '~/.wakatime.conf'
api_key = None
try:
cf = open(os.path.expanduser(configFile))
for line in cf:
line = line.split('=', 1)
if line[0] == 'api_key':
api_key = line[1].strip()
cf.close()
except IOError:
print('Error: Could not read from config file.')
return api_key
def get_user_agent(plugin):
user_agent = 'wakatime/%s (%s)' % (__version__, platform.platform())
if plugin:
user_agent = user_agent+' '+plugin
return user_agent
def send_action(project=None, tags=None, key=None, targetFile=None,
timestamp=None, endtime=None, isWrite=None, plugin=None, **kwargs):
url = 'https://www.wakati.me/api/v1/actions'
log.debug('Sending action to api at %s' % url)
data = {
'time': timestamp,
'file': targetFile,
}
if endtime:
data['endtime'] = endtime
if isWrite:
data['isWrite'] = isWrite
if project:
data['project'] = project
if tags:
data['tags'] = tags
log.debug(data)
request = urllib2.Request(url=url, data=json.dumps(data))
user_agent = get_user_agent(plugin)
request.add_header('User-Agent', user_agent)
request.add_header('Content-Type', 'application/json')
request.add_header('Authorization', 'Basic %s' % base64.b64encode(key))
response = None
try:
response = urllib2.urlopen(request)
except urllib2.HTTPError as exc:
data = {
'response_code': exc.getcode(),
'response_content': exc.read(),
sys.exc_info()[0].__name__: str(sys.exc_info()[1]),
}
if log.isEnabledFor(logging.DEBUG):
data['traceback'] = traceback.format_exc()
log.error(data)
except:
data = {
sys.exc_info()[0].__name__: str(sys.exc_info()[1]),
}
if log.isEnabledFor(logging.DEBUG):
data['traceback'] = traceback.format_exc()
log.error(data)
else:
if response.getcode() >= 200 and response.getcode() < 300:
log.debug({
'response_code': response.getcode(),
'response_content': response.read(),
})
return True
log.error({
'response_code': response.getcode(),
'response_content': response.read(),
})
return False
def main():
args = parseArguments()
setup_logging(args, __version__)
if os.path.isfile(args.targetFile):
project = find_project(args.targetFile)
tags = project.tags()
if send_action(project=project.name(), tags=tags, **vars(args)):
return 0
return 102
else:
log.debug('File does not exist; ignoring this action.')
return 101
if __name__ == '__main__':
sys.exit(main())