Compare commits

..

6 Commits

14 changed files with 133 additions and 85 deletions

View File

@ -3,6 +3,19 @@ History
-------
3.0.15 (2015-04-01)
+++++++++++++++++++
- obfuscate api key when logging to Sublime Text Console in debug mode
3.0.14 (2015-03-31)
+++++++++++++++++++
- always use external python binary because ST builtin python does not support checking SSL certs
- upgrade wakatime cli to v4.0.6
3.0.13 (2015-03-23)
+++++++++++++++++++

View File

@ -6,7 +6,9 @@ License: BSD, see LICENSE for more details.
Website: https://wakatime.com/
==========================================================="""
__version__ = '3.0.13'
__version__ = '3.0.15'
import sublime
import sublime_plugin
@ -19,13 +21,14 @@ import time
import threading
import webbrowser
from datetime import datetime
from os.path import expanduser, dirname, basename, realpath, join
from subprocess import Popen
# globals
ACTION_FREQUENCY = 2
ST_VERSION = int(sublime.version())
PLUGIN_DIR = dirname(realpath(__file__))
API_CLIENT = '%s/packages/wakatime/cli.py' % PLUGIN_DIR
PLUGIN_DIR = os.path.dirname(os.path.realpath(__file__))
API_CLIENT = os.path.join(PLUGIN_DIR, 'packages', 'wakatime', 'cli.py')
SETTINGS_FILE = 'WakaTime.sublime-settings'
SETTINGS = {}
LAST_ACTION = {
@ -33,27 +36,16 @@ LAST_ACTION = {
'file': None,
'is_write': False,
}
HAS_SSL = False
LOCK = threading.RLock()
PYTHON_LOCATION = None
# add wakatime package to path
sys.path.insert(0, join(PLUGIN_DIR, 'packages'))
# check if we have SSL support
sys.path.insert(0, os.path.join(PLUGIN_DIR, 'packages'))
try:
import ssl
import socket
assert ssl
assert socket.ssl
assert ssl.OPENSSL_VERSION
HAS_SSL = True
except (ImportError, AttributeError):
from subprocess import Popen
if HAS_SSL:
# import wakatime package so we can use built-in python
import wakatime
from wakatime.base import parseConfigFile
except ImportError:
pass
def createConfigFile():
@ -81,7 +73,6 @@ def prompt_api_key():
default_key = ''
try:
from wakatime.base import parseConfigFile
configs = parseConfigFile()
if configs is not None:
if configs.has_option('settings', 'api_key'):
@ -123,7 +114,7 @@ def python_binary():
except:
pass
for path in glob.iglob('/python*'):
path = realpath(join(path, 'pythonw'))
path = os.path.realpath(os.path.join(path, 'pythonw'))
try:
Popen([path, '--version'])
PYTHON_LOCATION = path
@ -133,6 +124,17 @@ def python_binary():
return None
def obfuscate_apikey(cmd):
apikey_index = None
for num in range(len(cmd)):
if cmd[num] == '--key':
apikey_index = num + 1
break
if apikey_index is not None and apikey_index < len(cmd):
cmd[apikey_index] = '********-****-****-****-********' + cmd[apikey_index][-4:]
return cmd
def enough_time_passed(now, last_time):
if now - last_time > ACTION_FREQUENCY * 60:
return True
@ -151,10 +153,13 @@ def find_project_name_from_folders(folders):
def handle_action(view, is_write=False):
target_file = view.file_name()
project = view.window().project_file_name() if hasattr(view.window(), 'project_file_name') else None
thread = SendActionThread(target_file, view, is_write=is_write, project=project, folders=view.window().folders())
thread.start()
window = view.window()
if window is not None:
target_file = view.file_name()
project = window.project_file_name() if hasattr(window, 'project_file_name') else None
folders = window.folders()
thread = SendActionThread(target_file, view, is_write=is_write, project=project, folders=folders)
thread.start()
class SendActionThread(threading.Thread):
@ -195,7 +200,7 @@ class SendActionThread(threading.Thread):
if self.is_write:
cmd.append('--write')
if self.project:
self.project = basename(self.project).replace('.sublime-project', '', 1)
self.project = os.path.basename(self.project).replace('.sublime-project', '', 1)
if self.project:
cmd.extend(['--project', self.project])
elif self.folders:
@ -206,28 +211,18 @@ class SendActionThread(threading.Thread):
cmd.extend(['--ignore', pattern])
if self.debug:
cmd.append('--verbose')
if HAS_SSL:
if python_binary():
cmd.insert(0, python_binary())
if self.debug:
print('[WakaTime] %s' % ' '.join(cmd))
code = wakatime.main(cmd)
if code != 0:
print('[WakaTime] Error: Response code %d from wakatime package.' % code)
print('[WakaTime] %s' % ' '.join(obfuscate_apikey(cmd)))
if platform.system() == 'Windows':
Popen(cmd, shell=False)
else:
self.sent()
with open(os.path.join(os.path.expanduser('~'), '.wakatime.log'), 'a') as stderr:
Popen(cmd, stderr=stderr)
self.sent()
else:
python = python_binary()
if python:
cmd.insert(0, python)
if self.debug:
print('[WakaTime] %s %s' % (python, ' '.join(cmd)))
if platform.system() == 'Windows':
Popen(cmd, shell=False)
else:
with open(join(expanduser('~'), '.wakatime.log'), 'a') as stderr:
Popen(cmd, stderr=stderr)
self.sent()
else:
print('[WakaTime] Error: Unable to find python binary.')
print('[WakaTime] Error: Unable to find python binary.')
def sent(self):
sublime.set_timeout(self.set_status_bar, 0)
@ -250,11 +245,9 @@ def plugin_loaded():
global SETTINGS
print('[WakaTime] Initializing WakaTime plugin v%s' % __version__)
if not HAS_SSL:
python = python_binary()
if not python:
sublime.error_message("Unable to find Python binary!\nWakaTime needs Python to work correctly.\n\nGo to https://www.python.org/downloads")
return
if not python_binary():
sublime.error_message("Unable to find Python binary!\nWakaTime needs Python to work correctly.\n\nGo to https://www.python.org/downloads")
return
SETTINGS = sublime.load_settings(SETTINGS_FILE)
after_loaded()

View File

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

View File

@ -24,8 +24,9 @@ try:
except ImportError:
import configparser
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'packages'))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'packages', 'requests', 'packages'))
from .__about__ import __version__
from .compat import u, open, is_py3

View File

@ -24,6 +24,9 @@ except ImportError:
class CustomEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, bytes):
obj = bytes.decode(obj)
return json.dumps(obj)
try:
encoded = super(CustomEncoder, self).default(obj)
except UnicodeDecodeError:

View File

@ -42,7 +42,7 @@ is at <http://python-requests.org>.
"""
__title__ = 'requests'
__version__ = '2.5.3'
__version__ = '2.6.0'
__build__ = 0x020503
__author__ = 'Kenneth Reitz'
__license__ = 'Apache 2.0'

