Compare commits

...

52 Commits
1.4.4 ... 2.0.3

Author SHA1 Message Date
66fddc07b9 v2.0.3 2014-05-26 15:03:24 -07:00
e56a07e909 update wakatime package to v2.0.1 2014-05-26 15:01:32 -07:00
64ea40b3f5 v2.0.2 2014-05-26 14:07:29 -07:00
17fd6ef8e1 disable queue until bug fixed 2014-05-26 14:06:36 -07:00
e5e399dfbe v2.0.1 2014-05-25 17:30:26 -07:00
bcf037e8a4 v2.0.0 2014-05-25 17:29:47 -07:00
7e678a38bd bump version of wakatime package to 2.0.0 2014-05-25 17:27:52 -07:00
533aaac313 change www.wakatime.com to wakatime.com 2014-03-14 13:36:20 -07:00
7f4f70cc85 add bsd license 2014-03-12 17:03:14 -07:00
4adb8a8796 v1.6.5 2014-03-05 13:55:08 -08:00
48e1993b24 upgrade external wakatime package to v1.0.1 2014-03-05 13:51:29 -08:00
8a3375bb23 v1.6.4 2014-02-05 01:03:05 -08:00
8bd54a7427 upgrade wakatime package to v1.0.0 for mercurial support 2014-02-05 01:01:47 -08:00
fcbbf05933 v1.6.3 2014-01-15 16:46:32 -08:00
9733087094 upgrade wakatime package to v0.5.3 2014-01-15 16:45:07 -08:00
da4e02199a v1.6.2 2014-01-14 05:14:25 -08:00
09a16dea1e upgrade wakatime package to v0.5.2 2014-01-14 05:11:00 -08:00
4c7adf0943 v1.6.1 2013-12-13 16:38:52 +01:00
216a8eaa0a upgrade common wakatime package to v0.5.1 2013-12-13 16:37:54 +01:00
81f838489d v1.6.0 2013-12-13 16:11:46 +01:00
d6228b8dce use [WakaTime] namespace for all console messages 2013-12-13 16:08:34 +01:00
7a2c2b9750 fix variable definition bug 2013-12-13 15:41:45 +01:00
d9cc911595 upgrade common wakatime package to v0.5.0 2013-12-13 15:35:49 +01:00
805e2fe222 v1.5.2 2013-12-03 02:41:54 +01:00
bbcb39b2cf fix #18 by using a non-localized datetime in log 2013-12-03 02:38:30 +01:00
9f9b97c69f v1.5.1 2013-12-02 09:17:35 +01:00
908ff98613 decode file names with filesystem encoding, then encode as utf-8 before encoding with simplejson 2013-12-02 09:16:21 +01:00
37f74b4b56 v1.5.0 2013-11-28 12:18:44 +01:00
a1c0d7e489 prevent sending timestamp when saving same file multiple times. increase send frequency from 5 minutes to 2 minutes. 2013-11-28 12:16:43 +01:00
3127f264b4 v1.4.12 2013-11-21 01:11:40 -08:00
049bc57019 fix #17 by removing non-utf8 characters 2013-11-21 01:09:37 -08:00
03ec38bb67 v1.4.11 2013-11-13 18:07:41 -08:00
4fc1a55ff7 set project name with .wakatime-project file. upgrade wakatime package to 0.4.10. 2013-11-13 18:06:55 -08:00
f60815b813 upgrade wakatime package to recognize .markdown file extension 2013-11-03 11:36:43 -08:00
ca47c2308d v1.4.10 2013-10-31 17:20:12 -07:00
146a959747 recognize jinja2 file extensions as HTML 2013-10-31 17:19:17 -07:00
906184cd88 v1.4.9 2013-10-28 18:12:12 -07:00
a13e11d24d handle case where ignore patterns not defined 2013-10-28 18:09:51 -07:00
d34432217f v1.4.8 2013-10-27 21:31:34 -07:00
f2e8f85198 fix syntax in default sublime-settings file 2013-10-27 21:31:15 -07:00
05b08b6ab2 new sublime-setting ingore for ignoring files by regular expressions 2013-10-27 21:30:10 -07:00
685d242c60 upgrade wakatime package to v0.4.9. adds new ignore config to .wakatime.conf to ignore files based on regex patterns. 2013-10-27 21:07:42 -07:00
023c1dfbe3 v1.4.7 2013-10-26 19:12:29 -07:00
9255fd2c34 update language lexer translations 2013-10-26 17:59:41 -07:00
784ad38c38 update readme 2013-10-26 17:52:39 -07:00
36def5c8b8 update screen shot in readme 2013-10-26 17:50:38 -07:00
2c8dd6c9e7 v1.4.6 2013-10-25 21:35:00 -07:00
e8151535c1 update history file 2013-10-25 21:34:47 -07:00
744116079a upgrade wakatime package 2013-10-25 21:33:31 -07:00
791a969a10 Update screen shots in readme 2013-10-14 22:04:12 -07:00
46c5171d6a v1.4.5 2013-10-14 21:52:53 -07:00
fe641d01d4 remove support for subversion on Windows to prevent cmd windows from opening 2013-10-14 21:51:35 -07:00
22 changed files with 895 additions and 204 deletions

