summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Cramer <dcramer@gmail.com>2015-03-29 11:34:31 -0700
committerDavid Cramer <dcramer@gmail.com>2015-05-21 17:25:24 -0700
commit12bb73770747ae8a6cc688bff32279257362e76b (patch)
tree079c4982783e0389539491d0c7fb8d6667e3e17a
parent23bd04541dfa81b888bbe39043bc9a7f20e26abe (diff)
downloadraven-12bb73770747ae8a6cc688bff32279257362e76b.tar.gz
Refactor DSN and transport configurationdsn-refactor
Passing a transport via a scheme prefix is now deprecated in favor of explicit class loading: >>> Client(dsn, transport=HttpTransport) Additionally DSN-related configuration is now held in the RemoteConfig helper, which is bound on Client.remote. Fixes GH-587
-rw-r--r--CHANGES8
-rw-r--r--docs/config/index.rst5
-rw-r--r--docs/transports/index.rst89
-rw-r--r--docs/usage.rst4
-rw-r--r--raven/__init__.py2
-rw-r--r--raven/base.py95
-rw-r--r--raven/conf/__init__.py39
-rw-r--r--raven/conf/remote.py96
-rw-r--r--raven/contrib/django/client.py2
-rw-r--r--raven/contrib/django/middleware/__init__.py2
-rw-r--r--raven/contrib/pylons/__init__.py4
-rw-r--r--raven/contrib/tornado/__init__.py34
-rw-r--r--raven/exceptions.py8
-rw-r--r--raven/scripts/runner.py5
-rw-r--r--raven/transport/base.py33
-rw-r--r--raven/transport/registry.py3
-rw-r--r--raven/utils/urlparse.py1
-rw-r--r--tests/base/tests.py30
-rw-r--r--tests/conf/__init__.py (renamed from tests/config/__init__.py)0
-rw-r--r--tests/conf/tests.py119
-rw-r--r--tests/config/tests.py128
-rw-r--r--tests/transport/tests.py10
22 files changed, 314 insertions, 403 deletions
diff --git a/CHANGES b/CHANGES
index 9d7b71b..088dc2a 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,11 @@
+Version 5.4.0
+-------------
+
+* Binding transports via a scheme prefix on DSNs is now deprecated.
+* ``raven.conf.load`` has been removed.
+* Upstream-related configuration (such as url, project_id, and keys) is now contained in ``RemoteConfig``
+ attached to ``Client.remote``
+
Version 5.3.1
-------------
diff --git a/docs/config/index.rst b/docs/config/index.rst
index 66c1b9f..f71538d 100644
--- a/docs/config/index.rst
+++ b/docs/config/index.rst
@@ -66,11 +66,6 @@ It is composed of six important pieces:
* The project ID which the authenticated user is bound to.
-.. note::
-
- Protocol may also contain transporter type: gevent+http, gevent+https, twisted+http, tornado+http, eventlet+http, eventlet+https
-
- For *Python 3.3+* also available: aiohttp+http and aiohttp+https
Client Arguments
----------------
diff --git a/docs/transports/index.rst b/docs/transports/index.rst
index f3ffe87..e568478 100644
--- a/docs/transports/index.rst
+++ b/docs/transports/index.rst
@@ -3,7 +3,12 @@ Transports
A transport is the mechanism in which Raven sends the HTTP request to the Sentry server. By default, Raven uses a threaded asynchronous transport, but you can easily adjust this by modifying your ``SENTRY_DSN`` value.
-Transport registration is done via the URL prefix, so for example, a synchronous transport is as simple as prefixing your ``SENTRY_DSN`` with the ``sync+`` value.
+Transport registration is done as part of the Client configuration:
+
+.. code-block:: python
+
+ # Use the synchronous HTTP transport
+ client = Client('http://public:secret@example.com/1', transport=HTTPTransport)
Options are passed to transports via the querystring.
@@ -25,82 +30,38 @@ For example, to increase the timeout and to disable SSL verification:
SENTRY_DSN = 'http://public:secret@example.com/1?timeout=5&verify_ssl=0'
-aiohttp
--------
-
-Should only be used within a :pep:`3156` compatible event loops
-(*asyncio* itself and others).
-
-::
-
- SENTRY_DSN = 'aiohttp+http://public:secret@example.com/1'
-
-Eventlet
---------
-
-Should only be used within an Eventlet IO loop.
-
-::
-
- SENTRY_DSN = 'eventlet+http://public:secret@example.com/1'
-
-
-Gevent
-------
-
-Should only be used within a Gevent IO loop.
-
-::
-
- SENTRY_DSN = 'gevent+http://public:secret@example.com/1'
-
-
-Requests
---------
-
-Requires the ``requests`` library. Synchronous.
-
-::
-
- SENTRY_DSN = 'requests+http://public:secret@example.com/1'
-
-
-Sync
-----
-
-A synchronous blocking transport.
-
-::
-
- SENTRY_DSN = 'sync+http://public:secret@example.com/1'
+Builtin Transports
+------------------
+.. data:: sentry.transport.thread.ThreadedHTTPTransport
-Threaded (Default)
-------------------
+ The default transport. Manages a threaded worker for processing messages asynchronous.
-Spawns an async worker for processing messages.
+.. data:: sentry.transport.http.HTTPTransport
-::
+ A synchronous blocking transport.
- SENTRY_DSN = 'threaded+http://public:secret@example.com/1'
+.. data:: sentry.transport.aiohttp.AioHttpTransport
+ Should only be used within a :pep:`3156` compatible event loops
+ (*asyncio* itself and others).
-Tornado
--------
+.. data:: sentry.transport.eventlet.EventletHTTPTransport
-Should only be used within a Tornado IO loop.
+ Should only be used within an Eventlet IO loop.
-::
+.. data:: sentry.transport.gevent.GeventedHTTPTransport
- SENTRY_DSN = 'tornado+http://public:secret@example.com/1'
+ Should only be used within a Gevent IO loop.
+.. data:: sentry.transport.requests.RequestsHTTPTransport
-Twisted
--------
+ A synchronous transport which relies on the ``requests`` library.
-Should only be used within a Twisted event loop.
+.. data:: sentry.transport.tornado.TornadoHTTPTransport
-::
+ Should only be used within a Tornado IO loop.
- SENTRY_DSN = 'twisted+http://public:secret@example.com/1'
+.. data:: sentry.transport.twisted.TwistedHTTPTransport
+ Should only be used within a Twisted event loop.
diff --git a/docs/usage.rst b/docs/usage.rst
index 9d2ce26..e5384c6 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -55,9 +55,9 @@ the optional DSN argument::
You should get something like the following, assuming you're configured everything correctly::
- $ raven test sync+http://dd2c825ff9b1417d88a99573903ebf80:91631495b10b45f8a1cdbc492088da6a@localhost:9000/1
+ $ raven test http://dd2c825ff9b1417d88a99573903ebf80:91631495b10b45f8a1cdbc492088da6a@localhost:9000/1
Using DSN configuration:
- sync+http://dd2c825ff9b1417d88a99573903ebf80:91631495b10b45f8a1cdbc492088da6a@localhost:9000/1
+ http://dd2c825ff9b1417d88a99573903ebf80:91631495b10b45f8a1cdbc492088da6a@localhost:9000/1
Client configuration:
servers : ['http://localhost:9000/api/store/']
diff --git a/raven/__init__.py b/raven/__init__.py
index 2a453eb..8bd7cd7 100644
--- a/raven/__init__.py
+++ b/raven/__init__.py
@@ -14,7 +14,7 @@ from raven.conf import * # NOQA
from raven.versioning import * # NOQA
-__all__ = ('VERSION', 'Client', 'load', 'get_version')
+__all__ = ('VERSION', 'Client', 'get_version')
try:
VERSION = __import__('pkg_resources') \
diff --git a/raven/base.py b/raven/base.py
index 5eda9f9..5da0fc6 100644
--- a/raven/base.py
+++ b/raven/base.py
@@ -24,13 +24,13 @@ from types import FunctionType
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 six, json, get_versions, get_auth_header, merge_dicts
from raven.utils.encoding import to_unicode
from raven.utils.serializer import transform
from raven.utils.stacks import get_stack_info, iter_stack_frames, get_culprit
-from raven.utils.urlparse import urlparse
from raven.transport.registry import TransportRegistry, default_transports
__all__ = ('Client',)
@@ -117,7 +117,7 @@ class Client(object):
_registry = TransportRegistry(transports=default_transports)
- def __init__(self, dsn=None, raise_send_errors=False, **options):
+ def __init__(self, dsn=None, raise_send_errors=False, transport=None, **options):
global Raven
o = options
@@ -134,8 +134,8 @@ class Client(object):
self.error_logger = logging.getLogger('sentry.errors')
self.uncaught_logger = logging.getLogger('sentry.errors.uncaught')
- self.dsns = {}
- self.set_dsn(dsn, **options)
+ self._transport_cache = {}
+ self.set_dsn(dsn, transport)
self.include_paths = set(o.get('include_paths') or [])
self.exclude_paths = set(o.get('exclude_paths') or [])
@@ -174,42 +174,27 @@ class Client(object):
self._context = Context()
- def set_dsn(self, dsn=None, **options):
- o = options
-
+ def set_dsn(self, dsn=None, transport=None):
if dsn is None and os.environ.get('SENTRY_DSN'):
msg = "Configuring Raven from environment variable 'SENTRY_DSN'"
self.logger.debug(msg)
dsn = os.environ['SENTRY_DSN']
- try:
- servers, public_key, secret_key, project, transport_options = self.dsns[dsn]
- except KeyError:
- if dsn:
- # TODO: should we validate other options weren't sent?
- urlparts = urlparse(dsn)
- self.logger.debug(
- "Configuring Raven for host: %s://%s:%s" % (urlparts.scheme,
- urlparts.netloc, urlparts.path))
- dsn_config = raven.load(dsn, transport_registry=self._registry)
- servers = dsn_config['SENTRY_SERVERS']
- project = dsn_config['SENTRY_PROJECT']
- public_key = dsn_config['SENTRY_PUBLIC_KEY']
- secret_key = dsn_config['SENTRY_SECRET_KEY']
- transport_options = dsn_config.get('SENTRY_TRANSPORT_OPTIONS', {})
+ if dsn not in self._transport_cache:
+ if dsn is None:
+ result = RemoteConfig(transport=transport)
else:
- servers = ()
- project = None
- public_key = None
- secret_key = None
- transport_options = {}
- self.dsns[dsn] = servers, public_key, secret_key, project, transport_options
-
- self.servers = servers
- self.public_key = public_key
- self.secret_key = secret_key
- self.project = project or defaults.PROJECT
- self.transport_options = transport_options
+ result = RemoteConfig.from_string(
+ dsn,
+ transport=transport,
+ transport_registry=self._registry,
+ )
+ self._transport_cache[dsn] = result
+ self.remote = result
+ else:
+ self.remote = self._transport_cache[dsn]
+
+ self.logger.debug("Configuring Raven for host: {0}".format(self.remote))
@classmethod
def register_scheme(cls, scheme, transport_class):
@@ -252,14 +237,6 @@ class Client(object):
def get_handler(self, name):
return self.module_cache[name](self)
- def _get_public_dsn(self):
- url = urlparse(self.servers[0])
- netloc = url.hostname
- if url.port:
- netloc += ':%s' % url.port
- path = url.path.replace('api/%s/store/' % (self.project,), self.project)
- return '//%s@%s%s' % (self.public_key, netloc, path)
-
def get_public_dsn(self, scheme=None):
"""
Returns a public DSN which is consumable by raven-js
@@ -272,7 +249,7 @@ class Client(object):
"""
if not self.is_enabled():
return
- url = self._get_public_dsn()
+ url = self.remote.get_public_dsn()
if not scheme:
return url
return '%s:%s' % (scheme, url)
@@ -397,7 +374,7 @@ class Client(object):
data['extra'][k] = self.transform(v)
# It's important date is added **after** we serialize
- data.setdefault('project', self.project)
+ data.setdefault('project', self.remote.project)
data.setdefault('timestamp', date or datetime.utcnow())
data.setdefault('time_spent', time_spent)
data.setdefault('event_id', event_id)
@@ -532,7 +509,7 @@ class Client(object):
Return a boolean describing whether the client should attempt to send
events.
"""
- return bool(self.servers)
+ return self.remote.is_active()
def _successful_send(self):
self.state.set_success()
@@ -590,9 +567,7 @@ class Client(object):
self._failed_send(e, url, self.decode(data))
try:
- parsed = urlparse(url)
- transport = self._registry.get_transport(
- parsed, **self.transport_options)
+ transport = self.remote.get_transport()
if transport.async:
transport.async_send(data, headers, self._successful_send,
failed_send)
@@ -626,18 +601,22 @@ class Client(object):
protocol=self.protocol_version,
timestamp=timestamp,
client=client_string,
- api_key=self.public_key,
- api_secret=self.secret_key,
+ api_key=self.remote.public_key,
+ api_secret=self.remote.secret_key,
)
- for url in self.servers:
- headers = {
- 'User-Agent': client_string,
- 'X-Sentry-Auth': auth_header,
- 'Content-Type': 'application/octet-stream',
- }
-
- self.send_remote(url=url, data=message, headers=headers)
+ headers = {
+ 'User-Agent': client_string,
+ 'X-Sentry-Auth': auth_header,
+ 'Content-Type': 'application/octet-stream',
+ }
+
+ self.send_remote(
+ url=self.remote.store_endpoint,
+ data=message,
+ headers=headers,
+ **kwargs
+ )
def encode(self, data):
"""
diff --git a/raven/conf/__init__.py b/raven/conf/__init__.py
index 4e5e56e..e6d94f5 100644
--- a/raven/conf/__init__.py
+++ b/raven/conf/__init__.py
@@ -8,9 +8,8 @@ raven.conf
from __future__ import absolute_import
import logging
-from raven.utils.urlparse import urlparse
-__all__ = ('load', 'setup_logging')
+__all__ = ['setup_logging']
EXCLUDE_LOGGER_DEFAULTS = (
'raven',
@@ -21,42 +20,6 @@ EXCLUDE_LOGGER_DEFAULTS = (
)
-# TODO (vng): this seems weirdly located in raven.conf. Seems like
-# it's really a part of raven.transport.TransportRegistry
-# Not quite sure what to do with this
-def load(dsn, scope=None, transport_registry=None):
- """
- Parses a Sentry compatible DSN and loads it
- into the given scope.
-
- >>> import raven
-
- >>> dsn = 'https://public_key:secret_key@sentry.local/project_id'
-
- >>> # Apply configuration to local scope
- >>> raven.load(dsn, locals())
-
- >>> # Return DSN configuration
- >>> options = raven.load(dsn)
- """
-
- if not transport_registry:
- from raven.transport import TransportRegistry, default_transports
- transport_registry = TransportRegistry(default_transports)
-
- url = urlparse(dsn)
-
- if not transport_registry.supported_scheme(url.scheme):
- raise ValueError('Unsupported Sentry DSN scheme: %r' % url.scheme)
-
- if scope is None:
- scope = {}
- scope_extras = transport_registry.compute_scope(url, scope)
- scope.update(scope_extras)
-
- return scope
-
-
def setup_logging(handler, exclude=EXCLUDE_LOGGER_DEFAULTS):
"""
Configures logging to pipe to Sentry.
diff --git a/raven/conf/remote.py b/raven/conf/remote.py
new file mode 100644
index 0000000..7ba4cdf
--- /dev/null
+++ b/raven/conf/remote.py
@@ -0,0 +1,96 @@
+from __future__ import absolute_import
+
+import warnings
+
+from raven.exceptions import InvalidDsn
+from raven.transport.threaded import ThreadedHTTPTransport
+from raven.utils import six
+from raven.utils.urlparse import parse_qsl, urlparse
+
+ERR_UNKNOWN_SCHEME = 'Unsupported Sentry DSN scheme: {0}'
+
+DEFAULT_TRANSPORT = ThreadedHTTPTransport
+
+
+class RemoteConfig(object):
+ def __init__(self, base_url=None, project=None, public_key=None,
+ secret_key=None, transport=None, options=None):
+ if base_url:
+ base_url = base_url.rstrip('/')
+ store_endpoint = '%s/api/%s/store/' % (base_url, project)
+ else:
+ store_endpoint = None
+
+ self.base_url = base_url
+ self.project = project
+ self.public_key = public_key
+ self.secret_key = secret_key
+ self.options = options or {}
+ self.store_endpoint = store_endpoint
+
+ self._transport_cls = transport or DEFAULT_TRANSPORT
+
+ def __unicode__(self):
+ return six.text_type(self.base_url)
+
+ def is_active(self):
+ return all([self.base_url, self.project, self.public_key, self.secret_key])
+
+ # TODO(dcramer): we dont want transports bound to a URL
+ def get_transport(self):
+ if not self.store_endpoint:
+ return
+
+ if not hasattr(self, '_transport'):
+ parsed = urlparse(self.store_endpoint)
+ self._transport = self._transport_cls(parsed, **self.options)
+ return self._transport
+
+ def get_public_dsn(self):
+ url = urlparse(self.base_url)
+ netloc = url.hostname
+ if url.port:
+ netloc += ':%s' % url.port
+ return '//%s@%s%s/%s' % (self.public_key, netloc, url.path, self.project)
+
+ @classmethod
+ def from_string(cls, value, transport=None, transport_registry=None):
+ url = urlparse(value)
+
+ if url.scheme not in ('http', 'https'):
+ warnings.warn('Transport selection via DSN is deprecated. You should explicitly pass the transport class to Client() instead.')
+
+ if transport is None:
+ if not transport_registry:
+ from raven.transport import TransportRegistry, default_transports
+ transport_registry = TransportRegistry(default_transports)
+
+ if not transport_registry.supported_scheme(url.scheme):
+ raise InvalidDsn(ERR_UNKNOWN_SCHEME.format(url.scheme))
+
+ transport = transport_registry.get_transport_cls(url.scheme)
+
+ netloc = url.hostname
+ if url.port:
+ netloc += ':%s' % url.port
+
+ path_bits = url.path.rsplit('/', 1)
+ if len(path_bits) > 1:
+ path = path_bits[0]
+ else:
+ path = ''
+ project = path_bits[-1]
+
+ if not all([netloc, project, url.username, url.password]):
+ raise InvalidDsn('Invalid Sentry DSN: %r' % url.geturl())
+
+ base_url = '%s://%s%s' % (url.scheme, netloc, path)
+
+ return cls(
+ base_url=base_url,
+ project=project,
+ public_key=url.username,
+ secret_key=url.password,
+ options=dict(parse_qsl(url.query)),
+ transport=transport,
+ )
diff --git a/raven/contrib/django/client.py b/raven/contrib/django/client.py
index 6784789..04cc5ab 100644
--- a/raven/contrib/django/client.py
+++ b/raven/contrib/django/client.py
@@ -159,7 +159,7 @@ class DjangoClient(Client):
if is_http_request and result:
# attach the sentry object to the request
request.sentry = {
- 'project_id': data.get('project', self.project),
+ 'project_id': data.get('project', self.remote.project),
'id': self.get_ident(result),
}
diff --git a/raven/contrib/django/middleware/__init__.py b/raven/contrib/django/middleware/__init__.py
index 270f4e0..275aef7 100644
--- a/raven/contrib/django/middleware/__init__.py
+++ b/raven/contrib/django/middleware/__init__.py
@@ -41,7 +41,7 @@ class Sentry404CatchMiddleware(object):
return
request.sentry = {
- 'project_id': data.get('project', client.project),
+ 'project_id': data.get('project', client.remote.project),
'id': client.get_ident(result),
}
return response
diff --git a/raven/contrib/pylons/__init__.py b/raven/contrib/pylons/__init__.py
index 4c3889a..353de96 100644
--- a/raven/contrib/pylons/__init__.py
+++ b/raven/contrib/pylons/__init__.py
@@ -22,11 +22,7 @@ class Sentry(Middleware):
def __init__(self, app, config, client_cls=Client):
client = client_cls(
dsn=config.get('sentry.dsn'),
- servers=list_from_setting(config, 'sentry.servers'),
name=config.get('sentry.name'),
- public_key=config.get('sentry.public_key'),
- secret_key=config.get('sentry.secret_key'),
- project=config.get('sentry.project'),
site=config.get('sentry.site'),
include_paths=list_from_setting(config, 'sentry.include_paths'),
exclude_paths=list_from_setting(config, 'sentry.exclude_paths'),
diff --git a/raven/contrib/tornado/__init__.py b/raven/contrib/tornado/__init__.py
index 0ba2760..4b1645a 100644
--- a/raven/contrib/tornado/__init__.py
+++ b/raven/contrib/tornado/__init__.py
@@ -7,12 +7,9 @@ raven.contrib.tornado
"""
from __future__ import absolute_import
-import time
+from tornado.httpclient import AsyncHTTPClient, HTTPError
-import raven
from raven.base import Client
-from raven.utils import get_auth_header
-from tornado.httpclient import AsyncHTTPClient, HTTPError
class AsyncSentryClient(Client):
@@ -49,35 +46,6 @@ class AsyncSentryClient(Client):
return self.send_encoded(message, auth_header=auth_header, callback=callback)
- def send_encoded(self, message, auth_header=None, **kwargs):
- """
- Given an already serialized message, signs the message and passes the
- payload off to ``send_remote`` for each server specified in the servers
- configuration.
-
- callback can be specified as a keyword argument
- """
- if not auth_header:
- timestamp = time.time()
- auth_header = get_auth_header(
- protocol=self.protocol_version,
- timestamp=timestamp,
- client='raven-python/%s' % (raven.VERSION,),
- api_key=self.public_key,
- api_secret=self.secret_key,
- )
-
- for url in self.servers:
- headers = {
- 'X-Sentry-Auth': auth_header,
- 'Content-Type': 'application/octet-stream',
- }
-
- self.send_remote(
- url=url, data=message, headers=headers,
- callback=kwargs.get('callback', None)
- )
-
def send_remote(self, url, data, headers=None, callback=None):
if headers is None:
headers = {}
diff --git a/raven/exceptions.py b/raven/exceptions.py
index 598a9f5..4130839 100644
--- a/raven/exceptions.py
+++ b/raven/exceptions.py
@@ -20,3 +20,11 @@ class RateLimited(APIError):
class InvalidGitRepository(Exception):
pass
+
+
+class ConfigurationError(ValueError):
+ pass
+
+
+class InvalidDsn(ConfigurationError):
+ pass
diff --git a/raven/scripts/runner.py b/raven/scripts/runner.py
index 8110813..b58077e 100644
--- a/raven/scripts/runner.py
+++ b/raven/scripts/runner.py
@@ -49,8 +49,9 @@ def send_test_message(client, options):
sys.stdout.write(' %-15s: %s\n' % (k, getattr(client, k)))
sys.stdout.write('\n')
- if not all([client.servers, client.project, client.public_key, client.secret_key]):
- sys.stdout.write("Error: All values must be set!\n")
+ remote_config = client.remote
+ if not remote_config.is_active():
+ sys.stdout.write("Error: DSN configuration is not valid!\n")
sys.exit(1)
if not client.is_enabled():
diff --git a/raven/transport/base.py b/raven/transport/base.py
index b11dc01..ed4e15d 100644
--- a/raven/transport/base.py
+++ b/raven/transport/base.py
@@ -8,7 +8,6 @@ raven.transport.base
from __future__ import absolute_import
from raven.transport.exceptions import InvalidScheme
-from raven.utils.compat import urlparse
class Transport(object):
@@ -36,38 +35,6 @@ class Transport(object):
"""
raise NotImplementedError
- def compute_scope(self, url, scope):
- """
- You need to override this to compute the SENTRY specific
- additions to the variable scope. See the HTTPTransport for an
- example.
- """
- netloc = url.hostname
- if url.port:
- netloc += ':%s' % url.port
-
- path_bits = url.path.rsplit('/', 1)
- if len(path_bits) > 1:
- path = path_bits[0]
- else:
- path = ''
- project = path_bits[-1]
-
- if not all([netloc, project, url.username, url.password]):
- raise ValueError('Invalid Sentry DSN: %r' % url.geturl())
-
- server = '%s://%s%s/api/%s/store/' % (
- url.scheme, netloc, path, project)
-
- scope.update({
- 'SENTRY_SERVERS': [server],
- 'SENTRY_PROJECT': project,
- 'SENTRY_PUBLIC_KEY': url.username,
- 'SENTRY_SECRET_KEY': url.password,
- 'SENTRY_TRANSPORT_OPTIONS': dict(urlparse.parse_qsl(url.query)),
- })
- return scope
-
class AsyncTransport(Transport):
"""
diff --git a/raven/transport/registry.py b/raven/transport/registry.py
index 857b8b6..6c5f3bf 100644
--- a/raven/transport/registry.py
+++ b/raven/transport/registry.py
@@ -63,6 +63,9 @@ class TransportRegistry(object):
self._transports[full_url] = self._schemes[parsed_url.scheme](parsed_url, **options)
return self._transports[full_url]
+ def get_transport_cls(self, scheme):
+ return self._schemes[scheme]
+
def compute_scope(self, url, scope):
"""
Compute a scope dictionary. This may be overridden by custom
diff --git a/raven/utils/urlparse.py b/raven/utils/urlparse.py
index ac0ac36..4be96c0 100644
--- a/raven/utils/urlparse.py
+++ b/raven/utils/urlparse.py
@@ -15,3 +15,4 @@ def register_scheme(scheme):
urlparse = _urlparse.urlparse
+parse_qsl = _urlparse.parse_qsl
diff --git a/tests/base/tests.py b/tests/base/tests.py
index 7fe1873..d2588e0 100644
--- a/tests/base/tests.py
+++ b/tests/base/tests.py
@@ -133,7 +133,7 @@ class ClientTest(TestCase):
self.assertEquals(client.state.status, client.state.ONLINE)
self.assertEqual(client.state.retry_after, 0)
- @mock.patch('raven.base.Client._registry.get_transport')
+ @mock.patch('raven.conf.remote.RemoteConfig.get_transport')
@mock.patch('raven.base.ClientState.should_try')
def test_async_send_remote_failover(self, should_try, get_transport):
should_try.return_value = True
@@ -238,37 +238,11 @@ class ClientTest(TestCase):
self.assertTrue(type(encoded), str)
self.assertEquals(data, self.client.decode(encoded))
- def test_dsn(self):
- client = Client(dsn='http://public:secret@example.com/1')
- self.assertEquals(client.servers, ['http://example.com/api/1/store/'])
- self.assertEquals(client.project, '1')
- self.assertEquals(client.public_key, 'public')
- self.assertEquals(client.secret_key, 'secret')
-
- def test_dsn_as_first_arg(self):
- client = Client('http://public:secret@example.com/1')
- self.assertEquals(client.servers, ['http://example.com/api/1/store/'])
- self.assertEquals(client.project, '1')
- self.assertEquals(client.public_key, 'public')
- self.assertEquals(client.secret_key, 'secret')
-
- def test_slug_in_dsn(self):
- client = Client('http://public:secret@example.com/slug-name')
- self.assertEquals(client.servers, ['http://example.com/api/slug-name/store/'])
- self.assertEquals(client.project, 'slug-name')
- self.assertEquals(client.public_key, 'public')
- self.assertEquals(client.secret_key, 'secret')
-
def test_get_public_dsn(self):
- client = Client('threaded+http://public:secret@example.com/1')
+ client = Client('http://public:secret@example.com/1')
public_dsn = client.get_public_dsn()
self.assertEquals(public_dsn, '//public@example.com/1')
- def test_get_public_dsn_override_scheme(self):
- client = Client('threaded+http://public:secret@example.com/1')
- public_dsn = client.get_public_dsn('https')
- self.assertEquals(public_dsn, 'https://public@example.com/1')
-
def test_explicit_message_on_message_event(self):
self.client.captureMessage(message='test', data={
'message': 'foo'
diff --git a/tests/config/__init__.py b/tests/conf/__init__.py
index e69de29..e69de29 100644
--- a/tests/config/__init__.py
+++ b/tests/conf/__init__.py
diff --git a/tests/conf/tests.py b/tests/conf/tests.py
new file mode 100644
index 0000000..9caa242
--- /dev/null
+++ b/tests/conf/tests.py
@@ -0,0 +1,119 @@
+from __future__ import with_statement
+
+import logging
+import mock
+
+from raven.conf import setup_logging
+from raven.conf.remote import RemoteConfig
+from raven.exceptions import InvalidDsn
+from raven.utils.testutils import TestCase
+
+
+class RemoteConfigTest(TestCase):
+ def test_path(self):
+ dsn = 'https://foo:bar@sentry.local/app/1'
+ res = RemoteConfig.from_string(dsn)
+ assert res.project == '1'
+ assert res.base_url == 'https://sentry.local/app'
+ assert res.store_endpoint == 'https://sentry.local/app/api/1/store/'
+ assert res.public_key == 'foo'
+ assert res.secret_key == 'bar'
+ assert res.options == {}
+
+ def test_http(self):
+ dsn = 'http://foo:bar@sentry.local/1'
+ res = RemoteConfig.from_string(dsn)
+ assert res.project == '1'
+ assert res.base_url == 'http://sentry.local'
+ assert res.store_endpoint == 'http://sentry.local/api/1/store/'
+ assert res.public_key == 'foo'
+ assert res.secret_key == 'bar'
+ assert res.options == {}
+
+ def test_http_with_port(self):
+ dsn = 'http://foo:bar@sentry.local:9000/1'
+ res = RemoteConfig.from_string(dsn)
+ assert res.project == '1'
+ assert res.base_url == 'http://sentry.local:9000'
+ assert res.store_endpoint == 'http://sentry.local:9000/api/1/store/'
+ assert res.public_key == 'foo'
+ assert res.secret_key == 'bar'
+ assert res.options == {}
+
+ def test_https(self):
+ dsn = 'https://foo:bar@sentry.local/1'
+ res = RemoteConfig.from_string(dsn)
+ assert res.project == '1'
+ assert res.base_url == 'https://sentry.local'
+ assert res.store_endpoint == 'https://sentry.local/api/1/store/'
+ assert res.public_key == 'foo'
+ assert res.secret_key == 'bar'
+ assert res.options == {}
+
+ def test_https_with_port(self):
+ dsn = 'https://foo:bar@sentry.local:9000/app/1'
+ res = RemoteConfig.from_string(dsn)
+ assert res.project == '1'
+ assert res.base_url == 'https://sentry.local:9000/app'
+ assert res.store_endpoint == 'https://sentry.local:9000/app/api/1/store/'
+ assert res.public_key == 'foo'
+ assert res.secret_key == 'bar'
+ assert res.options == {}
+
+ def test_options(self):
+ dsn = 'http://foo:bar@sentry.local/1?timeout=1'
+ res = RemoteConfig.from_string(dsn)
+ assert res.project == '1'
+ assert res.base_url == 'http://sentry.local'
+ assert res.store_endpoint == 'http://sentry.local/api/1/store/'
+ assert res.public_key == 'foo'
+ assert res.secret_key == 'bar'
+ assert res.options == {'timeout': '1'}
+
+ def test_missing_netloc(self):
+ dsn = 'https://foo:bar@/1'
+ self.assertRaises(InvalidDsn, RemoteConfig.from_string, dsn)
+
+ def test_missing_project(self):
+ dsn = 'https://foo:bar@example.com'
+ self.assertRaises(InvalidDsn, RemoteConfig.from_string, dsn)
+
+ def test_missing_public_key(self):
+ dsn = 'https://:bar@example.com'
+ self.assertRaises(InvalidDsn, RemoteConfig.from_string, dsn)
+
+ def test_missing_secret_key(self):
+ dsn = 'https://bar@example.com'
+ self.assertRaises(InvalidDsn, RemoteConfig.from_string, dsn)
+
+ def test_invalid_scheme(self):
+ dsn = 'ftp://foo:bar@sentry.local/1'
+ self.assertRaises(InvalidDsn, RemoteConfig.from_string, dsn)
+
+ def test_get_public_dsn(self):
+ res = RemoteConfig(
+ base_url='http://example.com',
+ project='1',
+ public_key='public',
+ secret_key='secret',
+ )
+ public_dsn = res.get_public_dsn()
+ assert public_dsn == '//public@example.com/1'
+
+
+class SetupLoggingTest(TestCase):
+ def test_basic_not_configured(self):
+ with mock.patch('logging.getLogger', spec=logging.getLogger) as getLogger:
+ logger = getLogger()
+ logger.handlers = []
+ handler = mock.Mock()
+ result = setup_logging(handler)
+ self.assertTrue(result)
+
+ def test_basic_already_configured(self):
+ with mock.patch('logging.getLogger', spec=logging.getLogger) as getLogger:
+ handler = mock.Mock()
+ logger = getLogger()
+ logger.handlers = [handler]
+ result = setup_logging(handler)
+ self.assertFalse(result)
diff --git a/tests/config/tests.py b/tests/config/tests.py
deleted file mode 100644
index 073498b..0000000
--- a/tests/config/tests.py
+++ /dev/null
@@ -1,128 +0,0 @@
-from __future__ import with_statement
-import logging
-import mock
-from raven.conf import load, setup_logging
-from raven.utils.testutils import TestCase
-
-
-class LoadTest(TestCase):
- def test_basic(self):
- dsn = 'https://foo:bar@sentry.local/1'
- res = {}
- load(dsn, res)
- self.assertEquals(res, {
- 'SENTRY_PROJECT': '1',
- 'SENTRY_SERVERS': ['https://sentry.local/api/1/store/'],
- 'SENTRY_PUBLIC_KEY': 'foo',
- 'SENTRY_SECRET_KEY': 'bar',
- 'SENTRY_TRANSPORT_OPTIONS': {},
- })
-
- def test_path(self):
- dsn = 'https://foo:bar@sentry.local/app/1'
- res = {}
- load(dsn, res)
- self.assertEquals(res, {
- 'SENTRY_PROJECT': '1',
- 'SENTRY_SERVERS': ['https://sentry.local/app/api/1/store/'],
- 'SENTRY_PUBLIC_KEY': 'foo',
- 'SENTRY_SECRET_KEY': 'bar',
- 'SENTRY_TRANSPORT_OPTIONS': {},
- })
-
- def test_port(self):
- dsn = 'https://foo:bar@sentry.local:9000/app/1'
- res = {}
- load(dsn, res)
- self.assertEquals(res, {
- 'SENTRY_PROJECT': '1',
- 'SENTRY_SERVERS': ['https://sentry.local:9000/app/api/1/store/'],
- 'SENTRY_PUBLIC_KEY': 'foo',
- 'SENTRY_SECRET_KEY': 'bar',
- 'SENTRY_TRANSPORT_OPTIONS': {},
- })
-
- def test_scope_is_optional(self):
- dsn = 'https://foo:bar@sentry.local/1'
- res = load(dsn)
- self.assertEquals(res, {
- 'SENTRY_PROJECT': '1',
- 'SENTRY_SERVERS': ['https://sentry.local/api/1/store/'],
- 'SENTRY_PUBLIC_KEY': 'foo',
- 'SENTRY_SECRET_KEY': 'bar',
- 'SENTRY_TRANSPORT_OPTIONS': {},
- })
-
- def test_http(self):
- dsn = 'http://foo:bar@sentry.local/app/1'
- res = {}
- load(dsn, res)
- self.assertEquals(res, {
- 'SENTRY_PROJECT': '1',
- 'SENTRY_SERVERS': ['http://sentry.local/app/api/1/store/'],
- 'SENTRY_PUBLIC_KEY': 'foo',
- 'SENTRY_SECRET_KEY': 'bar',
- 'SENTRY_TRANSPORT_OPTIONS': {},
- })
-
- def test_http_with_port(self):
- dsn = 'http://foo:bar@sentry.local:9000/app/1'
- res = {}
- load(dsn, res)
- self.assertEquals(res, {
- 'SENTRY_PROJECT': '1',
- 'SENTRY_SERVERS': ['http://sentry.local:9000/app/api/1/store/'],
- 'SENTRY_PUBLIC_KEY': 'foo',
- 'SENTRY_SECRET_KEY': 'bar',
- 'SENTRY_TRANSPORT_OPTIONS': {},
- })
-
- def test_options(self):
- dsn = 'http://foo:bar@sentry.local:9001/1?timeout=1'
- res = {}
- load(dsn, res)
- self.assertEquals(res, {
- 'SENTRY_PROJECT': '1',
- 'SENTRY_SERVERS': ['http://sentry.local:9001/api/1/store/'],
- 'SENTRY_PUBLIC_KEY': 'foo',
- 'SENTRY_SECRET_KEY': 'bar',
- 'SENTRY_TRANSPORT_OPTIONS': {'timeout': '1'},
- })
-
- def test_missing_netloc(self):
- dsn = 'https://foo:bar@/1'
- self.assertRaises(ValueError, load, dsn)
-
- def test_missing_project(self):
- dsn = 'https://foo:bar@example.com'
- self.assertRaises(ValueError, load, dsn)
-
- def test_missing_public_key(self):
- dsn = 'https://:bar@example.com'
- self.assertRaises(ValueError, load, dsn)
-
- def test_missing_secret_key(self):
- dsn = 'https://bar@example.com'
- self.assertRaises(ValueError, load, dsn)
-
- def test_invalid_scheme(self):
- dsn = 'ftp://foo:bar@sentry.local/1'
- self.assertRaises(ValueError, load, dsn)
-
-
-class SetupLoggingTest(TestCase):
- def test_basic_not_configured(self):
- with mock.patch('logging.getLogger', spec=logging.getLogger) as getLogger:
- logger = getLogger()
- logger.handlers = []
- handler = mock.Mock()
- result = setup_logging(handler)
- self.assertTrue(result)
-
- def test_basic_already_configured(self):
- with mock.patch('logging.getLogger', spec=logging.getLogger) as getLogger:
- handler = mock.Mock()
- logger = getLogger()
- logger.handlers = [handler]
- result = setup_logging(handler)
- self.assertFalse(result)
diff --git a/tests/transport/tests.py b/tests/transport/tests.py
index ac60a4c..2eba3b8 100644
--- a/tests/transport/tests.py
+++ b/tests/transport/tests.py
@@ -6,6 +6,7 @@ from raven.base import Client
# Some internal stuff to extend the transport layer
from raven.transport import Transport
+from raven.transport.exceptions import DuplicateScheme
# Simplify comparing dicts with primitive values:
from raven.utils import json
@@ -38,7 +39,7 @@ class TransportTest(TestCase):
def setUp(self):
try:
Client.register_scheme('mock', DummyScheme)
- except:
+ except DuplicateScheme:
pass
def test_basic_config(self):
@@ -46,7 +47,7 @@ class TransportTest(TestCase):
dsn="mock://some_username:some_password@localhost:8143/1?timeout=1",
name="test_server"
)
- assert c.transport_options == {
+ assert c.remote.options == {
'timeout': '1',
}
@@ -56,10 +57,9 @@ class TransportTest(TestCase):
data = dict(a=42, b=55, c=list(range(50)))
c.send(**data)
- expected_message = zlib.decompress(base64.b64decode(c.encode(data)))
- self.assertIn('mock://localhost:8143/api/1/store/', Client._registry._transports)
- mock_cls = Client._registry._transports['mock://localhost:8143/api/1/store/']
+ mock_cls = c._transport_cache['mock://some_username:some_password@localhost:8143/1'].get_transport()
+ expected_message = zlib.decompress(base64.b64decode(c.encode(data)))
actual_message = zlib.decompress(base64.b64decode(mock_cls._data))
# These loads()/dumps() pairs order the dict keys before comparing the string.