├── .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 | MIIJJgIBAAKCAgEAwaOK16e3rvrfO0b458LoLFEhjD0UQ8oPLYAonpIaNQb8DkLH
3 | Fd7JJdecFP4Iu2oB8SVp4ugbIYmCgpkrHUAucXutnuv+D7slMiWP3vDLpTYR2Nr/
4 | Lo9QCmGfASjKveDyva8luVUVnLs8Io396ogVK3oPbAaJ+q9dK5SwX8fyxBUWV/Dl
5 | NzgbBOBINAvO8fOAzqE+CIqxsZPpzOZN+ljolMGbvfDPrXeZ6W4NMXcDOyzbovjM
6 | Lt6sL8zh395lPJKUpk+ZDmpd3GiucNX/8tS5Eu8bp8IuaAD9z0xYlFGkKcyokv7q
7 | mNKaoPSxm/nVqgTzGs3PmWPFq++/wAhdJvJZ7cW8GAnOqkyULRiBvaIrx1JcOuS8
8 | ehQ9tHxKLW5C9VibVsZA2EkSuuCeavyYyvBZKdtFvZFOtiOt2WkflXakK7yFoOj+
9 | /XFF0qpp5aFycVQrnJQSDbzlpyTHIW0j9EuxpVfpNiOFBvCLVoCviq0Aia2Tapo1
10 | A0WxZA7FSXmxMRD7i79oqhzlKoXTYAc6bzDLOqjxYrTnjv18SqF2HEDtRLT7uxhh
11 | utMQYCwsfea3tzNXPgs+ffPS9H3lWM4JaytE2mFPUMvfUQaHMk8KsUTU/xfhLN1m
12 | vcgzqqKcxRl+qmLZOqkpUv39X9KKOIgMnPDY5ymYVRCWnr1FxcwCfa8Npk0CAwEA
13 | AQKCAgBjq08644wrV9vxQf26JVul+/idm47DucyIKhA+VouAweCZYovg2PSGMu2W
14 | 7I8IEG+BdTWEYt4cLBBuMnK7sp51MSjTxTrXVAe4QRdFtIHNvv/+s/JnP8L+JPNY
15 | AGwiwhePxQhQ1dey/bjdPGL3BiaHY2NuwgrhasQ1O2pxUpTFkukWSNtiydE2eE8R
16 | 4wYZCbJCKUKp2OHPuoe8PMrkUkEc2G7WnI35BrfFLC1ESbLzEYrX3uISOfE9BWM5
17 | /Nn1DKnQ1OW+QsefPI6Va8E7d3zvnv2IIu4KAICj4/MwHLm3/izCxM1x7e1Dbc/B
18 | rh3pTnTnVgpGNNG5R0VWjbeM5W+dizwRmur/KTa5irjAbooS/k7PXfK/6J8kEdb4
19 | lcxIvCpmYee5Zpl0AjzyUuQn7cX56c/Hnv5S3YqKI3oGfn3yGLsb7sZLcgQCypgM
20 | iNmYwCk0ZXzjZWAJ1O0WOelK6Yv0zTpkWhRoMAVCwK3pBH7C+MiO3FsDx9PiIM9r
21 | 6w2CkR8ije0jUykxERTJOlZe2tUEl8bDzmBOM6nF9TLd6uqpKjGcy8RNXNcWl5e7
22 | OfQKBP+O8HxoK8rxNZR/o4XqRhi2yriiJFdd5nL7zxRHNNSkLzVc1+tb9miXFBQw
23 | 5GX7zECIZ1U4kv0vg3SSGv5SBibDKCxsq5HaQBDyTO/NHn2U4QKCAQEA5XdVV7Sq
24 | YKON1gjYcezaH85SUq9gxs7Itv17uI5VV3TSD22yZ/HRIj0HbLAPZEGKkkDvLxp8
25 | cCwpdKU0FP5I+SVi/2jJNBqzMDdqipWlySUWU3EVYgp77n04QXJJauGoFTEpe9RT
26 | RZ0BzJ+Q9tvjwg/2o9HnJowjLCWSf9EHD56YiNzydZhy+AoYKegiDmjbh8XjFc/M
27 | BJAo/Hchq2GGZujplPqp0nHWRN07x+j/DGsMBWn192GxkGsoOoE+m3OHulODJ5kn
28 | 3+w9hahheZL5mZ4hEBR61VTsBV3AscXeUiK75+58ecyZwHB453y2IhRyXdtRAUdn
29 | fIhJahlLtZfEtQKCAQEA2AelwFdwkE20AS6JeRyTHPE8YqY7HcQvki/zjcP/Dava
30 | ZjqNr96Sq3Qq3auHU+tOBhQWUF7JHon0RWfuhoUHTJN4XxfcT3JocOjSKKR82M45
31 | lUJdz3NJ9JVticVgotllo3B7DmyqEms7hBJzpgp8OrsqpUyu7LQ1OxmWtDpilYxe
32 | XpCgJekns79h3wJhSBZhMBB+DprXl4hoOucr1qXH2HAgFkWc80Jb/CGicUKSxqEB
33 | BSzHXjQCsqoqcbf8ghL2Bm9W4apPoONK6+IMNomKR3r0NKbXhy969+USj9DG40Zk
34 | lsO8nQ3Orerd8tbCkQwxWlLkcayXpDkV01PvJqmyOQKB/1qHuiPgI1f9LvhChSJt
35 | T6E8xT3Z81R8QLPxTd6CSSk37agonzpjLR9U9Jjs3SWwtfr9o1/yEyYuRiy/AM1H
36 | hYLGPUiHDtp/rjJXqrECWWYCO8yv0L/dYwe0X31ymYSRgr7ZpoQ0QKY2S39vdMHv
37 | /uuRYL1BEvEiWL4SFLpYvXBsIcHdacr7WmCBmwbtjoIg3Hu0luMEGHm0Znc0iRQU
38 | ZfIz8fPU8SsVvnNs1SkJw5YipZt9Mo1m/ab8n+J1Gz45VlMsn5H/2rt9eMhCpjJQ
39 | yijROjod2lhQKM31LxDz/8Jn8bqPXIyxK/fAZ/LsQO8xIe3lmQ/oG+wF2PEDCdub
40 | BQKCAQB9GECVFo0qIrS/knEs3q0Zr1+mSFgnLnnVj0rbpslE42T+mZ1+X8ZS3lwM
41 | LM2afMGbp3ocZCbWNlBq+HoZD2NgpmyntCtxHfD4oPlBa66X5SNXGS01ea8zoGvj
42 | wZXp9zVx5Sp8+dOqAspd+klZtuylHcjeG3+Xteq1JGYuSzjXHIdw/xKdoVvKLGLC
43 | PqCSm9L/gC1ey69YIjcpFMA/9ZO584PBIeJ2wtB9OgTUzRYtSwJKOtnf5QJC72LQ
44 | oxfnQo+QvlxzJKojojq6SRWFZzPZnItZCdv4fjgY4F9VRDJHXXXWD9Zio6Iw97Y6
45 | br4QPB1ADowWfzj4cc3/p7TukImRAoIBAQDJZxTKSeSQSrK6RM+dHKiP5SSR3oeW
46 | 7NwpcMUTQz1x4Dc4OHvmviQNpIT8SjiYcO2pa/dy2tSp+0lkd08iDvOl5BJ5YUm5
47 | O8nJol81u8qHtpOW1UmsZBdSsuCBRQfNa8szGvjzF9gusG/rjfGHoPLHGpl2sQYB
48 | MtzK91q7UaDFb8fi86ruZQT0Q4k+SXjeMCW6/k11XoqZp8gfotToNgLzBxh50yye
49 | EVTw4MuSyhX17yJShD+SA1RG58GUTa/2L/vqUi1z3P0+zUFyxxQDNYoPJvXnvTx+
50 | uLjqzkOmw31r+kbjWD5NAx80SC01mI2T/VeTj2HtYkX3KGxfJQKfYOet
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 | MIIF+DCCA+CgAwIBAgIBATANBgkqhkiG9w0BAQ0FADCBgzELMAkGA1UEBhMCUlUx
37 | DzANBgNVBAgTBk1vc2NvdzEPMA0GA1UEBxMGTW9zY293MQ0wCwYDVQQKEwRIb21l
38 | MRAwDgYDVQQLEwdJVCBDcmV3MRAwDgYDVQQDEwdUZXN0IENBMR8wHQYJKoZIhvcN
39 | AQkBFhByb290QGV4YW1wbGUuY29tMCAXDTE4MTIxNTExMzEwMFoYDzIyMTgxMjE1
40 | MTEzMTAwWjCBgzELMAkGA1UEBhMCUlUxDzANBgNVBAgTBk1vc2NvdzEPMA0GA1UE
41 | BxMGTW9zY293MQ0wCwYDVQQKEwRIb21lMRAwDgYDVQQLEwdJVCBDcmV3MRAwDgYD
42 | VQQDEwdUZXN0IENBMR8wHQYJKoZIhvcNAQkBFhByb290QGV4YW1wbGUuY29tMIIC
43 | IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2PtC5aCUO3dj1H6rK3pXwUFY
44 | msIMB6uI3cKqx4U3thyL3orTAYu51Ax/nG8iVi9X3CY0v0Bfwrq004oqCFuyygl0
45 | yNRKomx9prpunCRv+vW6ojpif+iMOJmyGKQ8vhMSCbUgbk2Z53U1FKYWybPk9bXA
46 | fpI1KdyT2iVB0wDKKLkbLTtMuSOiFDCYZK5+yVBhbxIBQtoldhejbgHh0z7r78Bz
47 | v5vRwTyL73xHJydj+7yxJp4BgcptGYMJO6pb+c8vbLBdQy38i1vAAQa1XQzh8jUU
48 | KMXsO6LssMAdVMpA53uscQNz8j7g+cGwnWPe2t1f8I81tbI/oZ3MAf42zSAfbvde
49 | x9LbaXmmYAcifqdkkHaaaTKr8jVZyxK/CKMUzsL7JckDxXMAQgoKofWespyi2cqF
50 | /XISEnFaqOFh5brxwZmIKy2/GAqpyIv2BPWefgEhw1+d6GZMysPiSZKOdGUtoqlW
51 | 1Ni55z/gKLYNcTRyah7cGP6tGasSFjZAvMmgW6I+Q559pUXfsCSI+MGzGgeBNiPX
52 | 460j4qfyM/2kvR2vIjtY5choV2utDk3XMr5jSPatFKPX0K136TTMqEgBHL3SQ228
53 | apR18MrNYwnlOSxXsr/85s/Hf7dtk7OPRIxJPiewUnUK2UrLQ8eLYdSMjJgJxfmc
54 | 2a2DQwq/0MwQomcJbtECAwEAAaNzMHEwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E
55 | FgQUcylnYt/dChvaK5Ndc/Ko25dXrfswCwYDVR0PBAQDAgEGMBEGCWCGSAGG+EIB
56 | AQQEAwIABzAfBglghkgBhvhCAQ0EEhYQdGVzdCBjZXJ0aWZpY2F0ZTANBgkqhkiG
57 | 9w0BAQ0FAAOCAgEATHN9WJedxwW3bDr5yO1BgOVGYaGpbg2DheHnL61smQwimfZm
58 | T8z/1mn9iKBJd++lh/lfCTINlX4N3ViCTgDx+dEilQ80RQlbpi5fcrsU0xbuBHBX
59 | Pn/I61CuGebfFMQLD8qazkuZ7SDZMA5LdmPL7FnIJpHdl7DnnghyLHuakLn/7Qlu
60 | UDh8WBjWHGIwIuS9g5gB9cwVPV1tTPz+PrBUdvQKUWUgBuS5MbdpPzNKJq9Q6qTB
61 | khnso6s0+CQ7oIR30El3vxSueS7T6wfIFp/PL1jwesJ2AmBWz84I5n8dTwchFrXD
62 | dDMsAgx1Ea2QFbXSRnsjdY8Mufkt31SIG4e5xSERoQ2STkfsDiqWWhEywI2hOc3x
63 | E8+QhXyjwKw6W6d4Nt5tg5sFB2+4CJ4jKxzyE5jZHSNrf8365dicz9YEGSPuDUs7
64 | oZcErGlbP+ixR0G/C0R0FG3+8XnhUJBJDd6wGiivM/y2ajfvIoEtfL0vg5pia2Bh
65 | hCUI8+jXg6TDvrGfGYAghDXNzY02KHP/sfGcDtQzc2qFuhlikdfowhT8+djJrVtm
66 | YNQeKie9XE8wYVUiuQE0ZRUFv2bY02Ur2WMzF+v7np+XcM5SMhGAWsV5xOQBv+6W
67 | msfhw6XNx2U5lGDtmI+r9l8dJgLGYIXKTJSfSGn1t8cLBbu/4OmpsP9UkWk=
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 |
--------------------------------------------------------------------------------