Hello community,

here is the log from the commit of package python3-raven for openSUSE:Factory 
checked in at 2016-05-30 09:58:35
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python3-raven (Old)
 and      /work/SRC/openSUSE:Factory/.python3-raven.new (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python3-raven"

Changes:
--------
--- /work/SRC/openSUSE:Factory/python3-raven/python3-raven.changes      
2016-05-25 21:26:06.000000000 +0200
+++ /work/SRC/openSUSE:Factory/.python3-raven.new/python3-raven.changes 
2016-05-30 09:58:40.000000000 +0200
@@ -1,0 +2,44 @@
+Sat May 28 19:34:06 UTC 2016 - [email protected]
+
+- update to version 5.19.0:
+  * remove duration from SQL query breadcrumbs. This was not rendered
+    in the UI and will come back in future versions of Sentry with a
+    different interface.
+  * resolved a bug that caused crumbs to be recorded incorrectly.
+
+- changes from version 5.18.0:
+  * Breadcrumbs are now attempted to be deduplicated to catch some
+    common cases where log messages just spam up the breadcrumbs.
+  * Improvements to the public breadcrumbs API and stabilized some.
+  * Automatically activate the context on calls to `merge`
+
+- changes from version 5.17.0:
+  * if breadcrumbs fail to process due to an error they are now
+    skipped.
+
+- changes from version 5.16.0:
+  * exc_info is no longer included in logger based breadcrumbs.
+  * log the entire logger name as category.
+  * added a `enable_breadcrumbs` flag to the client to allow the
+    enabling or disabling of breadcrumbs quickly.
+  * corrected an issue where python interpreters with bytecode writing
+    enabled would report incorrect logging locations when breadcrumb
+    patching for logging was enabled.
+
+- changes from version 5.15.0:
+  * Improve thread binding for the context.  This makes the main
+    thread never deactivate the client automatically on clear which
+    means that more code should automatically support breadcrumbs
+    without changes.
+
+- changes from version 5.14.0:
+  * Added support for reading git sha's from packed references.
+  * Detect disabled thread support for uwsgi.
+  * Added preliminary support for breadcrumbs.
+
+- changes from version 5.13.0:
+  * Resolved an issue where Raven would fail with an exception if the
+    package name did not match the setuptools name in some isolated
+    cases.
+
+-------------------------------------------------------------------
@@ -6 +49,0 @@
-

Old:
----
  raven-5.12.0.tar.gz

New:
----
  raven-5.19.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python3-raven.spec ++++++
--- /var/tmp/diff_new_pack.HxsDAj/_old  2016-05-30 09:58:43.000000000 +0200
+++ /var/tmp/diff_new_pack.HxsDAj/_new  2016-05-30 09:58:43.000000000 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           python3-raven
-Version:        5.12.0
+Version:        5.19.0
 Release:        0
 Url:            https://pypi.python.org/pypi/raven
 Summary:        A client for Sentry

++++++ raven-5.12.0.tar.gz -> raven-5.19.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/raven-5.12.0/PKG-INFO new/raven-5.19.0/PKG-INFO
--- old/raven-5.12.0/PKG-INFO   2016-03-30 00:07:14.000000000 +0200
+++ new/raven-5.19.0/PKG-INFO   2016-05-27 18:55:30.000000000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: raven
-Version: 5.12.0
+Version: 5.19.0
 Summary: Raven is a client for Sentry (https://getsentry.com)
 Home-page: https://github.com/getsentry/raven-python
 Author: Sentry
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/raven-5.12.0/raven/__init__.py 
new/raven-5.19.0/raven/__init__.py
--- old/raven-5.12.0/raven/__init__.py  2015-07-12 10:30:42.000000000 +0200
+++ new/raven-5.19.0/raven/__init__.py  2016-04-22 23:47:13.000000000 +0200
@@ -9,10 +9,6 @@
 
 import os
 import os.path
-from raven.base import *  # NOQA
-from raven.conf import *  # NOQA
-from raven.versioning import *  # NOQA
-
 
 __all__ = ('VERSION', 'Client', 'get_version')
 
@@ -55,3 +51,9 @@
 
 __build__ = get_revision()
 __docformat__ = 'restructuredtext en'
+
+
+# Declare child imports last to prevent recursion
+from raven.base import *  # NOQA
+from raven.conf import *  # NOQA
+from raven.versioning import *  # NOQA
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/raven-5.12.0/raven/_compat.py 
new/raven-5.19.0/raven/_compat.py
--- old/raven-5.12.0/raven/_compat.py   2016-01-13 17:52:15.000000000 +0100
+++ new/raven-5.19.0/raven/_compat.py   2016-05-03 00:45:35.000000000 +0200
@@ -175,3 +175,10 @@
 def with_metaclass(meta, base=object):
     """Create a base class with a metaclass."""
     return meta("NewBase", (base,), {})
+
+
+def get_code(func):
+    rv = getattr(func, '__code__', getattr(func, 'func_code', None))
+    if rv is None:
+        raise TypeError('Could not get code from %r' % type(func).__name__)
+    return rv
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/raven-5.12.0/raven/base.py 
new/raven-5.19.0/raven/base.py
--- old/raven-5.12.0/raven/base.py      2016-03-30 00:06:55.000000000 +0200
+++ new/raven-5.19.0/raven/base.py      2016-05-19 19:41:50.000000000 +0200
@@ -25,10 +25,14 @@
 else:
     import contextlib2 as contextlib
 
+try:
+    from thread import get_ident as get_thread_ident
+except ImportError:
+    from _thread import get_ident as get_thread_ident
+
 import raven
 from raven.conf import defaults
 from raven.conf.remote import RemoteConfig
-from raven.context import Context
 from raven.exceptions import APIError, RateLimited
 from raven.utils import json, get_versions, get_auth_header, merge_dicts
 from raven._compat import text_type, iteritems
@@ -47,6 +51,11 @@
 
 PLATFORM_NAME = 'python'
 
+SDK_VALUE = {
+    'name': 'raven-python',
+    'version': raven.VERSION,
+}
+
 # singleton for the client
 Raven = None
 
@@ -128,7 +137,8 @@
     _registry = TransportRegistry(transports=default_transports)
 
     def __init__(self, dsn=None, raise_send_errors=False, transport=None,
-                 install_sys_hook=True, **options):
+                 install_sys_hook=True, install_logging_hook=True,
+                 hook_libraries=None, enable_breadcrumbs=True, **options):
         global Raven
 
         o = options
@@ -181,11 +191,22 @@
         if Raven is None:
             Raven = self
 
-        self._context = Context()
+        # We want to remember the creating thread id here because this
+        # comes in useful for the context special handling
+        self.main_thread_id = get_thread_ident()
+        self.enable_breadcrumbs = enable_breadcrumbs
+
+        from raven.context import Context
+        self._context = Context(self)
 
         if install_sys_hook:
             self.install_sys_hook()
 
+        if install_logging_hook:
+            self.install_logging_hook()
+
+        self.hook_libraries(hook_libraries)
+
     def set_dsn(self, dsn=None, transport=None):
         if not dsn and os.environ.get('SENTRY_DSN'):
             msg = "Configuring Raven from environment variable 'SENTRY_DSN'"
@@ -219,6 +240,14 @@
             __excepthook__(*exc_info)
         sys.excepthook = handle_exception
 
+    def install_logging_hook(self):
+        from raven.breadcrumbs import install_logging_hook
+        install_logging_hook()
+
+    def hook_libraries(self, libraries):
+        from raven.breadcrumbs import hook_libraries
+        hook_libraries(libraries)
+
     @classmethod
     def register_scheme(cls, scheme, transport_class):
         cls._registry.register_scheme(scheme, transport_class)
@@ -429,6 +458,17 @@
         data.setdefault('time_spent', time_spent)
         data.setdefault('event_id', event_id)
         data.setdefault('platform', PLATFORM_NAME)
+        data.setdefault('sdk', SDK_VALUE)
+
+        # insert breadcrumbs
+        if self.enable_breadcrumbs:
+            crumbs = self.context.breadcrumbs.get_buffer()
+            if crumbs:
+                # Make sure we send the crumbs here as "values" as we use the
+                # raven client internally in sentry and the alternative
+                # submission option of a list here is not supported by the
+                # internal sender.
+                data.setdefault('breadcrumbs', {'values': crumbs})
 
         return data
 
@@ -676,7 +716,7 @@
             'Content-Type': 'application/octet-stream',
         }
 
