├── .gitignore ├── .travis.yml ├── README.md ├── broker_rabbit ├── __init__.py ├── broker.py ├── channels.py ├── connection_handler.py ├── exceptions.py ├── exchange_handler.py ├── producer.py ├── queue_handler.py └── worker.py ├── examples ├── __init__.py ├── flask │ ├── __init__.py │ ├── app │ │ ├── __init__.py │ │ ├── events │ │ │ ├── __init__.py │ │ │ ├── events.py │ │ │ └── events_handler_template.py │ │ └── view │ │ │ ├── __init__.py │ │ │ └── api.py │ ├── broker │ │ ├── __init__.py │ │ ├── event_handler.py │ │ └── processor.py │ ├── default-config.cfg │ ├── main.py │ └── manager.py └── getting_started │ ├── __init__.py │ ├── app.py │ ├── consumer.py │ └── producer.py ├── requirements.txt ├── setup.cfg └── tests ├── __init__.py ├── base_test.py ├── broker_rabbit ├── __init__.py ├── rabbit_api.py ├── test_broker.py ├── test_channels.py ├── test_connection_handler.py ├── test_exchange_handler.py ├── test_producer.py ├── test_queue_handler.py └── test_worker.py └── common.py /.gitignore: -------------------------------------------------------------------------------- 1 | # User specific environment variables 2 | init.env 3 | venv/ 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | .ropeproject/ 29 | .python-version 30 | .pytest* 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | cov_html/ 45 | .coverage 46 | .tox/ 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | 58 | # Sphinx documentation 59 | docs/_build/ 60 | 61 | # PyBuilder 62 | target/ 63 | 64 | # Editors 65 | *.swp 66 | *.swo 67 | # Simple Idea exclusion. Some use Intellij, some use Pycharm, in != versions 68 | .idea/ 69 | *.iml 70 | 71 | # EMACS 72 | *~ 73 | \#*\# 74 | .\#* 75 | 76 | # Floobits 77 | .floo 78 | .flooignore 79 | 80 | # Vagrant 81 | env_setup/.vagrant/ 82 | 83 | # Visual Studio 84 | .vscode 85 | 86 | # Fonts 87 | *.pkl 88 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | sudo: true 3 | language: python 4 | cache: pip 5 | 6 | addons: 7 | apt: 8 | packages: rabbitmq-server 9 | 10 | services: rabbitmq 11 | python: 12 | - "3.6" 13 | 14 | install: pip install -r requirements.txt 15 | 16 | before_script: 17 | - sudo rabbitmq-plugins enable rabbitmq_management 18 | - sudo rabbitmqctl add_vhost $VHOST 19 | - sudo rabbitmqctl set_permissions -p $VHOST $USERNAME ".*" ".*" ".*" 20 | 21 | script: 22 | - flake8 broker_rabbit 23 | - pytest tests -x --cov-report=term --cov=broker_rabbit --cov-fail-under=90 24 | 25 | after_success: 26 | - codecov 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask with Rabbit MQ message broker in Python 2 | 3 | [![Build Status](https://travis-ci.org/lemzoo/flask-rabbitmq.svg?branch=master)](https://travis-ci.org/lemzoo/flask-rabbitmq) 4 | 5 | [![codecov.io](https://codecov.io/gh/lemzoo/flask-rabbitmq/branch/master/graphs/badge.svg?branch=master)](https://codecov.io/gh/lemzoo/flask-rabbitmq) 6 | 7 | Overview 8 | -------- 9 | 10 | Flask with Rabbit MQ message broker is an example project which demonstrates the use 11 | of flask API which publishing a message to the Rabbit MQ server. 12 | 13 | It contains four routes. And in it's route, an event will send to the message broker. 14 | The broker message will route the message to Rabbit MQ by publishing the message on the correct queue. 15 | 16 | * Flask application 17 | * A producer which push the messages to the right queue 18 | * A worker which consume the pushed messages from RabbitMQ server. 19 | 20 | Requirements 21 | ------------ 22 | 23 | * Python 3 and requirements file dependencies 24 | * RabbitMQ broker 25 | 26 | Installing 27 | ---------- 28 | 29 | Install and update using pip: 30 | 31 | Create a virtualenv 32 | 33 | $ python -m venv venv 34 | 35 | 36 | Source the virtualenv 37 | 38 | $ source venv/bin/activate 39 | 40 | 41 | Install all the dependencies on the requirements file 42 | 43 | $ pip install -Ur requirements.txt 44 | 45 | 46 | A simple example 47 | ---------------- 48 | 49 | create three files which represent the application 50 | 51 | 52 | app.py 53 | 54 | from time import sleep 55 | 56 | from flask import Flask 57 | 58 | from broker_rabbit import BrokerRabbitMQ 59 | 60 | 61 | def process_message(message): 62 | print('Message received and content is ’{content}’'.format(content=message)) 63 | sleep(2) 64 | 65 | 66 | app = Flask(__name__) 67 | app.config['RABBIT_MQ_URL'] = 'amqp://guest:guest@localhost:5672/test-flask-rabbitmq' 68 | app.config['EXCHANGE_NAME'] = 'test-exchange' 69 | 70 | 71 | broker = BrokerRabbitMQ() 72 | broker.init_app(app=app, queues=['users'], on_message_callback=process_message) 73 | 74 | 75 | producer.py 76 | 77 | from app import broker 78 | 79 | broker.send(queue='users', context={'key': 'value', 'number': 1}) 80 | 81 | 82 | consumer.py 83 | 84 | from app import broker 85 | 86 | broker.start(queue='users') 87 | 88 | 89 | Contributing 90 | ------------ 91 | 92 | This section will be soon available 93 | -------------------------------------------------------------------------------- /broker_rabbit/__init__.py: -------------------------------------------------------------------------------- 1 | from broker_rabbit.broker import BrokerRabbitMQ 2 | from broker_rabbit.connection_handler import ConnectionHandler 3 | from broker_rabbit.exceptions import UnknownQueueError 4 | 5 | from broker_rabbit.channels import ProducerChannel 6 | from broker_rabbit.producer import Producer 7 | 8 | 9 | __all__ = ('BrokerRabbitMQ', 'ConnectionHandler', 'Producer', 10 | 'ProducerChannel', 'UnknownQueueError') 11 | -------------------------------------------------------------------------------- /broker_rabbit/broker.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | 4 | from broker_rabbit.channels import ProducerChannel 5 | from broker_rabbit.exceptions import UnknownQueueError 6 | 7 | from broker_rabbit.connection_handler import ConnectionHandler 8 | from broker_rabbit.producer import Producer 9 | from broker_rabbit.worker import Worker 10 | 11 | DEFAULT_URL = 'amqp://test:test@localhost:5672/foo-test' 12 | DEFAULT_EXCHANGE = 'FOO-EXCHANGE' 13 | DEFAULT_APP = 'FOO-APPLICATION-ID' 14 | DEFAULT_DELIVERY = 2 15 | STATUS_READY = 'READY' 16 | 17 | 18 | class BrokerRabbitMQ: 19 | """Message Broker based on RabbitMQ middleware""" 20 | 21 | def __init__(self, app=None): 22 | """ 23 | Create a new instance of Broker Rabbit by using 24 | the given parameters to connect to RabbitMQ. 25 | """ 26 | self.app = app 27 | self.connection_handler = None 28 | self.producer = None 29 | self.url = None 30 | self.exchange_name = None 31 | self.application_id = None 32 | self.delivery_mode = None 33 | self.queues = None 34 | self.on_message_callback = None 35 | 36 | def init_app(self, app, queues, on_message_callback): 37 | """ Init the Broker by using the given configuration instead 38 | default settings. 39 | 40 | :param app: Current application context 41 | :param list queues: Queues which the message will be post 42 | :param callback on_message_callback: callback to execute when new 43 | message is pulled to RabbitMQ 44 | """ 45 | self.url = app.config.get('RABBIT_MQ_URL', DEFAULT_URL) 46 | self.exchange_name = app.config.get('EXCHANGE_NAME', DEFAULT_EXCHANGE) 47 | self.application_id = app.config.get('APPLICATION_ID', DEFAULT_APP) 48 | self.delivery_mode = app.config.get('DELIVERY_MODE', DEFAULT_DELIVERY) 49 | self.queues = queues 50 | self.on_message_callback = on_message_callback 51 | 52 | # Open Connection to RabbitMQ 53 | self.connection_handler = ConnectionHandler(self.url) 54 | connection = self.connection_handler.get_current_connection() 55 | 56 | # Setup default producer for broker_rabbit 57 | channel = ProducerChannel(connection, self.application_id, 58 | self.delivery_mode) 59 | self.producer = Producer(channel, self.exchange_name) 60 | self.producer.bootstrap(self.queues) 61 | 62 | def send(self, queue, context={}): 63 | """Post the message to the correct queue with the given context 64 | 65 | :param str queue: queue which to post the message 66 | :param dict context: content of the message to post to RabbitMQ server 67 | """ 68 | if queue not in self.queues: 69 | error_msg = f'Queue ‘{queue}‘ is not registered' 70 | raise UnknownQueueError(error_msg) 71 | 72 | message = { 73 | 'created_at': datetime.utcnow().isoformat(), 74 | 'queue': queue, 75 | 'context': context 76 | } 77 | 78 | return self.producer.publish(queue, message) 79 | 80 | def list_queues(self): 81 | """List all available queue in the app""" 82 | for queue in self.queues: 83 | print('Queue name : `%s`' % queue) 84 | 85 | def start(self, queue): 86 | """Start worker on a given queue 87 | :param queue: the queue which you consume message for 88 | """ 89 | if queue not in self.queues: 90 | raise RuntimeError(f'Queue with name`{queue}` not found') 91 | 92 | worker = Worker(connection_handler=self.connection_handler, 93 | message_callback=self.on_message_callback, queue=queue) 94 | print(f'Start consuming message on the queue `{queue}`') 95 | worker.consume_message() 96 | -------------------------------------------------------------------------------- /broker_rabbit/channels.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | 4 | import pika 5 | from pika.utils import is_callable 6 | 7 | from broker_rabbit.exceptions import ( 8 | ConnectionNotOpenedError, ChannelUndefinedError, 9 | WorkerExitError, ConnectionIsClosedError, CallBackError) 10 | 11 | LOG_FORMAT = ('%(levelname) -10s %(asctime)s %(name) -30s %(funcName) ' 12 | '-35s %(lineno) -5d: %(message)s') 13 | LOGGER = logging.getLogger(__name__) 14 | 15 | 16 | class ChannelHandler: 17 | """This is a Channel Handler which use the connection handler to get a new 18 | channel to allow the client to communicate with RabbitMQ. 19 | 20 | """ 21 | 22 | def __init__(self, connection): 23 | """Create a new instance of the channel class by using the current 24 | connection. 25 | 26 | :param ConnectionHandler connection: The given connection to allow 27 | communication with RabbitMQ. 28 | 29 | """ 30 | self._connection = connection 31 | self._channel = None 32 | 33 | def open(self): 34 | LOGGER.info('Opening channel for the producer') 35 | if self._connection is None: 36 | LOGGER.error('The connection is not opened') 37 | raise ConnectionNotOpenedError('The connection is not opened') 38 | 39 | if self._connection.is_closed: 40 | LOGGER.error('The connection is closed') 41 | raise ConnectionIsClosedError('The connection is closed') 42 | 43 | self._channel = self._connection.channel() 44 | 45 | def close(self): 46 | LOGGER.info('The channel will close in a few time') 47 | self._channel.close() 48 | 49 | def get_channel(self): 50 | LOGGER.info('Getting the channel object') 51 | if self._channel is None: 52 | LOGGER.error('The channel doesn''t exist yet') 53 | raise ChannelUndefinedError('The channel does not exist yet') 54 | 55 | return self._channel 56 | 57 | 58 | class WorkerChannel(ChannelHandler): 59 | 60 | def __init__(self, connection, queue, on_message_callback): 61 | super().__init__(connection) 62 | self._queue = queue 63 | self._on_message_callback = on_message_callback 64 | 65 | def run(self): 66 | LOGGER.info('Consuming message on queue : %s', self._queue) 67 | 68 | try: 69 | """The queue can be non exist on broker_rabbit, so ChannelClosed exception 70 | is handled by RabbitMQ and then the TCP connection is closed. 71 | Re-implement this if others worker can be launch and handle the 72 | Exception. 73 | """ 74 | self.open() 75 | self.add_on_cancel_callback() 76 | self._channel.basic_consume(self.on_message, self._queue) 77 | LOGGER.info(' [*] Waiting for messages. To exit press CTRL+C') 78 | self._channel.start_consuming() 79 | except KeyboardInterrupt: 80 | LOGGER.info('The Worker will be exit after CTRL+C signal') 81 | raise WorkerExitError('Worker stopped pulling message') 82 | finally: 83 | self.close() 84 | 85 | def consume_one_message(self): 86 | method, header, body = self._channel.basic_get(self._queue) 87 | if not method or method.NAME == 'Basic.GetEmpty': 88 | return False 89 | else: 90 | self.on_message(self._channel, method, header, body) 91 | return True 92 | 93 | def bind_callback(self, message): 94 | if not is_callable(self._on_message_callback): 95 | LOGGER.info('Wrong callback is binded') 96 | raise CallBackError('The callback is not callable') 97 | 98 | try: 99 | self._on_message_callback(message) 100 | LOGGER.info('Received message # %s #', message) 101 | except TypeError as error: 102 | LOGGER.info('Error on the callback definition. Message is not ' 103 | 'acknowledge. And it will be keep on the RabbitMQ') 104 | error_message = 'You should implement your callback ' \ 105 | 'like my_callback(content)' 106 | raise CallBackError(error_message, original_exception=str(error)) 107 | 108 | def on_message(self, channel, method, properties, body): 109 | try: 110 | # TODO: Handle here a dead letter queue after deciding 111 | # TODO: the next step of the received message 112 | decoded_message = body.decode() 113 | raw_message = json.loads(decoded_message) 114 | self.bind_callback(raw_message) 115 | except Exception as exception_info: 116 | # TODO: handle dead letter 117 | LOGGER.error( 118 | 'Exception {exception} occurred when trying to decode the data' 119 | 'received RabbitMQ. So the message content will be put on ' 120 | 'queue dead letter. Here are the content of the message : ' 121 | '{content}'.format(exception=exception_info, content=body)) 122 | 123 | self.acknowledge_message(method.delivery_tag) 124 | 125 | def acknowledge_message(self, delivery_tag): 126 | LOGGER.info('Acknowledging message %s', delivery_tag) 127 | self._channel.basic_ack(delivery_tag) 128 | 129 | def add_on_cancel_callback(self): 130 | LOGGER.info('Adding consumer cancellation callback') 131 | self._channel.add_on_cancel_callback(self.on_consumer_cancelled) 132 | 133 | def on_consumer_cancelled(self, method_frame): 134 | LOGGER.info('Consumer was cancelled remotely, shutting down: %r', 135 | method_frame) 136 | if self._channel: 137 | self.close() 138 | 139 | 140 | class ProducerChannel(ChannelHandler): 141 | 142 | def __init__(self, connection, application_id, delivery_mode): 143 | super().__init__(connection) 144 | self.basic_properties = self.apply_basic_properties(application_id, 145 | delivery_mode) 146 | 147 | def send_message(self, exchange, queue, message): 148 | serialized_message = json.dumps(message) 149 | 150 | self._channel.basic_publish( 151 | exchange=exchange, routing_key=queue, 152 | body=serialized_message, properties=self.basic_properties) 153 | LOGGER.info('message was published successfully into RabbitMQ') 154 | 155 | @staticmethod 156 | def apply_basic_properties(app_id, delivery_mode, 157 | content_type='application/json'): 158 | """Apply the basic properties for RabbitMQ. 159 | 160 | :param str app_id : The id of the current app. 161 | :param int delivery_mode : The delivering mode for RabbitMQ. 162 | `2` means the message will be persisted on the disk 163 | `1` means the message will not be persisted. 164 | :param str content_type : The content type of the message 165 | """ 166 | LOGGER.info('Applying the properties for RabbitMQ') 167 | properties = pika.BasicProperties(app_id=app_id, 168 | content_type=content_type, 169 | delivery_mode=delivery_mode) 170 | return properties 171 | -------------------------------------------------------------------------------- /broker_rabbit/connection_handler.py: -------------------------------------------------------------------------------- 1 | import pika 2 | 3 | 4 | class ConnectionHandler: 5 | """This is a Connection Handler to manage the connection between the 6 | client and RabbitMQ. 7 | 8 | """ 9 | 10 | def __init__(self, rabbit_url): 11 | """Create a new instance of Connection Handler by using the given 12 | parameters to connect to RabbitMQ which is on environment variable 13 | """ 14 | self._connection = None 15 | self.parameters = None 16 | self.init_connection(rabbit_url) 17 | 18 | def init_connection(self, url, timeout=5): 19 | # TODO: add ssl certification 20 | """Setup the publisher object, passing in the host, port, user id and 21 | the password to create a parameters objects to connect to RabbitMQ. 22 | 23 | :param str url: url for RabbitMQ server 24 | :param int timeout: Timeout for handling the connection. 25 | By default, it's 0.25 seconds. 26 | It's not recommended to keep it to 0.25. So, we change it to 5. 27 | """ 28 | 29 | self.parameters = pika.URLParameters(url) 30 | self.parameters.heartbeat = 0 31 | self.parameters.socket_timeout = timeout 32 | self._connection = pika.BlockingConnection(self.parameters) 33 | 34 | def close_connection(self): 35 | self._connection.close() 36 | 37 | def get_current_connection(self): 38 | return self._connection 39 | -------------------------------------------------------------------------------- /broker_rabbit/exceptions.py: -------------------------------------------------------------------------------- 1 | class BrokerRabbitError(Exception): 2 | def __init__(self, *args, **kwargs): 3 | super().__init__(args, kwargs) 4 | 5 | 6 | class UnknownQueueError(BrokerRabbitError): 7 | pass 8 | 9 | 10 | class ExchangeUndefinedError(BrokerRabbitError): 11 | pass 12 | 13 | 14 | class ChannelUndefinedError(BrokerRabbitError): 15 | pass 16 | 17 | 18 | class ChannelIsAlreadyInUseError(BrokerRabbitError): 19 | pass 20 | 21 | 22 | class ConnectionNotOpenedError(BrokerRabbitError): 23 | pass 24 | 25 | 26 | class ConnectionIsAlreadyInUseError(BrokerRabbitError): 27 | pass 28 | 29 | 30 | class ConnectionIsClosedError(BrokerRabbitError): 31 | pass 32 | 33 | 34 | class QueueDoesNotExistError(BrokerRabbitError): 35 | pass 36 | 37 | 38 | class WorkerExitError(BrokerRabbitError): 39 | pass 40 | 41 | 42 | class BadFormatMessageError(BrokerRabbitError): 43 | pass 44 | 45 | 46 | class CallBackError(BrokerRabbitError): 47 | pass 48 | -------------------------------------------------------------------------------- /broker_rabbit/exchange_handler.py: -------------------------------------------------------------------------------- 1 | from broker_rabbit.exceptions import (ChannelUndefinedError, 2 | ExchangeUndefinedError) 3 | 4 | 5 | class ExchangeHandler: 6 | """This is an Exchange Handler which used by the channel handler to 7 | set a new or default exchange in RabbitMQ. 8 | 9 | """ 10 | 11 | def __init__(self, channel, name): 12 | """Create a new instance of exchange handler class by using the channel 13 | 14 | :param ChannelHandler channel: The given channel to connect to RabbitMQ 15 | :param str name : The name of the exchange to set 16 | """ 17 | self._channel = channel 18 | self._name = name 19 | 20 | def setup_exchange(self, exchange_type='direct', 21 | durable=True, auto_delete=False): 22 | """ 23 | :param str exchange_type : The type of exchange. 24 | By default, the exchange is set to direct type to allow simple 25 | routing via the queue name. 26 | Here are the type of exchange : direct - fanout - topic. 27 | :param boolean durable : The durability of the exchange. 28 | Durable exchange remain active when a server restarts. 29 | Non-durable exchanges (transient exchanges) are purged when the 30 | server restarts. This is not recommended. 31 | :param boolean auto_delete : Delete the exchange when all queues have 32 | finished using it. By default, it's False. 33 | """ 34 | if self._channel is None: 35 | raise ChannelUndefinedError('The channel was not defined') 36 | 37 | # Check Me : self._channel.basic_qos(prefetch_count=1) 38 | self._channel.exchange_declare( 39 | exchange=self._name, type=exchange_type, 40 | durable=durable, auto_delete=auto_delete) 41 | 42 | @property 43 | def name(self): 44 | if self._name is None: 45 | raise ExchangeUndefinedError('The exchange is not defined') 46 | 47 | return self._name 48 | -------------------------------------------------------------------------------- /broker_rabbit/producer.py: -------------------------------------------------------------------------------- 1 | from broker_rabbit.exceptions import QueueDoesNotExistError 2 | from broker_rabbit.exchange_handler import ExchangeHandler 3 | from broker_rabbit.queue_handler import QueueHandler 4 | 5 | 6 | class Producer: 7 | """Producer component that will publish message and handle 8 | connection and channel interactions with RabbitMQ. 9 | 10 | """ 11 | 12 | def __init__(self, channel, exchange_name, queues=None): 13 | self._channel = channel 14 | self._exchange_name = exchange_name 15 | 16 | if queues: 17 | self.bootstrap(queues) 18 | 19 | def bootstrap(self, queues): 20 | """Initialize the queue on RabbitMQ 21 | 22 | :param list queues: List of queue to setup on broker_rabbit 23 | """ 24 | self._channel.open() 25 | try: 26 | channel = self._channel.get_channel() 27 | 28 | exchange_handler = ExchangeHandler(channel, self._exchange_name) 29 | exchange_handler.setup_exchange() 30 | 31 | queue_handler = QueueHandler(channel, self._exchange_name) 32 | for queue in queues: 33 | queue_handler.setup_queue(queue) 34 | finally: 35 | self._queues = queues 36 | self._channel.close() 37 | 38 | def publish(self, queue, message): 39 | """Publish the given message in the given queue 40 | 41 | :param str queue : The queue name which to publish the given message 42 | :param dict message : The message to publish in RabbitMQ 43 | """ 44 | 45 | if queue not in self._queues: 46 | error_msg = f'Queue with name `{queue}` is not declared.' \ 47 | f'Please call bootstrap before using publish' 48 | raise QueueDoesNotExistError(error_msg) 49 | 50 | self._channel.open() 51 | try: 52 | self._channel.send_message(self._exchange_name, queue, message) 53 | finally: 54 | self._channel.close() 55 | -------------------------------------------------------------------------------- /broker_rabbit/queue_handler.py: -------------------------------------------------------------------------------- 1 | from broker_rabbit.exceptions import (ChannelUndefinedError, 2 | ExchangeUndefinedError) 3 | 4 | 5 | class QueueHandler: 6 | """This is an Exchange Handler which use the channel handler to set a new 7 | or default exchange in RabbitMQ. 8 | 9 | """ 10 | 11 | def __init__(self, channel, exchange_name): 12 | """Create a new instance of exchange handler class by using the channel 13 | 14 | :param ChannelHandler channel: The given channel to connect to RabbitMQ 15 | :param str exchange_name : The name of the exchange to set 16 | """ 17 | self._channel = channel 18 | self._exchange = exchange_name 19 | 20 | def setup_queue(self, name): 21 | """Setting the queue to allow pushong in a specied exchange 22 | 23 | :param str name : The name of the queue to set in RabbitMQ. 24 | """ 25 | # Check if the channel is set or not 26 | self._check_basic_config() 27 | 28 | # Create the queue 29 | self.create_queue(name) 30 | 31 | # Bind the queue to the exchange 32 | self._channel.queue_bind(queue=name, exchange=self._exchange) 33 | 34 | def _check_basic_config(self): 35 | if self._channel is None: 36 | raise ChannelUndefinedError('The Channel is not defined yet') 37 | 38 | if self._exchange is None: 39 | raise ExchangeUndefinedError('The exchange is not defined') 40 | 41 | def create_queue(self, queue_name, durable=True, auto_delete=False): 42 | """Create a new queue with the arguments such as its name. 43 | 44 | :param str queue_name : The name of the queue to set in RabbitMQ. 45 | :param boolean durable : The durability of the queue in RabbitMQ. 46 | :param boolean auto_delete : Auto delete the queue when the message 47 | are purged (No consumer/publisher working on this particular queue) 48 | """ 49 | 50 | # TODO : Check declared_queue to return the real name of the queue 51 | self._channel.queue_declare(queue=queue_name, durable=durable, 52 | auto_delete=auto_delete, 53 | arguments={'x-ha-policy': 'all'}) 54 | -------------------------------------------------------------------------------- /broker_rabbit/worker.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from broker_rabbit.channels import WorkerChannel 4 | 5 | 6 | class Worker: 7 | """This is a Worker that will handle a connection and queue to work 8 | on the available messages in RabbitMQ server. 9 | The worker will setup the channel to use and when finished, it will also 10 | close the current channel. 11 | """ 12 | 13 | def __init__(self, connection_handler, queue, message_callback): 14 | """ 15 | Instantiate a Worker with an opened connection and a queue name to work 16 | The channel is opened in the instantiation of the module for ready use. 17 | It will be closed after consuming the message on the given queue. 18 | 19 | :param ConnectionHandler connection_handler : The connection to use 20 | between the worker and RabbitMQ. 21 | :param str queue : The name of the queue which to consume message 22 | :param callback message_callback: the callback to call when message 23 | is received from RabbitMQ 24 | """ 25 | self._connection = connection_handler.get_current_connection() 26 | self._queue = queue 27 | self._worker_channel = WorkerChannel(self._connection, self._queue, 28 | message_callback) 29 | 30 | self.logger = logging.getLogger('RabbitMQ-Worker') 31 | 32 | def consume_message(self): 33 | self.logger.info('Consuming message on queue %s' % self._queue) 34 | self._worker_channel.run() 35 | 36 | def consume_one_message(self): 37 | try: 38 | self.logger.info('Consuming one message on queue %s' % self._queue) 39 | self._worker_channel.open() 40 | ret = self._worker_channel.consume_one_message() 41 | finally: 42 | self._worker_channel.close() 43 | return ret 44 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lemzoo/flask-rabbitmq/722dfeeefc03e687f56c6e0b6e5cf69e04757da4/examples/__init__.py -------------------------------------------------------------------------------- /examples/flask/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lemzoo/flask-rabbitmq/722dfeeefc03e687f56c6e0b6e5cf69e04757da4/examples/flask/__init__.py -------------------------------------------------------------------------------- /examples/flask/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lemzoo/flask-rabbitmq/722dfeeefc03e687f56c6e0b6e5cf69e04757da4/examples/flask/app/__init__.py -------------------------------------------------------------------------------- /examples/flask/app/events/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lemzoo/flask-rabbitmq/722dfeeefc03e687f56c6e0b6e5cf69e04757da4/examples/flask/app/events/__init__.py -------------------------------------------------------------------------------- /examples/flask/app/events/events.py: -------------------------------------------------------------------------------- 1 | """ 2 | Centralize the events 3 | """ 4 | 5 | from flask import current_app 6 | 7 | from examples.flask.app.events import EVENT_MESSAGE_MANAGER 8 | from examples.flask.broker.event_handler import EventManager 9 | from broker_rabbit.broker import BrokerRabbitMQ 10 | 11 | 12 | class Tree: 13 | """ 14 | Tree can be used to represent a tree of object :: 15 | 16 | >>> t = Tree({ 17 | ... 'node_1': ('leaf_1_1', 'leaf_1_2'), 18 | ... 'node_2': ('leaf_2_1', {'node_2_2': ('leaf_2_2_1', 'leaf_2_2_2')}), 19 | ... }) 20 | ... 21 | >>> t.node_1.leaf_1_1 22 | 'node_1.leaf_1_1' 23 | >>> t.node_1.node_2_2.leaf_2_2_1 24 | 'node_1.node_2_2.leaf_2_2_1' 25 | 26 | Each node in the tree is a Tree object, each leaf is build 27 | using :method:`build_leaf` 28 | 29 | Tree can be used as an iterable to walk the leafs recursively 30 | 31 | >>> len(t) 32 | 5 33 | >>> [x for x in t] 34 | ['node_1.leaf_1_1', 'node_1.leaf_1_2', 'node_2.leaf_2_1', 35 | 'node_2.node_2_2.leaf_2_2_1', 'node_2.node_2_2.leaf_2_2_2'] 36 | """ 37 | 38 | def __init__(self, nodes, basename=''): 39 | self.nodes = [] 40 | if basename: 41 | make = lambda *args: basename + '.' + '.'.join(args) # noqa: E731 42 | else: 43 | make = lambda *args: '.'.join(args) # noqa: E731 44 | if isinstance(nodes, dict): 45 | for key, value in nodes.items(): 46 | self._set_leaf(key, self.__class__(value, make(key))) 47 | elif isinstance(nodes, (tuple, list, set)): 48 | for node in nodes: 49 | if isinstance(node, str): 50 | self._set_leaf(node, self.build_leaf(make(node))) 51 | elif isinstance(node, dict): 52 | for key, value in node.items(): 53 | self._set_leaf(key, self.__class__(value, make(key))) 54 | else: 55 | raise ValueError('Bad node type' % node) 56 | elif isinstance(nodes, str): 57 | self._set_leaf(nodes, self.build_leaf(make(nodes))) 58 | else: 59 | raise ValueError('Bad node type' % nodes) 60 | 61 | def build_leaf(self, route): 62 | return route 63 | 64 | def _set_leaf(self, key, value): 65 | setattr(self, key, value) 66 | self.nodes.append(value) 67 | 68 | def __iter__(self): 69 | for node in self.nodes: 70 | if isinstance(node, Tree): 71 | for sub_node in node: 72 | yield sub_node 73 | else: 74 | yield node 75 | 76 | def __len__(self): 77 | return len([e for e in self]) 78 | 79 | def __getitem__(self, i): 80 | return [e for e in self][i] 81 | 82 | def __str__(self): 83 | return str([e for e in self]) 84 | 85 | 86 | class Event: 87 | 88 | def __init__(self, name): 89 | self.name = name 90 | 91 | def __str__(self): 92 | return self.name 93 | 94 | def __repr__(self): 95 | return '' % self.name 96 | 97 | def __eq__(self, other): 98 | if isinstance(other, Event): 99 | return self.name == other.name 100 | else: 101 | return self.name == other 102 | 103 | def send(self, **kwargs): 104 | broker = current_app.extensions['broker'] 105 | return broker.send(queue='user', context=kwargs) 106 | 107 | 108 | class EventTree(Tree): 109 | 110 | def build_leaf(self, route): 111 | return Event(route) 112 | 113 | 114 | EVENTS = EventTree( 115 | { 116 | 'user': ('create', 'read', 'update', 'delete'), 117 | } 118 | ) 119 | 120 | 121 | def init_events(app): 122 | app.config['BROKER_AVAILABLE_EVENTS'] = [e.name for e in EVENTS] 123 | event_handler_items = EVENT_MESSAGE_MANAGER.copy() 124 | 125 | queues = [] 126 | for item in event_handler_items: 127 | queue = item['queue'] 128 | if queue not in queues: 129 | queues.append(queue) 130 | 131 | broker = BrokerRabbitMQ() 132 | on_message_callback = EventManager.process_message 133 | broker.init_app(app, queues, on_message_callback) 134 | app.extensions['broker'] = broker 135 | 136 | return event_handler_items 137 | -------------------------------------------------------------------------------- /examples/flask/app/events/events_handler_template.py: -------------------------------------------------------------------------------- 1 | EVENT_MESSAGE_MANAGER = [ 2 | { 3 | 'label': 'Register a user', 4 | 'queue': 'user', 5 | 'processor': 'register_processor', 6 | 'event': 'user.create' 7 | }, 8 | { 9 | 'label': 'View user', 10 | 'queue': 'user', 11 | 'processor': 'count_user_viewing_processor', 12 | 'event': 'user.read' 13 | }, 14 | { 15 | 'label': 'Update user', 16 | 'queue': 'user', 17 | 'processor': 'notify_user_update', 18 | 'event': 'user.update' 19 | }, 20 | { 21 | 'label': 'Delete a user', 22 | 'queue': 'user', 23 | 'processor': 'disable_user_processor', 24 | 'event': 'user.delete' 25 | }, 26 | ] 27 | -------------------------------------------------------------------------------- /examples/flask/app/view/__init__.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Api 2 | 3 | from examples.flask.app.view import UserRegistrationAPI, UserAPI 4 | 5 | api = Api() 6 | 7 | api.add_resource(UserRegistrationAPI, '/user') 8 | api.add_resource(UserAPI, '/user/') 9 | 10 | 11 | __all__ = ('api',) 12 | -------------------------------------------------------------------------------- /examples/flask/app/view/api.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | from flask_restful import Resource 3 | 4 | from examples.flask.app.events import EVENTS as event_managers 5 | 6 | 7 | class UserRegistrationAPI(Resource): 8 | """API for registration or listing user""" 9 | 10 | def get(self): 11 | raise NotImplementedError('API ‘GET‘ a list of user is not implemented') 12 | 13 | def post(self): 14 | payload = request.get_json() 15 | event_managers.user.create.send(**payload) 16 | return payload 17 | 18 | 19 | class UserAPI(Resource): 20 | """API for viewing a single user""" 21 | 22 | def get(self, email): 23 | return NotImplementedError('API `GET` is in construction') 24 | 25 | def patch(self, email): 26 | raise NotImplementedError('API `PATCH` is in construction') 27 | 28 | def delete(self, email): 29 | raise NotImplementedError('API `DELETE` is in construction') 30 | -------------------------------------------------------------------------------- /examples/flask/broker/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lemzoo/flask-rabbitmq/722dfeeefc03e687f56c6e0b6e5cf69e04757da4/examples/flask/broker/__init__.py -------------------------------------------------------------------------------- /examples/flask/broker/event_handler.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | 4 | class EventError(Exception): 5 | pass 6 | 7 | 8 | class UnknownEventHandlerError(EventError): 9 | pass 10 | 11 | 12 | class UnknownEventError(EventError): 13 | pass 14 | 15 | 16 | class EventMessage: 17 | 18 | def __init__(self, event_message_item): 19 | self.label = event_message_item['label'] 20 | self.queue = event_message_item.get('queue') 21 | self.processor = event_message_item.get('processor') 22 | self.event = event_message_item.get('event') 23 | 24 | def modify(self, **kwargs): 25 | if 'label' in kwargs: 26 | self.label = kwargs['label'] 27 | if 'queue' in kwargs: 28 | self.queue = kwargs['queue'] 29 | if 'processor' in kwargs: 30 | self.processor = kwargs['processor'] 31 | if 'event' in kwargs: 32 | self.event = kwargs['event'] 33 | 34 | 35 | class EventManager: 36 | 37 | def __init__(self, handlers): 38 | self.items = [EventMessage(eh) for eh in handlers] 39 | 40 | @staticmethod 41 | def process_message(message): 42 | print('Message Content = ’{content}’'.format(content=message)) 43 | sleep(2) 44 | 45 | def get(self, label, default=None): 46 | try: 47 | return self[label] 48 | except IndexError: 49 | return default 50 | 51 | def __getitem__(self, label): 52 | for item in self.items: 53 | if item.label == label: 54 | return item 55 | raise IndexError() 56 | 57 | def filter(self, event=None): 58 | ret = self.items 59 | if event: 60 | ret = [x for x in ret if x.event == event] 61 | return ret 62 | 63 | def flush(self): 64 | self.items = [] 65 | 66 | def append(self, item): 67 | if isinstance(item, EventMessage): 68 | self.items.append(item) 69 | elif isinstance(item, dict): 70 | self.items.append(EventMessage(item)) 71 | else: 72 | raise ValueError() 73 | -------------------------------------------------------------------------------- /examples/flask/broker/processor.py: -------------------------------------------------------------------------------- 1 | class ProcessorManager: 2 | 3 | """ 4 | Hold a list of processor (i.e. functions) a worker can apply to a message. 5 | """ 6 | 7 | def __init__(self): 8 | self._processors = {} 9 | 10 | def exists(self, name): 11 | return name in self._processors 12 | 13 | def register(self, function, name=None): 14 | "Register a new processor" 15 | if not name: 16 | name = function.__name__ 17 | self._processors[name] = function 18 | return function 19 | 20 | def find(self, name): 21 | "Retreive a processor from it name" 22 | return self._processors.get(name) 23 | 24 | def execute(self, processor, *args, **kwargs): 25 | "Process the given message with it assign processor" 26 | f = self.find(processor) 27 | if f: 28 | return f(*args, **kwargs) 29 | else: 30 | raise UnknownProcessorError('Cannot execute processor %s' % 31 | processor) 32 | 33 | def list(self): 34 | return list(self._processors.keys()) 35 | 36 | 37 | class UnknownProcessorError(Exception): 38 | pass 39 | 40 | 41 | processor_manager = ProcessorManager() 42 | find_and_execute = processor_manager.execute 43 | register_processor = processor_manager.register 44 | -------------------------------------------------------------------------------- /examples/flask/default-config.cfg: -------------------------------------------------------------------------------- 1 | from os import environ, path 2 | 3 | # API PREFIX 4 | API_PREFIX = environ.get('BACKEND_API_PREFIX', '') 5 | ### MongoDB ### 6 | MONGODB_URL = environ.get('MONGODB_URL', 'mongodb://localhost:27017/developer') 7 | # Flask-mongoengine use MONGODB_HOST variable to configure mongodb connection 8 | MONGODB_HOST = MONGODB_URL 9 | MONGODB_TEST_URL = environ.get('MONGODB_TEST_URL', 'mongodb://localhost:27017/test') 10 | 11 | ### RabbitMQ ### 12 | EXCHANGE_NAME = environ.get('EXCHANGE_NAME', 'my-exchange') 13 | EXCHANGE_TYPE = environ.get('EXCHANGE_TYPE', 'direct') 14 | APPLICATION_ID = environ.get('APPLICATION_ID', 'my-app-id') 15 | PERSIST_MESSAGE_MODE = 2 16 | DELIVERY_MODE = environ.get('DELIVERY_MODE', PERSIST_MESSAGE_MODE) 17 | DEFAULT_RABBIT_URL = 'amqp://guest:guest@localhost:5672/flask-rabbitmq' 18 | RABBIT_MQ_URL = environ.get('RABBIT_MQ_URL', DEFAULT_RABBIT_URL) 19 | 20 | EXCHANGE_TEST_NAME = environ.get('EXCHANGE_TEST_NAME', 'exchange-test') 21 | RABBIT_MQ_TEST_URL = environ.get('RABBIT_MQ_TEST_URL', 'amqp://guest:guest@localhost:5672/%2F') 22 | # RABBIT_MQ_TEST_MANAGEMENT_PORT = _environ_int('# RABBIT_MQ_TEST_MANAGEMENT_PORT', 15672) 23 | 24 | -------------------------------------------------------------------------------- /examples/flask/main.py: -------------------------------------------------------------------------------- 1 | from os.path import abspath, dirname 2 | 3 | from flask import Flask 4 | 5 | 6 | class CoreApp(Flask): 7 | """ 8 | CoreApp is a regular :class:`Flask` app 9 | """ 10 | 11 | def __init__(self, *args, **kwargs): 12 | super().__init__(*args, **kwargs) 13 | self.root_path = abspath(dirname(__file__)) 14 | 15 | def bootstrap(self): 16 | # self.db.init_app(self) 17 | pass 18 | 19 | 20 | def create_app(config=None): 21 | """ 22 | Build the app build don't initialize it, useful to get back the default 23 | app config, correct it, then call ``bootstrap_app`` with the new config 24 | """ 25 | app = CoreApp(__name__) 26 | 27 | if config: 28 | app.config.update(config) 29 | app.config.from_pyfile('default-config.cfg') 30 | 31 | return app 32 | 33 | 34 | def bootstrap_app(app=None, config=None): 35 | """ 36 | Create and initialize the app 37 | """ 38 | 39 | if not app: 40 | app = create_app(config) 41 | elif config: 42 | app.config.update(config) 43 | 44 | from examples.flask.app.events import events 45 | from examples.flask.app.view import api 46 | 47 | app.bootstrap() 48 | 49 | api.prefix = app.config['API_PREFIX'] 50 | api.init_app(app) 51 | events.init_events(app) 52 | 53 | return app 54 | -------------------------------------------------------------------------------- /examples/flask/manager.py: -------------------------------------------------------------------------------- 1 | from flask_script import Manager, Server, Shell 2 | 3 | from examples.flask.main import bootstrap_app 4 | from broker_rabbit.manager import broker_rabbit_manager 5 | 6 | app = bootstrap_app() 7 | 8 | manager = Manager(app) 9 | 10 | 11 | manager.add_command("runserver", Server()) 12 | manager.add_command("shell", Shell()) 13 | manager.add_command("broker", broker_rabbit_manager) 14 | 15 | if __name__ == "__main__": 16 | manager.run() 17 | -------------------------------------------------------------------------------- /examples/getting_started/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lemzoo/flask-rabbitmq/722dfeeefc03e687f56c6e0b6e5cf69e04757da4/examples/getting_started/__init__.py -------------------------------------------------------------------------------- /examples/getting_started/app.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | from flask import Flask 4 | 5 | from broker_rabbit import BrokerRabbitMQ 6 | 7 | 8 | def process_message(message): 9 | print('Message received and content is ’{content}’'.format(content=message)) 10 | sleep(1) 11 | 12 | 13 | app = Flask(__name__) 14 | connection_string = 'amqp://guest:guest@localhost:5672/test-flask-rabbitmq' 15 | app.config['RABBIT_MQ_URL'] = connection_string 16 | app.config['EXCHANGE_NAME'] = 'test-exchange' 17 | 18 | broker = BrokerRabbitMQ() 19 | broker.init_app(app=app, queues=['users'], on_message_callback=process_message) 20 | -------------------------------------------------------------------------------- /examples/getting_started/consumer.py: -------------------------------------------------------------------------------- 1 | from examples.getting_started.app import broker 2 | 3 | broker.start(queue='users') 4 | -------------------------------------------------------------------------------- /examples/getting_started/producer.py: -------------------------------------------------------------------------------- 1 | from examples.getting_started.app import broker 2 | 3 | broker.send(queue='users', context={'key': 'value', 'number': 1}) 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flake8==3.7.7 2 | pika==0.10.0 3 | pyrabbit==1.1.0 4 | pytest==4.5.0 5 | pytest-cov==2.7.1 6 | codecov==2.0.15 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # E127: continuation line over-indented for visual indent 2 | # E128: continuation line under-indented for visual indent 3 | [flake8] 4 | ignore = E127,E128 5 | max-line-length = 79 6 | exclude=.git,docs,tests,restkit/compat.py,env,venv,.ropeproject 7 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lemzoo/flask-rabbitmq/722dfeeefc03e687f56c6e0b6e5cf69e04757da4/tests/__init__.py -------------------------------------------------------------------------------- /tests/base_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tests.broker_rabbit.rabbit_api import get_queues, delete_queue, client_rabbit 4 | from tests.common import get_rabbit_management_parsed_url 5 | 6 | 7 | @pytest.mark.rabbit 8 | @pytest.mark.functional_test 9 | class RabbitBrokerTest: 10 | 11 | @staticmethod 12 | def _clean_rabbit(): 13 | """Get all queues names and delete each.""" 14 | parsed_url = get_rabbit_management_parsed_url() 15 | url = parsed_url['url'] 16 | user = parsed_url['username'] 17 | password = parsed_url['password'] 18 | vhost = parsed_url['vhost'] 19 | 20 | client_api_rabbit = client_rabbit(url, user, password) 21 | 22 | queues = get_queues(client_api_rabbit, vhost) 23 | for queue in queues: 24 | delete_queue(client_api_rabbit, vhost, queue) 25 | 26 | def setup_method(self, method): 27 | self._clean_rabbit() 28 | -------------------------------------------------------------------------------- /tests/broker_rabbit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lemzoo/flask-rabbitmq/722dfeeefc03e687f56c6e0b6e5cf69e04757da4/tests/broker_rabbit/__init__.py -------------------------------------------------------------------------------- /tests/broker_rabbit/rabbit_api.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import subprocess 4 | 5 | import pika 6 | from pyrabbit import Client 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | def client_rabbit_bis(url: str): 12 | parameters = pika.URLParameters(url) 13 | client_rabbit = pika.BlockingConnection(parameters=parameters) 14 | return client_rabbit 15 | 16 | 17 | def client_rabbit(url, username, password): 18 | """Configure the client for broker_rabbit 19 | 20 | :param str url: The address url of the host where broker_rabbit located 21 | :param str username: The username id 22 | :param str password: The password 23 | :return Client client: authenticated client which is ready to use 24 | """ 25 | client = Client(url, username, password) 26 | return client 27 | 28 | 29 | def get_queues_old(admin_path, vhost): 30 | cmd = "{} list queues name vhost | grep {} | tr -d \| |tr -d '\n'".format( 31 | admin_path, vhost) 32 | proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True) 33 | (out, err) = proc.communicate() 34 | 35 | return str(out.decode('utf8').replace(vhost, "")).split() 36 | 37 | 38 | def get_queues(client, vhost): 39 | """Get all queues, or all queues in a vhost if vhost is not None. 40 | Returns a list. 41 | 42 | :param Client client : authenticated client which is ready to use 43 | :param str vhost: virtual host where the queues will be found 44 | :return A list of dicts, each representing a queue 45 | """ 46 | queues = [q['name'] for q in client.get_queues(vhost)] 47 | 48 | return queues 49 | 50 | 51 | def delete_queue(client, vhost, queue): 52 | """Deletes the named queue from the named vhost. 53 | 54 | :param Client client : authenticated client which is ready to use 55 | :param str vhost : Name of the virtual host ot get exchange 56 | :param str queue : Name of the queue to delete 57 | """ 58 | client.delete_queue(vhost, queue) 59 | 60 | 61 | def get_messages(client, queue): 62 | """Gets messages from the queue.. 63 | 64 | :param pika.BlockingConnection client : Authentificated client which is ready to use 65 | :param str queue: Name of the queue to consume from. 66 | :return list messages: list of dicts. messages[msg-index][‘payload’] 67 | will contain the message body. 68 | """ 69 | messages = list() 70 | channel = client.channel() 71 | channel.queue_declare(queue=queue, durable=True, auto_delete=False) 72 | 73 | try: 74 | for method_frame, properties, body in channel.consume( 75 | queue=queue, 76 | exclusive=True, 77 | inactivity_timeout=10): 78 | message_body = json.loads(body.decode('utf8').replace("'", '"')) 79 | message_body = json.dumps(message_body) 80 | messages.append({'method_frame': method_frame, 81 | 'properties': properties, 'body': message_body}) 82 | # Acknowledge the message 83 | channel.basic_ack(method_frame.delivery_tag) 84 | # Escape out of the loop after 10 messages 85 | if method_frame.delivery_tag == 1: 86 | break 87 | except TypeError: 88 | logger.info("Fin de la lecture des messages.") 89 | 90 | # Cancel the consumer and return any pending messages 91 | requeued_messages = channel.cancel() 92 | logger.info('Requeued %i messages' % requeued_messages) 93 | 94 | # Close the channel and the connection 95 | channel.close() 96 | return messages 97 | 98 | 99 | def get_number_message_on_queue(client, queue): 100 | """Gets messages from the queue.. 101 | 102 | :param pika.BlockingConnection client : Authentificated client which is ready to use 103 | :param str queue: Name of the queue to consume from. 104 | :return int numbe_msg: The number of the message availabale on 105 | the specified queue 106 | """ 107 | messages = list() 108 | channel = client.channel() 109 | channel.queue_declare(queue=queue, durable=True, auto_delete=False) 110 | 111 | try: 112 | for method_frame, properties, body in channel.consume( 113 | queue=queue, 114 | exclusive=True, 115 | inactivity_timeout=30): 116 | messages.append({'method_frame': method_frame, 'properties': properties, 'body': body}) 117 | # Acknowledge the message 118 | channel.basic_ack(method_frame.delivery_tag) 119 | except TypeError: 120 | print("Fin de la lecture des messages.") 121 | 122 | # Cancel the consumer and return any pending messages 123 | requeued_messages = channel.cancel() 124 | logger.info('Requeued %i messages' % requeued_messages) 125 | 126 | # Close the channel and the connection 127 | channel.close() 128 | return len(messages) 129 | 130 | 131 | def purge_queue(client, queue): 132 | """Gets messages from the queue.. 133 | 134 | :param pika.BlockingConnection client : Authentificated client which is ready to use 135 | :param str queue: Name of the queue to consume from. 136 | :return int numbe_msg: The number of the message availabale on 137 | the specified queue 138 | """ 139 | channel = client.channel() 140 | 141 | channel.queue_declare(queue=queue, durable=True, auto_delete=False) 142 | channel.queue_purge(queue) 143 | channel.close() 144 | -------------------------------------------------------------------------------- /tests/broker_rabbit/test_broker.py: -------------------------------------------------------------------------------- 1 | import json 2 | from datetime import datetime 3 | from unittest.mock import Mock 4 | 5 | import pytest 6 | 7 | from broker_rabbit import BrokerRabbitMQ, UnknownQueueError 8 | from tests.base_test import RabbitBrokerTest 9 | from tests.broker_rabbit.rabbit_api import get_messages, client_rabbit_bis 10 | from tests.common import BROKER_URL_TEST 11 | 12 | 13 | class TestBrokerRabbitMQ(RabbitBrokerTest): 14 | queue = 'test-queue' 15 | broker = BrokerRabbitMQ() 16 | 17 | @property 18 | def client_rabbit(self): 19 | return client_rabbit_bis(BROKER_URL_TEST) 20 | 21 | def setup(self): 22 | attributes = {'config': {'RABBIT_MQ_URL': BROKER_URL_TEST, 23 | 'EXCHANGE_NAME': 'test-exchange'}} 24 | app = Mock(name='broker', **attributes) 25 | 26 | self.broker.init_app(app=app, queues=[self.queue], 27 | on_message_callback=self.handler) 28 | 29 | @staticmethod 30 | def handler(message): 31 | pass 32 | 33 | def test_return_success_after_publishing_a_message(self): 34 | # Given 35 | context = {'key': 'value', 'number': 1} 36 | 37 | # When 38 | self.broker.send(queue=self.queue, context=context) 39 | 40 | # Then 41 | # TODO: retrieve the message on the broker and make the assertion 42 | messages = get_messages(client=self.client_rabbit, queue=self.queue) 43 | assert len(messages) == 1 44 | 45 | body = json.loads(messages[0]['body']) 46 | assert body['queue'] == self.queue 47 | now = datetime.utcnow() 48 | assert body['created_at'][:19] == now.isoformat()[:19] 49 | assert body['context'] == context 50 | 51 | def test_return_error_when_trying_to_publish_on_not_registered_queue(self): 52 | # Given 53 | queue = 'UNKNOWN_QUEUE' 54 | context = {'key': 'value', 'number': 1} 55 | 56 | # When 57 | with pytest.raises(UnknownQueueError) as error: 58 | self.broker.send(queue=queue, context=context) 59 | 60 | # Then 61 | assert error.value.args[0][0] == f'Queue ‘{queue}‘ is not registered' 62 | -------------------------------------------------------------------------------- /tests/broker_rabbit/test_channels.py: -------------------------------------------------------------------------------- 1 | import json 2 | from unittest.mock import Mock 3 | 4 | import pytest 5 | from pika.connection import Connection 6 | from pika.spec import Basic 7 | 8 | from broker_rabbit.exceptions import ( 9 | ConnectionNotOpenedError, ConnectionIsClosedError, 10 | WorkerExitError, ChannelUndefinedError, BadFormatMessageError, 11 | CallBackError) 12 | 13 | from broker_rabbit.channels import (ChannelHandler, WorkerChannel, 14 | ProducerChannel) 15 | 16 | from broker_rabbit.connection_handler import ConnectionHandler 17 | 18 | from tests import common 19 | from tests.base_test import RabbitBrokerTest 20 | 21 | 22 | # TODO: Test the new added method 23 | @pytest.mark.functional_test 24 | class TestChannelHandler(RabbitBrokerTest): 25 | def setup_method(self, method): 26 | self.connection = ConnectionHandler(common.BROKER_URL_TEST) 27 | current_connection = self.connection.get_current_connection() 28 | self.channel_handler = ChannelHandler(current_connection) 29 | 30 | def test_should_raise_when_connection_is_not_defined(self): 31 | # Given 32 | connection = None 33 | channel_handler = ChannelHandler(connection) 34 | 35 | # When 36 | with pytest.raises(ConnectionNotOpenedError) as error: 37 | channel_handler.open() 38 | 39 | # Then 40 | assert 'The connection is not opened' == error.value.args[0][0] 41 | 42 | def test_should_raise_when_connection_is_closed(self): 43 | # Given 44 | self.connection.close_connection() 45 | 46 | # When 47 | with pytest.raises(ConnectionIsClosedError) as error: 48 | self.channel_handler.open() 49 | 50 | # Then 51 | assert 'The connection is closed' == error.value.args[0][0] 52 | 53 | def test_should_open_channel(self): 54 | # When 55 | self.channel_handler.open() 56 | 57 | # Then 58 | assert self.channel_handler.get_channel().is_open is True 59 | 60 | def test_should_close_channel(self): 61 | # Given 62 | self.channel_handler.open() 63 | 64 | # When 65 | self.channel_handler.close() 66 | 67 | # Then 68 | assert self.channel_handler.get_channel().is_closed is True 69 | 70 | def test_should_raise_when_channel_is_not_defined(self): 71 | # Given 72 | self.channel_handler._channel = None 73 | 74 | # When 75 | with pytest.raises(ChannelUndefinedError) as error: 76 | self.channel_handler.get_channel() 77 | 78 | # Then 79 | assert 'The channel does not exist yet' == error.value.args[0][0] 80 | 81 | 82 | @pytest.mark.unit_test 83 | class TestWorkerChannel: 84 | def setup(self): 85 | connection = Mock(Connection) 86 | connection.is_closed = False 87 | self.worker = WorkerChannel(connection, None, None) 88 | self.worker.open() 89 | 90 | def teardown_method(self): 91 | self.worker.close() 92 | 93 | def test_raise_error_on_keyboard_interrupt(self): 94 | # Given 95 | self.worker._channel.start_consuming.side_effect = KeyboardInterrupt() 96 | 97 | # When 98 | with pytest.raises(WorkerExitError) as error: 99 | self.worker.run() 100 | 101 | # Then 102 | assert 'Worker stopped pulling message' == error.value.args[0][0] 103 | 104 | def test_raise_when_error_occurs_in_decode_message_content(self): 105 | empty_body_as_bytes = b'{}' 106 | self.worker.event_handler = Mock() 107 | 108 | # When 109 | self.worker.on_message(None, Basic.GetOk(), None, empty_body_as_bytes) 110 | 111 | # Then 112 | assert not self.worker.event_handler.execute_rabbit.called 113 | 114 | def test_execute_rabbit_is_not_called_when_exception_raised(self): 115 | empty_body_as_bytes = b'{}' 116 | self.worker.event_handler = Mock() 117 | 118 | # When 119 | self.worker.on_message(None, Basic.GetOk(), None, empty_body_as_bytes) 120 | 121 | # Then 122 | assert not self.worker.event_handler.execute_rabbit.called 123 | 124 | 125 | @pytest.mark.unit_test 126 | class TestWorkerChannelBindCallBack(TestWorkerChannel): 127 | def setup(self): 128 | super().setup() 129 | 130 | @pytest.mark.skip 131 | def test_raise_error_when_message_is_not_a_json_content_type(self): 132 | # Given 133 | not_serializable_message = 'foo-content' 134 | 135 | # When 136 | with pytest.raises(BadFormatMessageError) as error: 137 | self.worker.bind_callback(not_serializable_message) 138 | 139 | # Then 140 | expected = 'Error while trying to jsonify message with `foo-content`' 141 | assert expected == error.value.args[0][0] 142 | original_msg = 'Expecting value: line 1 column 1 (char 0)' 143 | assert original_msg == error.value.args[1]['original_exception'] 144 | 145 | def test_raise_error_when_callback_is_not_callable(self): 146 | # Given 147 | message = {'key': 'value', 'number': 1, 'foo': 'bar'} 148 | raw_message = json.dumps(message) 149 | # When 150 | with pytest.raises(CallBackError) as error: 151 | self.worker.bind_callback(raw_message) 152 | 153 | # Then 154 | assert 'The callback is not callable' == error.value.args[0][0] 155 | 156 | def test_should_raise_callback_with_decoded_message(self): 157 | # Given 158 | def test_callback(arg1, arg2): 159 | pass 160 | 161 | # Monkey Patch the call back 162 | self.worker._on_message_callback = test_callback 163 | 164 | message = {'key': 'value', 'number': 1, 'foo': 'bar'} 165 | raw_message = json.dumps(message) 166 | 167 | # When 168 | with pytest.raises(CallBackError) as error: 169 | self.worker.bind_callback(raw_message) 170 | 171 | # Then 172 | error_message = 'You should implement your callback ' \ 173 | 'like my_callback(content)' 174 | assert error_message == error.value.args[0][0] 175 | orig = "test_callback() missing 1 required positional argument: 'arg2'" 176 | assert orig == error.value.args[1]['original_exception'] 177 | 178 | def test_should_call_callback_with_decoded_message(self): 179 | # Given 180 | # Monkey Patch the call back 181 | callback_mock = Mock() 182 | self.worker._on_message_callback = callback_mock 183 | message = {'key': 'value', 'number': 1, 'foo': 'bar'} 184 | raw_message = json.dumps(message) 185 | 186 | # When 187 | self.worker.bind_callback(raw_message) 188 | 189 | # Then 190 | callback_mock.assert_called_with(raw_message) 191 | 192 | 193 | @pytest.mark.unit_test 194 | class TestProducerChannel: 195 | def setup_method(self): 196 | connection = Mock(Connection) 197 | connection.is_closed = False 198 | self.channel = ProducerChannel(connection, delivery_mode=2, 199 | application_id='TEST-APP_ID') 200 | self.channel.open() 201 | self.exchange = 'TEST-EXCHANGE' 202 | self.queue = 'TEST-QUEUE' 203 | 204 | def teardown_method(self): 205 | self.channel.close() 206 | 207 | def test_send_message(self): 208 | # Given 209 | message = {'key': 'value', 'number': 1, 'foo': 'bar'} 210 | self.channel.basic_properties = 'TEST-PROPERTIES' 211 | 212 | # When 213 | self.channel.send_message(self.exchange, self.queue, message) 214 | 215 | # Then 216 | body = json.dumps(message) 217 | channel = self.channel.get_channel() 218 | channel.basic_publish.assert_called_with( 219 | exchange=self.exchange, routing_key=self.queue, 220 | body=body, properties='TEST-PROPERTIES') 221 | 222 | def test_returns_correct_for_properties(self): 223 | # Given 224 | application_id = 'TEST-APPLICATION-ID' 225 | delivery_mode = 2 226 | 227 | # When 228 | properties = self.channel.apply_basic_properties(application_id, 229 | delivery_mode) 230 | 231 | # Then 232 | assert 'TEST-APPLICATION-ID' == properties.app_id 233 | assert 2 == properties.delivery_mode 234 | -------------------------------------------------------------------------------- /tests/broker_rabbit/test_connection_handler.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pika import BlockingConnection 3 | from pika.exceptions import ConnectionClosed, ProbableAuthenticationError 4 | 5 | from tests.base_test import RabbitBrokerTest 6 | from broker_rabbit.connection_handler import ConnectionHandler 7 | from tests import common 8 | 9 | 10 | class TestConnectionHandler(RabbitBrokerTest): 11 | def test_initialize_new_connection(self): 12 | # When 13 | connection_handler = ConnectionHandler(common.BROKER_URL_TEST) 14 | 15 | # Then 16 | connection = connection_handler.get_current_connection() 17 | assert connection.is_open 18 | assert isinstance(connection, BlockingConnection) 19 | 20 | def test_raise_when_trying_to_open_connection_with_bad_port(self): 21 | # Given 22 | rabbit_url = "amqp://guest:guest@localhost:8225/%2F" 23 | 24 | # When and Then is expecting an exception to be raised 25 | with pytest.raises(ConnectionClosed): 26 | ConnectionHandler(rabbit_url) 27 | 28 | def test_raises_when_trying_to_open_connection_with_bad_credentials(self): 29 | # Given 30 | rabbit_url = "amqp://vip:vip@localhost:5672/%2F" 31 | 32 | # When and Then is expecting an exception to be raised 33 | with pytest.raises(ProbableAuthenticationError): 34 | ConnectionHandler(rabbit_url) 35 | 36 | def test_open_connection_successfully(self): 37 | # When 38 | connection_handler = ConnectionHandler(common.BROKER_URL_TEST) 39 | 40 | # Then 41 | connection = connection_handler.get_current_connection() 42 | assert connection.is_open is True 43 | 44 | def test_close_connection_successfully(self): 45 | # Given 46 | connection_handler = ConnectionHandler(common.BROKER_URL_TEST) 47 | 48 | # When 49 | connection_handler.close_connection() 50 | 51 | # Then 52 | connection = connection_handler.get_current_connection() 53 | assert connection.is_closed is True 54 | assert connection.is_open is False 55 | -------------------------------------------------------------------------------- /tests/broker_rabbit/test_exchange_handler.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | 3 | import pytest 4 | 5 | from broker_rabbit.exceptions import (ChannelUndefinedError, 6 | ExchangeUndefinedError) 7 | from broker_rabbit.exchange_handler import ExchangeHandler 8 | 9 | 10 | @pytest.mark.unit_test 11 | class TestExchangeHandler: 12 | def test_raises_when_channel_is_not_defined(self): 13 | # Given 14 | channel = None 15 | exchange_name = 'TEST-EXCHANGE-NAME' 16 | 17 | # When 18 | exchange_handler = ExchangeHandler(channel, exchange_name) 19 | 20 | with pytest.raises(ChannelUndefinedError) as error: 21 | exchange_handler.setup_exchange() 22 | 23 | # Then 24 | assert 'The channel was not defined' == error.value.args[0][0] 25 | 26 | def test_should_setup_exchange_via_channel(self): 27 | # Given 28 | channel = Mock() 29 | exchange_name = 'TEST-EXCHANGE-NAME' 30 | exchange_handler = ExchangeHandler(channel, exchange_name) 31 | 32 | # When 33 | exchange_handler.setup_exchange() 34 | 35 | # Then 36 | channel.exchange_declare.assert_called_once_with( 37 | exchange=exchange_name, type='direct', 38 | durable=True, auto_delete=False) 39 | 40 | def test_raises_when_trying_to_get_exchange(self): 41 | # Given 42 | exchange_handler = ExchangeHandler(None, None) 43 | 44 | # When 45 | with pytest.raises(ExchangeUndefinedError) as error: 46 | exchange_handler.name 47 | 48 | # Then 49 | assert 'The exchange is not defined' == error.value.args[0][0] 50 | 51 | def test_get_exchange_name(self): 52 | # Given 53 | exchange_name = 'TEST-EXCHANGE-NAME' 54 | exchange_handler = ExchangeHandler(None, exchange_name) 55 | 56 | # When 57 | result = exchange_handler.name 58 | # Then 59 | assert exchange_name == result 60 | -------------------------------------------------------------------------------- /tests/broker_rabbit/test_producer.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock, patch, call 2 | 3 | import pytest 4 | 5 | from broker_rabbit.channels import ProducerChannel 6 | from broker_rabbit.connection_handler import ConnectionHandler 7 | from broker_rabbit.exceptions import (QueueDoesNotExistError, 8 | ConnectionNotOpenedError) 9 | from broker_rabbit.producer import Producer 10 | 11 | from tests import common 12 | 13 | 14 | class TestBase: 15 | def setup(self): 16 | connection = ConnectionHandler(common.BROKER_URL_TEST) 17 | current_connection = connection.get_current_connection() 18 | self.exchange_name = 'TEST-EXCHANGE-NAME' 19 | self.application_id = 'TEST-APPLICATION-ID' 20 | self.delivery_mode = 2 21 | channel_handler = ProducerChannel(current_connection, 22 | self.application_id, 23 | self.delivery_mode) 24 | channel_handler.open() 25 | self.channel = channel_handler.get_channel() 26 | self.channel = Mock(ProducerChannel) 27 | self.message = {"content": "TEST-CONTENT-MESSAGE"} 28 | 29 | 30 | @pytest.mark.unit_test 31 | class TestProducer(TestBase): 32 | def setup(self): 33 | super().setup() 34 | self.queues = ['first-queue', 'second-queue'] 35 | self.producer = Producer(self.channel, self.exchange_name, self.queues) 36 | self.first_queue = self.queues[0] 37 | 38 | def test_should_raise_when_queue_to_publish_does_not_exist(self): 39 | # Given 40 | unknown_queue = 'UNKNOWN' 41 | 42 | # When 43 | with pytest.raises(QueueDoesNotExistError) as error: 44 | self.producer.publish(unknown_queue, self.message) 45 | 46 | # Then 47 | error_message = 'Queue with name `UNKNOWN` is not declared.' \ 48 | 'Please call bootstrap before using publish' 49 | 50 | assert error_message == error.value.args[0][0] 51 | 52 | def test_should_open_channel_before_sending_message(self): 53 | # When 54 | self.producer.publish(self.first_queue, self.message) 55 | 56 | # Then 57 | method_calls = self.channel.method_calls 58 | assert call.open() == method_calls[3] 59 | call_send_message = call.send_message( 60 | self.exchange_name, self.first_queue, self.message) 61 | assert call_send_message == method_calls[4] 62 | 63 | def test_should_publish_on_given_queue(self): 64 | # When 65 | self.producer.publish(self.first_queue, self.message) 66 | 67 | # Then 68 | self.channel.send_message.assert_called_with( 69 | self.exchange_name, self.first_queue, self.message) 70 | 71 | def test_should_close_used_channel_after_publishing_message(self): 72 | # When 73 | self.producer.publish(self.first_queue, self.message) 74 | 75 | # Then 76 | assert self.channel.close.called is True 77 | 78 | 79 | class TestProducerBootstrap(TestBase): 80 | 81 | def setup(self): 82 | super().setup() 83 | self.producer = Producer(self.channel, self.exchange_name) 84 | self.queues = ['queue-1', 'queue-2', 'queue-3', 'queue-4'] 85 | 86 | queue_handler = patch('broker_rabbit.producer.QueueHandler') 87 | self.queue_handler = queue_handler.start() 88 | 89 | exchange_handler = patch('broker_rabbit.producer.ExchangeHandler') 90 | self.exchange_handler = exchange_handler.start() 91 | 92 | def test_should_setup_exchange(self): 93 | # When 94 | self.producer.bootstrap(self.queues) 95 | 96 | # Then 97 | channel = self.channel.get_channel() 98 | self.exchange_handler.assert_called_once_with(channel, 99 | self.exchange_name) 100 | self.exchange_handler().setup_exchange.assert_called_once() 101 | 102 | def test_should_setup_queue(self): 103 | # When 104 | self.producer.bootstrap(self.queues) 105 | 106 | # Then 107 | channel = self.channel.get_channel() 108 | self.queue_handler.assert_called_once_with(channel, self.exchange_name) 109 | assert 4 == self.queue_handler().setup_queue.call_count 110 | 111 | def test_should_close_channel_at_the_end(self): 112 | # When 113 | self.producer.bootstrap(self.queues) 114 | 115 | # Then 116 | self.channel.close.assert_called_once() 117 | 118 | def test_should_close_channel_at_the_end_while_error_occurred(self): 119 | # Given 120 | channel = self.channel.get_channel() 121 | error_msg = 'connection not opened' 122 | channel.open.side_effect = ConnectionNotOpenedError(error_msg) 123 | 124 | # When 125 | self.producer.bootstrap(self.queues) 126 | 127 | # Then 128 | self.channel.close.assert_called_once() 129 | -------------------------------------------------------------------------------- /tests/broker_rabbit/test_queue_handler.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | 3 | import pytest 4 | from pika import BlockingConnection 5 | 6 | from broker_rabbit.exceptions import (ChannelUndefinedError, 7 | ExchangeUndefinedError) 8 | from broker_rabbit.queue_handler import QueueHandler 9 | 10 | 11 | @pytest.mark.unit_test 12 | class TestQueueHandler: 13 | def test_setup_raise_when_channel_is_not_defined(self): 14 | queue_handler = QueueHandler(None, None) 15 | 16 | with pytest.raises(ChannelUndefinedError) as error: 17 | queue_handler.setup_queue(None) 18 | 19 | # Then 20 | assert 'The Channel is not defined yet' == error.value.args[0][0] 21 | 22 | def test_setup_raises_when_exchange_is_not_defined(self): 23 | # Given 24 | connection = Mock(BlockingConnection) 25 | channel = connection.channel() 26 | queue_handler = QueueHandler(channel, None) 27 | 28 | # When 29 | with pytest.raises(ExchangeUndefinedError) as error: 30 | queue_handler.setup_queue(None) 31 | 32 | # Then 33 | assert 'The exchange is not defined' == error.value.args[0][0] 34 | 35 | def test_setup_queue(self): 36 | # Given 37 | channel = Mock() 38 | exchange_name = 'TEST-EXCHANGE-NAME' 39 | queue_name = 'TEST-QUEUE-NAME' 40 | 41 | queue_handler = QueueHandler(channel, exchange_name) 42 | queue_handler.create_queue = Mock() 43 | 44 | # When 45 | queue_handler.setup_queue(queue_name) 46 | 47 | # Then 48 | queue_handler.create_queue.assert_called_once_with(queue_name) 49 | channel.queue_bind.assert_called_once_with(queue=queue_name, 50 | exchange= exchange_name) 51 | 52 | def test_create_queue(self): 53 | # Given 54 | channel = Mock() 55 | exchange_name = 'TEST-EXCHANGE-NAME' 56 | queue_name = 'TEST-QUEUE-NAME' 57 | queue_handler = QueueHandler(channel, exchange_name) 58 | 59 | # When 60 | queue_handler.create_queue(queue_name) 61 | 62 | # Then 63 | channel.queue_declare.assert_called_once_with( 64 | queue=queue_name, durable=True, 65 | auto_delete=False, arguments={'x-ha-policy': 'all'} 66 | ) 67 | -------------------------------------------------------------------------------- /tests/broker_rabbit/test_worker.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from unittest.mock import Mock 3 | 4 | import pytest 5 | 6 | from broker_rabbit.worker import Worker 7 | from broker_rabbit.channels import WorkerChannel 8 | 9 | RABBIT_MQ_WORKER = 'test-logger' 10 | QUEUE = 'test-queue' 11 | 12 | 13 | @pytest.fixture 14 | def mocked_worker(): 15 | class MockedWorker(Worker): 16 | def __init__(self): 17 | self._worker_channel = Mock(WorkerChannel) 18 | self._queue = QUEUE 19 | self.logger = logging.getLogger(RABBIT_MQ_WORKER) 20 | 21 | return MockedWorker() 22 | 23 | 24 | @pytest.mark.unit_test 25 | class TestWorker: 26 | def test_consume_message(self, mocked_worker): 27 | # When 28 | mocked_worker.consume_message() 29 | 30 | # Then 31 | assert mocked_worker._worker_channel.run.called 32 | 33 | def test_consume_one_message(self, mocked_worker): 34 | # When 35 | mocked_worker.consume_one_message() 36 | 37 | # Then 38 | channel = mocked_worker._worker_channel 39 | assert channel.open.called is True 40 | assert channel.consume_one_message.called is True 41 | assert channel.close.called is True 42 | -------------------------------------------------------------------------------- /tests/common.py: -------------------------------------------------------------------------------- 1 | from urllib import parse 2 | 3 | 4 | BROKER_MANAGEMENT_PORT = 15672 5 | BROKER_URL_TEST = 'amqp://guest:guest@localhost:5672/test-flask-rabbitmq' 6 | 7 | 8 | def get_rabbit_management_parsed_url(): 9 | parse.uses_netloc.append("amqp") 10 | parsed_url = parse.urlparse(BROKER_URL_TEST) 11 | 12 | port = BROKER_MANAGEMENT_PORT 13 | url = '{host}:{port}'.format(host=parsed_url.hostname, port=port) 14 | 15 | return { 16 | 'url': url, 17 | 'username': parsed_url.username, 18 | 'password': parsed_url.password, 19 | 'vhost': parsed_url.path[1:], 20 | } 21 | --------------------------------------------------------------------------------