├── MANIFEST.in ├── .checkignore ├── compose.yml ├── tox.ini ├── docs ├── api │ ├── message.rst │ ├── amqp.rst │ ├── tx.rst │ ├── channel.rst │ ├── exchange.rst │ ├── connection.rst │ ├── queue.rst │ └── exceptions.rst ├── examples │ ├── ha_queues.rst │ ├── consumer.rst │ ├── getter.rst │ ├── transactional_publisher.rst │ └── publisher_confirms.rst ├── simple.rst ├── index.rst ├── threads.rst ├── Makefile ├── conf.py └── history.rst ├── .codeclimate.yml ├── examples ├── getter.py ├── consumer.py ├── publisher.py └── transactional_publisher.py ├── tests ├── __init__.py ├── test_amqp.py ├── base_tests.py ├── helpers.py ├── utils_tests.py ├── channel_test.py ├── events_tests.py ├── test_exchange.py └── test_tx.py ├── .gitignore ├── .github └── workflows │ ├── test.yaml │ └── deploy.yaml ├── bootstrap.sh ├── LICENSE ├── README.rst ├── rabbitpy ├── __init__.py ├── heartbeat.py ├── utils.py ├── tx.py ├── events.py ├── exchange.py ├── exceptions.py ├── simple.py ├── channel0.py ├── message.py ├── amqp.py ├── amqp_queue.py └── base.py ├── pyproject.toml └── .pylintrc /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst LICENSE -------------------------------------------------------------------------------- /.checkignore: -------------------------------------------------------------------------------- 1 | tests 2 | docs 3 | setup.py 4 | -------------------------------------------------------------------------------- /compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rabbitmq: 3 | image: rabbitmq:3.13 4 | ports: 5 | - 5672 6 | - 15672 7 | healthcheck: 8 | test: rabbitmq-diagnostics -q check_running 9 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | indexserver = 3 | default = https://pypi.python.org/simple 4 | envlist = py26,py27,py32,py33,py34 5 | deps = -rrequirements.txt 6 | commands=nosetests 7 | 8 | [tox:py26] 9 | deps = unittest2 -------------------------------------------------------------------------------- /docs/api/message.rst: -------------------------------------------------------------------------------- 1 | Message 2 | ======= 3 | The :class:`Message ` class is used to create messages that you intend to publish to RabbitMQ and is created when a message is received by RabbitMQ by a consumer or as the result of a :meth:`Queue.get() ` request. 4 | 5 | API Documentation 6 | ----------------- 7 | 8 | .. autoclass:: rabbitpy.Message 9 | :members: 10 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | duplication: 3 | enabled: true 4 | config: 5 | languages: 6 | - python 7 | pep8: 8 | enabled: true 9 | radon: 10 | enabled: true 11 | ratings: 12 | paths: 13 | - rabbitpy/** 14 | exclude_paths: 15 | - docs/** 16 | - setup.* 17 | - test-requirements.txt 18 | - tests/** 19 | - README.rst 20 | - MANIFEST.in 21 | - LICENSE 22 | - tox.ini 23 | - examples/** 24 | -------------------------------------------------------------------------------- /docs/examples/ha_queues.rst: -------------------------------------------------------------------------------- 1 | Declaring HA Queues 2 | =================== 3 | The following example will create a HA :py:meth:`queue ` on each node in a RabbitMQ cluster.:: 4 | 5 | import rabbitpy 6 | 7 | with rabbitpy.Connection('amqp://guest:guest@localhost:5672/%2f') as conn: 8 | with conn.channel() as channel: 9 | queue = rabbitpy.Queue(channel, 'example') 10 | queue.ha_declare() 11 | -------------------------------------------------------------------------------- /examples/getter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import rabbitpy 3 | 4 | with rabbitpy.Connection('amqp://guest:guest@localhost:5672/%2f') as conn: 5 | with conn.channel() as channel: 6 | queue = rabbitpy.Queue(channel, 'example') 7 | while len(queue) > 0: 8 | message = queue.get() 9 | message.pprint(True) 10 | message.ack() 11 | print('There are {} more messages in the queue'.format(len(queue))) -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import os 3 | import warnings 4 | 5 | warnings.simplefilter('ignore', UserWarning) 6 | 7 | 8 | def setup_module(): 9 | try: 10 | with open('build/test-environment') as f: 11 | for line in f: 12 | if line.startswith('export '): 13 | line = line[7:] 14 | name, _, value = line.strip().partition('=') 15 | os.environ[name] = value 16 | except IOError: 17 | pass -------------------------------------------------------------------------------- /examples/consumer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import logging 3 | import rabbitpy 4 | 5 | URL = 'amqp://guest:guest@localhost:5672/%2f?heartbeat=15' 6 | 7 | logging.basicConfig(level=logging.INFO) 8 | 9 | with rabbitpy.Connection(URL) as conn: 10 | with conn.channel() as channel: 11 | # Exit on CTRL-C 12 | try: 13 | for message in rabbitpy.Queue(channel, 'test'): 14 | message.pprint(True) 15 | message.ack() 16 | except KeyboardInterrupt: 17 | logging.info('Exited consumer') 18 | -------------------------------------------------------------------------------- /tests/test_amqp.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the rabbitpy.amqp class 3 | 4 | """ 5 | import mock 6 | 7 | from rabbitpy import amqp, base, channel 8 | 9 | from tests import helpers 10 | 11 | class BasicAckTests(helpers.TestCase): 12 | 13 | def test_basic_ack_invokes_write_frame(self): 14 | with mock.patch.object(self.channel, 'write_frame') as method: 15 | obj = amqp.AMQP(self.channel) 16 | obj.basic_ack(123, True) 17 | args, kwargs = method.call_args 18 | self.assertEqual(len(args), 1) 19 | self.assertEqual(args[0].delivery_tag, 123) 20 | self.assertEqual(args[0].multiple, True) 21 | -------------------------------------------------------------------------------- /docs/examples/consumer.rst: -------------------------------------------------------------------------------- 1 | Message Consumer 2 | ================ 3 | The following example will subscribe to a queue named "example" and consume messages 4 | until CTRL-C is pressed:: 5 | 6 | import rabbitpy 7 | 8 | with rabbitpy.Connection('amqp://guest:guest@localhost:5672/%2f') as conn: 9 | with conn.channel() as channel: 10 | queue = rabbitpy.Queue(channel, 'example') 11 | 12 | # Exit on CTRL-C 13 | try: 14 | # Consume the message 15 | for message in queue: 16 | message.pprint(True) 17 | message.ack() 18 | 19 | except KeyboardInterrupt: 20 | print 'Exited consumer' 21 | -------------------------------------------------------------------------------- /docs/examples/getter.rst: -------------------------------------------------------------------------------- 1 | Message Getter 2 | ============== 3 | The following example will get a single message at a time from the "example" queue 4 | as long as there are messages in it. It uses :code:`len(queue)` to check the current 5 | queue depth while it is looping:: 6 | 7 | import rabbitpy 8 | 9 | with rabbitpy.Connection('amqp://guest:guest@localhost:5672/%2f') as conn: 10 | with conn.channel() as channel: 11 | queue = rabbitpy.Queue(channel, 'example') 12 | while len(queue) > 0: 13 | message = queue.get() 14 | message.pprint(True) 15 | message.ack() 16 | print('There are {} more messages in the queue'.format(len(queue))) 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | 45 | # Rope 46 | .ropeproject 47 | 48 | # Django stuff: 49 | *.log 50 | *.pot 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | .idea 56 | .vagrant 57 | -------------------------------------------------------------------------------- /docs/simple.rst: -------------------------------------------------------------------------------- 1 | Simple API Methods 2 | ================== 3 | 4 | rabbitpy's simple API methods are meant for one off use, either in your apps or in 5 | the python interpreter. For example, if your application publishes a single 6 | message as part of its lifetime, :py:meth:`rabbitpy.publish` should be enough 7 | for almost any publishing concern. However if you are publishing more than 8 | one message, it is not an efficient method to use as it connects and disconnects 9 | from RabbitMQ on each invocation. :py:meth:`rabbitpy.get` also connects and 10 | disconnects on each invocation. :py:meth:`rabbitpy.consume` does stay connected 11 | as long as you're iterating through the messages returned by it. Exiting the 12 | generator will close the connection. For a more complete api, see the rabbitpy 13 | core API. 14 | 15 | .. automodule:: rabbitpy.simple 16 | :members: 17 | -------------------------------------------------------------------------------- /docs/examples/transactional_publisher.rst: -------------------------------------------------------------------------------- 1 | Transactional Publisher 2 | ======================== 3 | The following example uses RabbitMQ's Transactions feature to send the message, 4 | then roll it back:: 5 | 6 | import rabbitpy 7 | 8 | # Connect to RabbitMQ on localhost, port 5672 as guest/guest 9 | with rabbitpy.Connection('amqp://guest:guest@localhost:5672/%2f') as conn: 10 | 11 | # Open the channel to communicate with RabbitMQ 12 | with conn.channel() as channel: 13 | 14 | # Start the transaction 15 | tx = rabbitpy.Tx(channel) 16 | tx.select() 17 | 18 | # Create the message to publish & publish it 19 | message = rabbitpy.Message(channel, 'message body value') 20 | message.publish('test_exchange', 'test-routing-key') 21 | 22 | # Rollback the transaction 23 | tx.rollback() 24 | -------------------------------------------------------------------------------- /docs/api/amqp.rst: -------------------------------------------------------------------------------- 1 | AMQP Adapter 2 | ============ 3 | While the core rabbitpy API strives to provide an easy to use, Pythonic interface 4 | for RabbitMQ, some developers may prefer a less opinionated AMQP interface. The 5 | :py:class:`rabbitpy.AMQP` adapter provides a more traditional AMQP client library 6 | API seen in libraries like `pika `_. 7 | 8 | .. versionadded:: 0.26 9 | 10 | Example 11 | ------- 12 | The following example will connect to RabbitMQ and use the :py:class:`rabbitpy.AMQP` 13 | adapter to consume and acknowledge messages. 14 | 15 | .. code:: python 16 | 17 | import rabbitpy 18 | 19 | with rabbitpy.Connection() as conn: 20 | with conn.channel() as channel: 21 | amqp = rabbitpy.AMQP(channel) 22 | 23 | for message in amqp.basic_consume('queue-name'): 24 | print(message) 25 | 26 | API Documentation 27 | ----------------- 28 | 29 | .. autoclass:: rabbitpy.AMQP 30 | :members: 31 | -------------------------------------------------------------------------------- /docs/examples/publisher_confirms.rst: -------------------------------------------------------------------------------- 1 | Mandatory Publishing 2 | ==================== 3 | The following example uses RabbitMQ's Publisher Confirms feature to allow for validation 4 | that the message was successfully published:: 5 | 6 | import rabbitpy 7 | 8 | # Connect to RabbitMQ on localhost, port 5672 as guest/guest 9 | with rabbitpy.Connection('amqp://guest:guest@localhost:5672/%2f') as conn: 10 | 11 | # Open the channel to communicate with RabbitMQ 12 | with conn.channel() as channel: 13 | 14 | # Turn on publisher confirmations 15 | channel.enable_publisher_confirms() 16 | 17 | # Create the message to publish 18 | message = rabbitpy.Message(channel, 'message body value') 19 | 20 | # Publish the message, looking for the return value to be a bool True/False 21 | if message.publish('test_exchange', 'test-routing-key', mandatory=True): 22 | print 'Message publish confirmed by RabbitMQ' 23 | else: 24 | print 'RabbitMQ indicates message publishing failure' 25 | -------------------------------------------------------------------------------- /tests/base_tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the rabbitpy.base classes 3 | 4 | """ 5 | from rabbitpy import base, utils 6 | 7 | from tests import helpers 8 | 9 | 10 | class AMQPClassTests(helpers.TestCase): 11 | 12 | def test_channel_valid(self): 13 | obj = base.AMQPClass(self.channel, 'Foo') 14 | self.assertEqual(obj.channel, self.channel) 15 | 16 | def test_channel_invalid(self): 17 | self.assertRaises(ValueError, base.AMQPClass, 'Foo', 'Bar') 18 | 19 | def test_name_bytes(self): 20 | obj = base.AMQPClass(self.channel, b'Foo') 21 | self.assertIsInstance(obj.name, bytes) 22 | 23 | def test_name_str(self): 24 | obj = base.AMQPClass(self.channel, 'Foo') 25 | self.assertIsInstance(obj.name, str) 26 | 27 | @helpers.unittest.skipIf(utils.PYTHON3, 'No unicode in Python 3') 28 | def test_name_unicode(self): 29 | obj = base.AMQPClass(self.channel, unicode('Foo')) 30 | self.assertIsInstance(obj.name, unicode) 31 | 32 | def test_name_value(self): 33 | obj = base.AMQPClass(self.channel, 'Foo') 34 | self.assertEqual(obj.name, 'Foo') 35 | 36 | def test_name_invalid(self): 37 | self.assertRaises(ValueError, base.AMQPClass, self.channel, 1) 38 | -------------------------------------------------------------------------------- /docs/api/tx.rst: -------------------------------------------------------------------------------- 1 | Transactions 2 | ============ 3 | The :class:`Tx ` or transaction class implements transactional functionality with RabbitMQ and allows for any AMQP command to be issued, then committed or rolled back. 4 | 5 | It can be used as a normal Python object: 6 | 7 | .. code:: python 8 | 9 | with rabbitpy.Connection() as connection: 10 | with connection.channel() as channel: 11 | tx = rabbitpy.Tx(channel) 12 | tx.select() 13 | exchange = rabbitpy.Exchange(channel, 'my-exchange') 14 | exchange.declare() 15 | tx.commit() 16 | 17 | Or as a context manager (See :pep:`0343`) where the transaction will automatically be started and committed for you: 18 | 19 | .. code:: python 20 | 21 | with rabbitpy.Connection() as connection: 22 | with connection.channel() as channel: 23 | with rabbitpy.Tx(channel) as tx: 24 | exchange = rabbitpy.Exchange(channel, 'my-exchange') 25 | exchange.declare() 26 | 27 | In the event of an exception exiting the block when used as a context manager, the transaction will be rolled back for you automatically. 28 | 29 | API Documentation 30 | ----------------- 31 | 32 | .. autoclass:: rabbitpy.Tx 33 | :members: 34 | -------------------------------------------------------------------------------- /docs/api/channel.rst: -------------------------------------------------------------------------------- 1 | Channel 2 | ======= 3 | A :class:`Channel` is created on an active connection using the :meth:`Connection.channel() ` method. Channels can act as normal Python objects: 4 | 5 | .. code:: python 6 | 7 | conn = rabbitpy.Connection() 8 | chan = conn.channel() 9 | chan.enable_publisher_confirms() 10 | chan.close() 11 | 12 | or as a Python context manager (See :pep:`0343`): 13 | 14 | .. code:: python 15 | 16 | with rabbitpy.Connection() as conn: 17 | with conn.channel() as chan: 18 | chan.enable_publisher_confirms() 19 | 20 | When they are used as a context manager with the `with` statement, when your code exits the block, the channel will automatically close, issuing a clean shutdown with RabbitMQ via the ``Channel.Close`` RPC request. 21 | 22 | You should be aware that if you perform actions on a channel with exchanges, queues, messages or transactions that RabbitMQ does not like, it will close the channel by sending an AMQP ``Channel.Close`` RPC request to your application. Upon receipt of such a request, rabbitpy will raise the :doc:`appropriate exception ` referenced in the request. 23 | 24 | API Documentation 25 | ----------------- 26 | 27 | .. autoclass:: rabbitpy.Channel 28 | :members: 29 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | on: 3 | push: 4 | branches: ["*"] 5 | paths-ignore: 6 | - 'docs/**' 7 | - '*.rst' 8 | tags-ignore: ["*"] 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 5 13 | strategy: 14 | matrix: 15 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 16 | env: 17 | TEST_HOST: docker 18 | steps: 19 | - name: Check out repository 20 | uses: actions/checkout@v4 21 | 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v5 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | 27 | - name: Install testing dependencies 28 | run: pip install -e '.[dev]' 29 | 30 | - name: Bootstrap 31 | run: ./bootstrap.sh 32 | 33 | - name: Run unit tests 34 | run: coverage run 35 | 36 | - name: Generate coverage report 37 | run: coverage report && coverage xml 38 | 39 | - name: Upload coverage to Codecov 40 | uses: codecov/codecov-action@v4 41 | with: 42 | fail_ci_if_error: true 43 | file: ./build/coverage.xml 44 | flags: unittests 45 | name: codecov-umbrella 46 | token: ${{secrets.CODECOV_TOKEN}} 47 | -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | try: 2 | import unittest2 as unittest 3 | except ImportError: 4 | import unittest 5 | 6 | import mock 7 | 8 | from rabbitpy import channel, connection, events 9 | 10 | 11 | class TestCase(unittest.TestCase): 12 | 13 | def setUp(self): 14 | self.connection = mock.MagicMock('rabbitpy.connection.Connection') 15 | self.connection._io = mock.Mock() 16 | self.connection._io.write_trigger = mock.Mock('socket.socket') 17 | self.connection._io.write_trigger.send = mock.Mock() 18 | self.connection._channel0 = mock.Mock() 19 | self.connection._channel0.properties = {} 20 | self.connection._events = events.Events() 21 | self.connection._exceptions = connection.queue.Queue() 22 | self.connection.open = True 23 | self.connection.closed = False 24 | self.channel = channel.Channel(1, {}, 25 | self.connection._events, 26 | self.connection._exceptions, 27 | connection.queue.Queue(), 28 | connection.queue.Queue(), 32768, 29 | self.connection._io.write_trigger, 30 | connection=self.connection) 31 | self.channel._set_state(self.channel.OPEN) -------------------------------------------------------------------------------- /examples/publisher.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import rabbitpy 3 | import logging 4 | import datetime 5 | import uuid 6 | logging.basicConfig(level=logging.DEBUG) 7 | 8 | # Use a new connection as a context manager 9 | with rabbitpy.Connection('amqp://guest:guest@localhost:5672/%2f') as conn: 10 | 11 | # Use the channel as a context manager 12 | with conn.channel() as channel: 13 | 14 | # Create the exchange 15 | exchange = rabbitpy.Exchange(channel, 'example_exchange') 16 | exchange.declare() 17 | 18 | # Create the queue 19 | queue = rabbitpy.Queue(channel, 'example') 20 | queue.declare() 21 | 22 | # Bind the queue 23 | queue.bind(exchange, 'test-routing-key') 24 | 25 | # Create the msg by passing channel, message and properties (as a dict) 26 | message = rabbitpy.Message(channel, 27 | 'Lorem ipsum dolor sit amet, consectetur ' 28 | 'adipiscing elit.', 29 | {'content_type': 'text/plain', 30 | 'delivery_mode': 1, 31 | 'message_type': 'Lorem ipsum', 32 | 'timestamp': datetime.datetime.now(), 33 | 'message_id': uuid.uuid1()}) 34 | 35 | # Publish the message 36 | message.publish(exchange, 'test-routing-key') 37 | -------------------------------------------------------------------------------- /examples/transactional_publisher.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import rabbitpy 3 | import datetime 4 | import uuid 5 | 6 | with rabbitpy.Connection('amqp://guest:guest@localhost:5672/%2f') as conn: 7 | with conn.channel() as channel: 8 | 9 | # Create the exchange 10 | exchange = rabbitpy.Exchange(channel, 'example_exchange') 11 | exchange.declare() 12 | 13 | # Create the queue 14 | queue = rabbitpy.Queue(channel, 'example') 15 | queue.declare() 16 | 17 | # Bind the queue 18 | queue.bind(exchange, 'test-routing-key') 19 | 20 | # Create and start the transaction 21 | tx = rabbitpy.Tx(channel) 22 | tx.select() 23 | 24 | # Create the message 25 | message = rabbitpy.Message(channel, 26 | 'Lorem ipsum dolor sit amet, consectetur ' 27 | 'adipiscing elit.', 28 | {'content_type': 'text/plain', 29 | 'message_type': 'Lorem ipsum', 30 | 'timestamp': datetime.datetime.now(), 31 | 'message_id': uuid.uuid1() 32 | }) 33 | 34 | # Publish the message 35 | message.publish(exchange, 'test-routing-key') 36 | 37 | # Commit the message 38 | tx.commit() 39 | 40 | print('Message published') 41 | -------------------------------------------------------------------------------- /bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # 3 | # NAME 4 | # bootstrap -- initialize/update docker environment 5 | 6 | # vim: set ts=2 sts=2 sw=2 et: 7 | set -e 8 | 9 | TEST_HOST=${TEST_HOST:-"127.0.0.1"} 10 | 11 | echo "Integration test host: ${TEST_HOST}" 12 | 13 | get_exposed_port() { 14 | docker compose port $1 $2 | cut -d: -f2 15 | } 16 | 17 | wait_for() { 18 | printf 'Waiting for %s... ' $1 19 | counter="0" 20 | while true 21 | do 22 | if [ "$( docker compose ps | grep $1 | grep -c healthy )" -eq 1 ]; then 23 | break 24 | fi 25 | counter=$((counter+1)) 26 | if [ "${counter}" -eq 120 ]; then 27 | echo " ERROR: container failed to start" 28 | exit 1 29 | fi 30 | sleep 1 31 | done 32 | echo 'done.' 33 | } 34 | 35 | # Ensure Docker is Running 36 | echo "Docker Information:" 37 | echo "" 38 | docker version 39 | echo "" 40 | 41 | # Activate the virtual environment 42 | if test -e env/bin/activate 43 | then 44 | . env/bin/activate 45 | fi 46 | 47 | mkdir -p build 48 | 49 | # Stop any running instances and clean up after them, then pull images 50 | docker compose down --volumes --remove-orphans 51 | docker compose pull -q 52 | docker compose up -d 53 | 54 | wait_for rabbitmq 55 | 56 | docker compose exec -T rabbitmq rabbitmqctl await_startup 57 | 58 | cat > build/test-environment<` class is used to work with RabbitMQ exchanges on an open channel. The following example shows how you can create an exchange using the :class:`rabbitpy.Exchange` class. 4 | 5 | .. code:: python 6 | 7 | import rabbitpy 8 | 9 | with rabbitpy.Connection() as connection: 10 | with connection.channel() as channel: 11 | exchange = rabbitpy.Exchange(channel, 'my-exchange') 12 | exchange.declare() 13 | 14 | In addition, there are four convenience classes (:class:`DirectExchange `, :class:`FanoutExchange `, :class:`HeadersExchange `, and :class:`TopicExchange `) for creating each built-in exchange type in RabbitMQ. 15 | 16 | API Documentation 17 | ----------------- 18 | 19 | .. autoclass:: rabbitpy.Exchange 20 | :members: 21 | :inherited-members: 22 | :member-order: bysource 23 | 24 | .. autoclass:: rabbitpy.DirectExchange 25 | :members: 26 | :inherited-members: 27 | :member-order: bysource 28 | 29 | .. autoclass:: rabbitpy.FanoutExchange 30 | :members: 31 | :inherited-members: 32 | :member-order: bysource 33 | 34 | .. autoclass:: rabbitpy.HeadersExchange 35 | :members: 36 | :inherited-members: 37 | :member-order: bysource 38 | 39 | .. autoclass:: rabbitpy.TopicExchange 40 | :members: 41 | :inherited-members: 42 | :member-order: bysource 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2019 Gavin M. Roy 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | * Neither the name of the copyright holder nor the names of its contributors may 13 | be used to endorse or promote products derived from this software without 14 | specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 19 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 20 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 23 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 24 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 25 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Deployment 2 | on: 3 | push: 4 | branches-ignore: ["*"] 5 | tags: ["*"] 6 | jobs: 7 | build: 8 | name: Build distribution 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v4 13 | 14 | - name: Set up Python 15 | uses: actions/setup-python@v5 16 | with: 17 | python-version: "3.x" 18 | 19 | - name: Install pypa/build 20 | run: python3 -m pip install build --user 21 | 22 | - name: Build a binary wheel and a source tarball 23 | run: python3 -m build 24 | 25 | - name: Store the distribution packages 26 | uses: actions/upload-artifact@v3 27 | with: 28 | name: python-package-distributions 29 | path: dist/ 30 | 31 | - name: Publish distribution to PyPI 32 | uses: pypa/gh-action-pypi-publish@release/v1 33 | 34 | publish-to-pypi: 35 | name: Publish Python distribution to PyPI 36 | if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes 37 | needs: 38 | - build 39 | runs-on: ubuntu-latest 40 | environment: 41 | name: pypi 42 | url: https://pypi.org/p/rabbitpy 43 | permissions: 44 | id-token: write 45 | steps: 46 | - name: Download all the dists 47 | uses: actions/download-artifact@v4.1.7 48 | with: 49 | name: python-package-distributions 50 | path: dist/ 51 | 52 | - name: Publish distribution to PyPI 53 | uses: pypa/gh-action-pypi-publish@release/v1 54 | -------------------------------------------------------------------------------- /docs/api/connection.rst: -------------------------------------------------------------------------------- 1 | Connection 2 | ========== 3 | rabbitpy Connection objects are used to connect to RabbitMQ. They provide a thread-safe connection to RabbitMQ that is used to authenticate and send all channel based RPC commands over. Connections use `AMQP URI syntax `_ for specifying the all of the connection information, including any connection negotiation options, such as the heartbeat interval. For more information on the various query parameters that can be specified, see the `official documentation `_. 4 | 5 | A :class:`Connection` is a normal python object that you use: 6 | 7 | .. code:: python 8 | 9 | conn = rabbitpy.Connection('amqp://guest:guest@localhost:5672/%2F') 10 | conn.close() 11 | 12 | or it can be used as a Python context manager (See :pep:`0343`): 13 | 14 | .. code:: python 15 | 16 | with rabbitpy.Connection() as conn: 17 | # Foo 18 | 19 | When it is used as a context manager with the `with` statement, when your code exits the block, the connection will automatically close. 20 | 21 | If RabbitMQ remotely closes your connection via the AMQP `Connection.Close` RPC request, rabbitpy will raise the :doc:`appropriate exception ` referenced in the request. 22 | 23 | If heartbeats are enabled (default: 5 minutes) and RabbitMQ does not send a heartbeat request in >= 2 heartbeat intervals, a :py:class:`ConnectionResetException ` will be raised. 24 | 25 | API Documentation 26 | ----------------- 27 | 28 | .. autoclass:: rabbitpy.Connection 29 | :members: 30 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | rabbitpy - RabbitMQ simplified 2 | ============================== 3 | 4 | A pure python, thread-safe, minimalistic and Pythonic BSD Licensed 5 | AMQP/RabbitMQ library that supports Python 2.7+ and Python 3.4+. 6 | rabbitpy aims to provide a simple and easy to use API for interfacing with 7 | RabbitMQ, minimizing the programming overhead often found in other libraries. 8 | 9 | |Version| |Downloads| |Status| |Coverage| |License| 10 | 11 | Installation 12 | ------------ 13 | 14 | rabbitpy may be installed via the Python package index with the tool of 15 | your choice. I prefer pip: 16 | 17 | .. code:: bash 18 | 19 | pip install rabbitpy 20 | 21 | But there's always easy_install: 22 | 23 | .. code:: bash 24 | 25 | easy_install rabbitpy 26 | 27 | Documentation 28 | ------------- 29 | 30 | Documentation is available on `ReadTheDocs `_. 31 | 32 | 33 | Requirements 34 | ------------ 35 | 36 | - `pamqp `_ 37 | 38 | Version History 39 | --------------- 40 | Available at https://rabbitpy.readthedocs.org/en/latest/history.html 41 | 42 | .. |Version| image:: https://img.shields.io/pypi/v/rabbitpy.svg? 43 | :target: http://badge.fury.io/py/rabbitpy 44 | 45 | .. |Status| image:: https://img.shields.io/travis/gmr/rabbitpy.svg? 46 | :target: https://travis-ci.org/gmr/rabbitpy 47 | 48 | .. |Coverage| image:: https://img.shields.io/codecov/c/github/gmr/rabbitpy.svg? 49 | :target: https://codecov.io/github/gmr/rabbitpy?branch=master 50 | 51 | .. |Downloads| image:: https://img.shields.io/pypi/dm/rabbitpy.svg? 52 | :target: https://pypi.python.org/pypi/rabbitpy 53 | 54 | .. |License| image:: https://img.shields.io/pypi/l/rabbitpy.svg? 55 | :target: https://rabbitpy.readthedocs.org 56 | -------------------------------------------------------------------------------- /rabbitpy/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | rabbitpy, a pythonic RabbitMQ client 3 | 4 | """ 5 | __version__ = '2.0.1' 6 | 7 | import logging 8 | 9 | from rabbitpy.amqp import AMQP 10 | from rabbitpy.connection import Connection 11 | from rabbitpy.channel import Channel 12 | from rabbitpy.exchange import Exchange 13 | from rabbitpy.exchange import DirectExchange 14 | from rabbitpy.exchange import FanoutExchange 15 | from rabbitpy.exchange import HeadersExchange 16 | from rabbitpy.exchange import TopicExchange 17 | from rabbitpy.message import Message 18 | from rabbitpy.amqp_queue import Queue 19 | from rabbitpy.tx import Tx 20 | 21 | from rabbitpy.simple import SimpleChannel 22 | from rabbitpy.simple import consume 23 | from rabbitpy.simple import get 24 | from rabbitpy.simple import publish 25 | from rabbitpy.simple import create_queue 26 | from rabbitpy.simple import delete_queue 27 | from rabbitpy.simple import create_direct_exchange 28 | from rabbitpy.simple import create_fanout_exchange 29 | from rabbitpy.simple import create_headers_exchange 30 | from rabbitpy.simple import create_topic_exchange 31 | from rabbitpy.simple import delete_exchange 32 | 33 | logging.getLogger('rabbitpy').addHandler(logging.NullHandler()) 34 | 35 | __all__ = [ 36 | '__version__', 37 | 'amqp_queue', 38 | 'channel', 39 | 'connection', 40 | 'exceptions', 41 | 'exchange', 42 | 'message', 43 | 'simple', 44 | 'tx', 45 | 'AMQP', 46 | 'Connection', 47 | 'Channel', 48 | 'SimpleChannel', 49 | 'Exchange', 50 | 'DirectExchange', 51 | 'FanoutExchange', 52 | 'HeadersExchange', 53 | 'TopicExchange', 54 | 'Message', 55 | 'Queue', 56 | 'Tx', 57 | 'consume', 58 | 'get', 59 | 'publish', 60 | 'create_queue', 61 | 'delete_queue', 62 | 'create_direct_exchange', 63 | 'create_fanout_exchange', 64 | 'create_headers_exchange', 65 | 'create_topic_exchange', 66 | 'delete_exchange' 67 | ] 68 | -------------------------------------------------------------------------------- /docs/api/queue.rst: -------------------------------------------------------------------------------- 1 | Queue 2 | ===== 3 | The :class:`Queue ` class is used to work with RabbitMQ queues on an open channel. The following example shows how you can create a queue using the :meth:`Queue.declare ` method. 4 | 5 | .. code:: python 6 | 7 | import rabbitpy 8 | 9 | with rabbitpy.Connection() as connection: 10 | with connection.channel() as channel: 11 | queue = rabbitpy.Queue(channel, 'my-queue') 12 | queue.durable = True 13 | queue.declare() 14 | 15 | To consume messages you can iterate over the Queue object itself if the defaults for the :py:meth:`Queue.__iter__() ` method work for your needs: 16 | 17 | .. code:: python 18 | 19 | with conn.channel() as channel: 20 | for message in rabbitpy.Queue(channel, 'example'): 21 | print 'Message: %r' % message 22 | message.ack() 23 | 24 | or by the :py:meth:`Queue.consume() ` method if you would like to specify `no_ack`, `prefetch_count`, or `priority`: 25 | 26 | .. code:: python 27 | 28 | with conn.channel() as channel: 29 | queue = rabbitpy.Queue(channel, 'example') 30 | for message in queue.consume(): 31 | print 'Message: %r' % message 32 | message.ack() 33 | 34 | .. warning:: If you use either the :py:class:`Queue` as an iterator method or :py:meth:`Queue.consume` method of consuming messages in PyPy, 35 | you must manually invoke :py:meth:`Queue.stop_consuming`. This is due to PyPy not predictably cleaning up after the generator 36 | used for allowing the iteration over messages. Should your code want to test to see if the code is being executed in PyPy, 37 | you can evaluate the boolean ``rabbitpy.PYPY`` constant value. 38 | 39 | API Documentation 40 | ----------------- 41 | 42 | .. autoclass:: rabbitpy.Queue 43 | :members: 44 | :special-members: 45 | -------------------------------------------------------------------------------- /docs/api/exceptions.rst: -------------------------------------------------------------------------------- 1 | Exceptions 2 | ========== 3 | rabbitpy contains two types of exceptions, exceptions that are specific to rabbitpy and exceptions that are raised as a result of a Channel or Connection closure from RabbitMQ. These exceptions will be raised to let you know when you have performed an action like redeclared a pre-existing queue with different values. Consider the following example: 4 | 5 | .. code:: python 6 | 7 | >>> import rabbitpy 8 | >>> 9 | >>> with rabbitpy.Connection() as connection: 10 | ... with connection.channel() as channel: 11 | ... queue = rabbitpy.Queue(channel, 'exception-test') 12 | ... queue.durable = True 13 | ... queue.declare() 14 | ... queue.durable = False 15 | ... queue.declare() 16 | ... 17 | Traceback (most recent call last): 18 | File "", line 7, in 19 | File "rabbitpy/connection.py", line 131, in __exit__ 20 | self._shutdown_connection() 21 | File "rabbitpy/connection.py", line 469, in _shutdown_connection 22 | self._channels[chan_id].close() 23 | File "rabbitpy/channel.py", line 124, in close 24 | super(Channel, self).close() 25 | File "rabbitpy/base.py", line 185, in close 26 | self.rpc(frame_value) 27 | File "rabbitpy/base.py", line 199, in rpc 28 | self._write_frame(frame_value) 29 | File "rabbitpy/base.py", line 311, in _write_frame 30 | raise exception 31 | rabbitpy.exceptions.AMQPPreconditionFailed: 32 | 33 | In this example, the channel that was created on the second line was closed and RabbitMQ is raising the :class:`AMQPPreconditionFailed ` exception via RPC sent to your application using the AMQP Channel.Close method. 34 | 35 | .. automodule:: rabbitpy.exceptions 36 | :members: 37 | :private-members: 38 | :undoc-members: 39 | 40 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "rabbitpy" 3 | version = "2.0.1" 4 | description = "A pure python, thread-safe, minimalistic and pythonic RabbitMQ client library" 5 | readme = "README.rst" 6 | requires-python = ">=3.8" 7 | license = { file = "LICENSE" } 8 | authors = [ { name = "Gavin M. Roy", email = "gavinmroy@gmail.com" } ] 9 | classifiers = [ 10 | "Development Status :: 5 - Production/Stable", 11 | "Intended Audience :: Developers", 12 | "License :: OSI Approved :: BSD License", 13 | "Operating System :: OS Independent", 14 | "Programming Language :: Python :: 3 :: Only", 15 | "Programming Language :: Python :: 3.8", 16 | "Programming Language :: Python :: 3.9", 17 | "Programming Language :: Python :: 3.10", 18 | "Programming Language :: Python :: 3.11", 19 | "Programming Language :: Python :: 3.12", 20 | "Programming Language :: Python :: Implementation :: CPython", 21 | "Programming Language :: Python :: Implementation :: PyPy", 22 | "Topic :: Communications", 23 | "Topic :: Internet", 24 | "Topic :: Software Development :: Libraries" 25 | ] 26 | dependencies = [ 27 | "pamqp>=2.3.0,<3.0", 28 | ] 29 | 30 | [project.optional-dependencies] 31 | dev = [ 32 | "codecov", 33 | "coverage[toml]", 34 | "mock", 35 | "flake8", 36 | "flake8-comprehensions", 37 | "flake8-deprecated", 38 | "flake8-html", 39 | "flake8-import-order", 40 | "flake8-pyproject", 41 | "flake8-quotes", 42 | "flake8-rst-docstrings", 43 | "flake8-tuple", 44 | "codeclimate-test-reporter", 45 | ] 46 | 47 | [project.urls] 48 | Documentation = "https://rabbitpy.readthedocs.io" 49 | Repository = "https://github.com/gmr/rabbitpy.git" 50 | "Code Coverage" = "https://app.codecov.io/github/gmr/rabbitpy" 51 | 52 | [tool.flake8] 53 | application-import-names = ["rabbitpy"] 54 | exclude = ["build", "ci", "docs", "env"] 55 | ignore = "RST304" 56 | import-order-style = "google" 57 | 58 | [tool.coverage.xml] 59 | output = "build/coverage.xml" 60 | 61 | [tool.coverage.run] 62 | branch = true 63 | source = ["rabbitpy"] 64 | command_line = "-m unittest discover tests --buffer --verbose" 65 | -------------------------------------------------------------------------------- /rabbitpy/heartbeat.py: -------------------------------------------------------------------------------- 1 | """ 2 | The heartbeat class implements the logic for sending heartbeats every 3 | configured interval. 4 | 5 | """ 6 | import logging 7 | import threading 8 | 9 | LOGGER = logging.getLogger(__name__) 10 | 11 | 12 | class Heartbeat(object): 13 | """Send a heartbeat frame every interval if no data has been written. 14 | 15 | :param rabbitpy.io.IO io: Used to get the # of bytes written each interval 16 | :param rabbitpy.channel0.Channel channel0: The channel that the heartbeat 17 | is sent over. 18 | 19 | """ 20 | 21 | def __init__(self, io, channel0, interval): 22 | self._channel0 = channel0 23 | self._interval = float(interval) / 2.0 24 | self._io = io 25 | self._last_written = self._io.bytes_written 26 | self._lock = threading.Lock() 27 | self._timer = None 28 | 29 | def start(self): 30 | """Start the heartbeat checker""" 31 | if not self._interval: 32 | LOGGER.debug('Heartbeats are disabled, not starting') 33 | return 34 | self._start_timer() 35 | LOGGER.debug('Heartbeat started, ensuring data is written at least ' 36 | 'every %.2f seconds', self._interval) 37 | 38 | def stop(self): 39 | """Stop the heartbeat checker""" 40 | if self._timer: 41 | self._timer.cancel() 42 | self._timer = None 43 | 44 | def _start_timer(self): 45 | """Create a new thread timer, destroying the last if it existed.""" 46 | if self._timer: 47 | del self._timer 48 | self._timer = threading.Timer(self._interval, self._maybe_send) 49 | self._timer.daemon = True 50 | self._timer.start() 51 | 52 | def _maybe_send(self): 53 | """Fired by threading.Timer every ``self._interval`` seconds to 54 | maybe send a heartbeat to the remote connection, if no other frames 55 | have been written. 56 | 57 | """ 58 | if not self._io.bytes_written - self._last_written: 59 | self._channel0.send_heartbeat() 60 | self._lock.acquire(True) 61 | self._last_written = self._io.bytes_written 62 | self._lock.release() 63 | self._start_timer() 64 | -------------------------------------------------------------------------------- /tests/utils_tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the rabbitpy utils module 3 | 4 | """ 5 | try: 6 | import unittest2 as unittest 7 | except ImportError: 8 | import unittest 9 | import sys 10 | from rabbitpy import utils 11 | 12 | # 3 Unicode Compatibility hack 13 | if sys.version_info[0] == 3: 14 | unicode = str 15 | 16 | 17 | class UtilsTestCase(unittest.TestCase): 18 | 19 | AMQP = 'amqp://guest:guest@localhost:5672/%2F?heartbeat_interval=1' 20 | AMQPS = 'amqps://guest:guest@localhost:5672/%2F?heartbeat_interval=1' 21 | 22 | NETLOC = 'guest:guest@localhost:5672' 23 | PATH = '/%2F' 24 | PARAMS = '' 25 | QUERY = 'heartbeat_interval=1' 26 | FRAGMENT = '' 27 | 28 | def test_urlparse_amqp_scheme(self): 29 | self.assertEqual(utils.urlparse(self.AMQP).scheme, 'amqp') 30 | 31 | def test_urlparse_amqps_scheme(self): 32 | self.assertEqual(utils.urlparse(self.AMQPS).scheme, 'amqps') 33 | 34 | def test_urlparse_netloc(self): 35 | self.assertEqual(utils.urlparse(self.AMQPS).netloc, self.NETLOC) 36 | 37 | def test_urlparse_url(self): 38 | self.assertEqual(utils.urlparse(self.AMQPS).path, self.PATH) 39 | 40 | def test_urlparse_params(self): 41 | self.assertEqual(utils.urlparse(self.AMQPS).params, self.PARAMS) 42 | 43 | def test_urlparse_query(self): 44 | self.assertEqual(utils.urlparse(self.AMQPS).query, self.QUERY) 45 | 46 | def test_urlparse_fragment(self): 47 | self.assertEqual(utils.urlparse(self.AMQPS).fragment, self.FRAGMENT) 48 | 49 | def test_parse_qs(self): 50 | self.assertDictEqual(utils.parse_qs(self.QUERY), 51 | {'heartbeat_interval': ['1']}) 52 | 53 | def test_is_string_str(self): 54 | self.assertTrue(utils.is_string('Foo')) 55 | 56 | def test_is_string_bytes(self): 57 | self.assertTrue(utils.is_string(b'Foo')) 58 | 59 | @unittest.skipIf(sys.version_info[0] == 3, 'No unicode obj in 3') 60 | def test_is_string_unicode(self): 61 | self.assertTrue(utils.is_string(unicode('Foo'))) 62 | 63 | def test_is_string_false_int(self): 64 | self.assertFalse(utils.is_string(123)) 65 | 66 | def test_unqoute(self): 67 | self.assertEqual(utils.unquote(self.PATH), '//') 68 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. rabbitpy documentation master file, created by 2 | sphinx-quickstart on Wed Mar 27 18:31:37 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | rabbitpy: RabbitMQ Simplified 7 | ============================= 8 | rabbitpy is a pure python, thread-safe [#]_, and pythonic BSD Licensed AMQP/RabbitMQ 9 | library that supports Python 2.7+ and 3.4+. rabbitpy aims to provide a simple 10 | and easy to use API for interfacing with RabbitMQ, minimizing the programming 11 | overhead often found in other libraries. 12 | 13 | |Version| 14 | 15 | Installation 16 | ------------ 17 | rabbitpy is available from the `Python Package Index `_ and can be 18 | installed by running :command:`easy_install rabbitpy` or :command:`pip install rabbitpy` 19 | 20 | API Documentation 21 | ----------------- 22 | rabbitpy is designed to have as simple and pythonic of an API as possible while 23 | still remaining true to RabbitMQ and to the AMQP 0-9-1 specification. There are 24 | two basic ways to interact with rabbitpy, using the simple wrapper methods: 25 | 26 | .. toctree:: 27 | :glob: 28 | :maxdepth: 2 29 | 30 | simple 31 | 32 | And by using the core objects: 33 | 34 | .. toctree:: 35 | :glob: 36 | :maxdepth: 1 37 | 38 | api/* 39 | 40 | .. [#] If you're looking to use rabbitpy in a multi-threaded application, you should the notes about multi-threaded use in :doc:`threads`. 41 | 42 | Examples 43 | -------- 44 | .. toctree:: 45 | :maxdepth: 2 46 | :glob: 47 | 48 | examples/* 49 | 50 | Issues 51 | ------ 52 | Please report any issues to the Github repo at `https://github.com/gmr/rabbitpy/issues `_ 53 | 54 | Source 55 | ------ 56 | rabbitpy source is available on Github at `https://github.com/gmr/rabbitpy `_ 57 | 58 | Version History 59 | --------------- 60 | See :doc:`history` 61 | 62 | Inspiration 63 | ----------- 64 | rabbitpy's simple and more pythonic interface is inspired by `Kenneth Reitz's `_ awesome work on `requests `_. 65 | 66 | Indices and tables 67 | ------------------ 68 | 69 | * :ref:`genindex` 70 | * :ref:`modindex` 71 | * :ref:`search` 72 | 73 | 74 | .. |Version| image:: https://badge.fury.io/py/rabbitpy.svg? 75 | :target: http://badge.fury.io/py/rabbitpy 76 | -------------------------------------------------------------------------------- /tests/channel_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the rabbitpy.channel classes 3 | 4 | """ 5 | from rabbitpy import exceptions 6 | 7 | from tests import helpers 8 | 9 | class ServerCapabilitiesTest(helpers.TestCase): 10 | 11 | def test_basic_nack_disabled(self): 12 | self.channel._server_capabilities['basic.nack'] = False 13 | self.assertFalse(self.channel._supports_basic_nack) 14 | 15 | def test_basic_nack_enabled(self): 16 | self.channel._server_capabilities['basic.nack'] = True 17 | self.assertTrue(self.channel._supports_basic_nack) 18 | 19 | def test_consumer_cancel_notify_disabled(self): 20 | self.channel._server_capabilities['consumer_cancel_notify'] = False 21 | self.assertFalse(self.channel._supports_consumer_cancel_notify) 22 | 23 | def test_consumer_cancel_notify_enabled(self): 24 | self.channel._server_capabilities['consumer_cancel_notify'] = True 25 | self.assertTrue(self.channel._supports_consumer_cancel_notify) 26 | 27 | def test_consumer_priorities_disabled(self): 28 | self.channel._server_capabilities['consumer_priorities'] = False 29 | self.assertFalse(self.channel._supports_consumer_priorities) 30 | 31 | def test_consumer_priorities_enabled(self): 32 | self.channel._server_capabilities['consumer_priorities'] = True 33 | self.assertTrue(self.channel._supports_consumer_priorities) 34 | 35 | def test_per_consumer_qos_disabled(self): 36 | self.channel._server_capabilities['per_consumer_qos'] = False 37 | self.assertFalse(self.channel._supports_per_consumer_qos) 38 | 39 | def test_per_consumer_qos_enabled(self): 40 | self.channel._server_capabilities['per_consumer_qos'] = True 41 | self.assertTrue(self.channel._supports_per_consumer_qos) 42 | 43 | def test_publisher_confirms_disabled(self): 44 | self.channel._server_capabilities['publisher_confirms'] = False 45 | self.assertFalse(self.channel._supports_publisher_confirms) 46 | 47 | def test_publisher_confirms_enabled(self): 48 | self.channel._server_capabilities['publisher_confirms'] = True 49 | self.assertTrue(self.channel._supports_publisher_confirms) 50 | 51 | def test_invoking_consume_raises(self): 52 | self.channel._server_capabilities['consumer_priorities'] = False 53 | self.assertRaises(exceptions.NotSupportedError, 54 | self.channel._consume, self, True, 100) 55 | 56 | def test_invoking_basic_nack_raises(self): 57 | self.channel._server_capabilities['basic_nack'] = False 58 | self.assertRaises(exceptions.NotSupportedError, 59 | self.channel._multi_nack, 100) 60 | 61 | def test_invoking_enable_publisher_confirms_raises(self): 62 | self.channel._server_capabilities['publisher_confirms'] = False 63 | self.assertRaises(exceptions.NotSupportedError, 64 | self.channel.enable_publisher_confirms) 65 | -------------------------------------------------------------------------------- /tests/events_tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the rabbitpy events class 3 | 4 | """ 5 | import mock 6 | import threading 7 | try: 8 | import unittest2 as unittest 9 | except ImportError: 10 | import unittest 11 | 12 | from rabbitpy import events 13 | 14 | 15 | class BaseEventsTest(unittest.TestCase): 16 | 17 | def setUp(self): 18 | self._events = events.Events() 19 | 20 | def tearDown(self): 21 | del self._events 22 | 23 | 24 | class EventClearTests(BaseEventsTest): 25 | 26 | def test_invalid_event(self): 27 | self.assertIsNone(self._events.clear(0)) 28 | 29 | def test_valid_clear_returns_true(self): 30 | self._events.set(events.CHANNEL0_OPENED) 31 | self.assertTrue(self._events.clear(events.CHANNEL0_OPENED)) 32 | 33 | def test_unset_event_returns_false(self): 34 | self.assertFalse(self._events.clear(events.CHANNEL0_OPENED)) 35 | 36 | 37 | class EventInitTests(BaseEventsTest): 38 | 39 | def test_all_events_created(self): 40 | try: 41 | cls = threading._Event 42 | except AttributeError: 43 | cls = threading.Event 44 | for event in events.DESCRIPTIONS.keys(): 45 | self.assertIsInstance(self._events._events[event], cls, 46 | type(self._events._events[event])) 47 | 48 | 49 | class EventIsSetTests(BaseEventsTest): 50 | 51 | def test_invalid_event(self): 52 | self.assertIsNone(self._events.is_set(0)) 53 | 54 | def test_valid_is_set_returns_true(self): 55 | self._events.set(events.CHANNEL0_CLOSED) 56 | self.assertTrue(self._events.is_set(events.CHANNEL0_CLOSED)) 57 | 58 | def test_unset_event_returns_false(self): 59 | self.assertFalse(self._events.is_set(events.CHANNEL0_OPENED)) 60 | 61 | 62 | class EventSetTests(BaseEventsTest): 63 | 64 | def test_invalid_event(self): 65 | self.assertIsNone(self._events.set(0)) 66 | 67 | def test_valid_set_returns_true(self): 68 | self.assertTrue(self._events.set(events.CHANNEL0_CLOSED)) 69 | 70 | def test_already_set_event_returns_false(self): 71 | self._events.set(events.CHANNEL0_OPENED) 72 | self.assertFalse(self._events.set(events.CHANNEL0_OPENED)) 73 | 74 | 75 | class EventWaitTests(BaseEventsTest): 76 | 77 | def test_invalid_event(self): 78 | self.assertIsNone(self._events.wait(0)) 79 | 80 | def test_blocking_wait_returns_true(self): 81 | try: 82 | cls = threading._Event 83 | except AttributeError: 84 | cls = threading.Event 85 | with mock.patch.object(cls, 'wait') as mock_method: 86 | mock_method.return_value = True 87 | self.assertTrue(self._events.wait(events.CHANNEL0_CLOSED)) 88 | 89 | def test_blocking_wait_returns_false(self): 90 | try: 91 | cls = threading._Event 92 | except AttributeError: 93 | cls = threading.Event 94 | with mock.patch.object(cls, 'wait') as mock_method: 95 | mock_method.return_value = False 96 | self.assertFalse(self._events.wait(events.CHANNEL0_CLOSED, 1)) 97 | -------------------------------------------------------------------------------- /rabbitpy/utils.py: -------------------------------------------------------------------------------- 1 | """Utilities to make Python 3 support easier, providing wrapper methods which 2 | can call the appropriate method for either Python 2 or Python 3 but creating 3 | a single API point for rabbitpy to use. 4 | 5 | """ 6 | import collections 7 | # pylint: disable=unused-import,import-error 8 | try: 9 | import Queue as queue 10 | except ImportError: 11 | import queue 12 | import platform 13 | import socket 14 | # pylint: disable=import-error 15 | try: 16 | from urllib import parse as _urlparse 17 | except ImportError: 18 | import urlparse as _urlparse 19 | 20 | from pamqp import PYTHON3 21 | 22 | PYPY = platform.python_implementation() == 'PyPy' 23 | 24 | Parsed = collections.namedtuple('Parsed', 25 | 'scheme,netloc,path,params,query,fragment,' 26 | 'username,password,hostname,port') 27 | 28 | 29 | def maybe_utf8_encode(value): 30 | """Cross-python version method that will attempt to utf-8 encode a string. 31 | 32 | :param mixed value: The value to maybe encode 33 | :return: str 34 | 35 | """ 36 | 37 | if PYTHON3: 38 | if is_string(value) and not isinstance(value, bytes): 39 | return bytes(value, 'utf-8') 40 | return value 41 | if isinstance(value, unicode): # pylint: disable=undefined-variable 42 | return value.encode('utf-8') 43 | return value 44 | 45 | 46 | def parse_qs(query_string): 47 | """Cross-python version method for parsing a query string. 48 | 49 | :param str query_string: The query string to parse 50 | :return: tuple 51 | """ 52 | return _urlparse.parse_qs(query_string) 53 | 54 | 55 | def urlparse(url): 56 | """Parse a URL, returning a named tuple result. 57 | 58 | :param str url: The URL to parse 59 | :rtype: collections.namedtuple 60 | 61 | """ 62 | value = 'http%s' % url[4:] if url[:4] == 'amqp' else url 63 | parsed = _urlparse.urlparse(value) 64 | return Parsed(parsed.scheme.replace('http', 'amqp'), parsed.netloc, 65 | parsed.path, parsed.params, parsed.query, parsed.fragment, 66 | parsed.username, 67 | parsed.password, 68 | parsed.hostname, parsed.port) 69 | 70 | 71 | def unquote(value): 72 | """Cross-python version method for unquoting a URI value. 73 | 74 | :param str value: The value to unquote 75 | :rtype: str 76 | 77 | """ 78 | return _urlparse.unquote(value) 79 | 80 | 81 | def is_string(value): 82 | """Check to see if the value is a string in Python 2 and 3. 83 | 84 | :param bytes|str|unicode value: The value to check 85 | :rtype: bool 86 | 87 | """ 88 | checks = [isinstance(value, bytes), isinstance(value, str)] 89 | if not PYTHON3: 90 | # pylint: disable=undefined-variable 91 | checks.append(isinstance(value, unicode)) 92 | return any(checks) 93 | 94 | 95 | def trigger_write(sock): 96 | """Notifies the IO loop we need to write a frame by writing a byte 97 | to a local socket. 98 | 99 | """ 100 | try: 101 | sock.send(b'0') 102 | except socket.error: 103 | pass 104 | -------------------------------------------------------------------------------- /tests/test_exchange.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the rabbitpy.exchange classes 3 | 4 | """ 5 | import mock 6 | from pamqp import specification 7 | 8 | from rabbitpy import exchange 9 | 10 | from tests import helpers 11 | 12 | 13 | class TxTests(helpers.TestCase): 14 | 15 | @mock.patch('rabbitpy.exchange.Exchange._rpc') 16 | def test_bind_sends_exchange_declare(self, rpc): 17 | rpc.return_value = specification.Exchange.DeclareOk 18 | obj = exchange.Exchange(self.channel, 'foo') 19 | obj.declare() 20 | self.assertIsInstance(rpc.mock_calls[0][1][0], 21 | specification.Exchange.Declare) 22 | 23 | @mock.patch('rabbitpy.exchange.Exchange._rpc') 24 | def test_bind_sends_exchange_delete(self, rpc): 25 | rpc.return_value = specification.Exchange.DeleteOk 26 | obj = exchange.Exchange(self.channel, 'foo') 27 | obj.delete() 28 | self.assertIsInstance(rpc.mock_calls[0][1][0], 29 | specification.Exchange.Delete) 30 | 31 | @mock.patch('rabbitpy.exchange.Exchange._rpc') 32 | def test_bind_sends_exchange_bind(self, rpc): 33 | rpc.return_value = specification.Exchange.BindOk 34 | obj = exchange.Exchange(self.channel, 'foo') 35 | obj.bind('a', 'b') 36 | self.assertIsInstance(rpc.mock_calls[0][1][0], 37 | specification.Exchange.Bind) 38 | 39 | @mock.patch('rabbitpy.exchange.Exchange._rpc') 40 | def test_bind_sends_exchange_unbind(self, rpc): 41 | rpc.return_value = specification.Exchange.UnbindOk 42 | obj = exchange.Exchange(self.channel, 'foo') 43 | obj.unbind('a', 'b') 44 | self.assertIsInstance(rpc.mock_calls[0][1][0], 45 | specification.Exchange.Unbind) 46 | 47 | @mock.patch('rabbitpy.exchange.Exchange._rpc') 48 | def test_bind_sends_exchange_bind_obj(self, rpc): 49 | rpc.return_value = specification.Exchange.BindOk 50 | obj = exchange.Exchange(self.channel, 'foo') 51 | val = mock.Mock() 52 | val.name = 'bar' 53 | obj.bind(val, 'b') 54 | self.assertIsInstance(rpc.mock_calls[0][1][0], 55 | specification.Exchange.Bind) 56 | 57 | @mock.patch('rabbitpy.exchange.Exchange._rpc') 58 | def test_bind_sends_exchange_unbind_obj(self, rpc): 59 | rpc.return_value = specification.Exchange.UnbindOk 60 | obj = exchange.Exchange(self.channel, 'foo') 61 | val = mock.Mock() 62 | val.name = 'bar' 63 | obj.unbind(val, 'b') 64 | self.assertIsInstance(rpc.mock_calls[0][1][0], 65 | specification.Exchange.Unbind) 66 | 67 | 68 | class DirectExchangeCreationTests(helpers.TestCase): 69 | 70 | def test_init_creates_direct_exchange(self): 71 | obj = exchange.DirectExchange(self.channel, 'direct-test') 72 | self.assertEqual(obj.type, 'direct') 73 | 74 | 75 | class FanoutExchangeCreationTests(helpers.TestCase): 76 | 77 | def test_init_creates_direct_exchange(self): 78 | obj = exchange.FanoutExchange(self.channel, 'fanout-test') 79 | self.assertEqual(obj.type, 'fanout') 80 | 81 | 82 | class HeadersExchangeCreationTests(helpers.TestCase): 83 | 84 | def test_init_creates_direct_exchange(self): 85 | obj = exchange.HeadersExchange(self.channel, 'headers-test') 86 | self.assertEqual(obj.type, 'headers') 87 | 88 | 89 | class TopicExchangeCreationTests(helpers.TestCase): 90 | 91 | def test_init_creates_direct_exchange(self): 92 | obj = exchange.TopicExchange(self.channel, 'topic-test') 93 | self.assertEqual(obj.type, 'topic') 94 | -------------------------------------------------------------------------------- /tests/test_tx.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the rabbitpy.tx classes 3 | 4 | """ 5 | import mock 6 | from pamqp import specification 7 | 8 | from rabbitpy import exceptions, tx 9 | 10 | from tests import helpers 11 | 12 | 13 | class TxTests(helpers.TestCase): 14 | 15 | def test_obj_creation_does_not_invoke_select(self): 16 | with mock.patch('rabbitpy.tx.Tx.select') as select: 17 | transaction = tx.Tx(self.channel) 18 | self.assertFalse(transaction._selected) 19 | select.assert_not_called() 20 | 21 | def test_enter_invokes_select(self): 22 | with mock.patch('rabbitpy.tx.Tx.select') as select: 23 | with tx.Tx(self.channel): 24 | select.assert_called_once() 25 | 26 | @mock.patch('rabbitpy.tx.Tx._rpc') 27 | def test_exit_invokes_commit(self, rpc): 28 | rpc.return_value = specification.Tx.SelectOk 29 | with mock.patch('rabbitpy.tx.Tx.select') as select: 30 | with mock.patch('rabbitpy.tx.Tx.commit') as commit: 31 | with tx.Tx(self.channel) as transaction: 32 | transaction._selected = True 33 | commit.assert_called_once() 34 | 35 | @mock.patch('rabbitpy.tx.Tx._rpc') 36 | def test_exit_on_exception_invokes_commit_with_selected(self, rpc): 37 | rpc.return_value = specification.Tx.SelectOk 38 | with mock.patch('rabbitpy.tx.Tx.select') as select: 39 | with mock.patch('rabbitpy.tx.Tx.rollback') as rollback: 40 | try: 41 | with tx.Tx(self.channel) as transaction: 42 | transaction._selected = True 43 | raise exceptions.AMQPChannelError() 44 | except exceptions.AMQPChannelError: 45 | pass 46 | rollback.assert_called_once() 47 | 48 | @mock.patch('rabbitpy.tx.Tx._rpc') 49 | def test_select_invokes_rpc_with_tx_select(self, rpc): 50 | rpc.return_value = specification.Tx.CommitOk 51 | with tx.Tx(self.channel): 52 | pass 53 | self.assertIsInstance(rpc.mock_calls[0][1][0], 54 | specification.Tx.Select) 55 | 56 | @mock.patch('rabbitpy.tx.Tx._rpc') 57 | def test_commit_invokes_rpc_with_tx_commit(self, rpc): 58 | rpc.return_value = specification.Tx.SelectOk 59 | obj = tx.Tx(self.channel) 60 | obj.select() 61 | rpc.return_value = specification.Tx.CommitOk 62 | obj.commit() 63 | self.assertIsInstance(rpc.mock_calls[1][1][0], 64 | specification.Tx.Commit) 65 | 66 | @mock.patch('rabbitpy.tx.Tx._rpc') 67 | def test_commit_raises_when_channel_closed(self, rpc): 68 | obj = tx.Tx(self.channel) 69 | obj.select() 70 | rpc.side_effect = exceptions.ChannelClosedException 71 | self.assertRaises(exceptions.NoActiveTransactionError, 72 | obj.commit) 73 | 74 | @mock.patch('rabbitpy.tx.Tx._rpc') 75 | def test_rollback_invokes_rpc_with_tx_rollback(self, rpc): 76 | rpc.return_value = specification.Tx.SelectOk 77 | obj = tx.Tx(self.channel) 78 | obj.select() 79 | rpc.return_value = specification.Tx.RollbackOk 80 | obj.rollback() 81 | self.assertIsInstance(rpc.mock_calls[1][1][0], 82 | specification.Tx.Rollback) 83 | 84 | @mock.patch('rabbitpy.tx.Tx._rpc') 85 | def test_rollback_raises_when_channel_closed(self, rpc): 86 | obj = tx.Tx(self.channel) 87 | obj.select() 88 | rpc.side_effect = exceptions.ChannelClosedException 89 | self.assertRaises(exceptions.NoActiveTransactionError, 90 | obj.rollback) 91 | -------------------------------------------------------------------------------- /docs/threads.rst: -------------------------------------------------------------------------------- 1 | Multi-threaded Use Notes 2 | ======================== 3 | To ensure that the network communication module at the core of rabbitpy is 4 | thread safe, the :py:class:`rabbitpy.io.IO` class is a daemonic Python thread 5 | that uses a combination of :py:class:`threading.Event`, :py:class:`Queue.Queue`, 6 | and a local cross-platform implementation of a read-write socket pair in 7 | :py:attr:`rabbitpy.IO.write_trigger`. 8 | 9 | While ensuring that the core socket IO and dispatching of AMQP frames across 10 | threads goes a long way to make sure that multi-threaded applications can safely 11 | use rabbitpy, it does not protect against cross-thread channel utilization. 12 | 13 | Due to the way that channels events are managed, it is recommend that you restrict 14 | the use of a channel to an individual thread. By not sharing channels across 15 | threads, you will ensure that you do not accidentally create issues with 16 | channel state in the AMQP protocol. As an asynchronous RPC style protocol, when 17 | you issue commands, such as a queue declaration, or are publishing a message, 18 | there are expectations in the conversation on a channel about the order of 19 | events and frames sent and received. 20 | 21 | The following example uses the main Python thread to connect to RabbitMQ and 22 | then spawns a thread for publishing and a thread for consuming. 23 | 24 | .. code :: python 25 | 26 | import rabbitpy 27 | import threading 28 | 29 | EXCHANGE = 'threading_example' 30 | QUEUE = 'threading_queue' 31 | ROUTING_KEY = 'test' 32 | MESSAGE_COUNT = 100 33 | 34 | 35 | def consumer(connection): 36 | """Consume MESSAGE_COUNT messages on the connection and then exit. 37 | 38 | :param rabbitpy.Connection connection: The connection to consume on 39 | 40 | """ 41 | received = 0 42 | with connection.channel() as channel: 43 | for message in rabbitpy.Queue(channel, QUEUE).consume_messages(): 44 | print message.body 45 | message.ack() 46 | received += 1 47 | if received == MESSAGE_COUNT: 48 | break 49 | 50 | 51 | def publisher(connection): 52 | """Pubilsh up to MESSAGE_COUNT messages on connection 53 | on an individual thread. 54 | 55 | :param rabbitpy.Connection connection: The connection to publish on 56 | 57 | """ 58 | with connection.channel() as channel: 59 | for index in range(0, MESSAGE_COUNT): 60 | message = rabbitpy.Message(channel, 'Message #%i' % index) 61 | message.publish(EXCHANGE, ROUTING_KEY) 62 | 63 | 64 | # Connect to RabbitMQ 65 | with rabbitpy.Connection() as connection: 66 | 67 | # Open the channel, declare and bind the exchange and queue 68 | with connection.channel() as channel: 69 | 70 | # Declare the exchange 71 | exchange = rabbitpy.Exchange(channel, EXCHANGE) 72 | exchange.declare() 73 | 74 | # Declare the queue 75 | queue = rabbitpy.Queue(channel, QUEUE) 76 | queue.declare() 77 | 78 | # Bind the queue to the exchange 79 | queue.bind(EXCHANGE, ROUTING_KEY) 80 | 81 | 82 | # Pass in the kwargs 83 | kwargs = {'connection': connection} 84 | 85 | # Start the consumer thread 86 | consumer_thread = threading.Thread(target=consumer, kwargs=kwargs) 87 | consumer_thread.start() 88 | 89 | # Start the pubisher thread 90 | publisher_thread = threading.Thread(target=publisher, kwargs=kwargs) 91 | publisher_thread.start() 92 | 93 | # Join the consumer thread, waiting for it to consume all MESSAGE_COUNT messages 94 | consumer_thread.join() 95 | 96 | -------------------------------------------------------------------------------- /rabbitpy/tx.py: -------------------------------------------------------------------------------- 1 | """ 2 | The TX or transaction class implements transactional functionality in RabbitMQ 3 | and allows for any AMQP command to be issued, then committed or rolled back. 4 | 5 | """ 6 | import logging 7 | from pamqp import specification as spec 8 | 9 | from rabbitpy import base 10 | from rabbitpy import exceptions 11 | 12 | LOGGER = logging.getLogger(__name__) 13 | 14 | 15 | class Tx(base.AMQPClass): 16 | """Work with transactions 17 | 18 | The Tx class allows publish and ack operations to be batched into atomic 19 | units of work. The intention is that all publish and ack requests issued 20 | within a transaction will complete successfully or none of them will. 21 | Servers SHOULD implement atomic transactions at least where all publish or 22 | ack requests affect a single queue. Transactions that cover multiple 23 | queues may be non-atomic, given that queues can be created and destroyed 24 | asynchronously, and such events do not form part of any transaction. 25 | Further, the behaviour of transactions with respect to the immediate and 26 | mandatory flags on Basic.Publish methods is not defined. 27 | 28 | :param channel: The channel object to start the transaction on 29 | :type channel: :py:class:`rabbitpy.channel.Channel` 30 | 31 | """ 32 | def __init__(self, channel): 33 | super(Tx, self).__init__(channel, 'Tx') 34 | self._selected = False 35 | 36 | def __enter__(self): 37 | """For use as a context manager, return a handle to this object 38 | instance. 39 | 40 | :rtype: Connection 41 | 42 | """ 43 | self.select() 44 | return self 45 | 46 | def __exit__(self, exc_type, exc_val, exc_tb): 47 | """When leaving the context, examine why the context is leaving, if 48 | it's an exception or what. 49 | 50 | """ 51 | if exc_type: 52 | LOGGER.warning('Exiting Transaction on exception: %r', exc_val) 53 | if self._selected: 54 | self.rollback() 55 | raise exc_val 56 | else: 57 | LOGGER.debug('Committing transaction on exit of context block') 58 | if self._selected: 59 | self.commit() 60 | 61 | def select(self): 62 | """Select standard transaction mode 63 | 64 | This method sets the channel to use standard transactions. The client 65 | must use this method at least once on a channel before using the Commit 66 | or Rollback methods. 67 | 68 | :rtype: bool 69 | 70 | """ 71 | response = self._rpc(spec.Tx.Select()) 72 | result = isinstance(response, spec.Tx.SelectOk) 73 | self._selected = result 74 | return result 75 | 76 | def commit(self): 77 | """Commit the current transaction 78 | 79 | This method commits all message publications and acknowledgments 80 | performed in the current transaction. A new transaction starts 81 | immediately after a commit. 82 | 83 | :raises: rabbitpy.exceptions.NoActiveTransactionError 84 | :rtype: bool 85 | 86 | """ 87 | try: 88 | response = self._rpc(spec.Tx.Commit()) 89 | except exceptions.ChannelClosedException as error: 90 | LOGGER.warning('Error committing transaction: %s', error) 91 | raise exceptions.NoActiveTransactionError() 92 | self._selected = False 93 | return isinstance(response, spec.Tx.CommitOk) 94 | 95 | def rollback(self): 96 | """Abandon the current transaction 97 | 98 | This method abandons all message publications and acknowledgments 99 | performed in the current transaction. A new transaction starts 100 | immediately after a rollback. Note that unacked messages will not be 101 | automatically redelivered by rollback; if that is required an explicit 102 | recover call should be issued. 103 | 104 | :raises: rabbitpy.exceptions.NoActiveTransactionError 105 | :rtype: bool 106 | 107 | """ 108 | try: 109 | response = self._rpc(spec.Tx.Rollback()) 110 | except exceptions.ChannelClosedException as error: 111 | LOGGER.warning('Error rolling back transaction: %s', error) 112 | raise exceptions.NoActiveTransactionError() 113 | self._selected = False 114 | return isinstance(response, spec.Tx.RollbackOk) 115 | -------------------------------------------------------------------------------- /rabbitpy/events.py: -------------------------------------------------------------------------------- 1 | """ 2 | Common rabbitpy events 3 | 4 | """ 5 | import logging 6 | import threading 7 | 8 | LOGGER = logging.getLogger(__name__) 9 | 10 | CHANNEL0_CLOSE = 0x01 11 | CHANNEL0_CLOSED = 0x02 12 | CHANNEL0_OPENED = 0x03 13 | CONNECTION_BLOCKED = 0x04 14 | CONNECTION_EVENT = 0x05 15 | EXCEPTION_RAISED = 0x06 16 | SOCKET_CLOSE = 0x07 17 | SOCKET_CLOSED = 0x08 18 | SOCKET_OPENED = 0x09 19 | 20 | DESCRIPTIONS = {0x01: 'Channel 0 Close Requested', 21 | 0x02: 'Channel 0 Closed', 22 | 0x03: 'Channel 0 Opened', 23 | 0x04: 'Connection is blocked', 24 | 0x05: 'Connection Event Occurred', 25 | 0x06: 'Exception Raised', 26 | 0x07: 'Socket Close Requested', 27 | 0x08: 'Socket Closed', 28 | 0x09: 'Socket Connected'} 29 | 30 | 31 | def description(event_id): 32 | """Return the text description for an event""" 33 | return DESCRIPTIONS.get(event_id, event_id) 34 | 35 | 36 | class Events(object): 37 | """All events that get triggered in rabbitpy are funneled through this 38 | object for a common structure and method for raising and checking for them. 39 | 40 | """ 41 | def __init__(self): 42 | """Create a new instance of Events""" 43 | self._events = self._create_event_objects() 44 | 45 | @staticmethod 46 | def _create_event_objects(): 47 | """Events are used like signals across threads for communicating state 48 | changes, used by the various threaded objects to communicate with each 49 | other when an action needs to be taken. 50 | 51 | :rtype: dict 52 | 53 | """ 54 | events = dict() 55 | for event in [CHANNEL0_CLOSE, 56 | CHANNEL0_CLOSED, 57 | CHANNEL0_OPENED, 58 | CONNECTION_BLOCKED, 59 | CONNECTION_EVENT, 60 | EXCEPTION_RAISED, 61 | SOCKET_CLOSE, 62 | SOCKET_CLOSED, 63 | SOCKET_OPENED]: 64 | events[event] = threading.Event() 65 | return events 66 | 67 | def clear(self, event_id): 68 | """Clear a set event, returning bool indicating success and None for 69 | an invalid event. 70 | 71 | :param int event_id: The event to set 72 | :rtype: bool 73 | 74 | """ 75 | if event_id not in self._events: 76 | LOGGER.debug('Event does not exist: %s', description(event_id)) 77 | return None 78 | 79 | if not self.is_set(event_id): 80 | LOGGER.debug('Event is not set: %s', description(event_id)) 81 | return False 82 | 83 | self._events[event_id].clear() 84 | return True 85 | 86 | def is_set(self, event_id): 87 | """Check if an event is triggered. Returns bool indicating state of the 88 | event being set. If the event is invalid, a None is returned instead. 89 | 90 | :param int event_id: The event to fire 91 | :rtype: bool 92 | 93 | """ 94 | if event_id not in self._events: 95 | LOGGER.debug('Event does not exist: %s', description(event_id)) 96 | return None 97 | return self._events[event_id].is_set() 98 | 99 | def set(self, event_id): 100 | """Trigger an event to fire. Returns bool indicating success in firing 101 | the event. If the event is not valid, return None. 102 | 103 | :param int event_id: The event to fire 104 | :rtype: bool 105 | 106 | """ 107 | if event_id not in self._events: 108 | LOGGER.debug('Event does not exist: %s', description(event_id)) 109 | return None 110 | 111 | if self.is_set(event_id): 112 | LOGGER.debug('Event is already set: %s', description(event_id)) 113 | return False 114 | 115 | self._events[event_id].set() 116 | return True 117 | 118 | def wait(self, event_id, timeout=1): 119 | """Wait for an event to be set for up to `timeout` seconds. If 120 | `timeout` is None, block until the event is set. If the event is 121 | invalid, None will be returned, otherwise False is used to indicate 122 | the event is still not set when using a timeout. 123 | 124 | :param int event_id: The event to wait for 125 | :param float timeout: The number of seconds to wait 126 | 127 | """ 128 | if event_id not in self._events: 129 | LOGGER.debug('Event does not exist: %s', description(event_id)) 130 | return None 131 | LOGGER.debug('Waiting for %i seconds on event: %s', 132 | timeout, description(event_id)) 133 | return self._events[event_id].wait(timeout) 134 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/rabbitpy.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/rabbitpy.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/rabbitpy" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/rabbitpy" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /rabbitpy/exchange.py: -------------------------------------------------------------------------------- 1 | """ 2 | The :py:class:`Exchange` class is used to create and manage exchanges in 3 | RabbitMQ and provides four classes as wrappers: 4 | 5 | * :py:class:`DirectExchange` 6 | * :py:class:`FanoutExchange` 7 | * :py:class:`HeadersExchange` 8 | * :py:class:`TopicExchange` 9 | 10 | """ 11 | import logging 12 | from pamqp import specification 13 | 14 | from rabbitpy import base 15 | 16 | LOGGER = logging.getLogger(__name__) 17 | 18 | 19 | class _Exchange(base.AMQPClass): 20 | """Exchange class for interacting with an exchange in RabbitMQ including 21 | declaration, binding and deletion. 22 | 23 | :param channel: The channel object to communicate on 24 | :type channel: :py:class:`rabbitpy.channel.Channel` 25 | :param str name: The name of the exchange 26 | :param str exchange_type: The exchange type 27 | :param bool durable: Request a durable exchange 28 | :param bool auto_delete: Automatically delete when not in use 29 | :param dict arguments: Optional key/value arguments 30 | 31 | """ 32 | durable = False 33 | arguments = dict() 34 | auto_delete = False 35 | type = 'direct' 36 | 37 | def __init__(self, channel, name, durable=False, auto_delete=False, 38 | arguments=None): 39 | """Create a new instance of the exchange object.""" 40 | super(_Exchange, self).__init__(channel, name) 41 | self.durable = durable 42 | self.auto_delete = auto_delete 43 | self.arguments = arguments or dict() 44 | 45 | def bind(self, source, routing_key=None): 46 | """Bind to another exchange with the routing key. 47 | 48 | :param source: The exchange to bind to 49 | :type source: str or :py:class:`rabbitpy.Exchange` 50 | :param str routing_key: The routing key to use 51 | 52 | """ 53 | if hasattr(source, 'name'): 54 | source = source.name 55 | self._rpc(specification.Exchange.Bind(destination=self.name, 56 | source=source, 57 | routing_key=routing_key)) 58 | 59 | def declare(self, passive=False): 60 | """Declare the exchange with RabbitMQ. If passive is True and the 61 | command arguments do not match, the channel will be closed. 62 | 63 | :param bool passive: Do not actually create the exchange 64 | 65 | """ 66 | self._rpc(specification.Exchange.Declare(exchange=self.name, 67 | exchange_type=self.type, 68 | durable=self.durable, 69 | passive=passive, 70 | auto_delete=self.auto_delete, 71 | arguments=self.arguments)) 72 | 73 | def delete(self, if_unused=False): 74 | """Delete the exchange from RabbitMQ. 75 | 76 | :param bool if_unused: Delete only if unused 77 | 78 | """ 79 | self._rpc(specification.Exchange.Delete(exchange=self.name, 80 | if_unused=if_unused)) 81 | 82 | def unbind(self, source, routing_key=None): 83 | """Unbind the exchange from the source exchange with the 84 | routing key. If routing key is None, use the queue or exchange name. 85 | 86 | :param source: The exchange to unbind from 87 | :type source: str or :py:class:`rabbitpy.Exchange` 88 | :param str routing_key: The routing key that binds them 89 | 90 | """ 91 | if hasattr(source, 'name'): 92 | source = source.name 93 | self._rpc(specification.Exchange.Unbind(destination=self.name, 94 | source=source, 95 | routing_key=routing_key)) 96 | 97 | 98 | class Exchange(_Exchange): 99 | """Exchange class for interacting with an exchange in RabbitMQ including 100 | declaration, binding and deletion. 101 | 102 | :param channel: The channel object to communicate on 103 | :type channel: :py:class:`rabbitpy.channel.Channel` 104 | :param str name: The name of the exchange 105 | :param str exchange_type: The exchange type 106 | :param bool durable: Request a durable exchange 107 | :param bool auto_delete: Automatically delete when not in use 108 | :param dict arguments: Optional key/value arguments 109 | 110 | """ 111 | def __init__(self, channel, name, exchange_type='direct', 112 | durable=False, auto_delete=False, 113 | arguments=None): 114 | """Create a new instance of the exchange object.""" 115 | self.type = exchange_type 116 | super(Exchange, self).__init__(channel, name, durable, auto_delete, 117 | arguments) 118 | 119 | 120 | class DirectExchange(_Exchange): 121 | """The DirectExchange class is used for interacting with direct exchanges 122 | only. 123 | 124 | :param channel: The channel object to communicate on 125 | :type channel: :py:class:`rabbitpy.channel.Channel` 126 | :param str name: The name of the exchange 127 | :param bool durable: Request a durable exchange 128 | :param bool auto_delete: Automatically delete when not in use 129 | :param dict arguments: Optional key/value arguments 130 | 131 | """ 132 | type = 'direct' 133 | 134 | 135 | class FanoutExchange(_Exchange): 136 | """The FanoutExchange class is used for interacting with fanout exchanges 137 | only. 138 | 139 | :param channel: The channel object to communicate on 140 | :type channel: :py:class:`rabbitpy.channel.Channel` 141 | :param str name: The name of the exchange 142 | :param bool durable: Request a durable exchange 143 | :param bool auto_delete: Automatically delete when not in use 144 | :param dict arguments: Optional key/value arguments 145 | 146 | """ 147 | type = 'fanout' 148 | 149 | 150 | class HeadersExchange(_Exchange): 151 | """The HeadersExchange class is used for interacting with direct exchanges 152 | only. 153 | 154 | :param channel: The channel object to communicate on 155 | :type channel: :py:class:`rabbitpy.channel.Channel` 156 | :param str name: The name of the exchange 157 | :param bool durable: Request a durable exchange 158 | :param bool auto_delete: Automatically delete when not in use 159 | :param dict arguments: Optional key/value arguments 160 | 161 | """ 162 | type = 'headers' 163 | 164 | 165 | class TopicExchange(_Exchange): 166 | """The TopicExchange class is used for interacting with topic exchanges 167 | only. 168 | 169 | :param channel: The channel object to communicate on 170 | :type channel: :py:class:`rabbitpy.channel.Channel` 171 | :param str name: The name of the exchange 172 | :param bool durable: Request a durable exchange 173 | :param bool auto_delete: Automatically delete when not in use 174 | :param dict arguments: Optional key/value arguments 175 | 176 | """ 177 | type = 'topic' 178 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # rabbitpy documentation build configuration file, created by 4 | # sphinx-quickstart on Wed Mar 27 18:31:37 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys 15 | 16 | sys.path.insert(0, '../') 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | #sys.path.insert(0, os.path.abspath('.')) 22 | 23 | # -- General configuration ----------------------------------------------------- 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be extensions 29 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 30 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.viewcode', 31 | 'sphinx.ext.autosummary', 'sphinx.ext.intersphinx'] 32 | # Add any paths that contain templates here, relative to this directory. 33 | templates_path = ['_templates'] 34 | 35 | # The suffix of source filenames. 36 | source_suffix = '.rst' 37 | 38 | # The encoding of source files. 39 | #source_encoding = 'utf-8-sig' 40 | 41 | # The master toctree document. 42 | master_doc = 'index' 43 | 44 | # General information about the project. 45 | project = u'rabbitpy' 46 | copyright = u'2013, Gavin M. Roy' 47 | 48 | # The version info for the project you're documenting, acts as replacement for 49 | # |version| and |release|, also used in various other places throughout the 50 | # built documents. 51 | 52 | import rabbitpy 53 | release = rabbitpy.__version__ 54 | version = '.'.join(release.split('.')[0:1]) 55 | 56 | # The language for content autogenerated by Sphinx. Refer to documentation 57 | # for a list of supported languages. 58 | #language = None 59 | 60 | # There are two options for replacing |today|: either, you set today to some 61 | # non-false value, then it is used: 62 | #today = '' 63 | # Else, today_fmt is used as the format for a strftime call. 64 | #today_fmt = '%B %d, %Y' 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | exclude_patterns = ['_build'] 69 | 70 | # The reST default role (used for this markup: `text`) to use for all documents. 71 | #default_role = None 72 | 73 | # If true, '()' will be appended to :func: etc. cross-reference text. 74 | #add_function_parentheses = True 75 | 76 | # If true, the current module name will be prepended to all description 77 | # unit titles (such as .. function::). 78 | #add_module_names = True 79 | 80 | # If true, sectionauthor and moduleauthor directives will be shown in the 81 | # output. They are ignored by default. 82 | #show_authors = False 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | pygments_style = 'sphinx' 86 | 87 | # A list of ignored prefixes for module index sorting. 88 | #modindex_common_prefix = [] 89 | 90 | intersphinx_mapping = {'pamqp': ('https://pamqp.readthedocs.org/en/latest/', 91 | None), 92 | 'python': ('https://docs.python.org/2/', None)} 93 | 94 | # -- Options for HTML output --------------------------------------------------- 95 | 96 | # The theme to use for HTML and HTML Help pages. See the documentation for 97 | # a list of builtin themes. 98 | html_theme = 'default' 99 | 100 | # Theme options are theme-specific and customize the look and feel of a theme 101 | # further. For a list of options available for each theme, see the 102 | # documentation. 103 | #html_theme_options = {} 104 | 105 | # Add any paths that contain custom themes here, relative to this directory. 106 | #html_theme_path = [] 107 | 108 | # The name for this set of Sphinx documents. If None, it defaults to 109 | # " v documentation". 110 | #html_title = None 111 | 112 | # A shorter title for the navigation bar. Default is the same as html_title. 113 | #html_short_title = None 114 | 115 | # The name of an image file (relative to this directory) to place at the top 116 | # of the sidebar. 117 | #html_logo = None 118 | 119 | # The name of an image file (within the static path) to use as favicon of the 120 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 121 | # pixels large. 122 | #html_favicon = None 123 | 124 | # Add any paths that contain custom static files (such as style sheets) here, 125 | # relative to this directory. They are copied after the builtin static files, 126 | # so a file named "default.css" will overwrite the builtin "default.css". 127 | html_static_path = ['_static'] 128 | 129 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 130 | # using the given strftime format. 131 | #html_last_updated_fmt = '%b %d, %Y' 132 | 133 | # If true, SmartyPants will be used to convert quotes and dashes to 134 | # typographically correct entities. 135 | #html_use_smartypants = True 136 | 137 | # Custom sidebar templates, maps document names to template names. 138 | #html_sidebars = {} 139 | 140 | # Additional templates that should be rendered to pages, maps page names to 141 | # template names. 142 | #html_additional_pages = {} 143 | 144 | # If false, no module index is generated. 145 | #html_domain_indices = True 146 | 147 | # If false, no index is generated. 148 | #html_use_index = True 149 | 150 | # If true, the index is split into individual pages for each letter. 151 | #html_split_index = False 152 | 153 | # If true, links to the reST sources are added to the pages. 154 | #html_show_sourcelink = True 155 | 156 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 157 | #html_show_sphinx = True 158 | 159 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 160 | #html_show_copyright = True 161 | 162 | # If true, an OpenSearch description file will be output, and all pages will 163 | # contain a tag referring to it. The value of this option must be the 164 | # base URL from which the finished HTML is served. 165 | #html_use_opensearch = '' 166 | 167 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 168 | #html_file_suffix = None 169 | 170 | # Output file base name for HTML help builder. 171 | htmlhelp_basename = 'rabbitpydoc' 172 | 173 | 174 | # -- Options for LaTeX output -------------------------------------------------- 175 | 176 | latex_elements = { 177 | # The paper size ('letterpaper' or 'a4paper'). 178 | #'papersize': 'letterpaper', 179 | 180 | # The font size ('10pt', '11pt' or '12pt'). 181 | #'pointsize': '10pt', 182 | 183 | # Additional stuff for the LaTeX preamble. 184 | #'preamble': '', 185 | } 186 | 187 | # Grouping the document tree into LaTeX files. List of tuples 188 | # (source start file, target name, title, author, documentclass [howto/manual]). 189 | latex_documents = [ 190 | ('index', 'rabbitpy.tex', u'rabbitpy Documentation', 191 | u'Gavin M. Roy', 'manual'), 192 | ] 193 | 194 | # The name of an image file (relative to this directory) to place at the top of 195 | # the title page. 196 | #latex_logo = None 197 | 198 | # For "manual" documents, if this is true, then toplevel headings are parts, 199 | # not chapters. 200 | #latex_use_parts = False 201 | 202 | # If true, show page references after internal links. 203 | #latex_show_pagerefs = False 204 | 205 | # If true, show URL addresses after external links. 206 | #latex_show_urls = False 207 | 208 | # Documents to append as an appendix to all manuals. 209 | #latex_appendices = [] 210 | 211 | # If false, no module index is generated. 212 | #latex_domain_indices = True 213 | 214 | 215 | # -- Options for manual page output -------------------------------------------- 216 | 217 | # One entry per manual page. List of tuples 218 | # (source start file, name, description, authors, manual section). 219 | man_pages = [ 220 | ('index', 'rabbitpy', u'rabbitpy Documentation', 221 | [u'Gavin M. Roy'], 1) 222 | ] 223 | 224 | # If true, show URL addresses after external links. 225 | #man_show_urls = False 226 | 227 | 228 | # -- Options for Texinfo output ------------------------------------------------ 229 | 230 | # Grouping the document tree into Texinfo files. List of tuples 231 | # (source start file, target name, title, author, 232 | # dir menu entry, description, category) 233 | texinfo_documents = [ 234 | ('index', 'rabbitpy', u'rabbitpy Documentation', 235 | u'Gavin M. Roy', 'rabbitpy', 'One line description of project.', 236 | 'Miscellaneous'), 237 | ] 238 | 239 | # Documents to append as an appendix to all manuals. 240 | #texinfo_appendices = [] 241 | 242 | # If false, no module index is generated. 243 | #texinfo_domain_indices = True 244 | 245 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 246 | #texinfo_show_urls = 'footnote' 247 | -------------------------------------------------------------------------------- /rabbitpy/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Exceptions that may be raised by rabbitpy during use 3 | ---------------------------------------------------- 4 | 5 | """ 6 | 7 | 8 | class RabbitpyException(Exception): 9 | """Base exception of all rabbitpy exceptions.""" 10 | pass 11 | 12 | 13 | class AMQPException(RabbitpyException): 14 | """Base exception of all AMQP exceptions.""" 15 | pass 16 | 17 | 18 | class ActionException(RabbitpyException): 19 | """Raised when an action is taken on a Rabbitpy object that is not 20 | supported due to the state of the object. An example would be trying to 21 | ack a Message object when the message object was locally created and not 22 | sent by RabbitMQ via an AMQP Basic.Get or Basic.Consume. 23 | 24 | """ 25 | def __str__(self): 26 | return self.args[0] 27 | 28 | 29 | class ChannelClosedException(RabbitpyException): 30 | """Raised when an action is attempted on a channel that is closed.""" 31 | def __str__(self): 32 | return 'Can not perform RPC requests on a closed channel, you must ' \ 33 | 'create a new channel' 34 | 35 | 36 | class ConnectionException(RabbitpyException): 37 | """Raised when Rabbitpy can not connect to the specified server and if 38 | a connection fails and the RabbitMQ version does not support the 39 | authentication_failure_close feature added in RabbitMQ 3.2. 40 | 41 | """ 42 | def __str__(self): 43 | return 'Unable to connect to the remote server {0}'.format(self.args) 44 | 45 | 46 | class ConnectionClosed(ConnectionException): 47 | """Raised if a connection.close() is invoked when the connection is not 48 | open. 49 | 50 | """ 51 | def __str__(self): 52 | return 'The connection is closed' 53 | 54 | 55 | class ConnectionResetException(ConnectionException): 56 | """Raised if the socket level connection was reset. This can happen due 57 | to the loss of network connection or socket timeout, or more than 2 58 | missed heartbeat intervals if heartbeats are enabled. 59 | 60 | """ 61 | def __str__(self): 62 | return 'Connection was reset at socket level' 63 | 64 | 65 | class RemoteCancellationException(RabbitpyException): 66 | """Raised if RabbitMQ cancels an active consumer""" 67 | def __str__(self): 68 | return 'Remote server cancelled the active consumer' 69 | 70 | 71 | class RemoteClosedChannelException(RabbitpyException): 72 | """Raised if RabbitMQ closes the channel and the reply_code in the 73 | Channel.Close RPC request does not have a mapped exception in Rabbitpy. 74 | 75 | """ 76 | def __str__(self): 77 | return 'Channel {0} was closed by the remote server ' \ 78 | '({1}): {2}'.format(*self.args) 79 | 80 | 81 | class RemoteClosedException(RabbitpyException): 82 | """Raised if RabbitMQ closes the connection and the reply_code in the 83 | Connection.Close RPC request does not have a mapped exception in Rabbitpy. 84 | 85 | """ 86 | def __str__(self): 87 | return 'Connection was closed by the remote server ' \ 88 | '({0}): {1}'.format(*self.args) 89 | 90 | 91 | class MessageReturnedException(RabbitpyException): 92 | """Raised if the RabbitMQ sends a message back to a publisher via 93 | the Basic.Return RPC call. 94 | 95 | """ 96 | def __str__(self): 97 | return 'Message was returned by RabbitMQ: ({0}) ' \ 98 | 'for exchange {1}'.format(*self.args) 99 | 100 | 101 | class NoActiveTransactionError(RabbitpyException): 102 | """Raised when a transaction method is issued but the transaction has not 103 | been initiated. 104 | 105 | """ 106 | def __str__(self): 107 | return 'No active transaction for the request, channel closed' 108 | 109 | 110 | class NotConsumingError(RabbitpyException): 111 | """Raised Queue.cancel_consumer() is invoked but the queue is not 112 | actively consuming. 113 | 114 | """ 115 | def __str__(self): 116 | return 'No active consumer to cancel' 117 | 118 | 119 | class NotSupportedError(RabbitpyException): 120 | """Raised when a feature is requested that is not supported by the RabbitMQ 121 | server. 122 | 123 | """ 124 | def __str__(self): 125 | return 'The selected feature "{0}" is not supported'.format(self.args) 126 | 127 | 128 | class TooManyChannelsError(RabbitpyException): 129 | """Raised if an application attempts to create a channel, exceeding the 130 | maximum number of channels (MAXINT or 2,147,483,647) available for a 131 | single connection. Note that each time a channel object is created, it will 132 | take a new channel id. If you create and destroy 2,147,483,648 channels, 133 | this exception will be raised. 134 | 135 | """ 136 | def __str__(self): 137 | return 'The maximum amount of negotiated channels has been reached' 138 | 139 | 140 | class UnexpectedResponseError(RabbitpyException): 141 | """Raised when an RPC call is made to RabbitMQ but the response it sent 142 | back is not recognized. 143 | 144 | """ 145 | def __str__(self): 146 | return 'Received an expected response, expected {0}, ' \ 147 | 'received {1}'.format(*self.args) 148 | 149 | 150 | # AMQP Exceptions 151 | 152 | 153 | class AMQPContentTooLarge(AMQPException): 154 | """ 155 | The client attempted to transfer content larger than the server could 156 | accept at the present time. The client may retry at a later time. 157 | 158 | """ 159 | pass 160 | 161 | 162 | class AMQPNoRoute(AMQPException): 163 | """ 164 | Undocumented AMQP Soft Error 165 | 166 | """ 167 | pass 168 | 169 | 170 | class AMQPNoConsumers(AMQPException): 171 | """ 172 | When the exchange cannot deliver to a consumer when the immediate flag is 173 | set. As a result of pending data on the queue or the absence of any 174 | consumers of the queue. 175 | 176 | """ 177 | pass 178 | 179 | 180 | class AMQPAccessRefused(AMQPException): 181 | """ 182 | The client attempted to work with a server entity to which it has no access 183 | due to security settings. 184 | 185 | """ 186 | pass 187 | 188 | 189 | class AMQPNotFound(AMQPException): 190 | """ 191 | The client attempted to work with a server entity that does not exist. 192 | 193 | """ 194 | pass 195 | 196 | 197 | class AMQPResourceLocked(AMQPException): 198 | """ 199 | The client attempted to work with a server entity to which it has no access 200 | because another client is working with it. 201 | 202 | """ 203 | pass 204 | 205 | 206 | class AMQPPreconditionFailed(AMQPException): 207 | """ 208 | The client requested a method that was not allowed because some 209 | precondition failed. 210 | 211 | """ 212 | pass 213 | 214 | 215 | class AMQPConnectionForced(AMQPException): 216 | """ 217 | An operator intervened to close the connection for some reason. The client 218 | may retry at some later date. 219 | 220 | """ 221 | pass 222 | 223 | 224 | class AMQPInvalidPath(AMQPException): 225 | """ 226 | The client tried to work with an unknown virtual host. 227 | 228 | """ 229 | pass 230 | 231 | 232 | class AMQPFrameError(AMQPException): 233 | """ 234 | The sender sent a malformed frame that the recipient could not decode. This 235 | strongly implies a programming error in the sending peer. 236 | 237 | """ 238 | pass 239 | 240 | 241 | class AMQPSyntaxError(AMQPException): 242 | """ 243 | The sender sent a frame that contained illegal values for one or more 244 | fields. This strongly implies a programming error in the sending peer. 245 | 246 | """ 247 | pass 248 | 249 | 250 | class AMQPCommandInvalid(AMQPException): 251 | """ 252 | The client sent an invalid sequence of frames, attempting to perform an 253 | operation that was considered invalid by the server. This usually implies a 254 | programming error in the client. 255 | 256 | """ 257 | pass 258 | 259 | 260 | class AMQPChannelError(AMQPException): 261 | """ 262 | The client attempted to work with a channel that had not been correctly 263 | opened. This most likely indicates a fault in the client layer. 264 | 265 | """ 266 | pass 267 | 268 | 269 | class AMQPUnexpectedFrame(AMQPException): 270 | """ 271 | The peer sent a frame that was not expected, usually in the context of a 272 | content header and body. This strongly indicates a fault in the peer's 273 | content processing. 274 | 275 | """ 276 | pass 277 | 278 | 279 | class AMQPResourceError(AMQPException): 280 | """ 281 | The server could not complete the method because it lacked sufficient 282 | resources. This may be due to the client creating too many of some type of 283 | entity. 284 | 285 | """ 286 | pass 287 | 288 | 289 | class AMQPNotAllowed(AMQPException): 290 | """ 291 | The client tried to work with some entity in a manner that is prohibited by 292 | the server, due to security settings or by some other criteria. 293 | 294 | """ 295 | pass 296 | 297 | 298 | class AMQPNotImplemented(AMQPException): 299 | """ 300 | The client tried to use functionality that is not implemented in the 301 | server. 302 | 303 | """ 304 | pass 305 | 306 | 307 | class AMQPInternalError(AMQPException): 308 | """ 309 | The server could not complete the method because of an internal error. The 310 | server may require intervention by an operator in order to resume normal 311 | operations. 312 | 313 | """ 314 | pass 315 | 316 | 317 | AMQP = {311: AMQPContentTooLarge, 318 | 312: AMQPNoRoute, 319 | 313: AMQPNoConsumers, 320 | 320: AMQPConnectionForced, 321 | 402: AMQPInvalidPath, 322 | 403: AMQPAccessRefused, 323 | 404: AMQPNotFound, 324 | 405: AMQPResourceLocked, 325 | 406: AMQPPreconditionFailed, 326 | 501: AMQPFrameError, 327 | 502: AMQPSyntaxError, 328 | 503: AMQPCommandInvalid, 329 | 504: AMQPChannelError, 330 | 505: AMQPUnexpectedFrame, 331 | 506: AMQPResourceError, 332 | 530: AMQPNotAllowed, 333 | 540: AMQPNotImplemented, 334 | 541: AMQPInternalError} 335 | -------------------------------------------------------------------------------- /rabbitpy/simple.py: -------------------------------------------------------------------------------- 1 | """ 2 | Wrapper methods for easy access to common operations, making them both less 3 | complex and less verbose for one off or simple use cases. 4 | 5 | """ 6 | from rabbitpy import amqp_queue 7 | from rabbitpy import connection 8 | from rabbitpy import exchange 9 | from rabbitpy import message 10 | 11 | 12 | class SimpleChannel(object): 13 | """The rabbitpy.simple.Channel class creates a context manager 14 | implementation for use on a single channel where the connection is 15 | automatically created and managed for you. 16 | 17 | Example: 18 | 19 | .. code:: python 20 | 21 | import rabbitpy 22 | 23 | with rabbitpy.SimpleChannel('amqp://localhost/%2f') as channel: 24 | queue = rabbitpy.Queue(channel, 'my-queue') 25 | 26 | :param str uri: The AMQP URI to connect with. For URI options, see the 27 | :class:`~rabbitpy.connection.Connection` class documentation. 28 | 29 | """ 30 | def __init__(self, uri): 31 | self.connection = None 32 | self.channel = None 33 | self.uri = uri 34 | 35 | def __enter__(self): 36 | self.connection = connection.Connection(self.uri) 37 | self.channel = self.connection.channel() 38 | return self.channel 39 | 40 | def __exit__(self, exc_type, exc_val, exc_tb): 41 | if not self.channel.closed: 42 | self.channel.close() 43 | if not self.connection.closed: 44 | self.connection.close() 45 | if exc_type and exc_val: 46 | raise 47 | 48 | 49 | def consume(uri=None, queue_name=None, no_ack=False, prefetch=None, 50 | priority=None): 51 | """Consume messages from the queue as a generator: 52 | 53 | .. code:: python 54 | 55 | for message in rabbitpy.consume('amqp://localhost/%2F', 'my_queue'): 56 | message.ack() 57 | 58 | :param str uri: AMQP connection URI 59 | :param str queue_name: The name of the queue to consume from 60 | :param bool no_ack: Do not require acknowledgements 61 | :param int prefetch: Set a prefetch count for the channel 62 | :param int priority: Set the consumer priority 63 | :rtype: :py:class:`Iterator` 64 | :raises: py:class:`ValueError` 65 | 66 | """ 67 | _validate_name(queue_name, 'queue') 68 | with SimpleChannel(uri) as channel: 69 | queue = amqp_queue.Queue(channel, queue_name) 70 | for msg in queue.consume(no_ack, prefetch, priority): 71 | yield msg 72 | 73 | 74 | def get(uri=None, queue_name=None): 75 | """Get a message from RabbitMQ, auto-acknowledging with RabbitMQ if one 76 | is returned. 77 | 78 | Invoke directly as ``rabbitpy.get()`` 79 | 80 | :param str uri: AMQP URI to connect to 81 | :param str queue_name: The queue name to get the message from 82 | :rtype: py:class:`rabbitpy.message.Message` or None 83 | :raises: py:class:`ValueError` 84 | 85 | """ 86 | _validate_name(queue_name, 'queue') 87 | with SimpleChannel(uri) as channel: 88 | queue = amqp_queue.Queue(channel, queue_name) 89 | return queue.get(False) 90 | 91 | 92 | def publish(uri=None, exchange_name=None, routing_key=None, 93 | body=None, properties=None, confirm=False): 94 | """Publish a message to RabbitMQ. This should only be used for one-off 95 | publishing, as you will suffer a performance penalty if you use it 96 | repeatedly instead creating a connection and channel and publishing on that 97 | 98 | :param str uri: AMQP URI to connect to 99 | :param str exchange_name: The exchange to publish to 100 | :param str routing_key: The routing_key to publish with 101 | :param body: The message body 102 | :type body: str or unicode or bytes or dict or list 103 | :param dict properties: Dict representation of Basic.Properties 104 | :param bool confirm: Confirm this delivery with Publisher Confirms 105 | :rtype: bool or None 106 | 107 | """ 108 | if exchange_name is None: 109 | exchange_name = '' 110 | 111 | with SimpleChannel(uri) as channel: 112 | msg = message.Message(channel, body or '', properties or dict()) 113 | if confirm: 114 | channel.enable_publisher_confirms() 115 | return msg.publish(exchange_name, routing_key or '', 116 | mandatory=True) 117 | else: 118 | msg.publish(exchange_name, routing_key or '') 119 | 120 | 121 | def create_queue(uri=None, queue_name='', durable=True, auto_delete=False, 122 | max_length=None, message_ttl=None, expires=None, 123 | dead_letter_exchange=None, dead_letter_routing_key=None, 124 | arguments=None): 125 | """Create a queue with RabbitMQ. This should only be used for one-off 126 | operations. If a queue name is omitted, the name will be automatically 127 | generated by RabbitMQ. 128 | 129 | :param str uri: AMQP URI to connect to 130 | :param str queue_name: The queue name to create 131 | :param durable: Indicates if the queue should survive a RabbitMQ is restart 132 | :type durable: bool 133 | :param bool auto_delete: Automatically delete when all consumers disconnect 134 | :param int max_length: Maximum queue length 135 | :param int message_ttl: Time-to-live of a message in milliseconds 136 | :param expires: Milliseconds until a queue is removed after becoming idle 137 | :type expires: int 138 | :param dead_letter_exchange: Dead letter exchange for rejected messages 139 | :type dead_letter_exchange: str 140 | :param dead_letter_routing_key: Routing key for dead lettered messages 141 | :type dead_letter_routing_key: str 142 | :param dict arguments: Custom arguments for the queue 143 | :raises: :py:class:`ValueError` 144 | :raises: :py:class:`rabbitpy.RemoteClosedException` 145 | 146 | """ 147 | _validate_name(queue_name, 'queue') 148 | with SimpleChannel(uri) as channel: 149 | amqp_queue.Queue(channel, queue_name, 150 | durable=durable, 151 | auto_delete=auto_delete, 152 | max_length=max_length, 153 | message_ttl=message_ttl, 154 | expires=expires, 155 | dead_letter_exchange=dead_letter_exchange, 156 | dead_letter_routing_key=dead_letter_routing_key, 157 | arguments=arguments).declare() 158 | 159 | 160 | def delete_queue(uri=None, queue_name=None): 161 | """Delete a queue from RabbitMQ. This should only be used for one-off 162 | operations. 163 | 164 | :param str uri: AMQP URI to connect to 165 | :param str queue_name: The queue name to delete 166 | :rtype: bool 167 | :raises: :py:class:`ValueError` 168 | :raises: :py:class:`rabbitpy.RemoteClosedException` 169 | 170 | """ 171 | _validate_name(queue_name, 'queue') 172 | with SimpleChannel(uri) as channel: 173 | amqp_queue.Queue(channel, queue_name).delete() 174 | 175 | 176 | def create_direct_exchange(uri=None, exchange_name=None, durable=True): 177 | """Create a direct exchange with RabbitMQ. This should only be used for 178 | one-off operations. 179 | 180 | :param str uri: AMQP URI to connect to 181 | :param str exchange_name: The exchange name to create 182 | :param bool durable: Exchange should survive server restarts 183 | :raises: :py:class:`ValueError` 184 | :raises: :py:class:`rabbitpy.RemoteClosedException` 185 | 186 | """ 187 | _create_exchange(uri, exchange_name, exchange.DirectExchange, durable) 188 | 189 | 190 | def create_fanout_exchange(uri=None, exchange_name=None, durable=True): 191 | """Create a fanout exchange with RabbitMQ. This should only be used for 192 | one-off operations. 193 | 194 | :param str uri: AMQP URI to connect to 195 | :param str exchange_name: The exchange name to create 196 | :param bool durable: Exchange should survive server restarts 197 | :raises: :py:class:`ValueError` 198 | :raises: :py:class:`rabbitpy.RemoteClosedException` 199 | 200 | """ 201 | _create_exchange(uri, exchange_name, exchange.FanoutExchange, durable) 202 | 203 | 204 | def create_headers_exchange(uri=None, exchange_name=None, durable=True): 205 | """Create a headers exchange with RabbitMQ. This should only be used for 206 | one-off operations. 207 | 208 | :param str uri: AMQP URI to connect to 209 | :param str exchange_name: The exchange name to create 210 | :param bool durable: Exchange should survive server restarts 211 | :raises: :py:class:`ValueError` 212 | :raises: :py:class:`rabbitpy.RemoteClosedException` 213 | 214 | """ 215 | _create_exchange(uri, exchange_name, exchange.HeadersExchange, durable) 216 | 217 | 218 | def create_topic_exchange(uri=None, exchange_name=None, durable=True): 219 | """Create an exchange from RabbitMQ. This should only be used for one-off 220 | operations. 221 | 222 | :param str uri: AMQP URI to connect to 223 | :param str exchange_name: The exchange name to create 224 | :param bool durable: Exchange should survive server restarts 225 | :raises: :py:class:`ValueError` 226 | :raises: :py:class:`rabbitpy.RemoteClosedException` 227 | 228 | """ 229 | _create_exchange(uri, exchange_name, exchange.TopicExchange, durable) 230 | 231 | 232 | def delete_exchange(uri=None, exchange_name=None): 233 | """Delete an exchange from RabbitMQ. This should only be used for one-off 234 | operations. 235 | 236 | :param str uri: AMQP URI to connect to 237 | :param str exchange_name: The exchange name to delete 238 | :raises: :py:class:`ValueError` 239 | :raises: :py:class:`rabbitpy.RemoteClosedException` 240 | 241 | """ 242 | _validate_name(exchange_name, 'exchange') 243 | with SimpleChannel(uri) as channel: 244 | exchange.Exchange(channel, exchange_name).delete() 245 | 246 | 247 | def _create_exchange(uri, exchange_name, exchange_class, durable): 248 | """Create an exchange from RabbitMQ. This should only be used for one-off 249 | operations. 250 | 251 | :param str uri: AMQP URI to connect to 252 | :param str exchange_name: The exchange name to create 253 | :param bool durable: Exchange should survive server restarts 254 | :raises: :py:class:`ValueError` 255 | :raises: :py:class:`rabbitpy.RemoteClosedException` 256 | 257 | """ 258 | _validate_name(exchange_name, 'exchange') 259 | with SimpleChannel(uri) as channel: 260 | exchange_class(channel, exchange_name, durable=durable).declare() 261 | 262 | 263 | def _validate_name(value, obj_type): 264 | """Validate the specified name is set. 265 | 266 | :param str value: The value to validate 267 | :param str obj_type: The object type for the error message if needed 268 | :raises: ValueError 269 | 270 | """ 271 | if not value: 272 | raise ValueError('You must specify the {} name'.format(obj_type)) 273 | -------------------------------------------------------------------------------- /docs/history.rst: -------------------------------------------------------------------------------- 1 | Version History 2 | --------------- 3 | - 3.0.0 - released *2024-05-08* 4 | - Change to use `ssl.SSLContext` since `ssl.wrap_socket` was deprecated in Python 3.7 and removed in 3.12 5 | - Drops support for Python < 3.8 6 | - Adds support for Python 3.10, 3.11 and 3.12 7 | - Change tests over to use `coverage` instead of `nose` 8 | - 2.0.1 - released *2019-08-06* 9 | - Fixed an issue with the IO loop poller on MacOS (#111) 10 | - 2.0.0 - released *2019-04-19* 11 | - Updated to use pamqp>=2.3,<3 which has the following implications: 12 | - Field table keys are now strings and no longer bytes. This may be a breaking change means in Python3 keys will always be type str for short strings. This includes frame values and field table values. 13 | - In Python 2.7 if a short-string (key, frame field value, etc) has UTF-8 characters in it, it will be a unicode object. 14 | - field-table integer encoding changes 15 | - Drops support for Python < 3.4 16 | - Adds support for Python 3.6 and 3.7 17 | - 1.0.0 - released *2016-10-27* 18 | - Reworked Heartbeat logic to send a heartbeat every ``interval / 2`` seconds when data has not been written to the socket (#70, #74, #98, #99) 19 | - Improved performance when consuming large mesages (#104) - `Jelle Aalbers `_ 20 | - Allow for username and password to be default again (#96, #97) - `Grzegorz Śliwiński `_ 21 | - Cleanup of Connection and Channel teardown (#103) 22 | - 0.27.1 - released *2016-05-12* 23 | - Fix a bug where the IO write trigger socketpair is not being cleaned up on close 24 | - 0.27.0 - released *2016-05-11* 25 | - Added new :class:`~rabbitpy.simple.SimpleChannel` class 26 | - Exception formatting changes 27 | - Thread locking optimizations 28 | - Connection shutdown cleanup 29 | - :class:`~rabbitpy.message.Message` now assigns a UTC timestamp instead of local timestamp to the AMQP message property if requested (#94) 30 | - Unquote username and password in URI (#93) - `sunbit `_ 31 | - Connections now allow for a configurable timeout (#84, #85) - `vmarkovtsev `_ 32 | - Bugfix for :meth:`~rabbitpy.amqp.AMQP.basic_publish` (#92) - `canardleteer `_ 33 | - Added ``args`` property to :class:`~rabbitpy.connection.Connection` (#88) - `vmarkovtsev `_ 34 | - Fix locale in connection setup from causing hanging (#87) - `vmarkovtsev `_ 35 | - Fix heartbeat behavior (#69, #70, #74) 36 | - Cancel consuming in case of exceptions (#68) - `kmelnikov `_ 37 | - Documentation correction (#79) - `jonahbull `_ 38 | - 0.26.2 - released *2015-03-17* 39 | - Fix behavior for Basic.Return frames sent from RabbitMQ 40 | - Pin pamqp 1.6.1 fixing an issue with max-channels 41 | - 0.26.1 - released *2015-03-09* 42 | - Add the ability to interrupt rabbitpy when waiting on a frame (#38) 43 | - Use a custom base class for all Exceptions (#57) Jeremy Tillman 44 | - Fix for consumer example in documentation (#60) Michael Becker 45 | - Add rabbitpy.amqp module for unopinionated access to AMQP API 46 | - Refactor how client side heartbeat checking is managed when no heartbeat frames have been sent from the server. (#58) 47 | - Address an issue when client side channel max count is not set and server side channel max is set to 65535 (#62) 48 | - Clean up handling of remote channel and connection closing 49 | - Clean up context manager exiting for rabbitpy.Queue 50 | - Remove default prefetch count for simple consuming 51 | - Fix URI query parameter names to match AMQP URI spec on rabbitmq.com 52 | - Fix behavior of SSL flags in query parameters (#63, #64) 53 | - PYPY behavior fixes related to garbage collection 54 | - 0.25.0 - released *2014-12-16* 55 | - Acquire a lock when creating a new channel to fix multi-threaded channel creation behavior (#56) 56 | - Add client side heartbeat checking. If 2 heartbeats are missed, a ConnectionResetException exception will be raised (#55) 57 | - Fix a bug where Basic.Nack checking was checking for the wrong string to test for support 58 | - Add support for Python3 memoryviews for the message body when creating a new rabbitpy.Message (#50) 59 | - Improve Python3 behavior in rabbitpy.utils.maybe_utf8_encode: ensure the object being cast as a bytes object with utf-8 encoding is a string 60 | - 0.24.0 - released *2014-12-12* 61 | - Update to reflect changes in pamqp 1.6.0 62 | - Update how message property data types are retrieved 63 | - Fix tests relying on .__dict__ 64 | - 0.23.0 - released *2014-11-5* 65 | - Fix a bug where message body length was being assigned to the content header prior to converting the unicode string to bytes (#49) 66 | - Add a new rabbitpy.utils.maybe_utf8_encode method for handling strings that may or may not contain unicode (#49) 67 | - Fix the automatic coercion of header types to UTF-8 encoded bytes (#49) 68 | - Fix an integration test that was not cleaning up its queue after itself 69 | - Raise TypeError if a timestamp property can not be converted properly 70 | - 0.22.0 - released *2014-11-4* 71 | - Address an issue when RabbitMQ is configured with a max-frame-size of 0 (#48) 72 | - Do not lose the traceback when exiting a context manager due to a an exception (#46) 73 | - Adds server capability checking in rabbitpy.Channel methods that require RabbitMQ enhancements to the AMQP protocol (Publisher confirms, consumer priorities, & Baisc.Nack). If unsupported functionality is used, a rabbitpy.exceptions.NotSupportedError exception will be raised. 74 | - Pin pamqp version range to >= 1.4, < 2.0 75 | - Fix wheel distribution 76 | - 0.21.1 - released *2014-10-23* 77 | - Clean up KQueue issues found when troubleshooting #44, checking for socket EOF in flags to detect connection reset 78 | - Remove sockets from KQueue when in error state 79 | - Change behavior when there is a poll exception list 80 | - Handle socket connect errors more cleanly (#44) 81 | - Handle bug for how we pull the error string from an exception in IO.on_error (#44) 82 | - Re-raise exceptions causing the exit of Connection or Channel so they can be cleanly caught (#44) 83 | - 0.21.0 - released *2014-10-21* 84 | - Address a possible edge case where message frames can be interspersed when publishing in a multi-threaded environment 85 | - Add exception handling around select.error (#43) 86 | - Check all frames for Channel.CloseOk when consuming 87 | - Add a new ``opinionated`` flag in rabbitpy.Message construction that deprecates the ``auto_id`` flag 88 | - Add wheel distribution 89 | - 0.20.0 - released *2014-10-01* 90 | - Added support for KQueue and Poll in IOLoop for performance improvements 91 | - Fixed issues with publishing large messages and socket resource availability errors (#37) 92 | - Add exchange property to rabbitpy.Message (#40) 93 | - Fix exception when timestamp is None in received Message (#41) 94 | - Fix rabbitpy.Message.json() in Python 3.4 (#42) 95 | - Add out-of-band consumer cancellation with Queue.stop_consuming() (#38, #39) 96 | - Add new simple method rabbitpy.create_headers_exchange() 97 | - Significantly increase test coverage 98 | - 0.19.0 - released *2014-06-30* 99 | - Fix the socket read/write buffer size (#35) 100 | - Add new flag in channels to use blocking queue.get operations increasing throughput and lowering overhead. 101 | - 0.18.1 - released *2014-05-15* 102 | - Fix unicode message body encoding in Python 2 103 | - 0.18.0 - released *2014-05-15* 104 | - Make IO thread daemonic 105 | - block on RPC reads for 1 second instead of 100ms 106 | - add the Message.redelivered property 107 | - 0.17.0 - released *2014-04-16* 108 | - Refactor cross-thread communication for RabbitMQ invoked RPC methods 109 | - fix unclean shutdown conditions and cross-thread exceptions 110 | - 0.16.0 - released *2014-04-10* 111 | - Fix an issue with no_ack=True consumer cancellation 112 | - Fix exchange and queue unbinding 113 | - Add wait on the SOCKET_OPENED event when connecting 114 | - Deal with str message body values in Python 3 by casting to bytes and encoding as UTF-8. 115 | - 0.15.1 - released *2014-01-27* 116 | - Fix an issue with Python 3 IO write trigger 117 | - 0.15.0 - released *2014-01-27* 118 | - Change default durability for Exchange and Queue to False 119 | - Fix a SSL connection issue 120 | - 0.14.2 - released *2014-01-23* 121 | - Fix an issue when IPv6 is the default protocol for the box rabbitpy is being used on 122 | - 0.14.1 - released *2014-01-23* 123 | - Assign queue name for RabbitMQ named queues in rabbitpy.Queue.declare 124 | - 0.14.0 - released *2014-01-22* 125 | - Add support for authentication_failure_close 126 | - Add consumer priorities 127 | - Exception cleanup 128 | - Queue consuming via Queue.__iter__ 129 | - Queue & Exchange attributes are no longer private 130 | - Tx objects can be used as a context manager 131 | - Experimental support for Windows. 132 | - 0.13.0 - released *2014-01-17* 133 | - Validate heartbeat is always an integer 134 | - add arguments to Queue for expires, message-ttl, max-length, & dead-lettering 135 | - 0.12.3 - released *2013-12-23* 136 | - Minor Message.pprint() reformatting 137 | - 0.12.2 - released *2013-12-23* 138 | - Add Exchange and Routing Key to Message.pprint, check for empty method frames in Channel._create_message 139 | - 0.12.1 - released *2013-12-19* 140 | - Fix exception with pika.exceptions.AMQP 141 | - 0.12.0 - released *2013-12-19* 142 | - Updated simple consumer to potential one-liner 143 | - Added rabbitpy.Message.pprint() 144 | - 0.11.0 - released *2013-12-19* 145 | - Major bugfix focused on receiving multiple AMQP frames at the same time. 146 | - Add auto-coercion of property data-types. 147 | - 0.10.0 - released *2013-12-11* 148 | - Rewrite of IO layer yielding improved performance and reduction of CPU usage, bugfixes 149 | - 0.9.0 - released *2013-10-02* 150 | - Major performance improvements, CPU usage reduction, minor bug-fixes 151 | - 0.8.0 - released *2013-10-01* 152 | - Major bugfixes 153 | - IPv6 support 154 | - 0.7.0 - released *2013-10-01* 155 | - Bugfixes and code cleanup. 156 | - Most notable fix around Basic.Return and recursion in Channel._wait_on_frame. 157 | - 0.6.0 - released *2013-09-30* 158 | - Bugfix with Queue.get() 159 | - Bugfix with RPC requests expecting multiple responses 160 | - Add Queue.consume_messages() method. 161 | - 0.5.1 - released *2013-09-24* 162 | - Installer/setup fix 163 | - 0.5.0 - released *2013-09-23* 164 | - Bugfix release including low level socket sending fix and connection timeouts. 165 | - < 0.5.0 166 | - Previously called rmqid 167 | -------------------------------------------------------------------------------- /rabbitpy/channel0.py: -------------------------------------------------------------------------------- 1 | """ 2 | Channel0 is used for connection level communication between RabbitMQ and the 3 | client on channel 0. 4 | 5 | """ 6 | import locale 7 | import logging 8 | import sys 9 | 10 | from pamqp import header 11 | from pamqp import heartbeat 12 | from pamqp import specification 13 | 14 | from rabbitpy import __version__ 15 | from rabbitpy import base 16 | from rabbitpy import events 17 | from rabbitpy import exceptions 18 | from rabbitpy.utils import queue 19 | 20 | LOGGER = logging.getLogger(__name__) 21 | DEFAULT_LOCALE = locale.getdefaultlocale() 22 | del locale 23 | 24 | 25 | class Channel0(base.AMQPChannel): 26 | """Channel0 is used to negotiate a connection with RabbitMQ and for 27 | processing and dispatching events on channel 0 once connected. 28 | 29 | :param dict connection_args: Data required to negotiate the connection 30 | :param events_obj: The shared events coordination object 31 | :type events_obj: rabbitpy.events.Events 32 | :param exception_queue: The queue where any pending exceptions live 33 | :type exception_queue: queue.Queue 34 | :param write_queue: The queue to place data to write in 35 | :type write_queue: queue.Queue 36 | :param write_trigger: The socket to write to, to trigger IO writes 37 | :type write_trigger: socket.socket 38 | 39 | """ 40 | CHANNEL = 0 41 | 42 | CLOSE_REQUEST_FRAME = specification.Connection.Close 43 | DEFAULT_LOCALE = 'en-US' 44 | 45 | def __init__(self, connection_args, events_obj, exception_queue, 46 | write_queue, write_trigger, connection): 47 | super(Channel0, self).__init__( 48 | exception_queue, write_trigger, connection) 49 | self._channel_id = 0 50 | self._args = connection_args 51 | self._events = events_obj 52 | self._exceptions = exception_queue 53 | self._read_queue = queue.Queue() 54 | self._write_queue = write_queue 55 | self._write_trigger = write_trigger 56 | self._state = self.CLOSED 57 | self._max_channels = connection_args['channel_max'] 58 | self._max_frame_size = connection_args['frame_max'] 59 | self._heartbeat_interval = connection_args['heartbeat'] 60 | self.properties = None 61 | 62 | def close(self): 63 | """Close the connection via Channel0 communication.""" 64 | if self.open: 65 | self._set_state(self.CLOSING) 66 | self.rpc(specification.Connection.Close()) 67 | 68 | @property 69 | def heartbeat_interval(self): 70 | """Return the AMQP heartbeat interval for the connection 71 | 72 | :rtype: int 73 | 74 | """ 75 | return self._heartbeat_interval 76 | 77 | @property 78 | def maximum_channels(self): 79 | """Return the AMQP maximum channel count for the connection 80 | 81 | :rtype: int 82 | 83 | """ 84 | return self._max_channels 85 | 86 | @property 87 | def maximum_frame_size(self): 88 | """Return the AMQP maximum frame size for the connection 89 | 90 | :rtype: int 91 | 92 | """ 93 | return self._max_frame_size 94 | 95 | def on_frame(self, value): 96 | """Process a RPC frame received from the server 97 | 98 | :param pamqp.message.Message value: The message value 99 | 100 | """ 101 | LOGGER.debug('Received frame: %r', value.name) 102 | if value.name == 'Connection.Close': 103 | LOGGER.warning('RabbitMQ closed the connection (%s): %s', 104 | value.reply_code, value.reply_text) 105 | self._set_state(self.CLOSED) 106 | self._events.set(events.SOCKET_CLOSED) 107 | self._events.set(events.CHANNEL0_CLOSED) 108 | self._connection.close() 109 | if value.reply_code in exceptions.AMQP: 110 | err = exceptions.AMQP[value.reply_code](value.reply_text) 111 | else: 112 | err = exceptions.RemoteClosedException(value.reply_code, 113 | value.reply_text) 114 | self._exceptions.put(err) 115 | self._trigger_write() 116 | elif value.name == 'Connection.Blocked': 117 | LOGGER.warning('RabbitMQ has blocked the connection: %s', 118 | value.reason) 119 | self._events.set(events.CONNECTION_BLOCKED) 120 | elif value.name == 'Connection.CloseOk': 121 | self._set_state(self.CLOSED) 122 | self._events.set(events.CHANNEL0_CLOSED) 123 | elif value.name == 'Connection.OpenOk': 124 | self._on_connection_open_ok() 125 | elif value.name == 'Connection.Start': 126 | self._on_connection_start(value) 127 | elif value.name == 'Connection.Tune': 128 | self._on_connection_tune(value) 129 | elif value.name == 'Connection.Unblocked': 130 | LOGGER.info('Connection is no longer blocked') 131 | self._events.clear(events.CONNECTION_BLOCKED) 132 | elif value.name == 'Heartbeat': 133 | pass 134 | else: 135 | LOGGER.warning('Unexpected Channel0 Frame: %r', value) 136 | raise specification.AMQPUnexpectedFrame(value) 137 | 138 | def send_heartbeat(self): 139 | """Send a heartbeat frame to the remote connection.""" 140 | self.write_frame(heartbeat.Heartbeat()) 141 | 142 | def start(self): 143 | """Start the AMQP protocol negotiation""" 144 | self._set_state(self.OPENING) 145 | self._write_protocol_header() 146 | 147 | def _build_open_frame(self): 148 | """Build and return the Connection.Open frame. 149 | 150 | :rtype: pamqp.specification.Connection.Open 151 | 152 | """ 153 | return specification.Connection.Open(self._args['virtual_host']) 154 | 155 | def _build_start_ok_frame(self): 156 | """Build and return the Connection.StartOk frame. 157 | 158 | :rtype: pamqp.specification.Connection.StartOk 159 | 160 | """ 161 | properties = { 162 | 'product': 'rabbitpy', 163 | 'platform': 'Python {0}.{1}.{2}'.format(*sys.version_info), 164 | 'capabilities': {'authentication_failure_close': True, 165 | 'basic.nack': True, 166 | 'connection.blocked': True, 167 | 'consumer_cancel_notify': True, 168 | 'publisher_confirms': True}, 169 | 'information': 'See https://rabbitpy.readthedocs.io', 170 | 'version': __version__} 171 | return specification.Connection.StartOk(client_properties=properties, 172 | response=self._credentials, 173 | locale=self._get_locale()) 174 | 175 | def _build_tune_ok_frame(self): 176 | """Build and return the Connection.TuneOk frame. 177 | 178 | :rtype: pamqp.specification.Connection.TuneOk 179 | 180 | """ 181 | return specification.Connection.TuneOk(self._max_channels, 182 | self._max_frame_size, 183 | self._heartbeat_interval) 184 | 185 | @property 186 | def _credentials(self): 187 | """Return the marshaled credentials for the AMQP connection. 188 | 189 | :rtype: str 190 | 191 | """ 192 | return '\0%s\0%s' % (self._args['username'], self._args['password']) 193 | 194 | def _get_locale(self): 195 | """Return the current locale for the python interpreter or the default 196 | locale. 197 | 198 | :rtype: str 199 | 200 | """ 201 | if not self._args['locale']: 202 | return DEFAULT_LOCALE[0] or self.DEFAULT_LOCALE 203 | return self._args['locale'] 204 | 205 | @staticmethod 206 | def _negotiate(client_value, server_value): 207 | """Return the negotiated value between what the client has requested 208 | and the server has requested for how the two will communicate. 209 | 210 | :param int client_value: 211 | :param int server_value: 212 | :return: int 213 | 214 | """ 215 | return min(client_value, server_value) or \ 216 | (client_value or server_value) 217 | 218 | def _on_connection_open_ok(self): 219 | LOGGER.debug('Connection opened') 220 | self._set_state(self.OPEN) 221 | self._events.set(events.CHANNEL0_OPENED) 222 | 223 | def _on_connection_start(self, frame_value): 224 | """Negotiate the Connection.Start process, writing out a 225 | Connection.StartOk frame when the Connection.Start frame is received. 226 | 227 | :type frame_value: pamqp.specification.Connection.Start 228 | :raises: rabbitpy.exceptions.ConnectionException 229 | 230 | """ 231 | if not self._validate_connection_start(frame_value): 232 | LOGGER.error('Could not negotiate a connection, disconnecting') 233 | raise exceptions.ConnectionResetException() 234 | 235 | self.properties = frame_value.server_properties 236 | for key in self.properties: 237 | if key == 'capabilities': 238 | for capability in self.properties[key]: 239 | LOGGER.debug('Server supports %s: %r', 240 | capability, self.properties[key][capability]) 241 | else: 242 | LOGGER.debug('Server %s: %r', key, self.properties[key]) 243 | self.write_frame(self._build_start_ok_frame()) 244 | 245 | def _on_connection_tune(self, frame_value): 246 | """Negotiate the Connection.Tune frames, waiting for the 247 | Connection.Tune frame from RabbitMQ and sending the Connection.TuneOk 248 | frame. 249 | 250 | :param specification.Connection.Tune frame_value: Tune frame 251 | 252 | """ 253 | self._max_frame_size = self._negotiate(self._max_frame_size, 254 | frame_value.frame_max) 255 | self._max_channels = self._negotiate(self._max_channels, 256 | frame_value.channel_max) 257 | 258 | LOGGER.debug('Heartbeat interval (server/client): %r/%r', 259 | frame_value.heartbeat, self._heartbeat_interval) 260 | 261 | # Properly negotiate the heartbeat interval 262 | if self._heartbeat_interval is None: 263 | self._heartbeat_interval = frame_value.heartbeat 264 | elif self._heartbeat_interval == 0 or frame_value.heartbeat == 0: 265 | self._heartbeat_interval = 0 266 | 267 | self.write_frame(self._build_tune_ok_frame()) 268 | self.write_frame(self._build_open_frame()) 269 | 270 | @staticmethod 271 | def _validate_connection_start(frame_value): 272 | """Validate the received Connection.Start frame 273 | 274 | :param specification.Connection.Start frame_value: Frame to validate 275 | :rtype: bool 276 | 277 | """ 278 | if (frame_value.version_major, frame_value.version_minor) != \ 279 | (specification.VERSION[0], specification.VERSION[1]): 280 | LOGGER.warning('AMQP version error (received %i.%i, expected %r)', 281 | frame_value.version_major, 282 | frame_value.version_minor, 283 | specification.VERSION) 284 | return False 285 | return True 286 | 287 | def _write_protocol_header(self): 288 | """Send the protocol header to the connected server.""" 289 | self.write_frame(header.ProtocolHeader()) 290 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Add files or directories to the blacklist. They should be base names, not 11 | # paths. 12 | ignore=CVS 13 | 14 | # Pickle collected data for later comparisons. 15 | persistent=yes 16 | 17 | # List of plugins (as comma separated values of python modules names) to load, 18 | # usually to register additional checkers. 19 | load-plugins=pylint.extensions.check_docs 20 | 21 | # Use multiple processes to speed up Pylint. 22 | jobs=1 23 | 24 | # Allow loading of arbitrary C extensions. Extensions are imported into the 25 | # active Python interpreter and may run arbitrary code. 26 | unsafe-load-any-extension=no 27 | 28 | # A comma-separated list of package or module names from where C extensions may 29 | # be loaded. Extensions are loading into the active Python interpreter and may 30 | # run arbitrary code 31 | extension-pkg-whitelist= 32 | 33 | # Allow optimization of some AST trees. This will activate a peephole AST 34 | # optimizer, which will apply various small optimizations. For instance, it can 35 | # be used to obtain the result of joining multiple strings with the addition 36 | # operator. Joining a lot of strings can lead to a maximum recursion error in 37 | # Pylint and this flag can prevent that. It has one side effect, the resulting 38 | # AST will be different than the one from reality. 39 | optimize-ast=no 40 | 41 | 42 | [MESSAGES CONTROL] 43 | 44 | # Only show warnings with the listed confidence levels. Leave empty to show 45 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 46 | confidence= 47 | 48 | # Enable the message, report, category or checker with the given id(s). You can 49 | # either give multiple identifier separated by comma (,) or put this option 50 | # multiple time (only on the command line, not in the configuration file where 51 | # it should appear only once). See also the "--disable" option for examples. 52 | #enable= 53 | 54 | # Disable the message, report, category or checker with the given id(s). You 55 | # can either give multiple identifiers separated by comma (,) or put this 56 | # option multiple times (only on the command line, not in the configuration 57 | # file where it should appear only once).You can also use "--disable=all" to 58 | # disable everything first and then reenable specific checks. For example, if 59 | # you want to run only the similarities checker, you can use "--disable=all 60 | # --enable=similarities". If you want to run only the classes checker, but have 61 | # no Warning level messages displayed, use"--disable=all --enable=classes 62 | # --disable=W" 63 | disable=coerce-builtin,dict-iter-method,old-octal-literal,old-ne-operator,reduce-builtin,xrange-builtin,dict-view-method,cmp-builtin,raising-string,buffer-builtin,input-builtin,unichr-builtin,coerce-method,backtick,print-statement,old-division,delslice-method,nonzero-method,long-builtin,using-cmp-argument,standarderror-builtin,setslice-method,indexing-exception,file-builtin,parameter-unpacking,suppressed-message,raw_input-builtin,long-suffix,useless-suppression,unpacking-in-except,getslice-method,zip-builtin-not-iterating,hex-method,import-star-module-level,unicode-builtin,map-builtin-not-iterating,apply-builtin,execfile-builtin,next-method-called,cmp-method,metaclass-assignment,reload-builtin,round-builtin,old-raise-syntax,filter-builtin-not-iterating,intern-builtin,no-absolute-import,oct-method,basestring-builtin,range-builtin-not-iterating 64 | 65 | 66 | [REPORTS] 67 | 68 | # Set the output format. Available formats are text, parseable, colorized, msvs 69 | # (visual studio) and html. You can also give a reporter class, eg 70 | # mypackage.mymodule.MyReporterClass. 71 | output-format=text 72 | 73 | # Put messages in a separate file for each module / package specified on the 74 | # command line instead of printing them on stdout. Reports (if any) will be 75 | # written in a file name "pylint_global.[txt|html]". 76 | files-output=no 77 | 78 | # Tells whether to display a full report or only the messages 79 | reports=yes 80 | 81 | # Python expression which should return a note less than 10 (10 is the highest 82 | # note). You have access to the variables errors warning, statement which 83 | # respectively contain the number of errors / warnings messages and the total 84 | # number of statements analyzed. This is used by the global evaluation report 85 | # (RP0004). 86 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 87 | 88 | # Template used to display messages. This is a python new-style format string 89 | # used to format the message information. See doc for all details 90 | #msg-template= 91 | 92 | 93 | [BASIC] 94 | 95 | # List of builtins function names that should not be used, separated by a comma 96 | bad-functions=map,filter 97 | 98 | # Good variable names which should always be accepted, separated by a comma 99 | good-names=i,j,k,ex,Run,_ 100 | 101 | # Bad variable names which should always be refused, separated by a comma 102 | bad-names=foo,bar,baz,toto,tutu,tata 103 | 104 | # Colon-delimited sets of names that determine each other's naming style when 105 | # the name regexes allow several styles. 106 | name-group= 107 | 108 | # Include a hint for the correct naming format with invalid-name 109 | include-naming-hint=no 110 | 111 | # Regular expression matching correct module names 112 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 113 | 114 | # Naming hint for module names 115 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 116 | 117 | # Regular expression matching correct class names 118 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 119 | 120 | # Naming hint for class names 121 | class-name-hint=[A-Z_][a-zA-Z0-9]+$ 122 | 123 | # Regular expression matching correct attribute names 124 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 125 | 126 | # Naming hint for attribute names 127 | attr-name-hint=[a-z_][a-z0-9_]{2,30}$ 128 | 129 | # Regular expression matching correct constant names 130 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 131 | 132 | # Naming hint for constant names 133 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 134 | 135 | # Regular expression matching correct argument names 136 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 137 | 138 | # Naming hint for argument names 139 | argument-name-hint=[a-z_][a-z0-9_]{2,30}$ 140 | 141 | # Regular expression matching correct variable names 142 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 143 | 144 | # Naming hint for variable names 145 | variable-name-hint=[a-z_][a-z0-9_]{2,30}$ 146 | 147 | # Regular expression matching correct method names 148 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 149 | 150 | # Naming hint for method names 151 | method-name-hint=[a-z_][a-z0-9_]{2,30}$ 152 | 153 | # Regular expression matching correct function names 154 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 155 | 156 | # Naming hint for function names 157 | function-name-hint=[a-z_][a-z0-9_]{2,30}$ 158 | 159 | # Regular expression matching correct class attribute names 160 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 161 | 162 | # Naming hint for class attribute names 163 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 164 | 165 | # Regular expression matching correct inline iteration names 166 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 167 | 168 | # Naming hint for inline iteration names 169 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 170 | 171 | # Regular expression which should only match function or class names that do 172 | # not require a docstring. 173 | no-docstring-rgx=^_ 174 | 175 | # Minimum line length for functions/classes that require docstrings, shorter 176 | # ones are exempt. 177 | docstring-min-length=-1 178 | 179 | 180 | [ELIF] 181 | 182 | # Maximum number of nested blocks for function / method body 183 | max-nested-blocks=5 184 | 185 | 186 | [FORMAT] 187 | 188 | # Maximum number of characters on a single line. 189 | max-line-length=100 190 | 191 | # Regexp for a line that is allowed to be longer than the limit. 192 | ignore-long-lines=^\s*(# )??$ 193 | 194 | # Allow the body of an if to be on the same line as the test if there is no 195 | # else. 196 | single-line-if-stmt=no 197 | 198 | # List of optional constructs for which whitespace checking is disabled. `dict- 199 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 200 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 201 | # `empty-line` allows space-only lines. 202 | no-space-check=trailing-comma,dict-separator 203 | 204 | # Maximum number of lines in a module 205 | max-module-lines=1000 206 | 207 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 208 | # tab). 209 | indent-string=' ' 210 | 211 | # Number of spaces of indent required inside a hanging or continued line. 212 | indent-after-paren=4 213 | 214 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 215 | expected-line-ending-format= 216 | 217 | 218 | [LOGGING] 219 | 220 | # Logging modules to check that the string format arguments are in logging 221 | # function parameter format 222 | logging-modules=logging 223 | 224 | 225 | [MISCELLANEOUS] 226 | 227 | # List of note tags to take in consideration, separated by a comma. 228 | notes=FIXME,XXX,TODO 229 | 230 | 231 | [SIMILARITIES] 232 | 233 | # Minimum lines number of a similarity. 234 | min-similarity-lines=4 235 | 236 | # Ignore comments when computing similarities. 237 | ignore-comments=yes 238 | 239 | # Ignore docstrings when computing similarities. 240 | ignore-docstrings=yes 241 | 242 | # Ignore imports when computing similarities. 243 | ignore-imports=no 244 | 245 | 246 | [SPELLING] 247 | 248 | # Spelling dictionary name. Available dictionaries: none. To make it working 249 | # install python-enchant package. 250 | spelling-dict= 251 | 252 | # List of comma separated words that should not be checked. 253 | spelling-ignore-words= 254 | 255 | # A path to a file that contains private dictionary; one word per line. 256 | spelling-private-dict-file= 257 | 258 | # Tells whether to store unknown words to indicated private dictionary in 259 | # --spelling-private-dict-file option instead of raising a message. 260 | spelling-store-unknown-words=no 261 | 262 | 263 | [TYPECHECK] 264 | 265 | # Tells whether missing members accessed in mixin class should be ignored. A 266 | # mixin class is detected if its name ends with "mixin" (case insensitive). 267 | ignore-mixin-members=yes 268 | 269 | # List of module names for which member attributes should not be checked 270 | # (useful for modules/projects where namespaces are manipulated during runtime 271 | # and thus existing member attributes cannot be deduced by static analysis. It 272 | # supports qualified module names, as well as Unix pattern matching. 273 | ignored-modules= 274 | 275 | # List of classes names for which member attributes should not be checked 276 | # (useful for classes with attributes dynamically set). This supports can work 277 | # with qualified names. 278 | ignored-classes= 279 | 280 | # List of members which are set dynamically and missed by pylint inference 281 | # system, and so shouldn't trigger E1101 when accessed. Python regular 282 | # expressions are accepted. 283 | generated-members= 284 | 285 | 286 | [VARIABLES] 287 | 288 | # Tells whether we should check for unused import in __init__ files. 289 | init-import=no 290 | 291 | # A regular expression matching the name of dummy variables (i.e. expectedly 292 | # not used). 293 | dummy-variables-rgx=_$|dummy 294 | 295 | # List of additional names supposed to be defined in builtins. Remember that 296 | # you should avoid to define new builtins when possible. 297 | additional-builtins= 298 | 299 | # List of strings which can identify a callback function by name. A callback 300 | # name must start or end with one of those strings. 301 | callbacks=cb_,_cb 302 | 303 | 304 | [CLASSES] 305 | 306 | # List of method names used to declare (i.e. assign) instance attributes. 307 | defining-attr-methods=__init__,__new__,setUp 308 | 309 | # List of valid names for the first argument in a class method. 310 | valid-classmethod-first-arg=cls 311 | 312 | # List of valid names for the first argument in a metaclass class method. 313 | valid-metaclass-classmethod-first-arg=mcs 314 | 315 | # List of member names, which should be excluded from the protected access 316 | # warning. 317 | exclude-protected=_asdict,_fields,_replace,_source,_make 318 | 319 | 320 | [DESIGN] 321 | 322 | # Maximum number of arguments for function / method 323 | max-args=10 324 | 325 | # Argument names that match this expression will be ignored. Default to name 326 | # with leading underscore 327 | ignored-argument-names=_.* 328 | 329 | # Maximum number of locals for function / method body 330 | max-locals=15 331 | 332 | # Maximum number of return / yield for function / method body 333 | max-returns=6 334 | 335 | # Maximum number of branch for function / method body 336 | max-branches=12 337 | 338 | # Maximum number of statements in function / method body 339 | max-statements=50 340 | 341 | # Maximum number of parents for a class (see R0901). 342 | max-parents=7 343 | 344 | # Maximum number of attributes for a class (see R0902). 345 | max-attributes=15 346 | 347 | # Minimum number of public methods for a class (see R0903). 348 | min-public-methods=1 349 | 350 | # Maximum number of public methods for a class (see R0904). 351 | max-public-methods=20 352 | 353 | # Maximum number of boolean expressions in a if statement 354 | max-bool-expr=5 355 | 356 | 357 | [IMPORTS] 358 | 359 | # Deprecated modules which should not be used, separated by a comma 360 | deprecated-modules=optparse 361 | 362 | # Create a graph of every (i.e. internal and external) dependencies in the 363 | # given file (report RP0402 must not be disabled) 364 | import-graph= 365 | 366 | # Create a graph of external dependencies in the given file (report RP0402 must 367 | # not be disabled) 368 | ext-import-graph= 369 | 370 | # Create a graph of internal dependencies in the given file (report RP0402 must 371 | # not be disabled) 372 | int-import-graph= 373 | 374 | 375 | [EXCEPTIONS] 376 | 377 | # Exceptions that will emit a warning when being caught. Defaults to 378 | # "Exception" 379 | overgeneral-exceptions=Exception 380 | -------------------------------------------------------------------------------- /rabbitpy/message.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Message class represents a message that is sent or received and contains 3 | methods for publishing the message, or in the case that the message was 4 | delivered by RabbitMQ, acknowledging it, rejecting it or negatively 5 | acknowledging it. 6 | 7 | """ 8 | import datetime 9 | import json 10 | import logging 11 | import math 12 | import time 13 | import pprint 14 | import uuid 15 | 16 | from pamqp import body 17 | from pamqp import header 18 | from pamqp import specification 19 | 20 | from rabbitpy import base 21 | from rabbitpy import exceptions 22 | from rabbitpy import utils 23 | 24 | LOGGER = logging.getLogger(__name__) 25 | 26 | 27 | # Python 2.6 does not have a memoryview object, create dummy for isinstance 28 | try: 29 | _PY_VERSION_CHECK = memoryview(b'foo') 30 | except NameError: 31 | # pylint: disable=too-few-public-methods, redefined-builtin 32 | # pylint: disable=invalid-name, missing-docstring 33 | class memoryview(object): 34 | pass 35 | 36 | 37 | class Properties(specification.Basic.Properties): 38 | """Proxy class for :py:class:`pamqp.specification.Basic.Properties`""" 39 | pass 40 | 41 | 42 | class Message(base.AMQPClass): 43 | """Created by both rabbitpy internally when a message is delivered or 44 | returned from RabbitMQ and by implementing applications, the Message class 45 | is used to publish a message to and access and respond to a message from 46 | RabbitMQ. 47 | 48 | When specifying properties for a message, pass in a dict of key value items 49 | that match the AMQP Basic.Properties specification with a small caveat. 50 | 51 | Due to an overlap in the AMQP specification and the Python keyword 52 | :code:`type`, the :code:`type` property is referred to as 53 | :code:`message_type`. 54 | 55 | The following is a list of the available properties: 56 | 57 | * app_id 58 | * content_type 59 | * content_encoding 60 | * correlation_id 61 | * delivery_mode 62 | * expiration 63 | * headers 64 | * message_id 65 | * message_type 66 | * priority 67 | * reply_to 68 | * timestamp 69 | * user_id 70 | 71 | **Automated features** 72 | 73 | When passing in the body value, if it is a dict or list, it will 74 | automatically be JSON serialized and the content type ``application/json`` 75 | will be set on the message properties. 76 | 77 | When publishing a message to RabbitMQ, if the opinionated value is ``True`` 78 | and no ``message_id`` value was passed in as a property, a UUID will be 79 | generated and specified as a property of the message. 80 | 81 | Additionally, if opinionated is ``True`` and the ``timestamp`` property 82 | is not specified when passing in ``properties``, the current Unix epoch 83 | value will be set in the message properties. 84 | 85 | .. note:: As of 0.21.0 ``auto_id`` is deprecated in favor of 86 | ``opinionated`` and it will be removed in a future version. As of 87 | 0.22.0 ``opinionated`` is defaulted to ``False``. 88 | 89 | :param channel: The channel object for the message object to act upon 90 | :type channel: :py:class:`rabbitpy.channel.Channel` 91 | :param body_value: The message body 92 | :type body_value: str|bytes|unicode|memoryview|dict|json 93 | :param dict properties: A dictionary of message properties 94 | :param bool auto_id: Add a message id if no properties were passed in. 95 | :param bool opinionated: Automatically populate properties if True 96 | :raises KeyError: Raised when an invalid property is passed in 97 | 98 | """ 99 | method = None 100 | name = 'Message' 101 | 102 | def __init__(self, channel, body_value, properties=None, auto_id=False, 103 | opinionated=False): 104 | """Create a new instance of the Message object.""" 105 | super(Message, self).__init__(channel, 'Message') 106 | 107 | # Always have a dict of properties set 108 | self.properties = properties or {} 109 | 110 | # Assign the body value 111 | if isinstance(body_value, memoryview): 112 | self.body = bytes(body_value) 113 | else: 114 | # pylint: disable=redefined-variable-type 115 | self.body = self._auto_serialize(body_value) 116 | 117 | # Add a message id if auto_id is not turned off and it is not set 118 | if (opinionated or auto_id) and 'message_id' not in self.properties: 119 | if auto_id: 120 | raise DeprecationWarning('Use opinionated instead of auto_id') 121 | self._add_auto_message_id() 122 | 123 | if opinionated: 124 | if 'timestamp' not in self.properties: 125 | self._add_timestamp() 126 | 127 | # Enforce datetime timestamps 128 | if 'timestamp' in self.properties: 129 | self.properties['timestamp'] = \ 130 | self._as_datetime(self.properties['timestamp']) 131 | 132 | # Don't let invalid property keys in 133 | if self._invalid_properties: 134 | msg = 'Invalid property: %s' % self._invalid_properties[0] 135 | raise KeyError(msg) 136 | 137 | @property 138 | def delivery_tag(self): 139 | """Return the delivery tag for a message that was delivered or gotten 140 | from RabbitMQ. 141 | 142 | :rtype: int or None 143 | 144 | """ 145 | return self.method.delivery_tag if self.method else None 146 | 147 | @property 148 | def redelivered(self): 149 | """Indicates if this message may have been delivered before (but not 150 | acknowledged)" 151 | 152 | :rtype: bool or None 153 | 154 | """ 155 | return self.method.redelivered if self.method else None 156 | 157 | @property 158 | def routing_key(self): 159 | """Return the routing_key for a message that was delivered or gotten 160 | from RabbitMQ. 161 | 162 | :rtype: int or None 163 | 164 | """ 165 | return self.method.routing_key if self.method else None 166 | 167 | @property 168 | def exchange(self): 169 | """Return the source exchange for a message that was delivered or 170 | gotten from RabbitMQ. 171 | 172 | :rtype: string or None 173 | 174 | """ 175 | return self.method.exchange if self.method else None 176 | 177 | def ack(self, all_previous=False): 178 | """Acknowledge receipt of the message to RabbitMQ. Will raise an 179 | ActionException if the message was not received from a broker. 180 | 181 | :raises: ActionException 182 | 183 | """ 184 | if not self.method: 185 | raise exceptions.ActionException('Can not ack non-received ' 186 | 'message') 187 | basic_ack = specification.Basic.Ack(self.method.delivery_tag, 188 | multiple=all_previous) 189 | self.channel.write_frame(basic_ack) 190 | 191 | def json(self): 192 | """Deserialize the message body if it is JSON, returning the value. 193 | 194 | :rtype: any 195 | 196 | """ 197 | try: 198 | return json.loads(self.body) 199 | except TypeError: # pragma: no cover 200 | return json.loads(self.body.decode('utf-8')) 201 | 202 | def nack(self, requeue=False, all_previous=False): 203 | """Negatively acknowledge receipt of the message to RabbitMQ. Will 204 | raise an ActionException if the message was not received from a broker. 205 | 206 | :param bool requeue: Requeue the message 207 | :param bool all_previous: Nack all previous unacked messages up to and 208 | including this one 209 | :raises: ActionException 210 | 211 | """ 212 | if not self.method: 213 | raise exceptions.ActionException('Can not nack non-received ' 214 | 'message') 215 | basic_nack = specification.Basic.Nack(self.method.delivery_tag, 216 | requeue=requeue, 217 | multiple=all_previous) 218 | self.channel.write_frame(basic_nack) 219 | 220 | def pprint(self, properties=False): # pragma: no cover 221 | """Print a formatted representation of the message. 222 | 223 | :param bool properties: Include properties in the representation 224 | 225 | """ 226 | print('Exchange: %s\n' % self.method.exchange) 227 | print('Routing Key: %s\n' % self.method.routing_key) 228 | if properties: 229 | print('Properties:\n') 230 | pprint.pprint(self.properties) 231 | print('\nBody:\n') 232 | pprint.pprint(self.body) 233 | 234 | def publish(self, exchange, routing_key='', mandatory=False, 235 | immediate=False): 236 | """Publish the message to the exchange with the specified routing 237 | key. 238 | 239 | In Python 2 if the message is a ``unicode`` value it will be converted 240 | to a ``str`` using ``str.encode('UTF-8')``. If you do not want the 241 | auto-conversion to take place, set the body to a ``str`` or ``bytes`` 242 | value prior to publishing. 243 | 244 | In Python 3 if the message is a ``str`` value it will be converted to 245 | a ``bytes`` value using ``bytes(value.encode('UTF-8'))``. If you do 246 | not want the auto-conversion to take place, set the body to a 247 | ``bytes`` value prior to publishing. 248 | 249 | :param exchange: The exchange to publish the message to 250 | :type exchange: str or :class:`rabbitpy.Exchange` 251 | :param str routing_key: The routing key to use 252 | :param bool mandatory: Requires the message is published 253 | :param bool immediate: Request immediate delivery 254 | :return: bool or None 255 | :raises: rabbitpy.exceptions.MessageReturnedException 256 | 257 | """ 258 | if isinstance(exchange, base.AMQPClass): 259 | exchange = exchange.name 260 | 261 | # Coerce the body to the proper type 262 | payload = utils.maybe_utf8_encode(self.body) 263 | 264 | frames = [specification.Basic.Publish(exchange=exchange, 265 | routing_key=routing_key or '', 266 | mandatory=mandatory, 267 | immediate=immediate), 268 | header.ContentHeader(body_size=len(payload), 269 | properties=self._properties)] 270 | 271 | # Calculate how many body frames are needed 272 | pieces = int(math.ceil(len(payload) / 273 | float(self.channel.maximum_frame_size))) 274 | 275 | # Send the message 276 | for offset in range(0, pieces): 277 | start = self.channel.maximum_frame_size * offset 278 | end = start + self.channel.maximum_frame_size 279 | if end > len(payload): 280 | end = len(payload) 281 | frames.append(body.ContentBody(payload[start:end])) 282 | 283 | # Write the frames out 284 | self.channel.write_frames(frames) 285 | 286 | # If publisher confirmations are enabled, wait for the response 287 | if self.channel.publisher_confirms: 288 | response = self.channel.wait_for_confirmation() 289 | if isinstance(response, specification.Basic.Ack): 290 | return True 291 | elif isinstance(response, specification.Basic.Nack): 292 | return False 293 | else: 294 | raise exceptions.UnexpectedResponseError(response) 295 | 296 | def reject(self, requeue=False): 297 | """Reject receipt of the message to RabbitMQ. Will raise 298 | an ActionException if the message was not received from a broker. 299 | 300 | :param bool requeue: Requeue the message 301 | :raises: ActionException 302 | 303 | """ 304 | if not self.method: 305 | raise exceptions.ActionException('Can not reject non-received ' 306 | 'message') 307 | basic_reject = specification.Basic.Reject(self.method.delivery_tag, 308 | requeue=requeue) 309 | self.channel.write_frame(basic_reject) 310 | 311 | def _add_auto_message_id(self): 312 | """Set the message_id property to a new UUID.""" 313 | self.properties['message_id'] = str(uuid.uuid4()) 314 | 315 | def _add_timestamp(self): 316 | """Add the timestamp to the properties""" 317 | self.properties['timestamp'] = datetime.datetime.utcnow() 318 | 319 | @staticmethod 320 | def _as_datetime(value): 321 | """Return the passed in value as a ``datetime.datetime`` value. 322 | 323 | :param value: The value to convert or pass through 324 | :type value: datetime.datetime 325 | :type value: time.struct_time 326 | :type value: int 327 | :type value: float 328 | :type value: str 329 | :type value: bytes 330 | :type value: unicode 331 | :rtype: datetime.datetime 332 | :raises: TypeError 333 | 334 | """ 335 | if value is None: 336 | return None 337 | 338 | if isinstance(value, datetime.datetime): 339 | return value 340 | 341 | if isinstance(value, time.struct_time): 342 | return datetime.datetime(*value[:6]) 343 | 344 | if utils.is_string(value): 345 | value = int(value) 346 | 347 | if isinstance(value, float) or isinstance(value, int): 348 | return datetime.datetime.fromtimestamp(value) 349 | 350 | raise TypeError('Could not cast a %s value to a datetime.datetime' % 351 | type(value)) 352 | 353 | def _auto_serialize(self, body_value): 354 | """Automatically serialize the body as JSON if it is a dict or list. 355 | 356 | :param mixed body_value: The message body passed into the constructor 357 | :return: bytes|str 358 | 359 | """ 360 | if isinstance(body_value, dict) or isinstance(body_value, list): 361 | self.properties['content_type'] = 'application/json' 362 | return json.dumps(body_value, ensure_ascii=False) 363 | return body_value 364 | 365 | def _coerce_properties(self): 366 | """Force properties to be set to the correct data type""" 367 | for key, value in self.properties.items(): 368 | _type = specification.Basic.Properties.type(key) 369 | if self.properties[key] is None: 370 | continue 371 | if _type == 'shortstr': 372 | if not utils.is_string(value): 373 | LOGGER.warning('Coercing property %s to bytes', key) 374 | value = str(value) 375 | self.properties[key] = utils.maybe_utf8_encode(value) 376 | elif _type == 'octet' and not isinstance(value, int): 377 | LOGGER.warning('Coercing property %s to int', key) 378 | try: 379 | self.properties[key] = int(value) 380 | except TypeError as error: 381 | LOGGER.warning('Could not coerce %s: %s', key, error) 382 | elif _type == 'table' and not isinstance(value, dict): 383 | LOGGER.warning('Resetting invalid value for %s to None', key) 384 | self.properties[key] = {} 385 | if key == 'timestamp': 386 | self.properties[key] = self._as_datetime(value) 387 | 388 | @property 389 | def _invalid_properties(self): 390 | """Return a list of invalid properties that currently exist in the the 391 | properties that are set. 392 | 393 | :rtype: list 394 | 395 | """ 396 | return [key for key in self.properties 397 | if key not in specification.Basic.Properties.attributes()] 398 | 399 | @property 400 | def _properties(self): 401 | """Return a new Basic.Properties object representing the message 402 | properties. 403 | 404 | :rtype: pamqp.specification.Basic.Properties 405 | 406 | """ 407 | self._prune_invalid_properties() 408 | self._coerce_properties() 409 | return specification.Basic.Properties(**self.properties) 410 | 411 | def _prune_invalid_properties(self): 412 | """Remove invalid properties from the message properties.""" 413 | for key in self._invalid_properties: 414 | LOGGER.warning('Removing invalid property "%s"', key) 415 | del self.properties[key] 416 | -------------------------------------------------------------------------------- /rabbitpy/amqp.py: -------------------------------------------------------------------------------- 1 | """ 2 | AMQP Adapter 3 | 4 | """ 5 | from pamqp import specification as spec 6 | 7 | from rabbitpy import base 8 | from rabbitpy import message 9 | from rabbitpy import exceptions 10 | from rabbitpy import utils 11 | 12 | 13 | # pylint: disable=too-many-public-methods 14 | class AMQP(base.ChannelWriter): 15 | """The AMQP Adapter provides a more generic, non-opinionated interface to 16 | RabbitMQ by providing methods that map to the AMQP API. 17 | 18 | :param rabbitmq.channel.Channel channel: The channel to use 19 | 20 | """ 21 | def __init__(self, channel): 22 | super(AMQP, self).__init__(channel) 23 | self.consumer_tag = 'rabbitpy.%s.%s' % (self.channel.id, id(self)) 24 | self._consuming = False 25 | 26 | def basic_ack(self, delivery_tag=0, multiple=False): 27 | """Acknowledge one or more messages 28 | 29 | This method acknowledges one or more messages delivered via the Deliver 30 | or Get-Ok methods. The client can ask to confirm a single message or a 31 | set of messages up to and including a specific message. 32 | 33 | :param delivery_tag: Server-assigned delivery tag 34 | :type delivery_tag: int|long 35 | :param bool multiple: Acknowledge multiple messages 36 | 37 | """ 38 | self._write_frame(spec.Basic.Ack(delivery_tag, multiple)) 39 | 40 | def basic_consume(self, queue='', consumer_tag='', no_local=False, 41 | no_ack=False, exclusive=False, nowait=False, 42 | arguments=None): 43 | """Start a queue consumer 44 | 45 | This method asks the server to start a "consumer", which is a transient 46 | request for messages from a specific queue. Consumers last as long as 47 | the channel they were declared on, or until the client cancels them. 48 | 49 | This method will act as an generator, returning messages as they are 50 | delivered from the server. 51 | 52 | Example use: 53 | 54 | .. code:: python 55 | 56 | for message in basic_consume(queue_name): 57 | print message.body 58 | message.ack() 59 | 60 | :param str queue: The queue name to consume from 61 | :param str consumer_tag: The consumer tag 62 | :param bool no_local: Do not deliver own messages 63 | :param bool no_ack: No acknowledgement needed 64 | :param bool exclusive: Request exclusive access 65 | :param bool nowait: Do not send a reply method 66 | :param dict arguments: Arguments for declaration 67 | 68 | """ 69 | if not consumer_tag: 70 | consumer_tag = self.consumer_tag 71 | # pylint: disable=protected-access 72 | self.channel._consumers[consumer_tag] = (self, no_ack) 73 | self._rpc(spec.Basic.Consume(0, queue, consumer_tag, no_local, no_ack, 74 | exclusive, nowait, arguments)) 75 | self._consuming = True 76 | try: 77 | while self._consuming: 78 | # pylint: disable=protected-access 79 | msg = self.channel._consume_message() 80 | if msg: 81 | yield msg 82 | else: 83 | if self._consuming: 84 | self.basic_cancel(consumer_tag) 85 | break 86 | finally: 87 | if self._consuming: 88 | self.basic_cancel(consumer_tag) 89 | 90 | def basic_cancel(self, consumer_tag='', nowait=False): 91 | """End a queue consumer 92 | 93 | This method cancels a consumer. This does not affect already delivered 94 | messages, but it does mean the server will not send any more messages 95 | for that consumer. The client may receive an arbitrary number of 96 | messages in between sending the cancel method and receiving the cancel- 97 | ok reply. 98 | 99 | :param str consumer_tag: Consumer tag 100 | :param bool nowait: Do not send a reply method 101 | 102 | """ 103 | if utils.PYPY and not self._consuming: 104 | return 105 | if not self._consuming: 106 | raise exceptions.NotConsumingError() 107 | # pylint: disable=protected-access 108 | self.channel._cancel_consumer(self, consumer_tag, nowait) 109 | self._consuming = False 110 | 111 | def basic_get(self, queue='', no_ack=False): 112 | """Direct access to a queue 113 | 114 | This method provides a direct access to the messages in a queue using a 115 | synchronous dialogue that is designed for specific types of application 116 | where synchronous functionality is more important than performance. 117 | 118 | :param str queue: The queue name 119 | :param bool no_ack: No acknowledgement needed 120 | 121 | """ 122 | self._rpc(spec.Basic.Get(0, queue, no_ack)) 123 | 124 | def basic_nack(self, delivery_tag=0, multiple=False, requeue=True): 125 | """Reject one or more incoming messages. 126 | 127 | This method allows a client to reject one or more incoming messages. It 128 | can be used to interrupt and cancel large incoming messages, or return 129 | untreatable messages to their original queue. This method is also used 130 | by the server to inform publishers on channels in confirm mode of 131 | unhandled messages. If a publisher receives this method, it probably 132 | needs to republish the offending messages. 133 | 134 | :param delivery_tag: Server-assigned delivery tag 135 | :type delivery_tag: int|long 136 | :param bool multiple: Reject multiple messages 137 | :param bool requeue: Requeue the message 138 | 139 | """ 140 | self._write_frame(spec.Basic.Nack(delivery_tag, multiple, requeue)) 141 | 142 | def basic_publish(self, exchange='', routing_key='', body='', 143 | properties=None, mandatory=False, immediate=False): 144 | """Publish a message 145 | 146 | This method publishes a message to a specific exchange. The message 147 | will be routed to queues as defined by the exchange configuration and 148 | distributed to any active consumers when the transaction, if any, is 149 | committed. 150 | 151 | :param str exchange: The exchange name 152 | :param str routing_key: Message routing key 153 | :param body: The message body 154 | :type body: str|bytes 155 | :param dict properties: AMQP message properties 156 | :param bool mandatory: Indicate mandatory routing 157 | :param bool immediate: Request immediate delivery 158 | :return: bool or None 159 | 160 | """ 161 | msg = message.Message(self.channel, body, 162 | properties or {}, False, False) 163 | return msg.publish(exchange, routing_key, mandatory, immediate) 164 | 165 | def basic_qos(self, prefetch_size=0, prefetch_count=0, global_flag=False): 166 | """Specify quality of service 167 | 168 | This method requests a specific quality of service. The QoS can be 169 | specified for the current channel or for all channels on the 170 | connection. The particular properties and semantics of a qos method 171 | always depend on the content class semantics. Though the qos method 172 | could in principle apply to both peers, it is currently meaningful only 173 | for the server. 174 | 175 | :param prefetch_size: Prefetch window in octets 176 | :type prefetch_size: int|long 177 | :param int prefetch_count: Prefetch window in messages 178 | :param bool global_flag: Apply to entire connection 179 | 180 | """ 181 | self._rpc(spec.Basic.Qos(prefetch_size, prefetch_count, global_flag)) 182 | 183 | def basic_reject(self, delivery_tag=0, requeue=True): 184 | """Reject an incoming message 185 | 186 | This method allows a client to reject a message. It can be used to 187 | interrupt and cancel large incoming messages, or return untreatable 188 | messages to their original queue. 189 | 190 | :param delivery_tag: Server-assigned delivery tag 191 | :type delivery_tag: int|long 192 | :param bool requeue: Requeue the message 193 | 194 | """ 195 | self._write_frame(spec.Basic.Reject(delivery_tag, requeue)) 196 | 197 | def basic_recover(self, requeue=False): 198 | """Redeliver unacknowledged messages 199 | 200 | This method asks the server to redeliver all unacknowledged messages on 201 | a specified channel. Zero or more messages may be redelivered. This 202 | method replaces the asynchronous Recover. 203 | 204 | :param bool requeue: Requeue the message 205 | 206 | """ 207 | self._rpc(spec.Basic.Recover(requeue)) 208 | 209 | def confirm_select(self): 210 | """This method sets the channel to use publisher acknowledgements. The 211 | client can only use this method on a non-transactional channel. 212 | 213 | """ 214 | self._rpc(spec.Confirm.Select()) 215 | 216 | def exchange_declare(self, exchange='', exchange_type='direct', 217 | passive=False, durable=False, auto_delete=False, 218 | internal=False, nowait=False, arguments=None): 219 | """Verify exchange exists, create if needed 220 | 221 | This method creates an exchange if it does not already exist, and if 222 | the exchange exists, verifies that it is of the correct and expected 223 | class. 224 | 225 | :param str exchange: The exchange name 226 | :param str exchange_type: Exchange type 227 | :param bool passive: Do not create exchange 228 | :param bool durable: Request a durable exchange 229 | :param bool auto_delete: Automatically delete when not in use 230 | :param bool internal: Deprecated 231 | :param bool nowait: Do not send a reply method 232 | :param dict arguments: Arguments for declaration 233 | 234 | """ 235 | self._rpc(spec.Exchange.Declare(0, exchange, exchange_type, passive, 236 | durable, auto_delete, internal, nowait, 237 | arguments)) 238 | 239 | def exchange_delete(self, exchange='', if_unused=False, 240 | nowait=False): 241 | """Delete an exchange 242 | 243 | This method deletes an exchange. When an exchange is deleted all queue 244 | bindings on the exchange are cancelled. 245 | 246 | :param str exchange: The exchange name 247 | :param bool if_unused: Delete only if unused 248 | :param bool nowait: Do not send a reply method 249 | 250 | """ 251 | self._rpc(spec.Exchange.Delete(0, exchange, if_unused, nowait)) 252 | 253 | def exchange_bind(self, destination='', source='', 254 | routing_key='', nowait=False, arguments=None): 255 | """Bind exchange to an exchange. 256 | 257 | This method binds an exchange to an exchange. 258 | 259 | :param str destination: The destination exchange name 260 | :param str source: The source exchange name 261 | :param str routing_key: The routing key to bind with 262 | :param bool nowait: Do not send a reply method 263 | :param dict arguments: Optional arguments 264 | 265 | """ 266 | self._rpc(spec.Exchange.Bind(0, destination, source, routing_key, 267 | nowait, arguments)) 268 | 269 | def exchange_unbind(self, destination='', source='', 270 | routing_key='', nowait=False, arguments=None): 271 | """Unbind an exchange from an exchange. 272 | 273 | This method unbinds an exchange from an exchange. 274 | 275 | :param str destination: The destination exchange name 276 | :param str source: The source exchange name 277 | :param str routing_key: The routing key to bind with 278 | :param bool nowait: Do not send a reply method 279 | :param dict arguments: Optional arguments 280 | 281 | """ 282 | self._rpc(spec.Exchange.Unbind(0, destination, source, routing_key, 283 | nowait, arguments)) 284 | 285 | def queue_bind(self, queue='', exchange='', routing_key='', 286 | nowait=False, arguments=None): 287 | """Bind queue to an exchange 288 | 289 | This method binds a queue to an exchange. Until a queue is bound it 290 | will not receive any messages. In a classic messaging model, store-and- 291 | forward queues are bound to a direct exchange and subscription queues 292 | are bound to a topic exchange. 293 | 294 | :param str queue: The queue name 295 | :param str exchange: Name of the exchange to bind to 296 | :param str routing_key: Message routing key 297 | :param bool nowait: Do not send a reply method 298 | :param dict arguments: Arguments for binding 299 | 300 | """ 301 | self._rpc(spec.Queue.Bind(0, queue, exchange, routing_key, nowait, 302 | arguments)) 303 | 304 | def queue_declare(self, queue='', passive=False, durable=False, 305 | exclusive=False, auto_delete=False, nowait=False, 306 | arguments=None): 307 | """Declare queue, create if needed 308 | 309 | This method creates or checks a queue. When creating a new queue the 310 | client can specify various properties that control the durability of 311 | the queue and its contents, and the level of sharing for the queue. 312 | 313 | :param str queue: The queue name 314 | :param bool passive: Do not create queue 315 | :param bool durable: Request a durable queue 316 | :param bool exclusive: Request an exclusive queue 317 | :param bool auto_delete: Auto-delete queue when unused 318 | :param bool nowait: Do not send a reply method 319 | :param dict arguments: Arguments for declaration 320 | 321 | """ 322 | self._rpc(spec.Queue.Declare(0, queue, passive, durable, exclusive, 323 | auto_delete, nowait, arguments)) 324 | 325 | def queue_delete(self, queue='', if_unused=False, if_empty=False, 326 | nowait=False): 327 | """Delete a queue 328 | 329 | This method deletes a queue. When a queue is deleted any pending 330 | messages are sent to a dead-letter queue if this is defined in the 331 | server configuration, and all consumers on the queue are cancelled. 332 | 333 | :param str queue: The queue name 334 | :param bool if_unused: Delete only if unused 335 | :param bool if_empty: Delete only if empty 336 | :param bool nowait: Do not send a reply method 337 | 338 | """ 339 | self._rpc(spec.Queue.Delete(0, queue, if_unused, if_empty, nowait)) 340 | 341 | def queue_purge(self, queue='', nowait=False): 342 | """Purge a queue 343 | 344 | This method removes all messages from a queue which are not awaiting 345 | acknowledgment. 346 | 347 | :param str queue: The queue name 348 | :param bool nowait: Do not send a reply method 349 | 350 | """ 351 | self._rpc(spec.Queue.Purge(0, queue, nowait)) 352 | 353 | def queue_unbind(self, queue='', exchange='', routing_key='', 354 | arguments=None): 355 | """Unbind a queue from an exchange 356 | 357 | This method unbinds a queue from an exchange. 358 | 359 | :param str queue: The queue name 360 | :param str exchange: The exchange name 361 | :param str routing_key: Routing key of binding 362 | :param dict arguments: Arguments of binding 363 | 364 | """ 365 | self._rpc(spec.Queue.Unbind(0, queue, exchange, routing_key, 366 | arguments)) 367 | 368 | def tx_select(self): 369 | """Select standard transaction mode 370 | 371 | This method sets the channel to use standard transactions. The client 372 | must use this method at least once on a channel before using the Commit 373 | or Rollback methods. 374 | 375 | """ 376 | self._rpc(spec.Tx.Select()) 377 | 378 | def tx_commit(self): 379 | """Commit the current transaction 380 | 381 | This method commits all message publications and acknowledgments 382 | performed in the current transaction. A new transaction starts 383 | immediately after a commit. 384 | 385 | """ 386 | self._rpc(spec.Tx.Commit()) 387 | 388 | def tx_rollback(self): 389 | """Abandon the current transaction 390 | 391 | This method abandons all message publications and acknowledgments 392 | performed in the current transaction. A new transaction starts 393 | immediately after a rollback. Note that unacked messages will not be 394 | automatically redelivered by rollback; if that is required an explicit 395 | recover call should be issued. 396 | 397 | """ 398 | self._rpc(spec.Tx.Rollback()) 399 | -------------------------------------------------------------------------------- /rabbitpy/amqp_queue.py: -------------------------------------------------------------------------------- 1 | """ 2 | The rabbitpy.amqp_queue module contains two classes :py:class:`Queue` and 3 | :py:class:`Consumer`. The :py:class:`Queue` class is an object that is used 4 | create and work with queues on a RabbitMQ server. 5 | 6 | To consume messages you can iterate over the Queue object itself if the 7 | defaults for the :py:meth:`Queue.__iter__() ` method work 8 | for your needs: 9 | 10 | .. code:: python 11 | 12 | with conn.channel() as channel: 13 | for message in rabbitpy.Queue(channel, 'example'): 14 | print('Message: %r' % message) 15 | message.ack() 16 | 17 | or by the :py:meth:`Queue.consume() ` method 18 | if you would like to specify `no_ack`, `prefetch_count`, or `priority`: 19 | 20 | .. code:: python 21 | 22 | with conn.channel() as channel: 23 | queue = rabbitpy.Queue(channel, 'example') 24 | for message in queue.consume: 25 | print('Message: %r' % message) 26 | message.ack() 27 | 28 | """ 29 | import logging 30 | import warnings 31 | 32 | from pamqp import specification 33 | 34 | from rabbitpy import base 35 | from rabbitpy import exceptions 36 | from rabbitpy import utils 37 | 38 | LOGGER = logging.getLogger(__name__) 39 | 40 | 41 | class Queue(base.AMQPClass): 42 | """Create and manage RabbitMQ queues. 43 | 44 | :param channel: The channel object to communicate on 45 | :type channel: :py:class:`~rabbitpy.Channel` 46 | :param str name: The name of the queue 47 | :param exclusive: Queue can only be used by this channel and will 48 | auto-delete once the channel is closed. 49 | :type exclusive: bool 50 | :param durable: Indicates if the queue should survive a RabbitMQ is restart 51 | :type durable: bool 52 | :param bool auto_delete: Automatically delete when all consumers disconnect 53 | :param int max_length: Maximum queue length 54 | :param int message_ttl: Time-to-live of a message in milliseconds 55 | :param expires: Milliseconds until a queue is removed after becoming idle 56 | :type expires: int 57 | :param dead_letter_exchange: Dead letter exchange for rejected messages 58 | :type dead_letter_exchange: str 59 | :param dead_letter_routing_key: Routing key for dead lettered messages 60 | :type dead_letter_routing_key: str 61 | :param dict arguments: Custom arguments for the queue 62 | 63 | :attributes: 64 | - **consumer_tag** (*str*) – Contains the consumer tag used to register 65 | with RabbitMQ. Can be overwritten with custom value prior to consuming. 66 | 67 | :raises: :py:exc:`~rabbitpy.exceptions.RemoteClosedChannelException` 68 | :raises: :py:exc:`~rabbitpy.exceptions.RemoteCancellationException` 69 | 70 | """ 71 | arguments = dict() 72 | auto_delete = False 73 | dead_letter_exchange = None 74 | dead_letter_routing_key = None 75 | durable = False 76 | exclusive = False 77 | expires = None 78 | max_length = None 79 | message_ttl = None 80 | 81 | # pylint: disable=too-many-arguments 82 | def __init__(self, channel, name='', 83 | durable=False, exclusive=False, auto_delete=False, 84 | max_length=None, message_ttl=None, expires=None, 85 | dead_letter_exchange=None, dead_letter_routing_key=None, 86 | arguments=None): 87 | """Create a new Queue object instance. Only the 88 | :py:class:`rabbitpy.Channel` object is required. 89 | 90 | .. warning:: You should only use a single 91 | :py:class:`~rabbitpy.Queue` instance per channel 92 | when consuming or getting messages. Failure to do so can 93 | have unintended consequences. 94 | 95 | """ 96 | super(Queue, self).__init__(channel, name) 97 | 98 | # Defaults 99 | self.consumer_tag = 'rabbitpy.%s.%s' % (self.channel.id, id(self)) 100 | self.consuming = False 101 | 102 | # Assign Arguments 103 | self.durable = durable 104 | self.exclusive = exclusive 105 | self.auto_delete = auto_delete 106 | self.arguments = arguments or {} 107 | self.max_length = max_length 108 | self.message_ttl = message_ttl 109 | self.expires = expires 110 | self.dead_letter_exchange = dead_letter_exchange 111 | self.dead_letter_routing_key = dead_letter_routing_key 112 | 113 | def __iter__(self): 114 | """Quick way to consume messages using defaults of ``no_ack=False``, 115 | prefetch and priority not set. 116 | 117 | .. warning:: You should only use a single :py:class:`~rabbitpy.Queue` 118 | instance per channel when consuming messages. Failure to do so can 119 | have unintended consequences. 120 | 121 | :yields: :class:`~rabbitpy.Message` 122 | 123 | """ 124 | return self.consume() 125 | 126 | def __len__(self): 127 | """Return the pending number of messages in the queue by doing a 128 | passive Queue declare. 129 | 130 | :rtype: int 131 | 132 | """ 133 | response = self._rpc(self._declare(True)) 134 | return response.message_count 135 | 136 | def __setattr__(self, name, value): 137 | """Validate the data types for specific attributes when setting them, 138 | otherwise fall throw to the parent ``__setattr__`` 139 | 140 | :param str name: The attribute to set 141 | :param mixed value: The value to set 142 | :raises: ValueError 143 | 144 | """ 145 | if value is not None: 146 | if (name in ['auto_delete', 'durable', 'exclusive'] and 147 | not isinstance(value, bool)): 148 | raise ValueError('%s must be True or False' % name) 149 | elif (name in ['max_length', 'message_ttl', 'expires'] and 150 | not isinstance(value, int)): 151 | raise ValueError('%s must be an int' % name) 152 | elif (name in ['consumer_tag', 153 | 'dead_letter_exchange', 154 | 'dead_letter_routing_key'] and 155 | not utils.is_string(value)): 156 | raise ValueError('%s must be a str, bytes or unicode' % name) 157 | elif name == 'arguments' and not isinstance(value, dict): 158 | raise ValueError('arguments must be a dict') 159 | 160 | # Set the value 161 | super(Queue, self).__setattr__(name, value) 162 | 163 | def bind(self, source, routing_key=None, arguments=None): 164 | """Bind the queue to the specified exchange or routing key. 165 | 166 | :type source: str or :py:class:`rabbitpy.exchange.Exchange` exchange 167 | :param source: The exchange to bind to 168 | :param str routing_key: The routing key to use 169 | :param dict arguments: Optional arguments for for RabbitMQ 170 | :return: bool 171 | 172 | """ 173 | if hasattr(source, 'name'): 174 | source = source.name 175 | frame = specification.Queue.Bind(queue=self.name, 176 | exchange=source, 177 | routing_key=routing_key or '', 178 | arguments=arguments) 179 | response = self._rpc(frame) 180 | return isinstance(response, specification.Queue.BindOk) 181 | 182 | def consume(self, no_ack=False, prefetch=None, priority=None, 183 | consumer_tag=None): 184 | """Consume messages from the queue as a :py:class:`generator`: 185 | 186 | .. code:: python 187 | for message in queue.consume(): 188 | message.ack() 189 | 190 | You can use this method instead of the queue object as an iterator 191 | if you need to alter the prefect count, set the consumer priority or 192 | consume in no_ack mode. 193 | 194 | .. versionadded:: 0.26 195 | 196 | .. warning:: You should only use a single :py:class:`~rabbitpy.Queue` 197 | instance per channel when consuming messages. Failure to do so can 198 | have unintended consequences. 199 | 200 | :param bool no_ack: Do not require acknowledgements 201 | :param int prefetch: Set a prefetch count for the channel 202 | :param int priority: Consumer priority 203 | :param str consumer_tag: Optional consumer tag 204 | :rtype: :py:class:`generator` 205 | :raises: :exc:`~rabbitpy.exceptions.RemoteCancellationException` 206 | 207 | """ 208 | if consumer_tag: 209 | self.consumer_tag = consumer_tag 210 | self._consume(no_ack, prefetch, priority) 211 | try: 212 | while self.consuming: 213 | # pylint: disable=protected-access 214 | message = self.channel._consume_message() 215 | if message: 216 | yield message 217 | else: 218 | if self.consuming: 219 | self.stop_consuming() 220 | break 221 | finally: 222 | if self.consuming: 223 | self.stop_consuming() 224 | 225 | def consume_messages(self, no_ack=False, prefetch=None, priority=None): 226 | """Consume messages from the queue as a generator. 227 | 228 | .. warning:: This method is deprecated in favor of 229 | :py:meth:`Queue.consume` and will be removed in future releases. 230 | 231 | .. deprecated:: 0.26 232 | 233 | You can use this message instead of the queue object as an iterator 234 | if you need to alter the prefect count, set the consumer priority or 235 | consume in no_ack mode. 236 | 237 | :param bool no_ack: Do not require acknowledgements 238 | :param int prefetch: Set a prefetch count for the channel 239 | :param int priority: Consumer priority 240 | :rtype: :py:class:`Generator` 241 | :raises: :exc:`~rabbitpy.exceptions.RemoteCancellationException` 242 | 243 | """ 244 | warnings.warn('This method is deprecated in favor Queue.consume', 245 | DeprecationWarning) 246 | return self.consume(no_ack, prefetch, priority) 247 | 248 | # pylint: disable=no-self-use, unused-argument 249 | def consumer(self, no_ack=False, prefetch=None, priority=None): 250 | """Method for returning the contextmanager for consuming messages. You 251 | should not use this directly. 252 | 253 | .. warning:: This method is deprecated and will be removed in a future 254 | release. 255 | 256 | .. deprecated:: 0.26 257 | 258 | :param bool no_ack: Do not require acknowledgements 259 | :param int prefetch: Set a prefetch count for the channel 260 | :param int priority: Consumer priority 261 | :return: None 262 | 263 | """ 264 | raise DeprecationWarning() 265 | 266 | def declare(self, passive=False): 267 | """Declare the queue on the RabbitMQ channel passed into the 268 | constructor, returning the current message count for the queue and 269 | its consumer count as a tuple. 270 | 271 | :param bool passive: Passive declare to retrieve message count and 272 | consumer count information 273 | :return: Message count, Consumer count 274 | :rtype: tuple(int, int) 275 | 276 | """ 277 | response = self._rpc(self._declare(passive)) 278 | if not self.name: 279 | self.name = response.queue 280 | return response.message_count, response.consumer_count 281 | 282 | def delete(self, if_unused=False, if_empty=False): 283 | """Delete the queue 284 | 285 | :param bool if_unused: Delete only if unused 286 | :param bool if_empty: Delete only if empty 287 | 288 | """ 289 | self._rpc(specification.Queue.Delete(queue=self.name, 290 | if_unused=if_unused, 291 | if_empty=if_empty)) 292 | 293 | def get(self, acknowledge=True): 294 | """Request a single message from RabbitMQ using the Basic.Get AMQP 295 | command. 296 | 297 | .. warning:: You should only use a single :py:class:`~rabbitpy.Queue` 298 | instance per channel when getting messages. Failure to do so can 299 | have unintended consequences. 300 | 301 | 302 | :param bool acknowledge: Let RabbitMQ know if you will manually 303 | acknowledge or negatively acknowledge the 304 | message after each get. 305 | :rtype: :class:`~rabbitpy.Message` or None 306 | 307 | """ 308 | self._write_frame(specification.Basic.Get(queue=self.name, 309 | no_ack=not acknowledge)) 310 | 311 | return self.channel._get_message() # pylint: disable=protected-access 312 | 313 | def ha_declare(self, nodes=None): 314 | """Declare a the queue as highly available, passing in a list of nodes 315 | the queue should live on. If no nodes are passed, the queue will be 316 | declared across all nodes in the cluster. 317 | 318 | :param list nodes: A list of nodes to declare. If left empty, queue 319 | will be declared on all cluster nodes. 320 | :return: Message count, Consumer count 321 | :rtype: tuple(int, int) 322 | 323 | """ 324 | if nodes: 325 | self.arguments['x-ha-policy'] = 'nodes' 326 | self.arguments['x-ha-nodes'] = nodes 327 | else: 328 | self.arguments['x-ha-policy'] = 'all' 329 | if 'x-ha-nodes' in self.arguments: 330 | del self.arguments['x-ha-nodes'] 331 | return self.declare() 332 | 333 | def purge(self): 334 | """Purge the queue of all of its messages.""" 335 | self._rpc(specification.Queue.Purge()) 336 | 337 | def stop_consuming(self): 338 | """Stop consuming messages. This is usually invoked if you want to 339 | cancel your consumer from outside the context manager or generator. 340 | 341 | If you invoke this, there is a possibility that the generator method 342 | will return None instead of a :py:class:`rabbitpy.Message`. 343 | 344 | """ 345 | if utils.PYPY and not self.consuming: 346 | return 347 | if not self.consuming: 348 | raise exceptions.NotConsumingError() 349 | self.channel._cancel_consumer(self) # pylint: disable=protected-access 350 | self.consuming = False 351 | 352 | def unbind(self, source, routing_key=None): 353 | """Unbind queue from the specified exchange where it is bound the 354 | routing key. If routing key is None, use the queue name. 355 | 356 | :type source: str or :py:class:`rabbitpy.exchange.Exchange` exchange 357 | :param source: The exchange to unbind from 358 | :param str routing_key: The routing key that binds them 359 | 360 | """ 361 | if hasattr(source, 'name'): 362 | source = source.name 363 | routing_key = routing_key or self.name 364 | self._rpc(specification.Queue.Unbind(queue=self.name, exchange=source, 365 | routing_key=routing_key)) 366 | 367 | def _consume(self, no_ack=False, prefetch=None, priority=None): 368 | """Return a :py:class:_Consumer instance as a contextmanager, properly 369 | shutting down the consumer when the generator is exited. 370 | 371 | :param bool no_ack: Do not require acknowledgements 372 | :param int prefetch: Set a prefetch count for the channel 373 | :param int priority: Consumer priority 374 | :return: _Consumer 375 | 376 | """ 377 | if prefetch: 378 | self.channel.prefetch_count(prefetch, False) 379 | # pylint: disable=protected-access 380 | self.channel._consume(self, no_ack, priority) 381 | self.consuming = True 382 | 383 | def _declare(self, passive=False): 384 | """Return a specification.Queue.Declare class pre-composed for the rpc 385 | method since this can be called multiple times. 386 | 387 | :param bool passive: Passive declare to retrieve message count and 388 | consumer count information 389 | :rtype: pamqp.specification.Queue.Declare 390 | 391 | """ 392 | arguments = dict(self.arguments) 393 | if self.expires: 394 | arguments['x-expires'] = self.expires 395 | if self.message_ttl: 396 | arguments['x-message-ttl'] = self.message_ttl 397 | if self.max_length: 398 | arguments['x-max-length'] = self.max_length 399 | if self.dead_letter_exchange: 400 | arguments['x-dead-letter-exchange'] = self.dead_letter_exchange 401 | if self.dead_letter_routing_key: 402 | arguments['x-dead-letter-routing-key'] = \ 403 | self.dead_letter_routing_key 404 | 405 | LOGGER.debug('Declaring Queue %s, durable=%s, passive=%s, ' 406 | 'exclusive=%s, auto_delete=%s, arguments=%r', 407 | self.name, self.durable, passive, self.exclusive, 408 | self.auto_delete, arguments) 409 | return specification.Queue.Declare(queue=self.name, 410 | durable=self.durable, 411 | passive=passive, 412 | exclusive=self.exclusive, 413 | auto_delete=self.auto_delete, 414 | arguments=arguments) 415 | -------------------------------------------------------------------------------- /rabbitpy/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base classes for various parts of rabbitpy 3 | 4 | """ 5 | import logging 6 | import threading 7 | 8 | from pamqp import specification 9 | 10 | from rabbitpy import exceptions 11 | from rabbitpy import utils 12 | from rabbitpy.utils import queue 13 | 14 | 15 | LOGGER = logging.getLogger(__name__) 16 | 17 | 18 | class ChannelWriter(object): # pylint: disable=too-few-public-methods 19 | 20 | """The AMQP Adapter provides a more generic, non-opinionated interface to 21 | RabbitMQ by providing methods that map to the AMQP API. 22 | 23 | :param channel: The channel to use 24 | :type channel: rabbitpy.channel.Channel 25 | 26 | """ 27 | def __init__(self, channel): 28 | self.channel = channel 29 | 30 | def _rpc(self, frame_value): 31 | """Execute the RPC command for the frame. 32 | 33 | :param pamqp.specification.Frame frame_value: The frame to send 34 | :rtype: pamqp.specification.Frame or pamqp.message.Message 35 | 36 | """ 37 | LOGGER.debug('Issuing RPC to RabbitMQ: %r', frame_value) 38 | if self.channel.closed: 39 | raise exceptions.ChannelClosedException() 40 | return self.channel.rpc(frame_value) 41 | 42 | def _write_frame(self, frame_value): 43 | """Write a frame to the channel's connection 44 | 45 | :param pamqp.specification.Frame frame_value: The frame to send 46 | 47 | """ 48 | self.channel.write_frame(frame_value) 49 | 50 | 51 | class AMQPClass(ChannelWriter): # pylint: disable=too-few-public-methods 52 | """Base Class object AMQP object classes""" 53 | def __init__(self, channel, name): 54 | """Create a new ClassObject. 55 | 56 | :param channel: The channel to execute commands on 57 | :type channel: rabbitpy.Channel 58 | :param str name: Set the name 59 | :raises: ValueError 60 | 61 | """ 62 | super(AMQPClass, self).__init__(channel) 63 | # Use type so there's not a circular dependency 64 | if channel.__class__.__name__ != 'Channel': 65 | raise ValueError('channel must be a valid rabbitpy Channel object') 66 | elif not utils.is_string(name): 67 | raise ValueError('name must be str, bytes or unicode') 68 | self.name = name 69 | 70 | 71 | class StatefulObject(object): 72 | """Base object for rabbitpy classes that need to maintain state such as 73 | connection and channel. 74 | 75 | """ 76 | CLOSED = 0x00 77 | CLOSING = 0x01 78 | OPEN = 0x02 79 | OPENING = 0x03 80 | 81 | STATES = {0x00: 'Closed', 82 | 0x01: 'Closing', 83 | 0x02: 'Open', 84 | 0x03: 'Opening'} 85 | 86 | def __init__(self): 87 | """Create a new instance of the object defaulting to a closed state.""" 88 | self._state = self.CLOSED 89 | 90 | def _set_state(self, value): 91 | """Set the state to the specified value, validating it is a supported 92 | state value. 93 | 94 | :param int value: The new state value 95 | :raises: ValueError 96 | """ 97 | if value not in list(self.STATES.keys()): 98 | raise ValueError('Invalid state value: %r' % value) 99 | LOGGER.debug('%s setting state to %r', 100 | self.__class__.__name__, self.STATES[value]) 101 | self._state = value 102 | 103 | @property 104 | def closed(self): 105 | """Returns True if in the CLOSED runtime state 106 | 107 | :rtype: bool 108 | 109 | """ 110 | return self._state == self.CLOSED 111 | 112 | @property 113 | def closing(self): 114 | """Returns True if in the CLOSING runtime state 115 | 116 | :rtype: bool 117 | 118 | """ 119 | return self._state == self.CLOSING 120 | 121 | @property 122 | def open(self): 123 | """Returns True if in the OPEN runtime state 124 | 125 | :rtype: bool 126 | 127 | """ 128 | return self._state == self.OPEN 129 | 130 | @property 131 | def opening(self): 132 | """Returns True if in the OPENING runtime state 133 | 134 | :rtype: bool 135 | 136 | """ 137 | return self._state == self.OPENING 138 | 139 | @property 140 | def state(self): 141 | """Return the runtime state value 142 | 143 | :rtype: int 144 | 145 | """ 146 | return self._state 147 | 148 | @property 149 | def state_description(self): 150 | """Returns the text based description of the runtime state 151 | 152 | :rtype: str 153 | 154 | """ 155 | return self.STATES[self._state] 156 | 157 | 158 | class AMQPChannel(StatefulObject): 159 | """Base AMQP Channel Object""" 160 | 161 | CLOSE_REQUEST_FRAME = specification.Channel.Close 162 | DEFAULT_CLOSE_CODE = 200 163 | DEFAULT_CLOSE_REASON = 'Normal Shutdown' 164 | REMOTE_CLOSED = 0x04 165 | 166 | def __init__(self, exception_queue, write_trigger, connection, 167 | blocking_read=False): 168 | super(AMQPChannel, self).__init__() 169 | if blocking_read: 170 | LOGGER.debug('Initialized with blocking read') 171 | self.blocking_read = blocking_read 172 | self._debugging = None 173 | self._interrupt = {'event': threading.Event(), 174 | 'callback': None, 175 | 'args': None} 176 | self._channel_id = None 177 | self._connection = connection 178 | self._exceptions = exception_queue 179 | self._state = self.CLOSED 180 | self._read_queue = None 181 | self._waiting = False 182 | self._write_lock = threading.Lock() 183 | self._write_queue = None 184 | self._write_trigger = write_trigger 185 | 186 | def __int__(self): 187 | return self._channel_id 188 | 189 | def close(self): 190 | """Close the AMQP channel""" 191 | if self._connection.closed: 192 | LOGGER.debug('Connection is closed, bailing') 193 | return 194 | 195 | if self.closed: 196 | LOGGER.debug('AMQPChannel %i close invoked and already closed', 197 | self._channel_id) 198 | return 199 | 200 | LOGGER.debug('Channel %i close invoked while %s', 201 | self._channel_id, self.state_description) 202 | 203 | # Make sure there are no RPC frames pending 204 | self._check_for_pending_frames() 205 | 206 | if not self.closing: 207 | self._set_state(self.CLOSING) 208 | 209 | frame_value = self._build_close_frame() 210 | if self._is_debugging: 211 | LOGGER.debug('Channel %i Waiting for a valid response for %s', 212 | self._channel_id, frame_value.name) 213 | self.rpc(frame_value) 214 | self._set_state(self.CLOSED) 215 | if self._is_debugging: 216 | LOGGER.debug('Channel #%i closed', self._channel_id) 217 | 218 | def rpc(self, frame_value): 219 | """Send a RPC command to the remote server. This should not be directly 220 | invoked. 221 | 222 | :param pamqp.specification.Frame frame_value: The frame to send 223 | :rtype: pamqp.specification.Frame or None 224 | 225 | """ 226 | if self.closed: 227 | raise exceptions.ChannelClosedException() 228 | if self._is_debugging: 229 | LOGGER.debug('Sending %r', frame_value.name) 230 | self.write_frame(frame_value) 231 | if frame_value.synchronous: 232 | return self._wait_on_frame(frame_value.valid_responses) 233 | 234 | def wait_for_confirmation(self): 235 | """Used by the Message.publish method when publisher confirmations are 236 | enabled. 237 | 238 | :rtype: pamqp.frame.Frame 239 | 240 | """ 241 | return self._wait_on_frame([specification.Basic.Ack, 242 | specification.Basic.Nack]) 243 | 244 | def write_frame(self, frame): 245 | """Put the frame in the write queue for the IOWriter object to write to 246 | the socket when it can. This should not be directly invoked. 247 | 248 | :param pamqp.specification.Frame frame: The frame to write 249 | 250 | """ 251 | if self._can_write(): 252 | if self._is_debugging: 253 | LOGGER.debug('Writing frame: %s', frame.name) 254 | with self._write_lock: 255 | self._write_queue.put((self._channel_id, frame)) 256 | self._trigger_write() 257 | 258 | def write_frames(self, frames): 259 | """Add a list of frames for the IOWriter object to write to the socket 260 | when it can. 261 | 262 | :param list frames: The list of frame to write 263 | 264 | """ 265 | if self._can_write(): 266 | if self._is_debugging: 267 | LOGGER.debug('Writing frames: %r', 268 | [frame.name for frame in frames]) 269 | with self._write_lock: 270 | # pylint: disable=expression-not-assigned 271 | [self._write_queue.put((self._channel_id, frame)) 272 | for frame in frames] 273 | self._trigger_write() 274 | 275 | def _build_close_frame(self): 276 | """Return the proper close frame for this object. 277 | 278 | :rtype: pamqp.specification.Channel.Close 279 | 280 | """ 281 | return self.CLOSE_REQUEST_FRAME(self.DEFAULT_CLOSE_CODE, 282 | self.DEFAULT_CLOSE_REASON) 283 | 284 | def _can_write(self): 285 | self._check_for_exceptions() 286 | if self._connection.closed: 287 | raise exceptions.ConnectionClosed() 288 | elif self.closed: 289 | raise exceptions.ChannelClosedException() 290 | return True 291 | 292 | @property 293 | def _is_debugging(self): 294 | """Indicates that something has set the logger to ``logging.DEBUG`` 295 | to perform a minor micro-optimization preventing ``LOGGER.debug`` calls 296 | when they are not required. 297 | 298 | :return: bool 299 | 300 | """ 301 | if self._debugging is None: 302 | self._debugging = LOGGER.getEffectiveLevel() == logging.DEBUG 303 | return self._debugging 304 | 305 | def _check_for_exceptions(self): 306 | """Check if there are any queued exceptions to raise, raising it if 307 | there is. 308 | 309 | """ 310 | if not self._exceptions.empty(): 311 | exception = self._exceptions.get() 312 | self._exceptions.task_done() 313 | raise exception 314 | 315 | def _check_for_pending_frames(self): 316 | value = self._read_from_queue() 317 | if value: 318 | self._check_for_rpc_request(value) 319 | LOGGER.debug('Read frame while shutting down: %r', value) 320 | 321 | def _check_for_rpc_request(self, value): 322 | """Implement in child objects to inspect frames for channel specific 323 | RPC requests from RabbitMQ. 324 | 325 | """ 326 | if isinstance(value, specification.Channel.Close): 327 | LOGGER.debug('Channel closed') 328 | self._on_remote_close(value) 329 | 330 | def _force_close(self): 331 | """Force the channel to mark itself as closed""" 332 | self._set_state(self.CLOSED) 333 | LOGGER.debug('Channel #%i closed', self._channel_id) 334 | 335 | def _interrupt_wait_on_frame(self, callback, *args): 336 | """Invoke to interrupt the current self._wait_on_frame blocking loop 337 | in order to allow for a flow such as waiting on a full message while 338 | consuming. Will wait until the ``_wait_on_frame_interrupt`` is cleared 339 | to make this a blocking operation. 340 | 341 | :param callback: The method to call 342 | :type callback: typing.Callable 343 | :param list args: Args to pass to the callback 344 | 345 | """ 346 | self._check_for_exceptions() 347 | if not self._waiting: 348 | if self._is_debugging: 349 | LOGGER.debug('No need to interrupt wait') 350 | return callback(*args) 351 | LOGGER.debug('Interrupting the wait on frame') 352 | self._interrupt['callback'] = callback 353 | self._interrupt['args'] = args 354 | self._interrupt['event'].set() 355 | 356 | @property 357 | def _interrupt_is_set(self): 358 | return self._interrupt['event'].is_set() 359 | 360 | def _on_interrupt_set(self): 361 | # pylint: disable=not-an-iterable,not-callable 362 | self._interrupt['callback'](*self._interrupt['args']) 363 | self._interrupt['event'].clear() 364 | self._interrupt['callback'] = None 365 | self._interrupt['args'] = None 366 | 367 | def _on_remote_close(self, value): 368 | """Handle RabbitMQ remotely closing the channel 369 | 370 | :param value: The Channel.Close method frame 371 | :type value: pamqp.spec.Channel.Close 372 | :raises: exceptions.RemoteClosedChannelException 373 | :raises: exceptions.AMQPException 374 | 375 | """ 376 | self._set_state(self.REMOTE_CLOSED) 377 | if value.reply_code in exceptions.AMQP: 378 | LOGGER.error('Received remote close (%s): %s', 379 | value.reply_code, value.reply_text) 380 | raise exceptions.AMQP[value.reply_code](value) 381 | else: 382 | raise exceptions.RemoteClosedChannelException(self._channel_id, 383 | value.reply_code, 384 | value.reply_text) 385 | 386 | def _read_from_queue(self): 387 | """Check to see if a frame is in the queue and if so, return it 388 | 389 | :rtype: amqp.specification.Frame or None 390 | 391 | """ 392 | if self._can_write() and not self.closing and self.blocking_read: 393 | if self._is_debugging: 394 | LOGGER.debug('Performing a blocking read') 395 | value = self._read_queue.get() 396 | self._read_queue.task_done() 397 | else: 398 | try: 399 | value = self._read_queue.get(True, .1) 400 | self._read_queue.task_done() 401 | except queue.Empty: 402 | value = None 403 | return value 404 | 405 | def _trigger_write(self): 406 | """Notifies the IO loop we need to write a frame by writing a byte 407 | to a local socket. 408 | 409 | """ 410 | utils.trigger_write(self._write_trigger) 411 | 412 | def _validate_frame_type(self, frame_value, frame_type): 413 | """Validate the frame value against the frame type. The frame type can 414 | be an individual frame type or a list of frame types. 415 | 416 | :param frame_value: The frame to check 417 | :type frame_value: pamqp.specification.Frame 418 | :param frame_type: The frame(s) to check against 419 | :type frame_type: pamqp.specification.Frame or list 420 | :rtype: bool 421 | 422 | """ 423 | if frame_value is None: 424 | if self._is_debugging: 425 | LOGGER.debug('Frame value is none?') 426 | return False 427 | if isinstance(frame_type, str): 428 | if frame_value.name == frame_type: 429 | return True 430 | elif isinstance(frame_type, list): 431 | for frame_t in frame_type: 432 | result = self._validate_frame_type(frame_value, frame_t) 433 | if result: 434 | return True 435 | return False 436 | elif isinstance(frame_value, specification.Frame): 437 | return frame_value.name == frame_type.name 438 | return False 439 | 440 | def _wait_on_frame(self, frame_type=None): 441 | """Read from the queue, blocking until a result is returned. An 442 | individual frame type or a list of frame types can be passed in to wait 443 | for specific frame types. If there is no match on the frame retrieved 444 | from the queue, put the frame back in the queue and recursively 445 | call the method. 446 | 447 | :param frame_type: The name or list of names of the frame type(s) 448 | :type frame_type: str|list|pamqp.specification.Frame 449 | :rtype: Frame 450 | 451 | """ 452 | self._check_for_exceptions() 453 | if isinstance(frame_type, list) and len(frame_type) == 1: 454 | frame_type = frame_type[0] 455 | if self._is_debugging: 456 | LOGGER.debug('Waiting on %r frame(s)', frame_type) 457 | start_state = self.state 458 | self._waiting = True 459 | while (start_state == self.state and 460 | not self.closed and 461 | not self._connection.closed): 462 | value = self._read_from_queue() 463 | if value is not None: 464 | self._check_for_rpc_request(value) 465 | if frame_type and self._validate_frame_type(value, frame_type): 466 | self._waiting = False 467 | return value 468 | self._read_queue.put(value) 469 | 470 | # Allow for any exceptions to be raised 471 | self._check_for_exceptions() 472 | 473 | # If the wait interrupt is set, break out of the loop 474 | if self._interrupt_is_set: 475 | break 476 | 477 | self._waiting = False 478 | 479 | # Clear here to ensure out of processing loop before proceeding 480 | if self._interrupt_is_set: 481 | self._on_interrupt_set() 482 | --------------------------------------------------------------------------------