-        self.send_remote(
+        return self.send_remote(
             url=self.remote.store_endpoint,
             data=message,
             headers=headers,
@@ -791,6 +831,17 @@
             DeprecationWarning)
         return self.context(**kwargs)
 
+    def captureBreadcrumb(self, *args, **kwargs):
+        """Records a breadcrumb with the current context.  They will be
+        sent with the next event.
+        """
+        # Note: framework integration should not call this method but
+        # instead use the raven.breadcrumbs.record_breadcrumb function
+        # which will record to the correct client automatically.
+        self.context.breadcrumbs.record(*args, **kwargs)
+
+    capture_breadcrumb = captureBreadcrumb
+
 
 class DummyClient(Client):
     "Sends messages into an empty void"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/raven-5.12.0/raven/breadcrumbs.py 
new/raven-5.19.0/raven/breadcrumbs.py
--- old/raven-5.12.0/raven/breadcrumbs.py       1970-01-01 01:00:00.000000000 
+0100
+++ new/raven-5.19.0/raven/breadcrumbs.py       2016-05-27 18:26:35.000000000 
+0200
@@ -0,0 +1,349 @@
+from __future__ import absolute_import
+
+import time
+import logging
+from types import FunctionType
+
+from raven._compat import iteritems, get_code, text_type, string_types
+from raven.utils import once
+
+
+special_logger_handlers = {}
+
+
+logger = logging.getLogger('raven')
+
+
+def event_payload_considered_equal(a, b):
+    return (
+        a['type'] == b['type'] and
+        a['level'] == b['level'] and
+        a['message'] == b['message'] and
+        a['category'] == b['category'] and
+        a['data'] == b['data']
+    )
+
+
+class BreadcrumbBuffer(object):
+
+    def __init__(self, limit=100):
+        self.buffer = []
+        self.limit = limit
+
+    def record(self, timestamp=None, level=None, message=None,
+               category=None, data=None, type=None, processor=None):
+        if not (message or data or processor):
+            raise ValueError('You must pass either `message`, `data`, '
+                             'or `processor`')
+        if timestamp is None:
+            timestamp = time.time()
+        self.buffer.append(({
+            'type': type or 'default',
+            'timestamp': timestamp,
+            'level': level,
+            'message': message,
+            'category': category,
+            'data': data,
+        }, processor))
+        del self.buffer[:-self.limit]
+
+    def clear(self):
+        del self.buffer[:]
+
+    def get_buffer(self):
+        rv = []
+        for idx, (payload, processor) in enumerate(self.buffer):
+            if processor is not None:
+                try:
+                    processor(payload)
+                except Exception:
+                    logger.exception('Failed to process breadcrumbs. Ignored')
+                    payload = None
+                self.buffer[idx] = (payload, None)
+            if payload is not None and \
+               (not rv or not event_payload_considered_equal(rv[-1], payload)):
+                rv.append(payload)
+        return rv
+
+
+class BlackholeBreadcrumbBuffer(BreadcrumbBuffer):
+    def record(self, *args, **kwargs):
+        pass
+
+
+def make_buffer(enabled=True):
+    if enabled:
+        return BreadcrumbBuffer()
+    return BlackholeBreadcrumbBuffer()
+
+
+def record_breadcrumb(type, *args, **kwargs):
+    # Legacy alias
+    kwargs['type'] = type
+    return record(*args, **kwargs)
+
+
+def record(message=None, timestamp=None, level=None, category=None,
+           data=None, type=None, processor=None):
+    """Records a breadcrumb for all active clients.  This is what integration
+    code should use rather than invoking the `captureBreadcrumb` method
+    on a specific client.
+    """
+    if timestamp is None:
+        timestamp = time.time()
+    for ctx in raven.context.get_active_contexts():
+        ctx.breadcrumbs.record(timestamp, level, message, category,
+                               data, type, processor)
+
+
+def _record_log_breadcrumb(logger, level, msg, *args, **kwargs):
+    handler = special_logger_handlers.get(logger.name)
+    if handler is not None:
+        rv = handler(logger, level, msg, args, kwargs)
+        if rv:
+            return
+
+    def processor(data):
+        formatted_msg = msg
+
+        # If people log bad things, this can happen.  Then just don't do
+        # anything.
+        try:
+            formatted_msg = text_type(msg)
+            if args:
+                formatted_msg = msg % args
+        except Exception:
+            pass
+
+        # We do not want to include exc_info as argument because it often
+        # lies (set to a constant value like 1 or True) or even if it's a
+        # tuple it will not be particularly useful for us as we cannot
+        # process it anyways.
+        kwargs.pop('exc_info', None)
+        data.update({
+            'message': formatted_msg,
+            'category': logger.name,
+            'level': logging.getLevelName(level).lower(),
+            'data': kwargs,
+        })
+    record(processor=processor)
+
+
+def _wrap_logging_method(meth, level=None):
+    if not isinstance(meth, FunctionType):
+        func = meth.im_func
+    else:
+        func = meth
+
+    # We were patched for raven before
+    if getattr(func, '__patched_for_raven__', False):
+        return
+
+    if level is None:
+        args = ('level', 'msg')
+        fwd = 'level, msg'
+    else:
+        args = ('msg',)
+        fwd = '%d, msg' % level
+
+    code = get_code(func)
+
+    # This requires a bit of explanation why we're doing this.  Due to how
+    # logging itself works we need to pretend that the method actually was
+    # created within the logging module.  There are a few ways to detect
+    # this and we fake all of them: we use the same function globals (the
+    # one from the logging module), we create it entirely there which
+    # means that also the filename is set correctly.  This fools the
+    # detection code in logging and it makes logging itself skip past our
+    # code when determining the code location.
+    #
+    # Because we point the globals to the logging module we now need to
+    # refer to our own functions (original and the crumb recording
+    # function) through a closure instead of the global scope.
+    #
+    # We also add a lot of newlines in front of the code so that the
+    # code location lines up again in case someone runs inspect.getsource
+    # on the function.
+    ns = {}
+    eval(compile('''%(offset)sif 1:
+    def factory(original, record_crumb):
+        def %(name)s(self, %(args)s, *args, **kwargs):
+            record_crumb(self, %(fwd)s, *args, **kwargs)
+            return original(self, %(args)s, *args, **kwargs)
+        return %(name)s
+    \n''' % {
+        'offset': '\n' * (code.co_firstlineno - 3),
+        'name': func.__name__,
+        'args': ', '.join(args),
+        'fwd': fwd,
+        'level': level,
+    }, logging._srcfile, 'exec'), logging.__dict__, ns)
+
+    new_func = ns['factory'](meth, _record_log_breadcrumb)
+    new_func.__doc__ = func.__doc__
+
+    assert code.co_firstlineno == get_code(func).co_firstlineno
+    assert new_func.__module__ == func.__module__
+    assert new_func.__name__ == func.__name__
+    new_func.__patched_for_raven__ = True
+
+    return new_func
+
+
+def _patch_logger():
+    cls = logging.Logger
+
+    methods = {
+        'debug': logging.DEBUG,
+        'info': logging.INFO,
+        'warning': logging.WARNING,
+        'warn': logging.WARN,
+        'error': logging.ERROR,
+        'exception': logging.ERROR,
+        'critical': logging.CRITICAL,
+        'fatal': logging.FATAL
+    }
+
+    for method_name, level in iteritems(methods):
+        new_func = _wrap_logging_method(
+            getattr(cls, method_name), level)
+        setattr(logging.Logger, method_name, new_func)
+
+    logging.Logger.log = _wrap_logging_method(
+        logging.Logger.log)
+
+
+@once
+def install_logging_hook():
+    """Installs the logging hook if it was not installed yet.  Otherwise
+    does nothing.
+    """
+    _patch_logger()
+
+
+def ignore_logger(name_or_logger, allow_level=None):
+    """Ignores a logger for the regular breadcrumb code.  This is useful
+    for framework integration code where some log messages should be
+    specially handled.
+    """
+    def handler(logger, level, msg, args, kwargs):
+        if allow_level is not None and \
+           level >= allow_level:
+            return False
+        return True
+    register_special_log_handler(name_or_logger, handler)
+
+
+def register_special_log_handler(name_or_logger, callback):
+    """Registers a callback for log handling.  The callback is invoked
+    with give arguments: `logger`, `level`, `msg`, `args` and `kwargs`
+    which are the values passed to the logging system.  If the callback
+    returns `True` the default handling is disabled.
+    """
+    if isinstance(name_or_logger, string_types):
+        name = name_or_logger
+    else:
+        name = name_or_logger.name
+    special_logger_handlers[name] = callback
+
+
+hooked_libraries = {}
+
+
+def libraryhook(name):
+    def decorator(f):
+        f = once(f)
+        hooked_libraries[name] = f
+        return f
+    return decorator
+
+
+@libraryhook('requests')
+def _hook_requests():
+    try:
+        from requests.sessions import Session
+    except ImportError:
+        return
+
+    real_send = Session.send
+
+    def send(self, request, *args, **kwargs):
+        def _record_request(response):
+            record(type='http', category='requests', data={
+                'url': request.url,
+                'method': request.method,
+                'status_code': response and response.status_code or None,
+                'reason': response and response.reason or None,
+            })
+        try:
+            resp = real_send(self, request, *args, **kwargs)
+        except Exception:
+            _record_request(None)
+            raise
+        else:
+            _record_request(resp)
+        return resp
+
+    Session.send = send
+
+    ignore_logger('requests.packages.urllib3.connectionpool',
+                  allow_level=logging.WARNING)
+
+
+@libraryhook('httplib')
+def _install_httplib():
+    try:
+        from httplib import HTTPConnection
+    except ImportError:
+        from http.client import HTTPConnection
+
+    real_putrequest = HTTPConnection.putrequest
+    real_getresponse = HTTPConnection.getresponse
+
+    def putrequest(self, method, url, *args, **kwargs):
+        self._raven_status_dict = status = {}
+        host = self.host
+        port = self.port
+        default_port = self.default_port
+
+        def processor(data):
+            real_url = url
+            if not real_url.startswith(('http://', 'https://')):
+                real_url = '%s://%s%s%s' % (
+                    default_port == 443 and 'https' or 'http',
+                    host,
+                    port != default_port and ':%s' % port or '',
+                    url,
+                )
+            data['data'] = {
+                'url': real_url,
+                'method': method,
+            }
+            data['data'].update(status)
+            return data
+        record(type='http', category='requests', processor=processor)
+        return real_putrequest(self, method, url, *args, **kwargs)
+
+    def getresponse(self, *args, **kwargs):
+        rv = real_getresponse(self, *args, **kwargs)
+        status = getattr(self, '_raven_status_dict', None)
+        if status is not None and 'status_code' not in status:
+            status['status_code'] = rv.status
+            status['reason'] = rv.reason
+        return rv
+
+    HTTPConnection.putrequest = putrequest
+    HTTPConnection.getresponse = getresponse
+
+
+def hook_libraries(libraries):
+    if libraries is None:
+        libraries = hooked_libraries.keys()
+    for lib in libraries:
+        func = hooked_libraries.get(lib)
+        if func is None:
+            raise RuntimeError('Unknown library %r for hooking' % lib)
+        func()
+
+
+import raven.context
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/raven-5.12.0/raven/context.py 
new/raven-5.19.0/raven/context.py
--- old/raven-5.12.0/raven/context.py   2016-03-30 00:06:55.000000000 +0200
+++ new/raven-5.19.0/raven/context.py   2016-05-19 19:41:50.000000000 +0200
@@ -7,36 +7,27 @@
 """
 from __future__ import absolute_import
 
-import time
-
 from collections import Mapping, Iterable
-from datetime import datetime
 from threading import local
+from weakref import ref as weakref
 
 from raven._compat import iteritems
 
+try:
+    from thread import get_ident as get_thread_ident
+except ImportError:
+    from _thread import get_ident as get_thread_ident
+
 
-class BreadcrumbBuffer(object):
+_active_contexts = local()
 
-    def __init__(self, limit=100):
-        self.buffer = []
-        self.limit = limit
-
-    def record(self, type, data=None, timestamp=None):
-        if timestamp is None:
-            timestamp = time.time()
-        elif isinstance(timestamp, datetime):
-            timestamp = datetime
-
-        self.buffer.append({
-            'type': type,
-            'timestamp': timestamp,
-            'data': data or {},
-        })
-        del self.buffer[:-self.limit]
 
-    def clear(self):
-        del self.buffer[:]
+def get_active_contexts():
+    """Returns all the active contexts for the current thread."""
+    try:
+        return list(_active_contexts.contexts)
+    except AttributeError:
+        return []
 
 
 class Context(local, Mapping, Iterable):
@@ -51,9 +42,36 @@
     >>>     finally:
     >>>         context.clear()
     """
-    def __init__(self):
+
+    def __init__(self, client=None):
+        breadcrumbs = raven.breadcrumbs.make_buffer(
+            client is None or client.enable_breadcrumbs)
+        if client is not None:
+            client = weakref(client)
+        self._client = client
+        # Because the thread auto activates the thread local this also
+        # means that we auto activate this thing.  Only if someone decides
+        # to deactivate manually later another call to activate is
+        # technically necessary.
+        self.activate()
         self.data = {}
         self.exceptions_to_skip = set()
+        self.breadcrumbs = breadcrumbs
+
+    @property
+    def client(self):
+        if self._client is None:
+            return None
+        return self._client()
+
+    def __hash__(self):
+        return id(self)
+
+    def __eq__(self, other):
+        return self is other
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
 
     def __getitem__(self, key):
         return self.data[key]
@@ -67,7 +85,27 @@
     def __repr__(self):
         return '<%s: %s>' % (type(self).__name__, self.data)
 
-    def merge(self, data):
+    def __enter__(self):
+        self.activate()
+        return self
+
+    def __exit__(self, exc_type, exc_value, tb):
+        self.deactivate()
+
+    def activate(self, sticky=False):
+        if sticky:
+            self._sticky_thread = get_thread_ident()
+        _active_contexts.__dict__.setdefault('contexts', set()).add(self)
+
+    def deactivate(self):
+        try:
+            _active_contexts.contexts.discard(self)
+        except AttributeError:
+            pass
+
+    def merge(self, data, activate=True):
+        if activate:
+            self.activate()
         d = self.data
         for key, value in iteritems(data):
             if key in ('tags', 'extra'):
@@ -83,6 +121,21 @@
     def get(self):
         return self.data
 
-    def clear(self):
+    def clear(self, deactivate=None):
         self.data = {}
         self.exceptions_to_skip.clear()
+        self.breadcrumbs.clear()
+
+        # If the caller did not specify if it wants to deactivate the
+        # context for the thread we only deactivate it if we're not the
+        # thread that created the context (main thread).
+        if deactivate is None:
+            client = self.client
+            if client is not None:
+                deactivate = get_thread_ident() != client.main_thread_id
+
+        if deactivate:
+            self.deactivate()
+
+
+import raven.breadcrumbs
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/raven-5.12.0/raven/contrib/celery/__init__.py 
new/raven-5.19.0/raven/contrib/celery/__init__.py
--- old/raven-5.12.0/raven/contrib/celery/__init__.py   2016-01-27 
20:19:13.000000000 +0100
+++ new/raven-5.19.0/raven/contrib/celery/__init__.py   2016-05-27 
18:24:21.000000000 +0200
@@ -9,6 +9,7 @@
 
 import logging
 
+from celery.exceptions import SoftTimeLimitExceeded
 from celery.signals import after_setup_logger, task_failure
 from raven.handlers.logging import SentryHandler
 
@@ -24,15 +25,21 @@
 
 
 def register_signal(client):
-    def process_failure_signal(sender, task_id, args, kwargs, **kw):
+    def process_failure_signal(sender, task_id, args, kwargs, einfo, **kw):
         # This signal is fired inside the stack so let raven do its magic
+        if isinstance(einfo.exception, SoftTimeLimitExceeded):
+            fingerprint = ['celery', 'SoftTimeLimitExceeded', sender]
+        else:
+            fingerprint = None
         client.captureException(
             extra={
                 'task_id': task_id,
                 'task': sender,
                 'args': args,
                 'kwargs': kwargs,
-            })
+            },
+            fingerprint=fingerprint,
+        )
 
     task_failure.connect(process_failure_signal, weak=False)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/raven-5.12.0/raven/contrib/django/client.py 
new/raven-5.19.0/raven/contrib/django/client.py
--- old/raven-5.12.0/raven/contrib/django/client.py     2016-01-21 
20:02:59.000000000 +0100
+++ new/raven-5.19.0/raven/contrib/django/client.py     2016-05-27 
18:24:21.000000000 +0200
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
 """
 raven.contrib.django.client
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -8,6 +9,7 @@
 
 from __future__ import absolute_import
 
+import time
 import logging
 
 from django.conf import settings
@@ -26,13 +28,114 @@
 from raven.contrib.django.utils import get_data_from_template, get_host
 from raven.contrib.django.middleware import SentryLogMiddleware
 from raven.utils.wsgi import get_headers, get_environ
+from raven.utils import once
+from raven import breadcrumbs
+from raven._compat import string_types, binary_type
 
 __all__ = ('DjangoClient',)
 
 
+class _FormatConverter(object):
+
+    def __init__(self, param_mapping):
+        self.param_mapping = param_mapping
+        self.params = []
+
+    def __getitem__(self, val):
+        self.params.append(self.param_mapping.get(val))
+        return '%s'
+
+
+def format_sql(sql, params):
+    rv = []
+
+    if isinstance(params, dict):
+        conv = _FormatConverter(params)
+        sql = sql % conv
+        params = conv.params
+
+    for param in params or ():
+        if param is None:
+            rv.append('NULL')
+        elif isinstance(param, string_types):
+            if isinstance(param, binary_type):
+                param = param.decode('utf-8', 'replace')
+            if len(param) > 256:
+                param = param[:256] + u'…'
+            rv.append("'%s'" % param.replace("'", "''"))
+        else:
+            rv.append(repr(param))
+
+    return sql, rv
+
+
+@once
+def install_sql_hook():
+    """If installed this causes Django's queries to be captured."""
+    try:
+        from django.db.backends.utils import CursorWrapper
+    except ImportError:
+        from django.db.backends.util import CursorWrapper
+
+    try:
+        real_execute = CursorWrapper.execute
+        real_executemany = CursorWrapper.executemany
+    except AttributeError:
+        # XXX(mitsuhiko): On some very old django versions (<1.6) this
+        # trickery would have to look different but I can't be bothered.
+        return
+
+    def record_sql(vendor, alias, start, duration, sql, params):
+        def processor(data):
+            real_sql, real_params = format_sql(sql, params)
+            if real_params:
+                real_sql = real_sql % tuple(real_params)
+            # maybe category to 'django.%s.%s' % (vendor, alias or
+            #   'default') ?
+            data.update({
+                'message': real_sql,
+                'category': 'query',
+            })
+        breadcrumbs.record(processor=processor)
+
+    def record_many_sql(vendor, alias, start, sql, param_list):
+        duration = time.time() - start
+        for params in param_list:
+            record_sql(vendor, alias, start, duration, sql, params)
+
+    def execute(self, sql, params=None):
+        start = time.time()
+        try:
+            return real_execute(self, sql, params)
+        finally:
+            record_sql(self.db.vendor, getattr(self.db, 'alias', None),
+                       start, time.time() - start, sql, params)
+
+    def executemany(self, sql, param_list):
+        start = time.time()
+        try:
+            return real_executemany(self, sql, param_list)
+        finally:
+            record_many_sql(self.db.vendor, getattr(self.db, 'alias', None),
+                            start, sql, param_list)
+
+    CursorWrapper.execute = execute
+    CursorWrapper.executemany = executemany
+    breadcrumbs.ignore_logger('django.db.backends')
+
+
 class DjangoClient(Client):
     logger = logging.getLogger('sentry.errors.client.django')
 