View File

@ -11,10 +11,10 @@ and maintain connections.
import socket
from .models import Response
from .packages.urllib3 import Retry
from .packages.urllib3.poolmanager import PoolManager, proxy_from_url
from .packages.urllib3.response import HTTPResponse
from .packages.urllib3.util import Timeout as TimeoutSauce
from .packages.urllib3.util.retry import Retry
from .compat import urlparse, basestring
from .utils import (DEFAULT_CA_BUNDLE_PATH, get_encoding_from_headers,
prepend_scheme_if_needed, get_auth_from_url, urldefragauth)

View File

@ -16,7 +16,6 @@ from . import sessions
def request(method, url, **kwargs):
"""Constructs and sends a :class:`Request <Request>`.
Returns :class:`Response <Response>` object.
:param method: method for the new :class:`Request` object.
:param url: URL for the new :class:`Request` object.
@ -37,6 +36,8 @@ def request(method, url, **kwargs):
:param verify: (optional) if ``True``, the SSL cert will be verified. A CA_BUNDLE path can also be provided.
:param stream: (optional) if ``False``, the response content will be immediately downloaded.
:param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
:return: :class:`Response <Response>` object
:rtype: requests.Response
Usage::
@ -55,10 +56,12 @@ def request(method, url, **kwargs):
def get(url, **kwargs):
"""Sends a GET request. Returns :class:`Response` object.
"""Sends a GET request.
:param url: URL for the new :class:`Request` object.
:param \*\*kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response
"""
kwargs.setdefault('allow_redirects', True)
@ -66,10 +69,12 @@ def get(url, **kwargs):
def options(url, **kwargs):
"""Sends a OPTIONS request. Returns :class:`Response` object.
"""Sends a OPTIONS request.
:param url: URL for the new :class:`Request` object.
:param \*\*kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response
"""
kwargs.setdefault('allow_redirects', True)
@ -77,10 +82,12 @@ def options(url, **kwargs):
def head(url, **kwargs):
"""Sends a HEAD request. Returns :class:`Response` object.
"""Sends a HEAD request.
:param url: URL for the new :class:`Request` object.
:param \*\*kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response
"""
kwargs.setdefault('allow_redirects', False)
@ -88,44 +95,52 @@ def head(url, **kwargs):
def post(url, data=None, json=None, **kwargs):
"""Sends a POST request. Returns :class:`Response` object.
"""Sends a POST request.
:param url: URL for the new :class:`Request` object.
:param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
:param json: (optional) json data to send in the body of the :class:`Request`.
:param \*\*kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response
"""
return request('post', url, data=data, json=json, **kwargs)
def put(url, data=None, **kwargs):
"""Sends a PUT request. Returns :class:`Response` object.
"""Sends a PUT request.
:param url: URL for the new :class:`Request` object.
:param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
:param \*\*kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response
"""
return request('put', url, data=data, **kwargs)
def patch(url, data=None, **kwargs):
"""Sends a PATCH request. Returns :class:`Response` object.
"""Sends a PATCH request.
:param url: URL for the new :class:`Request` object.
:param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
:param \*\*kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response
"""
return request('patch', url, data=data, **kwargs)
def delete(url, **kwargs):
"""Sends a DELETE request. Returns :class:`Response` object.
"""Sends a DELETE request.
:param url: URL for the new :class:`Request` object.
:param \*\*kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response
"""
return request('delete', url, **kwargs)

