Compare commits

...

23 Commits

Author SHA1 Message Date
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
22 changed files with 646 additions and 179 deletions

View File

@ -3,6 +3,64 @@ History
-------
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)
+++++++++++++++++++

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

@ -8,7 +8,7 @@ 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,16 +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/Screen Shot 2013-10-26 at 5.04.01 PM.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.12'
__version__ = '1.6.5'
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()
@ -56,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
@ -76,20 +80,24 @@ 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_action(view, is_write=False):
global LOCK, LAST_FILE, LAST_ACTION
global LOCK, LAST_ACTION
with LOCK:
target_file = view.file_name()
thread = SendActionThread(target_file, is_write=is_write)
thread.start()
LAST_FILE = target_file
LAST_ACTION = time.time()
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):
@ -102,17 +110,17 @@ class SendActionThread(threading.Thread):
self.debug = SETTINGS.get('debug')
self.api_key = SETTINGS.get('api_key', '')
self.ignore = SETTINGS.get('ignore', [])
self.last_file = LAST_FILE
self.last_action = LAST_ACTION
def run(self):
if self.target_file:
self.timestamp = time.time()
if self.force or self.is_write or self.target_file != 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 = [
@ -133,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:
@ -146,7 +154,7 @@ 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():

View File

@ -3,7 +3,7 @@
// 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": "",

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,55 @@ History
-------
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)
+++++++++++++++++++

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.10'
__version__ = '2.0.0'
__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,48 +58,84 @@ class FileAction(argparse.Action):
setattr(namespace, self.dest, values)
def parseConfigFile(configFile):
if not configFile:
configFile = os.path.join(os.path.expanduser('~'), '.wakatime.conf')
def upgradeConfigFile(configFile):
"""For backwards-compatibility, upgrade the existing config file
to work with configparser and rename from .wakatime.conf to .wakatime.cfg.
"""
# define default config values
configs = {
'api_key': None,
'ignore': [],
'verbose': False,
}
if not os.path.isfile(configFile):
return configs
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:
with open(configFile) as fh:
for line in fh:
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():
line[0] = line[0].strip()
line[1] = line[1].strip()
if line[0] in configs:
if isinstance(configs[line[0]], list):
configs[line[0]].append(line[1])
elif isinstance(configs[line[0]], bool):
configs[line[0]] = True if line[1].lower() == 'true' else False
else:
configs[line[0]] = line[1]
if line[0].strip() == 'ignore':
configs['ignore'].append(line[1].strip())
else:
configs[line[0]] = line[1]
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:
print('Error: Could not read from config file ~/.wakatime.conf')
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')
@ -107,9 +149,14 @@ 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',
@ -119,27 +166,47 @@ 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()
# set arguments from config file
# 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 = configs.get('api_key')
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')
for pattern in configs.get('ignore', []):
if not args.ignore:
args.ignore = []
args.ignore.append(pattern)
if not args.verbose and 'verbose' in configs:
args.verbose = configs['verbose']
if not args.logfile and 'logfile' in configs:
args.logfile = configs['logfile']
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 should_ignore(fileName, patterns):
@ -166,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,
@ -209,52 +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:
@ -28,7 +29,8 @@ class CustomEncoder(json.JSONEncoder):
try:
encoded = super(CustomEncoder, self).default(obj)
except UnicodeDecodeError:
obj = obj.decode('utf-8', 'ignore')
encoding = sys.getfilesystemencoding()
obj = obj.decode(encoding, 'ignore').encode('utf-8')
encoded = super(CustomEncoder, self).default(obj)
return encoded
@ -74,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,
@ -88,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

@ -12,25 +12,34 @@
import logging
import os
from .projects.wakatime import WakaTime
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

@ -24,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()

View File

@ -4,7 +4,8 @@
~~~~~~~~~~~~~~~~~~~~~~~~~~
Information from a .wakatime-project file about the project for
a given file.
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.
@ -19,25 +20,37 @@ 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):
project_name = None
try:
with open(self.config) as fh:
project_name = fh.readline().strip()
except IOError as e:
log.exception("Exception:")
return project_name
return self._project_name
def branch(self):
return None
return self._project_branch
def _find_config(self, path):
path = os.path.realpath(path)

View File

@ -0,0 +1,93 @@
# -*- 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
from time import sleep
log = logging.getLogger(__name__)
class Queue(object):
DB_FILE = os.path.join(os.path.expanduser('~'), '.wakatime.db')
def connect(self):
exists = os.path.exists(self.DB_FILE)
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):
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()
def pop(self):
tries = 3
wait = 0.1
action = None
conn, c = self.connect()
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:
c.execute('''DELETE FROM action WHERE
file=? AND time=? AND project=? AND language=? AND
lines=? AND branch=? AND is_write=?''', row[0:7])
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(str(e))
sleep(wait)
tries -= 1
conn.close()
return action

View File

@ -28,6 +28,7 @@ EXTENSIONS = {
'j2': 'HTML',
'markdown': 'Markdown',
'md': 'Markdown',
'twig': 'Twig',
}
TRANSLATIONS = {
'CSS+Genshi Text': 'CSS',