mirror of
https://github.com/wakatime/sublime-wakatime.git
synced 2023-08-10 21:13:02 +03:00
Compare commits
51 Commits
Author | SHA1 | Date | |
---|---|---|---|
fb303e048f | |||
4f11222c2b | |||
72b72dc9f0 | |||
1b07d0442b | |||
fbd8e84ea1 | |||
01c0e7758e | |||
28556de3b6 | |||
809e43cfe5 | |||
22ddbe27b0 | |||
ddaf60b8b0 | |||
2e6a87c67e | |||
01503b1c20 | |||
5a4ac9c11d | |||
fa0a3aacb5 | |||
d588451468 | |||
483d8f596e | |||
03f2d6d580 | |||
e1390d7647 | |||
c87bdd041c | |||
3a65395636 | |||
0b2f3aa9a4 | |||
58ef2cd794 | |||
04173d3bcc | |||
3206a07476 | |||
9330236816 | |||
885c11f01a | |||
8acda0157a | |||
935ddbd5f6 | |||
b57b1eb696 | |||
6ec097b9d1 | |||
b3ed36d3b2 | |||
3669e4df6a | |||
3504096082 | |||
5990947706 | |||
2246e31244 | |||
b55fe702d3 | |||
e0fbbb50bb | |||
32c0cb5a97 | |||
67d8b0d24f | |||
b8b2f4944b | |||
a20161164c | |||
405211bb07 | |||
ffc879c4eb | |||
1e23919694 | |||
b2086a3cd2 | |||
005b07520c | |||
60608bd322 | |||
cde8f8f1de | |||
4adfca154c | |||
f7b3924a30 | |||
db00024455 |
1
AUTHORS
1
AUTHORS
@ -14,3 +14,4 @@ Patches and Suggestions
|
||||
- Jimmy Selgen Nielsen <jimmy.selgen@gmail.com>
|
||||
- Patrik Kernstock <info@pkern.at>
|
||||
- Krishna Glick <krishnaglick@gmail.com>
|
||||
- Carlos Henrique Gandarez <gandarez@gmail.com>
|
||||
|
103
HISTORY.rst
103
HISTORY.rst
@ -3,11 +3,106 @@ History
|
||||
-------
|
||||
|
||||
|
||||
10.0.1 (2020-12-28)
|
||||
++++++++++++++++++
|
||||
|
||||
- Improve readme subtitle.
|
||||
|
||||
|
||||
10.0.0 (2020-12-28)
|
||||
++++++++++++++++++
|
||||
|
||||
- Support for standalone wakatime-cli, disabled by default.
|
||||
|
||||
|
||||
9.1.2 (2020-02-13)
|
||||
++++++++++++++++++
|
||||
|
||||
- Upgrade wakatime-cli to v13.0.7.
|
||||
- Split bundled pygments library for Python 2.7+.
|
||||
- Upgrade pygments for py27+ to v2.5.2 development master.
|
||||
- Force requests to use bundled ca cert from certifi by default.
|
||||
- Upgrade bundled certifi to v2019.11.28.
|
||||
|
||||
|
||||
9.1.1 (2020-02-11)
|
||||
++++++++++++++++++
|
||||
|
||||
- Fix typo in python detection on Windows platform.
|
||||
|
||||
|
||||
9.1.0 (2020-02-09)
|
||||
++++++++++++++++++
|
||||
|
||||
- Detect python in Windows LocalAppData install locations.
|
||||
- Upgrade wakatime-cli to v13.0.4.
|
||||
- Bundle cryptography, pyopenssl, and ipaddress packages for improved SSL
|
||||
support on Python2.
|
||||
|
||||
|
||||
9.0.2 (2019-12-04)
|
||||
++++++++++++++++++
|
||||
|
||||
- Upgrade wakatime-cli to v13.0.3.
|
||||
- Support slashes in Mercurial and Git branch names.
|
||||
`wakatime#199 <https://github.com/wakatime/wakatime/issues/199>`_
|
||||
|
||||
|
||||
9.0.1 (2019-11-24)
|
||||
++++++++++++++++++
|
||||
|
||||
- Upgrade wakatime-cli to v13.0.2.
|
||||
- Filter dependencies longer than 200 characters.
|
||||
- Close sqlite connection even when error raised.
|
||||
`wakatime#196 <https://github.com/wakatime/wakatime/issues/196>`_
|
||||
- Detect ColdFusion as root language instead of HTML.
|
||||
- New arguments for reading and writing ini config file.
|
||||
- Today argument shows categories when available.
|
||||
- Prevent unnecessarily debug log when syncing offline heartbeats.
|
||||
- Support for Python 3.7.
|
||||
|
||||
|
||||
9.0.0 (2019-06-23)
|
||||
++++++++++++++++++
|
||||
|
||||
- New optional config option hide_branch_names.
|
||||
`wakatime#183 <https://github.com/wakatime/wakatime/issues/183>`_
|
||||
|
||||
|
||||
8.7.0 (2019-05-29)
|
||||
++++++++++++++++++
|
||||
|
||||
- Prevent creating user sublime-settings file when api key already exists in
|
||||
common wakatime.cfg file.
|
||||
`#98 <https://github.com/wakatime/sublime-wakatime/issues/98>`_
|
||||
|
||||
|
||||
8.6.1 (2019-05-28)
|
||||
++++++++++++++++++
|
||||
|
||||
- Fix parsing common wakatime.cfg file.
|
||||
`#98 <https://github.com/wakatime/sublime-wakatime/issues/98>`_
|
||||
|
||||
|
||||
8.6.0 (2019-05-27)
|
||||
++++++++++++++++++
|
||||
|
||||
- Prevent prompting for api key when found from config file.
|
||||
`#98 <https://github.com/wakatime/sublime-wakatime/issues/98>`_
|
||||
|
||||
|
||||
8.5.0 (2019-05-10)
|
||||
++++++++++++++++++
|
||||
|
||||
- Remove clock icon from status bar.
|
||||
- Use wakatime-cli to fetch status bar coding time.
|
||||
|
||||
|
||||
8.4.2 (2019-05-07)
|
||||
++++++++++++++++++
|
||||
|
||||
- Upgrade wakatime-cli to v11.0.0.
|
||||
- Rename largument --show-time-today to --today.
|
||||
- Rename argument --show-time-today to --today.
|
||||
- New argument --show-time-today for printing Today's coding time.
|
||||
|
||||
|
||||
@ -28,7 +123,7 @@ History
|
||||
|
||||
- Upgrade wakatime-cli to v10.8.4.
|
||||
- Use wakatime fork of certifi package.
|
||||
`sublime-wakatime#95 <https://github.com/wakatime/sublime-wakatime/issues/95>`_
|
||||
`#95 <https://github.com/wakatime/sublime-wakatime/issues/95>`_
|
||||
|
||||
|
||||
8.3.5 (2019-04-30)
|
||||
@ -47,11 +142,11 @@ History
|
||||
- Detect C++ language from all C++ file extensions.
|
||||
`vscode-wakatime#87 <https://github.com/wakatime/vscode-wakatime/issues/87>`_
|
||||
- Add ssl_certs_file arg and config for custom ca bundles.
|
||||
`#164 <https://github.com/wakatime/wakatime/issues/164>`_
|
||||
`wakatime#164 <https://github.com/wakatime/wakatime/issues/164>`_
|
||||
- Fix bug causing random project names when hide project names enabled.
|
||||
`vscode-wakatime#162 <https://github.com/wakatime/vscode-wakatime/issues/61>`_
|
||||
- Add support for UNC network shares without drive letter mapped on Winows.
|
||||
`#162 <https://github.com/wakatime/wakatime/issues/162>`_
|
||||
`wakatime#162 <https://github.com/wakatime/wakatime/issues/162>`_
|
||||
|
||||
|
||||
8.3.3 (2018-12-19)
|
||||
|
21
README.md
21
README.md
@ -1,11 +1,11 @@
|
||||
sublime-wakatime
|
||||
================
|
||||
# sublime-wakatime
|
||||
|
||||
Metrics, insights, and time tracking automatically generated from your programming activity.
|
||||
[](https://wakatime.com/badge/github/wakatime/sublime-wakatime)
|
||||
|
||||
[WakaTime][wakatime] is an open source Sublime Text plugin for metrics, insights, and time tracking automatically generated from your programming activity.
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
## Installation
|
||||
|
||||
1. Install [Package Control](https://packagecontrol.io/installation).
|
||||
|
||||
@ -20,14 +20,12 @@ Installation
|
||||
6. Use Sublime and your coding activity will be displayed on your [WakaTime dashboard](https://wakatime.com).
|
||||
|
||||
|
||||
Screen Shots
|
||||
------------
|
||||
## Screen Shots
|
||||
|
||||

|
||||
|
||||
|
||||
Unresponsive Plugin Warning
|
||||
---------------------------
|
||||
## Unresponsive Plugin Warning
|
||||
|
||||
In Sublime Text 2, if you get a warning message:
|
||||
|
||||
@ -38,8 +36,7 @@ To fix this, go to `Preferences → Settings - User` then add the following sett
|
||||
`"detect_slow_plugins": false`
|
||||
|
||||
|
||||
Troubleshooting
|
||||
---------------
|
||||
## Troubleshooting
|
||||
|
||||
First, turn on debug mode in your `WakaTime.sublime-settings` file.
|
||||
|
||||
@ -53,7 +50,7 @@ Also, tail your `$HOME/.wakatime.log` file to debug wakatime cli problems.
|
||||
The [How to Debug Plugins][how to debug] guide shows how to check when coding activity was last received from your editor using the [User Agents API][user agents api].
|
||||
For more general troubleshooting info, see the [wakatime-cli Troubleshooting Section][wakatime-cli-help].
|
||||
|
||||
|
||||
[wakatime]: https://wakatime.com/sublime-text
|
||||
[wakatime-cli-help]: https://github.com/wakatime/wakatime#troubleshooting
|
||||
[how to debug]: https://wakatime.com/faq#debug-plugins
|
||||
[user agents api]: https://wakatime.com/developers#user_agents
|
||||
|
413
WakaTime.py
413
WakaTime.py
@ -8,7 +8,7 @@ Website: https://wakatime.com/
|
||||
==========================================================="""
|
||||
|
||||
|
||||
__version__ = '8.4.2'
|
||||
__version__ = '10.0.1'
|
||||
|
||||
|
||||
import sublime
|
||||
@ -19,6 +19,8 @@ import json
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import ssl
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
@ -26,9 +28,9 @@ import threading
|
||||
import traceback
|
||||
import urllib
|
||||
import webbrowser
|
||||
from contextlib import closing
|
||||
from subprocess import STDOUT, PIPE
|
||||
from zipfile import ZipFile
|
||||
|
||||
try:
|
||||
import _winreg as winreg # py2
|
||||
except ImportError:
|
||||
@ -36,15 +38,20 @@ except ImportError:
|
||||
import winreg # py3
|
||||
except ImportError:
|
||||
winreg = None
|
||||
|
||||
try:
|
||||
import Queue as queue # py2
|
||||
except ImportError:
|
||||
import queue # py3
|
||||
|
||||
try:
|
||||
import urllib2
|
||||
import ConfigParser as configparser
|
||||
except ImportError:
|
||||
import urllib.request as urllib2
|
||||
import configparser
|
||||
try:
|
||||
from urllib2 import urlretrieve
|
||||
except ImportError:
|
||||
from urllib.request import urlretrieve
|
||||
|
||||
|
||||
is_py2 = (sys.version_info[0] == 2)
|
||||
@ -53,8 +60,6 @@ is_win = platform.system() == 'Windows'
|
||||
|
||||
|
||||
if is_py2:
|
||||
STATUS_BAR_PREFIX = 'WakaTime'
|
||||
|
||||
def u(text):
|
||||
if text is None:
|
||||
return None
|
||||
@ -78,8 +83,6 @@ if is_py2:
|
||||
return unicode('')
|
||||
|
||||
elif is_py3:
|
||||
STATUS_BAR_PREFIX = '🕘'
|
||||
|
||||
def u(text):
|
||||
if text is None:
|
||||
return None
|
||||
@ -122,7 +125,10 @@ class Popen(subprocess.Popen):
|
||||
# globals
|
||||
ST_VERSION = int(sublime.version())
|
||||
PLUGIN_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||
RESOURCES_FOLDER = os.path.join(os.getenv('APPDATA'), 'WakaTime') if is_win else os.path.join(os.path.expanduser('~'), '.wakatime')
|
||||
API_CLIENT = os.path.join(PLUGIN_DIR, 'packages', 'wakatime', 'cli.py')
|
||||
API_CLIENT_STANDALONE = os.path.join(RESOURCES_FOLDER, 'wakatime-cli', 'wakatime-cli' + ('.exe' if is_win else ''))
|
||||
S3_HOST = 'https://wakatime-cli.s3-us-west-2.amazonaws.com'
|
||||
SETTINGS_FILE = 'WakaTime.sublime-settings'
|
||||
SETTINGS = {}
|
||||
LAST_HEARTBEAT = {
|
||||
@ -138,7 +144,6 @@ PYTHON_LOCATION = None
|
||||
HEARTBEATS = queue.Queue()
|
||||
HEARTBEAT_FREQUENCY = 2 # minutes between logging heartbeat when editing same file
|
||||
SEND_BUFFER_SECONDS = 30 # seconds between sending buffered heartbeats to API
|
||||
API_SUMMARIES_URL = 'https://api.wakatime.com/api/v1/users/current/summaries?start=today&end=today&api_key={}'
|
||||
|
||||
|
||||
# Log Levels
|
||||
@ -151,9 +156,70 @@ ERROR = 'ERROR'
|
||||
# add wakatime package to path
|
||||
sys.path.insert(0, os.path.join(PLUGIN_DIR, 'packages'))
|
||||
try:
|
||||
from wakatime.main import parseConfigFile
|
||||
from wakatime.configs import parseConfigFile
|
||||
except ImportError:
|
||||
pass
|
||||
def _configFile(self):
|
||||
home = os.environ.get('WAKATIME_HOME')
|
||||
if home:
|
||||
return os.path.join(os.path.expanduser(home), '.wakatime.cfg')
|
||||
|
||||
return os.path.join(os.path.expanduser('~'), '.wakatime.cfg')
|
||||
|
||||
def parseConfigFile(self):
|
||||
"""Returns a configparser.SafeConfigParser instance with configs
|
||||
read from the config file. Default location of the config file is
|
||||
at ~/.wakatime.cfg.
|
||||
"""
|
||||
|
||||
configFile = _configFile()
|
||||
|
||||
configs = configparser.SafeConfigParser()
|
||||
try:
|
||||
with open(configFile, 'r', encoding='utf-8') as fh:
|
||||
try:
|
||||
configs.readfp(fh)
|
||||
return configs
|
||||
except configparser.Error:
|
||||
log(ERROR, traceback.format_exc())
|
||||
return None
|
||||
except IOError:
|
||||
log(DEBUG, "Error: Could not read from config file {0}\n".format(configFile))
|
||||
return None
|
||||
|
||||
|
||||
class ApiKey(object):
|
||||
_key = None
|
||||
|
||||
def read(self):
|
||||
if self._key:
|
||||
return self._key
|
||||
|
||||
key = SETTINGS.get('api_key')
|
||||
if key:
|
||||
self._key = key
|
||||
return self._key
|
||||
|
||||
try:
|
||||
configs = parseConfigFile()
|
||||
if configs:
|
||||
if configs.has_option('settings', 'api_key'):
|
||||
key = configs.get('settings', 'api_key')
|
||||
if key:
|
||||
self._key = key
|
||||
return self._key
|
||||
except:
|
||||
pass
|
||||
|
||||
return self._key
|
||||
|
||||
def write(self, key):
|
||||
global SETTINGS
|
||||
self._key = key
|
||||
SETTINGS.set('api_key', str(key))
|
||||
sublime.save_settings(SETTINGS_FILE)
|
||||
|
||||
|
||||
APIKEY = ApiKey()
|
||||
|
||||
|
||||
def set_timeout(callback, seconds):
|
||||
@ -187,13 +253,6 @@ def log(lvl, message, *args, **kwargs):
|
||||
set_timeout(lambda: log(lvl, message, *args, **kwargs), 0)
|
||||
|
||||
|
||||
def resources_folder():
|
||||
if platform.system() == 'Windows':
|
||||
return os.path.join(os.getenv('APPDATA'), 'WakaTime')
|
||||
else:
|
||||
return os.path.join(os.path.expanduser('~'), '.wakatime')
|
||||
|
||||
|
||||
def update_status_bar(status=None, debounced=False, msg=None):
|
||||
"""Updates the status bar."""
|
||||
global LAST_FETCH_TODAY_CODING_TIME, FETCH_TODAY_DEBOUNCE_COUNTER
|
||||
@ -215,7 +274,7 @@ def update_status_bar(status=None, debounced=False, msg=None):
|
||||
set_timeout(lambda: update_status_bar(status, debounced=True), FETCH_TODAY_DEBOUNCE_SECONDS)
|
||||
return
|
||||
else:
|
||||
msg = '{prefix}: {status}'.format(prefix=STATUS_BAR_PREFIX, status=status)
|
||||
msg = 'WakaTime: {status}'.format(status=status)
|
||||
|
||||
if msg:
|
||||
active_window = sublime.active_window()
|
||||
@ -232,45 +291,73 @@ class FetchStatusBarCodingTime(threading.Thread):
|
||||
def __init__(self):
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
self.api_key = SETTINGS.get('api_key', '')
|
||||
self.debug = SETTINGS.get('debug')
|
||||
self.api_key = APIKEY.read() or ''
|
||||
self.proxy = SETTINGS.get('proxy')
|
||||
self.python_binary = SETTINGS.get('python_binary')
|
||||
self.standalone = SETTINGS.get('standalone')
|
||||
|
||||
def run(self):
|
||||
if not self.api_key:
|
||||
log(DEBUG, 'Missing WakaTime API key.')
|
||||
return
|
||||
if not isCliInstalled():
|
||||
return
|
||||
|
||||
url = API_SUMMARIES_URL.format(self.api_key)
|
||||
ua = 'sublime/%d sublime-wakatime/%s' % (ST_VERSION, __version__)
|
||||
|
||||
cmd = []
|
||||
if self.standalone:
|
||||
cmd.append(API_CLIENT_STANDALONE)
|
||||
else:
|
||||
python = self.python_binary
|
||||
if not python or not python.strip():
|
||||
python = python_binary()
|
||||
if not python:
|
||||
log(DEBUG, 'Missing Python.')
|
||||
return
|
||||
cmd.extend([
|
||||
python,
|
||||
API_CLIENT,
|
||||
])
|
||||
|
||||
cmd.extend([
|
||||
'--today',
|
||||
'--key', str(bytes.decode(self.api_key.encode('utf8'))),
|
||||
'--plugin', ua,
|
||||
])
|
||||
if self.debug:
|
||||
cmd.append('--verbose')
|
||||
if self.proxy:
|
||||
cmd.extend(['--proxy', self.proxy])
|
||||
|
||||
log(DEBUG, ' '.join(obfuscate_apikey(cmd)))
|
||||
try:
|
||||
with closing(urllib2.urlopen(url)) as response:
|
||||
grand_total_text = json.loads(u(response.read()))['data'][0]['grand_total']['text']
|
||||
msg = '{} Today: {}'.format(STATUS_BAR_PREFIX, grand_total_text)
|
||||
process = Popen(cmd, stdout=PIPE, stderr=STDOUT)
|
||||
output, err = process.communicate()
|
||||
output = u(output)
|
||||
retcode = process.poll()
|
||||
if not retcode and output:
|
||||
msg = 'Today: {output}'.format(output=output)
|
||||
update_status_bar(msg=msg)
|
||||
else:
|
||||
log(DEBUG, 'wakatime-core today exited with status: {0}'.format(retcode))
|
||||
if output:
|
||||
log(DEBUG, u('wakatime-core today output: {0}').format(output))
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def prompt_api_key():
|
||||
global SETTINGS
|
||||
|
||||
if SETTINGS.get('api_key'):
|
||||
if APIKEY.read():
|
||||
return True
|
||||
|
||||
default_key = ''
|
||||
try:
|
||||
configs = parseConfigFile()
|
||||
if configs is not None:
|
||||
if configs.has_option('settings', 'api_key'):
|
||||
default_key = configs.get('settings', 'api_key')
|
||||
except:
|
||||
pass
|
||||
|
||||
window = sublime.active_window()
|
||||
if window:
|
||||
def got_key(text):
|
||||
if text:
|
||||
SETTINGS.set('api_key', str(text))
|
||||
sublime.save_settings(SETTINGS_FILE)
|
||||
window.show_input_panel('[WakaTime] Enter your wakatime.com api key:', default_key, got_key, None, None)
|
||||
APIKEY.write(text)
|
||||
window.show_input_panel('[WakaTime] Enter your wakatime.com api key:', '', got_key, None, None)
|
||||
return True
|
||||
else:
|
||||
log(ERROR, 'Could not prompt for api key because no window found.')
|
||||
@ -283,12 +370,27 @@ def python_binary():
|
||||
|
||||
# look for python in PATH and common install locations
|
||||
paths = [
|
||||
os.path.join(resources_folder(), 'python'),
|
||||
os.path.join(RESOURCES_FOLDER, 'python'),
|
||||
None,
|
||||
'/',
|
||||
'/usr/local/bin/',
|
||||
'/usr/bin/',
|
||||
]
|
||||
|
||||
if is_win and os.getenv('LOCALAPPDATA'):
|
||||
appdata = os.getenv('LOCALAPPDATA')
|
||||
ver = 39
|
||||
while ver >= 27:
|
||||
if ver >= 30 and ver <= 33:
|
||||
ver -= 1
|
||||
continue
|
||||
paths.append('\\python{ver}\\'.format(ver=ver))
|
||||
paths.append('\\Python{ver}\\'.format(ver=ver))
|
||||
paths.append('{appdata}\\Programs\\Python{ver}\\'.format(appdata=appdata, ver=ver))
|
||||
paths.append('{appdata}\\Programs\\Python{ver}-32\\'.format(appdata=appdata, ver=ver))
|
||||
paths.append('{appdata}\\Programs\\Python{ver}-64\\'.format(appdata=appdata, ver=ver))
|
||||
ver -= 1
|
||||
|
||||
for path in paths:
|
||||
path = find_python_in_folder(path)
|
||||
if path is not None:
|
||||
@ -315,7 +417,7 @@ def set_python_binary_location(path):
|
||||
|
||||
|
||||
def find_python_from_registry(location, reg=None):
|
||||
if platform.system() != 'Windows' or winreg is None:
|
||||
if not is_win or winreg is None:
|
||||
return None
|
||||
|
||||
if reg is None:
|
||||
@ -511,6 +613,9 @@ def append_heartbeat(entity, timestamp, is_write, view, project, folders):
|
||||
def process_queue(timestamp):
|
||||
global LAST_HEARTBEAT_SENT_AT
|
||||
|
||||
if not isCliInstalled():
|
||||
return
|
||||
|
||||
# Prevent sending heartbeats more often than SEND_BUFFER_SECONDS
|
||||
now = int(time.time())
|
||||
if timestamp != LAST_HEARTBEAT['time'] and LAST_HEARTBEAT_SENT_AT > now - SEND_BUFFER_SECONDS:
|
||||
@ -545,12 +650,13 @@ class SendHeartbeatsThread(threading.Thread):
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
self.debug = SETTINGS.get('debug')
|
||||
self.api_key = SETTINGS.get('api_key', '')
|
||||
self.api_key = APIKEY.read() or ''
|
||||
self.ignore = SETTINGS.get('ignore', [])
|
||||
self.include = SETTINGS.get('include', [])
|
||||
self.hidefilenames = SETTINGS.get('hidefilenames')
|
||||
self.proxy = SETTINGS.get('proxy')
|
||||
self.python_binary = SETTINGS.get('python_binary')
|
||||
self.standalone = SETTINGS.get('standalone')
|
||||
|
||||
self.heartbeat = heartbeat
|
||||
self.has_extra_heartbeats = False
|
||||
@ -587,14 +693,10 @@ class SendHeartbeatsThread(threading.Thread):
|
||||
return heartbeat
|
||||
|
||||
def send_heartbeats(self):
|
||||
python = self.python_binary
|
||||
if not python or not python.strip():
|
||||
python = python_binary()
|
||||
if python:
|
||||
if self.standalone:
|
||||
heartbeat = self.build_heartbeat(**self.heartbeat)
|
||||
ua = 'sublime/%d sublime-wakatime/%s' % (ST_VERSION, __version__)
|
||||
cmd = [
|
||||
python,
|
||||
API_CLIENT,
|
||||
'--entity', heartbeat['entity'],
|
||||
'--time', str('%f' % heartbeat['timestamp']),
|
||||
@ -631,8 +733,7 @@ class SendHeartbeatsThread(threading.Thread):
|
||||
log(DEBUG, ' '.join(obfuscate_apikey(cmd)))
|
||||
try:
|
||||
process = Popen(cmd, stdin=stdin, stdout=PIPE, stderr=STDOUT)
|
||||
output, err = process.communicate(input=inp)
|
||||
output = u(output)
|
||||
output, _err = process.communicate(input=inp)
|
||||
retcode = process.poll()
|
||||
if (not retcode or retcode == 102) and not output:
|
||||
self.sent()
|
||||
@ -645,10 +746,69 @@ class SendHeartbeatsThread(threading.Thread):
|
||||
except:
|
||||
log(ERROR, u(sys.exc_info()[1]))
|
||||
update_status_bar('Error')
|
||||
|
||||
else:
|
||||
log(ERROR, 'Unable to find python binary.')
|
||||
update_status_bar('Error')
|
||||
python = self.python_binary
|
||||
if not python or not python.strip():
|
||||
python = python_binary()
|
||||
if python:
|
||||
heartbeat = self.build_heartbeat(**self.heartbeat)
|
||||
ua = 'sublime/%d sublime-wakatime/%s' % (ST_VERSION, __version__)
|
||||
cmd = [
|
||||
python,
|
||||
API_CLIENT,
|
||||
'--entity', heartbeat['entity'],
|
||||
'--time', str('%f' % heartbeat['timestamp']),
|
||||
'--plugin', ua,
|
||||
]
|
||||
if self.api_key:
|
||||
cmd.extend(['--key', str(bytes.decode(self.api_key.encode('utf8')))])
|
||||
if heartbeat['is_write']:
|
||||
cmd.append('--write')
|
||||
if heartbeat.get('alternate_project'):
|
||||
cmd.extend(['--alternate-project', heartbeat['alternate_project']])
|
||||
if heartbeat.get('cursorpos') is not None:
|
||||
cmd.extend(['--cursorpos', heartbeat['cursorpos']])
|
||||
for pattern in self.ignore:
|
||||
cmd.extend(['--exclude', pattern])
|
||||
for pattern in self.include:
|
||||
cmd.extend(['--include', pattern])
|
||||
if self.debug:
|
||||
cmd.append('--verbose')
|
||||
if self.hidefilenames:
|
||||
cmd.append('--hidefilenames')
|
||||
if self.proxy:
|
||||
cmd.extend(['--proxy', self.proxy])
|
||||
if self.has_extra_heartbeats:
|
||||
cmd.append('--extra-heartbeats')
|
||||
stdin = PIPE
|
||||
extra_heartbeats = json.dumps([self.build_heartbeat(**x) for x in self.extra_heartbeats])
|
||||
inp = "{0}\n".format(extra_heartbeats).encode('utf-8')
|
||||
else:
|
||||
extra_heartbeats = None
|
||||
stdin = None
|
||||
inp = None
|
||||
|
||||
log(DEBUG, ' '.join(obfuscate_apikey(cmd)))
|
||||
try:
|
||||
process = Popen(cmd, stdin=stdin, stdout=PIPE, stderr=STDOUT)
|
||||
output, err = process.communicate(input=inp)
|
||||
output = u(output)
|
||||
retcode = process.poll()
|
||||
if (not retcode or retcode == 102) and not output:
|
||||
self.sent()
|
||||
else:
|
||||
update_status_bar('Error')
|
||||
if retcode:
|
||||
log(DEBUG if retcode == 102 else ERROR, 'wakatime-core exited with status: {0}'.format(retcode))
|
||||
if output:
|
||||
log(ERROR, u('wakatime-core output: {0}').format(output))
|
||||
except:
|
||||
log(ERROR, u(sys.exc_info()[1]))
|
||||
update_status_bar('Error')
|
||||
|
||||
else:
|
||||
log(ERROR, 'Unable to find python binary.')
|
||||
update_status_bar('Error')
|
||||
|
||||
def sent(self):
|
||||
update_status_bar('OK')
|
||||
@ -666,17 +826,17 @@ class DownloadPython(threading.Thread):
|
||||
def run(self):
|
||||
log(INFO, 'Downloading embeddable Python...')
|
||||
|
||||
ver = '3.5.2'
|
||||
ver = '3.8.1'
|
||||
arch = 'amd64' if platform.architecture()[0] == '64bit' else 'win32'
|
||||
url = 'https://www.python.org/ftp/python/{ver}/python-{ver}-embed-{arch}.zip'.format(
|
||||
ver=ver,
|
||||
arch=arch,
|
||||
)
|
||||
|
||||
if not os.path.exists(resources_folder()):
|
||||
os.makedirs(resources_folder())
|
||||
if not os.path.exists(RESOURCES_FOLDER):
|
||||
os.makedirs(RESOURCES_FOLDER)
|
||||
|
||||
zip_file = os.path.join(resources_folder(), 'python.zip')
|
||||
zip_file = os.path.join(RESOURCES_FOLDER, 'python.zip')
|
||||
try:
|
||||
urllib.urlretrieve(url, zip_file)
|
||||
except AttributeError:
|
||||
@ -684,7 +844,7 @@ class DownloadPython(threading.Thread):
|
||||
|
||||
log(INFO, 'Extracting Python...')
|
||||
with contextlib.closing(ZipFile(zip_file)) as zf:
|
||||
path = os.path.join(resources_folder(), 'python')
|
||||
path = os.path.join(RESOURCES_FOLDER, 'python')
|
||||
zf.extractall(path)
|
||||
|
||||
try:
|
||||
@ -702,13 +862,20 @@ def plugin_loaded():
|
||||
log(INFO, 'Initializing WakaTime plugin v%s' % __version__)
|
||||
update_status_bar('Initializing...')
|
||||
|
||||
if not python_binary():
|
||||
log(WARNING, 'Python binary not found.')
|
||||
if platform.system() == 'Windows':
|
||||
set_timeout(download_python, 0)
|
||||
else:
|
||||
sublime.error_message("Unable to find Python binary!\nWakaTime needs Python to work correctly.\n\nGo to https://www.python.org/downloads")
|
||||
return
|
||||
standalone = SETTINGS.get('standalone')
|
||||
|
||||
if standalone:
|
||||
if not isCliLatest():
|
||||
thread = DownloadCLI()
|
||||
thread.start()
|
||||
else:
|
||||
if not python_binary():
|
||||
log(WARNING, 'Python binary not found.')
|
||||
if is_win:
|
||||
set_timeout(download_python, 0)
|
||||
else:
|
||||
sublime.error_message("Unable to find Python binary!\nWakaTime needs Python to work correctly.\n\nGo to https://www.python.org/downloads")
|
||||
return
|
||||
|
||||
after_loaded()
|
||||
|
||||
@ -742,3 +909,121 @@ class WakatimeDashboardCommand(sublime_plugin.ApplicationCommand):
|
||||
|
||||
def run(self):
|
||||
webbrowser.open_new_tab('https://wakatime.com/dashboard')
|
||||
|
||||
|
||||
class DownloadCLI(threading.Thread):
|
||||
"""Non-blocking thread for downloading latest wakatime-cli from GitHub.
|
||||
"""
|
||||
|
||||
def run(self):
|
||||
log(INFO, 'Downloading wakatime-cli...')
|
||||
|
||||
if not os.path.exists(RESOURCES_FOLDER):
|
||||
os.makedirs(RESOURCES_FOLDER)
|
||||
|
||||
try:
|
||||
shutil.rmtree(os.path.join(RESOURCES_FOLDER, 'wakatime-cli'))
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
url = self._getCliUrl()
|
||||
zip_file = os.path.join(RESOURCES_FOLDER, 'wakatime-cli.zip')
|
||||
download(url, zip_file)
|
||||
|
||||
log(INFO, 'Extracting wakatime-cli...')
|
||||
with ZipFile(zip_file) as zf:
|
||||
zf.extractall(RESOURCES_FOLDER)
|
||||
|
||||
try:
|
||||
shutil.rmtree(os.path.join(RESOURCES_FOLDER, 'wakatime-cli.zip'))
|
||||
except:
|
||||
pass
|
||||
except:
|
||||
log(DEBUG, traceback.format_exc())
|
||||
|
||||
log(INFO, 'Finished extracting wakatime-cli.')
|
||||
|
||||
def _getCliUrl(self):
|
||||
os = platform.system().lower().replace('darwin', 'mac')
|
||||
arch = '64' if sys.maxsize > 2**32 else '32'
|
||||
return '{host}/{os}-x86-{arch}/wakatime-cli.zip'.format(
|
||||
host=S3_HOST,
|
||||
os=os,
|
||||
arch=arch,
|
||||
)
|
||||
|
||||
|
||||
def isCliInstalled():
|
||||
return os.path.exists(API_CLIENT)
|
||||
|
||||
|
||||
def isCliLatest():
|
||||
if not isCliInstalled():
|
||||
return False
|
||||
|
||||
args = [API_CLIENT, '--version']
|
||||
stdout, stderr = Popen(args, stdout=PIPE, stderr=PIPE).communicate()
|
||||
stdout = (stdout or b'') + (stderr or b'')
|
||||
localVer = extractVersion(stdout.decode('utf-8'))
|
||||
if not localVer:
|
||||
return False
|
||||
|
||||
log(INFO, 'Current wakatime-cli version is %s' % localVer)
|
||||
log(INFO, 'Checking for updates to wakatime-cli...')
|
||||
|
||||
remoteVer = getLatestCliVersion()
|
||||
|
||||
if not remoteVer:
|
||||
return True
|
||||
|
||||
if remoteVer == localVer:
|
||||
log(INFO, 'wakatime-cli is up to date.')
|
||||
return True
|
||||
|
||||
log(INFO, 'Found an updated wakatime-cli v%s' % remoteVer)
|
||||
return False
|
||||
|
||||
|
||||
def getLatestCliVersion():
|
||||
url = getCliVersionUrl()
|
||||
try:
|
||||
localFile = os.path.join(RESOURCES_FOLDER, 'current_version.txt')
|
||||
download(url, localFile)
|
||||
ver = None
|
||||
with open(localFile) as fh:
|
||||
ver = extractVersion(fh.read())
|
||||
try:
|
||||
shutil.rmtree(localFile)
|
||||
except:
|
||||
pass
|
||||
return ver
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def getCliVersionUrl():
|
||||
os = platform.system().lower().replace('darwin', 'mac')
|
||||
arch = '64' if sys.maxsize > 2**32 else '32'
|
||||
return '{host}/{os}-x86-{arch}/current_version.txt'.format(
|
||||
host=S3_HOST,
|
||||
os=os,
|
||||
arch=arch,
|
||||
)
|
||||
|
||||
|
||||
def extractVersion(text):
|
||||
log(DEBUG, 'extracting version.')
|
||||
pattern = re.compile(r"([0-9]+\.[0-9]+\.[0-9]+)")
|
||||
match = pattern.search(text)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return None
|
||||
|
||||
|
||||
def download(url, filePath):
|
||||
try:
|
||||
urlretrieve(url, filePath)
|
||||
except IOError:
|
||||
ssl._create_default_https_context = ssl._create_unverified_context
|
||||
urlretrieve(url, filePath)
|
||||
|
@ -3,7 +3,7 @@
|
||||
// This settings file will be overwritten when upgrading.
|
||||
|
||||
{
|
||||
// Your api key from https://wakatime.com/#apikey
|
||||
// Your api key from https://wakatime.com/api-key
|
||||
// Set this in your User specific WakaTime.sublime-settings file.
|
||||
"api_key": "",
|
||||
|
||||
@ -33,5 +33,8 @@
|
||||
"hidefilenames": false,
|
||||
|
||||
// Python binary location. Uses python from your PATH by default.
|
||||
"python_binary": ""
|
||||
"python_binary": "",
|
||||
|
||||
// Use standalone compiled Python wakatime-cli (Will not work on ARM Macs)
|
||||
"standalone": false
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
__title__ = 'wakatime'
|
||||
__description__ = 'Common interface to the WakaTime api.'
|
||||
__url__ = 'https://github.com/wakatime/wakatime'
|
||||
__version_info__ = ('11', '0', '0')
|
||||
__version_info__ = ('13', '0', '7')
|
||||
__version__ = '.'.join(__version_info__)
|
||||
__author__ = 'Alan Hamlett'
|
||||
__author_email__ = 'alan@wakatime.com'
|
||||
|
@ -19,10 +19,10 @@ 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
|
||||
from .packages import certifi
|
||||
|
||||
|
||||
log = logging.getLogger('WakaTime')
|
||||
@ -38,6 +38,9 @@ except ImportError: # pragma: nocover
|
||||
sys.exit(UNKNOWN_ERROR)
|
||||
|
||||
|
||||
from .packages.requests.exceptions import RequestException
|
||||
|
||||
|
||||
def send_heartbeats(heartbeats, args, configs, use_ntlm_proxy=False):
|
||||
"""Send heartbeats to WakaTime API.
|
||||
|
||||
@ -99,16 +102,12 @@ def send_heartbeats(heartbeats, args, configs, use_ntlm_proxy=False):
|
||||
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
|
||||
|
||||
# send request to api
|
||||
response, code = None, None
|
||||
try:
|
||||
response = session.post(api_url, data=request_body, headers=headers,
|
||||
proxies=proxies, timeout=timeout,
|
||||
verify=ssl_verify)
|
||||
verify=_get_verify(args))
|
||||
except RequestException:
|
||||
if should_try_ntlm:
|
||||
return send_heartbeats(heartbeats, args, configs, use_ntlm_proxy=True)
|
||||
@ -202,10 +201,6 @@ def get_time_today(args, use_ntlm_proxy=False):
|
||||
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': 'today',
|
||||
'end': 'today',
|
||||
@ -216,7 +211,7 @@ def get_time_today(args, use_ntlm_proxy=False):
|
||||
try:
|
||||
response = session.get(url, params=params, headers=headers,
|
||||
proxies=proxies, timeout=timeout,
|
||||
verify=ssl_verify)
|
||||
verify=_get_verify(args))
|
||||
except RequestException:
|
||||
if should_try_ntlm:
|
||||
return get_time_today(args, use_ntlm_proxy=True)
|
||||
@ -250,7 +245,11 @@ def get_time_today(args, use_ntlm_proxy=False):
|
||||
|
||||
if code == requests.codes.ok:
|
||||
try:
|
||||
text = response.json()['data'][0]['grand_total']['text']
|
||||
summary = response.json()['data'][0]
|
||||
if len(summary['categories']) > 1:
|
||||
text = ', '.join(['{0} {1}'.format(x['text'], x['name'].lower()) for x in summary['categories']])
|
||||
else:
|
||||
text = summary['grand_total']['text']
|
||||
session_cache.save(session)
|
||||
return text, SUCCESS
|
||||
except:
|
||||
@ -276,6 +275,16 @@ def get_time_today(args, use_ntlm_proxy=False):
|
||||
return None, API_ERROR
|
||||
|
||||
|
||||
def _get_verify(args):
|
||||
verify = not args.nosslverify
|
||||
if verify:
|
||||
if args.ssl_certs_file:
|
||||
verify = args.ssl_certs_file
|
||||
else:
|
||||
verify = certifi.where()
|
||||
return verify
|
||||
|
||||
|
||||
def _process_server_results(heartbeats, code, content, results, args, configs):
|
||||
log.debug({
|
||||
'response_code': code,
|
||||
|
@ -19,8 +19,8 @@ import time
|
||||
import traceback
|
||||
from .__about__ import __version__
|
||||
from .compat import basestring
|
||||
from .configs import parseConfigFile
|
||||
from .constants import AUTH_ERROR, DEFAULT_SYNC_OFFLINE_ACTIVITY
|
||||
from .configs import getConfigFile, parseConfigFile
|
||||
from .constants import AUTH_ERROR, DEFAULT_SYNC_OFFLINE_ACTIVITY, SUCCESS
|
||||
from .packages import argparse
|
||||
|
||||
|
||||
@ -62,6 +62,7 @@ def parse_arguments():
|
||||
parser.add_argument('--file', dest='file', action=FileAction,
|
||||
help=argparse.SUPPRESS)
|
||||
parser.add_argument('--key', dest='key', action=StoreWithoutQuotes,
|
||||
metavar='API_KEY',
|
||||
help='Your wakatime api key; uses api_key from ' +
|
||||
'~/.wakatime.cfg by default.')
|
||||
parser.add_argument('--write', dest='is_write', action='store_true',
|
||||
@ -75,10 +76,11 @@ def parse_arguments():
|
||||
help='Optional floating-point unix epoch timestamp. ' +
|
||||
'Uses current time by default.')
|
||||
parser.add_argument('--lineno', dest='lineno', action=StoreWithoutQuotes,
|
||||
metavar='INT',
|
||||
help='Optional line number. This is the current ' +
|
||||
'line being edited.')
|
||||
parser.add_argument('--cursorpos', dest='cursorpos',
|
||||
action=StoreWithoutQuotes,
|
||||
metavar='INT', action=StoreWithoutQuotes,
|
||||
help='Optional cursor position in the current file.')
|
||||
parser.add_argument('--entity-type', dest='entity_type',
|
||||
action=StoreWithoutQuotes,
|
||||
@ -104,13 +106,13 @@ def parse_arguments():
|
||||
'requests. By default, SSL certificates are ' +
|
||||
'verified.')
|
||||
parser.add_argument('--ssl-certs-file', dest='ssl_certs_file',
|
||||
action=StoreWithoutQuotes,
|
||||
metavar='FILE', action=StoreWithoutQuotes,
|
||||
help='Override the bundled Python Requests CA certs ' +
|
||||
'file. By default, uses certifi for ca certs.')
|
||||
parser.add_argument('--project', dest='project', action=StoreWithoutQuotes,
|
||||
help='Optional project name.')
|
||||
parser.add_argument('--alternate-project', dest='alternate_project',
|
||||
action=StoreWithoutQuotes,
|
||||
metavar='PROJECT', action=StoreWithoutQuotes,
|
||||
help='Optional alternate project name. ' +
|
||||
'Auto-discovered project takes priority.')
|
||||
parser.add_argument('--alternate-language', dest='alternate_language',
|
||||
@ -153,7 +155,12 @@ def parse_arguments():
|
||||
'folder name as the project, a ' +
|
||||
'.wakatime-project file is created with a ' +
|
||||
'random project name.')
|
||||
parser.add_argument('--hide-branch-names', dest='hide_branch_names',
|
||||
action='store_true',
|
||||
help='Obfuscate branch names. Will not send revision ' +
|
||||
'control branch names to api.')
|
||||
parser.add_argument('--exclude', dest='exclude', action='append',
|
||||
metavar='PATH',
|
||||
help='Filename patterns to exclude from logging. ' +
|
||||
'POSIX regex syntax. Can be used more than once.')
|
||||
parser.add_argument('--exclude-unknown-project',
|
||||
@ -161,6 +168,7 @@ def parse_arguments():
|
||||
help='When set, any activity where the project ' +
|
||||
'cannot be detected will be ignored.')
|
||||
parser.add_argument('--include', dest='include', action='append',
|
||||
metavar='PATH',
|
||||
help='Filename patterns to log. When used in ' +
|
||||
'combination with --exclude, files matching ' +
|
||||
'include will still be logged. POSIX regex ' +
|
||||
@ -177,35 +185,50 @@ def parse_arguments():
|
||||
help='Reads extra heartbeats from STDIN as a JSON ' +
|
||||
'array until EOF.')
|
||||
parser.add_argument('--log-file', dest='log_file',
|
||||
action=StoreWithoutQuotes,
|
||||
metavar='FILE', action=StoreWithoutQuotes,
|
||||
help='Defaults to ~/.wakatime.log.')
|
||||
parser.add_argument('--logfile', dest='logfile', action=StoreWithoutQuotes,
|
||||
help=argparse.SUPPRESS)
|
||||
parser.add_argument('--api-url', dest='api_url', action=StoreWithoutQuotes,
|
||||
metavar='URL',
|
||||
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,
|
||||
metavar='SECONDS', action=StoreWithoutQuotes,
|
||||
help='Number of seconds to wait when sending ' +
|
||||
'heartbeats to api. Defaults to 60 seconds.')
|
||||
parser.add_argument('--sync-offline-activity',
|
||||
parser.add_argument('--sync-offline-activity', metavar='AMOUNT',
|
||||
dest='sync_offline_activity',
|
||||
action=StoreWithoutQuotes,
|
||||
help='Amount of offline activity to sync from your ' +
|
||||
'local ~/.wakatime.db sqlite3 file to your ' +
|
||||
'WakaTime Dashboard before exiting. Can be ' +
|
||||
'"none" or a positive integer number. Defaults ' +
|
||||
'to 5, meaning for every heartbeat sent while ' +
|
||||
'online 5 offline heartbeats are synced. Can ' +
|
||||
'be used without --entity to only sync offline ' +
|
||||
'activity without generating new heartbeats.')
|
||||
'to 100, meaning for every heartbeat sent ' +
|
||||
'while online, 100 offline heartbeats are ' +
|
||||
'synced. Can be used without --entity to only ' +
|
||||
'sync offline activity without generating new ' +
|
||||
'heartbeats.')
|
||||
parser.add_argument('--today', dest='today',
|
||||
action='store_true',
|
||||
help='Prints dashboard time for Today, then exits.')
|
||||
parser.add_argument('--config', dest='config', action=StoreWithoutQuotes,
|
||||
help='Defaults to ~/.wakatime.cfg.')
|
||||
metavar='FILE', help='Defaults to ~/.wakatime.cfg.')
|
||||
parser.add_argument('--config-section', dest='config_section',
|
||||
metavar='SECTION', action=StoreWithoutQuotes,
|
||||
help='Optional config section when reading or ' +
|
||||
'writing a config key. Defaults to [settings].')
|
||||
parser.add_argument('--config-read', dest='config_read',
|
||||
metavar='KEY', action=StoreWithoutQuotes,
|
||||
help='Prints value for the given config key, then ' +
|
||||
'exits.')
|
||||
parser.add_argument('--config-write', dest='config_write',
|
||||
nargs=2, metavar=('KEY', 'VALUE'),
|
||||
action=StoreWithoutQuotes,
|
||||
help='Writes value to a config key, then exits. ' +
|
||||
'Expects two arguments, key and value.')
|
||||
parser.add_argument('--verbose', dest='verbose', action='store_true',
|
||||
help='Turns on debug messages in log file.')
|
||||
parser.add_argument('--version', action='version', version=__version__)
|
||||
@ -213,13 +236,30 @@ def parse_arguments():
|
||||
# parse command line arguments
|
||||
args = parser.parse_args()
|
||||
|
||||
# parse ~/.wakatime.cfg file
|
||||
configs = parseConfigFile(args.config)
|
||||
|
||||
if args.config_read:
|
||||
section = args.config_section or 'settings'
|
||||
key = args.config_read
|
||||
print(configs.get(section, key))
|
||||
raise SystemExit(SUCCESS)
|
||||
|
||||
if args.config_write:
|
||||
section = args.config_section or 'settings'
|
||||
key = args.config_write[0]
|
||||
val = args.config_write[1]
|
||||
if not configs.has_section(section):
|
||||
configs.add_section(section)
|
||||
configs.set(section, key, val)
|
||||
with open(args.config or getConfigFile(), 'w', encoding='utf-8') as fh:
|
||||
configs.write(fh)
|
||||
raise SystemExit(SUCCESS)
|
||||
|
||||
# use current unix epoch timestamp by default
|
||||
if not args.timestamp:
|
||||
args.timestamp = time.time()
|
||||
|
||||
# parse ~/.wakatime.cfg file
|
||||
configs = parseConfigFile(args.config)
|
||||
|
||||
# update args from configs
|
||||
if not args.hostname:
|
||||
if configs.has_option('settings', 'hostname'):
|
||||
@ -294,8 +334,9 @@ def parse_arguments():
|
||||
pass
|
||||
if not args.exclude_unknown_project and configs.has_option('settings', 'exclude_unknown_project'):
|
||||
args.exclude_unknown_project = configs.getboolean('settings', 'exclude_unknown_project')
|
||||
boolean_or_list('hide_file_names', args, configs, alternative_names=['hide_filenames', 'hidefilenames'])
|
||||
boolean_or_list('hide_project_names', args, configs, alternative_names=['hide_projectnames', 'hideprojectnames'])
|
||||
_boolean_or_list('hide_file_names', args, configs, alternative_names=['hide_filenames', 'hidefilenames'])
|
||||
_boolean_or_list('hide_project_names', args, configs, alternative_names=['hide_projectnames', 'hideprojectnames'])
|
||||
_boolean_or_list('hide_branch_names', args, configs, alternative_names=['hide_branchnames', 'hidebranchnames'], default=None)
|
||||
if args.offline_deprecated:
|
||||
args.offline = False
|
||||
if args.offline and configs.has_option('settings', 'offline'):
|
||||
@ -340,7 +381,7 @@ def parse_arguments():
|
||||
return args, configs
|
||||
|
||||
|
||||
def boolean_or_list(config_name, args, configs, alternative_names=[]):
|
||||
def _boolean_or_list(config_name, args, configs, alternative_names=[], default=[]):
|
||||
"""Get a boolean or list of regexes from args and configs."""
|
||||
|
||||
# when argument flag present, set to wildcard regex
|
||||
@ -349,7 +390,7 @@ def boolean_or_list(config_name, args, configs, alternative_names=[]):
|
||||
setattr(args, config_name, ['.*'])
|
||||
return
|
||||
|
||||
setattr(args, config_name, [])
|
||||
setattr(args, config_name, default)
|
||||
|
||||
option = None
|
||||
alternative_names.insert(0, config_name)
|
||||
@ -361,7 +402,11 @@ def boolean_or_list(config_name, args, configs, alternative_names=[]):
|
||||
if option is not None:
|
||||
if option.strip().lower() == 'true':
|
||||
setattr(args, config_name, ['.*'])
|
||||
elif option.strip().lower() != 'false':
|
||||
elif option.strip().lower() == 'false':
|
||||
setattr(args, config_name, [])
|
||||
else:
|
||||
for pattern in option.split("\n"):
|
||||
if pattern.strip() != '':
|
||||
if not getattr(args, config_name):
|
||||
setattr(args, config_name, [])
|
||||
getattr(args, config_name).append(pattern)
|
||||
|
@ -18,6 +18,7 @@ import sys
|
||||
|
||||
|
||||
is_py2 = (sys.version_info[0] == 2)
|
||||
is_py26 = (sys.version_info[0] == 2 and sys.version_info[1] == 6)
|
||||
is_py3 = (sys.version_info[0] == 3)
|
||||
is_win = platform.system() == 'Windows'
|
||||
|
||||
|
@ -131,5 +131,9 @@ class DependencyParser(object):
|
||||
if self.parser:
|
||||
plugin = self.parser(self.source_file, lexer=self.lexer)
|
||||
dependencies = plugin.parse()
|
||||
return list(filter(bool, set(dependencies)))
|
||||
|
||||
def filter_dependencies(dep):
|
||||
return dep and len(dep) <= 200
|
||||
|
||||
return list(filter(filter_dependencies, set(dependencies)))
|
||||
return []
|
||||
|
@ -39,6 +39,8 @@ class JavaParser(TokenParser):
|
||||
self._process_attribute(token, content)
|
||||
elif self.partial(token) == 'Operator':
|
||||
self._process_operator(token, content)
|
||||
elif self.partial(token) == 'Punctuation':
|
||||
self._process_operator(token, content)
|
||||
else:
|
||||
self._process_other(token, content)
|
||||
|
||||
|
@ -9,6 +9,58 @@
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
"""
|
||||
Token.Keyword.Namespace import None
|
||||
Token.Text import
|
||||
Token.Name.Namespace os import
|
||||
Token.Operator , import-2
|
||||
Token.Text import-2
|
||||
Token.Name.Namespace sys import-2
|
||||
Token.Text
|
||||
import-2
|
||||
Token.Keyword.Namespace import None
|
||||
Token.Text import
|
||||
Token.Name.Namespace django.forms.monstertruck import
|
||||
Token.Text
|
||||
import-2
|
||||
|
||||
|
||||
|
||||
Token.Keyword.Namespace import None
|
||||
Token.Text import
|
||||
Token.Name.Namespace os import
|
||||
Token.Operator , import-2
|
||||
Token.Text import-2
|
||||
Token.Name.Namespace sys import-2
|
||||
Token.Text
|
||||
import-2
|
||||
Token.Keyword.Namespace import None
|
||||
Token.Text import
|
||||
Token.Name.Namespace django import
|
||||
Token.Name.Namespace . import-2
|
||||
Token.Name.Namespace forms import-2
|
||||
Token.Name.Namespace . import-2
|
||||
Token.Name.Namespace monstertruck import-2
|
||||
Token.Text
|
||||
import-2
|
||||
|
||||
|
||||
|
||||
None Token.Keyword.Namespace import
|
||||
import Token.Text
|
||||
import Token.Name.Namespace django
|
||||
import-2 Token.Name.Namespace .
|
||||
import-2 Token.Name.Namespace forms
|
||||
import-2 Token.Name.Namespace .
|
||||
import-2 Token.Name.Namespace monstertruck
|
||||
import-2 Token.Text
|
||||
|
||||
None Token.Keyword.Namespace from
|
||||
from Token.Text
|
||||
from Token.Name.Namespace app
|
||||
from-2 Token.Text
|
||||
"""
|
||||
|
||||
from . import TokenParser
|
||||
|
||||
|
||||
@ -32,7 +84,7 @@ class PythonParser(TokenParser):
|
||||
if self.partial(token) == 'Namespace':
|
||||
self._process_namespace(token, content)
|
||||
elif self.partial(token) == 'Operator':
|
||||
self._process_operator(token, content)
|
||||
self._process_punctuation(token, content)
|
||||
elif self.partial(token) == 'Punctuation':
|
||||
self._process_punctuation(token, content)
|
||||
elif self.partial(token) == 'Text':
|
||||
@ -43,21 +95,20 @@ class PythonParser(TokenParser):
|
||||
def _process_namespace(self, token, content):
|
||||
if self.state is None:
|
||||
self.state = content
|
||||
elif content == 'as':
|
||||
self.nonpackage = True
|
||||
else:
|
||||
if content == 'as':
|
||||
self.nonpackage = True
|
||||
else:
|
||||
self._process_import(token, content)
|
||||
|
||||
def _process_operator(self, token, content):
|
||||
pass
|
||||
self._process_import(token, content)
|
||||
|
||||
def _process_punctuation(self, token, content):
|
||||
if content == '(':
|
||||
self.parens += 1
|
||||
self.nonpackage = False
|
||||
elif content == ')':
|
||||
self.parens -= 1
|
||||
self.nonpackage = False
|
||||
self.nonpackage = False
|
||||
elif content == ',' and self.state == 'import-2':
|
||||
self.state = 'import'
|
||||
|
||||
def _process_text(self, token, content):
|
||||
if self.state is not None:
|
||||
@ -72,12 +123,12 @@ class PythonParser(TokenParser):
|
||||
if not self.nonpackage:
|
||||
if self.state == 'from':
|
||||
self.append(content, truncate=True, truncate_to=1)
|
||||
self.state = 'from-2'
|
||||
self.state = None
|
||||
elif self.state == 'import':
|
||||
self.append(content, truncate=True, truncate_to=1)
|
||||
self.state = 'import-2'
|
||||
elif self.state == 'import-2':
|
||||
self.append(content, truncate=True, truncate_to=1)
|
||||
pass
|
||||
else:
|
||||
self.state = None
|
||||
self.nonpackage = False
|
||||
|
@ -43,7 +43,15 @@ class Heartbeat(object):
|
||||
cursorpos = None
|
||||
user_agent = None
|
||||
|
||||
_sensitive = ('dependencies', 'lines', 'lineno', 'cursorpos', 'branch')
|
||||
_sensitive_when_hiding_filename = (
|
||||
'dependencies',
|
||||
'lines',
|
||||
'lineno',
|
||||
'cursorpos',
|
||||
)
|
||||
_sensitive_when_hiding_branch = (
|
||||
'branch',
|
||||
)
|
||||
|
||||
def __init__(self, data, args, configs, _clone=None):
|
||||
if not data:
|
||||
@ -141,21 +149,24 @@ class Heartbeat(object):
|
||||
Returns a Heartbeat.
|
||||
"""
|
||||
|
||||
if not self.args.hide_file_names:
|
||||
return self
|
||||
|
||||
if self.entity is None:
|
||||
return self
|
||||
|
||||
if self.type != 'file':
|
||||
return self
|
||||
|
||||
if self.should_obfuscate_filename():
|
||||
self._sanitize_metadata()
|
||||
if self._should_obfuscate_filename():
|
||||
self._sanitize_metadata(keys=self._sensitive_when_hiding_filename)
|
||||
if self._should_obfuscate_branch(default=True):
|
||||
self._sanitize_metadata(keys=self._sensitive_when_hiding_branch)
|
||||
extension = u(os.path.splitext(self.entity)[1])
|
||||
self.entity = u('HIDDEN{0}').format(extension)
|
||||
elif self.should_obfuscate_project():
|
||||
self._sanitize_metadata()
|
||||
self._sanitize_metadata(keys=self._sensitive_when_hiding_filename)
|
||||
if self._should_obfuscate_branch(default=True):
|
||||
self._sanitize_metadata(keys=self._sensitive_when_hiding_branch)
|
||||
elif self._should_obfuscate_branch():
|
||||
self._sanitize_metadata(keys=self._sensitive_when_hiding_branch)
|
||||
|
||||
return self
|
||||
|
||||
@ -193,22 +204,6 @@ class Heartbeat(object):
|
||||
is_write=self.is_write,
|
||||
)
|
||||
|
||||
def should_obfuscate_filename(self):
|
||||
"""Returns True if hide_file_names is true or the entity file path
|
||||
matches one in the list of obfuscated file paths."""
|
||||
|
||||
for pattern in self.args.hide_file_names:
|
||||
try:
|
||||
compiled = re.compile(pattern, re.IGNORECASE)
|
||||
if compiled.search(self.entity):
|
||||
return True
|
||||
except re.error as ex:
|
||||
log.warning(u('Regex error ({msg}) for hide_file_names pattern: {pattern}').format(
|
||||
msg=u(ex),
|
||||
pattern=u(pattern),
|
||||
))
|
||||
return False
|
||||
|
||||
def should_obfuscate_project(self):
|
||||
"""Returns True if hide_project_names is true or the entity file path
|
||||
matches one in the list of obfuscated project paths."""
|
||||
@ -223,6 +218,49 @@ class Heartbeat(object):
|
||||
msg=u(ex),
|
||||
pattern=u(pattern),
|
||||
))
|
||||
|
||||
return False
|
||||
|
||||
def _should_obfuscate_filename(self):
|
||||
"""Returns True if hide_file_names is true or the entity file path
|
||||
matches one in the list of obfuscated file paths."""
|
||||
|
||||
for pattern in self.args.hide_file_names:
|
||||
try:
|
||||
compiled = re.compile(pattern, re.IGNORECASE)
|
||||
if compiled.search(self.entity):
|
||||
return True
|
||||
except re.error as ex:
|
||||
log.warning(u('Regex error ({msg}) for hide_file_names pattern: {pattern}').format(
|
||||
msg=u(ex),
|
||||
pattern=u(pattern),
|
||||
))
|
||||
|
||||
return False
|
||||
|
||||
def _should_obfuscate_branch(self, default=False):
|
||||
"""Returns True if hide_file_names is true or the entity file path
|
||||
matches one in the list of obfuscated file paths."""
|
||||
|
||||
# when project names or file names are hidden and hide_branch_names is
|
||||
# not set, we default to hiding branch names along with file/project.
|
||||
if default and self.args.hide_branch_names is None:
|
||||
return True
|
||||
|
||||
if not self.branch or not self.args.hide_branch_names:
|
||||
return False
|
||||
|
||||
for pattern in self.args.hide_branch_names:
|
||||
try:
|
||||
compiled = re.compile(pattern, re.IGNORECASE)
|
||||
if compiled.search(self.entity) or compiled.search(self.branch):
|
||||
return True
|
||||
except re.error as ex:
|
||||
log.warning(u('Regex error ({msg}) for hide_branch_names pattern: {pattern}').format(
|
||||
msg=u(ex),
|
||||
pattern=u(pattern),
|
||||
))
|
||||
|
||||
return False
|
||||
|
||||
def _unicode(self, value):
|
||||
@ -333,8 +371,8 @@ class Heartbeat(object):
|
||||
return False
|
||||
return find_project_file(self.entity) is None
|
||||
|
||||
def _sanitize_metadata(self):
|
||||
for key in self._sensitive:
|
||||
def _sanitize_metadata(self, keys=[]):
|
||||
for key in keys:
|
||||
setattr(self, key, None)
|
||||
|
||||
def __repr__(self):
|
||||
|
@ -21,6 +21,13 @@ pwd = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, os.path.dirname(pwd))
|
||||
sys.path.insert(0, os.path.join(pwd, 'packages'))
|
||||
|
||||
from .compat import is_py26
|
||||
|
||||
if is_py26:
|
||||
sys.path.insert(0, os.path.join(pwd, 'packages', 'py26'))
|
||||
else:
|
||||
sys.path.insert(0, os.path.join(pwd, 'packages', 'py27'))
|
||||
|
||||
from .__about__ import __version__
|
||||
from .api import send_heartbeats, get_time_today
|
||||
from .arguments import parse_arguments
|
||||
@ -28,8 +35,10 @@ from .compat import u, json
|
||||
from .constants import SUCCESS, UNKNOWN_ERROR, HEARTBEATS_PER_REQUEST
|
||||
from .logger import setup_logging
|
||||
|
||||
|
||||
log = logging.getLogger('WakaTime')
|
||||
|
||||
|
||||
from .heartbeat import Heartbeat
|
||||
from .offlinequeue import Queue
|
||||
|
||||
@ -38,7 +47,10 @@ def execute(argv=None):
|
||||
if argv:
|
||||
sys.argv = ['wakatime'] + argv
|
||||
|
||||
args, configs = parse_arguments()
|
||||
try:
|
||||
args, configs = parse_arguments()
|
||||
except SystemExit as ex:
|
||||
return ex.code
|
||||
|
||||
setup_logging(args, __version__)
|
||||
|
||||
@ -54,7 +66,7 @@ def execute(argv=None):
|
||||
hb = Heartbeat(vars(args), args, configs)
|
||||
if hb:
|
||||
heartbeats.append(hb)
|
||||
else:
|
||||
elif args.entity:
|
||||
log.debug(hb.skip)
|
||||
|
||||
if args.extra_heartbeats:
|
||||
|
@ -60,9 +60,12 @@ class Queue(object):
|
||||
}
|
||||
c.execute('INSERT INTO {0} VALUES (:id,:heartbeat)'.format(self.table_name), data)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
except sqlite3.Error:
|
||||
log.traceback()
|
||||
try:
|
||||
conn.close()
|
||||
except: # pragma: nocover
|
||||
pass
|
||||
|
||||
def pop(self):
|
||||
if not HAS_SQL:
|
||||
|
@ -1,3 +1,3 @@
|
||||
from .core import where
|
||||
|
||||
__version__ = "2019.03.09"
|
||||
__version__ = "2019.11.28"
|
||||
|
@ -1,2 +1,2 @@
|
||||
from . import where
|
||||
from certifi import where
|
||||
print(where())
|
||||
|
@ -771,36 +771,6 @@ vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep
|
||||
+OkuE6N36B9K
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=Class 2 Primary CA O=Certplus
|
||||
# Subject: CN=Class 2 Primary CA O=Certplus
|
||||
# Label: "Certplus Class 2 Primary CA"
|
||||
# Serial: 177770208045934040241468760488327595043
|
||||
# MD5 Fingerprint: 88:2c:8c:52:b8:a2:3c:f3:f7:bb:03:ea:ae:ac:42:0b
|
||||
# SHA1 Fingerprint: 74:20:74:41:72:9c:dd:92:ec:79:31:d8:23:10:8d:c2:81:92:e2:bb
|
||||
# SHA256 Fingerprint: 0f:99:3c:8a:ef:97:ba:af:56:87:14:0e:d5:9a:d1:82:1b:b4:af:ac:f0:aa:9a:58:b5:d5:7a:33:8a:3a:fb:cb
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDkjCCAnqgAwIBAgIRAIW9S/PY2uNp9pTXX8OlRCMwDQYJKoZIhvcNAQEFBQAw
|
||||
PTELMAkGA1UEBhMCRlIxETAPBgNVBAoTCENlcnRwbHVzMRswGQYDVQQDExJDbGFz
|
||||
cyAyIFByaW1hcnkgQ0EwHhcNOTkwNzA3MTcwNTAwWhcNMTkwNzA2MjM1OTU5WjA9
|
||||
MQswCQYDVQQGEwJGUjERMA8GA1UEChMIQ2VydHBsdXMxGzAZBgNVBAMTEkNsYXNz
|
||||
IDIgUHJpbWFyeSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANxQ
|
||||
ltAS+DXSCHh6tlJw/W/uz7kRy1134ezpfgSN1sxvc0NXYKwzCkTsA18cgCSR5aiR
|
||||
VhKC9+Ar9NuuYS6JEI1rbLqzAr3VNsVINyPi8Fo3UjMXEuLRYE2+L0ER4/YXJQyL
|
||||
kcAbmXuZVg2v7tK8R1fjeUl7NIknJITesezpWE7+Tt9avkGtrAjFGA7v0lPubNCd
|
||||
EgETjdyAYveVqUSISnFOYFWe2yMZeVYHDD9jC1yw4r5+FfyUM1hBOHTE4Y+L3yas
|
||||
H7WLO7dDWWuwJKZtkIvEcupdM5i3y95ee++U8Rs+yskhwcWYAqqi9lt3m/V+llU0
|
||||
HGdpwPFC40es/CgcZlUCAwEAAaOBjDCBiTAPBgNVHRMECDAGAQH/AgEKMAsGA1Ud
|
||||
DwQEAwIBBjAdBgNVHQ4EFgQU43Mt38sOKAze3bOkynm4jrvoMIkwEQYJYIZIAYb4
|
||||
QgEBBAQDAgEGMDcGA1UdHwQwMC4wLKAqoCiGJmh0dHA6Ly93d3cuY2VydHBsdXMu
|
||||
Y29tL0NSTC9jbGFzczIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQCnVM+IRBnL39R/
|
||||
AN9WM2K191EBkOvDP9GIROkkXe/nFL0gt5o8AP5tn9uQ3Nf0YtaLcF3n5QRIqWh8
|
||||
yfFC82x/xXp8HVGIutIKPidd3i1RTtMTZGnkLuPT55sJmabglZvOGtd/vjzOUrMR
|
||||
FcEPF80Du5wlFbqidon8BvEY0JNLDnyCt6X09l/+7UCmnYR0ObncHoUW2ikbhiMA
|
||||
ybuJfm6AiB4vFLQDJKgybwOaRywwvlbGp0ICcBvqQNi6BQNwB6SW//1IMwrh3KWB
|
||||
kJtN3X3n57LNXMhqlfil9o3EXXgIvnsG1knPGTZQIy4I5p4FTUcY1Rbpsda2ENW7
|
||||
l7+ijrRU
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=DST Root CA X3 O=Digital Signature Trust Co.
|
||||
# Subject: CN=DST Root CA X3 O=Digital Signature Trust Co.
|
||||
# Label: "DST Root CA X3"
|
||||
@ -1219,36 +1189,6 @@ t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw
|
||||
WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg==
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=Deutsche Telekom Root CA 2 O=Deutsche Telekom AG OU=T-TeleSec Trust Center
|
||||
# Subject: CN=Deutsche Telekom Root CA 2 O=Deutsche Telekom AG OU=T-TeleSec Trust Center
|
||||
# Label: "Deutsche Telekom Root CA 2"
|
||||
# Serial: 38
|
||||
# MD5 Fingerprint: 74:01:4a:91:b1:08:c4:58:ce:47:cd:f0:dd:11:53:08
|
||||
# SHA1 Fingerprint: 85:a4:08:c0:9c:19:3e:5d:51:58:7d:cd:d6:13:30:fd:8c:de:37:bf
|
||||
# SHA256 Fingerprint: b6:19:1a:50:d0:c3:97:7f:7d:a9:9b:cd:aa:c8:6a:22:7d:ae:b9:67:9e:c7:0b:a3:b0:c9:d9:22:71:c1:70:d3
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDnzCCAoegAwIBAgIBJjANBgkqhkiG9w0BAQUFADBxMQswCQYDVQQGEwJERTEc
|
||||
MBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxlU2Vj
|
||||
IFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290IENB
|
||||
IDIwHhcNOTkwNzA5MTIxMTAwWhcNMTkwNzA5MjM1OTAwWjBxMQswCQYDVQQGEwJE
|
||||
RTEcMBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxl
|
||||
U2VjIFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290
|
||||
IENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrC6M14IspFLEU
|
||||
ha88EOQ5bzVdSq7d6mGNlUn0b2SjGmBmpKlAIoTZ1KXleJMOaAGtuU1cOs7TuKhC
|
||||
QN/Po7qCWWqSG6wcmtoIKyUn+WkjR/Hg6yx6m/UTAtB+NHzCnjwAWav12gz1Mjwr
|
||||
rFDa1sPeg5TKqAyZMg4ISFZbavva4VhYAUlfckE8FQYBjl2tqriTtM2e66foai1S
|
||||
NNs671x1Udrb8zH57nGYMsRUFUQM+ZtV7a3fGAigo4aKSe5TBY8ZTNXeWHmb0moc
|
||||
QqvF1afPaA+W5OFhmHZhyJF81j4A4pFQh+GdCuatl9Idxjp9y7zaAzTVjlsB9WoH
|
||||
txa2bkp/AgMBAAGjQjBAMB0GA1UdDgQWBBQxw3kbuvVT1xfgiXotF2wKsyudMzAP
|
||||
BgNVHRMECDAGAQH/AgEFMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOC
|
||||
AQEAlGRZrTlk5ynrE/5aw4sTV8gEJPB0d8Bg42f76Ymmg7+Wgnxu1MM9756Abrsp
|
||||
tJh6sTtU6zkXR34ajgv8HzFZMQSyzhfzLMdiNlXiItiJVbSYSKpk+tYcNthEeFpa
|
||||
IzpXl/V6ME+un2pMSyuOoAPjPuCp1NJ70rOo4nI8rZ7/gFnkm0W09juwzTkZmDLl
|
||||
6iFhkOQxIY40sfcvNUqFENrnijchvllj4PKFiDFT1FQUhXB59C4Gdyd1Lx+4ivn+
|
||||
xbrYNuSD7Odlt79jWvNGr4GUN9RBjNYj1h7P9WgbRGOiWrqnNVmh5XAFmw4jV5mU
|
||||
Cm26OWMohpLzGITY+9HPBVZkVw==
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=Cybertrust Global Root O=Cybertrust, Inc
|
||||
# Subject: CN=Cybertrust Global Root O=Cybertrust, Inc
|
||||
# Label: "Cybertrust Global Root"
|
||||
@ -3453,46 +3393,6 @@ AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ
|
||||
5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=Certinomis - Root CA O=Certinomis OU=0002 433998903
|
||||
# Subject: CN=Certinomis - Root CA O=Certinomis OU=0002 433998903
|
||||
# Label: "Certinomis - Root CA"
|
||||
# Serial: 1
|
||||
# MD5 Fingerprint: 14:0a:fd:8d:a8:28:b5:38:69:db:56:7e:61:22:03:3f
|
||||
# SHA1 Fingerprint: 9d:70:bb:01:a5:a4:a0:18:11:2e:f7:1c:01:b9:32:c5:34:e7:88:a8
|
||||
# SHA256 Fingerprint: 2a:99:f5:bc:11:74:b7:3c:bb:1d:62:08:84:e0:1c:34:e5:1c:cb:39:78:da:12:5f:0e:33:26:88:83:bf:41:58
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFkjCCA3qgAwIBAgIBATANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJGUjET
|
||||
MBEGA1UEChMKQ2VydGlub21pczEXMBUGA1UECxMOMDAwMiA0MzM5OTg5MDMxHTAb
|
||||
BgNVBAMTFENlcnRpbm9taXMgLSBSb290IENBMB4XDTEzMTAyMTA5MTcxOFoXDTMz
|
||||
MTAyMTA5MTcxOFowWjELMAkGA1UEBhMCRlIxEzARBgNVBAoTCkNlcnRpbm9taXMx
|
||||
FzAVBgNVBAsTDjAwMDIgNDMzOTk4OTAzMR0wGwYDVQQDExRDZXJ0aW5vbWlzIC0g
|
||||
Um9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANTMCQosP5L2
|
||||
fxSeC5yaah1AMGT9qt8OHgZbn1CF6s2Nq0Nn3rD6foCWnoR4kkjW4znuzuRZWJfl
|
||||
LieY6pOod5tK8O90gC3rMB+12ceAnGInkYjwSond3IjmFPnVAy//ldu9n+ws+hQV
|
||||
WZUKxkd8aRi5pwP5ynapz8dvtF4F/u7BUrJ1Mofs7SlmO/NKFoL21prbcpjp3vDF
|
||||
TKWrteoB4owuZH9kb/2jJZOLyKIOSY008B/sWEUuNKqEUL3nskoTuLAPrjhdsKkb
|
||||
5nPJWqHZZkCqqU2mNAKthH6yI8H7KsZn9DS2sJVqM09xRLWtwHkziOC/7aOgFLSc
|
||||
CbAK42C++PhmiM1b8XcF4LVzbsF9Ri6OSyemzTUK/eVNfaoqoynHWmgE6OXWk6Ri
|
||||
wsXm9E/G+Z8ajYJJGYrKWUM66A0ywfRMEwNvbqY/kXPLynNvEiCL7sCCeN5LLsJJ
|
||||
wx3tFvYk9CcbXFcx3FXuqB5vbKziRcxXV4p1VxngtViZSTYxPDMBbRZKzbgqg4SG
|
||||
m/lg0h9tkQPTYKbVPZrdd5A9NaSfD171UkRpucC63M9933zZxKyGIjK8e2uR73r4
|
||||
F2iw4lNVYC2vPsKD2NkJK/DAZNuHi5HMkesE/Xa0lZrmFAYb1TQdvtj/dBxThZng
|
||||
WVJKYe2InmtJiUZ+IFrZ50rlau7SZRFDAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIB
|
||||
BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTvkUz1pcMw6C8I6tNxIqSSaHh0
|
||||
2TAfBgNVHSMEGDAWgBTvkUz1pcMw6C8I6tNxIqSSaHh02TANBgkqhkiG9w0BAQsF
|
||||
AAOCAgEAfj1U2iJdGlg+O1QnurrMyOMaauo++RLrVl89UM7g6kgmJs95Vn6RHJk/
|
||||
0KGRHCwPT5iVWVO90CLYiF2cN/z7ZMF4jIuaYAnq1fohX9B0ZedQxb8uuQsLrbWw
|
||||
F6YSjNRieOpWauwK0kDDPAUwPk2Ut59KA9N9J0u2/kTO+hkzGm2kQtHdzMjI1xZS
|
||||
g081lLMSVX3l4kLr5JyTCcBMWwerx20RoFAXlCOotQqSD7J6wWAsOMwaplv/8gzj
|
||||
qh8c3LigkyfeY+N/IZ865Z764BNqdeuWXGKRlI5nU7aJ+BIJy29SWwNyhlCVCNSN
|
||||
h4YVH5Uk2KRvms6knZtt0rJ2BobGVgjF6wnaNsIbW0G+YSrjcOa4pvi2WsS9Iff/
|
||||
ql+hbHY5ZtbqTFXhADObE5hjyW/QASAJN1LnDE8+zbz1X5YnpyACleAu6AdBBR8V
|
||||
btaw5BngDwKTACdyxYvRVB9dSsNAl35VpnzBMwQUAR1JIGkLGZOdblgi90AMRgwj
|
||||
Y/M50n92Uaf0yKHxDHYiI0ZSKS3io0EHVmmY0gUJvGnHWmHNj4FgFU2A3ZDifcRQ
|
||||
8ow7bkrHxuaAKzyBvBGAFhAn1/DNP3nMcyrDflOR1m749fPH0FFNjkulW+YZFzvW
|
||||
gQncItzujrnEj1PhZ7szuIgVRs/taTX/dQ1G885x4cVrhkIGuUE=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed
|
||||
# Subject: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed
|
||||
# Label: "OISTE WISeKey Global Root GB CA"
|
||||
@ -4656,3 +4556,47 @@ L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa
|
||||
LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG
|
||||
mpv0
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=Entrust Root Certification Authority - G4 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2015 Entrust, Inc. - for authorized use only
|
||||
# Subject: CN=Entrust Root Certification Authority - G4 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2015 Entrust, Inc. - for authorized use only
|
||||
# Label: "Entrust Root Certification Authority - G4"
|
||||
# Serial: 289383649854506086828220374796556676440
|
||||
# MD5 Fingerprint: 89:53:f1:83:23:b7:7c:8e:05:f1:8c:71:38:4e:1f:88
|
||||
# SHA1 Fingerprint: 14:88:4e:86:26:37:b0:26:af:59:62:5c:40:77:ec:35:29:ba:96:01
|
||||
# SHA256 Fingerprint: db:35:17:d1:f6:73:2a:2d:5a:b9:7c:53:3e:c7:07:79:ee:32:70:a6:2f:b4:ac:42:38:37:24:60:e6:f0:1e:88
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIGSzCCBDOgAwIBAgIRANm1Q3+vqTkPAAAAAFVlrVgwDQYJKoZIhvcNAQELBQAw
|
||||
gb4xCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQL
|
||||
Ex9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykg
|
||||
MjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAw
|
||||
BgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0
|
||||
MB4XDTE1MDUyNzExMTExNloXDTM3MTIyNzExNDExNlowgb4xCzAJBgNVBAYTAlVT
|
||||
MRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1
|
||||
c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJ
|
||||
bmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3Qg
|
||||
Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0MIICIjANBgkqhkiG9w0B
|
||||
AQEFAAOCAg8AMIICCgKCAgEAsewsQu7i0TD/pZJH4i3DumSXbcr3DbVZwbPLqGgZ
|
||||
2K+EbTBwXX7zLtJTmeH+H17ZSK9dE43b/2MzTdMAArzE+NEGCJR5WIoV3imz/f3E
|
||||
T+iq4qA7ec2/a0My3dl0ELn39GjUu9CH1apLiipvKgS1sqbHoHrmSKvS0VnM1n4j
|
||||
5pds8ELl3FFLFUHtSUrJ3hCX1nbB76W1NhSXNdh4IjVS70O92yfbYVaCNNzLiGAM
|
||||
C1rlLAHGVK/XqsEQe9IFWrhAnoanw5CGAlZSCXqc0ieCU0plUmr1POeo8pyvi73T
|
||||
DtTUXm6Hnmo9RR3RXRv06QqsYJn7ibT/mCzPfB3pAqoEmh643IhuJbNsZvc8kPNX
|
||||
wbMv9W3y+8qh+CmdRouzavbmZwe+LGcKKh9asj5XxNMhIWNlUpEbsZmOeX7m640A
|
||||
2Vqq6nPopIICR5b+W45UYaPrL0swsIsjdXJ8ITzI9vF01Bx7owVV7rtNOzK+mndm
|
||||
nqxpkCIHH2E6lr7lmk/MBTwoWdPBDFSoWWG9yHJM6Nyfh3+9nEg2XpWjDrk4JFX8
|
||||
dWbrAuMINClKxuMrLzOg2qOGpRKX/YAr2hRC45K9PvJdXmd0LhyIRyk0X+IyqJwl
|
||||
N4y6mACXi0mWHv0liqzc2thddG5msP9E36EYxr5ILzeUePiVSj9/E15dWf10hkNj
|
||||
c0kCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD
|
||||
VR0OBBYEFJ84xFYjwznooHFs6FRM5Og6sb9nMA0GCSqGSIb3DQEBCwUAA4ICAQAS
|
||||
5UKme4sPDORGpbZgQIeMJX6tuGguW8ZAdjwD+MlZ9POrYs4QjbRaZIxowLByQzTS
|
||||
Gwv2LFPSypBLhmb8qoMi9IsabyZIrHZ3CL/FmFz0Jomee8O5ZDIBf9PD3Vht7LGr
|
||||
hFV0d4QEJ1JrhkzO3bll/9bGXp+aEJlLdWr+aumXIOTkdnrG0CSqkM0gkLpHZPt/
|
||||
B7NTeLUKYvJzQ85BK4FqLoUWlFPUa19yIqtRLULVAJyZv967lDtX/Zr1hstWO1uI
|
||||
AeV8KEsD+UmDfLJ/fOPtjqF/YFOOVZ1QNBIPt5d7bIdKROf1beyAN/BYGW5KaHbw
|
||||
H5Lk6rWS02FREAutp9lfx1/cH6NcjKF+m7ee01ZvZl4HliDtC3T7Zk6LERXpgUl+
|
||||
b7DUUH8i119lAg2m9IUe2K4GS0qn0jFmwvjO5QimpAKWRGhXxNUzzxkvFMSUHHuk
|
||||
2fCfDrGA4tGeEWSpiBE6doLlYsKA2KSD7ZPvfC+QsDJMlhVoSFLUmQjAJOgc47Ol
|
||||
IQ6SwJAfzyBfyjs4x7dtOvPmRLgOMWuIjnDrnBdSqEGULoe256YSxXXfW8AKbnuk
|
||||
5F6G+TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuY
|
||||
n/PIjhs4ViFqUZPTkcpG2om3PVODLAgfi49T3f+sHw==
|
||||
-----END CERTIFICATE-----
|
||||
|
0
packages/wakatime/packages/py26/__init__.py
Normal file
0
packages/wakatime/packages/py26/__init__.py
Normal file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user