mirror of
https://github.com/wakatime/sublime-wakatime.git
synced 2023-08-10 21:13:02 +03:00
Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
b2086a3cd2 | |||
005b07520c | |||
60608bd322 | |||
cde8f8f1de | |||
4adfca154c | |||
f7b3924a30 | |||
db00024455 | |||
9a6be7ca4e | |||
1ea9b2a761 | |||
bd5e87e030 | |||
0256ff4a6a | |||
9d170b3276 | |||
c54e575210 | |||
07513d8f10 | |||
30902cc050 | |||
aa7962d49a |
34
HISTORY.rst
34
HISTORY.rst
@ -3,6 +3,40 @@ History
|
|||||||
-------
|
-------
|
||||||
|
|
||||||
|
|
||||||
|
8.6.0 (2019-05-27)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- Prevent prompting for api key when found from config file.
|
||||||
|
`sublime-wakatime#98 <https://github.com/wakatime/sublime-wakatime/issues/98>`_
|
||||||
|
|
||||||
|
|
||||||
|
8.5.0 (2019-05-10)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- Remove clock icon from status bar.
|
||||||
|
- Use wakatime-cli to fetch status bar coding time.
|
||||||
|
|
||||||
|
|
||||||
|
8.4.2 (2019-05-07)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- Upgrade wakatime-cli to v11.0.0.
|
||||||
|
- Rename argument --show-time-today to --today.
|
||||||
|
- New argument --show-time-today for printing Today's coding time.
|
||||||
|
|
||||||
|
|
||||||
|
8.4.1 (2019-05-01)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- Use api subdomain for fetching today's coding activity.
|
||||||
|
|
||||||
|
|
||||||
|
8.4.0 (2019-05-01)
|
||||||
|
++++++++++++++++++
|
||||||
|
|
||||||
|
- Show today's coding time in status bar.
|
||||||
|
|
||||||
|
|
||||||
8.3.6 (2019-04-30)
|
8.3.6 (2019-04-30)
|
||||||
++++++++++++++++++
|
++++++++++++++++++
|
||||||
|
|
||||||
|
102
WakaTime.py
102
WakaTime.py
@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
""" ==========================================================
|
""" ==========================================================
|
||||||
File: WakaTime.py
|
File: WakaTime.py
|
||||||
Description: Automatic time tracking for Sublime Text 2 and 3.
|
Description: Automatic time tracking for Sublime Text 2 and 3.
|
||||||
@ -7,7 +8,7 @@ Website: https://wakatime.com/
|
|||||||
==========================================================="""
|
==========================================================="""
|
||||||
|
|
||||||
|
|
||||||
__version__ = '8.3.6'
|
__version__ = '8.6.0'
|
||||||
|
|
||||||
|
|
||||||
import sublime
|
import sublime
|
||||||
@ -25,7 +26,6 @@ import threading
|
|||||||
import traceback
|
import traceback
|
||||||
import urllib
|
import urllib
|
||||||
import webbrowser
|
import webbrowser
|
||||||
from datetime import datetime
|
|
||||||
from subprocess import STDOUT, PIPE
|
from subprocess import STDOUT, PIPE
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
try:
|
try:
|
||||||
@ -121,6 +121,9 @@ LAST_HEARTBEAT = {
|
|||||||
'is_write': False,
|
'is_write': False,
|
||||||
}
|
}
|
||||||
LAST_HEARTBEAT_SENT_AT = 0
|
LAST_HEARTBEAT_SENT_AT = 0
|
||||||
|
LAST_FETCH_TODAY_CODING_TIME = 0
|
||||||
|
FETCH_TODAY_DEBOUNCE_COUNTER = 0
|
||||||
|
FETCH_TODAY_DEBOUNCE_SECONDS = 60
|
||||||
PYTHON_LOCATION = None
|
PYTHON_LOCATION = None
|
||||||
HEARTBEATS = queue.Queue()
|
HEARTBEATS = queue.Queue()
|
||||||
HEARTBEAT_FREQUENCY = 2 # minutes between logging heartbeat when editing same file
|
HEARTBEAT_FREQUENCY = 2 # minutes between logging heartbeat when editing same file
|
||||||
@ -139,7 +142,8 @@ sys.path.insert(0, os.path.join(PLUGIN_DIR, 'packages'))
|
|||||||
try:
|
try:
|
||||||
from wakatime.main import parseConfigFile
|
from wakatime.main import parseConfigFile
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
def parseConfigFile():
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def set_timeout(callback, seconds):
|
def set_timeout(callback, seconds):
|
||||||
@ -180,21 +184,89 @@ def resources_folder():
|
|||||||
return os.path.join(os.path.expanduser('~'), '.wakatime')
|
return os.path.join(os.path.expanduser('~'), '.wakatime')
|
||||||
|
|
||||||
|
|
||||||
def update_status_bar(status):
|
def update_status_bar(status=None, debounced=False, msg=None):
|
||||||
"""Updates the status bar."""
|
"""Updates the status bar."""
|
||||||
|
global LAST_FETCH_TODAY_CODING_TIME, FETCH_TODAY_DEBOUNCE_COUNTER
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if SETTINGS.get('status_bar_message'):
|
if not msg and SETTINGS.get('status_bar_message') is not False and SETTINGS.get('status_bar_enabled'):
|
||||||
msg = datetime.now().strftime(SETTINGS.get('status_bar_message_fmt'))
|
if SETTINGS.get('status_bar_coding_activity') and status == 'OK':
|
||||||
if '{status}' in msg:
|
if debounced:
|
||||||
msg = msg.format(status=status)
|
FETCH_TODAY_DEBOUNCE_COUNTER -= 1
|
||||||
|
if debounced or not LAST_FETCH_TODAY_CODING_TIME:
|
||||||
|
now = int(time.time())
|
||||||
|
if LAST_FETCH_TODAY_CODING_TIME and (FETCH_TODAY_DEBOUNCE_COUNTER > 0 or LAST_FETCH_TODAY_CODING_TIME > now - FETCH_TODAY_DEBOUNCE_SECONDS):
|
||||||
|
return
|
||||||
|
LAST_FETCH_TODAY_CODING_TIME = now
|
||||||
|
FetchStatusBarCodingTime().start()
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
FETCH_TODAY_DEBOUNCE_COUNTER += 1
|
||||||
|
set_timeout(lambda: update_status_bar(status, debounced=True), FETCH_TODAY_DEBOUNCE_SECONDS)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
msg = 'WakaTime: {status}'.format(status=status)
|
||||||
|
|
||||||
|
if msg:
|
||||||
active_window = sublime.active_window()
|
active_window = sublime.active_window()
|
||||||
if active_window:
|
if active_window:
|
||||||
for view in active_window.views():
|
for view in active_window.views():
|
||||||
view.set_status('wakatime', msg)
|
view.set_status('wakatime', msg)
|
||||||
|
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
set_timeout(lambda: update_status_bar(status), 0)
|
set_timeout(lambda: update_status_bar(status=status, debounced=debounced, msg=msg), 0)
|
||||||
|
|
||||||
|
|
||||||
|
class FetchStatusBarCodingTime(threading.Thread):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
threading.Thread.__init__(self)
|
||||||
|
|
||||||
|
self.debug = SETTINGS.get('debug')
|
||||||
|
self.api_key = SETTINGS.get('api_key', '')
|
||||||
|
self.proxy = SETTINGS.get('proxy')
|
||||||
|
self.python_binary = SETTINGS.get('python_binary')
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
if not self.api_key:
|
||||||
|
log(DEBUG, 'Missing WakaTime API key.')
|
||||||
|
return
|
||||||
|
|
||||||
|
python = self.python_binary
|
||||||
|
if not python or not python.strip():
|
||||||
|
python = python_binary()
|
||||||
|
if not python:
|
||||||
|
log(DEBUG, 'Missing Python.')
|
||||||
|
return
|
||||||
|
|
||||||
|
ua = 'sublime/%d sublime-wakatime/%s' % (ST_VERSION, __version__)
|
||||||
|
cmd = [
|
||||||
|
python,
|
||||||
|
API_CLIENT,
|
||||||
|
'--today',
|
||||||
|
'--key', str(bytes.decode(self.api_key.encode('utf8'))),
|
||||||
|
'--plugin', ua,
|
||||||
|
]
|
||||||
|
if self.debug:
|
||||||
|
cmd.append('--verbose')
|
||||||
|
if self.proxy:
|
||||||
|
cmd.extend(['--proxy', self.proxy])
|
||||||
|
|
||||||
|
log(DEBUG, ' '.join(obfuscate_apikey(cmd)))
|
||||||
|
try:
|
||||||
|
process = Popen(cmd, stdout=PIPE, stderr=STDOUT)
|
||||||
|
output, err = process.communicate()
|
||||||
|
output = u(output)
|
||||||
|
retcode = process.poll()
|
||||||
|
if not retcode and output:
|
||||||
|
msg = 'Today: {output}'.format(output=output)
|
||||||
|
update_status_bar(msg=msg)
|
||||||
|
else:
|
||||||
|
log(DEBUG, 'wakatime-core today exited with status: {0}'.format(retcode))
|
||||||
|
if output:
|
||||||
|
log(DEBUG, u('wakatime-core today output: {0}').format(output))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def prompt_api_key():
|
def prompt_api_key():
|
||||||
@ -203,12 +275,15 @@ def prompt_api_key():
|
|||||||
if SETTINGS.get('api_key'):
|
if SETTINGS.get('api_key'):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
default_key = ''
|
|
||||||
try:
|
try:
|
||||||
configs = parseConfigFile()
|
configs = parseConfigFile()
|
||||||
if configs is not None:
|
if configs is not None:
|
||||||
if configs.has_option('settings', 'api_key'):
|
if configs.has_option('settings', 'api_key'):
|
||||||
default_key = configs.get('settings', 'api_key')
|
key = configs.get('settings', 'api_key')
|
||||||
|
if key:
|
||||||
|
SETTINGS.set('api_key', str(key))
|
||||||
|
sublime.save_settings(SETTINGS_FILE)
|
||||||
|
return True
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -218,7 +293,7 @@ def prompt_api_key():
|
|||||||
if text:
|
if text:
|
||||||
SETTINGS.set('api_key', str(text))
|
SETTINGS.set('api_key', str(text))
|
||||||
sublime.save_settings(SETTINGS_FILE)
|
sublime.save_settings(SETTINGS_FILE)
|
||||||
window.show_input_panel('[WakaTime] Enter your wakatime.com api key:', default_key, got_key, None, None)
|
window.show_input_panel('[WakaTime] Enter your wakatime.com api key:', '', got_key, None, None)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
log(ERROR, 'Could not prompt for api key because no window found.')
|
log(ERROR, 'Could not prompt for api key because no window found.')
|
||||||
@ -648,7 +723,7 @@ def plugin_loaded():
|
|||||||
SETTINGS = sublime.load_settings(SETTINGS_FILE)
|
SETTINGS = sublime.load_settings(SETTINGS_FILE)
|
||||||
|
|
||||||
log(INFO, 'Initializing WakaTime plugin v%s' % __version__)
|
log(INFO, 'Initializing WakaTime plugin v%s' % __version__)
|
||||||
update_status_bar('Initializing')
|
update_status_bar('Initializing...')
|
||||||
|
|
||||||
if not python_binary():
|
if not python_binary():
|
||||||
log(WARNING, 'Python binary not found.')
|
log(WARNING, 'Python binary not found.')
|
||||||
@ -664,6 +739,7 @@ def plugin_loaded():
|
|||||||
def after_loaded():
|
def after_loaded():
|
||||||
if not prompt_api_key():
|
if not prompt_api_key():
|
||||||
set_timeout(after_loaded, 0.5)
|
set_timeout(after_loaded, 0.5)
|
||||||
|
update_status_bar('OK')
|
||||||
|
|
||||||
|
|
||||||
# need to call plugin_loaded because only ST3 will auto-call it
|
# need to call plugin_loaded because only ST3 will auto-call it
|
||||||
|
@ -21,12 +21,13 @@
|
|||||||
// POSIX regular expressions will bypass your ignore setting.
|
// POSIX regular expressions will bypass your ignore setting.
|
||||||
"include": [".*"],
|
"include": [".*"],
|
||||||
|
|
||||||
// Status bar message. Set to false to hide status bar message.
|
// Status bar for surfacing errors and displaying today's coding time. Set
|
||||||
// Defaults to true.
|
// to false to hide. Defaults to true.
|
||||||
"status_bar_message": true,
|
"status_bar_enabled": true,
|
||||||
|
|
||||||
// Status bar message format.
|
// Show today's coding activity in WakaTime status bar item.
|
||||||
"status_bar_message_fmt": "WakaTime {status} %I:%M %p",
|
// Defaults to true.
|
||||||
|
"status_bar_coding_activity": true,
|
||||||
|
|
||||||
// Obfuscate file paths when sending to API. Your dashboard will no longer display coding activity per file.
|
// Obfuscate file paths when sending to API. Your dashboard will no longer display coding activity per file.
|
||||||
"hidefilenames": false,
|
"hidefilenames": false,
|
||||||
|
@ -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__ = ('10', '8', '4')
|
__version_info__ = ('11', '0', '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'
|
||||||
|
@ -163,6 +163,119 @@ def send_heartbeats(heartbeats, args, configs, use_ntlm_proxy=False):
|
|||||||
return AUTH_ERROR if code == 401 else API_ERROR
|
return AUTH_ERROR if code == 401 else API_ERROR
|
||||||
|
|
||||||
|
|
||||||
|
def get_time_today(args, use_ntlm_proxy=False):
|
||||||
|
"""Get coding time from WakaTime API for given time range.
|
||||||
|
|
||||||
|
Returns total time as string or `None` when unable to fetch summary from
|
||||||
|
the API. When verbose output enabled, returns error message when unable to
|
||||||
|
fetch summary.
|
||||||
|
"""
|
||||||
|
|
||||||
|
url = 'https://api.wakatime.com/api/v1/users/current/summaries'
|
||||||
|
timeout = args.timeout
|
||||||
|
if not timeout:
|
||||||
|
timeout = 60
|
||||||
|
|
||||||
|
api_key = u(base64.b64encode(str.encode(args.key) if is_py3 else args.key))
|
||||||
|
auth = u('Basic {api_key}').format(api_key=api_key)
|
||||||
|
headers = {
|
||||||
|
'User-Agent': get_user_agent(args.plugin),
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Authorization': auth,
|
||||||
|
}
|
||||||
|
|
||||||
|
session_cache = SessionCache()
|
||||||
|
session = session_cache.get()
|
||||||
|
|
||||||
|
should_try_ntlm = False
|
||||||
|
proxies = {}
|
||||||
|
if args.proxy:
|
||||||
|
if use_ntlm_proxy:
|
||||||
|
from .packages.requests_ntlm import HttpNtlmAuth
|
||||||
|
username = args.proxy.rsplit(':', 1)
|
||||||
|
password = ''
|
||||||
|
if len(username) == 2:
|
||||||
|
password = username[1]
|
||||||
|
username = username[0]
|
||||||
|
session.auth = HttpNtlmAuth(username, password, session)
|
||||||
|
else:
|
||||||
|
should_try_ntlm = '\\' in args.proxy
|
||||||
|
proxies['https'] = args.proxy
|
||||||
|
|
||||||
|
ssl_verify = not args.nosslverify
|
||||||
|
if args.ssl_certs_file and ssl_verify:
|
||||||
|
ssl_verify = args.ssl_certs_file
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'start': 'today',
|
||||||
|
'end': 'today',
|
||||||
|
}
|
||||||
|
|
||||||
|
# send request to api
|
||||||
|
response, code = None, None
|
||||||
|
try:
|
||||||
|
response = session.get(url, params=params, headers=headers,
|
||||||
|
proxies=proxies, timeout=timeout,
|
||||||
|
verify=ssl_verify)
|
||||||
|
except RequestException:
|
||||||
|
if should_try_ntlm:
|
||||||
|
return get_time_today(args, use_ntlm_proxy=True)
|
||||||
|
|
||||||
|
session_cache.delete()
|
||||||
|
if log.isEnabledFor(logging.DEBUG):
|
||||||
|
exception_data = {
|
||||||
|
sys.exc_info()[0].__name__: u(sys.exc_info()[1]),
|
||||||
|
'traceback': traceback.format_exc(),
|
||||||
|
}
|
||||||
|
log.error(exception_data)
|
||||||
|
return '{0}: {1}'.format(sys.exc_info()[0].__name__, u(sys.exc_info()[1])), API_ERROR
|
||||||
|
return None, API_ERROR
|
||||||
|
|
||||||
|
except: # delete cached session when requests raises unknown exception
|
||||||
|
if should_try_ntlm:
|
||||||
|
return get_time_today(args, use_ntlm_proxy=True)
|
||||||
|
|
||||||
|
session_cache.delete()
|
||||||
|
if log.isEnabledFor(logging.DEBUG):
|
||||||
|
exception_data = {
|
||||||
|
sys.exc_info()[0].__name__: u(sys.exc_info()[1]),
|
||||||
|
'traceback': traceback.format_exc(),
|
||||||
|
}
|
||||||
|
log.error(exception_data)
|
||||||
|
return '{0}: {1}'.format(sys.exc_info()[0].__name__, u(sys.exc_info()[1])), API_ERROR
|
||||||
|
return None, API_ERROR
|
||||||
|
|
||||||
|
code = response.status_code if response is not None else None
|
||||||
|
content = response.text if response is not None else None
|
||||||
|
|
||||||
|
if code == requests.codes.ok:
|
||||||
|
try:
|
||||||
|
text = response.json()['data'][0]['grand_total']['text']
|
||||||
|
session_cache.save(session)
|
||||||
|
return text, SUCCESS
|
||||||
|
except:
|
||||||
|
if log.isEnabledFor(logging.DEBUG):
|
||||||
|
exception_data = {
|
||||||
|
sys.exc_info()[0].__name__: u(sys.exc_info()[1]),
|
||||||
|
'traceback': traceback.format_exc(),
|
||||||
|
}
|
||||||
|
log.error(exception_data)
|
||||||
|
return '{0}: {1}'.format(sys.exc_info()[0].__name__, u(sys.exc_info()[1])), API_ERROR
|
||||||
|
return None, API_ERROR
|
||||||
|
else:
|
||||||
|
if should_try_ntlm:
|
||||||
|
return get_time_today(args, use_ntlm_proxy=True)
|
||||||
|
|
||||||
|
session_cache.delete()
|
||||||
|
log.debug({
|
||||||
|
'response_code': code,
|
||||||
|
'response_text': content,
|
||||||
|
})
|
||||||
|
if log.isEnabledFor(logging.DEBUG):
|
||||||
|
return 'Error: {0}'.format(code), API_ERROR
|
||||||
|
return None, API_ERROR
|
||||||
|
|
||||||
|
|
||||||
def _process_server_results(heartbeats, code, content, results, args, configs):
|
def _process_server_results(heartbeats, code, content, results, args, configs):
|
||||||
log.debug({
|
log.debug({
|
||||||
'response_code': code,
|
'response_code': code,
|
||||||
|
@ -201,6 +201,9 @@ def parse_arguments():
|
|||||||
'online 5 offline heartbeats are synced. Can ' +
|
'online 5 offline heartbeats are synced. Can ' +
|
||||||
'be used without --entity to only sync offline ' +
|
'be used without --entity to only sync offline ' +
|
||||||
'activity without generating new heartbeats.')
|
'activity without generating new heartbeats.')
|
||||||
|
parser.add_argument('--today', dest='today',
|
||||||
|
action='store_true',
|
||||||
|
help='Prints dashboard time for Today, then exits.')
|
||||||
parser.add_argument('--config', dest='config', action=StoreWithoutQuotes,
|
parser.add_argument('--config', dest='config', action=StoreWithoutQuotes,
|
||||||
help='Defaults to ~/.wakatime.cfg.')
|
help='Defaults to ~/.wakatime.cfg.')
|
||||||
parser.add_argument('--verbose', dest='verbose', action='store_true',
|
parser.add_argument('--verbose', dest='verbose', action='store_true',
|
||||||
@ -245,7 +248,7 @@ def parse_arguments():
|
|||||||
if not args.entity:
|
if not args.entity:
|
||||||
if args.file:
|
if args.file:
|
||||||
args.entity = args.file
|
args.entity = args.file
|
||||||
elif not args.sync_offline_activity or args.sync_offline_activity == 'none':
|
elif (not args.sync_offline_activity or args.sync_offline_activity == 'none') and not args.today:
|
||||||
parser.error('argument --entity is required')
|
parser.error('argument --entity is required')
|
||||||
|
|
||||||
if not args.sync_offline_activity:
|
if not args.sync_offline_activity:
|
||||||
|
@ -22,7 +22,7 @@ sys.path.insert(0, os.path.dirname(pwd))
|
|||||||
sys.path.insert(0, os.path.join(pwd, 'packages'))
|
sys.path.insert(0, os.path.join(pwd, 'packages'))
|
||||||
|
|
||||||
from .__about__ import __version__
|
from .__about__ import __version__
|
||||||
from .api import send_heartbeats
|
from .api import send_heartbeats, get_time_today
|
||||||
from .arguments import parse_arguments
|
from .arguments import parse_arguments
|
||||||
from .compat import u, json
|
from .compat import u, json
|
||||||
from .constants import SUCCESS, UNKNOWN_ERROR, HEARTBEATS_PER_REQUEST
|
from .constants import SUCCESS, UNKNOWN_ERROR, HEARTBEATS_PER_REQUEST
|
||||||
@ -42,6 +42,12 @@ def execute(argv=None):
|
|||||||
|
|
||||||
setup_logging(args, __version__)
|
setup_logging(args, __version__)
|
||||||
|
|
||||||
|
if args.today:
|
||||||
|
text, retval = get_time_today(args)
|
||||||
|
if text:
|
||||||
|
print(text)
|
||||||
|
return retval
|
||||||
|
|
||||||
try:
|
try:
|
||||||
heartbeats = []
|
heartbeats = []
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user