View File

@ -3,6 +3,133 @@ History
-------
2.0.3 (2014-05-26)
++++++++++++++++++
- upgrade external wakatime package to v2.0.1
- fix bug in queue preventing completed tasks from being purged
2.0.2 (2014-05-26)
++++++++++++++++++
- disable syncing offline time until bug fixed
2.0.1 (2014-05-25)
++++++++++++++++++
- upgrade external wakatime package to v2.0.0
- offline time logging using sqlite3 to queue editor events
1.6.5 (2014-03-05)
++++++++++++++++++
- upgrade external wakatime package to v1.0.1
- use new domain wakatime.com
1.6.4 (2014-02-05)
++++++++++++++++++
- upgrade external wakatime package to v1.0.0
- support for mercurial revision control
1.6.3 (2014-01-15)
++++++++++++++++++
- upgrade common wakatime package to v0.5.3
1.6.2 (2014-01-14)
++++++++++++++++++
- upgrade common wakatime package to v0.5.2
1.6.1 (2013-12-13)
++++++++++++++++++
- upgrade common wakatime package to v0.5.1
- second line in .wakatime-project now sets branch name
1.6.0 (2013-12-13)
++++++++++++++++++
- upgrade common wakatime package to v0.5.0
1.5.2 (2013-12-03)
++++++++++++++++++
- use non-localized datetime in log
1.5.1 (2013-12-02)
++++++++++++++++++
- decode file names with filesystem encoding, then encode as utf-8 for logging
1.5.0 (2013-11-28)
++++++++++++++++++
- increase "ping" frequency from every 5 minutes to every 2 minutes
- prevent sending multiple api requests when saving the same file
1.4.12 (2013-11-21)
+++++++++++++++++++
- handle UnicodeDecodeError exceptions when json encoding log messages
1.4.11 (2013-11-13)
+++++++++++++++++++
- placing .wakatime-project file in a folder will read the project's name from that file
1.4.10 (2013-10-31)
++++++++++++++++++
- recognize jinja2 file extensions as HTML
1.4.9 (2013-10-28)
++++++++++++++++++
- handle case where ignore patterns not defined
1.4.8 (2013-10-27)
++++++++++++++++++
- new setting to ignore files that match a regular expression pattern
1.4.7 (2013-10-26)
++++++++++++++++++
- simplify some language lexer names into more common versions
1.4.6 (2013-10-25)
++++++++++++++++++
- force some file extensions to be recognized as certain language
1.4.5 (2013-10-14)
++++++++++++++++++
- remove support for subversion projects on Windows to prevent cmd window popups
- ignore all errors from pygments library
1.4.4 (2013-10-13)
++++++++++++++++++

29
LICENSE Normal file
View File

