rana-cli/wakatime/heartbeat.py

226 lines
6.8 KiB
Python
Raw Normal View History

2017-11-09 06:54:33 +00:00
# -*- 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 .exceptions import SkipHeartbeat
2017-11-09 06:54:33 +00:00
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
2017-11-09 06:54:33 +00:00
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
2018-04-07 05:37:01 +00:00
category = None
2017-11-09 06:54:33 +00:00
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):
2017-11-23 17:25:30 +00:00
if not data:
self.skip = u('Skipping because heartbeat data is missing.')
return
2017-11-09 06:54:33 +00:00
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'
2018-04-07 05:37:01 +00:00
self.category = data.get('category')
allowed_categories = [
'coding',
'building',
'debugging',
'running tests',
'browsing',
'code reviewing'
]
if self.category not in allowed_categories:
self.category = None
2017-11-09 06:54:33 +00:00
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
2017-11-09 06:54:33 +00:00
project, branch = get_project_info(configs, self, data)
self.project = project
self.branch = branch
try:
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'))
except SkipHeartbeat as ex:
self.skip = u(ex) or 'Skipping'
return
2017-11-09 06:54:33 +00:00
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:
2017-11-09 06:54:33 +00:00
return self
if self.entity is None:
return self
if self.type != 'file':
return self
for pattern in self.args.hide_filenames:
2017-11-09 06:54:33 +00:00
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),
2017-11-09 06:54:33 +00:00
'type': self.type,
2018-04-07 05:37:01 +00:00
'category': self.category,
2017-11-09 06:54:33 +00:00
'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),
2017-11-09 06:54:33 +00:00
'lines': self.lines,
'lineno': self.lineno,
'cursorpos': self.cursorpos,
'user_agent': self._unicode(self.user_agent),
2017-11-09 06:54:33 +00:00
}
def items(self):
return self.dict().items()
def get_id(self):
2018-04-07 05:37:01 +00:00
return u('{time}-{type}-{category}-{project}-{branch}-{entity}-{is_write}').format(
time=self.time,
type=self.type,
2018-04-07 05:37:01 +00:00
category=self.category,
project=self._unicode(self.project),
branch=self._unicode(self.branch),
entity=self._unicode(self.entity),
is_write=self.is_write,
2017-11-09 06:54:33 +00:00
)
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]
2017-11-09 06:54:33 +00:00
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
2017-11-09 06:54:33 +00:00
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]