Compare commits

...

24 Commits

Author SHA1 Message Date
9e102d7c5c v8.0.6 2018-01-04 23:34:40 -08:00
5c1770fb48 changes for v8.0.6 2018-01-04 23:34:13 -08:00
683397534c upgrade wakatime-cli to v10.1.0 2018-01-04 23:33:07 -08:00
1c92017543 v8.0.5 2017-11-24 16:16:47 -08:00
fda1307668 changes for v8.0.5 2017-11-24 16:16:34 -08:00
1c84d457c5 upgrade wakatime-cli to v10.0.5 2017-11-24 16:16:04 -08:00
1e680ce739 v8.0.4 2017-11-23 12:49:07 -08:00
376adbb7d7 changes for v8.0.4 2017-11-23 12:48:44 -08:00
e0040e185b upgrade wakatime-cli to v10.0.4 2017-11-23 12:41:59 -08:00
c4a88541d0 v8.0.3 2017-11-22 13:12:09 -08:00
0cf621d177 changes for v8.0.3 2017-11-22 13:11:48 -08:00
db9d6cec97 upgrade wakatime-cli to v10.0.3 2017-11-22 13:09:17 -08:00
2c17f49a6b v8.0.2 2017-11-15 18:36:43 -08:00
95116d6007 changes for v8.0.2 2017-11-15 18:36:28 -08:00
8c52596f8f upgrade wakatime-cli to v10.0.2 2017-11-15 18:35:43 -08:00
3109817dc7 v8.0.1 2017-11-09 09:10:19 -08:00
0c0f965763 changes for v8.0.1 2017-11-09 09:10:06 -08:00
1573e9c825 upgrade wakatime-cli to v10.0.1 2017-11-09 09:09:28 -08:00
a0b8f349c2 v8.0.0 2017-11-08 23:14:04 -08:00
2fb60b1589 changes for v8.0.0 2017-11-08 23:13:47 -08:00
02786a744e upgrade wakatime-cli to v10.0.0 2017-11-08 23:12:05 -08:00
729a4360ba v7.0.26 2017-11-07 18:55:25 -08:00
8f45de85ec changes for v7.0.26 2017-11-07 18:55:03 -08:00
4672f70c87 upgrade wakatime-cli to v9.0.1 2017-11-07 18:54:17 -08:00
18 changed files with 682 additions and 476 deletions

View File

@ -3,6 +3,69 @@ History
------- -------
8.0.6 (2018-01-04)
++++++++++++++++++
- Upgrade wakatime-cli to v10.1.0.
- Ability to only track folders containing a .wakatime-project file using new
include_only_with_project_file argument and config option.
8.0.5 (2017-11-24)
++++++++++++++++++
- Upgrade wakatime-cli to v10.0.5.
- Fix bug that caused heartbeats to be cached locally instead of sent to API.
8.0.4 (2017-11-23)
++++++++++++++++++
- Upgrade wakatime-cli to v10.0.4.
- Improve Java dependency detection.
- Skip null or missing heartbeats from extra heartbeats argument.
8.0.3 (2017-11-22)
++++++++++++++++++
- Upgrade wakatime-cli to v10.0.3.
- Support saving unicode heartbeats when working offline.
`wakatime#112 <https://github.com/wakatime/wakatime/issues/112>`_
8.0.2 (2017-11-15)
++++++++++++++++++
- Upgrade wakatime-cli to v10.0.2.
- Limit bulk syncing to 5 heartbeats per request.
`wakatime#109 <https://github.com/wakatime/wakatime/issues/109>`_
8.0.1 (2017-11-09)
++++++++++++++++++
- Upgrade wakatime-cli to v10.0.1.
- Parse array of results from bulk heartbeats endpoint, only saving heartbeats
to local offline cache when they were not accepted by the api.
8.0.0 (2017-11-08)
++++++++++++++++++
- Upgrade wakatime-cli to v10.0.0.
- Upload multiple heartbeats to bulk endpoint for improved network performance.
`wakatime#107 <https://github.com/wakatime/wakatime/issues/107>`_
7.0.26 (2017-11-07)
++++++++++++++++++
- Upgrade wakatime-cli to v9.0.1.
- Fix bug causing 401 response when hidefilenames is enabled.
`wakatime#106 <https://github.com/wakatime/wakatime/issues/106>`_
7.0.25 (2017-11-05) 7.0.25 (2017-11-05)
++++++++++++++++++ ++++++++++++++++++
@ -10,7 +73,7 @@ History
`#78 <https://github.com/wakatime/sublime-wakatime/issues/78>`_ `#78 <https://github.com/wakatime/sublime-wakatime/issues/78>`_
- Upgrade wakatime-cli to v9.0.0. - Upgrade wakatime-cli to v9.0.0.
- Detect project and branch names from git submodules. - Detect project and branch names from git submodules.
`wakatime/#105 <https://github.com/wakatime/wakatime/issues/105>`_ `wakatime#105 <https://github.com/wakatime/wakatime/issues/105>`_
7.0.24 (2017-10-29) 7.0.24 (2017-10-29)
@ -23,7 +86,7 @@ History
- Upgrade requests to v2.18.4. - Upgrade requests to v2.18.4.
- Upgrade tzlocal to v1.4. - Upgrade tzlocal to v1.4.
- Use WAKATIME_HOME env variable for offline and session caching. - Use WAKATIME_HOME env variable for offline and session caching.
`wakatime/#102 <https://github.com/wakatime/wakatime/issues/102>`_ `wakatime#102 <https://github.com/wakatime/wakatime/issues/102>`_
7.0.23 (2017-09-14) 7.0.23 (2017-09-14)