+    def __init__(self, *args, **kwargs):
+        install_sql_hook = kwargs.pop('install_sql_hook', True)
+        Client.__init__(self, *args, **kwargs)
+        if install_sql_hook:
+            self.install_sql_hook()
+
+    def install_sql_hook(self):
+        install_sql_hook()
+
     def get_user_info(self, user):
         if hasattr(user, 'is_authenticated') and \
            not user.is_authenticated():
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/raven-5.12.0/raven/contrib/django/models.py 
new/raven-5.19.0/raven/contrib/django/models.py
--- old/raven-5.12.0/raven/contrib/django/models.py     2016-01-13 
17:52:15.000000000 +0100
+++ new/raven-5.19.0/raven/contrib/django/models.py     2016-05-03 
00:45:35.000000000 +0200
@@ -181,7 +181,11 @@
 
 
 def register_handlers():
-    from django.core.signals import got_request_exception
+    from django.core.signals import got_request_exception, request_started
+
+    def before_request(*args, **kwargs):
+        client.context.activate()
+    request_started.connect(before_request, weak=False)
 
     # HACK: support Sentry's internal communication
     if 'sentry' in settings.INSTALLED_APPS:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/raven-5.12.0/raven/contrib/tornado/__init__.py 
new/raven-5.19.0/raven/contrib/tornado/__init__.py
--- old/raven-5.12.0/raven/contrib/tornado/__init__.py  2016-01-12 
20:23:55.000000000 +0100
+++ new/raven-5.19.0/raven/contrib/tornado/__init__.py  2016-04-22 
23:47:13.000000000 +0200
@@ -38,9 +38,9 @@
 
         data = self.build_msg(*args, **kwargs)
 
