New argument --show-time-today for fetching dashboard coding activity

This commit is contained in:
Alan Hamlett 2019-05-07 00:12:30 -07:00
parent 1f6d0f4981
commit 708001c6e7
7 changed files with 167 additions and 4 deletions

View file

@ -10,4 +10,4 @@ usage: wakatime [-h] [--entity FILE] [--key KEY] [--write] [--plugin PLUGIN]
[--include-only-with-project-file] [--extra-heartbeats] [--include-only-with-project-file] [--extra-heartbeats]
[--log-file LOG_FILE] [--api-url API_URL] [--timeout TIMEOUT] [--log-file LOG_FILE] [--api-url API_URL] [--timeout TIMEOUT]
[--sync-offline-activity SYNC_OFFLINE_ACTIVITY] [--sync-offline-activity SYNC_OFFLINE_ACTIVITY]
[--config CONFIG] [--verbose] [--version] [--show-time-today] [--config CONFIG] [--verbose] [--version]

View file

@ -76,6 +76,7 @@ optional arguments:
sent while online 5 offline heartbeats are synced. Can sent while online 5 offline heartbeats are synced. Can
be used without --entity to only sync offline activity be used without --entity to only sync offline activity
without generating new heartbeats. without generating new heartbeats.
--show-time-today Returns dashboard time for Today.
--config CONFIG Defaults to ~/.wakatime.cfg. --config CONFIG Defaults to ~/.wakatime.cfg.
--verbose Turns on debug messages in log file. --verbose Turns on debug messages in log file.
--version show program's version number and exit --version show program's version number and exit

View file

@ -21,7 +21,7 @@ from wakatime.constants import (
from wakatime.packages.requests.exceptions import RequestException from wakatime.packages.requests.exceptions import RequestException
from wakatime.packages.requests.models import Response from wakatime.packages.requests.models import Response
from wakatime.utils import get_user_agent 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): class ArgumentsTestCase(TestCase):
@ -322,6 +322,27 @@ class ArgumentsTestCase(TestCase):
self.assertHeartbeatNotSavedOffline() self.assertHeartbeatNotSavedOffline()
self.assertOfflineHeartbeatsNotSynced() 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() @log_capture()
def test_missing_api_key(self, logs): def test_missing_api_key(self, logs):
logging.disable(logging.NOTSET) logging.disable(logging.NOTSET)

View file

@ -287,3 +287,22 @@ class CustomResponse(Response):
return self.second_response_text if self.second_response_text is not None else self.response_text return self.second_response_text if self.second_response_text is not None else self.response_text
self._count += 1 self._count += 1
return self.response_text 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

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_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): 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('--show-time-today', dest='show_time_today',
action='store_true',
help='Returns dashboard time for Today.')
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.show_time_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_coding_time
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.show_time_today:
text, retval = get_coding_time('today', 'today', args)
if text:
print(text)
return retval
try: try:
heartbeats = [] heartbeats = []