View File

@ -143,12 +143,13 @@ class RequestEncodingMixin(object):
else:
fn = guess_filename(v) or k
fp = v
if isinstance(fp, str):
fp = StringIO(fp)
if isinstance(fp, (bytes, bytearray)):
fp = BytesIO(fp)
rf = RequestField(name=k, data=fp.read(),
if isinstance(fp, (str, bytes, bytearray)):
fdata = fp
else:
fdata = fp.read()
rf = RequestField(name=k, data=fdata,
filename=fn, headers=fh)
rf.make_multipart(content_type=ft)
new_fields.append(rf)
@ -572,7 +573,11 @@ class Response(object):
self.cookies = cookiejar_from_dict({})
#: The amount of time elapsed between sending the request
#: and the arrival of the response (as a timedelta)
#: and the arrival of the response (as a timedelta).
#: This property specifically measures the time taken between sending
#: the first byte of the request and finishing parsing the headers. It
#: is therefore unaffected by consuming the response content or the
#: value of the ``stream`` keyword argument.
self.elapsed = datetime.timedelta(0)
#: The :class:`PreparedRequest <PreparedRequest>` object to which this

View File

@ -4,7 +4,7 @@ urllib3 - Thread-safe connection pooling and re-using.
__author__ = 'Andrey Petrov (andrey.petrov@shazow.net)'
__license__ = 'MIT'
__version__ = 'dev'
__version__ = '1.10.2'
from .connectionpool import (

View File

@ -20,8 +20,6 @@ from .packages.six import iterkeys, itervalues, PY3
__all__ = ['RecentlyUsedContainer', 'HTTPHeaderDict']
MULTIPLE_HEADERS_ALLOWED = frozenset(['cookie', 'set-cookie', 'set-cookie2'])
_Null = object()
@ -143,7 +141,10 @@ class HTTPHeaderDict(dict):
def __init__(self, headers=None, **kwargs):
dict.__init__(self)
if headers is not None:
self.extend(headers)
if isinstance(headers, HTTPHeaderDict):
self._copy_from(headers)
else:
self.extend(headers)
if kwargs:
self.extend(kwargs)
@ -223,11 +224,8 @@ class HTTPHeaderDict(dict):
vals.append(val)
else:
# vals should be a tuple then, i.e. only one item so far
if key_lower in MULTIPLE_HEADERS_ALLOWED:
# Need to convert the tuple to list for further extension
_dict_setitem(self, key_lower, [vals[0], vals[1], val])
else:
_dict_setitem(self, key_lower, new_vals)
# Need to convert the tuple to list for further extension
_dict_setitem(self, key_lower, [vals[0], vals[1], val])
def extend(*args, **kwargs):
"""Generic import function for any type of header-like object.
@ -276,14 +274,17 @@ class HTTPHeaderDict(dict):
def __repr__(self):
return "%s(%s)" % (type(self).__name__, dict(self.itermerged()))
def copy(self):
clone = type(self)()
for key in self:
val = _dict_getitem(self, key)
def _copy_from(self, other):
for key in other:
val = _dict_getitem(other, key)
if isinstance(val, list):
# Don't need to convert tuples
val = list(val)
_dict_setitem(clone, key, val)
_dict_setitem(self, key, val)
def copy(self):
clone = type(self)()
clone._copy_from(self)
return clone
def iteritems(self):

View File

@ -157,3 +157,8 @@ class InsecureRequestWarning(SecurityWarning):
class SystemTimeWarning(SecurityWarning):
"Warned when system time is suspected to be wrong"
pass
class InsecurePlatformWarning(SecurityWarning):
"Warned when certain SSL configuration is not available on a platform."
pass

View File

@ -1,7 +1,7 @@
from binascii import hexlify, unhexlify
from hashlib import md5, sha1, sha256
from ..exceptions import SSLError
from ..exceptions import SSLError, InsecurePlatformWarning
SSLContext = None
@ -10,6 +10,7 @@ create_default_context = None
import errno
import ssl
import warnings
try: # Test for SSL features
from ssl import wrap_socket, CERT_NONE, PROTOCOL_SSLv23
@ -69,6 +70,14 @@ except ImportError:
self.ciphers = cipher_suite
def wrap_socket(self, socket, server_hostname=None):
warnings.warn(
'A true SSLContext object is not available. This prevents '
'urllib3 from configuring SSL appropriately and may cause '
'certain SSL connections to fail. For more information, see '
'https://urllib3.readthedocs.org/en/latest/security.html'
'#insecureplatformwarning.',
InsecurePlatformWarning
)
kwargs = {
'keyfile': self.keyfile,
'certfile': self.certfile,

View File

@ -171,7 +171,10 @@ class SessionRedirectMixin(object):
except KeyError:
pass
extract_cookies_to_jar(prepared_request._cookies, prepared_request, resp.raw)
# Extract any cookies sent on the response to the cookiejar
# in the new request. Because we've mutated our copied prepared
# request, use the old one that we haven't yet touched.
extract_cookies_to_jar(prepared_request._cookies, req, resp.raw)
prepared_request._cookies.update(self.cookies)
prepared_request.prepare_cookies(prepared_request._cookies)