New argument --show-time-today for fetching dashboard coding activity
This commit is contained in:
parent
1f6d0f4981
commit
708001c6e7
7 changed files with 167 additions and 4 deletions
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
113
wakatime/api.py
113
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
|
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,
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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 = []
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue