├── aiorabbit ├── py.typed ├── __version__.py ├── types.py ├── protocol.py ├── __init__.py ├── state.py ├── message.py ├── exceptions.py ├── channel0.py └── client.py ├── tests ├── __init__.py ├── test_tx.py ├── test_protocol.py ├── test_validation.py ├── test_state.py ├── testing.py ├── test_exchange.py ├── test_queue.py ├── test_message.py ├── test_client_edge_cases.py ├── test_publish.py ├── test_integration.py ├── test_channel0.py └── test_basic.py ├── docs ├── genindex.rst ├── exceptions.rst ├── requirements.txt ├── examples.rst ├── types.rst ├── api.rst ├── connect.rst ├── message.rst ├── _static │ └── css │ │ └── custom.css ├── conf.py ├── example-publisher.rst ├── example-simple-consumer.rst ├── index.rst └── example-callback-consumer.rst ├── MANIFEST.in ├── setup.py ├── .gitignore ├── docker-compose.yml ├── .editorconfig ├── CONTRIBUTING.md ├── .github └── workflows │ ├── deploy.yaml │ └── testing.yaml ├── LICENSE ├── bootstrap ├── setup.cfg └── README.rst /aiorabbit/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/genindex.rst: -------------------------------------------------------------------------------- 1 | Index 2 | ===== 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | setuptools.setup() 4 | -------------------------------------------------------------------------------- /aiorabbit/__version__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | version = '1.0.1' 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | aiorabbit.egg-info 2 | __pycache__ 3 | build/ 4 | dist/ 5 | env/ 6 | docs/_build 7 | .idea 8 | .mypy_cache 9 | -------------------------------------------------------------------------------- /docs/exceptions.rst: -------------------------------------------------------------------------------- 1 | Exceptions 2 | ========== 3 | 4 | .. automodule:: aiorabbit.exceptions 5 | :members: 6 | :member-order: bysource 7 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | pamqp>=3.0.0a6,<4 2 | Sphinx==2.4.4 3 | sphinx-autodoc-typehints 4 | sphinx-material==0.0.23 5 | typed_ast 6 | yarl>=1.4.2,<2 7 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | Usage Examples 2 | ============== 3 | .. toctree:: 4 | :maxdepth: 1 5 | 6 | example-simple-consumer 7 | example-callback-consumer 8 | example-publisher 9 | -------------------------------------------------------------------------------- /docs/types.rst: -------------------------------------------------------------------------------- 1 | Typing Information 2 | ================== 3 | 4 | The following type aliases are defined for ``aiorabbit`` usage: 5 | 6 | .. automodule:: aiorabbit.types 7 | :members: 8 | :member-order: bysource 9 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | Client API 2 | ========== 3 | 4 | The :class:`~aiorabbit.client.Client` class provides all of the methods required 5 | for interfacing with RabbitMQ. 6 | 7 | .. autoclass:: aiorabbit.client.Client 8 | :members: 9 | :no-undoc-members: 10 | :member-order: bysource 11 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | rabbitmq: 4 | image: rabbitmq:3.11.16-management-alpine 5 | healthcheck: 6 | test: rabbitmq-diagnostics -q check_running && rabbitmq-diagnostics -q check_local_alarms 7 | interval: 15s 8 | timeout: 10s 9 | retries: 3 10 | ports: 11 | - 5672 12 | - 15672 13 | -------------------------------------------------------------------------------- /docs/connect.rst: -------------------------------------------------------------------------------- 1 | Context Manager 2 | =============== 3 | 4 | :meth:`~aiorabbit.connect` is an asynchronous `context manager `_ 5 | that returns a connected instance of the :class:`aiorabbit.client.Client` class. 6 | When exiting the runtime context, the client connection is automatically closed. 7 | 8 | .. autofunction:: aiorabbit.connect 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | 9 | # 4 space indentation 10 | [*.py] 11 | indent_style = space 12 | indent_size = 4 13 | 14 | # 2 space indentation 15 | [*.yml] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [bootstrap] 20 | indent_style = space 21 | indent_size = 2 22 | -------------------------------------------------------------------------------- /docs/message.rst: -------------------------------------------------------------------------------- 1 | Message 2 | ======= 3 | 4 | The :class:`~aiorabbit.message.Message` represents a message that was 5 | delivered via ``Basic.GetOk`` or ``Basic.Deliver`` as well as a message 6 | that was returned by RabbitMQ via ``Basic.Return``. 7 | 8 | .. autoclass:: aiorabbit.message.Message 9 | :members: 10 | :no-undoc-members: 11 | :special-members: 12 | :member-order: bysource 13 | :exclude-members: __init__,__weakref__ 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | To get setup in the environment and run the tests, take the following steps: 4 | 5 | ```bash 6 | python3 -m venv env 7 | source env/bin/activate 8 | pip install -e '.[test]' 9 | 10 | flake8 11 | coverage run && coverage report 12 | ``` 13 | 14 | ## Test Coverage 15 | 16 | Pull requests that make changes or additions that are not covered by tests 17 | will likely be closed without review. 18 | 19 | In addition, all tests must pass the tests **AND** flake8 linter. If flake8 20 | exceptions are included, the reasoning for adding the exception must be included 21 | in the pull request. 22 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Deployment 2 | on: 3 | push: 4 | branches-ignore: ["*"] 5 | tags: ["*"] 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-latest 9 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') && github.repository == 'gmr/aiorabbit' 10 | container: python:3.11-alpine 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v1 14 | - name: Install wheel 15 | run: pip3 install build wheel 16 | - name: Build package 17 | run: python3 -m build 18 | - name: Publish package 19 | uses: pypa/gh-action-pypi-publish@release/v1 20 | with: 21 | user: __token__ 22 | password: ${{ secrets.PYPI_PASSWORD }} 23 | -------------------------------------------------------------------------------- /docs/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | .md-content { 2 | margin-right: 2em; 3 | } 4 | .md-main__inner { 5 | padding: 0; 6 | } 7 | .navheader { 8 | padding-left: 1em; 9 | } 10 | .md-sidebar--secondary { 11 | display: none; 12 | } 13 | .viewcode-link { 14 | font-size: .75em; 15 | margin-left: .5em; 16 | } 17 | .caption-text { 18 | font-style: oblique; 19 | } 20 | code.descname { 21 | background: transparent; 22 | box-shadow: none; 23 | font-size: 100% !important; 24 | } 25 | code.descname { 26 | background: transparent; 27 | box-shadow: none; 28 | font-size: 100% !important; 29 | font-weight: bold; 30 | } 31 | code.descclassname { 32 | background: transparent; 33 | box-shadow: none; 34 | font-size: 100% !important; 35 | font-weight: bold; 36 | } 37 | .rst-versions { 38 | font-size: 1.75em; 39 | } 40 | -------------------------------------------------------------------------------- /tests/test_tx.py: -------------------------------------------------------------------------------- 1 | from aiorabbit import exceptions 2 | from . import testing 3 | 4 | 5 | class TransactionTestCase(testing.ClientTestCase): 6 | 7 | @testing.async_test 8 | async def test_commit_and_rollback(self): 9 | await self.connect() 10 | await self.client.tx_select() 11 | await self.client.tx_rollback() 12 | await self.client.tx_commit() 13 | 14 | @testing.async_test 15 | async def test_double_select(self): 16 | await self.connect() 17 | await self.client.tx_select() 18 | await self.client.tx_select() 19 | 20 | @testing.async_test 21 | async def test_commit_without_select(self): 22 | await self.connect() 23 | with self.assertRaises(exceptions.NoTransactionError): 24 | await self.client.tx_commit() 25 | 26 | @testing.async_test 27 | async def test_rollback_without_select(self): 28 | await self.connect() 29 | with self.assertRaises(exceptions.NoTransactionError): 30 | await self.client.tx_rollback() 31 | -------------------------------------------------------------------------------- /aiorabbit/types.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import decimal 3 | import typing 4 | 5 | 6 | FieldArray = typing.List['FieldValue'] 7 | """A data structure for holding an array of field values.""" 8 | 9 | FieldTable = typing.Dict[str, 'FieldValue'] 10 | """Field tables are data structures that contain packed name-value pairs. 11 | 12 | The name-value pairs are encoded as short string defining the name, and octet 13 | defining the values type and then the value itself. The valid field types for 14 | tables are an extension of the native integer, bit, string, and timestamp 15 | types, and are shown in the grammar. Multi-octet integer fields are always 16 | held in network byte order. 17 | 18 | """ 19 | 20 | FieldValue = typing.Union[bool, 21 | bytearray, 22 | decimal.Decimal, 23 | FieldArray, 24 | FieldTable, 25 | float, 26 | int, 27 | None, 28 | str, 29 | datetime.datetime] 30 | """Defines valid field values for a :const:`FieldTable` and a 31 | :const:`FieldValue` 32 | 33 | """ 34 | 35 | Arguments = typing.Optional[FieldTable] 36 | """Defines an AMQP method arguments argument data type""" 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019-2023 Gavin M. Roy 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | * Neither the name of the copyright holder nor the names of its contributors may 13 | be used to endorse or promote products derived from this software without 14 | specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 19 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 20 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 23 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 24 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 25 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import pkg_resources 4 | import sphinx_material 5 | 6 | html_theme = 'sphinx_material' 7 | html_theme_path = sphinx_material.html_theme_path() 8 | html_context = sphinx_material.get_html_context() 9 | html_sidebars = { 10 | "**": ["globaltoc.html", "searchbox.html"] 11 | } 12 | html_theme_options = { 13 | 'base_url': 'http://aiorabbit.readthedocs.io', 14 | 'repo_url': 'https://github.com/gmr/aiorabbit/', 15 | 'repo_name': 'aiorabbit', 16 | 'html_minify': True, 17 | 'css_minify': True, 18 | 'nav_title': 'aiorabbit', 19 | 'logo_icon': '🐇', 20 | 'globaltoc_depth': 2, 21 | 'theme_color': 'fc6600', 22 | 'color_primary': 'grey', 23 | 'color_accent': 'orange', 24 | 'version_dropdown': False 25 | } 26 | html_static_path = ['_static'] 27 | html_css_files = [ 28 | 'css/custom.css' 29 | ] 30 | 31 | master_doc = 'index' 32 | project = 'aiorabbit' 33 | release = version = pkg_resources.get_distribution(project).version 34 | copyright = '{}, Gavin M. Roy'.format(datetime.date.today().year) 35 | 36 | extensions = [ 37 | 'sphinx.ext.autodoc', 38 | 'sphinx_autodoc_typehints', 39 | 'sphinx.ext.intersphinx', 40 | 'sphinx.ext.viewcode', 41 | 'sphinx_material' 42 | ] 43 | 44 | set_type_checking_flag = True 45 | typehints_fully_qualified = True 46 | always_document_param_types = True 47 | typehints_document_rtype = True 48 | 49 | templates_path = ['_templates'] 50 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 51 | intersphinx_mapping = {'python': ('https://docs.python.org/3', None), 52 | 'pamqp': ('https://pamqp.readthedocs.io/en/latest', 53 | None)} 54 | 55 | autodoc_default_options = {'autodoc_typehints': 'description'} 56 | 57 | -------------------------------------------------------------------------------- /aiorabbit/protocol.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import asyncio 3 | import logging 4 | import typing 5 | 6 | from pamqp import exceptions, frame 7 | 8 | LOGGER = logging.getLogger(__name__) 9 | 10 | 11 | class AMQP(asyncio.Protocol): 12 | """AMQP Protocol adapter for AsyncIO""" 13 | def __init__(self, 14 | on_connected: callable, 15 | on_disconnected: callable, 16 | on_frame_received: callable): 17 | self.buffer: bytes = b'' 18 | self.loop = asyncio.get_running_loop() 19 | self.on_connected = on_connected 20 | self.on_disconnected = on_disconnected 21 | self.on_frame_received = on_frame_received 22 | self.transport: typing.Optional[asyncio.Transport] = None 23 | 24 | def connection_made(self, transport) -> None: 25 | self.transport = transport 26 | self.on_connected() 27 | 28 | def connection_lost(self, exc: typing.Optional[Exception]) -> None: 29 | self.on_disconnected(exc) 30 | 31 | def data_received(self, data: bytes) -> None: 32 | self.buffer += data 33 | while self.buffer: 34 | try: 35 | count, channel, value = frame.unmarshal(self.buffer) 36 | except exceptions.UnmarshalingException as error: 37 | LOGGER.warning('Failed to unmarshal a frame: %r', error) 38 | LOGGER.debug('Bad frame: %r', self.buffer) 39 | break 40 | else: 41 | self.buffer = self.buffer[count:] 42 | self.loop.call_soon(self.on_frame_received, channel, value) 43 | 44 | def pause_writing(self) -> None: # pragma: nocover 45 | LOGGER.critical('Should pause writing, but it is not implemented') 46 | 47 | def resume_writing(self) -> None: # pragma: nocover 48 | LOGGER.info('Can resume writing, but it is not implemented') 49 | -------------------------------------------------------------------------------- /.github/workflows/testing.yaml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | on: 3 | push: 4 | branches: ["*"] 5 | paths-ignore: 6 | - 'docs/**' 7 | - 'setup.*' 8 | - '*.md' 9 | - '*.rst' 10 | tags-ignore: ["*"] 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 3 15 | services: 16 | rabbitmq: 17 | image: rabbitmq:3.12 18 | options: >- 19 | --health-cmd "/opt/rabbitmq/sbin/rabbitmqctl node_health_check" 20 | --health-interval 10s 21 | --health-timeout 10s 22 | --health-retries 5 23 | ports: 24 | - 5672 25 | - 15672 26 | strategy: 27 | matrix: 28 | python: ["3.7", "3.8", "3.9", "3.10", "3.11"] 29 | container: 30 | image: python:${{ matrix.python }}-alpine 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v1 34 | 35 | - name: Install OS dependencies 36 | run: apk --update add gcc make musl-dev linux-headers 37 | 38 | - name: Install testing dependencies 39 | run: pip3 --no-cache-dir install -e '.[test]' 40 | 41 | - name: Create build directory 42 | run: mkdir build 43 | 44 | - name: Create build/test-environment 45 | run: | 46 | echo "export RABBITMQ_URI=amqp://guest:guest@rabbitmq:5672/%2f" > build/test-environment 47 | 48 | - name: Run flake8 tests 49 | run: flake8 50 | 51 | - name: Run tests 52 | run: coverage run 53 | 54 | - name: Output coverage 55 | run: coverage report && coverage xml 56 | 57 | - name: Upload Coverage 58 | uses: codecov/codecov-action@v1.0.2 59 | if: github.event_name == 'push' && github.repository == 'gmr/aiorabbit' 60 | with: 61 | token: ${{secrets.CODECOV_TOKEN}} 62 | file: build/coverage.xml 63 | flags: unittests 64 | fail_ci_if_error: true 65 | -------------------------------------------------------------------------------- /bootstrap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | set -e 3 | 4 | # Common constants 5 | COLOR_RESET='\033[0m' 6 | COLOR_GREEN='\033[0;32m' 7 | COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-${PWD##*/}}" 8 | TEST_HOST="${TEST_HOST:-localhost}" 9 | 10 | echo "Integration test host: ${TEST_HOST}" 11 | 12 | get_exposed_port() { 13 | if [ -z $3 ] 14 | then 15 | docker compose port $1 $2 | cut -d: -f2 16 | else 17 | docker compose port --index=$3 $1 $2 | cut -d: -f2 18 | fi 19 | } 20 | 21 | report_start() { 22 | printf "Waiting for $1 ... " 23 | } 24 | 25 | report_done() { 26 | printf "${COLOR_GREEN}done${COLOR_RESET}\n" 27 | } 28 | 29 | wait_for_healthy_containers() { 30 | IDs=$(docker compose ps -q | paste -sd " " -) 31 | report_start "${1} containers to report healthy" 32 | counter="0" 33 | while true 34 | do 35 | if [ "$(docker inspect -f "{{.State.Health.Status}}" ${IDs} | grep -c healthy)" -eq "${1}" ]; then 36 | break 37 | fi 38 | counter=$((++counter)) 39 | if [ "${counter}" -eq 120 ]; then 40 | echo " ERROR: containers failed to start" 41 | exit 1 42 | fi 43 | sleep 1 44 | done 45 | report_done 46 | } 47 | 48 | # Ensure Docker is Running 49 | echo "Docker Information:" 50 | echo "" 51 | docker version 52 | echo "" 53 | 54 | # Activate the virtual environment 55 | if test -e env/bin/activate 56 | then 57 | . ./env/bin/activate 58 | fi 59 | 60 | rm -rf build 61 | mkdir -p build build/data 62 | 63 | # Stop any running instances and clean up after them, then pull images 64 | docker compose down --volumes --remove-orphans 65 | docker compose up -d --quiet-pull 66 | 67 | wait_for_healthy_containers 1 68 | 69 | RABBITMQ_PORT=$(get_exposed_port rabbitmq 5672) 70 | 71 | cat > build/test-environment<`_ 6 | and then publishes a message. 7 | 8 | .. note:: Specify the RabbitMQ URL to connect to in the ``RABBITMQ_URL`` environment 9 | variable prior to running this example. 10 | 11 | .. code-block:: python3 12 | :caption: publisher-example.py 13 | 14 | import asyncio 15 | import datetime 16 | import logging 17 | import os 18 | import uuid 19 | 20 | import aiorabbit 21 | 22 | LOGGER = logging.getLogger(__name__) 23 | 24 | 25 | async def main(): 26 | async with aiorabbit.connect(os.environ.get('RABBITMQ_URL', '')) as client: 27 | await client.confirm_select() 28 | if not await client.publish( 29 | 'amq.direct', 30 | 'routing-key', 31 | b'message body', 32 | app_id='example', 33 | message_id=str(uuid.uuid4()), 34 | timestamp=datetime.datetime.utcnow()): 35 | LOGGER.error('Publishing failure') 36 | else: 37 | LOGGER.info('Message published') 38 | 39 | 40 | if __name__ == '__main__': 41 | logging.basicConfig(level=logging.INFO) 42 | asyncio.get_event_loop().run_until_complete(main()) 43 | 44 | 45 | If you do not alter the code, when you run it, you should see output similar to the following: 46 | 47 | .. code-block:: 48 | 49 | $ python3 publisher-example.py 50 | INFO:aiorabbit.client:Connecting to amqp://guest:*****@localhost:32773/%2F 51 | INFO:__main__:Message published 52 | 53 | .. warning:: 54 | 55 | RabbitMQ will only indicate a publishing failure via publisher confirms 56 | when there is an internal error in RabbitMQ. They are not a mechanism for 57 | guaranteeing a message is routed. Usage of the ``mandatory`` flag when 58 | publishing will guarantee that the message is at least routed into a valid 59 | exchange, but not that they are routed into a queue. Not using the ``mandatory`` 60 | flag will allow your messages to be published to a non-existent exchange. 61 | -------------------------------------------------------------------------------- /aiorabbit/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import asyncio 3 | import contextlib 4 | import logging 5 | import ssl 6 | import typing 7 | 8 | from aiorabbit import exceptions 9 | from aiorabbit.__version__ import version 10 | 11 | DEFAULT_LOCALE = 'en-US' 12 | DEFAULT_PRODUCT = 'aiorabbit/{}'.format(version) 13 | DEFAULT_URL = 'amqp://guest:guest@localhost' 14 | 15 | LOGGER = logging.getLogger('aiorabbit') 16 | 17 | 18 | @contextlib.asynccontextmanager 19 | async def connect(url: str = DEFAULT_URL, 20 | locale: str = DEFAULT_LOCALE, 21 | product: str = DEFAULT_PRODUCT, 22 | loop: typing.Optional[asyncio.AbstractEventLoop] = None, 23 | on_return: typing.Optional[typing.Callable] = None, 24 | ssl_context: typing.Optional[ssl.SSLContext] = None): 25 | """Asynchronous :ref:`context-manager ` that 26 | connects to RabbitMQ, returning a connected 27 | :class:`~aiorabbit.client.Client` as the target. 28 | 29 | .. code-block:: python3 30 | :caption: Example Usage 31 | 32 | async with aiorabbit.connect(RABBITMQ_URL) as client: 33 | await client.exchange_declare('test', 'topic') 34 | 35 | :param url: The URL to connect to RabbitMQ with 36 | :param locale: The locale for the connection, default `en-US` 37 | :param product: The product name for the connection, default `aiorabbit` 38 | :param loop: Optional :mod:`asyncio` event loop to use 39 | :param on_return: An optional callback method to be invoked if the server 40 | returns a published method. Can also be set using the 41 | :meth:`~Client.register_basic_return_callback` method. 42 | :param ssl_context: Optional :class:`ssl.SSLContext` for the connection 43 | 44 | """ 45 | from aiorabbit import client 46 | 47 | rmq_client = client.Client( 48 | url, locale, product, loop, on_return, ssl_context) 49 | await rmq_client.connect() 50 | try: 51 | yield rmq_client 52 | finally: 53 | if not rmq_client.is_closed: 54 | await rmq_client.close() 55 | 56 | __all__ = [ 57 | 'client', 58 | 'connect', 59 | 'DEFAULT_PRODUCT', 60 | 'DEFAULT_LOCALE', 61 | 'DEFAULT_URL', 62 | 'exceptions', 63 | 'message', 64 | 'types', 65 | 'version' 66 | ] 67 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = aiorabbit 3 | version = attr: aiorabbit.__version__.version 4 | description = An AsyncIO RabbitMQ Client Library 5 | long_description = file: README.rst 6 | long_description_content_type = text/x-rst; charset=UTF-8 7 | license = BSD 3-Clause License 8 | license-file = LICENSE 9 | home-page = https://github.com/gmr/aiorabbit 10 | project_urls = 11 | Bug Tracker = https://github.com/gmr/aiorabbit/issues 12 | Documentation = https://aiorabbit.readthedocs.io 13 | Source Code = https://github.com/gmr/aiorabbit/ 14 | author = Gavin M. Roy 15 | author_email = gavinmroy@gmail.com 16 | classifiers = 17 | Development Status :: 5 - Production/Stable 18 | Intended Audience :: Developers 19 | License :: OSI Approved :: BSD License 20 | Natural Language :: English 21 | Operating System :: OS Independent 22 | Programming Language :: Python :: 3 23 | Programming Language :: Python :: 3.7 24 | Programming Language :: Python :: 3.8 25 | Programming Language :: Python :: 3.9 26 | Programming Language :: Python :: 3.10 27 | Programming Language :: Python :: 3.11 28 | Topic :: Communications 29 | Topic :: Internet 30 | Topic :: Software Development 31 | Typing :: Typed 32 | requires-dist = setuptools 33 | keywords = 34 | amqp 35 | rabbitmq 36 | 37 | [options] 38 | include_package_data = True 39 | install_requires = 40 | pamqp>=3.0.0a6,<4 41 | yarl>=1.4.2,<2 42 | packages = 43 | aiorabbit 44 | zip_safe = true 45 | 46 | [options.extras_require] 47 | test = 48 | coverage 49 | flake8 50 | flake8-comprehensions 51 | flake8-deprecated 52 | flake8-import-order 53 | flake8-print 54 | flake8-quotes 55 | flake8-rst-docstrings 56 | flake8-tuple 57 | mypy 58 | pygments 59 | pytest 60 | 61 | [coverage:run] 62 | branch = True 63 | command_line = -m unittest discover --verbose 64 | data_file = build/.coverage 65 | 66 | [coverage:report] 67 | show_missing = True 68 | include = 69 | aiorabbit/*.py 70 | 71 | [coverage:html] 72 | directory = build/coverage 73 | 74 | [coverage:xml] 75 | output = build/coverage.xml 76 | 77 | [flake8] 78 | application-import-names = aiorabbit, tests 79 | exclude = build,docs,env 80 | ignore = RST306,RST307,RST399,W503 81 | import-order-style = google 82 | rst-directives = seealso 83 | rst-roles = attr,class,const,data,exc,func,meth,mod,obj,ref,yields 84 | 85 | [mypy] 86 | check_untyped_defs = True 87 | disallow_incomplete_defs = True 88 | disallow_untyped_defs = True 89 | no_implicit_optional = true 90 | strict_optional = True 91 | -------------------------------------------------------------------------------- /docs/example-simple-consumer.rst: -------------------------------------------------------------------------------- 1 | Async Generator Consumer 2 | ======================== 3 | 4 | The following example implements a simple consumer using the async generator 5 | based :meth:`aiorabbit.client.Client.consume` method. It performs similar 6 | high-level logic to the :doc:`example-callback-consumer`: 7 | 8 | 1. Connect to RabbitMQ 9 | 2. Ensures the queue to consume from exists 10 | 3. Starts the consumer 11 | 4. As each message is received: 12 | - If the message body is ``stop``, break out of the consumer 13 | - or ack with a 75% chance 14 | - or nack without requeue 15 | 5. Stop consuming and close the connection 16 | 17 | .. note:: Specify the RabbitMQ URL to connect to in the ``RABBITMQ_URL`` environment 18 | variable prior to running this example. 19 | 20 | .. code-block:: python3 21 | :caption: simple-consumer.py 22 | 23 | import asyncio 24 | import logging 25 | import os 26 | import random 27 | 28 | import aiorabbit 29 | 30 | LOGGER = logging.getLogger(__name__) 31 | 32 | 33 | async def main(): 34 | queue_name = 'test-queue' 35 | async with aiorabbit.connect(os.environ.get('RABBITMQ_URL', '')) as client: 36 | await client.queue_declare(queue_name) 37 | LOGGER.info('Consuming from %s', queue_name) 38 | async for msg in client.consume(queue_name): 39 | LOGGER.info('Received message published to %s: %r', 40 | queue_name, msg.body) 41 | if msg.body == b'stop': 42 | await client.basic_ack(msg.delivery_tag) 43 | break 44 | elif random.randint(1, 100) <= 75: 45 | await client.basic_ack(msg.delivery_tag) 46 | else: 47 | await client.basic_nack(msg.delivery_tag, requeue=False) 48 | LOGGER.info('Stopped consuming') 49 | 50 | if __name__ == '__main__': 51 | logging.basicConfig(level=logging.INFO) 52 | asyncio.get_event_loop().run_until_complete(main()) 53 | 54 | 55 | Run the code, open the RabbitMQ management UI in your browser, and publish a 56 | few messages to the queue. When you've sent enough, publish a message with the 57 | body of ``stop``. You should see output similar to the following: 58 | 59 | .. code-block:: 60 | 61 | $ python3 simple-consumer.py 62 | INFO:aiorabbit.client:Connecting to amqp://guest:*****@localhost:32773/%2F 63 | INFO:__main__:Consuming from test-queue 64 | INFO:__main__:Received message published to test-queue: b'Simple Example Message 1' 65 | INFO:__main__:Received message published to test-queue: b'Simple Example Message 2' 66 | INFO:__main__:Received message published to test-queue: b'stop' 67 | INFO:__main__:Stopped consuming 68 | -------------------------------------------------------------------------------- /tests/test_state.py: -------------------------------------------------------------------------------- 1 | from aiorabbit import exceptions, state 2 | from . import testing 3 | 4 | STATE_FOO = 0x10 5 | STATE_BAR = 0x11 6 | STATE_BAZ = 0x12 7 | 8 | 9 | class State(state.StateManager): 10 | 11 | STATE_MAP = { 12 | state.STATE_UNINITIALIZED: 'Uninitialized', 13 | state.STATE_EXCEPTION: 'Exception', 14 | STATE_FOO: 'Foo', 15 | STATE_BAR: 'Bar', 16 | STATE_BAZ: 'Baz', 17 | 18 | } 19 | STATE_TRANSITIONS = { 20 | state.STATE_UNINITIALIZED: [STATE_FOO, STATE_BAR], 21 | state.STATE_EXCEPTION: [], 22 | STATE_FOO: [STATE_BAR], 23 | STATE_BAR: [STATE_BAZ], 24 | STATE_BAZ: [STATE_FOO] 25 | } 26 | 27 | def set_state(self, value: int) -> None: 28 | self._set_state(value) 29 | 30 | def set_exception(self, exc): 31 | self._set_state(state.STATE_EXCEPTION, exc) 32 | 33 | 34 | class TestCase(testing.AsyncTestCase): 35 | 36 | def setUp(self) -> None: 37 | super().setUp() 38 | self.obj = State(self.loop) 39 | 40 | def assert_state(self, value): 41 | self.assertEqual(self.obj.state, self.obj.STATE_MAP[value]) 42 | 43 | def test_state_transitions(self): 44 | self.assert_state(state.STATE_UNINITIALIZED) 45 | self.obj.set_state(STATE_FOO) 46 | self.assert_state(STATE_FOO) 47 | self.obj.set_state(STATE_BAR) 48 | self.assert_state(STATE_BAR) 49 | self.obj.set_state(STATE_BAZ) 50 | self.assert_state(STATE_BAZ) 51 | self.obj.set_state(STATE_FOO) 52 | self.assert_state(STATE_FOO) 53 | 54 | def test_invalid_state_transition(self): 55 | self.assert_state(state.STATE_UNINITIALIZED) 56 | with self.assertRaises(exceptions.StateTransitionError): 57 | self.obj.set_state(STATE_BAZ) 58 | self.assertIsInstance(self.obj._exception, 59 | exceptions.StateTransitionError) 60 | 61 | def test_setting_state_to_same_value(self): 62 | self.assert_state(state.STATE_UNINITIALIZED) 63 | self.obj.set_state(STATE_FOO) 64 | self.assert_state(STATE_FOO) 65 | self.obj.set_state(STATE_FOO) 66 | 67 | @testing.async_test 68 | async def test_wait_on_state(self): 69 | self.loop.call_soon(self.obj.set_state, STATE_FOO) 70 | await self.obj._wait_on_state(STATE_FOO) 71 | self.loop.call_soon(self.obj.set_state, STATE_BAR) 72 | await self.obj._wait_on_state(STATE_BAR) 73 | self.assert_state(STATE_BAR) 74 | 75 | @testing.async_test 76 | async def test_exception_while_waiting(self): 77 | self.loop.call_soon(self.obj.set_state, STATE_FOO) 78 | await self.obj._wait_on_state(STATE_FOO) 79 | self.loop.call_soon(self.obj.set_exception, RuntimeError) 80 | with self.assertRaises(RuntimeError): 81 | await self.obj._wait_on_state(STATE_BAR) 82 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | aiorabbit 2 | ========= 3 | 4 | aiorabbit is an opinionated :mod:`asyncio` `RabbitMQ `_ client for Python 3. 5 | 6 | |Version| |License| 7 | 8 | Project Goals 9 | ------------- 10 | - To create a simple, robust `RabbitMQ `_ client library for :mod:`asyncio` development in Python 3. 11 | - To make use of new features and capabilities in Python 3.7+. 12 | - Remove some complexity in using an `AMQP `_ client by: 13 | - Abstracting away the AMQP channel and use it only as a protocol coordination mechanism inside the client. 14 | - Remove the `nowait `_ keyword to ensure a single round-trip pattern of behavior for client usage. 15 | - To automatically reconnect when a connection is closed due to an AMQP exception/error. 16 | 17 | *When such a behavior is encountered, the exception is raised, but the client continues to operate if the user catches and logs the error.* 18 | - To automatically create a new channel when the channel is closed due to an AMQP exception/error. 19 | 20 | *When such a behavior is encountered, the exception is raised, but the client continues to operate if the user catches and logs the error.* 21 | - To ensure correctness of API usage, including values passed to RabbitMQ in AMQ method calls. 22 | 23 | Installation 24 | ------------ 25 | aiorabbit is available via the `Python Package Index `_. 26 | 27 | .. code:: 28 | 29 | pip3 install aiorabbit 30 | 31 | Documentation 32 | ------------- 33 | 34 | .. toctree:: 35 | :maxdepth: 1 36 | :hidden: 37 | 38 | connect 39 | api 40 | message 41 | types 42 | exceptions 43 | examples 44 | genindex 45 | 46 | License 47 | ------- 48 | 49 | Copyright (c) 2019-2020 Gavin M. Roy 50 | All rights reserved. 51 | 52 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 53 | 54 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 55 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 56 | * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 57 | 58 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 59 | 60 | .. |Version| image:: https://img.shields.io/pypi/v/aiorabbit.svg? 61 | :target: https://pypi.python.org/pypi/aiorabbit 62 | :alt: Package Version 63 | 64 | .. |License| image:: https://img.shields.io/pypi/l/aiorabbit.svg? 65 | :target: https://github.com/gmr/aiorabbit/blob/master/LICENSE 66 | :alt: BSD 67 | -------------------------------------------------------------------------------- /tests/testing.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import functools 3 | import logging 4 | import os 5 | import pathlib 6 | import unittest 7 | import uuid 8 | 9 | from aiorabbit import client 10 | 11 | LOGGER = logging.getLogger(__name__) 12 | 13 | 14 | def async_test(*func): 15 | if func: 16 | @functools.wraps(func[0]) 17 | def wrapper(*args, **kwargs): 18 | loop = asyncio.get_event_loop() 19 | LOGGER.debug('Starting test') 20 | loop.run_until_complete(func[0](*args, **kwargs)) 21 | LOGGER.debug('Test completed') 22 | return wrapper 23 | 24 | 25 | class AsyncTestCase(unittest.TestCase): 26 | 27 | @classmethod 28 | def setUpClass(cls) -> None: 29 | """Ensure the test environment variables are set""" 30 | test_env = pathlib.Path('build/test-environment') 31 | if not test_env.is_file(): 32 | test_env = pathlib.Path('../build/test-environment') 33 | if not test_env.is_file(): 34 | raise RuntimeError('Could not find test-environment') 35 | with test_env.open('r') as handle: 36 | for line in handle: 37 | if line.startswith('export '): 38 | line = line[7:] 39 | name, _, value = line.strip().partition('=') 40 | os.environ[name] = value 41 | 42 | def setUp(self) -> None: 43 | self.loop = asyncio.new_event_loop() 44 | asyncio.set_event_loop(self.loop) 45 | self.loop.set_debug(True) 46 | self.timeout = int(os.environ.get('ASYNC_TIMEOUT', '5')) 47 | self.timeout_handle = self.loop.call_later( 48 | self.timeout, self.on_timeout) 49 | 50 | def tearDown(self): 51 | LOGGER.debug('In AsyncTestCase.tearDown') 52 | if not self.timeout_handle.cancelled(): 53 | self.timeout_handle.cancel() 54 | self.loop.run_until_complete(self.loop.shutdown_asyncgens()) 55 | if self.loop.is_running: 56 | self.loop.close() 57 | super().tearDown() 58 | 59 | def on_timeout(self): 60 | self.loop.stop() 61 | raise TimeoutError( 62 | 'Test duration exceeded {} seconds'.format(self.timeout)) 63 | pass 64 | 65 | 66 | class ClientTestCase(AsyncTestCase): 67 | 68 | def setUp(self) -> None: 69 | super().setUp() 70 | self.rabbitmq_url = os.environ['RABBITMQ_URI'] 71 | self.client = client.Client(self.rabbitmq_url, loop=self.loop) 72 | self.test_finished = asyncio.Event() 73 | 74 | def tearDown(self) -> None: 75 | LOGGER.debug('In ClientTestCase.tearDown') 76 | if not self.client.is_closed: 77 | LOGGER.debug('Closing on tearDown') 78 | self.loop.run_until_complete(self.client.close()) 79 | super().tearDown() 80 | 81 | def assert_state(self, *state): 82 | self.assertIn( 83 | self.client.state, [self.client.STATE_MAP[s] for s in state]) 84 | 85 | async def connect(self): 86 | LOGGER.debug('Client connecting') 87 | self.assert_state(client.STATE_DISCONNECTED, client.STATE_CLOSED) 88 | await self.client.connect() 89 | self.assert_state(client.STATE_CHANNEL_OPENOK_RECEIVED) 90 | self.assertFalse(self.client.is_closed) 91 | 92 | async def close(self): 93 | LOGGER.debug('Client closing') 94 | await self.client.close() 95 | self.assert_state(client.STATE_CLOSED) 96 | 97 | @staticmethod 98 | def uuid4() -> str: 99 | return str(uuid.uuid4()) 100 | -------------------------------------------------------------------------------- /docs/example-callback-consumer.rst: -------------------------------------------------------------------------------- 1 | Callback-Based Consumer 2 | ======================= 3 | 4 | The following example implements a callback-based consumer. This style of consumer 5 | could be useful as part of a larger application, where consuming is only one part 6 | of the main application flow. It performs similar high-level logic to the 7 | :doc:`example-simple-consumer`. 8 | 9 | .. code-block:: python3 10 | :caption: callback-consumer.py 11 | 12 | import asyncio 13 | import logging 14 | import os 15 | import random 16 | 17 | from aiorabbit import client, exceptions, message 18 | 19 | LOGGER = logging.getLogger(__name__) 20 | 21 | 22 | class Consumer: 23 | """Class that demonstrates a consumer application lifecycle""" 24 | 25 | def __init__(self, rabbitmq_url: str, queue_name: str): 26 | self.client = client.Client(rabbitmq_url) 27 | self.queue_name = queue_name 28 | self.shutdown = asyncio.Event() 29 | 30 | async def execute(self) -> None: 31 | """Performs the following steps: 32 | 33 | 1. Connects to RabbitMQ and exits on authentication failure 34 | 2. Ensures the queue to consume from exists 35 | 3. Starts the consumer 36 | 4. Blocks until ``self.shutdown`` is set 37 | 5. Stops consuming 38 | 6. Closes the connection 39 | 40 | """ 41 | try: 42 | await self.client.connect() 43 | except exceptions.AccessRefused as err: 44 | LOGGER.error('Failed to authenticate to RabbitMQ: %s', err) 45 | return 46 | 47 | await self.client.queue_declare(self.queue_name) 48 | 49 | consumer_tag = await self.client.basic_consume( 50 | self.queue_name, callback=self.on_message) 51 | LOGGER.info('Started consuming on queue %s with consumer tag %s', 52 | self.queue_name, consumer_tag) 53 | 54 | await self.shutdown.wait() 55 | LOGGER.info('Shutting down') 56 | 57 | await self.client.basic_cancel(consumer_tag) 58 | await self.client.close() 59 | 60 | async def on_message(self, msg: message.Message) -> None: 61 | """Receives the message from RabbitMQ and... 62 | 63 | - If the message body is ``stop``, ack it and set shutdown event 64 | - or ack with a 75% chance 65 | - or nack without requeue 66 | 67 | """ 68 | LOGGER.info('Received message published to %s: %r', 69 | self.queue_name, msg.body) 70 | if msg.body == b'stop': 71 | await self.client.basic_ack(msg.delivery_tag) 72 | self.shutdown.set() 73 | elif random.randint(1, 100) <= 75: 74 | await self.client.basic_ack(msg.delivery_tag) 75 | else: 76 | await self.client.basic_nack(msg.delivery_tag, requeue=False) 77 | 78 | 79 | async def main(): 80 | await Consumer(os.environ.get('RABBITMQ_URL', ''), 'test-queue').execute() 81 | 82 | 83 | if __name__ == '__main__': 84 | logging.basicConfig(level=logging.INFO) 85 | asyncio.get_event_loop().run_until_complete(main()) 86 | 87 | Run the code, open the RabbitMQ management UI in your browser, and publish a 88 | few messages to the queue. When you've sent enough, publish a message with the 89 | body of ``stop``. You should see output similar to the following: 90 | 91 | .. code-block:: 92 | 93 | $ python3 callback-consumer.py 94 | INFO:aiorabbit.client:Connecting to amqp://guest:*****@localhost:32773/%2F 95 | INFO:__main__:Started consuming on queue test-queue with consumer tag amq.ctag-4DSHNNZGxrf22bxS_i0uqA 96 | INFO:__main__:Received message published to test-queue: b'example #1' 97 | INFO:__main__:Received message published to test-queue: b'example #2' 98 | INFO:__main__:Received message published to test-queue: b'stop' 99 | INFO:__main__:Shutting down 100 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | aiorabbit 2 | ========= 3 | aiorabbit is an opinionated AsyncIO RabbitMQ client for `Python 3 `_ (3.7+). 4 | 5 | |Version| |Status| |Coverage| |License| 6 | 7 | Project Goals 8 | ------------- 9 | - To create a simple, robust `RabbitMQ `_ client library for `AsyncIO `_ development in Python 3. 10 | - To make use of new features and capabilities in Python 3.7+. 11 | - Remove some complexity in using an `AMQP `_ client by: 12 | - Abstracting away the AMQP channel and use it only as a protocol coordination mechanism inside the client. 13 | - Remove the `nowait `_ keyword to ensure a single round-trip pattern of behavior for client usage. 14 | - To automatically reconnect when a connection is closed due to an AMQP exception/error. 15 | 16 | *When such a behavior is encountered, the exception is raised, but the client continues to operate if the user catches and logs the error.* 17 | - To automatically create a new channel when the channel is closed due to an AMQP exception/error. 18 | 19 | *When such a behavior is encountered, the exception is raised, but the client continues to operate if the user catches and logs the error.* 20 | - To ensure correctness of API usage, including values passed to RabbitMQ in AMQ method calls. 21 | 22 | Example Use 23 | ----------- 24 | The following demonstrates an example of using the library to publish a message with publisher confirmations enabled: 25 | 26 | .. code-block:: python 27 | 28 | import asyncio 29 | import datetime 30 | import uuid 31 | 32 | import aiorabbit 33 | 34 | RABBITMQ_URL = 'amqps://guest:guest@localhost:5672/%2f' 35 | 36 | 37 | async def main(): 38 | async with aiorabbit.connect(RABBITMQ_URL) as client: 39 | await client.confirm_select() 40 | if not await client.publish( 41 | 'exchange', 42 | 'routing-key', 43 | 'message-body', 44 | app_id='example', 45 | message_id=str(uuid.uuid4()), 46 | timestamp=datetime.datetime.utcnow()): 47 | print('Publishing failure') 48 | 49 | if __name__ == '__main__': 50 | asyncio.get_event_loop().run_until_complete(main()) 51 | 52 | Documentation 53 | ------------- 54 | http://aiorabbit.readthedocs.org 55 | 56 | License 57 | ------- 58 | Copyright (c) 2019-2023 Gavin M. Roy 59 | All rights reserved. 60 | 61 | Redistribution and use in source and binary forms, with or without modification, 62 | are permitted provided that the following conditions are met: 63 | 64 | * Redistributions of source code must retain the above copyright notice, this 65 | list of conditions and the following disclaimer. 66 | * Redistributions in binary form must reproduce the above copyright notice, 67 | this list of conditions and the following disclaimer in the documentation 68 | and/or other materials provided with the distribution. 69 | * Neither the name of the copyright holder nor the names of its contributors may 70 | be used to endorse or promote products derived from this software without 71 | specific prior written permission. 72 | 73 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 74 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 75 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 76 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 77 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 78 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 79 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 80 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 81 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 82 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 83 | 84 | Python Versions Supported 85 | ------------------------- 86 | 3.7+ 87 | 88 | .. |Version| image:: https://img.shields.io/pypi/v/aiorabbit.svg? 89 | :target: https://pypi.python.org/pypi/aiorabbit 90 | 91 | .. |Status| image:: https://github.com/gmr/aiorabbit/workflows/Testing/badge.svg? 92 | :target: https://github.com/gmr/aiorabbit/actions?workflow=Testing 93 | :alt: Build Status 94 | 95 | .. |Coverage| image:: https://img.shields.io/codecov/c/github/gmr/aiorabbit.svg? 96 | :target: https://codecov.io/github/gmr/aiorabbit?branch=master 97 | 98 | .. |License| image:: https://img.shields.io/pypi/l/aiorabbit.svg? 99 | :target: https://aiorabbit.readthedocs.org 100 | -------------------------------------------------------------------------------- /aiorabbit/state.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import asyncio 3 | import inspect 4 | import logging 5 | import time 6 | import typing 7 | 8 | from aiorabbit import exceptions 9 | 10 | STATE_UNINITIALIZED = 0x00 11 | STATE_EXCEPTION = 0x01 12 | 13 | 14 | class StateManager: 15 | """Base Class used to implement state management""" 16 | STATE_MAP: dict = { 17 | STATE_UNINITIALIZED: 'Uninitialized', 18 | STATE_EXCEPTION: 'Exception Raised' 19 | } 20 | 21 | STATE_TRANSITIONS: dict = { 22 | STATE_UNINITIALIZED: [STATE_EXCEPTION] 23 | } 24 | 25 | def __init__(self, loop: asyncio.AbstractEventLoop): 26 | self._logger = logging.getLogger( 27 | dict(inspect.getmembers(self))['__module__']) 28 | self._exception: typing.Optional[Exception] = None 29 | self._loop: asyncio.AbstractEventLoop = loop 30 | self._loop.set_exception_handler(self._on_exception) 31 | self._state: int = STATE_UNINITIALIZED 32 | self._state_start: float = self._loop.time() 33 | self._waits: dict = {} 34 | 35 | @property 36 | def exception(self) -> typing.Optional[Exception]: 37 | """If an exception was set with the state, return the value""" 38 | return self._exception 39 | 40 | @property 41 | def state(self) -> str: 42 | """Return the current state as descriptive string""" 43 | return self.state_description(self._state) 44 | 45 | def state_description(self, state: int) -> str: 46 | """Return a state description for a given state""" 47 | return self.STATE_MAP[state] 48 | 49 | @property 50 | def time_in_state(self) -> float: 51 | """Return how long the current state has been active""" 52 | return self._loop.time() - self._state_start 53 | 54 | def _clear_waits(self, wait_id: int) -> None: 55 | for state in self._waits.keys(): 56 | if wait_id in self._waits[state].keys(): 57 | del self._waits[state][wait_id] 58 | 59 | def _on_exception(self, 60 | _loop: asyncio.AbstractEventLoop, 61 | context: typing.Dict[str, typing.Any]) -> None: 62 | self._logger.debug('Exception on IOLoop: %r', context) 63 | self._set_state(STATE_EXCEPTION, context.get('exception')) 64 | 65 | def _reset_state(self, value: int) -> None: 66 | self._logger.debug( 67 | 'Reset state %r while state is %r - %r', 68 | self.state_description(value), self.state, self._waits) 69 | self._state = value 70 | self._state_start = self._loop.time() 71 | self._exc = None 72 | self._waits = {} 73 | 74 | def _set_state(self, value: int, 75 | exc: typing.Optional[Exception] = None) -> None: 76 | self._logger.debug( 77 | 'Set state to 0x%x: %s while state is 0x%x: %s - %r [%r]', 78 | value, self.state_description(value), self._state, self.state, 79 | self._waits, exc) 80 | if value == self._state and exc == self._exception: 81 | return 82 | elif value != STATE_EXCEPTION \ 83 | and value not in self.STATE_TRANSITIONS[self._state]: 84 | exc = exceptions.StateTransitionError( 85 | 'Invalid state transition from {!r} to {!r}'.format( 86 | self.state, self.state_description(value))) 87 | self._exception = exc 88 | raise exc 89 | self._logger.debug( 90 | 'Transition to 0x%x: %s from 0x%x: %s after %.4f seconds', 91 | value, self.state_description(value), 92 | self._state, self.state, self.time_in_state) 93 | self._exception = exc 94 | self._state = value 95 | self._state_start = self._loop.time() 96 | if self._state in self._waits: 97 | [self._loop.call_soon(event.set) 98 | for event in self._waits[self._state].values()] 99 | 100 | async def _wait_on_state(self, *args) -> int: 101 | """Wait on a specific state value to transition""" 102 | wait_id, waits = time.monotonic_ns(), [] 103 | self._logger.debug( 104 | 'Waiter %i waiting on (%s) while in 0x%x: %s', 105 | wait_id, ' || '.join( 106 | '{}: {}'.format(s, self.state_description(s)) 107 | for s in args), self._state, self.state) 108 | for state in args: 109 | if state not in self._waits: 110 | self._waits[state] = {} 111 | self._waits[state][wait_id] = asyncio.Event() 112 | waits.append((state, self._waits[state][wait_id])) 113 | while not self._exception: 114 | for state, event in waits: 115 | if event.is_set(): 116 | self._logger.debug( 117 | 'Waiter %r wait on 0x%x: %s has finished [%r]', 118 | wait_id, state, self.state_description(state), 119 | self._exception) 120 | self._clear_waits(wait_id) 121 | return state 122 | await asyncio.sleep(0.001) 123 | self._clear_waits(wait_id) 124 | exc = self._exception 125 | self._exception = None 126 | raise exc 127 | -------------------------------------------------------------------------------- /tests/test_exchange.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import uuid 3 | 4 | from aiorabbit import exceptions 5 | from . import testing 6 | 7 | 8 | class ExchangeDeclareTestCase(testing.ClientTestCase): 9 | 10 | @testing.async_test 11 | async def test_exchange_declare(self): 12 | name = str(uuid.uuid4().hex) 13 | await self.connect() 14 | await self.client.exchange_declare(name, 'direct', durable=True) 15 | await self.client.exchange_declare(name, 'direct', passive=True) 16 | 17 | @testing.async_test 18 | async def test_exchange_declare_passive_raises(self): 19 | await self.connect() 20 | with self.assertRaises(exceptions.NotFound): 21 | await self.client.exchange_declare( 22 | str(uuid.uuid4().hex), 'direct', passive=True) 23 | 24 | 25 | class ExchangeTestCase(testing.ClientTestCase): 26 | 27 | @testing.async_test 28 | async def test_exchange_declare(self): 29 | await self.connect() 30 | await self.client.exchange_declare(self.uuid4(), 'direct') 31 | 32 | @testing.async_test 33 | async def test_exchange_declare_invalid_exchange_type(self): 34 | await self.connect() 35 | with self.assertRaises(exceptions.CommandInvalid): 36 | await self.client.exchange_declare(self.uuid4(), self.uuid4()) 37 | self.assertEqual(self.client.state, 'Channel Open') 38 | # Ensure a command will properly work after the error 39 | await self.client.exchange_declare(self.uuid4(), 'direct') 40 | 41 | @testing.async_test 42 | async def test_exchange_declare_validation_errors(self): 43 | await self.connect() 44 | with self.assertRaises(TypeError): 45 | await self.client.exchange_declare(1) 46 | with self.assertRaises(TypeError): 47 | await self.client.exchange_declare(self.uuid4(), 1) 48 | with self.assertRaises(TypeError): 49 | await self.client.exchange_declare(self.uuid4(), passive='1') 50 | with self.assertRaises(TypeError): 51 | await self.client.exchange_declare(self.uuid4(), durable='1') 52 | with self.assertRaises(TypeError): 53 | await self.client.exchange_declare(self.uuid4(), auto_delete='1') 54 | with self.assertRaises(TypeError): 55 | await self.client.exchange_declare(self.uuid4(), internal='1') 56 | with self.assertRaises(TypeError): 57 | await self.client.exchange_declare(self.uuid4(), arguments='1') 58 | 59 | @testing.async_test 60 | async def test_exchange_bind_validation_errors(self): 61 | await self.connect() 62 | with self.assertRaises(TypeError): 63 | await self.client.exchange_bind(1, self.uuid4(), self.uuid4()) 64 | with self.assertRaises(TypeError): 65 | await self.client.exchange_bind(self.uuid4(), 1, self.uuid4()) 66 | with self.assertRaises(TypeError): 67 | await self.client.exchange_bind(self.uuid4(), self.uuid4(), 1) 68 | with self.assertRaises(TypeError): 69 | await self.client.exchange_bind( 70 | self.uuid4(), self.uuid4(), self.uuid4(), self.uuid4()) 71 | 72 | @testing.async_test 73 | async def test_exchange_bind_raises_exchange_not_found(self): 74 | await self.connect() 75 | self.assertEqual(self.client._channel, 1) 76 | with self.assertRaises(exceptions.NotFound): 77 | await self.client.exchange_bind( 78 | self.uuid4(), self.uuid4(), self.uuid4()) 79 | self.assertEqual(self.client._channel, 2) 80 | # Ensure a command will properly work after the error 81 | await self.client.exchange_declare(self.uuid4(), 'direct') 82 | 83 | @testing.async_test 84 | async def test_exchange_bind(self): 85 | await self.connect() 86 | exchange_1 = self.uuid4() 87 | exchange_2 = self.uuid4() 88 | await self.client.exchange_declare(exchange_1, 'topic') 89 | await self.client.exchange_declare(exchange_2, 'topic') 90 | await self.client.exchange_bind(exchange_1, exchange_2, '#') 91 | await self.client.exchange_unbind(exchange_1, exchange_2, '#') 92 | await self.client.exchange_delete(exchange_2) 93 | await self.client.exchange_delete(exchange_1) 94 | 95 | @testing.async_test 96 | async def test_exchange_delete_invalid_exchange_name(self): 97 | await self.connect() 98 | self.assertEqual(self.client._channel, 1) 99 | with self.assertRaises(TypeError): 100 | await self.client.exchange_delete(327687) 101 | 102 | @testing.async_test 103 | async def test_exchange_unbind_validation_errors(self): 104 | await self.connect() 105 | with self.assertRaises(TypeError): 106 | await self.client.exchange_unbind(1, self.uuid4(), self.uuid4()) 107 | with self.assertRaises(TypeError): 108 | await self.client.exchange_unbind(self.uuid4(), 1, self.uuid4()) 109 | with self.assertRaises(TypeError): 110 | await self.client.exchange_unbind(self.uuid4(), self.uuid4(), 1) 111 | with self.assertRaises(TypeError): 112 | await self.client.exchange_unbind( 113 | self.uuid4(), self.uuid4(), self.uuid4(), self.uuid4()) 114 | 115 | @testing.async_test 116 | async def test_exchange_unbind_invalid_exchange(self): 117 | await self.connect() 118 | self.assertEqual(self.client._channel, 1) 119 | await self.client.exchange_unbind( 120 | self.uuid4(), self.uuid4(), self.uuid4()) 121 | self.assertEqual(self.client._channel, 1) 122 | -------------------------------------------------------------------------------- /tests/test_queue.py: -------------------------------------------------------------------------------- 1 | from aiorabbit import exceptions 2 | from . import testing 3 | 4 | 5 | class QueueTestCase(testing.ClientTestCase): 6 | 7 | @testing.async_test 8 | async def test_queue(self): 9 | queue = self.uuid4() 10 | exchange = 'amq.direct' 11 | routing_key = '#' 12 | await self.connect() 13 | msg_count, consumer_count = await self.client.queue_declare(queue) 14 | self.assertEqual(msg_count, 0) 15 | self.assertEqual(consumer_count, 0) 16 | await self.client.queue_bind(queue, exchange, routing_key) 17 | purged = await self.client.queue_purge(queue) 18 | self.assertEqual(purged, 0) 19 | await self.client.queue_unbind(queue, exchange, routing_key) 20 | await self.client.queue_delete(queue) 21 | 22 | @testing.async_test 23 | async def test_queue_bind_validation_errors(self): 24 | await self.connect() 25 | with self.assertRaises(TypeError): 26 | await self.client.queue_bind(1) 27 | with self.assertRaises(TypeError): 28 | await self.client.queue_bind('foo', 1) 29 | with self.assertRaises(TypeError): 30 | await self.client.queue_bind('foo', 'bar', 1) 31 | with self.assertRaises(TypeError): 32 | await self.client.queue_bind('foo', 'bar', 'baz', 1) 33 | 34 | @testing.async_test 35 | async def test_queue_bind_missing_exchange(self): 36 | queue = self.uuid4() 37 | exchange = self.uuid4() 38 | routing_key = '#' 39 | await self.connect() 40 | await self.client.queue_declare(queue) 41 | with self.assertRaises(exceptions.NotFound): 42 | await self.client.queue_bind(queue, exchange, routing_key) 43 | await self.client.queue_delete(queue) 44 | 45 | @testing.async_test 46 | async def test_queue_declare(self): 47 | queue = self.uuid4() 48 | await self.connect() 49 | msg_count, consumer_count = await self.client.queue_declare(queue) 50 | self.assertEqual(msg_count, 0) 51 | self.assertEqual(consumer_count, 0) 52 | msg_count, consumer_count = await self.client.queue_declare( 53 | queue, passive=True) 54 | self.assertEqual(msg_count, 0) 55 | self.assertEqual(consumer_count, 0) 56 | with self.assertRaises(exceptions.ResourceLocked): 57 | await self.client.queue_declare( 58 | queue, exclusive=True, auto_delete=True) 59 | with self.assertRaises(exceptions.PreconditionFailed): 60 | await self.client.queue_declare( 61 | queue, auto_delete=True) 62 | await self.client.queue_delete(queue) 63 | 64 | @testing.async_test 65 | async def test_queue_declare_validation_errors(self): 66 | await self.connect() 67 | with self.assertRaises(TypeError): 68 | await self.client.queue_declare(1) 69 | with self.assertRaises(TypeError): 70 | await self.client.queue_declare(self.uuid4(), 1) 71 | with self.assertRaises(TypeError): 72 | await self.client.queue_declare(self.uuid4(), passive='1') 73 | with self.assertRaises(TypeError): 74 | await self.client.queue_declare(self.uuid4(), durable='1') 75 | with self.assertRaises(TypeError): 76 | await self.client.queue_declare(self.uuid4(), exclusive='1') 77 | with self.assertRaises(TypeError): 78 | await self.client.queue_declare(self.uuid4(), auto_delete='1') 79 | with self.assertRaises(TypeError): 80 | await self.client.queue_declare(self.uuid4(), arguments='1') 81 | 82 | @testing.async_test 83 | async def test_queue_delete_not_found(self): 84 | await self.connect() 85 | await self.client.queue_delete(self.uuid4()) 86 | 87 | @testing.async_test 88 | async def test_queue_delete_validation_errors(self): 89 | await self.connect() 90 | with self.assertRaises(TypeError): 91 | await self.client.queue_delete(1) 92 | with self.assertRaises(TypeError): 93 | await self.client.queue_delete('foo', 1) 94 | with self.assertRaises(TypeError): 95 | await self.client.queue_delete('foo', False, 1) 96 | 97 | @testing.async_test 98 | async def test_queue_purge_not_found(self): 99 | await self.connect() 100 | with self.assertRaises(exceptions.NotFound): 101 | await self.client.queue_purge(self.uuid4()) 102 | 103 | @testing.async_test 104 | async def test_queue_purge_validation_errors(self): 105 | await self.connect() 106 | with self.assertRaises(TypeError): 107 | await self.client.queue_purge(1) 108 | 109 | @testing.async_test 110 | async def test_queue_unbind_missing_exchange(self): 111 | queue = self.uuid4() 112 | exchange = self.uuid4() 113 | routing_key = '#' 114 | await self.connect() 115 | await self.client.queue_declare(queue) 116 | await self.client.queue_unbind(queue, exchange, routing_key) 117 | 118 | @testing.async_test 119 | async def test_queue_unbind_validation_errors(self): 120 | await self.connect() 121 | with self.assertRaises(TypeError): 122 | await self.client.queue_unbind(1) 123 | with self.assertRaises(TypeError): 124 | await self.client.queue_unbind('foo', 1) 125 | with self.assertRaises(TypeError): 126 | await self.client.queue_unbind('foo', 'bar', 1) 127 | with self.assertRaises(TypeError): 128 | await self.client.queue_unbind('foo', 'bar', 'baz', 1) 129 | -------------------------------------------------------------------------------- /tests/test_message.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import random 3 | import time 4 | import unittest 5 | import uuid 6 | 7 | from pamqp import body, commands, constants, header 8 | 9 | from aiorabbit import message 10 | 11 | 12 | class TestCase(unittest.TestCase): 13 | 14 | def setUp(self) -> None: 15 | self.exchange = str(uuid.uuid4()) 16 | self.routing_key = str(uuid.uuid4()) 17 | self.app_id = str(uuid.uuid4()) 18 | self.content_encoding = str(uuid.uuid4()) 19 | self.content_type = str(uuid.uuid4()) 20 | self.correlation_id = str(uuid.uuid4()) 21 | self.delivery_mode = random.randint(1, 2) 22 | self.expiration = str(int(time.time()) + random.randint(60, 300)) 23 | self.headers = {str(uuid.uuid4()): str(uuid.uuid4())} 24 | self.message_id = str(uuid.uuid4()) 25 | self.message_type = str(uuid.uuid4()) 26 | self.priority = random.randint(0, 255) 27 | self.reply_to = str(uuid.uuid4()) 28 | self.timestamp = datetime.datetime.now() 29 | self.user_id = str(uuid.uuid4()) 30 | self.body = b'-'.join( 31 | [str(uuid.uuid4()).encode('latin-1') 32 | for _offset in range(0, random.randint(1, 100))]) 33 | 34 | self.message = message.Message(self.get_method()) 35 | self.message.header = header.ContentHeader( 36 | 0, len(self.body), 37 | commands.Basic.Properties( 38 | app_id=self.app_id, 39 | content_encoding=self.content_encoding, 40 | content_type=self.content_type, 41 | correlation_id=self.correlation_id, 42 | delivery_mode=self.delivery_mode, 43 | expiration=self.expiration, 44 | headers=self.headers, 45 | message_id=self.message_id, 46 | message_type=self.message_type, 47 | priority=self.priority, 48 | reply_to=self.reply_to, 49 | timestamp=self.timestamp, 50 | user_id=self.user_id)) 51 | 52 | value = bytes(self.body) 53 | while value: 54 | self.message.body_frames.append( 55 | body.ContentBody(value[:constants.FRAME_MAX_SIZE])) 56 | value = value[constants.FRAME_MAX_SIZE:] 57 | 58 | def get_method(self): 59 | raise NotImplementedError 60 | 61 | def compare_message(self): 62 | for attribute in [ 63 | 'exchange', 'routing_key', 'app_id', 'content_encoding', 64 | 'content_type', 'correlation_id', 'delivery_mode', 65 | 'expiration', 'headers', 'message_id', 'message_type', 66 | 'priority', 'reply_to', 'timestamp', 'user_id', 'body']: 67 | expectation = getattr(self, attribute) 68 | value = getattr(self.message, attribute) 69 | if isinstance(expectation, dict): 70 | self.assertDictEqual(value, expectation) 71 | else: 72 | self.assertEqual(value, expectation) 73 | self.assertEqual(bytes(self.message), self.body) 74 | self.assertEqual(len(self.message), len(self.body)) 75 | self.assertTrue(self.message.is_complete) 76 | 77 | 78 | class BasicDeliverTestCase(TestCase): 79 | 80 | def setUp(self) -> None: 81 | self.consumer_tag = uuid.uuid4().hex 82 | self.delivery_tag = random.randint(0, 100000000) 83 | self.redelivered = bool(random.randint(0, 1)) 84 | super().setUp() 85 | 86 | def get_method(self): 87 | return commands.Basic.Deliver( 88 | self.consumer_tag, self.delivery_tag, self.redelivered, 89 | self.exchange, self.routing_key) 90 | 91 | def test_properties(self): 92 | self.assertEqual(self.message.consumer_tag, self.consumer_tag) 93 | self.assertEqual(self.message.delivery_tag, self.delivery_tag) 94 | self.assertEqual(self.message.redelivered, self.redelivered) 95 | self.assertIsNone(self.message.message_count) 96 | self.assertIsNone(self.message.reply_code) 97 | self.assertIsNone(self.message.reply_text) 98 | self.compare_message() 99 | 100 | 101 | class BasicGetOkTestCase(TestCase): 102 | 103 | def setUp(self) -> None: 104 | self.delivery_tag = random.randint(0, 100000000) 105 | self.message_count = random.randint(0, 1000) 106 | self.redelivered = bool(random.randint(0, 1)) 107 | super().setUp() 108 | 109 | def get_method(self): 110 | return commands.Basic.GetOk( 111 | self.delivery_tag, self.redelivered, self.exchange, 112 | self.routing_key, self.message_count) 113 | 114 | def test_properties(self): 115 | self.assertEqual(self.message.delivery_tag, self.delivery_tag) 116 | self.assertEqual(self.message.message_count, self.message_count) 117 | self.assertEqual(self.message.redelivered, self.redelivered) 118 | self.assertIsNone(self.message.reply_code) 119 | self.assertIsNone(self.message.reply_text) 120 | self.compare_message() 121 | 122 | 123 | class BasicReturnTestCase(TestCase): 124 | 125 | def setUp(self) -> None: 126 | self.reply_code = random.randint(200, 500) 127 | self.reply_text = str(uuid.uuid4()) 128 | super().setUp() 129 | 130 | def get_method(self): 131 | return commands.Basic.Return( 132 | self.reply_code, self.reply_text, self.exchange, self.routing_key) 133 | 134 | def test_properties(self): 135 | self.assertEqual(self.message.reply_code, self.reply_code) 136 | self.assertEqual(self.message.reply_text, self.reply_text) 137 | self.assertIsNone(self.message.consumer_tag) 138 | self.assertIsNone(self.message.delivery_tag) 139 | self.assertIsNone(self.message.message_count) 140 | self.assertIsNone(self.message.redelivered) 141 | self.compare_message() 142 | -------------------------------------------------------------------------------- /tests/test_client_edge_cases.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | 4 | from pamqp import base, commands 5 | 6 | from aiorabbit import client, exceptions, state 7 | from . import testing 8 | 9 | 10 | class ClientCloseTestCase(testing.ClientTestCase): 11 | 12 | @testing.async_test 13 | async def test_close(self): 14 | await self.connect() 15 | await self.client.close() 16 | self.assertTrue(self.client.is_closed) 17 | 18 | @testing.async_test 19 | async def test_close_without_channel0(self): 20 | await self.connect() 21 | self.client._channel0 = None 22 | await self.client.close() 23 | self.assertTrue(self.client.is_closed) 24 | 25 | @testing.async_test 26 | async def test_close_when_in_exception(self): 27 | await self.connect() 28 | self.client._set_state(state.STATE_EXCEPTION) 29 | await self.client.close() 30 | self.assertTrue(self.client.is_closed) 31 | 32 | @testing.async_test 33 | async def test_close_when_in_exception_with_closed_channel(self): 34 | await self.connect() 35 | self.client._channel_open.clear() 36 | await self.client.close() 37 | self.assertTrue(self.client.is_closed) 38 | 39 | @testing.async_test 40 | async def test_contemporaneous_double_close(self): 41 | await self.connect() 42 | await asyncio.gather( 43 | self.client.close(), 44 | self.client.close()) 45 | self.assertTrue(self.client.is_closed) 46 | 47 | 48 | class ChannelRotationTestCase(testing.ClientTestCase): 49 | 50 | @testing.async_test 51 | async def test_channel_exceeds_max_channels(self): 52 | await self.connect() 53 | self.client._write_frames( 54 | commands.Channel.Close(200, 'Client Requested', 0, 0)) 55 | self.client._set_state(client.STATE_CHANNEL_CLOSE_SENT) 56 | await self.client._wait_on_state(client.STATE_CHANNEL_CLOSEOK_RECEIVED) 57 | self.client._channel = self.client._channel0.max_channels 58 | await self.client._open_channel() 59 | self.assertEqual(self.client._channel, 1) 60 | 61 | 62 | class PopMessageTestCase(testing.ClientTestCase): 63 | 64 | @testing.async_test 65 | async def test_channel_exceeds_max_channels(self): 66 | await self.connect() 67 | with self.assertRaises(RuntimeError): 68 | self.client._pop_message() 69 | 70 | 71 | class BasicNackReceivedTestCase(testing.ClientTestCase): 72 | 73 | @testing.async_test 74 | async def test_basic_nack_received(self): 75 | await self.connect() 76 | delivery_tag = 10 77 | self.client._delivery_tags[delivery_tag] = asyncio.Event() 78 | self.client._set_state(client.STATE_MESSAGE_PUBLISHED) 79 | self.client._on_frame(1, commands.Basic.Nack(delivery_tag)) 80 | await self.client._delivery_tags[delivery_tag].wait() 81 | self.assertFalse(self.client._confirmation_result[delivery_tag]) 82 | 83 | 84 | class BasicRejectReceivedTestCase(testing.ClientTestCase): 85 | 86 | @testing.async_test 87 | async def test_basic_nack_received(self): 88 | await self.connect() 89 | delivery_tag = 10 90 | self.client._delivery_tags[delivery_tag] = asyncio.Event() 91 | self.client._set_state(client.STATE_MESSAGE_PUBLISHED) 92 | self.client._on_frame(1, commands.Basic.Reject(delivery_tag)) 93 | await self.client._delivery_tags[delivery_tag].wait() 94 | self.assertFalse(self.client._confirmation_result[delivery_tag]) 95 | 96 | 97 | class UnsupportedFrameOnFrameTestCase(testing.ClientTestCase): 98 | 99 | @testing.async_test 100 | async def test_unsupported_frame(self): 101 | await self.connect() 102 | self.loop.call_soon(self.client._on_frame, 1, base.Frame()) 103 | with self.assertRaises(RuntimeError): 104 | await self.client._wait_on_state(state.STATE_EXCEPTION) 105 | 106 | 107 | class TimeoutOnConnectTestCase(testing.ClientTestCase): 108 | 109 | def setUp(self) -> None: 110 | self._old_uri = os.environ['RABBITMQ_URI'] 111 | os.environ['RABBITMQ_URI'] = '{}?connection_timeout=0.000001'.format( 112 | os.environ['RABBITMQ_URI']) 113 | super().setUp() 114 | 115 | def tearDown(self) -> None: 116 | os.environ['RABBITMQ_URI'] = self._old_uri 117 | super().tearDown() 118 | 119 | @testing.async_test 120 | async def test_timeout_error_on_connect_raises(self): 121 | with self.assertRaises(asyncio.TimeoutError): 122 | await self.connect() 123 | 124 | 125 | class InvalidUsernameTestCase(testing.ClientTestCase): 126 | 127 | def setUp(self) -> None: 128 | self._old_uri = os.environ['RABBITMQ_URI'] 129 | os.environ['RABBITMQ_URI'] = \ 130 | os.environ['RABBITMQ_URI'].replace('guest', 'foo') 131 | super().setUp() 132 | 133 | def tearDown(self) -> None: 134 | os.environ['RABBITMQ_URI'] = self._old_uri 135 | super().tearDown() 136 | 137 | @testing.async_test 138 | async def test_error_on_connect_raises(self): 139 | with self.assertRaises(exceptions.AccessRefused): 140 | await self.connect() 141 | 142 | 143 | class InvalidProtocolTestCase(testing.ClientTestCase): 144 | 145 | def setUp(self) -> None: 146 | self._old_uri = os.environ['RABBITMQ_URI'] 147 | os.environ['RABBITMQ_URI'] = \ 148 | os.environ['RABBITMQ_URI'].replace('amqp', 'amqps') 149 | super().setUp() 150 | 151 | def tearDown(self) -> None: 152 | os.environ['RABBITMQ_URI'] = self._old_uri 153 | super().tearDown() 154 | 155 | @testing.async_test 156 | async def test_error_on_connect_raises(self): 157 | with self.assertRaises(OSError): 158 | await self.connect() 159 | 160 | 161 | class InvalidVHostTestCase(testing.ClientTestCase): 162 | 163 | def setUp(self) -> None: 164 | self._old_uri = os.environ['RABBITMQ_URI'] 165 | os.environ['RABBITMQ_URI'] = \ 166 | os.environ['RABBITMQ_URI'].replace('%2f', 'invalid') 167 | super().setUp() 168 | 169 | def tearDown(self) -> None: 170 | os.environ['RABBITMQ_URI'] = self._old_uri 171 | super().tearDown() 172 | 173 | @testing.async_test 174 | async def test_error_on_connect_raises(self): 175 | with self.assertRaises(exceptions.NotAllowed): 176 | await self.connect() 177 | -------------------------------------------------------------------------------- /aiorabbit/message.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import datetime 3 | import typing 4 | 5 | from pamqp import body, commands, header 6 | 7 | METHODS = typing.Union[commands.Basic.Deliver, 8 | commands.Basic.GetOk, 9 | commands.Basic.Return] 10 | 11 | 12 | class Message: 13 | """Represents a message received from RabbitMQ 14 | 15 | :param method: **For internal use only** 16 | 17 | """ 18 | def __init__(self, method: METHODS) -> None: 19 | self.method = method 20 | self.header: typing.Optional[header.ContentHeader] = None 21 | self.body_frames: typing.List[body.ContentBody] = [] 22 | 23 | def __bytes__(self) -> bytes: 24 | """Return the message body if the instance is accessed using 25 | ``bytes(Message)`` 26 | 27 | """ 28 | return self.body 29 | 30 | def __len__(self) -> int: 31 | """Return the length of the message body""" 32 | return len(self.body) 33 | 34 | @property 35 | def consumer_tag(self) -> typing.Optional[str]: 36 | """If the delivered via ``Basic.Deliver``, this provides the 37 | consumer tag it was delivered to. 38 | 39 | """ 40 | if isinstance(self.method, commands.Basic.Deliver): 41 | return self.method.consumer_tag 42 | 43 | @property 44 | def delivery_tag(self) -> typing.Optional[int]: 45 | """If the message was delivered via ``Basic.Deliver``, or retrieved via 46 | ``Basic.Get``, this will indicate the delivery tag value, which 47 | is used when acknowledging, negative-acknowledging, or rejecting the 48 | message. 49 | 50 | """ 51 | if isinstance(self.method, (commands.Basic.Deliver, 52 | commands.Basic.GetOk)): 53 | return self.method.delivery_tag 54 | 55 | @property 56 | def exchange(self) -> str: 57 | """Provides the exchange the message was published to""" 58 | return self.method.exchange 59 | 60 | @property 61 | def routing_key(self) -> str: 62 | """Provides the routing key the message was published with""" 63 | return self.method.routing_key 64 | 65 | @property 66 | def message_count(self) -> typing.Optional[int]: 67 | """Provides the ``message_count`` if the message was retrieved using 68 | ``Basic.Get`` 69 | 70 | """ 71 | if isinstance(self.method, commands.Basic.GetOk): 72 | return self.method.message_count 73 | 74 | @property 75 | def redelivered(self) -> typing.Optional[bool]: 76 | """Indicates if the message was redelivered. 77 | 78 | Will return ``None`` if the message was returned via ``Basic.Return``. 79 | 80 | """ 81 | if isinstance(self.method, (commands.Basic.Deliver, 82 | commands.Basic.GetOk)): 83 | return self.method.redelivered 84 | 85 | @property 86 | def reply_code(self) -> typing.Optional[int]: 87 | """If the message was returned via ``Basic.Return``, indicates the 88 | the error code from RabbitMQ. 89 | 90 | """ 91 | if isinstance(self.method, commands.Basic.Return): 92 | return self.method.reply_code 93 | 94 | @property 95 | def reply_text(self) -> typing.Optional[str]: 96 | """If the message was returned via ``Basic.Return``, indicates the 97 | reason why. 98 | 99 | """ 100 | if isinstance(self.method, commands.Basic.Return): 101 | return self.method.reply_text 102 | 103 | @property 104 | def app_id(self) -> typing.Optional[str]: 105 | """Provides the ``app_id`` property value if it is set.""" 106 | return self.header.properties.app_id 107 | 108 | @property 109 | def content_encoding(self) -> typing.Optional[str]: 110 | """Provides the ``content_encoding`` property value if it is set.""" 111 | return self.header.properties.content_encoding 112 | 113 | @property 114 | def content_type(self) -> typing.Optional[str]: 115 | """Provides the ``content_type`` property value if it is set.""" 116 | return self.header.properties.content_type 117 | 118 | @property 119 | def correlation_id(self) -> typing.Optional[str]: 120 | """Provides the ``correlation_id`` property value if it is set.""" 121 | return self.header.properties.correlation_id 122 | 123 | @property 124 | def delivery_mode(self) -> typing.Optional[int]: 125 | """Provides the ``delivery_mode`` property value if it is set.""" 126 | return self.header.properties.delivery_mode 127 | 128 | @property 129 | def expiration(self) -> typing.Optional[str]: 130 | """Provides the ``expiration`` property value if it is set.""" 131 | return self.header.properties.expiration 132 | 133 | @property 134 | def headers(self) -> typing.Optional[typing.Dict[str, typing.Any]]: 135 | """Provides the ``headers`` property value if it is set.""" 136 | return self.header.properties.headers 137 | 138 | @property 139 | def message_id(self) -> typing.Optional[str]: 140 | """Provides the ``message_id`` property value if it is set.""" 141 | return self.header.properties.message_id 142 | 143 | @property 144 | def message_type(self) -> typing.Optional[str]: 145 | """Provides the ``message_type`` property value if it is set.""" 146 | return self.header.properties.message_type 147 | 148 | @property 149 | def priority(self) -> typing.Optional[int]: 150 | """Provides the ``priority`` property value if it is set.""" 151 | return self.header.properties.priority 152 | 153 | @property 154 | def reply_to(self) -> typing.Optional[str]: 155 | """Provides the ``reply_to`` property value if it is set.""" 156 | return self.header.properties.reply_to 157 | 158 | @property 159 | def timestamp(self) -> typing.Optional[datetime.datetime]: 160 | """Provides the ``timestamp`` property value if it is set.""" 161 | return self.header.properties.timestamp 162 | 163 | @property 164 | def user_id(self) -> typing.Optional[str]: 165 | """Provides the ``user_id`` property value if it is set.""" 166 | return self.header.properties.user_id 167 | 168 | @property 169 | def body(self) -> bytes: 170 | """Provides the message body""" 171 | return b''.join([b.value for b in self.body_frames]) 172 | 173 | @property 174 | def is_complete(self): 175 | # Used when receiving frames from RabbitMQ 176 | return len(self) == self.header.body_size 177 | -------------------------------------------------------------------------------- /aiorabbit/exceptions.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | 4 | class AIORabbitException(Exception): 5 | """Exception that is the base class of all other error exceptions. 6 | You can use this to catch all errors with one single except statement. 7 | Warnings are not considered errors and thus not use this class as base. 8 | It is a subclass of the :exc:`Exception`. 9 | 10 | """ 11 | 12 | 13 | class SoftError(AIORabbitException): 14 | """Base exception for all soft errors.""" 15 | 16 | 17 | class HardError(AIORabbitException): 18 | """Base exception for all hard errors.""" 19 | 20 | 21 | class ContentTooLarge(SoftError): 22 | """The client attempted to transfer content larger than the server could 23 | accept at the present time. The client may retry at a later time. 24 | 25 | """ 26 | name = 'CONTENT-TOO-LARGE' 27 | value = 311 28 | 29 | 30 | class NoConsumers(SoftError): 31 | """When the exchange cannot deliver to a consumer when the ``immediate`` 32 | flag is set. As a result of pending data on the queue or the absence of any 33 | consumers of the queue. 34 | 35 | """ 36 | name = 'NO-CONSUMERS' 37 | value = 313 38 | 39 | 40 | class AccessRefused(SoftError): 41 | """The client attempted to work with a server entity to which it has no 42 | access due to security settings. 43 | 44 | """ 45 | name = 'ACCESS-REFUSED' 46 | value = 403 47 | 48 | 49 | class NotFound(SoftError): 50 | """The client attempted to work with a server entity that does not exist""" 51 | name = 'NOT-FOUND' 52 | value = 404 53 | 54 | 55 | class ResourceLocked(SoftError): 56 | """The client attempted to work with a server entity to which it has no 57 | access because another client is working with it. 58 | 59 | """ 60 | name = 'RESOURCE-LOCKED' 61 | value = 405 62 | 63 | 64 | class PreconditionFailed(SoftError): 65 | """The client requested a method that was not allowed because some 66 | precondition failed. 67 | 68 | """ 69 | name = 'PRECONDITION-FAILED' 70 | value = 406 71 | 72 | 73 | class ConnectionForced(HardError): 74 | """An operator intervened to close the connection for some reason. The 75 | client may retry at some later date. 76 | 77 | """ 78 | name = 'CONNECTION-FORCED' 79 | value = 320 80 | 81 | 82 | class InvalidPath(HardError): 83 | """The client tried to work with an unknown virtual host""" 84 | name = 'INVALID-PATH' 85 | value = 402 86 | 87 | 88 | class FrameError(HardError): 89 | """The sender sent a malformed frame that the recipient could not decode. 90 | This strongly implies a programming error in the sending peer. 91 | 92 | """ 93 | name = 'FRAME-ERROR' 94 | value = 501 95 | 96 | 97 | class SyntaxError(HardError): 98 | """The sender sent a frame that contained illegal values for one or more 99 | fields. This strongly implies a programming error in the sending peer. 100 | 101 | """ 102 | name = 'SYNTAX-ERROR' 103 | value = 502 104 | 105 | 106 | class CommandInvalid(HardError): 107 | """The client sent an invalid sequence of frames, attempting to perform an 108 | operation that was considered invalid by the server. This usually implies 109 | a programming error in the client. 110 | 111 | """ 112 | name = 'COMMAND-INVALID' 113 | value = 503 114 | 115 | 116 | class ChannelError(HardError): 117 | """The client attempted to work with a channel that had not been correctly 118 | opened. This most likely indicates a fault in the client layer. 119 | 120 | """ 121 | name = 'CHANNEL-ERROR' 122 | value = 504 123 | 124 | 125 | class UnexpectedFrame(HardError): 126 | """The peer sent a frame that was not expected, usually in the context of 127 | a content header and body. This strongly indicates a fault in the peer's 128 | content processing. 129 | 130 | """ 131 | name = 'UNEXPECTED-FRAME' 132 | value = 505 133 | 134 | 135 | class ResourceError(HardError): 136 | """The server could not complete the method because it lacked sufficient 137 | resources. This may be due to the client creating too many of some type 138 | of entity. 139 | 140 | """ 141 | name = 'RESOURCE-ERROR' 142 | value = 506 143 | 144 | 145 | class NotAllowed(HardError): 146 | """The client tried to work with some entity in a manner that is 147 | prohibited by the server, due to security settings or by some other 148 | criteria. 149 | 150 | """ 151 | name = 'NOT-ALLOWED' 152 | value = 530 153 | 154 | 155 | class NotImplemented(HardError): 156 | """The client tried to use functionality that is not implemented in the 157 | server. 158 | 159 | """ 160 | name = 'NOT-IMPLEMENTED' 161 | value = 540 162 | 163 | 164 | class InternalError(HardError): 165 | """The server could not complete the method because of an internal error. 166 | The server may require intervention by an operator in order to resume 167 | normal operations. 168 | 169 | """ 170 | name = 'INTERNAL-ERROR' 171 | value = 541 172 | 173 | 174 | class ClientNegotiationException(AIORabbitException): 175 | """The client failed to connect to RabbitMQ due to a negotiation error.""" 176 | 177 | 178 | class ConnectionClosedException(AIORabbitException): 179 | """The remote server closed the connection or the connection was severed 180 | due to a networking error. 181 | 182 | """ 183 | 184 | 185 | class StateTransitionError(AIORabbitException): 186 | """The client implements a strict state machine for what is currently 187 | happening in the communication with RabbitMQ. 188 | 189 | If this exception is raised, one or more of the following is true: 190 | 191 | - An unexpected behavior was seen from the server 192 | - The client was used in an unexpect way 193 | - There is a bug in aiorabbit 194 | 195 | If you see this exception, please `create an issue 196 | `_ in the GitHub repository 197 | with a full traceback and `DEBUG` level logs. 198 | 199 | """ 200 | 201 | 202 | class InvalidRequestError(AIORabbitException): 203 | """The request violates the AMQ specification, usually by providing a 204 | value that does not validate according to the spec. 205 | 206 | """ 207 | 208 | 209 | class NoTransactionError(AIORabbitException): 210 | """Commit or Rollback Invoked without a Transaction 211 | 212 | :meth:`~aiorabbit.client.Client.tx_commit` or 213 | :meth:`~aiorabbit.client.Client.tx_rollback` 214 | were invoked without first invoking 215 | :meth:`~aiorabbit.client.Client.tx_select`. 216 | 217 | """ 218 | 219 | 220 | class UnknownError(AIORabbitException): 221 | """Unexplainable edge case exception. 222 | 223 | Please provide tracebacks if you encounter this. 224 | 225 | """ 226 | 227 | 228 | # Error code to class mapping 229 | CLASS_MAPPING = { 230 | 0: UnknownError, 231 | 311: ContentTooLarge, 232 | 313: NoConsumers, 233 | 403: AccessRefused, 234 | 404: NotFound, 235 | 405: ResourceLocked, 236 | 406: PreconditionFailed, 237 | 320: ConnectionForced, 238 | 402: InvalidPath, 239 | 501: FrameError, 240 | 502: SyntaxError, 241 | 503: CommandInvalid, 242 | 504: ChannelError, 243 | 505: UnexpectedFrame, 244 | 506: ResourceError, 245 | 530: NotAllowed, 246 | 540: NotImplemented, 247 | 541: InternalError 248 | } 249 | -------------------------------------------------------------------------------- /tests/test_publish.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import uuid 4 | 5 | from pamqp import constants 6 | 7 | from aiorabbit import client, exceptions 8 | from . import testing 9 | 10 | LOGGER = logging.getLogger(__name__) 11 | 12 | 13 | class PublishingArgumentsTestCase(testing.ClientTestCase): 14 | 15 | @testing.async_test 16 | async def test_bad_exchange(self): 17 | await self.connect() 18 | with self.assertRaises(TypeError): 19 | await self.client.publish(1, 'foo', b'bar') 20 | 21 | @testing.async_test 22 | async def test_bad_routing_key(self): 23 | await self.connect() 24 | with self.assertRaises(TypeError): 25 | await self.client.publish('foo', 2, b'bar') 26 | 27 | @testing.async_test 28 | async def test_bad_message_body(self): 29 | await self.connect() 30 | with self.assertRaises(TypeError): 31 | await self.client.publish('foo', 'bar', {'foo': 'bar'}) 32 | 33 | @testing.async_test 34 | async def test_bad_booleans(self): 35 | await self.connect() 36 | with self.assertRaises(TypeError): 37 | kwargs = {'mandatory': 'qux'} 38 | await self.client.publish('foo', 'bar', b'baz', **kwargs) 39 | 40 | @testing.async_test 41 | async def test_bad_strs(self): 42 | await self.connect() 43 | for field in ['app_id', 'content_encoding', 'content_type', 44 | 'correlation_id', 'expiration', 'message_id', 45 | 'message_type', 'reply_to', 'user_id']: 46 | LOGGER.debug('testing %s with non-string value', field) 47 | with self.assertRaises(TypeError): 48 | kwargs = {field: 32768} 49 | await self.client.publish('foo', 'bar', b'baz', **kwargs) 50 | 51 | @testing.async_test 52 | async def test_bad_ints(self): 53 | await self.connect() 54 | for field in ['delivery_mode', 'priority']: 55 | with self.assertRaises(TypeError): 56 | kwargs = {field: 'qux'} 57 | await self.client.publish('foo', 'bar', b'baz', **kwargs) 58 | 59 | @testing.async_test 60 | async def test_bad_delivery_mode(self): 61 | await self.connect() 62 | with self.assertRaises(ValueError): 63 | await self.client.publish( 64 | 'foo', 'bar', b'baz', delivery_mode=-1) 65 | with self.assertRaises(ValueError): 66 | await self.client.publish( 67 | 'foo', 'bar', b'baz', delivery_mode=3) 68 | 69 | @testing.async_test 70 | async def test_good_delivery_mode(self): 71 | await self.connect() 72 | await self.client.confirm_select() 73 | result = await self.client.publish('', 'bar', b'baz', delivery_mode=1) 74 | self.assertTrue(result) 75 | 76 | @testing.async_test 77 | async def test_bad_headers(self): 78 | await self.connect() 79 | with self.assertRaises(TypeError): 80 | await self.client.publish('foo', 'bar', b'baz', headers=1) 81 | 82 | @testing.async_test 83 | async def test_bad_message_type(self): 84 | await self.connect() 85 | with self.assertRaises(TypeError): 86 | await self.client.publish('foo', 'bar', b'baz', message_type=1) 87 | 88 | @testing.async_test 89 | async def test_bad_good(self): 90 | await self.connect() 91 | await self.client.confirm_select() 92 | result = await self.client.publish( 93 | '', 'bar', b'baz', message_type='foo') 94 | self.assertTrue(result) 95 | 96 | @testing.async_test 97 | async def test_bad_priority(self): 98 | await self.connect() 99 | with self.assertRaises(ValueError): 100 | await self.client.publish( 101 | 'foo', 'bar', b'baz', priority=-1) 102 | with self.assertRaises(ValueError): 103 | await self.client.publish( 104 | 'foo', 'bar', b'baz', priority=32768) 105 | 106 | @testing.async_test 107 | async def test_good_priority(self): 108 | await self.connect() 109 | await self.client.confirm_select() 110 | result = await self.client.publish('', 'bar', b'baz', priority=5) 111 | self.assertTrue(result) 112 | 113 | @testing.async_test 114 | async def test_bad_timestamp(self): 115 | await self.connect() 116 | with self.assertRaises(TypeError): 117 | await self.client.publish( 118 | 'foo', 'bar', b'baz', timestamp=1579390178) 119 | 120 | 121 | class PublishingTestCase(testing.ClientTestCase): 122 | 123 | def setUp(self) -> None: 124 | super().setUp() 125 | self.test_finished = asyncio.Event() 126 | self.exchange = '' 127 | self.routing_key = str(uuid.uuid4()) 128 | self.body = bytes(uuid.uuid4().hex, 'latin-1') 129 | 130 | @testing.async_test 131 | async def test_minimal_publish(self): 132 | await self.connect() 133 | await self.client.publish(self.exchange, self.routing_key, self.body) 134 | 135 | @testing.async_test 136 | async def test_minimal_publish_with_empty_routing_key(self): 137 | await self.connect() 138 | await self.client.publish(self.exchange, '', self.body) 139 | 140 | @testing.async_test 141 | async def test_minimal_publish_with_str_body(self): 142 | await self.connect() 143 | await self.client.publish( 144 | self.exchange, self.routing_key, str(self.body)) 145 | 146 | @testing.async_test 147 | async def test_minimal_publish_with_large_body(self): 148 | body = b'-'.join([uuid.uuid4().bytes 149 | for _i in range(0, constants.FRAME_MAX_SIZE)]) 150 | await self.connect() 151 | await self.client.publish(self.exchange, self.routing_key, body) 152 | 153 | @testing.async_test 154 | async def test_publish_with_bad_exchange(self): 155 | # Here we expect the exception to pass silently except for logging 156 | self.exchange = str(uuid.uuid4()) 157 | await self.connect() 158 | channel = self.client._channel 159 | await self.client.publish(self.exchange, self.routing_key, self.body) 160 | await self.client._wait_on_state(client.STATE_CHANNEL_OPENOK_RECEIVED) 161 | self.assertEqual(self.client._channel, channel + 1) 162 | 163 | @testing.async_test 164 | async def test_publish_with_bad_exchange_and_mandatory(self): 165 | def on_message_return(msg): 166 | self.assertEqual(msg.exchange, self.exchange) 167 | self.assertEqual(msg.routing_key, self.routing_key) 168 | self.assertEqual(msg.body, self.body) 169 | self.test_finished.set() 170 | 171 | self.client.register_basic_return_callback(on_message_return) 172 | await self.connect() 173 | await self.client.publish( 174 | self.exchange, self.routing_key, self.body, mandatory=True) 175 | await self.test_finished.wait() 176 | 177 | @testing.async_test 178 | async def test_publish_with_confirmation(self): 179 | await self.connect() 180 | await self.client.confirm_select() 181 | result = await self.client.publish( 182 | self.exchange, self.routing_key, self.body) 183 | self.assertTrue(result) 184 | 185 | @testing.async_test 186 | async def test_no_publisher_confirmation_support(self): 187 | await self.connect() 188 | del self.client._channel0.properties[ 189 | 'capabilities']['publisher_confirms'] 190 | with self.assertRaises(exceptions.NotImplemented): 191 | await self.client.confirm_select() 192 | 193 | @testing.async_test 194 | async def test_publish_bad_exchange_publisher_confirmation(self): 195 | await self.connect() 196 | await self.client.confirm_select() 197 | with self.assertRaises(exceptions.NotFound): 198 | await self.client.publish( 199 | self.uuid4(), self.routing_key, self.body) 200 | 201 | @testing.async_test 202 | async def test_publish_publisher_confirmation_mandatory_no_queue(self): 203 | await self.connect() 204 | await self.client.confirm_select() 205 | result = await self.client.publish( 206 | '', self.routing_key, self.body, mandatory=True) 207 | self.assertTrue(result) 208 | -------------------------------------------------------------------------------- /tests/test_integration.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import os 4 | from unittest import mock 5 | 6 | from pamqp import commands 7 | 8 | import aiorabbit 9 | from aiorabbit import client, exceptions 10 | from tests import testing 11 | 12 | LOGGER = logging.getLogger(__name__) 13 | 14 | 15 | class ContextManagerTestCase(testing.AsyncTestCase): 16 | 17 | @testing.async_test 18 | async def test_context_manager_open(self): 19 | async with aiorabbit.connect( 20 | os.environ['RABBITMQ_URI'], loop=self.loop) as client_: 21 | await client_.confirm_select() 22 | self.assertEqual(client_._state, 23 | client.STATE_CONFIRM_SELECTOK_RECEIVED) 24 | self.assertEqual(client_._state, client.STATE_CLOSED) 25 | 26 | @testing.async_test 27 | async def test_context_manager_exception(self): 28 | async with aiorabbit.connect( 29 | os.environ['RABBITMQ_URI'], loop=self.loop) as client_: 30 | await client_.confirm_select() 31 | with self.assertRaises(RuntimeError): 32 | await client_.confirm_select() 33 | self.assertEqual(client_._state, client.STATE_CLOSED) 34 | 35 | @testing.async_test 36 | async def test_context_manager_remote_close(self): 37 | async with aiorabbit.connect( 38 | os.environ['RABBITMQ_URI'], loop=self.loop) as client_: 39 | LOGGER.debug('Sending admin shutdown frame') 40 | client_._on_frame( 41 | 0, commands.Connection.Close(200, 'Admin Shutdown')) 42 | while not client_.is_closed: 43 | await asyncio.sleep(0.1) 44 | self.assertEqual(client_._state, client.STATE_CLOSED) 45 | 46 | @testing.async_test 47 | async def test_context_manager_already_closed_on_exit(self): 48 | async with aiorabbit.connect( 49 | os.environ['RABBITMQ_URI'], loop=self.loop) as client_: 50 | self.assertFalse(client_.is_closed) 51 | client_._state = client.STATE_CLOSED 52 | self.assertTrue(client_.is_closed) 53 | async with aiorabbit.connect( 54 | os.environ['RABBITMQ_URI'], loop=self.loop) as client_: 55 | self.assertFalse(client_.is_closed) 56 | self.assertTrue(client_.is_closed) 57 | 58 | 59 | class IntegrationTestCase(testing.ClientTestCase): 60 | 61 | @testing.async_test 62 | async def test_channel_recycling(self): 63 | await self.connect() 64 | self.assertEqual(self.client._channel, 1) 65 | await self.close() 66 | await self.connect() 67 | self.assertEqual(self.client._channel, 1) 68 | await self.close() 69 | 70 | @testing.async_test 71 | async def test_double_close(self): 72 | await self.connect() 73 | await self.close() 74 | await self.close() 75 | 76 | @testing.async_test 77 | async def test_confirm_select(self): 78 | await self.connect() 79 | await self.client.confirm_select() 80 | self.assert_state(client.STATE_CONFIRM_SELECTOK_RECEIVED) 81 | await self.close() 82 | 83 | @testing.async_test 84 | async def test_connect_timeout(self): 85 | with mock.patch.object(self.loop, 'create_connection') as create_conn: 86 | create_conn.side_effect = asyncio.TimeoutError() 87 | with self.assertRaises(asyncio.TimeoutError): 88 | await self.connect() 89 | 90 | @testing.async_test 91 | async def test_client_close_error(self): 92 | await self.connect() 93 | with mock.patch.object(self.client, 'close') as close: 94 | close.side_effect = RuntimeError('Faux Exception') 95 | with self.assertRaises(RuntimeError): 96 | await self.close() 97 | 98 | @testing.async_test 99 | async def test_update_secret_raises(self): 100 | await self.connect() 101 | with self.assertRaises(exceptions.CommandInvalid): 102 | self.client._write_frames( 103 | commands.Connection.UpdateSecret('foo', 'bar')) 104 | await self.client._wait_on_state( 105 | client.STATE_UPDATE_SECRETOK_RECEIVED) 106 | 107 | 108 | class ReconnectPublisherConfirmsTestCase(testing.ClientTestCase): 109 | 110 | @testing.async_test 111 | async def test_confirm_select_already_invoked_on_reconnect(self): 112 | await self.connect() 113 | await self.client.confirm_select() 114 | self.assertTrue(self.client._publisher_confirms) 115 | with self.assertRaises(exceptions.CommandInvalid): 116 | await self.client.exchange_declare(self.uuid4(), self.uuid4()) 117 | self.assertTrue(self.client._publisher_confirms) 118 | 119 | 120 | class QosPrefetchTestCase(testing.ClientTestCase): 121 | 122 | @testing.async_test 123 | async def test_basic_qos(self): 124 | await self.connect() 125 | await self.client.qos_prefetch(100, False) 126 | await self.client.qos_prefetch(125, True) 127 | 128 | @testing.async_test 129 | async def test_validation_errors(self): 130 | await self.connect() 131 | with self.assertRaises(TypeError): 132 | await self.client.qos_prefetch('foo') 133 | with self.assertRaises(TypeError): 134 | await self.client.qos_prefetch(0, 'foo') 135 | 136 | 137 | class ConsumeTestCase(testing.ClientTestCase): 138 | 139 | def setUp(self) -> None: 140 | super().setUp() 141 | self.queue = self.uuid4() 142 | self.exchange = 'amq.topic' 143 | 144 | async def rmq_setup(self): 145 | await self.connect() 146 | await self.client.queue_declare(self.queue) 147 | await self.client.queue_bind(self.queue, self.exchange, '#') 148 | await self.client.qos_prefetch(1, True) 149 | messages = [self.uuid4().encode('utf-8') for _offset in range(0, 5)] 150 | for message in messages: 151 | await self.client.publish(self.exchange, self.queue, message) 152 | 153 | msgs, _consumers = await self.client.queue_declare(self.queue) 154 | while msgs < len(messages): 155 | await asyncio.sleep(0.5) 156 | msgs, consumers = await self.client.queue_declare(self.queue) 157 | return messages 158 | 159 | @testing.async_test 160 | async def test_consume(self): 161 | messages = await self.rmq_setup() 162 | async for message in self.client.consume(self.queue): 163 | messages.remove(message.body) 164 | await self.client.basic_ack(message.delivery_tag) 165 | if not messages: 166 | break 167 | msgs, _consumers = await self.client.queue_declare(self.queue) 168 | self.assertEqual(msgs, 0) 169 | 170 | 171 | class ContextManagerConsumeTestCase(ConsumeTestCase): 172 | 173 | @testing.async_test 174 | async def test_consume(self): 175 | messages = await self.rmq_setup() 176 | async with aiorabbit.connect(self.rabbitmq_url) as rabbitmq: 177 | async for message in rabbitmq.consume(self.queue): 178 | messages.remove(message.body) 179 | await rabbitmq.basic_ack(message.delivery_tag) 180 | if not messages: 181 | break 182 | msgs, _consumers = await self.client.queue_declare(self.queue) 183 | self.assertEqual(msgs, 0) 184 | 185 | @testing.async_test 186 | async def test_emulated_heartbeat_timeout_while_consuming(self): 187 | messages = await self.rmq_setup() 188 | 189 | async def consume_messages(): 190 | async with aiorabbit.connect(self.rabbitmq_url) as rabbitmq: 191 | async for message in rabbitmq.consume(self.queue): 192 | messages.remove(message.body) 193 | await rabbitmq.basic_ack(message.delivery_tag) 194 | if not messages: 195 | rabbitmq._on_remote_close(599, 'Test Close') 196 | 197 | with self.assertRaises(exceptions.ConnectionClosedException): 198 | await consume_messages() 199 | 200 | msgs, _consumers = await self.client.queue_declare(self.queue) 201 | self.assertEqual(msgs, 0) 202 | 203 | @testing.async_test 204 | async def test_disconnected_while_consuming(self): 205 | messages = await self.rmq_setup() 206 | 207 | async def consume_messages(): 208 | async with aiorabbit.connect(self.rabbitmq_url) as rabbitmq: 209 | async for message in rabbitmq.consume(self.queue): 210 | messages.remove(message.body) 211 | await rabbitmq.basic_ack(message.delivery_tag) 212 | if not messages: 213 | rabbitmq._on_disconnected(None) 214 | 215 | with self.assertRaises(exceptions.ConnectionClosedException): 216 | await consume_messages() 217 | 218 | msgs, _consumers = await self.client.queue_declare(self.queue) 219 | self.assertEqual(msgs, 0) 220 | -------------------------------------------------------------------------------- /tests/test_channel0.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import platform 4 | import typing 5 | from unittest import mock 6 | import uuid 7 | 8 | from pamqp import commands, constants, frame, heartbeat 9 | 10 | from aiorabbit import channel0, exceptions, state, version 11 | from . import testing 12 | 13 | LOGGER = logging.getLogger(__name__) 14 | 15 | 16 | class TestCase(testing.AsyncTestCase): 17 | 18 | HEARTBEAT_INTERVAL = 10 19 | SERVER_HEARTBEAT_INTERVAL = 30 20 | MAX_CHANNELS = 256 21 | SERVER_MAX_CHANNELS = 32768 22 | 23 | def setUp(self): 24 | super().setUp() 25 | self.blocked = asyncio.Event() 26 | self.username = str(uuid.uuid4()) 27 | self.password = str(uuid.uuid4()) 28 | self.locale = str(uuid.uuid4()) 29 | self.product = str(uuid.uuid4()) 30 | self.virtual_host = '/' 31 | self.heartbeat = asyncio.Event() 32 | self.loop = asyncio.get_event_loop() 33 | self.on_remote_close = mock.Mock() 34 | self.server_properties = { 35 | 'capabilities': {'authentication_failure_close': True, 36 | 'basic.nack': True, 37 | 'connection.blocked': True, 38 | 'consumer_cancel_notify': True, 39 | 'consumer_priorities': True, 40 | 'direct_reply_to': True, 41 | 'per_consumer_qos': True, 42 | 'publisher_confirms': True}, 43 | 'cluster_name': 'mock@{}'.format(str(uuid.uuid4())), 44 | 'platform': 'Python {}'.format(platform.python_version()), 45 | 'production': 'aiorabbit', 46 | 'version': version 47 | } 48 | self.transport = mock.create_autospec(asyncio.Transport) 49 | self.transport.write = self._transport_write 50 | self.channel0 = channel0.Channel0( 51 | self.blocked, 52 | self.username, 53 | self.password, 54 | self.virtual_host, 55 | self.HEARTBEAT_INTERVAL, 56 | self.locale, 57 | self.loop, 58 | self.MAX_CHANNELS, 59 | self.product, 60 | self.on_remote_close) 61 | 62 | def _connection_start(self): 63 | self.channel0.process( 64 | commands.Connection.Start( 65 | server_properties=self.server_properties)) 66 | 67 | def _connection_tune(self): 68 | self.channel0.process( 69 | commands.Connection.Tune( 70 | self.SERVER_MAX_CHANNELS, constants.FRAME_MAX_SIZE, 71 | self.SERVER_HEARTBEAT_INTERVAL)) 72 | 73 | def _connection_open_ok(self): 74 | self.channel0.process(commands.Connection.OpenOk()) 75 | 76 | def _connection_close_ok(self): 77 | self.channel0.process(commands.Connection.CloseOk()) 78 | 79 | def _transport_write(self, value: bytes) -> typing.NoReturn: 80 | count, channel, frame_value = frame.unmarshal(value) 81 | self.assertEqual(count, len(value), 'All bytes used') 82 | self.assertEqual(channel, 0, 'Frame was published on channel 0') 83 | if frame_value.name == 'ProtocolHeader': 84 | self.loop.call_soon(self._connection_start) 85 | elif frame_value.name == 'Connection.StartOk': 86 | self.loop.call_soon(self._connection_tune) 87 | elif frame_value.name == 'Connection.TuneOk': 88 | pass 89 | elif frame_value.name == 'Connection.Open': 90 | self.loop.call_soon(self._connection_open_ok) 91 | elif frame_value.name == 'Connection.Close': 92 | self.loop.call_soon(self._connection_close_ok) 93 | elif frame_value.name == 'Connection.CloseOk': 94 | pass 95 | elif frame_value.name == 'Heartbeat': 96 | self.heartbeat.set() 97 | else: 98 | raise RuntimeError(count, channel, frame_value) 99 | 100 | async def open(self): 101 | self.assert_state(state.STATE_UNINITIALIZED) 102 | await self.channel0.open(self.transport) 103 | 104 | def assert_state(self, value): 105 | self.assertEqual( 106 | self.channel0.state_description(value), self.channel0.state) 107 | 108 | def test_negotiation(self): 109 | self.loop.run_until_complete(self.open()) 110 | 111 | 112 | class ProtocolMismatchTestCase(TestCase): 113 | 114 | def _connection_start(self): 115 | self.channel0.process( 116 | commands.Connection.Start( 117 | version_major=1, version_minor=0, 118 | server_properties=self.server_properties)) 119 | 120 | def test_negotiation(self): 121 | with self.assertRaises(exceptions.ClientNegotiationException): 122 | self.loop.run_until_complete(self.open()) 123 | self.assert_state(state.STATE_EXCEPTION) 124 | 125 | 126 | class RemoteCloseTestCase(TestCase): 127 | 128 | def test_with_remote_200(self): 129 | self.loop.run_until_complete(self.open()) 130 | self.channel0.process(commands.Connection.Close(200, 'OK')) 131 | self.assert_state(channel0.STATE_CLOSEOK_SENT) 132 | 133 | def test_with_invalid_path(self): 134 | self.loop.run_until_complete(self.open()) 135 | self.channel0.process( 136 | commands.Connection.Close(402, 'INVALID-PATH')) 137 | self.on_remote_close.assert_called_once_with(402, 'INVALID-PATH') 138 | 139 | 140 | class ClientCloseTestCase(TestCase): 141 | 142 | def test_close(self): 143 | self.loop.run_until_complete(self.open()) 144 | self.assert_state(channel0.STATE_OPENOK_RECEIVED) 145 | self.loop.run_until_complete(self.channel0.close()) 146 | self.assert_state(channel0.STATE_CLOSEOK_RECEIVED) 147 | 148 | 149 | class ConnectionBlockedTestCase(TestCase): 150 | 151 | def test_block_unblock(self): 152 | self.loop.run_until_complete(self.open()) 153 | self.assert_state(channel0.STATE_OPENOK_RECEIVED) 154 | self.channel0.process(commands.Connection.Blocked()) 155 | self.assert_state(channel0.STATE_BLOCKED_RECEIVED) 156 | self.assertTrue(self.channel0.blocked.is_set()) 157 | self.channel0.process(commands.Connection.Unblocked()) 158 | self.assert_state(channel0.STATE_UNBLOCKED_RECEIVED) 159 | self.assertFalse(self.channel0.blocked.is_set()) 160 | 161 | 162 | class HeartbeatTestCase(TestCase): 163 | 164 | def test_heartbeat(self): 165 | self.loop.run_until_complete(self.open()) 166 | self.assert_state(channel0.STATE_OPENOK_RECEIVED) 167 | self.channel0.process(heartbeat.Heartbeat()) 168 | self.assert_state(channel0.STATE_HEARTBEAT_SENT) 169 | self.assertTrue(self.heartbeat.is_set()) 170 | 171 | 172 | class NoHeartbeatTestCase(TestCase): 173 | 174 | HEARTBEAT_INTERVAL = 0 175 | SERVER_HEARTBEAT_INTERVAL = 0 176 | 177 | def test_negotiated_interval(self): 178 | self.loop.run_until_complete(self.open()) 179 | self.assert_state(channel0.STATE_OPENOK_RECEIVED) 180 | self.assertEqual(self.channel0._heartbeat_interval, 0) 181 | 182 | 183 | class NoClientHeartbeatTestCase(TestCase): 184 | 185 | HEARTBEAT_INTERVAL = None 186 | SERVER_HEARTBEAT_INTERVAL = 0 187 | 188 | def test_negotiated_interval(self): 189 | self.loop.run_until_complete(self.open()) 190 | self.assert_state(channel0.STATE_OPENOK_RECEIVED) 191 | self.assertEqual(self.channel0._heartbeat_interval, 0) 192 | 193 | 194 | class SmallerClientHeartbeatTestCase(TestCase): 195 | 196 | HEARTBEAT_INTERVAL = 10 197 | SERVER_HEARTBEAT_INTERVAL = 30 198 | 199 | def test_negotiated_interval(self): 200 | self.loop.run_until_complete(self.open()) 201 | self.assert_state(channel0.STATE_OPENOK_RECEIVED) 202 | self.assertEqual(self.channel0._heartbeat_interval, 10) 203 | 204 | 205 | class HeartbeatCheckTestCase(TestCase): 206 | 207 | HEARTBEAT_INTERVAL = 5 208 | 209 | def test_within_range(self): 210 | self.loop.run_until_complete(self.open()) 211 | self.assert_state(channel0.STATE_OPENOK_RECEIVED) 212 | self.channel0._last_heartbeat = self.loop.time() - 5 213 | self.channel0._heartbeat_check() 214 | self.assertIsInstance( 215 | self.channel0._heartbeat_timer, asyncio.TimerHandle) 216 | self.on_remote_close.assert_not_called() 217 | 218 | def test_too_many_missed(self): 219 | self.loop.run_until_complete(self.open()) 220 | self.assert_state(channel0.STATE_OPENOK_RECEIVED) 221 | current_time = self.loop.time() 222 | with mock.patch.object(self.loop, 'time') as time: 223 | time.return_value = current_time + (self.HEARTBEAT_INTERVAL * 4) 224 | self.channel0.update_last_heartbeat() 225 | self.channel0._last_heartbeat -= (self.HEARTBEAT_INTERVAL * 3) 226 | self.channel0._heartbeat_check() 227 | self.on_remote_close.assert_called_once_with( 228 | 599, 'Too many missed heartbeats') 229 | 230 | 231 | class InvalidFrameTestCase(TestCase): 232 | 233 | def test_invalid_frame_state(self): 234 | self.channel0.process(commands.Basic.Cancel('foo')) 235 | self.assert_state(state.STATE_EXCEPTION) 236 | self.assertIsInstance(self.channel0._exception, 237 | exceptions.AIORabbitException) 238 | 239 | 240 | class ResetTestCase(TestCase): 241 | 242 | def test_reset_attributes(self): 243 | self.loop.run_until_complete(self.open()) 244 | self.assertDictEqual(self.channel0.properties, self.server_properties) 245 | self.channel0.reset() 246 | self.assertDictEqual(self.channel0.properties, {}) 247 | -------------------------------------------------------------------------------- /tests/test_basic.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from pamqp import body, commands, header 4 | 5 | from aiorabbit import exceptions, message 6 | from . import testing 7 | 8 | 9 | class BasicAckTestCase(testing.ClientTestCase): 10 | 11 | @testing.async_test 12 | async def test_validation_errors(self): 13 | await self.connect() 14 | with self.assertRaises(TypeError): 15 | await self.client.basic_ack('foo') 16 | with self.assertRaises(TypeError): 17 | await self.client.basic_ack(1, 1) 18 | 19 | 20 | class BasicCancelTestCase(testing.ClientTestCase): 21 | 22 | @testing.async_test 23 | async def test_validation_errors(self): 24 | await self.connect() 25 | with self.assertRaises(TypeError): 26 | await self.client.basic_cancel(1) 27 | 28 | 29 | class BasicConsumeTestCase(testing.ClientTestCase): 30 | 31 | def setUp(self) -> None: 32 | super().setUp() 33 | self.queue = self.uuid4() 34 | self.exchange = 'amq.topic' 35 | self.routing_key = self.uuid4() 36 | self.body = uuid.uuid4().bytes 37 | 38 | async def on_message(self, msg): 39 | self.assertEqual(msg.exchange, self.exchange) 40 | self.assertEqual(msg.routing_key, self.routing_key) 41 | self.assertEqual(msg.body, self.body) 42 | await self.client.basic_ack(msg.delivery_tag) 43 | self.test_finished.set() 44 | 45 | @testing.async_test 46 | async def test_consume(self): 47 | await self.connect() 48 | await self.client.queue_declare(self.queue) 49 | await self.client.queue_bind(self.queue, self.exchange, '#') 50 | ctag = await self.client.basic_consume( 51 | self.queue, callback=self.on_message) 52 | await self.client.publish(self.exchange, self.routing_key, self.body) 53 | await self.test_finished.wait() 54 | await self.client.basic_cancel(ctag) 55 | 56 | @testing.async_test 57 | async def test_consume_large_message(self): 58 | self.body = '-'.join([ 59 | self.uuid4() for _i in range(0, 100000)]).encode('utf-8') 60 | await self.connect() 61 | await self.client.queue_declare(self.queue) 62 | await self.client.queue_bind(self.queue, self.exchange, '#') 63 | ctag = await self.client.basic_consume( 64 | self.queue, callback=self.on_message) 65 | await self.client.publish(self.exchange, self.routing_key, self.body) 66 | await self.test_finished.wait() 67 | await self.client.basic_cancel(ctag) 68 | 69 | @testing.async_test 70 | async def test_consume_message_pending(self): 71 | await self.connect() 72 | await self.client.queue_declare(self.queue) 73 | await self.client.queue_bind(self.queue, self.exchange, '#') 74 | await self.client.publish(self.exchange, self.routing_key, self.body) 75 | ctag = await self.client.basic_consume( 76 | self.queue, callback=self.on_message) 77 | await self.test_finished.wait() 78 | await self.client.basic_cancel(ctag) 79 | 80 | @testing.async_test 81 | async def test_consume_sync_callback(self): 82 | 83 | def on_message(msg): 84 | self.assertEqual(msg.exchange, self.exchange) 85 | self.assertEqual(msg.routing_key, self.routing_key) 86 | self.assertEqual(msg.body, self.body) 87 | self.test_finished.set() 88 | 89 | await self.connect() 90 | await self.client.queue_declare(self.queue) 91 | await self.client.queue_bind(self.queue, self.exchange, '#') 92 | await self.client.publish(self.exchange, self.routing_key, self.body) 93 | ctag = await self.client.basic_consume(self.queue, callback=on_message) 94 | await self.test_finished.wait() 95 | await self.client.basic_cancel(ctag) 96 | 97 | @testing.async_test 98 | async def test_not_found(self): 99 | await self.connect() 100 | with self.assertRaises(exceptions.NotFound): 101 | await self.client.basic_consume('foo', callback=lambda x: x) 102 | 103 | @testing.async_test 104 | async def test_validation_errors(self): 105 | await self.connect() 106 | with self.assertRaises(TypeError): 107 | await self.client.basic_consume(1, callback=lambda x: x) 108 | with self.assertRaises(TypeError): 109 | await self.client.basic_consume('foo', 1, callback=lambda x: x) 110 | with self.assertRaises(TypeError): 111 | await self.client.basic_consume( 112 | 'foo', False, 1, callback=lambda x: x) 113 | with self.assertRaises(TypeError): 114 | await self.client.basic_consume( 115 | 'foo', False, False, 1, callback=lambda x: x) 116 | with self.assertRaises(TypeError): 117 | await self.client.basic_consume( 118 | 'foo', False, False, False, 1, callback=lambda x: x) 119 | with self.assertRaises(TypeError): 120 | await self.client.basic_consume('foo', callback=True) 121 | with self.assertRaises(ValueError): 122 | await self.client.basic_consume('foo') 123 | with self.assertRaises(TypeError): 124 | await self.client.basic_consume( 125 | 'foo', callback=lambda x: x, consumer_tag=1) 126 | 127 | 128 | class BasicGetTestCase(testing.ClientTestCase): 129 | 130 | @testing.async_test 131 | async def test_basic_get(self): 132 | queue = self.uuid4() 133 | exchange = 'amq.direct' 134 | routing_key = '#' 135 | msg_body = uuid.uuid4().bytes 136 | await self.connect() 137 | msg_count, consumer_count = await self.client.queue_declare(queue) 138 | self.assertEqual(msg_count, 0) 139 | self.assertEqual(consumer_count, 0) 140 | result = await self.client.basic_get(queue) 141 | self.assertIsNone(result) 142 | await self.client.queue_bind(queue, exchange, routing_key) 143 | await self.client.publish(exchange, routing_key, msg_body) 144 | result = await self.client.basic_get(queue) 145 | self.assertIsInstance(result, message.Message) 146 | self.assertEqual(result.body, msg_body) 147 | self.assertEqual(result.message_count, 0) 148 | await self.client.basic_ack(result.delivery_tag) 149 | await self.client.queue_delete(queue) 150 | 151 | @testing.async_test 152 | async def test_basic_getok_message_count(self): 153 | queue = self.uuid4() 154 | exchange = 'amq.direct' 155 | routing_key = '#' 156 | msg_body = uuid.uuid4().bytes 157 | await self.connect() 158 | await self.client.queue_declare(queue) 159 | 160 | result = await self.client.basic_get(queue) 161 | self.assertIsNone(result) 162 | await self.client.queue_bind(queue, exchange, routing_key) 163 | await self.client.publish(exchange, routing_key, msg_body) 164 | await self.client.publish(exchange, routing_key, uuid.uuid4().bytes) 165 | await self.client.publish(exchange, routing_key, uuid.uuid4().bytes) 166 | 167 | result = await self.client.basic_get(queue) 168 | self.assertIsInstance(result, message.Message) 169 | self.assertEqual(result.body, msg_body) 170 | self.assertEqual(result.message_count, 2) 171 | await self.client.basic_ack(result.delivery_tag) 172 | 173 | result = await self.client.basic_get(queue) 174 | self.assertIsInstance(result, message.Message) 175 | self.assertEqual(result.message_count, 1) 176 | await self.client.basic_nack(result.delivery_tag, requeue=False) 177 | 178 | result = await self.client.basic_get(queue) 179 | self.assertIsInstance(result, message.Message) 180 | self.assertEqual(result.message_count, 0) 181 | await self.client.basic_reject(result.delivery_tag) 182 | 183 | await self.client.queue_delete(queue) 184 | 185 | @testing.async_test 186 | async def test_basic_get_validation_errors(self): 187 | await self.connect() 188 | with self.assertRaises(TypeError): 189 | await self.client.basic_get(1) 190 | with self.assertRaises(TypeError): 191 | await self.client.basic_get('foo', 1) 192 | 193 | 194 | class BasicNackTestCase(testing.ClientTestCase): 195 | 196 | @testing.async_test 197 | async def test_validation_errors(self): 198 | await self.connect() 199 | with self.assertRaises(TypeError): 200 | await self.client.basic_nack('foo') 201 | with self.assertRaises(TypeError): 202 | await self.client.basic_nack(1, 1) 203 | with self.assertRaises(TypeError): 204 | await self.client.basic_nack(1, False, 1) 205 | 206 | 207 | class BasicPublishTestCase(testing.ClientTestCase): 208 | 209 | @testing.async_test 210 | async def test_basic_publish_raises(self): 211 | with self.assertRaises(NotImplementedError): 212 | await self.client.basic_publish() 213 | 214 | 215 | class BasicQosTestCase(testing.ClientTestCase): 216 | 217 | @testing.async_test 218 | async def test_basic_qos_raises(self): 219 | self.raises = self.assertRaises(NotImplementedError) 220 | with self.raises: 221 | await self.client.basic_qos() 222 | 223 | 224 | class BasicRecoverTestCase(testing.ClientTestCase): 225 | 226 | @testing.async_test 227 | async def test_basic_recover(self): 228 | await self.connect() 229 | await self.client.basic_recover(True) 230 | 231 | @testing.async_test 232 | async def test_basic_recover_false_raises(self): 233 | await self.connect() 234 | with self.assertRaises(exceptions.NotImplemented): 235 | await self.client.basic_recover(False) 236 | 237 | @testing.async_test 238 | async def test_validation_errors(self): 239 | await self.connect() 240 | with self.assertRaises(TypeError): 241 | await self.client.basic_recover(1) 242 | 243 | 244 | class BasicRejectTestCase(testing.ClientTestCase): 245 | 246 | @testing.async_test 247 | async def test_validation_errors(self): 248 | await self.connect() 249 | with self.assertRaises(TypeError): 250 | await self.client.basic_reject('foo') 251 | with self.assertRaises(TypeError): 252 | await self.client.basic_reject(1, 1) 253 | 254 | 255 | class BasicReturnTestCase(testing.ClientTestCase): 256 | 257 | def setUp(self) -> None: 258 | super().setUp() 259 | self.exchange = 'amq.topic' 260 | self.routing_key = self.uuid4() 261 | self.body = uuid.uuid4().bytes 262 | 263 | @testing.async_test 264 | async def test_basic_return(self): 265 | 266 | async def on_return(msg: message.Message) -> None: 267 | self.assertEqual(msg.reply_code, 404) 268 | self.assertEqual(msg.reply_text, 'Not Found') 269 | self.assertEqual(msg.exchange, self.exchange) 270 | self.assertEqual(msg.body, self.body) 271 | self.test_finished.set() 272 | 273 | self.client.register_basic_return_callback(on_return) 274 | 275 | await self.connect() 276 | await self.client.publish(self.exchange, self.routing_key, self.body) 277 | 278 | # Fake the Basic.Return 279 | self.client._on_frame(self.client._channel, commands.Basic.Return( 280 | 404, 'Not Found', self.exchange, self.routing_key)) 281 | self.client._on_frame(self.client._channel, header.ContentHeader( 282 | 0, len(self.body), commands.Basic.Properties())) 283 | self.client._on_frame( 284 | self.client._channel, body.ContentBody(self.body)) 285 | 286 | await self.test_finished.wait() 287 | -------------------------------------------------------------------------------- /aiorabbit/channel0.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import asyncio 3 | import platform 4 | import typing 5 | 6 | from pamqp import commands, constants, frame, header, heartbeat 7 | 8 | from aiorabbit import exceptions, state 9 | from aiorabbit.__version__ import version 10 | 11 | COMMANDS = typing.Union[commands.Connection.Blocked, 12 | commands.Connection.Unblocked, 13 | commands.Connection.Start, 14 | commands.Connection.Tune, 15 | commands.Connection.OpenOk, 16 | commands.Connection.Close, 17 | commands.Connection.CloseOk, 18 | heartbeat.Heartbeat] 19 | 20 | FRAMES = { 21 | 'Connection.Blocked', 22 | 'Connection.Unblocked', 23 | 'Connection.Start', 24 | 'Connection.Tune', 25 | 'Connection.OpenOk', 26 | 'Connection.Close', 27 | 'Connection.CloseOk', 28 | 'Heartbeat' 29 | } 30 | 31 | STATE_PROTOCOL_HEADER_SENT = 0x10 32 | STATE_START_RECEIVED = 0x11 33 | STATE_STARTOK_SENT = 0x12 34 | STATE_TUNE_RECEIVED = 0x13 35 | STATE_TUNEOK_SENT = 0x14 36 | STATE_OPEN_SENT = 0x15 37 | STATE_OPENOK_RECEIVED = 0x16 38 | STATE_HEARTBEAT_RECEIVED = 0x17 39 | STATE_HEARTBEAT_SENT = 0x18 40 | STATE_CLOSE_RECEIVED = 0x19 41 | STATE_CLOSE_SENT = 0x20 42 | STATE_CLOSEOK_RECEIVED = 0x21 43 | STATE_CLOSEOK_SENT = 0x22 44 | STATE_BLOCKED_RECEIVED = 0x23 45 | STATE_UNBLOCKED_RECEIVED = 0x24 46 | 47 | _STATE_MAP = { 48 | state.STATE_UNINITIALIZED: 'Uninitialized', 49 | state.STATE_EXCEPTION: 'Exception Raised', 50 | STATE_PROTOCOL_HEADER_SENT: 'Protocol Header Sent', 51 | STATE_START_RECEIVED: 'Start Received', 52 | STATE_STARTOK_SENT: 'StartOk Sent', 53 | STATE_TUNE_RECEIVED: 'Tune Received', 54 | STATE_TUNEOK_SENT: 'TuneOk Sent', 55 | STATE_OPEN_SENT: 'Open Sent', 56 | STATE_OPENOK_RECEIVED: 'OpenOk Received', 57 | STATE_HEARTBEAT_RECEIVED: 'Heartbeat Received', 58 | STATE_HEARTBEAT_SENT: 'Heartbeat Sent', 59 | STATE_CLOSE_RECEIVED: 'Connection Close Received', 60 | STATE_CLOSE_SENT: 'Connection Close Sent', 61 | STATE_CLOSEOK_SENT: 'Connection CloseOk Sent', 62 | STATE_CLOSEOK_RECEIVED: 'Connection CloseOk Received', 63 | STATE_BLOCKED_RECEIVED: 'Connection Blocked Received', 64 | STATE_UNBLOCKED_RECEIVED: 'Connection Unblocked Received' 65 | } 66 | 67 | _STATE_TRANSITIONS = { 68 | state.STATE_UNINITIALIZED: [STATE_PROTOCOL_HEADER_SENT], 69 | state.STATE_EXCEPTION: [STATE_CLOSE_SENT], 70 | STATE_PROTOCOL_HEADER_SENT: [STATE_START_RECEIVED], 71 | STATE_START_RECEIVED: [STATE_STARTOK_SENT], 72 | STATE_STARTOK_SENT: [STATE_TUNE_RECEIVED, STATE_CLOSE_RECEIVED], 73 | STATE_TUNE_RECEIVED: [STATE_TUNEOK_SENT], 74 | STATE_TUNEOK_SENT: [STATE_OPEN_SENT, STATE_CLOSE_RECEIVED], 75 | STATE_OPEN_SENT: [STATE_OPENOK_RECEIVED, STATE_CLOSE_RECEIVED], 76 | STATE_OPENOK_RECEIVED: [ 77 | STATE_BLOCKED_RECEIVED, 78 | STATE_HEARTBEAT_RECEIVED, 79 | STATE_CLOSE_RECEIVED, 80 | STATE_CLOSE_SENT], 81 | STATE_CLOSE_RECEIVED: [STATE_CLOSEOK_SENT], 82 | STATE_CLOSE_SENT: [STATE_CLOSEOK_RECEIVED], 83 | STATE_CLOSEOK_RECEIVED: [STATE_PROTOCOL_HEADER_SENT], 84 | STATE_CLOSEOK_SENT: [STATE_PROTOCOL_HEADER_SENT], 85 | STATE_BLOCKED_RECEIVED: [ 86 | STATE_UNBLOCKED_RECEIVED, 87 | STATE_CLOSE_RECEIVED, 88 | STATE_HEARTBEAT_RECEIVED], 89 | STATE_UNBLOCKED_RECEIVED: [ 90 | STATE_CLOSE_RECEIVED, 91 | STATE_HEARTBEAT_RECEIVED], 92 | STATE_HEARTBEAT_RECEIVED: [ 93 | STATE_HEARTBEAT_SENT, 94 | STATE_BLOCKED_RECEIVED, 95 | STATE_UNBLOCKED_RECEIVED, 96 | STATE_CLOSE_RECEIVED], 97 | STATE_HEARTBEAT_SENT: [ 98 | STATE_HEARTBEAT_RECEIVED, 99 | STATE_BLOCKED_RECEIVED, 100 | STATE_UNBLOCKED_RECEIVED, 101 | STATE_CLOSE_RECEIVED, 102 | STATE_CLOSE_SENT] 103 | } 104 | 105 | 106 | class Channel0(state.StateManager): 107 | """Manages the state of the connection on Channel 0""" 108 | 109 | STATE_MAP = _STATE_MAP 110 | STATE_TRANSITIONS = _STATE_TRANSITIONS 111 | 112 | def __init__(self, 113 | blocked: asyncio.Event, 114 | username: str, 115 | password: str, 116 | virtual_host: str, 117 | heartbeat_interval: typing.Optional[int], 118 | locale: str, 119 | loop: asyncio.AbstractEventLoop, 120 | max_channels: int, 121 | product: str, 122 | on_remote_close: typing.Callable): 123 | super().__init__(loop) 124 | self.blocked = blocked 125 | self.max_channels = max_channels 126 | self.max_frame_size = constants.FRAME_MAX_SIZE 127 | self.properties: dict = {} 128 | self._heartbeat_interval = heartbeat_interval 129 | self._heartbeat_timer: typing.Optional[asyncio.TimerHandle] = None 130 | self._last_error: typing.Tuple[int, typing.Optional[str]] = (0, None) 131 | self._last_heartbeat: int = 0 132 | self._locale = locale 133 | self._on_remote_close = on_remote_close 134 | self._password = password 135 | self._product = product 136 | self._transport: typing.Optional[asyncio.Transport] = None 137 | self._username = username 138 | self._virtual_host = virtual_host 139 | 140 | def process(self, value: COMMANDS) -> None: 141 | if isinstance(value, commands.Connection.Start): 142 | self._set_state(STATE_START_RECEIVED) 143 | self._process_start(value) 144 | elif isinstance(value, commands.Connection.Tune): 145 | self._set_state(STATE_TUNE_RECEIVED) 146 | self._process_tune(value) 147 | elif isinstance(value, commands.Connection.OpenOk): 148 | self._set_state(STATE_OPENOK_RECEIVED) 149 | elif isinstance(value, commands.Connection.Blocked): 150 | self._set_state(STATE_BLOCKED_RECEIVED) 151 | self.blocked.set() 152 | elif isinstance(value, commands.Connection.Unblocked): 153 | self._set_state(STATE_UNBLOCKED_RECEIVED) 154 | self.blocked.clear() 155 | elif isinstance(value, commands.Connection.Close): 156 | self._set_state(STATE_CLOSE_RECEIVED) 157 | self._transport.write( 158 | frame.marshal(commands.Connection.CloseOk(), 0)) 159 | self._set_state(STATE_CLOSEOK_SENT) 160 | self._on_remote_close(value.reply_code, value.reply_text) 161 | elif isinstance(value, commands.Connection.CloseOk): 162 | self._set_state(STATE_CLOSEOK_RECEIVED) 163 | elif isinstance(value, heartbeat.Heartbeat): 164 | self._set_state(STATE_HEARTBEAT_RECEIVED) 165 | self._last_heartbeat = self._loop.time() 166 | self._transport.write(frame.marshal(heartbeat.Heartbeat(), 0)) 167 | self._set_state(STATE_HEARTBEAT_SENT) 168 | else: 169 | self._set_state(state.STATE_EXCEPTION, 170 | exceptions.AIORabbitException( 171 | 'Unsupported Frame Passed to Channel0')) 172 | 173 | async def open(self, transport: asyncio.Transport) -> bool: 174 | self._transport = transport 175 | self._transport.write(frame.marshal(header.ProtocolHeader(), 0)) 176 | self._set_state(STATE_PROTOCOL_HEADER_SENT) 177 | result = await self._wait_on_state( 178 | STATE_OPENOK_RECEIVED, STATE_CLOSEOK_SENT) 179 | if self._heartbeat_interval: 180 | self._logger.debug('Checking for heartbeats every %2f seconds', 181 | self._heartbeat_interval) 182 | self._heartbeat_timer = self._loop.call_later( 183 | self._heartbeat_interval, self._heartbeat_check) 184 | return result == STATE_OPENOK_RECEIVED 185 | 186 | async def close(self, code=200) -> None: 187 | if self._heartbeat_timer is not None: 188 | self._heartbeat_timer.cancel() 189 | self._heartbeat_timer = None 190 | self._transport.write(frame.marshal( 191 | commands.Connection.Close(code, 'Client Requested', 0, 0), 0)) 192 | self._set_state(STATE_CLOSE_SENT) 193 | await self._wait_on_state(STATE_CLOSEOK_RECEIVED) 194 | 195 | def reset(self): 196 | self._logger.debug('Resetting channel0') 197 | self._heartbeat_timer.cancel() 198 | self._heartbeat_timer = None 199 | self._reset_state(state.STATE_UNINITIALIZED) 200 | self._last_heartbeat = 0 201 | self._transport: typing.Optional[asyncio.Transport] = None 202 | self.properties: dict = {} 203 | 204 | @property 205 | def is_closed(self) -> bool: 206 | return self._state in [STATE_CLOSEOK_RECEIVED, 207 | STATE_CLOSEOK_SENT, 208 | state.STATE_EXCEPTION] 209 | 210 | def update_last_heartbeat(self) -> None: 211 | """Invoked by the client whenever traffic is received""" 212 | self._last_heartbeat = self._loop.time() 213 | 214 | def _heartbeat_check(self): 215 | threshold = self._loop.time() - (self._heartbeat_interval * 2) 216 | if 0 < self._last_heartbeat < threshold: 217 | msg = 'No heartbeat in {:2f} seconds'.format( 218 | self._loop.time() - self._last_heartbeat) 219 | self._logger.critical(msg) 220 | self._heartbeat_timer = None 221 | self._on_remote_close(599, 'Too many missed heartbeats') 222 | else: 223 | if self._heartbeat_timer: 224 | self._heartbeat_timer.cancel() 225 | self._heartbeat_timer = self._loop.call_later( 226 | self._heartbeat_interval, self._heartbeat_check) 227 | 228 | @staticmethod 229 | def _negotiate(client: int, server: int) -> int: 230 | """Return the negotiated value between what the client has requested 231 | and the server has requested for how the two will communicate. 232 | 233 | """ 234 | return min(client, server) or (client or server) 235 | 236 | def _process_start(self, value: commands.Connection.Start) -> None: 237 | if (value.version_major, 238 | value.version_minor) != (constants.VERSION[0], 239 | constants.VERSION[1]): 240 | self._logger.warning( 241 | 'AMQP version error (received %i.%i, expected %r)', 242 | value.version_major, value.version_minor, constants.VERSION) 243 | self._transport.close() 244 | return self._set_state( 245 | state.STATE_EXCEPTION, 246 | exceptions.ClientNegotiationException( 247 | 'AMQP version error (received {}.{}, expected {})'.format( 248 | value.version_major, value.version_minor, 249 | constants.VERSION))) 250 | 251 | self.properties = dict(value.server_properties) 252 | for key in self.properties: 253 | if key == 'capabilities': 254 | for capability in self.properties[key]: 255 | self._logger.debug( 256 | 'Server supports %s: %r', 257 | capability, self.properties[key][capability]) 258 | else: 259 | self._logger.debug('Server %s: %r', key, self.properties[key]) 260 | self._transport.write(frame.marshal( 261 | commands.Connection.StartOk( 262 | client_properties={ 263 | 'product': self._product, 264 | 'platform': 'Python {}'.format(platform.python_version()), 265 | 'capabilities': {'authentication_failure_close': True, 266 | 'basic.nack': True, 267 | 'connection.blocked': True, 268 | 'consumer_cancel_notify': True, 269 | 'consumer_priorities': True, 270 | 'direct_reply_to': True, 271 | 'per_consumer_qos': True, 272 | 'publisher_confirms': True}, 273 | 'information': 'See https://aiorabbit.readthedocs.io', 274 | 'version': version}, 275 | response='\0{}\0{}'.format(self._username, self._password), 276 | locale=self._locale), 0)) 277 | self._set_state(STATE_STARTOK_SENT) 278 | 279 | def _process_tune(self, value: commands.Connection.Tune) -> None: 280 | self.max_channels = self._negotiate( 281 | self.max_channels, value.channel_max) 282 | self.max_frame_size = self._negotiate( 283 | self.max_frame_size, value.frame_max) 284 | if self._heartbeat_interval is None: 285 | self._heartbeat_interval = value.heartbeat 286 | elif not self._heartbeat_interval and not value.heartbeat: 287 | self._heartbeat_interval = 0 288 | self._transport.write(frame.marshal( 289 | commands.Connection.TuneOk( 290 | self.max_channels, self.max_frame_size, 291 | self._heartbeat_interval), 0)) 292 | self._set_state(STATE_TUNEOK_SENT) 293 | self._transport.write( 294 | frame.marshal(commands.Connection.Open(self._virtual_host), 0)) 295 | self._set_state(STATE_OPEN_SENT) 296 | -------------------------------------------------------------------------------- /aiorabbit/client.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import asyncio 3 | import collections 4 | import dataclasses 5 | import datetime 6 | import math 7 | import re 8 | import socket 9 | import ssl 10 | import typing 11 | from urllib import parse 12 | 13 | from pamqp import base, body, commands, frame, header 14 | import yarl 15 | 16 | from aiorabbit import (channel0, DEFAULT_LOCALE, DEFAULT_PRODUCT, DEFAULT_URL, 17 | exceptions, message, protocol, state, types) 18 | 19 | NamePattern = re.compile(r'^[\w:.-]+$', flags=re.UNICODE) 20 | 21 | STATE_DISCONNECTED = 0x11 22 | STATE_CONNECTING = 0x12 23 | STATE_CONNECTED = 0x13 24 | STATE_OPENED = 0x14 25 | STATE_UPDATE_SECRET_SENT = 0x15 26 | STATE_UPDATE_SECRETOK_RECEIVED = 0x16 27 | STATE_OPENING_CHANNEL = 0x17 28 | STATE_CHANNEL_OPEN_SENT = 0x20 29 | STATE_CHANNEL_OPENOK_RECEIVED = 0x21 30 | STATE_CHANNEL_CLOSE_RECEIVED = 0x22 31 | STATE_CHANNEL_CLOSE_SENT = 0x23 32 | STATE_CHANNEL_CLOSEOK_RECEIVED = 0x24 33 | STATE_CHANNEL_CLOSEOK_SENT = 0x25 34 | STATE_CHANNEL_FLOW_RECEIVED = 0x26 35 | STATE_CHANNEL_FLOWOK_SENT = 0x27 36 | STATE_CONFIRM_SELECT_SENT = 0x30 37 | STATE_CONFIRM_SELECTOK_RECEIVED = 0x31 38 | STATE_EXCHANGE_BIND_SENT = 0x40 39 | STATE_EXCHANGE_BINDOK_RECEIVED = 0x41 40 | STATE_EXCHANGE_DECLARE_SENT = 0x42 41 | STATE_EXCHANGE_DECLAREOK_RECEIVED = 0x43 42 | STATE_EXCHANGE_DELETE_SENT = 0x44 43 | STATE_EXCHANGE_DELETEOK_RECEIVED = 0x45 44 | STATE_EXCHANGE_UNBIND_SENT = 0x46 45 | STATE_EXCHANGE_UNBINDOK_RECEIVED = 0x47 46 | STATE_QUEUE_BIND_SENT = 0x50 47 | STATE_QUEUE_BINDOK_RECEIVED = 0x51 48 | STATE_QUEUE_DECLARE_SENT = 0x52 49 | STATE_QUEUE_DECLAREOK_RECEIVED = 0x53 50 | STATE_QUEUE_DELETE_SENT = 0x54 51 | STATE_QUEUE_DELETEOK_RECEIVED = 0x55 52 | STATE_QUEUE_PURGE_SENT = 0x56 53 | STATE_QUEUE_PURGEOK_RECEIVED = 0x57 54 | STATE_QUEUE_UNBIND_SENT = 0x58 55 | STATE_QUEUE_UNBINDOK_RECEIVED = 0x59 56 | STATE_TX_SELECT_SENT = 0x60 57 | STATE_TX_SELECTOK_RECEIVED = 0x61 58 | STATE_TX_COMMIT_SENT = 0x62 59 | STATE_TX_COMMITOK_RECEIVED = 0x63 60 | STATE_TX_ROLLBACK_SENT = 0x64 61 | STATE_TX_ROLLBACKOK_RECEIVED = 0x65 62 | STATE_BASIC_ACK_RECEIVED = 0x70 63 | STATE_BASIC_ACK_SENT = 0x71 64 | STATE_BASIC_CANCEL_RECEIVED = 0x72 65 | STATE_BASIC_CANCEL_SENT = 0x73 66 | STATE_BASIC_CANCELOK_RECEIVED = 0x74 67 | STATE_BASIC_CANCELOK_SENT = 0x75 68 | STATE_BASIC_CONSUME_SENT = 0x76 69 | STATE_BASIC_CONSUMEOK_RECEIVED = 0x77 70 | STATE_BASIC_DELIVER_RECEIVED = 0x78 71 | STATE_CONTENT_HEADER_RECEIVED = 0x79 72 | STATE_CONTENT_BODY_RECEIVED = 0x80 73 | STATE_BASIC_GET_SENT = 0x81 74 | STATE_BASIC_GETEMPTY_RECEIVED = 0x82 75 | STATE_BASIC_GETOK_RECEIVED = 0x83 76 | STATE_BASIC_NACK_RECEIVED = 0x84 77 | STATE_BASIC_NACK_SENT = 0x85 78 | STATE_BASIC_QOS_SENT = 0x89 79 | STATE_BASIC_QOSOK_RECEIVED = 0x90 80 | STATE_BASIC_RECOVER_SENT = 0x91 81 | STATE_BASIC_RECOVEROK_RECEIVED = 0x92 82 | STATE_BASIC_REJECT_RECEIVED = 0x93 83 | STATE_BASIC_REJECT_SENT = 0x94 84 | STATE_BASIC_RETURN_RECEIVED = 0x95 85 | STATE_MESSAGE_ASSEMBLED = 0x100 86 | STATE_MESSAGE_PUBLISHED = 0x101 87 | STATE_CLOSING = 0x102 88 | STATE_CLOSED = 0x103 89 | 90 | _STATE_MAP = { 91 | state.STATE_UNINITIALIZED: 'Uninitialized', 92 | state.STATE_EXCEPTION: 'Exception Raised', 93 | STATE_DISCONNECTED: 'Disconnected', 94 | STATE_CONNECTING: 'Connecting', 95 | STATE_CONNECTED: 'Connected', 96 | STATE_OPENED: 'Opened', 97 | STATE_UPDATE_SECRET_SENT: 'Updating Secret', 98 | STATE_UPDATE_SECRETOK_RECEIVED: 'Secret Updated', 99 | STATE_OPENING_CHANNEL: 'Opening Channel', 100 | STATE_CHANNEL_OPEN_SENT: 'Channel Requested', 101 | STATE_CHANNEL_OPENOK_RECEIVED: 'Channel Open', 102 | STATE_CHANNEL_CLOSE_RECEIVED: 'Channel Close Received', 103 | STATE_CHANNEL_CLOSE_SENT: 'Channel Close Sent', 104 | STATE_CHANNEL_CLOSEOK_RECEIVED: 'Channel CloseOk Received', 105 | STATE_CHANNEL_CLOSEOK_SENT: 'Channel CloseOk Sent', 106 | STATE_CHANNEL_FLOW_RECEIVED: 'Channel Flow Received', 107 | STATE_CHANNEL_FLOWOK_SENT: 'Channel FlowOk Sent', 108 | STATE_CONFIRM_SELECT_SENT: 'Enabling Publisher Confirms', 109 | STATE_CONFIRM_SELECTOK_RECEIVED: 'Publisher Confirms Enabled', 110 | STATE_EXCHANGE_BIND_SENT: 'Binding Exchange', 111 | STATE_EXCHANGE_BINDOK_RECEIVED: 'Exchange Bound', 112 | STATE_EXCHANGE_DECLARE_SENT: 'Declaring Exchange', 113 | STATE_EXCHANGE_DECLAREOK_RECEIVED: 'Exchange Declared', 114 | STATE_EXCHANGE_DELETE_SENT: 'Deleting Exchange', 115 | STATE_EXCHANGE_DELETEOK_RECEIVED: 'Exchange Deleted', 116 | STATE_EXCHANGE_UNBIND_SENT: 'Unbinding Exchange', 117 | STATE_EXCHANGE_UNBINDOK_RECEIVED: 'Exchange unbound', 118 | STATE_QUEUE_BIND_SENT: 'Binding Queue', 119 | STATE_QUEUE_BINDOK_RECEIVED: 'Queue Bound', 120 | STATE_QUEUE_DECLARE_SENT: 'Declaring Queue', 121 | STATE_QUEUE_DECLAREOK_RECEIVED: 'Queue Declared', 122 | STATE_QUEUE_DELETE_SENT: 'Deleting Queue', 123 | STATE_QUEUE_DELETEOK_RECEIVED: 'Queue Deleted', 124 | STATE_QUEUE_PURGE_SENT: 'Purging Queue', 125 | STATE_QUEUE_PURGEOK_RECEIVED: 'Queue Purged', 126 | STATE_QUEUE_UNBIND_SENT: 'Unbinding Queue', 127 | STATE_QUEUE_UNBINDOK_RECEIVED: 'Queue unbound', 128 | STATE_TX_SELECT_SENT: 'Starting Transaction', 129 | STATE_TX_SELECTOK_RECEIVED: 'Transaction started', 130 | STATE_TX_COMMIT_SENT: 'Committing Transaction', 131 | STATE_TX_COMMITOK_RECEIVED: 'Transaction committed', 132 | STATE_TX_ROLLBACK_SENT: 'Aborting Transaction', 133 | STATE_TX_ROLLBACKOK_RECEIVED: 'Transaction aborted', 134 | STATE_BASIC_ACK_RECEIVED: 'Received message acknowledgement', 135 | STATE_BASIC_ACK_SENT: 'Sent message acknowledgement', 136 | STATE_BASIC_CANCEL_RECEIVED: 'Server canceled consumer', 137 | STATE_BASIC_CANCEL_SENT: 'Cancelling Consumer', 138 | STATE_BASIC_CANCELOK_RECEIVED: 'Consumer cancelled', 139 | STATE_BASIC_CANCELOK_SENT: 'Acknowledging cancelled consumer', 140 | STATE_BASIC_CONSUME_SENT: 'Initiating consuming of messages', 141 | STATE_BASIC_CONSUMEOK_RECEIVED: 'Consuming of messages initiated', 142 | STATE_BASIC_DELIVER_RECEIVED: 'Server delivered message', 143 | STATE_CONTENT_HEADER_RECEIVED: 'Received content header', 144 | STATE_CONTENT_BODY_RECEIVED: 'Received content body', 145 | STATE_BASIC_GET_SENT: 'Requesting individual message', 146 | STATE_BASIC_GETEMPTY_RECEIVED: 'Message not available', 147 | STATE_BASIC_GETOK_RECEIVED: 'Individual message to be delivered', 148 | STATE_BASIC_NACK_RECEIVED: 'Server sent negative acknowledgement', 149 | STATE_BASIC_NACK_SENT: 'Sending negative acknowledgement', 150 | STATE_MESSAGE_PUBLISHED: 'Message Published', 151 | STATE_BASIC_QOS_SENT: 'Setting QoS', 152 | STATE_BASIC_QOSOK_RECEIVED: 'QoS set', 153 | STATE_BASIC_RECOVER_SENT: 'Sending recover request', 154 | STATE_BASIC_RECOVEROK_RECEIVED: 'Recover request received', 155 | STATE_BASIC_REJECT_RECEIVED: 'Server rejected Message', 156 | STATE_BASIC_REJECT_SENT: 'Sending Message rejection', 157 | STATE_BASIC_RETURN_RECEIVED: 'Server returned message', 158 | STATE_MESSAGE_ASSEMBLED: 'Message assembled', 159 | STATE_CLOSING: 'Closing', 160 | STATE_CLOSED: 'Closed', 161 | } 162 | 163 | _IDLE_STATE = [ 164 | STATE_UPDATE_SECRET_SENT, 165 | STATE_BASIC_CANCEL_SENT, 166 | STATE_CHANNEL_CLOSE_RECEIVED, 167 | STATE_CHANNEL_CLOSE_SENT, 168 | STATE_CHANNEL_FLOW_RECEIVED, 169 | STATE_CONFIRM_SELECT_SENT, 170 | STATE_EXCHANGE_BIND_SENT, 171 | STATE_EXCHANGE_DECLARE_SENT, 172 | STATE_EXCHANGE_DELETE_SENT, 173 | STATE_EXCHANGE_UNBIND_SENT, 174 | STATE_QUEUE_BIND_SENT, 175 | STATE_QUEUE_DECLARE_SENT, 176 | STATE_QUEUE_DELETE_SENT, 177 | STATE_QUEUE_PURGE_SENT, 178 | STATE_QUEUE_UNBIND_SENT, 179 | STATE_TX_SELECT_SENT, 180 | STATE_TX_COMMIT_SENT, 181 | STATE_TX_ROLLBACK_SENT, 182 | STATE_BASIC_CONSUME_SENT, 183 | STATE_BASIC_DELIVER_RECEIVED, 184 | STATE_BASIC_GET_SENT, 185 | STATE_BASIC_QOS_SENT, 186 | STATE_BASIC_RECOVER_SENT, 187 | STATE_MESSAGE_PUBLISHED, 188 | STATE_CLOSING, 189 | STATE_CLOSED 190 | ] 191 | 192 | _STATE_TRANSITIONS = { 193 | state.STATE_UNINITIALIZED: [STATE_DISCONNECTED], 194 | state.STATE_EXCEPTION: [STATE_CLOSING, STATE_CLOSED, STATE_DISCONNECTED], 195 | STATE_DISCONNECTED: [STATE_CONNECTING], 196 | STATE_CONNECTING: [STATE_CONNECTED, STATE_CLOSED], 197 | STATE_CONNECTED: [STATE_OPENED, STATE_CLOSING, STATE_CLOSED], 198 | STATE_OPENED: [STATE_OPENING_CHANNEL], 199 | STATE_OPENING_CHANNEL: [STATE_CHANNEL_OPEN_SENT], 200 | STATE_UPDATE_SECRET_SENT: [STATE_UPDATE_SECRETOK_RECEIVED], 201 | STATE_UPDATE_SECRETOK_RECEIVED: _IDLE_STATE, 202 | STATE_CHANNEL_OPEN_SENT: [STATE_CHANNEL_OPENOK_RECEIVED], 203 | STATE_CHANNEL_OPENOK_RECEIVED: _IDLE_STATE, 204 | STATE_CHANNEL_CLOSE_RECEIVED: [STATE_CHANNEL_CLOSEOK_SENT], 205 | STATE_CHANNEL_CLOSE_SENT: [STATE_CHANNEL_CLOSEOK_RECEIVED], 206 | STATE_CHANNEL_CLOSEOK_RECEIVED: [STATE_OPENING_CHANNEL, STATE_CLOSING], 207 | STATE_CHANNEL_CLOSEOK_SENT: [STATE_OPENING_CHANNEL], 208 | STATE_CHANNEL_FLOW_RECEIVED: [STATE_CHANNEL_FLOWOK_SENT], 209 | STATE_CHANNEL_FLOWOK_SENT: _IDLE_STATE, 210 | STATE_CONFIRM_SELECT_SENT: [STATE_CONFIRM_SELECTOK_RECEIVED], 211 | STATE_CONFIRM_SELECTOK_RECEIVED: _IDLE_STATE, 212 | STATE_EXCHANGE_BIND_SENT: [ 213 | STATE_CHANNEL_CLOSE_RECEIVED, 214 | STATE_EXCHANGE_BINDOK_RECEIVED], 215 | STATE_EXCHANGE_BINDOK_RECEIVED: _IDLE_STATE, 216 | STATE_EXCHANGE_DECLARE_SENT: [ 217 | STATE_CHANNEL_CLOSE_RECEIVED, 218 | STATE_EXCHANGE_DECLAREOK_RECEIVED], 219 | STATE_EXCHANGE_DECLAREOK_RECEIVED: _IDLE_STATE, 220 | STATE_EXCHANGE_DELETE_SENT: [ 221 | STATE_CHANNEL_CLOSE_RECEIVED, 222 | STATE_EXCHANGE_DELETEOK_RECEIVED], 223 | STATE_EXCHANGE_DELETEOK_RECEIVED: _IDLE_STATE, 224 | STATE_EXCHANGE_UNBIND_SENT: [ 225 | STATE_CHANNEL_CLOSE_RECEIVED, 226 | STATE_EXCHANGE_UNBINDOK_RECEIVED], 227 | STATE_EXCHANGE_UNBINDOK_RECEIVED: _IDLE_STATE, 228 | STATE_QUEUE_BIND_SENT: [ 229 | STATE_CHANNEL_CLOSE_RECEIVED, 230 | STATE_QUEUE_BINDOK_RECEIVED], 231 | STATE_QUEUE_BINDOK_RECEIVED: _IDLE_STATE, 232 | STATE_QUEUE_DECLARE_SENT: [ 233 | STATE_CHANNEL_CLOSE_RECEIVED, 234 | STATE_QUEUE_DECLAREOK_RECEIVED], 235 | STATE_QUEUE_DECLAREOK_RECEIVED: _IDLE_STATE, 236 | STATE_QUEUE_DELETE_SENT: [ 237 | STATE_CHANNEL_CLOSE_RECEIVED, 238 | STATE_QUEUE_DELETEOK_RECEIVED], 239 | STATE_QUEUE_DELETEOK_RECEIVED: _IDLE_STATE, 240 | STATE_QUEUE_PURGE_SENT: [ 241 | STATE_CHANNEL_CLOSE_RECEIVED, 242 | STATE_QUEUE_PURGEOK_RECEIVED], 243 | STATE_QUEUE_PURGEOK_RECEIVED: _IDLE_STATE, 244 | STATE_QUEUE_UNBIND_SENT: [ 245 | STATE_CHANNEL_CLOSE_RECEIVED, 246 | STATE_QUEUE_UNBINDOK_RECEIVED], 247 | STATE_QUEUE_UNBINDOK_RECEIVED: _IDLE_STATE, 248 | STATE_TX_SELECT_SENT: [STATE_TX_SELECTOK_RECEIVED], 249 | STATE_TX_SELECTOK_RECEIVED: _IDLE_STATE + [ 250 | STATE_TX_COMMIT_SENT, 251 | STATE_TX_ROLLBACK_SENT 252 | ], 253 | STATE_TX_COMMIT_SENT: [STATE_TX_COMMITOK_RECEIVED], 254 | STATE_TX_COMMITOK_RECEIVED: _IDLE_STATE, 255 | STATE_TX_ROLLBACK_SENT: [STATE_TX_ROLLBACKOK_RECEIVED], 256 | STATE_TX_ROLLBACKOK_RECEIVED: _IDLE_STATE, 257 | STATE_BASIC_ACK_RECEIVED: _IDLE_STATE, 258 | STATE_BASIC_ACK_SENT: _IDLE_STATE, 259 | STATE_BASIC_CANCEL_RECEIVED: _IDLE_STATE, 260 | STATE_BASIC_CANCEL_SENT: [STATE_BASIC_CANCELOK_RECEIVED], 261 | STATE_BASIC_CANCELOK_RECEIVED: _IDLE_STATE, 262 | STATE_BASIC_CANCELOK_SENT: _IDLE_STATE, 263 | STATE_BASIC_CONSUME_SENT: [ 264 | STATE_CHANNEL_CLOSE_RECEIVED, 265 | STATE_BASIC_CONSUMEOK_RECEIVED], 266 | STATE_BASIC_CONSUMEOK_RECEIVED: _IDLE_STATE, 267 | STATE_BASIC_DELIVER_RECEIVED: [STATE_CONTENT_HEADER_RECEIVED], 268 | STATE_CONTENT_HEADER_RECEIVED: [STATE_CONTENT_BODY_RECEIVED], 269 | STATE_CONTENT_BODY_RECEIVED: [STATE_MESSAGE_ASSEMBLED], 270 | STATE_BASIC_GET_SENT: [ 271 | STATE_CHANNEL_CLOSE_RECEIVED, 272 | STATE_BASIC_GETEMPTY_RECEIVED, 273 | STATE_BASIC_GETOK_RECEIVED], 274 | STATE_BASIC_GETEMPTY_RECEIVED: _IDLE_STATE, 275 | STATE_BASIC_GETOK_RECEIVED: [STATE_CONTENT_HEADER_RECEIVED], 276 | STATE_BASIC_NACK_RECEIVED: _IDLE_STATE, 277 | STATE_BASIC_NACK_SENT: _IDLE_STATE, 278 | STATE_MESSAGE_PUBLISHED: _IDLE_STATE + [ 279 | STATE_BASIC_ACK_RECEIVED, 280 | STATE_BASIC_NACK_RECEIVED, 281 | STATE_BASIC_REJECT_RECEIVED, 282 | STATE_BASIC_RETURN_RECEIVED], 283 | STATE_BASIC_QOS_SENT: [ 284 | STATE_CHANNEL_CLOSE_RECEIVED, STATE_BASIC_QOSOK_RECEIVED], 285 | STATE_BASIC_QOSOK_RECEIVED: _IDLE_STATE, 286 | STATE_BASIC_RECOVER_SENT: [STATE_BASIC_RECOVEROK_RECEIVED], 287 | STATE_BASIC_RECOVEROK_RECEIVED: _IDLE_STATE, 288 | STATE_BASIC_REJECT_RECEIVED: _IDLE_STATE, 289 | STATE_BASIC_REJECT_SENT: _IDLE_STATE, 290 | STATE_BASIC_RETURN_RECEIVED: [STATE_CONTENT_HEADER_RECEIVED], 291 | STATE_MESSAGE_ASSEMBLED: _IDLE_STATE + [ 292 | STATE_BASIC_ACK_RECEIVED, 293 | STATE_BASIC_ACK_SENT, 294 | STATE_BASIC_NACK_SENT, 295 | STATE_BASIC_NACK_RECEIVED, 296 | STATE_BASIC_REJECT_SENT, 297 | STATE_BASIC_REJECT_RECEIVED 298 | ], 299 | STATE_CLOSING: [STATE_CLOSED], 300 | STATE_CLOSED: [STATE_CONNECTING] 301 | } 302 | 303 | 304 | @dataclasses.dataclass() 305 | class _Defaults: 306 | locale: str 307 | product: str 308 | 309 | 310 | class Client(state.StateManager): 311 | """AsyncIO RabbitMQ Client 312 | 313 | This client provides a streamlined interface for interacting with RabbitMQ. 314 | 315 | Instead of manually managing your channels, the client will do so for you. 316 | In addition, if you are disconnected remotely due to an error, it will 317 | attempt to automatically reconnect. Any non-connection related exception 318 | should leave you in a state where you can continue working with RabbitMQ, 319 | even if it disconnected the client as part of the exception. 320 | 321 | .. note:: AMQ Methods vs Opinionated Methods 322 | 323 | For the most part, the client directly implements the AMQ model 324 | combining class and method RPC calls as a function. For example, 325 | ``Basic.Ack`` is implemented as :meth:`Client.basic_ack`. However, some 326 | methods, such as :meth:`Client.consume`, :meth:`Client.publish`, and 327 | :meth:`Client.qos_prefetch` provide a higher-level and more opinionated 328 | implementation than their respected AMQ RPC methods. 329 | 330 | :param url: The URL to connect to RabbitMQ with 331 | :param locale: The locale to specify for the RabbitMQ connection 332 | :param product: The project name to specify for the RabbitMQ connection 333 | :param loop: An optional IO Loop to specify, if unspecified, 334 | :func:`asyncio.get_running_loop` will be used to determine the IO Loop. 335 | :type loop: :class:`~asyncio.AbstractEventLoop` 336 | :param on_return: An optional callback method to be invoked if the server 337 | returns a published method. Can also be set using the 338 | :meth:`~Client.register_basic_return_callback` method. 339 | :type on_return: :class:`~collections.abc.Callable` 340 | 341 | .. code-block:: python3 342 | :caption: Example Usage 343 | 344 | client = Client(RABBITMQ_URL) 345 | await client.connect() 346 | await client.exchange_declare('test', 'topic') 347 | await client.close() 348 | 349 | """ 350 | CONNECTING_EXCEPTIONS = (exceptions.AccessRefused, exceptions.NotAllowed) 351 | STATE_MAP = _STATE_MAP 352 | STATE_TRANSITIONS = _STATE_TRANSITIONS 353 | 354 | def __init__(self, 355 | url: str = DEFAULT_URL, 356 | locale: str = DEFAULT_LOCALE, 357 | product: str = DEFAULT_PRODUCT, 358 | loop: typing.Optional[asyncio.AbstractEventLoop] = None, 359 | on_return: typing.Optional[typing.Callable] = None, 360 | ssl_context: typing.Optional[ssl.SSLContext] = None): 361 | super().__init__(loop or asyncio.get_running_loop()) 362 | self._blocked = asyncio.Event() 363 | self._block_write = asyncio.Event() 364 | self._channel: int = 0 365 | self._channel0: typing.Optional[channel0.Channel0] = None 366 | self._channel_open = asyncio.Event() 367 | self._confirmation_result: typing.Dict[int, bool] = {} 368 | self._connected = asyncio.Event() 369 | self._consumers: typing.Dict[str, typing.Callable] = {} 370 | self._delivery_tag = 0 371 | self._delivery_tags: typing.Dict[int, asyncio.Event] = {} 372 | self._defaults = _Defaults(locale, product) 373 | self._get_future: typing.Optional[asyncio.Future] = None 374 | self._last_error: typing.Tuple[int, typing.Optional[str]] = (0, None) 375 | self._last_frame: typing.Optional[base.Frame] = None 376 | self._max_frame_size: typing.Optional[float] = None 377 | self._message: typing.Optional[message.Message] = None 378 | self._on_channel_close: typing.Optional[typing.Callable] = None 379 | self._on_message_return: typing.Optional[typing.Callable] = on_return 380 | self._pending_consumers: typing.Deque[ 381 | (asyncio.Future, typing.Callable)] = collections.deque([]) 382 | self._protocol: typing.Optional[asyncio.Protocol] = None 383 | self._publisher_confirms = False 384 | self._rpc_lock = asyncio.Lock() 385 | self._close_lock = asyncio.Lock() 386 | self._ssl_context = ssl_context 387 | self._transactional = False 388 | self._transport: typing.Optional[asyncio.Transport] = None 389 | self._url = yarl.URL(url) 390 | self._set_state(STATE_DISCONNECTED) 391 | 392 | async def connect(self) -> None: 393 | """Connect to the RabbitMQ Server 394 | 395 | .. seealso:: 396 | 397 | :meth:`aiorabbit.connect` for connecting as a 398 | :ref:`context-manager ` that 399 | automatically closes when complete. 400 | 401 | .. code-block:: python3 402 | :caption: Example Usage 403 | 404 | client = Client(RABBITMQ_URL) 405 | await client.connect() 406 | 407 | :raises asyncio.TimeoutError: on connection timeout 408 | :raises OSError: when a networking error occurs 409 | :raises aiorabbit.exceptions.AccessRefused: 410 | when authentication or authorization fails 411 | :raises aiorabbit.exceptions.ClientNegotiationException: 412 | when the client fails to negotiate with the server 413 | 414 | """ 415 | try: 416 | await self._connect() 417 | except (OSError, 418 | RuntimeError, 419 | asyncio.TimeoutError, 420 | exceptions.AccessRefused, 421 | exceptions.ClientNegotiationException) as exc: 422 | self._reset() 423 | self._logger.critical('Failed to connect to RabbitMQ: %s', exc) 424 | raise exc 425 | await self._open_channel() 426 | 427 | async def close(self) -> None: 428 | """Close the client connection to the server""" 429 | async with self._close_lock: 430 | if self.is_closed or not self._channel0 or not self._transport: 431 | self._logger.warning( 432 | 'Close called when connection is not open') 433 | if self._state != STATE_CLOSED: 434 | self._set_state(STATE_CLOSED) 435 | return 436 | if self._channel_open.is_set(): 437 | await self._send_rpc( 438 | commands.Channel.Close(200, 'Client Requested', 0, 0), 439 | STATE_CHANNEL_CLOSE_SENT, 440 | STATE_CHANNEL_CLOSEOK_RECEIVED) 441 | await self._close() 442 | 443 | @property 444 | def is_connected(self) -> bool: 445 | """Indicates if the connection is available""" 446 | return not self.is_closed 447 | 448 | @property 449 | def is_closed(self) -> bool: 450 | """Indicates if the connection is closed or closing""" 451 | return (not self._channel0 452 | or (self._channel0 and self._channel0.is_closed) 453 | or self._state in [STATE_CLOSING, 454 | STATE_CLOSED, 455 | STATE_DISCONNECTED, 456 | state.STATE_EXCEPTION, 457 | state.STATE_UNINITIALIZED] 458 | or not self._transport) 459 | 460 | @property 461 | def server_capabilities(self) -> typing.List[str]: 462 | """Contains the capabilities of the currently connected 463 | RabbitMQ Server. 464 | 465 | .. code-block:: python 466 | :caption: Example return value 467 | 468 | ['authentication_failure_close', 469 | 'basic.nack', 470 | 'connection.blocked', 471 | 'consumer_cancel_notify', 472 | 'consumer_priorities', 473 | 'direct_reply_to', 474 | 'exchange_exchange_bindings', 475 | 'per_consumer_qos', 476 | 'publisher_confirms'] 477 | 478 | """ 479 | return [key for key, value in 480 | self._channel0.properties['capabilities'].items() if value] 481 | 482 | @property 483 | def server_properties(self) \ 484 | -> typing.Dict[str, typing.Union[str, typing.Dict[str, bool]]]: 485 | """Contains the negotiated properties for the currently connected 486 | RabbitMQ Server. 487 | 488 | :rtype: :const:`FieldTable` 489 | 490 | .. code-block:: python 491 | :caption: Example return value 492 | 493 | {'capabilities': {'authentication_failure_close': True, 494 | 'basic.nack': True, 495 | 'connection.blocked': True, 496 | 'consumer_cancel_notify': True, 497 | 'consumer_priorities': True, 498 | 'direct_reply_to': True, 499 | 'exchange_exchange_bindings': True, 500 | 'per_consumer_qos': True, 501 | 'publisher_confirms': True}, 502 | 'cluster_name': 'rabbit@b6a4a6555767', 503 | 'copyright': 'Copyright (c) 2007-2019 Pivotal Software, Inc.', 504 | 'information': 'Licensed under the MPL 1.1. ' 505 | 'Website: https://rabbitmq.com', 506 | 'platform': 'Erlang/OTP 22.2.8', 507 | 'product': 'RabbitMQ', 508 | 'version': '3.8.2'} 509 | 510 | """ 511 | return self._channel0.properties 512 | 513 | async def consume(self, 514 | queue: str = '', 515 | no_local: bool = False, 516 | no_ack: bool = False, 517 | exclusive: bool = False, 518 | arguments: types.Arguments = None) \ 519 | -> typing.AsyncGenerator[message.Message, None]: 520 | """Generator function that consumes from a queue, yielding a 521 | :class:`~aiorabbit.message.Message` and automatically cancels when 522 | the generator is closed. 523 | 524 | .. seealso:: :pep:`525` for information on Async Generators and 525 | :meth:`Client.basic_consume` for callback style consuming. 526 | 527 | :param queue: Specifies the name of the queue to consume from 528 | :param no_local: Do not deliver own messages 529 | :param no_ack: No acknowledgement needed 530 | :param exclusive: Request exclusive access 531 | :param arguments: A set of arguments for the consume. The syntax and 532 | semantics of these arguments depends on the server implementation. 533 | :type arguments: :data:`~aiorabbit.types.Arguments` 534 | 535 | :rtype: typing.AsyncGenerator[aiorabbit.message.Message, None] 536 | 537 | :yields: :class:`aiorabbit.message.Message` 538 | 539 | .. code-block:: python3 540 | :caption: Example Usage 541 | 542 | consumer = self.client.consume(self.queue) 543 | async for msg in consumer: 544 | await self.client.basic_ack(msg.delivery_tag) 545 | if msg.body == b'stop': 546 | break 547 | 548 | """ 549 | messages = asyncio.Queue() 550 | consumer_tag = await self.basic_consume( 551 | queue, no_local, no_ack, exclusive, arguments, 552 | lambda m: self._execute_callback(messages.put, m)) 553 | try: 554 | while not self.is_closed: 555 | try: 556 | msg = messages.get_nowait() 557 | except asyncio.QueueEmpty: 558 | await asyncio.sleep(0.01) 559 | else: 560 | yield msg 561 | finally: 562 | if self._exception: 563 | raise self._exception 564 | await self.basic_cancel(consumer_tag) 565 | 566 | async def publish(self, 567 | exchange: str = 'amq.direct', 568 | routing_key: str = '', 569 | message_body: typing.Union[bytes, str] = b'', 570 | mandatory: bool = False, 571 | app_id: typing.Optional[str] = None, 572 | content_encoding: typing.Optional[str] = None, 573 | content_type: typing.Optional[str] = None, 574 | correlation_id: typing.Optional[str] = None, 575 | delivery_mode: typing.Optional[int] = None, 576 | expiration: typing.Optional[str] = None, 577 | headers: typing.Optional[types.FieldTable] = None, 578 | message_id: typing.Optional[str] = None, 579 | message_type: typing.Optional[str] = None, 580 | priority: typing.Optional[int] = None, 581 | reply_to: typing.Optional[str] = None, 582 | timestamp: typing.Optional[datetime.datetime] = None, 583 | user_id: typing.Optional[str] = None) \ 584 | -> typing.Optional[bool]: 585 | """Publish a message to RabbitMQ 586 | 587 | `message_body` can either be :class:`str` or :class:`bytes`. If 588 | it is a :class:`str`, it will be encoded to a :class:`bytes` instance 589 | using ``UTF-8`` encoding. 590 | 591 | If publisher confirms are enabled, will return `True` or `False` 592 | indicating success or failure. 593 | 594 | .. seealso:: 595 | 596 | :meth:`Client.confirm_select` for enabling publisher confirmation 597 | of published messages. 598 | 599 | .. note:: 600 | 601 | The ``immediate`` flag is not offered as it is not implemented in 602 | RabbitMQ as of this time. See ``basic / publish`` in the 603 | "Methods from the AMQP specification, version 0-9-1" table 604 | in RabbitMQ's `Compatibility and Conformance 605 | `_ page for 606 | more information. 607 | 608 | :param exchange: The exchange to publish to. Default: `amq.direct` 609 | :param routing_key: The routing key to publish with. Default: `` 610 | :param message_body: The message body to publish. Default: `` 611 | :param mandatory: Indicate mandatory routing. Default: `False` 612 | :param app_id: Creating application id 613 | :param content_type: MIME content type 614 | :param content_encoding: MIME content encoding 615 | :param correlation_id: Application correlation identifier 616 | :param delivery_mode: Non-persistent (`1`) or persistent (`2`) 617 | :param expiration: Message expiration specification 618 | :param headers: Message header field table 619 | :type headers: typing.Optional[:data:`~aiorabbit.types.FieldTable`] 620 | :param message_id: Application message identifier 621 | :param message_type: Message type name 622 | :param priority: Message priority, `0` to `9` 623 | :param reply_to: Address to reply to 624 | :param datetime.datetime timestamp: Message timestamp 625 | :param user_id: Creating user id 626 | :raises TypeError: if an argument is of the wrong data type 627 | :raises ValueError: if the value of one an argument does not validate 628 | :raises aiorabbit.exceptions.NotFound: When publisher confirms are 629 | enabled and mandatory is set and the exchange that is being 630 | published to does not exist. 631 | 632 | """ 633 | self._validate_exchange_name('exchange', exchange) 634 | self._validate_short_str('routing_key', routing_key) 635 | if not isinstance(message_body, (bytes, str)): 636 | raise TypeError('message_body must be of types bytes or str') 637 | self._validate_bool('mandatory', mandatory) 638 | if app_id is not None: 639 | self._validate_short_str('app_id', app_id) 640 | if content_encoding is not None: 641 | self._validate_short_str('content_encoding', content_encoding) 642 | if content_type is not None: 643 | self._validate_short_str('content_type', content_type) 644 | if correlation_id is not None: 645 | self._validate_short_str('correlation_id', correlation_id) 646 | if delivery_mode is not None: 647 | if not isinstance(delivery_mode, int): 648 | raise TypeError('delivery_mode must be of type int') 649 | elif not 0 < delivery_mode < 3: 650 | raise ValueError('delivery_mode must be 1 or 2') 651 | if expiration is not None: 652 | self._validate_short_str('expiration', expiration) 653 | if headers is not None: 654 | self._validate_field_table('headers', headers) 655 | if message_id is not None: 656 | self._validate_short_str('message_id', message_id) 657 | if message_type is not None: 658 | self._validate_short_str('message_type', message_type) 659 | if priority is not None: 660 | if not isinstance(priority, int): 661 | raise TypeError('delivery_mode must be of type int') 662 | elif not 0 < priority < 256: 663 | raise ValueError('priority must be between 0 and 256') 664 | if message_type: 665 | self._validate_short_str('message_type', message_type) 666 | if reply_to: 667 | self._validate_short_str('reply_to', reply_to) 668 | if timestamp and not isinstance(timestamp, datetime.datetime): 669 | raise TypeError('timestamp must be of type datetime.datetime') 670 | if user_id: 671 | self._validate_short_str('user_id', user_id) 672 | 673 | if isinstance(message_body, str): 674 | message_body = message_body.encode('utf-8') 675 | self._delivery_tag += 1 676 | if self._publisher_confirms: 677 | self._delivery_tags[self._delivery_tag] = asyncio.Event() 678 | delivery_tag = self._delivery_tag 679 | body_size = len(message_body) 680 | 681 | frames = [ 682 | commands.Basic.Publish( 683 | exchange=exchange, 684 | routing_key=routing_key, 685 | mandatory=mandatory), 686 | header.ContentHeader( 687 | body_size=body_size, 688 | properties=commands.Basic.Properties( 689 | app_id=app_id, 690 | content_encoding=content_encoding, 691 | content_type=content_type, 692 | correlation_id=correlation_id, 693 | delivery_mode=delivery_mode, 694 | expiration=expiration, 695 | headers=headers, 696 | message_id=message_id, 697 | message_type=message_type, 698 | priority=priority, 699 | reply_to=reply_to, 700 | timestamp=timestamp, 701 | user_id=user_id))] 702 | 703 | # Calculate how many body frames are needed 704 | chunks = int(math.ceil(body_size / self._max_frame_size)) 705 | for offset in range(0, chunks): # Send the message 706 | start = int(self._max_frame_size * offset) 707 | end = int(start + self._max_frame_size) 708 | if end > body_size: 709 | end = int(body_size) 710 | frames.append(body.ContentBody(message_body[start:end])) 711 | self._write_frames(*frames) 712 | self._set_state(STATE_MESSAGE_PUBLISHED) 713 | 714 | if self._publisher_confirms: 715 | result = await self._wait_on_state( 716 | STATE_BASIC_ACK_RECEIVED, 717 | STATE_BASIC_NACK_RECEIVED, 718 | STATE_BASIC_REJECT_RECEIVED) 719 | if result == STATE_CHANNEL_CLOSE_RECEIVED: 720 | del self._delivery_tags[delivery_tag] 721 | err = self._get_last_error() 722 | raise exceptions.CLASS_MAPPING[err[0]](err[1]) 723 | else: 724 | await self._delivery_tags[delivery_tag].wait() 725 | result = self._confirmation_result[delivery_tag] 726 | del self._delivery_tags[delivery_tag] 727 | del self._confirmation_result[delivery_tag] 728 | return result 729 | 730 | async def qos_prefetch(self, count=0, per_consumer=True) -> None: 731 | """Specify the number of messages to pre-allocate for a consumer. 732 | 733 | This method requests a specific quality of service. It uses 734 | ``Basic.QoS`` under the covers, but due to the redefinition of the 735 | ``global`` argument in RabbitMQ, along with the lack of 736 | ``prefetch_size``, it is redefined here as ``qos_prefetch`` and 737 | is used for the count only. 738 | 739 | The QoS can be specified for the current channel or individual 740 | consumers on the channel. 741 | 742 | :param count: Window in messages to pre-allocate for consumers 743 | :param per_consumer: Apply QoS to new consumers when ``True`` 744 | or to the whole channel when ``False``. 745 | 746 | """ 747 | if not isinstance(count, int): 748 | raise TypeError('prefetch_size must be of type int') 749 | elif not isinstance(per_consumer, bool): 750 | raise TypeError('per_consumer must be of type bool') 751 | if 'per_consumer_qos' not in self.server_capabilities \ 752 | and per_consumer: # pragma: nocover 753 | self._logger.warning('per_consumer QoS prefetch requested but it ' 754 | 'is not available on the server') 755 | await self._send_rpc( 756 | commands.Basic.Qos(0, count, not per_consumer), 757 | STATE_BASIC_QOS_SENT, 758 | STATE_BASIC_QOSOK_RECEIVED) 759 | 760 | def register_basic_return_callback(self, value: typing.Callable) -> None: 761 | """Register a callback that is invoked when RabbitMQ returns a 762 | published message. The callback can be a synchronous or asynchronous 763 | method and is invoked with the returned message as an instance of 764 | :class:`~aiorabbit.message.Message`. 765 | 766 | :param value: The method or function to invoke as a callback 767 | :type value: :class:`~collections.abc.Callable` 768 | 769 | .. code-block:: python3 770 | :caption: Example Usage 771 | 772 | async def on_return(msg: aiorabbit.message.Message) -> None: 773 | self._logger.warning('RabbitMQ Returned a message: %r', msg) 774 | 775 | client = Client(RABBITMQ_URL) 776 | client.register_basic_return_callback(on_return) 777 | await client.connect() 778 | 779 | # ... publish messages that could return 780 | 781 | """ 782 | self._on_message_return = value 783 | 784 | async def basic_qos(self) -> None: 785 | """This method is not implemented, as RabbitMQ does not fully implement 786 | it and changes the of the semantic meaning of how it is used. 787 | 788 | Use the :meth:`Client.qos_prefetch` method instead as it implements the 789 | ``Basic.QoS`` behavior as it currently works in RabbitMQ. 790 | 791 | .. seealso:: See the 792 | `RabbitMQ site `_ 793 | for more information on RabbitMQ's implementation and changes to 794 | ``Basic.QoS``. 795 | 796 | :raises NotImplementedError: when invoked 797 | 798 | """ 799 | raise NotImplementedError 800 | 801 | async def basic_consume(self, 802 | queue: str = '', 803 | no_local: bool = False, 804 | no_ack: bool = False, 805 | exclusive: bool = False, 806 | arguments: types.Arguments = None, 807 | callback: typing.Callable = None, 808 | consumer_tag: typing.Optional[str] = None) \ 809 | -> str: 810 | """Start a queue consumer 811 | 812 | This method asks the server to start a “consumer”, which is a transient 813 | request for messages from a specific queue. Consumers last as long as 814 | the channel they were declared on, or until the client cancels them. 815 | 816 | This method is used for callback passing style usage. For each message, 817 | the ``callback`` method will be invoked, passing in an instance of 818 | :class:`~pamqp.message.Message`. 819 | 820 | The :meth:`Client.consume ` method 821 | should be used for generator style consuming. 822 | 823 | :param queue: Specifies the name of the queue to consume from 824 | :param no_local: Do not deliver own messages 825 | :param no_ack: No acknowledgement needed 826 | :param exclusive: Request exclusive access 827 | :param arguments: A set of arguments for the consume. The syntax and 828 | semantics of these arguments depends on the server implementation. 829 | :type arguments: :data:`~aiorabbit.types.Arguments` 830 | :param callback: The method to invoke for each received message. 831 | :type callback: :class:`~collections.abc.Callable` 832 | :param consumer_tag: Specifies the identifier for the consumer. The 833 | consumer tag is local to a channel, so two clients can use the same 834 | consumer tags. If this field is empty the server will generate a 835 | unique tag. 836 | :returns: the consumer tag value 837 | 838 | """ 839 | if not isinstance(queue, str): 840 | raise TypeError('queue must be of type str') 841 | elif not isinstance(no_local, bool): 842 | raise TypeError('no_local must be of type bool') 843 | elif not isinstance(no_ack, bool): 844 | raise TypeError('no_ack must be of type bool') 845 | elif not isinstance(exclusive, bool): 846 | raise TypeError('exclusive must be of type bool') 847 | elif arguments and not isinstance(arguments, dict): 848 | raise TypeError('arguments must be of type dict') 849 | if callback is None: 850 | raise ValueError('callback must be specified') 851 | elif not callable(callback): 852 | raise TypeError('callback must be a callable') 853 | elif consumer_tag is not None and not isinstance(consumer_tag, str): 854 | raise TypeError('consumer_tag must be of type str') 855 | consumer_tag_future = asyncio.Future() 856 | self._pending_consumers.append((consumer_tag_future, callback)) 857 | await self._send_rpc( 858 | commands.Basic.Consume( 859 | 0, queue, consumer_tag or '', no_local, no_ack, exclusive, 860 | False, arguments), 861 | STATE_BASIC_CONSUME_SENT, 862 | STATE_BASIC_CONSUMEOK_RECEIVED) 863 | await consumer_tag_future 864 | return consumer_tag_future.result() 865 | 866 | async def basic_cancel(self, consumer_tag: str = '') -> None: 867 | """End a queue consumer 868 | 869 | This method cancels a consumer. This does not affect already delivered 870 | messages, but it does mean the server will not send any more messages 871 | for that consumer. The client may receive an arbitrary number of 872 | messages in between sending the cancel method and receiving the 873 | ``CancelOk`` reply. It may also be sent from the server to the client 874 | in the event of the consumer being unexpectedly cancelled (i.e. 875 | cancelled for any reason other than the server receiving the 876 | corresponding basic.cancel from the client). This allows clients to be 877 | notified of the loss of consumers due to events such as queue deletion. 878 | Note that as it is not a MUST for clients to accept this method from 879 | the server, it is advisable for the broker to be able to identify 880 | those clients that are capable of accepting the method, through some 881 | means of capability negotiation. 882 | 883 | :param consumer_tag: Consumer tag 884 | 885 | """ 886 | if not isinstance(consumer_tag, str): 887 | raise TypeError('consumer_tag must be of type str') 888 | await self._send_rpc( 889 | commands.Basic.Cancel(consumer_tag), 890 | STATE_BASIC_CANCEL_SENT, 891 | STATE_BASIC_CANCELOK_RECEIVED) 892 | 893 | async def basic_get(self, queue: str = '', no_ack: bool = False) \ 894 | -> typing.Optional[message.Message]: 895 | """Direct access to a queue 896 | 897 | This method provides a direct access to the messages in a queue using 898 | a synchronous dialogue that is designed for specific types of 899 | application where synchronous functionality is more important than 900 | performance. 901 | 902 | :param queue: Specifies the name of the queue to get a message from 903 | :param no_ack: No acknowledgement needed 904 | 905 | """ 906 | if not isinstance(queue, str): 907 | raise TypeError('queue must be of type str') 908 | elif not isinstance(no_ack, bool): 909 | raise TypeError('no_ack must be of type bool') 910 | future = asyncio.Future() 911 | self._get_future = future 912 | await self._send_rpc( 913 | commands.Basic.Get(0, queue, no_ack), 914 | STATE_BASIC_GET_SENT, 915 | STATE_BASIC_GETEMPTY_RECEIVED, 916 | STATE_BASIC_GETOK_RECEIVED) 917 | await future 918 | self._get_future = None 919 | return future.result() 920 | 921 | async def basic_ack(self, 922 | delivery_tag: int, 923 | multiple: bool = False) -> None: 924 | """Acknowledge one or more messages 925 | 926 | When sent by the client, this method acknowledges one or more messages 927 | delivered via the ``Basic.Deliver`` or ``Basic.GetOk`` methods. 928 | The acknowledgement can be for a single message or a set of messages up 929 | to and including a specific message. 930 | 931 | :param delivery_tag: Server-assigned delivery tag 932 | :param multiple: Acknowledge multiple messages 933 | 934 | """ 935 | if not isinstance(delivery_tag, int): 936 | raise TypeError('delivery_tag must be of type int') 937 | elif not isinstance(multiple, bool): 938 | raise TypeError('multiple must be of type bool') 939 | self._write_frames(commands.Basic.Ack(delivery_tag, multiple)) 940 | self._set_state(STATE_BASIC_ACK_SENT) 941 | 942 | async def basic_nack(self, 943 | delivery_tag: int, 944 | multiple: bool = False, 945 | requeue: bool = True) -> None: 946 | """Reject one or more incoming messages 947 | 948 | This method allows a client to reject one or more incoming messages. 949 | It can be used to interrupt and cancel large incoming messages, or 950 | return untreatable messages to their original queue. 951 | 952 | :param delivery_tag: Server-assigned delivery tag 953 | :param multiple: Reject multiple messages 954 | :param requeue: Requeue the message 955 | 956 | """ 957 | if not isinstance(delivery_tag, int): 958 | raise TypeError('delivery_tag must be of type int') 959 | elif not isinstance(multiple, bool): 960 | raise TypeError('multiple must be of type bool') 961 | elif not isinstance(requeue, bool): 962 | raise TypeError('requeue must be of type bool') 963 | self._write_frames( 964 | commands.Basic.Nack(delivery_tag, multiple, requeue)) 965 | self._set_state(STATE_BASIC_NACK_SENT) 966 | 967 | async def basic_reject(self, 968 | delivery_tag: int, 969 | requeue: bool = True) -> None: 970 | """Reject an incoming message 971 | 972 | This method allows a client to reject a message. It can be used to 973 | interrupt and cancel large incoming messages, or return untreatable 974 | messages to their original queue. 975 | 976 | :param delivery_tag: Server-assigned delivery tag 977 | :param requeue: Requeue the message 978 | 979 | """ 980 | if not isinstance(delivery_tag, int): 981 | raise TypeError('delivery_tag must be of type int') 982 | elif not isinstance(requeue, bool): 983 | raise TypeError('requeue must be of type bool') 984 | self._write_frames(commands.Basic.Reject(delivery_tag, requeue)) 985 | self._set_state(STATE_BASIC_REJECT_SENT) 986 | 987 | async def basic_publish(self) -> None: 988 | """This method is not implemented and the more opinionated 989 | :meth:`~Client.publish` method exists, implementing 990 | the ``Basic.Publish`` RPC. 991 | 992 | :raises NotImplementedError: when invoked 993 | 994 | """ 995 | raise NotImplementedError 996 | 997 | async def basic_recover(self, requeue: bool = False) -> None: 998 | """Redeliver unacknowledged messages 999 | 1000 | This method asks the server to redeliver all unacknowledged messages 1001 | on a specified channel. Zero or more messages may be redelivered. 1002 | 1003 | :param requeue: Requeue the message 1004 | :raises aiorabbit.exceptions.NotImplemented: when 1005 | `False` is specified for `requeue` 1006 | 1007 | """ 1008 | if not isinstance(requeue, bool): 1009 | raise TypeError('requeue must be of type bool') 1010 | await self._send_rpc( 1011 | commands.Basic.Recover(requeue), 1012 | STATE_BASIC_RECOVER_SENT, 1013 | STATE_BASIC_RECOVEROK_RECEIVED) 1014 | 1015 | async def confirm_select(self) -> None: 1016 | """Enable `Publisher Confirms 1017 | `_ 1018 | 1019 | .. warning:: 1020 | 1021 | RabbitMQ will only indicate a publishing failure via publisher 1022 | confirms when there is an internal error in RabbitMQ. They are 1023 | not a mechanism for guaranteeing a message is routed. Usage of the 1024 | ``mandatory`` flag when publishing will only guarantee that the 1025 | message is routed into an exchange, but not that it is published 1026 | into a queue. 1027 | 1028 | :raises RuntimeError: if publisher confirms are already enabled 1029 | :raises aiorabbit.exceptions.NotImplemented: 1030 | if publisher confirms are not available on the RabbitMQ server 1031 | 1032 | """ 1033 | if 'publisher_confirms' not in self.server_capabilities: 1034 | raise exceptions.NotImplemented( 1035 | 'Server does not support publisher confirms') 1036 | elif self._publisher_confirms: 1037 | raise RuntimeError('Publisher confirms are already enabled') 1038 | else: 1039 | await self._send_rpc( 1040 | commands.Confirm.Select(), 1041 | STATE_CONFIRM_SELECT_SENT, 1042 | STATE_CONFIRM_SELECTOK_RECEIVED) 1043 | self._publisher_confirms = True 1044 | 1045 | async def exchange_declare(self, 1046 | exchange: str = '', 1047 | exchange_type: str = 'direct', 1048 | passive: bool = False, 1049 | durable: bool = False, 1050 | auto_delete: bool = False, 1051 | internal: bool = False, 1052 | arguments: types.Arguments = None) \ 1053 | -> None: 1054 | """Verify exchange exists, create if needed 1055 | 1056 | This method creates an exchange if it does not already exist, and if 1057 | the exchange exists, verifies that it is of the correct and expected 1058 | class. 1059 | 1060 | :param exchange: Exchange name 1061 | :param exchange_type: Exchange type 1062 | :param passive: Do not create exchange 1063 | :param durable: Request a durable exchange 1064 | :param auto_delete: Auto-delete when unused 1065 | :param internal: Create internal exchange 1066 | :param arguments: Arguments for declaration 1067 | :type arguments: :data:`~aiorabbit.types.Arguments` 1068 | :raises TypeError: if an argument is of the wrong data type 1069 | :raises aiorabbit.exceptions.NotFound: 1070 | if the sent command is invalid due to an argument value 1071 | :raises aiorabbit.exceptions.CommandInvalid: 1072 | when an exchange type or other parameter is invalid 1073 | """ 1074 | if not isinstance(exchange, str): 1075 | raise TypeError('exchange must be of type str') 1076 | elif not isinstance(exchange_type, str): 1077 | raise TypeError('exchange_type must be of type str') 1078 | elif not isinstance(passive, bool): 1079 | raise TypeError('passive must be of type bool') 1080 | elif not isinstance(auto_delete, bool): 1081 | raise TypeError('auto_delete must be of type bool') 1082 | elif not isinstance(internal, bool): 1083 | raise TypeError('internal must be of type bool') 1084 | elif arguments and not isinstance(arguments, dict): 1085 | raise TypeError('arguments must be of type dict') 1086 | await self._send_rpc( 1087 | commands.Exchange.Declare( 1088 | exchange=exchange, exchange_type=exchange_type, 1089 | passive=passive, durable=durable, auto_delete=auto_delete, 1090 | internal=internal, arguments=arguments), 1091 | STATE_EXCHANGE_DECLARE_SENT, 1092 | STATE_EXCHANGE_DECLAREOK_RECEIVED) 1093 | 1094 | async def exchange_delete(self, 1095 | exchange: str = '', 1096 | if_unused: bool = False) -> None: 1097 | """Delete an exchange 1098 | 1099 | This method deletes an exchange. When an exchange is deleted all queue 1100 | bindings on the exchange are cancelled. 1101 | 1102 | :param exchange: exchange name 1103 | - Default: ``''`` 1104 | :param if_unused: Delete only if unused 1105 | - Default: ``False`` 1106 | :raises ValueError: when an argument fails to validate 1107 | 1108 | """ 1109 | await self._send_rpc( 1110 | commands.Exchange.Delete(0, exchange, if_unused, False), 1111 | STATE_EXCHANGE_DELETE_SENT, 1112 | STATE_EXCHANGE_DELETEOK_RECEIVED) 1113 | 1114 | async def exchange_bind(self, 1115 | destination: str = '', 1116 | source: str = '', 1117 | routing_key: str = '', 1118 | arguments: types.Arguments = None) \ 1119 | -> None: 1120 | """Bind exchange to an exchange. 1121 | 1122 | :param destination: Destination exchange name 1123 | :param source: Source exchange name 1124 | :param routing_key: Message routing key 1125 | :param arguments: Arguments for binding 1126 | :type arguments: :data:`~aiorabbit.types.Arguments` 1127 | :raises TypeError: if an argument is of the wrong data type 1128 | :raises aiorabbit.exceptions.NotFound: 1129 | if the one of the specified exchanges does not exist 1130 | 1131 | """ 1132 | if not isinstance(destination, str): 1133 | raise TypeError('destination must be of type str') 1134 | elif not isinstance(source, str): 1135 | raise TypeError('source must be of type str') 1136 | elif not isinstance(routing_key, str): 1137 | raise TypeError('routing_key must be of type str') 1138 | elif arguments and not isinstance(arguments, dict): 1139 | raise TypeError('arguments must be of type dict') 1140 | await self._send_rpc( 1141 | commands.Exchange.Bind( 1142 | destination=destination, source=source, 1143 | routing_key=routing_key, arguments=arguments), 1144 | STATE_EXCHANGE_BIND_SENT, 1145 | STATE_EXCHANGE_BINDOK_RECEIVED) 1146 | 1147 | async def exchange_unbind(self, 1148 | destination: str = '', 1149 | source: str = '', 1150 | routing_key: str = '', 1151 | arguments: types.Arguments = None) \ 1152 | -> None: 1153 | """Unbind an exchange from an exchange. 1154 | 1155 | :param destination: Destination exchange name 1156 | :param source: Source exchange name 1157 | :param routing_key: Message routing key 1158 | :param arguments: Arguments for binding 1159 | :type arguments: :data:`~aiorabbit.types.Arguments` 1160 | :raises TypeError: if an argument is of the wrong data type 1161 | :raises ValueError: if an argument value does not validate 1162 | 1163 | """ 1164 | if not isinstance(destination, str): 1165 | raise TypeError('destination must be of type str') 1166 | elif not isinstance(source, str): 1167 | raise TypeError('source must be of type str') 1168 | elif not isinstance(routing_key, str): 1169 | raise TypeError('routing_key must be of type str') 1170 | elif arguments and not isinstance(arguments, dict): 1171 | raise TypeError('arguments must be of type dict') 1172 | await self._send_rpc( 1173 | commands.Exchange.Unbind( 1174 | destination=destination, source=source, 1175 | routing_key=routing_key, arguments=arguments), 1176 | STATE_EXCHANGE_UNBIND_SENT, 1177 | STATE_EXCHANGE_UNBINDOK_RECEIVED) 1178 | 1179 | async def queue_declare(self, 1180 | queue: str = '', 1181 | passive: bool = False, 1182 | durable: bool = False, 1183 | exclusive: bool = False, 1184 | auto_delete: bool = False, 1185 | arguments: types.Arguments = None) \ 1186 | -> typing.Tuple[int, int]: 1187 | """Declare queue, create if needed 1188 | 1189 | This method creates or checks a queue. When creating a new queue the 1190 | client can specify various properties that control the durability of 1191 | the queue and its contents, and the level of sharing for the queue. 1192 | 1193 | Returns a tuple of message count, consumer count. 1194 | 1195 | :param queue: Queue name 1196 | :param passive: Do not create queue 1197 | :param durable: Request a durable queue 1198 | :param exclusive: Request an exclusive queue 1199 | :param auto_delete: Auto-delete queue when unused 1200 | :param arguments: Arguments for declaration 1201 | :type arguments: :data:`~aiorabbit.types.Arguments` 1202 | :raises TypeError: if an argument is of the wrong data type 1203 | :raises ValueError: when an argument fails to validate 1204 | :raises aiorabbit.exceptions.ResourceLocked: 1205 | when a queue is already declared and exclusive is requested 1206 | :raises aiorabbit.exceptions.PreconditionFailed: 1207 | when a queue is redeclared with a different definition than it 1208 | currently has 1209 | 1210 | """ 1211 | if not isinstance(queue, str): 1212 | raise TypeError('queue must be of type str') 1213 | elif not isinstance(passive, bool): 1214 | raise TypeError('passive must be of type bool') 1215 | elif not isinstance(durable, bool): 1216 | raise TypeError('durable must be of type bool') 1217 | elif not isinstance(exclusive, bool): 1218 | raise TypeError('exclusive must be of type bool') 1219 | elif not isinstance(auto_delete, bool): 1220 | raise TypeError('auto_delete must be of type bool') 1221 | elif arguments and not isinstance(arguments, dict): 1222 | raise TypeError('arguments must be of type dict') 1223 | await self._send_rpc( 1224 | commands.Queue.Declare( 1225 | 0, queue, passive, durable, exclusive, auto_delete, 1226 | False, arguments), 1227 | STATE_QUEUE_DECLARE_SENT, 1228 | STATE_QUEUE_DECLAREOK_RECEIVED) 1229 | return self._last_frame.message_count, self._last_frame.consumer_count 1230 | 1231 | async def queue_delete(self, 1232 | queue: str = '', 1233 | if_unused: bool = False, 1234 | if_empty: bool = False) -> None: 1235 | """Delete a queue 1236 | 1237 | This method deletes a queue. When a queue is deleted any pending 1238 | messages are sent to a dead-letter queue if this is defined in the 1239 | server configuration, and all consumers on the queue are cancelled. 1240 | 1241 | :param queue: Specifies the name of the queue to delete 1242 | :param if_unused: Delete only if unused 1243 | :param if_empty: Delete only if empty 1244 | 1245 | """ 1246 | if not isinstance(queue, str): 1247 | raise TypeError('queue must be of type str') 1248 | elif not isinstance(if_unused, bool): 1249 | raise TypeError('if_unused must be of type bool') 1250 | elif not isinstance(if_empty, bool): 1251 | raise TypeError('if_empty must be of type bool') 1252 | await self._send_rpc( 1253 | commands.Queue.Delete(0, queue, if_unused, if_empty, False), 1254 | STATE_QUEUE_DELETE_SENT, 1255 | STATE_QUEUE_DELETEOK_RECEIVED) 1256 | 1257 | async def queue_bind(self, 1258 | queue: str = '', 1259 | exchange: str = '', 1260 | routing_key: str = '', 1261 | arguments: types.Arguments = None) -> None: 1262 | """Bind queue to an exchange 1263 | 1264 | This method binds a queue to an exchange. Until a queue is bound it 1265 | will not receive any messages. In a classic messaging model, 1266 | store-and- forward queues are bound to a direct exchange and 1267 | subscription queues are bound to a topic exchange. 1268 | 1269 | :param queue: Specifies the name of the queue to bind 1270 | :param exchange: Name of the exchange to bind to 1271 | :param routing_key: Message routing key 1272 | :param arguments: Arguments of binding 1273 | :type arguments: :data:`~aiorabbit.types.Arguments` 1274 | :raises TypeError: if an argument is of the wrong data type 1275 | :raises ValueError: when an argument fails to validate 1276 | 1277 | """ 1278 | if not isinstance(queue, str): 1279 | raise TypeError('queue must be of type str') 1280 | elif not isinstance(exchange, str): 1281 | raise TypeError('exchange must be of type str') 1282 | elif not isinstance(routing_key, str): 1283 | raise TypeError('routing_Key must be of type str') 1284 | elif arguments and not isinstance(arguments, dict): 1285 | raise TypeError('arguments must be of type dict') 1286 | await self._send_rpc( 1287 | commands.Queue.Bind( 1288 | 0, queue, exchange, routing_key, False, arguments), 1289 | STATE_QUEUE_BIND_SENT, 1290 | STATE_QUEUE_BINDOK_RECEIVED) 1291 | 1292 | async def queue_unbind(self, 1293 | queue: str = '', 1294 | exchange: str = '', 1295 | routing_key: str = '', 1296 | arguments: types.Arguments = None) -> None: 1297 | """Unbind a queue from an exchange 1298 | 1299 | This method unbinds a queue from an exchange. 1300 | 1301 | :param queue: Specifies the name of the queue to unbind 1302 | :param exchange: Name of the exchange to unbind from 1303 | :param routing_key: Message routing key 1304 | :param arguments: Arguments of binding 1305 | :type arguments: :data:`~aiorabbit.types.Arguments` 1306 | :raises TypeError: if an argument is of the wrong data type 1307 | :raises ValueError: when an argument fails to validate 1308 | 1309 | """ 1310 | if not isinstance(queue, str): 1311 | raise TypeError('queue must be of type str') 1312 | elif not isinstance(exchange, str): 1313 | raise TypeError('exchange must be of type str') 1314 | elif not isinstance(routing_key, str): 1315 | raise TypeError('routing_Key must be of type str') 1316 | elif arguments and not isinstance(arguments, dict): 1317 | raise TypeError('arguments must be of type dict') 1318 | await self._send_rpc( 1319 | commands.Queue.Unbind(0, queue, exchange, routing_key, arguments), 1320 | STATE_QUEUE_UNBIND_SENT, 1321 | STATE_QUEUE_UNBINDOK_RECEIVED) 1322 | 1323 | async def queue_purge(self, queue: str = '') -> int: 1324 | """Purge a queue 1325 | 1326 | This method removes all messages from a queue which are not awaiting 1327 | acknowledgment. 1328 | 1329 | :param queue: Specifies the name of the queue to purge 1330 | :returns: The quantity of messages purged 1331 | 1332 | """ 1333 | if not isinstance(queue, str): 1334 | raise TypeError('queue must be of type str') 1335 | await self._send_rpc( 1336 | commands.Queue.Purge(0, queue, False), 1337 | STATE_QUEUE_PURGE_SENT, 1338 | STATE_QUEUE_PURGEOK_RECEIVED) 1339 | return self._last_frame.message_count 1340 | 1341 | async def tx_select(self) -> None: 1342 | """Select standard transaction mode 1343 | 1344 | This method sets the channel to use standard transactions. The client 1345 | must use this method at least once on a channel before using the 1346 | :meth:`~Client.tx_commit` or :meth:`~Client.tx_rollback` methods. 1347 | 1348 | """ 1349 | await self._send_rpc( 1350 | commands.Tx.Select(), 1351 | STATE_TX_SELECT_SENT, 1352 | STATE_TX_SELECTOK_RECEIVED) 1353 | 1354 | async def tx_commit(self) -> None: 1355 | """ Commit the current transaction 1356 | 1357 | This method commits all message publications and acknowledgments 1358 | performed in the current transaction. A new transaction starts 1359 | immediately after a commit. 1360 | 1361 | :raises aiorabbit.exceptions.NoTransactionError: when invoked prior 1362 | to invoking :meth:`~Client.tx_select`. 1363 | 1364 | """ 1365 | if not self._transactional: 1366 | raise exceptions.NoTransactionError() 1367 | await self._send_rpc( 1368 | commands.Tx.Commit(), 1369 | STATE_TX_COMMIT_SENT, 1370 | STATE_TX_COMMITOK_RECEIVED) 1371 | 1372 | async def tx_rollback(self) -> None: 1373 | """ Abandon the current transaction 1374 | 1375 | This method abandons all message publications and acknowledgments 1376 | performed in the current transaction. A new transaction starts 1377 | immediately after a rollback. Note that unacked messages will not be 1378 | automatically redelivered by rollback; if that is required an explicit 1379 | recover call should be issued. 1380 | 1381 | :raises aiorabbit.exceptions.NoTransactionError: when invoked prior 1382 | to invoking :meth:`~Client.tx_select`. 1383 | 1384 | """ 1385 | if not self._transactional: 1386 | raise exceptions.NoTransactionError() 1387 | await self._send_rpc( 1388 | commands.Tx.Rollback(), 1389 | STATE_TX_ROLLBACK_SENT, 1390 | STATE_TX_ROLLBACKOK_RECEIVED) 1391 | 1392 | async def _close(self) -> None: 1393 | self._set_state(STATE_CLOSING) 1394 | await self._channel0.close() 1395 | self._transport.close() 1396 | self._set_state(STATE_CLOSED) 1397 | self._reset() 1398 | 1399 | async def _connect(self) -> None: 1400 | self._set_state(STATE_CONNECTING) 1401 | port = self._url.port 1402 | if port is None: 1403 | port = 5671 if self._url.scheme == 'amqps' else 5672 1404 | self._logger.info( 1405 | 'Connecting to %s://%s:%s@%s:%s/%s', 1406 | self._url.scheme, self._url.user, 1407 | ''.ljust(len(self._url.password), '*'), 1408 | self._url.host, port, parse.quote(self._url.path[1:], '')) 1409 | heartbeat = self._url.query.get('heartbeat') 1410 | self._channel0 = channel0.Channel0( 1411 | self._blocked, 1412 | self._url.user, 1413 | self._url.password, 1414 | self._url.path[1:] if self._url.path[1:] else '/', 1415 | int(heartbeat) if heartbeat else None, 1416 | self._defaults.locale, 1417 | self._loop, 1418 | int(self._url.query.get('channel_max', '32768')), 1419 | self._defaults.product, 1420 | self._on_remote_close) 1421 | self._max_frame_size = float(self._channel0.max_frame_size) 1422 | ssl_enabled = self._url.scheme == 'amqps' 1423 | future = self._loop.create_connection( 1424 | lambda: protocol.AMQP( 1425 | self._on_connected, 1426 | self._on_disconnected, 1427 | self._on_frame, 1428 | ), self._url.host, port, 1429 | server_hostname=self._url.host if ssl_enabled else None, 1430 | ssl=self._ssl_context or ssl_enabled) 1431 | self._transport, self._protocol = await asyncio.wait_for( 1432 | future, timeout=self._connect_timeout) 1433 | self._max_frame_size = float(self._channel0.max_frame_size) 1434 | if await self._channel0.open(self._transport): 1435 | return self._set_state(STATE_OPENED) 1436 | await self._wait_on_state(STATE_OPENED) # To catch connection errors 1437 | 1438 | @property 1439 | def _connect_timeout(self) -> float: 1440 | temp = self._url.query.get('connection_timeout', '3.0') 1441 | return socket.getdefaulttimeout() if temp is None else float(temp) 1442 | 1443 | def _execute_callback(self, callback: typing.Callable, *args) -> None: 1444 | """Sync wrapper for invoking a sync/async callback and invoking 1445 | the callback on the IOLoop if it returned a coroutine (async def). 1446 | 1447 | """ 1448 | result = callback(*args) 1449 | if asyncio.iscoroutine(result): 1450 | self._loop.call_soon(asyncio.ensure_future, result) 1451 | 1452 | def _get_last_error(self) -> typing.Tuple[int, typing.Optional[str]]: 1453 | err = self._last_error 1454 | self._last_error = (0, None) 1455 | return err 1456 | 1457 | def _on_connected(self): 1458 | self._set_state(STATE_CONNECTED) 1459 | 1460 | def _on_disconnected(self, exc: typing.Optional[Exception]) -> None: 1461 | self._logger.debug('Disconnected: %r', exc) 1462 | if not self.is_closed: 1463 | self._set_state( 1464 | STATE_CLOSED, 1465 | exceptions.ConnectionClosedException( 1466 | 'Socket closed' if not exc else str(exc))) 1467 | 1468 | def _on_frame(self, channel: int, value: frame.FrameTypes) -> None: 1469 | self._last_frame = value 1470 | if channel == 0: 1471 | return self._channel0.process(value) 1472 | 1473 | # Reset last heartbeat timestamp since a frame was received 1474 | self._channel0.update_last_heartbeat() 1475 | 1476 | if isinstance(value, commands.Basic.Ack): 1477 | self._set_delivery_tag_result(value.delivery_tag, True) 1478 | self._set_state(STATE_BASIC_ACK_RECEIVED) 1479 | elif isinstance(value, commands.Basic.CancelOk): 1480 | del self._consumers[value.consumer_tag] 1481 | self._set_state(STATE_BASIC_CANCELOK_RECEIVED) 1482 | elif isinstance(value, commands.Basic.ConsumeOk): 1483 | future, callback = self._pending_consumers.popleft() 1484 | future.set_result(value.consumer_tag) 1485 | self._consumers[value.consumer_tag] = callback 1486 | self._set_state(STATE_BASIC_CONSUMEOK_RECEIVED) 1487 | elif isinstance(value, commands.Basic.Deliver): 1488 | self._set_state(STATE_BASIC_DELIVER_RECEIVED) 1489 | self._message = message.Message(value) 1490 | elif isinstance(value, commands.Basic.GetEmpty): 1491 | self._set_state(STATE_BASIC_GETEMPTY_RECEIVED) 1492 | self._get_future.set_result(None) 1493 | elif isinstance(value, commands.Basic.GetOk): 1494 | self._set_state(STATE_BASIC_GETOK_RECEIVED) 1495 | self._message = message.Message(value) 1496 | elif isinstance(value, commands.Basic.Nack): 1497 | self._set_delivery_tag_result(value.delivery_tag, False) 1498 | self._set_state(STATE_BASIC_NACK_RECEIVED) 1499 | elif isinstance(value, commands.Basic.QosOk): 1500 | self._set_state(STATE_BASIC_QOSOK_RECEIVED) 1501 | elif isinstance(value, commands.Basic.RecoverOk): 1502 | self._set_state(STATE_BASIC_RECOVEROK_RECEIVED) 1503 | elif isinstance(value, commands.Basic.Reject): 1504 | self._set_delivery_tag_result(value.delivery_tag, False) 1505 | self._set_state(STATE_BASIC_REJECT_RECEIVED) 1506 | elif isinstance(value, commands.Basic.Return): 1507 | self._set_state(STATE_BASIC_RETURN_RECEIVED) 1508 | self._message = message.Message(value) 1509 | elif isinstance(value, commands.Channel.Close): 1510 | self._set_state(STATE_CHANNEL_CLOSE_RECEIVED) 1511 | self._write_frames(commands.Channel.CloseOk()) 1512 | self._last_error = value.reply_code, value.reply_text 1513 | self._channel_open.clear() 1514 | self._set_state(STATE_CHANNEL_CLOSEOK_SENT) 1515 | elif isinstance(value, commands.Channel.CloseOk): 1516 | self._channel_open.clear() 1517 | self._set_state(STATE_CHANNEL_CLOSEOK_RECEIVED) 1518 | elif isinstance(value, commands.Channel.OpenOk): 1519 | self._channel_open.set() 1520 | self._set_state(STATE_CHANNEL_OPENOK_RECEIVED) 1521 | elif isinstance(value, commands.Confirm.SelectOk): 1522 | self._set_state(STATE_CONFIRM_SELECTOK_RECEIVED) 1523 | elif isinstance(value, header.ContentHeader): 1524 | self._set_state(STATE_CONTENT_HEADER_RECEIVED) 1525 | self._message.header = value 1526 | elif value.name == 'ContentBody': 1527 | self._set_state(STATE_CONTENT_BODY_RECEIVED) 1528 | self._message.body_frames.append(value) 1529 | if self._message.is_complete: 1530 | self._set_state(STATE_MESSAGE_ASSEMBLED) 1531 | if isinstance(self._message.method, commands.Basic.Deliver): 1532 | self._execute_callback( 1533 | self._consumers[self._message.consumer_tag], 1534 | self._pop_message()) 1535 | elif isinstance(self._message.method, commands.Basic.GetOk): 1536 | self._get_future.set_result(self._pop_message()) 1537 | else: # This will always be Basic.Return 1538 | self._execute_callback( 1539 | self._on_message_return, self._pop_message()) 1540 | elif isinstance(value, commands.Exchange.BindOk): 1541 | self._set_state(STATE_EXCHANGE_BINDOK_RECEIVED) 1542 | elif isinstance(value, commands.Exchange.DeclareOk): 1543 | self._set_state(STATE_EXCHANGE_DECLAREOK_RECEIVED) 1544 | elif isinstance(value, commands.Exchange.DeleteOk): 1545 | self._set_state(STATE_EXCHANGE_DELETEOK_RECEIVED) 1546 | elif isinstance(value, commands.Exchange.UnbindOk): 1547 | self._set_state(STATE_EXCHANGE_UNBINDOK_RECEIVED) 1548 | elif isinstance(value, commands.Queue.BindOk): 1549 | self._set_state(STATE_QUEUE_BINDOK_RECEIVED) 1550 | elif isinstance(value, commands.Queue.DeclareOk): 1551 | self._set_state(STATE_QUEUE_DECLAREOK_RECEIVED) 1552 | elif isinstance(value, commands.Queue.DeleteOk): 1553 | self._set_state(STATE_QUEUE_DELETEOK_RECEIVED) 1554 | elif isinstance(value, commands.Queue.PurgeOk): 1555 | self._set_state(STATE_QUEUE_PURGEOK_RECEIVED) 1556 | elif isinstance(value, commands.Queue.UnbindOk): 1557 | self._set_state(STATE_QUEUE_UNBINDOK_RECEIVED) 1558 | elif isinstance(value, commands.Tx.SelectOk): 1559 | self._transactional = True 1560 | self._set_state(STATE_TX_SELECTOK_RECEIVED) 1561 | elif isinstance(value, commands.Tx.CommitOk): 1562 | self._set_state(STATE_TX_COMMITOK_RECEIVED) 1563 | elif isinstance(value, commands.Tx.RollbackOk): 1564 | self._set_state(STATE_TX_ROLLBACKOK_RECEIVED) 1565 | else: 1566 | self._set_state(state.STATE_EXCEPTION, 1567 | RuntimeError('Unsupported AMQ method')) 1568 | 1569 | def _on_remote_close(self, 1570 | reply_code: int = 0, 1571 | reply_text: str = 'Unknown') -> None: 1572 | self._logger.info('Remote server closed the connection (%s) %s', 1573 | reply_code, reply_text) 1574 | self._last_error = (reply_code, reply_text) 1575 | if reply_code < 300: 1576 | return self._set_state(STATE_CLOSED) 1577 | elif reply_code == 599: 1578 | self._set_state( 1579 | STATE_CLOSED, exceptions.ConnectionClosedException(reply_text)) 1580 | else: 1581 | self._set_state( 1582 | state.STATE_EXCEPTION, 1583 | exceptions.CLASS_MAPPING[reply_code](reply_text)) 1584 | 1585 | async def _open_channel(self) -> None: 1586 | self._set_state(STATE_OPENING_CHANNEL) 1587 | self._channel += 1 1588 | if self._channel > self._channel0.max_channels: 1589 | self._channel = 1 1590 | self._transport.write( 1591 | frame.marshal(commands.Channel.Open(), self._channel)) 1592 | self._set_state(STATE_CHANNEL_OPEN_SENT) 1593 | await self._channel_open.wait() 1594 | 1595 | def _pop_message(self) -> message.Message: 1596 | if not self._message: 1597 | raise RuntimeError('Missing message') 1598 | value = self._message 1599 | self._message = None 1600 | return value 1601 | 1602 | async def _post_wait_on_state( 1603 | self, result: int = 0, 1604 | exc: typing.Optional[exceptions.AIORabbitException] = None, 1605 | raise_on_channel_close: bool = False) -> int: 1606 | """Process results from Client._send_rpc and Client._wait_on_state""" 1607 | if exc: 1608 | await asyncio.sleep(0.001) # Let pending things happen 1609 | await self._reconnect() 1610 | err = self._get_last_error() 1611 | raise exceptions.CLASS_MAPPING[err[0]](err[1]) 1612 | if result == STATE_CHANNEL_CLOSE_RECEIVED and self._last_error[0] > 0: 1613 | await self._open_channel() 1614 | await asyncio.sleep(0.001) # Sleep to let pending things happen 1615 | if raise_on_channel_close: 1616 | err = self._get_last_error() 1617 | raise exceptions.CLASS_MAPPING[err[0]](err[1]) 1618 | self._logger.warning('Channel was closed due to an error (%i) %s', 1619 | *self._last_error) 1620 | return result 1621 | 1622 | async def _reconnect(self) -> None: 1623 | self._logger.debug('Reconnecting to RabbitMQ') 1624 | publisher_confirms = self._publisher_confirms 1625 | self._reset() 1626 | await self._connect() 1627 | await self._open_channel() 1628 | if publisher_confirms: 1629 | await self.confirm_select() 1630 | 1631 | def _reset(self) -> None: 1632 | self._logger.debug('Resetting internal state') 1633 | self._blocked.clear() 1634 | self._channel = 0 1635 | self._channel_open.clear() 1636 | self._channel0 = None 1637 | self._connected.clear() 1638 | self._exception = None 1639 | self._protocol = None 1640 | self._publisher_confirms = False 1641 | self._transport = None 1642 | self._state = STATE_CLOSED 1643 | self._state_start = self._loop.time() 1644 | 1645 | async def _send_rpc(self, value: frame.FrameTypes, 1646 | new_state: int, 1647 | *states: int) -> int: 1648 | """Writes the RPC frame, blocking other RPCs, waiting on states, 1649 | returning the result from :meth:`Client._wait_on_state` 1650 | 1651 | """ 1652 | states = list(states) + [STATE_CHANNEL_CLOSE_RECEIVED] 1653 | exc, result = None, 0 1654 | async with self._rpc_lock: 1655 | if not self.is_closed: 1656 | self._write_frames(value) 1657 | self._set_state(new_state) 1658 | try: 1659 | result = await super()._wait_on_state(*states) 1660 | except exceptions.AIORabbitException as err: 1661 | exc = err 1662 | return await self._post_wait_on_state(result, exc, True) 1663 | 1664 | def _set_delivery_tag_result(self, delivery_tag: int, ack: bool): 1665 | for tag in range(min(self._delivery_tags.keys()), delivery_tag + 1): 1666 | self._confirmation_result[tag] = ack 1667 | self._delivery_tags[tag].set() 1668 | 1669 | @staticmethod 1670 | def _validate_bool(name: str, value: typing.Any) -> None: 1671 | if not isinstance(value, bool): 1672 | raise TypeError('{} must be of type bool'.format(name)) 1673 | 1674 | def _validate_exchange_name(self, name: str, value: typing.Any) -> None: 1675 | if value == '': 1676 | return 1677 | self._validate_short_str(name, value) 1678 | if NamePattern.match(value) is None: 1679 | raise ValueError('name must only contain letters, digits, hyphen, ' 1680 | 'underscore, period, or colon.') 1681 | 1682 | @staticmethod 1683 | def _validate_field_table(name: str, value: typing.Any) -> None: 1684 | if not isinstance(value, dict): 1685 | raise TypeError('{} must be of type dict'.format(name)) 1686 | elif not all(isinstance(k, str) and 0 < len(k) <= 256 1687 | for k in value.keys()): 1688 | raise ValueError('{} keys must all be of type str and ' 1689 | 'less than 256 characters'.format(name)) 1690 | 1691 | @staticmethod 1692 | def _validate_short_str(name: str, value: typing.Any) -> None: 1693 | if not isinstance(value, str): 1694 | raise TypeError('{} must be of type str'.format(name)) 1695 | elif len(value) > 256: 1696 | raise ValueError('{} must not exceed 256 characters'.format(name)) 1697 | 1698 | def _write_frames(self, *frames: frame.FrameTypes) -> None: 1699 | """Write one or more frames to the socket, marshalling on the way""" 1700 | for value in frames: 1701 | self._logger.debug('Writing frame: %r', value) 1702 | self._transport.write(frame.marshal(value, self._channel)) 1703 | 1704 | async def _wait_on_state(self, *args: int) -> int: 1705 | args = list(args) + [STATE_CHANNEL_CLOSE_RECEIVED] 1706 | try: 1707 | result = await super()._wait_on_state(*args) 1708 | except exceptions.AIORabbitException as exc: 1709 | if isinstance(exc, self.CONNECTING_EXCEPTIONS): 1710 | raise 1711 | await self._post_wait_on_state(exc=exc) 1712 | else: 1713 | return await self._post_wait_on_state(result) 1714 | --------------------------------------------------------------------------------