View File

@ -7,7 +7,7 @@ Website: https://wakatime.com/
===========================================================""" ==========================================================="""
__version__ = '7.0.25' __version__ = '8.0.6'
import sublime import sublime

View File

@ -1,7 +1,7 @@
__title__ = 'wakatime' __title__ = 'wakatime'
__description__ = 'Common interface to the WakaTime api.' __description__ = 'Common interface to the WakaTime api.'
__url__ = 'https://github.com/wakatime/wakatime' __url__ = 'https://github.com/wakatime/wakatime'
__version_info__ = ('9', '0', '0') __version_info__ = ('10', '1', '0')
__version__ = '.'.join(__version_info__) __version__ = '.'.join(__version_info__)
__author__ = 'Alan Hamlett' __author__ = 'Alan Hamlett'
__author_email__ = 'alan@wakatime.com' __author_email__ = 'alan@wakatime.com'

226
packages/wakatime/api.py Normal file
View File

@ -0,0 +1,226 @@
# -*- 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 .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
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)
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=not args.nosslverify)
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 _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

View File

@ -45,7 +45,7 @@ class StoreWithoutQuotes(argparse.Action):
setattr(namespace, self.dest, values) setattr(namespace, self.dest, values)
def parseArguments(): def parse_arguments():
"""Parse command line arguments and configs from ~/.wakatime.cfg. """Parse command line arguments and configs from ~/.wakatime.cfg.
Command line arguments take precedence over config file settings. Command line arguments take precedence over config file settings.
Returns instances of ArgumentParser and SafeConfigParser. Returns instances of ArgumentParser and SafeConfigParser.
@ -103,12 +103,17 @@ def parseArguments():
'auto-detected language') 'auto-detected language')
parser.add_argument('--hostname', dest='hostname', action=StoreWithoutQuotes, help='hostname of '+ parser.add_argument('--hostname', dest='hostname', action=StoreWithoutQuotes, help='hostname of '+
'current machine.') 'current machine.')
parser.add_argument('--disableoffline', dest='offline', parser.add_argument('--disable-offline', dest='offline',
action='store_false', action='store_false',
help='disables offline time logging instead of queuing logged time') help='disables offline time logging instead of queuing logged time')
parser.add_argument('--disableoffline', dest='offline_deprecated',
action='store_true', help=argparse.SUPPRESS)
parser.add_argument('--hide-filenames', dest='hide_filenames',
action='store_true',
help='obfuscate filenames; will not send file names to api')
parser.add_argument('--hidefilenames', dest='hidefilenames', parser.add_argument('--hidefilenames', dest='hidefilenames',
action='store_true', action='store_true',
help='obfuscate file names; will not send file names to api') help=argparse.SUPPRESS)
parser.add_argument('--exclude', dest='exclude', action='append', parser.add_argument('--exclude', dest='exclude', action='append',
help='filename patterns to exclude from logging; POSIX regex '+ help='filename patterns to exclude from logging; POSIX regex '+
'syntax; can be used more than once') 'syntax; can be used more than once')
@ -116,15 +121,24 @@ def parseArguments():
help='filename patterns to log; when used in combination with '+ help='filename patterns to log; when used in combination with '+
'--exclude, files matching include will still be logged; '+ '--exclude, files matching include will still be logged; '+
'POSIX regex syntax; can be used more than once') 'POSIX regex syntax; can be used more than once')
parser.add_argument('--include-only-with-project-file',
dest='include_only_with_project_file',
action='store_true',
help='disables tracking folders unless they contain '+
'a .wakatime-project file; defaults to false')
parser.add_argument('--ignore', dest='ignore', action='append', parser.add_argument('--ignore', dest='ignore', action='append',
help=argparse.SUPPRESS) help=argparse.SUPPRESS)
parser.add_argument('--extra-heartbeats', dest='extra_heartbeats', parser.add_argument('--extra-heartbeats', dest='extra_heartbeats',
action='store_true', action='store_true',
help='reads extra heartbeats from STDIN as a JSON array until EOF') help='reads extra heartbeats from STDIN as a JSON array until EOF')
parser.add_argument('--logfile', dest='logfile', action=StoreWithoutQuotes, parser.add_argument('--log-file', dest='log_file', action=StoreWithoutQuotes,
help='defaults to ~/.wakatime.log') help='defaults to ~/.wakatime.log')
parser.add_argument('--apiurl', dest='api_url', action=StoreWithoutQuotes, parser.add_argument('--logfile', dest='logfile', action=StoreWithoutQuotes,
help=argparse.SUPPRESS)
parser.add_argument('--api-url', dest='api_url', action=StoreWithoutQuotes,
help='heartbeats api url; for debugging with a local server') help='heartbeats api url; for debugging with a local server')
parser.add_argument('--apiurl', dest='apiurl', action=StoreWithoutQuotes,
help=argparse.SUPPRESS)
parser.add_argument('--timeout', dest='timeout', type=int, action=StoreWithoutQuotes, parser.add_argument('--timeout', dest='timeout', type=int, action=StoreWithoutQuotes,
help='number of seconds to wait when sending heartbeats to api; '+ help='number of seconds to wait when sending heartbeats to api; '+
'defaults to 60 seconds') 'defaults to 60 seconds')
@ -194,6 +208,8 @@ def parseArguments():
args.exclude.append(pattern) args.exclude.append(pattern)
except TypeError: # pragma: nocover except TypeError: # pragma: nocover
pass pass
if not args.include_only_with_project_file and configs.has_option('settings', 'include_only_with_project_file'):
args.include_only_with_project_file = configs.get('settings', 'include_only_with_project_file')
if not args.include: if not args.include:
args.include = [] args.include = []
if configs.has_option('settings', 'include'): if configs.has_option('settings', 'include'):
@ -203,18 +219,26 @@ def parseArguments():
args.include.append(pattern) args.include.append(pattern)
except TypeError: # pragma: nocover except TypeError: # pragma: nocover
pass pass
if args.hidefilenames: if not args.hide_filenames and args.hidefilenames:
args.hidefilenames = ['.*'] args.hide_filenames = args.hidefilenames
if args.hide_filenames:
args.hide_filenames = ['.*']
else: else:
args.hidefilenames = [] args.hide_filenames = []
option = None
if configs.has_option('settings', 'hidefilenames'): if configs.has_option('settings', 'hidefilenames'):
option = configs.get('settings', 'hidefilenames') option = configs.get('settings', 'hidefilenames')
if configs.has_option('settings', 'hide_filenames'):
option = configs.get('settings', 'hide_filenames')
if option is not None:
if option.strip().lower() == 'true': if option.strip().lower() == 'true':
args.hidefilenames = ['.*'] args.hide_filenames = ['.*']
elif option.strip().lower() != 'false': elif option.strip().lower() != 'false':
for pattern in option.split("\n"): for pattern in option.split("\n"):
if pattern.strip() != '': if pattern.strip() != '':
args.hidefilenames.append(pattern) args.hide_filenames.append(pattern)
if args.offline_deprecated:
args.offline = False
if args.offline and configs.has_option('settings', 'offline'): if args.offline and configs.has_option('settings', 'offline'):
args.offline = configs.getboolean('settings', 'offline') args.offline = configs.getboolean('settings', 'offline')
if not args.proxy and configs.has_option('settings', 'proxy'): if not args.proxy and configs.has_option('settings', 'proxy'):
@ -235,11 +259,15 @@ def parseArguments():
args.verbose = configs.getboolean('settings', 'verbose') args.verbose = configs.getboolean('settings', 'verbose')
if not args.verbose and configs.has_option('settings', 'debug'): if not args.verbose and configs.has_option('settings', 'debug'):
args.verbose = configs.getboolean('settings', 'debug') args.verbose = configs.getboolean('settings', 'debug')
if not args.logfile and configs.has_option('settings', 'logfile'): if not args.log_file and args.logfile:
args.logfile = configs.get('settings', 'logfile') args.log_file = args.logfile
if not args.logfile and os.environ.get('WAKATIME_HOME'): if not args.log_file and configs.has_option('settings', 'log_file'):
args.log_file = configs.get('settings', 'log_file')
if not args.log_file and os.environ.get('WAKATIME_HOME'):
home = os.environ.get('WAKATIME_HOME') home = os.environ.get('WAKATIME_HOME')
args.logfile = os.path.join(os.path.expanduser(home), '.wakatime.log') args.log_file = os.path.join(os.path.expanduser(home), '.wakatime.log')
if not args.api_url and args.apiurl:
args.api_url = args.apiurl
if not args.api_url and configs.has_option('settings', 'api_url'): if not args.api_url and configs.has_option('settings', 'api_url'):
args.api_url = configs.get('settings', 'api_url') args.api_url = configs.get('settings', 'api_url')
if not args.timeout and configs.has_option('settings', 'timeout'): if not args.timeout and configs.has_option('settings', 'timeout'):

