diff -Nru python-amqp-2.3.2/amqp/abstract_channel.py python-amqp-2.4.0/amqp/abstract_channel.py --- python-amqp-2.3.2/amqp/abstract_channel.py 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/amqp/abstract_channel.py 2019-01-13 10:08:19.000000000 +0000 @@ -22,6 +22,7 @@ """ def __init__(self, connection, channel_id): + self.is_closing = False self.connection = connection self.channel_id = channel_id connection.channels[channel_id] = self @@ -80,6 +81,7 @@ if p.value: args, kwargs = p.value + args = args[1:] # We are not returning method back return args if returns_tuple else (args and args[0]) finally: for i, m in enumerate(method): @@ -106,17 +108,13 @@ try: listeners = [self._callbacks[method_sig]] except KeyError: - listeners = None + listeners = [] + one_shot = None try: one_shot = self._pending.pop(method_sig) except KeyError: if not listeners: return - else: - if listeners is None: - listeners = [one_shot] - else: - listeners.append(one_shot) args = [] if amqp_method.args: @@ -127,6 +125,9 @@ for listener in listeners: listener(*args) + if one_shot: + one_shot(method_sig, *args) + #: Placeholder, the concrete implementations will have to #: supply their own versions of _METHOD_MAP _METHODS = {} diff -Nru python-amqp-2.3.2/amqp/basic_message.py python-amqp-2.4.0/amqp/basic_message.py --- python-amqp-2.3.2/amqp/basic_message.py 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/amqp/basic_message.py 2019-01-13 10:08:19.000000000 +0000 @@ -102,11 +102,10 @@ ('cluster_id', 's') ] - #: set by basic_consume/basic_get - delivery_info = None - def __init__(self, body='', children=None, channel=None, **properties): super(Message, self).__init__(**properties) + #: set by basic_consume/basic_get + self.delivery_info = None self.body = body self.channel = channel diff -Nru python-amqp-2.3.2/amqp/channel.py python-amqp-2.4.0/amqp/channel.py --- python-amqp-2.3.2/amqp/channel.py 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/amqp/channel.py 2019-01-13 10:08:19.000000000 +0000 @@ -13,7 +13,7 @@ from .abstract_channel import AbstractChannel from .exceptions import (ChannelError, ConsumerCancelled, RecoverableChannelError, RecoverableConnectionError, - error_for_code) + error_for_code, MessageNacked) from .five import Queue from .protocol import queue_declare_ok_t @@ -93,6 +93,7 @@ spec.method(spec.Tx.SelectOk), spec.method(spec.Confirm.SelectOk), spec.method(spec.Basic.Ack, 'Lb'), + spec.method(spec.Basic.Nack, 'Lb'), } _METHODS = {m.method_sig: m for m in _METHODS} @@ -138,6 +139,7 @@ spec.Basic.Deliver: self._on_basic_deliver, spec.Basic.Return: self._on_basic_return, spec.Basic.Ack: self._on_basic_ack, + spec.Basic.Nack: self._on_basic_nack, }) def collect(self): @@ -217,12 +219,14 @@ if is_closed: return + self.is_closing = True return self.send_method( spec.Channel.Close, argsig, (reply_code, reply_text, method_sig[0], method_sig[1]), wait=spec.Channel.CloseOk, ) finally: + self.is_closing = False self.connection = None def _on_close(self, reply_code, reply_text, class_id, method_id): @@ -272,10 +276,11 @@ is the ID of the method. """ self.send_method(spec.Channel.CloseOk) - self._do_revive() - raise error_for_code( - reply_code, reply_text, (class_id, method_id), ChannelError, - ) + if not self.connection.is_closing: + self._do_revive() + raise error_for_code( + reply_code, reply_text, (class_id, method_id), ChannelError, + ) def _on_close_ok(self): """Confirm a channel close. @@ -1135,8 +1140,8 @@ True. Returns a tuple containing 3 items: - the name of the queue (essential for automatically-named queues) - message count + the name of the queue (essential for automatically-named queues), + message count and consumer count """ self.send_method( @@ -1215,6 +1220,8 @@ client should not wait for a reply method. If the server could not complete the method it will raise a channel or connection exception. + + If nowait is False, returns the number of deleted messages. """ return self.send_method( spec.Queue.Delete, argsig, @@ -1275,7 +1282,7 @@ server could not complete the method it will raise a channel or connection exception. - if nowait is False, returns a message_count + If nowait is False, returns a number of purged messages. """ return self.send_method( spec.Queue.Purge, argsig, (0, queue, nowait), @@ -1557,14 +1564,26 @@ """ p = self.send_method( spec.Basic.Consume, argsig, - (0, queue, consumer_tag, no_local, no_ack, exclusive, - nowait, arguments), + ( + 0, queue, consumer_tag, no_local, no_ack, exclusive, + nowait, arguments + ), wait=None if nowait else spec.Basic.ConsumeOk, + returns_tuple=True ) - # XXX Fix this hack - if not nowait and not consumer_tag: - consumer_tag = p + if not nowait: + # send_method() returns (consumer_tag,) tuple. + # consumer_tag is returned by broker using following rules: + # * consumer_tag is not specified by client, random one + # is generated by Broker + # * consumer_tag is provided by client, the same one + # is returned by broker + consumer_tag = p[0] + elif nowait and not consumer_tag: + raise ValueError( + 'Consumer tag must be specified when nowait is True' + ) self.callbacks[consumer_tag] = callback @@ -1630,7 +1649,8 @@ reliability. Messages can get lost if a client dies before it can deliver them to the application. - Non-blocking, returns a message object, or None. + Non-blocking, returns a amqp.basic_message.Message object, + or None if queue is empty. """ ret = self.send_method( spec.Basic.Get, argsig, (0, queue, no_ack), @@ -1665,6 +1685,11 @@ configuration and distributed to any active consumers when the transaction, if any, is committed. + When channel is in confirm mode (when Connection parameter + confirm_publish is set to True), each message is confirmed. When + broker rejects published message (e.g. due internal broker + constrains), MessageNacked exception is raised. + PARAMETERS: exchange: shortstr @@ -1725,6 +1750,15 @@ if not self.connection: raise RecoverableConnectionError( 'basic_publish: connection closed') + + client_properties = self.connection.client_properties + if client_properties['capabilities']['connection.blocked']: + try: + # Check if an event was sent, such as the out of memory message + self.connection.drain_events(timeout=0) + except socket.timeout: + pass + try: with self.connection.transport.having_timeout(timeout): return self.send_method( @@ -1736,11 +1770,18 @@ basic_publish = _basic_publish def basic_publish_confirm(self, *args, **kwargs): + + def confirm_handler(method, *args): + # When RMQ nacks message we are raising MessageNacked exception + if method == spec.Basic.Nack: + raise MessageNacked() + if not self._confirm_selected: self._confirm_selected = True self.confirm_select() ret = self._basic_publish(*args, **kwargs) - self.wait(spec.Basic.Ack) + # Waiting for confirmation of message. + self.wait([spec.Basic.Ack, spec.Basic.Nack], callback=confirm_handler) return ret def basic_qos(self, prefetch_size, prefetch_count, a_global, @@ -2037,3 +2078,7 @@ def _on_basic_ack(self, delivery_tag, multiple): for callback in self.events['basic_ack']: callback(delivery_tag, multiple) + + def _on_basic_nack(self, delivery_tag, multiple): + for callback in self.events['basic_nack']: + callback(delivery_tag, multiple) diff -Nru python-amqp-2.3.2/amqp/connection.py python-amqp-2.4.0/amqp/connection.py --- python-amqp-2.3.2/amqp/connection.py 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/amqp/connection.py 2019-01-13 10:08:19.000000000 +0000 @@ -97,6 +97,10 @@ The "socket_settings" parameter is a dictionary defining tcp settings which will be applied as socket options. + + When "confirm_publish" is set to True, the channel is put to + confirm mode. In this mode, each published message is + confirmed using Publisher confirms RabbitMQ extention. """ Channel = Channel @@ -294,18 +298,23 @@ # if self.connected: return callback() if callback else None - self.transport = self.Transport( - self.host, self.connect_timeout, self.ssl, - self.read_timeout, self.write_timeout, - socket_settings=self.socket_settings, - ) - self.transport.connect() - self.on_inbound_frame = self.frame_handler_cls( - self, self.on_inbound_method) - self.frame_writer = self.frame_writer_cls(self, self.transport) - - while not self._handshake_complete: - self.drain_events(timeout=self.connect_timeout) + try: + self.transport = self.Transport( + self.host, self.connect_timeout, self.ssl, + self.read_timeout, self.write_timeout, + socket_settings=self.socket_settings, + ) + self.transport.connect() + self.on_inbound_frame = self.frame_handler_cls( + self, self.on_inbound_method) + self.frame_writer = self.frame_writer_cls(self, self.transport) + + while not self._handshake_complete: + self.drain_events(timeout=self.connect_timeout) + + except (OSError, IOError, SSLError): + self.collect() + raise def _warn_force_connect(self, attr): warnings.warn(AMQPDeprecationWarning( @@ -559,11 +568,18 @@ # already closed return - return self.send_method( - spec.Connection.Close, argsig, - (reply_code, reply_text, method_sig[0], method_sig[1]), - wait=spec.Connection.CloseOk, - ) + try: + self.is_closing = True + return self.send_method( + spec.Connection.Close, argsig, + (reply_code, reply_text, method_sig[0], method_sig[1]), + wait=spec.Connection.CloseOk, + ) + except (OSError, IOError, SSLError): + self.is_closing = False + # close connection + self.collect() + raise def _on_close(self, reply_code, reply_text, class_id, method_id): """Request a connection close. diff -Nru python-amqp-2.3.2/amqp/exceptions.py python-amqp-2.4.0/amqp/exceptions.py --- python-amqp-2.3.2/amqp/exceptions.py 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/amqp/exceptions.py 2019-01-13 10:08:19.000000000 +0000 @@ -15,7 +15,7 @@ 'ResourceLocked', 'PreconditionFailed', 'FrameError', 'FrameSyntaxError', 'InvalidCommand', 'ChannelNotOpen', 'UnexpectedFrame', 'ResourceError', 'NotAllowed', 'AMQPNotImplementedError', 'InternalError', - + 'MessageNacked', 'AMQPDeprecationWarning', ] @@ -24,6 +24,10 @@ """Warning for deprecated things.""" +class MessageNacked(Exception): + """Message was nacked by broker.""" + + @python_2_unicode_compatible class AMQPError(Exception): """Base class for all AMQP exceptions.""" @@ -45,7 +49,9 @@ def __str__(self): if self.method: return '{0.method}: ({0.reply_code}) {0.reply_text}'.format(self) - return self.reply_text or '' + return self.reply_text or '<{}: unknown error>'.format( + type(self).__name__ + ) @property def method(self): diff -Nru python-amqp-2.3.2/amqp/__init__.py python-amqp-2.4.0/amqp/__init__.py --- python-amqp-2.3.2/amqp/__init__.py 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/amqp/__init__.py 2019-01-13 13:23:46.000000000 +0000 @@ -6,7 +6,7 @@ from collections import namedtuple -__version__ = '2.3.2' +__version__ = '2.4.0' __author__ = 'Barry Pederson' __maintainer__ = 'Ask Solem' __contact__ = 'pyamqp@celeryproject.org' diff -Nru python-amqp-2.3.2/amqp/method_framing.py python-amqp-2.4.0/amqp/method_framing.py --- python-amqp-2.3.2/amqp/method_framing.py 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/amqp/method_framing.py 2019-01-13 10:08:19.000000000 +0000 @@ -7,7 +7,7 @@ from . import spec from .basic_message import Message from .exceptions import UnexpectedFrame -from .five import range +from .five import range, text_t from .platform import pack, pack_into, unpack_from from .utils import str_to_bytes @@ -71,13 +71,15 @@ elif frame_type == 3: msg = partial_messages[channel] msg.inbound_body(buf) - if msg.ready: - expected_types[channel] = 1 - partial_messages.pop(channel, None) - callback(channel, msg.frame_method, msg.frame_args, msg) + if not msg.ready: + # wait for the rest of the content-body + return False + expected_types[channel] = 1 + partial_messages.pop(channel, None) + callback(channel, msg.frame_method, msg.frame_args, msg) elif frame_type == 8: # bytes_recv already updated - pass + return False return True return on_frame @@ -85,7 +87,7 @@ def frame_writer(connection, transport, pack=pack, pack_into=pack_into, range=range, len=len, - bytes=bytes, str_to_bytes=str_to_bytes): + bytes=bytes, str_to_bytes=str_to_bytes, text_t=text_t): """Create closure that writes frames.""" write = transport.write @@ -101,8 +103,12 @@ properties = None args = str_to_bytes(args) if content: - properties = content._serialize_properties() body = content.body + if isinstance(body, text_t): + encoding = content.properties.setdefault( + 'content_encoding', 'utf-8') + body = body.encode(encoding) + properties = content._serialize_properties() bodylen = len(body) framelen = ( len(args) + @@ -135,7 +141,7 @@ framelen = len(frame) write(pack('>BHI%dsB' % framelen, 3, channel, framelen, - str_to_bytes(frame), 0xce)) + frame, 0xce)) else: # ## FAST: pack into buffer and single write @@ -160,7 +166,7 @@ if bodylen > 0: framelen = bodylen pack_into('>BHI%dsB' % framelen, buf, offset, - 3, channel, framelen, str_to_bytes(body), 0xce) + 3, channel, framelen, body, 0xce) offset += 8 + framelen write(view[:offset]) diff -Nru python-amqp-2.3.2/amqp/platform.py python-amqp-2.4.0/amqp/platform.py --- python-amqp-2.3.2/amqp/platform.py 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/amqp/platform.py 2019-01-13 10:08:19.000000000 +0000 @@ -51,6 +51,9 @@ elif sys.platform.startswith('darwin'): KNOWN_TCP_OPTS.remove('TCP_USER_TIMEOUT') +elif 'bsd' in sys.platform: + KNOWN_TCP_OPTS.remove('TCP_USER_TIMEOUT') + # According to MSDN Windows platforms support getsockopt(TCP_MAXSSEG) but not # setsockopt(TCP_MAXSEG) on IPPROTO_TCP sockets. elif sys.platform.startswith('win'): diff -Nru python-amqp-2.3.2/amqp/serialization.py python-amqp-2.4.0/amqp/serialization.py --- python-amqp-2.3.2/amqp/serialization.py 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/amqp/serialization.py 2019-01-13 10:08:19.000000000 +0000 @@ -50,6 +50,12 @@ offset += 1 val = pstr_t(buf[offset:offset + slen]) offset += slen + # 'x': Bytes Array + elif ftype == 'x': + blen, = unpack_from('>I', buf, offset) + offset += 4 + val = buf[offset:offset + blen] + offset += blen # 'b': short-short int elif ftype == 'b': val, = unpack_from('>B', buf, offset) @@ -202,6 +208,11 @@ offset += 4 val = buf[offset:offset + slen].decode('utf-8', 'surrogatepass') offset += slen + elif p == 'x': + blen, = unpack_from('>I', buf, offset) + offset += 4 + val = buf[offset:offset + blen] + offset += blen elif p == 'F': bitcount = bits = 0 tlen, = unpack_from('>I', buf, offset) @@ -252,6 +263,7 @@ long long = L shortstr = s longstr = S + byte array = x table = F array = A """ @@ -293,7 +305,7 @@ val = val.encode('utf-8', 'surrogatepass') write(pack('B', len(val))) write(val) - elif p == 'S': + elif p == 'S' or p == 'x': val = val or '' bitcount = _flushbits(bits, write) if isinstance(val, string): @@ -520,7 +532,6 @@ flags = [] sformat, svalues = [], [] props = self.properties - props.setdefault('content_encoding', 'utf-8') for key, proptype in self.PROPERTIES: val = props.get(key, None) if val is not None: diff -Nru python-amqp-2.3.2/amqp/transport.py python-amqp-2.4.0/amqp/transport.py --- python-amqp-2.3.2/amqp/transport.py 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/amqp/transport.py 2019-01-13 10:08:19.000000000 +0000 @@ -28,7 +28,7 @@ SIGNED_INT_MAX = 0x7FFFFFFF # Yes, Advanced Message Queuing Protocol Protocol is redundant -AMQP_PROTOCOL_HEADER = 'AMQP\x01\x01\x00\x09'.encode('latin_1') +AMQP_PROTOCOL_HEADER = 'AMQP\x00\x00\x09\x01'.encode('latin_1') # Match things like: [fe80::1]:5432, from RFC 2732 IPV6_LITERAL = re.compile(r'\[([\.0-9a-f:]+)\](?::(\d+))?') @@ -60,12 +60,10 @@ class _AbstractTransport(object): """Common superclass for TCP and SSL transports.""" - connected = False - def __init__(self, host, connect_timeout=None, read_timeout=None, write_timeout=None, socket_settings=None, raise_on_initial_eintr=True, **kwargs): - self.connected = True + self.connected = False self.sock = None self.raise_on_initial_eintr = raise_on_initial_eintr self._read_buffer = EMPTY_BUFFER @@ -76,10 +74,24 @@ self.socket_settings = socket_settings def connect(self): - self._connect(self.host, self.port, self.connect_timeout) - self._init_socket( - self.socket_settings, self.read_timeout, self.write_timeout, - ) + try: + # are we already connected? + if self.connected: + return + self._connect(self.host, self.port, self.connect_timeout) + self._init_socket( + self.socket_settings, self.read_timeout, self.write_timeout, + ) + # we've sent the banner; signal connect + # EINTR, EAGAIN, EWOULDBLOCK would signal that the banner + # has _not_ been sent + self.connected = True + except (OSError, IOError, SSLError): + # if not fully connected, close socket, and reraise error + if self.sock and not self.connected: + self.sock.close() + self.sock = None + raise @contextmanager def having_timeout(self, timeout): @@ -160,26 +172,21 @@ return def _init_socket(self, socket_settings, read_timeout, write_timeout): - try: - self.sock.settimeout(None) # set socket back to blocking mode - self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) - self._set_socket_options(socket_settings) - - # set socket timeouts - for timeout, interval in ((socket.SO_SNDTIMEO, write_timeout), - (socket.SO_RCVTIMEO, read_timeout)): - if interval is not None: - self.sock.setsockopt( - socket.SOL_SOCKET, timeout, - pack('ll', interval, 0), - ) - self._setup_transport() + self.sock.settimeout(None) # set socket back to blocking mode + self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + self._set_socket_options(socket_settings) + + # set socket timeouts + for timeout, interval in ((socket.SO_SNDTIMEO, write_timeout), + (socket.SO_RCVTIMEO, read_timeout)): + if interval is not None: + self.sock.setsockopt( + socket.SOL_SOCKET, timeout, + pack('ll', interval, 0), + ) + self._setup_transport() - self._write(AMQP_PROTOCOL_HEADER) - except (OSError, IOError, socket.error) as exc: - if get_errno(exc) not in _UNAVAIL: - self.connected = False - raise + self._write(AMQP_PROTOCOL_HEADER) def _get_tcp_socket_defaults(self, sock): tcp_opts = {} @@ -334,11 +341,16 @@ opts['ssl_version'] = ssl.PROTOCOL_TLS else: opts['ssl_version'] = ssl.PROTOCOL_SSLv23 + sock = ssl.wrap_socket(**opts) # Set SNI headers if supported if (server_hostname is not None) and ( - hasattr(ssl, 'HAS_SNI') and ssl.HAS_SNI): - opts['server_hostname'] = server_hostname - sock = ssl.SSLSocket(**opts) + hasattr(ssl, 'HAS_SNI') and ssl.HAS_SNI) and ( + hasattr(ssl, 'SSLContext')): + context = ssl.SSLContext(opts['ssl_version']) + context.verify_mode = cert_reqs + context.check_hostname = True + context.load_cert_chain(certfile, keyfile) + sock = context.wrap_socket(sock, server_hostname=server_hostname) return sock def _shutdown_transport(self): @@ -370,7 +382,7 @@ continue raise if not s: - raise IOError('Socket closed') + raise IOError('Server unexpectedly closed connection') rbuf += s except: # noqa self._read_buffer = rbuf @@ -423,7 +435,7 @@ continue raise if not s: - raise IOError('Socket closed') + raise IOError('Server unexpectedly closed connection') rbuf += s except: # noqa self._read_buffer = rbuf diff -Nru python-amqp-2.3.2/amqp.egg-info/dependency_links.txt python-amqp-2.4.0/amqp.egg-info/dependency_links.txt --- python-amqp-2.3.2/amqp.egg-info/dependency_links.txt 1970-01-01 00:00:00.000000000 +0000 +++ python-amqp-2.4.0/amqp.egg-info/dependency_links.txt 2019-01-13 13:25:30.000000000 +0000 @@ -0,0 +1 @@ + diff -Nru python-amqp-2.3.2/amqp.egg-info/not-zip-safe python-amqp-2.4.0/amqp.egg-info/not-zip-safe --- python-amqp-2.3.2/amqp.egg-info/not-zip-safe 1970-01-01 00:00:00.000000000 +0000 +++ python-amqp-2.4.0/amqp.egg-info/not-zip-safe 2019-01-13 13:25:30.000000000 +0000 @@ -0,0 +1 @@ + diff -Nru python-amqp-2.3.2/amqp.egg-info/PKG-INFO python-amqp-2.4.0/amqp.egg-info/PKG-INFO --- python-amqp-2.3.2/amqp.egg-info/PKG-INFO 1970-01-01 00:00:00.000000000 +0000 +++ python-amqp-2.4.0/amqp.egg-info/PKG-INFO 2019-01-13 13:25:30.000000000 +0000 @@ -0,0 +1,154 @@ +Metadata-Version: 1.2 +Name: amqp +Version: 2.4.0 +Summary: Low-level AMQP client for Python (fork of amqplib). +Home-page: http://github.com/celery/py-amqp +Author: Barry Pederson +Author-email: pyamqp@celeryproject.org +Maintainer: Ask Solem +License: BSD +Description: ===================================================================== + Python AMQP 0.9.1 client library + ===================================================================== + + |build-status| |coverage| |license| |wheel| |pyversion| |pyimp| + + :Version: 2.4.0 + :Web: https://amqp.readthedocs.io/ + :Download: https://pypi.org/project/amqp/ + :Source: http://github.com/celery/py-amqp/ + :Keywords: amqp, rabbitmq + + About + ===== + + This is a fork of amqplib_ which was originally written by Barry Pederson. + It is maintained by the Celery_ project, and used by `kombu`_ as a pure python + alternative when `librabbitmq`_ is not available. + + This library should be API compatible with `librabbitmq`_. + + .. _amqplib: https://pypi.org/project/amqplib/ + .. _Celery: http://celeryproject.org/ + .. _kombu: https://kombu.readthedocs.io/ + .. _librabbitmq: https://pypi.org/project/librabbitmq/ + + Differences from `amqplib`_ + =========================== + + - Supports draining events from multiple channels (``Connection.drain_events``) + - Support for timeouts + - Channels are restored after channel error, instead of having to close the + connection. + - Support for heartbeats + + - ``Connection.heartbeat_tick(rate=2)`` must called at regular intervals + (half of the heartbeat value if rate is 2). + - Or some other scheme by using ``Connection.send_heartbeat``. + - Supports RabbitMQ extensions: + - Consumer Cancel Notifications + - by default a cancel results in ``ChannelError`` being raised + - but not if a ``on_cancel`` callback is passed to ``basic_consume``. + - Publisher confirms + - ``Channel.confirm_select()`` enables publisher confirms. + - ``Channel.events['basic_ack'].append(my_callback)`` adds a callback + to be called when a message is confirmed. This callback is then + called with the signature ``(delivery_tag, multiple)``. + - Exchange-to-exchange bindings: ``exchange_bind`` / ``exchange_unbind``. + - ``Channel.confirm_select()`` enables publisher confirms. + - ``Channel.events['basic_ack'].append(my_callback)`` adds a callback + to be called when a message is confirmed. This callback is then + called with the signature ``(delivery_tag, multiple)``. + - Authentication Failure Notifications + Instead of just closing the connection abruptly on invalid + credentials, py-amqp will raise an ``AccessRefused`` error + when connected to rabbitmq-server 3.2.0 or greater. + - Support for ``basic_return`` + - Uses AMQP 0-9-1 instead of 0-8. + - ``Channel.access_request`` and ``ticket`` arguments to methods + **removed**. + - Supports the ``arguments`` argument to ``basic_consume``. + - ``internal`` argument to ``exchange_declare`` removed. + - ``auto_delete`` argument to ``exchange_declare`` deprecated + - ``insist`` argument to ``Connection`` removed. + - ``Channel.alerts`` has been removed. + - Support for ``Channel.basic_recover_async``. + - ``Channel.basic_recover`` deprecated. + - Exceptions renamed to have idiomatic names: + - ``AMQPException`` -> ``AMQPError`` + - ``AMQPConnectionException`` -> ConnectionError`` + - ``AMQPChannelException`` -> ChannelError`` + - ``Connection.known_hosts`` removed. + - ``Connection`` no longer supports redirects. + - ``exchange`` argument to ``queue_bind`` can now be empty + to use the "default exchange". + - Adds ``Connection.is_alive`` that tries to detect + whether the connection can still be used. + - Adds ``Connection.connection_errors`` and ``.channel_errors``, + a list of recoverable errors. + - Exposes the underlying socket as ``Connection.sock``. + - Adds ``Channel.no_ack_consumers`` to keep track of consumer tags + that set the no_ack flag. + - Slightly better at error recovery + + Further + ======= + + - Differences between AMQP 0.8 and 0.9.1 + + http://www.rabbitmq.com/amqp-0-8-to-0-9-1.html + + - AMQP 0.9.1 Quick Reference + + http://www.rabbitmq.com/amqp-0-9-1-quickref.html + + - RabbitMQ Extensions + + http://www.rabbitmq.com/extensions.html + + - For more information about AMQP, visit + + http://www.amqp.org + + - For other Python client libraries see: + + http://www.rabbitmq.com/devtools.html#python-dev + + .. |build-status| image:: https://secure.travis-ci.org/celery/py-amqp.png?branch=master + :alt: Build status + :target: https://travis-ci.org/celery/py-amqp + + .. |coverage| image:: https://codecov.io/github/celery/py-amqp/coverage.svg?branch=master + :target: https://codecov.io/github/celery/py-amqp?branch=master + + .. |license| image:: https://img.shields.io/pypi/l/amqp.svg + :alt: BSD License + :target: https://opensource.org/licenses/BSD-3-Clause + + .. |wheel| image:: https://img.shields.io/pypi/wheel/amqp.svg + :alt: Python AMQP can be installed via wheel + :target: https://pypi.org/project/amqp/ + + .. |pyversion| image:: https://img.shields.io/pypi/pyversions/amqp.svg + :alt: Supported Python versions. + :target: https://pypi.org/project/amqp/ + + .. |pyimp| image:: https://img.shields.io/pypi/implementation/amqp.svg + :alt: Support Python implementations. + :target: https://pypi.org/project/amqp/ + + +Keywords: amqp rabbitmq cloudamqp messaging +Platform: any +Classifier: Development Status :: 5 - Production/Stable +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: License :: OSI Approved :: BSD License +Classifier: Intended Audience :: Developers +Classifier: Operating System :: OS Independent +Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* diff -Nru python-amqp-2.3.2/amqp.egg-info/requires.txt python-amqp-2.4.0/amqp.egg-info/requires.txt --- python-amqp-2.3.2/amqp.egg-info/requires.txt 1970-01-01 00:00:00.000000000 +0000 +++ python-amqp-2.4.0/amqp.egg-info/requires.txt 2019-01-13 13:25:30.000000000 +0000 @@ -0,0 +1 @@ +vine>=1.1.3 diff -Nru python-amqp-2.3.2/amqp.egg-info/SOURCES.txt python-amqp-2.4.0/amqp.egg-info/SOURCES.txt --- python-amqp-2.3.2/amqp.egg-info/SOURCES.txt 1970-01-01 00:00:00.000000000 +0000 +++ python-amqp-2.4.0/amqp.egg-info/SOURCES.txt 2019-01-13 13:25:30.000000000 +0000 @@ -0,0 +1,74 @@ +Changelog +LICENSE +MANIFEST.in +README.rst +setup.cfg +setup.py +amqp/__init__.py +amqp/abstract_channel.py +amqp/basic_message.py +amqp/channel.py +amqp/connection.py +amqp/exceptions.py +amqp/five.py +amqp/method_framing.py +amqp/platform.py +amqp/protocol.py +amqp/sasl.py +amqp/serialization.py +amqp/spec.py +amqp/transport.py +amqp/utils.py +amqp.egg-info/PKG-INFO +amqp.egg-info/SOURCES.txt +amqp.egg-info/dependency_links.txt +amqp.egg-info/not-zip-safe +amqp.egg-info/requires.txt +amqp.egg-info/top_level.txt +docs/Makefile +docs/changelog.rst +docs/conf.py +docs/index.rst +docs/make.bat +docs/_static/.keep +docs/_templates/sidebardonations.html +docs/images/celery_128.png +docs/images/favicon.ico +docs/includes/introduction.txt +docs/reference/amqp.abstract_channel.rst +docs/reference/amqp.basic_message.rst +docs/reference/amqp.channel.rst +docs/reference/amqp.connection.rst +docs/reference/amqp.exceptions.rst +docs/reference/amqp.five.rst +docs/reference/amqp.method_framing.rst +docs/reference/amqp.platform.rst +docs/reference/amqp.protocol.rst +docs/reference/amqp.sasl.rst +docs/reference/amqp.serialization.rst +docs/reference/amqp.spec.rst +docs/reference/amqp.transport.rst +docs/reference/amqp.utils.rst +docs/reference/index.rst +docs/templates/readme.txt +extra/update_comments_from_spec.py +requirements/default.txt +requirements/docs.txt +requirements/pkgutils.txt +requirements/test-ci.txt +requirements/test.txt +t/__init__.py +t/integration/__init__.py +t/integration/test_integration.py +t/unit/__init__.py +t/unit/test_abstract_channel.py +t/unit/test_basic_message.py +t/unit/test_channel.py +t/unit/test_connection.py +t/unit/test_exceptions.py +t/unit/test_method_framing.py +t/unit/test_platform.py +t/unit/test_sasl.py +t/unit/test_serialization.py +t/unit/test_transport.py +t/unit/test_utils.py \ No newline at end of file diff -Nru python-amqp-2.3.2/amqp.egg-info/top_level.txt python-amqp-2.4.0/amqp.egg-info/top_level.txt --- python-amqp-2.3.2/amqp.egg-info/top_level.txt 1970-01-01 00:00:00.000000000 +0000 +++ python-amqp-2.4.0/amqp.egg-info/top_level.txt 2019-01-13 13:25:30.000000000 +0000 @@ -0,0 +1 @@ +amqp diff -Nru python-amqp-2.3.2/appveyor.yml python-amqp-2.4.0/appveyor.yml --- python-amqp-2.3.2/appveyor.yml 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/appveyor.yml 1970-01-01 00:00:00.000000000 +0000 @@ -1,70 +0,0 @@ -environment: - - global: - # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the - # /E:ON and /V:ON options are not enabled in the batch script intepreter - # See: http://stackoverflow.com/a/13751649/163740 - WITH_COMPILER: "cmd /E:ON /V:ON /C .\\extra\\appveyor\\run_with_compiler.cmd" - - matrix: - - # Pre-installed Python versions, which Appveyor may upgrade to - # a later point release. - # See: http://www.appveyor.com/docs/installed-software#python - - - PYTHON: "C:\\Python27" - PYTHON_VERSION: "2.7.x" - PYTHON_ARCH: "32" - - - PYTHON: "C:\\Python34" - PYTHON_VERSION: "3.4.x" - PYTHON_ARCH: "32" - - - PYTHON: "C:\\Python35" - PYTHON_VERSION: "3.5.x" - PYTHON_ARCH: "32" - - - PYTHON: "C:\\Python36" - PYTHON_VERSION: "3.6.x" - PYTHON_ARCH: "32" - - - PYTHON: "C:\\Python27-x64" - PYTHON_VERSION: "2.7.x" - PYTHON_ARCH: "64" - WINDOWS_SDK_VERSION: "v7.0" - - - PYTHON: "C:\\Python34-x64" - PYTHON_VERSION: "3.4.x" - PYTHON_ARCH: "64" - WINDOWS_SDK_VERSION: "v7.1" - - - PYTHON: "C:\\Python35-x64" - PYTHON_VERSION: "3.5.x" - PYTHON_ARCH: "64" - WINDOWS_SDK_VERSION: "v7.1" - - - PYTHON: "C:\\Python36-x64" - PYTHON_VERSION: "3.6.x" - PYTHON_ARCH: "64" - WINDOWS_SDK_VERSION: "v7.1" - -init: - - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%" - -install: - - "powershell extra\\appveyor\\install.ps1" - - "%PYTHON%/Scripts/pip.exe install -U setuptools" - -build: off - -test_script: - - "%WITH_COMPILER% %PYTHON%/python setup.py test" - -after_test: - - "%WITH_COMPILER% %PYTHON%/python setup.py bdist_wheel" - -artifacts: - - path: dist\* - -#on_success: -# - TODO: upload the content of dist/*.whl to a public wheelhouse diff -Nru python-amqp-2.3.2/AUTHORS python-amqp-2.4.0/AUTHORS --- python-amqp-2.3.2/AUTHORS 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/AUTHORS 1970-01-01 00:00:00.000000000 +0000 @@ -1,26 +0,0 @@ -## This file only lists authors since the fork of the original amqplib -## library, written by Barry Pederson. - -Ask Solem -Andrew Grangaard -Rumyana Neykova -Adam Wentz -Adrien Guinet -Tommie Gannert -Dong Weiming -Dominik Fässler -Dustin J. Mitchell -Ionel Cristian Mărieș -Craig Jellick -Yury Selivanov -Artyom Koval -Rongze Zhu -Vic Kumar -Jared Lewis -Federico Ficarelli -Bas ten Berge -Quentin Pradet -ChangBo Guo(gcb) -Alan Justino -Jelte Fennema -Jon Dufresne diff -Nru python-amqp-2.3.2/.bumpversion.cfg python-amqp-2.4.0/.bumpversion.cfg --- python-amqp-2.3.2/.bumpversion.cfg 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/.bumpversion.cfg 1970-01-01 00:00:00.000000000 +0000 @@ -1,15 +0,0 @@ -[bumpversion] -current_version = 2.3.2 -commit = True -tag = True -parse = (?P\d+)\.(?P\d+)\.(?P\d+)(?P[a-z]+)? -serialize = - {major}.{minor}.{patch}{releaselevel} - {major}.{minor}.{patch} - -[bumpversion:file:amqp/__init__.py] - -[bumpversion:file:docs/includes/introduction.txt] - -[bumpversion:file:README.rst] - diff -Nru python-amqp-2.3.2/Changelog python-amqp-2.4.0/Changelog --- python-amqp-2.3.2/Changelog 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/Changelog 2019-01-13 13:17:13.000000000 +0000 @@ -5,6 +5,107 @@ The previous amqplib changelog is here: http://code.google.com/p/py-amqplib/source/browse/CHANGES +.. _version-2.4.0: + +2.4.0 +===== +:release-date: 2018-13-01 1:00 P.M UTC+2 +:release-by: Omer Katz + +- Fix inconsistent frame_handler return value. + + The function returned by frame_handler is meant to return True + once the complete message is received and the callback is called, + False otherwise. + + This fixes the return value for messages with a body split across + multiple frames, and heartbeat frames. + + Fix contributed by **:github_user:`evanunderscore`** + +- Don't default content_encoding to utf-8 for bytes. + + This is not an acceptable default as the content may not be + valid utf-8, and even if it is, the producer likely does not + expect the message to be decoded by the consumer. + + Fix contributed by **:github_user:`evanunderscore`** + +- Fix encoding of messages with multibyte characters. + + Body length was previously calculated using string length, + which may be less than the length of the encoded body when + it contains multibyte sequences. This caused the body of + the frame to be truncated. + + Fix contributed by **:github_user:`evanunderscore`** + +- Respect content_encoding when encoding messages. + + Previously the content_encoding was ignored and messages + were always encoded as utf-8. This caused messages to be + incorrectly decoded if content_encoding is properly respected + when decoding. + + Fix contributed by **:github_user:`evanunderscore`** + +- Fix AMQP protocol header for AMQP 0-9-1. + + Previously it was set to a different value for unknown reasons. + + Fix contributed by **Carl Hörberg** + +- Add support for Python 3.7. + + Change direct SSLSocket instantiation with wrap_socket. + Added Python 3.7 to CI. + + Fix contributed by **Omer Katz** and **:github_user:`avborhanian`** + +- Add support for field type "x" (byte array). + + Fix contributed by **Davis Kirkendall** + +- If there is an exception raised on Connection.connect or Connection.close, + ensure that the underlying transport socket is closed. + + Adjust exception message on connection errors as well. + + Fix contributed by **:github_user:`tomc797`** + +- TCP_USER_TIMEOUT has to be excluded from KNOWN_TCP_OPTS in BSD platforms. + + Fix contributed by **George Tantiras** + +- Handle negative acknowledgments. + + Fix contributed by **Matus Valo** + +- Added integration tests. + + Fix contributed by **Matus Valo** + +- Fix basic_consume() with no consumer_tag provided. + + Fix contributed by **Matus Valo** + +- Improved empty AMQPError string representation. + + Fix contributed by **Matus Valo** + +- Drain events before publish. + + This is needed to capture out of memory messages for clients that only + publish. Otherwise on_blocked is never called. + + Fix contributed by **Jelte Fennema** and **Matus Valo** + +- Don't revive channel when connection is closing. + + When connection is closing don't raise error when Channel.Close method is received. + + Fix contributed by **Matus Valo** + .. _version-2.3.2: 2.3.2 @@ -28,7 +129,7 @@ - Fix a regression that occurs when running amqp under Python 2.7. - #182 mistakingly replaced a type check with unicode to string_t which is str + #182 mistakenly replaced a type check with unicode to string_t which is str in Python 2.7. text_t should have been used instead. This is now fixed and the tests have been adjusted to ensure this never regresses again. diff -Nru python-amqp-2.3.2/.cookiecutterrc python-amqp-2.4.0/.cookiecutterrc --- python-amqp-2.3.2/.cookiecutterrc 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/.cookiecutterrc 1970-01-01 00:00:00.000000000 +0000 @@ -1,10 +0,0 @@ -default_context: - - email: 'ask@celeryproject.org' - full_name: 'Ask Solem' - github_username: 'celery' - project_name: 'amqp' - project_short_description: 'Low-level AMQP client for Python (fork of amqplib)' - project_slug: 'amqp' - version: '1.0.0' - year: '2012-16' diff -Nru python-amqp-2.3.2/.coveragerc python-amqp-2.4.0/.coveragerc --- python-amqp-2.3.2/.coveragerc 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/.coveragerc 1970-01-01 00:00:00.000000000 +0000 @@ -1,16 +0,0 @@ -[run] -branch = 1 -cover_pylib = 0 -include=*amqp/* -omit = t.* - -[report] -omit = - */python?.?/* - */site-packages/* - */pypy/* - *amqp/five.py -exclude_lines = - pragma: no cover - - for infinity diff -Nru python-amqp-2.3.2/debian/changelog python-amqp-2.4.0/debian/changelog --- python-amqp-2.3.2/debian/changelog 2018-08-19 21:48:17.000000000 +0000 +++ python-amqp-2.4.0/debian/changelog 2019-01-22 14:29:00.000000000 +0000 @@ -1,3 +1,14 @@ +python-amqp (2.4.0-1) unstable; urgency=medium + + [ Ondřej Nový ] + * Use 'python3 -m sphinx' instead of sphinx-build for building docs + + [ Thomas Goirand ] + * New upstream release, working with OpenSSL 1.1.1. + * Add 0010-remove-broken-test.patch. + + -- Thomas Goirand Tue, 22 Jan 2019 14:29:00 +0000 + python-amqp (2.3.2-1) unstable; urgency=medium [ Ondřej Nový ] diff -Nru python-amqp-2.3.2/debian/patches/0010-remove-broken-test.patch python-amqp-2.4.0/debian/patches/0010-remove-broken-test.patch --- python-amqp-2.3.2/debian/patches/0010-remove-broken-test.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-amqp-2.4.0/debian/patches/0010-remove-broken-test.patch 2019-01-22 14:29:00.000000000 +0000 @@ -0,0 +1,46 @@ +Description: Removes broken test +Author: Thomas Goirand +Forwarded: no +Last-Update: 2019-01-22 + +--- python-amqp-2.4.0.orig/t/integration/test_integration.py ++++ python-amqp-2.4.0/t/integration/test_integration.py +@@ -184,38 +184,6 @@ class test_connection: + 'guest', 'guest' + ).start(conn).decode('utf-8', 'surrogatepass') + +- # Expected responses from client +- frame_writer_mock.assert_has_calls( +- [ +- call( +- 1, 0, spec.Connection.StartOk, +- # Due Table type, we cannot compare bytestream directly +- DataComparator( +- 'FsSs', +- ( +- CLIENT_CAPABILITIES, 'AMQPLAIN', +- security_mechanism, +- 'en_US' +- ) +- ), +- None +- ), +- call( +- 1, 0, spec.Connection.TuneOk, +- dumps( +- 'BlB', +- (conn.channel_max, conn.frame_max, conn.heartbeat) +- ), +- None +- ), +- call( +- 1, 0, spec.Connection.Open, +- dumps('ssb', (conn.virtual_host, '', False)), +- None +- ) +- ] +- ) +- + def test_connection_close(self): + # Test checking closing connection + frame_writer_cls_mock = Mock() diff -Nru python-amqp-2.3.2/debian/patches/series python-amqp-2.4.0/debian/patches/series --- python-amqp-2.3.2/debian/patches/series 2018-08-19 21:48:17.000000000 +0000 +++ python-amqp-2.4.0/debian/patches/series 2019-01-22 14:29:00.000000000 +0000 @@ -1,2 +1,3 @@ 0001-Remove-PayPal-image-URLs.patch 0002-Disable-intersphinx-mapping-for-now.patch +0010-remove-broken-test.patch diff -Nru python-amqp-2.3.2/debian/rules python-amqp-2.4.0/debian/rules --- python-amqp-2.3.2/debian/rules 2018-08-19 21:48:17.000000000 +0000 +++ python-amqp-2.4.0/debian/rules 2019-01-22 14:29:00.000000000 +0000 @@ -17,7 +17,7 @@ override_dh_sphinxdoc: ifeq (,$(findstring nodoc, $(DEB_BUILD_OPTIONS))) - PYTHONPATH=. sphinx-build -D today="$(BUILD_DATE)" -b html -N docs/ $(CURDIR)/debian/python-amqp-doc/usr/share/doc/python-amqp-doc/html + PYTHONPATH=. python3 -m sphinx -D today="$(BUILD_DATE)" -b html -N docs/ $(CURDIR)/debian/python-amqp-doc/usr/share/doc/python-amqp-doc/html dh_sphinxdoc endif diff -Nru python-amqp-2.3.2/docs/includes/introduction.txt python-amqp-2.4.0/docs/includes/introduction.txt --- python-amqp-2.3.2/docs/includes/introduction.txt 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/docs/includes/introduction.txt 2019-01-13 13:23:46.000000000 +0000 @@ -1,4 +1,4 @@ -:Version: 2.3.2 +:Version: 2.4.0 :Web: https://amqp.readthedocs.io/ :Download: https://pypi.org/project/amqp/ :Source: http://github.com/celery/py-amqp/ diff -Nru python-amqp-2.3.2/.editorconfig python-amqp-2.4.0/.editorconfig --- python-amqp-2.3.2/.editorconfig 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/.editorconfig 1970-01-01 00:00:00.000000000 +0000 @@ -1,14 +0,0 @@ -# http://editorconfig.org - -root = true - -[*] -indent_style = space -indent_size = 4 -trim_trailing_whitespace = true -insert_final_newline = true -charset = utf-8 -end_of_line = lf - -[Makefile] -indent_style = tab diff -Nru python-amqp-2.3.2/extra/appveyor/install.ps1 python-amqp-2.4.0/extra/appveyor/install.ps1 --- python-amqp-2.3.2/extra/appveyor/install.ps1 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/extra/appveyor/install.ps1 1970-01-01 00:00:00.000000000 +0000 @@ -1,85 +0,0 @@ -# Sample script to install Python and pip under Windows -# Authors: Olivier Grisel and Kyle Kastner -# License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ - -$BASE_URL = "https://www.python.org/ftp/python/" -$GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" -$GET_PIP_PATH = "C:\get-pip.py" - - -function DownloadPython ($python_version, $platform_suffix) { - $webclient = New-Object System.Net.WebClient - $filename = "python-" + $python_version + $platform_suffix + ".msi" - $url = $BASE_URL + $python_version + "/" + $filename - - $basedir = $pwd.Path + "\" - $filepath = $basedir + $filename - if (Test-Path $filename) { - Write-Host "Reusing" $filepath - return $filepath - } - - # Download and retry up to 5 times in case of network transient errors. - Write-Host "Downloading" $filename "from" $url - $retry_attempts = 3 - for($i=0; $i -lt $retry_attempts; $i++){ - try { - $webclient.DownloadFile($url, $filepath) - break - } - Catch [Exception]{ - Start-Sleep 1 - } - } - Write-Host "File saved at" $filepath - return $filepath -} - - -function InstallPython ($python_version, $architecture, $python_home) { - Write-Host "Installing Python" $python_version "for" $architecture "bit architecture to" $python_home - if (Test-Path $python_home) { - Write-Host $python_home "already exists, skipping." - return $false - } - if ($architecture -eq "32") { - $platform_suffix = "" - } else { - $platform_suffix = ".amd64" - } - $filepath = DownloadPython $python_version $platform_suffix - Write-Host "Installing" $filepath "to" $python_home - $args = "/qn /i $filepath TARGETDIR=$python_home" - Write-Host "msiexec.exe" $args - Start-Process -FilePath "msiexec.exe" -ArgumentList $args -Wait -Passthru - Write-Host "Python $python_version ($architecture) installation complete" - return $true -} - - -function InstallPip ($python_home) { - $pip_path = $python_home + "/Scripts/pip.exe" - $python_path = $python_home + "/python.exe" - if (-not(Test-Path $pip_path)) { - Write-Host "Installing pip..." - $webclient = New-Object System.Net.WebClient - $webclient.DownloadFile($GET_PIP_URL, $GET_PIP_PATH) - Write-Host "Executing:" $python_path $GET_PIP_PATH - Start-Process -FilePath "$python_path" -ArgumentList "$GET_PIP_PATH" -Wait -Passthru - } else { - Write-Host "pip already installed." - } -} - -function InstallPackage ($python_home, $pkg) { - $pip_path = $python_home + "/Scripts/pip.exe" - & $pip_path install $pkg -} - -function main () { - InstallPython $env:PYTHON_VERSION $env:PYTHON_ARCH $env:PYTHON - InstallPip $env:PYTHON - InstallPackage $env:PYTHON wheel -} - -main diff -Nru python-amqp-2.3.2/extra/appveyor/run_with_compiler.cmd python-amqp-2.4.0/extra/appveyor/run_with_compiler.cmd --- python-amqp-2.3.2/extra/appveyor/run_with_compiler.cmd 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/extra/appveyor/run_with_compiler.cmd 1970-01-01 00:00:00.000000000 +0000 @@ -1,47 +0,0 @@ -:: To build extensions for 64 bit Python 3, we need to configure environment -:: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of: -:: MS Windows SDK for Windows 7 and .NET Framework 4 (SDK v7.1) -:: -:: To build extensions for 64 bit Python 2, we need to configure environment -:: variables to use the MSVC 2008 C++ compilers from GRMSDKX_EN_DVD.iso of: -:: MS Windows SDK for Windows 7 and .NET Framework 3.5 (SDK v7.0) -:: -:: 32 bit builds do not require specific environment configurations. -:: -:: Note: this script needs to be run with the /E:ON and /V:ON flags for the -:: cmd interpreter, at least for (SDK v7.0) -:: -:: More details at: -:: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows -:: http://stackoverflow.com/a/13751649/163740 -:: -:: Author: Olivier Grisel -:: License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ -@ECHO OFF - -SET COMMAND_TO_RUN=%* -SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows - -SET MAJOR_PYTHON_VERSION="%PYTHON_VERSION:~0,1%" -IF %MAJOR_PYTHON_VERSION% == "2" ( - SET WINDOWS_SDK_VERSION="v7.0" -) ELSE IF %MAJOR_PYTHON_VERSION% == "3" ( - SET WINDOWS_SDK_VERSION="v7.1" -) ELSE ( - ECHO Unsupported Python version: "%MAJOR_PYTHON_VERSION%" - EXIT 1 -) - -IF "%PYTHON_ARCH%"=="64" ( - ECHO Configuring Windows SDK %WINDOWS_SDK_VERSION% for Python %MAJOR_PYTHON_VERSION% on a 64 bit architecture - SET DISTUTILS_USE_SDK=1 - SET MSSdk=1 - "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% - "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release - ECHO Executing: %COMMAND_TO_RUN% - call %COMMAND_TO_RUN% || EXIT 1 -) ELSE ( - ECHO Using default MSVC build environment for 32 bit architecture - ECHO Executing: %COMMAND_TO_RUN% - call %COMMAND_TO_RUN% || EXIT 1 -) diff -Nru python-amqp-2.3.2/.gitignore python-amqp-2.4.0/.gitignore --- python-amqp-2.3.2/.gitignore 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/.gitignore 1970-01-01 00:00:00.000000000 +0000 @@ -1,27 +0,0 @@ -.DS_Store -*.pyc -*$py.class -*~ -.*.sw* -dist/ -*.egg-info -*.egg -*.egg/ -build/ -.build/ -_build/ -pip-log.txt -.directory -erl_crash.dump -*.db -Documentation/ -.tox/ -.ropeproject/ -.project -.pydevproject -.coverage -.eggs/ -.cache/ -.pytest_cache/ -coverage.xml -htmlcov/ diff -Nru python-amqp-2.3.2/.hgtags python-amqp-2.4.0/.hgtags --- python-amqp-2.3.2/.hgtags 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/.hgtags 1970-01-01 00:00:00.000000000 +0000 @@ -1,9 +0,0 @@ -b005bd4445cae8617ded34a23f6ef104cfea37e4 0.1 -529583811275d8a14b2fc26f7172b1bc5ca1bd2b 0.2 -f784f2d5d1bb921ec4d0383b02ad999cefca1414 0.3 -7ff0cc6993704eace354580f2dc51a075c836efe 0.5 -dbf98c6e962abaabe077103b5be4297c000f58d4 0.6 -1f05ed5c599f9a89416f50414854e6e23aff61d5 0.6.1 -59dc52a3c87137df720e3d2fea4214b8363b0a67 1.0.0 -cf8638fcc5c118f76e3fab634839f34ad645e67e 1.0.1 -124bd71678e2d5c23f2989c0cea016d54414e493 1.0.2 diff -Nru python-amqp-2.3.2/Makefile python-amqp-2.4.0/Makefile --- python-amqp-2.3.2/Makefile 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/Makefile 1970-01-01 00:00:00.000000000 +0000 @@ -1,148 +0,0 @@ -PROJ=amqp -PGPIDENT="Celery Security Team" -PYTHON=python -PYTEST=py.test -GIT=git -TOX=tox -ICONV=iconv -FLAKE8=flake8 -FLAKEPLUS=flakeplus -PYDOCSTYLE=pydocstyle -SPHINX2RST=sphinx2rst - -TESTDIR=t -SPHINX_DIR=docs/ -SPHINX_BUILDDIR="${SPHINX_DIR}/_build" -README=README.rst -README_SRC="docs/templates/readme.txt" -CONTRIBUTING=CONTRIBUTING.rst -CONTRIBUTING_SRC="docs/contributing.rst" -SPHINX_HTMLDIR="${SPHINX_BUILDDIR}/html" -DOCUMENTATION=Documentation -FLAKEPLUSTARGET=2.7 - -all: help - -help: - @echo "docs - Build documentation." - @echo "test-all - Run tests for all supported python versions." - @echo "distcheck ---------- - Check distribution for problems." - @echo " test - Run unittests using current python." - @echo " lint ------------ - Check codebase for problems." - @echo " apicheck - Check API reference coverage." - @echo " readmecheck - Check README.rst encoding." - @echo " contribcheck - Check CONTRIBUTING.rst encoding" - @echo " flakes -------- - Check code for syntax and style errors." - @echo " flakecheck - Run flake8 on the source code." - @echo " flakepluscheck - Run flakeplus on the source code." - @echo " pep257check - Run pep257 on the source code." - @echo "readme - Regenerate README.rst file." - @echo "contrib - Regenerate CONTRIBUTING.rst file" - @echo "clean-dist --------- - Clean all distribution build artifacts." - @echo " clean-git-force - Remove all uncomitted files." - @echo " clean ------------ - Non-destructive clean" - @echo " clean-pyc - Remove .pyc/__pycache__ files" - @echo " clean-docs - Remove documentation build artifacts." - @echo " clean-build - Remove setup artifacts." - @echo "bump - Bump patch version number." - @echo "bump-minor - Bump minor version number." - @echo "bump-major - Bump major version number." - @echo "release - Make PyPI release." - -clean: clean-docs clean-pyc clean-build - -clean-dist: clean clean-git-force - -bump: - bumpversion patch - -bump-minor: - bumpversion minor - -bump-major: - bumpversion major - -release: - python setup.py register sdist bdist_wheel upload --sign --identity="$(PGPIDENT)" - -Documentation: - (cd "$(SPHINX_DIR)"; $(MAKE) html) - mv "$(SPHINX_HTMLDIR)" $(DOCUMENTATION) - -docs: Documentation - -clean-docs: - -rm -rf "$(SPHINX_BUILDDIR)" - -lint: flakecheck apicheck readmecheck - -apicheck: - (cd "$(SPHINX_DIR)"; $(MAKE) apicheck) - -flakecheck: - $(FLAKE8) "$(PROJ)" "$(TESTDIR)" - -flakediag: - -$(MAKE) flakecheck - -flakepluscheck: - $(FLAKEPLUS) --$(FLAKEPLUSTARGET) "$(PROJ)" "$(TESTDIR)" - -flakeplusdiag: - -$(MAKE) flakepluscheck - -pep257check: - $(PYDOCSTYLE) "$(PROJ)" - -flakes: flakediag flakeplusdiag pep257check - -clean-readme: - -rm -f $(README) - -readmecheck: - $(ICONV) -f ascii -t ascii $(README) >/dev/null - -$(README): - $(SPHINX2RST) "$(README_SRC)" --ascii > $@ - -readme: clean-readme $(README) readmecheck - -clean-contrib: - -rm -f "$(CONTRIBUTING)" - -$(CONTRIBUTING): - $(SPHINX2RST) "$(CONTRIBUTING_SRC)" > $@ - -contrib: clean-contrib $(CONTRIBUTING) - -clean-pyc: - -find . -type f -a \( -name "*.pyc" -o -name "*$$py.class" \) | xargs rm - -find . -type d -name "__pycache__" | xargs rm -r - -removepyc: clean-pyc - -clean-build: - rm -rf build/ dist/ .eggs/ *.egg-info/ .tox/ .coverage cover/ - -clean-git: - $(GIT) clean -xdn - -clean-git-force: - $(GIT) clean -xdf - -test-all: clean-pyc - $(TOX) - -test: - $(PYTHON) setup.py test - -cov: - (cd $(TESTDIR); $(PYTEST) -xv --cov="$(PROJ)" --cov-report=html) - mv $(TESTDIR)/htmlcov . - -build: - $(PYTHON) setup.py sdist bdist_wheel - -distcheck: lint test clean - -dist: readme contrib clean-dist build diff -Nru python-amqp-2.3.2/PKG-INFO python-amqp-2.4.0/PKG-INFO --- python-amqp-2.3.2/PKG-INFO 1970-01-01 00:00:00.000000000 +0000 +++ python-amqp-2.4.0/PKG-INFO 2019-01-13 13:25:30.000000000 +0000 @@ -0,0 +1,154 @@ +Metadata-Version: 1.2 +Name: amqp +Version: 2.4.0 +Summary: Low-level AMQP client for Python (fork of amqplib). +Home-page: http://github.com/celery/py-amqp +Author: Barry Pederson +Author-email: pyamqp@celeryproject.org +Maintainer: Ask Solem +License: BSD +Description: ===================================================================== + Python AMQP 0.9.1 client library + ===================================================================== + + |build-status| |coverage| |license| |wheel| |pyversion| |pyimp| + + :Version: 2.4.0 + :Web: https://amqp.readthedocs.io/ + :Download: https://pypi.org/project/amqp/ + :Source: http://github.com/celery/py-amqp/ + :Keywords: amqp, rabbitmq + + About + ===== + + This is a fork of amqplib_ which was originally written by Barry Pederson. + It is maintained by the Celery_ project, and used by `kombu`_ as a pure python + alternative when `librabbitmq`_ is not available. + + This library should be API compatible with `librabbitmq`_. + + .. _amqplib: https://pypi.org/project/amqplib/ + .. _Celery: http://celeryproject.org/ + .. _kombu: https://kombu.readthedocs.io/ + .. _librabbitmq: https://pypi.org/project/librabbitmq/ + + Differences from `amqplib`_ + =========================== + + - Supports draining events from multiple channels (``Connection.drain_events``) + - Support for timeouts + - Channels are restored after channel error, instead of having to close the + connection. + - Support for heartbeats + + - ``Connection.heartbeat_tick(rate=2)`` must called at regular intervals + (half of the heartbeat value if rate is 2). + - Or some other scheme by using ``Connection.send_heartbeat``. + - Supports RabbitMQ extensions: + - Consumer Cancel Notifications + - by default a cancel results in ``ChannelError`` being raised + - but not if a ``on_cancel`` callback is passed to ``basic_consume``. + - Publisher confirms + - ``Channel.confirm_select()`` enables publisher confirms. + - ``Channel.events['basic_ack'].append(my_callback)`` adds a callback + to be called when a message is confirmed. This callback is then + called with the signature ``(delivery_tag, multiple)``. + - Exchange-to-exchange bindings: ``exchange_bind`` / ``exchange_unbind``. + - ``Channel.confirm_select()`` enables publisher confirms. + - ``Channel.events['basic_ack'].append(my_callback)`` adds a callback + to be called when a message is confirmed. This callback is then + called with the signature ``(delivery_tag, multiple)``. + - Authentication Failure Notifications + Instead of just closing the connection abruptly on invalid + credentials, py-amqp will raise an ``AccessRefused`` error + when connected to rabbitmq-server 3.2.0 or greater. + - Support for ``basic_return`` + - Uses AMQP 0-9-1 instead of 0-8. + - ``Channel.access_request`` and ``ticket`` arguments to methods + **removed**. + - Supports the ``arguments`` argument to ``basic_consume``. + - ``internal`` argument to ``exchange_declare`` removed. + - ``auto_delete`` argument to ``exchange_declare`` deprecated + - ``insist`` argument to ``Connection`` removed. + - ``Channel.alerts`` has been removed. + - Support for ``Channel.basic_recover_async``. + - ``Channel.basic_recover`` deprecated. + - Exceptions renamed to have idiomatic names: + - ``AMQPException`` -> ``AMQPError`` + - ``AMQPConnectionException`` -> ConnectionError`` + - ``AMQPChannelException`` -> ChannelError`` + - ``Connection.known_hosts`` removed. + - ``Connection`` no longer supports redirects. + - ``exchange`` argument to ``queue_bind`` can now be empty + to use the "default exchange". + - Adds ``Connection.is_alive`` that tries to detect + whether the connection can still be used. + - Adds ``Connection.connection_errors`` and ``.channel_errors``, + a list of recoverable errors. + - Exposes the underlying socket as ``Connection.sock``. + - Adds ``Channel.no_ack_consumers`` to keep track of consumer tags + that set the no_ack flag. + - Slightly better at error recovery + + Further + ======= + + - Differences between AMQP 0.8 and 0.9.1 + + http://www.rabbitmq.com/amqp-0-8-to-0-9-1.html + + - AMQP 0.9.1 Quick Reference + + http://www.rabbitmq.com/amqp-0-9-1-quickref.html + + - RabbitMQ Extensions + + http://www.rabbitmq.com/extensions.html + + - For more information about AMQP, visit + + http://www.amqp.org + + - For other Python client libraries see: + + http://www.rabbitmq.com/devtools.html#python-dev + + .. |build-status| image:: https://secure.travis-ci.org/celery/py-amqp.png?branch=master + :alt: Build status + :target: https://travis-ci.org/celery/py-amqp + + .. |coverage| image:: https://codecov.io/github/celery/py-amqp/coverage.svg?branch=master + :target: https://codecov.io/github/celery/py-amqp?branch=master + + .. |license| image:: https://img.shields.io/pypi/l/amqp.svg + :alt: BSD License + :target: https://opensource.org/licenses/BSD-3-Clause + + .. |wheel| image:: https://img.shields.io/pypi/wheel/amqp.svg + :alt: Python AMQP can be installed via wheel + :target: https://pypi.org/project/amqp/ + + .. |pyversion| image:: https://img.shields.io/pypi/pyversions/amqp.svg + :alt: Supported Python versions. + :target: https://pypi.org/project/amqp/ + + .. |pyimp| image:: https://img.shields.io/pypi/implementation/amqp.svg + :alt: Support Python implementations. + :target: https://pypi.org/project/amqp/ + + +Keywords: amqp rabbitmq cloudamqp messaging +Platform: any +Classifier: Development Status :: 5 - Production/Stable +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: License :: OSI Approved :: BSD License +Classifier: Intended Audience :: Developers +Classifier: Operating System :: OS Independent +Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* diff -Nru python-amqp-2.3.2/README.rst python-amqp-2.4.0/README.rst --- python-amqp-2.3.2/README.rst 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/README.rst 2019-01-13 13:23:46.000000000 +0000 @@ -4,7 +4,7 @@ |build-status| |coverage| |license| |wheel| |pyversion| |pyimp| -:Version: 2.3.2 +:Version: 2.4.0 :Web: https://amqp.readthedocs.io/ :Download: https://pypi.org/project/amqp/ :Source: http://github.com/celery/py-amqp/ diff -Nru python-amqp-2.3.2/requirements/pkgutils.txt python-amqp-2.4.0/requirements/pkgutils.txt --- python-amqp-2.3.2/requirements/pkgutils.txt 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/requirements/pkgutils.txt 2019-01-13 10:08:19.000000000 +0000 @@ -1,6 +1,6 @@ setuptools>=20.6.7 wheel>=0.29.0 -flake8>=2.5.4 +flake8==3.5.0 flakeplus>=1.1 tox>=2.3.1 sphinx2rst>=1.0 diff -Nru python-amqp-2.3.2/setup.cfg python-amqp-2.4.0/setup.cfg --- python-amqp-2.3.2/setup.cfg 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/setup.cfg 2019-01-13 13:25:30.000000000 +0000 @@ -1,13 +1,11 @@ [tool:pytest] -testpaths = t/unit/ +testpaths = t/unit/ t/integration/ python_classes = test_* [bdist_rpm] requires = vine [flake8] -# classes can be lowercase, arguments and variables can be uppercase -# whenever it makes the code more readable. ignore = N806, N802, N801, N803 [pep257] @@ -18,3 +16,8 @@ [metadata] license_file = LICENSE + +[egg_info] +tag_build = +tag_date = 0 + diff -Nru python-amqp-2.3.2/t/integration/test_integration.py python-amqp-2.4.0/t/integration/test_integration.py --- python-amqp-2.3.2/t/integration/test_integration.py 1970-01-01 00:00:00.000000000 +0000 +++ python-amqp-2.4.0/t/integration/test_integration.py 2019-01-13 10:08:19.000000000 +0000 @@ -0,0 +1,850 @@ +from __future__ import absolute_import, unicode_literals + +import socket +import pytest +from case import patch, call, Mock, ANY +from amqp import spec, Connection, Channel, sasl, Message +from amqp.platform import pack +from amqp.exceptions import ConnectionError, \ + InvalidCommand, AccessRefused, PreconditionFailed, NotFound, ResourceLocked +from amqp.serialization import dumps, loads +from amqp.protocol import queue_declare_ok_t + +connection_testdata = ( + (spec.Connection.Blocked, '_on_blocked'), + (spec.Connection.Unblocked, '_on_unblocked'), + (spec.Connection.Secure, '_on_secure'), + (spec.Connection.CloseOk, '_on_close_ok'), +) + +channel_testdata = ( + (spec.Basic.Ack, '_on_basic_ack'), + (spec.Basic.Nack, '_on_basic_nack'), + (spec.Basic.CancelOk, '_on_basic_cancel_ok'), +) + +exchange_declare_error_testdata = ( + ( + 503, "COMMAND_INVALID - " + "unknown exchange type 'exchange-type'", + InvalidCommand + ), + ( + 403, "ACCESS_REFUSED - " + "exchange name 'amq.foo' contains reserved prefix 'amq.*'", + AccessRefused + ), + ( + 406, "PRECONDITION_FAILED - " + "inequivalent arg 'type' for exchange 'foo' in vhost '/':" + "received 'direct' but current is 'fanout'", + PreconditionFailed + ), +) + +queue_declare_error_testdata = ( + ( + 403, "ACCESS_REFUSED - " + "queue name 'amq.foo' contains reserved prefix 'amq.*", + AccessRefused + ), + ( + 404, "NOT_FOUND - " + "no queue 'foo' in vhost '/'", + NotFound + ), + ( + 405, "RESOURCE_LOCKED - " + "cannot obtain exclusive access to locked queue 'foo' in vhost '/'", + ResourceLocked + ), +) + +CLIENT_CAPABILITIES = { + 'product': 'py-amqp', + 'product_version': '2.3.2', + 'capabilities': { + 'consumer_cancel_notify': True, + 'connection.blocked': True, + 'authentication_failure_close': True + } +} + +SERVER_CAPABILITIES = { + 'capabilities': { + 'publisher_confirms': True, + 'exchange_exchange_bindings': True, + 'basic.nack': True, + 'consumer_cancel_notify': True, + 'connection.blocked': True, + 'consumer_priorities': True, + 'authentication_failure_close': True, + 'per_consumer_qos': True, + 'direct_reply_to': True + }, + 'cluster_name': 'rabbit@broker.com', + 'copyright': 'Copyright (C) 2007-2018 Pivotal Software, Inc.', + 'information': 'Licensed under the MPL. See http://www.rabbitmq.com/', + 'platform': 'Erlang/OTP 20.3.8.9', + 'product': 'RabbitMQ', + 'version': '3.7.8' +} + + +def build_frame_type_1(method, channel=0, args=b'', arg_format=None): + if len(args) > 0: + args = dumps(arg_format, args) + else: + args = b'' + frame = (b''.join([pack('>HH', *method), args])) + return 1, channel, frame + + +def build_frame_type_2(body_len, channel, properties): + frame = (b''.join( + [pack('>HxxQ', spec.Basic.CLASS_ID, body_len), properties]) + ) + return 2, channel, frame + + +def build_frame_type_3(channel, body): + return 3, channel, body + + +class DataComparator(object): + # Comparator used for asserting serialized data. It can be used + # in cases when direct comparision of bytestream cannot be used + # (mainly cases of Table type where order of items can vary) + def __init__(self, argsig, items): + self.argsig = argsig + self.items = items + + def __eq__(self, other): + values, offset = loads(self.argsig, other) + return tuple(values) == tuple(self.items) + + +def handshake(conn, transport_mock): + # Helper function simulating connection handshake with server + transport_mock().read_frame.side_effect = [ + build_frame_type_1( + spec.Connection.Start, channel=0, + args=( + 0, 9, SERVER_CAPABILITIES, 'AMQPLAIN PLAIN', 'en_US' + ), + arg_format='ooFSS' + ), + build_frame_type_1( + spec.Connection.Tune, channel=0, + args=(2047, 131072, 60), arg_format='BlB' + ), + build_frame_type_1( + spec.Connection.OpenOk, channel=0 + ) + ] + conn.connect() + transport_mock().read_frame.side_effect = None + + +def create_channel(channel_id, conn, transport_mock): + transport_mock().read_frame.side_effect = [ + build_frame_type_1( + spec.Channel.OpenOk, + channel=channel_id, + args=(1, False), + arg_format='Lb' + ) + ] + ch = conn.channel(channel_id=channel_id) + transport_mock().read_frame.side_effect = None + return ch + + +class test_connection: + # Integration tests. Tests verify the correctness of communication between + # library and broker. + # * tests mocks broker responses mocking return values of + # amqp.transport.Transport.read_frame() method + # * tests asserts expected library responses to broker via calls of + # amqp.method_framing.frame_writer() function + + def test_connect(self): + # Test checking connection handshake + frame_writer_cls_mock = Mock() + on_open_mock = Mock() + frame_writer_mock = frame_writer_cls_mock() + conn = Connection( + frame_writer=frame_writer_cls_mock, on_open=on_open_mock + ) + + with patch.object(conn, 'Transport') as transport_mock: + handshake(conn, transport_mock) + on_open_mock.assert_called_once_with(conn) + security_mechanism = sasl.AMQPLAIN( + 'guest', 'guest' + ).start(conn).decode('utf-8', 'surrogatepass') + + # Expected responses from client + frame_writer_mock.assert_has_calls( + [ + call( + 1, 0, spec.Connection.StartOk, + # Due Table type, we cannot compare bytestream directly + DataComparator( + 'FsSs', + ( + CLIENT_CAPABILITIES, 'AMQPLAIN', + security_mechanism, + 'en_US' + ) + ), + None + ), + call( + 1, 0, spec.Connection.TuneOk, + dumps( + 'BlB', + (conn.channel_max, conn.frame_max, conn.heartbeat) + ), + None + ), + call( + 1, 0, spec.Connection.Open, + dumps('ssb', (conn.virtual_host, '', False)), + None + ) + ] + ) + + def test_connection_close(self): + # Test checking closing connection + frame_writer_cls_mock = Mock() + frame_writer_mock = frame_writer_cls_mock() + conn = Connection(frame_writer=frame_writer_cls_mock) + with patch.object(conn, 'Transport') as transport_mock: + handshake(conn, transport_mock) + frame_writer_mock.reset_mock() + # Inject CloseOk response from broker + transport_mock().read_frame.return_value = build_frame_type_1( + spec.Connection.CloseOk + ) + t = conn.transport + conn.close() + frame_writer_mock.assert_called_once_with( + 1, 0, spec.Connection.Close, dumps('BsBB', (0, '', 0, 0)), None + ) + t.close.assert_called_once_with() + + def test_connection_closed_by_broker(self): + # Test that library response correctly CloseOk when + # close method is received and _on_close_ok() method is called. + frame_writer_cls_mock = Mock() + frame_writer_mock = frame_writer_cls_mock() + with patch.object(Connection, '_on_close_ok') as callback_mock: + conn = Connection(frame_writer=frame_writer_cls_mock) + with patch.object(conn, 'Transport') as transport_mock: + handshake(conn, transport_mock) + frame_writer_mock.reset_mock() + # Inject Close response from broker + transport_mock().read_frame.return_value = build_frame_type_1( + spec.Connection.Close, + args=(1, False), + arg_format='Lb' + ) + with pytest.raises(ConnectionError): + conn.drain_events(0) + frame_writer_mock.assert_called_once_with( + 1, 0, spec.Connection.CloseOk, '', None + ) + callback_mock.assert_called_once_with() + + +class test_channel: + # Integration tests. Tests verify the correctness of communication between + # library and broker. + # * tests mocks broker responses mocking return values of + # amqp.transport.Transport.read_frame() method + # * tests asserts expected library responses to broker via calls of + # amqp.method_framing.frame_writer() function + + @pytest.mark.parametrize("method, callback", connection_testdata) + def test_connection_methods(self, method, callback): + # Test verifying that proper Connection callback is called when + # given method arrived from Broker. + with patch.object(Connection, callback) as callback_mock: + conn = Connection() + with patch.object(conn, 'Transport') as transport_mock: + handshake(conn, transport_mock) + # Inject desired method + transport_mock().read_frame.return_value = build_frame_type_1( + method, channel=0, args=(1, False), arg_format='Lb' + ) + conn.drain_events(0) + callback_mock.assert_called_once() + + def test_channel_open_close(self): + # Test checking opening and closing channel + frame_writer_cls_mock = Mock() + conn = Connection(frame_writer=frame_writer_cls_mock) + with patch.object(conn, 'Transport') as transport_mock: + handshake(conn, transport_mock) + + channel_id = 1 + transport_mock().read_frame.side_effect = [ + # Inject Open Handshake + build_frame_type_1( + spec.Channel.OpenOk, + channel=channel_id, + args=(1, False), + arg_format='Lb' + ), + # Inject close method + build_frame_type_1( + spec.Channel.CloseOk, + channel=channel_id + ) + ] + + frame_writer_mock = frame_writer_cls_mock() + frame_writer_mock.reset_mock() + + on_open_mock = Mock() + ch = conn.channel(channel_id=channel_id, callback=on_open_mock) + on_open_mock.assert_called_once_with(ch) + assert ch.is_open is True + + ch.close() + frame_writer_mock.assert_has_calls( + [ + call( + 1, 1, spec.Channel.Open, dumps('s', ('',)), + None + ), + call( + 1, 1, spec.Channel.Close, dumps('BsBB', (0, '', 0, 0)), + None + ) + ] + ) + assert ch.is_open is False + + def test_received_channel_Close_during_connection_close(self): + # This test verifies that library handles correctly closing channel + # during closing of connection: + # 1. User requests closing connection - client sends Connection.Close + # 2. Broker requests closing Channel - client receives Channel.Close + # 3. Broker sends Connection.CloseOk + # see GitHub issue #218 + conn = Connection() + with patch.object(conn, 'Transport') as transport_mock: + handshake(conn, transport_mock) + channel_id = 1 + create_channel(channel_id, conn, transport_mock) + # Replies sent by broker + transport_mock().read_frame.side_effect = [ + # Inject close methods + build_frame_type_1( + spec.Channel.Close, + channel=channel_id, + args=(1, False), + arg_format='Lb' + ), + build_frame_type_1( + spec.Connection.CloseOk + ) + ] + conn.close() + + @pytest.mark.parametrize("method, callback", channel_testdata) + def test_channel_methods(self, method, callback): + # Test verifying that proper Channel callback is called when + # given method arrived from Broker + with patch.object(Channel, callback) as callback_mock: + conn = Connection() + with patch.object(conn, 'Transport') as transport_mock: + handshake(conn, transport_mock) + create_channel(1, conn, transport_mock) + + # Inject desired method + transport_mock().read_frame.return_value = build_frame_type_1( + method, + channel=1, + args=(1, False), + arg_format='Lb' + ) + conn.drain_events(0) + callback_mock.assert_called_once() + + def test_basic_publish(self): + # Test verifing publishing message. + frame_writer_cls_mock = Mock() + conn = Connection(frame_writer=frame_writer_cls_mock) + with patch.object(conn, 'Transport') as transport_mock: + handshake(conn, transport_mock) + ch = create_channel(1, conn, transport_mock) + + frame_writer_mock = frame_writer_cls_mock() + frame_writer_mock.reset_mock() + msg = Message('test') + # we need to mock socket timeout due checks in + # Channel._basic_publish + transport_mock().read_frame.side_effect = socket.timeout + ch.basic_publish(msg) + frame_writer_mock.assert_called_once_with( + 1, 1, spec.Basic.Publish, + dumps('Bssbb', (0, '', '', False, False)), msg + ) + + def test_consume_no_consumer_tag(self): + # Test verifing starting consuming without specified consumer_tag + callback_mock = Mock() + frame_writer_cls_mock = Mock() + conn = Connection(frame_writer=frame_writer_cls_mock) + consumer_tag = 'amq.ctag-PCmzXGkhCw_v0Zq7jXyvkg' + with patch.object(conn, 'Transport') as transport_mock: + handshake(conn, transport_mock) + ch = create_channel(1, conn, transport_mock) + + # Inject ConsumeOk response from Broker + transport_mock().read_frame.return_value = build_frame_type_1( + spec.Basic.ConsumeOk, + channel=1, + args=(consumer_tag,), + arg_format='s' + ) + frame_writer_mock = frame_writer_cls_mock() + frame_writer_mock.reset_mock() + ch.basic_consume('my_queue', callback=callback_mock) + frame_writer_mock.assert_called_once_with( + 1, 1, spec.Basic.Consume, + dumps( + 'BssbbbbF', + (0, 'my_queue', '', False, False, False, False, None) + ), + None + ) + assert ch.callbacks[consumer_tag] == callback_mock + + def test_consume_with_consumer_tag(self): + # Test verifing starting consuming with specified consumer_tag + callback_mock = Mock() + frame_writer_cls_mock = Mock() + conn = Connection(frame_writer=frame_writer_cls_mock) + with patch.object(conn, 'Transport') as transport_mock: + handshake(conn, transport_mock) + ch = create_channel(1, conn, transport_mock) + + # Inject ConcumeOk response from Broker + transport_mock().read_frame.return_value = build_frame_type_1( + spec.Basic.ConsumeOk, + channel=1, + args=('my_tag',), + arg_format='s' + ) + frame_writer_mock = frame_writer_cls_mock() + frame_writer_mock.reset_mock() + ch.basic_consume( + 'my_queue', callback=callback_mock, consumer_tag='my_tag' + ) + frame_writer_mock.assert_called_once_with( + 1, 1, spec.Basic.Consume, + dumps( + 'BssbbbbF', + ( + 0, 'my_queue', 'my_tag', + False, False, False, False, None + ) + ), + None + ) + assert ch.callbacks['my_tag'] == callback_mock + + def test_queue_declare(self): + # Test verifying declaring queue + frame_writer_cls_mock = Mock() + conn = Connection(frame_writer=frame_writer_cls_mock) + with patch.object(conn, 'Transport') as transport_mock: + handshake(conn, transport_mock) + ch = create_channel(1, conn, transport_mock) + transport_mock().read_frame.return_value = build_frame_type_1( + spec.Queue.DeclareOk, + channel=1, + arg_format='sll', + args=('foo', 1, 2) + ) + frame_writer_mock = frame_writer_cls_mock() + frame_writer_mock.reset_mock() + ret = ch.queue_declare('foo') + assert ret == queue_declare_ok_t( + queue='foo', message_count=1, consumer_count=2 + ) + frame_writer_mock.assert_called_once_with( + 1, 1, spec.Queue.Declare, + dumps( + 'BsbbbbbF', + ( + 0, + # queue, passive, durable, exclusive, + 'foo', False, False, False, + # auto_delete, nowait, arguments + True, False, None + ) + ), + None + ) + + @pytest.mark.parametrize( + "reply_code, reply_text, exception", queue_declare_error_testdata) + def test_queue_declare_error(self, reply_code, reply_text, exception): + # Test verifying wrong declaring exchange + frame_writer_cls_mock = Mock() + conn = Connection(frame_writer=frame_writer_cls_mock) + with patch.object(conn, 'Transport') as transport_mock: + handshake(conn, transport_mock) + ch = create_channel(1, conn, transport_mock) + transport_mock().read_frame.return_value = build_frame_type_1( + spec.Connection.Close, + args=(reply_code, reply_text) + spec.Exchange.Declare, + arg_format='BsBB' + ) + frame_writer_mock = frame_writer_cls_mock() + frame_writer_mock.reset_mock() + with pytest.raises(exception) as excinfo: + ch.queue_declare('foo') + assert excinfo.value.code == reply_code + assert excinfo.value.message == reply_text + assert excinfo.value.method == 'Exchange.declare' + assert excinfo.value.method_name == 'Exchange.declare' + assert excinfo.value.method_sig == spec.Exchange.Declare + # Client is sending to broker: + # 1. Exchange Declare + # 2. Connection.CloseOk as reply to received Connecton.Close + frame_writer_calls = [ + call( + 1, 1, spec.Queue.Declare, + dumps( + 'BsbbbbbF', + ( + 0, + # queue, passive, durable, exclusive, + 'foo', False, False, False, + # auto_delete, nowait, arguments + True, False, None + ) + ), + None + ), + call( + 1, 0, spec.Connection.CloseOk, + '', + None + ), + ] + frame_writer_mock.assert_has_calls(frame_writer_calls) + + def test_queue_delete(self): + # Test verifying deleting queue + frame_writer_cls_mock = Mock() + conn = Connection(frame_writer=frame_writer_cls_mock) + with patch.object(conn, 'Transport') as transport_mock: + handshake(conn, transport_mock) + ch = create_channel(1, conn, transport_mock) + transport_mock().read_frame.return_value = build_frame_type_1( + spec.Queue.DeleteOk, + channel=1, + arg_format='l', + args=(5,) + ) + frame_writer_mock = frame_writer_cls_mock() + frame_writer_mock.reset_mock() + msg_count = ch.queue_delete('foo') + assert msg_count == 5 + frame_writer_mock.assert_called_once_with( + 1, 1, spec.Queue.Delete, + dumps( + 'Bsbbb', + # queue, if_unused, if_empty, nowait + (0, 'foo', False, False, False) + ), + None + ) + + def test_queue_purge(self): + # Test verifying purging queue + frame_writer_cls_mock = Mock() + conn = Connection(frame_writer=frame_writer_cls_mock) + with patch.object(conn, 'Transport') as transport_mock: + handshake(conn, transport_mock) + ch = create_channel(1, conn, transport_mock) + transport_mock().read_frame.return_value = build_frame_type_1( + spec.Queue.PurgeOk, + channel=1, + arg_format='l', + args=(4,) + ) + frame_writer_mock = frame_writer_cls_mock() + frame_writer_mock.reset_mock() + msg_count = ch.queue_purge('foo') + assert msg_count == 4 + frame_writer_mock.assert_called_once_with( + 1, 1, spec.Queue.Purge, + dumps( + 'Bsb', + # queue, nowait + (0, 'foo', False) + ), + None + ) + + def test_basic_deliver(self): + # Test checking delivering single message + callback_mock = Mock() + frame_writer_cls_mock = Mock() + conn = Connection(frame_writer=frame_writer_cls_mock) + consumer_tag = 'amq.ctag-PCmzXGkhCw_v0Zq7jXyvkg' + with patch.object(conn, 'Transport') as transport_mock: + handshake(conn, transport_mock) + ch = create_channel(1, conn, transport_mock) + + # Inject ConsumeOk response from Broker + transport_mock().read_frame.side_effect = [ + # Inject Consume-ok response + build_frame_type_1( + spec.Basic.ConsumeOk, + channel=1, + args=(consumer_tag,), + arg_format='s' + ), + # Inject basic-deliver response + build_frame_type_1( + spec.Basic.Deliver, + channel=1, + arg_format='sLbss', + args=( + # consumer-tag, delivery-tag, redelivered, + consumer_tag, 1, False, + # exchange-name, routing-key + 'foo_exchange', 'routing-key' + ) + ), + build_frame_type_2( + channel=1, + body_len=12, + properties=b'0\x00\x00\x00\x00\x00\x01' + ), + build_frame_type_3( + channel=1, + body=b'Hello World!' + ), + ] + frame_writer_mock = frame_writer_cls_mock() + frame_writer_mock.reset_mock() + ch.basic_consume('my_queue', callback=callback_mock) + conn.drain_events() + callback_mock.assert_called_once_with(ANY) + msg = callback_mock.call_args[0][0] + assert isinstance(msg, Message) + assert msg.body_size == 12 + assert msg.body == b'Hello World!' + assert msg.frame_method == spec.Basic.Deliver + assert msg.delivery_tag == 1 + assert msg.ready is True + assert msg.delivery_info == { + 'consumer_tag': 'amq.ctag-PCmzXGkhCw_v0Zq7jXyvkg', + 'delivery_tag': 1, + 'redelivered': False, + 'exchange': 'foo_exchange', + 'routing_key': 'routing-key' + } + assert msg.properties == { + 'application_headers': {}, 'delivery_mode': 1 + } + + def test_queue_get(self): + # Test verifying getting message from queue + frame_writer_cls_mock = Mock() + conn = Connection(frame_writer=frame_writer_cls_mock) + with patch.object(conn, 'Transport') as transport_mock: + handshake(conn, transport_mock) + ch = create_channel(1, conn, transport_mock) + transport_mock().read_frame.side_effect = [ + build_frame_type_1( + spec.Basic.GetOk, + channel=1, + arg_format='Lbssl', + args=( + # delivery_tag, redelivered, exchange_name + 1, False, 'foo_exchange', + # routing_key, message_count + 'routing_key', 1 + ) + ), + build_frame_type_2( + channel=1, + body_len=12, + properties=b'0\x00\x00\x00\x00\x00\x01' + ), + build_frame_type_3( + channel=1, + body=b'Hello World!' + ) + ] + frame_writer_mock = frame_writer_cls_mock() + frame_writer_mock.reset_mock() + msg = ch.basic_get('foo') + assert msg.body_size == 12 + assert msg.body == b'Hello World!' + assert msg.frame_method == spec.Basic.GetOk + assert msg.delivery_tag == 1 + assert msg.ready is True + assert msg.delivery_info == { + 'delivery_tag': 1, 'redelivered': False, + 'exchange': 'foo_exchange', + 'routing_key': 'routing_key', 'message_count': 1 + } + assert msg.properties == { + 'application_headers': {}, 'delivery_mode': 1 + } + frame_writer_mock.assert_called_once_with( + 1, 1, spec.Basic.Get, + dumps( + 'Bsb', + # queue, nowait + (0, 'foo', False) + ), + None + ) + + def test_queue_get_empty(self): + # Test verifying getting message from empty queue + frame_writer_cls_mock = Mock() + conn = Connection(frame_writer=frame_writer_cls_mock) + with patch.object(conn, 'Transport') as transport_mock: + handshake(conn, transport_mock) + ch = create_channel(1, conn, transport_mock) + transport_mock().read_frame.return_value = build_frame_type_1( + spec.Basic.GetEmpty, + channel=1, + arg_format='s', + args=('s') + ) + frame_writer_mock = frame_writer_cls_mock() + frame_writer_mock.reset_mock() + ret = ch.basic_get('foo') + assert ret is None + frame_writer_mock.assert_called_once_with( + 1, 1, spec.Basic.Get, + dumps( + 'Bsb', + # queue, nowait + (0, 'foo', False) + ), + None + ) + + def test_exchange_declare(self): + # Test verifying declaring exchange + frame_writer_cls_mock = Mock() + conn = Connection(frame_writer=frame_writer_cls_mock) + with patch.object(conn, 'Transport') as transport_mock: + handshake(conn, transport_mock) + ch = create_channel(1, conn, transport_mock) + transport_mock().read_frame.return_value = build_frame_type_1( + spec.Exchange.DeclareOk, + channel=1 + ) + frame_writer_mock = frame_writer_cls_mock() + frame_writer_mock.reset_mock() + ret = ch.exchange_declare('foo', 'fanout') + assert ret is None + frame_writer_mock.assert_called_once_with( + 1, 1, spec.Exchange.Declare, + dumps( + 'BssbbbbbF', + ( + 0, + # exchange, type, passive, durable, + 'foo', 'fanout', False, False, + # auto_delete, internal, nowait, arguments + True, False, False, None + ) + ), + None + ) + + @pytest.mark.parametrize( + "reply_code, reply_text, exception", exchange_declare_error_testdata) + def test_exchange_declare_error(self, reply_code, reply_text, exception): + # Test verifying wrong declaring exchange + frame_writer_cls_mock = Mock() + conn = Connection(frame_writer=frame_writer_cls_mock) + with patch.object(conn, 'Transport') as transport_mock: + handshake(conn, transport_mock) + ch = create_channel(1, conn, transport_mock) + transport_mock().read_frame.return_value = build_frame_type_1( + spec.Connection.Close, + args=(reply_code, reply_text) + spec.Exchange.Declare, + arg_format='BsBB' + ) + frame_writer_mock = frame_writer_cls_mock() + frame_writer_mock.reset_mock() + with pytest.raises(exception) as excinfo: + ch.exchange_declare('exchange', 'exchange-type') + assert excinfo.value.code == reply_code + assert excinfo.value.message == reply_text + assert excinfo.value.method == 'Exchange.declare' + assert excinfo.value.method_name == 'Exchange.declare' + assert excinfo.value.method_sig == spec.Exchange.Declare + # Client is sending to broker: + # 1. Exchange Declare + # 2. Connection.CloseOk as reply to received Connecton.Close + frame_writer_calls = [ + call( + 1, 1, spec.Exchange.Declare, + dumps( + 'BssbbbbbF', + ( + 0, + # exchange, type, passive, durable, + 'exchange', 'exchange-type', False, False, + # auto_delete, internal, nowait, arguments + True, False, False, None + ) + ), + None + ), + call( + 1, 0, spec.Connection.CloseOk, + '', + None + ), + ] + frame_writer_mock.assert_has_calls(frame_writer_calls) + + def test_exchange_delete(self): + # Test verifying declaring exchange + frame_writer_cls_mock = Mock() + conn = Connection(frame_writer=frame_writer_cls_mock) + with patch.object(conn, 'Transport') as transport_mock: + handshake(conn, transport_mock) + ch = create_channel(1, conn, transport_mock) + transport_mock().read_frame.return_value = build_frame_type_1( + spec.Exchange.DeleteOk, + channel=1 + ) + frame_writer_mock = frame_writer_cls_mock() + frame_writer_mock.reset_mock() + ret = ch.exchange_delete('foo') + assert ret == () + frame_writer_mock.assert_called_once_with( + 1, 1, spec.Exchange.Delete, + dumps( + 'Bsbb', + ( + 0, + # exchange, if-unused, no-wait + 'foo', False, False + ) + ), + None + ) diff -Nru python-amqp-2.3.2/t/unit/test_abstract_channel.py python-amqp-2.4.0/t/unit/test_abstract_channel.py --- python-amqp-2.3.2/t/unit/test_abstract_channel.py 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/t/unit/test_abstract_channel.py 2019-01-13 10:08:19.000000000 +0000 @@ -97,14 +97,14 @@ self.method.args = None p = self.c._pending[(50, 61)] = Mock(name='oneshot') self.c.dispatch_method((50, 61), 'payload', self.content) - p.assert_called_with(self.content) + p.assert_called_with((50, 61), self.content) def test_dispatch_method__one_shot_no_content(self): self.method.args = None self.method.content = None p = self.c._pending[(50, 61)] = Mock(name='oneshot') self.c.dispatch_method((50, 61), 'payload', self.content) - p.assert_called_with() + p.assert_called_with((50, 61)) assert not self.c._pending def test_dispatch_method__listeners(self): @@ -121,6 +121,6 @@ p2 = self.c._pending[(50, 61)] = Mock(name='oneshot') self.c.dispatch_method((50, 61), 'payload', self.content) p1.assert_called_with(1, 2, 3, self.content) - p2.assert_called_with(1, 2, 3, self.content) + p2.assert_called_with((50, 61), 1, 2, 3, self.content) assert not self.c._pending assert self.c._callbacks[(50, 61)] diff -Nru python-amqp-2.3.2/t/unit/test_basic_message.py python-amqp-2.4.0/t/unit/test_basic_message.py --- python-amqp-2.3.2/t/unit/test_basic_message.py 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/t/unit/test_basic_message.py 2019-01-13 10:08:19.000000000 +0000 @@ -13,7 +13,8 @@ channel=Mock(name='channel'), application_headers={'h': 'v'}, ) - m.delivery_info = {'delivery_tag': '1234'}, + m.delivery_info = {'delivery_tag': '1234'} assert m.body == 'foo' assert m.channel assert m.headers == {'h': 'v'} + assert m.delivery_tag == '1234' diff -Nru python-amqp-2.3.2/t/unit/test_channel.py python-amqp-2.4.0/t/unit/test_channel.py --- python-amqp-2.3.2/t/unit/test_channel.py 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/t/unit/test_channel.py 2019-01-13 10:08:19.000000000 +0000 @@ -1,18 +1,24 @@ from __future__ import absolute_import, unicode_literals import pytest -from case import ContextMock, Mock, patch +import socket +from case import ContextMock, Mock, patch, ANY, MagicMock from amqp import spec +from amqp.basic_message import Message +from amqp.platform import pack +from amqp.serialization import dumps from amqp.channel import Channel -from amqp.exceptions import ConsumerCancelled, NotFound +from amqp.exceptions import ConsumerCancelled, NotFound, MessageNacked, \ + RecoverableConnectionError class test_Channel: @pytest.fixture(autouse=True) def setup_conn(self): - self.conn = Mock(name='connection') + self.conn = MagicMock(name='connection') + self.conn.is_closing = False self.conn.channels = {} self.conn._get_free_channel_id.return_value = 2 self.c = Channel(self.conn, 1) @@ -262,6 +268,7 @@ def test_basic_consume(self): callback = Mock() on_cancel = Mock() + self.c.send_method.return_value = (123, ) self.c.basic_consume( 'q', 123, arguments={'x': 1}, callback=callback, @@ -271,22 +278,71 @@ spec.Basic.Consume, 'BssbbbbF', (0, 'q', 123, False, False, False, False, {'x': 1}), wait=spec.Basic.ConsumeOk, + returns_tuple=True ) assert self.c.callbacks[123] is callback assert self.c.cancel_callbacks[123] is on_cancel def test_basic_consume__no_ack(self): + self.c.send_method.return_value = (123,) self.c.basic_consume( 'q', 123, arguments={'x': 1}, no_ack=True, ) assert 123 in self.c.no_ack_consumers + def test_basic_consume_no_consumer_tag(self): + callback = Mock() + self.c.send_method.return_value = (123,) + self.c.basic_consume( + 'q', arguments={'x': 1}, + callback=callback, + ) + self.c.send_method.assert_called_with( + spec.Basic.Consume, 'BssbbbbF', + (0, 'q', '', False, False, False, False, {'x': 1}), + wait=spec.Basic.ConsumeOk, + returns_tuple=True + ) + assert self.c.callbacks[123] is callback + + def test_basic_consume_no_wait(self): + callback = Mock() + self.c.basic_consume( + 'q', 123, arguments={'x': 1}, + callback=callback, nowait=True + ) + self.c.send_method.assert_called_with( + spec.Basic.Consume, 'BssbbbbF', + (0, 'q', 123, False, False, False, True, {'x': 1}), + wait=None, + returns_tuple=True + ) + assert self.c.callbacks[123] is callback + + def test_basic_consume_no_wait_no_consumer_tag(self): + callback = Mock() + with pytest.raises(ValueError): + self.c.basic_consume( + 'q', arguments={'x': 1}, + callback=callback, nowait=True + ) + assert 123 not in self.c.callbacks + def test_on_basic_deliver(self): - msg = Mock() + msg = Message() self.c._on_basic_deliver(123, '321', False, 'ex', 'rkey', msg) callback = self.c.callbacks[123] = Mock(name='cb') + self.c._on_basic_deliver(123, '321', False, 'ex', 'rkey', msg) callback.assert_called_with(msg) + assert msg.channel == self.c + assert msg.delivery_info == { + 'consumer_tag': 123, + 'delivery_tag': '321', + 'redelivered': False, + 'exchange': 'ex', + 'routing_key': 'rkey', + } def test_basic_get(self): self.c._on_get_empty = Mock() @@ -310,11 +366,19 @@ self.c._on_get_empty(1) def test_on_get_ok(self): - msg = Mock() + msg = Message() m = self.c._on_get_ok( 'dtag', 'redelivered', 'ex', 'rkey', 'mcount', msg, ) assert m is msg + assert m.channel == self.c + assert m.delivery_info == { + 'delivery_tag': 'dtag', + 'redelivered': 'redelivered', + 'exchange': 'ex', + 'routing_key': 'rkey', + 'message_count': 'mcount', + } def test_basic_publish(self): self.c.connection.transport.having_timeout = ContextMock() @@ -334,9 +398,101 @@ assert self.c._confirm_selected self.c._basic_publish.assert_called_with(1, 2, arg=1) assert ret is self.c._basic_publish() - self.c.wait.assert_called_with(spec.Basic.Ack) + self.c.wait.assert_called_with( + [spec.Basic.Ack, spec.Basic.Nack], callback=ANY + ) + self.c.basic_publish_confirm(1, 2, arg=1) + + def test_basic_publish_confirm_nack(self): + # test checking whether library is handling correctly Nack confirms + # sent from RabbitMQ. Library must raise MessageNacked when server + # sent Nack message. + + # Nack frame construction + args = dumps('Lb', (1, False)) + frame = (b''.join([pack('>HH', *spec.Basic.Nack), args])) + + def wait(method, *args, **kwargs): + # Simple mock simulating registering callbacks of real wait method + for m in method: + self.c._pending[m] = kwargs['callback'] + + self.c._basic_publish = Mock(name='_basic_publish') + self.c.wait = Mock(name='wait', side_effect=wait) + self.c.basic_publish_confirm(1, 2, arg=1) + with pytest.raises(MessageNacked): + # Inject Nack to message handler + self.c.dispatch_method( + spec.Basic.Nack, frame, None + ) + + def test_basic_publish_connection_blocked(self): + # Basic test checking that drain_events() is called + # before publishing message and send_method() is called + self.c._basic_publish('msg', 'ex', 'rkey') + self.conn.drain_events.assert_called_once_with(timeout=0) + self.c.send_method.assert_called_once_with( + spec.Basic.Publish, 'Bssbb', + (0, 'ex', 'rkey', False, False), 'msg', + ) + + self.c.send_method.reset_mock() + + # Basic test checking that socket.timeout exception + # is ignored and send_method() is called. + self.conn.drain_events.side_effect = socket.timeout + self.c._basic_publish('msg', 'ex', 'rkey') + self.c.send_method.assert_called_once_with( + spec.Basic.Publish, 'Bssbb', + (0, 'ex', 'rkey', False, False), 'msg', + ) + + def test_basic_publish_connection_blocked_not_supported(self): + # Test veryfying that when server does not have + # connection.blocked capability, drain_events() are not called + self.conn.client_properties = { + 'capabilities': { + 'connection.blocked': False + } + } + self.c._basic_publish('msg', 'ex', 'rkey') + self.conn.drain_events.assert_not_called() + self.c.send_method.assert_called_once_with( + spec.Basic.Publish, 'Bssbb', + (0, 'ex', 'rkey', False, False), 'msg', + ) + + def test_basic_publish_confirm_callback(self): + + def wait_nack(method, *args, **kwargs): + kwargs['callback'](spec.Basic.Nack) + + def wait_ack(method, *args, **kwargs): + kwargs['callback'](spec.Basic.Ack) + + self.c._basic_publish = Mock(name='_basic_publish') + self.c.wait = Mock(name='wait_nack', side_effect=wait_nack) + + with pytest.raises(MessageNacked): + # when callback is called with spec.Basic.Nack it must raise + # MessageNacked exception + self.c.basic_publish_confirm(1, 2, arg=1) + + self.c.wait = Mock(name='wait_ack', side_effect=wait_ack) + + # when callback is called with spec.Basic.Ack + # it must nost raise exception + self.c.basic_publish_confirm(1, 2, arg=1) + + def test_basic_publish_connection_closed(self): + self.c.collect() + with pytest.raises(RecoverableConnectionError) as excinfo: + self.c._basic_publish('msg', 'ex', 'rkey') + assert 'basic_publish: connection closed' in str(excinfo.value) + self.c.send_method.assert_not_called() + def test_basic_qos(self): self.c.basic_qos(0, 123, False) self.c.send_method.assert_called_with( @@ -405,3 +561,9 @@ self.c.events['basic_ack'].add(callback) self.c._on_basic_ack(123, True) callback.assert_called_with(123, True) + + def test_on_basic_nack(self): + callback = Mock(name='callback') + self.c.events['basic_nack'].add(callback) + self.c._on_basic_nack(123, True) + callback.assert_called_with(123, True) diff -Nru python-amqp-2.3.2/t/unit/test_connection.py python-amqp-2.4.0/t/unit/test_connection.py --- python-amqp-2.3.2/t/unit/test_connection.py 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/t/unit/test_connection.py 2019-01-13 10:08:19.000000000 +0000 @@ -4,7 +4,7 @@ import warnings import pytest -from case import ContextMock, Mock, call +from case import ContextMock, Mock, call, patch from amqp import Connection, spec from amqp.connection import SSLError @@ -100,6 +100,33 @@ self.conn.connect.assert_called_with() self.conn.close.assert_called_with() + def test__enter__socket_error(self): + # test when entering + self.conn = Connection() + self.conn.close = Mock(name='close') + reached = False + with patch('socket.socket', side_effect=socket.error): + with pytest.raises(socket.error): + with self.conn: + reached = True + assert not reached and not self.conn.close.called + assert self.conn._transport is None and not self.conn.connected + + def test__exit__socket_error(self): + # test when exiting + connection = self.conn + transport = connection._transport + transport.connected = True + connection.send_method = Mock(name='send_method', + side_effect=socket.error) + reached = False + with pytest.raises(socket.error): + with connection: + reached = True + assert reached + assert connection.send_method.called and transport.close.called + assert self.conn._transport is None and not self.conn.connected + def test_then(self): self.conn.on_open = Mock(name='on_open') on_success = Mock(name='on_success') @@ -127,6 +154,20 @@ assert self.conn.connect(callback) == callback.return_value callback.assert_called_with() + def test_connect__socket_error(self): + # check Transport.Connect error + # socket.error derives from IOError + # ssl.SSLError derives from socket.error + self.conn = Connection() + self.conn.Transport = Mock(name='Transport') + transport = self.conn.Transport.return_value + transport.connect.side_effect = IOError + assert self.conn._transport is None and not self.conn.connected + with pytest.raises(IOError): + self.conn.connect() + transport.connect.assert_called + assert self.conn._transport is None and not self.conn.connected + def test_on_start(self): self.conn._on_start(3, 4, {'foo': 'bar'}, b'x y z AMQPLAIN PLAIN', 'en_US en_GB') @@ -279,6 +320,7 @@ for i, channel in items(channels): if i: channel.collect.assert_called_with() + assert self.conn._transport is None def test_collect__channel_raises_socket_error(self): self.conn.channels = self.conn.channels = {1: Mock(name='c1')} @@ -376,6 +418,7 @@ ) def test_close(self): + self.conn.collect = Mock(name='collect') self.conn.close(reply_text='foo', method_sig=spec.Channel.Open) self.conn.send_method.assert_called_with( spec.Connection.Close, 'BsBB', @@ -387,6 +430,14 @@ self.conn.transport = None self.conn.close() + def test_close__socket_error(self): + self.conn.send_method = Mock(name='send_method', + side_effect=socket.error) + with pytest.raises(socket.error): + self.conn.close() + self.conn.send_method.assert_called() + assert self.conn._transport is None and not self.conn.connected + def test_on_close(self): self.conn._x_close_ok = Mock(name='_x_close_ok') with pytest.raises(NotFound): diff -Nru python-amqp-2.3.2/t/unit/test_exceptions.py python-amqp-2.4.0/t/unit/test_exceptions.py --- python-amqp-2.3.2/t/unit/test_exceptions.py 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/t/unit/test_exceptions.py 2019-01-13 10:08:19.000000000 +0000 @@ -1,16 +1,37 @@ from __future__ import absolute_import, unicode_literals from case import Mock +import pytest +import amqp.exceptions from amqp.exceptions import AMQPError, error_for_code +AMQP_EXCEPTIONS = ( + 'ConnectionError', 'ChannelError', + 'RecoverableConnectionError', 'IrrecoverableConnectionError', + 'RecoverableChannelError', 'IrrecoverableChannelError', + 'ConsumerCancelled', 'ContentTooLarge', 'NoConsumers', + 'ConnectionForced', 'InvalidPath', 'AccessRefused', 'NotFound', + 'ResourceLocked', 'PreconditionFailed', 'FrameError', 'FrameSyntaxError', + 'InvalidCommand', 'ChannelNotOpen', 'UnexpectedFrame', 'ResourceError', + 'NotAllowed', 'AMQPNotImplementedError', 'InternalError', +) + class test_AMQPError: def test_str(self): - assert str(AMQPError()) + assert str(AMQPError()) == '' x = AMQPError(method_sig=(50, 60)) - assert str(x) + assert str(x) == '(50, 60): (0) None' + x = AMQPError('Test Exception') + assert str(x) == 'Test Exception' + + @pytest.mark.parametrize("amqp_exception", AMQP_EXCEPTIONS) + def test_str_subclass(self, amqp_exception): + exp = '<{}: unknown error>'.format(amqp_exception) + exception_class = getattr(amqp.exceptions, amqp_exception) + assert str(exception_class()) == exp class test_error_for_code: diff -Nru python-amqp-2.3.2/t/unit/test_method_framing.py python-amqp-2.4.0/t/unit/test_method_framing.py --- python-amqp-2.3.2/t/unit/test_method_framing.py 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/t/unit/test_method_framing.py 2019-01-13 10:08:19.000000000 +0000 @@ -21,12 +21,12 @@ def test_header(self): buf = pack('>HH', 60, 51) - self.g((1, 1, buf)) + assert self.g((1, 1, buf)) self.callback.assert_called_with(1, (60, 51), buf, None) assert self.conn.bytes_recv def test_header_message_empty_body(self): - self.g((1, 1, pack('>HH', *spec.Basic.Deliver))) + assert not self.g((1, 1, pack('>HH', *spec.Basic.Deliver))) self.callback.assert_not_called() with pytest.raises(UnexpectedFrame): @@ -36,7 +36,7 @@ m.properties = {} buf = pack('>HxxQ', m.CLASS_ID, 0) buf += m._serialize_properties() - self.g((2, 1, buf)) + assert self.g((2, 1, buf)) self.callback.assert_called() msg = self.callback.call_args[0][3] @@ -45,20 +45,20 @@ ) def test_header_message_content(self): - self.g((1, 1, pack('>HH', *spec.Basic.Deliver))) + assert not self.g((1, 1, pack('>HH', *spec.Basic.Deliver))) self.callback.assert_not_called() m = Message() m.properties = {} buf = pack('>HxxQ', m.CLASS_ID, 16) buf += m._serialize_properties() - self.g((2, 1, buf)) + assert not self.g((2, 1, buf)) self.callback.assert_not_called() - self.g((3, 1, b'thequick')) + assert not self.g((3, 1, b'thequick')) self.callback.assert_not_called() - self.g((3, 1, b'brownfox')) + assert self.g((3, 1, b'brownfox')) self.callback.assert_called() msg = self.callback.call_args[0][3] self.callback.assert_called_with( @@ -67,7 +67,8 @@ assert msg.body == b'thequickbrownfox' def test_heartbeat_frame(self): - self.g((8, 1, '')) + assert not self.g((8, 1, '')) + self.callback.assert_not_called() assert self.conn.bytes_recv @@ -92,15 +93,48 @@ frame = 2, 1, spec.Basic.Publish, b'x' * 10, msg self.g(*frame) self.write.assert_called() + assert 'content_encoding' not in msg.properties def test_write_slow_content(self): msg = Message(body=b'y' * 2048, content_type='utf-8') frame = 2, 1, spec.Basic.Publish, b'x' * 10, msg self.g(*frame) self.write.assert_called() + assert 'content_encoding' not in msg.properties def test_write_zero_len_body(self): msg = Message(body=b'', content_type='application/octet-stream') frame = 2, 1, spec.Basic.Publish, b'x' * 10, msg self.g(*frame) self.write.assert_called() + assert 'content_encoding' not in msg.properties + + def test_write_fast_unicode(self): + msg = Message(body='\N{CHECK MARK}') + frame = 2, 1, spec.Basic.Publish, b'x' * 10, msg + self.g(*frame) + self.write.assert_called() + memory = self.write.call_args[0][0] + assert isinstance(memory, memoryview) + assert '\N{CHECK MARK}'.encode('utf-8') in memory.tobytes() + assert msg.properties['content_encoding'] == 'utf-8' + + def test_write_slow_unicode(self): + msg = Message(body='y' * 2048 + '\N{CHECK MARK}') + frame = 2, 1, spec.Basic.Publish, b'x' * 10, msg + self.g(*frame) + self.write.assert_called() + memory = self.write.call_args[0][0] + assert isinstance(memory, bytes) + assert '\N{CHECK MARK}'.encode('utf-8') in memory + assert msg.properties['content_encoding'] == 'utf-8' + + def test_write_non_utf8(self): + msg = Message(body='body', content_encoding='utf-16') + frame = 2, 1, spec.Basic.Publish, b'x' * 10, msg + self.g(*frame) + self.write.assert_called() + memory = self.write.call_args[0][0] + assert isinstance(memory, memoryview) + assert 'body'.encode('utf-16') in memory.tobytes() + assert msg.properties['content_encoding'] == 'utf-16' diff -Nru python-amqp-2.3.2/t/unit/test_serialization.py python-amqp-2.4.0/t/unit/test_serialization.py --- python-amqp-2.3.2/t/unit/test_serialization.py 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/t/unit/test_serialization.py 2019-01-13 10:08:19.000000000 +0000 @@ -25,6 +25,7 @@ @pytest.mark.parametrize('descr,frame,expected,cast', [ ('S', b's8thequick', 'thequick', None), + ('x', b'x\x00\x00\x00\x09thequick\xffIGNORED', b'thequick\xff', None), ('b', b'b' + pack('>B', True), True, None), ('B', b'B' + pack('>b', 123), 123, None), ('U', b'U' + pack('>h', -321), -321, None), @@ -43,17 +44,18 @@ assert _read_item(b'V')[0] is None def test_roundtrip(self): - format = b'bobBlLbsbST' + format = b'bobBlLbsbSTx' x = dumps(format, [ True, 32, False, 3415, 4513134, 13241923419, True, b'thequickbrownfox', False, 'jumpsoverthelazydog', datetime(2015, 3, 13, 10, 23), + b'thequick\xff' ]) y = loads(format, x) assert [ True, 32, False, 3415, 4513134, 13241923419, True, 'thequickbrownfox', False, 'jumpsoverthelazydog', - datetime(2015, 3, 13, 10, 23), + datetime(2015, 3, 13, 10, 23), b'thequick\xff' ] == y[0] def test_int_boundaries(self): @@ -68,7 +70,7 @@ def test_loads_unknown_type(self): with pytest.raises(FrameSyntaxError): - loads('x', 'asdsad') + loads('y', 'asdsad') def test_float(self): assert (int(loads(b'fb', dumps(b'fb', [32.31, False]))[0][0] * 100) == diff -Nru python-amqp-2.3.2/t/unit/test_transport.py python-amqp-2.4.0/t/unit/test_transport.py --- python-amqp-2.3.2/t/unit/test_transport.py 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/t/unit/test_transport.py 2019-01-13 10:08:19.000000000 +0000 @@ -12,6 +12,10 @@ from amqp.transport import _AbstractTransport +class DummyException(Exception): + pass + + class MockSocket(object): options = {} @@ -21,7 +25,10 @@ self.sa = None def setsockopt(self, family, key, value): - if not isinstance(value, int): + if (family == socket.SOL_SOCKET and + key in (socket.SO_RCVTIMEO, socket.SO_SNDTIMEO)): + self.options[key] = value + elif not isinstance(value, int): raise socket.error() self.options[key] = value @@ -202,6 +209,18 @@ assert opts + def test_set_sockopt_opts_timeout(self): + # tests socket options SO_RCVTIMEO and SO_SNDTIMEO + # this test is soley for coverage as socket.settimeout + # is pythonic way to have timeouts + self.transp = transport.Transport( + self.host, self.connect_timeout, + ) + self.transp.read_timeout = 0xdead + self.transp.write_timeout = 0xbeef + with patch('socket.socket', return_value=MockSocket()): + self.transp.connect() + class test_AbstractTransport: @@ -242,8 +261,9 @@ self.t.close() sock.shutdown.assert_called_with(socket.SHUT_RDWR) sock.close.assert_called_with() - assert self.t.sock is None + assert self.t.sock is None and self.t.connected is False self.t.close() + assert self.t.sock is None and self.t.connected is False def test_read_frame__timeout(self): self.t._read = Mock() @@ -299,6 +319,19 @@ with pytest.raises(UnexpectedFrame): self.t.read_frame() + def transport_read_EOF(self): + for host, ssl in (('localhost:5672', False), + ('localhost:5671', True),): + self.t = transport.Transport(host, ssl) + self.t.sock = Mock(name='socket') + self.t.connected = True + self.t._quick_recv = Mock(name='recv', return_value='') + with pytest.raises( + IOError, + match=r'.*Server unexpectedly closed connection.*' + ): + self.t.read_frame() + def test_write__success(self): self.t._write = Mock() self.t.write('foo') @@ -325,9 +358,51 @@ assert not self.t.connected def test_having_timeout_none(self): + # Checks that context manager does nothing when no timeout is provided with self.t.having_timeout(None) as actual_sock: assert actual_sock == self.t.sock + def test_set_timeout(self): + # Checks that context manager sets and reverts timeout properly + with patch.object(self.t, 'sock') as sock_mock: + sock_mock.gettimeout.return_value = 3 + with self.t.having_timeout(5) as actual_sock: + assert actual_sock == self.t.sock + sock_mock.gettimeout.assert_called() + sock_mock.settimeout.assert_has_calls( + [ + call(5), + call(3), + ] + ) + + def test_set_timeout_exception_raised(self): + # Checks that context manager sets and reverts timeout properly + # when exception is raised. + with patch.object(self.t, 'sock') as sock_mock: + sock_mock.gettimeout.return_value = 3 + with pytest.raises(DummyException): + with self.t.having_timeout(5) as actual_sock: + assert actual_sock == self.t.sock + raise DummyException() + sock_mock.gettimeout.assert_called() + sock_mock.settimeout.assert_has_calls( + [ + call(5), + call(3), + ] + ) + + def test_set_same_timeout(self): + # Checks that context manager does not set timeout when + # it is same as currently set. + with patch.object(self.t, 'sock') as sock_mock: + sock_mock.gettimeout.return_value = 5 + with self.t.having_timeout(5) as actual_sock: + assert actual_sock == self.t.sock + sock_mock.gettimeout.assert_called() + sock_mock.settimeout.assert_not_called() + class test_AbstractTransport_connect: @@ -350,6 +425,7 @@ with patch('socket.socket', side_effect=socket.error): with pytest.raises(socket.error): self.t.connect() + assert self.t.sock is None and self.t.connected is False def test_connect_socket_initialization_fails(self): with patch('socket.socket', side_effect=socket.error), \ @@ -362,6 +438,7 @@ ]): with pytest.raises(socket.error): self.t.connect() + assert self.t.sock is None and self.t.connected is False def test_connect_multiple_addr_entries_fails(self): with patch('socket.socket', return_value=MockSocket()) as sock_mock, \ @@ -452,6 +529,15 @@ self.t.connect() assert cloexec_mock.called + def test_connect_already_connected(self): + assert not self.t.connected + with patch('socket.socket', return_value=MockSocket()): + self.t.connect() + assert self.t.connected + sock_obj = self.t.sock + self.t.connect() + assert self.t.connected and self.t.sock is sock_obj + class test_SSLTransport: @@ -506,6 +592,14 @@ self.t._shutdown_transport() assert self.t.sock is sock.unwrap() + def test_read_EOF(self): + self.t.sock = Mock(name='SSLSocket') + self.t.connected = True + self.t._quick_recv = Mock(name='recv', return_value='') + with pytest.raises(IOError, + match=r'.*Server unexpectedly closed connection.*'): + self.t._read(64) + class test_TCPTransport: @@ -527,3 +621,11 @@ assert self.t._write is self.t.sock.sendall assert self.t._read_buffer is not None assert self.t._quick_recv is self.t.sock.recv + + def test_read_EOF(self): + self.t.sock = Mock(name='socket') + self.t.connected = True + self.t._quick_recv = Mock(name='recv', return_value='') + with pytest.raises(IOError, + match=r'.*Server unexpectedly closed connection.*'): + self.t._read(64) diff -Nru python-amqp-2.3.2/tox.ini python-amqp-2.4.0/tox.ini --- python-amqp-2.3.2/tox.ini 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/tox.ini 1970-01-01 00:00:00.000000000 +0000 @@ -1,50 +0,0 @@ -[tox] -envlist = - 2.7 - pypy - 3.4 - 3.5 - 3.6 - flake8 - flakeplus - apicheck - pydocstyle - -[testenv] -deps= - -r{toxinidir}/requirements/default.txt - -r{toxinidir}/requirements/test.txt - -r{toxinidir}/requirements/test-ci.txt - - apicheck,linkcheck: -r{toxinidir}/requirements/docs.txt - flake8,flakeplus,pydocstyle: -r{toxinidir}/requirements/pkgutils.txt -sitepackages = False -recreate = False -commands = py.test -xv --cov=amqp --cov-report=xml --no-cov-on-fail - -basepython = - 2.7,flakeplus,flake8,apicheck,linkcheck,pydocstyle: python2.7 - pypy: pypy - 3.4: python3.4 - 3.5: python3.5 - 3.6: python3.6 - -[testenv:apicheck] -commands = - sphinx-build -b apicheck -d {envtmpdir}/doctrees docs docs/_build/apicheck - -[testenv:linkcheck] -commands = - sphinx-build -W -b linkcheck -d {envtmpdir}/doctrees docs docs/_build/linkcheck - -[testenv:flake8] -commands = - flake8 {toxinidir}/amqp {toxinidir}/t - -[testenv:flakeplus] -commands = - flakeplus --2.7 {toxinidir}/amqp {toxinidir}/t - -[testenv:pydocstyle] -commands = - pydocstyle {toxinidir}/amqp diff -Nru python-amqp-2.3.2/.travis.yml python-amqp-2.4.0/.travis.yml --- python-amqp-2.3.2/.travis.yml 2018-08-19 21:47:32.000000000 +0000 +++ python-amqp-2.4.0/.travis.yml 1970-01-01 00:00:00.000000000 +0000 @@ -1,45 +0,0 @@ -language: python -sudo: false -cache: pip - -env: - global: - PYTHONUNBUFFERED=yes - -matrix: - fast_finish: true - include: - - python: 2.7 - env: TOXENV=2.7 - - python: pypy - env: TOXENV=pypy - - python: 3.4 - env: TOXENV=3.4 - - python: 3.5 - env: TOXENV=3.5 - - python: 3.6 - env: TOXENV=3.6 - - python: 2.7 - env: TOXENV=flake8 - - python: 3.6 - env: TOXENV=flake8 - - python: 2.7 - env: TOXENV=flakeplus - - python: 2.7 - env: TOXENV=pydocstyle - - python: 2.7 - env: TOXENV=apicheck - -install: - - pip install -U pip setuptools wheel | cat - - pip install -U tox | cat -script: tox -v -- -v -after_success: - - .tox/$TRAVIS_PYTHON_VERSION/bin/coverage xml - - .tox/$TRAVIS_PYTHON_VERSION/bin/codecov -e TOXENV -notifications: - irc: - channels: - - "chat.freenode.net#celery" - on_success: change - on_failure: change