Compare commits

..

40 Commits

Author SHA1 Message Date
3edaed53aa v4.0.11 2015-07-31 15:20:57 -07:00
865b0bcee9 install python on Windows if not already installed 2015-07-31 15:19:56 -07:00
d440fe912c v4.0.10 2015-07-31 13:27:58 -07:00
627455167f downgrade requests library to v2.6.0 2015-07-31 13:27:04 -07:00
aba89d3948 v4.0.9 2015-07-29 00:04:39 -07:00
18d87118e1 catch exceptions from get_filetype_from_buffer 2015-07-29 00:03:18 -07:00
fd91b9e032 link to wakatime/wakatime#troubleshooting 2015-07-15 13:46:26 -07:00
16b15773bf troubleshooting section in readme 2015-07-15 13:44:07 -07:00
f0b518862a upgrade wakatime cli to v4.1.0 2015-06-29 19:47:04 -07:00
7ee7de70d5 v4.0.8 2015-06-23 18:17:25 -07:00
fb479f8e84 fix offline logging with wakatime cli v4.0.16 2015-06-23 18:15:38 -07:00
7d37193f65 v4.0.7 2015-06-21 10:45:51 -07:00
6bd62b95db allow customizing status bar message in sublime-settings file 2015-06-21 10:42:31 -07:00
abf4a94a59 upgrade wakatime cli to v4.0.15 2015-06-21 10:35:14 -07:00
9337e3173b v4.0.6 2015-05-16 14:38:58 -07:00
57fa4d4d84 upgrade wakatime cli to v4.0.13 2015-05-16 14:38:19 -07:00
9b5c59e677 v4.0.5 2015-05-15 15:34:17 -07:00
71ce25a326 upgrade wakatime cli to v4.0.12 2015-05-15 15:33:03 -07:00
f2f14207f5 use new --alternate-project argument so auto detected project will take priority 2015-05-15 15:32:03 -07:00
ac2ec0e73c v4.0.4 2015-05-12 15:04:39 -07:00
040a76b93c upgrade wakatime cli to v4.0.11 2015-05-12 15:03:23 -07:00
dab0621b97 v4.0.3 2015-05-06 16:35:01 -07:00
675f9ecd69 send cursorpos to wakatime cli 2015-05-06 16:34:15 -07:00
a6f92b9c74 upgrade wakatime cli to v4.0.10 2015-05-06 16:33:32 -07:00
bfcc242d7e upgrade wakatime cli to v4.0.9 2015-05-06 15:45:34 -07:00
762027644f send current cursor line number to wakatime cli 2015-05-06 15:43:41 -07:00
3c4ceb95fa separate active view logic into own function 2015-05-06 14:06:06 -07:00
d6d8bceca0 v4.0.2 2015-05-06 14:01:35 -07:00
acaad2dc83 only send heartbeats for the currently active buffer, for cases where another process modifies files which are open in sublime text 2015-05-06 14:00:33 -07:00
23c5801080 v4.0.1 2015-05-06 12:30:36 -07:00
05a3bfbb53 include package and lineno in log outout 2015-05-06 12:30:26 -07:00
8faaa3b0e3 send all last_heartbeat data to enough_time_passed function 2015-05-06 12:27:39 -07:00
4bcddf2a98 use heartbeat name instead of action 2015-05-06 12:25:52 -07:00
b51ae5c2c4 don't send two write heartbeats within 2 seconds of eachother 2015-05-06 12:22:42 -07:00
5cd0061653 ignore git temporary files 2015-05-06 09:21:12 -07:00
651c84325e v4.0.0 2015-04-12 16:46:30 -07:00
89368529cb listen for selection modified instead of buffer activated 2015-04-12 16:45:16 -07:00
f1f408284b improve install instructions 2015-04-09 19:08:29 -07:00
7053932731 v3.0.19 2015-04-07 14:21:25 -07:00
b6c4956521 don't call os.path.basename when folder was not found 2015-04-07 14:20:21 -07:00
13 changed files with 582 additions and 106 deletions

View File

@ -3,6 +3,97 @@ History
------- -------
4.0.11 (2015-07-31)
++++++++++++++++++
- install python if missing on Windows OS
4.0.10 (2015-07-31)
++++++++++++++++++
- downgrade requests library to v2.6.0
4.0.9 (2015-07-29)
++++++++++++++++++
- catch exceptions from pygments.modeline.get_filetype_from_buffer
4.0.8 (2015-06-23)
++++++++++++++++++
- fix offline logging
- limit language detection to known file extensions, unless file contents has a vim modeline
- upgrade wakatime cli to v4.0.16
4.0.7 (2015-06-21)
++++++++++++++++++
- allow customizing status bar message in sublime-settings file
- guess language using multiple methods, then use most accurate guess
- use entity and type for new heartbeats api resource schema
- correctly log message from py.warnings module
- upgrade wakatime cli to v4.0.15
4.0.6 (2015-05-16)
++++++++++++++++++
- fix bug with auto detecting project name
- upgrade wakatime cli to v4.0.13
4.0.5 (2015-05-15)
++++++++++++++++++
- correctly display caller and lineno in log file when debug is true
- project passed with --project argument will always be used
- new --alternate-project argument
- upgrade wakatime cli to v4.0.12
4.0.4 (2015-05-12)
++++++++++++++++++
- reuse SSL connection over multiple processes for improved performance
- upgrade wakatime cli to v4.0.11
4.0.3 (2015-05-06)
++++++++++++++++++
- send cursorpos to wakatime cli
- upgrade wakatime cli to v4.0.10
4.0.2 (2015-05-06)
++++++++++++++++++
- only send heartbeats for the currently active buffer
4.0.1 (2015-05-06)
++++++++++++++++++
- ignore git temporary files
- don't send two write heartbeats within 2 seconds of eachother
4.0.0 (2015-04-12)
++++++++++++++++++
- listen for selection modified instead of buffer activated for better performance
3.0.19 (2015-04-07)
+++++++++++++++++++
- fix bug in project detection when folder not found
3.0.18 (2015-04-04) 3.0.18 (2015-04-04)
+++++++++++++++++++ +++++++++++++++++++

