├── .coveragerc ├── .flake8 ├── .github └── workflows │ ├── publish.yml │ └── testing.yml ├── .gitignore ├── .readthedocs.yaml ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── amqpstorm ├── __init__.py ├── base.py ├── basic.py ├── channel.py ├── channel0.py ├── compatibility.py ├── connection.py ├── exception.py ├── exchange.py ├── heartbeat.py ├── io.py ├── management │ ├── __init__.py │ ├── api.py │ ├── base.py │ ├── basic.py │ ├── channel.py │ ├── connection.py │ ├── exception.py │ ├── exchange.py │ ├── healthchecks.py │ ├── http_client.py │ ├── queue.py │ ├── user.py │ └── virtual_host.py ├── message.py ├── queue.py ├── rpc.py ├── tests │ ├── __init__.py │ ├── functional │ │ ├── __init__.py │ │ ├── management │ │ │ ├── __init__.py │ │ │ ├── test_api.py │ │ │ ├── test_basic.py │ │ │ ├── test_channel.py │ │ │ ├── test_connection.py │ │ │ ├── test_exchange.py │ │ │ ├── test_healthcheck.py │ │ │ ├── test_queue.py │ │ │ ├── test_user.py │ │ │ └── test_virtual_host.py │ │ ├── ssl │ │ │ ├── __init__.py │ │ │ └── test_reliability.py │ │ ├── test_basic.py │ │ ├── test_exchange.py │ │ ├── test_generic.py │ │ ├── test_legacy.py │ │ ├── test_queue.py │ │ ├── test_reliability.py │ │ ├── test_tx.py │ │ ├── test_web_based.py │ │ └── utility.py │ ├── resources │ │ └── ssl │ │ │ └── .empty │ ├── unit │ │ ├── __init__.py │ │ ├── base │ │ │ ├── __init__.py │ │ │ ├── test_base_channel.py │ │ │ └── test_stateful.py │ │ ├── basic │ │ │ ├── __init__.py │ │ │ ├── test_basic.py │ │ │ └── test_basic_exception.py │ │ ├── channel │ │ │ ├── __init__.py │ │ │ ├── test_channel.py │ │ │ ├── test_channel_exception.py │ │ │ ├── test_channel_frame.py │ │ │ └── test_channel_message_handling.py │ │ ├── channel0 │ │ │ ├── __init__.py │ │ │ ├── test_channel0.py │ │ │ └── test_channel0_frame.py │ │ ├── connection │ │ │ ├── __init__.py │ │ │ ├── test_connection.py │ │ │ └── test_connection_exception.py │ │ ├── exchange │ │ │ ├── __init__.py │ │ │ ├── test_exchange.py │ │ │ └── test_exchange_exception.py │ │ ├── io │ │ │ ├── __init__.py │ │ │ ├── test_io.py │ │ │ └── test_io_exception.py │ │ ├── management │ │ │ ├── __init__.py │ │ │ ├── test_api.py │ │ │ ├── test_basic.py │ │ │ ├── test_exception.py │ │ │ └── test_http_client.py │ │ ├── queue │ │ │ ├── __init__.py │ │ │ ├── test_queue_exception.py │ │ │ └── test_queuepy │ │ ├── test_compatibility.py │ │ ├── test_exception.py │ │ ├── test_heartbeat.py │ │ ├── test_message.py │ │ ├── test_rpc.py │ │ ├── test_tx.py │ │ └── uri_connection │ │ │ ├── __init__.py │ │ │ ├── test_uri_connection.py │ │ │ └── test_uri_connection_exception.py │ └── utility.py ├── tx.py └── uri_connection.py ├── doc-requirements.txt ├── docker ├── Dockerfile └── files │ ├── generate-certs │ ├── openssl.cnf │ ├── rabbitmq.conf │ └── wait-for-rabbitmq ├── docs ├── Makefile ├── _static │ └── override.css ├── _templates │ └── layout.html ├── api_usage │ ├── api.rst │ └── exception.rst ├── conf.py ├── examples │ ├── flask_rpc_client.rst │ ├── robust_consumer.rst │ ├── simple_consumer.rst │ ├── simple_publisher.rst │ ├── simple_rpc_client.rst │ ├── simple_rpc_server.rst │ └── ssl_with_context.rst ├── index.rst ├── make.bat ├── management_examples │ ├── aliveness_test.rst │ ├── create_user.rst │ ├── create_virtual_host.rst │ ├── declare_queue.rst │ ├── delete_queue.rst │ ├── does_queue_exist.rst │ ├── get_user.rst │ ├── list_queues.rst │ └── overview.rst ├── pool_examples │ ├── pool_example.rst │ └── ssl_pool_example.rst └── usage │ ├── channel.rst │ ├── connection.rst │ ├── exceptions.rst │ └── message.rst ├── examples ├── consume_queue_until_empty.py ├── create_queue_with_a_ttl_on_messages.py ├── flask_threaded_rpc_client.py ├── management │ ├── aliveness_test.py │ ├── create_user.py │ ├── create_virtual_host.py │ ├── declare_queue.py │ ├── delete_queue.py │ ├── delete_user.py │ ├── does_queue_exist.py │ ├── get_user.py │ ├── list_queues.py │ ├── overview.py │ └── whoami.py ├── pool │ ├── pool_example.py │ └── ssl_pool_example.py ├── publish_message_with_expiration.py ├── robust_consumer.py ├── scalable_consumer.py ├── scalable_rpc_server.py ├── simple_consumer.py ├── simple_generator_consumer.py ├── simple_publisher.py ├── simple_rpc_client.py ├── simple_rpc_server.py ├── simple_transaction_publisher.py └── ssl_with_context.py ├── requirements.txt ├── run_ci_locally.sh ├── setup.cfg ├── setup.py └── test-requirements.txt /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = amqpstorm 4 | omit = amqpstorm/tests/* 5 | parallel = True 6 | 7 | [report] 8 | ignore_errors = True 9 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = F821 3 | max-line-length = 120 -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '3.*' 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-20.04 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python 3.6 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: 3.6.15 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install flake8 23 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 24 | if [ -f test-requirements.txt ]; then pip install -r test-requirements.txt; fi 25 | - name: Lint with flake8 26 | run: | 27 | # stop the build if there are Python syntax errors or undefined names 28 | flake8 . 29 | - name: Install pypa/build 30 | run: >- 31 | python -m 32 | pip install 33 | build 34 | --user 35 | - name: Build a binary wheel and a source tarball 36 | run: >- 37 | python -m 38 | build 39 | --sdist 40 | --outdir dist/ 41 | . 42 | - name: Publish package 43 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 44 | uses: pypa/gh-action-pypi-publish@release/v1 45 | with: 46 | user: __token__ 47 | password: ${{ secrets.PYPI_API_TOKEN }} 48 | -------------------------------------------------------------------------------- /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: [ 2.x ] 7 | schedule: 8 | - cron: '0 13 * * 1' 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python-version: [3.11.9, 3.12.6, 3.13.0] 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 26 | if [ -f test-requirements.txt ]; then pip install -r test-requirements.txt; fi 27 | - name: Start RabbitMQ 28 | run: | 29 | docker build -t amqpstormdev ./docker/ 30 | docker run -d --hostname rmq.eandersson.net --name amqpstormdev -p 5671:5671 -p 5672:5672 -p 15671:15671 -p 15672:15672 amqpstormdev 31 | docker cp amqpstormdev:/etc/rabbitmq/ssl/ ./amqpstorm/tests/resources/ 32 | docker exec amqpstormdev wait-for-rabbitmq 33 | echo "RabbitMQ Version: $(docker exec amqpstormdev rabbitmqctl --version)" 34 | docker exec amqpstormdev rabbitmqctl add_user 'amqpstorm' '2a55f70a841f18b' 35 | docker exec amqpstormdev rabbitmqctl -p / set_permissions 'amqpstorm' '.*' '.*' '.*' 36 | docker exec amqpstormdev rabbitmqctl set_user_tags amqpstorm administrator 37 | nc -zv rmq.eandersson.net 5671 || exit 1 38 | nc -zv rmq.eandersson.net 5672 || exit 1 39 | nc -zv rmq.eandersson.net 15671 || exit 1 40 | nc -zv rmq.eandersson.net 15672 || exit 1 41 | - name: Lint with flake8 42 | run: | 43 | flake8 . 44 | - name: Test with pytest 45 | run: | 46 | pytest 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.bkp 4 | *.log 5 | *.class 6 | .idea 7 | __pycache__ -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version and other tools you might need 8 | build: 9 | os: ubuntu-24.04 10 | tools: 11 | python: "3.12" 12 | # You can also specify other tool versions: 13 | # nodejs: "20" 14 | # rust: "1.70" 15 | # golang: "1.20" 16 | 17 | # Build documentation in the "docs/" directory with Sphinx 18 | sphinx: 19 | configuration: docs/conf.py 20 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs 21 | # builder: "dirhtml" 22 | # Fail on all warnings to avoid broken references 23 | # fail_on_warning: true 24 | 25 | # Optionally build your docs in additional formats such as PDF and ePub 26 | # formats: 27 | # - pdf 28 | # - epub 29 | 30 | # Optional but recommended, declare the Python requirements required 31 | # to build your documentation 32 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 33 | python: 34 | install: 35 | - requirements: doc-requirements.txt 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2020 Erik Olof Gunnar Andersson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst LICENSE 2 | prune examples* 3 | prune resources* -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | AMQPStorm 2 | ========= 3 | Thread-safe Python RabbitMQ Client & Management library. 4 | 5 | |Version| 6 | 7 | Introduction 8 | ============ 9 | AMQPStorm is a library designed to be consistent, stable and thread-safe. 10 | 11 | - 100% Test Coverage! 12 | - Supports Python 2.7 and Python 3.6+. 13 | - Fully tested against Python Implementations; CPython and PyPy. 14 | 15 | Changelog 16 | ========= 17 | 18 | Version 2.11.1 19 | -------------- 20 | - Switch back to using select.select by default [#140]. 21 | 22 | Version 2.11.0 23 | -------------- 24 | - Added Python 3.13 support. 25 | - Replaced select.select with select.poll on Linux by default. 26 | - Replaced list with collections.deque - Thanks Bernhard Thiel. 27 | - Removed unnecessary lock when building messages. 28 | - Fixed various Python 2.7 tests. 29 | 30 | Version 2.10.8 31 | -------------- 32 | - Fixed bug with multi-threaded basic.consume not always being threadsafe [#132] - Thanks mic1on. 33 | 34 | Version 2.10.7 35 | -------------- 36 | - Fixed bug with heartbeat interval on the client not sent frequently enough [#127] - Thanks Ivan Héda. 37 | - Added support for Python 3.12. 38 | 39 | Version 2.10.6 40 | -------------- 41 | - Fixed deprecated warning when using Python 3.11. 42 | 43 | Version 2.10.5 44 | -------------- 45 | - Added support for bulk removing users using the Management Api. 46 | - Added support to get the Cluster name using the Management Api. 47 | - Fixed ConnectionUri to default to port 5761 when using ssl [#119] - Thanks s-at-ik. 48 | 49 | Version 2.10.4 50 | -------------- 51 | - Fixed issue with a forcefully closed channel not sending the appropriate response [#114] - Thanks Bernd Höhl. 52 | 53 | Version 2.10.3 54 | -------------- 55 | - Fixed install bug with cp1250 encoding on Windows [#112] - Thanks ZygusPatryk. 56 | 57 | Version 2.10.2 58 | -------------- 59 | - Fixed bad socket fd causing high cpu usage [#110] - Thanks aiden0z. 60 | 61 | Version 2.10.1 62 | -------------- 63 | - Fixed bug with UriConnection not handling amqps:// properly. 64 | - Improved documentation. 65 | 66 | Version 2.10.0 67 | -------------- 68 | - Added Pagination support to Management list calls (e.g. queues list). 69 | - Added Filtering support to Management list calls. 70 | - Re-use the requests sessions for Management calls. 71 | - Updated to use pytest framework instead of nose for testing. 72 | 73 | Version 2.9.0 74 | ------------- 75 | - Added support for custom Message implementations - Thanks Jay Hogg. 76 | - Fixed a bug with confirm_delivery not working after closing and re-opening an existing channel. 77 | - Re-worked the channel re-use code. 78 | 79 | Version 2.8.5 80 | ------------- 81 | - Fixed a potential deadlock when opening a channel with a broken connection [#97] - Thanks mehdigmira. 82 | 83 | Version 2.8.4 84 | ------------- 85 | - Fixed a bug in Message.create where it would mutate the properties dict [#92] - Thanks Killerama. 86 | 87 | Version 2.8.3 88 | ------------- 89 | - Fixed pip sdist circular dependency [#88] - Thanks Jay Hogg. 90 | - Fixed basic.consume argument type in documentation [#86] - Thanks TechmarkDavid. 91 | 92 | Version 2.8.2 93 | ------------- 94 | - Retry on SSLWantReadErrors [#82] - Thanks Bernhard Thiel. 95 | - Added getter/setter methods for Message properties expiration, message_type and user_id [#86] - Thanks Jay Hogg. 96 | 97 | Version 2.8.1 98 | ------------- 99 | - Cleaned up documentation. 100 | 101 | Version 2.8.0 102 | ------------- 103 | - Introduced a new channel function called check_for_exceptions. 104 | - Fixed issue where publish was successful but raises an error because connection was closed [#80] - Thanks Pavol Plaskon. 105 | - Updated SSL handling to use the non-deprecated way of creating a SSL Connection [#79] - Thanks Carl Hörberg from CloudAMQP. 106 | - Enabled SNI for SSL connections by default [#79] - Thanks Carl Hörberg from CloudAMQP. 107 | 108 | Version 2.7.2 109 | ------------- 110 | - Added ability to override client_properties [#77] - Thanks tkram01. 111 | 112 | Version 2.7.1 113 | ------------- 114 | - Fixed Connection close taking longer than intended when using SSL [#75]- Thanks troglas. 115 | - Fixed an issue with closing Channels taking too long after the server initiated it. 116 | 117 | Version 2.7.0 118 | ------------- 119 | - Added support for passing your own ssl context [#71] - Thanks troglas. 120 | - Improved logging verbosity on connection failures [#72] - Thanks troglas. 121 | - Fixed occasional error message when closing a SSL connection [#68] - Thanks troglas. 122 | 123 | Version 2.6.2 124 | ------------- 125 | - Set default TCP Timeout to 10s on UriConnection to match Connection [#67] - Thanks josemonteiro. 126 | - Internal RPC Timeout for Opening and Closing Connections are now set to a fixed 30s [#67] - Thanks josemonteiro. 127 | 128 | Version 2.6.1 129 | ------------- 130 | - Fixed minor issue with the last channel id not being available. 131 | 132 | Version 2.6.0 133 | ------------- 134 | - Re-use closed channel ids [#55] - Thanks mikemrm. 135 | - Changed Poller Timeout to be a constant. 136 | - Improved Connection Close performance. 137 | - Channels is now a publicly available variable in Connections. 138 | 139 | Version 2.5.0 140 | ------------- 141 | - Upgraded pamqp to v2.0.0. 142 | - Python 3 keys will now always be of type str. 143 | - For more information see https://pamqp.readthedocs.io/en/latest/history.html 144 | - Properly wait until the inbound queue is empty when break_on_empty is set [#63] - Thanks TomGudman. 145 | - Fixed issue with Management queue/exchange declare when the passive flag was set to True. 146 | 147 | Credits 148 | ======= 149 | Special thanks to gmr (Gavin M. Roy) for creating pamqp, and in addition amqpstorm is heavily influenced by his pika and rabbitpy libraries. 150 | 151 | .. |Version| image:: https://badge.fury.io/py/AMQPStorm.svg 152 | :target: https://badge.fury.io/py/AMQPStorm 153 | -------------------------------------------------------------------------------- /amqpstorm/__init__.py: -------------------------------------------------------------------------------- 1 | """AMQPStorm.""" 2 | __version__ = '2.11.1' # noqa 3 | __author__ = 'eandersson' # noqa 4 | 5 | import logging 6 | 7 | logging.getLogger('amqpstorm').addHandler(logging.NullHandler()) 8 | 9 | from amqpstorm.channel import Channel # noqa 10 | from amqpstorm.connection import Connection # noqa 11 | from amqpstorm.uri_connection import UriConnection # noqa 12 | from amqpstorm.message import Message # noqa 13 | from amqpstorm.exception import AMQPError # noqa 14 | from amqpstorm.exception import AMQPChannelError # noqa 15 | from amqpstorm.exception import AMQPMessageError # noqa 16 | from amqpstorm.exception import AMQPConnectionError # noqa 17 | from amqpstorm.exception import AMQPInvalidArgument # noqa 18 | -------------------------------------------------------------------------------- /amqpstorm/base.py: -------------------------------------------------------------------------------- 1 | """AMQPStorm Base.""" 2 | 3 | import locale 4 | 5 | from amqpstorm.compatibility import is_string 6 | from amqpstorm.exception import AMQPChannelError 7 | 8 | AUTH_MECHANISM = 'PLAIN' 9 | IDLE_WAIT = 0.01 10 | LOCALE = locale.getlocale()[0] or 'en_US' 11 | MAX_FRAME_SIZE = 131072 12 | MAX_CHANNELS = 65535 13 | 14 | 15 | class Stateful(object): 16 | """Stateful implementation.""" 17 | CLOSED = 0 18 | CLOSING = 1 19 | OPENING = 2 20 | OPEN = 3 21 | 22 | def __init__(self): 23 | self._state = self.CLOSED 24 | self._exceptions = [] 25 | 26 | def set_state(self, state): 27 | """Set State. 28 | 29 | :param int state: 30 | :return: 31 | """ 32 | self._state = state 33 | 34 | @property 35 | def current_state(self): 36 | """Get the State. 37 | 38 | :rtype: int 39 | """ 40 | return self._state 41 | 42 | @property 43 | def is_closed(self): 44 | """Is Closed? 45 | 46 | :rtype: bool 47 | """ 48 | return self._state == self.CLOSED 49 | 50 | @property 51 | def is_closing(self): 52 | """Is Closing? 53 | 54 | :rtype: bool 55 | """ 56 | return self._state == self.CLOSING 57 | 58 | @property 59 | def is_opening(self): 60 | """Is Opening? 61 | 62 | :rtype: bool 63 | """ 64 | return self._state == self.OPENING 65 | 66 | @property 67 | def is_open(self): 68 | """Is Open? 69 | 70 | :rtype: bool 71 | """ 72 | return self._state == self.OPEN 73 | 74 | @property 75 | def exceptions(self): 76 | """Stores all exceptions thrown by this instance. 77 | 78 | This is useful for troubleshooting, and is used internally 79 | to check the health of the connection. 80 | 81 | :rtype: list 82 | """ 83 | return self._exceptions 84 | 85 | 86 | class BaseChannel(Stateful): 87 | """Channel base class.""" 88 | __slots__ = [ 89 | '_channel_id', '_consumer_tags' 90 | ] 91 | 92 | def __init__(self, channel_id): 93 | super(BaseChannel, self).__init__() 94 | self._consumer_tags = [] 95 | self._channel_id = channel_id 96 | 97 | @property 98 | def channel_id(self): 99 | """Get Channel id. 100 | 101 | :rtype: int 102 | """ 103 | return self._channel_id 104 | 105 | @property 106 | def consumer_tags(self): 107 | """Get a list of consumer tags. 108 | 109 | :rtype: list 110 | """ 111 | return self._consumer_tags 112 | 113 | def add_consumer_tag(self, tag): 114 | """Add a Consumer tag. 115 | 116 | :param str tag: Consumer tag. 117 | :return: 118 | """ 119 | if not is_string(tag): 120 | raise AMQPChannelError('consumer tag needs to be a string') 121 | if tag not in self._consumer_tags: 122 | self._consumer_tags.append(tag) 123 | 124 | def remove_consumer_tag(self, tag=None): 125 | """Remove a Consumer tag. 126 | 127 | If no tag is specified, all all tags will be removed. 128 | 129 | :param str,None tag: Consumer tag. 130 | :return: 131 | """ 132 | if tag is not None: 133 | if tag in self._consumer_tags: 134 | self._consumer_tags.remove(tag) 135 | else: 136 | self._consumer_tags = [] 137 | 138 | 139 | class BaseMessage(object): 140 | """Message base class. 141 | 142 | :param Channel channel: AMQPStorm Channel 143 | :param str,unicode body: Message body 144 | :param dict method: Message method 145 | :param dict properties: Message properties 146 | :param bool auto_decode: This is not implemented in the base message class. 147 | """ 148 | __slots__ = [ 149 | '_auto_decode', '_body', '_channel', '_method', '_properties' 150 | ] 151 | 152 | def __init__(self, channel, body=None, method=None, properties=None, 153 | auto_decode=None): 154 | self._auto_decode = auto_decode 155 | self._channel = channel 156 | self._body = body 157 | self._method = method 158 | self._properties = properties or {} 159 | 160 | def __iter__(self): 161 | for attribute in ['_body', '_channel', '_method', '_properties']: 162 | yield attribute[1::], getattr(self, attribute) 163 | 164 | def to_dict(self): 165 | """Message to Dictionary. 166 | 167 | :rtype: dict 168 | """ 169 | return { 170 | 'body': self._body, 171 | 'method': self._method, 172 | 'properties': self._properties, 173 | 'channel': self._channel 174 | } 175 | 176 | def to_tuple(self): 177 | """Message to Tuple. 178 | 179 | :rtype: tuple 180 | """ 181 | return self._body, self._channel, self._method, self._properties 182 | 183 | 184 | class Handler(object): 185 | """Operations Handler (e.g. Queue, Exchange)""" 186 | __slots__ = [ 187 | '_channel' 188 | ] 189 | 190 | def __init__(self, channel): 191 | self._channel = channel 192 | -------------------------------------------------------------------------------- /amqpstorm/compatibility.py: -------------------------------------------------------------------------------- 1 | """Python 2/3 Compatibility layer.""" 2 | 3 | import sys 4 | 5 | try: 6 | import ssl 7 | except ImportError: 8 | ssl = None 9 | 10 | try: 11 | import simplejson as json # noqa 12 | except ImportError: 13 | import json # noqa 14 | 15 | try: 16 | import urlparse # noqa 17 | except ImportError: 18 | import urllib.parse as urlparse # noqa 19 | 20 | try: 21 | from urllib import quote # noqa 22 | except ImportError: 23 | from urllib.parse import quote # noqa 24 | 25 | PYTHON3 = sys.version_info >= (3, 0, 0) 26 | 27 | if PYTHON3: 28 | RANGE = range 29 | else: 30 | RANGE = xrange 31 | 32 | 33 | class DummyException(Exception): 34 | """ 35 | Never raised by anything. 36 | 37 | This is used in except blocks if the intended 38 | exception cannot be imported. 39 | """ 40 | 41 | 42 | SSL_CERT_MAP = {} 43 | SSL_VERSIONS = {} 44 | SSL_OPTIONS = [ 45 | 'keyfile', 46 | 'certfile', 47 | 'cert_reqs', 48 | 'ssl_version', 49 | 'ca_certs', 50 | 'server_hostname', 51 | ] 52 | 53 | 54 | def get_default_ssl_version(): 55 | """Get the highest support TLS version, if none is available, return None. 56 | 57 | :rtype: bool,None 58 | """ 59 | if hasattr(ssl, 'PROTOCOL_TLSv1_2'): 60 | return ssl.PROTOCOL_TLSv1_2 61 | elif hasattr(ssl, 'PROTOCOL_TLSv1_1'): 62 | return ssl.PROTOCOL_TLSv1_1 63 | elif hasattr(ssl, 'PROTOCOL_TLSv1'): 64 | return ssl.PROTOCOL_TLSv1 65 | return None 66 | 67 | 68 | DEFAULT_SSL_VERSION = get_default_ssl_version() 69 | SSL_SUPPORTED = DEFAULT_SSL_VERSION is not None 70 | if SSL_SUPPORTED: 71 | if hasattr(ssl, 'PROTOCOL_TLSv1_2'): 72 | SSL_VERSIONS['protocol_tlsv1_2'] = ssl.PROTOCOL_TLSv1_2 73 | if hasattr(ssl, 'PROTOCOL_TLSv1_1'): 74 | SSL_VERSIONS['protocol_tlsv1_1'] = ssl.PROTOCOL_TLSv1_1 75 | SSL_VERSIONS['protocol_tlsv1'] = ssl.PROTOCOL_TLSv1 76 | 77 | SSL_CERT_MAP = { 78 | 'cert_none': ssl.CERT_NONE, 79 | 'cert_optional': ssl.CERT_OPTIONAL, 80 | 'cert_required': ssl.CERT_REQUIRED 81 | } 82 | SSLWantReadError = ssl.SSLWantReadError 83 | else: 84 | SSLWantReadError = DummyException 85 | 86 | 87 | def is_string(obj): 88 | """Is this a string. 89 | 90 | :param object obj: 91 | :rtype: bool 92 | """ 93 | if PYTHON3: 94 | str_type = (bytes, str) 95 | else: 96 | str_type = (bytes, str, unicode) 97 | return isinstance(obj, str_type) 98 | 99 | 100 | def is_integer(obj): 101 | """Is this an integer. 102 | 103 | :param object obj: 104 | :return: 105 | """ 106 | if PYTHON3: 107 | return isinstance(obj, int) 108 | return isinstance(obj, (int, long)) 109 | 110 | 111 | def is_unicode(obj): 112 | """Is this a unicode string. 113 | 114 | This always returns False if running Python 3.x. 115 | 116 | :param object obj: 117 | :rtype: bool 118 | """ 119 | if PYTHON3: 120 | return False 121 | return isinstance(obj, unicode) 122 | 123 | 124 | def try_utf8_decode(value): 125 | """Try to decode an object. 126 | 127 | :param value: 128 | :return: 129 | """ 130 | if not value or not is_string(value): 131 | return value 132 | elif PYTHON3 and not isinstance(value, bytes): 133 | return value 134 | elif not PYTHON3 and not isinstance(value, unicode): 135 | return value 136 | 137 | try: 138 | return value.decode('utf-8') 139 | except UnicodeDecodeError: 140 | pass 141 | 142 | return value 143 | 144 | 145 | def patch_uri(uri): 146 | """If a custom uri schema is used with python 2.6 (e.g. amqps), 147 | it will ignore some of the parsing logic. 148 | 149 | As a work-around for this we change the amqp/amqps schema 150 | internally to use http/https. 151 | 152 | :param str uri: AMQP Connection string 153 | :rtype: str 154 | """ 155 | index = uri.find(':') 156 | if uri[:index] == 'amqps': 157 | uri = uri.replace('amqps', 'https', 1) 158 | elif uri[:index] == 'amqp': 159 | uri = uri.replace('amqp', 'http', 1) 160 | return uri 161 | -------------------------------------------------------------------------------- /amqpstorm/exception.py: -------------------------------------------------------------------------------- 1 | """AMQPStorm Exception.""" 2 | 3 | AMQP_ERROR_MAPPING = { 4 | 311: ('CONTENT-TOO-LARGE', 5 | 'The client attempted to transfer content larger than the ' 6 | 'server could accept at the present time. The client may ' 7 | 'retry at a later time.'), 8 | 312: ('NO-ROUTE', 'Undocumented AMQP Soft Error'), 9 | 313: ('NO-CONSUMERS', 10 | 'When the exchange cannot deliver to a consumer when the ' 11 | 'immediate flag is set. As a result of pending data on ' 12 | 'the queue or the absence of any consumers of the queue.'), 13 | 320: ('CONNECTION-FORCED', 14 | 'An operator intervened to close the connection for some reason. ' 15 | 'The client may retry at some later date.'), 16 | 402: ('INVALID-PATH', 17 | 'The client tried to work with an unknown virtual host.'), 18 | 403: ('ACCESS-REFUSED', 19 | 'The client attempted to work with a server entity to which ' 20 | 'has no access due to security settings.'), 21 | 404: ('NOT-FOUND', 22 | 'The client attempted to work with a server ' 23 | 'entity that does not exist.'), 24 | 405: ('RESOURCE-LOCKED', 25 | 'The client attempted to work with a server entity to which it ' 26 | 'has no access because another client is working with it.'), 27 | 406: ('PRECONDITION-FAILED', 28 | 'The client requested a method that was not ' 29 | 'allowed because some precondition failed.'), 30 | 501: ('FRAME-ERROR', 31 | 'The sender sent a malformed frame that the recipient could ' 32 | 'not decode. This strongly implies a programming error in ' 33 | 'the sending peer.'), 34 | 502: ('SYNTAX-ERROR', 35 | 'The sender sent a frame that contained illegal values for ' 36 | 'one or more fields. This strongly implies a programming ' 37 | 'error in the sending peer.'), 38 | 503: ('COMMAND-INVALID', 39 | 'The client sent an invalid sequence of frames, attempting to ' 40 | 'perform an operation that was considered invalid by the server. ' 41 | 'This usually implies a programming error in the client.'), 42 | 504: ('CHANNEL-ERROR', 43 | 'The client attempted to work with a channel that had not ' 44 | 'been correctly opened. This most likely indicates a ' 45 | 'fault in the client layer.'), 46 | 505: ('UNEXPECTED-FRAME', 47 | 'The peer sent a frame that was not expected, usually in the ' 48 | 'context of a content header and body. This strongly ' 49 | 'indicates a fault in the peer\'s content processing.'), 50 | 506: ('RESOURCE-ERROR', 51 | 'The server could not complete the method because it lacked ' 52 | 'sufficient resources. This may be due to the client ' 53 | 'creating too many of some type of entity.'), 54 | 530: ('NOT-ALLOWED', 55 | 'The client tried to work with some entity in a manner ' 56 | 'that is prohibited by the server, due to security ' 57 | 'settings or by some other criteria.'), 58 | 540: ('NOT-IMPLEMENTED', 59 | 'The client tried to use functionality that is ' 60 | 'notimplemented in the server.'), 61 | 541: ('INTERNAL-ERROR', 62 | 'The server could not complete the method because of an ' 63 | 'internal error. The server may require intervention by ' 64 | 'an operator in order to resume normal operations.') 65 | } 66 | 67 | 68 | class AMQPError(IOError): 69 | """General AMQP Error. 70 | 71 | Exceptions raised by AMQPStorm are mapped based to the 72 | AMQP 0.9.1 specifications (when applicable). 73 | 74 | e.g. 75 | :: 76 | 77 | except AMQPChannelError as why: 78 | if why.error_code == 312: 79 | self.channel.queue.declare(queue_name) 80 | """ 81 | _documentation = None 82 | _error_code = None 83 | _error_type = None 84 | 85 | @property 86 | def documentation(self): 87 | """AMQP Documentation string.""" 88 | return self._documentation or bytes() 89 | 90 | @property 91 | def error_code(self): 92 | """AMQP Error Code - A 3-digit reply code.""" 93 | return self._error_code 94 | 95 | @property 96 | def error_type(self): 97 | """AMQP Error Type e.g. NOT-FOUND.""" 98 | return self._error_type 99 | 100 | def __init__(self, *args, **kwargs): 101 | self._error_code = kwargs.pop('reply_code', None) 102 | super(AMQPError, self).__init__(*args, **kwargs) 103 | if self._error_code not in AMQP_ERROR_MAPPING: 104 | return 105 | self._error_type = AMQP_ERROR_MAPPING[self._error_code][0] 106 | self._documentation = AMQP_ERROR_MAPPING[self._error_code][1] 107 | 108 | 109 | class AMQPConnectionError(AMQPError): 110 | """AMQP Connection Error.""" 111 | pass 112 | 113 | 114 | class AMQPChannelError(AMQPError): 115 | """AMQP Channel Error.""" 116 | pass 117 | 118 | 119 | class AMQPMessageError(AMQPChannelError): 120 | """AMQP Message Error.""" 121 | pass 122 | 123 | 124 | class AMQPInvalidArgument(AMQPError): 125 | """AMQP Argument Error.""" 126 | -------------------------------------------------------------------------------- /amqpstorm/heartbeat.py: -------------------------------------------------------------------------------- 1 | """AMQPStorm Connection.Heartbeat.""" 2 | 3 | import logging 4 | import threading 5 | 6 | from amqpstorm.exception import AMQPConnectionError 7 | 8 | LOGGER = logging.getLogger(__name__) 9 | 10 | 11 | class Heartbeat(object): 12 | """Internal Heartbeat handler.""" 13 | 14 | def __init__(self, timeout, send_heartbeat_impl, timer=threading.Timer): 15 | self.send_heartbeat_impl = send_heartbeat_impl 16 | self.timer_impl = timer 17 | self._lock = threading.Lock() 18 | self._running = threading.Event() 19 | self._timer = None 20 | self._exceptions = None 21 | self._reads_since_check = 0 22 | self._writes_since_check = 0 23 | self._interval = None if timeout is None else max(timeout / 2, 0) 24 | self._threshold = 0 25 | 26 | def register_read(self): 27 | """Register that a frame has been received. 28 | 29 | :return: 30 | """ 31 | self._reads_since_check += 1 32 | 33 | def register_write(self): 34 | """Register that a frame has been sent. 35 | 36 | :return: 37 | """ 38 | self._writes_since_check += 1 39 | 40 | def start(self, exceptions): 41 | """Start the Heartbeat Checker. 42 | 43 | :param list exceptions: 44 | :return: 45 | """ 46 | if not self._interval: 47 | return False 48 | self._running.set() 49 | with self._lock: 50 | self._threshold = 0 51 | self._reads_since_check = 0 52 | self._writes_since_check = 0 53 | self._exceptions = exceptions 54 | LOGGER.debug('Heartbeat Checker Started') 55 | return self._start_new_timer() 56 | 57 | def stop(self): 58 | """Stop the Heartbeat Checker. 59 | 60 | :return: 61 | """ 62 | self._running.clear() 63 | with self._lock: 64 | if self._timer: 65 | self._timer.cancel() 66 | self._timer = None 67 | 68 | def _check_for_life_signs(self): 69 | """Check Connection for life signs. 70 | 71 | First check if any data has been sent, if not send a heartbeat 72 | to the remote server. 73 | 74 | If we have not received any data what so ever within two 75 | intervals, we need to raise an exception so that we can 76 | close the connection. 77 | 78 | :rtype: bool 79 | """ 80 | if not self._running.is_set(): 81 | return False 82 | if self._writes_since_check == 0: 83 | self.send_heartbeat_impl() 84 | self._lock.acquire() 85 | try: 86 | if self._reads_since_check == 0: 87 | self._threshold += 1 88 | if self._threshold >= 2: 89 | self._running.clear() 90 | self._raise_or_append_exception() 91 | return False 92 | else: 93 | self._threshold = 0 94 | finally: 95 | self._reads_since_check = 0 96 | self._writes_since_check = 0 97 | self._lock.release() 98 | 99 | return self._start_new_timer() 100 | 101 | def _raise_or_append_exception(self): 102 | """The connection is presumably dead and we need to raise or 103 | append an exception. 104 | 105 | If we have a list for exceptions, append the exception and let 106 | the connection handle it, if not raise the exception here. 107 | 108 | :return: 109 | """ 110 | message = ( 111 | 'Connection dead, no heartbeat or data received in >= ' 112 | '%ds' % ( 113 | self._interval * 2 114 | ) 115 | ) 116 | why = AMQPConnectionError(message) 117 | if self._exceptions is None: 118 | raise why 119 | self._exceptions.append(why) 120 | 121 | def _start_new_timer(self): 122 | """Create a timer that will be used to periodically check the 123 | connection for heartbeats. 124 | 125 | :return: 126 | """ 127 | if not self._running.is_set(): 128 | return False 129 | self._timer = self.timer_impl( 130 | interval=self._interval, 131 | function=self._check_for_life_signs 132 | ) 133 | self._timer.daemon = True 134 | self._timer.start() 135 | return True 136 | -------------------------------------------------------------------------------- /amqpstorm/management/__init__.py: -------------------------------------------------------------------------------- 1 | from amqpstorm.management.api import ManagementApi # noqa 2 | from amqpstorm.management.exception import ApiConnectionError # noqa 3 | from amqpstorm.management.exception import ApiError # noqa 4 | -------------------------------------------------------------------------------- /amqpstorm/management/base.py: -------------------------------------------------------------------------------- 1 | class ManagementHandler(object): 2 | """Management Api Operations Handler (e.g. Queue, Exchange)""" 3 | 4 | def __init__(self, http_client): 5 | self.http_client = http_client 6 | -------------------------------------------------------------------------------- /amqpstorm/management/basic.py: -------------------------------------------------------------------------------- 1 | from amqpstorm.compatibility import json 2 | from amqpstorm.compatibility import quote 3 | from amqpstorm.management.base import ManagementHandler 4 | from amqpstorm.message import Message 5 | 6 | API_BASIC_GET_MESSAGE = 'queues/%s/%s/get' 7 | API_BASIC_PUBLISH = 'exchanges/%s/%s/publish' 8 | 9 | 10 | class Basic(ManagementHandler): 11 | def publish(self, body, routing_key, exchange='amq.default', 12 | virtual_host='/', properties=None, payload_encoding='string'): 13 | """Publish a Message. 14 | 15 | :param bytes,str,unicode body: Message payload 16 | :param str routing_key: Message routing key 17 | :param str exchange: The exchange to publish the message to 18 | :param str virtual_host: Virtual host name 19 | :param dict properties: Message properties 20 | :param str payload_encoding: Payload encoding. 21 | 22 | :raises ApiError: Raises if the remote server encountered an error. 23 | :raises ApiConnectionError: Raises if there was a connectivity issue. 24 | 25 | :rtype: dict 26 | """ 27 | exchange = quote(exchange, '') 28 | properties = properties or {} 29 | body = json.dumps( 30 | { 31 | 'routing_key': routing_key, 32 | 'payload': body, 33 | 'payload_encoding': payload_encoding, 34 | 'properties': properties, 35 | 'vhost': virtual_host 36 | } 37 | ) 38 | virtual_host = quote(virtual_host, '') 39 | return self.http_client.post(API_BASIC_PUBLISH % 40 | ( 41 | virtual_host, 42 | exchange), 43 | payload=body) 44 | 45 | def get(self, queue, virtual_host='/', requeue=False, to_dict=False, 46 | count=1, truncate=50000, encoding='auto'): 47 | """Get Messages. 48 | 49 | :param str queue: Queue name 50 | :param str virtual_host: Virtual host name 51 | :param bool requeue: Re-queue message 52 | :param bool to_dict: Should incoming messages be converted to a 53 | dictionary before delivery. 54 | :param int count: How many messages should we try to fetch. 55 | :param int truncate: The maximum length in bytes, beyond that the 56 | server will truncate the message. 57 | :param str encoding: Message encoding. 58 | 59 | :raises ApiError: Raises if the remote server encountered an error. 60 | :raises ApiConnectionError: Raises if there was a connectivity issue. 61 | 62 | :rtype: list 63 | """ 64 | ackmode = 'ack_requeue_false' 65 | if requeue: 66 | ackmode = 'ack_requeue_true' 67 | 68 | get_messages = json.dumps( 69 | { 70 | 'count': count, 71 | 'requeue': requeue, 72 | 'ackmode': ackmode, 73 | 'encoding': encoding, 74 | 'truncate': truncate, 75 | 'vhost': virtual_host 76 | } 77 | ) 78 | virtual_host = quote(virtual_host, '') 79 | response = self.http_client.post(API_BASIC_GET_MESSAGE % 80 | ( 81 | virtual_host, 82 | queue 83 | ), 84 | payload=get_messages) 85 | if to_dict: 86 | return response 87 | messages = [] 88 | for message in response: 89 | body = message.get('body') 90 | if not body: 91 | body = message.get('payload') 92 | messages.append(Message( 93 | channel=None, 94 | body=body, 95 | properties=message.get('properties'), 96 | auto_decode=True, 97 | )) 98 | return messages 99 | -------------------------------------------------------------------------------- /amqpstorm/management/channel.py: -------------------------------------------------------------------------------- 1 | from amqpstorm.management.base import ManagementHandler 2 | 3 | API_CHANNEL = 'channels/%s' 4 | API_CHANNELS = 'channels' 5 | 6 | 7 | class Channel(ManagementHandler): 8 | def get(self, channel): 9 | """Get Connection details. 10 | 11 | :param channel: Channel name 12 | 13 | :raises ApiError: Raises if the remote server encountered an error. 14 | We also raise an exception if the channel cannot 15 | be found. 16 | :raises ApiConnectionError: Raises if there was a connectivity issue. 17 | 18 | :rtype: dict 19 | """ 20 | return self.http_client.get(API_CHANNEL % channel) 21 | 22 | def list(self, name=None, page_size=None, use_regex=False): 23 | """List all Channels. 24 | 25 | :param name: Filter by name 26 | :param use_regex: Enables regular expression for the param name 27 | :param page_size: Number of elements per page 28 | 29 | :raises ApiError: Raises if the remote server encountered an error. 30 | :raises ApiConnectionError: Raises if there was a connectivity issue. 31 | 32 | :rtype: list 33 | """ 34 | return self.http_client.list( 35 | API_CHANNELS, 36 | name=name, use_regex=use_regex, page_size=page_size, 37 | ) 38 | -------------------------------------------------------------------------------- /amqpstorm/management/connection.py: -------------------------------------------------------------------------------- 1 | from amqpstorm.compatibility import json 2 | from amqpstorm.compatibility import quote 3 | from amqpstorm.management.base import ManagementHandler 4 | 5 | API_CONNECTION = 'connections/%s' 6 | API_CONNECTIONS = 'connections' 7 | 8 | 9 | class Connection(ManagementHandler): 10 | def get(self, connection): 11 | """Get Connection details. 12 | 13 | :param str connection: Connection name 14 | 15 | :raises ApiError: Raises if the remote server encountered an error. 16 | We also raise an exception if the connection cannot 17 | be found. 18 | :raises ApiConnectionError: Raises if there was a connectivity issue. 19 | 20 | :rtype: dict 21 | """ 22 | return self.http_client.get(API_CONNECTION % connection) 23 | 24 | def list(self, name=None, page_size=100, use_regex=False): 25 | """Get Connections. 26 | 27 | :param name: Filter by name 28 | :param use_regex: Enables regular expression for the param name 29 | :param page_size: Number of elements per page 30 | 31 | :raises ApiError: Raises if the remote server encountered an error. 32 | :raises ApiConnectionError: Raises if there was a connectivity issue. 33 | 34 | :rtype: list 35 | """ 36 | return self.http_client.list( 37 | API_CONNECTIONS, 38 | name=name, use_regex=use_regex, page_size=page_size, 39 | ) 40 | 41 | def close(self, connection, reason='Closed via management api'): 42 | """Close Connection. 43 | 44 | :param str connection: Connection name 45 | :param str reason: Reason for closing connection. 46 | 47 | :raises ApiError: Raises if the remote server encountered an error. 48 | :raises ApiConnectionError: Raises if there was a connectivity issue. 49 | 50 | :rtype: None 51 | """ 52 | close_payload = json.dumps({ 53 | 'name': connection, 54 | 'reason': reason 55 | }) 56 | connection = quote(connection, '') 57 | return self.http_client.delete(API_CONNECTION % connection, 58 | payload=close_payload, 59 | headers={ 60 | 'X-Reason': reason 61 | }) 62 | -------------------------------------------------------------------------------- /amqpstorm/management/exception.py: -------------------------------------------------------------------------------- 1 | from amqpstorm.exception import AMQPError 2 | from amqpstorm.exception import AMQP_ERROR_MAPPING 3 | 4 | 5 | class ApiError(AMQPError): 6 | """Management Api Error""" 7 | 8 | def __init__(self, message=None, *args, **kwargs): 9 | self._message = message 10 | self._error_code = kwargs.pop('reply_code', None) 11 | super(AMQPError, self).__init__(*args, **kwargs) 12 | if self._error_code not in AMQP_ERROR_MAPPING: 13 | return 14 | self._error_type = AMQP_ERROR_MAPPING[self._error_code][0] 15 | self._documentation = AMQP_ERROR_MAPPING[self._error_code][1] 16 | 17 | def __str__(self): 18 | if self._error_code in AMQP_ERROR_MAPPING: 19 | return '%s - %s' % (self.error_type, self.documentation) 20 | return self._message 21 | 22 | 23 | class ApiConnectionError(AMQPError): 24 | """Management Api Connection Error""" 25 | pass 26 | -------------------------------------------------------------------------------- /amqpstorm/management/healthchecks.py: -------------------------------------------------------------------------------- 1 | from amqpstorm.management.base import ManagementHandler 2 | 3 | HEALTHCHECKS = 'healthchecks/node/' 4 | HEALTHCHECKS_NODE = 'healthchecks/node/%s' 5 | 6 | 7 | class HealthChecks(ManagementHandler): 8 | def get(self, node=None): 9 | """Run basic healthchecks against the current node, or against a given 10 | node. 11 | 12 | Example response: 13 | > {"status":"ok"} 14 | > {"status":"failed","reason":"string"} 15 | 16 | :param node: Node name 17 | 18 | :raises ApiError: Raises if the remote server encountered an error. 19 | :raises ApiConnectionError: Raises if there was a connectivity issue. 20 | 21 | :rtype: dict 22 | """ 23 | if not node: 24 | return self.http_client.get(HEALTHCHECKS) 25 | return self.http_client.get(HEALTHCHECKS_NODE % node) 26 | -------------------------------------------------------------------------------- /amqpstorm/management/user.py: -------------------------------------------------------------------------------- 1 | from amqpstorm.compatibility import json 2 | from amqpstorm.compatibility import quote 3 | from amqpstorm.management.base import ManagementHandler 4 | 5 | API_USER = 'users/%s' 6 | API_USER_PERMISSIONS = 'users/%s/permissions' 7 | API_USER_VIRTUAL_HOST_PERMISSIONS = 'permissions/%s/%s' 8 | API_USERS = 'users' 9 | API_USERS_BULK_DELETE = 'users/bulk-delete' 10 | 11 | 12 | class User(ManagementHandler): 13 | def get(self, username): 14 | """Get User details. 15 | 16 | :param str username: Username 17 | 18 | :raises ApiError: Raises if the remote server encountered an error. 19 | We also raise an exception if the user cannot 20 | be found. 21 | :raises ApiConnectionError: Raises if there was a connectivity issue. 22 | 23 | :rtype: dict 24 | """ 25 | return self.http_client.get(API_USER % username) 26 | 27 | def list(self): 28 | """List all Users. 29 | 30 | :rtype: list 31 | """ 32 | return self.http_client.get(API_USERS) 33 | 34 | def create(self, username, password, tags=''): 35 | """Create User. 36 | 37 | :param str username: Username 38 | :param str password: Password 39 | :param str,list tags: Comma-separate list of tags (e.g. monitoring) 40 | 41 | :raises ApiError: Raises if the remote server encountered an error. 42 | :raises ApiConnectionError: Raises if there was a connectivity issue. 43 | 44 | :rtype: None 45 | """ 46 | user_payload = json.dumps({ 47 | 'password': password, 48 | 'tags': tags 49 | }) 50 | return self.http_client.put(API_USER % username, 51 | payload=user_payload) 52 | 53 | def delete(self, username): 54 | """Delete User or a list of Users. 55 | 56 | :param str,list username: Username or a list of Usernames 57 | 58 | :raises ApiError: Raises if the remote server encountered an error. 59 | :raises ApiConnectionError: Raises if there was a connectivity issue. 60 | 61 | :rtype: dict 62 | """ 63 | if isinstance(username, list): 64 | return self.http_client.post( 65 | API_USERS_BULK_DELETE, 66 | payload=json.dumps({'users': username}) 67 | ) 68 | return self.http_client.delete(API_USER % username) 69 | 70 | def get_permission(self, username, virtual_host): 71 | """Get User permissions for the configured virtual host. 72 | 73 | :param str username: Username 74 | :param str virtual_host: Virtual host name 75 | 76 | :raises ApiError: Raises if the remote server encountered an error. 77 | :raises ApiConnectionError: Raises if there was a connectivity issue. 78 | 79 | :rtype: dict 80 | """ 81 | virtual_host = quote(virtual_host, '') 82 | return self.http_client.get(API_USER_VIRTUAL_HOST_PERMISSIONS % 83 | ( 84 | virtual_host, 85 | username 86 | )) 87 | 88 | def get_permissions(self, username): 89 | """Get all Users permissions. 90 | 91 | :param str username: Username 92 | 93 | :raises ApiError: Raises if the remote server encountered an error. 94 | :raises ApiConnectionError: Raises if there was a connectivity issue. 95 | 96 | :rtype: dict 97 | """ 98 | return self.http_client.get(API_USER_PERMISSIONS % 99 | ( 100 | username 101 | )) 102 | 103 | def set_permission(self, username, virtual_host, configure_regex='.*', 104 | write_regex='.*', read_regex='.*'): 105 | """Set User permissions for the configured virtual host. 106 | 107 | :param str username: Username 108 | :param str virtual_host: Virtual host name 109 | :param str configure_regex: Permission pattern for configuration 110 | operations for this user. 111 | :param str write_regex: Permission pattern for write operations 112 | for this user. 113 | :param str read_regex: Permission pattern for read operations 114 | for this user. 115 | 116 | :raises ApiError: Raises if the remote server encountered an error. 117 | :raises ApiConnectionError: Raises if there was a connectivity issue. 118 | 119 | :rtype: dict 120 | """ 121 | virtual_host = quote(virtual_host, '') 122 | permission_payload = json.dumps({ 123 | "configure": configure_regex, 124 | "read": read_regex, 125 | "write": write_regex 126 | }) 127 | return self.http_client.put(API_USER_VIRTUAL_HOST_PERMISSIONS % 128 | ( 129 | virtual_host, 130 | username 131 | ), 132 | payload=permission_payload) 133 | 134 | def delete_permission(self, username, virtual_host): 135 | """Delete User permissions for the configured virtual host. 136 | 137 | :param str username: Username 138 | :param str virtual_host: Virtual host name 139 | 140 | :raises ApiError: Raises if the remote server encountered an error. 141 | :raises ApiConnectionError: Raises if there was a connectivity issue. 142 | 143 | :rtype: dict 144 | """ 145 | virtual_host = quote(virtual_host, '') 146 | return self.http_client.delete( 147 | API_USER_VIRTUAL_HOST_PERMISSIONS % 148 | ( 149 | virtual_host, 150 | username 151 | )) 152 | -------------------------------------------------------------------------------- /amqpstorm/management/virtual_host.py: -------------------------------------------------------------------------------- 1 | from amqpstorm.compatibility import quote 2 | from amqpstorm.management.base import ManagementHandler 3 | 4 | API_VIRTUAL_HOST = 'vhosts/%s' 5 | API_VIRTUAL_HOSTS = 'vhosts' 6 | API_VIRTUAL_HOSTS_PERMISSION = 'vhosts/%s/permissions' 7 | 8 | 9 | class VirtualHost(ManagementHandler): 10 | def get(self, virtual_host): 11 | """Get Virtual Host details. 12 | 13 | :param str virtual_host: Virtual host name 14 | 15 | :raises ApiError: Raises if the remote server encountered an error. 16 | We also raise an exception if the virtual host cannot 17 | be found. 18 | :raises ApiConnectionError: Raises if there was a connectivity issue. 19 | 20 | :rtype: dict 21 | """ 22 | virtual_host = quote(virtual_host, '') 23 | return self.http_client.get(API_VIRTUAL_HOST % virtual_host) 24 | 25 | def list(self): 26 | """List all Virtual Hosts. 27 | 28 | :raises ApiError: Raises if the remote server encountered an error. 29 | :raises ApiConnectionError: Raises if there was a connectivity issue. 30 | 31 | :rtype: list 32 | """ 33 | return self.http_client.get(API_VIRTUAL_HOSTS) 34 | 35 | def create(self, virtual_host): 36 | """Create a Virtual Host. 37 | 38 | :param str virtual_host: Virtual host name 39 | 40 | :raises ApiError: Raises if the remote server encountered an error. 41 | :raises ApiConnectionError: Raises if there was a connectivity issue. 42 | 43 | :rtype: dict 44 | """ 45 | virtual_host = quote(virtual_host, '') 46 | return self.http_client.put(API_VIRTUAL_HOST % virtual_host) 47 | 48 | def delete(self, virtual_host): 49 | """Delete a Virtual Host. 50 | 51 | :param str virtual_host: Virtual host name 52 | 53 | :raises ApiError: Raises if the remote server encountered an error. 54 | :raises ApiConnectionError: Raises if there was a connectivity issue. 55 | 56 | :rtype: dict 57 | """ 58 | virtual_host = quote(virtual_host, '') 59 | return self.http_client.delete(API_VIRTUAL_HOST % virtual_host) 60 | 61 | def get_permissions(self, virtual_host): 62 | """Get all Virtual hosts permissions. 63 | 64 | :raises ApiError: Raises if the remote server encountered an error. 65 | :raises ApiConnectionError: Raises if there was a connectivity issue. 66 | 67 | :rtype: dict 68 | """ 69 | virtual_host = quote(virtual_host, '') 70 | return self.http_client.get(API_VIRTUAL_HOSTS_PERMISSION % 71 | ( 72 | virtual_host 73 | )) 74 | -------------------------------------------------------------------------------- /amqpstorm/rpc.py: -------------------------------------------------------------------------------- 1 | """AMQPStorm Rpc.""" 2 | 3 | import threading 4 | import time 5 | from uuid import uuid4 6 | 7 | from amqpstorm.base import IDLE_WAIT 8 | from amqpstorm.exception import AMQPChannelError 9 | 10 | 11 | class Rpc(object): 12 | """Internal RPC handler. 13 | 14 | :param object default_adapter: Connection or Channel. 15 | :param int,float timeout: Rpc timeout. 16 | """ 17 | 18 | def __init__(self, default_adapter, timeout=360): 19 | self._lock = threading.Lock() 20 | self._default_connection_adapter = default_adapter 21 | self._timeout = timeout 22 | self._response = {} 23 | self._request = {} 24 | 25 | @property 26 | def lock(self): 27 | return self._lock 28 | 29 | def on_frame(self, frame_in): 30 | """On RPC Frame. 31 | 32 | :param specification.Frame frame_in: Amqp frame. 33 | :return: 34 | """ 35 | if frame_in.name not in self._request: 36 | return False 37 | 38 | uuid = self._request[frame_in.name] 39 | if self._response[uuid]: 40 | self._response[uuid].append(frame_in) 41 | else: 42 | self._response[uuid] = [frame_in] 43 | return True 44 | 45 | def register_request(self, valid_responses): 46 | """Register a RPC request. 47 | 48 | :param list valid_responses: List of possible Responses that 49 | we should be waiting for. 50 | :return: 51 | """ 52 | uuid = str(uuid4()) 53 | self._response[uuid] = [] 54 | for action in valid_responses: 55 | self._request[action] = uuid 56 | return uuid 57 | 58 | def remove(self, uuid): 59 | """Remove any data related to a specific RPC request. 60 | 61 | :param str uuid: Rpc Identifier. 62 | :return: 63 | """ 64 | self.remove_request(uuid) 65 | self.remove_response(uuid) 66 | 67 | def remove_request(self, uuid): 68 | """Remove any RPC request(s) using this uuid. 69 | 70 | :param str uuid: Rpc Identifier. 71 | :return: 72 | """ 73 | for key in list(self._request): 74 | if self._request[key] == uuid: 75 | del self._request[key] 76 | 77 | def remove_response(self, uuid): 78 | """Remove a RPC Response using this uuid. 79 | 80 | :param str uuid: Rpc Identifier. 81 | :return: 82 | """ 83 | if uuid in self._response: 84 | del self._response[uuid] 85 | 86 | def get_request(self, uuid, raw=False, multiple=False, 87 | connection_adapter=None): 88 | """Get a RPC request. 89 | 90 | :param str uuid: Rpc Identifier 91 | :param bool raw: If enabled return the frame as is, else return 92 | result as a dictionary. 93 | :param bool multiple: Are we expecting multiple frames. 94 | :param obj connection_adapter: Provide custom connection adapter. 95 | :return: 96 | """ 97 | if uuid not in self._response: 98 | return 99 | self._wait_for_request( 100 | uuid, connection_adapter or self._default_connection_adapter 101 | ) 102 | frame = self._get_response_frame(uuid) 103 | if not multiple: 104 | self.remove(uuid) 105 | result = None 106 | if raw: 107 | result = frame 108 | elif frame is not None: 109 | result = dict(frame) 110 | return result 111 | 112 | def _get_response_frame(self, uuid): 113 | """Get a response frame. 114 | 115 | :param str uuid: Rpc Identifier 116 | :return: 117 | """ 118 | frame = None 119 | frames = self._response.get(uuid, None) 120 | if frames: 121 | frame = frames.pop(0) 122 | return frame 123 | 124 | def _wait_for_request(self, uuid, connection_adapter=None): 125 | """Wait for RPC request to arrive. 126 | 127 | :param str uuid: Rpc Identifier. 128 | :param obj connection_adapter: Provide custom connection adapter. 129 | :return: 130 | """ 131 | start_time = time.time() 132 | while not self._response[uuid]: 133 | connection_adapter.check_for_errors() 134 | if time.time() - start_time > self._timeout: 135 | self._raise_rpc_timeout_error(uuid) 136 | time.sleep(IDLE_WAIT) 137 | 138 | def _raise_rpc_timeout_error(self, uuid): 139 | """Gather information and raise an Rpc exception. 140 | 141 | :param str uuid: Rpc Identifier. 142 | :return: 143 | """ 144 | requests = [] 145 | for key, value in self._request.items(): 146 | if value == uuid: 147 | requests.append(key) 148 | self.remove(uuid) 149 | message = ( 150 | 'rpc requests %s (%s) took too long' % 151 | ( 152 | uuid, 153 | ', '.join(requests) 154 | ) 155 | ) 156 | raise AMQPChannelError(message) 157 | -------------------------------------------------------------------------------- /amqpstorm/tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | CURRENT_DIR = os.path.dirname(os.path.realpath(__file__)) 4 | 5 | HOST = os.environ.get( 6 | 'AMQP_HOST', 7 | 'rmq.eandersson.net' 8 | ) 9 | USERNAME = os.environ.get( 10 | 'AMQP_USERNAME', 11 | 'amqpstorm' 12 | ) 13 | PASSWORD = os.environ.get( 14 | 'AMQP_PASSWORD', 15 | '2a55f70a841f18b' 16 | ) 17 | URI = os.environ.get( 18 | 'AMQP_URI', 19 | 'amqp://{0}:{1}@rmq.eandersson.net:5672/%2F'.format(USERNAME, PASSWORD) 20 | ) 21 | HTTP_URL = os.environ.get( 22 | 'AMQP_HTTP_URL', 23 | 'http://rmq.eandersson.net:15672' 24 | ) 25 | HTTPS_URL = os.environ.get( 26 | 'AMQP_HTTP_URL', 27 | 'https://rmq.eandersson.net:15671' 28 | ) 29 | SSL_URI = os.environ.get( 30 | 'AMQP_SSL_URI', 31 | 'amqps://{0}:{1}@rmq.eandersson.net:5671/%2F'.format(USERNAME, PASSWORD) 32 | ) 33 | SSL_HOST = os.environ.get( 34 | 'AMQP_SSL_HOST', 35 | 'rmq.eandersson.net' 36 | ) 37 | CAFILE = os.environ.get( 38 | 'AMQP_CAFILE', 39 | '{0}/resources/ssl/ca_certificate.pem'.format(CURRENT_DIR) 40 | ) 41 | CERTFILE = os.environ.get( 42 | 'AMQP_CERTFILE', 43 | '{0}/resources/ssl/client/client_certificate.pem'.format(CURRENT_DIR) 44 | ) 45 | KEYFILE = os.environ.get( 46 | 'AMQP_KEYFILE', 47 | '{0}/resources/ssl/client/private_key.pem'.format(CURRENT_DIR) 48 | ) 49 | -------------------------------------------------------------------------------- /amqpstorm/tests/functional/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eandersson/amqpstorm/352c4c8338dd05e563d9e121e5a4fc0874f11bbc/amqpstorm/tests/functional/__init__.py -------------------------------------------------------------------------------- /amqpstorm/tests/functional/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eandersson/amqpstorm/352c4c8338dd05e563d9e121e5a4fc0874f11bbc/amqpstorm/tests/functional/management/__init__.py -------------------------------------------------------------------------------- /amqpstorm/tests/functional/management/test_api.py: -------------------------------------------------------------------------------- 1 | from amqpstorm.management import ManagementApi 2 | from amqpstorm.management.exception import ApiConnectionError 3 | from amqpstorm.management.exception import ApiError 4 | from amqpstorm.tests import CAFILE 5 | from amqpstorm.tests import HTTP_URL 6 | from amqpstorm.tests import HTTPS_URL 7 | from amqpstorm.tests import PASSWORD 8 | from amqpstorm.tests import USERNAME 9 | from amqpstorm.tests.functional.utility import TestFunctionalFramework 10 | 11 | 12 | class ApiFunctionalTests(TestFunctionalFramework): 13 | def test_api_url_with_slash(self): 14 | api = ManagementApi(HTTP_URL + '/', USERNAME, PASSWORD) 15 | self.assertEqual(api.aliveness_test('/'), {'status': 'ok'}) 16 | 17 | def test_api_with_invalid_url(self): 18 | api = ManagementApi('abc', USERNAME, PASSWORD) 19 | self.assertRaisesRegex( 20 | ApiConnectionError, 21 | 'Invalid URL', 22 | api.aliveness_test, '/' 23 | ) 24 | 25 | def test_api_with_inaccessible(self): 26 | api = ManagementApi('http://192.168.1.50', USERNAME, PASSWORD, 27 | timeout=0.1) 28 | self.assertRaisesRegex( 29 | ApiConnectionError, 30 | 'Max retries exceeded with url', 31 | api.aliveness_test 32 | ) 33 | 34 | def test_api_with_invalid_credentials(self): 35 | api = ManagementApi(HTTP_URL, 'travis_ci', PASSWORD) 36 | 37 | self.assertRaisesRegex( 38 | ApiError, 39 | '401 Client Error: Unauthorized', 40 | api.aliveness_test 41 | ) 42 | 43 | def test_api_ssl_test(self): 44 | api = ManagementApi(HTTPS_URL, USERNAME, PASSWORD, 45 | verify=CAFILE) 46 | self.assertEqual(api.aliveness_test(), {'status': 'ok'}) 47 | 48 | def test_api_aliveness_test(self): 49 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 50 | self.assertEqual(api.aliveness_test(), {'status': 'ok'}) 51 | 52 | def test_api_context_manager(self): 53 | with ManagementApi(HTTP_URL, USERNAME, PASSWORD) as api: 54 | self.assertEqual(api.aliveness_test(), {'status': 'ok'}) 55 | 56 | def test_api_overview(self): 57 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 58 | result = api.overview() 59 | 60 | self.assertIsInstance(result, dict) 61 | self.assertIn('node', result) 62 | self.assertIn('management_version', result) 63 | 64 | def test_api_cluster_name(self): 65 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 66 | result = api.cluster_name() 67 | 68 | self.assertIsInstance(result, dict) 69 | self.assertIn('name', result) 70 | self.assertEqual('rabbit@rmq.eandersson.net', result['name']) 71 | 72 | def test_api_nodes(self): 73 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 74 | result = api.nodes() 75 | 76 | self.assertIsInstance(result, list) 77 | self.assertTrue(result) 78 | self.assertEqual('rabbit@rmq.eandersson.net', result[0]['name']) 79 | 80 | def test_api_node(self): 81 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 82 | result = api.node('rabbit@rmq.eandersson.net') 83 | 84 | self.assertIsInstance(result, dict) 85 | self.assertTrue(result) 86 | self.assertEqual('rabbit@rmq.eandersson.net', result['name']) 87 | 88 | def test_api_whoami(self): 89 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 90 | result = api.whoami() 91 | 92 | self.assertIsInstance(result, dict) 93 | self.assertEqual(result['name'], USERNAME) 94 | 95 | # RabbitMQ 3.9.X compatibility 96 | if isinstance(result['tags'], list): 97 | tag = result['tags'][0] 98 | else: 99 | tag = result['tags'] 100 | self.assertEqual('administrator', tag) 101 | -------------------------------------------------------------------------------- /amqpstorm/tests/functional/management/test_basic.py: -------------------------------------------------------------------------------- 1 | from amqpstorm.management import ManagementApi 2 | from amqpstorm.message import Message 3 | from amqpstorm.tests import HTTP_URL 4 | from amqpstorm.tests import PASSWORD 5 | from amqpstorm.tests import USERNAME 6 | from amqpstorm.tests.functional.utility import TestFunctionalFramework 7 | from amqpstorm.tests.functional.utility import setup 8 | 9 | 10 | class ApiBasicFunctionalTests(TestFunctionalFramework): 11 | @setup(queue=True) 12 | def test_api_basic_publish(self): 13 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 14 | 15 | api.queue.declare(self.queue_name) 16 | try: 17 | self.assertEqual(api.basic.publish(self.message, self.queue_name), 18 | {'routed': True}) 19 | finally: 20 | api.queue.delete(self.queue_name) 21 | 22 | @setup(queue=True) 23 | def test_api_basic_get_message(self): 24 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 25 | 26 | api.queue.declare(self.queue_name) 27 | self.assertEqual(api.basic.publish(self.message, self.queue_name), 28 | {'routed': True}) 29 | 30 | result = api.basic.get(self.queue_name, requeue=False) 31 | self.assertIsInstance(result, list) 32 | self.assertIsInstance(result[0], Message) 33 | self.assertEqual(result[0].body, self.message) 34 | 35 | # Make sure the message wasn't re-queued. 36 | self.assertFalse(api.basic.get(self.queue_name, requeue=False)) 37 | 38 | @setup(queue=True) 39 | def test_api_basic_get_message_requeue(self): 40 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 41 | 42 | api.queue.declare(self.queue_name) 43 | self.assertEqual(api.basic.publish(self.message, self.queue_name), 44 | {'routed': True}) 45 | 46 | result = api.basic.get(self.queue_name, requeue=True) 47 | self.assertIsInstance(result, list) 48 | self.assertIsInstance(result[0], Message) 49 | self.assertEqual(result[0].body, self.message) 50 | 51 | # Make sure the message was re-queued. 52 | self.assertTrue(api.basic.get(self.queue_name, requeue=False)) 53 | 54 | @setup(queue=True) 55 | def test_api_basic_get_message_to_dict(self): 56 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 57 | 58 | api.queue.declare(self.queue_name) 59 | self.assertEqual(api.basic.publish(self.message, self.queue_name), 60 | {'routed': True}) 61 | 62 | result = api.basic.get(self.queue_name, requeue=False, to_dict=True) 63 | self.assertIsInstance(result, list) 64 | self.assertIsInstance(result[0], dict) 65 | self.assertEqual(result[0]['payload'], self.message) 66 | -------------------------------------------------------------------------------- /amqpstorm/tests/functional/management/test_channel.py: -------------------------------------------------------------------------------- 1 | from amqpstorm.management import ManagementApi 2 | from amqpstorm.tests import HTTP_URL 3 | from amqpstorm.tests import PASSWORD 4 | from amqpstorm.tests import USERNAME 5 | from amqpstorm.tests.functional.utility import TestFunctionalFramework 6 | from amqpstorm.tests.functional.utility import retry_function_wrapper 7 | from amqpstorm.tests.functional.utility import setup 8 | 9 | 10 | class ApiChannelFunctionalTests(TestFunctionalFramework): 11 | @setup() 12 | def test_channel_list(self): 13 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 14 | 15 | channels = retry_function_wrapper(api.channel.list) 16 | self.assertIsNotNone(channels) 17 | 18 | self.assertIsInstance(channels, list) 19 | self.assertIsInstance(channels[0], dict) 20 | 21 | self.channel.close() 22 | -------------------------------------------------------------------------------- /amqpstorm/tests/functional/management/test_connection.py: -------------------------------------------------------------------------------- 1 | import time 2 | import uuid 3 | 4 | from amqpstorm import Connection 5 | from amqpstorm.management import ManagementApi 6 | from amqpstorm.tests import HOST 7 | from amqpstorm.tests import HTTP_URL 8 | from amqpstorm.tests import PASSWORD 9 | from amqpstorm.tests import USERNAME 10 | from amqpstorm.tests.functional.utility import TestFunctionalFramework 11 | from amqpstorm.tests.functional.utility import retry_function_wrapper 12 | from amqpstorm.tests.functional.utility import setup 13 | 14 | 15 | class ApiConnectionFunctionalTests(TestFunctionalFramework): 16 | @setup() 17 | def test_api_connection_get(self): 18 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 19 | 20 | connections = retry_function_wrapper(api.connection.list) 21 | self.assertTrue(connections) 22 | 23 | for conn in connections: 24 | self.assertIsInstance(api.connection.get(conn['name']), dict) 25 | 26 | @setup() 27 | def test_api_connection_list(self): 28 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 29 | 30 | connections = retry_function_wrapper(api.connection.list) 31 | self.assertIsNotNone(connections) 32 | 33 | self.assertGreater(len(connections), 0) 34 | self.assertIsInstance(connections[0], dict) 35 | 36 | def test_api_connection_client_properties(self): 37 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD, timeout=1) 38 | 39 | connection = Connection( 40 | HOST, USERNAME, PASSWORD, 41 | client_properties={'platform': 'Atari', 'license': 'MIT'} 42 | ) 43 | self.assertTrue(connection.is_open) 44 | 45 | connection_found = False 46 | for _ in range(10): 47 | for conn in api.connection.list(): 48 | if conn['client_properties']['platform'] != 'Atari': 49 | continue 50 | connection_found = True 51 | if connection_found: 52 | break 53 | time.sleep(1) 54 | 55 | self.assertTrue( 56 | connection_found, 57 | 'Could not find connection with custom client properties' 58 | ) 59 | 60 | def test_api_connection_close(self): 61 | connection_id = str(uuid.uuid4()) 62 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD, timeout=1) 63 | 64 | connection = Connection( 65 | HOST, USERNAME, PASSWORD, 66 | client_properties={'platform': connection_id} 67 | ) 68 | self.assertTrue(connection.is_open) 69 | 70 | connection_found = False 71 | for _ in range(10): 72 | for conn in api.connection.list(): 73 | if conn['client_properties']['platform'] != connection_id: 74 | continue 75 | connection_found = True 76 | self.assertIsNone( 77 | api.connection.close(conn['name'], reason=connection_id) 78 | ) 79 | time.sleep(1) 80 | if connection_found: 81 | break 82 | 83 | self.assertTrue( 84 | connection_found, 85 | 'Could not find connection' 86 | ) 87 | self.assertTrue(connection.is_closed) 88 | -------------------------------------------------------------------------------- /amqpstorm/tests/functional/management/test_exchange.py: -------------------------------------------------------------------------------- 1 | from amqpstorm.management import ApiError 2 | from amqpstorm.management import ManagementApi 3 | from amqpstorm.tests import HTTP_URL 4 | from amqpstorm.tests import PASSWORD 5 | from amqpstorm.tests import USERNAME 6 | from amqpstorm.tests.functional.utility import TestFunctionalFramework 7 | from amqpstorm.tests.functional.utility import setup 8 | 9 | 10 | class ApiExchangeFunctionalTests(TestFunctionalFramework): 11 | def test_api_exchange_get(self): 12 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 13 | 14 | self.assertIsInstance(api.exchange.get('amq.direct'), dict) 15 | 16 | def test_api_exchange_list(self): 17 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 18 | 19 | exchanges = api.exchange.list() 20 | 21 | self.assertIsInstance(exchanges, list) 22 | self.assertGreater(len(exchanges), 0) 23 | 24 | for exchange in exchanges: 25 | self.assertIsInstance(exchange, dict) 26 | self.assertIn('name', exchange) 27 | self.assertIn('type', exchange) 28 | self.assertIn('auto_delete', exchange) 29 | 30 | def test_api_exchange_list_all(self): 31 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 32 | 33 | exchanges = api.exchange.list(show_all=True) 34 | 35 | self.assertIsInstance(exchanges, list) 36 | self.assertGreater(len(exchanges), 0) 37 | 38 | for exchange in exchanges: 39 | self.assertIsInstance(exchange, dict) 40 | self.assertIn('name', exchange) 41 | self.assertIn('type', exchange) 42 | self.assertIn('auto_delete', exchange) 43 | 44 | @setup(new_connection=False, exchange=True) 45 | def test_api_exchange_declare(self): 46 | exchange_type = 'direct' 47 | 48 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 49 | self.assertIsNone(api.exchange.declare(self.exchange_name, 50 | exchange_type, 51 | passive=False, 52 | durable=True)) 53 | 54 | result = api.exchange.get(self.exchange_name) 55 | self.assertIsInstance(result, dict) 56 | self.assertEqual(result['name'], self.exchange_name) 57 | self.assertEqual(result['type'], exchange_type) 58 | self.assertEqual(result['auto_delete'], False) 59 | self.assertEqual(result['durable'], True) 60 | 61 | def test_api_exchange_declare_passive(self): 62 | exchange = 'test_queue_declare_passive' 63 | 64 | expected_error_message = ( 65 | 'NOT-FOUND - The client attempted to work ' 66 | 'with a server entity that does not exist.' 67 | ) 68 | 69 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 70 | 71 | try: 72 | api.exchange.declare(exchange, passive=True) 73 | except ApiError as why: 74 | self.assertEqual(str(why), expected_error_message) 75 | self.assertEqual(why.error_type, 'NOT-FOUND') 76 | self.assertEqual(why.error_code, 404) 77 | 78 | def test_api_exchange_declare_passive_exists(self): 79 | exchange = 'test_queue_declare_passive_exists' 80 | 81 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 82 | api.exchange.declare(exchange) 83 | 84 | self.assertIsNotNone(api.exchange.declare(exchange, passive=True)) 85 | 86 | @setup(new_connection=False, exchange=True) 87 | def test_api_exchange_delete(self): 88 | exchange_type = 'direct' 89 | 90 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 91 | api.exchange.declare(self.exchange_name, 92 | exchange_type, 93 | passive=False, 94 | durable=True) 95 | 96 | self.assertIsNone(api.exchange.delete(self.exchange_name)) 97 | 98 | self.assertRaisesRegex( 99 | ApiError, 100 | 'NOT-FOUND - The client attempted to work ' 101 | 'with a server entity that does not exist.', 102 | api.exchange.get, self.exchange_name 103 | ) 104 | 105 | def test_api_exchange_bind_and_unbind(self): 106 | source_name = 'amq.match' 107 | destination_name = 'amq.direct' 108 | routing_key = 'travis-ci' 109 | 110 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 111 | 112 | bindings = len(api.exchange.bindings(source_name)) 113 | 114 | self.assertIsNone(api.exchange.bind(source=source_name, 115 | destination=destination_name, 116 | routing_key=routing_key, 117 | arguments=None)) 118 | 119 | self.assertEqual(len(api.exchange.bindings(source_name)), 120 | bindings + 1) 121 | 122 | self.assertIsNone(api.exchange.unbind(source=source_name, 123 | destination=destination_name, 124 | routing_key=routing_key)) 125 | 126 | self.assertEqual(len(api.exchange.bindings(source_name)), bindings) 127 | -------------------------------------------------------------------------------- /amqpstorm/tests/functional/management/test_healthcheck.py: -------------------------------------------------------------------------------- 1 | from amqpstorm.management import ManagementApi 2 | from amqpstorm.tests import HTTP_URL 3 | from amqpstorm.tests import PASSWORD 4 | from amqpstorm.tests import USERNAME 5 | from amqpstorm.tests.functional.utility import TestFunctionalFramework 6 | from amqpstorm.tests.functional.utility import setup 7 | 8 | 9 | class ApiHealthchecksFunctionalTests(TestFunctionalFramework): 10 | @setup() 11 | def test_healthtests_get(self): 12 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 13 | 14 | result = api.healthchecks.get() 15 | self.assertIsInstance(result, dict) 16 | self.assertEqual(result['status'], 'ok') 17 | 18 | @setup() 19 | def test_healthtests_get_with_node_name(self): 20 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 21 | 22 | node_name = api.overview()['contexts'][0]['node'] 23 | 24 | result = api.healthchecks.get(node_name) 25 | self.assertIsInstance(result, dict) 26 | self.assertEqual(result['status'], 'ok') 27 | -------------------------------------------------------------------------------- /amqpstorm/tests/functional/management/test_user.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from amqpstorm.management import ApiError 4 | from amqpstorm.management import ManagementApi 5 | from amqpstorm.tests import HTTP_URL 6 | from amqpstorm.tests import PASSWORD 7 | from amqpstorm.tests import USERNAME 8 | from amqpstorm.tests.functional.utility import TestFunctionalFramework 9 | 10 | 11 | class ApiUserFunctionalTests(TestFunctionalFramework): 12 | def test_api_user_get(self): 13 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 14 | 15 | user = api.user.get(USERNAME) 16 | self.assertIsInstance(user, dict) 17 | self.assertEqual(user['name'], USERNAME) 18 | 19 | # RabbitMQ 3.9.X compatibility 20 | if isinstance(user['tags'], list): 21 | tag = user['tags'][0] 22 | else: 23 | tag = user['tags'] 24 | self.assertEqual('administrator', tag) 25 | 26 | def test_api_user_list(self): 27 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 28 | 29 | users = api.user.list() 30 | self.assertIsInstance(users, list) 31 | self.assertGreater(len(users), 0) 32 | 33 | for user in users: 34 | self.assertIsInstance(user, dict) 35 | self.assertIn('name', user) 36 | self.assertIn('password_hash', user) 37 | self.assertIn('tags', user) 38 | 39 | def test_api_user_create(self): 40 | username = 'travis_ci' 41 | password = str(uuid.uuid4()) 42 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 43 | 44 | try: 45 | self.assertIsNone( 46 | api.user.create(username, password, tags='monitor')) 47 | user = api.user.get(username) 48 | self.assertEqual(user['name'], username) 49 | 50 | # RabbitMQ 3.9.X compatibility 51 | if isinstance(user['tags'], list): 52 | tag = user['tags'][0] 53 | else: 54 | tag = user['tags'] 55 | self.assertEqual('monitor', tag) 56 | finally: 57 | api.user.delete(username) 58 | self.assertRaises(ApiError, api.user.get, username) 59 | 60 | def test_api_delete_bulk(self): 61 | users = [] 62 | password = str(uuid.uuid4()) 63 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 64 | 65 | try: 66 | for _ in range(5): 67 | username = str(uuid.uuid4()) 68 | users.append(username) 69 | api.user.create(username, password) 70 | 71 | api.user.delete(users) 72 | 73 | for username in users: 74 | self.assertRaises(ApiError, api.user.get, username) 75 | finally: 76 | for username in users: 77 | try: 78 | api.user.delete(username) 79 | except Exception: 80 | pass 81 | 82 | def test_api_user_get_permission(self): 83 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 84 | 85 | permission = api.user.get_permission(USERNAME, '/') 86 | self.assertIsInstance(permission, dict) 87 | self.assertEqual(permission['read'], '.*') 88 | self.assertEqual(permission['write'], '.*') 89 | self.assertEqual(permission['configure'], '.*') 90 | self.assertEqual(permission['user'], USERNAME) 91 | self.assertEqual(permission['vhost'], '/') 92 | 93 | def test_api_user_get_permissions(self): 94 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 95 | 96 | permissions = api.user.get_permissions(USERNAME) 97 | self.assertIsInstance(permissions, list) 98 | self.assertIsInstance(permissions[0], dict) 99 | self.assertEqual(permissions[0]['read'], '.*') 100 | self.assertEqual(permissions[0]['write'], '.*') 101 | self.assertEqual(permissions[0]['configure'], '.*') 102 | self.assertEqual(permissions[0]['user'], USERNAME) 103 | self.assertEqual(permissions[0]['vhost'], '/') 104 | -------------------------------------------------------------------------------- /amqpstorm/tests/functional/management/test_virtual_host.py: -------------------------------------------------------------------------------- 1 | from amqpstorm.management import ManagementApi 2 | from amqpstorm.tests import HTTP_URL 3 | from amqpstorm.tests import PASSWORD 4 | from amqpstorm.tests import USERNAME 5 | from amqpstorm.tests.functional.utility import TestFunctionalFramework 6 | 7 | 8 | class ApiUserFunctionalTests(TestFunctionalFramework): 9 | def test_api_virtual_host_get(self): 10 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 11 | 12 | virtual_host = api.virtual_host.get('/') 13 | self.assertIsInstance(virtual_host, dict) 14 | self.assertEqual(virtual_host['name'], '/') 15 | 16 | def test_api_virtual_host_list(self): 17 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 18 | 19 | virtual_hosts = api.virtual_host.list() 20 | self.assertIsInstance(virtual_hosts, list) 21 | self.assertGreater(len(virtual_hosts), 0) 22 | 23 | for vhost in virtual_hosts: 24 | self.assertIsInstance(vhost, dict) 25 | self.assertIn('name', vhost) 26 | 27 | def test_api_virtual_host_create(self): 28 | vhost_name = 'test_api_virtual_host_create' 29 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 30 | 31 | try: 32 | self.assertIsNone(api.virtual_host.create(vhost_name)) 33 | virtual_host = api.virtual_host.get(vhost_name) 34 | self.assertIsInstance(virtual_host, dict) 35 | self.assertEqual(virtual_host['name'], vhost_name) 36 | 37 | api.user.set_permission(USERNAME, vhost_name) 38 | finally: 39 | api.virtual_host.delete(vhost_name) 40 | 41 | def test_api_virtual_host_permissions(self): 42 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD) 43 | 44 | permissions = api.virtual_host.get_permissions('/') 45 | self.assertIsInstance(permissions, list) 46 | self.assertIsInstance(permissions[0], dict) 47 | self.assertEqual(permissions[0]['read'], '.*') 48 | self.assertEqual(permissions[0]['write'], '.*') 49 | self.assertEqual(permissions[0]['configure'], '.*') 50 | self.assertEqual(permissions[0]['user'], USERNAME) 51 | self.assertEqual(permissions[0]['vhost'], '/') 52 | -------------------------------------------------------------------------------- /amqpstorm/tests/functional/ssl/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eandersson/amqpstorm/352c4c8338dd05e563d9e121e5a4fc0874f11bbc/amqpstorm/tests/functional/ssl/__init__.py -------------------------------------------------------------------------------- /amqpstorm/tests/functional/test_basic.py: -------------------------------------------------------------------------------- 1 | from amqpstorm.tests.functional.utility import TestFunctionalFramework 2 | from amqpstorm.tests.functional.utility import setup 3 | 4 | 5 | class BasicFunctionalTests(TestFunctionalFramework): 6 | @setup() 7 | def test_functional_basic_qos(self): 8 | result = self.channel.basic.qos(prefetch_count=100) 9 | self.assertFalse(result) 10 | 11 | @setup(queue=True) 12 | def test_functional_basic_get(self): 13 | self.channel.queue.declare(self.queue_name) 14 | self.channel.basic.publish(self.message, self.queue_name) 15 | 16 | message = self.channel.basic.get(self.queue_name) 17 | self.assertEqual(message.body, self.message) 18 | message.ack() 19 | 20 | @setup(queue=True) 21 | def test_functional_basic_get_empty(self): 22 | self.channel.queue.declare(self.queue_name) 23 | 24 | message = self.channel.basic.get(self.queue_name) 25 | self.assertIsNone(message) 26 | 27 | @setup(queue=True) 28 | def test_functional_basic_cancel(self): 29 | self.channel.queue.declare(self.queue_name) 30 | consumer_tag = self.channel.basic.consume(None, self.queue_name) 31 | 32 | result = self.channel.basic.cancel(consumer_tag) 33 | self.assertEqual(result['consumer_tag'], consumer_tag) 34 | 35 | @setup(queue=True) 36 | def test_functional_basic_recover(self): 37 | self.channel.queue.declare(self.queue_name) 38 | self.channel.basic.publish(self.message, self.queue_name) 39 | 40 | self.assertFalse(self.channel.basic.recover(requeue=True)) 41 | 42 | @setup(queue=True) 43 | def test_functional_basic_ack(self): 44 | self.channel.queue.declare(self.queue_name) 45 | self.channel.basic.publish(self.message, self.queue_name) 46 | 47 | message = self.channel.basic.get(self.queue_name) 48 | result = self.channel.basic.ack(delivery_tag=message.delivery_tag) 49 | 50 | self.assertIsNone(result) 51 | 52 | # Make sure the message wasn't requeued. 53 | self.assertFalse(self.channel.basic.get(self.queue_name)) 54 | 55 | @setup(queue=True) 56 | def test_functional_basic_ack_multiple(self): 57 | message = None 58 | self.channel.queue.declare(self.queue_name) 59 | 60 | for _ in range(5): 61 | self.channel.basic.publish(self.message, self.queue_name) 62 | 63 | for _ in range(5): 64 | message = self.channel.basic.get(self.queue_name) 65 | 66 | self.assertIsNotNone(message) 67 | 68 | self.channel.basic.ack( 69 | delivery_tag=message.delivery_tag, 70 | multiple=True 71 | ) 72 | 73 | # Make sure the message wasn't requeued. 74 | self.assertFalse(self.channel.basic.get(self.queue_name)) 75 | 76 | @setup(queue=True) 77 | def test_functional_basic_nack(self): 78 | self.channel.queue.declare(self.queue_name) 79 | self.channel.basic.publish(self.message, self.queue_name) 80 | 81 | message = self.channel.basic.get(self.queue_name) 82 | result = self.channel.basic.nack( 83 | requeue=False, 84 | delivery_tag=message.delivery_tag 85 | ) 86 | 87 | self.assertIsNone(result) 88 | 89 | # Make sure the message wasn't requeued. 90 | self.assertFalse(self.channel.basic.get(self.queue_name)) 91 | 92 | @setup(queue=True) 93 | def test_functional_basic_nack_multiple(self): 94 | message = None 95 | self.channel.queue.declare(self.queue_name) 96 | 97 | for _ in range(5): 98 | self.channel.basic.publish(self.message, self.queue_name) 99 | 100 | for _ in range(5): 101 | message = self.channel.basic.get(self.queue_name) 102 | 103 | self.assertIsNotNone(message) 104 | self.channel.basic.nack( 105 | delivery_tag=message.delivery_tag, 106 | requeue=False, 107 | multiple=True 108 | ) 109 | 110 | # Make sure the message wasn't requeued. 111 | self.assertFalse(self.channel.basic.get(self.queue_name)) 112 | 113 | @setup(queue=True) 114 | def test_functional_basic_nack_requeue(self): 115 | self.channel.queue.declare(self.queue_name) 116 | self.channel.basic.publish(self.message, self.queue_name) 117 | 118 | message = self.channel.basic.get(self.queue_name) 119 | result = self.channel.basic.nack( 120 | requeue=True, 121 | delivery_tag=message.delivery_tag 122 | ) 123 | 124 | self.assertIsNone(result) 125 | 126 | # Make sure the message was requeued. 127 | self.assertIsNotNone(self.channel.basic.get(self.queue_name)) 128 | 129 | @setup(queue=True) 130 | def test_functional_basic_reject(self): 131 | self.channel.queue.declare(self.queue_name) 132 | self.channel.basic.publish(self.message, self.queue_name) 133 | 134 | message = self.channel.basic.get(self.queue_name) 135 | result = self.channel.basic.reject( 136 | requeue=False, 137 | delivery_tag=message.delivery_tag 138 | ) 139 | 140 | self.assertIsNone(result) 141 | 142 | # Make sure the message wasn't requeued. 143 | self.assertFalse(self.channel.basic.get(self.queue_name)) 144 | 145 | @setup(queue=True) 146 | def test_functional_basic_reject_requeue(self): 147 | self.channel.queue.declare(self.queue_name) 148 | self.channel.basic.publish(self.message, self.queue_name) 149 | 150 | message = self.channel.basic.get(self.queue_name) 151 | result = self.channel.basic.reject( 152 | requeue=True, 153 | delivery_tag=message.delivery_tag 154 | ) 155 | 156 | self.assertIsNone(result) 157 | 158 | # Make sure the message was requeued. 159 | self.assertIsNotNone(self.channel.basic.get(self.queue_name)) 160 | -------------------------------------------------------------------------------- /amqpstorm/tests/functional/test_exchange.py: -------------------------------------------------------------------------------- 1 | import amqpstorm 2 | from amqpstorm.tests.functional.utility import TestFunctionalFramework 3 | from amqpstorm.tests.functional.utility import setup 4 | 5 | 6 | class ExchangeFunctionalTests(TestFunctionalFramework): 7 | @setup(exchange=True) 8 | def test_functional_exchange_declare(self): 9 | self.channel.exchange.declare(self.exchange_name, 10 | passive=False, 11 | durable=True, auto_delete=True) 12 | self.assertEqual({}, self.channel.exchange.declare(self.exchange_name, 13 | passive=True)) 14 | 15 | @setup(exchange=True) 16 | def test_functional_exchange_delete(self): 17 | self.channel.exchange.declare(self.exchange_name) 18 | self.channel.exchange.delete(self.exchange_name, 19 | if_unused=True) 20 | self.assertRaises(amqpstorm.AMQPChannelError, 21 | self.channel.exchange.declare, 22 | self.exchange_name, passive=True) 23 | 24 | @setup(exchange=True, override_names=['exchange1', 'exchange2']) 25 | def test_functional_exchange_bind(self): 26 | self.channel.exchange.declare('exchange1') 27 | self.channel.exchange.declare('exchange2') 28 | 29 | self.assertEqual(self.channel.exchange.bind('exchange1', 30 | 'exchange2', 31 | 'routing_key'), {}) 32 | 33 | @setup(exchange=True, override_names=['exchange1', 'exchange2']) 34 | def test_functional_exchange_unbind(self): 35 | self.channel.exchange.declare('exchange1') 36 | self.channel.exchange.declare('exchange2') 37 | self.channel.exchange.bind('exchange1', 'exchange2', 'routing_key') 38 | 39 | self.assertEqual(self.channel.exchange.unbind('exchange1', 'exchange2', 40 | 'routing_key'), {}) 41 | -------------------------------------------------------------------------------- /amqpstorm/tests/functional/test_legacy.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from amqpstorm import Channel 4 | from amqpstorm.tests.functional.utility import TestFunctionalFramework 5 | from amqpstorm.tests.functional.utility import setup 6 | 7 | 8 | class LegacyFunctionalTests(TestFunctionalFramework): 9 | def configure(self): 10 | self.disable_logging_validation() 11 | 12 | @setup(queue=True) 13 | def test_functional_start_stop_consumer_tuple(self): 14 | self.channel.queue.declare(self.queue_name) 15 | self.channel.confirm_deliveries() 16 | 17 | for _ in range(5): 18 | self.channel.basic.publish(body=self.message, 19 | routing_key=self.queue_name) 20 | 21 | # Store and inbound messages. 22 | inbound_messages = [] 23 | 24 | def on_message(body, channel, method, properties): 25 | self.assertIsInstance(body, (bytes, str)) 26 | self.assertIsInstance(channel, Channel) 27 | self.assertIsInstance(properties, dict) 28 | self.assertIsInstance(method, dict) 29 | inbound_messages.append(body) 30 | if len(inbound_messages) >= 5: 31 | channel.stop_consuming() 32 | 33 | self.channel.basic.consume(callback=on_message, 34 | queue=self.queue_name, 35 | no_ack=True) 36 | 37 | # Sleep for 0.01s to make sure RabbitMQ has time to catch up. 38 | time.sleep(0.01) 39 | 40 | self.channel.start_consuming(to_tuple=True) 41 | 42 | # Make sure all five messages were downloaded. 43 | self.assertEqual(len(inbound_messages), 5) 44 | 45 | @setup(queue=True) 46 | def test_functional_publish_and_consume_five_messages_tuple(self): 47 | self.channel.queue.declare(self.queue_name) 48 | self.channel.confirm_deliveries() 49 | 50 | for _ in range(5): 51 | self.channel.basic.publish(body=self.message, 52 | routing_key=self.queue_name) 53 | 54 | # Store and inbound messages. 55 | inbound_messages = [] 56 | 57 | def on_message(body, channel, method, properties): 58 | self.assertEqual(body, self.message.encode('utf-8')) 59 | self.assertIsInstance(body, (bytes, str)) 60 | self.assertIsInstance(channel, Channel) 61 | self.assertIsInstance(properties, dict) 62 | self.assertIsInstance(method, dict) 63 | inbound_messages.append(body) 64 | 65 | self.channel.basic.consume(callback=on_message, 66 | queue=self.queue_name, 67 | no_ack=True) 68 | 69 | # Sleep for 0.01s to make sure RabbitMQ has time to catch up. 70 | time.sleep(0.01) 71 | 72 | self.channel.process_data_events(to_tuple=True) 73 | 74 | # Make sure all five messages were downloaded. 75 | self.assertEqual(len(inbound_messages), 5) 76 | 77 | @setup(queue=True) 78 | def test_functional_generator_consume(self): 79 | self.channel.queue.declare(self.queue_name) 80 | self.channel.confirm_deliveries() 81 | for _ in range(5): 82 | self.channel.basic.publish(body=self.message, 83 | routing_key=self.queue_name) 84 | self.channel.basic.consume(queue=self.queue_name, 85 | no_ack=True) 86 | # Sleep for 0.01s to make sure RabbitMQ has time to catch up. 87 | time.sleep(0.01) 88 | 89 | # Store and inbound messages. 90 | inbound_messages = [] 91 | for message in self.channel.build_inbound_messages( 92 | break_on_empty=True, 93 | to_tuple=True): 94 | self.assertIsInstance(message, tuple) 95 | self.assertIsInstance(message[0], bytes) 96 | self.assertIsInstance(message[1], Channel) 97 | self.assertIsInstance(message[2], dict) 98 | self.assertIsInstance(message[3], dict) 99 | inbound_messages.append(message) 100 | 101 | # Make sure all five messages were downloaded. 102 | self.assertEqual(len(inbound_messages), 5) 103 | 104 | @setup(queue=True) 105 | def test_functional_publish_and_get_five_messages(self): 106 | self.channel.queue.declare(self.queue_name) 107 | 108 | # Publish 5 Messages. 109 | for _ in range(5): 110 | self.channel.basic.publish(body=self.message, 111 | routing_key=self.queue_name) 112 | 113 | # Sleep for 0.01s to make sure RabbitMQ has time to catch up. 114 | time.sleep(0.01) 115 | 116 | # Get 5 messages. 117 | for _ in range(5): 118 | payload = self.channel.basic.get(self.queue_name, to_dict=True) 119 | self.assertIsInstance(payload, dict) 120 | -------------------------------------------------------------------------------- /amqpstorm/tests/functional/test_queue.py: -------------------------------------------------------------------------------- 1 | import amqpstorm 2 | from amqpstorm.tests.functional.utility import TestFunctionalFramework 3 | from amqpstorm.tests.functional.utility import setup 4 | 5 | 6 | class QueueFunctionalTests(TestFunctionalFramework): 7 | @setup(queue=True) 8 | def test_functional_queue_declare(self): 9 | self.channel.queue.declare(self.queue_name, 10 | passive=False, 11 | durable=True, auto_delete=True) 12 | 13 | expected_result = { 14 | 'consumer_count': 0, 15 | 'message_count': 0, 16 | 'queue': self.queue_name 17 | } 18 | 19 | self.assertEqual( 20 | expected_result, 21 | self.channel.queue.declare(self.queue_name, 22 | passive=True) 23 | ) 24 | 25 | @setup(queue=True) 26 | def test_functional_queue_delete(self): 27 | self.channel.queue.declare(self.queue_name) 28 | self.channel.queue.delete(self.queue_name, 29 | if_unused=True) 30 | self.assertRaises(amqpstorm.AMQPChannelError, 31 | self.channel.queue.declare, 32 | self.queue_name, passive=True) 33 | 34 | @setup(queue=True) 35 | def test_functional_queue_purge(self): 36 | messages_to_send = 10 37 | self.channel.queue.declare(self.queue_name, auto_delete=True) 38 | for _ in range(messages_to_send): 39 | self.channel.basic.publish(self.message, self.queue_name) 40 | result = self.channel.queue.purge(self.queue_name) 41 | self.assertEqual(result['message_count'], messages_to_send) 42 | 43 | @setup(queue=True) 44 | def test_functional_queue_bind(self): 45 | self.channel.queue.declare(self.queue_name, 46 | passive=False, 47 | durable=True, auto_delete=True) 48 | self.assertEqual(self.channel.queue.bind(queue=self.queue_name, 49 | exchange='amq.direct'), {}) 50 | 51 | @setup(queue=True) 52 | def test_functional_queue_bind_no_queue(self): 53 | self.channel.queue.declare(self.queue_name, 54 | passive=False, 55 | durable=True, auto_delete=True) 56 | self.assertEqual(self.channel.queue.bind(queue=self.queue_name, 57 | exchange='amq.direct'), {}) 58 | 59 | @setup(queue=True) 60 | def test_functional_queue_unbind(self): 61 | self.channel.queue.declare(self.queue_name, 62 | passive=False, 63 | durable=True, auto_delete=True) 64 | self.channel.queue.bind(queue=self.queue_name, exchange='amq.direct') 65 | self.assertEqual(self.channel.queue.unbind(queue=self.queue_name, 66 | exchange='amq.direct'), {}) 67 | -------------------------------------------------------------------------------- /amqpstorm/tests/functional/test_tx.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from amqpstorm.exception import AMQPChannelError 4 | from amqpstorm.tests.functional.utility import TestFunctionalFramework 5 | from amqpstorm.tests.functional.utility import setup 6 | 7 | 8 | class TxFunctionalTests(TestFunctionalFramework): 9 | @setup() 10 | def test_functional_tx_select(self): 11 | self.channel.tx.select() 12 | 13 | @setup() 14 | def test_functional_tx_select_multiple(self): 15 | for _ in range(10): 16 | self.channel.tx.select() 17 | 18 | @setup(queue=True) 19 | def test_functional_tx_commit(self): 20 | self.channel.tx.select() 21 | 22 | self.channel.queue.declare(self.queue_name) 23 | self.channel.basic.publish(self.message, self.queue_name) 24 | 25 | # Sleep for 0.1s to make sure RabbitMQ has time to catch up. 26 | time.sleep(0.1) 27 | 28 | queue_status = self.channel.queue.declare(self.queue_name, 29 | passive=True) 30 | self.assertEqual(queue_status['message_count'], 0) 31 | 32 | self.channel.tx.commit() 33 | 34 | queue_status = self.channel.queue.declare(self.queue_name, 35 | passive=True) 36 | self.assertEqual(queue_status['message_count'], 1) 37 | 38 | @setup(queue=True) 39 | def test_functional_tx_commit_multiple(self): 40 | self.channel.tx.select() 41 | 42 | self.channel.queue.declare(self.queue_name) 43 | for _ in range(10): 44 | self.channel.basic.publish(self.message, self.queue_name) 45 | 46 | # Sleep for 0.1s to make sure RabbitMQ has time to catch up. 47 | time.sleep(0.1) 48 | 49 | queue_status = self.channel.queue.declare(self.queue_name, 50 | passive=True) 51 | self.assertEqual(queue_status['message_count'], 0) 52 | 53 | self.channel.tx.commit() 54 | 55 | queue_status = self.channel.queue.declare(self.queue_name, 56 | passive=True) 57 | self.assertEqual(queue_status['message_count'], 10) 58 | 59 | @setup() 60 | def test_functional_tx_commit_without_select(self): 61 | self.assertRaisesRegex( 62 | AMQPChannelError, 63 | 'Channel 1 was closed by remote server: ' 64 | 'PRECONDITION_FAILED - channel is not transactional', 65 | self.channel.tx.commit 66 | ) 67 | 68 | @setup(queue=True) 69 | def test_functional_tx_rollback(self): 70 | self.channel.tx.select() 71 | 72 | self.channel.queue.declare(self.queue_name) 73 | self.channel.basic.publish(self.message, self.queue_name) 74 | 75 | self.channel.tx.rollback() 76 | 77 | queue_status = self.channel.queue.declare(self.queue_name, 78 | passive=True) 79 | self.assertEqual(queue_status['message_count'], 0) 80 | 81 | @setup(queue=True) 82 | def test_functional_tx_rollback_multiple(self): 83 | self.channel.tx.select() 84 | 85 | self.channel.queue.declare(self.queue_name) 86 | for _ in range(10): 87 | self.channel.basic.publish(self.message, self.queue_name) 88 | 89 | self.channel.tx.rollback() 90 | 91 | queue_status = self.channel.queue.declare(self.queue_name, 92 | passive=True) 93 | self.assertEqual(queue_status['message_count'], 0) 94 | 95 | @setup() 96 | def test_functional_tx_rollback_without_select(self): 97 | self.assertRaisesRegex( 98 | AMQPChannelError, 99 | 'Channel 1 was closed by remote server: ' 100 | 'PRECONDITION_FAILED - channel is not transactional', 101 | self.channel.tx.rollback 102 | ) 103 | -------------------------------------------------------------------------------- /amqpstorm/tests/functional/test_web_based.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | 4 | from amqpstorm import Connection 5 | from amqpstorm.tests import HOST 6 | from amqpstorm.tests import PASSWORD 7 | from amqpstorm.tests import USERNAME 8 | from amqpstorm.tests.functional.utility import TestFunctionalFramework 9 | from amqpstorm.tests.functional.utility import setup 10 | 11 | LOGGER = logging.getLogger(__name__) 12 | 13 | 14 | class WebFunctionalTests(TestFunctionalFramework): 15 | def configure(self): 16 | self.disable_logging_validation() 17 | 18 | @setup(queue=True) 19 | def test_functional_consume_web_message(self): 20 | self.channel.queue.declare(self.queue_name) 21 | self.api.basic.publish(body=self.message, 22 | routing_key=self.queue_name) 23 | 24 | # Sleep for 1s to make sure RabbitMQ has time to catch up. 25 | time.sleep(1) 26 | 27 | result = self.channel.basic.get(self.queue_name) 28 | 29 | self.assertEqual(result.body, self.message) 30 | 31 | @setup(queue=True) 32 | def test_functional_remove_queue_while_consuming(self): 33 | self.channel.queue.declare(self.queue_name) 34 | for _ in range(10): 35 | self.api.basic.publish(body=self.message, 36 | routing_key=self.queue_name) 37 | 38 | self.channel.basic.consume(queue=self.queue_name, no_ack=True) 39 | 40 | queue_deleted = False 41 | messages_received = 0 42 | for _ in self.channel.build_inbound_messages(break_on_empty=True): 43 | messages_received += 1 44 | if not queue_deleted: 45 | self.api.queue.delete(self.queue_name) 46 | queue_deleted = True 47 | 48 | self.assertFalse(self.channel._inbound) 49 | 50 | @setup(new_connection=False, queue=True) 51 | def test_functional_alternative_virtual_host(self): 52 | self.api.virtual_host.create(self.virtual_host_name) 53 | 54 | self.api.user.set_permission(USERNAME, self.virtual_host_name) 55 | 56 | self.connection = Connection(HOST, USERNAME, PASSWORD, 57 | virtual_host=self.virtual_host_name, 58 | timeout=1) 59 | self.channel = self.connection.channel() 60 | self.channel.queue.declare(self.queue_name) 61 | self.channel.queue.delete(self.queue_name) 62 | self.api.user.delete_permission(USERNAME, self.virtual_host_name) 63 | self.api.virtual_host.delete(self.virtual_host_name) 64 | -------------------------------------------------------------------------------- /amqpstorm/tests/functional/utility.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import time 3 | 4 | from amqpstorm import Connection 5 | from amqpstorm.management import ManagementApi 6 | from amqpstorm.management import exception 7 | from amqpstorm.tests import HOST 8 | from amqpstorm.tests import HTTP_URL 9 | from amqpstorm.tests import PASSWORD 10 | from amqpstorm.tests import USERNAME 11 | from amqpstorm.tests.utility import TestFramework 12 | 13 | 14 | class TestFunctionalFramework(TestFramework): 15 | """Extended Test Base for functional unit-tests.""" 16 | 17 | def __init__(self, *args, **kwargs): 18 | self.api = ManagementApi(HTTP_URL, USERNAME, PASSWORD, timeout=1) 19 | self.queue_name = None 20 | self.exchange_name = None 21 | self.virtual_host_name = None 22 | super(TestFunctionalFramework, self).__init__(*args, **kwargs) 23 | 24 | 25 | def retry_function_wrapper(callable_function, retry_limit=10, 26 | sleep_interval=1): 27 | """Retry wrapper used to retry functions before failing. 28 | 29 | :param callable_function: Function to call. 30 | :param retry_limit: Re-try limit. 31 | :param sleep_interval: Sleep interval between retries. 32 | 33 | :return: 34 | """ 35 | retries = retry_limit 36 | while retries > 0: 37 | # noinspection PyCallingNonCallable 38 | result = callable_function() 39 | if result: 40 | return result 41 | retries -= 1 42 | time.sleep(sleep_interval) 43 | 44 | 45 | def setup(new_connection=True, new_channel=True, queue=False, exchange=False, 46 | override_names=None): 47 | """Set up default testing scenario. 48 | 49 | :param new_connection: Create a new connection. 50 | :param new_channel: Create a new channel. 51 | :param queue: Remove any queues created by the test. 52 | :param exchange: Remove any exchanges created by the test. 53 | :param override_names: Override default queue/exchange naming. 54 | 55 | :return: 56 | """ 57 | 58 | def outer(f): 59 | @functools.wraps(f) 60 | def setup_wrapper(self, *args, **kwargs): 61 | name = f.__name__ 62 | self.queue_name = name 63 | self.exchange_name = name 64 | self.virtual_host_name = name 65 | if new_connection: 66 | self.connection = Connection(HOST, USERNAME, PASSWORD, 67 | timeout=1) 68 | if new_channel: 69 | self.channel = self.connection.channel() 70 | try: 71 | result = f(self, *args, **kwargs) 72 | finally: 73 | names = [name] 74 | if override_names: 75 | names = override_names 76 | clean(names, queue, exchange) 77 | if self.channel: 78 | self.channel.close() 79 | if self.connection: 80 | self.connection.close() 81 | return result 82 | 83 | setup_wrapper.__wrapped_function = f 84 | setup_wrapper.__wrapper_name = 'setup_wrapper' 85 | return setup_wrapper 86 | 87 | return outer 88 | 89 | 90 | def clean(names, queue=False, exchange=False): 91 | """Clean any queues or exchanges created by the test. 92 | 93 | :param names: Queue/Exchange names. 94 | :param queue: Remove queues. 95 | :param exchange: Remove exchanges. 96 | 97 | :return: 98 | """ 99 | if not queue and not exchange: 100 | return 101 | api = ManagementApi(HTTP_URL, USERNAME, PASSWORD, timeout=10) 102 | if queue: 103 | _delete_queues(api, names) 104 | if exchange: 105 | _delete_exchanges(api, names) 106 | 107 | 108 | def _delete_exchanges(management_api, exchanges): 109 | """Delete exchanges. 110 | 111 | :param ManagementApi management_api: 112 | :param exchanges: 113 | """ 114 | for name in exchanges: 115 | try: 116 | management_api.exchange.delete(name) 117 | except exception.ApiError: 118 | pass 119 | 120 | 121 | def _delete_queues(api, queues): 122 | """Delete queues. 123 | 124 | :param ManagementApi api: 125 | :param list exchanges: 126 | """ 127 | for name in queues: 128 | try: 129 | api.queue.delete(name) 130 | except exception.ApiError: 131 | pass 132 | -------------------------------------------------------------------------------- /amqpstorm/tests/resources/ssl/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eandersson/amqpstorm/352c4c8338dd05e563d9e121e5a4fc0874f11bbc/amqpstorm/tests/resources/ssl/.empty -------------------------------------------------------------------------------- /amqpstorm/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eandersson/amqpstorm/352c4c8338dd05e563d9e121e5a4fc0874f11bbc/amqpstorm/tests/unit/__init__.py -------------------------------------------------------------------------------- /amqpstorm/tests/unit/base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eandersson/amqpstorm/352c4c8338dd05e563d9e121e5a4fc0874f11bbc/amqpstorm/tests/unit/base/__init__.py -------------------------------------------------------------------------------- /amqpstorm/tests/unit/base/test_base_channel.py: -------------------------------------------------------------------------------- 1 | from amqpstorm import AMQPChannelError 2 | from amqpstorm.base import BaseChannel 3 | 4 | from amqpstorm.tests.utility import TestFramework 5 | 6 | 7 | class BaseChannelTests(TestFramework): 8 | def test_base_channel_id(self): 9 | channel = BaseChannel(100) 10 | 11 | self.assertEqual(channel.channel_id, 100) 12 | 13 | def test_base_channel_add_consumer_tag(self): 14 | channel = BaseChannel(0) 15 | channel.add_consumer_tag('travis-ci') 16 | 17 | self.assertEqual(channel.consumer_tags[0], 'travis-ci') 18 | 19 | def test_base_channel_add_consumer_tag_none_raises(self): 20 | channel = BaseChannel(0) 21 | self.assertRaisesRegex( 22 | AMQPChannelError, 23 | 'consumer tag needs to be a string', 24 | channel.add_consumer_tag, None 25 | ) 26 | self.assertFalse(channel.consumer_tags) 27 | 28 | def test_base_channel_remove_empty_string(self): 29 | channel = BaseChannel(0) 30 | channel.add_consumer_tag('travis-ci') 31 | channel.add_consumer_tag('') 32 | 33 | channel.remove_consumer_tag('') 34 | 35 | self.assertTrue(len(channel.consumer_tags)) 36 | 37 | def test_base_channel_add_duplicate_consumer_tags(self): 38 | channel = BaseChannel(0) 39 | 40 | channel.add_consumer_tag('travis-ci') 41 | channel.add_consumer_tag('travis-ci') 42 | 43 | self.assertEqual(len(channel.consumer_tags), 1) 44 | self.assertEqual(channel.consumer_tags[0], 'travis-ci') 45 | 46 | def test_base_channel_remove_single_consumer_tag(self): 47 | channel = BaseChannel(0) 48 | 49 | self.assertEqual(len(channel.consumer_tags), 0) 50 | 51 | channel.add_consumer_tag('travis-ci-1') 52 | channel.add_consumer_tag('travis-ci-2') 53 | 54 | self.assertEqual(len(channel.consumer_tags), 2) 55 | 56 | channel.remove_consumer_tag('travis-ci-1') 57 | 58 | self.assertEqual(len(channel.consumer_tags), 1) 59 | self.assertEqual(channel.consumer_tags[0], 'travis-ci-2') 60 | 61 | def test_base_channel_remove_all_consumer_tags(self): 62 | channel = BaseChannel(0) 63 | 64 | self.assertEqual(len(channel.consumer_tags), 0) 65 | 66 | for _ in range(100): 67 | channel.add_consumer_tag('travis-ci') 68 | channel.remove_consumer_tag(None) 69 | 70 | self.assertEqual(len(channel.consumer_tags), 0) 71 | -------------------------------------------------------------------------------- /amqpstorm/tests/unit/base/test_stateful.py: -------------------------------------------------------------------------------- 1 | from amqpstorm.base import Stateful 2 | 3 | from amqpstorm.tests.utility import TestFramework 4 | 5 | 6 | class StatefulTests(TestFramework): 7 | def test_stateful_default_is_closed(self): 8 | stateful = Stateful() 9 | 10 | self.assertTrue(stateful.is_closed) 11 | 12 | def test_stateful_set_open(self): 13 | stateful = Stateful() 14 | stateful.set_state(Stateful.OPEN) 15 | 16 | self.assertTrue(stateful.is_open) 17 | 18 | def test_stateful_set_opening(self): 19 | stateful = Stateful() 20 | stateful.set_state(Stateful.OPENING) 21 | 22 | self.assertTrue(stateful.is_opening) 23 | 24 | stateful.set_state(Stateful.OPEN) 25 | 26 | self.assertFalse(stateful.is_opening) 27 | 28 | def test_stateful_set_closed(self): 29 | stateful = Stateful() 30 | stateful.set_state(Stateful.CLOSED) 31 | 32 | self.assertTrue(stateful.is_closed) 33 | 34 | def test_stateful_set_closing(self): 35 | stateful = Stateful() 36 | stateful.set_state(Stateful.CLOSING) 37 | 38 | self.assertTrue(stateful.is_closing) 39 | 40 | stateful.set_state(Stateful.CLOSED) 41 | 42 | self.assertFalse(stateful.is_closing) 43 | 44 | def test_stateful_get_current_state(self): 45 | stateful = Stateful() 46 | 47 | stateful.set_state(Stateful.CLOSED) 48 | self.assertEqual(stateful.current_state, Stateful.CLOSED) 49 | 50 | stateful.set_state(Stateful.CLOSING) 51 | self.assertEqual(stateful.current_state, Stateful.CLOSING) 52 | 53 | stateful.set_state(Stateful.OPEN) 54 | self.assertEqual(stateful.current_state, Stateful.OPEN) 55 | 56 | stateful.set_state(Stateful.OPENING) 57 | self.assertEqual(stateful.current_state, Stateful.OPENING) 58 | -------------------------------------------------------------------------------- /amqpstorm/tests/unit/basic/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eandersson/amqpstorm/352c4c8338dd05e563d9e121e5a4fc0874f11bbc/amqpstorm/tests/unit/basic/__init__.py -------------------------------------------------------------------------------- /amqpstorm/tests/unit/channel/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eandersson/amqpstorm/352c4c8338dd05e563d9e121e5a4fc0874f11bbc/amqpstorm/tests/unit/channel/__init__.py -------------------------------------------------------------------------------- /amqpstorm/tests/unit/channel/test_channel_frame.py: -------------------------------------------------------------------------------- 1 | import mock 2 | from pamqp import ContentHeader 3 | from pamqp import specification 4 | from pamqp.body import ContentBody 5 | 6 | import amqpstorm 7 | from amqpstorm import Channel 8 | from amqpstorm.exception import AMQPChannelError 9 | from amqpstorm.exception import AMQPConnectionError 10 | from amqpstorm.exception import AMQPMessageError 11 | from amqpstorm.tests.utility import FakeConnection 12 | from amqpstorm.tests.utility import FakeFrame 13 | from amqpstorm.tests.utility import TestFramework 14 | 15 | 16 | class ChannelFrameTests(TestFramework): 17 | def test_channel_content_frames(self): 18 | channel = Channel(0, FakeConnection(), rpc_timeout=1) 19 | channel.set_state(channel.OPEN) 20 | 21 | message = self.message.encode('utf-8') 22 | message_len = len(message) 23 | 24 | deliver = specification.Basic.Deliver() 25 | header = ContentHeader(body_size=message_len) 26 | body = ContentBody(value=message) 27 | 28 | channel.on_frame(deliver) 29 | channel.on_frame(header) 30 | channel.on_frame(body) 31 | 32 | for msg in channel.build_inbound_messages(break_on_empty=True): 33 | self.assertIsInstance(msg.body, str) 34 | self.assertEqual(msg.body.encode('utf-8'), message) 35 | 36 | def test_channel_basic_cancel_frame(self): 37 | connection = amqpstorm.Connection('localhost', 'guest', 'guest', 38 | lazy=True) 39 | channel = Channel(0, connection, rpc_timeout=1) 40 | 41 | channel.on_frame(specification.Basic.Cancel('travis-ci')) 42 | 43 | self.assertEqual( 44 | self.get_last_log(), 45 | 'Received Basic.Cancel on consumer_tag: travis-ci' 46 | ) 47 | 48 | def test_channel_cancel_ok_frame(self): 49 | tag = 'travis-ci' 50 | channel = Channel(0, mock.Mock(name='Connection'), rpc_timeout=1) 51 | channel.add_consumer_tag(tag) 52 | 53 | channel.on_frame(specification.Basic.CancelOk(tag)) 54 | 55 | self.assertFalse(channel.consumer_tags) 56 | 57 | def test_channel_consume_ok_frame(self): 58 | tag = 'travis-ci' 59 | channel = Channel(0, mock.Mock(name='Connection'), rpc_timeout=1) 60 | 61 | channel.on_frame(specification.Basic.ConsumeOk(tag)) 62 | 63 | self.assertEqual(channel.consumer_tags[0], tag) 64 | 65 | def test_channel_basic_return_frame(self): 66 | connection = amqpstorm.Connection('localhost', 'guest', 'guest', 67 | lazy=True) 68 | connection.set_state(connection.OPEN) 69 | channel = Channel(0, connection, rpc_timeout=1) 70 | channel.set_state(channel.OPEN) 71 | 72 | channel.on_frame( 73 | specification.Basic.Return( 74 | reply_code=500, 75 | reply_text='travis-ci', 76 | exchange='exchange', 77 | routing_key='routing_key' 78 | ) 79 | ) 80 | 81 | self.assertRaisesRegex( 82 | AMQPMessageError, 83 | r"Message not delivered: travis-ci \(500\) to queue " 84 | r"'routing_key' from exchange 'exchange'", 85 | channel.check_for_errors 86 | ) 87 | 88 | def test_channel_close_frame(self): 89 | connection = FakeConnection(state=FakeConnection.OPEN) 90 | channel = Channel(0, connection, rpc_timeout=1) 91 | channel.set_state(channel.OPEN) 92 | 93 | channel.on_frame( 94 | specification.Channel.Close( 95 | reply_code=500, 96 | reply_text='travis-ci' 97 | ) 98 | ) 99 | 100 | self.assertRaisesRegex( 101 | AMQPChannelError, 102 | 'Channel 0 was closed by remote server: travis-ci', 103 | channel.check_for_errors 104 | ) 105 | 106 | def test_channel_close_frame_when_connection_closed(self): 107 | connection = FakeConnection(state=FakeConnection.CLOSED) 108 | channel = Channel(0, connection, rpc_timeout=1) 109 | channel.set_state(channel.OPEN) 110 | 111 | channel.on_frame( 112 | specification.Channel.Close( 113 | reply_code=500, 114 | reply_text='travis-ci' 115 | ) 116 | ) 117 | 118 | self.assertIsNone(connection.get_last_frame()) 119 | self.assertEqual( 120 | str(channel.exceptions[0]), 121 | 'Channel 0 was closed by remote server: travis-ci' 122 | ) 123 | 124 | def test_channel_close_frame_socket_write_fail_silently(self): 125 | connection = FakeConnection(state=FakeConnection.OPEN) 126 | channel = Channel(0, connection, rpc_timeout=1) 127 | channel.set_state(channel.OPEN) 128 | 129 | def raise_on_write(*_): 130 | raise AMQPConnectionError('travis-ci') 131 | 132 | connection.write_frame = raise_on_write 133 | 134 | channel.on_frame( 135 | specification.Channel.Close( 136 | reply_code=500, 137 | reply_text='travis-ci' 138 | ) 139 | ) 140 | 141 | self.assertIsNone(connection.get_last_frame()) 142 | self.assertEqual( 143 | str(channel.exceptions[0]), 144 | 'Channel 0 was closed by remote server: travis-ci' 145 | ) 146 | 147 | def test_channel_flow_frame(self): 148 | connection = FakeConnection() 149 | connection.set_state(connection.OPEN) 150 | channel = Channel(0, connection, rpc_timeout=1) 151 | channel.set_state(channel.OPEN) 152 | 153 | channel.on_frame(specification.Channel.Flow()) 154 | 155 | self.assertIsInstance( 156 | connection.get_last_frame(), 157 | specification.Channel.FlowOk 158 | ) 159 | 160 | def test_channel_unhandled_frame(self): 161 | connection = amqpstorm.Connection('localhost', 'guest', 'guest', 162 | lazy=True) 163 | channel = Channel(0, connection, rpc_timeout=1) 164 | 165 | channel.on_frame(FakeFrame()) 166 | 167 | self.assertEqual( 168 | self.get_last_log(), 169 | "[Channel0] Unhandled Frame: FakeFrame -- " 170 | "{'data_1': 'hello world'}" 171 | ) 172 | -------------------------------------------------------------------------------- /amqpstorm/tests/unit/channel0/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eandersson/amqpstorm/352c4c8338dd05e563d9e121e5a4fc0874f11bbc/amqpstorm/tests/unit/channel0/__init__.py -------------------------------------------------------------------------------- /amqpstorm/tests/unit/channel0/test_channel0_frame.py: -------------------------------------------------------------------------------- 1 | from pamqp.heartbeat import Heartbeat 2 | from pamqp.specification import Connection 3 | 4 | import amqpstorm 5 | from amqpstorm import AMQPConnectionError 6 | from amqpstorm.channel0 import Channel0 7 | from amqpstorm.tests.utility import FakeConnection 8 | from amqpstorm.tests.utility import FakeFrame 9 | from amqpstorm.tests.utility import TestFramework 10 | 11 | 12 | class Channel0FrameTests(TestFramework): 13 | def configure(self): 14 | self.connection = amqpstorm.Connection('localhost', 'guest', 'guest', 15 | lazy=True) 16 | 17 | def test_channel0_heartbeat(self): 18 | channel = Channel0(self.connection) 19 | self.assertIsNone(channel.on_frame(Heartbeat())) 20 | 21 | def test_channel0_on_close_frame(self): 22 | self.connection.set_state(self.connection.OPEN) 23 | channel = Channel0(self.connection) 24 | 25 | self.assertFalse(self.connection.exceptions) 26 | 27 | channel.on_frame(Connection.Close()) 28 | 29 | self.assertTrue(self.connection.exceptions) 30 | self.assertTrue(self.connection.is_closed) 31 | self.assertRaisesRegex( 32 | AMQPConnectionError, 33 | 'Connection was closed by remote server: ', 34 | self.connection.check_for_errors 35 | ) 36 | 37 | def test_channel0_on_close_ok_frame(self): 38 | self.connection.set_state(self.connection.OPEN) 39 | channel = Channel0(self.connection) 40 | 41 | self.assertFalse(self.connection.is_closed) 42 | 43 | channel.on_frame(Connection.CloseOk()) 44 | 45 | self.assertTrue(self.connection.is_closed) 46 | 47 | def test_channel0_is_blocked(self): 48 | channel = Channel0(self.connection) 49 | 50 | self.assertFalse(channel.is_blocked) 51 | 52 | channel.on_frame(Connection.Blocked('travis-ci')) 53 | 54 | self.assertTrue(channel.is_blocked) 55 | self.assertEqual(self.get_last_log(), 56 | 'Connection is blocked by remote server: travis-ci') 57 | 58 | def test_channel0_unblocked(self): 59 | channel = Channel0(self.connection) 60 | 61 | channel.on_frame(Connection.Blocked()) 62 | 63 | self.assertTrue(channel.is_blocked) 64 | 65 | channel.on_frame(Connection.Unblocked()) 66 | 67 | self.assertFalse(channel.is_blocked) 68 | self.assertEqual(self.get_last_log(), 69 | 'Connection is blocked by remote server: ') 70 | 71 | def test_channel0_open_ok_frame(self): 72 | channel = Channel0(self.connection) 73 | 74 | self.assertFalse(self.connection.is_open) 75 | 76 | channel.on_frame(Connection.OpenOk()) 77 | 78 | self.assertTrue(self.connection.is_open) 79 | 80 | def test_channel0_start_frame(self): 81 | connection = FakeConnection() 82 | connection.parameters['username'] = 'guest' 83 | connection.parameters['password'] = 'guest' 84 | channel = Channel0(connection) 85 | 86 | properties = { 87 | 'version': 0 88 | } 89 | 90 | channel.on_frame(Connection.Start(server_properties=properties)) 91 | 92 | self.assertEqual(channel.server_properties, properties) 93 | self.assertIsInstance(connection.get_last_frame(), Connection.StartOk) 94 | 95 | def test_channel0_start_invalid_auth_frame(self): 96 | connection = FakeConnection() 97 | connection.parameters['username'] = 'guest' 98 | connection.parameters['password'] = 'guest' 99 | channel = Channel0(connection) 100 | 101 | channel.on_frame(Connection.Start(mechanisms='invalid')) 102 | 103 | self.assertRaisesRegex( 104 | AMQPConnectionError, 105 | r'Unsupported Security Mechanism\(s\): invalid', 106 | connection.check_for_errors 107 | ) 108 | 109 | def test_channel0_tune_frame(self): 110 | connection = FakeConnection() 111 | connection.parameters['virtual_host'] = '/' 112 | channel = Channel0(connection) 113 | 114 | channel.on_frame(Connection.Tune()) 115 | 116 | self.assertIsInstance(connection.get_last_frame(), Connection.TuneOk) 117 | self.assertIsInstance(connection.get_last_frame(), Connection.Open) 118 | 119 | def test_channel0_unhandled_frame(self): 120 | channel = Channel0(self.connection) 121 | 122 | channel.on_frame(FakeFrame()) 123 | 124 | self.assertEqual(self.get_last_log(), 125 | "[Channel0] Unhandled Frame: FakeFrame") 126 | -------------------------------------------------------------------------------- /amqpstorm/tests/unit/connection/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eandersson/amqpstorm/352c4c8338dd05e563d9e121e5a4fc0874f11bbc/amqpstorm/tests/unit/connection/__init__.py -------------------------------------------------------------------------------- /amqpstorm/tests/unit/connection/test_connection_exception.py: -------------------------------------------------------------------------------- 1 | import ssl 2 | 3 | from amqpstorm import AMQPInvalidArgument 4 | from amqpstorm import Connection 5 | from amqpstorm.tests.utility import TestFramework 6 | 7 | 8 | class ConnectionExceptionTests(TestFramework): 9 | def test_connection_set_hostname(self): 10 | connection = Connection('127.0.0.1', 'guest', 'guest', lazy=True) 11 | 12 | self.assertEqual(connection.parameters['username'], 'guest') 13 | 14 | def test_connection_set_username(self): 15 | connection = Connection('127.0.0.1', 'guest', 'guest', lazy=True) 16 | 17 | self.assertEqual(connection.parameters['username'], 'guest') 18 | 19 | def test_connection_set_password(self): 20 | connection = Connection('127.0.0.1', 'guest', 'guest', lazy=True) 21 | 22 | self.assertEqual(connection.parameters['username'], 'guest') 23 | 24 | def test_connection_set_parameters(self): 25 | connection = Connection( 26 | '127.0.0.1', 'guest', 'guest', 27 | virtual_host='travis', 28 | heartbeat=120, 29 | timeout=180, 30 | ssl=True, 31 | ssl_options={ 32 | 'ssl_version': ssl.PROTOCOL_TLSv1 33 | }, 34 | lazy=True 35 | ) 36 | 37 | self.assertEqual(connection.parameters['virtual_host'], 'travis') 38 | self.assertEqual(connection.parameters['heartbeat'], 120) 39 | self.assertEqual(connection.parameters['timeout'], 180) 40 | self.assertEqual(connection.parameters['ssl'], True) 41 | self.assertEqual(connection.parameters['ssl_options']['ssl_version'], 42 | ssl.PROTOCOL_TLSv1) 43 | 44 | def test_connection_invalid_hostname(self): 45 | self.assertRaisesRegex( 46 | AMQPInvalidArgument, 47 | 'hostname should be a string', 48 | Connection, 1, 'guest', 'guest', lazy=True 49 | ) 50 | 51 | def test_connection_invalid_username(self): 52 | self.assertRaisesRegex( 53 | AMQPInvalidArgument, 54 | 'username should be a string', 55 | Connection, '127.0.0.1', 2, 'guest', lazy=True 56 | ) 57 | self.assertRaisesRegex( 58 | AMQPInvalidArgument, 59 | 'username should be a string', 60 | Connection, '127.0.0.1', None, 'guest', lazy=True 61 | ) 62 | 63 | def test_connection_invalid_password(self): 64 | self.assertRaisesRegex( 65 | AMQPInvalidArgument, 66 | 'password should be a string', 67 | Connection, '127.0.0.1', 'guest', 3, lazy=True 68 | ) 69 | self.assertRaisesRegex( 70 | AMQPInvalidArgument, 71 | 'password should be a string', 72 | Connection, '127.0.0.1', 'guest', None, lazy=True 73 | ) 74 | 75 | def test_connection_invalid_virtual_host(self): 76 | self.assertRaisesRegex( 77 | AMQPInvalidArgument, 78 | 'virtual_host should be a string', 79 | Connection, '127.0.0.1', 'guest', 'guest', virtual_host=4, 80 | lazy=True 81 | ) 82 | self.assertRaisesRegex( 83 | AMQPInvalidArgument, 84 | 'virtual_host should be a string', 85 | Connection, '127.0.0.1', 'guest', 'guest', virtual_host=None, 86 | lazy=True 87 | ) 88 | 89 | def test_connection_invalid_port(self): 90 | self.assertRaisesRegex( 91 | AMQPInvalidArgument, 92 | 'port should be an integer', 93 | Connection, '127.0.0.1', 'guest', 'guest', port='', lazy=True 94 | ) 95 | self.assertRaisesRegex( 96 | AMQPInvalidArgument, 97 | 'port should be an integer', 98 | Connection, '127.0.0.1', 'guest', 'guest', port=None, lazy=True 99 | ) 100 | 101 | def test_connection_invalid_heartbeat(self): 102 | self.assertRaisesRegex( 103 | AMQPInvalidArgument, 104 | 'heartbeat should be an integer', 105 | Connection, '127.0.0.1', 'guest', 'guest', heartbeat='5', 106 | lazy=True 107 | ) 108 | self.assertRaisesRegex( 109 | AMQPInvalidArgument, 110 | 'heartbeat should be an integer', 111 | Connection, '127.0.0.1', 'guest', 'guest', heartbeat=None, 112 | lazy=True 113 | ) 114 | 115 | def test_connection_invalid_timeout(self): 116 | self.assertRaisesRegex( 117 | AMQPInvalidArgument, 118 | 'timeout should be an integer or float', 119 | Connection, '127.0.0.1', 'guest', 'guest', timeout='6', lazy=True 120 | ) 121 | self.assertRaisesRegex( 122 | AMQPInvalidArgument, 123 | 'timeout should be an integer or float', 124 | Connection, '127.0.0.1', 'guest', 'guest', timeout=None, lazy=True 125 | ) 126 | 127 | def test_connection_invalid_timeout_on_channel(self): 128 | connection = Connection( 129 | '127.0.0.1', 'guest', 'guest', timeout=0.1, 130 | lazy=True 131 | ) 132 | 133 | self.assertRaisesRegex( 134 | AMQPInvalidArgument, 135 | 'rpc_timeout should be an integer', 136 | connection.channel, None 137 | ) 138 | -------------------------------------------------------------------------------- /amqpstorm/tests/unit/exchange/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eandersson/amqpstorm/352c4c8338dd05e563d9e121e5a4fc0874f11bbc/amqpstorm/tests/unit/exchange/__init__.py -------------------------------------------------------------------------------- /amqpstorm/tests/unit/exchange/test_exchange.py: -------------------------------------------------------------------------------- 1 | from pamqp.specification import Exchange as pamqp_exchange 2 | 3 | from amqpstorm.channel import Channel 4 | from amqpstorm.channel import Exchange 5 | from amqpstorm.tests.utility import FakeConnection 6 | from amqpstorm.tests.utility import TestFramework 7 | 8 | 9 | class ExchangeTests(TestFramework): 10 | def test_exchange_declare(self): 11 | def on_declare(*_): 12 | channel.rpc.on_frame(pamqp_exchange.DeclareOk()) 13 | 14 | connection = FakeConnection(on_write=on_declare) 15 | channel = Channel(0, connection, 0.01) 16 | channel.set_state(Channel.OPEN) 17 | exchange = Exchange(channel) 18 | self.assertFalse(exchange.declare()) 19 | 20 | def test_exchange_delete(self): 21 | def on_delete(*_): 22 | channel.rpc.on_frame(pamqp_exchange.DeleteOk()) 23 | 24 | connection = FakeConnection(on_write=on_delete) 25 | channel = Channel(0, connection, 0.01) 26 | channel.set_state(Channel.OPEN) 27 | exchange = Exchange(channel) 28 | self.assertFalse(exchange.delete()) 29 | 30 | def test_exchange_bind(self): 31 | def on_bind(*_): 32 | channel.rpc.on_frame(pamqp_exchange.BindOk()) 33 | 34 | connection = FakeConnection(on_write=on_bind) 35 | channel = Channel(0, connection, 0.01) 36 | channel.set_state(Channel.OPEN) 37 | exchange = Exchange(channel) 38 | self.assertFalse(exchange.bind()) 39 | 40 | def test_exchange_unbind(self): 41 | def on_unbind(*_): 42 | channel.rpc.on_frame(pamqp_exchange.UnbindOk()) 43 | 44 | connection = FakeConnection(on_write=on_unbind) 45 | channel = Channel(0, connection, 0.01) 46 | channel.set_state(Channel.OPEN) 47 | exchange = Exchange(channel) 48 | self.assertFalse(exchange.unbind()) 49 | -------------------------------------------------------------------------------- /amqpstorm/tests/unit/exchange/test_exchange_exception.py: -------------------------------------------------------------------------------- 1 | from amqpstorm import Channel 2 | from amqpstorm import exception 3 | from amqpstorm.exchange import Exchange 4 | from amqpstorm.tests.utility import FakeConnection 5 | from amqpstorm.tests.utility import TestFramework 6 | 7 | 8 | class ExchangeExceptionTests(TestFramework): 9 | def test_exchange_declare_invalid_parameter(self): 10 | channel = Channel(0, FakeConnection(), 360) 11 | channel.set_state(Channel.OPEN) 12 | exchange = Exchange(channel) 13 | 14 | self.assertRaisesRegex( 15 | exception.AMQPInvalidArgument, 16 | 'exchange should be a string', 17 | exchange.declare, None 18 | ) 19 | 20 | self.assertRaisesRegex( 21 | exception.AMQPInvalidArgument, 22 | 'exchange_type should be a string', 23 | exchange.declare, 'travis-ci', None 24 | ) 25 | 26 | self.assertRaisesRegex( 27 | exception.AMQPInvalidArgument, 28 | 'passive should be a boolean', 29 | exchange.declare, 'travis-ci', 'travis-ci', None 30 | ) 31 | 32 | self.assertRaisesRegex( 33 | exception.AMQPInvalidArgument, 34 | 'durable should be a boolean', 35 | exchange.declare, 'travis-ci', 'travis-ci', True, None 36 | ) 37 | 38 | self.assertRaisesRegex( 39 | exception.AMQPInvalidArgument, 40 | 'auto_delete should be a boolean', 41 | exchange.declare, 'travis-ci', 'travis-ci', True, True, None 42 | ) 43 | 44 | self.assertRaisesRegex( 45 | exception.AMQPInvalidArgument, 46 | 'arguments should be a dict or None', 47 | exchange.declare, 'travis-ci', 'travis-ci', True, True, True, [] 48 | ) 49 | 50 | def test_exchange_delete_invalid_parameter(self): 51 | channel = Channel(0, FakeConnection(), 360) 52 | channel.set_state(Channel.OPEN) 53 | exchange = Exchange(channel) 54 | 55 | self.assertRaisesRegex( 56 | exception.AMQPInvalidArgument, 57 | 'exchange should be a string', 58 | exchange.delete, None 59 | ) 60 | 61 | def test_exchange_bind_invalid_parameter(self): 62 | channel = Channel(0, FakeConnection(), 360) 63 | channel.set_state(Channel.OPEN) 64 | exchange = Exchange(channel) 65 | 66 | self.assertRaisesRegex( 67 | exception.AMQPInvalidArgument, 68 | 'destination should be a string', 69 | exchange.bind, None 70 | ) 71 | 72 | self.assertRaisesRegex( 73 | exception.AMQPInvalidArgument, 74 | 'source should be a string', 75 | exchange.bind, '', None 76 | ) 77 | 78 | self.assertRaisesRegex( 79 | exception.AMQPInvalidArgument, 80 | 'routing_key should be a string', 81 | exchange.bind, '', '', None 82 | ) 83 | 84 | self.assertRaisesRegex( 85 | exception.AMQPInvalidArgument, 86 | 'arguments should be a dict or None', 87 | exchange.bind, '', '', '', [] 88 | ) 89 | 90 | def test_exchange_unbind_invalid_parameter(self): 91 | channel = Channel(0, FakeConnection(), 360) 92 | channel.set_state(Channel.OPEN) 93 | exchange = Exchange(channel) 94 | 95 | self.assertRaisesRegex( 96 | exception.AMQPInvalidArgument, 97 | 'destination should be a string', 98 | exchange.unbind, None 99 | ) 100 | 101 | self.assertRaisesRegex( 102 | exception.AMQPInvalidArgument, 103 | 'source should be a string', 104 | exchange.unbind, '', None 105 | ) 106 | 107 | self.assertRaisesRegex( 108 | exception.AMQPInvalidArgument, 109 | 'routing_key should be a string', 110 | exchange.unbind, '', '', None 111 | ) 112 | 113 | self.assertRaisesRegex( 114 | exception.AMQPInvalidArgument, 115 | 'arguments should be a dict or None', 116 | exchange.unbind, '', '', '', [] 117 | ) 118 | -------------------------------------------------------------------------------- /amqpstorm/tests/unit/io/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eandersson/amqpstorm/352c4c8338dd05e563d9e121e5a4fc0874f11bbc/amqpstorm/tests/unit/io/__init__.py -------------------------------------------------------------------------------- /amqpstorm/tests/unit/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eandersson/amqpstorm/352c4c8338dd05e563d9e121e5a4fc0874f11bbc/amqpstorm/tests/unit/management/__init__.py -------------------------------------------------------------------------------- /amqpstorm/tests/unit/management/test_api.py: -------------------------------------------------------------------------------- 1 | from amqpstorm.management import ManagementApi 2 | 3 | from amqpstorm.tests.utility import FakeHTTPClient 4 | from amqpstorm.tests.utility import TestFramework 5 | 6 | 7 | class ApiTests(TestFramework): 8 | def test_api_top(self): 9 | def on_get(name): 10 | if name == 'nodes': 11 | return [{'name': 'node1'}] 12 | elif name == 'top/node1': 13 | return { 14 | 'node': 'node1', 15 | 'processes': [ 16 | { 17 | 'status': u'running' 18 | } 19 | ] 20 | } 21 | 22 | api = ManagementApi('url', 'guest', 'guest') 23 | api.http_client = FakeHTTPClient(on_get) 24 | 25 | top = api.top() 26 | self.assertIsInstance(top, list) 27 | self.assertIsInstance(top[0], dict) 28 | self.assertEqual(top[0]['node'], 'node1') 29 | self.assertIn('processes', top[0]) 30 | -------------------------------------------------------------------------------- /amqpstorm/tests/unit/management/test_basic.py: -------------------------------------------------------------------------------- 1 | from amqpstorm.management.basic import Basic 2 | 3 | from amqpstorm.tests.utility import FakeHTTPClient 4 | from amqpstorm.tests.utility import TestFramework 5 | 6 | 7 | class BasicTests(TestFramework): 8 | def test_basic_get_with_payload(self): 9 | def on_post_with_payload(name): 10 | return [{'payload': name}] 11 | 12 | api = Basic(FakeHTTPClient(on_post=on_post_with_payload)) 13 | 14 | messages = api.get(queue='test') 15 | self.assertEqual(messages[0].body, 'queues/%2F/test/get') 16 | 17 | def test_basic_get_with_body(self): 18 | def on_post_with_body(name): 19 | return [{'body': name}] 20 | 21 | api = Basic(FakeHTTPClient(on_post=on_post_with_body)) 22 | 23 | messages = api.get(queue='test') 24 | self.assertEqual(messages[0].body, 'queues/%2F/test/get') 25 | -------------------------------------------------------------------------------- /amqpstorm/tests/unit/management/test_exception.py: -------------------------------------------------------------------------------- 1 | from amqpstorm.management.exception import ApiError 2 | 3 | from amqpstorm.tests.utility import TestFramework 4 | 5 | 6 | class ApiExceptionTests(TestFramework): 7 | def test_api_exception_error_code_matching(self): 8 | exception = ApiError('travis-ci', reply_code=400) 9 | 10 | self.assertEqual(str(exception), 'travis-ci') 11 | 12 | self.assertEqual(exception.error_code, 400) 13 | 14 | def test_api_exception_unknown_error_code(self): 15 | exception = ApiError('travis-ci', reply_code=123) 16 | 17 | self.assertEqual(str(exception), 'travis-ci') 18 | self.assertEqual(exception.error_code, 123) 19 | 20 | self.assertFalse(exception.error_type) 21 | self.assertFalse(exception.documentation) 22 | 23 | def test_api_exception_no_error_code(self): 24 | exception = ApiError('travis-ci') 25 | 26 | self.assertEqual(str(exception), 'travis-ci') 27 | 28 | self.assertFalse(exception.error_type) 29 | self.assertFalse(exception.error_code) 30 | self.assertFalse(exception.documentation) 31 | -------------------------------------------------------------------------------- /amqpstorm/tests/unit/management/test_http_client.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from amqpstorm.management.http_client import ApiError 4 | from amqpstorm.management.http_client import HTTPClient 5 | from amqpstorm.tests.utility import TestFramework 6 | 7 | 8 | class FakeResponse(object): 9 | """Fake Requests Response for Unit-Testing.""" 10 | 11 | def __init__(self, status_code=200, json=None, raises=None): 12 | self.status_code = status_code 13 | self._json = json 14 | self._raises = raises 15 | 16 | def raise_for_status(self): 17 | if not self._raises: 18 | return 19 | raise self._raises 20 | 21 | def json(self): 22 | if self._raises: 23 | raise self._raises 24 | return self._json 25 | 26 | 27 | class ApiHTTPTests(TestFramework): 28 | def test_api_valid_json(self): 29 | fake_payload = { 30 | 'hello': 'travis-ci' 31 | } 32 | 33 | response = HTTPClient._get_json_output(FakeResponse(json=fake_payload)) 34 | 35 | self.assertEqual(response, { 36 | 'hello': 'travis-ci' 37 | }) 38 | 39 | def test_api_invalid_json(self): 40 | response = HTTPClient._get_json_output( 41 | FakeResponse(raises=ValueError) 42 | ) 43 | self.assertIsNone(response) 44 | 45 | def test_api_http_standard_http_error(self): 46 | fake_payload = { 47 | 'error': 'travis-ci' 48 | } 49 | 50 | self.assertRaisesRegex( 51 | ApiError, 'travis-ci', 52 | HTTPClient._check_for_errors, 53 | FakeResponse(raises=requests.HTTPError('travis-ci')), 54 | fake_payload 55 | ) 56 | 57 | def test_api_http_non_standard_http_error(self): 58 | fake_payload = { 59 | 'error': 'travis-ci' 60 | } 61 | 62 | self.assertRaisesRegex( 63 | ApiError, 'travis-ci', 64 | HTTPClient._check_for_errors, 65 | FakeResponse(), 66 | fake_payload 67 | ) 68 | -------------------------------------------------------------------------------- /amqpstorm/tests/unit/queue/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eandersson/amqpstorm/352c4c8338dd05e563d9e121e5a4fc0874f11bbc/amqpstorm/tests/unit/queue/__init__.py -------------------------------------------------------------------------------- /amqpstorm/tests/unit/queue/test_queue_exception.py: -------------------------------------------------------------------------------- 1 | from amqpstorm import Channel 2 | from amqpstorm import exception 3 | from amqpstorm.queue import Queue 4 | from amqpstorm.tests.utility import FakeConnection 5 | from amqpstorm.tests.utility import TestFramework 6 | 7 | 8 | class QueueExceptionTests(TestFramework): 9 | def test_queue_declare_invalid_parameter(self): 10 | channel = Channel(0, FakeConnection(), 360) 11 | channel.set_state(Channel.OPEN) 12 | queue = Queue(channel) 13 | 14 | self.assertRaisesRegex( 15 | exception.AMQPInvalidArgument, 16 | 'queue should be a string', 17 | queue.declare, None 18 | ) 19 | 20 | self.assertRaisesRegex( 21 | exception.AMQPInvalidArgument, 22 | 'passive should be a boolean', 23 | queue.declare, 'travis-ci', None 24 | ) 25 | 26 | self.assertRaisesRegex( 27 | exception.AMQPInvalidArgument, 28 | 'durable should be a boolean', 29 | queue.declare, 'travis-ci', True, None 30 | ) 31 | 32 | self.assertRaisesRegex( 33 | exception.AMQPInvalidArgument, 34 | 'exclusive should be a boolean', 35 | queue.declare, 'travis-ci', True, True, None 36 | ) 37 | 38 | self.assertRaisesRegex( 39 | exception.AMQPInvalidArgument, 40 | 'auto_delete should be a boolean', 41 | queue.declare, 'travis-ci', True, True, True, None 42 | ) 43 | 44 | self.assertRaisesRegex( 45 | exception.AMQPInvalidArgument, 46 | 'arguments should be a dict or None', 47 | queue.declare, 'travis-ci', True, True, True, True, [] 48 | ) 49 | 50 | def test_queue_delete_invalid_parameter(self): 51 | channel = Channel(0, FakeConnection(), 360) 52 | channel.set_state(Channel.OPEN) 53 | queue = Queue(channel) 54 | 55 | self.assertRaisesRegex( 56 | exception.AMQPInvalidArgument, 57 | 'queue should be a string', 58 | queue.delete, None 59 | ) 60 | 61 | self.assertRaisesRegex( 62 | exception.AMQPInvalidArgument, 63 | 'if_unused should be a boolean', 64 | queue.delete, '', None 65 | ) 66 | 67 | self.assertRaisesRegex( 68 | exception.AMQPInvalidArgument, 69 | 'if_empty should be a boolean', 70 | queue.delete, '', True, None 71 | ) 72 | 73 | def test_queue_purge_invalid_parameter(self): 74 | channel = Channel(0, FakeConnection(), 360) 75 | channel.set_state(Channel.OPEN) 76 | queue = Queue(channel) 77 | 78 | self.assertRaisesRegex( 79 | exception.AMQPInvalidArgument, 80 | 'queue should be a string', 81 | queue.purge, None 82 | ) 83 | 84 | def test_queue_bind_invalid_parameter(self): 85 | channel = Channel(0, FakeConnection(), 360) 86 | channel.set_state(Channel.OPEN) 87 | queue = Queue(channel) 88 | 89 | self.assertRaisesRegex( 90 | exception.AMQPInvalidArgument, 91 | 'queue should be a string', 92 | queue.bind, None 93 | ) 94 | 95 | self.assertRaisesRegex( 96 | exception.AMQPInvalidArgument, 97 | 'exchange should be a string', 98 | queue.bind, '', None 99 | ) 100 | 101 | self.assertRaisesRegex( 102 | exception.AMQPInvalidArgument, 103 | 'routing_key should be a string', 104 | queue.bind, '', '', None 105 | ) 106 | 107 | self.assertRaisesRegex( 108 | exception.AMQPInvalidArgument, 109 | 'arguments should be a dict or None', 110 | queue.bind, '', '', '', [] 111 | ) 112 | 113 | def test_queue_unbind_invalid_parameter(self): 114 | channel = Channel(0, FakeConnection(), 360) 115 | channel.set_state(Channel.OPEN) 116 | queue = Queue(channel) 117 | 118 | self.assertRaisesRegex( 119 | exception.AMQPInvalidArgument, 120 | 'queue should be a string', 121 | queue.unbind, None 122 | ) 123 | 124 | self.assertRaisesRegex( 125 | exception.AMQPInvalidArgument, 126 | 'exchange should be a string', 127 | queue.unbind, '', None 128 | ) 129 | 130 | self.assertRaisesRegex( 131 | exception.AMQPInvalidArgument, 132 | 'routing_key should be a string', 133 | queue.unbind, '', '', None 134 | ) 135 | 136 | self.assertRaisesRegex( 137 | exception.AMQPInvalidArgument, 138 | 'arguments should be a dict or None', 139 | queue.unbind, '', '', '', [] 140 | ) 141 | -------------------------------------------------------------------------------- /amqpstorm/tests/unit/queue/test_queuepy: -------------------------------------------------------------------------------- 1 | from pamqp.specification import Queue as pamqp_queue 2 | 3 | from amqpstorm.channel import Channel 4 | from amqpstorm.channel import Queue 5 | from amqpstorm.tests.utility import FakeConnection 6 | from amqpstorm.tests.utility import TestFramework 7 | 8 | 9 | class QueueTests(TestFramework): 10 | def test_queue_declare(self): 11 | def on_declare(*_): 12 | channel.rpc.on_frame(pamqp_queue.DeclareOk()) 13 | 14 | connection = FakeConnection(on_write=on_declare) 15 | channel = Channel(0, connection, 0.01) 16 | channel.set_state(Channel.OPEN) 17 | queue = Queue(channel) 18 | 19 | expected_result = { 20 | 'queue': '', 21 | 'message_count': 0, 22 | 'consumer_count': 0 23 | } 24 | self.assertEqual(queue.declare(), expected_result) 25 | 26 | def test_queue_delete(self): 27 | def on_delete(*_): 28 | channel.rpc.on_frame(pamqp_queue.DeleteOk()) 29 | 30 | connection = FakeConnection(on_write=on_delete) 31 | channel = Channel(0, connection, 0.01) 32 | channel.set_state(Channel.OPEN) 33 | exchange = Queue(channel) 34 | 35 | self.assertEqual(exchange.delete(), {'message_count': 0}) 36 | 37 | def test_queue_purge(self): 38 | def on_purge(*_): 39 | channel.rpc.on_frame(pamqp_queue.PurgeOk()) 40 | 41 | connection = FakeConnection(on_write=on_purge) 42 | channel = Channel(0, connection, 0.01) 43 | channel.set_state(Channel.OPEN) 44 | exchange = Queue(channel) 45 | 46 | self.assertEqual(exchange.purge('travis_ci'), {'message_count': 0}) 47 | 48 | def test_queue_bind(self): 49 | def on_bind(*_): 50 | channel.rpc.on_frame(pamqp_queue.BindOk()) 51 | 52 | connection = FakeConnection(on_write=on_bind) 53 | channel = Channel(0, connection, 0.01) 54 | channel.set_state(Channel.OPEN) 55 | exchange = Queue(channel) 56 | 57 | self.assertFalse(exchange.bind()) 58 | 59 | def test_queue_unbind(self): 60 | def on_unbind(*_): 61 | channel.rpc.on_frame(pamqp_queue.UnbindOk()) 62 | 63 | connection = FakeConnection(on_write=on_unbind) 64 | channel = Channel(0, connection, 0.01) 65 | channel.set_state(Channel.OPEN) 66 | exchange = Queue(channel) 67 | 68 | self.assertFalse(exchange.unbind()) 69 | -------------------------------------------------------------------------------- /amqpstorm/tests/unit/test_exception.py: -------------------------------------------------------------------------------- 1 | from amqpstorm.exception import AMQPError 2 | 3 | from amqpstorm.tests.utility import TestFramework 4 | 5 | 6 | class ExceptionTests(TestFramework): 7 | def test_exception_documentation_matching(self): 8 | exception = AMQPError('travis-ci', reply_code=312) 9 | 10 | self.assertEqual(str(exception), 'travis-ci') 11 | 12 | self.assertEqual(exception.documentation, 13 | 'Undocumented AMQP Soft Error') 14 | 15 | def test_exception_error_type_matching(self): 16 | exception = AMQPError('travis-ci', reply_code=404) 17 | 18 | self.assertEqual(str(exception), 'travis-ci') 19 | 20 | self.assertEqual(exception.error_type, 21 | 'NOT-FOUND') 22 | 23 | def test_exception_error_code_matching(self): 24 | exception = AMQPError('travis-ci', reply_code=406) 25 | 26 | self.assertEqual(str(exception), 'travis-ci') 27 | 28 | self.assertEqual(exception.error_code, 406) 29 | 30 | def test_exception_unknown_error_code(self): 31 | exception = AMQPError('travis-ci', reply_code=123) 32 | 33 | self.assertEqual(str(exception), 'travis-ci') 34 | self.assertEqual(exception.error_code, 123) 35 | 36 | self.assertFalse(exception.error_type) 37 | self.assertFalse(exception.documentation) 38 | 39 | def test_exception_no_error_code(self): 40 | exception = AMQPError('travis-ci') 41 | 42 | self.assertEqual(str(exception), 'travis-ci') 43 | 44 | self.assertFalse(exception.error_type) 45 | self.assertFalse(exception.error_code) 46 | self.assertFalse(exception.documentation) 47 | -------------------------------------------------------------------------------- /amqpstorm/tests/unit/test_tx.py: -------------------------------------------------------------------------------- 1 | from pamqp import specification 2 | 3 | from amqpstorm.channel import Channel 4 | from amqpstorm.tests.utility import FakeConnection 5 | from amqpstorm.tests.utility import TestFramework 6 | from amqpstorm.tx import Tx 7 | 8 | 9 | class TxTests(TestFramework): 10 | def test_tx_select(self): 11 | def on_tx_select(*_): 12 | channel.rpc.on_frame(specification.Tx.SelectOk()) 13 | 14 | connection = FakeConnection(on_write=on_tx_select) 15 | channel = Channel(0, connection, 0.01) 16 | channel.set_state(Channel.OPEN) 17 | tx = Tx(channel) 18 | 19 | self.assertIsInstance(tx.select(), dict) 20 | self.assertTrue(tx._tx_active) 21 | 22 | def test_tx_commit(self): 23 | def on_tx_commit(*_): 24 | channel.rpc.on_frame(specification.Tx.CommitOk()) 25 | 26 | connection = FakeConnection(on_write=on_tx_commit) 27 | channel = Channel(0, connection, 0.01) 28 | channel.set_state(Channel.OPEN) 29 | tx = Tx(channel) 30 | 31 | self.assertIsInstance(tx.commit(), dict) 32 | self.assertFalse(tx._tx_active) 33 | 34 | def test_tx_rollback(self): 35 | def on_tx_rollback(*_): 36 | channel.rpc.on_frame(specification.Tx.RollbackOk()) 37 | 38 | connection = FakeConnection(on_write=on_tx_rollback) 39 | channel = Channel(0, connection, 0.01) 40 | channel.set_state(Channel.OPEN) 41 | tx = Tx(channel) 42 | 43 | self.assertIsInstance(tx.rollback(), dict) 44 | self.assertFalse(tx._tx_active) 45 | 46 | def test_tx_with_statement(self): 47 | self._active_transaction = False 48 | 49 | def on_tx(*_): 50 | if not self._active_transaction: 51 | channel.rpc.on_frame(specification.Tx.SelectOk()) 52 | self._active_transaction = True 53 | return 54 | self._active_transaction = False 55 | channel.rpc.on_frame(specification.Tx.CommitOk()) 56 | 57 | connection = FakeConnection(on_write=on_tx) 58 | channel = Channel(0, connection, 0.01) 59 | channel.set_state(Channel.OPEN) 60 | tx = Tx(channel) 61 | 62 | with tx: 63 | self.assertTrue(tx._tx_active) 64 | self.assertFalse(tx._tx_active) 65 | 66 | def test_tx_with_statement_already_commited(self): 67 | self._active_transaction = False 68 | 69 | def on_tx(*_): 70 | if not self._active_transaction: 71 | channel.rpc.on_frame(specification.Tx.SelectOk()) 72 | self._active_transaction = True 73 | return 74 | self._active_transaction = False 75 | channel.rpc.on_frame(specification.Tx.CommitOk()) 76 | 77 | connection = FakeConnection(on_write=on_tx) 78 | channel = Channel(0, connection, 0.01) 79 | channel.set_state(Channel.OPEN) 80 | tx = Tx(channel) 81 | 82 | with tx: 83 | tx.commit() 84 | self.assertFalse(tx._tx_active) 85 | self.assertFalse(tx._tx_active) 86 | 87 | def test_tx_with_statement_when_raises(self): 88 | def on_tx(_, frame): 89 | if isinstance(frame, specification.Tx.Select): 90 | channel.rpc.on_frame(specification.Tx.SelectOk()) 91 | return 92 | channel.rpc.on_frame(specification.Tx.CommitOk()) 93 | 94 | connection = FakeConnection(on_write=on_tx) 95 | channel = Channel(0, connection, 0.01) 96 | channel.set_state(Channel.OPEN) 97 | tx = Tx(channel) 98 | 99 | try: 100 | with tx: 101 | tx.commit() 102 | raise Exception('travis-ci') 103 | except Exception: 104 | self.assertEqual(self.get_last_log(), 105 | 'Leaving Transaction on exception: travis-ci') 106 | 107 | self.assertFalse(tx._tx_active) 108 | 109 | def test_tx_with_statement_when_failing(self): 110 | self._active_transaction = False 111 | 112 | def on_tx(*_): 113 | if not self._active_transaction: 114 | channel.rpc.on_frame(specification.Tx.SelectOk()) 115 | self._active_transaction = True 116 | return 117 | self._active_transaction = False 118 | channel.rpc.on_frame(specification.Tx.RollbackOk()) 119 | 120 | connection = FakeConnection(on_write=on_tx) 121 | channel = Channel(0, connection, 0.01) 122 | channel.set_state(Channel.OPEN) 123 | tx = Tx(channel) 124 | 125 | try: 126 | with tx: 127 | self.assertTrue(tx._tx_active) 128 | raise Exception('error') 129 | except Exception as why: 130 | self.assertEqual('error', str(why)) 131 | 132 | self.assertFalse(tx._tx_active) 133 | self.assertEqual(self.get_last_log(), 134 | 'Leaving Transaction on exception: error') 135 | -------------------------------------------------------------------------------- /amqpstorm/tests/unit/uri_connection/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eandersson/amqpstorm/352c4c8338dd05e563d9e121e5a4fc0874f11bbc/amqpstorm/tests/unit/uri_connection/__init__.py -------------------------------------------------------------------------------- /amqpstorm/tests/unit/uri_connection/test_uri_connection_exception.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import ssl 3 | import sys 4 | 5 | from amqpstorm import AMQPConnectionError 6 | from amqpstorm import UriConnection 7 | from amqpstorm import compatibility 8 | from amqpstorm.tests.utility import TestFramework 9 | from amqpstorm.tests.utility import unittest 10 | 11 | 12 | class UriConnectionExceptionTests(TestFramework): 13 | @unittest.skipIf(sys.version_info < (3, 3), 'Python 3.x test') 14 | def test_uri_py3_raises_on_invalid_uri(self): 15 | self.assertRaises(ValueError, UriConnection, 'amqp://a:b', {}, True) 16 | 17 | @unittest.skipIf(sys.version_info[0] == 3, 'Python 2.x test') 18 | def test_uri_py2_raises_on_invalid_uri(self): 19 | self.assertRaises(ValueError, UriConnection, 'amqp://a:b', {}, True) 20 | 21 | def test_uri_raises_on_invalid_object(self): 22 | self.assertRaises(AttributeError, UriConnection, None) 23 | self.assertRaises(AttributeError, UriConnection, {}) 24 | self.assertRaises(AttributeError, UriConnection, []) 25 | self.assertRaises(AttributeError, UriConnection, ()) 26 | 27 | def test_uri_invalid_ssl_options(self): 28 | connection = UriConnection( 29 | 'amqps://guest:guest@localhost:5672/%2F', lazy=True 30 | ) 31 | ssl_kwargs = { 32 | 'unit_test': ['not_required'], 33 | } 34 | ssl_options = connection._parse_ssl_options(ssl_kwargs) 35 | 36 | self.assertFalse(ssl_options) 37 | self.assertIn("invalid option: unit_test", 38 | self.get_last_log()) 39 | 40 | def test_uri_get_invalid_ssl_version(self): 41 | connection = UriConnection( 42 | 'amqps://guest:guest@localhost:5672/%2F', lazy=True 43 | ) 44 | 45 | self.assertEqual(connection._get_ssl_version('protocol_test'), 46 | ssl.PROTOCOL_TLSv1) 47 | self.assertIn("ssl_options: ssl_version 'protocol_test' not found " 48 | "falling back to PROTOCOL_TLSv1.", 49 | self.get_last_log()) 50 | 51 | def test_uri_get_invalid_ssl_validation(self): 52 | connection = UriConnection( 53 | 'amqps://guest:guest@localhost:5672/%2F', lazy=True 54 | ) 55 | 56 | self.assertEqual(ssl.CERT_NONE, 57 | connection._get_ssl_validation('cert_test')) 58 | self.assertIn("ssl_options: cert_reqs 'cert_test' not found " 59 | "falling back to CERT_NONE.", 60 | self.get_last_log()) 61 | 62 | @unittest.skipIf(sys.version_info < (3, 3), 'Python 3.x test') 63 | def test_uri_ssl_not_supported(self): 64 | restore_func = sys.modules['ssl'] 65 | try: 66 | sys.modules['ssl'] = None 67 | importlib.reload(compatibility) 68 | self.assertIsNone(compatibility.ssl) 69 | self.assertRaisesRegex( 70 | AMQPConnectionError, 71 | 'Python not compiled with support for TLSv1 or higher', 72 | UriConnection, 'amqps://localhost:5672/%2F' 73 | ) 74 | finally: 75 | sys.modules['ssl'] = restore_func 76 | importlib.reload(compatibility) 77 | -------------------------------------------------------------------------------- /amqpstorm/tx.py: -------------------------------------------------------------------------------- 1 | """AMQPStorm Channel.Tx.""" 2 | 3 | import logging 4 | 5 | from pamqp import specification 6 | 7 | from amqpstorm.base import Handler 8 | 9 | LOGGER = logging.getLogger(__name__) 10 | 11 | 12 | class Tx(Handler): 13 | """RabbitMQ Transactions. 14 | 15 | Server local transactions, in which the server will buffer published 16 | messages until the client commits (or rollback) the messages. 17 | 18 | """ 19 | __slots__ = ['_tx_active'] 20 | 21 | def __init__(self, channel): 22 | self._tx_active = True 23 | super(Tx, self).__init__(channel) 24 | 25 | def __enter__(self): 26 | self.select() 27 | return self 28 | 29 | def __exit__(self, exception_type, exception_value, _): 30 | if exception_type: 31 | LOGGER.warning( 32 | 'Leaving Transaction on exception: %s', 33 | exception_value 34 | ) 35 | if self._tx_active: 36 | self.rollback() 37 | return 38 | if self._tx_active: 39 | self.commit() 40 | 41 | def select(self): 42 | """Enable standard transaction mode. 43 | 44 | This will enable transaction mode on the channel. Meaning that 45 | messages will be kept in the remote server buffer until such a 46 | time that either commit or rollback is called. 47 | 48 | :return: 49 | """ 50 | self._tx_active = True 51 | return self._channel.rpc_request(specification.Tx.Select()) 52 | 53 | def commit(self): 54 | """Commit the current transaction. 55 | 56 | Commit all messages published during the current transaction 57 | session to the remote server. 58 | 59 | A new transaction session starts as soon as the command has 60 | been executed. 61 | 62 | :return: 63 | """ 64 | self._tx_active = False 65 | return self._channel.rpc_request(specification.Tx.Commit()) 66 | 67 | def rollback(self): 68 | """Abandon the current transaction. 69 | 70 | Rollback all messages published during the current transaction 71 | session to the remote server. 72 | 73 | Note that all messages published during this transaction session 74 | will be lost, and will have to be published again. 75 | 76 | A new transaction session starts as soon as the command has 77 | been executed. 78 | 79 | :return: 80 | """ 81 | self._tx_active = False 82 | return self._channel.rpc_request(specification.Tx.Rollback()) 83 | -------------------------------------------------------------------------------- /amqpstorm/uri_connection.py: -------------------------------------------------------------------------------- 1 | """AMQPStorm Uri wrapper for Connection.""" 2 | 3 | import logging 4 | 5 | from amqpstorm import compatibility 6 | from amqpstorm.compatibility import ssl 7 | from amqpstorm.compatibility import urlparse 8 | from amqpstorm.connection import Connection 9 | from amqpstorm.connection import DEFAULT_HEARTBEAT_TIMEOUT 10 | from amqpstorm.connection import DEFAULT_SOCKET_TIMEOUT 11 | from amqpstorm.connection import DEFAULT_VIRTUAL_HOST 12 | from amqpstorm.exception import AMQPConnectionError 13 | 14 | LOGGER = logging.getLogger(__name__) 15 | 16 | 17 | class UriConnection(Connection): 18 | """RabbitMQ Connection that takes a Uri string. 19 | 20 | e.g. 21 | :: 22 | 23 | import amqpstorm 24 | connection = amqpstorm.UriConnection( 25 | 'amqp://guest:guest@localhost:5672/%2F?heartbeat=60' 26 | ) 27 | 28 | Using a SSL Context: 29 | :: 30 | 31 | import ssl 32 | import amqpstorm 33 | ssl_options = { 34 | 'context': ssl.create_default_context(cafile='ca_certificate.pem'), 35 | 'server_hostname': 'rmq.eandersson.net' 36 | } 37 | connection = amqpstorm.UriConnection( 38 | 'amqps://guest:guest@rmq.eandersson.net:5671/%2F?heartbeat=60', 39 | ssl_options=ssl_options 40 | ) 41 | 42 | :param str uri: AMQP Connection string 43 | :param dict ssl_options: SSL kwargs 44 | :param dict client_properties: None or dict of client properties 45 | :param bool lazy: Lazy initialize the connection 46 | 47 | :raises TypeError: Raises on invalid uri. 48 | :raises ValueError: Raises on invalid uri. 49 | :raises AttributeError: Raises on invalid uri. 50 | :raises AMQPConnectionError: Raises if the connection 51 | encountered an error. 52 | """ 53 | __slots__ = [] 54 | 55 | def __init__(self, uri, ssl_options=None, client_properties=None, 56 | lazy=False): 57 | uri = compatibility.patch_uri(uri) 58 | parsed_uri = urlparse.urlparse(uri) 59 | use_ssl = parsed_uri.scheme == 'amqps' or parsed_uri.scheme == 'https' 60 | hostname = parsed_uri.hostname or 'localhost' 61 | port = parsed_uri.port or (5671 if use_ssl else 5672) 62 | username = urlparse.unquote(parsed_uri.username or 'guest') 63 | password = urlparse.unquote(parsed_uri.password or 'guest') 64 | kwargs = self._parse_uri_options(parsed_uri, use_ssl, ssl_options) 65 | super(UriConnection, self).__init__( 66 | hostname, username, password, port, 67 | client_properties=client_properties, 68 | lazy=lazy, 69 | **kwargs 70 | ) 71 | 72 | def _parse_uri_options(self, parsed_uri, use_ssl=False, ssl_options=None): 73 | """Parse the uri options. 74 | 75 | :param parsed_uri: 76 | :param bool use_ssl: 77 | :return: 78 | """ 79 | ssl_options = ssl_options or {} 80 | kwargs = urlparse.parse_qs(parsed_uri.query) 81 | vhost = urlparse.unquote(parsed_uri.path[1:]) or DEFAULT_VIRTUAL_HOST 82 | options = { 83 | 'ssl': use_ssl, 84 | 'virtual_host': vhost, 85 | 'heartbeat': int(kwargs.pop('heartbeat', 86 | [DEFAULT_HEARTBEAT_TIMEOUT])[0]), 87 | 'poller': kwargs.pop('poller', 88 | ['select'])[0], 89 | 'timeout': int(kwargs.pop('timeout', 90 | [DEFAULT_SOCKET_TIMEOUT])[0]) 91 | } 92 | if use_ssl: 93 | if not compatibility.SSL_SUPPORTED: 94 | raise AMQPConnectionError( 95 | 'Python not compiled with support ' 96 | 'for TLSv1 or higher' 97 | ) 98 | ssl_options.update(self._parse_ssl_options(kwargs)) 99 | options['ssl_options'] = ssl_options 100 | return options 101 | 102 | def _parse_ssl_options(self, ssl_kwargs): 103 | """Parse TLS Options. 104 | 105 | :param ssl_kwargs: 106 | :rtype: dict 107 | """ 108 | ssl_options = {} 109 | for key in ssl_kwargs: 110 | if key not in compatibility.SSL_OPTIONS: 111 | LOGGER.warning('invalid option: %s', key) 112 | continue 113 | if 'ssl_version' in key: 114 | value = self._get_ssl_version(ssl_kwargs[key][0]) 115 | elif 'cert_reqs' in key: 116 | value = self._get_ssl_validation(ssl_kwargs[key][0]) 117 | else: 118 | value = ssl_kwargs[key][0] 119 | ssl_options[key] = value 120 | return ssl_options 121 | 122 | def _get_ssl_version(self, value): 123 | """Get the TLS Version. 124 | 125 | :param str value: 126 | :return: TLS Version 127 | """ 128 | return self._get_ssl_attribute(value, compatibility.SSL_VERSIONS, 129 | ssl.PROTOCOL_TLSv1, 130 | 'ssl_options: ssl_version \'%s\' not ' 131 | 'found falling back to PROTOCOL_TLSv1.') 132 | 133 | def _get_ssl_validation(self, value): 134 | """Get the TLS Validation option. 135 | 136 | :param str value: 137 | :return: TLS Certificate Options 138 | """ 139 | return self._get_ssl_attribute(value, compatibility.SSL_CERT_MAP, 140 | ssl.CERT_NONE, 141 | 'ssl_options: cert_reqs \'%s\' not ' 142 | 'found falling back to CERT_NONE.') 143 | 144 | @staticmethod 145 | def _get_ssl_attribute(value, mapping, default_value, warning_message): 146 | """Get the TLS attribute based on the compatibility mapping. 147 | 148 | If no valid attribute can be found, fall-back on default and 149 | display a warning. 150 | 151 | :param str value: 152 | :param dict mapping: Dictionary based mapping 153 | :param default_value: Default fall-back value 154 | :param str warning_message: Warning message 155 | :return: 156 | """ 157 | for key in mapping: 158 | if not key.endswith(value.lower()): 159 | continue 160 | return mapping[key] 161 | LOGGER.warning(warning_message, value) 162 | return default_value 163 | -------------------------------------------------------------------------------- /doc-requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx_rtd_theme 3 | pamqp>=2.0.0,<3.0 4 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rabbitmq:management 2 | 3 | env RABBITMQ_USE_LONGNAME=true 4 | RUN mkdir -p /etc/rabbitmq/ssl/ 5 | 6 | COPY ./files/rabbitmq.conf /etc/rabbitmq/rabbitmq.conf 7 | COPY ./files/wait-for-rabbitmq /bin/wait-for-rabbitmq 8 | COPY ./files/generate-certs /etc/rabbitmq/ssl/generate-certs 9 | COPY ./files/openssl.cnf /etc/rabbitmq/ssl/openssl.cnf 10 | 11 | RUN /etc/rabbitmq/ssl/generate-certs && \ 12 | chown -R rabbitmq:rabbitmq /etc/rabbitmq/ssl/* 13 | -------------------------------------------------------------------------------- /docker/files/generate-certs: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | mkdir -p /etc/rabbitmq/ssl/certs 3 | mkdir -p /etc/rabbitmq/ssl/client 4 | mkdir -p /etc/rabbitmq/ssl/private 5 | mkdir -p /etc/rabbitmq/ssl/server 6 | 7 | echo 01 > /etc/rabbitmq/ssl/serial 8 | touch /etc/rabbitmq/ssl/index.txt 9 | 10 | cd /etc/rabbitmq/ssl/ 11 | openssl req -x509 -config openssl.cnf -newkey rsa:4096 -days 365 -out ca_certificate.pem -outform PEM -subj /CN=rmq.eandersson.net/ -nodes 12 | openssl x509 -in ca_certificate.pem -out ca_certificate.cer -outform DER 13 | 14 | cd /etc/rabbitmq/ssl/server 15 | openssl genrsa -out private_key.pem 4096 16 | openssl req -new -key private_key.pem -out req.pem -outform PEM -subj /CN=rmq.eandersson.net/O=server/ -nodes 17 | 18 | cd /etc/rabbitmq/ssl/ 19 | openssl ca -config openssl.cnf -in server/req.pem -out server/server_certificate.pem -notext -batch -extensions server_ca_extensions 20 | 21 | cd /etc/rabbitmq/ssl/client 22 | openssl genrsa -out private_key.pem 4096 23 | openssl req -new -key private_key.pem -out req.pem -outform PEM -subj /CN=rmq.eandersson.net/O=client/ -nodes 24 | 25 | cd /etc/rabbitmq/ssl/ 26 | openssl ca -config openssl.cnf -in client/req.pem -out client/client_certificate.pem -notext -batch -extensions client_ca_extensions 27 | -------------------------------------------------------------------------------- /docker/files/openssl.cnf: -------------------------------------------------------------------------------- 1 | [ ca ] 2 | default_ca = testca 3 | 4 | [ testca ] 5 | dir = /etc/rabbitmq/ssl 6 | certificate = $dir/ca_certificate.pem 7 | database = $dir/index.txt 8 | new_certs_dir = $dir/certs 9 | private_key = $dir/private/ca_private_key.pem 10 | serial = $dir/serial 11 | 12 | default_crl_days = 7 13 | default_days = 365 14 | default_md = sha512 15 | 16 | policy = testca_policy 17 | x509_extensions = certificate_extensions 18 | 19 | [ testca_policy ] 20 | commonName = supplied 21 | stateOrProvinceName = optional 22 | countryName = optional 23 | emailAddress = optional 24 | organizationName = optional 25 | organizationalUnitName = optional 26 | domainComponent = optional 27 | 28 | [ certificate_extensions ] 29 | basicConstraints = CA:false 30 | 31 | [ req ] 32 | default_bits = 4096 33 | default_keyfile = ./private/ca_private_key.pem 34 | default_md = sha512 35 | prompt = yes 36 | distinguished_name = root_ca_distinguished_name 37 | x509_extensions = root_ca_extensions 38 | 39 | [ root_ca_distinguished_name ] 40 | commonName = hostname 41 | 42 | [ root_ca_extensions ] 43 | basicConstraints = critical, CA:TRUE 44 | keyUsage = keyCertSign, cRLSign 45 | 46 | [ client_ca_extensions ] 47 | basicConstraints=critical, CA:TRUE 48 | keyUsage = critical, cRLSign, digitalSignature, keyCertSign 49 | extendedKeyUsage = 1.3.6.1.5.5.7.3.2 50 | 51 | [ server_ca_extensions ] 52 | basicConstraints=critical, CA:TRUE 53 | keyUsage = critical, cRLSign, digitalSignature, keyCertSign 54 | extendedKeyUsage = 1.3.6.1.5.5.7.3.1 55 | subjectAltName = DNS:rmq.eandersson.net 56 | 57 | -------------------------------------------------------------------------------- /docker/files/rabbitmq.conf: -------------------------------------------------------------------------------- 1 | log.console = true 2 | log.console.level = debug 3 | log.connection.level = debug 4 | 5 | listeners.ssl.default = 5671 6 | listeners.tcp.default = 5672 7 | 8 | ssl_options.cacertfile = /etc/rabbitmq/ssl/ca_certificate.pem 9 | ssl_options.certfile = /etc/rabbitmq/ssl/server/server_certificate.pem 10 | ssl_options.keyfile = /etc/rabbitmq/ssl/server/private_key.pem 11 | ssl_options.verify = verify_peer 12 | ssl_options.fail_if_no_peer_cert = true 13 | 14 | ssl_options.versions.1 = tlsv1.3 15 | 16 | management.tcp.port = 15672 17 | management.ssl.port = 15671 18 | management.ssl.cacertfile = /etc/rabbitmq/ssl/ca_certificate.pem 19 | management.ssl.certfile = /etc/rabbitmq/ssl/server/server_certificate.pem 20 | management.ssl.keyfile = /etc/rabbitmq/ssl/server/private_key.pem 21 | 22 | management.ssl.versions.1 = tlsv1.3 23 | -------------------------------------------------------------------------------- /docker/files/wait-for-rabbitmq: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | while [ ! -f /var/lib/rabbitmq/mnesia/rabbit@rmq.eandersson.net.pid ]; do sleep 1; done 3 | rabbitmqctl --timeout 16 wait /var/lib/rabbitmq/mnesia/rabbit@rmq.eandersson.net.pid --longnames -------------------------------------------------------------------------------- /docs/_static/override.css: -------------------------------------------------------------------------------- 1 | blockquote { 2 | font-size: 15px; 3 | } 4 | 5 | p { 6 | margin: 0 5px 10.5px; 7 | } 8 | 9 | .table>thead>tr>th, 10 | .table>tbody>tr>th, 11 | .table>tfoot>tr>th, 12 | .table>thead>tr>td, 13 | .table>tbody>tr>td, 14 | .table>tfoot>tr>td { 15 | padding: 10px; 16 | line-height: 1.42857143; 17 | vertical-align: top; 18 | border-top: 1px solid #dddddd; 19 | } -------------------------------------------------------------------------------- /docs/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {# Import the theme's layout. #} 2 | {% extends "!layout.html" %} 3 | 4 | {# Custom CSS overrides #} 5 | {% set bootswatch_css_custom = ['_static/override.css'] %} 6 | -------------------------------------------------------------------------------- /docs/api_usage/api.rst: -------------------------------------------------------------------------------- 1 | Management Api 2 | -------------- 3 | 4 | .. autoclass:: amqpstorm.management.ManagementApi 5 | :members: basic, channel, connection, exchange, queue, user, aliveness_test, overview, nodes, top, whoami 6 | 7 | .. autoclass:: amqpstorm.management.basic.Basic 8 | :members: 9 | 10 | .. autoclass:: amqpstorm.management.channel.Channel 11 | :members: 12 | 13 | .. autoclass:: amqpstorm.management.connection.Connection 14 | :members: 15 | 16 | .. autoclass:: amqpstorm.management.exchange.Exchange 17 | :members: 18 | 19 | .. autoclass:: amqpstorm.management.queue.Queue 20 | :members: 21 | 22 | .. autoclass:: amqpstorm.management.user.User 23 | :members: 24 | 25 | .. autoclass:: amqpstorm.management.virtual_host.VirtualHost 26 | :members: 27 | -------------------------------------------------------------------------------- /docs/api_usage/exception.rst: -------------------------------------------------------------------------------- 1 | Management Api Exceptions 2 | ------------------------- 3 | 4 | .. autoclass:: amqpstorm.management.ApiConnectionError 5 | :members: 6 | 7 | .. autoclass:: amqpstorm.management.ApiError 8 | :members: 9 | -------------------------------------------------------------------------------- /docs/examples/flask_rpc_client.rst: -------------------------------------------------------------------------------- 1 | Flask RPC Client 2 | ---------------- 3 | .. literalinclude:: ../../examples/flask_threaded_rpc_client.py -------------------------------------------------------------------------------- /docs/examples/robust_consumer.rst: -------------------------------------------------------------------------------- 1 | Robust Consumer 2 | --------------- 3 | .. literalinclude:: ../../examples/robust_consumer.py -------------------------------------------------------------------------------- /docs/examples/simple_consumer.rst: -------------------------------------------------------------------------------- 1 | Simple Consumer 2 | --------------- 3 | .. literalinclude:: ../../examples/simple_consumer.py -------------------------------------------------------------------------------- /docs/examples/simple_publisher.rst: -------------------------------------------------------------------------------- 1 | Simple Publisher 2 | ---------------- 3 | .. literalinclude:: ../../examples/simple_publisher.py -------------------------------------------------------------------------------- /docs/examples/simple_rpc_client.rst: -------------------------------------------------------------------------------- 1 | Simple RPC Client 2 | ----------------- 3 | .. literalinclude:: ../../examples/simple_rpc_client.py -------------------------------------------------------------------------------- /docs/examples/simple_rpc_server.rst: -------------------------------------------------------------------------------- 1 | Simple RPC Server 2 | ----------------- 3 | .. literalinclude:: ../../examples/simple_rpc_server.py -------------------------------------------------------------------------------- /docs/examples/ssl_with_context.rst: -------------------------------------------------------------------------------- 1 | SSL connection 2 | -------------- 3 | .. literalinclude:: ../../examples/ssl_with_context.py -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. amqpstorm documentation master file, created by 2 | sphinx-quickstart on Sun Apr 10 16:25:24 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | AMQPStorm Documentation 7 | ======================= 8 | Thread-safe Python RabbitMQ Client & Management library. 9 | 10 | Installation 11 | ------------ 12 | The latest version can be installed using `pip `_ and is available at pypi `here `_ 13 | :: 14 | 15 | pip install amqpstorm 16 | 17 | 18 | You can also install AMQPStorm with the management dependencies using. 19 | :: 20 | 21 | pip install amqpstorm[management] 22 | 23 | You can also install AMQPStorm with the pool dependencies using. 24 | :: 25 | 26 | pip install amqpstorm[pool] 27 | 28 | 29 | Basic Example 30 | ------------- 31 | 32 | :: 33 | 34 | with amqpstorm.Connection('rmq.eandersson.net', 'guest', 'guest') as connection: 35 | with connection.channel() as channel: 36 | channel.queue.declare('fruits') 37 | message = amqpstorm.Message.create( 38 | channel, body='Hello RabbitMQ!', properties={ 39 | 'content_type': 'text/plain' 40 | }) 41 | message.publish('fruits') 42 | 43 | Additional Examples 44 | ------------------- 45 | 46 | A wide verity of examples are available on Github at `here `_ 47 | 48 | .. toctree:: 49 | :caption: Usage 50 | :name: usage 51 | 52 | usage/connection 53 | usage/channel 54 | usage/exceptions 55 | usage/message 56 | 57 | .. toctree:: 58 | :caption: Management API Usage 59 | :name: api_usage 60 | 61 | api_usage/api 62 | api_usage/exception 63 | 64 | .. toctree:: 65 | :glob: 66 | :caption: Examples 67 | :name: examples 68 | 69 | examples/* 70 | 71 | .. toctree:: 72 | :glob: 73 | :caption: Pool Examples 74 | :name: pool_examples 75 | 76 | pool_examples/* 77 | 78 | .. toctree:: 79 | :glob: 80 | :caption: Management Examples 81 | :name: management_examples 82 | 83 | management_examples/* 84 | 85 | Issues 86 | ------ 87 | Please report any issues on Github `here `_ 88 | 89 | Source 90 | ------ 91 | AMQPStorm source code is available on Github `here `_ 92 | 93 | Indices and tables 94 | ================== 95 | 96 | * :ref:`genindex` 97 | * :ref:`modindex` 98 | * :ref:`search` -------------------------------------------------------------------------------- /docs/management_examples/aliveness_test.rst: -------------------------------------------------------------------------------- 1 | RabbitMQ Aliveness Test 2 | ----------------------- 3 | In order to use the management module you first need to install the python library, `requests`. 4 | :: 5 | 6 | pip install amqpstorm[management] 7 | 8 | .. literalinclude:: ../../examples/management/aliveness_test.py -------------------------------------------------------------------------------- /docs/management_examples/create_user.rst: -------------------------------------------------------------------------------- 1 | Create a RabbitMQ User 2 | ---------------------- 3 | In order to use the management module you first need to install the python library, `requests`. 4 | :: 5 | 6 | pip install amqpstorm[management] 7 | 8 | .. literalinclude:: ../../examples/management/create_user.py -------------------------------------------------------------------------------- /docs/management_examples/create_virtual_host.rst: -------------------------------------------------------------------------------- 1 | Create a RabbitMQ Virtual Host 2 | ------------------------------ 3 | In order to use the management module you first need to install the python library, `requests`. 4 | :: 5 | 6 | pip install amqpstorm[management] 7 | 8 | .. literalinclude:: ../../examples/management/create_virtual_host.py -------------------------------------------------------------------------------- /docs/management_examples/declare_queue.rst: -------------------------------------------------------------------------------- 1 | Create a RabbitMQ Queue 2 | ----------------------- 3 | In order to use the management module you first need to install the python library, `requests`. 4 | :: 5 | 6 | pip install amqpstorm[management] 7 | 8 | .. literalinclude:: ../../examples/management/declare_queue.py -------------------------------------------------------------------------------- /docs/management_examples/delete_queue.rst: -------------------------------------------------------------------------------- 1 | Delete a RabbitMQ Queue 2 | ----------------------- 3 | In order to use the management module you first need to install the python library, `requests`. 4 | :: 5 | 6 | pip install amqpstorm[management] 7 | 8 | .. literalinclude:: ../../examples/management/delete_queue.py -------------------------------------------------------------------------------- /docs/management_examples/does_queue_exist.rst: -------------------------------------------------------------------------------- 1 | Check if a RabbitMQ Queue exists 2 | -------------------------------- 3 | In order to use the management module you first need to install the python library, `requests`. 4 | :: 5 | 6 | pip install amqpstorm[management] 7 | 8 | .. literalinclude:: ../../examples/management/does_queue_exist.py -------------------------------------------------------------------------------- /docs/management_examples/get_user.rst: -------------------------------------------------------------------------------- 1 | Get a RabbitMQ User 2 | ------------------- 3 | In order to use the management module you first need to install the python library, `requests`. 4 | :: 5 | 6 | pip install amqpstorm[management] 7 | 8 | .. literalinclude:: ../../examples/management/get_user.py -------------------------------------------------------------------------------- /docs/management_examples/list_queues.rst: -------------------------------------------------------------------------------- 1 | List RabbitMQ Queues 2 | -------------------- 3 | In order to use the management module you first need to install the python library, `requests`. 4 | :: 5 | 6 | pip install amqpstorm[management] 7 | 8 | .. literalinclude:: ../../examples/management/list_queues.py -------------------------------------------------------------------------------- /docs/management_examples/overview.rst: -------------------------------------------------------------------------------- 1 | Get a RabbitMQ Overview 2 | ----------------------- 3 | In order to use the management module you first need to install the python library, `requests`. 4 | :: 5 | 6 | pip install amqpstorm[management] 7 | 8 | .. literalinclude:: ../../examples/management/overview.py -------------------------------------------------------------------------------- /docs/pool_examples/pool_example.rst: -------------------------------------------------------------------------------- 1 | Pool Example 2 | ------------ 3 | In order to use the pool module you first need to install the python library, `amqpstorm-pool`. 4 | :: 5 | 6 | pip install amqpstorm[pool] 7 | 8 | .. literalinclude:: ../../examples/pool/pool_example.py -------------------------------------------------------------------------------- /docs/pool_examples/ssl_pool_example.rst: -------------------------------------------------------------------------------- 1 | SSL Pool Example 2 | ---------------- 3 | In order to use the pool module you first need to install the python library, `amqpstorm-pool`. 4 | :: 5 | 6 | pip install amqpstorm[pool] 7 | 8 | .. literalinclude:: ../../examples/pool/ssl_pool_example.py -------------------------------------------------------------------------------- /docs/usage/channel.rst: -------------------------------------------------------------------------------- 1 | Channel 2 | ------- 3 | 4 | .. autoclass:: amqpstorm.Channel 5 | :members: basic, exchange, queue, tx, close, confirm_deliveries, start_consuming, stop_consuming, process_data_events, build_inbound_messages, check_for_errors, check_for_exceptions 6 | 7 | Channel.Basic 8 | ------------- 9 | 10 | .. autoclass:: amqpstorm.basic.Basic 11 | :members: 12 | 13 | Channel.Exchange 14 | ---------------- 15 | 16 | .. autoclass:: amqpstorm.exchange.Exchange 17 | :members: 18 | 19 | Channel.Queue 20 | ------------- 21 | 22 | .. autoclass:: amqpstorm.queue.Queue 23 | :members: 24 | 25 | Channel.Tx 26 | ---------- 27 | 28 | .. autoclass:: amqpstorm.tx.Tx 29 | :members: 30 | -------------------------------------------------------------------------------- /docs/usage/connection.rst: -------------------------------------------------------------------------------- 1 | Connection 2 | ---------- 3 | 4 | .. autoclass:: amqpstorm.Connection 5 | :members: is_blocked, server_properties, socket, fileno, open, close, channel, channels, check_for_errors, max_allowed_channels, max_frame_size 6 | 7 | UriConnection 8 | ------------- 9 | 10 | .. autoclass:: amqpstorm.UriConnection 11 | :members: 12 | -------------------------------------------------------------------------------- /docs/usage/exceptions.rst: -------------------------------------------------------------------------------- 1 | Exceptions 2 | ---------- 3 | 4 | .. autoclass:: amqpstorm.AMQPError 5 | :members: 6 | 7 | .. autoclass:: amqpstorm.AMQPConnectionError 8 | :members: 9 | 10 | .. autoclass:: amqpstorm.AMQPChannelError 11 | :members: 12 | 13 | .. autoclass:: amqpstorm.AMQPMessageError 14 | :members: 15 | 16 | .. autoclass:: amqpstorm.AMQPInvalidArgument 17 | :members: 18 | -------------------------------------------------------------------------------- /docs/usage/message.rst: -------------------------------------------------------------------------------- 1 | Message 2 | ------- 3 | 4 | .. autoclass:: amqpstorm.Message 5 | :members: 6 | 7 | -------------------------------------------------------------------------------- /examples/consume_queue_until_empty.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from amqpstorm import Connection 4 | 5 | logging.basicConfig(level=logging.INFO) 6 | 7 | 8 | def consume_until_queue_is_empty(): 9 | with Connection('localhost', 'guest', 'guest') as connection: 10 | with connection.channel() as channel: 11 | while True: 12 | message = channel.basic.get('simple_queue') 13 | if not message: 14 | print('Queue is empty') 15 | break 16 | print(message.body) 17 | message.ack() 18 | 19 | 20 | if __name__ == '__main__': 21 | consume_until_queue_is_empty() 22 | -------------------------------------------------------------------------------- /examples/create_queue_with_a_ttl_on_messages.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from amqpstorm import Connection 4 | from amqpstorm import Message 5 | 6 | logging.basicConfig(level=logging.INFO) 7 | 8 | 9 | with Connection('localhost', 'guest', 'guest') as connection: 10 | with connection.channel() as channel: 11 | # Declare a queue called, 'simple_queue' with 12 | # the message ttl set to 6000ms. 13 | channel.queue.declare('simple_ttl_queue', arguments={ 14 | 'x-message-ttl': 6000, 15 | }) 16 | 17 | # Create a message. 18 | message = Message.create(channel, 'Hello World') 19 | 20 | # Publish the message to a queue. 21 | message.publish('simple_ttl_queue') 22 | -------------------------------------------------------------------------------- /examples/flask_threaded_rpc_client.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example of a Flask web application using RabbitMQ for RPC calls. 3 | """ 4 | import threading 5 | from time import sleep 6 | 7 | import amqpstorm 8 | from amqpstorm import Message 9 | from flask import Flask 10 | 11 | APP = Flask(__name__) 12 | 13 | 14 | class RpcClient(object): 15 | """Asynchronous Rpc client.""" 16 | 17 | def __init__(self, host, username, password, rpc_queue): 18 | self.queue = {} 19 | self.host = host 20 | self.username = username 21 | self.password = password 22 | self.channel = None 23 | self.connection = None 24 | self.callback_queue = None 25 | self.rpc_queue = rpc_queue 26 | self.open() 27 | 28 | def open(self): 29 | """Open Connection.""" 30 | self.connection = amqpstorm.Connection(self.host, self.username, 31 | self.password) 32 | self.channel = self.connection.channel() 33 | self.channel.queue.declare(self.rpc_queue) 34 | result = self.channel.queue.declare(exclusive=True) 35 | self.callback_queue = result['queue'] 36 | self.channel.basic.consume(self._on_response, no_ack=True, 37 | queue=self.callback_queue) 38 | self._create_process_thread() 39 | 40 | def _create_process_thread(self): 41 | """Create a thread responsible for consuming messages in response 42 | RPC requests. 43 | """ 44 | thread = threading.Thread(target=self._process_data_events) 45 | thread.setDaemon(True) 46 | thread.start() 47 | 48 | def _process_data_events(self): 49 | """Process Data Events using the Process Thread.""" 50 | self.channel.start_consuming() 51 | 52 | def _on_response(self, message): 53 | """On Response store the message with the correlation id in a local 54 | dictionary. 55 | """ 56 | self.queue[message.correlation_id] = message.body 57 | 58 | def send_request(self, payload): 59 | # Create the Message object. 60 | message = Message.create(self.channel, payload) 61 | message.reply_to = self.callback_queue 62 | 63 | # Create an entry in our local dictionary, using the automatically 64 | # generated correlation_id as our key. 65 | self.queue[message.correlation_id] = None 66 | 67 | # Publish the RPC request. 68 | message.publish(routing_key=self.rpc_queue) 69 | 70 | # Return the Unique ID used to identify the request. 71 | return message.correlation_id 72 | 73 | 74 | @APP.route('/rpc_call/') 75 | def rpc_call(payload): 76 | """Simple Flask implementation for making asynchronous Rpc calls. """ 77 | 78 | # Send the request and store the requests Unique ID. 79 | corr_id = RPC_CLIENT.send_request(payload) 80 | 81 | # Wait until we have received a response. 82 | # TODO: Add a timeout here and clean up if it fails! 83 | while RPC_CLIENT.queue[corr_id] is None: 84 | sleep(0.1) 85 | 86 | # Return the response to the user. 87 | return RPC_CLIENT.queue.pop(corr_id) 88 | 89 | 90 | if __name__ == '__main__': 91 | RPC_CLIENT = RpcClient('localhost', 'guest', 'guest', 'rpc_queue') 92 | APP.run() 93 | -------------------------------------------------------------------------------- /examples/management/aliveness_test.py: -------------------------------------------------------------------------------- 1 | from amqpstorm import management 2 | 3 | if __name__ == '__main__': 4 | # If using a self-signed certificate, change verify=True to point at your CA bundle. 5 | # You can disable certificate verification for testing by passing in verify=False. 6 | API = management.ManagementApi('https://rmq.eandersson.net:15671', 'guest', 7 | 'guest', verify=True) 8 | try: 9 | result = API.aliveness_test('/') 10 | if result['status'] == 'ok': 11 | print('RabbitMQ is alive!') 12 | else: 13 | print('RabbitMQ is not alive! :(') 14 | except management.ApiConnectionError as why: 15 | print('Connection Error: %s' % why) 16 | except management.ApiError as why: 17 | print('ApiError: %s' % why) 18 | -------------------------------------------------------------------------------- /examples/management/create_user.py: -------------------------------------------------------------------------------- 1 | from amqpstorm import management 2 | 3 | if __name__ == '__main__': 4 | # If using a self-signed certificate, change verify=True to point at your CA bundle. 5 | # You can disable certificate verification for testing by passing in verify=False. 6 | API = management.ManagementApi('https://rmq.eandersson.net:15671', 'guest', 7 | 'guest', verify=True) 8 | try: 9 | API.user.create('my_user', 'password', tags='administrator') 10 | API.user.set_permission('my_user', 11 | virtual_host='/', 12 | configure_regex='.*', 13 | write_regex='.*', 14 | read_regex='.*') 15 | print('User Created') 16 | except management.ApiConnectionError as why: 17 | print('Connection Error: %s' % why) 18 | except management.ApiError as why: 19 | print('Failed to create user: %s' % why) 20 | -------------------------------------------------------------------------------- /examples/management/create_virtual_host.py: -------------------------------------------------------------------------------- 1 | from amqpstorm import management 2 | 3 | if __name__ == '__main__': 4 | # If using a self-signed certificate, change verify=True to point at your CA bundle. 5 | # You can disable certificate verification for testing by passing in verify=False. 6 | API = management.ManagementApi('https://rmq.eandersson.net:15671', 'guest', 7 | 'guest', verify=True) 8 | try: 9 | # Create a new Virtual Host called 'travis_ci'. 10 | API.virtual_host.create('travis_ci') 11 | print('Virtual Host created...') 12 | except management.ApiError as why: 13 | print('Failed to create virtual host: %s' % why) 14 | 15 | try: 16 | # Update the Virtual Host permissions for guest. 17 | API.user.set_permission('guest', 18 | virtual_host='travis_ci', 19 | configure_regex='.*', 20 | write_regex='.*', 21 | read_regex='.*') 22 | print('Permission updated created...') 23 | except management.ApiError as why: 24 | print('Failed to update permissions: %s' % why) 25 | -------------------------------------------------------------------------------- /examples/management/declare_queue.py: -------------------------------------------------------------------------------- 1 | from amqpstorm import management 2 | 3 | if __name__ == '__main__': 4 | # If using a self-signed certificate, change verify=True to point at your CA bundle. 5 | # You can disable certificate verification for testing by passing in verify=False. 6 | API = management.ManagementApi('https://rmq.eandersson.net:15671', 'guest', 7 | 'guest', verify=True) 8 | try: 9 | API.queue.declare('my_queue', virtual_host='/') 10 | print('Queue created...') 11 | except management.ApiConnectionError as why: 12 | print('Connection Error: %s' % why) 13 | except management.ApiError as why: 14 | print('Failed to create queue: %s' % why) 15 | -------------------------------------------------------------------------------- /examples/management/delete_queue.py: -------------------------------------------------------------------------------- 1 | from amqpstorm import management 2 | 3 | if __name__ == '__main__': 4 | # If using a self-signed certificate, change verify=True to point at your CA bundle. 5 | # You can disable certificate verification for testing by passing in verify=False. 6 | API = management.ManagementApi('https://rmq.eandersson.net:15671', 'guest', 7 | 'guest', verify=True) 8 | try: 9 | API.queue.delete('my_queue', virtual_host='/') 10 | print('Queue deleted...') 11 | except management.ApiConnectionError as why: 12 | print('Connection Error: %s' % why) 13 | except management.ApiError as why: 14 | print('Failed to delete queue: %s' % why) 15 | -------------------------------------------------------------------------------- /examples/management/delete_user.py: -------------------------------------------------------------------------------- 1 | from amqpstorm import management 2 | 3 | if __name__ == '__main__': 4 | # If using a self-signed certificate, change verify=True to point at your CA bundle. 5 | # You can disable certificate verification for testing by passing in verify=False. 6 | API = management.ManagementApi('https://rmq.eandersson.net:15671', 'guest', 7 | 'guest', verify=True) 8 | try: 9 | API.user.delete('my_user') 10 | print('User deleted...') 11 | except management.ApiConnectionError as why: 12 | print('Connection Error: %s' % why) 13 | except management.ApiError as why: 14 | print('Failed to create user: %s' % why) 15 | -------------------------------------------------------------------------------- /examples/management/does_queue_exist.py: -------------------------------------------------------------------------------- 1 | from amqpstorm import management 2 | 3 | if __name__ == '__main__': 4 | # If using a self-signed certificate, change verify=True to point at your CA bundle. 5 | # You can disable certificate verification for testing by passing in verify=False. 6 | API = management.ManagementApi('https://rmq.eandersson.net:15671', 'guest', 7 | 'guest', verify=True) 8 | try: 9 | API.queue.declare('my_queue', virtual_host='/', passive=True) 10 | print('Queue does exist...') 11 | except management.ApiConnectionError as why: 12 | print('Connection Error: %s' % why) 13 | except management.ApiError as why: 14 | if why.error_code != 404: 15 | raise 16 | print('Queue does not exist') 17 | -------------------------------------------------------------------------------- /examples/management/get_user.py: -------------------------------------------------------------------------------- 1 | from amqpstorm import management 2 | 3 | if __name__ == '__main__': 4 | # If using a self-signed certificate, change verify=True to point at your CA bundle. 5 | # You can disable certificate verification for testing by passing in verify=False. 6 | API = management.ManagementApi('https://rmq.eandersson.net:15671', 'guest', 7 | 'guest', verify=True) 8 | API.user.create('my_user', 'password') 9 | 10 | # Get a user 11 | print(API.user.get('my_user')) 12 | 13 | # User that does not exist throws an exception 14 | API.user.delete('my_user') 15 | try: 16 | API.user.get('NOT_FOUND') 17 | except management.ApiError as why: 18 | if why.error_code == 404: 19 | print('User not found') 20 | -------------------------------------------------------------------------------- /examples/management/list_queues.py: -------------------------------------------------------------------------------- 1 | from amqpstorm import management 2 | 3 | if __name__ == '__main__': 4 | # If using a self-signed certificate, change verify=True to point at your CA bundle. 5 | # You can disable certificate verification for testing by passing in verify=False. 6 | API = management.ManagementApi('https://rmq.eandersson.net:15671', 'guest', 7 | 'guest', verify=True) 8 | 9 | print('List all queues.') 10 | for queue in API.queue.list(): 11 | print('%s: %s' % (queue.get('name'), queue.get('messages'))) 12 | print('') 13 | 14 | print('List all queues containing the keyword: amqpstorm.') 15 | for queue in API.queue.list(name='amqpstorm'): 16 | print('%s: %s' % (queue.get('name'), queue.get('messages'))) 17 | print('') 18 | 19 | print('List all queues using regex that starts with: amqpstorm.') 20 | for queue in API.queue.list(name='^amqpstorm', use_regex=True): 21 | print('%s: %s' % (queue.get('name'), queue.get('messages'))) 22 | -------------------------------------------------------------------------------- /examples/management/overview.py: -------------------------------------------------------------------------------- 1 | from amqpstorm import management 2 | 3 | if __name__ == '__main__': 4 | # If using a self-signed certificate, change verify=True to point at your CA bundle. 5 | # You can disable certificate verification for testing by passing in verify=False. 6 | API = management.ManagementApi('https://rmq.eandersson.net:15671', 'guest', 7 | 'guest', verify=True) 8 | try: 9 | result = API.overview() 10 | print('%s: %s' % (result.get('product_name'), result.get('product_version'))) 11 | print('Erlang Version: %s' % result.get('erlang_full_version')) 12 | print('Cluster Name: %s' % result.get('cluster_name')) 13 | print('Total Messages: %s' % result.get('queue_totals').get('messages')) 14 | except management.ApiConnectionError as why: 15 | print('Connection Error: %s' % why) 16 | except management.ApiError as why: 17 | print('ApiError: %s' % why) 18 | -------------------------------------------------------------------------------- /examples/management/whoami.py: -------------------------------------------------------------------------------- 1 | from amqpstorm import management 2 | 3 | if __name__ == '__main__': 4 | # If using a self-signed certificate, change verify=True to point at your CA bundle. 5 | # You can disable certificate verification for testing by passing in verify=False. 6 | API = management.ManagementApi('https://rmq.eandersson.net:15671', 'guest', 7 | 'guest', verify=True) 8 | try: 9 | result = API.whoami() 10 | print('I am: %s' % result.get('name')) 11 | print('I have the following tags: %s' % ','.join(result.get('tags'))) 12 | except management.ApiConnectionError as why: 13 | print('Connection Error: %s' % why) 14 | except management.ApiError as why: 15 | print('ApiError: %s' % why) 16 | -------------------------------------------------------------------------------- /examples/pool/pool_example.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import amqpstorm 4 | import amqpstorm_pool 5 | 6 | uri = 'amqp://guest:guest@rmq.eandersson.net:5672/%2F?heartbeat=60' 7 | pool = amqpstorm_pool.QueuedPool( 8 | create=lambda: amqpstorm.UriConnection(uri), 9 | max_size=10, 10 | max_overflow=10, 11 | timeout=10, 12 | recycle=3600, 13 | stale=45, 14 | ) 15 | 16 | with pool.acquire() as cxn: 17 | cxn.channel.queue.declare('game.matchmaking') 18 | cxn.channel.basic.publish( 19 | body=json.dumps({ 20 | 'type': 'matchmaking', 21 | 'description': 'matchmaking message' 22 | }), 23 | exchange='', 24 | routing_key='game.matchmaking', 25 | properties={ 26 | 'content_type': 'text/plain', 27 | 'headers': {'data': 'game'} 28 | } 29 | ) 30 | -------------------------------------------------------------------------------- /examples/pool/ssl_pool_example.py: -------------------------------------------------------------------------------- 1 | import json 2 | import ssl 3 | 4 | import amqpstorm 5 | import amqpstorm_pool 6 | 7 | context = ssl.create_default_context(cafile='ca_certificate.pem') 8 | context.load_cert_chain( 9 | certfile='client_certificate.pem', 10 | keyfile='private_key.pem', 11 | ) 12 | ssl_options = { 13 | 'context': context, 14 | 'server_hostname': 'rmq.eandersson.net' 15 | } 16 | 17 | uri = 'amqps://guest:guest@rmq.eandersson.net:5671/%2F?heartbeat=60' 18 | pool = amqpstorm_pool.QueuedPool( 19 | create=lambda: amqpstorm.UriConnection(uri, ssl_options=ssl_options), 20 | max_size=10, 21 | max_overflow=10, 22 | timeout=10, 23 | recycle=3600, 24 | stale=45, 25 | ) 26 | 27 | with pool.acquire() as cxn: 28 | cxn.channel.queue.declare('game.matchmaking') 29 | cxn.channel.basic.publish( 30 | body=json.dumps({ 31 | 'type': 'matchmaking', 32 | 'description': 'matchmaking message' 33 | }), 34 | exchange='', 35 | routing_key='game.matchmaking', 36 | properties={ 37 | 'content_type': 'text/plain', 38 | 'headers': {'data': 'game'} 39 | } 40 | ) 41 | -------------------------------------------------------------------------------- /examples/publish_message_with_expiration.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from amqpstorm import Connection 4 | from amqpstorm import Message 5 | 6 | logging.basicConfig(level=logging.INFO) 7 | 8 | with Connection('localhost', 'guest', 'guest') as connection: 9 | with connection.channel() as channel: 10 | # Declare a queue called, 'simple_queue'. 11 | channel.queue.declare('simple_queue') 12 | 13 | # Create the message with a expiration (time to live) set to 6000. 14 | message = Message.create( 15 | channel, 'Hello World', 16 | properties={"expiration": '6000'} 17 | ) 18 | 19 | # Publish the message to the queue, 'simple_queue'. 20 | message.publish('simple_queue') 21 | -------------------------------------------------------------------------------- /examples/robust_consumer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Robust Consumer that will automatically re-connect on failure. 3 | """ 4 | import logging 5 | import time 6 | 7 | import amqpstorm 8 | from amqpstorm import Connection 9 | 10 | logging.basicConfig(level=logging.INFO) 11 | LOGGER = logging.getLogger() 12 | 13 | 14 | class Consumer(object): 15 | def __init__(self, max_retries=None): 16 | self.max_retries = max_retries 17 | self.connection = None 18 | 19 | def create_connection(self): 20 | """Create a connection. 21 | 22 | :return: 23 | """ 24 | attempts = 0 25 | while True: 26 | attempts += 1 27 | try: 28 | self.connection = Connection('localhost', 'guest', 'guest') 29 | break 30 | except amqpstorm.AMQPError as why: 31 | LOGGER.exception(why) 32 | if self.max_retries and attempts > self.max_retries: 33 | break 34 | time.sleep(min(attempts * 2, 30)) 35 | except KeyboardInterrupt: 36 | break 37 | 38 | def start(self): 39 | """Start the Consumers. 40 | 41 | :return: 42 | """ 43 | if not self.connection: 44 | self.create_connection() 45 | while True: 46 | try: 47 | channel = self.connection.channel() 48 | channel.queue.declare('simple_queue') 49 | channel.basic.consume(self, 'simple_queue', no_ack=False) 50 | channel.start_consuming() 51 | if not channel.consumer_tags: 52 | channel.close() 53 | except amqpstorm.AMQPError as why: 54 | LOGGER.exception(why) 55 | self.create_connection() 56 | except KeyboardInterrupt: 57 | self.connection.close() 58 | break 59 | 60 | def __call__(self, message): 61 | print("Message:", message.body) 62 | 63 | # Acknowledge that we handled the message without any issues. 64 | message.ack() 65 | 66 | # Reject the message. 67 | # message.reject() 68 | 69 | # Reject the message, and put it back in the queue. 70 | # message.reject(requeue=True) 71 | 72 | 73 | if __name__ == '__main__': 74 | CONSUMER = Consumer() 75 | CONSUMER.start() 76 | -------------------------------------------------------------------------------- /examples/simple_consumer.py: -------------------------------------------------------------------------------- 1 | """ 2 | A simple example consuming messages from RabbitMQ. 3 | """ 4 | import logging 5 | 6 | from amqpstorm import Connection 7 | 8 | logging.basicConfig(level=logging.INFO) 9 | 10 | 11 | def on_message(message): 12 | """This function is called on message received. 13 | 14 | :param message: 15 | :return: 16 | """ 17 | print("Message:", message.body) 18 | 19 | # Acknowledge that we handled the message without any issues. 20 | message.ack() 21 | 22 | # Reject the message. 23 | # message.reject() 24 | 25 | # Reject the message, and put it back in the queue. 26 | # message.reject(requeue=True) 27 | 28 | 29 | with Connection('localhost', 'guest', 'guest') as connection: 30 | with connection.channel() as channel: 31 | # Declare the Queue, 'simple_queue'. 32 | channel.queue.declare('simple_queue') 33 | 34 | # Set QoS to 100. 35 | # This will limit the consumer to only prefetch a 100 messages. 36 | 37 | # This is a recommended setting, as it prevents the 38 | # consumer from keeping all of the messages in a queue to itself. 39 | channel.basic.qos(100) 40 | 41 | # Start consuming the queue 'simple_queue' using the callback 42 | # 'on_message' and last require the message to be acknowledged. 43 | channel.basic.consume(on_message, 'simple_queue', no_ack=False) 44 | 45 | try: 46 | # Start consuming messages. 47 | channel.start_consuming() 48 | except KeyboardInterrupt: 49 | channel.close() 50 | -------------------------------------------------------------------------------- /examples/simple_generator_consumer.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from amqpstorm import Connection 4 | 5 | logging.basicConfig(level=logging.INFO) 6 | 7 | with Connection('localhost', 'guest', 'guest') as connection: 8 | with connection.channel() as channel: 9 | channel.queue.declare('simple_queue') 10 | channel.basic.consume(queue='simple_queue', no_ack=False) 11 | for message in channel.build_inbound_messages(): 12 | print(message.body) 13 | message.ack() 14 | -------------------------------------------------------------------------------- /examples/simple_publisher.py: -------------------------------------------------------------------------------- 1 | """ 2 | A simple example publishing a message to RabbitMQ. 3 | """ 4 | import logging 5 | 6 | from amqpstorm import Connection 7 | from amqpstorm import Message 8 | 9 | logging.basicConfig(level=logging.INFO) 10 | 11 | with Connection('localhost', 'guest', 'guest') as connection: 12 | with connection.channel() as channel: 13 | # Declare the Queue, 'simple_queue'. 14 | channel.queue.declare('simple_queue') 15 | 16 | # Message Properties. 17 | properties = { 18 | 'content_type': 'text/plain', 19 | 'headers': {'key': 'value'} 20 | } 21 | 22 | # Create the message. 23 | message = Message.create(channel, 'Hello World!', properties) 24 | 25 | # Publish the message to a queue called, 'simple_queue'. 26 | message.publish('simple_queue') 27 | -------------------------------------------------------------------------------- /examples/simple_rpc_client.py: -------------------------------------------------------------------------------- 1 | """ 2 | A simple RPC Client. 3 | """ 4 | import amqpstorm 5 | 6 | from amqpstorm import Message 7 | 8 | 9 | class FibonacciRpcClient(object): 10 | def __init__(self, host, username, password): 11 | """ 12 | :param host: RabbitMQ Server e.g. localhost 13 | :param username: RabbitMQ Username e.g. guest 14 | :param password: RabbitMQ Password e.g. guest 15 | :return: 16 | """ 17 | self.host = host 18 | self.username = username 19 | self.password = password 20 | self.channel = None 21 | self.response = None 22 | self.connection = None 23 | self.callback_queue = None 24 | self.correlation_id = None 25 | self.open() 26 | 27 | def open(self): 28 | self.connection = amqpstorm.Connection(self.host, 29 | self.username, 30 | self.password) 31 | 32 | self.channel = self.connection.channel() 33 | 34 | result = self.channel.queue.declare(exclusive=True) 35 | self.callback_queue = result['queue'] 36 | 37 | self.channel.basic.consume(self._on_response, no_ack=True, 38 | queue=self.callback_queue) 39 | 40 | def close(self): 41 | self.channel.stop_consuming() 42 | self.channel.close() 43 | self.connection.close() 44 | 45 | def call(self, number): 46 | self.response = None 47 | message = Message.create(self.channel, body=str(number)) 48 | message.reply_to = self.callback_queue 49 | self.correlation_id = message.correlation_id 50 | message.publish(routing_key='rpc_queue') 51 | 52 | while not self.response: 53 | self.channel.process_data_events() 54 | return int(self.response) 55 | 56 | def _on_response(self, message): 57 | if self.correlation_id != message.correlation_id: 58 | return 59 | self.response = message.body 60 | 61 | 62 | if __name__ == '__main__': 63 | FIBONACCI_RPC = FibonacciRpcClient('localhost', 'guest', 'guest') 64 | 65 | print(" [x] Requesting fib(30)") 66 | RESPONSE = FIBONACCI_RPC.call(30) 67 | print(" [.] Got %r" % (RESPONSE,)) 68 | FIBONACCI_RPC.close() 69 | -------------------------------------------------------------------------------- /examples/simple_rpc_server.py: -------------------------------------------------------------------------------- 1 | """ 2 | A simple RPC Server. 3 | """ 4 | import amqpstorm 5 | 6 | from amqpstorm import Message 7 | 8 | 9 | def fib(number): 10 | if number == 0: 11 | return 0 12 | elif number == 1: 13 | return 1 14 | else: 15 | return fib(number - 1) + fib(number - 2) 16 | 17 | 18 | def on_request(message): 19 | number = int(message.body) 20 | 21 | print(" [.] fib(%s)" % (number,)) 22 | 23 | response = str(fib(number)) 24 | 25 | properties = { 26 | 'correlation_id': message.correlation_id 27 | } 28 | 29 | response = Message.create(message.channel, response, properties) 30 | response.publish(message.reply_to) 31 | 32 | message.ack() 33 | 34 | 35 | if __name__ == '__main__': 36 | CONNECTION = amqpstorm.Connection('localhost', 'guest', 'guest') 37 | CHANNEL = CONNECTION.channel() 38 | 39 | CHANNEL.queue.declare(queue='rpc_queue') 40 | CHANNEL.basic.qos(prefetch_count=1) 41 | CHANNEL.basic.consume(on_request, queue='rpc_queue') 42 | 43 | print(" [x] Awaiting RPC requests") 44 | CHANNEL.start_consuming() 45 | -------------------------------------------------------------------------------- /examples/simple_transaction_publisher.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from amqpstorm import Connection 4 | from amqpstorm import Message 5 | 6 | logging.basicConfig(level=logging.INFO) 7 | 8 | with Connection('localhost', 'guest', 'guest') as connection: 9 | with connection.channel() as channel: 10 | # Declare the Queue, 'simple_queue'. 11 | channel.queue.declare('simple_queue') 12 | 13 | # Message Properties. 14 | properties = { 15 | 'content_type': 'text/plain', 16 | 'headers': {'key': 'value'} 17 | } 18 | 19 | # Enable server local transactions on channel. 20 | channel.tx.select() 21 | 22 | # Create the message. 23 | message = Message.create(channel, 'Hello World!', properties) 24 | 25 | # Publish the message to a queue called, 'simple_queue'. 26 | message.publish('simple_queue') 27 | 28 | # Commit the message(s). 29 | channel.tx.commit() 30 | 31 | # Alternatively rollback the message(s). 32 | # channel.tx.rollback() 33 | 34 | # You can also use the context manager. 35 | with channel.tx: 36 | message.publish('simple_queue') 37 | -------------------------------------------------------------------------------- /examples/ssl_with_context.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example of connecting to RabbitMQ using a SSL Certificate. 3 | """ 4 | import logging 5 | import ssl 6 | 7 | from amqpstorm import Connection 8 | 9 | logging.basicConfig(level=logging.INFO) 10 | 11 | 12 | def on_message(message): 13 | """This function is called on message received. 14 | 15 | :param message: 16 | :return: 17 | """ 18 | print("Message:", message.body) 19 | 20 | # Acknowledge that we handled the message without any issues. 21 | message.ack() 22 | 23 | # Reject the message. 24 | # message.reject() 25 | 26 | # Reject the message, and put it back in the queue. 27 | # message.reject(requeue=True) 28 | 29 | 30 | CONTEXT = ssl.create_default_context(cafile='ca_certificate.pem') 31 | CONTEXT.load_cert_chain( 32 | certfile='client_certificate.pem', 33 | keyfile='private_key.pem', 34 | ) 35 | SSL_OPTIONS = { 36 | 'context': CONTEXT, 37 | 'server_hostname': 'rmq.eandersson.net' 38 | } 39 | 40 | with Connection('rmq.eandersson.net', 'guest', 'guest', port=5671, 41 | ssl=True, ssl_options=SSL_OPTIONS) as connection: 42 | with connection.channel() as channel: 43 | # Declare the Queue, 'simple_queue'. 44 | channel.queue.declare('simple_queue') 45 | 46 | # Set QoS to 100. 47 | # This will limit the consumer to only prefetch a 100 messages. 48 | 49 | # This is a recommended setting, as it prevents the 50 | # consumer from keeping all of the messages in a queue to itself. 51 | channel.basic.qos(100) 52 | 53 | # Start consuming the queue 'simple_queue' using the callback 54 | # 'on_message' and last require the message to be acknowledged. 55 | channel.basic.consume(on_message, 'simple_queue', no_ack=False) 56 | 57 | try: 58 | # Start consuming messages. 59 | channel.start_consuming() 60 | except KeyboardInterrupt: 61 | channel.close() 62 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pamqp>=2.0.0,<3.0 2 | -------------------------------------------------------------------------------- /run_ci_locally.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o pipefail 3 | set -e 4 | 5 | # Build RabbitMQ container and start it. 6 | docker rm amqpstormdev -f || true 7 | docker build -t amqpstormdev ./docker/ 8 | docker run -d --hostname rmq.eandersson.net --name amqpstormdev -p 5671:5671 -p 5672:5672 -p 15671:15671 -p 15672:15672 amqpstormdev 9 | docker cp amqpstormdev:/etc/rabbitmq/ssl/ ./amqpstorm/tests/resources/ 10 | 11 | # Wait for RabbitMQ to startup properly. 12 | docker exec amqpstormdev wait-for-rabbitmq 13 | 14 | # Print RabbitMQ version 15 | echo "RabbitMQ Version: $(docker exec amqpstormdev rabbitmqctl --version)" 16 | 17 | # Add user. 18 | docker exec amqpstormdev rabbitmqctl add_user 'amqpstorm' '2a55f70a841f18b' 19 | docker exec amqpstormdev rabbitmqctl -p / set_permissions 'amqpstorm' '.*' '.*' '.*' 20 | docker exec amqpstormdev rabbitmqctl set_user_tags amqpstorm administrator 21 | 22 | # Confirm all ports are reachable. 23 | nc -zv rmq.eandersson.net 5671 || exit 1 24 | nc -zv rmq.eandersson.net 5672 || exit 1 25 | nc -zv rmq.eandersson.net 15671 || exit 1 26 | nc -zv rmq.eandersson.net 15672 || exit 1 27 | 28 | # Wait for a few seconds to make sure RabbitMQ has time so start properly. 29 | sleep 3 30 | 31 | # Run tests. 32 | pytest --cov=./amqpstorm --durations=5 33 | flake8 --ignore=F821 amqpstorm/ 34 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | verbosity = 2 3 | 4 | [bdist_wheel] 5 | universal = 1 6 | 7 | [metadata] 8 | description-file = README.rst -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import os.path 3 | 4 | from setuptools import find_packages 5 | from setuptools import setup 6 | 7 | 8 | def read(rel_path): 9 | here = os.path.abspath(os.path.dirname(__file__)) 10 | with codecs.open(os.path.join(here, rel_path), 'r') as fp: 11 | return fp.read() 12 | 13 | 14 | def get_version(rel_path): 15 | for line in read(rel_path).splitlines(): 16 | if line.startswith('__version__'): 17 | delim = '"' if '"' in line else "'" 18 | return line.split(delim)[1] 19 | else: 20 | raise RuntimeError("Unable to find version string.") 21 | 22 | 23 | setup( 24 | name='AMQPStorm', 25 | python_requires='>=2.7', 26 | version=get_version('amqpstorm/__init__.py'), 27 | description='Thread-safe Python RabbitMQ Client & Management library.', 28 | long_description=open('README.rst').read(), 29 | author='Erik Olof Gunnar Andersson', 30 | author_email='me@eandersson.net', 31 | include_package_data=True, 32 | packages=find_packages(), 33 | license='MIT License', 34 | url='https://amqpstorm.readthedocs.io/', 35 | install_requires=['pamqp>=2.0.0,<3.0'], 36 | extras_require={ 37 | 'management': ['requests>2'], 38 | 'pool': ['amqpstorm-pool'] 39 | }, 40 | package_data={'': ['README.rst', 'LICENSE', 'CHANGELOG.rst']}, 41 | classifiers=[ 42 | 'Development Status :: 5 - Production/Stable', 43 | 'Intended Audience :: Developers', 44 | 'License :: OSI Approved :: MIT License', 45 | 'Natural Language :: English', 46 | 'Operating System :: OS Independent', 47 | 'Programming Language :: Python :: 2', 48 | 'Programming Language :: Python :: 2.7', 49 | 'Programming Language :: Python :: 3', 50 | 'Programming Language :: Python :: 3.6', 51 | 'Programming Language :: Python :: 3.7', 52 | 'Programming Language :: Python :: 3.8', 53 | 'Programming Language :: Python :: 3.9', 54 | 'Programming Language :: Python :: 3.10', 55 | 'Programming Language :: Python :: 3.11', 56 | 'Programming Language :: Python :: 3.12', 57 | 'Programming Language :: Python :: 3.13', 58 | 'Programming Language :: Python :: Implementation :: CPython', 59 | 'Programming Language :: Python :: Implementation :: PyPy', 60 | 'Topic :: Communications', 61 | 'Topic :: Internet', 62 | 'Topic :: Internet :: WWW/HTTP', 63 | 'Topic :: Software Development :: Libraries', 64 | 'Topic :: Software Development :: Libraries :: Python Modules', 65 | 'Topic :: System :: Networking' 66 | ] 67 | ) 68 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | coverage 2 | pytest 3 | pytest-cov 4 | mock 5 | requests 6 | flake8 7 | unittest2;python_version=='2.7' 8 | --------------------------------------------------------------------------------