2017-11-09 10:12:05 +03:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
|
|
wakatime.api
|
|
|
|
~~~~~~~~~~~~
|
|
|
|
|
|
|
|
:copyright: (c) 2017 Alan Hamlett.
|
|
|
|
:license: BSD, see LICENSE for more details.
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import print_function
|
|
|
|
|
|
|
|
import base64
|
|
|
|
import logging
|
|
|
|
import sys
|
|
|
|
import traceback
|
|
|
|
|
|
|
|
from .compat import u, is_py3, json
|
2017-11-09 20:09:28 +03:00
|
|
|
from .constants import API_ERROR, AUTH_ERROR, SUCCESS, UNKNOWN_ERROR
|
2017-11-09 10:12:05 +03:00
|
|
|
|
|
|
|
from .offlinequeue import Queue
|
|
|
|
from .packages.requests.exceptions import RequestException
|
|
|
|
from .session_cache import SessionCache
|
|
|
|
from .utils import get_hostname, get_user_agent
|
|
|
|
from .packages import tzlocal
|
|
|
|
|
|
|
|
|
|
|
|
log = logging.getLogger('WakaTime')
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
from .packages import requests
|
2017-11-23 23:41:59 +03:00
|
|
|
except ImportError: # pragma: nocover
|
2017-11-09 10:12:05 +03:00
|
|
|
log.traceback(logging.ERROR)
|
|
|
|
print(traceback.format_exc())
|
|
|
|
log.error('Please upgrade Python to the latest version.')
|
|
|
|
print('Please upgrade Python to the latest version.')
|
|
|
|
sys.exit(UNKNOWN_ERROR)
|
|
|
|
|
|
|
|
|
|
|
|
def send_heartbeats(heartbeats, args, configs, use_ntlm_proxy=False):
|
|
|
|
"""Send heartbeats to WakaTime API.
|
|
|
|
|
|
|
|
Returns `SUCCESS` when heartbeat was sent, otherwise returns an error code.
|
|
|
|
"""
|
|
|
|
|
|
|
|
if len(heartbeats) == 0:
|
|
|
|
return SUCCESS
|
|
|
|
|
|
|
|
api_url = args.api_url
|
|
|
|
if not api_url:
|
2017-11-09 20:09:28 +03:00
|
|
|
api_url = 'https://api.wakatime.com/api/v1/users/current/heartbeats.bulk'
|
2017-11-09 10:12:05 +03:00
|
|
|
log.debug('Sending heartbeats to api at %s' % api_url)
|
|
|
|
timeout = args.timeout
|
|
|
|
if not timeout:
|
|
|
|
timeout = 60
|
|
|
|
|
|
|
|
data = [h.sanitize().dict() for h in heartbeats]
|
|
|
|
log.debug(data)
|
|
|
|
|
|
|
|
# setup api request
|
|
|
|
request_body = json.dumps(data)
|
|
|
|
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),
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
'Accept': 'application/json',
|
|
|
|
'Authorization': auth,
|
|
|
|
}
|
|
|
|
|
|
|
|
hostname = get_hostname(args)
|
|
|
|
if hostname:
|
|
|
|
headers['X-Machine-Name'] = u(hostname).encode('utf-8')
|
|
|
|
|
|
|
|
# add Olson timezone to request
|
|
|
|
try:
|
|
|
|
tz = tzlocal.get_localzone()
|
|
|
|
except:
|
|
|
|
tz = None
|
|
|
|
if tz:
|
|
|
|
headers['TimeZone'] = u(tz.zone).encode('utf-8')
|
|
|
|
|
|
|
|
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
|
|
|
|
|
2019-03-31 04:53:40 +03:00
|
|
|
ssl_verify = not args.nosslverify
|
|
|
|
if args.ssl_certs_file and ssl_verify:
|
|
|
|
ssl_verify = args.ssl_certs_file
|
|
|
|
|
2017-11-09 10:12:05 +03:00
|
|
|
# send request to api
|
|
|
|
response, code = None, None
|
|
|
|
try:
|
|
|
|
response = session.post(api_url, data=request_body, headers=headers,
|
|
|
|
proxies=proxies, timeout=timeout,
|
2019-03-31 04:53:40 +03:00
|
|
|
verify=ssl_verify)
|
2017-11-09 10:12:05 +03:00
|
|
|
except RequestException:
|
|
|
|
if should_try_ntlm:
|
|
|
|
return send_heartbeats(heartbeats, args, configs, use_ntlm_proxy=True)
|
|
|
|
else:
|
|
|
|
exception_data = {
|
|
|
|
sys.exc_info()[0].__name__: u(sys.exc_info()[1]),
|
|
|
|
}
|
|
|
|
if log.isEnabledFor(logging.DEBUG):
|
|
|
|
exception_data['traceback'] = traceback.format_exc()
|
|
|
|
if args.offline:
|
|
|
|
queue = Queue(args, configs)
|
|
|
|
queue.push_many(heartbeats)
|
|
|
|
if log.isEnabledFor(logging.DEBUG):
|
|
|
|
log.warn(exception_data)
|
|
|
|
else:
|
|
|
|
log.error(exception_data)
|
|
|
|
|
|
|
|
except: # delete cached session when requests raises unknown exception
|
|
|
|
if should_try_ntlm:
|
|
|
|
return send_heartbeats(heartbeats, args, configs, use_ntlm_proxy=True)
|
|
|
|
else:
|
|
|
|
exception_data = {
|
|
|
|
sys.exc_info()[0].__name__: u(sys.exc_info()[1]),
|
|
|
|
'traceback': traceback.format_exc(),
|
|
|
|
}
|
|
|
|
if args.offline:
|
|
|
|
queue = Queue(args, configs)
|
|
|
|
queue.push_many(heartbeats)
|
|
|
|
log.warn(exception_data)
|
|
|
|
|
|
|
|
else:
|
|
|
|
code = response.status_code if response is not None else None
|
|
|
|
content = response.text if response is not None else None
|
2017-11-09 20:09:28 +03:00
|
|
|
|
2017-11-23 23:41:59 +03:00
|
|
|
if _success(code):
|
|
|
|
results = _get_results(response)
|
|
|
|
_process_server_results(heartbeats, code, content, results, args, configs)
|
2017-11-09 10:12:05 +03:00
|
|
|
session_cache.save(session)
|
|
|
|
return SUCCESS
|
2018-01-05 10:33:07 +03:00
|
|
|
else:
|
|
|
|
log.debug({
|
|
|
|
'response_code': code,
|
|
|
|
'response_text': content,
|
|
|
|
})
|
2017-11-09 10:12:05 +03:00
|
|
|
|
|
|
|
if should_try_ntlm:
|
|
|
|
return send_heartbeats(heartbeats, args, configs, use_ntlm_proxy=True)
|
2017-11-23 23:41:59 +03:00
|
|
|
|
|
|
|
_handle_unsent_heartbeats(heartbeats, code, content, args, configs)
|
2017-11-09 10:12:05 +03:00
|
|
|
|
|
|
|
session_cache.delete()
|
|
|
|
return AUTH_ERROR if code == 401 else API_ERROR
|
2017-11-09 20:09:28 +03:00
|
|
|
|
|
|
|
|
2019-05-08 04:42:31 +03:00
|
|
|
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:
|
2019-11-24 18:46:13 +03:00
|
|
|
summary = response.json()['data'][0]
|
|
|
|
if len(summary['categories']) > 1:
|
|
|
|
text = ', '.join(['{0} {1}'.format(x['text'], x['name'].lower()) for x in summary['categories']])
|
|
|
|
else:
|
|
|
|
text = summary['grand_total']['text']
|
2019-05-08 04:42:31 +03:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2017-11-23 23:41:59 +03:00
|
|
|
def _process_server_results(heartbeats, code, content, results, args, configs):
|
|
|
|
log.debug({
|
|
|
|
'response_code': code,
|
2018-01-05 10:33:07 +03:00
|
|
|
'results': results,
|
2017-11-23 23:41:59 +03:00
|
|
|
})
|
|
|
|
|
|
|
|
for i in range(len(results)):
|
|
|
|
if len(heartbeats) <= i:
|
|
|
|
log.warn('Results from api not matching heartbeats sent.')
|
|
|
|
break
|
|
|
|
|
|
|
|
try:
|
|
|
|
c = results[i][1]
|
|
|
|
except:
|
|
|
|
log.traceback(logging.WARNING)
|
|
|
|
c = 0
|
|
|
|
try:
|
|
|
|
text = json.dumps(results[i][0])
|
|
|
|
except:
|
|
|
|
log.traceback(logging.WARNING)
|
|
|
|
text = ''
|
|
|
|
if not _success(c):
|
|
|
|
_handle_unsent_heartbeats([heartbeats[i]], c, text, args, configs)
|
|
|
|
|
|
|
|
leftover = len(heartbeats) - len(results)
|
|
|
|
if leftover > 0:
|
|
|
|
log.warn('Missing {0} results from api.'.format(leftover))
|
|
|
|
start = len(heartbeats) - leftover
|
|
|
|
_handle_unsent_heartbeats(heartbeats[start:], code, content, args, configs)
|
|
|
|
|
2017-11-09 20:09:28 +03:00
|
|
|
|
2017-11-23 23:41:59 +03:00
|
|
|
def _handle_unsent_heartbeats(heartbeats, code, content, args, configs):
|
2017-11-09 20:09:28 +03:00
|
|
|
if args.offline:
|
|
|
|
if code == 400:
|
|
|
|
log.error({
|
|
|
|
'response_code': code,
|
|
|
|
'response_content': content,
|
|
|
|
})
|
|
|
|
else:
|
|
|
|
if log.isEnabledFor(logging.DEBUG):
|
|
|
|
log.warn({
|
|
|
|
'response_code': code,
|
|
|
|
'response_content': content,
|
|
|
|
})
|
|
|
|
queue = Queue(args, configs)
|
2017-11-23 23:41:59 +03:00
|
|
|
queue.push_many(heartbeats)
|
2017-11-09 20:09:28 +03:00
|
|
|
else:
|
|
|
|
log.error({
|
|
|
|
'response_code': code,
|
|
|
|
'response_content': content,
|
|
|
|
})
|
2017-11-23 23:41:59 +03:00
|
|
|
|
|
|
|
|
|
|
|
def _get_results(response):
|
|
|
|
results = []
|
|
|
|
if response is not None:
|
|
|
|
try:
|
2017-11-25 03:16:04 +03:00
|
|
|
results = response.json()['responses']
|
2017-11-23 23:41:59 +03:00
|
|
|
except:
|
|
|
|
log.traceback(logging.WARNING)
|
|
|
|
return results
|
|
|
|
|
|
|
|
|
|
|
|
def _success(code):
|
|
|
|
return code == requests.codes.created or code == requests.codes.accepted
|