initial commit
This commit is contained in:
parent
b7fe36f882
commit
960fe601e1
3 changed files with 278 additions and 0 deletions
27
README.md
27
README.md
|
@ -2,3 +2,30 @@ sublime-wakatime
|
||||||
================
|
================
|
||||||
|
|
||||||
automatic time tracking for Sublime Text 2
|
automatic time tracking for Sublime Text 2
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
1) Get an api key from:
|
||||||
|
|
||||||
|
https://wakati.me
|
||||||
|
|
||||||
|
2) Run this shell command replacing KEY with your api key:
|
||||||
|
|
||||||
|
echo "api_key=KEY" >> ~/.wakatime
|
||||||
|
|
||||||
|
3) Install the plugin in Sublime's Packages directory.
|
||||||
|
|
||||||
|
4) Use Sublime and your time will automatically be tracked for you.
|
||||||
|
|
||||||
|
Visit https://wakati.me to view your time spent in each file.
|
||||||
|
|
||||||
|
Screen Shots
|
||||||
|
------------
|
||||||
|
|
||||||
|
![Project Overview](https://www.wakati.me/static/img/ScreenShots/Screenshot%20from%202013-06-26%2001:12:59.png)
|
||||||
|
|
||||||
|
![Files in a Project](https://www.wakati.me/static/img/ScreenShots/Screenshot%20from%202013-06-26%2001:13:13.png)
|
||||||
|
|
||||||
|
![Changing Date Range](https://www.wakati.me/static/img/ScreenShots/Screenshot%20from%202013-06-26%2001:13:53.png)
|
||||||
|
|
||||||
|
|
185
libs/wakatime.py
Normal file
185
libs/wakatime.py
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
#!/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.0'
|
||||||
|
user_agent = 'sublime-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:]))
|
66
sublime-wakatime.py
Normal file
66
sublime-wakatime.py
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
""" ======================================================
|
||||||
|
File: sublime-wakatime.py
|
||||||
|
Description: Automatic time tracking for Sublime Text 2.
|
||||||
|
Maintainer: Wakati.Me <support@wakatime.com>
|
||||||
|
Version: 0.1.0
|
||||||
|
======================================================="""
|
||||||
|
|
||||||
|
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
from os.path import expanduser, dirname, realpath
|
||||||
|
from subprocess import call, Popen
|
||||||
|
|
||||||
|
import sublime
|
||||||
|
import sublime_plugin
|
||||||
|
|
||||||
|
|
||||||
|
# Create logfile if does not exist
|
||||||
|
call(['touch', '~/.wakatime.log'])
|
||||||
|
|
||||||
|
PLUGIN_DIR = dirname(realpath(__file__))
|
||||||
|
API_CLIENT = '%s/libs/wakatime.py' % PLUGIN_DIR
|
||||||
|
INSTANCE_ID = str(uuid.uuid4())
|
||||||
|
|
||||||
|
|
||||||
|
def get_api_key():
|
||||||
|
api_key = None
|
||||||
|
try:
|
||||||
|
cf = open(expanduser('~/.wakatime'))
|
||||||
|
for line in cf:
|
||||||
|
line = line.split('=', 1)
|
||||||
|
if line[0] == 'api_key':
|
||||||
|
api_key = line[1]
|
||||||
|
cf.close()
|
||||||
|
except IOError:
|
||||||
|
pass
|
||||||
|
return api_key
|
||||||
|
|
||||||
|
|
||||||
|
def api(action, task, timestamp):
|
||||||
|
if task:
|
||||||
|
api_key = get_api_key()
|
||||||
|
if api_key:
|
||||||
|
cmd = ['python', API_CLIENT,
|
||||||
|
'--key', api_key,
|
||||||
|
'--instance', INSTANCE_ID,
|
||||||
|
'--action', action,
|
||||||
|
'--task', task,
|
||||||
|
'--time', str('%f' % timestamp)]
|
||||||
|
Popen(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
class WakatimeListener(sublime_plugin.EventListener):
|
||||||
|
|
||||||
|
def on_post_save(self, view):
|
||||||
|
api('write_file', view.file_name(), time.time())
|
||||||
|
|
||||||
|
def on_activated(self, view):
|
||||||
|
api('open_file', view.file_name(), time.time())
|
||||||
|
|
||||||
|
def on_deactivated(self, view):
|
||||||
|
api('close_file', view.file_name(), time.time())
|
||||||
|
|
||||||
|
|
||||||
|
if get_api_key() is None:
|
||||||
|
sublime.error_message('Missing your Wakati.Me api key')
|
Loading…
Reference in a new issue