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
|
||||
|
||||
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