Compare commits

...

64 Commits

Author SHA1 Message Date
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
68a2557884 v3.0.18 2015-04-04 11:05:35 -07:00
c7ee7258fb upgrade wakatime cli to v4.0.8 2015-04-04 11:03:55 -07:00
aaff2503fb v3.0.17 2015-04-02 16:42:50 -07:00
00a1193bd3 use open folder as current project when not using revision control 2015-04-02 16:41:55 -07:00
2371daac1b v3.0.16 2015-04-02 15:05:06 -07:00
4395db2b2d copy list so we don't obfuscate api key in original list 2015-04-02 15:04:05 -07:00
fc8c61fa3f v3.0.15 2015-04-01 13:02:00 -07:00
aa30110343 obfuscate api key when logging to ST console 2015-04-01 13:01:10 -07:00
b671856341 v3.0.14 2015-03-31 16:21:05 -07:00
b801759cdf always use external python binary because sublime text bundled binary does not fully support SSL with requests package 2015-03-31 16:19:30 -07:00
919064200b update wakatime cli to v4.0.6 2015-03-31 15:44:48 -07:00
911b5656d7 prevent exception when view.window() is None 2015-03-25 11:08:27 -07:00
48bbab33b4 v3.0.13 2015-03-24 10:05:23 -07:00
3b2aafe004 check built-in SSL more effectively 2015-03-24 10:00:53 -07:00
aa0b2d6d70 v3.0.12 2015-03-23 15:06:27 -07:00
1a6f588d94 always use unicode function from compat module when encoding log messages 2015-03-23 15:05:37 -07:00
373ebf933f v3.0.11 2015-03-23 14:01:40 -07:00
7fb47228f9 update simplejson to v3.6.5 2015-03-23 14:00:40 -07:00
4fca5e1c06 v3.0.10 2015-03-22 17:14:20 -07:00
cb2d126c47 ability to disable status bar message 2015-03-22 17:13:32 -07:00
17404bf848 v3.0.9 2015-03-20 01:16:34 -07:00
510eea0a8b status message showing when WakaTime is enabled 2015-03-20 01:14:53 -07:00
d16d1ca747 v3.0.8 2015-03-09 15:25:40 -07:00
440e33b8b7 upgrade wakatime cli to v4.0.4 2015-03-09 15:23:29 -07:00
307029c37a detect python binary by executing interpreter 2015-03-09 14:32:27 -07:00
60c8ea4454 camelcase 2015-02-23 22:31:12 -08:00
e4fe604a93 move import to top of file. remove unused imports. 2015-02-23 22:20:27 -08:00
308187b2ed Merge pull request #31 from freewizard/master
add "Wakatime: Open Dashboard" into command palette
2015-02-23 22:16:39 -08:00
97f4077675 add "Wakatime: Open Dashboard" into command palette 2015-02-23 17:59:10 -05:00
4960289ed1 v3.0.7 2015-02-05 17:42:19 -08:00
82530cef4f handle errors from reading project directory while looking for .sublime-project file 2015-02-05 17:40:45 -08:00
08172098e2 v3.0.6 2015-01-13 14:07:23 -08:00
56f54fb064 upgrade wakatime-cli to v3.0.5 2015-01-13 14:06:19 -08:00
1bea7cde8c update name of Package Control 2015-01-06 14:07:55 -08:00
038847e665 v3.0.5 2015-01-06 11:38:26 -08:00
d233494a39 upgrade wakatime-cli to v3.0.4 2015-01-06 11:37:09 -08:00
070ad5a023 use new sublime package control domain 2015-01-04 15:31:42 -08:00
757a4c6905 v3.0.4 2014-12-26 18:21:13 -06:00
dd61a4f5f4 fix reading config file in python2 2014-12-26 18:18:52 -06:00
69f9bbdc78 v3.0.3 2014-12-25 13:37:22 -06:00
e1dc4039fd update wakatime-cli to v3.0.3 2014-12-25 13:36:47 -06:00
7c07925527 v3.0.2 2014-12-25 01:05:12 -06:00
ee8c0dfed8 upgrade wakatime-cli to v3.0.2 2014-12-25 01:04:39 -06:00
ad4df93b04 create .wakatime.cfg INI file if it does not already exist 2014-12-24 12:01:56 -06:00
9a600df969 v3.0.1 2014-12-23 12:43:11 -06:00
a0abeac3e2 parse use namespaces from php files 2014-12-23 12:42:19 -06:00
12b8c36c5f v3.0.0 2014-12-23 05:41:18 -06:00
7d4d50ee62 upgrade wakatime-cli to v3.0.1 2014-12-23 05:39:07 -06:00
520db283cb v2.1.21 2014-12-22 01:02:08 -06:00
f3179b75d9 upgrade wakatime-cli to v2.1.11 2014-12-22 01:01:09 -06:00
1bc8b9b9c7 add authors file and update license 2014-12-21 19:50:32 -06:00
584d109357 v2.0.20 2014-12-05 02:40:54 -08:00
327c0e448b upgrade external wakatime-cli to v2.1.9 2014-12-05 02:40:07 -08:00
1096 changed files with 23751 additions and 998 deletions

View File

@ -11,5 +11,5 @@ Development Lead
Patches and Suggestions
-----------------------
- 3onyc <3onyc@x3tech.com>
- userid <xixico@ymail.com>
- Jimmy Selgen Nielsen <jimmy.selgen@gmail.com>
- Patrik Kernstock <info@pkern.at>

6
Default.sublime-commands Normal file
View File

@ -0,0 +1,6 @@
[
{
"caption": "WakaTime: Open Dashboard",
"command": "wakatime_dashboard"
}
]

View File

@ -3,6 +3,166 @@ History
-------
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)
+++++++++++++++++++
- upgrade wakatime cli to v4.0.8
- added api_url config option to .wakatime.cfg file
3.0.17 (2015-04-02)
+++++++++++++++++++
- use open folder as current project when not using revision control
3.0.16 (2015-04-02)
+++++++++++++++++++
- copy list when obfuscating api key so original command is not modified
3.0.15 (2015-04-01)
+++++++++++++++++++
- obfuscate api key when logging to Sublime Text Console in debug mode
3.0.14 (2015-03-31)
+++++++++++++++++++
- always use external python binary because ST builtin python does not support checking SSL certs
- upgrade wakatime cli to v4.0.6
3.0.13 (2015-03-23)
+++++++++++++++++++
- correctly check for SSL support in ST built-in python
- fix status bar message
3.0.12 (2015-03-23)
+++++++++++++++++++
- always use unicode function from compat module when encoding log messages
3.0.11 (2015-03-23)
+++++++++++++++++++
- upgrade simplejson package to v3.6.5
3.0.10 (2015-03-22)
+++++++++++++++++++
- ability to disable status bar message from WakaTime.sublime-settings file
3.0.9 (2015-03-20)
++++++++++++++++++
- status bar message showing when WakaTime plugin is enabled
- moved some logic into thread to help prevent slow plugin warning message
3.0.8 (2015-03-09)
++++++++++++++++++
- upgrade wakatime cli to v4.0.4
- use requests library instead of urllib2, so api SSL cert is verified
- new --notfile argument to support logging time without a real file
- new --proxy argument for https proxy support
- new options for excluding and including directories
3.0.7 (2015-02-05)
++++++++++++++++++
- handle errors encountered when looking for .sublime-project file
3.0.6 (2015-01-13)
++++++++++++++++++
- upgrade external wakatime package to v3.0.5
- ignore errors from malformed markup (too many closing tags)
3.0.5 (2015-01-06)
++++++++++++++++++
- upgrade external wakatime package to v3.0.4
- remove unused dependency, which is missing in some python environments
3.0.4 (2014-12-26)
++++++++++++++++++
- fix bug causing plugin to not work in Sublime Text 2
3.0.3 (2014-12-25)
++++++++++++++++++
- upgrade external wakatime package to v3.0.3
- detect JavaScript frameworks from script tags in Html template files
3.0.2 (2014-12-25)
++++++++++++++++++
- upgrade external wakatime package to v3.0.2
- detect frameworks from JavaScript and JSON files
3.0.1 (2014-12-23)
++++++++++++++++++
- parse use namespaces from php files
3.0.0 (2014-12-23)
++++++++++++++++++
- upgrade external wakatime package to v3.0.1
- detect libraries and frameworks for C++, Java, .NET, PHP, and Python files
2.0.21 (2014-12-22)
++++++++++++++++++
- upgrade external wakatime package to v2.1.11
- fix bug in offline logging when no response from api
2.0.20 (2014-12-05)
++++++++++++++++++
- upgrade external wakatime package to v2.1.9
- fix bug preventing offline heartbeats from being purged after uploaded
2.0.19 (2014-12-04)
++++++++++++++++++

