added version 0.0.1

This commit is contained in:
Alan Hamlett 2013-07-06 00:51:09 -07:00
parent 3da94756aa
commit a902a2b948
18 changed files with 3075 additions and 4 deletions

10
HISTORY.rst Normal file
View file

@ -0,0 +1,10 @@
History
-------
0.0.1 (2013-07-05)
++++++++++++++++++
- Birth

29
LICENSE Normal file
View file

@ -0,0 +1,29 @@
Copyright (c) 2013 Alan Hamlett https://wakati.me
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided
with the distribution.
* Neither the names of Wakatime or Wakati.Me, nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

2
MANIFEST.in Normal file
View file

@ -0,0 +1,2 @@
include README.rst LICENSE HISTORY.rst
recursive-include wakatime *.py

View file

@ -1,4 +0,0 @@
wakatime
========
Command line event appender for Wakati.Me

12
README.rst Normal file
View file

@ -0,0 +1,12 @@
Wakati.Me
=========
Wakati.Me is a time tracking api for text editors. This is the command line
event appender for the Wakati.Me api. You shouldn't need to directly use
this outside of a text editor plugin.
Installation
------------
https://www.wakati.me/help/plugins/installing-plugins

37
setup.py Normal file
View file

@ -0,0 +1,37 @@
from setuptools import setup
from wakatime.wakatime import __version__ as VERSION
packages = [
'wakatime',
]
setup(
name='wakatime',
version=VERSION,
license='BSD 3 Clause',
description=(
'Event appender for Wakati.Me, a time',
'tracking api for text editors.',
),
author='Alan Hamlett',
author_email='alan.hamlett@gmail.com',
url='https://github.com/wakatime/wakatime',
packages=packages,
package_dir={'wakatime': 'wakatime'},
include_package_data=True,
zip_safe=False,
entry_points={
'console_scripts': ['wakatime = wakatime.wakatime:main'],
},
classifiers=(
'Development Status :: 3 - Alpha',
'Environment :: Console',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Natural Language :: English',
'Programming Language :: Python',
'Topic :: Text Editors',
),
)

0
wakatime/__init__.py Normal file
View file

85
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),
('timestamp', 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

35
wakatime/project.py Normal file
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

47
wakatime/projects/base.py Normal 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 ''

81
wakatime/projects/git.py Normal file
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(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

192
wakatime/wakatime.py Normal file
View file

@ -0,0 +1,192 @@
# -*- coding: utf-8 -*-
"""
wakatime
~~~~~~~~
Event appender for Wakati.Me, a time tracking api for programmers.
:copyright: (c) 2013 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
from __future__ import print_function
__title__ = 'wakatime'
__version__ = '0.0.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 event')
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 event into '+
'a duration; if a non-duration event occurs within a '+
'duration, the duration is ignored')
parser.add_argument('--write', dest='isWrite',
action='store_true',
help='note event 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')
return args
def get_api_key(configFile):
if not configFile:
configFile = '~/.wakatime.conf'
api_key = None
try:
cf = open(os.path.expanduser('~/.wakatime'))
for line in cf:
line = line.split('=', 1)
if line[0] == 'api_key':
api_key = line[1].strip()
cf.close()
except IOError:
log.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 = plugin+' '+user_agent
return user_agent
def send_action(project_name=None, tags=None, key=None, targetFile=None,
timestamp=None, endtime=None, isWrite=None, plugin=None, **kwargs):
url = 'https://www.wakati.me/api/v1/events'
log.debug('Sending event to api at %s' % url)
data = {
'timestamp': timestamp,
'file': targetFile,
}
if endtime:
data['endtime'] = endtime
if isWrite:
data['isWrite'] = isWrite
if project_name:
data['project'] = project_name
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:
log.debug({
'response_code': response.getcode(),
'response_content': response.read(),
})
if response.getcode() >= 200 and response.getcode() < 300:
return True
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, tags=tags, **vars(args)):
return 0
return 102
else:
log.debug('File does not exist; ignoring this event.')
return 101
if __name__ == '__main__':
sys.exit(main())