View File

@ -18,7 +18,7 @@ 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. c) Type `wakatime`, then press `enter` with the `WakaTime` plugin selected.
3. Enter your [api key](https://wakatime.com/settings#apikey) from https://wakatime.com/settings#apikey, then press `enter`. 3. Enter your [api key](https://wakatime.com/settings#apikey), then press `enter`.
4. Use Sublime and your time will be tracked for you automatically. 4. Use Sublime and your time will be tracked for you automatically.
@ -29,3 +29,15 @@ Screen Shots
![Project Overview](https://wakatime.com/static/img/ScreenShots/ScreenShot-2014-10-29.png) ![Project Overview](https://wakatime.com/static/img/ScreenShots/ScreenShot-2014-10-29.png)
Troubleshooting
---------------
First, turn on debug mode in your `WakaTime.sublime-settings` file.
![sublime user settings](https://wakatime.com/static/img/ScreenShots/sublime-wakatime-settings-menu.png)
Add the line: `"debug": true`
Then, open your Sublime Console with `View -> Show Console` to see the plugin executing the wakatime cli process when sending a heartbeat. Also, tail your `$HOME/.wakatime.log` file to debug wakatime cli problems.
For more general troubleshooting information, see [wakatime/wakatime#troubleshooting](https://github.com/wakatime/wakatime#troubleshooting).

View File

@ -7,7 +7,7 @@ Website: https://wakatime.com/
===========================================================""" ==========================================================="""
__version__ = '3.0.18' __version__ = '4.0.11'
import sublime import sublime
@ -19,19 +19,20 @@ import platform
import sys import sys
import time import time
import threading import threading
import urllib
import webbrowser import webbrowser
from datetime import datetime from datetime import datetime
from subprocess import Popen from subprocess import Popen
# globals # globals
ACTION_FREQUENCY = 2 HEARTBEAT_FREQUENCY = 2
ST_VERSION = int(sublime.version()) ST_VERSION = int(sublime.version())
PLUGIN_DIR = os.path.dirname(os.path.realpath(__file__)) PLUGIN_DIR = os.path.dirname(os.path.realpath(__file__))
API_CLIENT = os.path.join(PLUGIN_DIR, 'packages', 'wakatime', 'cli.py') API_CLIENT = os.path.join(PLUGIN_DIR, 'packages', 'wakatime', 'cli.py')
SETTINGS_FILE = 'WakaTime.sublime-settings' SETTINGS_FILE = 'WakaTime.sublime-settings'
SETTINGS = {} SETTINGS = {}
LAST_ACTION = { LAST_HEARTBEAT = {
'time': 0, 'time': 0,
'file': None, 'file': None,
'is_write': False, 'is_write': False,
@ -136,8 +137,10 @@ def obfuscate_apikey(command_list):
return cmd return cmd
def enough_time_passed(now, last_time): def enough_time_passed(now, last_heartbeat, is_write):
if now - last_time > ACTION_FREQUENCY * 60: if now - last_heartbeat['time'] > HEARTBEAT_FREQUENCY * 60:
return True
if is_write and now - last_heartbeat['time'] > 2:
return True return True
return False return False
@ -168,20 +171,30 @@ def find_project_from_folders(folders, current_file):
""" """
folder = find_folder_containing_file(folders, current_file) folder = find_folder_containing_file(folders, current_file)
return os.path.basename(folder) return os.path.basename(folder) if folder else None
def handle_action(view, is_write=False): def is_view_active(view):
if view:
active_window = sublime.active_window()
if active_window:
active_view = active_window.active_view()
if active_view:
return active_view.buffer_id() == view.buffer_id()
return False
def handle_heartbeat(view, is_write=False):
window = view.window() window = view.window()
if window is not None: if window is not None:
target_file = view.file_name() target_file = view.file_name()
project = window.project_data() if hasattr(window, 'project_data') else None project = window.project_data() if hasattr(window, 'project_data') else None
folders = window.folders() folders = window.folders()
thread = SendActionThread(target_file, view, is_write=is_write, project=project, folders=folders) thread = SendHeartbeatThread(target_file, view, is_write=is_write, project=project, folders=folders)
thread.start() thread.start()
class SendActionThread(threading.Thread): class SendHeartbeatThread(threading.Thread):
def __init__(self, target_file, view, is_write=False, project=None, folders=None, force=False): def __init__(self, target_file, view, is_write=False, project=None, folders=None, force=False):
threading.Thread.__init__(self) threading.Thread.__init__(self)
@ -194,14 +207,15 @@ class SendActionThread(threading.Thread):
self.debug = SETTINGS.get('debug') self.debug = SETTINGS.get('debug')
self.api_key = SETTINGS.get('api_key', '') self.api_key = SETTINGS.get('api_key', '')
self.ignore = SETTINGS.get('ignore', []) self.ignore = SETTINGS.get('ignore', [])
self.last_action = LAST_ACTION.copy() self.last_heartbeat = LAST_HEARTBEAT.copy()
self.cursorpos = view.sel()[0].begin() if view.sel() else None
self.view = view self.view = view
def run(self): def run(self):
with self.lock: with self.lock:
if self.target_file: if self.target_file:
self.timestamp = time.time() self.timestamp = time.time()
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']): if self.force or self.target_file != self.last_heartbeat['file'] or enough_time_passed(self.timestamp, self.last_heartbeat, self.is_write):
self.send_heartbeat() self.send_heartbeat()
def send_heartbeat(self): def send_heartbeat(self):
@ -219,11 +233,13 @@ class SendActionThread(threading.Thread):
if self.is_write: if self.is_write:
cmd.append('--write') cmd.append('--write')
if self.project and self.project.get('name'): if self.project and self.project.get('name'):
cmd.extend(['--project', self.project.get('name')]) cmd.extend(['--alternate-project', self.project.get('name')])
elif self.folders: elif self.folders:
project_name = find_project_from_folders(self.folders, self.target_file) project_name = find_project_from_folders(self.folders, self.target_file)
if project_name: if project_name:
cmd.extend(['--project', project_name]) cmd.extend(['--alternate-project', project_name])
if self.cursorpos is not None:
cmd.extend(['--cursorpos', '{0}'.format(self.cursorpos)])
for pattern in self.ignore: for pattern in self.ignore:
cmd.extend(['--ignore', pattern]) cmd.extend(['--ignore', pattern])
if self.debug: if self.debug:
@ -243,15 +259,15 @@ class SendActionThread(threading.Thread):
def sent(self): def sent(self):
sublime.set_timeout(self.set_status_bar, 0) sublime.set_timeout(self.set_status_bar, 0)
sublime.set_timeout(self.set_last_action, 0) sublime.set_timeout(self.set_last_heartbeat, 0)
def set_status_bar(self): def set_status_bar(self):
if SETTINGS.get('status_bar_message'): if SETTINGS.get('status_bar_message'):
self.view.set_status('wakatime', 'WakaTime active {0}'.format(datetime.now().strftime('%I:%M %p'))) self.view.set_status('wakatime', datetime.now().strftime(SETTINGS.get('status_bar_message_fmt')))
def set_last_action(self): def set_last_heartbeat(self):
global LAST_ACTION global LAST_HEARTBEAT
LAST_ACTION = { LAST_HEARTBEAT = {
'file': self.target_file, 'file': self.target_file,
'time': self.timestamp, 'time': self.timestamp,
'is_write': self.is_write, 'is_write': self.is_write,
@ -263,8 +279,12 @@ def plugin_loaded():
print('[WakaTime] Initializing WakaTime plugin v%s' % __version__) print('[WakaTime] Initializing WakaTime plugin v%s' % __version__)
if not python_binary(): if not python_binary():
sublime.error_message("Unable to find Python binary!\nWakaTime needs Python to work correctly.\n\nGo to https://www.python.org/downloads") print('[WakaTime] Warning: Python binary not found.')
return if platform.system() == 'Windows':
install_python()
else:
sublime.error_message("Unable to find Python binary!\nWakaTime needs Python to work correctly.\n\nGo to https://www.python.org/downloads")
return
SETTINGS = sublime.load_settings(SETTINGS_FILE) SETTINGS = sublime.load_settings(SETTINGS_FILE)
after_loaded() after_loaded()
@ -275,6 +295,23 @@ def after_loaded():
sublime.set_timeout(after_loaded, 500) sublime.set_timeout(after_loaded, 500)
def install_python():
print('[WakaTime] Downloading and installing python...')
url = 'https://www.python.org/ftp/python/3.4.3/python-3.4.3.msi'
if platform.architecture()[0] == '64bit':
url = 'https://www.python.org/ftp/python/3.4.3/python-3.4.3.amd64.msi'
python_msi = os.path.join(os.path.expanduser('~'), 'python.msi')
urllib.urlretrieve(url, python_msi)
args = [
'msiexec',
'/i',
python_msi,
'/norestart',
'/qb!',
]
Popen(args)
# need to call plugin_loaded because only ST3 will auto-call it # need to call plugin_loaded because only ST3 will auto-call it
if ST_VERSION < 3000: if ST_VERSION < 3000:
plugin_loaded() plugin_loaded()
@ -283,13 +320,15 @@ if ST_VERSION < 3000:
class WakatimeListener(sublime_plugin.EventListener): class WakatimeListener(sublime_plugin.EventListener):
def on_post_save(self, view): def on_post_save(self, view):
handle_action(view, is_write=True) handle_heartbeat(view, is_write=True)
def on_activated(self, view): def on_selection_modified(self, view):
handle_action(view) if is_view_active(view):
handle_heartbeat(view)
def on_modified(self, view): def on_modified(self, view):
handle_action(view) if is_view_active(view):
handle_heartbeat(view)
class WakatimeDashboardCommand(sublime_plugin.ApplicationCommand): class WakatimeDashboardCommand(sublime_plugin.ApplicationCommand):

View File

@ -9,12 +9,15 @@
// Ignore files; Files (including absolute paths) that match one of these // Ignore files; Files (including absolute paths) that match one of these
// POSIX regular expressions will not be logged. // POSIX regular expressions will not be logged.
"ignore": ["^/tmp/", "^/etc/", "^/var/"], "ignore": ["^/tmp/", "^/etc/", "^/var/", "COMMIT_EDITMSG$", "PULLREQ_EDITMSG$", "MERGE_MSG$", "TAG_EDITMSG$"],
// Debug mode. Set to true for verbose logging. Defaults to false. // Debug mode. Set to true for verbose logging. Defaults to false.
"debug": false, "debug": false,
// Status bar message. Set to false to hide status bar message. // Status bar message. Set to false to hide status bar message.
// Defaults to true. // Defaults to true.
"status_bar_message": true "status_bar_message": true,
// Status bar message format.
"status_bar_message_fmt": "WakaTime active %I:%M %p"
} }

View File

@ -1,7 +1,7 @@
__title__ = 'wakatime' __title__ = 'wakatime'
__description__ = 'Common interface to the WakaTime api.' __description__ = 'Common interface to the WakaTime api.'
__url__ = 'https://github.com/wakatime/wakatime' __url__ = 'https://github.com/wakatime/wakatime'
__version_info__ = ('4', '0', '8') __version_info__ = ('4', '1', '0')
__version__ = '.'.join(__version_info__) __version__ = '.'.join(__version_info__)
__author__ = 'Alan Hamlett' __author__ = 'Alan Hamlett'
__author_email__ = 'alan@wakatime.com' __author_email__ = 'alan@wakatime.com'

View File

@ -19,6 +19,7 @@ import re
import sys import sys
import time import time
import traceback import traceback
import socket
try: try:
import ConfigParser as configparser import ConfigParser as configparser
except ImportError: except ImportError:
@ -29,14 +30,14 @@ sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pac
from .__about__ import __version__ from .__about__ import __version__
from .compat import u, open, is_py3 from .compat import u, open, is_py3
from .offlinequeue import Queue
from .logger import setup_logging from .logger import setup_logging
from .project import find_project from .offlinequeue import Queue
from .stats import get_file_stats
from .packages import argparse from .packages import argparse
from .packages import simplejson as json from .packages import simplejson as json
from .packages import requests
from .packages.requests.exceptions import RequestException from .packages.requests.exceptions import RequestException
from .project import get_project_info
from .session_cache import SessionCache
from .stats import get_file_stats
try: try:
from .packages import tzlocal from .packages import tzlocal
except: except:
@ -147,14 +148,21 @@ def parseArguments(argv):
type=float, type=float,
help='optional floating-point unix epoch timestamp; '+ help='optional floating-point unix epoch timestamp; '+
'uses current time by default') 'uses current time by default')
parser.add_argument('--lineno', dest='lineno',
help='optional line number; current line being edited')
parser.add_argument('--cursorpos', dest='cursorpos',
help='optional cursor position in the current file')
parser.add_argument('--notfile', dest='notfile', action='store_true', parser.add_argument('--notfile', dest='notfile', action='store_true',
help='when set, will accept any value for the file. for example, '+ help='when set, will accept any value for the file. for example, '+
'a domain name or other item you want to log time towards.') 'a domain name or other item you want to log time towards.')
parser.add_argument('--proxy', dest='proxy', parser.add_argument('--proxy', dest='proxy',
help='optional https proxy url; for example: '+ help='optional https proxy url; for example: '+
'https://user:pass@localhost:8080') 'https://user:pass@localhost:8080')
parser.add_argument('--project', dest='project_name', parser.add_argument('--project', dest='project',
help='optional project name; auto-discovered project takes priority') help='optional project name')
parser.add_argument('--alternate-project', dest='alternate_project',
help='optional alternate project name; auto-discovered project takes priority')
parser.add_argument('--hostname', dest='hostname', help='hostname of current machine.')
parser.add_argument('--disableoffline', dest='offline', parser.add_argument('--disableoffline', dest='offline',
action='store_false', action='store_false',
help='disables offline time logging instead of queuing logged time') help='disables offline time logging instead of queuing logged time')
@ -297,7 +305,7 @@ def get_user_agent(plugin):
return user_agent return user_agent
def send_heartbeat(project=None, branch=None, stats={}, key=None, targetFile=None, def send_heartbeat(project=None, branch=None, hostname=None, stats={}, key=None, targetFile=None,
timestamp=None, isWrite=None, plugin=None, offline=None, notfile=False, timestamp=None, isWrite=None, plugin=None, offline=None, notfile=False,
hidefilenames=None, proxy=None, api_url=None, **kwargs): hidefilenames=None, proxy=None, api_url=None, **kwargs):
"""Sends heartbeat as POST request to WakaTime api server. """Sends heartbeat as POST request to WakaTime api server.
@ -308,20 +316,25 @@ def send_heartbeat(project=None, branch=None, stats={}, key=None, targetFile=Non
log.debug('Sending heartbeat to api at %s' % api_url) log.debug('Sending heartbeat to api at %s' % api_url)
data = { data = {
'time': timestamp, 'time': timestamp,
'file': targetFile, 'entity': targetFile,
'type': 'file',
} }
if hidefilenames and targetFile is not None and not notfile: if hidefilenames and targetFile is not None and not notfile:
data['file'] = data['file'].rsplit('/', 1)[-1].rsplit('\\', 1)[-1] data['entity'] = data['entity'].rsplit('/', 1)[-1].rsplit('\\', 1)[-1]
if len(data['file'].strip('.').split('.', 1)) > 1: if len(data['entity'].strip('.').split('.', 1)) > 1:
data['file'] = u('HIDDEN.{ext}').format(ext=u(data['file'].strip('.').rsplit('.', 1)[-1])) data['entity'] = u('HIDDEN.{ext}').format(ext=u(data['entity'].strip('.').rsplit('.', 1)[-1]))
else: else:
data['file'] = u('HIDDEN') data['entity'] = u('HIDDEN')
if stats.get('lines'): if stats.get('lines'):
data['lines'] = stats['lines'] data['lines'] = stats['lines']
if stats.get('language'): if stats.get('language'):
data['language'] = stats['language'] data['language'] = stats['language']
if stats.get('dependencies'): if stats.get('dependencies'):
data['dependencies'] = stats['dependencies'] data['dependencies'] = stats['dependencies']
if stats.get('lineno'):
data['lineno'] = stats['lineno']
if stats.get('cursorpos'):
data['cursorpos'] = stats['cursorpos']
if isWrite: if isWrite:
data['is_write'] = isWrite data['is_write'] = isWrite
if project: if project:
@ -340,6 +353,8 @@ def send_heartbeat(project=None, branch=None, stats={}, key=None, targetFile=Non
'Accept': 'application/json', 'Accept': 'application/json',
'Authorization': auth, 'Authorization': auth,
} }
if hostname:
headers['X-Machine-Name'] = hostname
proxies = {} proxies = {}
if proxy: if proxy:
proxies['https'] = proxy proxies['https'] = proxy
@ -352,10 +367,13 @@ def send_heartbeat(project=None, branch=None, stats={}, key=None, targetFile=Non
if tz: if tz:
headers['TimeZone'] = u(tz.zone) headers['TimeZone'] = u(tz.zone)
session_cache = SessionCache()
session = session_cache.get()
# log time to api # log time to api
response = None response = None
try: try:
response = requests.post(api_url, data=request_body, headers=headers, response = session.post(api_url, data=request_body, headers=headers,
proxies=proxies) proxies=proxies)
except RequestException: except RequestException:
exception_data = { exception_data = {
@ -377,6 +395,7 @@ def send_heartbeat(project=None, branch=None, stats={}, key=None, targetFile=Non
log.debug({ log.debug({
'response_code': response_code, 'response_code': response_code,
}) })
session_cache.save(session)
return True return True
if offline: if offline:
if response_code != 400: if response_code != 400:
@ -402,6 +421,7 @@ def send_heartbeat(project=None, branch=None, stats={}, key=None, targetFile=Non
'response_code': response_code, 'response_code': response_code,
'response_content': response_content, 'response_content': response_content,
}) })
session_cache.delete()
return False return False
@ -424,23 +444,20 @@ def main(argv=None):
if os.path.isfile(args.targetFile) or args.notfile: if os.path.isfile(args.targetFile) or args.notfile:
stats = get_file_stats(args.targetFile, notfile=args.notfile) stats = get_file_stats(args.targetFile, notfile=args.notfile,
lineno=args.lineno, cursorpos=args.cursorpos)
project = None project, branch = None, None
if not args.notfile: if not args.notfile:
project = find_project(args.targetFile, configs=configs) project, branch = get_project_info(configs=configs, args=args)
branch = None
project_name = args.project_name
if project:
branch = project.branch()
project_name = project.name()
if send_heartbeat( kwargs = vars(args)
project=project_name, kwargs['project'] = project
branch=branch, kwargs['branch'] = branch
stats=stats, kwargs['stats'] = stats
**vars(args) kwargs['hostname'] = args.hostname or socket.gethostname()
):
if send_heartbeat(**kwargs):
queue = Queue() queue = Queue()
while True: while True:
heartbeat = queue.pop() heartbeat = queue.pop()
@ -451,6 +468,7 @@ def main(argv=None):
targetFile=heartbeat['file'], targetFile=heartbeat['file'],
timestamp=heartbeat['time'], timestamp=heartbeat['time'],
branch=heartbeat['branch'], branch=heartbeat['branch'],
hostname=kwargs['hostname'],
stats=json.loads(heartbeat['stats']), stats=json.loads(heartbeat['stats']),
key=args.key, key=args.key,
isWrite=heartbeat['is_write'], isWrite=heartbeat['is_write'],

View File

@ -11,8 +11,25 @@
import os import os
import sys import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import wakatime
# get path to local wakatime package
package_folder = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# add local wakatime package to sys.path
sys.path.insert(0, package_folder)
# import local wakatime package
try:
import wakatime
except TypeError:
# on Windows, non-ASCII characters in import path can be fixed using
# the script path from sys.argv[0].
# More info at https://github.com/wakatime/wakatime/issues/32
package_folder = os.path.dirname(os.path.dirname(os.path.abspath(sys.argv[0])))
sys.path.insert(0, package_folder)
import wakatime
if __name__ == '__main__': if __name__ == '__main__':
sys.exit(wakatime.main(sys.argv)) sys.exit(wakatime.main(sys.argv))

View File

@ -37,28 +37,34 @@ class CustomEncoder(json.JSONEncoder):
class JsonFormatter(logging.Formatter): class JsonFormatter(logging.Formatter):
def setup(self, timestamp, isWrite, targetFile, version, plugin): def setup(self, timestamp, isWrite, targetFile, version, plugin, verbose,
warnings=False):
self.timestamp = timestamp self.timestamp = timestamp
self.isWrite = isWrite self.isWrite = isWrite
self.targetFile = targetFile self.targetFile = targetFile
self.version = version self.version = version
self.plugin = plugin self.plugin = plugin
self.verbose = verbose
self.warnings = warnings
def format(self, record): def format(self, record, *args):
data = OrderedDict([ data = OrderedDict([
('now', self.formatTime(record, self.datefmt)), ('now', self.formatTime(record, self.datefmt)),
('version', self.version),
('plugin', self.plugin),
('time', self.timestamp),
('isWrite', self.isWrite),
('file', self.targetFile),
('level', record.levelname),
('message', record.msg),
]) ])
data['version'] = self.version
data['plugin'] = self.plugin
data['time'] = self.timestamp
if self.verbose:
data['caller'] = record.pathname
data['lineno'] = record.lineno
data['isWrite'] = self.isWrite
data['file'] = self.targetFile
if not self.isWrite:
del data['isWrite']
data['level'] = record.levelname
data['message'] = record.getMessage() if self.warnings else record.msg
if not self.plugin: if not self.plugin:
del data['plugin'] del data['plugin']
if not self.isWrite:
del data['isWrite']
return CustomEncoder().encode(data) return CustomEncoder().encode(data)
def formatException(self, exc_info): def formatException(self, exc_info):
@ -73,7 +79,6 @@ def set_log_level(logger, args):
def setup_logging(args, version): def setup_logging(args, version):
logging.captureWarnings(True)
logger = logging.getLogger('WakaTime') logger = logging.getLogger('WakaTime')
set_log_level(logger, args) set_log_level(logger, args)
if len(logger.handlers) > 0: if len(logger.handlers) > 0:
@ -84,6 +89,7 @@ def setup_logging(args, version):
targetFile=args.targetFile, targetFile=args.targetFile,
version=version, version=version,
plugin=args.plugin, plugin=args.plugin,
verbose=args.verbose,
) )
logger.handlers[0].setFormatter(formatter) logger.handlers[0].setFormatter(formatter)
return logger return logger
@ -98,7 +104,27 @@ def setup_logging(args, version):
targetFile=args.targetFile, targetFile=args.targetFile,
version=version, version=version,
plugin=args.plugin, plugin=args.plugin,
verbose=args.verbose,
) )
handler.setFormatter(formatter) handler.setFormatter(formatter)
logger.addHandler(handler) logger.addHandler(handler)
warnings_formatter = JsonFormatter(datefmt='%Y/%m/%d %H:%M:%S %z')
warnings_formatter.setup(
timestamp=args.timestamp,
isWrite=args.isWrite,
targetFile=args.targetFile,
version=version,
plugin=args.plugin,
verbose=args.verbose,
warnings=True,
)
warnings_handler = logging.FileHandler(os.path.expanduser(logfile))
warnings_handler.setFormatter(warnings_formatter)
logging.getLogger('py.warnings').addHandler(warnings_handler)
try:
logging.captureWarnings(True)
except AttributeError:
pass # Python >= 2.7 is needed to capture warnings
return logger return logger

View File

@ -1,10 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
wakatime.queue wakatime.offlinequeue
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~
Queue for offline time logging. Queue for saving heartbeats while offline.
http://wakatime.com
:copyright: (c) 2014 Alan Hamlett. :copyright: (c) 2014 Alan Hamlett.
:license: BSD, see LICENSE for more details. :license: BSD, see LICENSE for more details.
@ -51,7 +50,7 @@ class Queue(object):
try: try:
conn, c = self.connect() conn, c = self.connect()
heartbeat = { heartbeat = {
'file': data.get('file'), 'file': data.get('entity'),
'time': data.get('time'), 'time': data.get('time'),
'project': data.get('project'), 'project': data.get('project'),
'branch': data.get('branch'), 'branch': data.get('branch'),

View File

@ -15,30 +15,70 @@ from .projects.git import Git
from .projects.mercurial import Mercurial from .projects.mercurial import Mercurial
from .projects.projectmap import ProjectMap from .projects.projectmap import ProjectMap
from .projects.subversion import Subversion from .projects.subversion import Subversion
from .projects.wakatime import WakaTime from .projects.wakatime_project_file import WakaTimeProjectFile
log = logging.getLogger('WakaTime') log = logging.getLogger('WakaTime')
# List of plugin classes to find a project for the current file path. # List of plugin classes to find a project for the current file path.
# Project plugins will be processed with priority in the order below. CONFIG_PLUGINS = [
PLUGINS = [ WakaTimeProjectFile,
WakaTime,
ProjectMap, ProjectMap,
]
REV_CONTROL_PLUGINS = [
Git, Git,
Mercurial, Mercurial,
Subversion, Subversion,
] ]
def find_project(path, configs=None): def get_project_info(configs=None, args=None):
for plugin in PLUGINS: """Find the current project and branch.
plugin_name = plugin.__name__.lower()
plugin_configs = None First looks for a .wakatime-project file. Second, uses the --project arg.
if configs and configs.has_section(plugin_name): Third, uses the folder name from a revision control repository. Last, uses
plugin_configs = dict(configs.items(plugin_name)) the --alternate-project arg.
project = plugin(path, configs=plugin_configs)
Returns a project, branch tuple.
"""
project_name, branch_name = None, None
for plugin_cls in CONFIG_PLUGINS:
plugin_name = plugin_cls.__name__.lower()
plugin_configs = get_configs_for_plugin(plugin_name, configs)
project = plugin_cls(args.targetFile, configs=plugin_configs)
if project.process(): if project.process():
return project project_name = project.name()
branch_name = project.branch()
break
if project_name is None:
project_name = args.project
if project_name is None or branch_name is None:
for plugin_cls in REV_CONTROL_PLUGINS:
plugin_name = plugin_cls.__name__.lower()
plugin_configs = get_configs_for_plugin(plugin_name, configs)
project = plugin_cls(args.targetFile, configs=plugin_configs)
if project.process():
project_name = project_name or project.name()
branch_name = branch_name or project.branch()
break
if project_name is None:
project_name = args.alternate_project
return project_name, branch_name
def get_configs_for_plugin(plugin_name, configs):
if configs and configs.has_section(plugin_name):
return dict(configs.items(plugin_name))
return None return None

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
wakatime.projects.wakatime wakatime.projects.wakatime_project_file
~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Information from a .wakatime-project file about the project for Information from a .wakatime-project file about the project for
a given file. First line of .wakatime-project sets the project a given file. First line of .wakatime-project sets the project
@ -21,7 +21,7 @@ from ..compat import u, open
log = logging.getLogger('WakaTime') log = logging.getLogger('WakaTime')
class WakaTime(BaseProject): class WakaTimeProjectFile(BaseProject):
def process(self): def process(self):
self.config = self._find_config(self.path) self.config = self._find_config(self.path)

View File

@ -0,0 +1,109 @@
# -*- coding: utf-8 -*-
"""
wakatime.session_cache
~~~~~~~~~~~~~~~~~~~~~~
Persist requests.Session for multiprocess SSL handshake pooling.
:copyright: (c) 2015 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
import logging
import os
import pickle
import sys
import traceback
try:
import sqlite3
HAS_SQL = True
except ImportError:
HAS_SQL = False
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'packages'))
from .packages import requests
log = logging.getLogger('WakaTime')
class SessionCache(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 session (
value BLOB)
''')
return (conn, c)
def save(self, session):
"""Saves a requests.Session object for the next heartbeat process.
"""
if not HAS_SQL:
return
try:
conn, c = self.connect()
c.execute('DELETE FROM session')
values = {
'value': pickle.dumps(session),
}
c.execute('INSERT INTO session VALUES (:value)', values)
conn.commit()
conn.close()
except:
log.error(traceback.format_exc())
def get(self):
"""Returns a requests.Session object.
Gets Session from sqlite3 cache or creates a new Session.
"""
if not HAS_SQL:
return requests.session()
try:
conn, c = self.connect()
except:
log.error(traceback.format_exc())
return requests.session()
session = None
try:
c.execute('BEGIN IMMEDIATE')
c.execute('SELECT value FROM session LIMIT 1')
row = c.fetchone()
if row is not None:
session = pickle.loads(row[0])
except:
log.error(traceback.format_exc())
try:
conn.close()
except:
log.error(traceback.format_exc())
return session if session is not None else requests.session()
def delete(self):
"""Clears all cached Session objects.
"""
if not HAS_SQL:
return
try:
conn, c = self.connect()
c.execute('DELETE FROM session')
conn.commit()
conn.close()
except:
log.error(traceback.format_exc())

View File

@ -20,13 +20,15 @@ if sys.version_info[0] == 2:
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'packages', 'pygments_py2')) sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'packages', 'pygments_py2'))
else: else:
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'packages', 'pygments_py3')) sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'packages', 'pygments_py3'))
from pygments.lexers import guess_lexer_for_filename from pygments.lexers import get_lexer_by_name, guess_lexer_for_filename
from pygments.modeline import get_filetype_from_buffer
from pygments.util import ClassNotFound
log = logging.getLogger('WakaTime') log = logging.getLogger('WakaTime')
# force file name extensions to be recognized as a certain language # extensions taking priority over lexer
EXTENSIONS = { EXTENSIONS = {
'j2': 'HTML', 'j2': 'HTML',
'markdown': 'Markdown', 'markdown': 'Markdown',
@ -34,6 +36,8 @@ EXTENSIONS = {
'mdown': 'Markdown', 'mdown': 'Markdown',
'twig': 'Twig', 'twig': 'Twig',
} }
# lexers to human readable languages
TRANSLATIONS = { TRANSLATIONS = {
'CSS+Genshi Text': 'CSS', 'CSS+Genshi Text': 'CSS',
'CSS+Lasso': 'CSS', 'CSS+Lasso': 'CSS',
@ -45,31 +49,132 @@ TRANSLATIONS = {
'RHTML': 'HTML', 'RHTML': 'HTML',
} }
# extensions for when no lexer is found
AUXILIARY_EXTENSIONS = {
'vb': 'VB.net',
}
def guess_language(file_name): def guess_language(file_name):
language, lexer = None, None """Guess lexer and language for a file.
try:
with open(file_name, 'r', encoding='utf-8') as fh: Returns (language, lexer) tuple where language is a unicode string.
lexer = guess_lexer_for_filename(file_name, fh.read(512000)) """
except:
pass lexer = smart_guess_lexer(file_name)
language = None
# guess language from file extension
if file_name: if file_name:
language = guess_language_from_extension(file_name.rsplit('.', 1)[-1]) language = get_language_from_extension(file_name, EXTENSIONS)
if lexer and language is None:
language = translate_language(u(lexer.name)) # get language from lexer if we didn't have a hard-coded extension rule
if language is None and lexer:
language = u(lexer.name)
if language is None:
language = get_language_from_extension(file_name, AUXILIARY_EXTENSIONS)
if language is not None:
language = translate_language(language)
return language, lexer return language, lexer
def guess_language_from_extension(extension): def smart_guess_lexer(file_name):
"""Guess Pygments lexer for a file.
Looks for a vim modeline in file contents, then compares the accuracy
of that lexer with a second guess. The second guess looks up all lexers
matching the file name, then runs a text analysis for the best choice.
"""
lexer = None
text = get_file_contents(file_name)
lexer_1, accuracy_1 = guess_lexer_using_filename(file_name, text)
lexer_2, accuracy_2 = guess_lexer_using_modeline(text)
if lexer_1:
lexer = lexer_1
if (lexer_2 and accuracy_2 and
(not accuracy_1 or accuracy_2 > accuracy_1)):
lexer = lexer_2
return lexer
def guess_lexer_using_filename(file_name, text):
"""Guess lexer for given text, limited to lexers for this file's extension.
Returns a tuple of (lexer, accuracy).
"""
lexer, accuracy = None, None
try:
lexer = guess_lexer_for_filename(file_name, text)
except:
pass
if lexer is not None:
try:
accuracy = lexer.analyse_text(text)
except:
pass
return lexer, accuracy
def guess_lexer_using_modeline(text):
"""Guess lexer for given text using Vim modeline.
Returns a tuple of (lexer, accuracy).
"""
lexer, accuracy = None, None
file_type = None
try:
file_type = get_filetype_from_buffer(text)
except:
pass
if file_type is not None:
try:
lexer = get_lexer_by_name(file_type)
except ClassNotFound:
pass
if lexer is not None:
try:
accuracy = lexer.analyse_text(text)
except:
pass
return lexer, accuracy
def get_language_from_extension(file_name, extension_map):
"""Returns a matching language for the given file_name using extension_map.
"""
extension = file_name.rsplit('.', 1)[-1] if len(file_name.rsplit('.', 1)) > 1 else None
if extension: if extension:
if extension in EXTENSIONS: if extension in extension_map:
return EXTENSIONS[extension] return extension_map[extension]
if extension.lower() in EXTENSIONS: if extension.lower() in extension_map:
return EXTENSIONS[extension.lower()] return extension_map[extension.lower()]
return None return None
def translate_language(language): def translate_language(language):
"""Turns Pygments lexer class name string into human-readable language.
"""
if language in TRANSLATIONS: if language in TRANSLATIONS:
language = TRANSLATIONS[language] language = TRANSLATIONS[language]
return language return language
@ -86,12 +191,14 @@ def number_lines_in_file(file_name):
return lines return lines
def get_file_stats(file_name, notfile=False): def get_file_stats(file_name, notfile=False, lineno=None, cursorpos=None):
if notfile: if notfile:
stats = { stats = {
'language': None, 'language': None,
'dependencies': [], 'dependencies': [],
'lines': None, 'lines': None,
'lineno': lineno,
'cursorpos': cursorpos,
} }
else: else:
language, lexer = guess_language(file_name) language, lexer = guess_language(file_name)
@ -101,5 +208,20 @@ def get_file_stats(file_name, notfile=False):
'language': language, 'language': language,
'dependencies': dependencies, 'dependencies': dependencies,
'lines': number_lines_in_file(file_name), 'lines': number_lines_in_file(file_name),
'lineno': lineno,
'cursorpos': cursorpos,
} }
return stats return stats
def get_file_contents(file_name):
"""Returns the first 512000 bytes of the file's contents.
"""
text = None
try:
with open(file_name, 'r', encoding='utf-8') as fh:
text = fh.read(512000)
except:
pass
return text