View File

@ -9,6 +9,7 @@
:license: BSD, see LICENSE for more details. :license: BSD, see LICENSE for more details.
""" """
import codecs import codecs
import sys import sys
@ -91,3 +92,9 @@ except ImportError: # pragma: nocover
name = _resolve_name(name[level:], package, level) name = _resolve_name(name[level:], package, level)
__import__(name) __import__(name)
return sys.modules[name] return sys.modules[name]
try:
from .packages import simplejson as json
except (ImportError, SyntaxError): # pragma: nocover
import json

View File

@ -21,7 +21,7 @@ from .constants import CONFIG_FILE_PARSE_ERROR
try: try:
import configparser import configparser
except ImportError: except ImportError: # pragma: nocover
from .packages import configparser from .packages import configparser

View File

@ -34,11 +34,6 @@ Exit code used when there was an unhandled exception.
""" """
UNKNOWN_ERROR = 105 UNKNOWN_ERROR = 105
""" Malformed Heartbeat Error
Exit code used when the JSON input from `--extra-heartbeats` is malformed.
"""
MALFORMED_HEARTBEAT_ERROR = 106
""" Connection Error """ Connection Error
Exit code used when there was proxy or other problem connecting to the WakaTime Exit code used when there was proxy or other problem connecting to the WakaTime
API servers. API servers.

View File

@ -43,7 +43,7 @@ class JavaParser(TokenParser):
self._process_other(token, content) self._process_other(token, content)
def _process_namespace(self, token, content): def _process_namespace(self, token, content):
if u(content) == u('import'): if u(content).split() and u(content).split()[0] == u('import'):
self.state = 'import' self.state = 'import'
elif self.state == 'import': elif self.state == 'import':

View File

