#!/usr/bin/env python import os import sys import argparse import platform import urllib2 import json import base64 import uuid import time import re from collections import OrderedDict from httplib import BadStatusLine, IncompleteRead from urllib2 import HTTPError, URLError import logging as log # Config version = '0.1.3' user_agent = 'vim-wakatime/%s (%s)' % (version, platform.platform()) def project_from_path(path): project = git_project(path) if project: return project return None def tags_from_path(path): tags = [] if os.path.exists(path): tags.extend(git_tags(path)) tags.extend(mercurial_tags(path)) return list(set(tags)) def git_project(path): config_file = find_git_config(path) if config_file: folder = os.path.split(os.path.split(os.path.split(config_file)[0])[0])[1] if folder: return folder return None def find_git_config(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 find_git_config(split_path[0]) def parse_git_config(config): sections = OrderedDict() 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] = OrderedDict() 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 def git_tags(path): tags = [] config_file = find_git_config(path) if config_file: sections = parse_git_config(config_file) for section in sections: if section.split(' ', 1)[0] == 'remote' and 'url' in sections[section]: tags.append(sections[section]['url']) return tags def mercurial_tags(path): tags = [] return tags def svn_tags(path): tags = [] return tags def log_action(**kwargs): kwargs['User-Agent'] = user_agent log.info(json.dumps(kwargs)) def send_action(key, instance, action, task, timestamp, project, tags): url = 'https://www.wakati.me/api/v1/actions' data = { 'type': action, 'task': os.path.realpath(task), 'time': timestamp, 'instance_id': instance, 'project': project, 'tags': tags, } request = urllib2.Request(url=url, data=json.dumps(data)) request.add_header('User-Agent', user_agent) request.add_header('Content-Type', 'application/json') request.add_header('Authorization', 'Basic %s' % base64.b64encode(key)) log_action(**data) response = None try: response = urllib2.urlopen(request) except HTTPError as ex: log.error("%s:\ndata=%s\nresponse=%s" % (ex.getcode(), json.dumps(data), ex.read())) if log.getLogger().isEnabledFor(log.DEBUG): log.exception("Exception for %s:\n%s" % (data['time'], json.dumps(data))) except (URLError, IncompleteRead, BadStatusLine) as ex: log.error("%s:\ndata=%s\nmessage=%s" % (ex.__class__.__name__, json.dumps(data), ex)) if log.getLogger().isEnabledFor(log.DEBUG): log.exception("Exception for %s:\n%s" % (data['time'], json.dumps(data))) if response: log.debug('response_code=%s response_content=%s' % (response.getcode(), response.read())) if response and (response.getcode() == 200 or response.getcode() == 201): return True return False def parse_args(argv): parser = argparse.ArgumentParser(description='Log time to the wakati.me api') parser.add_argument('--key', dest='key', required=True, help='your wakati.me api key') parser.add_argument('--action', dest='action', required=True, choices=['open_file', 'ping', 'close_file', 'write_file', 'open_editor', 'quit_editor', 'minimize_editor', 'maximize_editor', 'start', 'stop']) parser.add_argument('--task', dest='task', required=True, help='path to file or named task') parser.add_argument('--instance', dest='instance', required=True, help='the UUID4 representing the current editor') parser.add_argument('--time', dest='timestamp', metavar='time', type=float, help='optional floating-point timestamp in seconds') parser.add_argument('--verbose', dest='verbose', action='store_true', help='turns on debug messages in logfile') parser.add_argument('--version', action='version', version=version) return parser.parse_args(argv) def main(argv): args = parse_args(argv) level = log.INFO if args.verbose: level = log.DEBUG del args.verbose if not args.timestamp: args.timestamp = time.time() log.basicConfig(filename=os.path.expanduser('~/.wakatime.log'), format='%(asctime)s vim-wakatime/'+version+' %(levelname)s %(message)s', datefmt='%Y-%m-%dT%H:%M:%SZ', level=level) if os.path.isfile(os.path.realpath(args.task)): tags = tags_from_path(args.task) project = project_from_path(args.task) send_action(project=project, tags=tags, **vars(args)) return 0 if __name__ == '__main__': sys.exit(main(sys.argv[1:]))