@ -0,0 +1,29 @@
Copyright (c) 2014 Alan Hamlett https://wakatime.com
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided
with the distribution.
* Neither the names of Wakatime or WakaTime, nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,14 +1,14 @@
sublime-wakatime
================
Automatic time tracking for Sublime Text 2 & 3.
Fully automatic time tracking for Sublime Text 2 & 3.
Installation
------------
Heads Up! For Sublime Text 2 on Windows & Linux, WakaTime depends on [Python](http://www.python.org/getit/) being installed to work correctly.
1. Get an api key from: https://wakati.me
1. Get an api key from: https://wakatime.com/#apikey
2. Using [Sublime Package Control](http://wbond.net/sublime_packages/package_control):
@ -18,20 +18,16 @@ Heads Up! For Sublime Text 2 on Windows & Linux, WakaTime depends on [Python](ht
c) Type `wakatime`, then press `enter` with the `WakaTime` plugin selected.
3. You will see a prompt at the bottom asking for your [api key](https://www.wakati.me/#apikey). Enter your api key, then press `enter`.
3. You will see a prompt at the bottom asking for your [api key](https://wakatime.com/#apikey). Enter your api key, then press `enter`.
4. Use Sublime and your time will automatically be tracked for you.
5. Visit https://wakati.me to see your logged time.
5. Visit https://wakatime.com to see your logged time.
6. Consider installing [BIND9](https://help.ubuntu.com/community/BIND9ServerHowto#Caching_Server_configuration) to cache your repeated DNS requests: `sudo apt-get install bind9`
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)
![Project Overview](https://wakatime.com/static/img/ScreenShots/Screen Shot 2013-10-26 at 5.04.01 PM.png)

View File

@ -1,11 +1,12 @@
""" ==========================================================
File: WakaTime.py
Description: Automatic time tracking for Sublime Text 2 and 3.
Maintainer: WakaTi.me <support@wakatime.com>
Website: https://www.wakati.me/
Maintainer: WakaTime <support@wakatime.com>
License: BSD, see LICENSE for more details.
Website: https://wakatime.com/
==========================================================="""
__version__ = '1.4.4'
__version__ = '2.0.3'
import sublime
import sublime_plugin
@ -21,14 +22,17 @@ from os.path import expanduser, dirname, realpath, isfile, join, exists
# globals
ACTION_FREQUENCY = 5
ACTION_FREQUENCY = 2
ST_VERSION = int(sublime.version())
PLUGIN_DIR = dirname(realpath(__file__))
API_CLIENT = '%s/packages/wakatime/wakatime-cli.py' % PLUGIN_DIR
SETTINGS_FILE = 'WakaTime.sublime-settings'
SETTINGS = {}
LAST_ACTION = 0
LAST_FILE = None
LAST_ACTION = {
'time': 0,
'file': None,
'is_write': False,
}
HAS_SSL = False
LOCK = threading.RLock()
@ -47,31 +51,6 @@ if HAS_SSL:
import wakatime
def setup_settings_file():
""" Convert ~/.wakatime.conf to WakaTime.sublime-settings
"""
global SETTINGS
# To be backwards compatible, rename config file
SETTINGS = sublime.load_settings(SETTINGS_FILE)
api_key = SETTINGS.get('api_key', '')
if not api_key:
api_key = ''
try:
with open(join(expanduser('~'), '.wakatime.conf')) as old_file:
for line in old_file:
line = line.split('=', 1)
if line[0] == 'api_key':
api_key = str(line[1].strip())
try:
os.remove(join(expanduser('~'), '.wakatime.conf'))
except:
pass
except IOError:
pass
SETTINGS.set('api_key', api_key)
sublime.save_settings(SETTINGS_FILE)
def prompt_api_key():
global SETTINGS
if not SETTINGS.get('api_key'):
@ -81,10 +60,10 @@ def prompt_api_key():
sublime.save_settings(SETTINGS_FILE)
window = sublime.active_window()
if window:
window.show_input_panel('Enter your WakaTime api key:', '', got_key, None, None)
window.show_input_panel('[WakaTime] Enter your wakatime.com api key:', '', got_key, None, None)
return True
else:
print('Error: Could not prompt for api key because no window found.')
print('[WakaTime] Error: Could not prompt for api key because no window found.')
return False
@ -101,63 +80,60 @@ def python_binary():
return 'python'
def enough_time_passed(now):
if now - LAST_ACTION > ACTION_FREQUENCY * 60:
def enough_time_passed(now, last_time):
if now - last_time > ACTION_FREQUENCY * 60:
return True
return False
def handle_write_action(view):
global LOCK, LAST_FILE, LAST_ACTION
def handle_action(view, is_write=False):
global LOCK, LAST_ACTION
with LOCK:
targetFile = view.file_name()
thread = SendActionThread(targetFile, isWrite=True)
thread.start()
LAST_FILE = targetFile
LAST_ACTION = time.time()
def handle_normal_action(view):
global LOCK, LAST_FILE, LAST_ACTION
with LOCK:
targetFile = view.file_name()
thread = SendActionThread(targetFile)
thread.start()
LAST_FILE = targetFile
LAST_ACTION = time.time()
target_file = view.file_name()
if target_file:
thread = SendActionThread(target_file, is_write=is_write)
thread.start()
LAST_ACTION = {
'file': target_file,
'time': time.time(),
'is_write': is_write,
}
class SendActionThread(threading.Thread):
def __init__(self, targetFile, isWrite=False, force=False):
def __init__(self, target_file, is_write=False, force=False):
threading.Thread.__init__(self)
self.targetFile = targetFile
self.isWrite = isWrite
self.target_file = target_file
self.is_write = is_write
self.force = force
self.debug = SETTINGS.get('debug')
self.api_key = SETTINGS.get('api_key', '')
self.last_file = LAST_FILE
self.ignore = SETTINGS.get('ignore', [])
self.last_action = LAST_ACTION
def run(self):
if self.targetFile:
if self.target_file:
self.timestamp = time.time()
if self.force or self.isWrite or self.targetFile != self.last_file or enough_time_passed(self.timestamp):
if self.force or (self.is_write and not self.last_action['is_write']) or self.target_file != self.last_action['file'] or enough_time_passed(self.timestamp, self.last_action['time']):
self.send()
def send(self):
if not self.api_key:
print('missing api key')
print('[WakaTime] Error: missing api key.')
return
ua = 'sublime/%d sublime-wakatime/%s' % (ST_VERSION, __version__)
cmd = [
API_CLIENT,
'--file', self.targetFile,
'--file', self.target_file,
'--time', str('%f' % self.timestamp),
'--plugin', ua,
'--key', str(bytes.decode(self.api_key.encode('utf8'))),
]
if self.isWrite:
if self.is_write:
cmd.append('--write')
for pattern in self.ignore:
cmd.extend(['--ignore', pattern])
if self.debug:
cmd.append('--verbose')
if HAS_SSL:
@ -165,7 +141,7 @@ class SendActionThread(threading.Thread):
print(cmd)
code = wakatime.main(cmd)
if code != 0:
print('Error: Response code %d from wakatime package' % code)
print('[WakaTime] Error: Response code %d from wakatime package.' % code)
else:
python = python_binary()
if python:
@ -178,11 +154,12 @@ class SendActionThread(threading.Thread):
with open(join(expanduser('~'), '.wakatime.log'), 'a') as stderr:
Popen(cmd, stderr=stderr)
else:
print('Error: Unable to find python binary.')
print('[WakaTime] Error: Unable to find python binary.')
def plugin_loaded():
setup_settings_file()
global SETTINGS
SETTINGS = sublime.load_settings(SETTINGS_FILE)
after_loaded()
@ -199,10 +176,10 @@ if ST_VERSION < 3000:
class WakatimeListener(sublime_plugin.EventListener):
def on_post_save(self, view):
handle_write_action(view)
handle_action(view, is_write=True)
def on_activated(self, view):
handle_normal_action(view)
handle_action(view)
def on_modified(self, view):
handle_normal_action(view)
handle_action(view)

View File

@ -3,10 +3,14 @@
// This settings file will be overwritten when upgrading.
{
// Your api key from https://www.wakati.me/#apikey
// Your api key from https://wakatime.com/#apikey
// Set this in your User specific WakaTime.sublime-settings file.
"api_key": "",
// Ignore files; Files (including absolute paths) that match one of these
// POSIX regular expressions will not be logged.
"ignore": ["^/tmp/", "^/etc/", "^/var/"],
// Debug mode. Set to true for verbose logging. Defaults to false.
"debug": false
}

15
packages/wakatime/AUTHORS Normal file
View File

@ -0,0 +1,15 @@
WakaTime is written and maintained by Alan Hamlett and
various contributors:
Development Lead
----------------
- Alan Hamlett <alan.hamlett@gmail.com>
Patches and Suggestions
-----------------------
- 3onyc <3onyc@x3tech.com>
- userid <xixico@ymail.com>

View File

@ -3,6 +3,74 @@ History
-------
2.0.1 (2014-05-26)
++++++++++++++++++
- fix bug in queue preventing actions with NULL values from being purged
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)
++++++++++++++++++
- use new domain name wakatime.com
1.0.0 (2014-02-05)
++++++++++++++++++
- detect project name and branch name from mercurial revision control
0.5.3 (2014-01-15)
++++++++++++++++++
- bug fix for unicode in Python3
0.5.2 (2014-01-14)
++++++++++++++++++
- minor bug fix for Subversion on non-English systems
0.5.1 (2013-12-13)
++++++++++++++++++
- second line in .wakatime-project file now sets branch name
0.5.0 (2013-12-13)
++++++++++++++++++
- Convert ~/.wakatime.conf to ~/.wakatime.cfg and use configparser format
- new [projectmap] section in cfg file for naming projects based on folders
0.4.10 (2013-11-13)
+++++++++++++++++++
- Placing .wakatime-project file in a folder will read the project's name from that file
0.4.9 (2013-10-27)
++++++++++++++++++
- New config for ignoring files from regular expressions
- Parse more options from config file (verbose, logfile, ignore)
0.4.8 (2013-10-13)
++++++++++++++++++

View File

@ -1,4 +1,4 @@
Copyright (c) 2013 Alan Hamlett https://wakati.me
Copyright (c) 2013 Alan Hamlett https://wakatime.com
All rights reserved.
Redistribution and use in source and binary forms, with or without
@ -12,7 +12,7 @@ modification, are permitted provided that the following conditions are met:
in the documentation and/or other materials provided
with the distribution.
* Neither the names of Wakatime or Wakati.Me, nor the names of its
* Neither the names of Wakatime or WakaTime, nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.

View File

@ -1,12 +1,20 @@
WakaTime
========
Automatic time tracking for your text editor. This is the command line
event appender for the WakaTime api. You shouldn't need to directly
use this outside of a text editor plugin.
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 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.
Installation
------------
https://www.wakati.me/help/plugins/installing-plugins
pip install wakatime
Usage
-----
https://wakatime.com/

View File

@ -11,13 +11,10 @@ setup(
name='wakatime',
version=VERSION,
license='BSD 3 Clause',
description=' '.join([
'Action event appender for Wakati.Me, 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',

View File

@ -1,10 +1,9 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
wakatime-cli
~~~~~~~~~~~~
Action event appender for Wakati.Me, auto time tracking for text editors.
Command-line entry point.
:copyright: (c) 2013 Alan Hamlett.
:license: BSD, see LICENSE for more details.

View File

@ -3,7 +3,8 @@
wakatime
~~~~~~~~
Action event appender for Wakati.Me, auto time tracking for text editors.
Common interface to the WakaTime api.
http://wakatime.com
:copyright: (c) 2013 Alan Hamlett.
:license: BSD, see LICENSE for more details.
@ -12,10 +13,10 @@
from __future__ import print_function
__title__ = 'wakatime'
__version__ = '0.4.8'
__version__ = '2.0.1'
__author__ = 'Alan Hamlett'
__license__ = 'BSD'
__copyright__ = 'Copyright 2013 Alan Hamlett'
__copyright__ = 'Copyright 2014 Alan Hamlett'
import base64
@ -26,20 +27,25 @@ import re
import sys
import time
import traceback
try:
import ConfigParser as configparser
except ImportError:
import configparser
try:
from urllib2 import HTTPError, Request, urlopen
except ImportError:
from urllib.error import HTTPError
from urllib.request import Request, urlopen
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
from .packages import argparse
from .packages import simplejson as json
from .packages import tzlocal
try:
from urllib2 import HTTPError, Request, urlopen
except ImportError:
from urllib.error import HTTPError
from urllib.request import Request, urlopen
log = logging.getLogger(__name__)
@ -52,13 +58,84 @@ class FileAction(argparse.Action):
setattr(namespace, self.dest, values)
def upgradeConfigFile(configFile):
"""For backwards-compatibility, upgrade the existing config file
to work with configparser and rename from .wakatime.conf to .wakatime.cfg.
"""
if os.path.isfile(configFile):
# if upgraded cfg file already exists, don't overwrite it
return
oldConfig = os.path.join(os.path.expanduser('~'), '.wakatime.conf')
try:
configs = {
'ignore': [],
}
with open(oldConfig) as fh:
for line in fh.readlines():
line = line.split('=', 1)
if len(line) == 2 and line[0].strip() and line[1].strip():
if line[0].strip() == 'ignore':
configs['ignore'].append(line[1].strip())
else:
configs[line[0].strip()] = line[1].strip()
with open(configFile, 'w') as fh:
fh.write("[settings]\n")
for name, value in configs.items():
if isinstance(value, list):
fh.write("%s=\n" % name)
for item in value:
fh.write(" %s\n" % item)
else:
fh.write("%s = %s\n" % (name, value))
os.remove(oldConfig)
except IOError:
pass
def parseConfigFile(configFile):
"""Returns a configparser.SafeConfigParser instance with configs
read from the config file. Default location of the config file is
at ~/.wakatime.cfg.
"""
if not configFile:
configFile = os.path.join(os.path.expanduser('~'), '.wakatime.cfg')
upgradeConfigFile(configFile)
configs = configparser.SafeConfigParser()
try:
with open(configFile) as fh:
try:
configs.readfp(fh)
except configparser.Error:
print(traceback.format_exc())
return None
except IOError:
if not os.path.isfile(configFile):
print('Error: Could not read from config file ~/.wakatime.conf')
return configs
def parseArguments(argv):
"""Parse command line arguments and configs from ~/.wakatime.cfg.
Command line arguments take precedence over config file settings.
Returns instances of ArgumentParser and SafeConfigParser.
"""
try:
sys.argv
except AttributeError:
sys.argv = argv
# define supported command line arguments
parser = argparse.ArgumentParser(
description='Wakati.Me event api appender')
description='Common interface for the WakaTime api.')
parser.add_argument('--file', dest='targetFile', metavar='file',
action=FileAction, required=True,
help='absolute path to file for current action')
@ -72,9 +149,16 @@ 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 wakati.me api key; uses api_key from '+
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',
help='defaults to ~/.wakatime.log')
parser.add_argument('--config', dest='config',
@ -82,32 +166,61 @@ def parseArguments(argv):
parser.add_argument('--verbose', dest='verbose', action='store_true',
help='turns on debug messages in log file')
parser.add_argument('--version', action='version', version=__version__)
# parse command line arguments
args = parser.parse_args(args=argv[1:])
# use current unix epoch timestamp by default
if not args.timestamp:
args.timestamp = time.time()
# parse ~/.wakatime.cfg file
configs = parseConfigFile(args.config)
if configs is None:
return args, configs
# update args from configs
if not args.key:
default_key = get_api_key(args.config)
default_key = None
if configs.has_option('settings', 'api_key'):
default_key = configs.get('settings', 'api_key')
if default_key:
args.key = default_key
else:
parser.error('Missing api key')
return args
if not args.ignore:
args.ignore = []
if configs.has_option('settings', 'ignore'):
try:
for pattern in configs.get('settings', 'ignore').split("\n"):
if pattern.strip() != '':
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'):
args.verbose = configs.getboolean('settings', 'debug')
if not args.logfile and configs.has_option('settings', 'logfile'):
args.logfile = configs.get('settings', 'logfile')
return args, configs
def get_api_key(configFile):
if not configFile:
configFile = os.path.join(os.path.expanduser('~'), '.wakatime.conf')
api_key = None
def should_ignore(fileName, patterns):
try:
cf = open(configFile)
for line in cf:
line = line.split('=', 1)
if line[0] == 'api_key':
api_key = line[1].strip()
cf.close()
except IOError:
print('Error: Could not read from config file.')
return api_key
for pattern in patterns:
try:
compiled = re.compile(pattern, re.IGNORECASE)
if compiled.search(fileName):
return pattern
except re.error as ex:
log.warning('Regex error (%s) for ignore pattern: %s' % (str(ex), pattern))
except TypeError:
pass
return False
def get_user_agent(plugin):
@ -120,9 +233,9 @@ 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):
url = 'https://www.wakati.me/api/v1/actions'
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 = {
'time': timestamp,
@ -163,49 +276,93 @@ 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
def main(argv=None):
if not argv:
argv = sys.argv
args = parseArguments(argv)
args, configs = parseArguments(argv)
if configs is None:
return 103 # config file parsing error
setup_logging(args, __version__)
ignore = should_ignore(args.targetFile, args.ignore)
if ignore is not False:
log.debug('File ignored because matches pattern: %s' % ignore)
return 0
if os.path.isfile(args.targetFile):
branch = None
name = None
stats = get_file_stats(args.targetFile)
project = find_project(args.targetFile)
project = find_project(args.targetFile, configs=configs)
branch = None
project_name = None
if project:
branch = project.branch()
name = project.name()
project_name = args.project_name or project.name()
if send_action(
project=name,
project=project_name,
branch=branch,
stats=stats,
**vars(args)
):
return 0
return 102
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
else:
log.debug('File does not exist; ignoring this action.')
return 101
return 0

View File

@ -11,6 +11,7 @@
import logging
import os
import sys
from .packages import simplejson as json
try:
@ -25,7 +26,13 @@ class CustomEncoder(json.JSONEncoder):
if isinstance(obj, bytes):
obj = bytes.decode(obj)
return json.dumps(obj)
return super(CustomEncoder, self).default(obj)
try:
encoded = super(CustomEncoder, self).default(obj)
except UnicodeDecodeError:
encoding = sys.getfilesystemencoding()
obj = obj.decode(encoding, 'ignore').encode('utf-8')
encoded = super(CustomEncoder, self).default(obj)
return encoded
class JsonFormatter(logging.Formatter):
@ -69,7 +76,7 @@ def setup_logging(args, version):
logger = logging.getLogger()
set_log_level(logger, args)
if len(logger.handlers) > 0:
formatter = JsonFormatter(datefmt='%a %b %d %H:%M:%S %Z %Y')
formatter = JsonFormatter(datefmt='%Y/%m/%d %H:%M:%S %z')
formatter.setup(
timestamp=args.timestamp,
isWrite=args.isWrite,
@ -83,7 +90,7 @@ def setup_logging(args, version):
if not logfile:
logfile = '~/.wakatime.log'
handler = logging.FileHandler(os.path.expanduser(logfile))
formatter = JsonFormatter(datefmt='%a %b %d %H:%M:%S %Z %Y')
formatter = JsonFormatter(datefmt='%Y/%m/%d %H:%M:%S %z')
formatter.setup(
timestamp=args.timestamp,
isWrite=args.isWrite,

View File

@ -14,21 +14,32 @@ import os
from .projects.git import Git
from .projects.mercurial import Mercurial
from .projects.projectmap import ProjectMap
from .projects.subversion import Subversion
from .projects.wakatime import WakaTime
log = logging.getLogger(__name__)
# List of plugin classes to find a project for the current file path.
# Project plugins will be processed with priority in the order below.
PLUGINS = [
WakaTime,
ProjectMap,
Git,
Mercurial,
Subversion,
]
def find_project(path):
def find_project(path, configs=None):
for plugin in PLUGINS:
project = plugin(path)
plugin_name = plugin.__name__.lower()
plugin_configs = None
if configs and configs.has_section(plugin_name):
plugin_configs = dict(configs.items(plugin_name))
project = plugin(path, configs=plugin_configs)
if project.process():
return project
return None

View File

@ -22,18 +22,19 @@ class BaseProject(object):
be found for the current path.
"""
def __init__(self, path):
def __init__(self, path, configs=None):
self.path = path
self._configs = configs
def type(self):
def project_type(self):
""" Returns None if this is the base class.
Returns the type of project if this is a
valid project.
"""
type = self.__class__.__name__.lower()
if type == 'baseproject':
type = None
return type
project_type = self.__class__.__name__.lower()
if project_type == 'baseproject':
project_type = None
return project_type
def process(self):
""" Processes self.path into a project and

View File

@ -13,47 +13,47 @@ import logging
import os
from .base import BaseProject
try:
from collections import OrderedDict
except ImportError:
from ..packages.ordereddict import OrderedDict
log = logging.getLogger(__name__)
# str is unicode in Python3
try:
unicode
except NameError:
unicode = str
class Git(BaseProject):
def process(self):
self.config = self._find_config(self.path)
if self.config:
return True
return False
self.configFile = self._find_git_config_file(self.path)
return self.configFile is not None
def name(self):
base = self._project_base()
if base:
return os.path.basename(base)
return unicode(os.path.basename(base))
return None
def branch(self):
branch = None
base = self._project_base()
if base:
head = os.path.join(self._project_base(), '.git', 'HEAD')
try:
with open(head) as f:
branch = f.readline().strip().rsplit('/', 1)[-1]
with open(head) as fh:
return unicode(fh.readline().strip().rsplit('/', 1)[-1])
except IOError:
pass
return branch
def _project_base(self):
if self.config:
return os.path.dirname(os.path.dirname(self.config))
return None
def _find_config(self, path):
def _project_base(self):
if self.configFile:
return os.path.dirname(os.path.dirname(self.configFile))
return None
def _find_git_config_file(self, path):
path = os.path.realpath(path)
if os.path.isfile(path):
path = os.path.split(path)[0]
@ -62,34 +62,4 @@ class Git(BaseProject):
split_path = os.path.split(path)
if split_path[1] == '':
return None
return self._find_config(split_path[0])
def _parse_config(self):
sections = {}
try:
f = open(self.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] = {}
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
return self._find_git_config_file(split_path[0])

View File

@ -18,13 +18,41 @@ from .base import BaseProject
log = logging.getLogger(__name__)
# str is unicode in Python3
try:
unicode
except NameError:
unicode = str
class Mercurial(BaseProject):
def process(self):
return False
self.configDir = self._find_hg_config_dir(self.path)
return self.configDir is not None
def name(self):
if self.configDir:
return unicode(os.path.basename(os.path.dirname(self.configDir)))
return None
def branch(self):
return None
if self.configDir:
branch_file = os.path.join(self.configDir, 'branch')
try:
with open(branch_file) as fh:
return unicode(fh.readline().strip().rsplit('/', 1)[-1])
except IOError:
pass
return unicode('default')
def _find_hg_config_dir(self, path):
path = os.path.realpath(path)
if os.path.isfile(path):
path = os.path.split(path)[0]
if os.path.isdir(os.path.join(path, '.hg')):
return os.path.join(path, '.hg')
split_path = os.path.split(path)
if split_path[1] == '':
return None
return self._find_hg_config_dir(split_path[0])

View File

@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
"""
wakatime.projects.projectmap
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Use the ~/.wakatime.cfg file to set custom project names by
recursively matching folder paths.
Project maps go under the [projectmap] config section.
For example:
[projectmap]
/home/user/projects/foo = new project name
/home/user/projects/bar = project2
Will result in file `/home/user/projects/foo/src/main.c` to have
project name `new project name`.
:copyright: (c) 2013 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
import logging
import os
from .base import BaseProject
log = logging.getLogger(__name__)
# str is unicode in Python3
try:
unicode
except NameError:
unicode = str
class ProjectMap(BaseProject):
def process(self):
if not self._configs:
return False
self.project = self._find_project(self.path)
return self.project is not None
def _find_project(self, path):
path = os.path.realpath(path)
if os.path.isfile(path):
path = os.path.split(path)[0]
if self._configs.get(path.lower()):
return self._configs.get(path.lower())
if self._configs.get('%s/' % path.lower()):
return self._configs.get('%s/' % path.lower())
if self._configs.get('%s\\' % path.lower()):
return self._configs.get('%s\\' % path.lower())
split_path = os.path.split(path)
if split_path[1] == '':
return None
return self._find_project(split_path[0])
def branch(self):
return None
def name(self):
if self.project:
return unicode(self.project)
return None

View File

@ -11,6 +11,7 @@
import logging
import os
import platform
from subprocess import Popen, PIPE
from .base import BaseProject
@ -23,24 +24,31 @@ except ImportError:
log = logging.getLogger(__name__)
# str is unicode in Python3
try:
unicode
except NameError:
unicode = str
class Subversion(BaseProject):
def process(self):
return self._find_project_base(self.path)
def name(self):
return self.info['Repository Root'].split('/')[-1]
return unicode(self.info['Repository Root'].split('/')[-1])
def branch(self):
branch = None
if self.base:
branch = os.path.basename(self.base)
return branch
unicode(os.path.basename(self.base))
return None
def _get_info(self, path):
info = OrderedDict()
stdout = None
try:
os.environ['LANG'] = 'en_US'
stdout, stderr = Popen([
'svn', 'info', os.path.realpath(path)
], stdout=PIPE, stderr=PIPE).communicate()
@ -62,6 +70,8 @@ class Subversion(BaseProject):
return info
def _find_project_base(self, path, found=False):
if platform.system() == 'Windows':
return False
path = os.path.realpath(path)
if os.path.isfile(path):
path = os.path.split(path)[0]

View File

@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
"""
wakatime.projects.wakatime
~~~~~~~~~~~~~~~~~~~~~~~~~~
Information from a .wakatime-project file about the project for
a given file. First line of .wakatime-project sets the project
name. Second line sets the current branch name.
:copyright: (c) 2013 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
import logging
import os
from .base import BaseProject
log = logging.getLogger(__name__)
# str is unicode in Python3
try:
unicode
except NameError:
unicode = str
class WakaTime(BaseProject):
def process(self):
self.config = self._find_config(self.path)
self._project_name = None
self._project_branch = None
if self.config:
try:
with open(self.config) as fh:
self._project_name = unicode(fh.readline().strip())
self._project_branch = unicode(fh.readline().strip())
except IOError as e:
log.exception("Exception:")
return True
return False
def name(self):
return self._project_name
def branch(self):
return self._project_branch
def _find_config(self, 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, '.wakatime-project')):
return os.path.join(path, '.wakatime-project')
split_path = os.path.split(path)
if split_path[1] == '':
return None
return self._find_config(split_path[0])

View File

@ -0,0 +1,114 @@
# -*- 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
import traceback
from time import sleep
log = logging.getLogger(__name__)
class Queue(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 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):
try:
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()
except sqlite3.Error, e:
log.error(str(e))
def pop(self):
tries = 3
wait = 0.1
action = None
try:
conn, c = self.connect()
except sqlite3.Error, e:
log.debug(traceback.format_exc())
return None
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:
values = []
clauses = []
index = 0
for row_name in ['file', 'time', 'project', 'language', 'lines', 'branch', 'is_write']:
if row[index] is not None:
clauses.append('{0}=?'.format(row_name))
values.append(row[index])
else:
clauses.append('{0} IS NULL'.format(row_name))
index += 1
if len(values) > 0:
c.execute('DELETE FROM action WHERE {0}'.format(u' AND '.join(clauses)), values)
else:
c.execute('DELETE FROM action WHERE {0}'.format(u' AND '.join(clauses)))
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(traceback.format_exc())
sleep(wait)
tries -= 1
try:
conn.close()
except sqlite3.Error, e:
log.debug(traceback.format_exc())
return action

View File

@ -18,25 +18,62 @@ if sys.version_info[0] == 2:
else:
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'packages', 'pygments3'))
from pygments.lexers import guess_lexer_for_filename
from pygments.util import ClassNotFound
log = logging.getLogger(__name__)
# force file name extensions to be recognized as a certain language
EXTENSIONS = {
'j2': 'HTML',
'markdown': 'Markdown',
'md': 'Markdown',
'twig': 'Twig',
}
TRANSLATIONS = {
'CSS+Genshi Text': 'CSS',
'CSS+Lasso': 'CSS',
'HTML+Django/Jinja': 'HTML',
'HTML+Lasso': 'HTML',
'JavaScript+Genshi Text': 'JavaScript',
'JavaScript+Lasso': 'JavaScript',
'Perl6': 'Perl',
'RHTML': 'HTML',
}
def guess_language(file_name):
if file_name:
language = guess_language_from_extension(file_name.rsplit('.', 1)[-1])
if language:
return language
lexer = None
try:
with open(file_name) as f:
lexer = guess_lexer_for_filename(file_name, f.read(512000))
except (ClassNotFound, IOError):
except:
pass
if lexer:
return str(lexer.name)
return translate_language(str(lexer.name))
else:
return None
def guess_language_from_extension(extension):
if extension:
if extension in EXTENSIONS:
return EXTENSIONS[extension]
if extension.lower() in EXTENSIONS:
return mapping[EXTENSIONS.lower()]
return None
def translate_language(language):
if language in TRANSLATIONS:
language = TRANSLATIONS[language]
return language
def number_lines_in_file(file_name):
lines = 0
try: