From 708001c6e72e553ba6daa707a063904745a60aa7 Mon Sep 17 00:00:00 2001 From: Alan Hamlett Date: Tue, 7 May 2019 00:12:30 -0700 Subject: [PATCH] New argument --show-time-today for fetching dashboard coding activity --- tests/samples/output/common_usage_header | 2 +- tests/samples/output/test_help_contents | 1 + tests/test_arguments.py | 23 ++++- tests/utils.py | 19 ++++ wakatime/api.py | 113 +++++++++++++++++++++++ wakatime/arguments.py | 5 +- wakatime/main.py | 8 +- 7 files changed, 167 insertions(+), 4 deletions(-) diff --git a/tests/samples/output/common_usage_header b/tests/samples/output/common_usage_header index 4e1f70a..16de27a 100644 --- a/tests/samples/output/common_usage_header +++ b/tests/samples/output/common_usage_header @@ -10,4 +10,4 @@ usage: wakatime [-h] [--entity FILE] [--key KEY] [--write] [--plugin PLUGIN] [--include-only-with-project-file] [--extra-heartbeats] [--log-file LOG_FILE] [--api-url API_URL] [--timeout TIMEOUT] [--sync-offline-activity SYNC_OFFLINE_ACTIVITY] - [--config CONFIG] [--verbose] [--version] + [--show-time-today] [--config CONFIG] [--verbose] [--version] diff --git a/tests/samples/output/test_help_contents b/tests/samples/output/test_help_contents index d420bda..9be4798 100644 --- a/tests/samples/output/test_help_contents +++ b/tests/samples/output/test_help_contents @@ -76,6 +76,7 @@ optional arguments: sent while online 5 offline heartbeats are synced. Can be used without --entity to only sync offline activity without generating new heartbeats. + --show-time-today Returns dashboard time for Today. --config CONFIG Defaults to ~/.wakatime.cfg. --verbose Turns on debug messages in log file. --version show program's version number and exit diff --git a/tests/test_arguments.py b/tests/test_arguments.py index 202a48a..2cd29b4 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -21,7 +21,7 @@ from wakatime.constants import ( from wakatime.packages.requests.exceptions import RequestException from wakatime.packages.requests.models import Response from wakatime.utils import get_user_agent -from .utils import mock, json, ANY, CustomResponse, TemporaryDirectory, TestCase, NamedTemporaryFile +from .utils import mock, json, ANY, CustomResponse, SummaryResponse, TemporaryDirectory, TestCase, NamedTemporaryFile class ArgumentsTestCase(TestCase): @@ -322,6 +322,27 @@ class ArgumentsTestCase(TestCase): self.assertHeartbeatNotSavedOffline() self.assertOfflineHeartbeatsNotSynced() + @log_capture() + def test_missing_entity_argument_with_show_time_today_arg(self, logs): + logging.disable(logging.NOTSET) + + self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].return_value = SummaryResponse() + + config = 'tests/samples/configs/good_config.cfg' + args = ['--config', config, '--show-time-today', '--verbose'] + + retval = execute(args) + + self.assertEquals(retval, SUCCESS) + self.assertNothingLogged(logs) + + expected = '4 hrs 23 mins\n' + actual = self.getPrintedOutput() + self.assertEquals(actual, expected) + + self.assertHeartbeatNotSavedOffline() + self.assertOfflineHeartbeatsNotSynced() + @log_capture() def test_missing_api_key(self, logs): logging.disable(logging.NOTSET) diff --git a/tests/utils.py b/tests/utils.py index a626ffd..dc28cda 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -287,3 +287,22 @@ class CustomResponse(Response): return self.second_response_text if self.second_response_text is not None else self.response_text self._count += 1 return self.response_text + + +class SummaryResponse(Response): + response_code = 200 + response_text = '{"data": [{"grand_total": {"text": "4 hrs 23 mins"}}]}' + + _count = 0 + + @property + def status_code(self): + return self.response_code + + @status_code.setter + def status_code(self, value): + pass + + @property + def text(self): + return self.response_text diff --git a/wakatime/api.py b/wakatime/api.py index a98ebeb..4665ccc 100644 --- a/wakatime/api.py +++ b/wakatime/api.py @@ -163,6 +163,119 @@ def send_heartbeats(heartbeats, args, configs, use_ntlm_proxy=False): return AUTH_ERROR if code == 401 else API_ERROR +def get_coding_time(start, end, 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': start, + 'end': end, + } + + # 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_coding_time(start, end, 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 '{}: {}'.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_coding_time(start, end, 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 '{}: {}'.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 '{}: {}'.format(sys.exc_info()[0].__name__, u(sys.exc_info()[1])), API_ERROR + return None, API_ERROR + else: + if should_try_ntlm: + return get_coding_time(start, end, args, use_ntlm_proxy=True) + + session_cache.delete() + log.debug({ + 'response_code': code, + 'response_text': content, + }) + if log.isEnabledFor(logging.DEBUG): + return 'Error: {}'.format(code), API_ERROR + return None, API_ERROR + + def _process_server_results(heartbeats, code, content, results, args, configs): log.debug({ 'response_code': code, diff --git a/wakatime/arguments.py b/wakatime/arguments.py index d02b57a..fdab389 100644 --- a/wakatime/arguments.py +++ b/wakatime/arguments.py @@ -201,6 +201,9 @@ def parse_arguments(): 'online 5 offline heartbeats are synced. Can ' + 'be used without --entity to only sync offline ' + 'activity without generating new heartbeats.') + parser.add_argument('--show-time-today', dest='show_time_today', + action='store_true', + help='Returns dashboard time for Today.') parser.add_argument('--config', dest='config', action=StoreWithoutQuotes, help='Defaults to ~/.wakatime.cfg.') parser.add_argument('--verbose', dest='verbose', action='store_true', @@ -245,7 +248,7 @@ def parse_arguments(): if not args.entity: if 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.show_time_today: parser.error('argument --entity is required') if not args.sync_offline_activity: diff --git a/wakatime/main.py b/wakatime/main.py index a1a6dca..c169eb4 100644 --- a/wakatime/main.py +++ b/wakatime/main.py @@ -22,7 +22,7 @@ sys.path.insert(0, os.path.dirname(pwd)) sys.path.insert(0, os.path.join(pwd, 'packages')) from .__about__ import __version__ -from .api import send_heartbeats +from .api import send_heartbeats, get_coding_time from .arguments import parse_arguments from .compat import u, json from .constants import SUCCESS, UNKNOWN_ERROR, HEARTBEATS_PER_REQUEST @@ -42,6 +42,12 @@ def execute(argv=None): setup_logging(args, __version__) + if args.show_time_today: + text, retval = get_coding_time('today', 'today', args) + if text: + print(text) + return retval + try: heartbeats = []