diff --git a/CHANGES b/CHANGES index 9d7b71b0..088dc2a9 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 66c1b9f8..f71538dd 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 f3ffe874..e568478c 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' - - -Threaded (Default) +Builtin Transports ------------------ -Spawns an async worker for processing messages. +.. data:: sentry.transport.thread.ThreadedHTTPTransport -:: + The default transport. Manages a threaded worker for processing messages asynchronous. - SENTRY_DSN = 'threaded+http://public:secret@example.com/1' +.. data:: sentry.transport.http.HTTPTransport + A synchronous blocking transport. -Tornado -------- +.. data:: sentry.transport.aiohttp.AioHttpTransport -Should only be used within a Tornado IO loop. + Should only be used within a :pep:`3156` compatible event loops + (*asyncio* itself and others). -:: +.. data:: sentry.transport.eventlet.EventletHTTPTransport - SENTRY_DSN = 'tornado+http://public:secret@example.com/1' + Should only be used within an Eventlet IO loop. +.. data:: sentry.transport.gevent.GeventedHTTPTransport -Twisted -------- + Should only be used within a Gevent IO loop. -Should only be used within a Twisted event loop. +.. data:: sentry.transport.requests.RequestsHTTPTransport -:: + A synchronous transport which relies on the ``requests`` library. - SENTRY_DSN = 'twisted+http://public:secret@example.com/1' +.. data:: sentry.transport.tornado.TornadoHTTPTransport + Should only be used within a Tornado IO loop. + +.. 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 9d2ce262..e5384c63 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 2a453eb0..8bd7cd71 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 5eda9f90..5da0fc6d 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 + 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.servers = servers - self.public_key = public_key - self.secret_key = secret_key - self.project = project or defaults.PROJECT - self.transport_options = transport_options + 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', - } + headers = { + 'User-Agent': client_string, + 'X-Sentry-Auth': auth_header, + 'Content-Type': 'application/octet-stream', + } - self.send_remote(url=url, data=message, headers=headers) + 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 4e5e56e8..e6d94f55 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 00000000..7ba4cdfc --- /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 67847893..04cc5ab5 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 270f4e0d..275aef73 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 4c3889a0..353de967 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 0ba27606..4b1645ab 100644 --- a/raven/contrib/tornado/__init__.py +++ b/raven/contrib/tornado/__init__.py @@ -7,13 +7,10 @@ raven.contrib.tornado """ from __future__ import absolute_import -import time - -import raven -from raven.base import Client -from raven.utils import get_auth_header from tornado.httpclient import AsyncHTTPClient, HTTPError +from raven.base import Client + class AsyncSentryClient(Client): """A mixin class that could be used along with request handlers to @@ -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 598a9f5b..4130839b 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 8110813b..b58077e3 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 b11dc019..ed4e15d1 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 857b8b6d..6c5f3bf5 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 ac0ac360..4be96c00 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 7fe1873b..d2588e0c 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 similarity index 100% rename from tests/config/__init__.py rename to tests/conf/__init__.py diff --git a/tests/conf/tests.py b/tests/conf/tests.py new file mode 100644 index 00000000..9caa2421 --- /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 073498be..00000000 --- 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 ac60a4c7..2eba3b85 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.