-        self.send(callback=kwargs.get('callback', None), **data)
+        future = self.send(callback=kwargs.get('callback', None), **data)
 
-        return (data['event_id'],)
+        return (data['event_id'], future)
 
     def send(self, auth_header=None, callback=None, **data):
         """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/raven-5.12.0/raven/transport/threaded.py 
new/raven-5.19.0/raven/transport/threaded.py
--- old/raven-5.12.0/raven/transport/threaded.py        2015-12-10 
20:09:34.000000000 +0100
+++ new/raven-5.19.0/raven/transport/threaded.py        2016-05-03 
00:35:28.000000000 +0200
@@ -16,7 +16,7 @@
 
 from raven.transport.base import AsyncTransport
 from raven.transport.http import HTTPTransport
-from raven.utils.compat import Queue
+from raven.utils.compat import Queue, check_threads
 
 DEFAULT_TIMEOUT = 10
 
@@ -27,6 +27,7 @@
     _terminator = object()
 
     def __init__(self, shutdown_timeout=DEFAULT_TIMEOUT):
+        check_threads()
         self._queue = Queue(-1)
         self._lock = threading.Lock()
         self._thread = None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/raven-5.12.0/raven/utils/__init__.py 
new/raven-5.19.0/raven/utils/__init__.py
--- old/raven-5.12.0/raven/utils/__init__.py    2016-01-13 17:52:15.000000000 
+0100
+++ new/raven-5.19.0/raven/utils/__init__.py    2016-05-03 00:45:35.000000000 
+0200
@@ -9,6 +9,8 @@
 
 from raven._compat import iteritems, string_types
 import logging
+import threading
+from functools import update_wrapper
 try:
     import pkg_resources
 except ImportError:
@@ -65,7 +67,7 @@
         # pull version from pkg_resources if distro exists
         try:
             return pkg_resources.get_distribution(module_name).version
-        except pkg_resources.DistributionNotFound:
+        except Exception:
             pass
 
     if hasattr(app, 'get_version'):
@@ -167,3 +169,22 @@
         if n not in d:
             d[n] = self.func(obj)
         return d[n]
+
+
+def once(func):
+    """Runs a thing once and once only."""
+    lock = threading.Lock()
+
+    def new_func(*args, **kwargs):
+        if new_func.called:
+            return
+        with lock:
+            if new_func.called:
+                return
+            rv = func(*args, **kwargs)
+            new_func.called = True
+            return rv
+
+    new_func = update_wrapper(new_func, func)
+    new_func.called = False
+    return new_func
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/raven-5.12.0/raven/utils/compat.py 
new/raven-5.19.0/raven/utils/compat.py
--- old/raven-5.12.0/raven/utils/compat.py      2015-07-12 10:30:42.000000000 
+0200
+++ new/raven-5.19.0/raven/utils/compat.py      2016-05-03 00:35:28.000000000 
+0200
@@ -46,3 +46,17 @@
     from urllib import parse as _urlparse  # NOQA
 
 urlparse = _urlparse
+
+
+def check_threads():
+    try:
+        from uwsgi import opt
+    except ImportError:
+        return
+
+    if str(opt.get('enable-threads', '0')).lower() in ('false', 'off', 'no', 
'0'):
+        from warnings import warn
+        warn(Warning('We detected the use of uwsgi with disabled threads.  '
+                     'This will cause issues with the transport you are '
+                     'trying to use.  Please enable threading for uwsgi.  '
+                     '(Enable the "enable-threads" flag).'))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/raven-5.12.0/raven/utils/stacks.py 
new/raven-5.19.0/raven/utils/stacks.py
--- old/raven-5.12.0/raven/utils/stacks.py      2016-03-25 22:20:21.000000000 
+0100
+++ new/raven-5.19.0/raven/utils/stacks.py      2016-05-27 18:24:21.000000000 
+0200
@@ -5,7 +5,7 @@
 :copyright: (c) 2010-2012 by the Sentry Team, see AUTHORS for more details.
 :license: BSD, see LICENSE for more details.
 """
