diff --git a/plugin/packages/wakatime/__about__.py b/plugin/packages/wakatime/__about__.py index 4e3dd84..a4ec680 100644 --- a/plugin/packages/wakatime/__about__.py +++ b/plugin/packages/wakatime/__about__.py @@ -1,7 +1,7 @@ __title__ = 'wakatime' __description__ = 'Common interface to the WakaTime api.' __url__ = 'https://github.com/wakatime/wakatime' -__version_info__ = ('4', '0', '8') +__version_info__ = ('4', '0', '11') __version__ = '.'.join(__version_info__) __author__ = 'Alan Hamlett' __author_email__ = 'alan@wakatime.com' diff --git a/plugin/packages/wakatime/base.py b/plugin/packages/wakatime/base.py index 8c47287..30186c8 100644 --- a/plugin/packages/wakatime/base.py +++ b/plugin/packages/wakatime/base.py @@ -29,14 +29,14 @@ sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pac from .__about__ import __version__ from .compat import u, open, is_py3 -from .offlinequeue import Queue from .logger import setup_logging -from .project import find_project -from .stats import get_file_stats +from .offlinequeue import Queue from .packages import argparse from .packages import simplejson as json -from .packages import requests from .packages.requests.exceptions import RequestException +from .project import find_project +from .session_cache import SessionCache +from .stats import get_file_stats try: from .packages import tzlocal except: @@ -147,6 +147,10 @@ def parseArguments(argv): type=float, help='optional floating-point unix epoch timestamp; '+ 'uses current time by default') + parser.add_argument('--lineno', dest='lineno', + help='optional line number; current line being edited') + parser.add_argument('--cursorpos', dest='cursorpos', + help='optional cursor position in the current file') parser.add_argument('--notfile', dest='notfile', action='store_true', help='when set, will accept any value for the file. for example, '+ 'a domain name or other item you want to log time towards.') @@ -322,6 +326,10 @@ def send_heartbeat(project=None, branch=None, stats={}, key=None, targetFile=Non data['language'] = stats['language'] if stats.get('dependencies'): data['dependencies'] = stats['dependencies'] + if stats.get('lineno'): + data['lineno'] = stats['lineno'] + if stats.get('cursorpos'): + data['cursorpos'] = stats['cursorpos'] if isWrite: data['is_write'] = isWrite if project: @@ -352,10 +360,13 @@ def send_heartbeat(project=None, branch=None, stats={}, key=None, targetFile=Non if tz: headers['TimeZone'] = u(tz.zone) + session_cache = SessionCache() + session = session_cache.get() + # log time to api response = None try: - response = requests.post(api_url, data=request_body, headers=headers, + response = session.post(api_url, data=request_body, headers=headers, proxies=proxies) except RequestException: exception_data = { @@ -377,6 +388,7 @@ def send_heartbeat(project=None, branch=None, stats={}, key=None, targetFile=Non log.debug({ 'response_code': response_code, }) + session_cache.save(session) return True if offline: if response_code != 400: @@ -402,6 +414,7 @@ def send_heartbeat(project=None, branch=None, stats={}, key=None, targetFile=Non 'response_code': response_code, 'response_content': response_content, }) + session_cache.delete() return False @@ -424,7 +437,8 @@ def main(argv=None): if os.path.isfile(args.targetFile) or args.notfile: - stats = get_file_stats(args.targetFile, notfile=args.notfile) + stats = get_file_stats(args.targetFile, notfile=args.notfile, + lineno=args.lineno, cursorpos=args.cursorpos) project = None if not args.notfile: diff --git a/plugin/packages/wakatime/logger.py b/plugin/packages/wakatime/logger.py index 62ed9ad..fdae903 100644 --- a/plugin/packages/wakatime/logger.py +++ b/plugin/packages/wakatime/logger.py @@ -9,6 +9,7 @@ :license: BSD, see LICENSE for more details. """ +import inspect import logging import os import sys @@ -47,14 +48,19 @@ class JsonFormatter(logging.Formatter): def format(self, record): data = OrderedDict([ ('now', self.formatTime(record, self.datefmt)), - ('version', self.version), - ('plugin', self.plugin), - ('time', self.timestamp), - ('isWrite', self.isWrite), - ('file', self.targetFile), - ('level', record.levelname), - ('message', record.msg), ]) + try: + data['package'] = inspect.stack()[9][0].f_globals.get('__package__') + data['lineno'] = inspect.stack()[9][2] + except: + pass + data['version'] = self.version + data['plugin'] = self.plugin + data['time'] = self.timestamp + data['isWrite'] = self.isWrite + data['file'] = self.targetFile + data['level'] = record.levelname + data['message'] = record.msg if not self.plugin: del data['plugin'] if not self.isWrite: diff --git a/plugin/packages/wakatime/offlinequeue.py b/plugin/packages/wakatime/offlinequeue.py index 2f57ab3..ebbc2d3 100644 --- a/plugin/packages/wakatime/offlinequeue.py +++ b/plugin/packages/wakatime/offlinequeue.py @@ -1,10 +1,9 @@ # -*- coding: utf-8 -*- """ - wakatime.queue - ~~~~~~~~~~~~~~ + wakatime.offlinequeue + ~~~~~~~~~~~~~~~~~~~~~ - Queue for offline time logging. - http://wakatime.com + Queue for saving heartbeats while offline. :copyright: (c) 2014 Alan Hamlett. :license: BSD, see LICENSE for more details. diff --git a/plugin/packages/wakatime/session_cache.py b/plugin/packages/wakatime/session_cache.py new file mode 100644 index 0000000..7986fa0 --- /dev/null +++ b/plugin/packages/wakatime/session_cache.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +""" + wakatime.session_cache + ~~~~~~~~~~~~~~~~~~~~~~ + + Persist requests.Session for multiprocess SSL handshake pooling. + + :copyright: (c) 2015 Alan Hamlett. + :license: BSD, see LICENSE for more details. +""" + + +import logging +import os +import pickle +import sys +import traceback + +try: + import sqlite3 + HAS_SQL = True +except ImportError: + HAS_SQL = False + +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'packages')) + +from .packages import requests + + +log = logging.getLogger('WakaTime') + + +class SessionCache(object): + DB_FILE = os.path.join(os.path.expanduser('~'), '.wakatime.db') + + def connect(self): + conn = sqlite3.connect(self.DB_FILE) + c = conn.cursor() + c.execute('''CREATE TABLE IF NOT EXISTS session ( + value BLOB) + ''') + return (conn, c) + + + def save(self, session): + """Saves a requests.Session object for the next heartbeat process. + """ + + if not HAS_SQL: + return + try: + conn, c = self.connect() + c.execute('DELETE FROM session') + values = { + 'value': pickle.dumps(session), + } + c.execute('INSERT INTO session VALUES (:value)', values) + conn.commit() + conn.close() + except: + log.error(traceback.format_exc()) + + + def get(self): + """Returns a requests.Session object. + + Gets Session from sqlite3 cache or creates a new Session. + """ + + if not HAS_SQL: + return requests.session() + + try: + conn, c = self.connect() + except: + log.error(traceback.format_exc()) + return requests.session() + + session = None + try: + c.execute('BEGIN IMMEDIATE') + c.execute('SELECT value FROM session LIMIT 1') + row = c.fetchone() + if row is not None: + session = pickle.loads(row[0]) + except: + log.error(traceback.format_exc()) + + try: + conn.close() + except: + log.error(traceback.format_exc()) + + return session if session is not None else requests.session() + + + def delete(self): + """Clears all cached Session objects. + """ + + if not HAS_SQL: + return + try: + conn, c = self.connect() + c.execute('DELETE FROM session') + conn.commit() + conn.close() + except: + log.error(traceback.format_exc()) diff --git a/plugin/packages/wakatime/stats.py b/plugin/packages/wakatime/stats.py index 9caa09c..a245c1d 100644 --- a/plugin/packages/wakatime/stats.py +++ b/plugin/packages/wakatime/stats.py @@ -86,12 +86,14 @@ def number_lines_in_file(file_name): return lines -def get_file_stats(file_name, notfile=False): +def get_file_stats(file_name, notfile=False, lineno=None, cursorpos=None): if notfile: stats = { 'language': None, 'dependencies': [], 'lines': None, + 'lineno': lineno, + 'cursorpos': cursorpos, } else: language, lexer = guess_language(file_name) @@ -101,5 +103,7 @@ def get_file_stats(file_name, notfile=False): 'language': language, 'dependencies': dependencies, 'lines': number_lines_in_file(file_name), + 'lineno': lineno, + 'cursorpos': cursorpos, } return stats