Compare commits

..

16 Commits
8.3.6 ... 8.6.0

Author SHA1 Message Date
b2086a3cd2 v8.6.0 2019-05-27 12:41:14 -07:00
005b07520c changes for v8.6.0 2019-05-27 12:40:55 -07:00
60608bd322 Skip prompting for api key when found from common config file 2019-05-27 12:39:48 -07:00
cde8f8f1de v8.5.0 2019-05-10 09:45:11 -07:00
4adfca154c changes for v8.5.0 2019-05-10 09:44:57 -07:00
f7b3924a30 use wakatime-cli to fetch today coding time 2019-05-10 09:43:30 -07:00
db00024455 remove clock icon from status bar 2019-05-10 09:18:38 -07:00
9a6be7ca4e v8.4.2 2019-05-07 18:44:24 -07:00
1ea9b2a761 changes for v8.4.2 2019-05-07 18:43:57 -07:00
bd5e87e030 upgrade wakatime-cli to v11.0.0 2019-05-07 18:42:31 -07:00
0256ff4a6a v8.4.1 2019-05-01 10:57:50 -07:00
9d170b3276 changes for v8.4.1 2019-05-01 10:57:38 -07:00
c54e575210 use api subdomain for fetching summaries 2019-05-01 10:57:11 -07:00
07513d8f10 v8.4.0 2019-05-01 10:49:59 -07:00
30902cc050 changes for v8.4.0 2019-05-01 10:49:40 -07:00
aa7962d49a show today coding activity time in status bar 2019-05-01 10:48:28 -07:00
7 changed files with 254 additions and 21 deletions

View File

@ -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)
++++++++++++++++++ ++++++++++++++++++

View File

@ -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

View File

@ -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,

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__ = ('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'

View File

@ -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,

View File

@ -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:

View File

@ -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 = []