@ -0,0 +1,204 @@
# -*- coding: utf-8 -*-
"""
wakatime.heartbeat
~~~~~~~~~~~~~~~~~~
:copyright: (c) 2017 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
import os
import logging
import re
from .compat import u, json
from .project import get_project_info
from .stats import get_file_stats
from .utils import get_user_agent, should_exclude, format_file_path, find_project_file
log = logging.getLogger('WakaTime')
class Heartbeat(object):
"""Heartbeat data for sending to API or storing in offline cache."""
skip = False
args = None
configs = None
time = None
entity = None
type = None
is_write = None
project = None
branch = None
language = None
dependencies = None
lines = None
lineno = None
cursorpos = None
user_agent = None
def __init__(self, data, args, configs, _clone=None):
if not data:
self.skip = u('Skipping because heartbeat data is missing.')
return
self.args = args
self.configs = configs
self.entity = data.get('entity')
self.time = data.get('time', data.get('timestamp'))
self.is_write = data.get('is_write')
self.user_agent = data.get('user_agent') or get_user_agent(args.plugin)
self.type = data.get('type', data.get('entity_type'))
if self.type not in ['file', 'domain', 'app']:
self.type = 'file'
if not _clone:
exclude = self._excluded_by_pattern()
if exclude:
self.skip = u('Skipping because matches exclude pattern: {pattern}').format(
pattern=u(exclude),
)
return
if self.type == 'file':
self.entity = format_file_path(self.entity)
if not self.entity or not os.path.isfile(self.entity):
self.skip = u('File does not exist; ignoring this heartbeat.')
return
if self._excluded_by_missing_project_file():
self.skip = u('Skipping because missing .wakatime-project file in parent path.')
return
project, branch = get_project_info(configs, self, data)
self.project = project
self.branch = branch
stats = get_file_stats(self.entity,
entity_type=self.type,
lineno=data.get('lineno'),
cursorpos=data.get('cursorpos'),
plugin=args.plugin,
language=data.get('language'))
else:
self.project = data.get('project')
self.branch = data.get('branch')
stats = data
for key in ['language', 'dependencies', 'lines', 'lineno', 'cursorpos']:
if stats.get(key) is not None:
setattr(self, key, stats[key])
def update(self, attrs):
"""Return a copy of the current Heartbeat with updated attributes."""
data = self.dict()
data.update(attrs)
heartbeat = Heartbeat(data, self.args, self.configs, _clone=True)
return heartbeat
def sanitize(self):
"""Removes sensitive data including file names and dependencies.
Returns a Heartbeat.
"""
if not self.args.hide_filenames:
return self
if self.entity is None:
return self
if self.type != 'file':
return self
for pattern in self.args.hide_filenames:
try:
compiled = re.compile(pattern, re.IGNORECASE)
if compiled.search(self.entity):
sanitized = {}
sensitive = ['dependencies', 'lines', 'lineno', 'cursorpos', 'branch']
for key, val in self.items():
if key in sensitive:
sanitized[key] = None
else:
sanitized[key] = val
extension = u(os.path.splitext(self.entity)[1])
sanitized['entity'] = u('HIDDEN{0}').format(extension)
return self.update(sanitized)
except re.error as ex:
log.warning(u('Regex error ({msg}) for include pattern: {pattern}').format(
msg=u(ex),
pattern=u(pattern),
))
return self
def json(self):
return json.dumps(self.dict())
def dict(self):
return {
'time': self.time,
'entity': self._unicode(self.entity),
'type': self.type,
'is_write': self.is_write,
'project': self._unicode(self.project),
'branch': self._unicode(self.branch),
'language': self._unicode(self.language),
'dependencies': self._unicode_list(self.dependencies),
'lines': self.lines,
'lineno': self.lineno,
'cursorpos': self.cursorpos,
'user_agent': self._unicode(self.user_agent),
}
def items(self):
return self.dict().items()
def get_id(self):
return u('{time}-{type}-{project}-{branch}-{entity}-{is_write}').format(
time=self.time,
type=self.type,
project=self._unicode(self.project),
branch=self._unicode(self.branch),
entity=self._unicode(self.entity),
is_write=self.is_write,
)
def _unicode(self, value):
if value is None:
return None
return u(value)
def _unicode_list(self, values):
if values is None:
return None
return [self._unicode(value) for value in values]
def _excluded_by_pattern(self):
return should_exclude(self.entity, self.args.include, self.args.exclude)
def _excluded_by_missing_project_file(self):
if not self.args.include_only_with_project_file:
return False
return find_project_file(self.entity) is None
def __repr__(self):
return self.json()
def __bool__(self):
return not self.skip
def __nonzero__(self):
return self.__bool__()
def __getitem__(self, key):
return self.dict()[key]

View File

@ -75,7 +75,7 @@ def setup_logging(args, version):
for handler in logger.handlers: for handler in logger.handlers:
logger.removeHandler(handler) logger.removeHandler(handler)
set_log_level(logger, args) set_log_level(logger, args)
logfile = args.logfile logfile = args.log_file
if not logfile: if not logfile:
logfile = '~/.wakatime.log' logfile = '~/.wakatime.log'
handler = logging.FileHandler(os.path.expanduser(logfile)) handler = logging.FileHandler(os.path.expanduser(logfile))

View File

@ -11,387 +11,64 @@
from __future__ import print_function from __future__ import print_function
import base64
import logging import logging
import os import os
import re
import sys import sys
import traceback import traceback
import socket
pwd = os.path.dirname(os.path.abspath(__file__)) pwd = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(pwd)) 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 .arguments import parseArguments from .api import send_heartbeats
from .compat import u, is_py3 from .arguments import parse_arguments
from .constants import ( from .compat import u, json
API_ERROR, from .constants import SUCCESS, UNKNOWN_ERROR
AUTH_ERROR,
SUCCESS,
UNKNOWN_ERROR,
MALFORMED_HEARTBEAT_ERROR,
)
from .logger import setup_logging from .logger import setup_logging
log = logging.getLogger('WakaTime') log = logging.getLogger('WakaTime')
try: from .heartbeat import Heartbeat
from .packages import requests
except ImportError:
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 .offlinequeue import Queue from .offlinequeue import Queue
from .packages.requests.exceptions import RequestException
from .project import get_project_info
from .session_cache import SessionCache
from .stats import get_file_stats
from .utils import get_user_agent, should_exclude, format_file_path
try:
from .packages import simplejson as json # pragma: nocover
except (ImportError, SyntaxError): # pragma: nocover
import json
from .packages import tzlocal
def send_heartbeat(project=None, branch=None, hostname=None, stats={}, key=None,
entity=None, timestamp=None, is_write=None, plugin=None,
offline=None, entity_type='file', hidefilenames=None,
proxy=None, nosslverify=None, api_url=None, timeout=None,
use_ntlm_proxy=False, **kwargs):
"""Sends heartbeat as POST request to WakaTime api server.
Returns `SUCCESS` when heartbeat was sent, otherwise returns an
error code constant.
"""
if not api_url:
api_url = 'https://api.wakatime.com/api/v1/heartbeats'
if not timeout:
timeout = 60
log.debug('Sending heartbeat to api at %s' % api_url)
data = {
'time': timestamp,
'entity': entity,
'type': entity_type,
}
if stats.get('lines'):
data['lines'] = stats['lines']
if stats.get('language'):
data['language'] = stats['language']
if stats.get('dependencies'):
data['dependencies'] = stats['dependencies']
if stats.get('lineno'):
data['lineno'] = stats['lineno']
if stats.get('cursorpos'):
data['cursorpos'] = stats['cursorpos']
if is_write:
data['is_write'] = is_write
if project:
data['project'] = project
if branch:
data['branch'] = branch
if hidefilenames and entity is not None and entity_type == 'file':
for pattern in hidefilenames:
try:
compiled = re.compile(pattern, re.IGNORECASE)
if compiled.search(entity):
extension = u(os.path.splitext(data['entity'])[1])
data['entity'] = u('HIDDEN{0}').format(extension)
# also delete any sensitive info when hiding file names
sensitive = ['dependencies', 'lines', 'lineno', 'cursorpos', 'branch']
for key in sensitive:
if key in data:
del data[key]
break
except re.error as ex:
log.warning(u('Regex error ({msg}) for include pattern: {pattern}').format(
msg=u(ex),
pattern=u(pattern),
))
log.debug(data)
# setup api request
request_body = json.dumps(data)
api_key = u(base64.b64encode(str.encode(key) if is_py3 else key))
auth = u('Basic {api_key}').format(api_key=api_key)
headers = {
'User-Agent': get_user_agent(plugin),
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': auth,
}
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 proxy:
if use_ntlm_proxy:
from .packages.requests_ntlm import HttpNtlmAuth
username = 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 proxy
proxies['https'] = proxy
# send request to api
response = None
try:
response = session.post(api_url, data=request_body, headers=headers,
proxies=proxies, timeout=timeout,
verify=not nosslverify)
except RequestException:
if should_try_ntlm:
return send_heartbeat(
project=project,
entity=entity,
timestamp=timestamp,
branch=branch,
hostname=hostname,
stats=stats,
key=key,
is_write=is_write,
plugin=plugin,
offline=offline,
hidefilenames=hidefilenames,
entity_type=entity_type,
proxy=proxy,
api_url=api_url,
timeout=timeout,
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 offline:
queue = Queue()
queue.push(data, json.dumps(stats), plugin)
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_heartbeat(
project=project,
entity=entity,
timestamp=timestamp,
branch=branch,
hostname=hostname,
stats=stats,
key=key,
is_write=is_write,
plugin=plugin,
offline=offline,
hidefilenames=hidefilenames,
entity_type=entity_type,
proxy=proxy,
api_url=api_url,
timeout=timeout,
use_ntlm_proxy=True,
)
else:
exception_data = {
sys.exc_info()[0].__name__: u(sys.exc_info()[1]),
'traceback': traceback.format_exc(),
}
if offline:
queue = Queue()
queue.push(data, json.dumps(stats), plugin)
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 code == requests.codes.created or code == requests.codes.accepted:
log.debug({
'response_code': code,
})
session_cache.save(session)
return SUCCESS
if should_try_ntlm:
return send_heartbeat(
project=project,
entity=entity,
timestamp=timestamp,
branch=branch,
hostname=hostname,
stats=stats,
key=key,
is_write=is_write,
plugin=plugin,
offline=offline,
hidefilenames=hidefilenames,
entity_type=entity_type,
proxy=proxy,
api_url=api_url,
timeout=timeout,
use_ntlm_proxy=True,
)
else:
if offline:
if code != 400:
queue = Queue()
queue.push(data, json.dumps(stats), plugin)
if code == 401:
log.error({
'response_code': code,
'response_content': content,
})
session_cache.delete()
return AUTH_ERROR
elif log.isEnabledFor(logging.DEBUG):
log.warn({
'response_code': code,
'response_content': content,
})
else:
log.error({
'response_code': code,
'response_content': content,
})
else:
log.error({
'response_code': code,
'response_content': content,
})
session_cache.delete()
return API_ERROR
def sync_offline_heartbeats(args, hostname):
"""Sends all heartbeats which were cached in the offline Queue."""
queue = Queue()
while True:
heartbeat = queue.pop()
if heartbeat is None:
break
status = send_heartbeat(
project=heartbeat['project'],
entity=heartbeat['entity'],
timestamp=heartbeat['time'],
branch=heartbeat['branch'],
hostname=hostname,
stats=json.loads(heartbeat['stats']),
key=args.key,
is_write=heartbeat['is_write'],
plugin=heartbeat['plugin'],
offline=args.offline,
hidefilenames=args.hidefilenames,
entity_type=heartbeat['type'],
proxy=args.proxy,
api_url=args.api_url,
timeout=args.timeout,
)
if status != SUCCESS:
if status == AUTH_ERROR:
return AUTH_ERROR
break
return SUCCESS
def process_heartbeat(args, configs, hostname, heartbeat):
exclude = should_exclude(heartbeat['entity'], args.include, args.exclude)
if exclude is not False:
log.debug(u('Skipping because matches exclude pattern: {pattern}').format(
pattern=u(exclude),
))
return SUCCESS
if heartbeat.get('entity_type') not in ['file', 'domain', 'app']:
heartbeat['entity_type'] = 'file'
if heartbeat['entity_type'] == 'file':
heartbeat['entity'] = format_file_path(heartbeat['entity'])
if heartbeat['entity_type'] != 'file' or os.path.isfile(heartbeat['entity']):
stats = get_file_stats(heartbeat['entity'],
entity_type=heartbeat['entity_type'],
lineno=heartbeat.get('lineno'),
cursorpos=heartbeat.get('cursorpos'),
plugin=args.plugin,
language=heartbeat.get('language'))
project = heartbeat.get('project') or heartbeat.get('alternate_project')
branch = None
if heartbeat['entity_type'] == 'file':
project, branch = get_project_info(configs, heartbeat)
heartbeat['project'] = project
heartbeat['branch'] = branch
heartbeat['stats'] = stats
heartbeat['hostname'] = hostname
heartbeat['timeout'] = args.timeout
heartbeat['key'] = args.key
heartbeat['plugin'] = args.plugin
heartbeat['offline'] = args.offline
heartbeat['hidefilenames'] = args.hidefilenames
heartbeat['proxy'] = args.proxy
heartbeat['nosslverify'] = args.nosslverify
heartbeat['api_url'] = args.api_url
return send_heartbeat(**heartbeat)
else:
log.debug('File does not exist; ignoring this heartbeat.')
return SUCCESS
def execute(argv=None): def execute(argv=None):
if argv: if argv:
sys.argv = ['wakatime'] + argv sys.argv = ['wakatime'] + argv
args, configs = parseArguments() args, configs = parse_arguments()
setup_logging(args, __version__) setup_logging(args, __version__)
try: try:
heartbeats = []
hostname = args.hostname or socket.gethostname() hb = Heartbeat(vars(args), args, configs)
if hb:
heartbeat = vars(args) heartbeats.append(hb)
retval = process_heartbeat(args, configs, hostname, heartbeat) else:
log.debug(hb.skip)
if args.extra_heartbeats: if args.extra_heartbeats:
try: try:
for heartbeat in json.loads(sys.stdin.readline()): for extra_data in json.loads(sys.stdin.readline()):
retval = process_heartbeat(args, configs, hostname, heartbeat) hb = Heartbeat(extra_data, args, configs)
except json.JSONDecodeError: if hb:
retval = MALFORMED_HEARTBEAT_ERROR heartbeats.append(hb)
else:
log.debug(hb.skip)
except json.JSONDecodeError as ex:
log.warning(u('Malformed extra heartbeats json: {msg}').format(
msg=u(ex),
))
retval = send_heartbeats(heartbeats, args, configs)
if retval == SUCCESS: if retval == SUCCESS:
retval = sync_offline_heartbeats(args, hostname) queue = Queue(args, configs)
offline_heartbeats = queue.pop_many()
if len(offline_heartbeats) > 0:
retval = send_heartbeats(offline_heartbeats, args, configs)
return retval return retval

View File

@ -14,77 +14,68 @@ import logging
import os import os
from time import sleep from time import sleep
from .compat import json
from .heartbeat import Heartbeat
try: try:
import sqlite3 import sqlite3
HAS_SQL = True HAS_SQL = True
except ImportError: # pragma: nocover except ImportError: # pragma: nocover
HAS_SQL = False HAS_SQL = False
from .compat import u
log = logging.getLogger('WakaTime') log = logging.getLogger('WakaTime')
class Queue(object): class Queue(object):
db_file = '.wakatime.db' db_file = '.wakatime.db'
table_name = 'heartbeat_1' table_name = 'heartbeat_2'
def get_db_file(self): args = None
home = '~' configs = None
if os.environ.get('WAKATIME_HOME'):
home = os.environ.get('WAKATIME_HOME') def __init__(self, args, configs):
return os.path.join(os.path.expanduser(home), '.wakatime.db') self.args = args
self.configs = configs
def connect(self): def connect(self):
conn = sqlite3.connect(self.get_db_file(), isolation_level=None) conn = sqlite3.connect(self._get_db_file(), isolation_level=None)
c = conn.cursor() c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS {0} ( c.execute('''CREATE TABLE IF NOT EXISTS {0} (
entity text, id text,
type text, heartbeat text)
time real,
project text,
branch text,
is_write integer,
stats text,
misc text,
plugin text)
'''.format(self.table_name)) '''.format(self.table_name))
return (conn, c) return (conn, c)
def push(self, data, stats, plugin, misc=None): def push(self, heartbeat):
if not HAS_SQL: # pragma: nocover if not HAS_SQL:
return return
try: try:
conn, c = self.connect() conn, c = self.connect()
heartbeat = { data = {
'entity': u(data.get('entity')), 'id': heartbeat.get_id(),
'type': u(data.get('type')), 'heartbeat': heartbeat.json(),
'time': data.get('time'),
'project': u(data.get('project')),
'branch': u(data.get('branch')),
'is_write': 1 if data.get('is_write') else 0,
'stats': u(stats),
'misc': u(misc),
'plugin': u(plugin),
} }
c.execute('INSERT INTO {0} VALUES (:entity,:type,:time,:project,:branch,:is_write,:stats,:misc,:plugin)'.format(self.table_name), heartbeat) c.execute('INSERT INTO {0} VALUES (:id,:heartbeat)'.format(self.table_name), data)
conn.commit() conn.commit()
conn.close() conn.close()
except sqlite3.Error: except sqlite3.Error:
log.traceback() log.traceback()
def pop(self): def pop(self):
if not HAS_SQL: # pragma: nocover if not HAS_SQL:
return None return None
tries = 3 tries = 3
wait = 0.1 wait = 0.1
heartbeat = None
try: try:
conn, c = self.connect() conn, c = self.connect()
except sqlite3.Error: except sqlite3.Error:
log.traceback(logging.DEBUG) log.traceback(logging.DEBUG)
return None return None
heartbeat = None
loop = True loop = True
while loop and tries > -1: while loop and tries > -1:
try: try:
@ -92,40 +83,43 @@ class Queue(object):
c.execute('SELECT * FROM {0} LIMIT 1'.format(self.table_name)) c.execute('SELECT * FROM {0} LIMIT 1'.format(self.table_name))
row = c.fetchone() row = c.fetchone()
if row is not None: if row is not None:
values = [] id = row[0]
clauses = [] heartbeat = Heartbeat(json.loads(row[1]), self.args, self.configs, _clone=True)
index = 0 c.execute('DELETE FROM {0} WHERE id=?'.format(self.table_name), [id])
for row_name in ['entity', 'type', 'time', 'project', 'branch', 'is_write']:
if row[index] is not None:
clauses.append('{0}=?'.format(row_name))
values.append(row[index])
else: # pragma: nocover
clauses.append('{0} IS NULL'.format(row_name))
index += 1
if len(values) > 0:
c.execute('DELETE FROM {0} WHERE {1}'.format(self.table_name, ' AND '.join(clauses)), values)
else: # pragma: nocover
c.execute('DELETE FROM {0} WHERE {1}'.format(self.table_name, ' AND '.join(clauses)))
conn.commit() conn.commit()
if row is not None:
heartbeat = {
'entity': row[0],
'type': row[1],
'time': row[2],
'project': row[3],
'branch': row[4],
'is_write': True if row[5] is 1 else False,
'stats': row[6],
'misc': row[7],
'plugin': row[8],
}
loop = False loop = False
except sqlite3.Error: # pragma: nocover except sqlite3.Error:
log.traceback(logging.DEBUG) log.traceback(logging.DEBUG)
sleep(wait) sleep(wait)
tries -= 1 tries -= 1
try: try:
conn.close() conn.close()
except sqlite3.Error: # pragma: nocover except sqlite3.Error:
log.traceback(logging.DEBUG) log.traceback(logging.DEBUG)
return heartbeat return heartbeat
def push_many(self, heartbeats):
for heartbeat in heartbeats:
self.push(heartbeat)
def pop_many(self, limit=None):
if limit is None:
limit = 5
heartbeats = []
count = 0
while limit == 0 or count < limit:
heartbeat = self.pop()
if not heartbeat:
break
heartbeats.append(heartbeat)
count += 1
return heartbeats
def _get_db_file(self):
home = '~'
if os.environ.get('WAKATIME_HOME'):
home = os.environ.get('WAKATIME_HOME')
return os.path.join(os.path.expanduser(home), '.wakatime.db')

View File

@ -33,7 +33,7 @@ REV_CONTROL_PLUGINS = [
] ]
def get_project_info(configs, heartbeat): def get_project_info(configs, heartbeat, data):
"""Find the current project and branch. """Find the current project and branch.
First looks for a .wakatime-project file. Second, uses the --project arg. First looks for a .wakatime-project file. Second, uses the --project arg.
@ -43,21 +43,27 @@ def get_project_info(configs, heartbeat):
Returns a project, branch tuple. Returns a project, branch tuple.
""" """
project_name, branch_name = None, None project_name, branch_name = heartbeat.project, heartbeat.branch
for plugin_cls in CONFIG_PLUGINS: if heartbeat.type != 'file':
project_name = project_name or heartbeat.args.project or heartbeat.args.alternate_project
return project_name, branch_name
plugin_name = plugin_cls.__name__.lower() if project_name is None or branch_name is None:
plugin_configs = get_configs_for_plugin(plugin_name, configs)
project = plugin_cls(heartbeat['entity'], configs=plugin_configs) for plugin_cls in CONFIG_PLUGINS:
if project.process():
project_name = project_name or project.name() plugin_name = plugin_cls.__name__.lower()
branch_name = project.branch() plugin_configs = get_configs_for_plugin(plugin_name, configs)
break
project = plugin_cls(heartbeat.entity, configs=plugin_configs)
if project.process():
project_name = project_name or project.name()
branch_name = project.branch()
break
if project_name is None: if project_name is None:
project_name = heartbeat.get('project') project_name = data.get('project') or heartbeat.args.project
if project_name is None or branch_name is None: if project_name is None or branch_name is None:
@ -66,14 +72,14 @@ def get_project_info(configs, heartbeat):
plugin_name = plugin_cls.__name__.lower() plugin_name = plugin_cls.__name__.lower()
plugin_configs = get_configs_for_plugin(plugin_name, configs) plugin_configs = get_configs_for_plugin(plugin_name, configs)
project = plugin_cls(heartbeat['entity'], configs=plugin_configs) project = plugin_cls(heartbeat.entity, configs=plugin_configs)
if project.process(): if project.process():
project_name = project_name or project.name() project_name = project_name or project.name()
branch_name = branch_name or project.branch() branch_name = branch_name or project.branch()
break break
if project_name is None: if project_name is None:
project_name = heartbeat.get('alternate_project') project_name = data.get('alternate_project') or heartbeat.args.alternate_project
return project_name, branch_name return project_name, branch_name

View File

@ -123,6 +123,6 @@ class Git(BaseProject):
subpath = line[len('gitdir: '):].strip() subpath = line[len('gitdir: '):].strip()
if os.path.isfile(os.path.join(path, subpath, 'config')) and \ if os.path.isfile(os.path.join(path, subpath, 'config')) and \
os.path.isfile(os.path.join(path, subpath, 'HEAD')): os.path.isfile(os.path.join(path, subpath, 'HEAD')):
return os.path.join(path, subpath) return os.path.realpath(os.path.join(path, subpath))
return None return None

View File

@ -12,11 +12,11 @@
""" """
import logging import logging
import os
import sys import sys
from .base import BaseProject from .base import BaseProject
from ..compat import u, open from ..compat import u, open
from ..utils import find_project_file
log = logging.getLogger('WakaTime') log = logging.getLogger('WakaTime')
@ -25,7 +25,7 @@ log = logging.getLogger('WakaTime')
class ProjectFile(BaseProject): class ProjectFile(BaseProject):
def process(self): def process(self):
self.config = self._find_config(self.path) self.config = find_project_file(self.path)
self._project_name = None self._project_name = None
self._project_branch = None self._project_branch = None
@ -33,13 +33,13 @@ class ProjectFile(BaseProject):
try: try:
with open(self.config, 'r', encoding='utf-8') as fh: with open(self.config, 'r', encoding='utf-8') as fh:
self._project_name = u(fh.readline().strip()) self._project_name = u(fh.readline().strip()) or None
self._project_branch = u(fh.readline().strip()) self._project_branch = u(fh.readline().strip()) or None
except UnicodeDecodeError: # pragma: nocover except UnicodeDecodeError: # pragma: nocover
try: try:
with open(self.config, 'r', encoding=sys.getfilesystemencoding()) as fh: with open(self.config, 'r', encoding=sys.getfilesystemencoding()) as fh:
self._project_name = u(fh.readline().strip()) self._project_name = u(fh.readline().strip()) or None
self._project_branch = u(fh.readline().strip()) self._project_branch = u(fh.readline().strip()) or None
except: except:
log.traceback(logging.WARNING) log.traceback(logging.WARNING)
except IOError: # pragma: nocover except IOError: # pragma: nocover
@ -53,14 +53,3 @@ class ProjectFile(BaseProject):
def branch(self): def branch(self):
return self._project_branch return self._project_branch
def _find_config(self, path):
path = os.path.realpath(path)
if os.path.isfile(path):
path = os.path.split(path)[0]
if os.path.isfile(os.path.join(path, '.wakatime-project')):
return os.path.join(path, '.wakatime-project')
split_path = os.path.split(path)
if split_path[1] == '':
return None
return self._find_config(split_path[0])

View File

@ -33,14 +33,8 @@ class SessionCache(object):
db_file = '.wakatime.db' db_file = '.wakatime.db'
table_name = 'session' table_name = 'session'
def get_db_file(self):
home = '~'
if os.environ.get('WAKATIME_HOME'):
home = os.environ.get('WAKATIME_HOME')
return os.path.join(os.path.expanduser(home), '.wakatime.db')
def connect(self): def connect(self):
conn = sqlite3.connect(self.get_db_file(), isolation_level=None) conn = sqlite3.connect(self._get_db_file(), isolation_level=None)
c = conn.cursor() c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS {0} ( c.execute('''CREATE TABLE IF NOT EXISTS {0} (
value BLOB) value BLOB)
@ -110,3 +104,9 @@ class SessionCache(object):
conn.close() conn.close()
except: except:
log.traceback(logging.DEBUG) log.traceback(logging.DEBUG)
def _get_db_file(self):
home = '~'
if os.environ.get('WAKATIME_HOME'):
home = os.environ.get('WAKATIME_HOME')
return os.path.join(os.path.expanduser(home), '.wakatime.db')

View File

@ -14,6 +14,7 @@ import platform
import logging import logging
import os import os
import re import re
import socket
import sys import sys
from .__about__ import __version__ from .__about__ import __version__
@ -48,7 +49,7 @@ def should_exclude(entity, include, exclude):
return False return False
def get_user_agent(plugin): def get_user_agent(plugin=None):
ver = sys.version_info ver = sys.version_info
python_version = '%d.%d.%d.%s.%d' % (ver[0], ver[1], ver[2], ver[3], ver[4]) python_version = '%d.%d.%d.%s.%d' % (ver[0], ver[1], ver[2], ver[3], ver[4])
user_agent = u('wakatime/{ver} ({platform}) Python{py_ver}').format( user_agent = u('wakatime/{ver} ({platform}) Python{py_ver}').format(
@ -77,3 +78,19 @@ def format_file_path(filepath):
except: # pragma: nocover except: # pragma: nocover
pass pass
return filepath return filepath
def get_hostname(args):
return args.hostname or socket.gethostname()
def find_project_file(path):
path = os.path.realpath(path)
if os.path.isfile(path):
path = os.path.split(path)[0]
if os.path.isfile(os.path.join(path, '.wakatime-project')):
return os.path.join(path, '.wakatime-project')
split_path = os.path.split(path)
if split_path[1] == '':
return None
return find_project_file(split_path[0])