View File

@ -1,4 +1,6 @@
Copyright (c) 2014 Alan Hamlett https://wakatime.com
BSD 3-Clause License
Copyright (c) 2014 by the respective authors (see AUTHORS file).
All rights reserved.
Redistribution and use in source and binary forms, with or without
@ -12,7 +14,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 WakaTime, nor the names of its
* Neither the names of 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

@ -8,9 +8,9 @@ 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. Install [Sublime Package Control](https://sublime.wbond.net/installation).
1. Install [Package Control](https://packagecontrol.io/installation).
2. Using [Sublime Package Control](http://wbond.net/sublime_packages/package_control):
2. Using [Package Control](https://packagecontrol.io/docs/usage):
a) Inside Sublime, press `ctrl+shift+p`(Windows, Linux) or `cmd+shift+p`(OS X).
@ -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.
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.

View File

@ -6,7 +6,9 @@ License: BSD, see LICENSE for more details.
Website: https://wakatime.com/
==========================================================="""
__version__ = '2.0.19'
__version__ = '4.0.1'
import sublime
import sublime_plugin
@ -17,42 +19,67 @@ import platform
import sys
import time
import threading
import uuid
from os.path import expanduser, dirname, basename, realpath, isfile, join, exists
import webbrowser
from datetime import datetime
from subprocess import Popen
# globals
ACTION_FREQUENCY = 2
HEARTBEAT_FREQUENCY = 2
ST_VERSION = int(sublime.version())
PLUGIN_DIR = dirname(realpath(__file__))
API_CLIENT = '%s/packages/wakatime/wakatime-cli.py' % PLUGIN_DIR
PLUGIN_DIR = os.path.dirname(os.path.realpath(__file__))
API_CLIENT = os.path.join(PLUGIN_DIR, 'packages', 'wakatime', 'cli.py')
SETTINGS_FILE = 'WakaTime.sublime-settings'
SETTINGS = {}
LAST_ACTION = {
LAST_HEARTBEAT = {
'time': 0,
'file': None,
'is_write': False,
}
HAS_SSL = False
LOCK = threading.RLock()
PYTHON_LOCATION = None
# check if we have SSL support
# add wakatime package to path
sys.path.insert(0, os.path.join(PLUGIN_DIR, 'packages'))
try:
import ssl
import socket
socket.ssl
HAS_SSL = True
except (ImportError, AttributeError):
from subprocess import Popen
from wakatime.base import parseConfigFile
except ImportError:
pass
if HAS_SSL:
# import wakatime package
sys.path.insert(0, join(PLUGIN_DIR, 'packages', 'wakatime'))
import wakatime
def createConfigFile():
"""Creates the .wakatime.cfg INI file in $HOME directory, if it does
not already exist.
"""
configFile = os.path.join(os.path.expanduser('~'), '.wakatime.cfg')
try:
with open(configFile) as fh:
pass
except IOError:
try:
with open(configFile, 'w') as fh:
fh.write("[settings]\n")
fh.write("debug = false\n")
fh.write("hidefilenames = false\n")
except IOError:
pass
def prompt_api_key():
global SETTINGS
createConfigFile()
default_key = ''
try:
configs = parseConfigFile()
if configs is not None:
if configs.has_option('settings', 'api_key'):
default_key = configs.get('settings', 'api_key')
except:
pass
if SETTINGS.get('api_key'):
return True
else:
@ -62,7 +89,7 @@ def prompt_api_key():
sublime.save_settings(SETTINGS_FILE)
window = sublime.active_window()
if window:
window.show_input_panel('[WakaTime] Enter your wakatime.com api key:', '', got_key, None, None)
window.show_input_panel('[WakaTime] Enter your wakatime.com api key:', default_key, got_key, None, None)
return True
else:
print('[WakaTime] Error: Could not prompt for api key because no window found.')
@ -70,53 +97,97 @@ def prompt_api_key():
def python_binary():
if platform.system() == 'Windows':
global PYTHON_LOCATION
if PYTHON_LOCATION is not None:
return PYTHON_LOCATION
paths = [
"pythonw",
"python",
"/usr/local/bin/python",
"/usr/bin/python",
]
for path in paths:
try:
Popen(['pythonw', '--version'])
return 'pythonw'
Popen([path, '--version'])
PYTHON_LOCATION = path
return path
except:
for path in glob.iglob('/python*'):
if exists(realpath(join(path, 'pythonw.exe'))):
return realpath(join(path, 'pythonw'))
return None
return 'python'
pass
for path in glob.iglob('/python*'):
path = os.path.realpath(os.path.join(path, 'pythonw'))
try:
Popen([path, '--version'])
PYTHON_LOCATION = path
return path
except:
pass
return None
def enough_time_passed(now, last_time):
if now - last_time > ACTION_FREQUENCY * 60:
def obfuscate_apikey(command_list):
cmd = list(command_list)
apikey_index = None
for num in range(len(cmd)):
if cmd[num] == '--key':
apikey_index = num + 1
break
if apikey_index is not None and apikey_index < len(cmd):
cmd[apikey_index] = '********-****-****-****-********' + cmd[apikey_index][-4:]
return cmd
def enough_time_passed(now, last_heartbeat, is_write):
if now - last_heartbeat['time'] > HEARTBEAT_FREQUENCY * 60:
return True
if is_write and now - last_heartbeat['time'] > 2:
return True
return False
def find_project_name_from_folders(folders):
for folder in folders:
for file_name in os.listdir(folder):
if file_name.endswith('.sublime-project'):
return file_name.replace('.sublime-project', '', 1)
return None
def find_folder_containing_file(folders, current_file):
"""Returns absolute path to folder containing the file.
"""
parent_folder = None
current_folder = current_file
while True:
for folder in folders:
if os.path.realpath(os.path.dirname(current_folder)) == os.path.realpath(folder):
parent_folder = folder
break
if parent_folder is not None:
break
if not current_folder or os.path.dirname(current_folder) == current_folder:
break
current_folder = os.path.dirname(current_folder)
return parent_folder
def handle_action(view, is_write=False):
global LOCK, LAST_ACTION
with LOCK:
def find_project_from_folders(folders, current_file):
"""Find project name from open folders.
"""
folder = find_folder_containing_file(folders, current_file)
return os.path.basename(folder) if folder else None
def handle_heartbeat(view, is_write=False):
window = view.window()
if window is not None:
target_file = view.file_name()
if target_file:
project = view.window().project_file_name() if hasattr(view.window(), 'project_file_name') else None
if project:
project = basename(project).replace('.sublime-project', '', 1)
thread = SendActionThread(target_file, is_write=is_write, project=project, folders=view.window().folders())
thread.start()
LAST_ACTION = {
'file': target_file,
'time': time.time(),
'is_write': is_write,
}
project = window.project_data() if hasattr(window, 'project_data') else None
folders = window.folders()
thread = SendHeartbeatThread(target_file, view, is_write=is_write, project=project, folders=folders)
thread.start()
class SendActionThread(threading.Thread):
class SendHeartbeatThread(threading.Thread):
def __init__(self, target_file, 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)
self.lock = LOCK
self.target_file = target_file
self.is_write = is_write
self.project = project
@ -125,15 +196,17 @@ class SendActionThread(threading.Thread):
self.debug = SETTINGS.get('debug')
self.api_key = SETTINGS.get('api_key', '')
self.ignore = SETTINGS.get('ignore', [])
self.last_action = LAST_ACTION
self.last_heartbeat = LAST_HEARTBEAT.copy()
self.view = view
def run(self):
if self.target_file:
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']):
self.send()
with self.lock:
if self.target_file:
self.timestamp = time.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()
def send(self):
def send_heartbeat(self):
if not self.api_key:
print('[WakaTime] Error: missing api key.')
return
@ -147,46 +220,53 @@ class SendActionThread(threading.Thread):
]
if self.is_write:
cmd.append('--write')
if self.project:
cmd.extend(['--project', self.project])
if self.project and self.project.get('name'):
cmd.extend(['--project', self.project.get('name')])
elif self.folders:
project_name = find_project_name_from_folders(self.folders)
project_name = find_project_from_folders(self.folders, self.target_file)
if project_name:
cmd.extend(['--project', project_name])
for pattern in self.ignore:
cmd.extend(['--ignore', pattern])
if self.debug:
cmd.append('--verbose')
if HAS_SSL:
if python_binary():
cmd.insert(0, python_binary())
if self.debug:
print('[WakaTime] %s' % ' '.join(cmd))
code = wakatime.main(cmd)
if code != 0:
print('[WakaTime] Error: Response code %d from wakatime package.' % code)
else:
python = python_binary()
if python:
cmd.insert(0, python)
if self.debug:
print('[WakaTime] %s' % ' '.join(cmd))
if platform.system() == 'Windows':
Popen(cmd, shell=False)
else:
with open(join(expanduser('~'), '.wakatime.log'), 'a') as stderr:
Popen(cmd, stderr=stderr)
print('[WakaTime] %s' % ' '.join(obfuscate_apikey(cmd)))
if platform.system() == 'Windows':
Popen(cmd, shell=False)
else:
print('[WakaTime] Error: Unable to find python binary.')
with open(os.path.join(os.path.expanduser('~'), '.wakatime.log'), 'a') as stderr:
Popen(cmd, stderr=stderr)
self.sent()
else:
print('[WakaTime] Error: Unable to find python binary.')
def sent(self):
sublime.set_timeout(self.set_status_bar, 0)
sublime.set_timeout(self.set_last_heartbeat, 0)
def set_status_bar(self):
if SETTINGS.get('status_bar_message'):
self.view.set_status('wakatime', 'WakaTime active {0}'.format(datetime.now().strftime('%I:%M %p')))
def set_last_heartbeat(self):
global LAST_HEARTBEAT
LAST_HEARTBEAT = {
'file': self.target_file,
'time': self.timestamp,
'is_write': self.is_write,
}
def plugin_loaded():
global SETTINGS
print('[WakaTime] Initializing WakaTime plugin v%s' % __version__)
if not HAS_SSL:
python = python_binary()
if not python:
sublime.error_message("Unable to find Python binary!\nWakaTime needs Python to work correctly.\n\nGo to https://www.python.org/downloads")
return
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")
return
SETTINGS = sublime.load_settings(SETTINGS_FILE)
after_loaded()
@ -205,10 +285,16 @@ if ST_VERSION < 3000:
class WakatimeListener(sublime_plugin.EventListener):
def on_post_save(self, view):
handle_action(view, is_write=True)
handle_heartbeat(view, is_write=True)
def on_activated(self, view):
handle_action(view)
def on_selection_modified(self, view):
handle_heartbeat(view)
def on_modified(self, view):
handle_action(view)
handle_heartbeat(view)
class WakatimeDashboardCommand(sublime_plugin.ApplicationCommand):
def run(self):
webbrowser.open_new_tab('https://wakatime.com/dashboard')

View File

@ -9,8 +9,12 @@
// Ignore files; Files (including absolute paths) that match one of these
// 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": false
"debug": false,
// Status bar message. Set to false to hide status bar message.
// Defaults to true.
"status_bar_message": true
}

View File

@ -1,39 +0,0 @@
*.py[cod]
# C extensions
*.so
# Packages
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
lib
lib64
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
nosetests.xml
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
virtualenv
venv
.DS_Store

View File

@ -1,250 +0,0 @@
History
-------
2.1.8 (2014-12-04)
++++++++++++++++++
- fix UnicodeDecodeError when building user agent string
- handle case where response is None
2.1.7 (2014-11-30)
++++++++++++++++++
- upgrade pygments to v2.0.1
- always log an error when api key is incorrect
2.1.6 (2014-11-18)
++++++++++++++++++
- fix list index error when detecting subversion project
2.1.5 (2014-11-17)
++++++++++++++++++
- catch exceptions when getting current machine time zone
2.1.4 (2014-11-12)
++++++++++++++++++
- when Python was not compiled with https support, log an error to the log file
2.1.3 (2014-11-10)
++++++++++++++++++
- correctly detect branch name for subversion projects
2.1.2 (2014-10-07)
++++++++++++++++++
- still log heartbeat when something goes wrong while reading num lines in file
2.1.1 (2014-09-30)
++++++++++++++++++
- fix bug where binary file opened as utf-8
2.1.0 (2014-09-30)
++++++++++++++++++
- python3 compatibility changes
2.0.8 (2014-08-29)
++++++++++++++++++
- supress output from svn command
2.0.7 (2014-08-27)
++++++++++++++++++
- find svn binary location from common install directories
2.0.6 (2014-08-07)
++++++++++++++++++
- encode json data as str when passing to urllib
2.0.5 (2014-07-25)
++++++++++++++++++
- option in .wakatime.cfg to obfuscate file names
2.0.4 (2014-07-25)
++++++++++++++++++
- use unique logger namespace to prevent collisions in shared plugin environments
2.0.3 (2014-06-18)
++++++++++++++++++
- use project from command line arg when no revision control project is found
2.0.2 (2014-06-09)
++++++++++++++++++
- include python3.2 compatible versions of simplejson, pytz, and tzlocal
- disable offline logging when Python was not compiled with sqlite3 module
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)
++++++++++++++++++
- Read git HEAD file to find current branch instead of running git command line
0.4.7 (2013-09-30)
++++++++++++++++++
- Sending local olson timezone string in api request
0.4.6 (2013-09-22)
++++++++++++++++++
- Sending total lines in file and language name to api
0.4.5 (2013-09-07)
++++++++++++++++++
- Fixed relative import error by adding packages directory to sys path
0.4.4 (2013-09-06)
++++++++++++++++++
- Using urllib2 again because of intermittent problems sending json with requests library
0.4.3 (2013-09-04)
++++++++++++++++++
- Encoding json as utf-8 before making request
0.4.2 (2013-09-04)
++++++++++++++++++
- Using requests package v1.2.3 from pypi
0.4.1 (2013-08-25)
++++++++++++++++++
- Fix bug causing requests library to omit POST content
0.4.0 (2013-08-15)
++++++++++++++++++
- Sending single branch instead of multiple tags
0.3.1 (2013-08-08)
++++++++++++++++++
- Using requests module instead of urllib2 to verify SSL certs
0.3.0 (2013-08-08)
++++++++++++++++++
- Allow importing directly from Python plugins
0.1.1 (2013-07-07)
++++++++++++++++++
- Refactored
- Simplified action events schema
0.0.1 (2013-07-05)
++++++++++++++++++
- Birth

View File

@ -1,29 +0,0 @@
Copyright (c) 2013 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,2 +0,0 @@
include README.rst LICENSE HISTORY.rst
recursive-include wakatime *.py

View File

@ -1,20 +0,0 @@
WakaTime
========
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
------------
pip install wakatime
Usage
-----
https://wakatime.com/

View File

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

View File

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
"""
wakatime
~~~~~~~~
Common interface to the WakaTime api.
http://wakatime.com
:copyright: (c) 2013 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
__all__ = ['main']
from .base import main

View File

@ -1,10 +1,9 @@
# -*- coding: utf-8 -*-
"""
wakatime
~~~~~~~~
wakatime.base
~~~~~~~~~~~~~
Common interface to the WakaTime api.
http://wakatime.com
wakatime module entry point.
:copyright: (c) 2013 Alan Hamlett.
:license: BSD, see LICENSE for more details.
@ -12,13 +11,6 @@
from __future__ import print_function
__title__ = 'wakatime'
__version__ = '2.1.8'
__author__ = 'Alan Hamlett'
__license__ = 'BSD'
__copyright__ = 'Copyright 2014 Alan Hamlett'
import base64
import logging
import os
@ -31,26 +23,24 @@ 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.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'packages'))
from .compat import u, open, is_py2, is_py3
from .queue import Queue
from .log import setup_logging
from .__about__ import __version__
from .compat import u, open, is_py3
from .offlinequeue import Queue
from .logger 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 requests
from .packages.requests.exceptions import RequestException
try:
from .packages import tzlocal
except:
from .packages import tzlocal3
from .packages import tzlocal3 as tzlocal
log = logging.getLogger('WakaTime')
@ -102,7 +92,7 @@ def upgradeConfigFile(configFile):
pass
def parseConfigFile(configFile):
def parseConfigFile(configFile=None):
"""Returns a configparser.SafeConfigParser instance with configs
read from the config file. Default location of the config file is
at ~/.wakatime.cfg.
@ -142,32 +132,48 @@ def parseArguments(argv):
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')
parser.add_argument('--time', dest='timestamp', metavar='time',
type=float,
help='optional floating-point unix epoch timestamp; '+
'uses current time by default')
parser.add_argument('--write', dest='isWrite',
action='store_true',
help='note action was triggered from writing to a file')
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')
help='absolute path to file for current heartbeat')
parser.add_argument('--key', dest='key',
help='your wakatime api key; uses api_key from '+
'~/.wakatime.conf by default')
parser.add_argument('--write', dest='isWrite',
action='store_true',
help='when set, tells api this heartbeat was triggered from '+
'writing to a file')
parser.add_argument('--plugin', dest='plugin',
help='optional text editor plugin name and version '+
'for User-Agent header')
parser.add_argument('--time', dest='timestamp', metavar='time',
type=float,
help='optional floating-point unix epoch timestamp; '+
'uses current time by default')
parser.add_argument('--notfile', dest='notfile', action='store_true',
help='when set, will accept any value for the file. for example, '+
'a domain name or other item you want to log time towards.')
parser.add_argument('--proxy', dest='proxy',
help='optional https proxy url; for example: '+
'https://user:pass@localhost:8080')
parser.add_argument('--project', dest='project_name',
help='optional project name; auto-discovered project takes priority')
parser.add_argument('--disableoffline', dest='offline',
action='store_false',
help='disables offline time logging instead of queuing logged time')
parser.add_argument('--hidefilenames', dest='hidefilenames',
action='store_true',
help='obfuscate file names; will not send file names to api')
parser.add_argument('--exclude', dest='exclude', action='append',
help='filename patterns to exclude from logging; POSIX regex '+
'syntax; can be used more than once')
parser.add_argument('--include', dest='include', action='append',
help='filename patterns to log; when used in combination with '+
'--exclude, files matching include will still be logged; '+
'POSIX regex syntax; can be used more than once')
parser.add_argument('--ignore', dest='ignore', action='append',
help='filename patterns to ignore; POSIX regex syntax; can be used more than once')
help=argparse.SUPPRESS)
parser.add_argument('--logfile', dest='logfile',
help='defaults to ~/.wakatime.log')
parser.add_argument('--apiurl', dest='api_url',
help='heartbeats api url; for debugging with a local server')
parser.add_argument('--config', dest='config',
help='defaults to ~/.wakatime.conf')
parser.add_argument('--verbose', dest='verbose', action='store_true',
@ -191,47 +197,83 @@ def parseArguments(argv):
default_key = None
if configs.has_option('settings', 'api_key'):
default_key = configs.get('settings', 'api_key')
elif configs.has_option('settings', 'apikey'):
default_key = configs.get('settings', 'apikey')
if default_key:
args.key = default_key
else:
parser.error('Missing api key')
if not args.ignore:
args.ignore = []
if not args.exclude:
args.exclude = []
if configs.has_option('settings', 'ignore'):
try:
for pattern in configs.get('settings', 'ignore').split("\n"):
if pattern.strip() != '':
args.ignore.append(pattern)
args.exclude.append(pattern)
except TypeError:
pass
if configs.has_option('settings', 'exclude'):
try:
for pattern in configs.get('settings', 'exclude').split("\n"):
if pattern.strip() != '':
args.exclude.append(pattern)
except TypeError:
pass
if not args.include:
args.include = []
if configs.has_option('settings', 'include'):
try:
for pattern in configs.get('settings', 'include').split("\n"):
if pattern.strip() != '':
args.include.append(pattern)
except TypeError:
pass
if args.offline and configs.has_option('settings', 'offline'):
args.offline = configs.getboolean('settings', 'offline')
if not args.hidefilenames and configs.has_option('settings', 'hidefilenames'):
args.hidefilenames = configs.getboolean('settings', 'hidefilenames')
if not args.proxy and configs.has_option('settings', 'proxy'):
args.proxy = configs.get('settings', 'proxy')
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')
if not args.api_url and configs.has_option('settings', 'api_url'):
args.api_url = configs.get('settings', 'api_url')
return args, configs
def should_ignore(fileName, patterns):
try:
for pattern in patterns:
try:
compiled = re.compile(pattern, re.IGNORECASE)
if compiled.search(fileName):
return pattern
except re.error as ex:
log.warning(u('Regex error ({msg}) for ignore pattern: {pattern}').format(
msg=u(ex),
pattern=u(pattern),
))
except TypeError:
pass
def should_exclude(fileName, include, exclude):
if fileName is not None and fileName.strip() != '':
try:
for pattern in include:
try:
compiled = re.compile(pattern, re.IGNORECASE)
if compiled.search(fileName):
return False
except re.error as ex:
log.warning(u('Regex error ({msg}) for include pattern: {pattern}').format(
msg=u(ex),
pattern=u(pattern),
))
except TypeError:
pass
try:
for pattern in exclude:
try:
compiled = re.compile(pattern, re.IGNORECASE)
if compiled.search(fileName):
return pattern
except re.error as ex:
log.warning(u('Regex error ({msg}) for exclude pattern: {pattern}').format(
msg=u(ex),
pattern=u(pattern),
))
except TypeError:
pass
return False
@ -248,19 +290,27 @@ def get_user_agent(plugin):
user_agent=user_agent,
plugin=u(plugin),
)
else:
user_agent = u('{user_agent} Unknown/0').format(
user_agent=user_agent,
)
return user_agent
def send_action(project=None, branch=None, stats=None, key=None, targetFile=None,
timestamp=None, isWrite=None, plugin=None, offline=None,
hidefilenames=None, **kwargs):
url = 'https://wakatime.com/api/v1/actions'
log.debug('Sending heartbeat to api at %s' % url)
def send_heartbeat(project=None, branch=None, stats={}, key=None, targetFile=None,
timestamp=None, isWrite=None, plugin=None, offline=None, notfile=False,
hidefilenames=None, proxy=None, api_url=None, **kwargs):
"""Sends heartbeat as POST request to WakaTime api server.
"""
if not api_url:
api_url = 'https://wakatime.com/api/v1/heartbeats'
log.debug('Sending heartbeat to api at %s' % api_url)
data = {
'time': timestamp,
'file': targetFile,
}
if hidefilenames and targetFile is not None:
if hidefilenames and targetFile is not None and not notfile:
data['file'] = data['file'].rsplit('/', 1)[-1].rsplit('\\', 1)[-1]
if len(data['file'].strip('.').split('.', 1)) > 1:
data['file'] = u('HIDDEN.{ext}').format(ext=u(data['file'].strip('.').rsplit('.', 1)[-1]))
@ -270,6 +320,8 @@ def send_action(project=None, branch=None, stats=None, key=None, targetFile=None
data['lines'] = stats['lines']
if stats.get('language'):
data['language'] = stats['language']
if stats.get('dependencies'):
data['dependencies'] = stats['dependencies']
if isWrite:
data['is_write'] = isWrite
if project:
@ -280,15 +332,17 @@ def send_action(project=None, branch=None, stats=None, key=None, targetFile=None
# setup api request
request_body = json.dumps(data)
request = Request(url=url, data=str.encode(request_body) if is_py3 else request_body)
request.add_header('User-Agent', get_user_agent(plugin))
request.add_header('Content-Type', 'application/json')
auth = u('Basic {key}').format(key=u(base64.b64encode(str.encode(key) if is_py3 else key)))
request.add_header('Authorization', auth)
ALWAYS_LOG_CODES = [
401,
]
api_key = u(base64.b64encode(str.encode(key) if is_py3 else key))
auth = u('Basic {api_key}').format(api_key=api_key)
headers = {
'User-Agent': get_user_agent(plugin),
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': auth,
}
proxies = {}
if proxy:
proxies['https'] = proxy
# add Olson timezone to request
try:
@ -296,65 +350,48 @@ def send_action(project=None, branch=None, stats=None, key=None, targetFile=None
except:
tz = None
if tz:
request.add_header('TimeZone', u(tz.zone))
headers['TimeZone'] = u(tz.zone)
# log time to api
response = None
try:
response = urlopen(request)
except HTTPError as exc:
response = requests.post(api_url, data=request_body, headers=headers,
proxies=proxies)
except RequestException:
exception_data = {
'response_code': exc.getcode(),
sys.exc_info()[0].__name__: u(sys.exc_info()[1]),
}
if log.isEnabledFor(logging.DEBUG):
exception_data['traceback'] = traceback.format_exc()
if offline:
queue = Queue()
queue.push(data, plugin)
queue.push(data, json.dumps(stats), plugin)
if log.isEnabledFor(logging.DEBUG):
log.warn(exception_data)
if response is not None and response.getcode() in ALWAYS_LOG_CODES:
log.error({
'response_code': response.getcode(),
})
else:
log.error(exception_data)
except:
exception_data = {
sys.exc_info()[0].__name__: u(sys.exc_info()[1]),
}
if log.isEnabledFor(logging.DEBUG):
exception_data['traceback'] = traceback.format_exc()
if offline:
queue = Queue()
queue.push(data, plugin)
if 'unknown url type: https' in u(sys.exc_info()[1]):
log.error(exception_data)
elif log.isEnabledFor(logging.DEBUG):
log.warn(exception_data)
if response is not None and response.getcode() in ALWAYS_LOG_CODES:
log.error({
'response_code': response.getcode(),
})
else:
log.error(exception_data)
else:
if response is not None and response.getcode() == 201:
response_code = response.status_code if response is not None else None
response_content = response.text if response is not None else None
if response_code == 201:
log.debug({
'response_code': response.getcode(),
'response_code': response_code,
})
return True
response_code = response.getcode() if response is not None else None
response_content = response.read() if response is not None else None
if offline:
queue = Queue()
queue.push(data, plugin)
if log.isEnabledFor(logging.DEBUG):
log.warn({
'response_code': response_code,
'response_content': response_content,
})
if response_code != 400:
queue = Queue()
queue.push(data, json.dumps(stats), plugin)
if response_code == 401:
log.error({
'response_code': response_code,
'response_content': response_content,
})
elif log.isEnabledFor(logging.DEBUG):
log.warn({
'response_code': response_code,
'response_content': response_content,
})
else:
log.error({
'response_code': response_code,
@ -378,25 +415,27 @@ def main(argv=None):
setup_logging(args, __version__)
ignore = should_ignore(args.targetFile, args.ignore)
if ignore is not False:
log.debug(u('File ignored because matches pattern: {pattern}').format(
pattern=u(ignore),
exclude = should_exclude(args.targetFile, args.include, args.exclude)
if exclude is not False:
log.debug(u('File not logged because matches exclude pattern: {pattern}').format(
pattern=u(exclude),
))
return 0
if os.path.isfile(args.targetFile):
if os.path.isfile(args.targetFile) or args.notfile:
stats = get_file_stats(args.targetFile)
stats = get_file_stats(args.targetFile, notfile=args.notfile)
project = find_project(args.targetFile, configs=configs)
project = None
if not args.notfile:
project = find_project(args.targetFile, configs=configs)
branch = None
project_name = args.project_name
if project:
branch = project.branch()
project_name = project.name()
if send_action(
if send_heartbeat(
project=project_name,
branch=branch,
stats=stats,
@ -404,18 +443,24 @@ def main(argv=None):
):
queue = Queue()
while True:
action = queue.pop()
if action is None:
heartbeat = queue.pop()
if heartbeat is None:
break
sent = 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,
hidefilenames=args.hidefilenames)
sent = send_heartbeat(
project=heartbeat['project'],
targetFile=heartbeat['file'],
timestamp=heartbeat['time'],
branch=heartbeat['branch'],
stats=json.loads(heartbeat['stats']),
key=args.key,
isWrite=heartbeat['is_write'],
plugin=heartbeat['plugin'],
offline=args.offline,
hidefilenames=args.hidefilenames,
notfile=args.notfile,
proxy=args.proxy,
api_url=args.api_url,
)
if not sent:
break
return 0 # success
@ -423,5 +468,5 @@ def main(argv=None):
return 102 # api error
else:
log.debug('File does not exist; ignoring this action.')
log.debug('File does not exist; ignoring this heartbeat.')
return 0

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
wakatime-cli
wakatime.cli
~~~~~~~~~~~~
Command-line entry point.
@ -9,11 +9,9 @@
:license: BSD, see LICENSE for more details.
"""
from __future__ import print_function
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import wakatime
if __name__ == '__main__':

View File

@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
"""
wakatime.compat
~~~~~~~~~~~~~~~
For working with Python2 and Python3.
:copyright: (c) 2014 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
import codecs
import sys
is_py2 = (sys.version_info[0] == 2)
is_py3 = (sys.version_info[0] == 3)
if is_py2:
def u(text):
try:
return text.decode('utf-8')
except:
try:
return unicode(text)
except:
return text
open = codecs.open
basestring = basestring
elif is_py3:
def u(text):
if isinstance(text, bytes):
return text.decode('utf-8')
return str(text)
open = open
basestring = (str, bytes)
try:
from importlib import import_module
except ImportError:
def _resolve_name(name, package, level):
"""Return the absolute name of the module to be imported."""
if not hasattr(package, 'rindex'):
raise ValueError("'package' not set to a string")
dot = len(package)
for x in xrange(level, 1, -1):
try:
dot = package.rindex('.', 0, dot)
except ValueError:
raise ValueError("attempted relative import beyond top-level "
"package")
return "%s.%s" % (package[:dot], name)
def import_module(name, package=None):
"""Import a module.
The 'package' argument is required when performing a relative import.
It specifies the package to use as the anchor point from which to
resolve the relative import to an absolute import.
"""
if name.startswith('.'):
if not package:
raise TypeError("relative imports require the 'package' "
+ "argument")
level = 0
for character in name:
if character != '.':
break
level += 1
name = _resolve_name(name[level:], package, level)
__import__(name)
return sys.modules[name]

View File

@ -0,0 +1,106 @@
# -*- coding: utf-8 -*-
"""
wakatime.languages
~~~~~~~~~~~~~~~~~~
Parse dependencies from a source code file.
:copyright: (c) 2014 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
import logging
import traceback
from ..compat import u, open, import_module
log = logging.getLogger('WakaTime')
class TokenParser(object):
"""The base class for all dependency parsers. To add support for your
language, inherit from this class and implement the :meth:`parse` method
to return a list of dependency strings.
"""
source_file = None
lexer = None
dependencies = []
tokens = []
def __init__(self, source_file, lexer=None):
self.source_file = source_file
self.lexer = lexer
def parse(self, tokens=[]):
""" Should return a list of dependencies.
"""
if not tokens and not self.tokens:
self.tokens = self._extract_tokens()
raise Exception('Not yet implemented.')
def append(self, dep, truncate=False, separator=None, truncate_to=None,
strip_whitespace=True):
if dep == 'as':
print('***************** as')
self._save_dependency(
dep,
truncate=truncate,
truncate_to=truncate_to,
separator=separator,
strip_whitespace=strip_whitespace,
)
def _extract_tokens(self):
if self.lexer:
with open(self.source_file, 'r', encoding='utf-8') as fh:
return self.lexer.get_tokens_unprocessed(fh.read(512000))
return []
def _save_dependency(self, dep, truncate=False, separator=None,
truncate_to=None, strip_whitespace=True):
if truncate:
if separator is None:
separator = u('.')
separator = u(separator)
dep = dep.split(separator)
if truncate_to is None or truncate_to < 0 or truncate_to > len(dep) - 1:
truncate_to = len(dep) - 1
dep = dep[0] if len(dep) == 1 else separator.join(dep[0:truncate_to])
if strip_whitespace:
dep = dep.strip()
if dep:
self.dependencies.append(dep)
class DependencyParser(object):
source_file = None
lexer = None
parser = None
def __init__(self, source_file, lexer):
self.source_file = source_file
self.lexer = lexer
if self.lexer:
module_name = self.lexer.__module__.split('.')[-1]
class_name = self.lexer.__class__.__name__.replace('Lexer', 'Parser', 1)
else:
module_name = 'unknown'
class_name = 'UnknownParser'
try:
module = import_module('.%s' % module_name, package=__package__)
try:
self.parser = getattr(module, class_name)
except AttributeError:
log.debug('Module {0} is missing class {1}'.format(module.__name__, class_name))
except ImportError:
log.debug(traceback.format_exc())
def parse(self):
if self.parser:
plugin = self.parser(self.source_file, lexer=self.lexer)
dependencies = plugin.parse()
return list(set(dependencies))
return []

View File

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
"""
wakatime.languages.c_cpp
~~~~~~~~~~~~~~~~~~~~~~~~
Parse dependencies from C++ code.
:copyright: (c) 2014 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
from . import TokenParser
from ..compat import u
class CppParser(TokenParser):
def parse(self, tokens=[]):
if not tokens and not self.tokens:
self.tokens = self._extract_tokens()
for index, token, content in self.tokens:
self._process_token(token, content)
return self.dependencies
def _process_token(self, token, content):
if u(token).split('.')[-1] == 'Preproc':
self._process_preproc(token, content)
else:
self._process_other(token, content)
def _process_preproc(self, token, content):
if content.strip().startswith('include ') or content.strip().startswith("include\t"):
content = content.replace('include', '', 1).strip()
self.append(content)
def _process_other(self, token, content):
pass

View File

@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
"""
wakatime.languages.data
~~~~~~~~~~~~~~~~~~~~~~~
Parse dependencies from data files.
:copyright: (c) 2014 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
import os
from . import TokenParser
from ..compat import u
FILES = {
'bower.json': {'exact': True, 'dependency': 'bower'},
'component.json': {'exact': True, 'dependency': 'bower'},
'package.json': {'exact': True, 'dependency': 'npm'},
}
class JsonParser(TokenParser):
state = None
level = 0
def parse(self, tokens=[]):
self._process_file_name(os.path.basename(self.source_file))
if not tokens and not self.tokens:
self.tokens = self._extract_tokens()
for index, token, content in self.tokens:
self._process_token(token, content)
return self.dependencies
def _process_file_name(self, file_name):
for key, value in FILES.items():
found = (key == file_name) if value.get('exact') else (key.lower() in file_name.lower())
if found:
self.append(value['dependency'])
def _process_token(self, token, content):
if u(token) == 'Token.Name.Tag':
self._process_tag(token, content)
elif u(token) == 'Token.Literal.String.Single' or u(token) == 'Token.Literal.String.Double':
self._process_literal_string(token, content)
elif u(token) == 'Token.Punctuation':
self._process_punctuation(token, content)
def _process_tag(self, token, content):
if content.strip('"').strip("'") == 'dependencies' or content.strip('"').strip("'") == 'devDependencies':
self.state = 'dependencies'
elif self.state == 'dependencies' and self.level == 2:
self.append(content.strip('"').strip("'"))
def _process_literal_string(self, token, content):
pass
def _process_punctuation(self, token, content):
if content == '{':
self.level += 1
elif content == '}':
self.level -= 1
if self.state is not None and self.level <= 1:
self.state = None

View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
"""
wakatime.languages.dotnet
~~~~~~~~~~~~~~~~~~~~~~~~~
Parse dependencies from .NET code.
:copyright: (c) 2014 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
from . import TokenParser
from ..compat import u
class CSharpParser(TokenParser):
def parse(self, tokens=[]):
if not tokens and not self.tokens:
self.tokens = self._extract_tokens()
for index, token, content in self.tokens:
self._process_token(token, content)
return self.dependencies
def _process_token(self, token, content):
if u(token).split('.')[-1] == 'Namespace':
self._process_namespace(token, content)
else:
self._process_other(token, content)
def _process_namespace(self, token, content):
if content != 'import' and content != 'package' and content != 'namespace':
self.append(content, truncate=True)
def _process_other(self, token, content):
pass

View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
"""
wakatime.languages.java
~~~~~~~~~~~~~~~~~~~~~~~
Parse dependencies from Java code.
:copyright: (c) 2014 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
from . import TokenParser
from ..compat import u
class JavaParser(TokenParser):
def parse(self, tokens=[]):
if not tokens and not self.tokens:
self.tokens = self._extract_tokens()
for index, token, content in self.tokens:
self._process_token(token, content)
return self.dependencies
def _process_token(self, token, content):
if u(token).split('.')[-1] == 'Namespace':
self._process_namespace(token, content)
else:
self._process_other(token, content)
def _process_namespace(self, token, content):
if content != 'import' and content != 'package' and content != 'namespace':
self.append(content, truncate=True)
def _process_other(self, token, content):
pass

View File

@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
"""
wakatime.languages.php
~~~~~~~~~~~~~~~~~~~~~~
Parse dependencies from PHP code.
:copyright: (c) 2014 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
from . import TokenParser
from ..compat import u
class PhpParser(TokenParser):
state = None
parens = 0
def parse(self, tokens=[]):
if not tokens and not self.tokens:
self.tokens = self._extract_tokens()
for index, token, content in self.tokens:
self._process_token(token, content)
return self.dependencies
def _process_token(self, token, content):
if u(token).split('.')[-1] == 'Keyword':
self._process_keyword(token, content)
elif u(token) == 'Token.Literal.String.Single' or u(token) == 'Token.Literal.String.Double':
self._process_literal_string(token, content)
elif u(token) == 'Token.Name.Other':
self._process_name(token, content)
elif u(token) == 'Token.Name.Function':
self._process_function(token, content)
elif u(token).split('.')[-1] == 'Punctuation':
self._process_punctuation(token, content)
elif u(token).split('.')[-1] == 'Text':
self._process_text(token, content)
else:
self._process_other(token, content)
def _process_name(self, token, content):
if self.state == 'use':
self.append(content, truncate=True, separator=u("\\"))
def _process_function(self, token, content):
if self.state == 'use function':
self.append(content, truncate=True, separator=u("\\"))
self.state = 'use'
def _process_keyword(self, token, content):
if content == 'include' or content == 'include_once' or content == 'require' or content == 'require_once':
self.state = 'include'
elif content == 'use':
self.state = 'use'
elif content == 'as':
self.state = 'as'
elif self.state == 'use' and content == 'function':
self.state = 'use function'
else:
self.state = None
def _process_literal_string(self, token, content):
if self.state == 'include':
if content != '"':
content = content.strip()
if u(token) == 'Token.Literal.String.Double':
content = u('"{0}"').format(content)
self.append(content)
self.state = None
def _process_punctuation(self, token, content):
if content == '(':
self.parens += 1
elif content == ')':
self.parens -= 1
elif (self.state == 'use' or self.state == 'as') and content == ',':
self.state = 'use'
else:
self.state = None
def _process_text(self, token, content):
pass
def _process_other(self, token, content):
self.state = None

View File

@ -0,0 +1,120 @@
# -*- coding: utf-8 -*-
"""
wakatime.languages.python
~~~~~~~~~~~~~~~~~~~~~~~~~
Parse dependencies from Python code.
:copyright: (c) 2014 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
from . import TokenParser
from ..compat import u
class PythonParser(TokenParser):
state = None
parens = 0
nonpackage = False
def parse(self, tokens=[]):
if not tokens and not self.tokens:
self.tokens = self._extract_tokens()
for index, token, content in self.tokens:
self._process_token(token, content)
return self.dependencies
def _process_token(self, token, content):
if u(token).split('.')[-1] == 'Namespace':
self._process_namespace(token, content)
elif u(token).split('.')[-1] == 'Name':
self._process_name(token, content)
elif u(token).split('.')[-1] == 'Word':
self._process_word(token, content)
elif u(token).split('.')[-1] == 'Operator':
self._process_operator(token, content)
elif u(token).split('.')[-1] == 'Punctuation':
self._process_punctuation(token, content)
elif u(token).split('.')[-1] == 'Text':
self._process_text(token, content)
else:
self._process_other(token, content)
def _process_namespace(self, token, content):
if self.state is None:
self.state = content
else:
if content == 'as':
self.nonpackage = True
else:
self._process_import(token, content)
def _process_name(self, token, content):
if self.state is not None:
if self.nonpackage:
self.nonpackage = False
else:
if self.state == 'from':
self.append(content, truncate=True, truncate_to=0)
if self.state == 'from-2' and content != 'import':
self.append(content, truncate=True, truncate_to=0)
elif self.state == 'import':
self.append(content, truncate=True, truncate_to=0)
elif self.state == 'import-2':
self.append(content, truncate=True, truncate_to=0)
else:
self.state = None
def _process_word(self, token, content):
if self.state is not None:
if self.nonpackage:
self.nonpackage = False
else:
if self.state == 'from':
self.append(content, truncate=True, truncate_to=0)
if self.state == 'from-2' and content != 'import':
self.append(content, truncate=True, truncate_to=0)
elif self.state == 'import':
self.append(content, truncate=True, truncate_to=0)
elif self.state == 'import-2':
self.append(content, truncate=True, truncate_to=0)
else:
self.state = None
def _process_operator(self, token, content):
if self.state is not None:
if content == '.':
self.nonpackage = True
def _process_punctuation(self, token, content):
if content == '(':
self.parens += 1
elif content == ')':
self.parens -= 1
self.nonpackage = False
def _process_text(self, token, content):
if self.state is not None:
if content == "\n" and self.parens == 0:
self.state = None
self.nonpackage = False
def _process_other(self, token, content):
pass
def _process_import(self, token, content):
if not self.nonpackage:
if self.state == 'from':
self.append(content, truncate=True, truncate_to=0)
self.state = 'from-2'
elif self.state == 'from-2' and content != 'import':
self.append(content, truncate=True, truncate_to=0)
elif self.state == 'import':
self.append(content, truncate=True, truncate_to=0)
self.state = 'import-2'
elif self.state == 'import-2':
self.append(content, truncate=True, truncate_to=0)
else:
self.state = None
self.nonpackage = False

View File

@ -0,0 +1,224 @@
# -*- coding: utf-8 -*-
"""
wakatime.languages.templates
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Parse dependencies from Templates.
:copyright: (c) 2014 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
from . import TokenParser
from ..compat import u
""" If these keywords are found in the source file, treat them as a dependency.
Must be lower-case strings.
"""
KEYWORDS = [
'_',
'$',
'angular',
'assert', # probably mocha
'backbone',
'batman',
'c3',
'can',
'casper',
'chai',
'chaplin',
'd3',
'define', # probably require
'describe', # mocha or jasmine
'eco',
'ember',
'espresso',
'expect', # probably jasmine
'exports', # probably npm
'express',
'gulp',
'handlebars',
'highcharts',
'jasmine',
'jquery',
'jstz',
'ko', # probably knockout
'm', # probably mithril
'marionette',
'meteor',
'moment',
'monitorio',
'mustache',
'phantom',
'pickadate',
'pikaday',
'qunit',
'react',
'reactive',
'require', # probably the commonjs spec
'ripple',
'rivets',
'socketio',
'spine',
'thorax',
'underscore',
'vue',
'way',
'zombie',
]
class LassoJavascriptParser(TokenParser):
def parse(self, tokens=[]):
if not tokens and not self.tokens:
self.tokens = self._extract_tokens()
for index, token, content in self.tokens:
self._process_token(token, content)
return self.dependencies
def _process_token(self, token, content):
if u(token) == 'Token.Name.Other':
self._process_name(token, content)
elif u(token) == 'Token.Literal.String.Single' or u(token) == 'Token.Literal.String.Double':
self._process_literal_string(token, content)
def _process_name(self, token, content):
if content.lower() in KEYWORDS:
self.append(content.lower())
def _process_literal_string(self, token, content):
if 'famous/core/' in content.strip('"').strip("'"):
self.append('famous')
class HtmlDjangoParser(TokenParser):
tags = []
getting_attrs = False
current_attr = None
current_attr_value = None
def parse(self, tokens=[]):
if not tokens and not self.tokens:
self.tokens = self._extract_tokens()
for index, token, content in self.tokens:
self._process_token(token, content)
return self.dependencies
def _process_token(self, token, content):
if u(token) == 'Token.Name.Tag':
self._process_tag(token, content)
elif u(token) == 'Token.Literal.String':
self._process_string(token, content)
elif u(token) == 'Token.Name.Attribute':
self._process_attribute(token, content)
@property
def current_tag(self):
return None if len(self.tags) == 0 else self.tags[0]
def _process_tag(self, token, content):
if content.startswith('</') or content.startswith('/'):
try:
self.tags.pop(0)
except IndexError:
# ignore errors from malformed markup
pass
self.getting_attrs = False
elif content.startswith('<'):
self.tags.insert(0, content.replace('<', '', 1).strip().lower())
self.getting_attrs = True
elif content.startswith('>'):
self.getting_attrs = False
self.current_attr = None
def _process_attribute(self, token, content):
if self.getting_attrs:
self.current_attr = content.lower().strip('=')
else:
self.current_attr = None
self.current_attr_value = None
def _process_string(self, token, content):
if self.getting_attrs and self.current_attr is not None:
if content.endswith('"') or content.endswith("'"):
if self.current_attr_value is not None:
self.current_attr_value += content
if self.current_tag == 'script' and self.current_attr == 'src':
self.append(self.current_attr_value)
self.current_attr = None
self.current_attr_value = None
else:
if len(content) == 1:
self.current_attr_value = content
else:
if self.current_tag == 'script' and self.current_attr == 'src':
self.append(content)
self.current_attr = None
self.current_attr_value = None
elif content.startswith('"') or content.startswith("'"):
if self.current_attr_value is None:
self.current_attr_value = content
else:
self.current_attr_value += content
class VelocityHtmlParser(HtmlDjangoParser):
pass
class MyghtyHtmlParser(HtmlDjangoParser):
pass
class MasonParser(HtmlDjangoParser):
pass
class MakoHtmlParser(HtmlDjangoParser):
pass
class CheetahHtmlParser(HtmlDjangoParser):
pass
class HtmlGenshiParser(HtmlDjangoParser):
pass
class RhtmlParser(HtmlDjangoParser):
pass
class HtmlPhpParser(HtmlDjangoParser):
pass
class HtmlSmartyParser(HtmlDjangoParser):
pass
class EvoqueHtmlParser(HtmlDjangoParser):
pass
class ColdfusionHtmlParser(HtmlDjangoParser):
pass
class LassoHtmlParser(HtmlDjangoParser):
pass
class HandlebarsHtmlParser(HtmlDjangoParser):
pass
class YamlJinjaParser(HtmlDjangoParser):
pass
class TwigHtmlParser(HtmlDjangoParser):
pass

View File

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
"""
wakatime.languages.unknown
~~~~~~~~~~~~~~~~~~~~~~~~~~
Parse dependencies from files of unknown language.
:copyright: (c) 2014 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
import os
from . import TokenParser
FILES = {
'bower': {'exact': False, 'dependency': 'bower'},
'grunt': {'exact': False, 'dependency': 'grunt'},
}
class UnknownParser(TokenParser):
def parse(self, tokens=[]):
self._process_file_name(os.path.basename(self.source_file))
return self.dependencies
def _process_file_name(self, file_name):
for key, value in FILES.items():
found = (key == file_name) if value.get('exact') else (key.lower() in file_name.lower())
if found:
self.append(value['dependency'])

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
wakatime.log
~~~~~~~~~~~~
wakatime.logger
~~~~~~~~~~~~~~~
Provides the configured logger for writing JSON to the log file.
@ -9,6 +9,7 @@
:license: BSD, see LICENSE for more details.
"""
import inspect
import logging
import os
import sys
@ -30,7 +31,6 @@ class CustomEncoder(json.JSONEncoder):
try:
encoded = super(CustomEncoder, self).default(obj)
except UnicodeDecodeError:
encoding = sys.getfilesystemencoding()
obj = u(obj)
encoded = super(CustomEncoder, self).default(obj)
return encoded
@ -48,14 +48,19 @@ class JsonFormatter(logging.Formatter):
def format(self, record):
data = OrderedDict([
('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),
])
try:
data['package'] = inspect.stack()[9][0].f_globals.get('__package__')
data['lineno'] = inspect.stack()[9][2]
except:
pass
data['version'] = self.version
data['plugin'] = self.plugin
data['time'] = self.timestamp
data['isWrite'] = self.isWrite
data['file'] = self.targetFile
data['level'] = record.levelname
data['message'] = record.msg
if not self.plugin:
del data['plugin']
if not self.isWrite:
@ -63,7 +68,7 @@ class JsonFormatter(logging.Formatter):
return CustomEncoder().encode(data)
def formatException(self, exc_info):
return exec_info[2].format_exc()
return sys.exec_info[2].format_exc()
def set_log_level(logger, args):
@ -74,6 +79,7 @@ def set_log_level(logger, args):
def setup_logging(args, version):
logging.captureWarnings(True)
logger = logging.getLogger('WakaTime')
set_log_level(logger, args)
if len(logger.handlers) > 0:
@ -101,4 +107,5 @@ def setup_logging(args, version):
)
handler.setFormatter(formatter)
logger.addHandler(handler)
logging.getLogger('py.warnings').addHandler(handler)
return logger

View File

@ -16,8 +16,6 @@ import os
import traceback
from time import sleep
from .compat import u
try:
import sqlite3
HAS_SQL = True
@ -34,35 +32,35 @@ class Queue(object):
def connect(self):
conn = sqlite3.connect(self.DB_FILE)
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS action (
c.execute('''CREATE TABLE IF NOT EXISTS heartbeat (
file text,
time real,
project text,
language text,
lines integer,
branch text,
is_write integer,
stats text,
misc text,
plugin text)
''')
return (conn, c)
def push(self, data, plugin):
def push(self, data, stats, plugin, misc=None):
if not HAS_SQL:
return
try:
conn, c = self.connect()
action = {
heartbeat = {
'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,
'stats': stats,
'misc': misc,
'plugin': plugin,
}
c.execute('INSERT INTO action VALUES (:file,:time,:project,:language,:lines,:branch,:is_write,:plugin)', action)
c.execute('INSERT INTO heartbeat VALUES (:file,:time,:project,:branch,:is_write,:stats,:misc,:plugin)', heartbeat)
conn.commit()
conn.close()
except sqlite3.Error:
@ -74,7 +72,7 @@ class Queue(object):
return None
tries = 3
wait = 0.1
action = None
heartbeat = None
try:
conn, c = self.connect()
except sqlite3.Error:
@ -84,33 +82,33 @@ class Queue(object):
while loop and tries > -1:
try:
c.execute('BEGIN IMMEDIATE')
c.execute('SELECT * FROM action LIMIT 1')
c.execute('SELECT * FROM heartbeat 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']:
for row_name in ['file', 'time', 'project', 'branch', 'is_write']:
if row[index] is not None:
clauses.append('{0}=?'.format(row_name))
values.append(u(row[index]))
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(' AND '.join(clauses)), values)
c.execute('DELETE FROM heartbeat WHERE {0}'.format(' AND '.join(clauses)), values)
else:
c.execute('DELETE FROM action WHERE {0}'.format(' AND '.join(clauses)))
c.execute('DELETE FROM heartbeat WHERE {0}'.format(' AND '.join(clauses)))
conn.commit()
if row is not None:
action = {
heartbeat = {
'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,
'branch': row[3],
'is_write': True if row[4] is 1 else False,
'stats': row[5],
'misc': row[6],
'plugin': row[7],
}
loop = False
@ -122,4 +120,4 @@ class Queue(object):
conn.close()
except sqlite3.Error:
log.debug(traceback.format_exc())
return action
return heartbeat

Some files were not shown because too many files have changed in this diff Show More