# -*- 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 from .constants import API_ERROR, AUTH_ERROR, SUCCESS, UNKNOWN_ERROR from .offlinequeue import Queue from .session_cache import SessionCache from .utils import get_hostname, get_user_agent from .packages import tzlocal from .packages import certifi log = logging.getLogger('WakaTime') try: from .packages import requests except ImportError: # pragma: nocover 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) from .packages.requests.exceptions import RequestException 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: api_url = 'https://api.wakatime.com/api/v1/users/current/heartbeats.bulk' 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 # send request to api response, code = None, None try: response = session.post(api_url, data=request_body, headers=headers, proxies=proxies, timeout=timeout, verify=_get_verify(args)) 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 if _success(code): results = _get_results(response) _process_server_results(heartbeats, code, content, results, args, configs) session_cache.save(session) return SUCCESS else: log.debug({ 'response_code': code, 'response_text': content, }) if should_try_ntlm: return send_heartbeats(heartbeats, args, configs, use_ntlm_proxy=True) _handle_unsent_heartbeats(heartbeats, code, content, args, configs) session_cache.delete() 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 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=_get_verify(args)) 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: 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'] 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 _get_verify(args): verify = not args.nosslverify if verify: if args.ssl_certs_file: verify = args.ssl_certs_file else: verify = certifi.where() return verify def _process_server_results(heartbeats, code, content, results, args, configs): log.debug({ 'response_code': code, 'results': results, }) 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) def _handle_unsent_heartbeats(heartbeats, code, content, args, configs): 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) queue.push_many(heartbeats) else: log.error({ 'response_code': code, 'response_content': content, }) def _get_results(response): results = [] if response is not None: try: results = response.json()['responses'] except: log.traceback(logging.WARNING) return results def _success(code): return code == requests.codes.created or code == requests.codes.accepted