├── .coveragerc ├── .editorconfig ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── COPYING ├── Makefile ├── README.rst ├── aiormq ├── __init__.py ├── abc.py ├── auth.py ├── base.py ├── channel.py ├── connection.py ├── exceptions.py ├── py.typed ├── tools.py └── types.py ├── gray.conf ├── poetry.lock ├── poetry.toml ├── pylama.ini ├── pyproject.toml └── tests ├── __init__.py ├── certs ├── Dockerfile ├── ca.pem ├── client.key ├── client.pem ├── server.key └── server.pem ├── conftest.py ├── test_channel.py ├── test_connection.py ├── test_future_store.py └── test_tools.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | omit = 4 | */env/* 5 | */tests/* 6 | */.*/* 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | 9 | [*.{py,yml}] 10 | indent_style = space 11 | 12 | [*.py] 13 | indent_size = 4 14 | 15 | [docs/**.py] 16 | max_line_length = 80 17 | 18 | [*.rst] 19 | indent_size = 4 20 | 21 | [Makefile] 22 | indent_style = tab 23 | 24 | [*.yml] 25 | indent_size = 2 26 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | 4 | on: 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | 10 | 11 | jobs: 12 | pylama: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Setup python3.10 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: "3.10" 20 | - name: Cache virtualenv 21 | id: venv-cache 22 | uses: actions/cache@v3 23 | with: 24 | path: .venv 25 | key: venv-${{ runner.os }}-${{ github.job }}-${{ github.ref }} 26 | - run: python -m pip install poetry 27 | - run: poetry install 28 | - run: poetry run pylama 29 | env: 30 | FORCE_COLOR: 1 31 | mypy: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v2 35 | - name: Setup python3.10 36 | uses: actions/setup-python@v2 37 | with: 38 | python-version: "3.10" 39 | - name: Cache virtualenv 40 | id: venv-cache 41 | uses: actions/cache@v3 42 | with: 43 | path: .venv 44 | key: venv-${{ runner.os }}-${{ github.job }}-${{ github.ref }} 45 | - run: python -m pip install poetry 46 | - run: poetry install 47 | - run: poetry run mypy 48 | env: 49 | FORCE_COLOR: 1 50 | docs: 51 | runs-on: ubuntu-latest 52 | services: 53 | rabbitmq: 54 | image: docker://mosquito/aiormq-rabbitmq 55 | ports: 56 | - 5672:5672 57 | - 5671:5671 58 | steps: 59 | - uses: actions/checkout@v2 60 | - name: Setup python3.10 61 | uses: actions/setup-python@v2 62 | with: 63 | python-version: "3.10" 64 | - name: Cache virtualenv 65 | id: venv-cache 66 | uses: actions/cache@v3 67 | with: 68 | path: .venv 69 | key: venv-${{ runner.os }}-${{ github.job }}-${{ github.ref }} 70 | - run: python -m pip install poetry 71 | - run: poetry install 72 | - run: poetry run pytest -svv README.rst 73 | env: 74 | FORCE_COLOR: 1 75 | 76 | tests: 77 | runs-on: ubuntu-latest 78 | 79 | services: 80 | rabbitmq: 81 | image: docker://mosquito/aiormq-rabbitmq 82 | ports: 83 | - 5672:5672 84 | - 5671:5671 85 | 86 | strategy: 87 | fail-fast: false 88 | 89 | matrix: 90 | python: 91 | - '3.8' 92 | - '3.9' 93 | - '3.10' 94 | - '3.11' 95 | - '3.12' 96 | steps: 97 | - uses: actions/checkout@v2 98 | - name: Setup python${{ matrix.python }} 99 | uses: actions/setup-python@v2 100 | with: 101 | python-version: "${{ matrix.python }}" 102 | - name: Cache virtualenv 103 | id: venv-cache 104 | uses: actions/cache@v3 105 | with: 106 | path: .venv 107 | key: venv-${{ runner.os }}-${{ github.job }}-${{ github.ref }}-${{ matrix.python }} 108 | - run: python -m pip install poetry 109 | - run: poetry install --with=uvloop 110 | - name: pytest 111 | run: >- 112 | poetry run pytest \ 113 | -vv \ 114 | --cov=aiormq \ 115 | --cov-report=term-missing \ 116 | --doctest-modules \ 117 | --aiomisc-test-timeout=120 \ 118 | tests 119 | env: 120 | FORCE_COLOR: 1 121 | - run: poetry run coveralls 122 | env: 123 | COVERALLS_PARALLEL: 'true' 124 | COVERALLS_SERVICE_NAME: github 125 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 126 | 127 | finish: 128 | needs: 129 | - tests 130 | runs-on: ubuntu-latest 131 | steps: 132 | - name: Coveralls Finished 133 | uses: coverallsapp/github-action@master 134 | with: 135 | github-token: ${{ secrets.github_token }} 136 | parallel-finished: true 137 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### VirtualEnv template 3 | # Virtualenv 4 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 5 | .Python 6 | [Bb]in 7 | [Ii]nclude 8 | [Ll]ib 9 | [Ll]ib64 10 | [Ll]ocal 11 | [Ss]cripts 12 | pyvenv.cfg 13 | .venv 14 | pip-selfcheck.json 15 | ### IPythonNotebook template 16 | # Temporary data 17 | .ipynb_checkpoints/ 18 | ### Python template 19 | # Byte-compiled / optimized / DLL files 20 | __pycache__/ 21 | *.py[cod] 22 | *$py.class 23 | 24 | # C extensions 25 | *.so 26 | 27 | # Distribution / packaging 28 | .Python 29 | env/ 30 | build/ 31 | develop-eggs/ 32 | dist/ 33 | downloads/ 34 | eggs/ 35 | .eggs/ 36 | lib/ 37 | lib64/ 38 | parts/ 39 | sdist/ 40 | var/ 41 | *.egg-info/ 42 | .installed.cfg 43 | *.egg 44 | 45 | # PyInstaller 46 | # Usually these files are written by a python script from a template 47 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 48 | *.manifest 49 | *.spec 50 | 51 | # Installer logs 52 | pip-log.txt 53 | pip-delete-this-directory.txt 54 | 55 | # Unit test / coverage reports 56 | htmlcov/ 57 | .tox/ 58 | .coverage 59 | .coverage.* 60 | .cache 61 | nosetests.xml 62 | coverage.xml 63 | *,cover 64 | .hypothesis/ 65 | 66 | # Translations 67 | *.mo 68 | *.pot 69 | 70 | # Django stuff: 71 | *.log 72 | local_settings.py 73 | 74 | # Flask stuff: 75 | instance/ 76 | .webassets-cache 77 | 78 | # Scrapy stuff: 79 | .scrapy 80 | 81 | # Sphinx documentation 82 | docs/_build/ 83 | docs/source/apidoc 84 | 85 | # PyBuilder 86 | target/ 87 | 88 | # IPython Notebook 89 | .ipynb_checkpoints 90 | 91 | # pyenv 92 | .python-version 93 | 94 | # pytest 95 | .pytest_cache 96 | 97 | # celery beat schedule file 98 | celerybeat-schedule 99 | 100 | # dotenv 101 | .env 102 | 103 | # virtualenv 104 | venv/ 105 | ENV/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | 110 | # Rope project settings 111 | .ropeproject 112 | ### JetBrains template 113 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 114 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 115 | 116 | # User-specific stuff: 117 | .idea/ 118 | 119 | ## File-based project format: 120 | *.iws 121 | 122 | ## Plugin-specific files: 123 | 124 | # IntelliJ 125 | /out/ 126 | 127 | # mpeltonen/sbt-idea plugin 128 | .idea_modules/ 129 | 130 | # JIRA plugin 131 | atlassian-ide-plugin.xml 132 | 133 | # Crashlytics plugin (for Android Studio and IntelliJ) 134 | com_crashlytics_export_strings.xml 135 | crashlytics.properties 136 | crashlytics-build.properties 137 | fabric.properties 138 | 139 | /htmlcov 140 | /temp 141 | .DS_Store 142 | 143 | .*cache 144 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Apache License 2 | ============== 3 | 4 | _Version 2.0, January 2004_ 5 | 6 | 7 | ### Terms and Conditions for use, reproduction, and distribution 8 | 9 | #### 1. Definitions 10 | 11 | “License” shall mean the terms and conditions for use, reproduction, and 12 | distribution as defined by Sections 1 through 9 of this document. 13 | 14 | “Licensor” shall mean the copyright owner or entity authorized by the copyright 15 | owner that is granting the License. 16 | 17 | “Legal Entity” shall mean the union of the acting entity and all other entities 18 | that control, are controlled by, or are under common control with that entity. 19 | For the purposes of this definition, “control” means **(i)** the power, direct or 20 | indirect, to cause the direction or management of such entity, whether by 21 | contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the 22 | outstanding shares, or **(iii)** beneficial ownership of such entity. 23 | 24 | “You” (or “Your”) shall mean an individual or Legal Entity exercising 25 | permissions granted by this License. 26 | 27 | “Source” form shall mean the preferred form for making modifications, including 28 | but not limited to software source code, documentation source, and configuration 29 | files. 30 | 31 | “Object” form shall mean any form resulting from mechanical transformation or 32 | translation of a Source form, including but not limited to compiled object code, 33 | generated documentation, and conversions to other media types. 34 | 35 | “Work” shall mean the work of authorship, whether in Source or Object form, made 36 | available under the License, as indicated by a copyright notice that is included 37 | in or attached to the work (an example is provided in the Appendix below). 38 | 39 | “Derivative Works” shall mean any work, whether in Source or Object form, that 40 | is based on (or derived from) the Work and for which the editorial revisions, 41 | annotations, elaborations, or other modifications represent, as a whole, an 42 | original work of authorship. For the purposes of this License, Derivative Works 43 | shall not include works that remain separable from, or merely link (or bind by 44 | name) to the interfaces of, the Work and Derivative Works thereof. 45 | 46 | “Contribution” shall mean any work of authorship, including the original version 47 | of the Work and any modifications or additions to that Work or Derivative Works 48 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 49 | by the copyright owner or by an individual or Legal Entity authorized to submit 50 | on behalf of the copyright owner. For the purposes of this definition, 51 | “submitted” means any form of electronic, verbal, or written communication sent 52 | to the Licensor or its representatives, including but not limited to 53 | communication on electronic mailing lists, source code control systems, and 54 | issue tracking systems that are managed by, or on behalf of, the Licensor for 55 | the purpose of discussing and improving the Work, but excluding communication 56 | that is conspicuously marked or otherwise designated in writing by the copyright 57 | owner as “Not a Contribution.” 58 | 59 | “Contributor” shall mean Licensor and any individual or Legal Entity on behalf 60 | of whom a Contribution has been received by Licensor and subsequently 61 | incorporated within the Work. 62 | 63 | #### 2. Grant of Copyright License 64 | 65 | Subject to the terms and conditions of this License, each Contributor hereby 66 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 67 | irrevocable copyright license to reproduce, prepare Derivative Works of, 68 | publicly display, publicly perform, sublicense, and distribute the Work and such 69 | Derivative Works in Source or Object form. 70 | 71 | #### 3. Grant of Patent License 72 | 73 | Subject to the terms and conditions of this License, each Contributor hereby 74 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 75 | irrevocable (except as stated in this section) patent license to make, have 76 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 77 | such license applies only to those patent claims licensable by such Contributor 78 | that are necessarily infringed by their Contribution(s) alone or by combination 79 | of their Contribution(s) with the Work to which such Contribution(s) was 80 | submitted. If You institute patent litigation against any entity (including a 81 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 82 | Contribution incorporated within the Work constitutes direct or contributory 83 | patent infringement, then any patent licenses granted to You under this License 84 | for that Work shall terminate as of the date such litigation is filed. 85 | 86 | #### 4. Redistribution 87 | 88 | You may reproduce and distribute copies of the Work or Derivative Works thereof 89 | in any medium, with or without modifications, and in Source or Object form, 90 | provided that You meet the following conditions: 91 | 92 | * **(a)** You must give any other recipients of the Work or Derivative Works a copy of 93 | this License; and 94 | * **(b)** You must cause any modified files to carry prominent notices stating that You 95 | changed the files; and 96 | * **(c)** You must retain, in the Source form of any Derivative Works that You distribute, 97 | all copyright, patent, trademark, and attribution notices from the Source form 98 | of the Work, excluding those notices that do not pertain to any part of the 99 | Derivative Works; and 100 | * **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any 101 | Derivative Works that You distribute must include a readable copy of the 102 | attribution notices contained within such NOTICE file, excluding those notices 103 | that do not pertain to any part of the Derivative Works, in at least one of the 104 | following places: within a NOTICE text file distributed as part of the 105 | Derivative Works; within the Source form or documentation, if provided along 106 | with the Derivative Works; or, within a display generated by the Derivative 107 | Works, if and wherever such third-party notices normally appear. The contents of 108 | the NOTICE file are for informational purposes only and do not modify the 109 | License. You may add Your own attribution notices within Derivative Works that 110 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 111 | provided that such additional attribution notices cannot be construed as 112 | modifying the License. 113 | 114 | You may add Your own copyright statement to Your modifications and may provide 115 | additional or different license terms and conditions for use, reproduction, or 116 | distribution of Your modifications, or for any such Derivative Works as a whole, 117 | provided Your use, reproduction, and distribution of the Work otherwise complies 118 | with the conditions stated in this License. 119 | 120 | #### 5. Submission of Contributions 121 | 122 | Unless You explicitly state otherwise, any Contribution intentionally submitted 123 | for inclusion in the Work by You to the Licensor shall be under the terms and 124 | conditions of this License, without any additional terms or conditions. 125 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 126 | any separate license agreement you may have executed with Licensor regarding 127 | such Contributions. 128 | 129 | #### 6. Trademarks 130 | 131 | This License does not grant permission to use the trade names, trademarks, 132 | service marks, or product names of the Licensor, except as required for 133 | reasonable and customary use in describing the origin of the Work and 134 | reproducing the content of the NOTICE file. 135 | 136 | #### 7. Disclaimer of Warranty 137 | 138 | Unless required by applicable law or agreed to in writing, Licensor provides the 139 | Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, 140 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 141 | including, without limitation, any warranties or conditions of TITLE, 142 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 143 | solely responsible for determining the appropriateness of using or 144 | redistributing the Work and assume any risks associated with Your exercise of 145 | permissions under this License. 146 | 147 | #### 8. Limitation of Liability 148 | 149 | In no event and under no legal theory, whether in tort (including negligence), 150 | contract, or otherwise, unless required by applicable law (such as deliberate 151 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 152 | liable to You for damages, including any direct, indirect, special, incidental, 153 | or consequential damages of any character arising as a result of this License or 154 | out of the use or inability to use the Work (including but not limited to 155 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 156 | any and all other commercial damages or losses), even if such Contributor has 157 | been advised of the possibility of such damages. 158 | 159 | #### 9. Accepting Warranty or Additional Liability 160 | 161 | While redistributing the Work or Derivative Works thereof, You may choose to 162 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 163 | other liability obligations and/or rights consistent with this License. However, 164 | in accepting such obligations, You may act only on Your own behalf and on Your 165 | sole responsibility, not on behalf of any other Contributor, and only if You 166 | agree to indemnify, defend, and hold each Contributor harmless for any liability 167 | incurred by, or claims asserted against, such Contributor by reason of your 168 | accepting any such warranty or additional liability. 169 | 170 | _END OF TERMS AND CONDITIONS_ 171 | 172 | ### APPENDIX: How to apply the Apache License to your work 173 | 174 | To apply the Apache License to your work, attach the following boilerplate 175 | notice, with the fields enclosed by brackets `[]` replaced with your own 176 | identifying information. (Don't include the brackets!) The text should be 177 | enclosed in the appropriate comment syntax for the file format. We also 178 | recommend that a file or class name and description of purpose be included on 179 | the same “printed page” as the copyright notice for easier identification within 180 | third-party archives. 181 | 182 | Copyright 2023 Dmitry Orlov 183 | 184 | Licensed under the Apache License, Version 2.0 (the "License"); 185 | you may not use this file except in compliance with the License. 186 | You may obtain a copy of the License at 187 | 188 | http://www.apache.org/licenses/LICENSE-2.0 189 | 190 | Unless required by applicable law or agreed to in writing, software 191 | distributed under the License is distributed on an "AS IS" BASIS, 192 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 193 | See the License for the specific language governing permissions and 194 | limitations under the License. 195 | 196 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: clean test 2 | 3 | NAME:=$(shell poetry version -n | awk '{print $1}') 4 | VERSION:=$(shell poetry version -s) 5 | RABBITMQ_CONTAINER_NAME:=aiormq_rabbitmq 6 | RABBITMQ_IMAGE:=mosquito/aiormq-rabbitmq 7 | 8 | rabbitmq: 9 | docker kill $(RABBITMQ_CONTAINER_NAME) || true 10 | docker run --pull=always --rm -d \ 11 | --name $(RABBITMQ_CONTAINER_NAME) \ 12 | -p 5671:5671 \ 13 | -p 5672:5672 \ 14 | -p 15671:15671 \ 15 | -p 15672:15672 \ 16 | $(RABBITMQ_IMAGE) 17 | 18 | upload: 19 | poetry publish --build --skip-existing 20 | 21 | test: 22 | poetry run pytest -vvx --cov=aiormq \ 23 | --cov-report=term-missing tests README.rst 24 | 25 | clean: 26 | rm -fr *.egg-info .tox 27 | 28 | develop: clean 29 | poetry install 30 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ====== 2 | AIORMQ 3 | ====== 4 | 5 | .. image:: https://coveralls.io/repos/github/mosquito/aiormq/badge.svg?branch=master 6 | :target: https://coveralls.io/github/mosquito/aiormq?branch=master 7 | :alt: Coveralls 8 | 9 | .. image:: https://img.shields.io/pypi/status/aiormq.svg 10 | :target: https://github.com/mosquito/aiormq 11 | :alt: Status 12 | 13 | .. image:: https://github.com/mosquito/aiormq/workflows/tests/badge.svg 14 | :target: https://github.com/mosquito/aiormq/actions?query=workflow%3Atests 15 | :alt: Build status 16 | 17 | .. image:: https://img.shields.io/pypi/v/aiormq.svg 18 | :target: https://pypi.python.org/pypi/aiormq/ 19 | :alt: Latest Version 20 | 21 | .. image:: https://img.shields.io/pypi/wheel/aiormq.svg 22 | :target: https://pypi.python.org/pypi/aiormq/ 23 | 24 | .. image:: https://img.shields.io/pypi/pyversions/aiormq.svg 25 | :target: https://pypi.python.org/pypi/aiormq/ 26 | 27 | .. image:: https://img.shields.io/pypi/l/aiormq.svg 28 | :target: https://github.com/mosquito/aiormq/blob/master/LICENSE.md 29 | 30 | 31 | aiormq is a pure python AMQP client library. 32 | 33 | .. contents:: Table of contents 34 | 35 | Status 36 | ====== 37 | 38 | * 3.x.x branch - Production/Stable 39 | * 4.x.x branch - Unstable (Experimental) 40 | * 5.x.x and greater is only Production/Stable releases. 41 | 42 | Features 43 | ======== 44 | 45 | * Connecting by URL 46 | 47 | * amqp example: **amqp://user:password@server.host/vhost** 48 | * secure amqp example: **amqps://user:password@server.host/vhost?cafile=ca.pem&keyfile=key.pem&certfile=cert.pem&no_verify_ssl=0** 49 | 50 | * Buffered queue for received frames 51 | * Only `PLAIN`_ auth mechanism support 52 | * `Publisher confirms`_ support 53 | * `Transactions`_ support 54 | * Channel based asynchronous locks 55 | 56 | .. note:: 57 | AMQP 0.9.1 requires serialize sending for some frame types 58 | on the channel. e.g. Content body must be following after 59 | content header. But frames might be sent asynchronously 60 | on another channels. 61 | 62 | * Tracking unroutable messages 63 | (Use **connection.channel(on_return_raises=False)** for disabling) 64 | * Full SSL/TLS support, using your choice of: 65 | * ``amqps://`` url query parameters: 66 | * ``cafile=`` - string contains path to ca certificate file 67 | * ``capath=`` - string contains path to ca certificates 68 | * ``cadata=`` - base64 encoded ca certificate data 69 | * ``keyfile=`` - string contains path to key file 70 | * ``certfile=`` - string contains path to certificate file 71 | * ``no_verify_ssl`` - boolean disables certificates validation 72 | * ``context=`` `SSLContext`_ keyword argument to ``connect()``. 73 | * Python `type hints`_ 74 | * Uses `pamqp`_ as an AMQP 0.9.1 frame encoder/decoder 75 | 76 | 77 | .. _Publisher confirms: https://www.rabbitmq.com/confirms.html 78 | .. _Transactions: https://www.rabbitmq.com/semantics.html 79 | .. _PLAIN: https://www.rabbitmq.com/authentication.html 80 | .. _type hints: https://docs.python.org/3/library/typing.html 81 | .. _pamqp: https://pypi.org/project/pamqp/ 82 | .. _SSLContext: https://docs.python.org/3/library/ssl.html#ssl.SSLContext 83 | 84 | Tutorial 85 | ======== 86 | 87 | Introduction 88 | ------------ 89 | 90 | Simple consumer 91 | *************** 92 | 93 | .. code-block:: python 94 | 95 | import asyncio 96 | import aiormq 97 | 98 | async def on_message(message): 99 | """ 100 | on_message doesn't necessarily have to be defined as async. 101 | Here it is to show that it's possible. 102 | """ 103 | print(f" [x] Received message {message!r}") 104 | print(f"Message body is: {message.body!r}") 105 | print("Before sleep!") 106 | await asyncio.sleep(5) # Represents async I/O operations 107 | print("After sleep!") 108 | 109 | 110 | async def main(): 111 | # Perform connection 112 | connection = await aiormq.connect("amqp://guest:guest@localhost/") 113 | 114 | # Creating a channel 115 | channel = await connection.channel() 116 | 117 | # Declaring queue 118 | declare_ok = await channel.queue_declare('helo') 119 | consume_ok = await channel.basic_consume( 120 | declare_ok.queue, on_message, no_ack=True 121 | ) 122 | 123 | 124 | loop = asyncio.get_event_loop() 125 | loop.run_until_complete(main()) 126 | loop.run_forever() 127 | 128 | 129 | Simple publisher 130 | **************** 131 | 132 | .. code-block:: python 133 | :name: test_simple_publisher 134 | 135 | import asyncio 136 | from typing import Optional 137 | 138 | import aiormq 139 | from aiormq.abc import DeliveredMessage 140 | 141 | 142 | MESSAGE: Optional[DeliveredMessage] = None 143 | 144 | 145 | async def main(): 146 | global MESSAGE 147 | 148 | body = b'Hello World!' 149 | 150 | # Perform connection 151 | connection = await aiormq.connect("amqp://guest:guest@localhost//") 152 | 153 | # Creating a channel 154 | channel = await connection.channel() 155 | 156 | declare_ok = await channel.queue_declare("hello", auto_delete=True) 157 | 158 | # Sending the message 159 | await channel.basic_publish(body, routing_key='hello') 160 | print(f" [x] Sent {body}") 161 | 162 | MESSAGE = await channel.basic_get(declare_ok.queue) 163 | print(f" [x] Received message from {declare_ok.queue!r}") 164 | 165 | 166 | loop = asyncio.get_event_loop() 167 | loop.run_until_complete(main()) 168 | 169 | assert MESSAGE is not None 170 | assert MESSAGE.routing_key == "hello" 171 | assert MESSAGE.body == b'Hello World!' 172 | 173 | 174 | Work Queues 175 | ----------- 176 | 177 | Create new task 178 | *************** 179 | 180 | .. code-block:: python 181 | 182 | import sys 183 | import asyncio 184 | import aiormq 185 | 186 | 187 | async def main(): 188 | # Perform connection 189 | connection = await aiormq.connect("amqp://guest:guest@localhost/") 190 | 191 | # Creating a channel 192 | channel = await connection.channel() 193 | 194 | body = b' '.join(sys.argv[1:]) or b"Hello World!" 195 | 196 | # Sending the message 197 | await channel.basic_publish( 198 | body, 199 | routing_key='task_queue', 200 | properties=aiormq.spec.Basic.Properties( 201 | delivery_mode=1, 202 | ) 203 | ) 204 | 205 | print(f" [x] Sent {body!r}") 206 | 207 | await connection.close() 208 | 209 | 210 | loop = asyncio.get_event_loop() 211 | loop.run_until_complete(main()) 212 | 213 | 214 | Simple worker 215 | ************* 216 | 217 | .. code-block:: python 218 | 219 | import asyncio 220 | import aiormq 221 | import aiormq.abc 222 | 223 | 224 | async def on_message(message: aiormq.abc.DeliveredMessage): 225 | print(f" [x] Received message {message!r}") 226 | print(f" Message body is: {message.body!r}") 227 | 228 | 229 | async def main(): 230 | # Perform connection 231 | connection = await aiormq.connect("amqp://guest:guest@localhost/") 232 | 233 | 234 | # Creating a channel 235 | channel = await connection.channel() 236 | await channel.basic_qos(prefetch_count=1) 237 | 238 | # Declaring queue 239 | declare_ok = await channel.queue_declare('task_queue', durable=True) 240 | 241 | # Start listening the queue with name 'task_queue' 242 | await channel.basic_consume(declare_ok.queue, on_message, no_ack=True) 243 | 244 | 245 | loop = asyncio.get_event_loop() 246 | loop.run_until_complete(main()) 247 | 248 | # we enter a never-ending loop that waits for data and runs 249 | # callbacks whenever necessary. 250 | print(" [*] Waiting for messages. To exit press CTRL+C") 251 | loop.run_forever() 252 | 253 | 254 | Publish Subscribe 255 | ----------------- 256 | 257 | Publisher 258 | ********* 259 | 260 | .. code-block:: python 261 | 262 | import sys 263 | import asyncio 264 | import aiormq 265 | 266 | 267 | async def main(): 268 | # Perform connection 269 | connection = await aiormq.connect("amqp://guest:guest@localhost/") 270 | 271 | # Creating a channel 272 | channel = await connection.channel() 273 | 274 | await channel.exchange_declare( 275 | exchange='logs', exchange_type='fanout' 276 | ) 277 | 278 | body = b' '.join(sys.argv[1:]) or b"Hello World!" 279 | 280 | # Sending the message 281 | await channel.basic_publish( 282 | body, routing_key='info', exchange='logs' 283 | ) 284 | 285 | print(f" [x] Sent {body!r}") 286 | 287 | await connection.close() 288 | 289 | 290 | loop = asyncio.get_event_loop() 291 | loop.run_until_complete(main()) 292 | 293 | 294 | Subscriber 295 | ********** 296 | 297 | .. code-block:: python 298 | 299 | import asyncio 300 | import aiormq 301 | import aiormq.abc 302 | 303 | 304 | async def on_message(message: aiormq.abc.DeliveredMessage): 305 | print(f"[x] {message.body!r}") 306 | 307 | await message.channel.basic_ack( 308 | message.delivery.delivery_tag 309 | ) 310 | 311 | 312 | async def main(): 313 | # Perform connection 314 | connection = await aiormq.connect("amqp://guest:guest@localhost/") 315 | 316 | # Creating a channel 317 | channel = await connection.channel() 318 | await channel.basic_qos(prefetch_count=1) 319 | 320 | await channel.exchange_declare( 321 | exchange='logs', exchange_type='fanout' 322 | ) 323 | 324 | # Declaring queue 325 | declare_ok = await channel.queue_declare(exclusive=True) 326 | 327 | # Binding the queue to the exchange 328 | await channel.queue_bind(declare_ok.queue, 'logs') 329 | 330 | # Start listening the queue with name 'task_queue' 331 | await channel.basic_consume(declare_ok.queue, on_message) 332 | 333 | 334 | loop = asyncio.get_event_loop() 335 | loop.create_task(main()) 336 | 337 | # we enter a never-ending loop that waits for data 338 | # and runs callbacks whenever necessary. 339 | print(' [*] Waiting for logs. To exit press CTRL+C') 340 | loop.run_forever() 341 | 342 | 343 | Routing 344 | ------- 345 | 346 | Direct consumer 347 | *************** 348 | 349 | .. code-block:: python 350 | 351 | import sys 352 | import asyncio 353 | import aiormq 354 | import aiormq.abc 355 | 356 | 357 | async def on_message(message: aiormq.abc.DeliveredMessage): 358 | print(f" [x] {message.delivery.routing_key!r}:{message.body!r}" 359 | await message.channel.basic_ack( 360 | message.delivery.delivery_tag 361 | ) 362 | 363 | 364 | async def main(): 365 | # Perform connection 366 | connection = aiormq.Connection("amqp://guest:guest@localhost/") 367 | await connection.connect() 368 | 369 | # Creating a channel 370 | channel = await connection.channel() 371 | await channel.basic_qos(prefetch_count=1) 372 | 373 | severities = sys.argv[1:] 374 | 375 | if not severities: 376 | sys.stderr.write(f"Usage: {sys.argv[0]} [info] [warning] [error]\n") 377 | sys.exit(1) 378 | 379 | # Declare an exchange 380 | await channel.exchange_declare( 381 | exchange='logs', exchange_type='direct' 382 | ) 383 | 384 | # Declaring random queue 385 | declare_ok = await channel.queue_declare(durable=True, auto_delete=True) 386 | 387 | for severity in severities: 388 | await channel.queue_bind( 389 | declare_ok.queue, 'logs', routing_key=severity 390 | ) 391 | 392 | # Start listening the random queue 393 | await channel.basic_consume(declare_ok.queue, on_message) 394 | 395 | 396 | loop = asyncio.get_event_loop() 397 | loop.run_until_complete(main()) 398 | 399 | # we enter a never-ending loop that waits for data 400 | # and runs callbacks whenever necessary. 401 | print(" [*] Waiting for messages. To exit press CTRL+C") 402 | loop.run_forever() 403 | 404 | 405 | Emitter 406 | ******* 407 | 408 | .. code-block:: python 409 | 410 | import sys 411 | import asyncio 412 | import aiormq 413 | 414 | 415 | async def main(): 416 | # Perform connection 417 | connection = await aiormq.connect("amqp://guest:guest@localhost/") 418 | 419 | # Creating a channel 420 | channel = await connection.channel() 421 | 422 | await channel.exchange_declare( 423 | exchange='logs', exchange_type='direct' 424 | ) 425 | 426 | body = ( 427 | b' '.join(arg.encode() for arg in sys.argv[2:]) 428 | or 429 | b"Hello World!" 430 | ) 431 | 432 | # Sending the message 433 | routing_key = sys.argv[1] if len(sys.argv) > 2 else 'info' 434 | 435 | await channel.basic_publish( 436 | body, exchange='logs', routing_key=routing_key, 437 | properties=aiormq.spec.Basic.Properties( 438 | delivery_mode=1 439 | ) 440 | ) 441 | 442 | print(f" [x] Sent {body!r}") 443 | 444 | await connection.close() 445 | 446 | 447 | loop = asyncio.get_event_loop() 448 | loop.run_until_complete(main()) 449 | 450 | Topics 451 | ------ 452 | 453 | Publisher 454 | ********* 455 | 456 | .. code-block:: python 457 | 458 | import sys 459 | import asyncio 460 | import aiormq 461 | 462 | 463 | async def main(): 464 | # Perform connection 465 | connection = await aiormq.connect("amqp://guest:guest@localhost/") 466 | 467 | # Creating a channel 468 | channel = await connection.channel() 469 | 470 | await channel.exchange_declare('topic_logs', exchange_type='topic') 471 | 472 | routing_key = ( 473 | sys.argv[1] if len(sys.argv) > 2 else 'anonymous.info' 474 | ) 475 | 476 | body = ( 477 | b' '.join(arg.encode() for arg in sys.argv[2:]) 478 | or 479 | b"Hello World!" 480 | ) 481 | 482 | # Sending the message 483 | await channel.basic_publish( 484 | body, exchange='topic_logs', routing_key=routing_key, 485 | properties=aiormq.spec.Basic.Properties( 486 | delivery_mode=1 487 | ) 488 | ) 489 | 490 | print(f" [x] Sent {body!r}") 491 | 492 | await connection.close() 493 | 494 | 495 | loop = asyncio.get_event_loop() 496 | loop.run_until_complete(main()) 497 | 498 | Consumer 499 | ******** 500 | 501 | .. code-block:: python 502 | 503 | import asyncio 504 | import sys 505 | import aiormq 506 | import aiormq.abc 507 | 508 | 509 | async def on_message(message: aiormq.abc.DeliveredMessage): 510 | print(f" [x] {message.delivery.routing_key!r}:{message.body!r}") 511 | await message.channel.basic_ack( 512 | message.delivery.delivery_tag 513 | ) 514 | 515 | 516 | async def main(): 517 | # Perform connection 518 | connection = await aiormq.connect( 519 | "amqp://guest:guest@localhost/", loop=loop 520 | ) 521 | 522 | # Creating a channel 523 | channel = await connection.channel() 524 | await channel.basic_qos(prefetch_count=1) 525 | 526 | # Declare an exchange 527 | await channel.exchange_declare('topic_logs', exchange_type='topic') 528 | 529 | # Declaring queue 530 | declare_ok = await channel.queue_declare('task_queue', durable=True) 531 | 532 | binding_keys = sys.argv[1:] 533 | 534 | if not binding_keys: 535 | sys.stderr.write( 536 | f"Usage: {sys.argv[0]} [binding_key]...\n" 537 | ) 538 | sys.exit(1) 539 | 540 | for binding_key in binding_keys: 541 | await channel.queue_bind( 542 | declare_ok.queue, 'topic_logs', routing_key=binding_key 543 | ) 544 | 545 | # Start listening the queue with name 'task_queue' 546 | await channel.basic_consume(declare_ok.queue, on_message) 547 | 548 | 549 | loop = asyncio.get_event_loop() 550 | loop.create_task(main()) 551 | 552 | # we enter a never-ending loop that waits for 553 | # data and runs callbacks whenever necessary. 554 | print(" [*] Waiting for messages. To exit press CTRL+C") 555 | loop.run_forever() 556 | 557 | Remote procedure call (RPC) 558 | --------------------------- 559 | 560 | RPC server 561 | ********** 562 | 563 | .. code-block:: python 564 | 565 | import asyncio 566 | import aiormq 567 | import aiormq.abc 568 | 569 | 570 | def fib(n): 571 | if n == 0: 572 | return 0 573 | elif n == 1: 574 | return 1 575 | else: 576 | return fib(n-1) + fib(n-2) 577 | 578 | 579 | async def on_message(message:aiormq.abc.DeliveredMessage): 580 | n = int(message.body.decode()) 581 | 582 | print(f" [.] fib({n})") 583 | response = str(fib(n)).encode() 584 | 585 | await message.channel.basic_publish( 586 | response, routing_key=message.header.properties.reply_to, 587 | properties=aiormq.spec.Basic.Properties( 588 | correlation_id=message.header.properties.correlation_id 589 | ), 590 | 591 | ) 592 | 593 | await message.channel.basic_ack(message.delivery.delivery_tag) 594 | print('Request complete') 595 | 596 | 597 | async def main(): 598 | # Perform connection 599 | connection = await aiormq.connect("amqp://guest:guest@localhost/") 600 | 601 | # Creating a channel 602 | channel = await connection.channel() 603 | 604 | # Declaring queue 605 | declare_ok = await channel.queue_declare('rpc_queue') 606 | 607 | # Start listening the queue with name 'hello' 608 | await channel.basic_consume(declare_ok.queue, on_message) 609 | 610 | 611 | loop = asyncio.get_event_loop() 612 | loop.create_task(main()) 613 | 614 | # we enter a never-ending loop that waits for data 615 | # and runs callbacks whenever necessary. 616 | print(" [x] Awaiting RPC requests") 617 | loop.run_forever() 618 | 619 | 620 | RPC client 621 | ********** 622 | 623 | .. code-block:: python 624 | 625 | import asyncio 626 | import uuid 627 | import aiormq 628 | import aiormq.abc 629 | 630 | 631 | class FibonacciRpcClient: 632 | def __init__(self): 633 | self.connection = None # type: aiormq.Connection 634 | self.channel = None # type: aiormq.Channel 635 | self.callback_queue = '' 636 | self.futures = {} 637 | self.loop = loop 638 | 639 | async def connect(self): 640 | self.connection = await aiormq.connect("amqp://guest:guest@localhost/") 641 | 642 | self.channel = await self.connection.channel() 643 | declare_ok = await self.channel.queue_declare( 644 | exclusive=True, auto_delete=True 645 | ) 646 | 647 | await self.channel.basic_consume(declare_ok.queue, self.on_response) 648 | 649 | self.callback_queue = declare_ok.queue 650 | 651 | return self 652 | 653 | async def on_response(self, message: aiormq.abc.DeliveredMessage): 654 | future = self.futures.pop(message.header.properties.correlation_id) 655 | future.set_result(message.body) 656 | 657 | async def call(self, n): 658 | correlation_id = str(uuid.uuid4()) 659 | future = loop.create_future() 660 | 661 | self.futures[correlation_id] = future 662 | 663 | await self.channel.basic_publish( 664 | str(n).encode(), routing_key='rpc_queue', 665 | properties=aiormq.spec.Basic.Properties( 666 | content_type='text/plain', 667 | correlation_id=correlation_id, 668 | reply_to=self.callback_queue, 669 | ) 670 | ) 671 | 672 | return int(await future) 673 | 674 | 675 | async def main(): 676 | fibonacci_rpc = await FibonacciRpcClient().connect() 677 | print(" [x] Requesting fib(30)") 678 | response = await fibonacci_rpc.call(30) 679 | print(r" [.] Got {response!r}") 680 | 681 | 682 | loop = asyncio.get_event_loop() 683 | loop.run_until_complete(main()) 684 | -------------------------------------------------------------------------------- /aiormq/__init__.py: -------------------------------------------------------------------------------- 1 | from pamqp import commands as spec 2 | 3 | from . import abc 4 | from .channel import Channel 5 | from .connection import Connection, connect 6 | from .exceptions import ( 7 | AMQPChannelError, AMQPConnectionError, AMQPError, AMQPException, 8 | AuthenticationError, ChannelAccessRefused, ChannelClosed, 9 | ChannelInvalidStateError, ChannelLockedResource, ChannelNotFoundEntity, 10 | ChannelPreconditionFailed, ConnectionChannelError, ConnectionClosed, 11 | ConnectionCommandInvalid, ConnectionFrameError, ConnectionInternalError, 12 | ConnectionNotAllowed, ConnectionNotImplemented, ConnectionResourceError, 13 | ConnectionSyntaxError, ConnectionUnexpectedFrame, DeliveryError, 14 | DuplicateConsumerTag, IncompatibleProtocolError, InvalidFrameError, 15 | MethodNotImplemented, ProbableAuthenticationError, ProtocolSyntaxError, 16 | PublishError, 17 | ) 18 | 19 | 20 | __all__ = ( 21 | "AMQPChannelError", 22 | "AMQPConnectionError", 23 | "AMQPError", 24 | "AMQPException", 25 | "AuthenticationError", 26 | "Channel", 27 | "ChannelAccessRefused", 28 | "ChannelClosed", 29 | "ChannelInvalidStateError", 30 | "ChannelLockedResource", 31 | "ChannelNotFoundEntity", 32 | "ChannelPreconditionFailed", 33 | "Connection", 34 | "ConnectionChannelError", 35 | "ConnectionClosed", 36 | "ConnectionCommandInvalid", 37 | "ConnectionFrameError", 38 | "ConnectionInternalError", 39 | "ConnectionNotAllowed", 40 | "ConnectionNotImplemented", 41 | "ConnectionResourceError", 42 | "ConnectionSyntaxError", 43 | "ConnectionUnexpectedFrame", 44 | "DeliveryError", 45 | "DuplicateConsumerTag", 46 | "IncompatibleProtocolError", 47 | "InvalidFrameError", 48 | "MethodNotImplemented", 49 | "ProbableAuthenticationError", 50 | "ProtocolSyntaxError", 51 | "PublishError", 52 | "abc", 53 | "connect", 54 | "spec", 55 | ) 56 | -------------------------------------------------------------------------------- /aiormq/abc.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import dataclasses 3 | import io 4 | import logging 5 | from abc import ABC, abstractmethod, abstractproperty 6 | from types import TracebackType 7 | from typing import ( 8 | Any, Awaitable, Callable, Coroutine, Dict, Iterable, Optional, Set, Tuple, 9 | Type, Union, 10 | ) 11 | 12 | import pamqp 13 | from pamqp import commands as spec 14 | from pamqp.base import Frame 15 | from pamqp.body import ContentBody 16 | from pamqp.commands import Basic, Channel, Confirm, Exchange, Queue, Tx 17 | from pamqp.common import FieldArray, FieldTable, FieldValue 18 | from pamqp.constants import REPLY_SUCCESS 19 | from pamqp.header import ContentHeader 20 | from pamqp.heartbeat import Heartbeat 21 | from yarl import URL 22 | 23 | 24 | ExceptionType = Union[BaseException, Type[BaseException]] 25 | 26 | 27 | # noinspection PyShadowingNames 28 | class TaskWrapper: 29 | __slots__ = "_exception", "task" 30 | 31 | _exception: Union[BaseException, Type[BaseException]] 32 | task: asyncio.Task 33 | 34 | def __init__(self, task: asyncio.Task): 35 | self.task = task 36 | self._exception = asyncio.CancelledError 37 | 38 | def throw(self, exception: ExceptionType) -> None: 39 | self._exception = exception 40 | self.task.cancel() 41 | 42 | async def __inner(self) -> Any: 43 | try: 44 | return await self.task 45 | except asyncio.CancelledError as e: 46 | raise self._exception from e 47 | 48 | def __await__(self, *args: Any, **kwargs: Any) -> Any: 49 | return self.__inner().__await__() 50 | 51 | def cancel(self) -> None: 52 | return self.throw(asyncio.CancelledError()) 53 | 54 | def __getattr__(self, item: str) -> Any: 55 | return getattr(self.task, item) 56 | 57 | def __repr__(self) -> str: 58 | return "<%s: %s>" % (self.__class__.__name__, repr(self.task)) 59 | 60 | 61 | TaskType = Union[asyncio.Task, TaskWrapper] 62 | CoroutineType = Coroutine[Any, None, Any] 63 | GetResultType = Union[Basic.GetEmpty, Basic.GetOk] 64 | 65 | 66 | @dataclasses.dataclass(frozen=True) 67 | class DeliveredMessage: 68 | delivery: Union[spec.Basic.Deliver, spec.Basic.Return, GetResultType] 69 | header: ContentHeader 70 | body: bytes 71 | channel: "AbstractChannel" 72 | 73 | @property 74 | def routing_key(self) -> Optional[str]: 75 | if isinstance( 76 | self.delivery, ( 77 | spec.Basic.Return, 78 | spec.Basic.GetOk, 79 | spec.Basic.Deliver, 80 | ), 81 | ): 82 | return self.delivery.routing_key 83 | return None 84 | 85 | @property 86 | def exchange(self) -> Optional[str]: 87 | if isinstance( 88 | self.delivery, ( 89 | spec.Basic.Return, 90 | spec.Basic.GetOk, 91 | spec.Basic.Deliver, 92 | ), 93 | ): 94 | return self.delivery.exchange 95 | return None 96 | 97 | @property 98 | def delivery_tag(self) -> Optional[int]: 99 | if isinstance( 100 | self.delivery, ( 101 | spec.Basic.GetOk, 102 | spec.Basic.Deliver, 103 | ), 104 | ): 105 | return self.delivery.delivery_tag 106 | return None 107 | 108 | @property 109 | def redelivered(self) -> Optional[bool]: 110 | if isinstance( 111 | self.delivery, ( 112 | spec.Basic.GetOk, 113 | spec.Basic.Deliver, 114 | ), 115 | ): 116 | return self.delivery.redelivered 117 | return None 118 | 119 | @property 120 | def consumer_tag(self) -> Optional[str]: 121 | if isinstance(self.delivery, spec.Basic.Deliver): 122 | return self.delivery.consumer_tag 123 | return None 124 | 125 | @property 126 | def message_count(self) -> Optional[int]: 127 | if isinstance(self.delivery, spec.Basic.GetOk): 128 | return self.delivery.message_count 129 | return None 130 | 131 | 132 | ChannelRType = Tuple[int, Channel.OpenOk] 133 | 134 | CallbackCoro = Coroutine[Any, Any, Any] 135 | ConsumerCallback = Callable[[DeliveredMessage], CallbackCoro] 136 | ReturnCallback = Callable[[DeliveredMessage], Any] 137 | 138 | ArgumentsType = FieldTable 139 | 140 | ConfirmationFrameType = Union[ 141 | Basic.Ack, Basic.Nack, Basic.Reject, 142 | ] 143 | 144 | 145 | @dataclasses.dataclass(frozen=True) 146 | class SSLCerts: 147 | cert: Optional[str] 148 | key: Optional[str] 149 | capath: Optional[str] 150 | cafile: Optional[str] 151 | cadata: Optional[bytes] 152 | verify: bool 153 | 154 | 155 | @dataclasses.dataclass(frozen=True) 156 | class FrameReceived: 157 | channel: int 158 | frame: str 159 | 160 | 161 | URLorStr = Union[URL, str] 162 | DrainResult = Awaitable[None] 163 | TimeoutType = Optional[Union[float, int]] 164 | FrameType = Union[Frame, ContentHeader, ContentBody] 165 | RpcReturnType = Optional[ 166 | Union[ 167 | Basic.CancelOk, 168 | Basic.ConsumeOk, 169 | Basic.GetOk, 170 | Basic.QosOk, 171 | Basic.RecoverOk, 172 | Channel.CloseOk, 173 | Channel.FlowOk, 174 | Channel.OpenOk, 175 | Confirm.SelectOk, 176 | Exchange.BindOk, 177 | Exchange.DeclareOk, 178 | Exchange.DeleteOk, 179 | Exchange.UnbindOk, 180 | Queue.BindOk, 181 | Queue.DeleteOk, 182 | Queue.DeleteOk, 183 | Queue.PurgeOk, 184 | Queue.UnbindOk, 185 | Tx.CommitOk, 186 | Tx.RollbackOk, 187 | Tx.SelectOk, 188 | ] 189 | ] 190 | 191 | 192 | @dataclasses.dataclass(frozen=True) 193 | class ChannelFrame: 194 | payload: bytes 195 | should_close: bool 196 | drain_future: Optional[asyncio.Future] = None 197 | 198 | def drain(self) -> None: 199 | if not self.should_drain: 200 | return 201 | 202 | if self.drain_future is not None and not self.drain_future.done(): 203 | self.drain_future.set_result(None) 204 | 205 | @property 206 | def should_drain(self) -> bool: 207 | return self.drain_future is not None and not self.drain_future.done() 208 | 209 | @classmethod 210 | def marshall( 211 | cls, channel_number: int, 212 | frames: Iterable[Union[FrameType, Heartbeat, ContentBody]], 213 | drain_future: Optional[asyncio.Future] = None, 214 | ) -> "ChannelFrame": 215 | should_close = False 216 | 217 | with io.BytesIO() as fp: 218 | for frame in frames: 219 | if should_close: 220 | logger = logging.getLogger( 221 | "aiormq.connection", 222 | ).getChild( 223 | "marshall", 224 | ) 225 | 226 | logger.warning( 227 | "It looks like you are going to send a frame %r after " 228 | "the connection is closed, it's pointless, " 229 | "the frame is dropped.", frame, 230 | ) 231 | continue 232 | if isinstance(frame, spec.Connection.CloseOk): 233 | should_close = True 234 | fp.write(pamqp.frame.marshal(frame, channel_number)) 235 | 236 | return cls( 237 | payload=fp.getvalue(), 238 | drain_future=drain_future, 239 | should_close=should_close, 240 | ) 241 | 242 | 243 | class AbstractFutureStore: 244 | futures: Set[Union[asyncio.Future, TaskType]] 245 | loop: asyncio.AbstractEventLoop 246 | 247 | @abstractmethod 248 | def add(self, future: Union[asyncio.Future, TaskWrapper]) -> None: 249 | raise NotImplementedError 250 | 251 | @abstractmethod 252 | def reject_all(self, exception: Optional[ExceptionType]) -> Any: 253 | raise NotImplementedError 254 | 255 | @abstractmethod 256 | def create_task(self, coro: CoroutineType) -> TaskType: 257 | raise NotImplementedError 258 | 259 | @abstractmethod 260 | def create_future(self) -> asyncio.Future: 261 | raise NotImplementedError 262 | 263 | @abstractmethod 264 | def get_child(self) -> "AbstractFutureStore": 265 | raise NotImplementedError 266 | 267 | 268 | class AbstractBase(ABC): 269 | loop: asyncio.AbstractEventLoop 270 | 271 | @abstractmethod 272 | def _future_store_child(self) -> AbstractFutureStore: 273 | raise NotImplementedError 274 | 275 | @abstractmethod 276 | def create_task(self, coro: CoroutineType) -> TaskType: 277 | raise NotImplementedError 278 | 279 | def create_future(self) -> asyncio.Future: 280 | raise NotImplementedError 281 | 282 | @abstractmethod 283 | async def _on_close(self, exc: Optional[Exception] = None) -> None: 284 | raise NotImplementedError 285 | 286 | @abstractmethod 287 | async def close( 288 | self, exc: Optional[ExceptionType] = asyncio.CancelledError(), 289 | ) -> None: 290 | raise NotImplementedError 291 | 292 | @abstractmethod 293 | def __str__(self) -> str: 294 | raise NotImplementedError 295 | 296 | @abstractproperty 297 | def is_closed(self) -> bool: 298 | raise NotImplementedError 299 | 300 | 301 | class AbstractChannel(AbstractBase): 302 | frames: asyncio.Queue 303 | connection: "AbstractConnection" 304 | number: int 305 | on_return_callbacks: Set[ReturnCallback] 306 | closing: asyncio.Future 307 | 308 | @abstractmethod 309 | async def open(self) -> spec.Channel.OpenOk: 310 | pass 311 | 312 | @abstractmethod 313 | async def basic_get( 314 | self, queue: str = "", no_ack: bool = False, 315 | timeout: TimeoutType = None, 316 | ) -> DeliveredMessage: 317 | raise NotImplementedError 318 | 319 | @abstractmethod 320 | async def basic_cancel( 321 | self, consumer_tag: str, *, nowait: bool = False, 322 | timeout: TimeoutType = None, 323 | ) -> spec.Basic.CancelOk: 324 | raise NotImplementedError 325 | 326 | @abstractmethod 327 | async def basic_consume( 328 | self, 329 | queue: str, 330 | consumer_callback: ConsumerCallback, 331 | *, 332 | no_ack: bool = False, 333 | exclusive: bool = False, 334 | arguments: Optional[ArgumentsType] = None, 335 | consumer_tag: Optional[str] = None, 336 | timeout: TimeoutType = None, 337 | ) -> spec.Basic.ConsumeOk: 338 | raise NotImplementedError 339 | 340 | @abstractmethod 341 | def basic_ack( 342 | self, delivery_tag: int, multiple: bool = False, wait: bool = True, 343 | ) -> DrainResult: 344 | raise NotImplementedError 345 | 346 | @abstractmethod 347 | def basic_nack( 348 | self, 349 | delivery_tag: int, 350 | multiple: bool = False, 351 | requeue: bool = True, 352 | wait: bool = True, 353 | ) -> DrainResult: 354 | raise NotImplementedError 355 | 356 | @abstractmethod 357 | def basic_reject( 358 | self, delivery_tag: int, *, requeue: bool = True, wait: bool = True, 359 | ) -> DrainResult: 360 | raise NotImplementedError 361 | 362 | @abstractmethod 363 | async def basic_publish( 364 | self, 365 | body: bytes, 366 | *, 367 | exchange: str = "", 368 | routing_key: str = "", 369 | properties: Optional[spec.Basic.Properties] = None, 370 | mandatory: bool = False, 371 | immediate: bool = False, 372 | timeout: TimeoutType = None, 373 | ) -> Optional[ConfirmationFrameType]: 374 | raise NotImplementedError 375 | 376 | @abstractmethod 377 | async def basic_qos( 378 | self, 379 | *, 380 | prefetch_size: Optional[int] = None, 381 | prefetch_count: Optional[int] = None, 382 | global_: bool = False, 383 | timeout: TimeoutType = None, 384 | ) -> spec.Basic.QosOk: 385 | raise NotImplementedError 386 | 387 | @abstractmethod 388 | async def basic_recover( 389 | self, *, nowait: bool = False, requeue: bool = False, 390 | timeout: TimeoutType = None, 391 | ) -> spec.Basic.RecoverOk: 392 | raise NotImplementedError 393 | 394 | @abstractmethod 395 | async def exchange_declare( 396 | self, 397 | exchange: str = "", 398 | *, 399 | exchange_type: str = "direct", 400 | passive: bool = False, 401 | durable: bool = False, 402 | auto_delete: bool = False, 403 | internal: bool = False, 404 | nowait: bool = False, 405 | arguments: Optional[ArgumentsType] = None, 406 | timeout: TimeoutType = None, 407 | ) -> spec.Exchange.DeclareOk: 408 | raise NotImplementedError 409 | 410 | @abstractmethod 411 | async def exchange_delete( 412 | self, 413 | exchange: str = "", 414 | *, 415 | if_unused: bool = False, 416 | nowait: bool = False, 417 | timeout: TimeoutType = None, 418 | ) -> spec.Exchange.DeleteOk: 419 | raise NotImplementedError 420 | 421 | @abstractmethod 422 | async def exchange_bind( 423 | self, 424 | destination: str = "", 425 | source: str = "", 426 | routing_key: str = "", 427 | *, 428 | nowait: bool = False, 429 | arguments: Optional[ArgumentsType] = None, 430 | timeout: TimeoutType = None, 431 | ) -> spec.Exchange.BindOk: 432 | raise NotImplementedError 433 | 434 | @abstractmethod 435 | async def exchange_unbind( 436 | self, 437 | destination: str = "", 438 | source: str = "", 439 | routing_key: str = "", 440 | *, 441 | nowait: bool = False, 442 | arguments: Optional[ArgumentsType] = None, 443 | timeout: TimeoutType = None, 444 | ) -> spec.Exchange.UnbindOk: 445 | raise NotImplementedError 446 | 447 | @abstractmethod 448 | async def flow( 449 | self, active: bool, 450 | timeout: TimeoutType = None, 451 | ) -> spec.Channel.FlowOk: 452 | raise NotImplementedError 453 | 454 | @abstractmethod 455 | async def queue_bind( 456 | self, 457 | queue: str, 458 | exchange: str, 459 | routing_key: str = "", 460 | nowait: bool = False, 461 | arguments: Optional[ArgumentsType] = None, 462 | timeout: TimeoutType = None, 463 | ) -> spec.Queue.BindOk: 464 | raise NotImplementedError 465 | 466 | @abstractmethod 467 | async def queue_declare( 468 | self, 469 | queue: str = "", 470 | *, 471 | passive: bool = False, 472 | durable: bool = False, 473 | exclusive: bool = False, 474 | auto_delete: bool = False, 475 | nowait: bool = False, 476 | arguments: Optional[ArgumentsType] = None, 477 | timeout: TimeoutType = None, 478 | ) -> spec.Queue.DeclareOk: 479 | raise NotImplementedError 480 | 481 | @abstractmethod 482 | async def queue_delete( 483 | self, 484 | queue: str = "", 485 | if_unused: bool = False, 486 | if_empty: bool = False, 487 | nowait: bool = False, 488 | timeout: TimeoutType = None, 489 | ) -> spec.Queue.DeleteOk: 490 | raise NotImplementedError 491 | 492 | @abstractmethod 493 | async def queue_purge( 494 | self, queue: str = "", nowait: bool = False, 495 | timeout: TimeoutType = None, 496 | ) -> spec.Queue.PurgeOk: 497 | raise NotImplementedError 498 | 499 | @abstractmethod 500 | async def queue_unbind( 501 | self, 502 | queue: str = "", 503 | exchange: str = "", 504 | routing_key: str = "", 505 | arguments: Optional[ArgumentsType] = None, 506 | timeout: TimeoutType = None, 507 | ) -> spec.Queue.UnbindOk: 508 | raise NotImplementedError 509 | 510 | @abstractmethod 511 | async def tx_commit( 512 | self, timeout: TimeoutType = None, 513 | ) -> spec.Tx.CommitOk: 514 | raise NotImplementedError 515 | 516 | @abstractmethod 517 | async def tx_rollback( 518 | self, timeout: TimeoutType = None, 519 | ) -> spec.Tx.RollbackOk: 520 | raise NotImplementedError 521 | 522 | @abstractmethod 523 | async def tx_select(self, timeout: TimeoutType = None) -> spec.Tx.SelectOk: 524 | raise NotImplementedError 525 | 526 | @abstractmethod 527 | async def confirm_delivery( 528 | self, nowait: bool = False, 529 | timeout: TimeoutType = None, 530 | ) -> spec.Confirm.SelectOk: 531 | raise NotImplementedError 532 | 533 | 534 | class AbstractConnection(AbstractBase): 535 | FRAME_BUFFER_SIZE: int = 10 536 | # Interval between sending heartbeats based on the heartbeat(timeout) 537 | HEARTBEAT_INTERVAL_MULTIPLIER: TimeoutType 538 | 539 | # Allow three missed heartbeats (based on heartbeat(timeout) 540 | HEARTBEAT_GRACE_MULTIPLIER: int 541 | 542 | server_properties: ArgumentsType 543 | connection_tune: spec.Connection.Tune 544 | channels: Dict[int, Optional[AbstractChannel]] 545 | write_queue: asyncio.Queue 546 | url: URL 547 | closing: asyncio.Future 548 | 549 | @abstractmethod 550 | def set_close_reason( 551 | self, reply_code: int = REPLY_SUCCESS, 552 | reply_text: str = "normally closed", 553 | class_id: int = 0, method_id: int = 0, 554 | ) -> None: 555 | raise NotImplementedError 556 | 557 | @abstractproperty 558 | def is_opened(self) -> bool: 559 | raise NotImplementedError 560 | 561 | @abstractmethod 562 | def __str__(self) -> str: 563 | raise NotImplementedError 564 | 565 | @abstractmethod 566 | async def connect( 567 | self, client_properties: Optional[FieldTable] = None, 568 | ) -> bool: 569 | raise NotImplementedError 570 | 571 | @abstractproperty 572 | def server_capabilities(self) -> ArgumentsType: 573 | raise NotImplementedError 574 | 575 | @abstractproperty 576 | def basic_nack(self) -> bool: 577 | raise NotImplementedError 578 | 579 | @abstractproperty 580 | def consumer_cancel_notify(self) -> bool: 581 | raise NotImplementedError 582 | 583 | @abstractproperty 584 | def exchange_exchange_bindings(self) -> bool: 585 | raise NotImplementedError 586 | 587 | @abstractproperty 588 | def publisher_confirms(self) -> Optional[bool]: 589 | raise NotImplementedError 590 | 591 | async def channel( 592 | self, 593 | channel_number: Optional[int] = None, 594 | publisher_confirms: bool = True, 595 | frame_buffer_size: int = FRAME_BUFFER_SIZE, 596 | timeout: TimeoutType = None, 597 | **kwargs: Any, 598 | ) -> AbstractChannel: 599 | raise NotImplementedError 600 | 601 | @abstractmethod 602 | async def __aenter__(self) -> "AbstractConnection": 603 | raise NotImplementedError 604 | 605 | @abstractmethod 606 | async def __aexit__( 607 | self, 608 | exc_type: Optional[Type[BaseException]], 609 | exc_val: Optional[BaseException], 610 | exc_tb: Optional[TracebackType], 611 | ) -> Optional[bool]: 612 | raise NotImplementedError 613 | 614 | @abstractmethod 615 | async def ready(self) -> None: 616 | raise NotImplementedError 617 | 618 | @abstractmethod 619 | async def update_secret( 620 | self, new_secret: str, *, 621 | reason: str = "", timeout: TimeoutType = None, 622 | ) -> spec.Connection.UpdateSecretOk: 623 | raise NotImplementedError 624 | 625 | 626 | __all__ = ( 627 | "AbstractBase", "AbstractChannel", "AbstractConnection", 628 | "AbstractFutureStore", "ArgumentsType", "CallbackCoro", "ChannelFrame", 629 | "ChannelRType", "ConfirmationFrameType", "ConsumerCallback", 630 | "CoroutineType", "DeliveredMessage", "DrainResult", "ExceptionType", 631 | "FieldArray", "FieldTable", "FieldValue", "FrameReceived", "FrameType", 632 | "GetResultType", "ReturnCallback", "RpcReturnType", "SSLCerts", 633 | "TaskType", "TaskWrapper", "TimeoutType", "URLorStr", 634 | ) 635 | -------------------------------------------------------------------------------- /aiormq/auth.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from enum import Enum 3 | from typing import Optional 4 | 5 | from .abc import AbstractConnection 6 | 7 | 8 | class AuthBase: 9 | value: Optional[str] 10 | 11 | def __init__(self, connector: AbstractConnection): 12 | self.connector = connector 13 | self.value = None 14 | 15 | @abc.abstractmethod 16 | def encode(self) -> str: 17 | raise NotImplementedError 18 | 19 | def marshal(self) -> str: 20 | if self.value is None: 21 | self.value = self.encode() 22 | return self.value 23 | 24 | 25 | class PlainAuth(AuthBase): 26 | def encode(self) -> str: 27 | return ( 28 | "\x00" 29 | + (self.connector.url.user or "guest") 30 | + "\x00" 31 | + (self.connector.url.password or "guest") 32 | ) 33 | 34 | 35 | class ExternalAuth(AuthBase): 36 | def encode(self) -> str: 37 | return "" 38 | 39 | 40 | class AuthMechanism(Enum): 41 | PLAIN = PlainAuth 42 | EXTERNAL = ExternalAuth 43 | -------------------------------------------------------------------------------- /aiormq/base.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import asyncio 3 | from contextlib import suppress 4 | from functools import wraps 5 | from typing import Any, Callable, Coroutine, Optional, Set, TypeVar, Union 6 | from weakref import WeakSet 7 | 8 | from .abc import ( 9 | AbstractBase, AbstractFutureStore, CoroutineType, ExceptionType, TaskType, 10 | TaskWrapper, TimeoutType, 11 | ) 12 | from .tools import Countdown, shield 13 | 14 | 15 | T = TypeVar("T") 16 | 17 | 18 | class FutureStore(AbstractFutureStore): 19 | __slots__ = "futures", "loop", "parent" 20 | 21 | futures: Set[Union[asyncio.Future, TaskType]] 22 | weak_futures: WeakSet 23 | loop: asyncio.AbstractEventLoop 24 | 25 | def __init__(self, loop: asyncio.AbstractEventLoop): 26 | self.futures = set() 27 | self.loop = loop 28 | self.parent: Optional[FutureStore] = None 29 | 30 | def __on_task_done( 31 | self, future: Union[asyncio.Future, TaskWrapper], 32 | ) -> Callable[..., Any]: 33 | def remover(*_: Any) -> None: 34 | nonlocal future 35 | if future in self.futures: 36 | self.futures.remove(future) 37 | 38 | return remover 39 | 40 | def add(self, future: Union[asyncio.Future, TaskWrapper]) -> None: 41 | self.futures.add(future) 42 | future.add_done_callback(self.__on_task_done(future)) 43 | 44 | if self.parent: 45 | self.parent.add(future) 46 | 47 | @shield 48 | async def reject_all(self, exception: Optional[ExceptionType]) -> None: 49 | tasks = [] 50 | 51 | while self.futures: 52 | future: Union[TaskType, asyncio.Future] = self.futures.pop() 53 | 54 | if future.done(): 55 | continue 56 | 57 | if isinstance(future, TaskWrapper): 58 | future.throw(exception or Exception) 59 | tasks.append(future) 60 | elif isinstance(future, asyncio.Future): 61 | future.set_exception(exception or Exception) 62 | 63 | if tasks: 64 | await asyncio.gather(*tasks, return_exceptions=True) 65 | 66 | def create_task(self, coro: CoroutineType) -> TaskType: 67 | task: TaskWrapper = TaskWrapper(self.loop.create_task(coro)) 68 | self.add(task) 69 | return task 70 | 71 | def create_future(self, weak: bool = False) -> asyncio.Future: 72 | future = self.loop.create_future() 73 | self.add(future) 74 | return future 75 | 76 | def get_child(self) -> "FutureStore": 77 | store = FutureStore(self.loop) 78 | store.parent = self 79 | return store 80 | 81 | 82 | class Base(AbstractBase): 83 | __slots__ = "loop", "__future_store", "closing" 84 | 85 | def __init__( 86 | self, *, loop: asyncio.AbstractEventLoop, 87 | parent: Optional[AbstractBase] = None, 88 | ): 89 | self.loop: asyncio.AbstractEventLoop = loop 90 | 91 | if parent: 92 | self.__future_store = parent._future_store_child() 93 | else: 94 | self.__future_store = FutureStore(loop=self.loop) 95 | 96 | self.closing = self._create_closing_future() 97 | 98 | def _create_closing_future(self) -> asyncio.Future: 99 | future = self.__future_store.create_future() 100 | future.add_done_callback(lambda x: x.exception()) 101 | return future 102 | 103 | def _cancel_tasks( 104 | self, exc: Optional[ExceptionType] = None, 105 | ) -> Coroutine[Any, Any, None]: 106 | return self.__future_store.reject_all(exc) 107 | 108 | def _future_store_child(self) -> AbstractFutureStore: 109 | return self.__future_store.get_child() 110 | 111 | def create_task(self, coro: CoroutineType) -> TaskType: 112 | return self.__future_store.create_task(coro) 113 | 114 | def create_future(self) -> asyncio.Future: 115 | return self.__future_store.create_future() 116 | 117 | @abc.abstractmethod 118 | async def _on_close( 119 | self, exc: Optional[ExceptionType] = None, 120 | ) -> None: # pragma: no cover 121 | return 122 | 123 | async def __closer(self, exc: Optional[ExceptionType]) -> None: 124 | if self.is_closed: # pragma: no cover 125 | return 126 | 127 | with suppress(Exception): 128 | await self._on_close(exc) 129 | 130 | with suppress(Exception): 131 | await self._cancel_tasks(exc) 132 | 133 | async def close( 134 | self, exc: Optional[ExceptionType] = asyncio.CancelledError, 135 | timeout: TimeoutType = None, 136 | ) -> None: 137 | if self.is_closed: 138 | return None 139 | 140 | countdown = Countdown(timeout) 141 | await countdown(self.__closer(exc)) 142 | 143 | def __repr__(self) -> str: 144 | cls_name = self.__class__.__name__ 145 | return '<{0}: "{1}" at 0x{2:02x}>'.format( 146 | cls_name, str(self), id(self), 147 | ) 148 | 149 | @abc.abstractmethod 150 | def __str__(self) -> str: # pragma: no cover 151 | raise NotImplementedError 152 | 153 | @property 154 | def is_closed(self) -> bool: 155 | return self.closing.done() 156 | 157 | 158 | TaskFunctionType = Callable[..., T] 159 | 160 | 161 | def task(func: TaskFunctionType) -> TaskFunctionType: 162 | @wraps(func) 163 | async def wrap(self: Base, *args: Any, **kwargs: Any) -> Any: 164 | return await self.create_task(func(self, *args, **kwargs)) 165 | 166 | return wrap 167 | -------------------------------------------------------------------------------- /aiormq/channel.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import io 3 | import logging 4 | from collections import OrderedDict 5 | from contextlib import suppress 6 | from functools import partial 7 | from io import BytesIO 8 | from random import getrandbits 9 | from types import MappingProxyType 10 | from typing import ( 11 | Any, Awaitable, Callable, Dict, List, Mapping, Optional, Set, Tuple, Type, 12 | Union, 13 | ) 14 | from uuid import UUID 15 | 16 | import pamqp.frame 17 | from pamqp import commands as spec 18 | from pamqp.base import Frame 19 | from pamqp.body import ContentBody 20 | from pamqp.constants import REPLY_SUCCESS 21 | from pamqp.exceptions import AMQPFrameError 22 | from pamqp.header import ContentHeader 23 | 24 | from aiormq.tools import Countdown, awaitable 25 | 26 | from .abc import ( 27 | AbstractChannel, AbstractConnection, ArgumentsType, ChannelFrame, 28 | ConfirmationFrameType, ConsumerCallback, DeliveredMessage, ExceptionType, 29 | FrameType, GetResultType, ReturnCallback, RpcReturnType, TimeoutType, 30 | ) 31 | from .base import Base, task 32 | from .exceptions import ( 33 | AMQPChannelError, AMQPError, ChannelAccessRefused, ChannelClosed, 34 | ChannelInvalidStateError, ChannelLockedResource, ChannelNotFoundEntity, 35 | ChannelPreconditionFailed, DeliveryError, DuplicateConsumerTag, 36 | InvalidFrameError, MethodNotImplemented, PublishError, 37 | ) 38 | 39 | 40 | log = logging.getLogger(__name__) 41 | 42 | 43 | EXCEPTION_MAPPING: Mapping[int, Type[AMQPChannelError]] = MappingProxyType({ 44 | 403: ChannelAccessRefused, 45 | 404: ChannelNotFoundEntity, 46 | 405: ChannelLockedResource, 47 | 406: ChannelPreconditionFailed, 48 | }) 49 | 50 | 51 | def exception_by_code(frame: spec.Channel.Close) -> AMQPError: 52 | if frame.reply_code is None: 53 | return ChannelClosed(frame.reply_code, frame.reply_text) 54 | 55 | exception_class = EXCEPTION_MAPPING.get(frame.reply_code) 56 | 57 | if exception_class is None: 58 | return ChannelClosed(frame.reply_code, frame.reply_text) 59 | 60 | return exception_class(frame.reply_text) 61 | 62 | 63 | def _check_routing_key(key: str) -> None: 64 | if len(key) > 255: 65 | raise ValueError("Routing key too long (max 255 bytes)") 66 | 67 | 68 | class Returning(asyncio.Future): 69 | pass 70 | 71 | 72 | ConfirmationType = Union[asyncio.Future, Returning] 73 | 74 | 75 | class Channel(Base, AbstractChannel): 76 | # noinspection PyTypeChecker 77 | CONTENT_FRAME_SIZE = len(pamqp.frame.marshal(ContentBody(b""), 0)) 78 | CHANNEL_CLOSE_TIMEOUT = 10 79 | confirmations: Dict[int, ConfirmationType] 80 | 81 | def __init__( 82 | self, 83 | connector: AbstractConnection, 84 | number: int, 85 | publisher_confirms: bool = True, 86 | frame_buffer: Optional[int] = None, 87 | on_return_raises: bool = True, 88 | ): 89 | 90 | super().__init__(loop=connector.loop, parent=connector) 91 | 92 | self.connection = connector 93 | 94 | if ( 95 | publisher_confirms and not connector.publisher_confirms 96 | ): # pragma: no cover 97 | raise ValueError("Server doesn't support publisher confirms") 98 | 99 | self.consumers: Dict[str, ConsumerCallback] = {} 100 | self.confirmations = OrderedDict() 101 | self.message_id_delivery_tag: Dict[str, int] = dict() 102 | 103 | self.delivery_tag = 0 104 | 105 | self.getter: Optional[asyncio.Future] = None 106 | self.getter_lock = asyncio.Lock() 107 | 108 | self.frames: asyncio.Queue = asyncio.Queue(maxsize=frame_buffer or 0) 109 | 110 | self.max_content_size = ( 111 | connector.connection_tune.frame_max - self.CONTENT_FRAME_SIZE 112 | ) 113 | 114 | self.__lock = asyncio.Lock() 115 | self.number: int = number 116 | self.publisher_confirms = publisher_confirms 117 | self.rpc_frames: asyncio.Queue = asyncio.Queue( 118 | maxsize=frame_buffer or 0, 119 | ) 120 | self.write_queue = connector.write_queue 121 | self.on_return_raises = on_return_raises 122 | self.on_return_callbacks: Set[ReturnCallback] = set() 123 | self._close_exception = None 124 | 125 | self.create_task(self._reader()) 126 | 127 | self.__close_reply_code: int = REPLY_SUCCESS 128 | self.__close_reply_text: str = "" 129 | self.__close_class_id: int = 0 130 | self.__close_method_id: int = 0 131 | self.__close_event: asyncio.Event = asyncio.Event() 132 | 133 | def set_close_reason( 134 | self, reply_code: int = REPLY_SUCCESS, 135 | reply_text: str = "", class_id: int = 0, method_id: int = 0, 136 | ) -> None: 137 | self.__close_reply_code = reply_code 138 | self.__close_reply_text = reply_text 139 | self.__close_class_id = class_id 140 | self.__close_method_id = method_id 141 | 142 | @property 143 | def lock(self) -> asyncio.Lock: 144 | if self.is_closed: 145 | raise ChannelInvalidStateError("%r closed" % self) 146 | 147 | return self.__lock 148 | 149 | async def _get_frame(self) -> FrameType: 150 | weight, frame = await self.frames.get() 151 | self.frames.task_done() 152 | return frame 153 | 154 | def __str__(self) -> str: 155 | return str(self.number) 156 | 157 | @task 158 | async def rpc( 159 | self, frame: Frame, timeout: TimeoutType = None, 160 | ) -> RpcReturnType: 161 | 162 | if self.__close_event.is_set(): 163 | raise ChannelInvalidStateError("Channel closed by RPC timeout") 164 | 165 | countdown = Countdown(timeout) 166 | lock = self.lock 167 | 168 | async with countdown.enter_context(lock): 169 | try: 170 | await countdown( 171 | self.write_queue.put( 172 | ChannelFrame.marshall( 173 | channel_number=self.number, 174 | frames=[frame], 175 | ), 176 | ), 177 | ) 178 | 179 | if not (frame.synchronous or getattr(frame, "nowait", False)): 180 | return None 181 | 182 | result = await countdown(self.rpc_frames.get()) 183 | 184 | self.rpc_frames.task_done() 185 | 186 | if result.name not in frame.valid_responses: # pragma: no cover 187 | raise InvalidFrameError(frame) 188 | 189 | return result 190 | except (asyncio.CancelledError, asyncio.TimeoutError): 191 | if self.is_closed: 192 | raise 193 | 194 | log.warning( 195 | "Closing channel %r because RPC call %s cancelled", 196 | self, frame, 197 | ) 198 | 199 | self.__close_event.set() 200 | await self.write_queue.put( 201 | ChannelFrame.marshall( 202 | channel_number=self.number, 203 | frames=[ 204 | spec.Channel.Close( 205 | class_id=0, 206 | method_id=0, 207 | reply_code=504, 208 | reply_text=( 209 | "RPC timeout on frame {!s}".format(frame) 210 | ), 211 | ), 212 | ], 213 | ), 214 | ) 215 | 216 | raise 217 | 218 | async def open(self, timeout: TimeoutType = None) -> spec.Channel.OpenOk: 219 | frame: spec.Channel.OpenOk = await self.rpc( 220 | spec.Channel.Open(), timeout=timeout, 221 | ) 222 | 223 | if self.publisher_confirms: 224 | await self.rpc(spec.Confirm.Select()) 225 | 226 | if frame is None: # pragma: no cover 227 | raise AMQPFrameError(frame) 228 | return frame 229 | 230 | async def __get_content_frame(self) -> ContentBody: 231 | content_frame = await self._get_frame() 232 | if not isinstance(content_frame, ContentBody): 233 | raise ValueError( 234 | "Unexpected frame {!r}".format(content_frame), 235 | content_frame, 236 | ) 237 | return content_frame 238 | 239 | async def _read_content( 240 | self, 241 | frame: Union[spec.Basic.Deliver, spec.Basic.Return, GetResultType], 242 | header: ContentHeader, 243 | ) -> DeliveredMessage: 244 | with BytesIO() as body: 245 | content: Optional[ContentBody] = None 246 | 247 | if header.body_size: 248 | content = await self.__get_content_frame() 249 | 250 | while content and body.tell() < header.body_size: 251 | body.write(content.value) 252 | 253 | if body.tell() < header.body_size: 254 | content = await self.__get_content_frame() 255 | 256 | return DeliveredMessage( 257 | delivery=frame, 258 | header=header, 259 | body=body.getvalue(), 260 | channel=self, 261 | ) 262 | 263 | async def __get_content_header(self) -> ContentHeader: 264 | frame: FrameType = await self._get_frame() 265 | 266 | if not isinstance(frame, ContentHeader): 267 | raise ValueError( 268 | "Unexpected frame {!r} instead of ContentHeader".format(frame), 269 | frame, 270 | ) 271 | 272 | return frame 273 | 274 | async def _on_deliver_frame(self, frame: spec.Basic.Deliver) -> None: 275 | header: ContentHeader = await self.__get_content_header() 276 | message = await self._read_content(frame, header) 277 | 278 | if frame.consumer_tag is None: 279 | log.warning("Frame %r has no consumer tag", frame) 280 | return 281 | 282 | consumer = self.consumers.get(frame.consumer_tag) 283 | if consumer is not None: 284 | # noinspection PyAsyncCall 285 | self.create_task(consumer(message)) 286 | 287 | async def _on_get_frame( 288 | self, frame: Union[spec.Basic.GetOk, spec.Basic.GetEmpty], 289 | ) -> None: 290 | message = None 291 | if isinstance(frame, spec.Basic.GetOk): 292 | header: ContentHeader = await self.__get_content_header() 293 | message = await self._read_content(frame, header) 294 | if isinstance(frame, spec.Basic.GetEmpty): 295 | message = DeliveredMessage( 296 | delivery=frame, 297 | header=ContentHeader(), 298 | body=b"", 299 | channel=self, 300 | ) 301 | 302 | getter = getattr(self, "getter", None) 303 | 304 | if getter is None: 305 | raise RuntimeError("Getter is None") 306 | 307 | if getter.done(): 308 | log.error("Got message but no active getter") 309 | return 310 | 311 | getter.set_result((frame, message)) 312 | return 313 | 314 | async def _on_return_frame(self, frame: spec.Basic.Return) -> None: 315 | header: ContentHeader = await self.__get_content_header() 316 | message = await self._read_content(frame, header) 317 | message_id = message.header.properties.message_id 318 | 319 | if message_id is None: 320 | log.error("message_id if None on returned message %r", message) 321 | return 322 | 323 | delivery_tag = self.message_id_delivery_tag.get(message_id) 324 | 325 | if delivery_tag is None: # pragma: nocover 326 | log.error("Unhandled message %r returning", message) 327 | return 328 | 329 | confirmation = self.confirmations.pop(delivery_tag, None) 330 | if confirmation is None: # pragma: nocover 331 | return 332 | 333 | self.confirmations[delivery_tag] = Returning() 334 | 335 | if self.on_return_raises: 336 | confirmation.set_exception(PublishError(message, frame)) 337 | return 338 | 339 | for cb in self.on_return_callbacks: 340 | # noinspection PyBroadException 341 | try: 342 | cb(message) 343 | except Exception: 344 | log.exception("Unhandled return callback exception") 345 | 346 | confirmation.set_result(message) 347 | 348 | def _confirm_delivery( 349 | self, delivery_tag: Optional[int], 350 | frame: ConfirmationFrameType, 351 | ) -> None: 352 | if delivery_tag not in self.confirmations: 353 | return 354 | 355 | confirmation = self.confirmations.pop(delivery_tag) 356 | 357 | if isinstance(confirmation, Returning): 358 | return 359 | elif confirmation.done(): # pragma: nocover 360 | log.warning( 361 | "Delivery tag %r confirmed %r was ignored", delivery_tag, frame, 362 | ) 363 | return 364 | elif isinstance(frame, spec.Basic.Ack): 365 | confirmation.set_result(frame) 366 | return 367 | 368 | confirmation.set_exception( 369 | DeliveryError(None, frame), 370 | ) # pragma: nocover 371 | 372 | async def _on_confirm_frame(self, frame: ConfirmationFrameType) -> None: 373 | if not self.publisher_confirms: # pragma: nocover 374 | return 375 | 376 | if frame.delivery_tag not in self.confirmations: 377 | log.error("Unexpected confirmation frame %r from broker", frame) 378 | return 379 | 380 | multiple = getattr(frame, "multiple", False) 381 | 382 | if multiple: 383 | for delivery_tag in self.confirmations.keys(): 384 | if frame.delivery_tag >= delivery_tag: 385 | # Should be called later to avoid keys copying 386 | self.loop.call_soon( 387 | self._confirm_delivery, delivery_tag, frame, 388 | ) 389 | else: 390 | self._confirm_delivery(frame.delivery_tag, frame) 391 | 392 | async def _on_cancel_frame( 393 | self, 394 | frame: Union[spec.Basic.CancelOk, spec.Basic.Cancel], 395 | ) -> None: 396 | if frame.consumer_tag is not None: 397 | self.consumers.pop(frame.consumer_tag, None) 398 | 399 | async def _on_close_frame(self, frame: spec.Channel.Close) -> None: 400 | exc: BaseException = exception_by_code(frame) 401 | with suppress(asyncio.QueueFull): 402 | self.write_queue.put_nowait( 403 | ChannelFrame.marshall( 404 | channel_number=self.number, 405 | frames=[spec.Channel.CloseOk()], 406 | ), 407 | ) 408 | self.connection.channels.pop(self.number, None) 409 | self.__close_event.set() 410 | raise exc 411 | 412 | async def _on_close_ok_frame(self, _: spec.Channel.CloseOk) -> None: 413 | self.connection.channels.pop(self.number, None) 414 | self.__close_event.set() 415 | raise ChannelClosed(None, None) 416 | 417 | async def _reader(self) -> None: 418 | hooks: Mapping[Any, Tuple[bool, Callable[[Any], Awaitable[None]]]] 419 | 420 | hooks = { 421 | spec.Basic.Deliver: (False, self._on_deliver_frame), 422 | spec.Basic.GetOk: (True, self._on_get_frame), 423 | spec.Basic.GetEmpty: (True, self._on_get_frame), 424 | spec.Basic.Return: (False, self._on_return_frame), 425 | spec.Basic.Cancel: (False, self._on_cancel_frame), 426 | spec.Basic.CancelOk: (True, self._on_cancel_frame), 427 | spec.Channel.Close: (False, self._on_close_frame), 428 | spec.Channel.CloseOk: (False, self._on_close_ok_frame), 429 | spec.Basic.Ack: (False, self._on_confirm_frame), 430 | spec.Basic.Nack: (False, self._on_confirm_frame), 431 | } 432 | 433 | last_exception: Optional[BaseException] = None 434 | 435 | try: 436 | while True: 437 | frame = await self._get_frame() 438 | should_add_to_rpc, hook = hooks.get(type(frame), (True, None)) 439 | 440 | if hook is not None: 441 | await hook(frame) 442 | 443 | if should_add_to_rpc: 444 | await self.rpc_frames.put(frame) 445 | except asyncio.CancelledError as e: 446 | self.__close_event.set() 447 | last_exception = e 448 | return 449 | except Exception as e: 450 | last_exception = e 451 | raise 452 | finally: 453 | await self.close( 454 | last_exception, timeout=self.CHANNEL_CLOSE_TIMEOUT, 455 | ) 456 | 457 | @task 458 | async def _on_close(self, exc: Optional[ExceptionType] = None) -> None: 459 | if not self.connection.is_opened or self.__close_event.is_set(): 460 | return 461 | 462 | await self.rpc( 463 | spec.Channel.Close( 464 | reply_code=self.__close_reply_code, 465 | class_id=self.__close_class_id, 466 | method_id=self.__close_method_id, 467 | ), 468 | timeout=self.connection.connection_tune.heartbeat or None, 469 | ) 470 | 471 | await self.__close_event.wait() 472 | 473 | async def basic_get( 474 | self, queue: str = "", no_ack: bool = False, 475 | timeout: TimeoutType = None, 476 | ) -> DeliveredMessage: 477 | 478 | countdown = Countdown(timeout) 479 | async with countdown.enter_context(self.getter_lock): 480 | self.getter = self.create_future() 481 | 482 | await self.rpc( 483 | spec.Basic.Get(queue=queue, no_ack=no_ack), 484 | timeout=countdown.get_timeout(), 485 | ) 486 | 487 | frame: Union[spec.Basic.GetEmpty, spec.Basic.GetOk] 488 | message: DeliveredMessage 489 | 490 | frame, message = await countdown(self.getter) 491 | del self.getter 492 | 493 | return message 494 | 495 | async def basic_cancel( 496 | self, consumer_tag: str, *, nowait: bool = False, 497 | timeout: TimeoutType = None, 498 | ) -> spec.Basic.CancelOk: 499 | return await self.rpc( 500 | spec.Basic.Cancel(consumer_tag=consumer_tag, nowait=nowait), 501 | timeout=timeout, 502 | ) 503 | 504 | async def basic_consume( 505 | self, 506 | queue: str, 507 | consumer_callback: ConsumerCallback, 508 | *, 509 | no_ack: bool = False, 510 | exclusive: bool = False, 511 | arguments: Optional[ArgumentsType] = None, 512 | consumer_tag: Optional[str] = None, 513 | timeout: TimeoutType = None, 514 | ) -> spec.Basic.ConsumeOk: 515 | 516 | consumer_tag = consumer_tag or "ctag%i.%s" % ( 517 | self.number, 518 | UUID(int=getrandbits(128), version=4).hex, 519 | ) 520 | 521 | if consumer_tag in self.consumers: 522 | raise DuplicateConsumerTag(self.number) 523 | 524 | self.consumers[consumer_tag] = awaitable(consumer_callback) 525 | 526 | return await self.rpc( 527 | spec.Basic.Consume( 528 | queue=queue, 529 | no_ack=no_ack, 530 | exclusive=exclusive, 531 | consumer_tag=consumer_tag, 532 | arguments=arguments, 533 | ), 534 | timeout=timeout, 535 | ) 536 | 537 | async def basic_ack( 538 | self, delivery_tag: int, multiple: bool = False, wait: bool = True, 539 | ) -> None: 540 | drain_future = self.create_future() if wait else None 541 | 542 | await self.write_queue.put( 543 | ChannelFrame.marshall( 544 | frames=[ 545 | spec.Basic.Ack( 546 | delivery_tag=delivery_tag, 547 | multiple=multiple, 548 | ), 549 | ], 550 | channel_number=self.number, 551 | drain_future=drain_future, 552 | ), 553 | ) 554 | 555 | if drain_future is not None: 556 | await drain_future 557 | 558 | async def basic_nack( 559 | self, 560 | delivery_tag: int, 561 | multiple: bool = False, 562 | requeue: bool = True, 563 | wait: bool = True, 564 | ) -> None: 565 | if not self.connection.basic_nack: 566 | raise MethodNotImplemented 567 | 568 | drain_future = self.create_future() if wait else None 569 | 570 | await self.write_queue.put( 571 | ChannelFrame.marshall( 572 | frames=[ 573 | spec.Basic.Nack( 574 | delivery_tag=delivery_tag, 575 | multiple=multiple, 576 | requeue=requeue, 577 | ), 578 | ], 579 | channel_number=self.number, 580 | drain_future=drain_future, 581 | ), 582 | ) 583 | 584 | if drain_future is not None: 585 | await drain_future 586 | 587 | async def basic_reject( 588 | self, delivery_tag: int, *, requeue: bool = True, wait: bool = True, 589 | ) -> None: 590 | drain_future = self.create_future() 591 | await self.write_queue.put( 592 | ChannelFrame.marshall( 593 | channel_number=self.number, 594 | frames=[ 595 | spec.Basic.Reject( 596 | delivery_tag=delivery_tag, 597 | requeue=requeue, 598 | ), 599 | ], 600 | drain_future=drain_future, 601 | ), 602 | ) 603 | 604 | if drain_future is not None: 605 | await drain_future 606 | 607 | def _split_body(self, body: bytes) -> List[ContentBody]: 608 | if not body: 609 | return [] 610 | 611 | if len(body) < self.max_content_size: 612 | return [ContentBody(body)] 613 | 614 | with io.BytesIO(body) as fp: 615 | reader = partial(fp.read, self.max_content_size) 616 | return list(map(ContentBody, iter(reader, b""))) 617 | 618 | async def basic_publish( 619 | self, 620 | body: bytes, 621 | *, 622 | exchange: str = "", 623 | routing_key: str = "", 624 | properties: Optional[spec.Basic.Properties] = None, 625 | mandatory: bool = False, 626 | immediate: bool = False, 627 | timeout: TimeoutType = None, 628 | wait: bool = True, 629 | ) -> Optional[ConfirmationFrameType]: 630 | _check_routing_key(routing_key) 631 | countdown = Countdown(timeout=timeout) 632 | 633 | publish_frame = spec.Basic.Publish( 634 | exchange=exchange, 635 | routing_key=routing_key, 636 | mandatory=mandatory, 637 | immediate=immediate, 638 | ) 639 | 640 | content_header = ContentHeader( 641 | properties=properties or spec.Basic.Properties(delivery_mode=1), 642 | body_size=len(body), 643 | ) 644 | 645 | if not content_header.properties.message_id: 646 | # UUID compatible random bytes 647 | rnd_uuid = UUID(int=getrandbits(128), version=4) 648 | content_header.properties.message_id = rnd_uuid.hex 649 | 650 | confirmation: Optional[ConfirmationType] = None 651 | 652 | async with countdown.enter_context(self.lock): 653 | self.delivery_tag += 1 654 | 655 | if self.publisher_confirms: 656 | message_id = content_header.properties.message_id 657 | 658 | if self.delivery_tag not in self.confirmations: 659 | self.confirmations[ 660 | self.delivery_tag 661 | ] = self.create_future() 662 | 663 | confirmation = self.confirmations[self.delivery_tag] 664 | self.message_id_delivery_tag[message_id] = self.delivery_tag 665 | 666 | if confirmation is None: 667 | return 668 | 669 | confirmation.add_done_callback( 670 | lambda _: self.message_id_delivery_tag.pop( 671 | message_id, None, 672 | ), 673 | ) 674 | 675 | body_frames: List[Union[FrameType, ContentBody]] 676 | body_frames = [publish_frame, content_header] 677 | body_frames += self._split_body(body) 678 | 679 | drain_future = self.create_future() if wait else None 680 | await countdown( 681 | self.write_queue.put( 682 | ChannelFrame.marshall( 683 | frames=body_frames, 684 | channel_number=self.number, 685 | drain_future=drain_future, 686 | ), 687 | ), 688 | ) 689 | 690 | if drain_future: 691 | await drain_future 692 | 693 | if not self.publisher_confirms: 694 | return None 695 | 696 | if confirmation is None: 697 | return None 698 | 699 | return await countdown(confirmation) 700 | 701 | async def basic_qos( 702 | self, 703 | *, 704 | prefetch_size: Optional[int] = None, 705 | prefetch_count: Optional[int] = None, 706 | global_: bool = False, 707 | timeout: TimeoutType = None, 708 | ) -> spec.Basic.QosOk: 709 | return await self.rpc( 710 | spec.Basic.Qos( 711 | prefetch_size=prefetch_size or 0, 712 | prefetch_count=prefetch_count or 0, 713 | global_=global_, 714 | ), 715 | timeout=timeout, 716 | ) 717 | 718 | async def basic_recover( 719 | self, *, nowait: bool = False, requeue: bool = False, 720 | timeout: TimeoutType = None, 721 | ) -> spec.Basic.RecoverOk: 722 | frame: Union[spec.Basic.RecoverAsync, spec.Basic.Recover] 723 | if nowait: 724 | frame = spec.Basic.RecoverAsync(requeue=requeue) 725 | else: 726 | frame = spec.Basic.Recover(requeue=requeue) 727 | 728 | return await self.rpc(frame, timeout=timeout) 729 | 730 | async def exchange_declare( 731 | self, 732 | exchange: str = "", 733 | *, 734 | exchange_type: str = "direct", 735 | passive: bool = False, 736 | durable: bool = False, 737 | auto_delete: bool = False, 738 | internal: bool = False, 739 | nowait: bool = False, 740 | arguments: Optional[Dict[str, Any]] = None, 741 | timeout: TimeoutType = None, 742 | ) -> spec.Exchange.DeclareOk: 743 | return await self.rpc( 744 | spec.Exchange.Declare( 745 | exchange=str(exchange), 746 | exchange_type=str(exchange_type), 747 | passive=bool(passive), 748 | durable=bool(durable), 749 | auto_delete=bool(auto_delete), 750 | internal=bool(internal), 751 | nowait=bool(nowait), 752 | arguments=arguments, 753 | ), 754 | timeout=timeout, 755 | ) 756 | 757 | async def exchange_delete( 758 | self, 759 | exchange: str = "", 760 | *, 761 | if_unused: bool = False, 762 | nowait: bool = False, 763 | timeout: TimeoutType = None, 764 | ) -> spec.Exchange.DeleteOk: 765 | return await self.rpc( 766 | spec.Exchange.Delete( 767 | exchange=exchange, nowait=nowait, if_unused=if_unused, 768 | ), 769 | timeout=timeout, 770 | ) 771 | 772 | async def exchange_bind( 773 | self, 774 | destination: str = "", 775 | source: str = "", 776 | routing_key: str = "", 777 | *, 778 | nowait: bool = False, 779 | arguments: Optional[ArgumentsType] = None, 780 | timeout: TimeoutType = None, 781 | ) -> spec.Exchange.BindOk: 782 | _check_routing_key(routing_key) 783 | return await self.rpc( 784 | spec.Exchange.Bind( 785 | destination=destination, 786 | source=source, 787 | routing_key=routing_key, 788 | nowait=nowait, 789 | arguments=arguments, 790 | ), 791 | timeout=timeout, 792 | ) 793 | 794 | async def exchange_unbind( 795 | self, 796 | destination: str = "", 797 | source: str = "", 798 | routing_key: str = "", 799 | *, 800 | nowait: bool = False, 801 | arguments: Optional[ArgumentsType] = None, 802 | timeout: TimeoutType = None, 803 | ) -> spec.Exchange.UnbindOk: 804 | _check_routing_key(routing_key) 805 | return await self.rpc( 806 | spec.Exchange.Unbind( 807 | destination=destination, 808 | source=source, 809 | routing_key=routing_key, 810 | nowait=nowait, 811 | arguments=arguments, 812 | ), 813 | timeout=timeout, 814 | ) 815 | 816 | async def flow( 817 | self, active: bool, 818 | timeout: TimeoutType = None, 819 | ) -> spec.Channel.FlowOk: 820 | return await self.rpc( 821 | spec.Channel.Flow(active=active), 822 | timeout=timeout, 823 | ) 824 | 825 | async def queue_bind( 826 | self, 827 | queue: str, 828 | exchange: str, 829 | routing_key: str = "", 830 | nowait: bool = False, 831 | arguments: Optional[ArgumentsType] = None, 832 | timeout: TimeoutType = None, 833 | ) -> spec.Queue.BindOk: 834 | _check_routing_key(routing_key) 835 | return await self.rpc( 836 | spec.Queue.Bind( 837 | queue=queue, 838 | exchange=exchange, 839 | routing_key=routing_key, 840 | nowait=nowait, 841 | arguments=arguments, 842 | ), 843 | timeout=timeout, 844 | ) 845 | 846 | async def queue_declare( 847 | self, 848 | queue: str = "", 849 | *, 850 | passive: bool = False, 851 | durable: bool = False, 852 | exclusive: bool = False, 853 | auto_delete: bool = False, 854 | nowait: bool = False, 855 | arguments: Optional[ArgumentsType] = None, 856 | timeout: TimeoutType = None, 857 | ) -> spec.Queue.DeclareOk: 858 | return await self.rpc( 859 | spec.Queue.Declare( 860 | queue=queue, 861 | passive=bool(passive), 862 | durable=bool(durable), 863 | exclusive=bool(exclusive), 864 | auto_delete=bool(auto_delete), 865 | nowait=bool(nowait), 866 | arguments=arguments, 867 | ), 868 | timeout=timeout, 869 | ) 870 | 871 | async def queue_delete( 872 | self, 873 | queue: str = "", 874 | if_unused: bool = False, 875 | if_empty: bool = False, 876 | nowait: bool = False, 877 | timeout: TimeoutType = None, 878 | ) -> spec.Queue.DeleteOk: 879 | return await self.rpc( 880 | spec.Queue.Delete( 881 | queue=queue, 882 | if_unused=if_unused, 883 | if_empty=if_empty, 884 | nowait=nowait, 885 | ), 886 | timeout=timeout, 887 | ) 888 | 889 | async def queue_purge( 890 | self, queue: str = "", nowait: bool = False, 891 | timeout: TimeoutType = None, 892 | ) -> spec.Queue.PurgeOk: 893 | return await self.rpc( 894 | spec.Queue.Purge(queue=queue, nowait=nowait), 895 | timeout=timeout, 896 | ) 897 | 898 | async def queue_unbind( 899 | self, 900 | queue: str = "", 901 | exchange: str = "", 902 | routing_key: str = "", 903 | arguments: Optional[ArgumentsType] = None, 904 | timeout: TimeoutType = None, 905 | ) -> spec.Queue.UnbindOk: 906 | _check_routing_key(routing_key) 907 | return await self.rpc( 908 | spec.Queue.Unbind( 909 | routing_key=routing_key, 910 | arguments=arguments, 911 | queue=queue, 912 | exchange=exchange, 913 | ), 914 | timeout=timeout, 915 | ) 916 | 917 | async def tx_commit( 918 | self, timeout: TimeoutType = None, 919 | ) -> spec.Tx.CommitOk: 920 | return await self.rpc(spec.Tx.Commit(), timeout=timeout) 921 | 922 | async def tx_rollback( 923 | self, timeout: TimeoutType = None, 924 | ) -> spec.Tx.RollbackOk: 925 | return await self.rpc(spec.Tx.Rollback(), timeout=timeout) 926 | 927 | async def tx_select(self, timeout: TimeoutType = None) -> spec.Tx.SelectOk: 928 | return await self.rpc(spec.Tx.Select(), timeout=timeout) 929 | 930 | async def confirm_delivery( 931 | self, nowait: bool = False, 932 | timeout: TimeoutType = None, 933 | ) -> spec.Confirm.SelectOk: 934 | return await self.rpc( 935 | spec.Confirm.Select(nowait=nowait), 936 | timeout=timeout, 937 | ) 938 | -------------------------------------------------------------------------------- /aiormq/connection.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import platform 4 | import ssl 5 | import sys 6 | from base64 import b64decode 7 | from collections.abc import AsyncIterable 8 | from contextlib import suppress 9 | from io import BytesIO 10 | from types import MappingProxyType, TracebackType 11 | from typing import ( 12 | Any, Awaitable, Callable, Dict, Mapping, Optional, Tuple, Type, Union, 13 | ) 14 | 15 | import pamqp.frame 16 | from pamqp import commands as spec 17 | from pamqp.base import Frame 18 | from pamqp.common import FieldTable 19 | from pamqp.constants import REPLY_SUCCESS 20 | from pamqp.exceptions import AMQPFrameError, AMQPInternalError, AMQPSyntaxError 21 | from pamqp.frame import FrameTypes 22 | from pamqp.header import ProtocolHeader 23 | from pamqp.heartbeat import Heartbeat 24 | from yarl import URL 25 | 26 | from .abc import ( 27 | AbstractChannel, AbstractConnection, ArgumentsType, ChannelFrame, 28 | ExceptionType, SSLCerts, TaskType, URLorStr, 29 | ) 30 | from .auth import AuthMechanism 31 | from .base import Base, task 32 | from .channel import Channel 33 | from .exceptions import ( 34 | AMQPConnectionError, AMQPError, AuthenticationError, ConnectionChannelError, 35 | ConnectionClosed, ConnectionCommandInvalid, ConnectionFrameError, 36 | ConnectionInternalError, ConnectionNotAllowed, ConnectionNotImplemented, 37 | ConnectionResourceError, ConnectionSyntaxError, ConnectionUnexpectedFrame, 38 | IncompatibleProtocolError, ProbableAuthenticationError, 39 | ) 40 | from .tools import Countdown, censor_url 41 | 42 | 43 | # noinspection PyUnresolvedReferences 44 | try: 45 | from importlib.metadata import Distribution 46 | __version__ = Distribution.from_name("aiormq").version 47 | except ImportError: 48 | import pkg_resources 49 | __version__ = pkg_resources.get_distribution("aiormq").version 50 | 51 | 52 | log = logging.getLogger(__name__) 53 | 54 | CHANNEL_CLOSE_RESPONSES = (spec.Channel.Close, spec.Channel.CloseOk) 55 | 56 | DEFAULT_PORTS = { 57 | "amqp": 5672, 58 | "amqps": 5671, 59 | } 60 | 61 | 62 | PRODUCT = "aiormq" 63 | PLATFORM = "{} {} ({} build {})".format( 64 | platform.python_implementation(), 65 | platform.python_version(), 66 | *platform.python_build(), 67 | ) 68 | 69 | 70 | TimeType = Union[float, int] 71 | TimeoutType = Optional[TimeType] 72 | ReceivedFrame = Tuple[int, int, FrameTypes] 73 | 74 | 75 | EXCEPTION_MAPPING = MappingProxyType({ 76 | 501: ConnectionFrameError, 77 | 502: ConnectionSyntaxError, 78 | 503: ConnectionCommandInvalid, 79 | 504: ConnectionChannelError, 80 | 505: ConnectionUnexpectedFrame, 81 | 506: ConnectionResourceError, 82 | 530: ConnectionNotAllowed, 83 | 540: ConnectionNotImplemented, 84 | 541: ConnectionInternalError, 85 | }) 86 | 87 | 88 | def exception_by_code(frame: spec.Connection.Close) -> AMQPError: 89 | if frame.reply_code is None: 90 | return ConnectionClosed(frame.reply_code, frame.reply_text) 91 | 92 | exc_class = EXCEPTION_MAPPING.get(frame.reply_code) 93 | 94 | if exc_class is None: 95 | return ConnectionClosed(frame.reply_code, frame.reply_text) 96 | 97 | return exc_class(frame.reply_text) 98 | 99 | 100 | def parse_bool(v: Any) -> bool: 101 | if isinstance(v, bool): 102 | return v 103 | 104 | v = str(v) 105 | return v.lower() in ( 106 | "true", "yes", "y", "enable", "on", "enabled", "1" 107 | ) 108 | 109 | 110 | def parse_int(v: Any) -> int: 111 | if isinstance(v, int): 112 | return v 113 | 114 | v = str(v) 115 | try: 116 | return int(v) 117 | except ValueError: 118 | return 0 119 | 120 | 121 | def parse_timeout(v: Any) -> TimeoutType: 122 | if isinstance(v, float): 123 | if v.is_integer(): 124 | return int(v) 125 | return v 126 | 127 | if isinstance(v, int): 128 | return v 129 | 130 | v = str(v) 131 | try: 132 | if "." in v: 133 | result = float(v) 134 | if result.is_integer(): 135 | return int(result) 136 | return result 137 | return int(v) 138 | except ValueError: 139 | return 0 140 | 141 | 142 | def parse_heartbeat(v: str) -> int: 143 | result = parse_int(v) 144 | return result if 0 <= result < 65535 else 0 145 | 146 | 147 | def parse_connection_name(connection_name: Optional[str]) -> Dict[str, str]: 148 | if not connection_name or not isinstance(connection_name, str): 149 | return {} 150 | return dict(connection_name=connection_name) 151 | 152 | 153 | class FrameReceiver(AsyncIterable): 154 | _loop: asyncio.AbstractEventLoop 155 | 156 | def __init__( 157 | self, reader: asyncio.StreamReader, 158 | ): 159 | self.reader: asyncio.StreamReader = reader 160 | self.started: bool = False 161 | self.lock = asyncio.Lock() 162 | 163 | @property 164 | def loop(self) -> asyncio.AbstractEventLoop: 165 | if not hasattr(self, "_loop"): 166 | self._loop = asyncio.get_event_loop() 167 | return self._loop 168 | 169 | def __aiter__(self) -> "FrameReceiver": 170 | return self 171 | 172 | async def get_frame(self) -> ReceivedFrame: 173 | if self.reader.at_eof(): 174 | del self.reader 175 | raise StopAsyncIteration 176 | 177 | with BytesIO() as fp: 178 | async with self.lock: 179 | try: 180 | fp.write(await self.reader.readexactly(1)) 181 | 182 | if fp.getvalue() == b"\0x00": 183 | fp.write(await self.reader.read()) 184 | raise AMQPFrameError(fp.getvalue()) 185 | 186 | if self.reader is None: 187 | raise AMQPConnectionError() 188 | 189 | fp.write(await self.reader.readexactly(6)) 190 | 191 | if not self.started and fp.getvalue().startswith(b"AMQP"): 192 | raise AMQPSyntaxError 193 | else: 194 | self.started = True 195 | 196 | frame_type, _, frame_length = pamqp.frame.frame_parts( 197 | fp.getvalue(), 198 | ) 199 | if frame_length is None: 200 | raise AMQPInternalError("No frame length", None) 201 | 202 | fp.write(await self.reader.readexactly(frame_length + 1)) 203 | except asyncio.IncompleteReadError as e: 204 | raise AMQPConnectionError( 205 | "Server connection unexpectedly closed. " 206 | f"Read {len(e.partial)} bytes but {e.expected} " 207 | "bytes expected", 208 | ) from e 209 | except ConnectionRefusedError as e: 210 | raise AMQPConnectionError( 211 | f"Server connection refused: {e!r}", 212 | ) from e 213 | except ConnectionResetError as e: 214 | raise AMQPConnectionError( 215 | f"Server connection reset: {e!r}", 216 | ) from e 217 | except ConnectionError as e: 218 | raise AMQPConnectionError( 219 | f"Server connection error: {e!r}", 220 | ) from e 221 | except OSError as e: 222 | raise AMQPConnectionError( 223 | f"Server communication error: {e!r}", 224 | ) from e 225 | 226 | return pamqp.frame.unmarshal(fp.getvalue()) 227 | 228 | async def __anext__(self) -> ReceivedFrame: 229 | return await self.get_frame() 230 | 231 | 232 | class FrameGenerator(AsyncIterable): 233 | def __init__(self, queue: asyncio.Queue): 234 | self.queue: asyncio.Queue = queue 235 | self.close_event: asyncio.Event = asyncio.Event() 236 | 237 | def __aiter__(self) -> "FrameGenerator": 238 | return self 239 | 240 | async def __anext__(self) -> ChannelFrame: 241 | if self.close_event.is_set(): 242 | raise StopAsyncIteration 243 | 244 | frame: ChannelFrame = await self.queue.get() 245 | self.queue.task_done() 246 | return frame 247 | 248 | 249 | class Connection(Base, AbstractConnection): 250 | FRAME_BUFFER_SIZE = 10 251 | # Interval between sending heartbeats based on the heartbeat(timeout) 252 | HEARTBEAT_INTERVAL_MULTIPLIER = 0.5 253 | # Allow three missed heartbeats (based on heartbeat(timeout) 254 | HEARTBEAT_GRACE_MULTIPLIER = 3 255 | 256 | READER_CLOSE_TIMEOUT = 2 257 | 258 | _reader_task: TaskType 259 | _writer_task: TaskType 260 | __create_connection_kwargs: Mapping[str, Any] 261 | 262 | write_queue: asyncio.Queue 263 | server_properties: ArgumentsType 264 | connection_tune: spec.Connection.Tune 265 | channels: Dict[int, Optional[AbstractChannel]] 266 | 267 | @staticmethod 268 | def _parse_ca_data(data: Optional[str]) -> Optional[bytes]: 269 | return b64decode(data) if data else None 270 | 271 | def __init__( 272 | self, 273 | url: URLorStr, 274 | *, 275 | loop: Optional[asyncio.AbstractEventLoop] = None, 276 | context: Optional[ssl.SSLContext] = None, 277 | **create_connection_kwargs: Any, 278 | ): 279 | 280 | super().__init__(loop=loop or asyncio.get_event_loop(), parent=None) 281 | 282 | self.url = URL(url) 283 | if self.url.is_absolute() and not self.url.port: 284 | self.url = self.url.with_port(DEFAULT_PORTS[self.url.scheme]) 285 | 286 | if self.url.path == "/" or not self.url.path: 287 | self.vhost = "/" 288 | else: 289 | quoted_vhost = self.url.path[1:] 290 | # yarl>=1.9.5 skips unquoting backslashes in path 291 | self.vhost = quoted_vhost.replace("%2F", "/") 292 | 293 | self.ssl_context = context 294 | self.ssl_certs = SSLCerts( 295 | cafile=self.url.query.get("cafile"), 296 | capath=self.url.query.get("capath"), 297 | cadata=self._parse_ca_data(self.url.query.get("cadata")), 298 | key=self.url.query.get("keyfile"), 299 | cert=self.url.query.get("certfile"), 300 | verify=self.url.query.get("no_verify_ssl", "0") == "0", 301 | ) 302 | 303 | self.started = False 304 | self.channels = {} 305 | self.write_queue = asyncio.Queue( 306 | maxsize=self.FRAME_BUFFER_SIZE, 307 | ) 308 | 309 | self.last_channel = 1 310 | 311 | self.timeout = parse_int(self.url.query.get("timeout", "60")) 312 | self.heartbeat_timeout = parse_heartbeat( 313 | self.url.query.get("heartbeat", "60"), 314 | ) 315 | self.last_channel_lock = asyncio.Lock() 316 | self.connected = asyncio.Event() 317 | self.connection_name = self.url.query.get("name") 318 | 319 | self.__close_reply_code: int = REPLY_SUCCESS 320 | self.__close_reply_text: str = "normally closed" 321 | self.__close_class_id: int = 0 322 | self.__close_method_id: int = 0 323 | self.__update_secret_lock: asyncio.Lock = asyncio.Lock() 324 | self.__update_secret_future: Optional[asyncio.Future] = None 325 | self.__connection_unblocked: asyncio.Event = asyncio.Event() 326 | self.__heartbeat_grace_timeout = ( 327 | (self.heartbeat_timeout + 1) * self.HEARTBEAT_GRACE_MULTIPLIER 328 | ) 329 | self.__last_frame_time: float = self.loop.time() 330 | self.__create_connection_kwargs = create_connection_kwargs 331 | 332 | async def ready(self) -> None: 333 | await self.connected.wait() 334 | await self.__connection_unblocked.wait() 335 | 336 | def set_close_reason( 337 | self, reply_code: int = REPLY_SUCCESS, 338 | reply_text: str = "normally closed", 339 | class_id: int = 0, method_id: int = 0, 340 | ) -> None: 341 | self.__close_reply_code = reply_code 342 | self.__close_reply_text = reply_text 343 | self.__close_class_id = class_id 344 | self.__close_method_id = method_id 345 | 346 | @property 347 | def is_opened(self) -> bool: 348 | is_reader_running = ( 349 | hasattr(self, "_reader_task") and not self._reader_task.done() 350 | ) 351 | is_writer_running = ( 352 | hasattr(self, "_writer_task") and not self._writer_task.done() 353 | ) 354 | 355 | return ( 356 | is_reader_running and 357 | is_writer_running and 358 | not self.is_closed 359 | ) 360 | 361 | def __str__(self) -> str: 362 | return str(censor_url(self.url)) 363 | 364 | def _get_ssl_context(self) -> ssl.SSLContext: 365 | context = ssl.create_default_context( 366 | ssl.Purpose.SERVER_AUTH, 367 | capath=self.ssl_certs.capath, 368 | cafile=self.ssl_certs.cafile, 369 | cadata=self.ssl_certs.cadata, 370 | ) 371 | 372 | if self.ssl_certs.cert: 373 | context.load_cert_chain(self.ssl_certs.cert, self.ssl_certs.key) 374 | 375 | if not self.ssl_certs.verify: 376 | context.check_hostname = False 377 | context.verify_mode = ssl.CERT_NONE 378 | 379 | return context 380 | 381 | def _client_properties(self, **kwargs: Any) -> Dict[str, Any]: 382 | properties = { 383 | "platform": PLATFORM, 384 | "version": __version__, 385 | "product": PRODUCT, 386 | "capabilities": { 387 | "authentication_failure_close": True, 388 | "basic.nack": True, 389 | "connection.blocked": True, 390 | "consumer_cancel_notify": True, 391 | "publisher_confirms": True, 392 | }, 393 | "information": "See https://github.com/mosquito/aiormq/", 394 | } 395 | 396 | properties.update( 397 | parse_connection_name(self.connection_name), 398 | ) 399 | properties.update(kwargs) 400 | return properties 401 | 402 | def _credentials_class( 403 | self, 404 | start_frame: spec.Connection.Start, 405 | ) -> AuthMechanism: 406 | auth_requested = self.url.query.get("auth", "plain").upper() 407 | auth_available = start_frame.mechanisms.split() 408 | if auth_requested in auth_available: 409 | with suppress(KeyError): 410 | return AuthMechanism[auth_requested] 411 | raise AuthenticationError( 412 | start_frame.mechanisms, [m.name for m in AuthMechanism], 413 | ) 414 | 415 | @staticmethod 416 | async def _rpc( 417 | request: Frame, writer: asyncio.StreamWriter, 418 | frame_receiver: FrameReceiver, 419 | wait_response: bool = True, 420 | ) -> Optional[FrameTypes]: 421 | 422 | writer.write(pamqp.frame.marshal(request, 0)) 423 | await writer.drain() 424 | 425 | if not wait_response: 426 | return None 427 | 428 | _, _, frame = await frame_receiver.get_frame() 429 | 430 | if request.synchronous and frame.name not in request.valid_responses: 431 | raise AMQPInternalError( 432 | "one of {!r}".format(request.valid_responses), frame, 433 | ) 434 | elif isinstance(frame, spec.Connection.Close): 435 | if frame.reply_code == 403: 436 | raise ProbableAuthenticationError(frame.reply_text) 437 | raise ConnectionClosed(frame.reply_code, frame.reply_text) 438 | return frame 439 | 440 | @task 441 | async def connect( 442 | self, client_properties: Optional[FieldTable] = None, 443 | ) -> bool: 444 | if self.is_opened: 445 | raise RuntimeError("Connection already opened") 446 | 447 | ssl_context = self.ssl_context 448 | 449 | if ssl_context is None and self.url.scheme == "amqps": 450 | ssl_context = await self.loop.run_in_executor( 451 | None, self._get_ssl_context, 452 | ) 453 | self.ssl_context = ssl_context 454 | 455 | log.debug("Connecting to: %s", self) 456 | try: 457 | reader, writer = await asyncio.open_connection( 458 | self.url.host, self.url.port, ssl=ssl_context, 459 | **self.__create_connection_kwargs, 460 | ) 461 | 462 | frame_receiver = FrameReceiver(reader) 463 | except OSError as e: 464 | raise AMQPConnectionError(*e.args) from e 465 | 466 | frame: Optional[FrameTypes] 467 | 468 | try: 469 | protocol_header = ProtocolHeader() 470 | writer.write(protocol_header.marshal()) 471 | 472 | _, _, frame = await frame_receiver.get_frame() 473 | except EOFError as e: 474 | raise IncompatibleProtocolError(*e.args) from e 475 | 476 | if not isinstance(frame, spec.Connection.Start): 477 | raise AMQPInternalError("Connection.StartOk", frame) 478 | 479 | credentials = self._credentials_class(frame) 480 | 481 | server_properties: ArgumentsType = frame.server_properties 482 | 483 | try: 484 | frame = await self._rpc( 485 | spec.Connection.StartOk( 486 | client_properties=self._client_properties( 487 | **(client_properties or {}), 488 | ), 489 | mechanism=credentials.name, 490 | response=credentials.value(self).marshal(), 491 | ), 492 | writer=writer, 493 | frame_receiver=frame_receiver, 494 | ) 495 | 496 | if not isinstance(frame, spec.Connection.Tune): 497 | raise AMQPInternalError("Connection.Tune", frame) 498 | 499 | connection_tune: spec.Connection.Tune = frame 500 | connection_tune.heartbeat = self.heartbeat_timeout 501 | 502 | await self._rpc( 503 | spec.Connection.TuneOk( 504 | channel_max=connection_tune.channel_max, 505 | frame_max=connection_tune.frame_max, 506 | heartbeat=connection_tune.heartbeat, 507 | ), 508 | writer=writer, 509 | frame_receiver=frame_receiver, 510 | wait_response=False, 511 | ) 512 | 513 | frame = await self._rpc( 514 | spec.Connection.Open(virtual_host=self.vhost), 515 | writer=writer, 516 | frame_receiver=frame_receiver, 517 | ) 518 | 519 | if not isinstance(frame, spec.Connection.OpenOk): 520 | raise AMQPInternalError("Connection.OpenOk", frame) 521 | except BaseException as e: 522 | await self.__close_writer(writer) 523 | await self.close(e) 524 | raise 525 | 526 | # noinspection PyAsyncCall 527 | self._reader_task = self.create_task(self.__reader(frame_receiver)) 528 | self._reader_task.add_done_callback(self._on_reader_done) 529 | 530 | # noinspection PyAsyncCall 531 | self._writer_task = self.create_task(self.__writer(writer)) 532 | 533 | self.connection_tune = connection_tune 534 | self.server_properties = server_properties 535 | return True 536 | 537 | def _on_reader_done(self, task: asyncio.Task) -> None: 538 | log.debug("Reader exited for %r", self) 539 | 540 | if not task.cancelled() and task.exception() is not None: 541 | log.debug("Cancelling cause reader exited abnormally") 542 | self.set_close_reason( 543 | reply_code=500, reply_text="reader unexpected closed", 544 | ) 545 | 546 | async def close_writer_task() -> None: 547 | if not self._writer_task.done(): 548 | self._writer_task.cancel() 549 | await asyncio.gather(self._writer_task, return_exceptions=True) 550 | try: 551 | exc = task.exception() 552 | except asyncio.CancelledError as e: 553 | exc = e 554 | await self.close(exc) 555 | 556 | self.loop.create_task(close_writer_task()) 557 | 558 | async def __handle_close_ok(self, _: spec.Connection.CloseOk) -> None: 559 | return 560 | 561 | async def __handle_heartbeat(self, _: Heartbeat) -> None: 562 | return 563 | 564 | async def __handle_close(self, frame: spec.Connection.Close) -> None: 565 | log.exception( 566 | "Unexpected connection close from remote \"%s\", " 567 | "Connection.Close(reply_code=%r, reply_text=%r)", 568 | self, frame.reply_code, frame.reply_text, 569 | ) 570 | 571 | with suppress(asyncio.QueueFull): 572 | self.write_queue.put_nowait( 573 | ChannelFrame.marshall( 574 | channel_number=0, 575 | frames=[spec.Connection.CloseOk()], 576 | ), 577 | ) 578 | 579 | exception = exception_by_code(frame) 580 | 581 | if ( 582 | self.__update_secret_future is not None and 583 | not self.__update_secret_future.done() 584 | ): 585 | self.__update_secret_future.set_exception(exception) 586 | raise exception 587 | 588 | async def __handle_channel_close_ok( 589 | self, _: spec.Channel.CloseOk, 590 | ) -> None: 591 | self.channels.pop(0, None) 592 | 593 | async def __handle_channel_update_secret_ok( 594 | self, frame: spec.Connection.UpdateSecretOk, 595 | ) -> None: 596 | if ( 597 | self.__update_secret_future is not None and 598 | not self.__update_secret_future.done() 599 | ): 600 | self.__update_secret_future.set_result(frame) 601 | return 602 | log.warning("Got unexpected UpdateSecretOk frame") 603 | 604 | async def __handle_connection_blocked( 605 | self, frame: spec.Connection.Blocked, 606 | ) -> None: 607 | log.warning("Connection %r was blocked by: %r", self, frame.reason) 608 | self.__connection_unblocked.clear() 609 | 610 | async def __handle_connection_unblocked( 611 | self, _: spec.Connection.Unblocked, 612 | ) -> None: 613 | log.warning("Connection %r was unblocked", self) 614 | self.__connection_unblocked.set() 615 | 616 | async def __reader(self, frame_receiver: FrameReceiver) -> None: 617 | self.__connection_unblocked.set() 618 | self.connected.set() 619 | 620 | # Not very optimal, but avoid creating a task for each frame sending 621 | # noinspection PyAsyncCall 622 | if self.heartbeat_timeout > 0: 623 | self.create_task(self.__heartbeat()) 624 | 625 | channel_frame_handlers: Mapping[Any, Callable[[Any], Awaitable[None]]] 626 | channel_frame_handlers = { 627 | spec.Connection.CloseOk: self.__handle_close_ok, 628 | spec.Connection.Close: self.__handle_close, 629 | Heartbeat: self.__handle_heartbeat, 630 | spec.Channel.CloseOk: self.__handle_channel_close_ok, 631 | spec.Connection.UpdateSecretOk: ( 632 | self.__handle_channel_update_secret_ok 633 | ), 634 | spec.Connection.Blocked: self.__handle_connection_blocked, 635 | spec.Connection.Unblocked: self.__handle_connection_unblocked, 636 | } 637 | 638 | try: 639 | async for weight, channel, frame in frame_receiver: 640 | self.__last_frame_time = self.loop.time() 641 | 642 | log.debug( 643 | "Received frame %r in channel #%d weight=%s on %r", 644 | frame, channel, weight, self, 645 | ) 646 | 647 | if channel == 0: 648 | handler = channel_frame_handlers.get(type(frame)) 649 | 650 | if handler is None: 651 | log.error("Unexpected frame %r", frame) 652 | continue 653 | 654 | await handler(frame) 655 | continue 656 | 657 | ch: Optional[AbstractChannel] = self.channels.get(channel) 658 | if ch is None: 659 | log.error( 660 | "Got frame for closed channel %d: %r", channel, frame, 661 | ) 662 | continue 663 | 664 | if isinstance(frame, CHANNEL_CLOSE_RESPONSES): 665 | self.channels[channel] = None 666 | 667 | await ch.frames.put((weight, frame)) 668 | except asyncio.CancelledError: 669 | if self.is_connection_was_stuck: 670 | log.warning( 671 | "Server connection %r was stuck. No frames were received " 672 | "in %d seconds.", self, self.__heartbeat_grace_timeout, 673 | ) 674 | self._writer_task.cancel() 675 | raise 676 | 677 | @property 678 | def is_connection_was_stuck(self) -> bool: 679 | delay = self.loop.time() - self.__last_frame_time 680 | return delay > self.__heartbeat_grace_timeout 681 | 682 | async def __heartbeat(self) -> None: 683 | heartbeat_timeout = max(1, self.heartbeat_timeout // 2) 684 | heartbeat = ChannelFrame.marshall( 685 | frames=[Heartbeat()], channel_number=0, 686 | ) 687 | 688 | while not self.closing.done(): 689 | if self.is_connection_was_stuck: 690 | self._reader_task.cancel() 691 | return 692 | 693 | await asyncio.sleep(heartbeat_timeout) 694 | 695 | try: 696 | await asyncio.wait_for( 697 | self.write_queue.put(heartbeat), 698 | timeout=self.__heartbeat_grace_timeout, 699 | ) 700 | except asyncio.TimeoutError: 701 | self._reader_task.cancel() 702 | return 703 | 704 | async def __writer(self, writer: asyncio.StreamWriter) -> None: 705 | channel_frame: ChannelFrame 706 | 707 | try: 708 | frame_iterator = FrameGenerator(self.write_queue) 709 | self.closing.add_done_callback( 710 | lambda _: frame_iterator.close_event.set(), 711 | ) 712 | 713 | if not self.__connection_unblocked.is_set(): 714 | await self.__connection_unblocked.wait() 715 | 716 | async for channel_frame in frame_iterator: 717 | log.debug("Prepare to send %r", channel_frame) 718 | 719 | writer.write(channel_frame.payload) 720 | if channel_frame.should_close: 721 | await writer.drain() 722 | channel_frame.drain() 723 | return 724 | 725 | if channel_frame.should_drain: 726 | await writer.drain() 727 | channel_frame.drain() 728 | 729 | except asyncio.CancelledError: 730 | if not self.__check_writer(writer) or self.is_connection_was_stuck: 731 | raise 732 | 733 | frame = spec.Connection.Close( 734 | reply_code=self.__close_reply_code, 735 | reply_text=self.__close_reply_text, 736 | class_id=self.__close_class_id, 737 | method_id=self.__close_method_id, 738 | ) 739 | 740 | writer.write(ChannelFrame.marshall(0, [frame]).payload) 741 | 742 | log.debug("Sending %r to %r", frame, self) 743 | 744 | try: 745 | await asyncio.wait_for( 746 | writer.drain(), timeout=self.__heartbeat_grace_timeout, 747 | ) 748 | finally: 749 | await self.__close_writer(writer) 750 | 751 | raise 752 | finally: 753 | log.debug("Writer exited for %r", self) 754 | 755 | if sys.version_info < (3, 7): 756 | async def __close_writer(self, writer: asyncio.StreamWriter) -> None: 757 | log.debug("Writer on connection %s closed", self) 758 | writer.close() 759 | else: 760 | async def __close_writer(self, writer: asyncio.StreamWriter) -> None: 761 | log.debug("Writer on connection %s closed", self) 762 | with suppress(OSError, RuntimeError): 763 | if writer.can_write_eof(): 764 | writer.write_eof() 765 | writer.close() 766 | await writer.wait_closed() 767 | 768 | @staticmethod 769 | def __check_writer(writer: asyncio.StreamWriter) -> bool: 770 | if writer is None: 771 | return False 772 | 773 | if hasattr(writer, "is_closing"): 774 | return not writer.is_closing() 775 | 776 | if writer.transport: 777 | return not writer.transport.is_closing() 778 | 779 | return writer.can_write_eof() 780 | 781 | async def _on_close( 782 | self, 783 | ex: Optional[ExceptionType] = ConnectionClosed(0, "normal closed"), 784 | ) -> None: 785 | log.debug("Closing connection %r cause: %r", self, ex) 786 | if not self._reader_task.done(): 787 | self._reader_task.cancel() 788 | if not self._writer_task.done(): 789 | self._writer_task.cancel() 790 | 791 | await asyncio.gather( 792 | self._reader_task, self._writer_task, return_exceptions=True, 793 | ) 794 | 795 | @property 796 | def server_capabilities(self) -> ArgumentsType: 797 | return self.server_properties["capabilities"] # type: ignore 798 | 799 | @property 800 | def basic_nack(self) -> bool: 801 | return bool(self.server_capabilities.get("basic.nack")) 802 | 803 | @property 804 | def consumer_cancel_notify(self) -> bool: 805 | return bool(self.server_capabilities.get("consumer_cancel_notify")) 806 | 807 | @property 808 | def exchange_exchange_bindings(self) -> bool: 809 | return bool(self.server_capabilities.get("exchange_exchange_bindings")) 810 | 811 | @property 812 | def publisher_confirms(self) -> Optional[bool]: 813 | publisher_confirms = self.server_capabilities.get("publisher_confirms") 814 | if publisher_confirms is None: 815 | return None 816 | return bool(publisher_confirms) 817 | 818 | async def channel( 819 | self, 820 | channel_number: Optional[int] = None, 821 | publisher_confirms: bool = True, 822 | frame_buffer_size: int = FRAME_BUFFER_SIZE, 823 | timeout: TimeoutType = None, 824 | **kwargs: Any, 825 | ) -> AbstractChannel: 826 | 827 | await self.connected.wait() 828 | 829 | if self.is_closed: 830 | raise RuntimeError("%r closed" % self) 831 | 832 | if not self.publisher_confirms and publisher_confirms: 833 | raise ValueError("Server doesn't support publisher_confirms") 834 | 835 | if channel_number is None: 836 | async with self.last_channel_lock: 837 | if self.channels: 838 | self.last_channel = max(self.channels.keys()) 839 | 840 | while self.last_channel in self.channels.keys(): 841 | self.last_channel += 1 842 | 843 | if self.last_channel > 65535: 844 | log.warning("Resetting channel number for %r", self) 845 | self.last_channel = 1 846 | # switching context for prevent blocking event-loop 847 | await asyncio.sleep(0) 848 | 849 | channel_number = self.last_channel 850 | elif channel_number in self.channels: 851 | raise ValueError("Channel %d already used" % channel_number) 852 | 853 | if channel_number < 0 or channel_number > 65535: 854 | raise ValueError("Channel number too large") 855 | 856 | channel = Channel( 857 | self, 858 | channel_number, 859 | frame_buffer=frame_buffer_size, 860 | publisher_confirms=publisher_confirms, 861 | **kwargs, 862 | ) 863 | 864 | self.channels[channel_number] = channel 865 | 866 | try: 867 | await channel.open(timeout=timeout) 868 | except Exception: 869 | self.channels[channel_number] = None 870 | raise 871 | 872 | return channel 873 | 874 | async def update_secret( 875 | self, new_secret: str, *, 876 | reason: str = "", timeout: TimeoutType = None, 877 | ) -> spec.Connection.UpdateSecretOk: 878 | channel_frame = ChannelFrame.marshall( 879 | channel_number=0, 880 | frames=[ 881 | spec.Connection.UpdateSecret( 882 | new_secret=new_secret, reason=reason, 883 | ), 884 | ], 885 | ) 886 | 887 | countdown = Countdown(timeout) 888 | 889 | async with countdown.enter_context(self.__update_secret_lock): 890 | self.__update_secret_future = self.loop.create_future() 891 | await self.write_queue.put(channel_frame) 892 | try: 893 | response: spec.Connection.UpdateSecretOk = ( 894 | await countdown(self.__update_secret_future) 895 | ) 896 | finally: 897 | self.__update_secret_future = None 898 | return response 899 | 900 | async def __aenter__(self) -> AbstractConnection: 901 | if not self.is_opened: 902 | await self.connect() 903 | return self 904 | 905 | async def __aexit__( 906 | self, 907 | exc_type: Optional[Type[BaseException]], 908 | exc_val: Optional[BaseException], 909 | exc_tb: Optional[TracebackType], 910 | ) -> None: 911 | await self.close(exc_val) 912 | 913 | 914 | async def connect( 915 | url: URLorStr, *args: Any, client_properties: Optional[FieldTable] = None, 916 | **kwargs: Any, 917 | ) -> AbstractConnection: 918 | connection = Connection(url, *args, **kwargs) 919 | 920 | await connection.connect(client_properties or {}) 921 | return connection 922 | -------------------------------------------------------------------------------- /aiormq/exceptions.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | 3 | from pamqp.base import Frame 4 | from pamqp.commands import Basic 5 | 6 | from .abc import DeliveredMessage 7 | 8 | 9 | class AMQPError(Exception): 10 | reason = "An unspecified AMQP error has occurred: %s" 11 | 12 | def __repr__(self) -> str: 13 | try: 14 | return "<%s: %s>" % ( 15 | self.__class__.__name__, self.reason % self.args, 16 | ) 17 | except TypeError: 18 | # FIXME: if you are here file an issue 19 | return f"<{self.__class__.__name__}: {self.args!r}>" 20 | 21 | 22 | # Backward compatibility 23 | AMQPException = AMQPError 24 | 25 | 26 | class AMQPConnectionError(AMQPError, ConnectionError): 27 | reason = "Unexpected connection problem" 28 | 29 | def __repr__(self) -> str: 30 | if self.args: 31 | return f"<{self.__class__.__name__}: {self.args!r}>" 32 | return AMQPError.__repr__(self) 33 | 34 | 35 | class IncompatibleProtocolError(AMQPConnectionError): 36 | reason = "The protocol returned by the server is not supported" 37 | 38 | 39 | class AuthenticationError(AMQPConnectionError): 40 | reason = ( 41 | "Server and client could not negotiate use of the " 42 | "authentication mechanisms. Server supports only %r, " 43 | "but client supports only %r." 44 | ) 45 | 46 | 47 | class ProbableAuthenticationError(AMQPConnectionError): 48 | reason = ( 49 | "Client was disconnected at a connection stage indicating a " 50 | "probable authentication error: %s" 51 | ) 52 | 53 | 54 | class ConnectionClosed(AMQPConnectionError): 55 | reason = "The AMQP connection was closed (%s) %s" 56 | 57 | 58 | class ConnectionSyntaxError(ConnectionClosed): 59 | reason = ( 60 | "The sender sent a frame that contained illegal values for " 61 | "one or more fields. This strongly implies a programming error " 62 | "in the sending peer: %r" 63 | ) 64 | 65 | 66 | class ConnectionFrameError(ConnectionClosed): 67 | reason = ( 68 | "The sender sent a malformed frame that the recipient could " 69 | "not decode. This strongly implies a programming error " 70 | "in the sending peer: %r" 71 | ) 72 | 73 | 74 | class ConnectionCommandInvalid(ConnectionClosed): 75 | reason = ( 76 | "The client sent an invalid sequence of frames, attempting to " 77 | "perform an operation that was considered invalid by the server." 78 | " This usually implies a programming error in the client: %r" 79 | ) 80 | 81 | 82 | class ConnectionChannelError(ConnectionClosed): 83 | reason = ( 84 | "The client attempted to work with a channel that had not been " 85 | "correctly opened. This most likely indicates a fault in the " 86 | "client layer: %r" 87 | ) 88 | 89 | 90 | class ConnectionUnexpectedFrame(ConnectionClosed): 91 | reason = ( 92 | "The peer sent a frame that was not expected, usually in the " 93 | "context of a content header and body. This strongly indicates " 94 | "a fault in the peer's content processing: %r" 95 | ) 96 | 97 | 98 | class ConnectionResourceError(ConnectionClosed): 99 | reason = ( 100 | "The server could not complete the method because it lacked " 101 | "sufficient resources. This may be due to the client creating " 102 | "too many of some type of entity: %r" 103 | ) 104 | 105 | 106 | class ConnectionNotAllowed(ConnectionClosed): 107 | reason = ( 108 | "The client tried to work with some entity in a manner that is " 109 | "prohibited by the server, due to security settings or by " 110 | "some other criteria: %r" 111 | ) 112 | 113 | 114 | class ConnectionNotImplemented(ConnectionClosed): 115 | reason = ( 116 | "The client tried to use functionality that is " 117 | "not implemented in the server: %r" 118 | ) 119 | 120 | 121 | class ConnectionInternalError(ConnectionClosed): 122 | reason = ( 123 | " The server could not complete the method because of an " 124 | "internal error. The server may require intervention by an " 125 | "operator in order to resume normal operations: %r" 126 | ) 127 | 128 | 129 | class AMQPChannelError(AMQPError): 130 | reason = "An unspecified AMQP channel error has occurred" 131 | 132 | 133 | class ChannelClosed(AMQPChannelError): 134 | reason = "The channel was closed (%s) %s" 135 | 136 | 137 | class ChannelAccessRefused(ChannelClosed): 138 | reason = ( 139 | "The client attempted to work with a server entity to " 140 | "which it has no access due to security settings: %r" 141 | ) 142 | 143 | 144 | class ChannelNotFoundEntity(ChannelClosed): 145 | reason = ( 146 | "The client attempted to work with a server " 147 | "entity that does not exist: %r" 148 | ) 149 | 150 | 151 | class ChannelLockedResource(ChannelClosed): 152 | reason = ( 153 | "The client attempted to work with a server entity to " 154 | "which it has no access because another client is working " 155 | "with it: %r" 156 | ) 157 | 158 | 159 | class ChannelPreconditionFailed(ChannelClosed): 160 | reason = ( 161 | "The client requested a method that was not allowed because " 162 | "some precondition failed: %r" 163 | ) 164 | 165 | 166 | class DuplicateConsumerTag(ChannelClosed): 167 | reason = "The consumer tag specified already exists for this channel: %s" 168 | 169 | 170 | class ProtocolSyntaxError(AMQPError): 171 | reason = "An unspecified protocol syntax error occurred" 172 | 173 | 174 | class InvalidFrameError(ProtocolSyntaxError): 175 | reason = "Invalid frame received: %r" 176 | 177 | 178 | class MethodNotImplemented(AMQPError): 179 | pass 180 | 181 | 182 | class DeliveryError(AMQPError): 183 | __slots__ = "message", "frame" 184 | 185 | reason = "Error when delivery message %r, frame %r" 186 | 187 | def __init__( 188 | self, message: Optional[DeliveredMessage], 189 | frame: Frame, *args: Any, 190 | ): 191 | self.message = message 192 | self.frame = frame 193 | 194 | super().__init__(self.message, self.frame) 195 | 196 | 197 | class PublishError(DeliveryError): 198 | reason = "%r for routing key %r" 199 | 200 | def __init__(self, message: DeliveredMessage, frame: Frame, *args: Any): 201 | assert isinstance(message.delivery, Basic.Return) 202 | 203 | self.message = message 204 | self.frame = frame 205 | 206 | super(DeliveryError, self).__init__( 207 | message.delivery.reply_text, message.delivery.routing_key, *args, 208 | ) 209 | 210 | 211 | class ChannelInvalidStateError(RuntimeError): 212 | pass 213 | -------------------------------------------------------------------------------- /aiormq/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mosquito/aiormq/120965c68a18db7dcab6da2b7c66d4b2a86dea45/aiormq/py.typed -------------------------------------------------------------------------------- /aiormq/tools.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import platform 3 | import time 4 | from functools import wraps 5 | from types import TracebackType 6 | from typing import ( 7 | Any, AsyncContextManager, Awaitable, Callable, Coroutine, Optional, Type, 8 | TypeVar, Union, 9 | ) 10 | 11 | from yarl import URL 12 | 13 | from aiormq.abc import TimeoutType 14 | 15 | 16 | T = TypeVar("T") 17 | 18 | 19 | def censor_url(url: URL) -> URL: 20 | if url.password is not None: 21 | return url.with_password("******") 22 | return url 23 | 24 | 25 | def shield(func: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: 26 | @wraps(func) 27 | def wrap(*args: Any, **kwargs: Any) -> Awaitable[T]: 28 | return asyncio.shield(func(*args, **kwargs)) 29 | 30 | return wrap 31 | 32 | 33 | def awaitable( 34 | func: Callable[..., Union[T, Awaitable[T]]], 35 | ) -> Callable[..., Coroutine[Any, Any, T]]: 36 | # Avoid python 3.8+ warning 37 | if asyncio.iscoroutinefunction(func): 38 | return func # type: ignore 39 | 40 | @wraps(func) 41 | async def wrap(*args: Any, **kwargs: Any) -> T: 42 | result = func(*args, **kwargs) 43 | 44 | if hasattr(result, "__await__"): 45 | return await result # type: ignore 46 | if asyncio.iscoroutine(result) or asyncio.isfuture(result): 47 | return await result 48 | 49 | return result # type: ignore 50 | 51 | return wrap 52 | 53 | 54 | class Countdown: 55 | __slots__ = "loop", "deadline" 56 | 57 | if platform.system() == "Windows": 58 | @staticmethod 59 | def _now() -> float: 60 | # windows monotonic timer resolution is not enough. 61 | # Have to use time.time() 62 | return time.time() 63 | else: 64 | @staticmethod 65 | def _now() -> float: 66 | return time.monotonic() 67 | 68 | def __init__(self, timeout: TimeoutType = None): 69 | self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() 70 | self.deadline: TimeoutType = None 71 | 72 | if timeout is not None: 73 | self.deadline = self._now() + timeout 74 | 75 | def get_timeout(self) -> TimeoutType: 76 | if self.deadline is None: 77 | return None 78 | 79 | current = self._now() 80 | if current >= self.deadline: 81 | raise asyncio.TimeoutError 82 | 83 | return self.deadline - current 84 | 85 | async def __call__(self, coro: Awaitable[T]) -> T: 86 | try: 87 | timeout = self.get_timeout() 88 | except asyncio.TimeoutError: 89 | fut = asyncio.ensure_future(coro) 90 | fut.cancel() 91 | await asyncio.gather(fut, return_exceptions=True) 92 | raise 93 | 94 | if self.deadline is None and not timeout: 95 | return await coro 96 | return await asyncio.wait_for(coro, timeout=timeout) 97 | 98 | def enter_context( 99 | self, ctx: AsyncContextManager[T], 100 | ) -> AsyncContextManager[T]: 101 | return CountdownContext(self, ctx) 102 | 103 | 104 | class CountdownContext(AsyncContextManager): 105 | def __init__(self, countdown: Countdown, ctx: AsyncContextManager): 106 | self.countdown: Countdown = countdown 107 | self.ctx: AsyncContextManager = ctx 108 | 109 | async def __aenter__(self) -> T: 110 | return await self.countdown(self.ctx.__aenter__()) 111 | 112 | async def __aexit__( 113 | self, exc_type: Optional[Type[BaseException]], 114 | exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], 115 | ) -> Any: 116 | return await self.countdown( 117 | self.ctx.__aexit__(exc_type, exc_val, exc_tb), 118 | ) 119 | -------------------------------------------------------------------------------- /aiormq/types.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from .abc import * # noqa 4 | 5 | 6 | warnings.warn( 7 | "aiormq.types was deprecated and will be removed in " 8 | "one of next major releases. Use aiormq.abc instead.", 9 | category=DeprecationWarning, 10 | stacklevel=2, 11 | ) 12 | -------------------------------------------------------------------------------- /gray.conf: -------------------------------------------------------------------------------- 1 | formatters = add-trailing-comma,isort,unify 2 | min-python-version = 3.7 3 | log-level = error 4 | -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | cache-dir = ".cache" 2 | 3 | [virtualenvs] 4 | path = ".venv" 5 | in-project = true 6 | -------------------------------------------------------------------------------- /pylama.ini: -------------------------------------------------------------------------------- 1 | [pylama] 2 | linters = mccabe,pycodestyle,pyflakes 3 | skip = *env*,.tox*,*build*,.*,env/*,.venv/* 4 | ignore = C901 5 | 6 | [pylama:pycodestyle] 7 | max_line_length = 80 8 | show-pep8 = True 9 | show-source = True 10 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "aiormq" 3 | version = "6.8.1" 4 | description = "Pure python AMQP asynchronous client library" 5 | authors = ["Dmitry Orlov "] 6 | readme = "README.rst" 7 | license = "Apache-2.0" 8 | keywords=["rabbitmq", "asyncio", "amqp", "amqp 0.9.1", "driver", "pamqp"] 9 | homepage = "https://github.com/mosquito/aiormq" 10 | classifiers = [ 11 | "Development Status :: 5 - Production/Stable", 12 | "License :: OSI Approved :: Apache Software License", 13 | "Topic :: Internet", 14 | "Topic :: Software Development", 15 | "Topic :: Software Development :: Libraries", 16 | "Topic :: System :: Clustering", 17 | "Intended Audience :: Developers", 18 | "Natural Language :: English", 19 | "Operating System :: MacOS", 20 | "Operating System :: POSIX", 21 | "Operating System :: Microsoft", 22 | "Programming Language :: Python", 23 | "Programming Language :: Python :: 3", 24 | "Programming Language :: Python :: 3 :: Only", 25 | "Programming Language :: Python :: 3.8", 26 | "Programming Language :: Python :: 3.9", 27 | "Programming Language :: Python :: 3.10", 28 | "Programming Language :: Python :: 3.11", 29 | "Programming Language :: Python :: 3.12", 30 | "Programming Language :: Python :: Implementation :: PyPy", 31 | "Programming Language :: Python :: Implementation :: CPython", 32 | ] 33 | packages = [{ include = "aiormq" }] 34 | 35 | [tool.poetry.urls] 36 | "Source" = "https://github.com/mosquito/aiormq" 37 | "Tracker" = "https://github.com/mosquito/aiormq/issues" 38 | "Documentation" = "https://github.com/mosquito/aiormq/blob/master/README.rst" 39 | 40 | [tool.poetry.dependencies] 41 | python = "^3.8" 42 | pamqp = "3.3.0" 43 | setuptools = [{ version = '*', python = "< 3.8" }] 44 | yarl = [{ version = '*'}] 45 | 46 | [tool.poetry.group.dev.dependencies] 47 | pytest = "^7.4.4" 48 | coverage = "^6.5.0" 49 | coveralls = "^3.3.1" 50 | pylama = "^8.4.1" 51 | pytest-cov = "^4.0.0" 52 | collective-checkdocs = "^0.2" 53 | mypy = "^0.991" 54 | pytest-rst = "^0.0.7" 55 | types-setuptools = "^65.6.0.2" 56 | aiomisc-pytest = "^1.1.1" 57 | setuptools = "^69.0.3" 58 | 59 | [tool.poetry.group.uvloop.dependencies] 60 | uvloop = ["^0.18"] 61 | 62 | [build-system] 63 | requires = ["poetry-core"] 64 | build-backend = "poetry.core.masonry.api" 65 | 66 | [tool.mypy] 67 | check_untyped_defs = true 68 | disallow_any_generics = false 69 | disallow_incomplete_defs = true 70 | disallow_subclassing_any = true 71 | disallow_untyped_calls = true 72 | disallow_untyped_decorators = true 73 | disallow_untyped_defs = true 74 | follow_imports = "silent" 75 | no_implicit_reexport = true 76 | strict_optional = true 77 | warn_redundant_casts = true 78 | warn_unused_configs = true 79 | warn_unused_ignores = false 80 | files = "aiormq" 81 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mosquito/aiormq/120965c68a18db7dcab6da2b7c66d4b2a86dea45/tests/__init__.py -------------------------------------------------------------------------------- /tests/certs/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rabbitmq:3.8-management-alpine 2 | 3 | RUN mkdir -p /certs/ 4 | 5 | COPY tests/certs/ca.pem /certs/ 6 | COPY tests/certs/server.key /certs/ 7 | COPY tests/certs/server.pem /certs/ 8 | 9 | ENV RABBITMQ_SSL_CERTFILE=/certs/server.pem 10 | ENV RABBITMQ_SSL_KEYFILE=/certs/server.key 11 | ENV RABBITMQ_SSL_CACERTFILE=/certs/ca.pem 12 | ENV RABBITMQ_SSL_FAIL_IF_NO_PEER_CERT=false 13 | 14 | ENV RABBITMQ_DEFAULT_USER=guest 15 | ENV RABBITMQ_DEFAULT_PASS=guest 16 | -------------------------------------------------------------------------------- /tests/certs/ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 34 | -----END CERTIFICATE----- 35 | -------------------------------------------------------------------------------- /tests/certs/client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | 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 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /tests/certs/client.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 34 | -----END CERTIFICATE----- 35 | -----BEGIN CERTIFICATE----- 36 | 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 68 | -----END CERTIFICATE----- 69 | -------------------------------------------------------------------------------- /tests/certs/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | 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 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /tests/certs/server.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 34 | MvekAEBEOasl6ZnGM/QQBN1o 35 | -----END CERTIFICATE----- 36 | -----BEGIN CERTIFICATE----- 37 | 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 69 | -----END CERTIFICATE----- 70 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import gc 3 | import logging 4 | import os 5 | import tracemalloc 6 | 7 | import pamqp 8 | import pytest 9 | from aiomisc_pytest import TCPProxy 10 | from yarl import URL 11 | 12 | from aiormq import Connection 13 | 14 | 15 | def cert_path(*args): 16 | return os.path.join( 17 | os.path.abspath(os.path.dirname(__file__)), "certs", *args, 18 | ) 19 | 20 | 21 | AMQP_URL = URL(os.getenv("AMQP_URL", "amqp://guest:guest@localhost/")) 22 | 23 | amqp_urls = { 24 | "amqp": AMQP_URL, 25 | "amqp-named": AMQP_URL.update_query(name="pytest"), 26 | "amqps": AMQP_URL.with_scheme("amqps").with_query( 27 | {"cafile": cert_path("ca.pem"), "no_verify_ssl": 1}, 28 | ), 29 | "amqps-client": AMQP_URL.with_scheme("amqps").with_query( 30 | { 31 | "cafile": cert_path("ca.pem"), 32 | "keyfile": cert_path("client.key"), 33 | "certfile": cert_path("client.pem"), 34 | "no_verify_ssl": 1, 35 | }, 36 | ), 37 | } 38 | 39 | 40 | amqp_url_list, amqp_url_ids = [], [] 41 | 42 | for name, url in amqp_urls.items(): 43 | amqp_url_list.append(url) 44 | amqp_url_ids.append(name) 45 | 46 | 47 | @pytest.fixture(params=amqp_url_list, ids=amqp_url_ids) 48 | async def amqp_url(request): 49 | return request.param 50 | 51 | 52 | @pytest.fixture 53 | async def amqp_connection(amqp_url, loop): 54 | connection = Connection(amqp_url, loop=loop) 55 | async with connection: 56 | yield connection 57 | 58 | 59 | channel_params = [ 60 | dict(channel_number=None, frame_buffer_size=10, publisher_confirms=True), 61 | dict(channel_number=None, frame_buffer_size=1, publisher_confirms=True), 62 | dict(channel_number=None, frame_buffer_size=10, publisher_confirms=False), 63 | dict(channel_number=None, frame_buffer_size=1, publisher_confirms=False), 64 | ] 65 | 66 | 67 | @pytest.fixture(params=channel_params) 68 | async def amqp_channel(request, amqp_connection): 69 | try: 70 | yield await amqp_connection.channel(**request.param) 71 | finally: 72 | await amqp_connection.close() 73 | 74 | 75 | skip_when_quick_test = pytest.mark.skipif( 76 | os.getenv("TEST_QUICK") is not None, reason="quick test", 77 | ) 78 | 79 | 80 | @pytest.fixture(autouse=True) 81 | def memory_tracer(): 82 | tracemalloc.start() 83 | tracemalloc.clear_traces() 84 | 85 | filters = ( 86 | tracemalloc.Filter(True, pamqp.__file__), 87 | tracemalloc.Filter(True, asyncio.__file__), 88 | ) 89 | 90 | snapshot_before = tracemalloc.take_snapshot().filter_traces(filters) 91 | 92 | def format_stat(stats): 93 | items = [ 94 | "TOP STATS:", 95 | "%-90s %6s %6s %6s" % ("Traceback", "line", "size", "count"), 96 | ] 97 | 98 | for stat in stats: 99 | fname = stat.traceback[0].filename 100 | lineno = stat.traceback[0].lineno 101 | items.append( 102 | "%-90s %6s %6s %6s" 103 | % (fname, lineno, stat.size_diff, stat.count_diff), 104 | ) 105 | 106 | return "\n".join(items) 107 | 108 | try: 109 | yield 110 | 111 | gc.collect() 112 | 113 | snapshot_after = tracemalloc.take_snapshot().filter_traces(filters) 114 | 115 | top_stats = snapshot_after.compare_to( 116 | snapshot_before, "lineno", cumulative=True, 117 | ) 118 | 119 | if top_stats: 120 | logging.error(format_stat(top_stats)) 121 | raise AssertionError("Possible memory leak") 122 | finally: 123 | tracemalloc.stop() 124 | 125 | 126 | @pytest.fixture() 127 | async def proxy(tcp_proxy, localhost, amqp_url: URL): 128 | port = amqp_url.port or 5672 if amqp_url.scheme == "amqp" else 5671 129 | async with tcp_proxy(amqp_url.host, port) as proxy: 130 | yield proxy 131 | 132 | 133 | @pytest.fixture 134 | async def proxy_connection(proxy: TCPProxy, amqp_url: URL, loop): 135 | url = amqp_url.with_host( 136 | "localhost", 137 | ).with_port( 138 | proxy.proxy_port, 139 | ) 140 | connection = Connection(url, loop=loop) 141 | 142 | await connection.connect() 143 | 144 | try: 145 | yield connection 146 | finally: 147 | await connection.close() 148 | -------------------------------------------------------------------------------- /tests/test_channel.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import uuid 3 | from os import urandom 4 | 5 | import pytest 6 | from aiomisc_pytest import TCPProxy 7 | 8 | import aiormq 9 | from aiormq.abc import DeliveredMessage 10 | 11 | 12 | async def test_simple(amqp_channel: aiormq.Channel): 13 | await amqp_channel.basic_qos(prefetch_count=1) 14 | assert amqp_channel.number 15 | 16 | queue = asyncio.Queue() 17 | 18 | deaclare_ok = await amqp_channel.queue_declare(auto_delete=True) 19 | consume_ok = await amqp_channel.basic_consume(deaclare_ok.queue, queue.put) 20 | await amqp_channel.basic_publish( 21 | b"foo", 22 | routing_key=deaclare_ok.queue, 23 | properties=aiormq.spec.Basic.Properties(message_id="123"), 24 | ) 25 | 26 | message: DeliveredMessage = await queue.get() 27 | assert message.body == b"foo" 28 | 29 | cancel_ok = await amqp_channel.basic_cancel(consume_ok.consumer_tag) 30 | assert cancel_ok.consumer_tag == consume_ok.consumer_tag 31 | assert cancel_ok.consumer_tag not in amqp_channel.consumers 32 | await amqp_channel.queue_delete(deaclare_ok.queue) 33 | 34 | deaclare_ok = await amqp_channel.queue_declare(auto_delete=True) 35 | await amqp_channel.basic_publish(b"foo bar", routing_key=deaclare_ok.queue) 36 | 37 | message = await amqp_channel.basic_get(deaclare_ok.queue, no_ack=True) 38 | assert message.body == b"foo bar" 39 | 40 | 41 | async def test_blank_body(amqp_channel: aiormq.Channel): 42 | await amqp_channel.basic_qos(prefetch_count=1) 43 | assert amqp_channel.number 44 | 45 | queue = asyncio.Queue() 46 | 47 | deaclare_ok = await amqp_channel.queue_declare(auto_delete=True) 48 | consume_ok = await amqp_channel.basic_consume(deaclare_ok.queue, queue.put) 49 | await amqp_channel.basic_publish( 50 | b"", 51 | routing_key=deaclare_ok.queue, 52 | properties=aiormq.spec.Basic.Properties(message_id="123"), 53 | ) 54 | 55 | message: DeliveredMessage = await queue.get() 56 | assert message.body == b"" 57 | 58 | cancel_ok = await amqp_channel.basic_cancel(consume_ok.consumer_tag) 59 | assert cancel_ok.consumer_tag == consume_ok.consumer_tag 60 | assert cancel_ok.consumer_tag not in amqp_channel.consumers 61 | await amqp_channel.queue_delete(deaclare_ok.queue) 62 | 63 | deaclare_ok = await amqp_channel.queue_declare(auto_delete=True) 64 | await amqp_channel.basic_publish(b"foo bar", routing_key=deaclare_ok.queue) 65 | 66 | message = await amqp_channel.basic_get(deaclare_ok.queue, no_ack=True) 67 | assert message.body == b"foo bar" 68 | 69 | 70 | async def test_bad_consumer(amqp_channel: aiormq.Channel, loop): 71 | channel: aiormq.Channel = amqp_channel 72 | await channel.basic_qos(prefetch_count=1) 73 | 74 | declare_ok = await channel.queue_declare() 75 | 76 | future = loop.create_future() 77 | 78 | await channel.basic_publish(b"urgent", routing_key=declare_ok.queue) 79 | 80 | consumer_tag = loop.create_future() 81 | 82 | async def bad_consumer(message): 83 | await channel.basic_cancel(await consumer_tag) 84 | future.set_result(message) 85 | raise Exception 86 | 87 | consume_ok = await channel.basic_consume( 88 | declare_ok.queue, bad_consumer, no_ack=False, 89 | ) 90 | 91 | consumer_tag.set_result(consume_ok.consumer_tag) 92 | 93 | message = await future 94 | await channel.basic_reject(message.delivery.delivery_tag, requeue=True) 95 | assert message.body == b"urgent" 96 | 97 | future = loop.create_future() 98 | 99 | await channel.basic_consume( 100 | declare_ok.queue, future.set_result, no_ack=True, 101 | ) 102 | 103 | message = await future 104 | 105 | assert message.body == b"urgent" 106 | assert message.delivery_tag is not None 107 | assert message.exchange is not None 108 | assert message.redelivered 109 | 110 | 111 | async def test_ack_nack_reject(amqp_channel: aiormq.Channel): 112 | channel: aiormq.Channel = amqp_channel 113 | await channel.basic_qos(prefetch_count=1) 114 | 115 | declare_ok = await channel.queue_declare(auto_delete=True) 116 | queue = asyncio.Queue() 117 | 118 | await channel.basic_consume(declare_ok.queue, queue.put, no_ack=False) 119 | 120 | await channel.basic_publish(b"rejected", routing_key=declare_ok.queue) 121 | message: DeliveredMessage = await queue.get() 122 | assert message.body == b"rejected" 123 | assert message.delivery_tag is not None 124 | assert message.exchange is not None 125 | assert not message.redelivered 126 | await channel.basic_reject(message.delivery.delivery_tag, requeue=False) 127 | 128 | await channel.basic_publish(b"nacked", routing_key=declare_ok.queue) 129 | message = await queue.get() 130 | assert message.body == b"nacked" 131 | await channel.basic_nack(message.delivery.delivery_tag, requeue=False) 132 | 133 | await channel.basic_publish(b"acked", routing_key=declare_ok.queue) 134 | message = await queue.get() 135 | assert message.body == b"acked" 136 | await channel.basic_ack(message.delivery.delivery_tag) 137 | 138 | 139 | async def test_confirm_multiple(amqp_channel: aiormq.Channel): 140 | """ 141 | RabbitMQ has been observed to send confirmations in a strange pattern 142 | when publishing simultaneously where only some messages are delivered 143 | to a queue. It sends acks like this 1 2 4 5(multiple, confirming also 3). 144 | This test is probably inconsequential without publisher_confirms 145 | This is a regression for https://github.com/mosquito/aiormq/issues/10 146 | """ 147 | channel: aiormq.Channel = amqp_channel 148 | exchange = uuid.uuid4().hex 149 | await channel.exchange_declare(exchange, exchange_type="topic") 150 | try: 151 | declare_ok = await channel.queue_declare(exclusive=True) 152 | await channel.queue_bind( 153 | declare_ok.queue, exchange, routing_key="test.5", 154 | ) 155 | 156 | for i in range(10): 157 | messages = [ 158 | asyncio.ensure_future( 159 | channel.basic_publish( 160 | b"test", exchange=exchange, 161 | routing_key="test.{}".format(i), 162 | ), 163 | ) 164 | for i in range(10) 165 | ] 166 | _, pending = await asyncio.wait(messages, timeout=0.2) 167 | assert not pending, "not all publishes were completed (confirmed)" 168 | await asyncio.sleep(0.05) 169 | finally: 170 | await channel.exchange_delete(exchange) 171 | 172 | 173 | async def test_exclusive_queue_locked(amqp_connection): 174 | channel0 = await amqp_connection.channel() 175 | channel1 = await amqp_connection.channel() 176 | 177 | qname = str(uuid.uuid4()) 178 | 179 | await channel0.queue_declare(qname, exclusive=True) 180 | 181 | try: 182 | await channel0.basic_consume(qname, print, exclusive=True) 183 | 184 | with pytest.raises(aiormq.exceptions.ChannelLockedResource): 185 | await channel1.queue_declare(qname) 186 | await channel1.basic_consume(qname, print, exclusive=True) 187 | finally: 188 | await channel0.queue_delete(qname) 189 | 190 | 191 | async def test_remove_writer_when_closed(amqp_channel: aiormq.Channel): 192 | with pytest.raises(aiormq.exceptions.ChannelClosed): 193 | await amqp_channel.queue_declare( 194 | "amq.forbidden_queue_name", auto_delete=True, 195 | ) 196 | 197 | with pytest.raises(aiormq.exceptions.ChannelInvalidStateError): 198 | await amqp_channel.queue_delete("amq.forbidden_queue_name") 199 | 200 | 201 | async def test_proxy_connection(proxy_connection, proxy: TCPProxy): 202 | channel: aiormq.Channel = await proxy_connection.channel() 203 | await channel.queue_declare(auto_delete=True) 204 | 205 | 206 | async def test_declare_queue_timeout(proxy_connection, proxy: TCPProxy): 207 | for _ in range(3): 208 | channel: aiormq.Channel = await proxy_connection.channel() 209 | 210 | qname = str(uuid.uuid4()) 211 | 212 | with proxy.slowdown(read_delay=5, write_delay=0): 213 | with pytest.raises(asyncio.TimeoutError): 214 | await channel.queue_declare( 215 | qname, auto_delete=True, timeout=0.5, 216 | ) 217 | 218 | 219 | async def test_big_message(amqp_channel: aiormq.Channel): 220 | size = 20 * 1024 * 1024 221 | message = urandom(size) 222 | await amqp_channel.basic_publish(message) 223 | 224 | 225 | async def test_routing_key_too_large(amqp_channel: aiormq.Channel): 226 | routing_key = "x" * 256 227 | 228 | with pytest.raises(ValueError): 229 | await amqp_channel.basic_publish(b"foo bar", routing_key=routing_key) 230 | 231 | exchange = uuid.uuid4().hex 232 | await amqp_channel.exchange_declare(exchange, exchange_type="topic") 233 | 234 | with pytest.raises(ValueError): 235 | await amqp_channel.exchange_bind(exchange, exchange, routing_key) 236 | 237 | with pytest.raises(ValueError): 238 | await amqp_channel.exchange_unbind(exchange, exchange, routing_key) 239 | 240 | queue = await amqp_channel.queue_declare(exclusive=True) 241 | 242 | with pytest.raises(ValueError): 243 | await amqp_channel.queue_bind(queue.queue, exchange, routing_key) 244 | 245 | with pytest.raises(ValueError): 246 | await amqp_channel.queue_unbind(queue.queue, exchange, routing_key) 247 | 248 | await amqp_channel.exchange_delete(exchange) 249 | -------------------------------------------------------------------------------- /tests/test_connection.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import itertools 3 | import os 4 | import ssl 5 | import uuid 6 | from binascii import hexlify 7 | from typing import Optional 8 | 9 | import aiomisc 10 | import pytest 11 | from pamqp.commands import Basic 12 | from yarl import URL 13 | 14 | import aiormq 15 | from aiormq.abc import DeliveredMessage 16 | from aiormq.auth import AuthBase, ExternalAuth, PlainAuth 17 | from aiormq.connection import parse_int, parse_timeout, parse_bool 18 | 19 | from .conftest import AMQP_URL, cert_path, skip_when_quick_test 20 | 21 | 22 | CERT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "certs")) 23 | 24 | 25 | async def test_simple(amqp_connection: aiormq.Connection): 26 | channel1 = await amqp_connection.channel() 27 | await channel1.basic_qos(prefetch_count=1) 28 | 29 | assert channel1.number 30 | 31 | channel2 = await amqp_connection.channel(11) 32 | assert channel2.number 33 | 34 | await channel1.close() 35 | await channel2.close() 36 | 37 | channel = await amqp_connection.channel() 38 | 39 | queue = asyncio.Queue() 40 | 41 | deaclare_ok = await channel.queue_declare(auto_delete=True) 42 | consume_ok = await channel.basic_consume(deaclare_ok.queue, queue.put) 43 | await channel.basic_publish(b"foo", routing_key=deaclare_ok.queue) 44 | 45 | message: DeliveredMessage = await queue.get() 46 | assert message.body == b"foo" 47 | 48 | with pytest.raises(aiormq.exceptions.PublishError) as e: 49 | await channel.basic_publish( 50 | b"bar", routing_key=deaclare_ok.queue + "foo", mandatory=True, 51 | ) 52 | 53 | message = e.value.message 54 | 55 | assert message.delivery.routing_key == deaclare_ok.queue + "foo" 56 | assert message.body == b"bar" 57 | assert "'NO_ROUTE' for routing key" in repr(e.value) 58 | 59 | cancel_ok = await channel.basic_cancel(consume_ok.consumer_tag) 60 | assert cancel_ok.consumer_tag == consume_ok.consumer_tag 61 | await channel.queue_delete(deaclare_ok.queue) 62 | 63 | deaclare_ok = await channel.queue_declare(auto_delete=True) 64 | await channel.basic_publish(b"foo bar", routing_key=deaclare_ok.queue) 65 | 66 | message = await channel.basic_get(deaclare_ok.queue, no_ack=True) 67 | assert message.body == b"foo bar" 68 | 69 | await amqp_connection.close() 70 | 71 | with pytest.raises(RuntimeError): 72 | await channel.basic_get(deaclare_ok.queue) 73 | 74 | with pytest.raises(RuntimeError): 75 | await amqp_connection.channel() 76 | 77 | 78 | async def test_channel_reuse(amqp_connection: aiormq.Connection): 79 | for _ in range(10): 80 | channel = await amqp_connection.channel(channel_number=1) 81 | await channel.basic_qos(prefetch_count=1) 82 | await channel.close() 83 | del channel 84 | 85 | 86 | async def test_channel_closed_reuse(amqp_connection: aiormq.Connection): 87 | for _ in range(10): 88 | channel = await amqp_connection.channel(channel_number=1) 89 | with pytest.raises(aiormq.ChannelAccessRefused): 90 | await channel.exchange_declare("", passive=True, auto_delete=True) 91 | 92 | 93 | async def test_bad_channel(amqp_connection: aiormq.Connection): 94 | with pytest.raises(ValueError): 95 | await amqp_connection.channel(65536) 96 | 97 | with pytest.raises(ValueError): 98 | await amqp_connection.channel(-1) 99 | 100 | channel = await amqp_connection.channel(65535) 101 | 102 | with pytest.raises(aiormq.exceptions.ChannelNotFoundEntity): 103 | await channel.queue_bind(uuid.uuid4().hex, uuid.uuid4().hex) 104 | 105 | 106 | async def test_properties(amqp_connection): 107 | assert amqp_connection.connection_tune.channel_max > 0 108 | assert amqp_connection.connection_tune.heartbeat > 1 109 | assert amqp_connection.connection_tune.frame_max > 1024 110 | await amqp_connection.close() 111 | 112 | 113 | async def test_open(amqp_connection): 114 | with pytest.raises(RuntimeError): 115 | await amqp_connection.connect() 116 | 117 | channel = await amqp_connection.channel() 118 | await channel.close() 119 | await amqp_connection.close() 120 | 121 | 122 | async def test_channel_close(amqp_connection): 123 | channel = await amqp_connection.channel() 124 | 125 | assert channel.number in amqp_connection.channels 126 | 127 | await channel.close() 128 | 129 | assert channel.number not in amqp_connection.channels 130 | 131 | 132 | async def test_conncetion_reject(loop): 133 | with pytest.raises(ConnectionError): 134 | await aiormq.connect( 135 | "amqp://guest:guest@127.0.0.1:59999/", loop=loop, 136 | ) 137 | 138 | connection = aiormq.Connection( 139 | "amqp://guest:guest@127.0.0.1:59999/", loop=loop, 140 | ) 141 | 142 | with pytest.raises(ConnectionError): 143 | await loop.create_task(connection.connect()) 144 | 145 | 146 | async def test_auth_base(amqp_connection): 147 | with pytest.raises(NotImplementedError): 148 | AuthBase(amqp_connection).marshal() 149 | 150 | 151 | async def test_auth_plain(amqp_connection, loop): 152 | auth = PlainAuth(amqp_connection).marshal() 153 | 154 | assert auth == PlainAuth(amqp_connection).marshal() 155 | 156 | auth_parts = auth.split("\x00") 157 | assert auth_parts == ["", "guest", "guest"] 158 | 159 | connection = aiormq.Connection( 160 | amqp_connection.url.with_user("foo").with_password("bar"), 161 | loop=loop, 162 | ) 163 | 164 | auth = PlainAuth(connection).marshal() 165 | 166 | auth_parts = auth.split("\x00") 167 | assert auth_parts == ["", "foo", "bar"] 168 | 169 | auth = PlainAuth(connection) 170 | auth.value = "boo" 171 | 172 | assert auth.marshal() == "boo" 173 | 174 | 175 | async def test_auth_external(loop): 176 | 177 | url = AMQP_URL.with_scheme("amqps") 178 | url.update_query(auth="external") 179 | 180 | connection = aiormq.Connection 181 | 182 | auth = ExternalAuth(connection).marshal() 183 | 184 | auth = ExternalAuth(connection) 185 | auth.value = "" 186 | 187 | assert auth.marshal() == "" 188 | 189 | 190 | async def test_channel_closed(amqp_connection): 191 | 192 | for i in range(10): 193 | channel = await amqp_connection.channel() 194 | 195 | with pytest.raises(aiormq.exceptions.ChannelNotFoundEntity): 196 | await channel.basic_consume("foo", lambda x: None) 197 | 198 | channel = await amqp_connection.channel() 199 | 200 | with pytest.raises(aiormq.exceptions.ChannelNotFoundEntity): 201 | await channel.queue_declare( 202 | "foo_%s" % i, auto_delete=True, passive=True, 203 | ) 204 | 205 | await amqp_connection.close() 206 | 207 | 208 | async def test_timeout_default(loop): 209 | connection = aiormq.Connection(AMQP_URL, loop=loop) 210 | await connection.connect() 211 | assert connection.timeout == 60 212 | await connection.close() 213 | 214 | 215 | async def test_timeout_1000(loop): 216 | url = AMQP_URL.update_query(timeout=1000) 217 | connection = aiormq.Connection(url, loop=loop) 218 | await connection.connect() 219 | assert connection.timeout 220 | await connection.close() 221 | 222 | 223 | async def test_heartbeat_0(loop): 224 | url = AMQP_URL.update_query(heartbeat=0) 225 | connection = aiormq.Connection(url, loop=loop) 226 | await connection.connect() 227 | assert connection.connection_tune.heartbeat == 0 228 | await connection.close() 229 | 230 | 231 | async def test_heartbeat_default(loop): 232 | connection = aiormq.Connection(AMQP_URL, loop=loop) 233 | await connection.connect() 234 | assert connection.connection_tune.heartbeat == 60 235 | await connection.close() 236 | 237 | 238 | async def test_heartbeat_above_range(loop): 239 | url = AMQP_URL.update_query(heartbeat=70000) 240 | connection = aiormq.Connection(url, loop=loop) 241 | await connection.connect() 242 | assert connection.connection_tune.heartbeat == 0 243 | await connection.close() 244 | 245 | 246 | async def test_heartbeat_under_range(loop): 247 | url = AMQP_URL.update_query(heartbeat=-1) 248 | connection = aiormq.Connection(url, loop=loop) 249 | await connection.connect() 250 | assert connection.connection_tune.heartbeat == 0 251 | await connection.close() 252 | 253 | 254 | async def test_heartbeat_not_int(loop): 255 | url = AMQP_URL.update_query(heartbeat="None") 256 | connection = aiormq.Connection(url, loop=loop) 257 | await connection.connect() 258 | assert connection.connection_tune.heartbeat == 0 259 | await connection.close() 260 | 261 | 262 | async def test_bad_credentials(amqp_url: URL): 263 | with pytest.raises(aiormq.exceptions.ProbableAuthenticationError): 264 | await aiormq.connect(amqp_url.with_password(uuid.uuid4().hex)) 265 | 266 | 267 | async def test_non_publisher_confirms(amqp_connection): 268 | amqp_connection.server_capabilities["publisher_confirms"] = False 269 | 270 | with pytest.raises(ValueError): 271 | await amqp_connection.channel(publisher_confirms=True) 272 | 273 | await amqp_connection.channel(publisher_confirms=False) 274 | 275 | 276 | @skip_when_quick_test 277 | async def test_no_free_channels(amqp_connection: aiormq.Connection): 278 | await asyncio.wait_for( 279 | asyncio.gather( 280 | *[ 281 | amqp_connection.channel(n + 1) 282 | for n in range(amqp_connection.connection_tune.channel_max) 283 | ], 284 | ), 285 | timeout=120, 286 | ) 287 | 288 | with pytest.raises(aiormq.exceptions.ConnectionNotAllowed): 289 | await asyncio.wait_for(amqp_connection.channel(), timeout=5) 290 | 291 | 292 | async def test_huge_message(amqp_connection: aiormq.Connection): 293 | conn: aiormq.Connection = amqp_connection 294 | body = os.urandom(int(conn.connection_tune.frame_max * 2.5)) 295 | channel: aiormq.Channel = await conn.channel() 296 | 297 | queue = asyncio.Queue() 298 | 299 | deaclare_ok = await channel.queue_declare(auto_delete=True) 300 | await channel.basic_consume(deaclare_ok.queue, queue.put) 301 | await channel.basic_publish(body, routing_key=deaclare_ok.queue) 302 | 303 | message: DeliveredMessage = await queue.get() 304 | assert message.body == body 305 | 306 | 307 | async def test_return_message(amqp_connection: aiormq.Connection): 308 | conn: aiormq.Connection = amqp_connection 309 | body = os.urandom(512) 310 | routing_key = hexlify(os.urandom(16)).decode() 311 | 312 | channel: aiormq.Channel = await conn.channel( 313 | on_return_raises=False, 314 | ) 315 | 316 | result: Optional[Basic.Return] = await channel.basic_publish( 317 | body, routing_key=routing_key, mandatory=True, 318 | ) 319 | 320 | assert result is not None 321 | 322 | assert result.delivery.name == "Basic.Return" 323 | assert result.delivery.routing_key == routing_key 324 | 325 | 326 | async def test_cancel_on_queue_deleted(amqp_connection, loop): 327 | conn: aiormq.Connection = amqp_connection 328 | channel: aiormq.Channel = await conn.channel() 329 | deaclare_ok = await channel.queue_declare(auto_delete=True) 330 | consume_ok = await channel.basic_consume(deaclare_ok.queue, print) 331 | 332 | assert consume_ok.consumer_tag in channel.consumers 333 | 334 | with pytest.raises(aiormq.DuplicateConsumerTag): 335 | await channel.basic_consume( 336 | deaclare_ok.queue, print, consumer_tag=consume_ok.consumer_tag, 337 | ) 338 | 339 | await channel.queue_delete(deaclare_ok.queue) 340 | 341 | await asyncio.sleep(0.1) 342 | 343 | assert consume_ok.consumer_tag not in channel.consumers 344 | 345 | 346 | URL_VHOSTS = [ 347 | ("amqp:///", "/"), 348 | ("amqp:////", "/"), 349 | ("amqp:///test", "test"), 350 | ("amqp:////test", "/test"), 351 | ("amqp://localhost/test", "test"), 352 | ("amqp://localhost//test", "/test"), 353 | ("amqps://localhost:5678//test", "/test"), 354 | ("amqps://localhost:5678/-test", "-test"), 355 | ("amqp://guest:guest@localhost/@test", "@test"), 356 | ("amqp://guest:guest@localhost//@test", "/@test"), 357 | ] 358 | 359 | 360 | async def test_ssl_verification_fails_without_trusted_ca(): 361 | url = AMQP_URL.with_scheme("amqps") 362 | with pytest.raises(ConnectionError, match=".*CERTIFICATE_VERIFY_FAILED.*"): 363 | connection = aiormq.Connection(url) 364 | await connection.connect() 365 | 366 | 367 | async def test_ssl_context(): 368 | url = AMQP_URL.with_scheme("amqps") 369 | context = ssl.create_default_context( 370 | purpose=ssl.Purpose.SERVER_AUTH, 371 | cafile=cert_path("ca.pem"), 372 | ) 373 | context.load_cert_chain(cert_path("client.pem"), cert_path("client.key")) 374 | context.check_hostname = False 375 | connection = aiormq.Connection(url, context=context) 376 | await connection.connect() 377 | await connection.close() 378 | 379 | 380 | @pytest.mark.parametrize("url,vhost", URL_VHOSTS) 381 | async def test_connection_urls_vhosts(url, vhost, loop): 382 | assert aiormq.Connection(url, loop=loop).vhost == vhost 383 | 384 | 385 | async def test_update_secret(amqp_connection, amqp_url: URL): 386 | respone = await amqp_connection.update_secret( 387 | amqp_url.password, timeout=1, 388 | ) 389 | assert isinstance(respone, aiormq.spec.Connection.UpdateSecretOk) 390 | 391 | 392 | @aiomisc.timeout(20) 393 | async def test_connection_stuck(proxy, amqp_url: URL): 394 | url = amqp_url.with_host( 395 | proxy.proxy_host, 396 | ).with_port( 397 | proxy.proxy_port, 398 | ).update_query(heartbeat="1") 399 | 400 | connection = await aiormq.connect(url) 401 | 402 | async with connection: 403 | # delay the delivery of each packet by 5 seconds, which 404 | # is more than the heartbeat 405 | with proxy.slowdown(50, 50): 406 | while connection.is_opened: 407 | await asyncio.sleep(1) 408 | 409 | writer_task: asyncio.Task = connection._writer_task # type: ignore 410 | 411 | assert writer_task.done() 412 | 413 | with pytest.raises(asyncio.CancelledError): 414 | assert writer_task.result() 415 | 416 | reader_task: asyncio.Task = connection._reader_task # type: ignore 417 | 418 | assert reader_task.done() 419 | 420 | with pytest.raises(asyncio.CancelledError): 421 | assert reader_task.result() 422 | 423 | 424 | class BadNetwork: 425 | def __init__(self, proxy, stair: int, disconnect_time: float): 426 | self.proxy = proxy 427 | self.stair = stair 428 | self.disconnect_time = disconnect_time 429 | self.num_bytes = 0 430 | self.loop = asyncio.get_event_loop() 431 | self.lock = asyncio.Lock() 432 | 433 | proxy.set_content_processors( 434 | self.client_to_server, 435 | self.server_to_client, 436 | ) 437 | 438 | async def disconnect(self): 439 | async with self.lock: 440 | await asyncio.sleep(self.disconnect_time) 441 | await self.proxy.disconnect_all() 442 | self.stair *= 2 443 | self.num_bytes = 0 444 | 445 | async def server_to_client(self, chunk: bytes) -> bytes: 446 | async with self.lock: 447 | self.num_bytes += len(chunk) 448 | if self.num_bytes < self.stair: 449 | return chunk 450 | self.loop.create_task(self.disconnect()) 451 | return chunk 452 | 453 | @staticmethod 454 | def client_to_server(chunk: bytes) -> bytes: 455 | return chunk 456 | 457 | 458 | DISCONNECT_OFFSETS = [2 << i for i in range(1, 10)] 459 | STAIR_STEPS = list( 460 | itertools.product([0.0, 0.005, 0.05, 0.1], DISCONNECT_OFFSETS), 461 | ) 462 | STAIR_STEPS_IDS = [ 463 | f"[{i // len(DISCONNECT_OFFSETS)}] {t}-{s}" 464 | for i, (t, s) in enumerate(STAIR_STEPS) 465 | ] 466 | 467 | 468 | @aiomisc.timeout(30) 469 | @pytest.mark.parametrize( 470 | "disconnect_time,stair", STAIR_STEPS, 471 | ids=STAIR_STEPS_IDS, 472 | ) 473 | async def test_connection_close_stairway( 474 | disconnect_time: float, stair: int, proxy, amqp_url: URL, 475 | ): 476 | url = amqp_url.with_host( 477 | proxy.proxy_host, 478 | ).with_port( 479 | proxy.proxy_port, 480 | ).update_query(heartbeat="1") 481 | 482 | BadNetwork(proxy, stair, disconnect_time) 483 | 484 | async def run(): 485 | connection = await aiormq.connect(url) 486 | queue = asyncio.Queue() 487 | channel = await connection.channel() 488 | declare_ok = await channel.queue_declare(auto_delete=True) 489 | await channel.basic_consume( 490 | declare_ok.queue, queue.put, no_ack=True, 491 | ) 492 | 493 | while True: 494 | await channel.basic_publish( 495 | b"test", routing_key=declare_ok.queue, 496 | ) 497 | message: DeliveredMessage = await queue.get() 498 | assert message.body == b"test" 499 | 500 | for _ in range(5): 501 | with pytest.raises(aiormq.AMQPError): 502 | await run() 503 | 504 | 505 | PARSE_INT_PARAMS = ( 506 | (1, 1), 507 | ("1", 1), 508 | ("0.1", 0), 509 | ("-1", -1), 510 | ) 511 | 512 | 513 | @pytest.mark.parametrize("value,expected", PARSE_INT_PARAMS) 514 | def test_parse_int(value, expected): 515 | assert parse_int(value) == expected 516 | 517 | 518 | PARSE_TIMEOUT_PARAMS = ( 519 | (1, 1), 520 | (1.0, 1), 521 | ("0", 0), 522 | ("0.0", 0), 523 | ("0.111", 0.111), 524 | ) 525 | 526 | 527 | @pytest.mark.parametrize("value,expected", PARSE_TIMEOUT_PARAMS) 528 | def test_parse_timeout(value, expected): 529 | assert parse_timeout(value) == expected 530 | 531 | 532 | PARSE_BOOL_PARAMS = ( 533 | ("no", False), 534 | ("nope", False), 535 | ("do not do it bro", False), 536 | ("YES", True), 537 | ("yes", True), 538 | ("yeS", True), 539 | ("yEs", True), 540 | ("True", True), 541 | ("true", True), 542 | ("TRUE", True), 543 | ("1", True), 544 | ("ENABLE", True), 545 | ("ENAbled", True), 546 | ("y", True), 547 | ("Y", True), 548 | ) 549 | 550 | 551 | @pytest.mark.parametrize("value,expected", PARSE_BOOL_PARAMS) 552 | def test_parse_bool(value, expected): 553 | assert parse_bool(value) == expected 554 | -------------------------------------------------------------------------------- /tests/test_future_store.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | from aiormq.abc import TaskWrapper 6 | from aiormq.base import FutureStore 7 | 8 | 9 | @pytest.fixture 10 | def root_store(loop): 11 | store = FutureStore(loop=loop) 12 | try: 13 | yield store 14 | finally: 15 | loop.run_until_complete( 16 | store.reject_all(Exception("Cancelling")), 17 | ) 18 | 19 | 20 | @pytest.fixture 21 | def child_store(loop, root_store): 22 | store = root_store.get_child() 23 | try: 24 | yield store 25 | finally: 26 | loop.run_until_complete( 27 | store.reject_all(Exception("Cancelling")), 28 | ) 29 | 30 | 31 | async def test_reject_all( 32 | loop, root_store: FutureStore, child_store: FutureStore, 33 | ): 34 | 35 | future1 = root_store.create_future() 36 | future2 = child_store.create_future() 37 | 38 | assert root_store.futures 39 | assert child_store.futures 40 | 41 | await root_store.reject_all(RuntimeError) 42 | await asyncio.sleep(0.1) 43 | 44 | assert isinstance(future1.exception(), RuntimeError) 45 | assert isinstance(future2.exception(), RuntimeError) 46 | assert not root_store.futures 47 | assert not child_store.futures 48 | 49 | 50 | async def test_result( 51 | loop, root_store: FutureStore, child_store: FutureStore, 52 | ): 53 | async def result(): 54 | await asyncio.sleep(0.1) 55 | return "result" 56 | 57 | assert await child_store.create_task(result()) == "result" 58 | 59 | 60 | async def test_siblings( 61 | loop, root_store: FutureStore, child_store: FutureStore, 62 | ): 63 | async def coro(store): 64 | await asyncio.sleep(0.1) 65 | await store.reject_all(RuntimeError) 66 | 67 | task1 = child_store.create_task(coro(child_store)) 68 | assert root_store.futures 69 | assert child_store.futures 70 | 71 | with pytest.raises(RuntimeError): 72 | await task1 73 | 74 | await asyncio.sleep(0.1) 75 | 76 | assert not root_store.futures 77 | assert not child_store.futures 78 | 79 | child = child_store.get_child().get_child().get_child() 80 | task = child.create_task(coro(child)) 81 | 82 | assert root_store.futures 83 | assert child_store.futures 84 | assert child.futures 85 | 86 | with pytest.raises(RuntimeError): 87 | await task 88 | 89 | await asyncio.sleep(0.1) 90 | 91 | assert not root_store.futures 92 | assert not child_store.futures 93 | assert not child.futures 94 | 95 | 96 | async def test_task_wrapper(loop): 97 | future = loop.create_future() 98 | wrapped = TaskWrapper(future) 99 | 100 | wrapped.throw(RuntimeError()) 101 | 102 | with pytest.raises(asyncio.CancelledError): 103 | await future 104 | 105 | with pytest.raises(RuntimeError): 106 | await wrapped 107 | -------------------------------------------------------------------------------- /tests/test_tools.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | from aiormq.tools import Countdown, awaitable 6 | 7 | 8 | def simple_func(): 9 | return 1 10 | 11 | 12 | def await_result_func(): 13 | return asyncio.sleep(0) 14 | 15 | 16 | async def await_func(): 17 | await asyncio.sleep(0) 18 | return 2 19 | 20 | 21 | def return_future(): 22 | loop = asyncio.get_event_loop() 23 | f = loop.create_future() 24 | loop.call_soon(f.set_result, 5) 25 | return f 26 | 27 | 28 | async def await_future(): 29 | return (await return_future()) + 1 30 | 31 | 32 | def return_coroutine(): 33 | return await_future() 34 | 35 | 36 | AWAITABLE_FUNCS = [ 37 | (simple_func, 1), 38 | (await_result_func, None), 39 | (await_func, 2), 40 | (return_future, 5), 41 | (await_future, 6), 42 | (return_coroutine, 6), 43 | ] 44 | 45 | 46 | @pytest.mark.parametrize("func,result", AWAITABLE_FUNCS) 47 | async def test_awaitable(func, result, loop): 48 | assert await awaitable(func)() == result 49 | 50 | 51 | async def test_countdown(loop): 52 | countdown = Countdown(timeout=0.1) 53 | await countdown(asyncio.sleep(0)) 54 | 55 | # waiting for the countdown exceeded 56 | await asyncio.sleep(0.2) 57 | 58 | task = asyncio.create_task(asyncio.sleep(0)) 59 | 60 | with pytest.raises(asyncio.TimeoutError): 61 | await countdown(task) 62 | 63 | assert task.cancelled() 64 | --------------------------------------------------------------------------------