From 7e678a38bd19e834c61d2554fdb60d24c58d4a68 Mon Sep 17 00:00:00 2001 From: Alan Hamlett Date: Sun, 25 May 2014 17:27:52 -0700 Subject: [PATCH] bump version of wakatime package to 2.0.0 --- packages/wakatime/HISTORY.rst | 12 ++++ packages/wakatime/README.rst | 12 +++- packages/wakatime/setup.py | 9 +-- packages/wakatime/wakatime/__init__.py | 60 ++++++++++++++--- packages/wakatime/wakatime/queue.py | 93 ++++++++++++++++++++++++++ 5 files changed, 166 insertions(+), 20 deletions(-) create mode 100644 packages/wakatime/wakatime/queue.py diff --git a/packages/wakatime/HISTORY.rst b/packages/wakatime/HISTORY.rst index ee07a6a..3028b0d 100644 --- a/packages/wakatime/HISTORY.rst +++ b/packages/wakatime/HISTORY.rst @@ -3,6 +3,18 @@ History ------- +2.0.0 (2014-05-25) +++++++++++++++++++ + +- offline time logging using sqlite3 to queue editor events + + +1.0.2 (2014-05-06) +++++++++++++++++++ + +- ability to set project from command line argument + + 1.0.1 (2014-03-05) ++++++++++++++++++ diff --git a/packages/wakatime/README.rst b/packages/wakatime/README.rst index 5e55ea2..804a49c 100644 --- a/packages/wakatime/README.rst +++ b/packages/wakatime/README.rst @@ -1,9 +1,9 @@ WakaTime ======== -Fully automatic time tracking for your text editor. +Fully automatic time tracking for programmers. -This is the common interface for the WakaTime api. You shouldn't need to directly use this package unless you are creating a new plugin. +This is the common interface for the WakaTime api. You shouldn't need to directly use this package unless you are creating a new plugin or your text editor's plugin asks you to install the wakatime-cli interface. Go to http://wakatime.com to install the plugin for your text editor. @@ -11,4 +11,10 @@ Go to http://wakatime.com to install the plugin for your text editor. Installation ------------ -https://wakatime.com/help/plugins/installing-plugins + pip install wakatime + + +Usage +----- + +https://wakatime.com/ diff --git a/packages/wakatime/setup.py b/packages/wakatime/setup.py index 0a305db..7d17375 100644 --- a/packages/wakatime/setup.py +++ b/packages/wakatime/setup.py @@ -11,13 +11,10 @@ setup( name='wakatime', version=VERSION, license='BSD 3 Clause', - description=' '.join([ - 'Action event appender for WakaTime, a time', - 'tracking api for text editors.', - ]), + description='Interface to the WakaTime api.', long_description=open('README.rst').read(), author='Alan Hamlett', - author_email='alan.hamlett@gmail.com', + author_email='alan@wakatime.com', url='https://github.com/wakatime/wakatime', packages=packages, package_dir={'wakatime': 'wakatime'}, @@ -28,7 +25,7 @@ setup( 'console_scripts': ['wakatime = wakatime.__init__:main'], }, classifiers=( - 'Development Status :: 3 - Alpha', + 'Development Status :: 5 - Production/Stable', 'Environment :: Console', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', diff --git a/packages/wakatime/wakatime/__init__.py b/packages/wakatime/wakatime/__init__.py index 1d4ee25..b9ae049 100644 --- a/packages/wakatime/wakatime/__init__.py +++ b/packages/wakatime/wakatime/__init__.py @@ -13,10 +13,10 @@ from __future__ import print_function __title__ = 'wakatime' -__version__ = '1.0.1' +__version__ = '2.0.0' __author__ = 'Alan Hamlett' __license__ = 'BSD' -__copyright__ = 'Copyright 2013 Alan Hamlett' +__copyright__ = 'Copyright 2014 Alan Hamlett' import base64 @@ -39,6 +39,7 @@ except ImportError: 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')) +from .queue import Queue from .log import setup_logging from .project import find_project from .stats import get_file_stats @@ -148,9 +149,14 @@ def parseArguments(argv): parser.add_argument('--plugin', dest='plugin', help='optional text editor plugin name and version '+ 'for User-Agent header') + parser.add_argument('--project', dest='project_name', + help='optional project name; will auto-discover by default') parser.add_argument('--key', dest='key', help='your wakatime api key; uses api_key from '+ '~/.wakatime.conf by default') + parser.add_argument('--disableoffline', dest='offline', + action='store_false', + help='disables offline time logging instead of queuing logged time') parser.add_argument('--ignore', dest='ignore', action='append', help='filename patterns to ignore; POSIX regex syntax; can be used more than once') parser.add_argument('--logfile', dest='logfile', @@ -191,6 +197,8 @@ def parseArguments(argv): args.ignore.append(pattern) except TypeError: pass + if args.offline and configs.has_option('settings', 'offline'): + args.offline = configs.getboolean('settings', 'offline') 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'): @@ -225,8 +233,8 @@ def get_user_agent(plugin): return user_agent -def send_action(project=None, branch=None, stats={}, key=None, targetFile=None, - timestamp=None, isWrite=None, plugin=None, **kwargs): +def send_action(project=None, branch=None, stats=None, key=None, targetFile=None, + timestamp=None, isWrite=None, plugin=None, offline=None, **kwargs): url = 'https://wakatime.com/api/v1/actions' log.debug('Sending action to api at %s' % url) data = { @@ -268,24 +276,45 @@ def send_action(project=None, branch=None, stats={}, key=None, targetFile=None, } if log.isEnabledFor(logging.DEBUG): exception_data['traceback'] = traceback.format_exc() - log.error(exception_data) + if offline: + queue = Queue() + queue.push(data, plugin) + if log.isEnabledFor(logging.DEBUG): + log.warn(exception_data) + else: + log.error(exception_data) except: exception_data = { sys.exc_info()[0].__name__: str(sys.exc_info()[1]), } if log.isEnabledFor(logging.DEBUG): exception_data['traceback'] = traceback.format_exc() - log.error(exception_data) + if offline: + queue = Queue() + queue.push(data, plugin) + if log.isEnabledFor(logging.DEBUG): + log.warn(exception_data) + else: + log.error(exception_data) else: if response.getcode() == 201: log.debug({ 'response_code': response.getcode(), }) return True - log.error({ - 'response_code': response.getcode(), - 'response_content': response.read(), - }) + if offline: + queue = Queue() + queue.push(data, plugin) + if log.isEnabledFor(logging.DEBUG): + log.warn({ + 'response_code': response.getcode(), + 'response_content': response.read(), + }) + else: + log.error({ + 'response_code': response.getcode(), + 'response_content': response.read(), + }) return False @@ -313,7 +342,7 @@ def main(argv=None): project_name = None if project: branch = project.branch() - project_name = project.name() + project_name = args.project_name or project.name() if send_action( project=project_name, @@ -321,6 +350,15 @@ def main(argv=None): stats=stats, **vars(args) ): + queue = Queue() + while True: + action = queue.pop() + if action is None: + break + if not send_action(project=action['project'], targetFile=action['file'], timestamp=action['time'], + branch=action['branch'], stats={'language': action['language'], 'lines': action['lines']}, + key=args.key, isWrite=action['is_write'], plugin=action['plugin'], offline=args.offline): + break return 0 # success return 102 # api error diff --git a/packages/wakatime/wakatime/queue.py b/packages/wakatime/wakatime/queue.py new file mode 100644 index 0000000..d9263e4 --- /dev/null +++ b/packages/wakatime/wakatime/queue.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +""" + wakatime.queue + ~~~~~~~~~~~~~~ + + Queue for offline time logging. + http://wakatime.com + + :copyright: (c) 2014 Alan Hamlett. + :license: BSD, see LICENSE for more details. +""" + + +import logging +import os +import sqlite3 +from time import sleep + + +log = logging.getLogger(__name__) + + +class Queue(object): + DB_FILE = os.path.join(os.path.expanduser('~'), '.wakatime.db') + + def connect(self): + exists = os.path.exists(self.DB_FILE) + conn = sqlite3.connect(self.DB_FILE) + c = conn.cursor() + c.execute('''CREATE TABLE IF NOT EXISTS action ( + file text, + time real, + project text, + language text, + lines integer, + branch text, + is_write integer, + plugin text) + ''') + return (conn, c) + + + def push(self, data, plugin): + conn, c = self.connect() + action = { + 'file': data.get('file'), + 'time': data.get('time'), + 'project': data.get('project'), + 'language': data.get('language'), + 'lines': data.get('lines'), + 'branch': data.get('branch'), + 'is_write': 1 if data.get('is_write') else 0, + 'plugin': plugin, + } + c.execute('INSERT INTO action VALUES (:file,:time,:project,:language,:lines,:branch,:is_write,:plugin)', action) + conn.commit() + conn.close() + + + def pop(self): + tries = 3 + wait = 0.1 + action = None + conn, c = self.connect() + loop = True + while loop and tries > -1: + try: + c.execute('BEGIN IMMEDIATE') + c.execute('SELECT * FROM action LIMIT 1') + row = c.fetchone() + if row is not None: + c.execute('''DELETE FROM action WHERE + file=? AND time=? AND project=? AND language=? AND + lines=? AND branch=? AND is_write=?''', row[0:7]) + conn.commit() + if row is not None: + action = { + 'file': row[0], + 'time': row[1], + 'project': row[2], + 'language': row[3], + 'lines': row[4], + 'branch': row[5], + 'is_write': True if row[6] is 1 else False, + 'plugin': row[7], + } + loop = False + except sqlite3.Error, e: + log.debug(str(e)) + sleep(wait) + tries -= 1 + conn.close() + return action