-from __future__ import absolute_import
+from __future__ import absolute_import, division
 
 import inspect
 import linecache
@@ -198,18 +198,48 @@
 
     Returns ``frames``.
     """
-    frames_len = len(frames)
+    frames_len = 0
+    app_frames = []
+    system_frames = []
+    for frame in frames:
+        frames_len += 1
+        if frame.get('in_app'):
+            app_frames.append(frame)
+        else:
+            system_frames.append(frame)
 
     if frames_len <= frame_allowance:
         return frames
 
-    half_max = int(frame_allowance / 2)
+    remaining = frames_len - frame_allowance
+    app_count = len(app_frames)
+    system_allowance = max(frame_allowance - app_count, 0)
+    if system_allowance:
+        half_max = int(system_allowance / 2)
+        # prioritize trimming system frames
+        for frame in system_frames[half_max:-half_max]:
+            frame.pop('vars', None)
+            frame.pop('pre_context', None)
+            frame.pop('post_context', None)
+            remaining -= 1
+
+    else:
+        for frame in system_frames:
+            frame.pop('vars', None)
+            frame.pop('pre_context', None)
+            frame.pop('post_context', None)
+            remaining -= 1
+
+    if not remaining:
+        return frames
+
+    app_allowance = app_count - remaining
+    half_max = int(app_allowance / 2)
 
-    for n in range(half_max, frames_len - half_max):
-        # remove heavy components
-        frames[n].pop('vars', None)
-        frames[n].pop('pre_context', None)
-        frames[n].pop('post_context', None)
+    for frame in app_frames[half_max:-half_max]:
+        frame.pop('vars', None)
+        frame.pop('pre_context', None)
+        frame.pop('post_context', None)
     return frames
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/raven-5.12.0/raven/versioning.py 
new/raven-5.19.0/raven/versioning.py
--- old/raven-5.12.0/raven/versioning.py        2016-01-13 17:52:15.000000000 
+0100
+++ new/raven-5.19.0/raven/versioning.py        2016-05-03 00:34:02.000000000 
+0200
@@ -28,8 +28,9 @@
             head = text_type(fp.read()).strip()
 
         if head.startswith('ref: '):
+            head = head[5:]
             revision_file = os.path.join(
-                path, '.git', *head.rsplit(' ', 1)[-1].split('/')
+                path, '.git', *head.split('/')
             )
         else:
             return head
@@ -40,6 +41,25 @@
         if not os.path.exists(os.path.join(path, '.git')):
             raise InvalidGitRepository(
                 '%s does not seem to be the root of a git repository' % 
(path,))
+
+        # Check for our .git/packed-refs' file since a `git gc` may have run
+        # 
https://git-scm.com/book/en/v2/Git-Internals-Maintenance-and-Data-Recovery
+        packed_file = os.path.join(path, '.git', 'packed-refs')
+        if os.path.exists(packed_file):
+            with open(packed_file, 'r') as fh:
+                for line in fh:
+                    line = line.rstrip()
+                    if not line:
+                        continue
+                    if line[:1] in ('#', '^'):
+                        continue
+                    try:
+                        revision, ref = line.split(' ', 1)
+                    except ValueError:
+                        continue
+                    if ref == head:
+                        return text_type(revision)
+
         raise InvalidGitRepository(
             'Unable to find ref to head "%s" in repository' % (head,))
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/raven-5.12.0/raven.egg-info/PKG-INFO 
new/raven-5.19.0/raven.egg-info/PKG-INFO
--- old/raven-5.12.0/raven.egg-info/PKG-INFO    2016-03-30 00:07:14.000000000 
+0200
+++ new/raven-5.19.0/raven.egg-info/PKG-INFO    2016-05-27 18:55:29.000000000 
+0200
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: raven
-Version: 5.12.0
+Version: 5.19.0
 Summary: Raven is a client for Sentry (https://getsentry.com)
 Home-page: https://github.com/getsentry/raven-python
 Author: Sentry
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/raven-5.12.0/raven.egg-info/SOURCES.txt 
new/raven-5.19.0/raven.egg-info/SOURCES.txt
--- old/raven-5.12.0/raven.egg-info/SOURCES.txt 2016-03-30 00:07:14.000000000 
+0200
+++ new/raven-5.19.0/raven.egg-info/SOURCES.txt 2016-05-27 18:55:29.000000000 
+0200
@@ -6,6 +6,7 @@
 raven/__init__.py
 raven/_compat.py
 raven/base.py
+raven/breadcrumbs.py
 raven/context.py
 raven/events.py
 raven/exceptions.py
@@ -101,6 +102,8 @@
 tests/__init__.py
 tests/base/__init__.py
 tests/base/tests.py
+tests/breadcrumbs/__init__.py
+tests/breadcrumbs/tests.py
 tests/conf/__init__.py
 tests/conf/tests.py
 tests/context/__init__.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/raven-5.12.0/setup.py new/raven-5.19.0/setup.py
--- old/raven-5.12.0/setup.py   2016-03-30 00:06:55.000000000 +0200
+++ new/raven-5.19.0/setup.py   2016-05-27 18:37:52.000000000 +0200
@@ -97,7 +97,7 @@
 
 setup(
     name='raven',
-    version='5.12.0',
+    version='5.19.0',
     author='Sentry',
     author_email='[email protected]',
     url='https://github.com/getsentry/raven-python',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/raven-5.12.0/tests/breadcrumbs/tests.py 
new/raven-5.19.0/tests/breadcrumbs/tests.py
--- old/raven-5.12.0/tests/breadcrumbs/tests.py 1970-01-01 01:00:00.000000000 
+0100
+++ new/raven-5.19.0/tests/breadcrumbs/tests.py 2016-05-27 18:37:12.000000000 
+0200
@@ -0,0 +1,121 @@
+import sys
+import logging
+
+from raven.utils.testutils import TestCase
+
+from raven.base import Client
+from raven import breadcrumbs
+
+from io import StringIO
+
+
+class BreadcrumbTestCase(TestCase):
+
+    def test_crumb_buffer(self):
+        for enable in 1, 0:
+            client = Client('http://foo:[email protected]/0',
+                            enable_breadcrumbs=enable)
+            with client.context:
+                breadcrumbs.record(type='foo', data={'bar': 'baz'},
+                                   message='aha', category='huhu')
+                crumbs = client.context.breadcrumbs.get_buffer()
+                assert len(crumbs) == enable
+
+    def test_log_crumb_reporting(self):
+        client = Client('http://foo:[email protected]/0')
+        with client.context:
+            log = logging.getLogger('whatever.foo')
+            log.info('This is a message with %s!', 'foo', blah='baz')
+            crumbs = client.context.breadcrumbs.get_buffer()
+
+        assert len(crumbs) == 1
+        assert crumbs[0]['type'] == 'default'
+        assert crumbs[0]['category'] == 'whatever.foo'
+        assert crumbs[0]['data'] == {'blah': 'baz'}
+        assert crumbs[0]['message'] == 'This is a message with foo!'
+
+    def test_log_location(self):
+        out = StringIO()
+        logger = logging.getLogger(__name__)
+        logger.setLevel(logging.DEBUG)
+        handler = logging.StreamHandler(out)
+        handler.setFormatter(logging.Formatter(
+            u'%(name)s|%(filename)s|%(funcName)s|%(lineno)d|'
+            u'%(levelname)s|%(message)s'))
+        logger.addHandler(handler)
+
+        client = Client('http://foo:[email protected]/0')
+        with client.context:
+            logger.info('Hello World!')
+            lineno = sys._getframe().f_lineno - 1
+
+        items = out.getvalue().strip().split('|')
+        assert items[0] == 'tests.breadcrumbs.tests'
+        assert items[1].rstrip('co') == 'tests.py'
+        assert items[2] == 'test_log_location'
+        assert int(items[3]) == lineno
+        assert items[4] == 'INFO'
+        assert items[5] == 'Hello World!'
+
+    def test_broken_logging(self):
+        client = Client('http://foo:[email protected]/0')
+        with client.context:
+            log = logging.getLogger('whatever.foo')
+            log.info('This is a message with %s. %s!', 42)
+            crumbs = client.context.breadcrumbs.get_buffer()
+
+        assert len(crumbs) == 1
+        assert crumbs[0]['type'] == 'default'
+        assert crumbs[0]['category'] == 'whatever.foo'
+        assert crumbs[0]['message'] == 'This is a message with %s. %s!'
+
+    def test_dedup_logging(self):
+        client = Client('http://foo:[email protected]/0')
+        with client.context:
+            log = logging.getLogger('whatever.foo')
+            log.info('This is a message with %s!', 42)
+            log.info('This is a message with %s!', 42)
+            log.info('This is a message with %s!', 42)
+            log.info('This is a message with %s!', 23)
+            log.info('This is a message with %s!', 23)
+            log.info('This is a message with %s!', 23)
+            log.info('This is a message with %s!', 42)
+            crumbs = client.context.breadcrumbs.get_buffer()
+
+        assert len(crumbs) == 3
+        assert crumbs[0]['type'] == 'default'
+        assert crumbs[0]['category'] == 'whatever.foo'
+        assert crumbs[0]['message'] == 'This is a message with 42!'
+        assert crumbs[1]['type'] == 'default'
+        assert crumbs[1]['category'] == 'whatever.foo'
+        assert crumbs[1]['message'] == 'This is a message with 23!'
+        assert crumbs[2]['type'] == 'default'
+        assert crumbs[2]['category'] == 'whatever.foo'
+        assert crumbs[2]['message'] == 'This is a message with 42!'
+
+    def test_manual_record(self):
+        client = Client('http://foo:[email protected]/0')
+        with client.context:
+            def processor(data):
+                assert data['message'] == 'whatever'
+                assert data['level'] == 'warning'
+                assert data['category'] == 'category'
+                assert data['type'] == 'the_type'
+                assert data['data'] == {'foo': 'bar'}
+                data['data']['extra'] = 'something'
+
+            breadcrumbs.record(message='whatever',
+                               level='warning',
+                               category='category',
+                               data={'foo': 'bar'},
+                               type='the_type',
+                               processor=processor)
+
+            crumbs = client.context.breadcrumbs.get_buffer()
+            assert len(crumbs) == 1
+            data = crumbs[0]
+            assert data['message'] == 'whatever'
+            assert data['level'] == 'warning'
+            assert data['category'] == 'category'
+            assert data['type'] == 'the_type'
+            assert data['data'] == {'foo': 'bar', 'extra': 'something'}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/raven-5.12.0/tests/context/tests.py 
new/raven-5.19.0/tests/context/tests.py
--- old/raven-5.12.0/tests/context/tests.py     2015-07-12 10:30:42.000000000 
+0200
+++ new/raven-5.19.0/tests/context/tests.py     2016-05-03 23:12:50.000000000 
+0200
@@ -1,5 +1,7 @@
+import threading
 from raven.utils.testutils import TestCase
 
+from raven.base import Client
 from raven.context import Context
 
 
@@ -35,3 +37,36 @@
                 'biz': 'baz',
             }
         }
+
+    def test_thread_binding(self):
+        client = Client()
+        called = []
+
+        class TestContext(Context):
+
+            def activate(self):
+                Context.activate(self)
+                called.append('activate')
+
+            def deactivate(self):
+                called.append('deactivate')
+                Context.deactivate(self)
+
+        # The main thread activates the context but clear does not
+        # deactivate.
+        context = TestContext(client)
+        context.clear()
+        assert called == ['activate']
+
+        # But another thread does.
+        del called[:]
+
+        def test_thread():
+            # This activate is unnecessary as the first activate happens
+            # automatically
+            context.activate()
+            context.clear()
+        t = threading.Thread(target=test_thread)
+        t.start()
+        t.join()
+        assert called == ['activate', 'activate', 'deactivate']


Reply via email to