├── .bumpversion.cfg ├── .cookiecutterrc ├── .coveragerc ├── .editorconfig ├── .github └── workflows │ ├── ci.yaml │ └── codeql-analysis.yml ├── .gitignore ├── .hgtags ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── AUTHORS ├── Changelog ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── amqp ├── __init__.py ├── abstract_channel.pxd ├── abstract_channel.py ├── basic_message.pxd ├── basic_message.py ├── channel.py ├── connection.py ├── exceptions.py ├── method_framing.pxd ├── method_framing.py ├── platform.py ├── protocol.py ├── sasl.py ├── serialization.pxd ├── serialization.py ├── spec.py ├── transport.py ├── utils.pxd └── utils.py ├── conftest.py ├── docs ├── Makefile ├── _static │ └── .keep ├── _templates │ └── sidebardonations.html ├── changelog.rst ├── conf.py ├── images │ ├── celery_128.png │ └── favicon.ico ├── includes │ └── introduction.txt ├── index.rst ├── make.bat ├── reference │ ├── amqp.abstract_channel.rst │ ├── amqp.basic_message.rst │ ├── amqp.channel.rst │ ├── amqp.connection.rst │ ├── amqp.exceptions.rst │ ├── amqp.method_framing.rst │ ├── amqp.platform.rst │ ├── amqp.protocol.rst │ ├── amqp.sasl.rst │ ├── amqp.serialization.rst │ ├── amqp.spec.rst │ ├── amqp.transport.rst │ ├── amqp.utils.rst │ └── index.rst └── templates │ └── readme.txt ├── extra └── update_comments_from_spec.py ├── rabbitmq_logs.sh ├── requirements ├── default.txt ├── docs.txt ├── pkgutils.txt ├── test-ci.txt └── test.txt ├── setup.cfg ├── setup.py ├── t ├── __init__.py ├── certs │ ├── ca_certificate.pem │ ├── client_certificate.pem │ ├── client_certificate_broken.pem │ ├── client_key.pem │ └── client_key_broken.pem ├── integration │ ├── __init__.py │ ├── conftest.py │ ├── test_integration.py │ └── test_rmq.py ├── mocks.py └── unit │ ├── __init__.py │ ├── conftest.py │ ├── test_abstract_channel.py │ ├── test_basic_message.py │ ├── test_channel.py │ ├── test_connection.py │ ├── test_exceptions.py │ ├── test_method_framing.py │ ├── test_platform.py │ ├── test_sasl.py │ ├── test_serialization.py │ ├── test_transport.py │ └── test_utils.py ├── tox.ini └── wait_for_rabbitmq.sh /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 5.3.1 3 | commit = True 4 | tag = True 5 | parse = (?P\d+)\.(?P\d+)\.(?P\d+)(?P[a-z\d]+)? 6 | serialize = 7 | {major}.{minor}.{patch}{releaselevel} 8 | {major}.{minor}.{patch} 9 | 10 | [bumpversion:file:amqp/__init__.py] 11 | 12 | [bumpversion:file:docs/includes/introduction.txt] 13 | 14 | [bumpversion:file:README.rst] 15 | -------------------------------------------------------------------------------- /.cookiecutterrc: -------------------------------------------------------------------------------- 1 | default_context: 2 | 3 | email: 'ask@celeryproject.org' 4 | full_name: 'Ask Solem' 5 | github_username: 'celery' 6 | project_name: 'amqp' 7 | project_short_description: 'Low-level AMQP client for Python (fork of amqplib)' 8 | project_slug: 'amqp' 9 | version: '1.0.0' 10 | year: '2012-16' 11 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = 1 3 | cover_pylib = 0 4 | include=*amqp/* 5 | omit = t.* 6 | 7 | [report] 8 | omit = 9 | */python?.?/* 10 | */site-packages/* 11 | */pypy/* 12 | *amqp/five.py 13 | exclude_lines = 14 | pragma: no cover 15 | 16 | for infinity 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [Makefile] 14 | indent_style = tab 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [pull_request, push] 3 | jobs: 4 | 5 | #################### Unittests #################### 6 | unittest: 7 | runs-on: blacksmith-4vcpu-ubuntu-2204 8 | strategy: 9 | matrix: 10 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] 11 | steps: 12 | - name: Check out code from GitHub 13 | uses: actions/checkout@v4 14 | - name: Set up Python ${{ matrix.python-version }} 15 | id: python 16 | uses: useblacksmith/setup-python@v6 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: pip install --upgrade pip setuptools wheel tox tox-docker 21 | - name: Run unittest 22 | run: tox -v -e ${{ matrix.python-version }}-unit -- -v 23 | #################### Integration tests #################### 24 | integration: 25 | needs: [unittest] 26 | runs-on: blacksmith-4vcpu-ubuntu-2204 27 | strategy: 28 | matrix: 29 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] 30 | steps: 31 | - name: Check out code from GitHub 32 | uses: actions/checkout@v4 33 | - name: Build rabbitmq:tls container 34 | run: docker build -t rabbitmq:tls . 35 | - name: Set up Python ${{ matrix.python-version }} 36 | id: python 37 | uses: useblacksmith/setup-python@v6 38 | with: 39 | python-version: ${{ matrix.python-version }} 40 | - name: Install dependencies 41 | run: pip install --upgrade pip setuptools wheel tox tox-docker 42 | - name: Run integration tests 43 | run: tox -v -e ${{ matrix.python-version }}-integration-rabbitmq -- -v 44 | - name: Run integration tests with speedups enabled 45 | run: | 46 | CELERY_ENABLE_SPEEDUPS=1 python setup.py develop 47 | tox -v -e ${{ matrix.python-version }}-integration-rabbitmq -- -v 48 | if: ${{ matrix.python-version != 'pypy-3.9'}} 49 | 50 | #################### Linters and checkers #################### 51 | lint: 52 | needs: [unittest, integration] 53 | runs-on: blacksmith-4vcpu-ubuntu-2204 54 | strategy: 55 | matrix: 56 | python-version: ['3.13'] 57 | steps: 58 | - name: Check out code from GitHub 59 | uses: actions/checkout@v4 60 | - name: Set up Python ${{ matrix.python-version }} 61 | id: python 62 | uses: useblacksmith/setup-python@v6 63 | with: 64 | python-version: ${{ matrix.python-version }} 65 | - name: Install dependencies 66 | run: pip install --upgrade pip setuptools wheel tox tox-docker 67 | - name: Run flake8 68 | run: tox -v -e py-flake8 -- -v 69 | - name: Run pydocstyle 70 | run: tox -v -e py-pydocstyle -- -v 71 | - name: Run apicheck 72 | run: tox -v -e py-apicheck -- -v 73 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | 21 | jobs: 22 | analyze: 23 | name: Analyze 24 | runs-on: blacksmith-4vcpu-ubuntu-2204 25 | permissions: 26 | actions: read 27 | contents: read 28 | security-events: write 29 | 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | language: [ 'python' ] 34 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 35 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 36 | 37 | steps: 38 | - name: Checkout repository 39 | uses: actions/checkout@v3 40 | 41 | # Initializes the CodeQL tools for scanning. 42 | - name: Initialize CodeQL 43 | uses: github/codeql-action/init@v2 44 | with: 45 | languages: ${{ matrix.language }} 46 | # If you wish to specify custom queries, you can do so here or in a config file. 47 | # By default, queries listed here will override any specified in a config file. 48 | # Prefix the list here with "+" to use these queries and those in the config file. 49 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 50 | 51 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 52 | # If this step fails, then you should remove it and run the build manually (see below) 53 | - name: Autobuild 54 | uses: github/codeql-action/autobuild@v2 55 | 56 | # ℹ️ Command-line programs to run using the OS shell. 57 | # 📚 https://git.io/JvXDl 58 | 59 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 60 | # and modify them (or add more) to build your code if your project 61 | # uses a compiled language 62 | 63 | #- run: | 64 | # make bootstrap 65 | # make release 66 | 67 | - name: Perform CodeQL Analysis 68 | uses: github/codeql-action/analyze@v2 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *$py.class 4 | *~ 5 | .*.sw* 6 | dist/ 7 | *.egg-info 8 | *.egg 9 | *.egg/ 10 | build/ 11 | .build/ 12 | _build/ 13 | pip-log.txt 14 | .directory 15 | erl_crash.dump 16 | *.db 17 | Documentation/ 18 | .tox/ 19 | .ropeproject/ 20 | .project 21 | .pydevproject 22 | .coverage 23 | .eggs/ 24 | .cache/ 25 | .pytest_cache/ 26 | .venv/ 27 | coverage.xml 28 | htmlcov/ 29 | .vscode/ 30 | .python-version 31 | .idea/ 32 | -------------------------------------------------------------------------------- /.hgtags: -------------------------------------------------------------------------------- 1 | b005bd4445cae8617ded34a23f6ef104cfea37e4 0.1 2 | 529583811275d8a14b2fc26f7172b1bc5ca1bd2b 0.2 3 | f784f2d5d1bb921ec4d0383b02ad999cefca1414 0.3 4 | 7ff0cc6993704eace354580f2dc51a075c836efe 0.5 5 | dbf98c6e962abaabe077103b5be4297c000f58d4 0.6 6 | 1f05ed5c599f9a89416f50414854e6e23aff61d5 0.6.1 7 | 59dc52a3c87137df720e3d2fea4214b8363b0a67 1.0.0 8 | cf8638fcc5c118f76e3fab634839f34ad645e67e 1.0.1 9 | 124bd71678e2d5c23f2989c0cea016d54414e493 1.0.2 10 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: trailing-whitespace 6 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the version of Python and other tools you might need 8 | build: 9 | os: ubuntu-20.04 10 | tools: 11 | python: "latest" 12 | 13 | # Build documentation in the docs/ directory with Sphinx 14 | sphinx: 15 | configuration: docs/conf.py 16 | 17 | # If using Sphinx, optionally build your docs in additional formats such as PDF 18 | # formats: 19 | # - pdf 20 | 21 | # Optionally declare the Python requirements required to build your docs 22 | python: 23 | install: 24 | - method: pip 25 | path: . 26 | - requirements: requirements/docs.txt 27 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | ## This file only lists authors since the fork of the original amqplib 2 | ## library, written by Barry Pederson. 3 | 4 | Ask Solem 5 | Andrew Grangaard 6 | Rumyana Neykova 7 | Adam Wentz 8 | Adrien Guinet 9 | Tommie Gannert 10 | Dong Weiming 11 | Dominik Fässler 12 | Dustin J. Mitchell 13 | Ionel Cristian Mărieș 14 | Craig Jellick 15 | Yury Selivanov 16 | Artyom Koval 17 | Rongze Zhu 18 | Vic Kumar 19 | Jared Lewis 20 | Federico Ficarelli 21 | Bas ten Berge 22 | Quentin Pradet 23 | ChangBo Guo(gcb) 24 | Alan Justino 25 | Jelte Fennema 26 | Jon Dufresne 27 | Colton Hicks 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | #This is a sample Image 2 | FROM rabbitmq:latest 3 | 4 | RUN mkdir /etc/rabbitmq/ca 5 | 6 | RUN echo '-----BEGIN CERTIFICATE-----\n\ 7 | MIIDRzCCAi+gAwIBAgIJAMa1mrcNQtapMA0GCSqGSIb3DQEBCwUAMDExIDAeBgNV\n\ 8 | BAMMF1RMU0dlblNlbGZTaWduZWR0Um9vdENBMQ0wCwYDVQQHDAQkJCQkMCAXDTIw\n\ 9 | MDEwMzEyMDE0MFoYDzIxMTkxMjEwMTIwMTQwWjAxMSAwHgYDVQQDDBdUTFNHZW5T\n\ 10 | ZWxmU2lnbmVkdFJvb3RDQTENMAsGA1UEBwwEJCQkJDCCASIwDQYJKoZIhvcNAQEB\n\ 11 | BQADggEPADCCAQoCggEBAKdmOg5vtuZ5vNZmceToiVBlcFg9Y/xKNyCPBij6Wm5p\n\ 12 | mXbnsjO1PhjGr97r2cMLq5QMvGt+FBEIjeeULtWVCBY7vMc4ATEZ1S2PmmKnOSXJ\n\ 13 | MLMDIutznopZkyqt3gqWgXZDxxHIlIzJl0HirQmfeLm6eTOYyFoyFZV3CE2IeW4Y\n\ 14 | n1zYhgZgIrU7Yo3I7wY9Js5yLk4p3etByN5tlLL2sdCOjRRXWGbOh/kb8uiyotEd\n\ 15 | cxNThk0RQDugoEzaGYBU3bzDhKkm4v/v/xp/JxGLDl/e3heRMUbcw9d/0ujflouy\n\ 16 | OQ66SNYGLWFQpmhtyHjalKzL5UbTcof4BQltoo/W7xECAwEAAaNgMF4wCwYDVR0P\n\ 17 | BAQDAgEGMB0GA1UdDgQWBBTKOnbaptqaUCAiwtnwLcRTcbuRejAfBgNVHSMEGDAW\n\ 18 | gBTKOnbaptqaUCAiwtnwLcRTcbuRejAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3\n\ 19 | DQEBCwUAA4IBAQB1tJUR9zoQ98bOz1es91PxgIt8VYR8/r6uIRtYWTBi7fgDRaaR\n\ 20 | Glm6ZqOSXNlkacB6kjUzIyKJwGWnD9zU/06CH+ME1U497SVVhvtUEbdJb1COU+/5\n\ 21 | KavEHVINfc3tHD5Z5LJR3okEILAzBYkEcjYUECzBNYVi4l6PBSMSC2+RBKGqHkY7\n\ 22 | ApmD5batRghH5YtadiyF4h6bba/XSUqxzFcLKjKSyyds4ndvA1/yfl/7CrRtiZf0\n\ 23 | jw1pFl33/PTOhgi66MHa4uaKlL/hIjIlh4kJgJajqCN+TVU4Q6JNmSuIsq6rksSw\n\ 24 | Rd5baBZrik2NHALr/ZN2Wy0nXiQJ3p+F20+X\n\ 25 | -----END CERTIFICATE-----' > /etc/rabbitmq/ca/ca_certificate.pem 26 | 27 | RUN echo '-----BEGIN CERTIFICATE-----\n\ 28 | MIIDjjCCAnagAwIBAgIBATANBgkqhkiG9w0BAQsFADAxMSAwHgYDVQQDDBdUTFNH\n\ 29 | ZW5TZWxmU2lnbmVkdFJvb3RDQTENMAsGA1UEBwwEJCQkJDAgFw0yMDAxMDMxMjAx\n\ 30 | NDFaGA8yMTE5MTIxMDEyMDE0MVowLzEcMBoGA1UEAwwTZXhhbXBsZS5leGFtcGxl\n\ 31 | LmNvbTEPMA0GA1UECgwGc2VydmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\n\ 32 | CgKCAQEAwlYnHfsXVhShuDPht5w9BgdEKDzjZtb+a0rvslg+9nYiHbFAZmAKDb69\n\ 33 | s0vMk55AtDdPVDxuoWawavvcZLuEVwEi4h51Ekc80trIC6oLy3hf3sLGl9Ru1zxG\n\ 34 | K+F46thhdcBdPZRxlJr5GMYLdXWtXiNtPZSJMLFfMespmZxNsMha2Uo8JZXKDZrO\n\ 35 | p7CChDQFjtdOyu/NesGO3mwGTLzI1MbldytkmQ6OR4cNXu1sEPU/zKG57oDtezFD\n\ 36 | t9TVMDIP6EWit4FXE3Yn8vQMz0tmgUNSR7CJDKIHO69u5BpWYYv4gTebWdHT7tRm\n\ 37 | eYgK7EyS0rETrzDf7+8xzyUVEyVonwIDAQABo4GwMIGtMAkGA1UdEwQCMAAwCwYD\n\ 38 | VR0PBAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMD4GA1UdEQQ3MDWCE2V4YW1w\n\ 39 | bGUuZXhhbXBsZS5jb22CE2V4YW1wbGUuZXhhbXBsZS5jb22CCWxvY2FsaG9zdDAd\n\ 40 | BgNVHQ4EFgQUiS0qh+Ntvzm1MgDmH2allqc2jkIwHwYDVR0jBBgwFoAUyjp22qba\n\ 41 | mlAgIsLZ8C3EU3G7kXowDQYJKoZIhvcNAQELBQADggEBAAfePCF0JYEASi0LeDqv\n\ 42 | ZScEN2GsRB3nD1HY6jtM0EZE9quSZO+TLMO28XgiunQagrp+Y01yNim6LV+0FjfI\n\ 43 | jj+LusnlGD6xWmwHS4bg61RizAoFdKoEja6gEeeN5v+Ko+NNsu6aJFh0Mlf55j4n\n\ 44 | hHS/yQyUsiYasgdm/t14HjhpzRVOntzm0mf1JStVk1TrtSMxjOi51NZuNUUlxy7k\n\ 45 | dtDNdfUmVIJo+hOaIu+JumIn5DZ/KDzoaQntB1NpHtWW5IfgN5U+Cc9vI8CdCL1r\n\ 46 | GEZdciNkK6LcvoK/XeJmE/siGXz8ovBMvqBMVYjMNVlvcIr8gvEzFYxZQFXowNwK\n\ 47 | c0c=\n\ 48 | -----END CERTIFICATE-----' > /etc/rabbitmq/ca/server_certificate.pem 49 | 50 | RUN echo '-----BEGIN RSA PRIVATE KEY-----\n\ 51 | MIIEpQIBAAKCAQEAwlYnHfsXVhShuDPht5w9BgdEKDzjZtb+a0rvslg+9nYiHbFA\n\ 52 | ZmAKDb69s0vMk55AtDdPVDxuoWawavvcZLuEVwEi4h51Ekc80trIC6oLy3hf3sLG\n\ 53 | l9Ru1zxGK+F46thhdcBdPZRxlJr5GMYLdXWtXiNtPZSJMLFfMespmZxNsMha2Uo8\n\ 54 | JZXKDZrOp7CChDQFjtdOyu/NesGO3mwGTLzI1MbldytkmQ6OR4cNXu1sEPU/zKG5\n\ 55 | 7oDtezFDt9TVMDIP6EWit4FXE3Yn8vQMz0tmgUNSR7CJDKIHO69u5BpWYYv4gTeb\n\ 56 | WdHT7tRmeYgK7EyS0rETrzDf7+8xzyUVEyVonwIDAQABAoIBAQCRVcTjUwjcw4k+\n\ 57 | LO69Vgb9HyoFvaODIX4b12rzQbO0thxFgG3dIi3ioadVE3bnXw6cuFCHerpx0k5V\n\ 58 | dA4a93G9b4ga+xQqm0QNnLjGoGE5xchM2/WRTrmmFdmUr4ayeyhH25jfmMhojo2D\n\ 59 | zXh8W4lQQcZMq2z+EWhT+L6ftpkTfzOW3G7aUg7eYN/edQ+jJiD0CvCd5FyuIfxf\n\ 60 | RFLtp1A+DXzh05OqO6uaVrucdMKuYF8dawI75imoxYCLvpCeKdEsQ4h5yNsNTkZu\n\ 61 | InYZhZJ1YZfgj/P+2QwKjdMcdAOB4P1F/xdV1GkREHOgsznfFgfrod90/D0jcKyz\n\ 62 | iVgkY05hAoGBAPfmG3PtQbO126hvKcAB7JgztmWJ0HiLPP+fVCSJvbkp+0fnVNZ0\n\ 63 | Bxl8r2dLNIDFsdJIiNeI/u8umoJT0gFGXkJjYyW2hY32hqm7kTbQo91O0Crv34Wg\n\ 64 | Z66Kj2NZox7T53HxdTMIZI28qXDpHVm8Q7uW+6CgLcshABhwowxW42y7AoGBAMiv\n\ 65 | 8w+4yzZJH37PWNOObe6hKXPvzWXDIKML3ENc3EPES+cxG4ke8+ltNZf6hgyvbbpf\n\ 66 | Fbs6Z14j5N3sfNxpuHp65O3MI3pF5kKAobjexg97GA3nuiyxQJ6/Z/hu1Dtt8hWU\n\ 67 | D7mWH7RwlnMFBpTC5A275dzoYfyC+qGW4xVDFQdtAoGBANbrJk3hGh8lwWRLy9Rt\n\ 68 | VqO14aIyUwzPGnk7twVebZ/Ep9f01PZ/7U/Ja4CQENq7iqkWvZyvZuYSb14iMWVt\n\ 69 | jnbcF68wiKVFYAZzWTg+tnI9y/gNsqn1IS6PbjTiF6u4Z2W/wq4VzqebMwNy90E/\n\ 70 | GTHfehQOCuWanKyTqqgeBFnVAoGBAJzJocqZo+GQdVO8KHh3oPk63cjfA4hKTvgy\n\ 71 | 7u2N4ePruyUvH4UcMpEeqi1HI21LrR1a5f51XYaV4ltjRBVrXx4JX0tNHjaL3537\n\ 72 | It3s5a34jE1oyfHatVKQ1WipJZQcjHJBT5u9Zp2xDElmFsMoE8WLE8VnpA4EQkz2\n\ 73 | NglJdGdtAoGAPbBvhaW18wdy1jgzcQB84ODdXAZ6dPMXAhWcTQ8t2r4hKpfutMA9\n\ 74 | SGRMy+oEhSToMDPALn7EnQGJdGeZJjlXn8mLiqo1hHhfyFPK87EyAONHamZJQ3k7\n\ 75 | LdsBZMl5sYt+9dt5/CE38cU/VkS5pqueLT8c1RGBHnObLw7sU5XK2o8=\n\ 76 | -----END RSA PRIVATE KEY-----' > /etc/rabbitmq/ca/server_key.pem 77 | 78 | RUN echo 'loopback_users = none\n\ 79 | listeners.ssl.default = 5671\n\ 80 | ssl_options.cacertfile = /etc/rabbitmq/ca/ca_certificate.pem\n\ 81 | ssl_options.certfile = /etc/rabbitmq/ca/server_certificate.pem\n\ 82 | ssl_options.keyfile = /etc/rabbitmq/ca/server_key.pem\n\ 83 | ssl_options.verify = verify_peer\n\ 84 | ssl_options.fail_if_no_peer_cert = true' > /etc/rabbitmq/rabbitmq.conf 85 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2016 Ask Solem & contributors. All rights reserved. 2 | Copyright (c) 2012-2014 GoPivotal, Inc. All rights reserved. 3 | Copyright (c) 2009, 2010, 2011, 2012 Ask Solem, and individual contributors. All rights reserved. 4 | Copyright (C) 2007-2008 Barry Pederson . All rights reserved. 5 | 6 | py-amqp is licensed under The BSD License (3 Clause, also known as 7 | the new BSD license). The license is an OSI approved Open Source 8 | license and is GPL-compatible(1). 9 | 10 | The license text can also be found here: 11 | http://www.opensource.org/licenses/BSD-3-Clause 12 | 13 | License 14 | ======= 15 | 16 | Redistribution and use in source and binary forms, with or without 17 | modification, are permitted provided that the following conditions are met: 18 | * Redistributions of source code must retain the above copyright 19 | notice, this list of conditions and the following disclaimer. 20 | * Redistributions in binary form must reproduce the above copyright 21 | notice, this list of conditions and the following disclaimer in the 22 | documentation and/or other materials provided with the distribution. 23 | * Neither the name of Ask Solem, nor the 24 | names of its contributors may be used to endorse or promote products 25 | derived from this software without specific prior written permission. 26 | 27 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 28 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 29 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 30 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Ask Solem OR CONTRIBUTORS 31 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 32 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 33 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 34 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 35 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 36 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 37 | POSSIBILITY OF SUCH DAMAGE. 38 | 39 | 40 | Footnotes 41 | ========= 42 | (1) A GPL-compatible license makes it possible to 43 | combine Celery with other software that is released 44 | under the GPL, it does not mean that we're distributing 45 | Celery under the GPL license. The BSD license, unlike the GPL, 46 | let you distribute a modified version without making your 47 | changes open source. 48 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst Changelog LICENSE 2 | recursive-include docs * 3 | recursive-include demo *.py 4 | recursive-include extra README *.py 5 | recursive-include requirements *.txt 6 | recursive-include t *.py 7 | 8 | recursive-exclude docs/_build * 9 | recursive-exclude * __pycache__ 10 | recursive-exclude * *.py[co] 11 | recursive-exclude * .*.sw* 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJ=amqp 2 | PGPIDENT="Celery Security Team" 3 | PYTHON=python 4 | PYTEST=py.test 5 | GIT=git 6 | TOX=tox 7 | ICONV=iconv 8 | FLAKE8=flake8 9 | PYDOCSTYLE=pydocstyle 10 | SPHINX2RST=sphinx2rst 11 | 12 | TESTDIR=t 13 | SPHINX_DIR=docs/ 14 | SPHINX_BUILDDIR="${SPHINX_DIR}/_build" 15 | README=README.rst 16 | README_SRC="docs/templates/readme.txt" 17 | CONTRIBUTING=CONTRIBUTING.rst 18 | CONTRIBUTING_SRC="docs/contributing.rst" 19 | SPHINX_HTMLDIR="${SPHINX_BUILDDIR}/html" 20 | DOCUMENTATION=Documentation 21 | 22 | 23 | all: help 24 | 25 | help: 26 | @echo "docs - Build documentation." 27 | @echo "test-all - Run tests for all supported python versions." 28 | @echo "distcheck ---------- - Check distribution for problems." 29 | @echo " test - Run unittests using current python." 30 | @echo " lint ------------ - Check codebase for problems." 31 | @echo " apicheck - Check API reference coverage." 32 | @echo " readmecheck - Check README.rst encoding." 33 | @echo " contribcheck - Check CONTRIBUTING.rst encoding" 34 | @echo " flakes -------- - Check code for syntax and style errors." 35 | @echo " flakecheck - Run flake8 on the source code." 36 | @echo " pep257check - Run pep257 on the source code." 37 | @echo "readme - Regenerate README.rst file." 38 | @echo "contrib - Regenerate CONTRIBUTING.rst file" 39 | @echo "clean-dist --------- - Clean all distribution build artifacts." 40 | @echo " clean-git-force - Remove all uncommitted files." 41 | @echo " clean ------------ - Non-destructive clean" 42 | @echo " clean-pyc - Remove .pyc/__pycache__ files" 43 | @echo " clean-docs - Remove documentation build artifacts." 44 | @echo " clean-build - Remove setup artifacts." 45 | @echo "bump - Bump patch version number." 46 | @echo "bump-minor - Bump minor version number." 47 | @echo "bump-major - Bump major version number." 48 | @echo "release - Make PyPI release." 49 | 50 | clean: clean-docs clean-pyc clean-build 51 | 52 | clean-dist: clean clean-git-force 53 | 54 | bump: 55 | bumpversion patch 56 | 57 | bump-minor: 58 | bumpversion minor 59 | 60 | bump-major: 61 | bumpversion major 62 | 63 | release: 64 | python setup.py register sdist bdist_wheel upload --sign --identity="$(PGPIDENT)" 65 | 66 | Documentation: 67 | (cd "$(SPHINX_DIR)"; $(MAKE) html) 68 | mv "$(SPHINX_HTMLDIR)" $(DOCUMENTATION) 69 | 70 | docs: Documentation 71 | 72 | clean-docs: 73 | -rm -rf "$(SPHINX_BUILDDIR)" 74 | 75 | lint: flakecheck apicheck readmecheck 76 | 77 | apicheck: 78 | (cd "$(SPHINX_DIR)"; $(MAKE) apicheck) 79 | 80 | flakecheck: 81 | $(FLAKE8) "$(PROJ)" "$(TESTDIR)" 82 | 83 | flakediag: 84 | -$(MAKE) flakecheck 85 | 86 | pep257check: 87 | $(PYDOCSTYLE) "$(PROJ)" 88 | 89 | flakes: flakediag pep257check 90 | 91 | clean-readme: 92 | -rm -f $(README) 93 | 94 | readmecheck: 95 | $(ICONV) -f ascii -t ascii $(README) >/dev/null 96 | 97 | $(README): 98 | $(SPHINX2RST) "$(README_SRC)" --ascii > $@ 99 | 100 | readme: clean-readme $(README) readmecheck 101 | 102 | clean-contrib: 103 | -rm -f "$(CONTRIBUTING)" 104 | 105 | $(CONTRIBUTING): 106 | $(SPHINX2RST) "$(CONTRIBUTING_SRC)" > $@ 107 | 108 | contrib: clean-contrib $(CONTRIBUTING) 109 | 110 | clean-pyc: 111 | -find . -type f -a \( -name "*.pyc" -o -name "*$$py.class" \) | xargs rm 112 | -find . -type d -name "__pycache__" | xargs rm -r 113 | 114 | removepyc: clean-pyc 115 | 116 | clean-build: 117 | rm -rf build/ dist/ .eggs/ *.egg-info/ .tox/ .coverage cover/ 118 | 119 | clean-git: 120 | $(GIT) clean -xdn 121 | 122 | clean-git-force: 123 | $(GIT) clean -xdf 124 | 125 | test-all: clean-pyc 126 | $(TOX) 127 | 128 | test: 129 | $(PYTHON) setup.py test 130 | 131 | cov: 132 | (cd $(TESTDIR); $(PYTEST) -xv --cov="$(PROJ)" --cov-report=html) 133 | mv $(TESTDIR)/htmlcov . 134 | 135 | build: 136 | $(PYTHON) setup.py sdist bdist_wheel 137 | 138 | distcheck: lint test clean 139 | 140 | dist: readme contrib clean-dist build 141 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ===================================================================== 2 | Python AMQP 0.9.1 client library 3 | ===================================================================== 4 | 5 | |build-status| |coverage| |license| |wheel| |pyversion| |pyimp| 6 | 7 | :Version: 5.3.1 8 | :Web: https://amqp.readthedocs.io/ 9 | :Download: https://pypi.org/project/amqp/ 10 | :Source: http://github.com/celery/py-amqp/ 11 | :DeepWiki: |deepwiki| 12 | :Keywords: amqp, rabbitmq 13 | 14 | About 15 | ===== 16 | 17 | This is a fork of amqplib_ which was originally written by Barry Pederson. 18 | It is maintained by the Celery_ project, and used by `kombu`_ as a pure python 19 | alternative when `librabbitmq`_ is not available. 20 | 21 | This library should be API compatible with `librabbitmq`_. 22 | 23 | .. _amqplib: https://pypi.org/project/amqplib/ 24 | .. _Celery: http://celeryproject.org/ 25 | .. _kombu: https://kombu.readthedocs.io/ 26 | .. _librabbitmq: https://pypi.org/project/librabbitmq/ 27 | 28 | Differences from `amqplib`_ 29 | =========================== 30 | 31 | - Supports draining events from multiple channels (``Connection.drain_events``) 32 | - Support for timeouts 33 | - Channels are restored after channel error, instead of having to close the 34 | connection. 35 | - Support for heartbeats 36 | 37 | - ``Connection.heartbeat_tick(rate=2)`` must called at regular intervals 38 | (half of the heartbeat value if rate is 2). 39 | - Or some other scheme by using ``Connection.send_heartbeat``. 40 | - Supports RabbitMQ extensions: 41 | - Consumer Cancel Notifications 42 | - by default a cancel results in ``ChannelError`` being raised 43 | - but not if a ``on_cancel`` callback is passed to ``basic_consume``. 44 | - Publisher confirms 45 | - ``Channel.confirm_select()`` enables publisher confirms. 46 | - ``Channel.events['basic_ack'].append(my_callback)`` adds a callback 47 | to be called when a message is confirmed. This callback is then 48 | called with the signature ``(delivery_tag, multiple)``. 49 | - Exchange-to-exchange bindings: ``exchange_bind`` / ``exchange_unbind``. 50 | - ``Channel.confirm_select()`` enables publisher confirms. 51 | - ``Channel.events['basic_ack'].append(my_callback)`` adds a callback 52 | to be called when a message is confirmed. This callback is then 53 | called with the signature ``(delivery_tag, multiple)``. 54 | - Authentication Failure Notifications 55 | Instead of just closing the connection abruptly on invalid 56 | credentials, py-amqp will raise an ``AccessRefused`` error 57 | when connected to rabbitmq-server 3.2.0 or greater. 58 | - Support for ``basic_return`` 59 | - Uses AMQP 0-9-1 instead of 0-8. 60 | - ``Channel.access_request`` and ``ticket`` arguments to methods 61 | **removed**. 62 | - Supports the ``arguments`` argument to ``basic_consume``. 63 | - ``internal`` argument to ``exchange_declare`` removed. 64 | - ``auto_delete`` argument to ``exchange_declare`` deprecated 65 | - ``insist`` argument to ``Connection`` removed. 66 | - ``Channel.alerts`` has been removed. 67 | - Support for ``Channel.basic_recover_async``. 68 | - ``Channel.basic_recover`` deprecated. 69 | - Exceptions renamed to have idiomatic names: 70 | - ``AMQPException`` -> ``AMQPError`` 71 | - ``AMQPConnectionException`` -> ConnectionError`` 72 | - ``AMQPChannelException`` -> ChannelError`` 73 | - ``Connection.known_hosts`` removed. 74 | - ``Connection`` no longer supports redirects. 75 | - ``exchange`` argument to ``queue_bind`` can now be empty 76 | to use the "default exchange". 77 | - Adds ``Connection.is_alive`` that tries to detect 78 | whether the connection can still be used. 79 | - Adds ``Connection.connection_errors`` and ``.channel_errors``, 80 | a list of recoverable errors. 81 | - Exposes the underlying socket as ``Connection.sock``. 82 | - Adds ``Channel.no_ack_consumers`` to keep track of consumer tags 83 | that set the no_ack flag. 84 | - Slightly better at error recovery 85 | 86 | Quick overview 87 | ============== 88 | 89 | Simple producer publishing messages to ``test`` queue using default exchange: 90 | 91 | .. code:: python 92 | 93 | import amqp 94 | 95 | with amqp.Connection('broker.example.com') as c: 96 | ch = c.channel() 97 | ch.basic_publish(amqp.Message('Hello World'), routing_key='test') 98 | 99 | Producer publishing to ``test_exchange`` exchange with publisher confirms enabled and using virtual_host ``test_vhost``: 100 | 101 | .. code:: python 102 | 103 | import amqp 104 | 105 | with amqp.Connection( 106 | 'broker.example.com', exchange='test_exchange', 107 | confirm_publish=True, virtual_host='test_vhost' 108 | ) as c: 109 | ch = c.channel() 110 | ch.basic_publish(amqp.Message('Hello World'), routing_key='test') 111 | 112 | Consumer with acknowledgments enabled: 113 | 114 | .. code:: python 115 | 116 | import amqp 117 | 118 | with amqp.Connection('broker.example.com') as c: 119 | ch = c.channel() 120 | def on_message(message): 121 | print('Received message (delivery tag: {}): {}'.format(message.delivery_tag, message.body)) 122 | ch.basic_ack(message.delivery_tag) 123 | ch.basic_consume(queue='test', callback=on_message) 124 | while True: 125 | c.drain_events() 126 | 127 | 128 | Consumer with acknowledgments disabled: 129 | 130 | .. code:: python 131 | 132 | import amqp 133 | 134 | with amqp.Connection('broker.example.com') as c: 135 | ch = c.channel() 136 | def on_message(message): 137 | print('Received message (delivery tag: {}): {}'.format(message.delivery_tag, message.body)) 138 | ch.basic_consume(queue='test', callback=on_message, no_ack=True) 139 | while True: 140 | c.drain_events() 141 | 142 | Speedups 143 | ======== 144 | 145 | This library has **experimental** support of speedups. Speedups are implemented using Cython. To enable speedups, ``CELERY_ENABLE_SPEEDUPS`` environment variable must be set during building/installation. 146 | Currently speedups can be installed: 147 | 148 | 1. using source package (using ``--no-binary`` switch): 149 | 150 | .. code:: shell 151 | 152 | CELERY_ENABLE_SPEEDUPS=true pip install --no-binary :all: amqp 153 | 154 | 155 | 2. building directly source code: 156 | 157 | .. code:: shell 158 | 159 | CELERY_ENABLE_SPEEDUPS=true python setup.py install 160 | 161 | Further 162 | ======= 163 | 164 | - Differences between AMQP 0.8 and 0.9.1 165 | 166 | http://www.rabbitmq.com/amqp-0-8-to-0-9-1.html 167 | 168 | - AMQP 0.9.1 Quick Reference 169 | 170 | http://www.rabbitmq.com/amqp-0-9-1-quickref.html 171 | 172 | - RabbitMQ Extensions 173 | 174 | http://www.rabbitmq.com/extensions.html 175 | 176 | - For more information about AMQP, visit 177 | 178 | http://www.amqp.org 179 | 180 | - For other Python client libraries see: 181 | 182 | http://www.rabbitmq.com/devtools.html#python-dev 183 | 184 | .. |build-status| image:: https://github.com/celery/py-amqp/actions/workflows/ci.yaml/badge.svg 185 | :alt: Build status 186 | :target: https://github.com/celery/py-amqp/actions/workflows/ci.yaml 187 | 188 | .. |coverage| image:: https://codecov.io/github/celery/py-amqp/coverage.svg?branch=main 189 | :target: https://codecov.io/github/celery/py-amqp?branch=main 190 | 191 | .. |license| image:: https://img.shields.io/pypi/l/amqp.svg 192 | :alt: BSD License 193 | :target: https://opensource.org/licenses/BSD-3-Clause 194 | 195 | .. |wheel| image:: https://img.shields.io/pypi/wheel/amqp.svg 196 | :alt: Python AMQP can be installed via wheel 197 | :target: https://pypi.org/project/amqp/ 198 | 199 | .. |pyversion| image:: https://img.shields.io/pypi/pyversions/amqp.svg 200 | :alt: Supported Python versions. 201 | :target: https://pypi.org/project/amqp/ 202 | 203 | .. |pyimp| image:: https://img.shields.io/pypi/implementation/amqp.svg 204 | :alt: Support Python implementations. 205 | :target: https://pypi.org/project/amqp/ 206 | 207 | .. |deepwiki| image:: https://devin.ai/assets/deepwiki-badge.png 208 | :alt: Ask http://DeepWiki.com 209 | :target: https://deepwiki.com/celery/py-amqp 210 | :width: 125px 211 | 212 | py-amqp as part of the Tidelift Subscription 213 | ============================================ 214 | 215 | The maintainers of py-amqp and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/pypi-amqp?utm_source=pypi-amqp&utm_medium=referral&utm_campaign=readme&utm_term=repo) 216 | 217 | -------------------------------------------------------------------------------- /amqp/__init__.py: -------------------------------------------------------------------------------- 1 | """Low-level AMQP client for Python (fork of amqplib).""" 2 | # Copyright (C) 2007-2008 Barry Pederson 3 | 4 | import re 5 | from collections import namedtuple 6 | 7 | __version__ = '5.3.1' 8 | __author__ = 'Barry Pederson' 9 | __maintainer__ = 'Asif Saif Uddin, Matus Valo' 10 | __contact__ = 'auvipy@gmail.com' 11 | __homepage__ = 'http://github.com/celery/py-amqp' 12 | __docformat__ = 'restructuredtext' 13 | 14 | # -eof meta- 15 | 16 | version_info_t = namedtuple('version_info_t', ( 17 | 'major', 'minor', 'micro', 'releaselevel', 'serial', 18 | )) 19 | 20 | # bumpversion can only search for {current_version} 21 | # so we have to parse the version here. 22 | _temp = re.match( 23 | r'(\d+)\.(\d+).(\d+)(.+)?', __version__).groups() 24 | VERSION = version_info = version_info_t( 25 | int(_temp[0]), int(_temp[1]), int(_temp[2]), _temp[3] or '', '') 26 | del(_temp) 27 | del(re) 28 | 29 | from .basic_message import Message # noqa 30 | from .channel import Channel # noqa 31 | from .connection import Connection # noqa 32 | from .exceptions import (AccessRefused, AMQPError, # noqa 33 | AMQPNotImplementedError, ChannelError, ChannelNotOpen, 34 | ConnectionError, ConnectionForced, ConsumerCancelled, 35 | ContentTooLarge, FrameError, FrameSyntaxError, 36 | InternalError, InvalidCommand, InvalidPath, 37 | IrrecoverableChannelError, 38 | IrrecoverableConnectionError, NoConsumers, NotAllowed, 39 | NotFound, PreconditionFailed, RecoverableChannelError, 40 | RecoverableConnectionError, ResourceError, 41 | ResourceLocked, UnexpectedFrame, error_for_code) 42 | from .utils import promise # noqa 43 | 44 | __all__ = ( 45 | 'Connection', 46 | 'Channel', 47 | 'Message', 48 | 'promise', 49 | 'AMQPError', 50 | 'ConnectionError', 51 | 'RecoverableConnectionError', 52 | 'IrrecoverableConnectionError', 53 | 'ChannelError', 54 | 'RecoverableChannelError', 55 | 'IrrecoverableChannelError', 56 | 'ConsumerCancelled', 57 | 'ContentTooLarge', 58 | 'NoConsumers', 59 | 'ConnectionForced', 60 | 'InvalidPath', 61 | 'AccessRefused', 62 | 'NotFound', 63 | 'ResourceLocked', 64 | 'PreconditionFailed', 65 | 'FrameError', 66 | 'FrameSyntaxError', 67 | 'InvalidCommand', 68 | 'ChannelNotOpen', 69 | 'UnexpectedFrame', 70 | 'ResourceError', 71 | 'NotAllowed', 72 | 'AMQPNotImplementedError', 73 | 'InternalError', 74 | 'error_for_code', 75 | ) 76 | -------------------------------------------------------------------------------- /amqp/abstract_channel.pxd: -------------------------------------------------------------------------------- 1 | # cython: language_level=3 2 | import cython 3 | 4 | from .serialization cimport dumps, loads 5 | 6 | 7 | cdef object AMQP_LOGGER 8 | cdef object IGNORED_METHOD_DURING_CHANNEL_CLOSE 9 | 10 | -------------------------------------------------------------------------------- /amqp/abstract_channel.py: -------------------------------------------------------------------------------- 1 | """Code common to Connection and Channel objects.""" 2 | # Copyright (C) 2007-2008 Barry Pederson ) 3 | 4 | import logging 5 | 6 | from vine import ensure_promise, promise 7 | 8 | from .exceptions import AMQPNotImplementedError, RecoverableConnectionError 9 | from .serialization import dumps, loads 10 | 11 | __all__ = ('AbstractChannel',) 12 | 13 | AMQP_LOGGER = logging.getLogger('amqp') 14 | 15 | IGNORED_METHOD_DURING_CHANNEL_CLOSE = """\ 16 | Received method %s during closing channel %s. This method will be ignored\ 17 | """ 18 | 19 | 20 | class AbstractChannel: 21 | """Superclass for Connection and Channel. 22 | 23 | The connection is treated as channel 0, then comes 24 | user-created channel objects. 25 | 26 | The subclasses must have a _METHOD_MAP class property, mapping 27 | between AMQP method signatures and Python methods. 28 | """ 29 | 30 | def __init__(self, connection, channel_id): 31 | self.is_closing = False 32 | self.connection = connection 33 | self.channel_id = channel_id 34 | connection.channels[channel_id] = self 35 | self.method_queue = [] # Higher level queue for methods 36 | self.auto_decode = False 37 | self._pending = {} 38 | self._callbacks = {} 39 | 40 | self._setup_listeners() 41 | 42 | __slots__ = ( 43 | "is_closing", 44 | "connection", 45 | "channel_id", 46 | "method_queue", 47 | "auto_decode", 48 | "_pending", 49 | "_callbacks", 50 | # adding '__dict__' to get dynamic assignment 51 | "__dict__", 52 | "__weakref__", 53 | ) 54 | 55 | def __enter__(self): 56 | return self 57 | 58 | def __exit__(self, *exc_info): 59 | self.close() 60 | 61 | def send_method(self, sig, 62 | format=None, args=None, content=None, 63 | wait=None, callback=None, returns_tuple=False): 64 | p = promise() 65 | conn = self.connection 66 | if conn is None: 67 | raise RecoverableConnectionError('connection already closed') 68 | args = dumps(format, args) if format else '' 69 | try: 70 | conn.frame_writer(1, self.channel_id, sig, args, content) 71 | except StopIteration: 72 | raise RecoverableConnectionError('connection already closed') 73 | 74 | # TODO temp: callback should be after write_method ... ;) 75 | if callback: 76 | p.then(callback) 77 | p() 78 | if wait: 79 | return self.wait(wait, returns_tuple=returns_tuple) 80 | return p 81 | 82 | def close(self): 83 | """Close this Channel or Connection.""" 84 | raise NotImplementedError('Must be overridden in subclass') 85 | 86 | def wait(self, method, callback=None, timeout=None, returns_tuple=False): 87 | p = ensure_promise(callback) 88 | pending = self._pending 89 | prev_p = [] 90 | if not isinstance(method, list): 91 | method = [method] 92 | 93 | for m in method: 94 | prev_p.append(pending.get(m)) 95 | pending[m] = p 96 | 97 | try: 98 | while not p.ready: 99 | self.connection.drain_events(timeout=timeout) 100 | 101 | if p.value: 102 | args, kwargs = p.value 103 | args = args[1:] # We are not returning method back 104 | return args if returns_tuple else (args and args[0]) 105 | finally: 106 | for i, m in enumerate(method): 107 | if prev_p[i] is not None: 108 | pending[m] = prev_p[i] 109 | else: 110 | pending.pop(m, None) 111 | 112 | def dispatch_method(self, method_sig, payload, content): 113 | if self.is_closing and method_sig not in ( 114 | self._ALLOWED_METHODS_WHEN_CLOSING 115 | ): 116 | # When channel.close() was called we must ignore all methods except 117 | # Channel.close and Channel.CloseOk 118 | AMQP_LOGGER.warning( 119 | IGNORED_METHOD_DURING_CHANNEL_CLOSE, 120 | method_sig, self.channel_id 121 | ) 122 | return 123 | 124 | if content and \ 125 | self.auto_decode and \ 126 | hasattr(content, 'content_encoding'): 127 | try: 128 | content.body = content.body.decode(content.content_encoding) 129 | except Exception: 130 | pass 131 | 132 | try: 133 | amqp_method = self._METHODS[method_sig] 134 | except KeyError: 135 | raise AMQPNotImplementedError( 136 | f'Unknown AMQP method {method_sig!r}') 137 | 138 | try: 139 | listeners = [self._callbacks[method_sig]] 140 | except KeyError: 141 | listeners = [] 142 | one_shot = None 143 | try: 144 | one_shot = self._pending.pop(method_sig) 145 | except KeyError: 146 | if not listeners: 147 | return 148 | 149 | args = [] 150 | if amqp_method.args: 151 | args, _ = loads(amqp_method.args, payload, 4) 152 | if amqp_method.content: 153 | args.append(content) 154 | 155 | for listener in listeners: 156 | listener(*args) 157 | 158 | if one_shot: 159 | one_shot(method_sig, *args) 160 | 161 | #: Placeholder, the concrete implementations will have to 162 | #: supply their own versions of _METHOD_MAP 163 | _METHODS = {} 164 | -------------------------------------------------------------------------------- /amqp/basic_message.pxd: -------------------------------------------------------------------------------- 1 | # cython: language_level=3 2 | from .serialization cimport GenericContent 3 | 4 | 5 | cdef class Message(GenericContent): 6 | cdef public object channel 7 | cdef public object delivery_info 8 | -------------------------------------------------------------------------------- /amqp/basic_message.py: -------------------------------------------------------------------------------- 1 | """AMQP Messages.""" 2 | # Copyright (C) 2007-2008 Barry Pederson 3 | from .serialization import GenericContent 4 | # Intended to fix #85: ImportError: cannot import name spec 5 | # Encountered on python 2.7.3 6 | # "The submodules often need to refer to each other. For example, the 7 | # surround [sic] module might use the echo module. In fact, such 8 | # references are so common that the import statement first looks in 9 | # the containing package before looking in the standard module search 10 | # path." 11 | # Source: 12 | # http://stackoverflow.com/a/14216937/4982251 13 | from .spec import Basic 14 | 15 | __all__ = ('Message',) 16 | 17 | 18 | class Message(GenericContent): 19 | """A Message for use with the Channel.basic_* methods. 20 | 21 | Expected arg types 22 | 23 | body: string 24 | children: (not supported) 25 | 26 | Keyword properties may include: 27 | 28 | content_type: shortstr 29 | MIME content type 30 | 31 | content_encoding: shortstr 32 | MIME content encoding 33 | 34 | application_headers: table 35 | Message header field table, a dict with string keys, 36 | and string | int | Decimal | datetime | dict values. 37 | 38 | delivery_mode: octet 39 | Non-persistent (1) or persistent (2) 40 | 41 | priority: octet 42 | The message priority, 0 to 9 43 | 44 | correlation_id: shortstr 45 | The application correlation identifier 46 | 47 | reply_to: shortstr 48 | The destination to reply to 49 | 50 | expiration: shortstr 51 | Message expiration specification 52 | 53 | message_id: shortstr 54 | The application message identifier 55 | 56 | timestamp: unsigned long 57 | The message timestamp 58 | 59 | type: shortstr 60 | The message type name 61 | 62 | user_id: shortstr 63 | The creating user id 64 | 65 | app_id: shortstr 66 | The creating application id 67 | 68 | cluster_id: shortstr 69 | Intra-cluster routing identifier 70 | 71 | Unicode bodies are encoded according to the 'content_encoding' 72 | argument. If that's None, it's set to 'UTF-8' automatically. 73 | 74 | Example:: 75 | 76 | msg = Message('hello world', 77 | content_type='text/plain', 78 | application_headers={'foo': 7}) 79 | """ 80 | 81 | CLASS_ID = Basic.CLASS_ID 82 | 83 | #: Instances of this class have these attributes, which 84 | #: are passed back and forth as message properties between 85 | #: client and server 86 | PROPERTIES = [ 87 | ('content_type', 's'), 88 | ('content_encoding', 's'), 89 | ('application_headers', 'F'), 90 | ('delivery_mode', 'o'), 91 | ('priority', 'o'), 92 | ('correlation_id', 's'), 93 | ('reply_to', 's'), 94 | ('expiration', 's'), 95 | ('message_id', 's'), 96 | ('timestamp', 'L'), 97 | ('type', 's'), 98 | ('user_id', 's'), 99 | ('app_id', 's'), 100 | ('cluster_id', 's') 101 | ] 102 | 103 | def __init__(self, body='', children=None, channel=None, **properties): 104 | super().__init__(**properties) 105 | #: set by basic_consume/basic_get 106 | self.delivery_info = None 107 | self.body = body 108 | self.channel = channel 109 | 110 | __slots__ = ( 111 | "delivery_info", 112 | "body", 113 | "channel", 114 | ) 115 | 116 | @property 117 | def headers(self): 118 | return self.properties.get('application_headers') 119 | 120 | @property 121 | def delivery_tag(self): 122 | return self.delivery_info.get('delivery_tag') 123 | -------------------------------------------------------------------------------- /amqp/exceptions.py: -------------------------------------------------------------------------------- 1 | """Exceptions used by amqp.""" 2 | # Copyright (C) 2007-2008 Barry Pederson 3 | 4 | from struct import pack, unpack 5 | 6 | __all__ = ( 7 | 'AMQPError', 8 | 'ConnectionError', 'ChannelError', 9 | 'RecoverableConnectionError', 'IrrecoverableConnectionError', 10 | 'RecoverableChannelError', 'IrrecoverableChannelError', 11 | 'ConsumerCancelled', 'ContentTooLarge', 'NoConsumers', 12 | 'ConnectionForced', 'InvalidPath', 'AccessRefused', 'NotFound', 13 | 'ResourceLocked', 'PreconditionFailed', 'FrameError', 'FrameSyntaxError', 14 | 'InvalidCommand', 'ChannelNotOpen', 'UnexpectedFrame', 'ResourceError', 15 | 'NotAllowed', 'AMQPNotImplementedError', 'InternalError', 16 | 'MessageNacked', 17 | 'AMQPDeprecationWarning', 18 | ) 19 | 20 | 21 | class AMQPDeprecationWarning(UserWarning): 22 | """Warning for deprecated things.""" 23 | 24 | 25 | class MessageNacked(Exception): 26 | """Message was nacked by broker.""" 27 | 28 | 29 | class AMQPError(Exception): 30 | """Base class for all AMQP exceptions.""" 31 | 32 | code = 0 33 | 34 | def __init__(self, reply_text=None, method_sig=None, 35 | method_name=None, reply_code=None): 36 | self.message = reply_text 37 | self.reply_code = reply_code or self.code 38 | self.reply_text = reply_text 39 | self.method_sig = method_sig 40 | self.method_name = method_name or '' 41 | if method_sig and not self.method_name: 42 | self.method_name = METHOD_NAME_MAP.get(method_sig, '') 43 | Exception.__init__(self, reply_code, 44 | reply_text, method_sig, self.method_name) 45 | 46 | def __str__(self): 47 | if self.method: 48 | return '{0.method}: ({0.reply_code}) {0.reply_text}'.format(self) 49 | return self.reply_text or '<{}: unknown error>'.format( 50 | type(self).__name__ 51 | ) 52 | 53 | @property 54 | def method(self): 55 | return self.method_name or self.method_sig 56 | 57 | 58 | class ConnectionError(AMQPError): 59 | """AMQP Connection Error.""" 60 | 61 | 62 | class ChannelError(AMQPError): 63 | """AMQP Channel Error.""" 64 | 65 | 66 | class RecoverableChannelError(ChannelError): 67 | """Exception class for recoverable channel errors.""" 68 | 69 | 70 | class IrrecoverableChannelError(ChannelError): 71 | """Exception class for irrecoverable channel errors.""" 72 | 73 | 74 | class RecoverableConnectionError(ConnectionError): 75 | """Exception class for recoverable connection errors.""" 76 | 77 | 78 | class IrrecoverableConnectionError(ConnectionError): 79 | """Exception class for irrecoverable connection errors.""" 80 | 81 | 82 | class Blocked(RecoverableConnectionError): 83 | """AMQP Connection Blocked Predicate.""" 84 | 85 | 86 | class ConsumerCancelled(RecoverableConnectionError): 87 | """AMQP Consumer Cancelled Predicate.""" 88 | 89 | 90 | class ContentTooLarge(RecoverableChannelError): 91 | """AMQP Content Too Large Error.""" 92 | 93 | code = 311 94 | 95 | 96 | class NoConsumers(RecoverableChannelError): 97 | """AMQP No Consumers Error.""" 98 | 99 | code = 313 100 | 101 | 102 | class ConnectionForced(RecoverableConnectionError): 103 | """AMQP Connection Forced Error.""" 104 | 105 | code = 320 106 | 107 | 108 | class InvalidPath(IrrecoverableConnectionError): 109 | """AMQP Invalid Path Error.""" 110 | 111 | code = 402 112 | 113 | 114 | class AccessRefused(IrrecoverableChannelError): 115 | """AMQP Access Refused Error.""" 116 | 117 | code = 403 118 | 119 | 120 | class NotFound(IrrecoverableChannelError): 121 | """AMQP Not Found Error.""" 122 | 123 | code = 404 124 | 125 | 126 | class ResourceLocked(RecoverableChannelError): 127 | """AMQP Resource Locked Error.""" 128 | 129 | code = 405 130 | 131 | 132 | class PreconditionFailed(IrrecoverableChannelError): 133 | """AMQP Precondition Failed Error.""" 134 | 135 | code = 406 136 | 137 | 138 | class FrameError(IrrecoverableConnectionError): 139 | """AMQP Frame Error.""" 140 | 141 | code = 501 142 | 143 | 144 | class FrameSyntaxError(IrrecoverableConnectionError): 145 | """AMQP Frame Syntax Error.""" 146 | 147 | code = 502 148 | 149 | 150 | class InvalidCommand(IrrecoverableConnectionError): 151 | """AMQP Invalid Command Error.""" 152 | 153 | code = 503 154 | 155 | 156 | class ChannelNotOpen(IrrecoverableConnectionError): 157 | """AMQP Channel Not Open Error.""" 158 | 159 | code = 504 160 | 161 | 162 | class UnexpectedFrame(IrrecoverableConnectionError): 163 | """AMQP Unexpected Frame.""" 164 | 165 | code = 505 166 | 167 | 168 | class ResourceError(RecoverableConnectionError): 169 | """AMQP Resource Error.""" 170 | 171 | code = 506 172 | 173 | 174 | class NotAllowed(IrrecoverableConnectionError): 175 | """AMQP Not Allowed Error.""" 176 | 177 | code = 530 178 | 179 | 180 | class AMQPNotImplementedError(IrrecoverableConnectionError): 181 | """AMQP Not Implemented Error.""" 182 | 183 | code = 540 184 | 185 | 186 | class InternalError(IrrecoverableConnectionError): 187 | """AMQP Internal Error.""" 188 | 189 | code = 541 190 | 191 | 192 | ERROR_MAP = { 193 | 311: ContentTooLarge, 194 | 313: NoConsumers, 195 | 320: ConnectionForced, 196 | 402: InvalidPath, 197 | 403: AccessRefused, 198 | 404: NotFound, 199 | 405: ResourceLocked, 200 | 406: PreconditionFailed, 201 | 501: FrameError, 202 | 502: FrameSyntaxError, 203 | 503: InvalidCommand, 204 | 504: ChannelNotOpen, 205 | 505: UnexpectedFrame, 206 | 506: ResourceError, 207 | 530: NotAllowed, 208 | 540: AMQPNotImplementedError, 209 | 541: InternalError, 210 | } 211 | 212 | 213 | def error_for_code(code, text, method, default): 214 | try: 215 | return ERROR_MAP[code](text, method, reply_code=code) 216 | except KeyError: 217 | return default(text, method, reply_code=code) 218 | 219 | 220 | METHOD_NAME_MAP = { 221 | (10, 10): 'Connection.start', 222 | (10, 11): 'Connection.start_ok', 223 | (10, 20): 'Connection.secure', 224 | (10, 21): 'Connection.secure_ok', 225 | (10, 30): 'Connection.tune', 226 | (10, 31): 'Connection.tune_ok', 227 | (10, 40): 'Connection.open', 228 | (10, 41): 'Connection.open_ok', 229 | (10, 50): 'Connection.close', 230 | (10, 51): 'Connection.close_ok', 231 | (20, 10): 'Channel.open', 232 | (20, 11): 'Channel.open_ok', 233 | (20, 20): 'Channel.flow', 234 | (20, 21): 'Channel.flow_ok', 235 | (20, 40): 'Channel.close', 236 | (20, 41): 'Channel.close_ok', 237 | (30, 10): 'Access.request', 238 | (30, 11): 'Access.request_ok', 239 | (40, 10): 'Exchange.declare', 240 | (40, 11): 'Exchange.declare_ok', 241 | (40, 20): 'Exchange.delete', 242 | (40, 21): 'Exchange.delete_ok', 243 | (40, 30): 'Exchange.bind', 244 | (40, 31): 'Exchange.bind_ok', 245 | (40, 40): 'Exchange.unbind', 246 | (40, 41): 'Exchange.unbind_ok', 247 | (50, 10): 'Queue.declare', 248 | (50, 11): 'Queue.declare_ok', 249 | (50, 20): 'Queue.bind', 250 | (50, 21): 'Queue.bind_ok', 251 | (50, 30): 'Queue.purge', 252 | (50, 31): 'Queue.purge_ok', 253 | (50, 40): 'Queue.delete', 254 | (50, 41): 'Queue.delete_ok', 255 | (50, 50): 'Queue.unbind', 256 | (50, 51): 'Queue.unbind_ok', 257 | (60, 10): 'Basic.qos', 258 | (60, 11): 'Basic.qos_ok', 259 | (60, 20): 'Basic.consume', 260 | (60, 21): 'Basic.consume_ok', 261 | (60, 30): 'Basic.cancel', 262 | (60, 31): 'Basic.cancel_ok', 263 | (60, 40): 'Basic.publish', 264 | (60, 50): 'Basic.return', 265 | (60, 60): 'Basic.deliver', 266 | (60, 70): 'Basic.get', 267 | (60, 71): 'Basic.get_ok', 268 | (60, 72): 'Basic.get_empty', 269 | (60, 80): 'Basic.ack', 270 | (60, 90): 'Basic.reject', 271 | (60, 100): 'Basic.recover_async', 272 | (60, 110): 'Basic.recover', 273 | (60, 111): 'Basic.recover_ok', 274 | (60, 120): 'Basic.nack', 275 | (90, 10): 'Tx.select', 276 | (90, 11): 'Tx.select_ok', 277 | (90, 20): 'Tx.commit', 278 | (90, 21): 'Tx.commit_ok', 279 | (90, 30): 'Tx.rollback', 280 | (90, 31): 'Tx.rollback_ok', 281 | (85, 10): 'Confirm.select', 282 | (85, 11): 'Confirm.select_ok', 283 | } 284 | 285 | 286 | for _method_id, _method_name in list(METHOD_NAME_MAP.items()): 287 | METHOD_NAME_MAP[unpack('>I', pack('>HH', *_method_id))[0]] = \ 288 | _method_name 289 | -------------------------------------------------------------------------------- /amqp/method_framing.pxd: -------------------------------------------------------------------------------- 1 | # cython: language_level=3 2 | from .basic_message cimport Message 3 | 4 | 5 | cdef object FRAME_OVERHEAD 6 | cdef object _CONTENT_METHODS 7 | 8 | cdef class Buffer: 9 | cdef bytearray _buf 10 | cdef object view 11 | -------------------------------------------------------------------------------- /amqp/method_framing.py: -------------------------------------------------------------------------------- 1 | """Convert between frames and higher-level AMQP methods.""" 2 | # Copyright (C) 2007-2008 Barry Pederson 3 | 4 | from collections import defaultdict 5 | from struct import pack, pack_into, unpack_from 6 | 7 | from . import spec 8 | from .basic_message import Message 9 | from .exceptions import UnexpectedFrame 10 | from .utils import str_to_bytes 11 | 12 | __all__ = ('frame_handler', 'frame_writer') 13 | 14 | #: Set of methods that require both a content frame and a body frame. 15 | _CONTENT_METHODS = frozenset([ 16 | spec.Basic.Return, 17 | spec.Basic.Deliver, 18 | spec.Basic.GetOk, 19 | ]) 20 | 21 | 22 | #: Number of bytes reserved for protocol in a content frame. 23 | #: We use this to calculate when a frame exceeeds the max frame size, 24 | #: and if it does not the message will fit into the preallocated buffer. 25 | FRAME_OVERHEAD = 40 26 | 27 | 28 | def frame_handler(connection, callback, 29 | unpack_from=unpack_from, content_methods=_CONTENT_METHODS): 30 | """Create closure that reads frames.""" 31 | expected_types = defaultdict(lambda: 1) 32 | partial_messages = {} 33 | 34 | def on_frame(frame): 35 | frame_type, channel, buf = frame 36 | connection.bytes_recv += 1 37 | if frame_type not in (expected_types[channel], 8): 38 | raise UnexpectedFrame( 39 | 'Received frame {} while expecting type: {}'.format( 40 | frame_type, expected_types[channel]), 41 | ) 42 | elif frame_type == 1: 43 | method_sig = unpack_from('>HH', buf, 0) 44 | 45 | if method_sig in content_methods: 46 | # Save what we've got so far and wait for the content-header 47 | partial_messages[channel] = Message( 48 | frame_method=method_sig, frame_args=buf, 49 | ) 50 | expected_types[channel] = 2 51 | return False 52 | 53 | callback(channel, method_sig, buf, None) 54 | 55 | elif frame_type == 2: 56 | msg = partial_messages[channel] 57 | msg.inbound_header(buf) 58 | 59 | if not msg.ready: 60 | # wait for the content-body 61 | expected_types[channel] = 3 62 | return False 63 | 64 | # bodyless message, we're done 65 | expected_types[channel] = 1 66 | partial_messages.pop(channel, None) 67 | callback(channel, msg.frame_method, msg.frame_args, msg) 68 | 69 | elif frame_type == 3: 70 | msg = partial_messages[channel] 71 | msg.inbound_body(buf) 72 | if not msg.ready: 73 | # wait for the rest of the content-body 74 | return False 75 | expected_types[channel] = 1 76 | partial_messages.pop(channel, None) 77 | callback(channel, msg.frame_method, msg.frame_args, msg) 78 | elif frame_type == 8: 79 | # bytes_recv already updated 80 | return False 81 | return True 82 | 83 | return on_frame 84 | 85 | 86 | class Buffer: 87 | def __init__(self, buf): 88 | self.buf = buf 89 | 90 | @property 91 | def buf(self): 92 | return self._buf 93 | 94 | @buf.setter 95 | def buf(self, buf): 96 | self._buf = buf 97 | # Using a memoryview allows slicing without copying underlying data. 98 | # Slicing this is much faster than slicing the bytearray directly. 99 | # More details: https://stackoverflow.com/a/34257357 100 | self.view = memoryview(buf) 101 | 102 | 103 | def frame_writer(connection, transport, 104 | pack=pack, pack_into=pack_into, range=range, len=len, 105 | bytes=bytes, str_to_bytes=str_to_bytes, text_t=str): 106 | """Create closure that writes frames.""" 107 | write = transport.write 108 | 109 | buffer_store = Buffer(bytearray(connection.frame_max - 8)) 110 | 111 | def write_frame(type_, channel, method_sig, args, content): 112 | chunk_size = connection.frame_max - 8 113 | offset = 0 114 | properties = None 115 | args = str_to_bytes(args) 116 | if content: 117 | body = content.body 118 | if isinstance(body, str): 119 | encoding = content.properties.setdefault( 120 | 'content_encoding', 'utf-8') 121 | body = body.encode(encoding) 122 | properties = content._serialize_properties() 123 | bodylen = len(body) 124 | properties_len = len(properties) or 0 125 | framelen = len(args) + properties_len + bodylen + FRAME_OVERHEAD 126 | bigbody = framelen > chunk_size 127 | else: 128 | body, bodylen, bigbody = None, 0, 0 129 | 130 | if bigbody: 131 | # ## SLOW: string copy and write for every frame 132 | frame = (b''.join([pack('>HH', *method_sig), args]) 133 | if type_ == 1 else b'') # encode method frame 134 | framelen = len(frame) 135 | write(pack('>BHI%dsB' % framelen, 136 | type_, channel, framelen, frame, 0xce)) 137 | if body: 138 | frame = b''.join([ 139 | pack('>HHQ', method_sig[0], 0, len(body)), 140 | properties, 141 | ]) 142 | framelen = len(frame) 143 | write(pack('>BHI%dsB' % framelen, 144 | 2, channel, framelen, frame, 0xce)) 145 | 146 | for i in range(0, bodylen, chunk_size): 147 | frame = body[i:i + chunk_size] 148 | framelen = len(frame) 149 | write(pack('>BHI%dsB' % framelen, 150 | 3, channel, framelen, 151 | frame, 0xce)) 152 | 153 | else: 154 | # frame_max can be updated via connection._on_tune. If 155 | # it became larger, then we need to resize the buffer 156 | # to prevent overflow. 157 | if chunk_size > len(buffer_store.buf): 158 | buffer_store.buf = bytearray(chunk_size) 159 | buf = buffer_store.buf 160 | 161 | # ## FAST: pack into buffer and single write 162 | frame = (b''.join([pack('>HH', *method_sig), args]) 163 | if type_ == 1 else b'') 164 | framelen = len(frame) 165 | pack_into('>BHI%dsB' % framelen, buf, offset, 166 | type_, channel, framelen, frame, 0xce) 167 | offset += 8 + framelen 168 | if body is not None: 169 | frame = b''.join([ 170 | pack('>HHQ', method_sig[0], 0, len(body)), 171 | properties, 172 | ]) 173 | framelen = len(frame) 174 | 175 | pack_into('>BHI%dsB' % framelen, buf, offset, 176 | 2, channel, framelen, frame, 0xce) 177 | offset += 8 + framelen 178 | 179 | bodylen = len(body) 180 | if bodylen > 0: 181 | framelen = bodylen 182 | pack_into('>BHI%dsB' % framelen, buf, offset, 183 | 3, channel, framelen, body, 0xce) 184 | offset += 8 + framelen 185 | 186 | write(buffer_store.view[:offset]) 187 | 188 | connection.bytes_sent += 1 189 | return write_frame 190 | -------------------------------------------------------------------------------- /amqp/platform.py: -------------------------------------------------------------------------------- 1 | """Platform compatibility.""" 2 | 3 | import platform 4 | import re 5 | import sys 6 | # Jython does not have this attribute 7 | import typing 8 | 9 | try: 10 | from socket import SOL_TCP 11 | except ImportError: # pragma: no cover 12 | from socket import IPPROTO_TCP as SOL_TCP # noqa 13 | 14 | 15 | RE_NUM = re.compile(r'(\d+).+') 16 | 17 | 18 | def _linux_version_to_tuple(s: str) -> typing.Tuple[int, int, int]: 19 | return tuple(map(_versionatom, s.split('.')[:3])) 20 | 21 | 22 | def _versionatom(s: str) -> int: 23 | if s.isdigit(): 24 | return int(s) 25 | match = RE_NUM.match(s) 26 | return int(match.groups()[0]) if match else 0 27 | 28 | 29 | # available socket options for TCP level 30 | KNOWN_TCP_OPTS = { 31 | 'TCP_CORK', 'TCP_DEFER_ACCEPT', 'TCP_KEEPCNT', 32 | 'TCP_KEEPIDLE', 'TCP_KEEPINTVL', 'TCP_LINGER2', 33 | 'TCP_MAXSEG', 'TCP_NODELAY', 'TCP_QUICKACK', 34 | 'TCP_SYNCNT', 'TCP_USER_TIMEOUT', 'TCP_WINDOW_CLAMP', 35 | } 36 | 37 | LINUX_VERSION = None 38 | if sys.platform.startswith('linux'): 39 | LINUX_VERSION = _linux_version_to_tuple(platform.release()) 40 | if LINUX_VERSION < (2, 6, 37): 41 | KNOWN_TCP_OPTS.remove('TCP_USER_TIMEOUT') 42 | 43 | # Windows Subsystem for Linux is an edge-case: the Python socket library 44 | # returns most TCP_* enums, but they aren't actually supported 45 | if platform.release().endswith("Microsoft"): 46 | KNOWN_TCP_OPTS = {'TCP_NODELAY', 'TCP_KEEPIDLE', 'TCP_KEEPINTVL', 47 | 'TCP_KEEPCNT'} 48 | 49 | elif sys.platform.startswith('darwin'): 50 | KNOWN_TCP_OPTS.remove('TCP_USER_TIMEOUT') 51 | 52 | elif 'bsd' in sys.platform: 53 | KNOWN_TCP_OPTS.remove('TCP_USER_TIMEOUT') 54 | 55 | # According to MSDN Windows platforms support getsockopt(TCP_MAXSSEG) but not 56 | # setsockopt(TCP_MAXSEG) on IPPROTO_TCP sockets. 57 | elif sys.platform.startswith('win'): 58 | KNOWN_TCP_OPTS = {'TCP_NODELAY'} 59 | 60 | elif sys.platform.startswith('cygwin'): 61 | KNOWN_TCP_OPTS = {'TCP_NODELAY'} 62 | 63 | # illumos does not allow to set the TCP_MAXSEG socket option, 64 | # even if the Oracle documentation says otherwise. 65 | # TCP_USER_TIMEOUT does not exist on Solaris 11.4 66 | elif sys.platform.startswith('sunos'): 67 | KNOWN_TCP_OPTS.remove('TCP_MAXSEG') 68 | KNOWN_TCP_OPTS.remove('TCP_USER_TIMEOUT') 69 | 70 | # aix does not allow to set the TCP_MAXSEG 71 | # or the TCP_USER_TIMEOUT socket options. 72 | elif sys.platform.startswith('aix'): 73 | KNOWN_TCP_OPTS.remove('TCP_MAXSEG') 74 | KNOWN_TCP_OPTS.remove('TCP_USER_TIMEOUT') 75 | __all__ = ( 76 | 'LINUX_VERSION', 77 | 'SOL_TCP', 78 | 'KNOWN_TCP_OPTS', 79 | ) 80 | -------------------------------------------------------------------------------- /amqp/protocol.py: -------------------------------------------------------------------------------- 1 | """Protocol data.""" 2 | 3 | from collections import namedtuple 4 | 5 | queue_declare_ok_t = namedtuple( 6 | 'queue_declare_ok_t', ('queue', 'message_count', 'consumer_count'), 7 | ) 8 | 9 | basic_return_t = namedtuple( 10 | 'basic_return_t', 11 | ('reply_code', 'reply_text', 'exchange', 'routing_key', 'message'), 12 | ) 13 | -------------------------------------------------------------------------------- /amqp/sasl.py: -------------------------------------------------------------------------------- 1 | """SASL mechanisms for AMQP authentication.""" 2 | 3 | import socket 4 | import warnings 5 | from io import BytesIO 6 | 7 | from amqp.serialization import _write_table 8 | 9 | 10 | class SASL: 11 | """The base class for all amqp SASL authentication mechanisms. 12 | 13 | You should sub-class this if you're implementing your own authentication. 14 | """ 15 | 16 | @property 17 | def mechanism(self): 18 | """Return a bytes containing the SASL mechanism name.""" 19 | raise NotImplementedError 20 | 21 | def start(self, connection): 22 | """Return the first response to a SASL challenge as a bytes object.""" 23 | raise NotImplementedError 24 | 25 | 26 | class PLAIN(SASL): 27 | """PLAIN SASL authentication mechanism. 28 | 29 | See https://tools.ietf.org/html/rfc4616 for details 30 | """ 31 | 32 | mechanism = b'PLAIN' 33 | 34 | def __init__(self, username, password): 35 | self.username, self.password = username, password 36 | 37 | __slots__ = ( 38 | "username", 39 | "password", 40 | ) 41 | 42 | def start(self, connection): 43 | if self.username is None or self.password is None: 44 | return NotImplemented 45 | login_response = BytesIO() 46 | login_response.write(b'\0') 47 | login_response.write(self.username.encode('utf-8')) 48 | login_response.write(b'\0') 49 | login_response.write(self.password.encode('utf-8')) 50 | return login_response.getvalue() 51 | 52 | 53 | class AMQPLAIN(SASL): 54 | """AMQPLAIN SASL authentication mechanism. 55 | 56 | This is a non-standard mechanism used by AMQP servers. 57 | """ 58 | 59 | mechanism = b'AMQPLAIN' 60 | 61 | def __init__(self, username, password): 62 | self.username, self.password = username, password 63 | 64 | __slots__ = ( 65 | "username", 66 | "password", 67 | ) 68 | 69 | def start(self, connection): 70 | if self.username is None or self.password is None: 71 | return NotImplemented 72 | login_response = BytesIO() 73 | _write_table({b'LOGIN': self.username, b'PASSWORD': self.password}, 74 | login_response.write, []) 75 | # Skip the length at the beginning 76 | return login_response.getvalue()[4:] 77 | 78 | 79 | def _get_gssapi_mechanism(): 80 | try: 81 | import gssapi 82 | import gssapi.raw.misc # Fail if the old python-gssapi is installed 83 | except ImportError: 84 | class FakeGSSAPI(SASL): 85 | """A no-op SASL mechanism for when gssapi isn't available.""" 86 | 87 | mechanism = None 88 | 89 | def __init__(self, client_name=None, service=b'amqp', 90 | rdns=False, fail_soft=False): 91 | if not fail_soft: 92 | raise NotImplementedError( 93 | "You need to install the `gssapi` module for GSSAPI " 94 | "SASL support") 95 | 96 | def start(self): # pragma: no cover 97 | return NotImplemented 98 | return FakeGSSAPI 99 | else: 100 | class GSSAPI(SASL): 101 | """GSSAPI SASL authentication mechanism. 102 | 103 | See https://tools.ietf.org/html/rfc4752 for details 104 | """ 105 | 106 | mechanism = b'GSSAPI' 107 | 108 | def __init__(self, client_name=None, service=b'amqp', 109 | rdns=False, fail_soft=False): 110 | if client_name and not isinstance(client_name, bytes): 111 | client_name = client_name.encode('ascii') 112 | self.client_name = client_name 113 | self.fail_soft = fail_soft 114 | self.service = service 115 | self.rdns = rdns 116 | 117 | __slots__ = ( 118 | "client_name", 119 | "fail_soft", 120 | "service", 121 | "rdns" 122 | ) 123 | 124 | def get_hostname(self, connection): 125 | sock = connection.transport.sock 126 | if self.rdns and sock.family in (socket.AF_INET, 127 | socket.AF_INET6): 128 | peer = sock.getpeername() 129 | hostname, _, _ = socket.gethostbyaddr(peer[0]) 130 | else: 131 | hostname = connection.transport.host 132 | if not isinstance(hostname, bytes): 133 | hostname = hostname.encode('ascii') 134 | return hostname 135 | 136 | def start(self, connection): 137 | try: 138 | if self.client_name: 139 | creds = gssapi.Credentials( 140 | name=gssapi.Name(self.client_name)) 141 | else: 142 | creds = None 143 | hostname = self.get_hostname(connection) 144 | name = gssapi.Name(b'@'.join([self.service, hostname]), 145 | gssapi.NameType.hostbased_service) 146 | context = gssapi.SecurityContext(name=name, creds=creds) 147 | return context.step(None) 148 | except gssapi.raw.misc.GSSError: 149 | if self.fail_soft: 150 | return NotImplemented 151 | else: 152 | raise 153 | return GSSAPI 154 | 155 | 156 | GSSAPI = _get_gssapi_mechanism() 157 | 158 | 159 | class EXTERNAL(SASL): 160 | """EXTERNAL SASL mechanism. 161 | 162 | Enables external authentication, i.e. not handled through this protocol. 163 | Only passes 'EXTERNAL' as authentication mechanism, but no further 164 | authentication data. 165 | """ 166 | 167 | mechanism = b'EXTERNAL' 168 | 169 | def start(self, connection): 170 | return b'' 171 | 172 | 173 | class RAW(SASL): 174 | """A generic custom SASL mechanism. 175 | 176 | This mechanism takes a mechanism name and response to send to the server, 177 | so can be used for simple custom authentication schemes. 178 | """ 179 | 180 | mechanism = None 181 | 182 | def __init__(self, mechanism, response): 183 | assert isinstance(mechanism, bytes) 184 | assert isinstance(response, bytes) 185 | self.mechanism, self.response = mechanism, response 186 | warnings.warn("Passing login_method and login_response to Connection " 187 | "is deprecated. Please implement a SASL subclass " 188 | "instead.", DeprecationWarning) 189 | 190 | def start(self, connection): 191 | return self.response 192 | -------------------------------------------------------------------------------- /amqp/serialization.pxd: -------------------------------------------------------------------------------- 1 | # cython: language_level=3 2 | import cython 3 | 4 | from amqp.utils cimport bytes_to_str as pstr_t 5 | from amqp.utils cimport str_to_bytes 6 | 7 | 8 | cdef int _flushbits(list bits, write) 9 | 10 | # Does not raise FrameSyntaxError due performance reasons 11 | @cython.locals(blen=cython.int, limit=cython.int, keylen=cython.int, tlen=cython.int, alen=cython.int, blen=cython.int, slen=cython.int, d=cython.int) 12 | cpdef tuple _read_item(buf, int offset) 13 | 14 | # Does not raise FrameSyntaxError due performance reasons 15 | @cython.locals(bitcount=cython.int, bits=cython.int, tlen=cython.int, limit=cython.int, slen=cython.int, keylen=cython.int) 16 | cpdef tuple loads(format, buf, int offset) 17 | 18 | @cython.locals(bitcount=cython.int, shift=cython.int) 19 | cpdef dumps(format, values) 20 | 21 | # Does not raise FrameSyntaxError due performance reasons 22 | cpdef int _write_table(d, write, bits) except -1 23 | 24 | # Does not raise FrameSyntaxError due performance reasons 25 | cdef int _write_array(l, write, bits) except -1 26 | 27 | @cython.locals(slen=cython.int, flags=cython.ushort) 28 | cdef tuple decode_properties_basic(buf, int offset) 29 | 30 | cdef int _write_item(v, write, bits) except -1 31 | 32 | cdef dict PROPERTY_CLASSES 33 | 34 | cdef class GenericContent: 35 | cdef public object frame_method 36 | cdef public object frame_args 37 | cdef public object body 38 | cdef list _pending_chunks 39 | cdef public int body_received 40 | cdef public int body_size 41 | cdef public bint ready 42 | cdef public dict properties 43 | 44 | @cython.locals(shift=cython.int, flag_bits=cython.int, flags=list) 45 | cpdef bytes _serialize_properties(self) 46 | 47 | cdef int _load_properties(self, class_id, buf, offset) 48 | -------------------------------------------------------------------------------- /amqp/serialization.py: -------------------------------------------------------------------------------- 1 | """Convert between bytestreams and higher-level AMQP types. 2 | 3 | 2007-11-05 Barry Pederson 4 | 5 | """ 6 | # Copyright (C) 2007 Barry Pederson 7 | 8 | import calendar 9 | from datetime import datetime 10 | from decimal import Decimal 11 | from io import BytesIO 12 | from struct import pack, unpack_from 13 | 14 | from .exceptions import FrameSyntaxError 15 | from .spec import Basic 16 | from .utils import bytes_to_str as pstr_t 17 | from .utils import str_to_bytes 18 | 19 | ILLEGAL_TABLE_TYPE = """\ 20 | Table type {0!r} not handled by amqp. 21 | """ 22 | 23 | ILLEGAL_TABLE_TYPE_WITH_KEY = """\ 24 | Table type {0!r} for key {1!r} not handled by amqp. [value: {2!r}] 25 | """ 26 | 27 | ILLEGAL_TABLE_TYPE_WITH_VALUE = """\ 28 | Table type {0!r} not handled by amqp. [value: {1!r}] 29 | """ 30 | 31 | 32 | def _read_item(buf, offset): 33 | ftype = chr(buf[offset]) 34 | offset += 1 35 | 36 | # 'S': long string 37 | if ftype == 'S': 38 | slen, = unpack_from('>I', buf, offset) 39 | offset += 4 40 | try: 41 | val = pstr_t(buf[offset:offset + slen]) 42 | except UnicodeDecodeError: 43 | val = buf[offset:offset + slen] 44 | 45 | offset += slen 46 | # 's': short string 47 | elif ftype == 's': 48 | slen, = unpack_from('>B', buf, offset) 49 | offset += 1 50 | val = pstr_t(buf[offset:offset + slen]) 51 | offset += slen 52 | # 'x': Bytes Array 53 | elif ftype == 'x': 54 | blen, = unpack_from('>I', buf, offset) 55 | offset += 4 56 | val = buf[offset:offset + blen] 57 | offset += blen 58 | # 'b': short-short int 59 | elif ftype == 'b': 60 | val, = unpack_from('>B', buf, offset) 61 | offset += 1 62 | # 'B': short-short unsigned int 63 | elif ftype == 'B': 64 | val, = unpack_from('>b', buf, offset) 65 | offset += 1 66 | # 'U': short int 67 | elif ftype == 'U': 68 | val, = unpack_from('>h', buf, offset) 69 | offset += 2 70 | # 'u': short unsigned int 71 | elif ftype == 'u': 72 | val, = unpack_from('>H', buf, offset) 73 | offset += 2 74 | # 'I': long int 75 | elif ftype == 'I': 76 | val, = unpack_from('>i', buf, offset) 77 | offset += 4 78 | # 'i': long unsigned int 79 | elif ftype == 'i': 80 | val, = unpack_from('>I', buf, offset) 81 | offset += 4 82 | # 'L': long long int 83 | elif ftype == 'L': 84 | val, = unpack_from('>q', buf, offset) 85 | offset += 8 86 | # 'l': long long unsigned int 87 | elif ftype == 'l': 88 | val, = unpack_from('>Q', buf, offset) 89 | offset += 8 90 | # 'f': float 91 | elif ftype == 'f': 92 | val, = unpack_from('>f', buf, offset) 93 | offset += 4 94 | # 'd': double 95 | elif ftype == 'd': 96 | val, = unpack_from('>d', buf, offset) 97 | offset += 8 98 | # 'D': decimal 99 | elif ftype == 'D': 100 | d, = unpack_from('>B', buf, offset) 101 | offset += 1 102 | n, = unpack_from('>i', buf, offset) 103 | offset += 4 104 | val = Decimal(n) / Decimal(10 ** d) 105 | # 'F': table 106 | elif ftype == 'F': 107 | tlen, = unpack_from('>I', buf, offset) 108 | offset += 4 109 | limit = offset + tlen 110 | val = {} 111 | while offset < limit: 112 | keylen, = unpack_from('>B', buf, offset) 113 | offset += 1 114 | key = pstr_t(buf[offset:offset + keylen]) 115 | offset += keylen 116 | val[key], offset = _read_item(buf, offset) 117 | # 'A': array 118 | elif ftype == 'A': 119 | alen, = unpack_from('>I', buf, offset) 120 | offset += 4 121 | limit = offset + alen 122 | val = [] 123 | while offset < limit: 124 | v, offset = _read_item(buf, offset) 125 | val.append(v) 126 | # 't' (bool) 127 | elif ftype == 't': 128 | val, = unpack_from('>B', buf, offset) 129 | val = bool(val) 130 | offset += 1 131 | # 'T': timestamp 132 | elif ftype == 'T': 133 | val, = unpack_from('>Q', buf, offset) 134 | offset += 8 135 | val = datetime.utcfromtimestamp(val) 136 | # 'V': void 137 | elif ftype == 'V': 138 | val = None 139 | else: 140 | raise FrameSyntaxError( 141 | 'Unknown value in table: {!r} ({!r})'.format( 142 | ftype, type(ftype))) 143 | return val, offset 144 | 145 | 146 | def loads(format, buf, offset): 147 | """Deserialize amqp format. 148 | 149 | bit = b 150 | octet = o 151 | short = B 152 | long = l 153 | long long = L 154 | float = f 155 | shortstr = s 156 | longstr = S 157 | table = F 158 | array = A 159 | timestamp = T 160 | """ 161 | bitcount = bits = 0 162 | 163 | values = [] 164 | append = values.append 165 | format = pstr_t(format) 166 | 167 | for p in format: 168 | if p == 'b': 169 | if not bitcount: 170 | bits = ord(buf[offset:offset + 1]) 171 | offset += 1 172 | bitcount = 8 173 | val = (bits & 1) == 1 174 | bits >>= 1 175 | bitcount -= 1 176 | elif p == 'o': 177 | bitcount = bits = 0 178 | val, = unpack_from('>B', buf, offset) 179 | offset += 1 180 | elif p == 'B': 181 | bitcount = bits = 0 182 | val, = unpack_from('>H', buf, offset) 183 | offset += 2 184 | elif p == 'l': 185 | bitcount = bits = 0 186 | val, = unpack_from('>I', buf, offset) 187 | offset += 4 188 | elif p == 'L': 189 | bitcount = bits = 0 190 | val, = unpack_from('>Q', buf, offset) 191 | offset += 8 192 | elif p == 'f': 193 | bitcount = bits = 0 194 | val, = unpack_from('>f', buf, offset) 195 | offset += 4 196 | elif p == 's': 197 | bitcount = bits = 0 198 | slen, = unpack_from('B', buf, offset) 199 | offset += 1 200 | val = buf[offset:offset + slen].decode('utf-8', 'surrogatepass') 201 | offset += slen 202 | elif p == 'S': 203 | bitcount = bits = 0 204 | slen, = unpack_from('>I', buf, offset) 205 | offset += 4 206 | val = buf[offset:offset + slen].decode('utf-8', 'surrogatepass') 207 | offset += slen 208 | elif p == 'x': 209 | blen, = unpack_from('>I', buf, offset) 210 | offset += 4 211 | val = buf[offset:offset + blen] 212 | offset += blen 213 | elif p == 'F': 214 | bitcount = bits = 0 215 | tlen, = unpack_from('>I', buf, offset) 216 | offset += 4 217 | limit = offset + tlen 218 | val = {} 219 | while offset < limit: 220 | keylen, = unpack_from('>B', buf, offset) 221 | offset += 1 222 | key = pstr_t(buf[offset:offset + keylen]) 223 | offset += keylen 224 | val[key], offset = _read_item(buf, offset) 225 | elif p == 'A': 226 | bitcount = bits = 0 227 | alen, = unpack_from('>I', buf, offset) 228 | offset += 4 229 | limit = offset + alen 230 | val = [] 231 | while offset < limit: 232 | aval, offset = _read_item(buf, offset) 233 | val.append(aval) 234 | elif p == 'T': 235 | bitcount = bits = 0 236 | val, = unpack_from('>Q', buf, offset) 237 | offset += 8 238 | val = datetime.utcfromtimestamp(val) 239 | else: 240 | raise FrameSyntaxError(ILLEGAL_TABLE_TYPE.format(p)) 241 | append(val) 242 | return values, offset 243 | 244 | 245 | def _flushbits(bits, write): 246 | if bits: 247 | write(pack('B' * len(bits), *bits)) 248 | bits[:] = [] 249 | return 0 250 | 251 | 252 | def dumps(format, values): 253 | """Serialize AMQP arguments. 254 | 255 | Notes: 256 | bit = b 257 | octet = o 258 | short = B 259 | long = l 260 | long long = L 261 | shortstr = s 262 | longstr = S 263 | byte array = x 264 | table = F 265 | array = A 266 | """ 267 | bitcount = 0 268 | bits = [] 269 | out = BytesIO() 270 | write = out.write 271 | 272 | format = pstr_t(format) 273 | 274 | for i, val in enumerate(values): 275 | p = format[i] 276 | if p == 'b': 277 | val = 1 if val else 0 278 | shift = bitcount % 8 279 | if shift == 0: 280 | bits.append(0) 281 | bits[-1] |= (val << shift) 282 | bitcount += 1 283 | elif p == 'o': 284 | bitcount = _flushbits(bits, write) 285 | write(pack('B', val)) 286 | elif p == 'B': 287 | bitcount = _flushbits(bits, write) 288 | write(pack('>H', int(val))) 289 | elif p == 'l': 290 | bitcount = _flushbits(bits, write) 291 | write(pack('>I', val)) 292 | elif p == 'L': 293 | bitcount = _flushbits(bits, write) 294 | write(pack('>Q', val)) 295 | elif p == 'f': 296 | bitcount = _flushbits(bits, write) 297 | write(pack('>f', val)) 298 | elif p == 's': 299 | val = val or '' 300 | bitcount = _flushbits(bits, write) 301 | if isinstance(val, str): 302 | val = val.encode('utf-8', 'surrogatepass') 303 | write(pack('B', len(val))) 304 | write(val) 305 | elif p == 'S' or p == 'x': 306 | val = val or '' 307 | bitcount = _flushbits(bits, write) 308 | if isinstance(val, str): 309 | val = val.encode('utf-8', 'surrogatepass') 310 | write(pack('>I', len(val))) 311 | write(val) 312 | elif p == 'F': 313 | bitcount = _flushbits(bits, write) 314 | _write_table(val or {}, write, bits) 315 | elif p == 'A': 316 | bitcount = _flushbits(bits, write) 317 | _write_array(val or [], write, bits) 318 | elif p == 'T': 319 | write(pack('>Q', int(calendar.timegm(val.utctimetuple())))) 320 | _flushbits(bits, write) 321 | 322 | return out.getvalue() 323 | 324 | 325 | def _write_table(d, write, bits): 326 | out = BytesIO() 327 | twrite = out.write 328 | for k, v in d.items(): 329 | if isinstance(k, str): 330 | k = k.encode('utf-8', 'surrogatepass') 331 | twrite(pack('B', len(k))) 332 | twrite(k) 333 | try: 334 | _write_item(v, twrite, bits) 335 | except ValueError: 336 | raise FrameSyntaxError( 337 | ILLEGAL_TABLE_TYPE_WITH_KEY.format(type(v), k, v)) 338 | table_data = out.getvalue() 339 | write(pack('>I', len(table_data))) 340 | write(table_data) 341 | 342 | 343 | def _write_array(list_, write, bits): 344 | out = BytesIO() 345 | awrite = out.write 346 | for v in list_: 347 | try: 348 | _write_item(v, awrite, bits) 349 | except ValueError: 350 | raise FrameSyntaxError( 351 | ILLEGAL_TABLE_TYPE_WITH_VALUE.format(type(v), v)) 352 | array_data = out.getvalue() 353 | write(pack('>I', len(array_data))) 354 | write(array_data) 355 | 356 | 357 | def _write_item(v, write, bits): 358 | if isinstance(v, (str, bytes)): 359 | if isinstance(v, str): 360 | v = v.encode('utf-8', 'surrogatepass') 361 | write(pack('>cI', b'S', len(v))) 362 | write(v) 363 | elif isinstance(v, bool): 364 | write(pack('>cB', b't', int(v))) 365 | elif isinstance(v, float): 366 | write(pack('>cd', b'd', v)) 367 | elif isinstance(v, int): 368 | if v > 2147483647 or v < -2147483647: 369 | write(pack('>cq', b'L', v)) 370 | else: 371 | write(pack('>ci', b'I', v)) 372 | elif isinstance(v, Decimal): 373 | sign, digits, exponent = v.as_tuple() 374 | v = 0 375 | for d in digits: 376 | v = (v * 10) + d 377 | if sign: 378 | v = -v 379 | write(pack('>cBi', b'D', -exponent, v)) 380 | elif isinstance(v, datetime): 381 | write( 382 | pack('>cQ', b'T', int(calendar.timegm(v.utctimetuple())))) 383 | elif isinstance(v, dict): 384 | write(b'F') 385 | _write_table(v, write, bits) 386 | elif isinstance(v, (list, tuple)): 387 | write(b'A') 388 | _write_array(v, write, bits) 389 | elif v is None: 390 | write(b'V') 391 | else: 392 | raise ValueError() 393 | 394 | 395 | def decode_properties_basic(buf, offset): 396 | """Decode basic properties.""" 397 | properties = {} 398 | 399 | flags, = unpack_from('>H', buf, offset) 400 | offset += 2 401 | 402 | if flags & 0x8000: 403 | slen, = unpack_from('>B', buf, offset) 404 | offset += 1 405 | properties['content_type'] = pstr_t(buf[offset:offset + slen]) 406 | offset += slen 407 | if flags & 0x4000: 408 | slen, = unpack_from('>B', buf, offset) 409 | offset += 1 410 | properties['content_encoding'] = pstr_t(buf[offset:offset + slen]) 411 | offset += slen 412 | if flags & 0x2000: 413 | _f, offset = loads('F', buf, offset) 414 | properties['application_headers'], = _f 415 | if flags & 0x1000: 416 | properties['delivery_mode'], = unpack_from('>B', buf, offset) 417 | offset += 1 418 | if flags & 0x0800: 419 | properties['priority'], = unpack_from('>B', buf, offset) 420 | offset += 1 421 | if flags & 0x0400: 422 | slen, = unpack_from('>B', buf, offset) 423 | offset += 1 424 | properties['correlation_id'] = pstr_t(buf[offset:offset + slen]) 425 | offset += slen 426 | if flags & 0x0200: 427 | slen, = unpack_from('>B', buf, offset) 428 | offset += 1 429 | properties['reply_to'] = pstr_t(buf[offset:offset + slen]) 430 | offset += slen 431 | if flags & 0x0100: 432 | slen, = unpack_from('>B', buf, offset) 433 | offset += 1 434 | properties['expiration'] = pstr_t(buf[offset:offset + slen]) 435 | offset += slen 436 | if flags & 0x0080: 437 | slen, = unpack_from('>B', buf, offset) 438 | offset += 1 439 | properties['message_id'] = pstr_t(buf[offset:offset + slen]) 440 | offset += slen 441 | if flags & 0x0040: 442 | properties['timestamp'], = unpack_from('>Q', buf, offset) 443 | offset += 8 444 | if flags & 0x0020: 445 | slen, = unpack_from('>B', buf, offset) 446 | offset += 1 447 | properties['type'] = pstr_t(buf[offset:offset + slen]) 448 | offset += slen 449 | if flags & 0x0010: 450 | slen, = unpack_from('>B', buf, offset) 451 | offset += 1 452 | properties['user_id'] = pstr_t(buf[offset:offset + slen]) 453 | offset += slen 454 | if flags & 0x0008: 455 | slen, = unpack_from('>B', buf, offset) 456 | offset += 1 457 | properties['app_id'] = pstr_t(buf[offset:offset + slen]) 458 | offset += slen 459 | if flags & 0x0004: 460 | slen, = unpack_from('>B', buf, offset) 461 | offset += 1 462 | properties['cluster_id'] = pstr_t(buf[offset:offset + slen]) 463 | offset += slen 464 | return properties, offset 465 | 466 | 467 | PROPERTY_CLASSES = { 468 | Basic.CLASS_ID: decode_properties_basic, 469 | } 470 | 471 | 472 | class GenericContent: 473 | """Abstract base class for AMQP content. 474 | 475 | Subclasses should override the PROPERTIES attribute. 476 | """ 477 | 478 | CLASS_ID = None 479 | PROPERTIES = [('dummy', 's')] 480 | 481 | def __init__(self, frame_method=None, frame_args=None, **props): 482 | self.frame_method = frame_method 483 | self.frame_args = frame_args 484 | 485 | self.properties = props 486 | self._pending_chunks = [] 487 | self.body_received = 0 488 | self.body_size = 0 489 | self.ready = False 490 | 491 | __slots__ = ( 492 | "frame_method", 493 | "frame_args", 494 | "properties", 495 | "_pending_chunks", 496 | "body_received", 497 | "body_size", 498 | "ready", 499 | # adding '__dict__' to get dynamic assignment 500 | "__dict__", 501 | "__weakref__", 502 | ) 503 | 504 | def __getattr__(self, name): 505 | # Look for additional properties in the 'properties' 506 | # dictionary, and if present - the 'delivery_info' dictionary. 507 | if name == '__setstate__': 508 | # Allows pickling/unpickling to work 509 | raise AttributeError('__setstate__') 510 | 511 | if name in self.properties: 512 | return self.properties[name] 513 | raise AttributeError(name) 514 | 515 | def _load_properties(self, class_id, buf, offset): 516 | """Load AMQP properties. 517 | 518 | Given the raw bytes containing the property-flags and property-list 519 | from a content-frame-header, parse and insert into a dictionary 520 | stored in this object as an attribute named 'properties'. 521 | """ 522 | # Read 16-bit shorts until we get one with a low bit set to zero 523 | props, offset = PROPERTY_CLASSES[class_id](buf, offset) 524 | self.properties = props 525 | return offset 526 | 527 | def _serialize_properties(self): 528 | """Serialize AMQP properties. 529 | 530 | Serialize the 'properties' attribute (a dictionary) into 531 | the raw bytes making up a set of property flags and a 532 | property list, suitable for putting into a content frame header. 533 | """ 534 | shift = 15 535 | flag_bits = 0 536 | flags = [] 537 | sformat, svalues = [], [] 538 | props = self.properties 539 | for key, proptype in self.PROPERTIES: 540 | val = props.get(key, None) 541 | if val is not None: 542 | if shift == 0: 543 | flags.append(flag_bits) 544 | flag_bits = 0 545 | shift = 15 546 | 547 | flag_bits |= (1 << shift) 548 | if proptype != 'bit': 549 | sformat.append(str_to_bytes(proptype)) 550 | svalues.append(val) 551 | 552 | shift -= 1 553 | flags.append(flag_bits) 554 | result = BytesIO() 555 | write = result.write 556 | for flag_bits in flags: 557 | write(pack('>H', flag_bits)) 558 | write(dumps(b''.join(sformat), svalues)) 559 | 560 | return result.getvalue() 561 | 562 | def inbound_header(self, buf, offset=0): 563 | class_id, self.body_size = unpack_from('>HxxQ', buf, offset) 564 | offset += 12 565 | self._load_properties(class_id, buf, offset) 566 | if not self.body_size: 567 | self.ready = True 568 | return offset 569 | 570 | def inbound_body(self, buf): 571 | chunks = self._pending_chunks 572 | self.body_received += len(buf) 573 | if self.body_received >= self.body_size: 574 | if chunks: 575 | chunks.append(buf) 576 | self.body = bytes().join(chunks) 577 | chunks[:] = [] 578 | else: 579 | self.body = buf 580 | self.ready = True 581 | else: 582 | chunks.append(buf) 583 | -------------------------------------------------------------------------------- /amqp/spec.py: -------------------------------------------------------------------------------- 1 | """AMQP Spec.""" 2 | 3 | from collections import namedtuple 4 | 5 | method_t = namedtuple('method_t', ('method_sig', 'args', 'content')) 6 | 7 | 8 | def method(method_sig, args=None, content=False): 9 | """Create amqp method specification tuple.""" 10 | return method_t(method_sig, args, content) 11 | 12 | 13 | class Connection: 14 | """AMQ Connection class.""" 15 | 16 | CLASS_ID = 10 17 | 18 | Start = (10, 10) 19 | StartOk = (10, 11) 20 | Secure = (10, 20) 21 | SecureOk = (10, 21) 22 | Tune = (10, 30) 23 | TuneOk = (10, 31) 24 | Open = (10, 40) 25 | OpenOk = (10, 41) 26 | Close = (10, 50) 27 | CloseOk = (10, 51) 28 | Blocked = (10, 60) 29 | Unblocked = (10, 61) 30 | 31 | 32 | class Channel: 33 | """AMQ Channel class.""" 34 | 35 | CLASS_ID = 20 36 | 37 | Open = (20, 10) 38 | OpenOk = (20, 11) 39 | Flow = (20, 20) 40 | FlowOk = (20, 21) 41 | Close = (20, 40) 42 | CloseOk = (20, 41) 43 | 44 | 45 | class Exchange: 46 | """AMQ Exchange class.""" 47 | 48 | CLASS_ID = 40 49 | 50 | Declare = (40, 10) 51 | DeclareOk = (40, 11) 52 | Delete = (40, 20) 53 | DeleteOk = (40, 21) 54 | Bind = (40, 30) 55 | BindOk = (40, 31) 56 | Unbind = (40, 40) 57 | UnbindOk = (40, 51) 58 | 59 | 60 | class Queue: 61 | """AMQ Queue class.""" 62 | 63 | CLASS_ID = 50 64 | 65 | Declare = (50, 10) 66 | DeclareOk = (50, 11) 67 | Bind = (50, 20) 68 | BindOk = (50, 21) 69 | Purge = (50, 30) 70 | PurgeOk = (50, 31) 71 | Delete = (50, 40) 72 | DeleteOk = (50, 41) 73 | Unbind = (50, 50) 74 | UnbindOk = (50, 51) 75 | 76 | 77 | class Basic: 78 | """AMQ Basic class.""" 79 | 80 | CLASS_ID = 60 81 | 82 | Qos = (60, 10) 83 | QosOk = (60, 11) 84 | Consume = (60, 20) 85 | ConsumeOk = (60, 21) 86 | Cancel = (60, 30) 87 | CancelOk = (60, 31) 88 | Publish = (60, 40) 89 | Return = (60, 50) 90 | Deliver = (60, 60) 91 | Get = (60, 70) 92 | GetOk = (60, 71) 93 | GetEmpty = (60, 72) 94 | Ack = (60, 80) 95 | Nack = (60, 120) 96 | Reject = (60, 90) 97 | RecoverAsync = (60, 100) 98 | Recover = (60, 110) 99 | RecoverOk = (60, 111) 100 | 101 | 102 | class Confirm: 103 | """AMQ Confirm class.""" 104 | 105 | CLASS_ID = 85 106 | 107 | Select = (85, 10) 108 | SelectOk = (85, 11) 109 | 110 | 111 | class Tx: 112 | """AMQ Tx class.""" 113 | 114 | CLASS_ID = 90 115 | 116 | Select = (90, 10) 117 | SelectOk = (90, 11) 118 | Commit = (90, 20) 119 | CommitOk = (90, 21) 120 | Rollback = (90, 30) 121 | RollbackOk = (90, 31) 122 | -------------------------------------------------------------------------------- /amqp/utils.pxd: -------------------------------------------------------------------------------- 1 | # cython: language_level=3 2 | 3 | cpdef str_to_bytes(s) 4 | cpdef bytes_to_str(s) 5 | -------------------------------------------------------------------------------- /amqp/utils.py: -------------------------------------------------------------------------------- 1 | """Compatibility utilities.""" 2 | import logging 3 | from logging import NullHandler 4 | 5 | # enables celery 3.1.23 to start again 6 | from vine import promise # noqa 7 | from vine.utils import wraps 8 | 9 | try: 10 | import fcntl 11 | except ImportError: # pragma: no cover 12 | fcntl = None # noqa 13 | 14 | 15 | def set_cloexec(fd, cloexec): 16 | """Set flag to close fd after exec.""" 17 | if fcntl is None: 18 | return 19 | try: 20 | FD_CLOEXEC = fcntl.FD_CLOEXEC 21 | except AttributeError: 22 | raise NotImplementedError( 23 | 'close-on-exec flag not supported on this platform', 24 | ) 25 | flags = fcntl.fcntl(fd, fcntl.F_GETFD) 26 | if cloexec: 27 | flags |= FD_CLOEXEC 28 | else: 29 | flags &= ~FD_CLOEXEC 30 | return fcntl.fcntl(fd, fcntl.F_SETFD, flags) 31 | 32 | 33 | def coro(gen): 34 | """Decorator to mark generator as a co-routine.""" 35 | @wraps(gen) 36 | def _boot(*args, **kwargs): 37 | co = gen(*args, **kwargs) 38 | next(co) 39 | return co 40 | 41 | return _boot 42 | 43 | 44 | def str_to_bytes(s): 45 | """Convert str to bytes.""" 46 | if isinstance(s, str): 47 | return s.encode('utf-8', 'surrogatepass') 48 | return s 49 | 50 | 51 | def bytes_to_str(s): 52 | """Convert bytes to str.""" 53 | if isinstance(s, bytes): 54 | return s.decode('utf-8', 'surrogatepass') 55 | return s 56 | 57 | 58 | def get_logger(logger): 59 | """Get logger by name.""" 60 | if isinstance(logger, str): 61 | logger = logging.getLogger(logger) 62 | if not logger.handlers: 63 | logger.addHandler(NullHandler()) 64 | return logger 65 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | def pytest_addoption(parser): 3 | parser.addoption("-E", action="store", metavar="NAME", 4 | help="only run tests matching the environment NAME.") 5 | 6 | def pytest_configure(config): 7 | # register an additional marker 8 | config.addinivalue_line("markers", 9 | "env(name): mark test to run only on named environment") 10 | 11 | def pytest_runtest_setup(item): 12 | envnames = [mark.args[0] for mark in item.iter_markers(name='env')] 13 | if envnames: 14 | if item.config.getoption("-E") not in envnames: 15 | pytest.skip("test requires env in %r" % envnames) 16 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " epub3 to make an epub3" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | @echo " apicheck to verify that all modules are present in autodoc" 51 | 52 | .PHONY: clean 53 | clean: 54 | rm -rf $(BUILDDIR)/* 55 | 56 | .PHONY: html 57 | html: 58 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 61 | 62 | .PHONY: dirhtml 63 | dirhtml: 64 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 65 | @echo 66 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 67 | 68 | .PHONY: singlehtml 69 | singlehtml: 70 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 71 | @echo 72 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 73 | 74 | .PHONY: pickle 75 | pickle: 76 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 77 | @echo 78 | @echo "Build finished; now you can process the pickle files." 79 | 80 | .PHONY: json 81 | json: 82 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 83 | @echo 84 | @echo "Build finished; now you can process the JSON files." 85 | 86 | .PHONY: htmlhelp 87 | htmlhelp: 88 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 89 | @echo 90 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 91 | ".hhp project file in $(BUILDDIR)/htmlhelp." 92 | 93 | .PHONY: qthelp 94 | qthelp: 95 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 96 | @echo 97 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 98 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 99 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PROJ.qhcp" 100 | @echo "To view the help file:" 101 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PROJ.qhc" 102 | 103 | .PHONY: applehelp 104 | applehelp: 105 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 106 | @echo 107 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 108 | @echo "N.B. You won't be able to view it unless you put it in" \ 109 | "~/Library/Documentation/Help or install it in your application" \ 110 | "bundle." 111 | 112 | .PHONY: devhelp 113 | devhelp: 114 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 115 | @echo 116 | @echo "Build finished." 117 | @echo "To view the help file:" 118 | @echo "# mkdir -p $$HOME/.local/share/devhelp/PROJ" 119 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PROJ" 120 | @echo "# devhelp" 121 | 122 | .PHONY: epub 123 | epub: 124 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 125 | @echo 126 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 127 | 128 | .PHONY: epub3 129 | epub3: 130 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 131 | @echo 132 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 133 | 134 | .PHONY: latex 135 | latex: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo 138 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 139 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 140 | "(use \`make latexpdf' here to do that automatically)." 141 | 142 | .PHONY: latexpdf 143 | latexpdf: 144 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 145 | @echo "Running LaTeX files through pdflatex..." 146 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 147 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 148 | 149 | .PHONY: latexpdfja 150 | latexpdfja: 151 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 152 | @echo "Running LaTeX files through platex and dvipdfmx..." 153 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 154 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 155 | 156 | .PHONY: text 157 | text: 158 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 159 | @echo 160 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 161 | 162 | .PHONY: man 163 | man: 164 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 165 | @echo 166 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 167 | 168 | .PHONY: texinfo 169 | texinfo: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo 172 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 173 | @echo "Run \`make' in that directory to run these through makeinfo" \ 174 | "(use \`make info' here to do that automatically)." 175 | 176 | .PHONY: info 177 | info: 178 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 179 | @echo "Running Texinfo files through makeinfo..." 180 | make -C $(BUILDDIR)/texinfo info 181 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 182 | 183 | .PHONY: gettext 184 | gettext: 185 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 186 | @echo 187 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 188 | 189 | .PHONY: changes 190 | changes: 191 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 192 | @echo 193 | @echo "The overview file is in $(BUILDDIR)/changes." 194 | 195 | .PHONY: linkcheck 196 | linkcheck: 197 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 198 | @echo 199 | @echo "Link check complete; look for any errors in the above output " \ 200 | "or in $(BUILDDIR)/linkcheck/output.txt." 201 | 202 | .PHONY: doctest 203 | doctest: 204 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 205 | @echo "Testing of doctests in the sources finished, look at the " \ 206 | "results in $(BUILDDIR)/doctest/output.txt." 207 | 208 | .PHONY: coverage 209 | coverage: 210 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 211 | @echo "Testing of coverage in the sources finished, look at the " \ 212 | "results in $(BUILDDIR)/coverage/python.txt." 213 | 214 | .PHONY: apicheck 215 | apicheck: 216 | $(SPHINXBUILD) -b apicheck $(ALLSPHINXOPTS) $(BUILDDIR)/apicheck 217 | 218 | .PHONY: xml 219 | xml: 220 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 221 | @echo 222 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 223 | 224 | .PHONY: pseudoxml 225 | pseudoxml: 226 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 227 | @echo 228 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 229 | -------------------------------------------------------------------------------- /docs/_static/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celery/py-amqp/1c1cb1726d74a05912ac3964a1422fd97bb0f62c/docs/_static/.keep -------------------------------------------------------------------------------- /docs/_templates/sidebardonations.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../Changelog 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | from sphinx_celery import conf 2 | 3 | globals().update(conf.build_config( 4 | 'amqp', __file__, 5 | project='py-amqp', 6 | description='Python Promises', 7 | version_dev='5.3', 8 | version_stable='5.3', 9 | canonical_url='https://amqp.readthedocs.io', 10 | webdomain='celeryproject.org', 11 | github_project='celery/py-amqp', 12 | author='Ask Solem & contributors', 13 | author_name='Ask Solem', 14 | copyright='2016', 15 | publisher='Celery Project', 16 | html_logo='images/celery_128.png', 17 | html_favicon='images/favicon.ico', 18 | html_prepend_sidebars=['sidebardonations.html'], 19 | extra_extensions=[], 20 | include_intersphinx={'python', 'sphinx'}, 21 | apicheck_package='amqp', 22 | apicheck_ignore_modules=['amqp'], 23 | )) 24 | -------------------------------------------------------------------------------- /docs/images/celery_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celery/py-amqp/1c1cb1726d74a05912ac3964a1422fd97bb0f62c/docs/images/celery_128.png -------------------------------------------------------------------------------- /docs/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celery/py-amqp/1c1cb1726d74a05912ac3964a1422fd97bb0f62c/docs/images/favicon.ico -------------------------------------------------------------------------------- /docs/includes/introduction.txt: -------------------------------------------------------------------------------- 1 | :Version: 5.3.1 2 | :Web: https://amqp.readthedocs.io/ 3 | :Download: https://pypi.org/project/amqp/ 4 | :Source: http://github.com/celery/py-amqp/ 5 | :Keywords: amqp, rabbitmq 6 | 7 | About 8 | ===== 9 | 10 | This is a fork of amqplib_ which was originally written by Barry Pederson. 11 | It is maintained by the Celery_ project, and used by `kombu`_ as a pure python 12 | alternative when `librabbitmq`_ is not available. 13 | 14 | This library should be API compatible with `librabbitmq`_. 15 | 16 | .. _amqplib: https://pypi.org/project/amqplib/ 17 | .. _Celery: http://celeryproject.org/ 18 | .. _kombu: https://kombu.readthedocs.io/ 19 | .. _librabbitmq: https://pypi.org/project/librabbitmq/ 20 | 21 | Differences from `amqplib`_ 22 | =========================== 23 | 24 | - Supports draining events from multiple channels (``Connection.drain_events``) 25 | - Support for timeouts 26 | - Channels are restored after channel error, instead of having to close the 27 | connection. 28 | - Support for heartbeats 29 | 30 | - ``Connection.heartbeat_tick(rate=2)`` must called at regular intervals 31 | (half of the heartbeat value if rate is 2). 32 | - Or some other scheme by using ``Connection.send_heartbeat``. 33 | - Supports RabbitMQ extensions: 34 | - Consumer Cancel Notifications 35 | - by default a cancel results in ``ChannelError`` being raised 36 | - but not if a ``on_cancel`` callback is passed to ``basic_consume``. 37 | - Publisher confirms 38 | - ``Channel.confirm_select()`` enables publisher confirms. 39 | - ``Channel.events['basic_ack'].append(my_callback)`` adds a callback 40 | to be called when a message is confirmed. This callback is then 41 | called with the signature ``(delivery_tag, multiple)``. 42 | - Exchange-to-exchange bindings: ``exchange_bind`` / ``exchange_unbind``. 43 | - ``Channel.confirm_select()`` enables publisher confirms. 44 | - ``Channel.events['basic_ack'].append(my_callback)`` adds a callback 45 | to be called when a message is confirmed. This callback is then 46 | called with the signature ``(delivery_tag, multiple)``. 47 | - Support for ``basic_return`` 48 | - Uses AMQP 0-9-1 instead of 0-8. 49 | - ``Channel.access_request`` and ``ticket`` arguments to methods 50 | **removed**. 51 | - Supports the ``arguments`` argument to ``basic_consume``. 52 | - ``internal`` argument to ``exchange_declare`` removed. 53 | - ``auto_delete`` argument to ``exchange_declare`` deprecated 54 | - ``insist`` argument to ``Connection`` removed. 55 | - ``Channel.alerts`` has been removed. 56 | - Support for ``Channel.basic_recover_async``. 57 | - ``Channel.basic_recover`` deprecated. 58 | - Exceptions renamed to have idiomatic names: 59 | - ``AMQPException`` -> ``AMQPError`` 60 | - ``AMQPConnectionException`` -> ConnectionError`` 61 | - ``AMQPChannelException`` -> ChannelError`` 62 | - ``Connection.known_hosts`` removed. 63 | - ``Connection`` no longer supports redirects. 64 | - ``exchange`` argument to ``queue_bind`` can now be empty 65 | to use the "default exchange". 66 | - Adds ``Connection.is_alive`` that tries to detect 67 | whether the connection can still be used. 68 | - Adds ``Connection.connection_errors`` and ``.channel_errors``, 69 | a list of recoverable errors. 70 | - Exposes the underlying socket as ``Connection.sock``. 71 | - Adds ``Channel.no_ack_consumers`` to keep track of consumer tags 72 | that set the no_ack flag. 73 | - Slightly better at error recovery 74 | 75 | Further 76 | ======= 77 | 78 | - Differences between AMQP 0.8 and 0.9.1 79 | 80 | http://www.rabbitmq.com/amqp-0-8-to-0-9-1.html 81 | 82 | - AMQP 0.9.1 Quick Reference 83 | 84 | http://www.rabbitmq.com/amqp-0-9-1-quickref.html 85 | 86 | - RabbitMQ Extensions 87 | 88 | http://www.rabbitmq.com/extensions.html 89 | 90 | - For more information about AMQP, visit 91 | 92 | http://www.amqp.org 93 | 94 | - For other Python client libraries see: 95 | 96 | http://www.rabbitmq.com/devtools.html#python-dev 97 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ============================================= 2 | amqp - Python AMQP low-level client library 3 | ============================================= 4 | 5 | .. include:: includes/introduction.txt 6 | 7 | Contents 8 | ======== 9 | 10 | .. toctree:: 11 | :maxdepth: 2 12 | 13 | reference/index 14 | changelog 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | 23 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. epub3 to make an epub3 31 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 32 | echo. text to make text files 33 | echo. man to make manual pages 34 | echo. texinfo to make Texinfo files 35 | echo. gettext to make PO message catalogs 36 | echo. changes to make an overview over all changed/added/deprecated items 37 | echo. xml to make Docutils-native XML files 38 | echo. pseudoxml to make pseudoxml-XML files for display purposes 39 | echo. linkcheck to check all external links for integrity 40 | echo. doctest to run all doctests embedded in the documentation if enabled 41 | echo. coverage to run coverage check of the documentation if enabled 42 | goto end 43 | ) 44 | 45 | if "%1" == "clean" ( 46 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 47 | del /q /s %BUILDDIR%\* 48 | goto end 49 | ) 50 | 51 | 52 | REM Check if sphinx-build is available and fallback to Python version if any 53 | %SPHINXBUILD% 1>NUL 2>NUL 54 | if errorlevel 9009 goto sphinx_python 55 | goto sphinx_ok 56 | 57 | :sphinx_python 58 | 59 | set SPHINXBUILD=python -m sphinx.__init__ 60 | %SPHINXBUILD% 2> nul 61 | if errorlevel 9009 ( 62 | echo. 63 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 64 | echo.installed, then set the SPHINXBUILD environment variable to point 65 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 66 | echo.may add the Sphinx directory to PATH. 67 | echo. 68 | echo.If you don't have Sphinx installed, grab it from 69 | echo.http://sphinx-doc.org/ 70 | exit /b 1 71 | ) 72 | 73 | :sphinx_ok 74 | 75 | 76 | if "%1" == "html" ( 77 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 78 | if errorlevel 1 exit /b 1 79 | echo. 80 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 81 | goto end 82 | ) 83 | 84 | if "%1" == "dirhtml" ( 85 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 86 | if errorlevel 1 exit /b 1 87 | echo. 88 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 89 | goto end 90 | ) 91 | 92 | if "%1" == "singlehtml" ( 93 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 94 | if errorlevel 1 exit /b 1 95 | echo. 96 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 97 | goto end 98 | ) 99 | 100 | if "%1" == "pickle" ( 101 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 102 | if errorlevel 1 exit /b 1 103 | echo. 104 | echo.Build finished; now you can process the pickle files. 105 | goto end 106 | ) 107 | 108 | if "%1" == "json" ( 109 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished; now you can process the JSON files. 113 | goto end 114 | ) 115 | 116 | if "%1" == "htmlhelp" ( 117 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished; now you can run HTML Help Workshop with the ^ 121 | .hhp project file in %BUILDDIR%/htmlhelp. 122 | goto end 123 | ) 124 | 125 | if "%1" == "qthelp" ( 126 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 127 | if errorlevel 1 exit /b 1 128 | echo. 129 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 130 | .qhcp project file in %BUILDDIR%/qthelp, like this: 131 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PROJ.qhcp 132 | echo.To view the help file: 133 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PROJ.ghc 134 | goto end 135 | ) 136 | 137 | if "%1" == "devhelp" ( 138 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 139 | if errorlevel 1 exit /b 1 140 | echo. 141 | echo.Build finished. 142 | goto end 143 | ) 144 | 145 | if "%1" == "epub" ( 146 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 147 | if errorlevel 1 exit /b 1 148 | echo. 149 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 150 | goto end 151 | ) 152 | 153 | if "%1" == "epub3" ( 154 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 155 | if errorlevel 1 exit /b 1 156 | echo. 157 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 158 | goto end 159 | ) 160 | 161 | if "%1" == "latex" ( 162 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 163 | if errorlevel 1 exit /b 1 164 | echo. 165 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 166 | goto end 167 | ) 168 | 169 | if "%1" == "latexpdf" ( 170 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 171 | cd %BUILDDIR%/latex 172 | make all-pdf 173 | cd %~dp0 174 | echo. 175 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 176 | goto end 177 | ) 178 | 179 | if "%1" == "latexpdfja" ( 180 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 181 | cd %BUILDDIR%/latex 182 | make all-pdf-ja 183 | cd %~dp0 184 | echo. 185 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 186 | goto end 187 | ) 188 | 189 | if "%1" == "text" ( 190 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 191 | if errorlevel 1 exit /b 1 192 | echo. 193 | echo.Build finished. The text files are in %BUILDDIR%/text. 194 | goto end 195 | ) 196 | 197 | if "%1" == "man" ( 198 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 199 | if errorlevel 1 exit /b 1 200 | echo. 201 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 202 | goto end 203 | ) 204 | 205 | if "%1" == "texinfo" ( 206 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 207 | if errorlevel 1 exit /b 1 208 | echo. 209 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 210 | goto end 211 | ) 212 | 213 | if "%1" == "gettext" ( 214 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 215 | if errorlevel 1 exit /b 1 216 | echo. 217 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 218 | goto end 219 | ) 220 | 221 | if "%1" == "changes" ( 222 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 223 | if errorlevel 1 exit /b 1 224 | echo. 225 | echo.The overview file is in %BUILDDIR%/changes. 226 | goto end 227 | ) 228 | 229 | if "%1" == "linkcheck" ( 230 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Link check complete; look for any errors in the above output ^ 234 | or in %BUILDDIR%/linkcheck/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "doctest" ( 239 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of doctests in the sources finished, look at the ^ 243 | results in %BUILDDIR%/doctest/output.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "coverage" ( 248 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Testing of coverage in the sources finished, look at the ^ 252 | results in %BUILDDIR%/coverage/python.txt. 253 | goto end 254 | ) 255 | 256 | if "%1" == "xml" ( 257 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 258 | if errorlevel 1 exit /b 1 259 | echo. 260 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 261 | goto end 262 | ) 263 | 264 | if "%1" == "pseudoxml" ( 265 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 266 | if errorlevel 1 exit /b 1 267 | echo. 268 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 269 | goto end 270 | ) 271 | 272 | :end 273 | -------------------------------------------------------------------------------- /docs/reference/amqp.abstract_channel.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | ``amqp.abstract_channel`` 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: amqp.abstract_channel 8 | 9 | .. automodule:: amqp.abstract_channel 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/reference/amqp.basic_message.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | ``amqp.basic_message`` 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: amqp.basic_message 8 | 9 | .. automodule:: amqp.basic_message 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/reference/amqp.channel.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | ``amqp.channel`` 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: amqp.channel 8 | 9 | .. automodule:: amqp.channel 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/reference/amqp.connection.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | ``amqp.connection`` 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: amqp.connection 8 | 9 | .. automodule:: amqp.connection 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/reference/amqp.exceptions.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | ``amqp.exceptions`` 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: amqp.exceptions 8 | 9 | .. automodule:: amqp.exceptions 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/reference/amqp.method_framing.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | ``amqp.method_framing`` 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: amqp.method_framing 8 | 9 | .. automodule:: amqp.method_framing 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/reference/amqp.platform.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | ``amqp.platform`` 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: amqp.platform 8 | 9 | .. automodule:: amqp.platform 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/reference/amqp.protocol.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | ``amqp.protocol`` 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: amqp.protocol 8 | 9 | .. automodule:: amqp.protocol 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/reference/amqp.sasl.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | amqp.spec 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: amqp.sasl 8 | 9 | .. automodule:: amqp.sasl 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/reference/amqp.serialization.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | ``amqp.serialization`` 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: amqp.serialization 8 | 9 | .. automodule:: amqp.serialization 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/reference/amqp.spec.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | ``amqp.spec`` 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: amqp.spec 8 | 9 | .. automodule:: amqp.spec 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/reference/amqp.transport.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | ``amqp.transport`` 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: amqp.transport 8 | 9 | .. automodule:: amqp.transport 10 | 11 | .. autoclass:: _AbstractTransport 12 | :members: 13 | :undoc-members: 14 | 15 | .. autoclass:: SSLTransport 16 | :members: 17 | :private-members: _wrap_context, _wrap_socket_sni 18 | :undoc-members: 19 | 20 | .. autoclass:: TCPTransport 21 | :members: 22 | :undoc-members: 23 | 24 | .. autoclass:: Transport 25 | :members: 26 | :undoc-members: 27 | -------------------------------------------------------------------------------- /docs/reference/amqp.utils.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | ``amqp.utils`` 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: amqp.utils 8 | 9 | .. automodule:: amqp.utils 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/reference/index.rst: -------------------------------------------------------------------------------- 1 | .. _apiref: 2 | 3 | =============== 4 | API Reference 5 | =============== 6 | 7 | :Release: |version| 8 | :Date: |today| 9 | 10 | .. toctree:: 11 | :maxdepth: 1 12 | 13 | amqp.connection 14 | amqp.channel 15 | amqp.basic_message 16 | amqp.exceptions 17 | amqp.abstract_channel 18 | amqp.transport 19 | amqp.method_framing 20 | amqp.platform 21 | amqp.protocol 22 | amqp.sasl 23 | amqp.serialization 24 | amqp.spec 25 | amqp.utils 26 | -------------------------------------------------------------------------------- /docs/templates/readme.txt: -------------------------------------------------------------------------------- 1 | ===================================================================== 2 | Python AMQP 0.9.1 client library 3 | ===================================================================== 4 | 5 | |build-status| |coverage| |license| |wheel| |pyversion| |pyimp| 6 | 7 | .. include:: ../includes/introduction.txt 8 | 9 | .. |build-status| image:: https://github.com/celery/py-amqp/actions/workflows/ci.yaml/badge.svg 10 | :alt: Build status 11 | :target: https://github.com/celery/py-amqp/actions/workflows/ci.yaml 12 | 13 | .. |coverage| image:: https://codecov.io/github/celery/py-amqp/coverage.svg?branch=main 14 | :target: https://codecov.io/github/celery/py-amqp?branch=main 15 | 16 | .. |license| image:: https://img.shields.io/pypi/l/amqp.svg 17 | :alt: BSD License 18 | :target: https://opensource.org/licenses/BSD-3-Clause 19 | 20 | .. |wheel| image:: https://img.shields.io/pypi/wheel/amqp.svg 21 | :alt: Python AMQP can be installed via wheel 22 | :target: https://pypi.org/project/amqp/ 23 | 24 | .. |pyversion| image:: https://img.shields.io/pypi/pyversions/amqp.svg 25 | :alt: Supported Python versions. 26 | :target: https://pypi.org/project/amqp/ 27 | 28 | .. |pyimp| image:: https://img.shields.io/pypi/implementation/amqp.svg 29 | :alt: Support Python implementations. 30 | :target: https://pypi.org/project/amqp/ 31 | -------------------------------------------------------------------------------- /extra/update_comments_from_spec.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import re 4 | 5 | default_source_file = os.path.join( 6 | os.path.dirname(__file__), 7 | '../amqp/channel.py', 8 | ) 9 | 10 | RE_COMMENTS = re.compile( 11 | r'(?Pdef\s+(?P[a-zA-Z0-9_]+)\(.*?\)' 12 | ':\n+\\s+""")(?P.*?)(?=""")', 13 | re.MULTILINE | re.DOTALL 14 | ) 15 | 16 | USAGE = """\ 17 | Usage: %s []\ 18 | """ 19 | 20 | 21 | def update_comments(comments_file, impl_file, result_file): 22 | text_file = open(impl_file) 23 | source = text_file.read() 24 | 25 | comments = get_comments(comments_file) 26 | for def_name, comment in comments.items(): 27 | source = replace_comment_per_def( 28 | source, result_file, def_name, comment 29 | ) 30 | 31 | new_file = open(result_file, 'w+') 32 | new_file.write(source) 33 | 34 | 35 | def get_comments(filename): 36 | text_file = open(filename) 37 | whole_source = text_file.read() 38 | comments = {} 39 | 40 | all_matches = RE_COMMENTS.finditer(whole_source) 41 | for match in all_matches: 42 | comments[match.group('mname')] = match.group('comment') 43 | # print('method: %s \ncomment: %s' % ( 44 | # match.group('mname'), match.group('comment'))) 45 | 46 | return comments 47 | 48 | 49 | def replace_comment_per_def(source, result_file, def_name, new_comment): 50 | regex = (r'(?Pdef\s+' + 51 | def_name + 52 | '\\(.*?\\):\n+\\s+""".*?\n).*?(?=""")') 53 | # print('method and comment:' + def_name + new_comment) 54 | result = re.sub(regex, r'\g' + new_comment, source, 0, 55 | re.MULTILINE | re.DOTALL) 56 | return result 57 | 58 | 59 | def main(argv=None): 60 | if argv is None: 61 | argv = sys.argv 62 | 63 | if len(argv) < 3: 64 | print(USAGE % argv[0]) 65 | return 1 66 | 67 | impl_file = default_source_file 68 | if len(argv) >= 4: 69 | impl_file = argv[3] 70 | 71 | update_comments(argv[1], impl_file, argv[2]) 72 | 73 | if __name__ == '__main__': 74 | sys.exit(main()) 75 | -------------------------------------------------------------------------------- /rabbitmq_logs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | containers=$(sudo docker ps -q | tail -n +1) 4 | 5 | for item in ${containers//\\n/} 6 | do 7 | env=$(sudo docker inspect -f '{{range $index, $value := .Config.Env}}{{$value}} {{end}}' $item); 8 | if [[ $env == *"PYAMQP_INTEGRATION_INSTANCE=1"* ]]; then 9 | grep -m1 'Server startup complete' <(sudo docker logs -f $item) 10 | sudo docker logs $item 11 | fi 12 | done; 13 | -------------------------------------------------------------------------------- /requirements/default.txt: -------------------------------------------------------------------------------- 1 | vine>=5.0.0,<6.0.0 2 | -------------------------------------------------------------------------------- /requirements/docs.txt: -------------------------------------------------------------------------------- 1 | sphinx_celery>=2.1.3 2 | -------------------------------------------------------------------------------- /requirements/pkgutils.txt: -------------------------------------------------------------------------------- 1 | setuptools>=20.6.7 2 | wheel>=0.29.0 3 | flake8>=3.8.3 4 | tox>=2.3.1 5 | sphinx2rst>=1.0 6 | bumpversion 7 | pydocstyle==1.1.1 8 | -------------------------------------------------------------------------------- /requirements/test-ci.txt: -------------------------------------------------------------------------------- 1 | pytest-cov 2 | codecov 3 | pytest-xdist 4 | -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | pytest>=6.2.5,<=8.0.0 2 | pytest-sugar>=0.9.1 3 | pytest-rerunfailures>=6.0 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | testpaths = t/unit/ t/integration/ 3 | python_classes = test_* 4 | 5 | [bdist_rpm] 6 | requires = vine 7 | 8 | [flake8] 9 | # classes can be lowercase, arguments and variables can be uppercase 10 | # whenever it makes the code more readable. 11 | ignore = N806, N802, N801, N803 12 | 13 | [pep257] 14 | ignore = D102,D104,D203,D105,D213 15 | 16 | [bdist_wheel] 17 | universal = 0 18 | 19 | [metadata] 20 | license_file = LICENSE 21 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import re 4 | import sys 5 | from os import environ 6 | from pathlib import Path 7 | 8 | import setuptools 9 | import setuptools.command.test 10 | 11 | NAME = 'amqp' 12 | 13 | # -*- Classifiers -*- 14 | 15 | classes = """ 16 | Development Status :: 5 - Production/Stable 17 | Programming Language :: Python 18 | Programming Language :: Python :: 3 :: Only 19 | Programming Language :: Python :: 3 20 | Programming Language :: Python :: 3.7 21 | Programming Language :: Python :: 3.8 22 | Programming Language :: Python :: 3.9 23 | Programming Language :: Python :: 3.10 24 | Programming Language :: Python :: Implementation :: CPython 25 | Programming Language :: Python :: Implementation :: PyPy 26 | License :: OSI Approved :: BSD License 27 | Intended Audience :: Developers 28 | Operating System :: OS Independent 29 | """ 30 | classifiers = [s.strip() for s in classes.split('\n') if s] 31 | 32 | # -*- Distribution Meta -*- 33 | 34 | re_meta = re.compile(r'__(\w+?)__\s*=\s*(.*)') 35 | re_doc = re.compile(r'^"""(.+?)"""') 36 | 37 | 38 | def add_default(m): 39 | attr_name, attr_value = m.groups() 40 | return (attr_name, attr_value.strip("\"'")), 41 | 42 | 43 | def add_doc(m): 44 | return ('doc', m.groups()[0]), 45 | 46 | 47 | pats = {re_meta: add_default, 48 | re_doc: add_doc} 49 | here = Path(__file__).parent 50 | meta = {} 51 | for line in (here / 'amqp/__init__.py').read_text().splitlines(): 52 | if line.strip() == '# -eof meta-': 53 | break 54 | for pattern, handler in pats.items(): 55 | m = pattern.match(line.strip()) 56 | if m: 57 | meta.update(handler(m)) 58 | 59 | # -*- Installation Requires -*- 60 | 61 | py_version = sys.version_info 62 | is_jython = sys.platform.startswith('java') 63 | is_pypy = hasattr(sys, 'pypy_version_info') 64 | 65 | 66 | def strip_comments(l): 67 | return l.split('#', 1)[0].strip() 68 | 69 | 70 | def reqs(f): 71 | lines = (here / 'requirements' / f).read_text().splitlines() 72 | reqs = [strip_comments(l) for l in lines] 73 | return list(filter(None, reqs)) 74 | 75 | 76 | # -*- %%% -*- 77 | 78 | 79 | class pytest(setuptools.command.test.test): 80 | user_options = [('pytest-args=', 'a', 'Arguments to pass to py.test')] 81 | 82 | def initialize_options(self): 83 | setuptools.command.test.test.initialize_options(self) 84 | self.pytest_args = '' 85 | 86 | def run_tests(self): 87 | import pytest 88 | pytest_args = self.pytest_args.split(' ') 89 | sys.exit(pytest.main(pytest_args)) 90 | 91 | 92 | if environ.get("CELERY_ENABLE_SPEEDUPS"): 93 | setup_requires = ['Cython'] 94 | ext_modules = [ 95 | setuptools.Extension( 96 | 'amqp.serialization', 97 | ["amqp/serialization.py"], 98 | ), 99 | setuptools.Extension( 100 | 'amqp.basic_message', 101 | ["amqp/basic_message.py"], 102 | ), 103 | setuptools.Extension( 104 | 'amqp.method_framing', 105 | ["amqp/method_framing.py"], 106 | ), 107 | setuptools.Extension( 108 | 'amqp.abstract_channel', 109 | ["amqp/abstract_channel.py"], 110 | ), 111 | setuptools.Extension( 112 | 'amqp.utils', 113 | ["amqp/utils.py"], 114 | ), 115 | ] 116 | else: 117 | setup_requires = [] 118 | ext_modules = [] 119 | 120 | setuptools.setup( 121 | name=NAME, 122 | packages=setuptools.find_packages(exclude=['ez_setup', 't', 't.*']), 123 | version=meta['version'], 124 | description=meta['doc'], 125 | long_description=(here / 'README.rst').read_text(), 126 | long_description_content_type="text/x-rst", 127 | keywords='amqp rabbitmq cloudamqp messaging', 128 | author=meta['author'], 129 | author_email=meta['contact'], 130 | maintainer=meta['maintainer'], 131 | url=meta['homepage'], 132 | platforms=['any'], 133 | license='BSD', 134 | classifiers=classifiers, 135 | python_requires=">=3.6", 136 | install_requires=reqs('default.txt'), 137 | setup_requires=setup_requires, 138 | tests_require=reqs('test.txt'), 139 | cmdclass={'test': pytest}, 140 | zip_safe=False, 141 | ext_modules=ext_modules, 142 | ) 143 | -------------------------------------------------------------------------------- /t/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celery/py-amqp/1c1cb1726d74a05912ac3964a1422fd97bb0f62c/t/__init__.py -------------------------------------------------------------------------------- /t/certs/ca_certificate.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDRzCCAi+gAwIBAgIJAMa1mrcNQtapMA0GCSqGSIb3DQEBCwUAMDExIDAeBgNV 3 | BAMMF1RMU0dlblNlbGZTaWduZWR0Um9vdENBMQ0wCwYDVQQHDAQkJCQkMCAXDTIw 4 | MDEwMzEyMDE0MFoYDzIxMTkxMjEwMTIwMTQwWjAxMSAwHgYDVQQDDBdUTFNHZW5T 5 | ZWxmU2lnbmVkdFJvb3RDQTENMAsGA1UEBwwEJCQkJDCCASIwDQYJKoZIhvcNAQEB 6 | BQADggEPADCCAQoCggEBAKdmOg5vtuZ5vNZmceToiVBlcFg9Y/xKNyCPBij6Wm5p 7 | mXbnsjO1PhjGr97r2cMLq5QMvGt+FBEIjeeULtWVCBY7vMc4ATEZ1S2PmmKnOSXJ 8 | MLMDIutznopZkyqt3gqWgXZDxxHIlIzJl0HirQmfeLm6eTOYyFoyFZV3CE2IeW4Y 9 | n1zYhgZgIrU7Yo3I7wY9Js5yLk4p3etByN5tlLL2sdCOjRRXWGbOh/kb8uiyotEd 10 | cxNThk0RQDugoEzaGYBU3bzDhKkm4v/v/xp/JxGLDl/e3heRMUbcw9d/0ujflouy 11 | OQ66SNYGLWFQpmhtyHjalKzL5UbTcof4BQltoo/W7xECAwEAAaNgMF4wCwYDVR0P 12 | BAQDAgEGMB0GA1UdDgQWBBTKOnbaptqaUCAiwtnwLcRTcbuRejAfBgNVHSMEGDAW 13 | gBTKOnbaptqaUCAiwtnwLcRTcbuRejAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 14 | DQEBCwUAA4IBAQB1tJUR9zoQ98bOz1es91PxgIt8VYR8/r6uIRtYWTBi7fgDRaaR 15 | Glm6ZqOSXNlkacB6kjUzIyKJwGWnD9zU/06CH+ME1U497SVVhvtUEbdJb1COU+/5 16 | KavEHVINfc3tHD5Z5LJR3okEILAzBYkEcjYUECzBNYVi4l6PBSMSC2+RBKGqHkY7 17 | ApmD5batRghH5YtadiyF4h6bba/XSUqxzFcLKjKSyyds4ndvA1/yfl/7CrRtiZf0 18 | jw1pFl33/PTOhgi66MHa4uaKlL/hIjIlh4kJgJajqCN+TVU4Q6JNmSuIsq6rksSw 19 | Rd5baBZrik2NHALr/ZN2Wy0nXiQJ3p+F20+X 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /t/certs/client_certificate.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDTDCCAjSgAwIBAgIBAjANBgkqhkiG9w0BAQsFADAxMSAwHgYDVQQDDBdUTFNH 3 | ZW5TZWxmU2lnbmVkdFJvb3RDQTENMAsGA1UEBwwEJCQkJDAgFw0yMDAxMDMxMjAx 4 | NDFaGA8yMTE5MTIxMDEyMDE0MVowLzEcMBoGA1UEAwwTZXhhbXBsZS5leGFtcGxl 5 | LmNvbTEPMA0GA1UECgwGY2xpZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 6 | CgKCAQEA7QvDEy/Ej4Xm83oZTLbM+luQBg0Ks+U3h4JRVFvbTf9ZORm/7Pu04EhN 7 | LV2xL5mm69iW3DNa4NbUnkpMA5eavU08C6lfTMK1q83uG9TYZMSqmZAWKV7Rx5Zq 8 | H1NMmSbfziHqBtDz/LuEDq0JDxzPhYnTjnNZkMuUA+Fshe3cCTSMuD7+tn+7U3+y 9 | p2Eyq9dUoc+ac/Nlz3IxtFnobV1o5TCzObZQwgZ1Cyfcn88gWev4z0jTc813b14T 10 | XGo8cbMxPAestMVG1WTW2h+lS1kQJnLaxXZBQdcQ+tLjli8Db9pzCH0EFlbL0yc1 11 | zxCp7kmiPhTmdGjgz5JIr3fSIBcbhQIDAQABo28wbTAJBgNVHRMEAjAAMAsGA1Ud 12 | DwQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDAjA+BgNVHREENzA1ghNleGFtcGxl 13 | LmV4YW1wbGUuY29tghNleGFtcGxlLmV4YW1wbGUuY29tgglsb2NhbGhvc3QwDQYJ 14 | KoZIhvcNAQELBQADggEBAEroIKfvydyOsvfDist54VfzVruckq4rEeottylgOWSQ 15 | TZZDp1wqG+I7UxDt2BcpQocM/DNsmhowiHTdCRF0NYGY/OkMTGVSagcUG07GVVGL 16 | F7W+iuTcCsqCPO//lgBjUas99vAYAgOlBuv5qeObQnum1MASoeJOfB7Hi039x0lH 17 | GXyoMl2U7w9MU9xEstC3D2HZPJpjXjBRVMaqYyec3qWtLTEpxBOS9PPgMb3bqw9y 18 | HysuJqufWLnj5pHY1zIYGsXYI8RW3TO4dy+70yUFbgyWLmRKOC2n4XZQZJD76aM7 19 | mmqZTEsVIKbG5eDW0Rx0WZAVZFqJ+Y4I88F/v2jJv6M= 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /t/certs/client_certificate_broken.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | BrokenCCAjSgAwIBAgIBAjANBgkqhkiG9w0BAQsFADAxMSAwHgYDVQQDDBdUTFNH 3 | ZW5TZWxmU2lnbmVkdFJvb3RDQTENMAsGA1UEBwwEJCQkJDAgFw0yMDAxMDMxMjAx 4 | NDFaGA8yMTE5MTIxMDEyMDE0MVowLzEcMBoGA1UEAwwTZXhhbXBsZS5leGFtcGxl 5 | LmNvbTEPMA0GA1UECgwGY2xpZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 6 | CgKCAQEA7QvDEy/Ej4Xm83oZTLbM+luQBg0Ks+U3h4JRVFvbTf9ZORm/7Pu04EhN 7 | LV2xL5mm69iW3DNa4NbUnkpMA5eavU08C6lfTMK1q83uG9TYZMSqmZAWKV7Rx5Zq 8 | H1NMmSbfziHqBtDz/LuEDq0JDxzPhYnTjnNZkMuUA+Fshe3cCTSMuD7+tn+7U3+y 9 | p2Eyq9dUoc+ac/Nlz3IxtFnobV1o5TCzObZQwgZ1Cyfcn88gWev4z0jTc813b14T 10 | XGo8cbMxPAestMVG1WTW2h+lS1kQJnLaxXZBQdcQ+tLjli8Db9pzCH0EFlbL0yc1 11 | zxCp7kmiPhTmdGjgz5JIr3fSIBcbhQIDAQABo28wbTAJBgNVHRMEAjAAMAsGA1Ud 12 | DwQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDAjA+BgNVHREENzA1ghNleGFtcGxl 13 | LmV4YW1wbGUuY29tghNleGFtcGxlLmV4YW1wbGUuY29tgglsb2NhbGhvc3QwDQYJ 14 | KoZIhvcNAQELBQADggEBAEroIKfvydyOsvfDist54VfzVruckq4rEeottylgOWSQ 15 | TZZDp1wqG+I7UxDt2BcpQocM/DNsmhowiHTdCRF0NYGY/OkMTGVSagcUG07GVVGL 16 | F7W+iuTcCsqCPO//lgBjUas99vAYAgOlBuv5qeObQnum1MASoeJOfB7Hi039x0lH 17 | GXyoMl2U7w9MU9xEstC3D2HZPJpjXjBRVMaqYyec3qWtLTEpxBOS9PPgMb3bqw9y 18 | HysuJqufWLnj5pHY1zIYGsXYI8RW3TO4dy+70yUFbgyWLmRKOC2n4XZQZJD76aM7 19 | mmqZTEsVIKbG5eDW0Rx0WZAVZFqJ+Y4I88F/v2jJv6M= 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /t/certs/client_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEA7QvDEy/Ej4Xm83oZTLbM+luQBg0Ks+U3h4JRVFvbTf9ZORm/ 3 | 7Pu04EhNLV2xL5mm69iW3DNa4NbUnkpMA5eavU08C6lfTMK1q83uG9TYZMSqmZAW 4 | KV7Rx5ZqH1NMmSbfziHqBtDz/LuEDq0JDxzPhYnTjnNZkMuUA+Fshe3cCTSMuD7+ 5 | tn+7U3+yp2Eyq9dUoc+ac/Nlz3IxtFnobV1o5TCzObZQwgZ1Cyfcn88gWev4z0jT 6 | c813b14TXGo8cbMxPAestMVG1WTW2h+lS1kQJnLaxXZBQdcQ+tLjli8Db9pzCH0E 7 | FlbL0yc1zxCp7kmiPhTmdGjgz5JIr3fSIBcbhQIDAQABAoIBAQCIxNGQH26E9NhV 8 | QOyaA5rvFKdbpmi3dRh9+iXcy4cUULF1LbM8E9q/0VeeRkG2WiwiRfx9HrBfq/RP 9 | RETpia6BbK+pdtcCnhFeCFpK+prEQWjBY4413nyhcNrYKiK29uQIti6mMiekebjv 10 | Es14R8JApU5IEavqzFshhg4slBnxwKDMbEHnupnsqeGORxTcuNCFud+bPsoTNn47 11 | zWBjOmRKxdZ4sUekVIktBp9PqbpTWHvNkFHC/pPGIIDh5jIyFDjN/YAR8MD36YZJ 12 | /bA7jNx1BBXLqUHQ/JOLDMzMd372FG8cBLeH+G8fccmQEqmPgcl4FjVPu6c12QM0 13 | edE+isH9AoGBAP4uUSvaTnnt0/0OOs4f/Waka2VuQIFzNqCgUoaTXP9EhtbG7Nym 14 | bbCqbzgXox93KfDdSVQ6ekMwgk0M6tWQ99Yep8Zrp6c4tbP81p+2t7GQ2uzM2lT0 15 | rLNkJp9ivQ8IAhbbX7+K7lAwP+FWGMIeE5TvYwCOC/eNiuWIoHpRpu7bAoGBAO6+ 16 | DVRrtaXz2GsCpSERx986Khp2Td2VVcqRhDBWWcE+8TJWec6goRBbAjR4Z2eoAf6W 17 | sCYEqFxXJd+8jQQZcVF4/H/q9t1tBNTcpiTRBP/DKOS7SN0PZg+uG9+dapsvQetJ 18 | nHEDWTcymBXQz0YE2MXc3tQyP/gYP6rS9y2cwj0fAoGAe0C0ZaJXOXKkv+m9hCQo 19 | +TOzTBz5NIxMqfjz64PbrYBqLg2J37joKamLgtSbae+bq8tx+mDc1mXYWUpxORwx 20 | wWdeDa44PrMWEPAClrHAQ18teXQgTt/SOq7Ot6zmZjqI4NKBjFzvEH1FDJx2JvL0 21 | pdyq0iMPFHxTJNhqkMW1P88CgYA6C582DVIG3TibG/OGDMqprXybHnRkEXDgZWzc 22 | S8Jax7Dg6kGHSWqfjWEF3NhXTXPesEh3ld5RRScCNNecTYtNobrSZxBKWb04+8Sx 23 | YthyTsLcieR3Ss0nts80GRPYUy8Vw7nziDrivrxXYYNjpL4HrWHH+cevzEV+hPhv 24 | 8JMRRQKBgHnN4/JBUif1rDvyeWRanxl0F0zPxkOm9goq575Og0oodoCChG7lkJAv 25 | 3mMTBdXS4soaR6KW+kNzMEUSd1JE34QyuZMDGTPVSKwMQdoadHAhOT1lrosvJpc9 26 | EJvDTQF/d0E8m5guFuqAZttuIwxzUxNC9V8C7TKZlt8vd3wYWQ/5 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /t/certs/client_key_broken.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | BrokenIBAAKCAQEA7QvDEy/Ej4Xm83oZTLbM+luQBg0Ks+U3h4JRVFvbTf9ZORm/ 3 | 7Pu04EhNLV2xL5mm69iW3DNa4NbUnkpMA5eavU08C6lfTMK1q83uG9TYZMSqmZAW 4 | KV7Rx5ZqH1NMmSbfziHqBtDz/LuEDq0JDxzPhYnTjnNZkMuUA+Fshe3cCTSMuD7+ 5 | tn+7U3+yp2Eyq9dUoc+ac/Nlz3IxtFnobV1o5TCzObZQwgZ1Cyfcn88gWev4z0jT 6 | c813b14TXGo8cbMxPAestMVG1WTW2h+lS1kQJnLaxXZBQdcQ+tLjli8Db9pzCH0E 7 | FlbL0yc1zxCp7kmiPhTmdGjgz5JIr3fSIBcbhQIDAQABAoIBAQCIxNGQH26E9NhV 8 | QOyaA5rvFKdbpmi3dRh9+iXcy4cUULF1LbM8E9q/0VeeRkG2WiwiRfx9HrBfq/RP 9 | RETpia6BbK+pdtcCnhFeCFpK+prEQWjBY4413nyhcNrYKiK29uQIti6mMiekebjv 10 | Es14R8JApU5IEavqzFshhg4slBnxwKDMbEHnupnsqeGORxTcuNCFud+bPsoTNn47 11 | zWBjOmRKxdZ4sUekVIktBp9PqbpTWHvNkFHC/pPGIIDh5jIyFDjN/YAR8MD36YZJ 12 | /bA7jNx1BBXLqUHQ/JOLDMzMd372FG8cBLeH+G8fccmQEqmPgcl4FjVPu6c12QM0 13 | edE+isH9AoGBAP4uUSvaTnnt0/0OOs4f/Waka2VuQIFzNqCgUoaTXP9EhtbG7Nym 14 | bbCqbzgXox93KfDdSVQ6ekMwgk0M6tWQ99Yep8Zrp6c4tbP81p+2t7GQ2uzM2lT0 15 | rLNkJp9ivQ8IAhbbX7+K7lAwP+FWGMIeE5TvYwCOC/eNiuWIoHpRpu7bAoGBAO6+ 16 | DVRrtaXz2GsCpSERx986Khp2Td2VVcqRhDBWWcE+8TJWec6goRBbAjR4Z2eoAf6W 17 | sCYEqFxXJd+8jQQZcVF4/H/q9t1tBNTcpiTRBP/DKOS7SN0PZg+uG9+dapsvQetJ 18 | nHEDWTcymBXQz0YE2MXc3tQyP/gYP6rS9y2cwj0fAoGAe0C0ZaJXOXKkv+m9hCQo 19 | +TOzTBz5NIxMqfjz64PbrYBqLg2J37joKamLgtSbae+bq8tx+mDc1mXYWUpxORwx 20 | wWdeDa44PrMWEPAClrHAQ18teXQgTt/SOq7Ot6zmZjqI4NKBjFzvEH1FDJx2JvL0 21 | pdyq0iMPFHxTJNhqkMW1P88CgYA6C582DVIG3TibG/OGDMqprXybHnRkEXDgZWzc 22 | S8Jax7Dg6kGHSWqfjWEF3NhXTXPesEh3ld5RRScCNNecTYtNobrSZxBKWb04+8Sx 23 | YthyTsLcieR3Ss0nts80GRPYUy8Vw7nziDrivrxXYYNjpL4HrWHH+cevzEV+hPhv 24 | 8JMRRQKBgHnN4/JBUif1rDvyeWRanxl0F0zPxkOm9goq575Og0oodoCChG7lkJAv 25 | 3mMTBdXS4soaR6KW+kNzMEUSd1JE34QyuZMDGTPVSKwMQdoadHAhOT1lrosvJpc9 26 | EJvDTQF/d0E8m5guFuqAZttuIwxzUxNC9V8C7TKZlt8vd3wYWQ/5 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /t/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celery/py-amqp/1c1cb1726d74a05912ac3964a1422fd97bb0f62c/t/integration/__init__.py -------------------------------------------------------------------------------- /t/integration/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | 4 | 5 | def pytest_sessionfinish(session, exitstatus): 6 | tox_env_dir = os.environ.get('TOX_WORK_DIR') 7 | if exitstatus and tox_env_dir: 8 | subprocess.call(["bash", "./rabbitmq_logs.sh"]) 9 | -------------------------------------------------------------------------------- /t/integration/test_rmq.py: -------------------------------------------------------------------------------- 1 | import os 2 | import ssl 3 | from unittest.mock import ANY, Mock 4 | 5 | import pytest 6 | 7 | import amqp 8 | from amqp import transport 9 | 10 | 11 | def get_connection( 12 | hostname, port, vhost, use_tls=False, 13 | keyfile=None, certfile=None, ca_certs=None 14 | ): 15 | host = f'{hostname}:{port}' 16 | if use_tls: 17 | return amqp.Connection(host=host, vhost=vhost, ssl={ 18 | 'keyfile': keyfile, 19 | 'certfile': certfile, 20 | 'ca_certs': ca_certs, 21 | } 22 | ) 23 | else: 24 | return amqp.Connection(host=host, vhost=vhost) 25 | 26 | 27 | @pytest.fixture(params=['plain', 'tls']) 28 | def connection(request): 29 | # this fixture yields plain connections to broker and TLS encrypted 30 | if request.param == 'plain': 31 | return get_connection( 32 | hostname=os.environ.get('RABBITMQ_HOST', 'localhost'), 33 | port=os.environ.get('RABBITMQ_5672_TCP', '5672'), 34 | vhost=getattr( 35 | request.config, "slaveinput", {} 36 | ).get("slaveid", None), 37 | ) 38 | elif request.param == 'tls': 39 | return get_connection( 40 | hostname=os.environ.get('RABBITMQ_HOST', 'localhost'), 41 | port=os.environ.get('RABBITMQ_5671_TCP', '5671'), 42 | vhost=getattr( 43 | request.config, "slaveinput", {} 44 | ).get("slaveid", None), 45 | use_tls=True, 46 | keyfile='t/certs/client_key.pem', 47 | certfile='t/certs/client_certificate.pem', 48 | ca_certs='t/certs/ca_certificate.pem', 49 | ) 50 | 51 | 52 | @pytest.mark.env('rabbitmq') 53 | @pytest.mark.flaky(reruns=5, reruns_delay=2) 54 | def test_connect(connection): 55 | connection.connect() 56 | repr(connection) 57 | connection.close() 58 | 59 | 60 | @pytest.mark.env('rabbitmq') 61 | @pytest.mark.flaky(reruns=5, reruns_delay=2) 62 | def test_tls_connect_fails(): 63 | # testing that wrong client key/certificate yields SSLError 64 | # when encrypted connection is used 65 | connection = get_connection( 66 | hostname=os.environ.get('RABBITMQ_HOST', 'localhost'), 67 | port=os.environ.get('RABBITMQ_5671_TCP', '5671'), 68 | vhost='/', 69 | use_tls=True, 70 | keyfile='t/certs/client_key_broken.pem', 71 | certfile='t/certs/client_certificate_broken.pem' 72 | ) 73 | with pytest.raises(ssl.SSLError): 74 | connection.connect() 75 | 76 | 77 | @pytest.mark.env('rabbitmq') 78 | @pytest.mark.flaky(reruns=5, reruns_delay=2) 79 | def test_tls_default_certs(): 80 | # testing TLS connection against badssl.com with default certs 81 | connection = transport.Transport( 82 | host="tls-v1-2.badssl.com:1012", 83 | ssl=True, 84 | ) 85 | assert type(connection) == transport.SSLTransport 86 | connection.connect() 87 | 88 | 89 | @pytest.mark.env('rabbitmq') 90 | @pytest.mark.flaky(reruns=5, reruns_delay=2) 91 | def test_tls_no_default_certs_fails(): 92 | # testing TLS connection fails against badssl.com without default certs 93 | connection = transport.Transport( 94 | host="tls-v1-2.badssl.com:1012", 95 | ssl={ 96 | "ca_certs": 't/certs/ca_certificate.pem', 97 | }, 98 | ) 99 | with pytest.raises(ssl.SSLError): 100 | connection.connect() 101 | 102 | 103 | @pytest.mark.env('rabbitmq') 104 | class test_rabbitmq_operations(): 105 | 106 | @pytest.fixture(autouse=True) 107 | def setup_conn(self, connection): 108 | self.connection = connection 109 | self.connection.connect() 110 | self.channel = self.connection.channel() 111 | yield 112 | self.channel.close() 113 | self.connection.close() 114 | 115 | @pytest.mark.parametrize( 116 | "publish_method,mandatory,immediate", 117 | ( 118 | ('basic_publish', False, True), 119 | ('basic_publish', True, True), 120 | ('basic_publish', False, False), 121 | ('basic_publish', True, False), 122 | ('basic_publish_confirm', False, True), 123 | ('basic_publish_confirm', True, True), 124 | ('basic_publish_confirm', False, False), 125 | ('basic_publish_confirm', True, False), 126 | ) 127 | ) 128 | @pytest.mark.flaky(reruns=5, reruns_delay=2) 129 | def test_publish_consume(self, publish_method, mandatory, immediate): 130 | callback = Mock() 131 | self.channel.queue_declare( 132 | queue='py-amqp-unittest', durable=False, exclusive=True 133 | ) 134 | # RabbitMQ 3 removed the support for the immediate flag 135 | # Since we confirm the message, RabbitMQ complains 136 | # See 137 | # http://www.rabbitmq.com/blog/2012/11/19/breaking-things-with-rabbitmq-3-0/ 138 | if immediate and publish_method == "basic_publish_confirm": 139 | with pytest.raises(amqp.exceptions.AMQPNotImplementedError) as exc: 140 | getattr(self.channel, publish_method)( 141 | amqp.Message('Unittest'), 142 | routing_key='py-amqp-unittest', 143 | mandatory=mandatory, 144 | immediate=immediate 145 | ) 146 | 147 | assert exc.value.reply_code == 540 148 | assert exc.value.method_name == 'Basic.publish' 149 | assert exc.value.reply_text == 'NOT_IMPLEMENTED - immediate=true' 150 | 151 | return 152 | else: 153 | getattr(self.channel, publish_method)( 154 | amqp.Message('Unittest'), 155 | routing_key='py-amqp-unittest', 156 | mandatory=mandatory, 157 | immediate=immediate 158 | ) 159 | # RabbitMQ 3 removed the support for the immediate flag 160 | # See 161 | # http://www.rabbitmq.com/blog/2012/11/19/breaking-things-with-rabbitmq-3-0/ 162 | if immediate: 163 | with pytest.raises(amqp.exceptions.AMQPNotImplementedError) as exc: 164 | self.channel.basic_consume( 165 | queue='py-amqp-unittest', 166 | callback=callback, 167 | consumer_tag='amq.ctag-PCmzXGkhCw_v0Zq7jXyvkg' 168 | ) 169 | assert exc.value.reply_code == 540 170 | assert exc.value.method_name == 'Basic.publish' 171 | assert exc.value.reply_text == 'NOT_IMPLEMENTED - immediate=true' 172 | 173 | return 174 | else: 175 | self.channel.basic_consume( 176 | queue='py-amqp-unittest', 177 | callback=callback, 178 | consumer_tag='amq.ctag-PCmzXGkhCw_v0Zq7jXyvkg' 179 | ) 180 | self.connection.drain_events() 181 | callback.assert_called_once_with(ANY) 182 | msg = callback.call_args[0][0] 183 | assert isinstance(msg, amqp.Message) 184 | assert msg.body_size == len('Unittest') 185 | assert msg.body == 'Unittest' 186 | assert msg.frame_method == amqp.spec.Basic.Deliver 187 | assert msg.delivery_tag == 1 188 | assert msg.ready is True 189 | assert msg.delivery_info == { 190 | 'consumer_tag': 'amq.ctag-PCmzXGkhCw_v0Zq7jXyvkg', 191 | 'delivery_tag': 1, 192 | 'redelivered': False, 193 | 'exchange': '', 194 | 'routing_key': 'py-amqp-unittest' 195 | } 196 | assert msg.properties == {'content_encoding': 'utf-8'} 197 | 198 | self.channel.basic_ack(msg.delivery_tag) 199 | 200 | @pytest.mark.flaky(reruns=5, reruns_delay=2) 201 | def test_publish_get(self): 202 | self.channel.queue_declare( 203 | queue='py-amqp-unittest', durable=False, exclusive=True 204 | ) 205 | self.channel.basic_publish( 206 | amqp.Message('Unittest'), routing_key='py-amqp-unittest' 207 | ) 208 | msg = self.channel.basic_get( 209 | queue='py-amqp-unittest', 210 | ) 211 | assert msg.body_size == 8 212 | assert msg.body == 'Unittest' 213 | assert msg.frame_method == amqp.spec.Basic.GetOk 214 | assert msg.delivery_tag == 1 215 | assert msg.ready is True 216 | assert msg.delivery_info == { 217 | 'delivery_tag': 1, 'redelivered': False, 218 | 'exchange': '', 219 | 'routing_key': 'py-amqp-unittest', 'message_count': 0 220 | } 221 | assert msg.properties == { 222 | 'content_encoding': 'utf-8' 223 | } 224 | 225 | self.channel.basic_ack(msg.delivery_tag) 226 | 227 | msg = self.channel.basic_get( 228 | queue='py-amqp-unittest', 229 | ) 230 | assert msg is None 231 | -------------------------------------------------------------------------------- /t/mocks.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | 3 | class _ContextMock(Mock): 4 | """Dummy class implementing __enter__ and __exit__ 5 | as the :keyword:`with` statement requires these to be implemented 6 | in the class, not just the instance.""" 7 | 8 | def __enter__(self): 9 | return self 10 | 11 | def __exit__(self, *exc_info): 12 | pass 13 | 14 | 15 | def ContextMock(*args, **kwargs): 16 | """Mock that mocks :keyword:`with` statement contexts.""" 17 | obj = _ContextMock(*args, **kwargs) 18 | obj.attach_mock(_ContextMock(), '__enter__') 19 | obj.attach_mock(_ContextMock(), '__exit__') 20 | obj.__enter__.return_value = obj 21 | # if __exit__ return a value the exception is ignored, 22 | # so it must return None here. 23 | obj.__exit__.return_value = None 24 | return obj 25 | -------------------------------------------------------------------------------- /t/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celery/py-amqp/1c1cb1726d74a05912ac3964a1422fd97bb0f62c/t/unit/__init__.py -------------------------------------------------------------------------------- /t/unit/conftest.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import MagicMock 2 | import pytest 3 | 4 | sentinel = object() 5 | 6 | class _patching: 7 | 8 | def __init__(self, monkeypatch, request): 9 | self.monkeypatch = monkeypatch 10 | self.request = request 11 | 12 | def __getattr__(self, name): 13 | return getattr(self.monkeypatch, name) 14 | 15 | def __call__(self, path, value=sentinel, name=None, 16 | new=MagicMock, **kwargs): 17 | value = self._value_or_mock(value, new, name, path, **kwargs) 18 | self.monkeypatch.setattr(path, value) 19 | return value 20 | 21 | def _value_or_mock(self, value, new, name, path, **kwargs): 22 | if value is sentinel: 23 | value = new(name=name or path.rpartition('.')[2]) 24 | for k, v in kwargs.items(): 25 | setattr(value, k, v) 26 | return value 27 | 28 | def setattr(self, target, name=sentinel, value=sentinel, **kwargs): 29 | # alias to __call__ with the interface of pytest.monkeypatch.setattr 30 | if value is sentinel: 31 | value, name = name, None 32 | return self(target, value, name=name) 33 | 34 | def setitem(self, dic, name, value=sentinel, new=MagicMock, **kwargs): 35 | # same as pytest.monkeypatch.setattr but default value is MagicMock 36 | value = self._value_or_mock(value, new, name, dic, **kwargs) 37 | self.monkeypatch.setitem(dic, name, value) 38 | return value 39 | 40 | 41 | @pytest.fixture 42 | def patching(monkeypatch, request): 43 | """Monkeypath.setattr shortcut. 44 | Example: 45 | .. code-block:: python 46 | def test_foo(patching): 47 | # execv value here will be mock.MagicMock by default. 48 | execv = patching('os.execv') 49 | patching('sys.platform', 'darwin') # set concrete value 50 | patching.setenv('DJANGO_SETTINGS_MODULE', 'x.settings') 51 | # val will be of type mock.MagicMock by default 52 | val = patching.setitem('path.to.dict', 'KEY') 53 | """ 54 | return _patching(monkeypatch, request) 55 | -------------------------------------------------------------------------------- /t/unit/test_abstract_channel.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock, patch, sentinel 2 | 3 | import pytest 4 | from vine import promise 5 | 6 | from amqp import spec 7 | from amqp.abstract_channel import (IGNORED_METHOD_DURING_CHANNEL_CLOSE, 8 | AbstractChannel) 9 | from amqp.exceptions import AMQPNotImplementedError, RecoverableConnectionError 10 | from amqp.serialization import dumps 11 | 12 | 13 | class test_AbstractChannel: 14 | 15 | class Channel(AbstractChannel): 16 | 17 | def _setup_listeners(self): 18 | pass 19 | 20 | @pytest.fixture(autouse=True) 21 | def setup_conn(self): 22 | self.conn = Mock(name='connection') 23 | self.conn.channels = {} 24 | self.channel_id = 1 25 | self.c = self.Channel(self.conn, self.channel_id) 26 | self.method = Mock(name='method') 27 | self.content = Mock(name='content') 28 | self.content.content_encoding = 'utf-8' 29 | self.c._METHODS = {(50, 61): self.method} 30 | 31 | def test_enter_exit(self): 32 | self.c.close = Mock(name='close') 33 | with self.c: 34 | pass 35 | self.c.close.assert_called_with() 36 | 37 | def test_send_method(self): 38 | self.c.send_method((50, 60), 'iB', (30, 0)) 39 | self.conn.frame_writer.assert_called_with( 40 | 1, self.channel_id, (50, 60), dumps('iB', (30, 0)), None, 41 | ) 42 | 43 | def test_send_method__callback(self): 44 | callback = Mock(name='callback') 45 | p = promise(callback) 46 | self.c.send_method((50, 60), 'iB', (30, 0), callback=p) 47 | callback.assert_called_with() 48 | 49 | def test_send_method__wait(self): 50 | self.c.wait = Mock(name='wait') 51 | self.c.send_method((50, 60), 'iB', (30, 0), wait=(50, 61)) 52 | self.c.wait.assert_called_with((50, 61), returns_tuple=False) 53 | 54 | def test_send_method__no_connection(self): 55 | self.c.connection = None 56 | with pytest.raises(RecoverableConnectionError): 57 | self.c.send_method((50, 60)) 58 | 59 | def test_send_method__connection_dropped(self): 60 | self.c.connection.frame_writer.side_effect = StopIteration 61 | with pytest.raises(RecoverableConnectionError): 62 | self.c.send_method((50, 60)) 63 | 64 | def test_close(self): 65 | with pytest.raises(NotImplementedError): 66 | self.c.close() 67 | 68 | def test_wait(self): 69 | with patch('amqp.abstract_channel.ensure_promise') as ensure_promise: 70 | p = ensure_promise.return_value 71 | p.ready = False 72 | 73 | def on_drain(*args, **kwargs): 74 | p.ready = True 75 | self.conn.drain_events.side_effect = on_drain 76 | 77 | p.value = (1,), {'arg': 2} 78 | self.c.wait((50, 61), timeout=1) 79 | self.conn.drain_events.assert_called_with(timeout=1) 80 | 81 | prev = self.c._pending[(50, 61)] = Mock(name='p2') 82 | p.value = None 83 | self.c.wait([(50, 61)]) 84 | assert self.c._pending[(50, 61)] is prev 85 | 86 | def test_dispatch_method__content_encoding(self): 87 | self.c.auto_decode = True 88 | self.method.args = None 89 | self.c.dispatch_method((50, 61), 'payload', self.content) 90 | self.content.body.decode.side_effect = KeyError() 91 | self.c.dispatch_method((50, 61), 'payload', self.content) 92 | 93 | def test_dispatch_method__unknown_method(self): 94 | with pytest.raises(AMQPNotImplementedError): 95 | self.c.dispatch_method((100, 131), 'payload', self.content) 96 | 97 | def test_dispatch_method__one_shot(self): 98 | self.method.args = None 99 | p = self.c._pending[(50, 61)] = Mock(name='oneshot') 100 | self.c.dispatch_method((50, 61), 'payload', self.content) 101 | p.assert_called_with((50, 61), self.content) 102 | 103 | def test_dispatch_method__one_shot_no_content(self): 104 | self.method.args = None 105 | self.method.content = None 106 | p = self.c._pending[(50, 61)] = Mock(name='oneshot') 107 | self.c.dispatch_method((50, 61), 'payload', self.content) 108 | p.assert_called_with((50, 61)) 109 | assert not self.c._pending 110 | 111 | def test_dispatch_method__listeners(self): 112 | with patch('amqp.abstract_channel.loads') as loads: 113 | loads.return_value = [1, 2, 3], 'foo' 114 | p = self.c._callbacks[(50, 61)] = Mock(name='p') 115 | self.c.dispatch_method((50, 61), 'payload', self.content) 116 | p.assert_called_with(1, 2, 3, self.content) 117 | 118 | def test_dispatch_method__listeners_and_one_shot(self): 119 | with patch('amqp.abstract_channel.loads') as loads: 120 | loads.return_value = [1, 2, 3], 'foo' 121 | p1 = self.c._callbacks[(50, 61)] = Mock(name='p') 122 | p2 = self.c._pending[(50, 61)] = Mock(name='oneshot') 123 | self.c.dispatch_method((50, 61), 'payload', self.content) 124 | p1.assert_called_with(1, 2, 3, self.content) 125 | p2.assert_called_with((50, 61), 1, 2, 3, self.content) 126 | assert not self.c._pending 127 | assert self.c._callbacks[(50, 61)] 128 | 129 | @pytest.mark.parametrize( 130 | "method", 131 | ( 132 | spec.Channel.Close, 133 | spec.Channel.CloseOk, 134 | spec.Basic.Deliver 135 | ) 136 | ) 137 | def test_dispatch_method__closing_connection(self, method, caplog): 138 | self.c._ALLOWED_METHODS_WHEN_CLOSING = ( 139 | spec.Channel.Close, spec.Channel.CloseOk 140 | ) 141 | self.c.is_closing = True 142 | with patch.object(self.c, '_METHODS'), \ 143 | patch.object(self.c, '_callbacks'): 144 | self.c.dispatch_method( 145 | method, sentinel.PAYLOAD, sentinel.CONTENT 146 | ) 147 | if method in (spec.Channel.Close, spec.Channel.CloseOk): 148 | self.c._METHODS.__getitem__.assert_called_once_with(method) 149 | self.c._callbacks[method].assert_called_once_with( 150 | sentinel.CONTENT 151 | ) 152 | else: 153 | self.c._METHODS.__getitem__.assert_not_called() 154 | self.c._callbacks[method].assert_not_called() 155 | assert caplog.records[0].msg == \ 156 | IGNORED_METHOD_DURING_CHANNEL_CLOSE 157 | assert caplog.records[0].args[0] == method 158 | assert caplog.records[0].args[1] == self.channel_id 159 | assert caplog.records[0].levelname == 'WARNING' 160 | -------------------------------------------------------------------------------- /t/unit/test_basic_message.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | 3 | from amqp.basic_message import Message 4 | 5 | 6 | class test_Message: 7 | 8 | def test_message(self): 9 | m = Message( 10 | 'foo', 11 | channel=Mock(name='channel'), 12 | application_headers={'h': 'v'}, 13 | ) 14 | m.delivery_info = {'delivery_tag': '1234'} 15 | assert m.body == 'foo' 16 | assert m.channel 17 | assert m.headers == {'h': 'v'} 18 | assert m.delivery_tag == '1234' 19 | -------------------------------------------------------------------------------- /t/unit/test_channel.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from struct import pack 3 | from unittest.mock import ANY, MagicMock, Mock, patch 4 | 5 | import pytest 6 | from vine import promise 7 | 8 | from amqp import spec 9 | from amqp.basic_message import Message 10 | from amqp.channel import Channel 11 | from amqp.exceptions import (ConsumerCancelled, MessageNacked, NotFound, 12 | RecoverableConnectionError) 13 | from amqp.serialization import dumps 14 | 15 | from t.mocks import ContextMock 16 | 17 | class test_Channel: 18 | 19 | @pytest.fixture(autouse=True) 20 | def setup_conn(self): 21 | self.conn = MagicMock(name='connection') 22 | self.conn.is_closing = False 23 | self.conn.channels = {} 24 | self.conn._get_free_channel_id.return_value = 2 25 | self.c = Channel(self.conn, 1) 26 | self.c.send_method = Mock(name='send_method') 27 | 28 | def test_init_confirm_enabled(self): 29 | self.conn.confirm_publish = True 30 | c = Channel(self.conn, 2) 31 | assert c.basic_publish == c.basic_publish_confirm 32 | 33 | def test_init_confirm_disabled(self): 34 | self.conn.confirm_publish = False 35 | c = Channel(self.conn, 2) 36 | assert c.basic_publish == c._basic_publish 37 | 38 | def test_init_auto_channel(self): 39 | c = Channel(self.conn, None) 40 | self.conn._get_free_channel_id.assert_called_with() 41 | assert c.channel_id is self.conn._get_free_channel_id() 42 | 43 | def test_init_explicit_channel(self): 44 | Channel(self.conn, 3) 45 | self.conn._claim_channel_id.assert_called_with(3) 46 | 47 | def test_then(self): 48 | self.c.on_open = Mock(name='on_open') 49 | on_success = Mock(name='on_success') 50 | on_error = Mock(name='on_error') 51 | self.c.then(on_success, on_error) 52 | self.c.on_open.then.assert_called_with(on_success, on_error) 53 | 54 | def test_collect(self): 55 | self.c.callbacks[(50, 61)] = Mock() 56 | self.c.cancel_callbacks['foo'] = Mock() 57 | self.c.events['bar'].add(Mock()) 58 | self.c.no_ack_consumers.add('foo') 59 | self.c.collect() 60 | assert not self.c.callbacks 61 | assert not self.c.cancel_callbacks 62 | assert not self.c.events 63 | assert not self.c.no_ack_consumers 64 | assert not self.c.is_open 65 | self.c.collect() 66 | 67 | def test_do_revive(self): 68 | self.c.open = Mock(name='open') 69 | self.c.is_open = True 70 | self.c._do_revive() 71 | assert not self.c.is_open 72 | self.c.open.assert_called_with() 73 | 74 | def test_close__not_open(self): 75 | self.c.is_open = False 76 | self.c.close() 77 | 78 | def test_close__no_connection(self): 79 | self.c.connection = None 80 | self.c.close() 81 | 82 | def test_close(self): 83 | self.c.is_open = True 84 | self.c.close(30, 'text', spec.Queue.Declare) 85 | self.c.send_method.assert_called_with( 86 | spec.Channel.Close, 'BsBB', 87 | (30, 'text', spec.Queue.Declare[0], spec.Queue.Declare[1]), 88 | wait=spec.Channel.CloseOk, 89 | ) 90 | assert self.c.is_closing is False 91 | assert self.c.connection is None 92 | 93 | def test_on_close(self): 94 | self.c._do_revive = Mock(name='_do_revive') 95 | with pytest.raises(NotFound): 96 | self.c._on_close(404, 'text', 50, 61) 97 | self.c.send_method.assert_called_with(spec.Channel.CloseOk) 98 | self.c._do_revive.assert_called_with() 99 | 100 | def test_on_close_ok(self): 101 | self.c.collect = Mock(name='collect') 102 | self.c._on_close_ok() 103 | self.c.collect.assert_called_with() 104 | 105 | def test_flow(self): 106 | self.c.flow(0) 107 | self.c.send_method.assert_called_with( 108 | spec.Channel.Flow, 'b', (0,), wait=spec.Channel.FlowOk, 109 | ) 110 | 111 | def test_on_flow(self): 112 | self.c._x_flow_ok = Mock(name='_x_flow_ok') 113 | self.c._on_flow(0) 114 | assert not self.c.active 115 | self.c._x_flow_ok.assert_called_with(0) 116 | 117 | def test_x_flow_ok(self): 118 | self.c._x_flow_ok(1) 119 | self.c.send_method.assert_called_with(spec.Channel.FlowOk, 'b', (1,)) 120 | 121 | def test_open(self): 122 | self.c.is_open = True 123 | self.c.open() 124 | self.c.is_open = False 125 | self.c.open() 126 | self.c.send_method.assert_called_with( 127 | spec.Channel.Open, 's', ('',), wait=spec.Channel.OpenOk, 128 | ) 129 | 130 | def test_on_open_ok(self): 131 | self.c.on_open = Mock(name='on_open') 132 | self.c.is_open = False 133 | self.c._on_open_ok() 134 | assert self.c.is_open 135 | self.c.on_open.assert_called_with(self.c) 136 | 137 | def test_exchange_declare(self): 138 | self.c.exchange_declare( 139 | 'foo', 'direct', False, True, 140 | auto_delete=False, nowait=False, arguments={'x': 1}, 141 | ) 142 | self.c.send_method.assert_called_with( 143 | spec.Exchange.Declare, 'BssbbbbbF', 144 | (0, 'foo', 'direct', False, True, False, 145 | False, False, {'x': 1}), 146 | wait=spec.Exchange.DeclareOk, 147 | ) 148 | 149 | def test_exchange_declare__auto_delete(self): 150 | self.c.exchange_declare( 151 | 'foo', 'direct', False, True, 152 | auto_delete=True, nowait=False, arguments={'x': 1}, 153 | ) 154 | 155 | def test_exchange_delete(self): 156 | self.c.exchange_delete('foo') 157 | self.c.send_method.assert_called_with( 158 | spec.Exchange.Delete, 'Bsbb', 159 | (0, 'foo', False, False), 160 | wait=spec.Exchange.DeleteOk, 161 | ) 162 | 163 | def test_exchange_bind(self): 164 | self.c.exchange_bind('dest', 'source', 'rkey', arguments={'x': 1}) 165 | self.c.send_method.assert_called_with( 166 | spec.Exchange.Bind, 'BsssbF', 167 | (0, 'dest', 'source', 'rkey', False, {'x': 1}), 168 | wait=spec.Exchange.BindOk, 169 | ) 170 | 171 | def test_exchange_unbind(self): 172 | self.c.exchange_unbind('dest', 'source', 'rkey', arguments={'x': 1}) 173 | self.c.send_method.assert_called_with( 174 | spec.Exchange.Unbind, 'BsssbF', 175 | (0, 'dest', 'source', 'rkey', False, {'x': 1}), 176 | wait=spec.Exchange.UnbindOk, 177 | ) 178 | 179 | def test_queue_bind(self): 180 | self.c.queue_bind('q', 'ex', 'rkey', arguments={'x': 1}) 181 | self.c.send_method.assert_called_with( 182 | spec.Queue.Bind, 'BsssbF', 183 | (0, 'q', 'ex', 'rkey', False, {'x': 1}), 184 | wait=spec.Queue.BindOk, 185 | ) 186 | 187 | def test_queue_unbind(self): 188 | self.c.queue_unbind('q', 'ex', 'rkey', arguments={'x': 1}) 189 | self.c.send_method.assert_called_with( 190 | spec.Queue.Unbind, 'BsssF', 191 | (0, 'q', 'ex', 'rkey', {'x': 1}), 192 | wait=spec.Queue.UnbindOk, 193 | ) 194 | 195 | def test_queue_declare(self): 196 | self.c.queue_declare('q', False, True, False, False, True, {'x': 1}) 197 | self.c.send_method.assert_called_with( 198 | spec.Queue.Declare, 'BsbbbbbF', 199 | (0, 'q', False, True, False, False, True, {'x': 1}), 200 | ) 201 | 202 | def test_queue_declare__sync(self): 203 | self.c.wait = Mock(name='wait') 204 | self.c.wait.return_value = ('name', 123, 45) 205 | ret = self.c.queue_declare( 206 | 'q', False, True, False, False, False, {'x': 1}, 207 | ) 208 | self.c.send_method.assert_called_with( 209 | spec.Queue.Declare, 'BsbbbbbF', 210 | (0, 'q', False, True, False, False, False, {'x': 1}), 211 | ) 212 | assert ret.queue == 'name' 213 | assert ret.message_count == 123 214 | assert ret.consumer_count == 45 215 | self.c.wait.assert_called_with( 216 | spec.Queue.DeclareOk, returns_tuple=True) 217 | 218 | def test_queue_delete(self): 219 | self.c.queue_delete('q') 220 | self.c.send_method.assert_called_with( 221 | spec.Queue.Delete, 'Bsbbb', 222 | (0, 'q', False, False, False), 223 | wait=spec.Queue.DeleteOk, 224 | ) 225 | 226 | def test_queue_purge(self): 227 | self.c.queue_purge('q') 228 | self.c.send_method.assert_called_with( 229 | spec.Queue.Purge, 'Bsb', (0, 'q', False), 230 | wait=spec.Queue.PurgeOk, 231 | ) 232 | 233 | def test_basic_ack(self): 234 | self.c.basic_ack(123, multiple=1) 235 | self.c.send_method.assert_called_with( 236 | spec.Basic.Ack, 'Lb', (123, 1), 237 | ) 238 | 239 | def test_basic_cancel(self): 240 | self.c.basic_cancel(123) 241 | self.c.send_method.assert_called_with( 242 | spec.Basic.Cancel, 'sb', (123, False), 243 | wait=spec.Basic.CancelOk, 244 | ) 245 | self.c.connection = None 246 | self.c.basic_cancel(123) 247 | 248 | def test_on_basic_cancel(self): 249 | self.c._remove_tag = Mock(name='_remove_tag') 250 | self.c._on_basic_cancel(123) 251 | self.c._remove_tag.return_value.assert_called_with(123) 252 | self.c._remove_tag.return_value = None 253 | with pytest.raises(ConsumerCancelled): 254 | self.c._on_basic_cancel(123) 255 | 256 | def test_on_basic_cancel_ok(self): 257 | self.c._remove_tag = Mock(name='remove_tag') 258 | self.c._on_basic_cancel_ok(123) 259 | self.c._remove_tag.assert_called_with(123) 260 | 261 | def test_remove_tag(self): 262 | self.c.callbacks[123] = Mock() 263 | p = self.c.cancel_callbacks[123] = Mock() 264 | assert self.c._remove_tag(123) is p 265 | assert 123 not in self.c.callbacks 266 | assert 123 not in self.c.cancel_callbacks 267 | 268 | def test_basic_consume(self): 269 | callback = Mock() 270 | on_cancel = Mock() 271 | self.c.send_method.return_value = (123, ) 272 | self.c.basic_consume( 273 | 'q', 123, arguments={'x': 1}, 274 | callback=callback, 275 | on_cancel=on_cancel, 276 | ) 277 | self.c.send_method.assert_called_with( 278 | spec.Basic.Consume, 'BssbbbbF', 279 | (0, 'q', 123, False, False, False, False, {'x': 1}), 280 | wait=spec.Basic.ConsumeOk, 281 | returns_tuple=True 282 | ) 283 | assert self.c.callbacks[123] is callback 284 | assert self.c.cancel_callbacks[123] is on_cancel 285 | 286 | def test_basic_consume__no_ack(self): 287 | self.c.send_method.return_value = (123,) 288 | self.c.basic_consume( 289 | 'q', 123, arguments={'x': 1}, no_ack=True, 290 | ) 291 | assert 123 in self.c.no_ack_consumers 292 | 293 | def test_basic_consume_no_consumer_tag(self): 294 | callback = Mock() 295 | self.c.send_method.return_value = (123,) 296 | ret = self.c.basic_consume( 297 | 'q', arguments={'x': 1}, 298 | callback=callback, 299 | ) 300 | self.c.send_method.assert_called_with( 301 | spec.Basic.Consume, 'BssbbbbF', 302 | (0, 'q', '', False, False, False, False, {'x': 1}), 303 | wait=spec.Basic.ConsumeOk, 304 | returns_tuple=True 305 | ) 306 | assert self.c.callbacks[123] is callback 307 | assert ret == 123 308 | 309 | def test_basic_consume_no_wait(self): 310 | callback = Mock() 311 | ret_promise = promise() 312 | self.c.send_method.return_value = ret_promise 313 | ret = self.c.basic_consume( 314 | 'q', 123, arguments={'x': 1}, 315 | callback=callback, nowait=True 316 | ) 317 | self.c.send_method.assert_called_with( 318 | spec.Basic.Consume, 'BssbbbbF', 319 | (0, 'q', 123, False, False, False, True, {'x': 1}), 320 | wait=None, 321 | returns_tuple=True 322 | ) 323 | assert self.c.callbacks[123] is callback 324 | assert ret == ret_promise 325 | 326 | def test_basic_consume_no_wait_no_consumer_tag(self): 327 | callback = Mock() 328 | with pytest.raises(ValueError): 329 | self.c.basic_consume( 330 | 'q', arguments={'x': 1}, 331 | callback=callback, nowait=True 332 | ) 333 | assert 123 not in self.c.callbacks 334 | 335 | def test_on_basic_deliver(self): 336 | msg = Message() 337 | self.c._on_basic_deliver(123, '321', False, 'ex', 'rkey', msg) 338 | callback = self.c.callbacks[123] = Mock(name='cb') 339 | 340 | self.c._on_basic_deliver(123, '321', False, 'ex', 'rkey', msg) 341 | callback.assert_called_with(msg) 342 | assert msg.channel == self.c 343 | assert msg.delivery_info == { 344 | 'consumer_tag': 123, 345 | 'delivery_tag': '321', 346 | 'redelivered': False, 347 | 'exchange': 'ex', 348 | 'routing_key': 'rkey', 349 | } 350 | 351 | def test_basic_get(self): 352 | self.c._on_get_empty = Mock() 353 | self.c._on_get_ok = Mock() 354 | self.c.send_method.return_value = ('cluster_id',) 355 | self.c.basic_get('q') 356 | self.c.send_method.assert_called_with( 357 | spec.Basic.Get, 'Bsb', (0, 'q', False), 358 | wait=[spec.Basic.GetOk, spec.Basic.GetEmpty], returns_tuple=True, 359 | ) 360 | self.c._on_get_empty.assert_called_with('cluster_id') 361 | self.c.send_method.return_value = ( 362 | 'dtag', 'redelivered', 'ex', 'rkey', 'mcount', 'msg', 363 | ) 364 | self.c.basic_get('q') 365 | self.c._on_get_ok.assert_called_with( 366 | 'dtag', 'redelivered', 'ex', 'rkey', 'mcount', 'msg', 367 | ) 368 | 369 | def test_on_get_empty(self): 370 | self.c._on_get_empty(1) 371 | 372 | def test_on_get_ok(self): 373 | msg = Message() 374 | m = self.c._on_get_ok( 375 | 'dtag', 'redelivered', 'ex', 'rkey', 'mcount', msg, 376 | ) 377 | assert m is msg 378 | assert m.channel == self.c 379 | assert m.delivery_info == { 380 | 'delivery_tag': 'dtag', 381 | 'redelivered': 'redelivered', 382 | 'exchange': 'ex', 383 | 'routing_key': 'rkey', 384 | 'message_count': 'mcount', 385 | } 386 | 387 | def test_basic_publish(self): 388 | self.c.connection.transport.having_timeout = ContextMock() 389 | self.c._basic_publish('msg', 'ex', 'rkey') 390 | self.c.send_method.assert_called_with( 391 | spec.Basic.Publish, 'Bssbb', 392 | (0, 'ex', 'rkey', False, False), 'msg', 393 | ) 394 | 395 | def test_basic_publish_confirm(self): 396 | self.c._confirm_selected = False 397 | self.c.confirm_select = Mock(name='confirm_select') 398 | self.c._basic_publish = Mock(name='_basic_publish') 399 | self.c.wait = Mock(name='wait') 400 | ret = self.c.basic_publish_confirm(1, 2, arg=1) 401 | self.c.confirm_select.assert_called_with() 402 | assert self.c._confirm_selected 403 | self.c._basic_publish.assert_called_with(1, 2, arg=1) 404 | assert ret is self.c._basic_publish() 405 | self.c.wait.assert_called_with( 406 | [spec.Basic.Ack, spec.Basic.Nack], 407 | callback=ANY, 408 | timeout=None 409 | ) 410 | self.c.basic_publish_confirm(1, 2, arg=1) 411 | 412 | def test_basic_publish_confirm_nack(self): 413 | # test checking whether library is handling correctly Nack confirms 414 | # sent from RabbitMQ. Library must raise MessageNacked when server 415 | # sent Nack message. 416 | 417 | # Nack frame construction 418 | args = dumps('Lb', (1, False)) 419 | frame = (b''.join([pack('>HH', *spec.Basic.Nack), args])) 420 | 421 | def wait(method, *args, **kwargs): 422 | # Simple mock simulating registering callbacks of real wait method 423 | for m in method: 424 | self.c._pending[m] = kwargs['callback'] 425 | 426 | self.c._basic_publish = Mock(name='_basic_publish') 427 | self.c.wait = Mock(name='wait', side_effect=wait) 428 | 429 | self.c.basic_publish_confirm(1, 2, arg=1) 430 | 431 | with pytest.raises(MessageNacked): 432 | # Inject Nack to message handler 433 | self.c.dispatch_method( 434 | spec.Basic.Nack, frame, None 435 | ) 436 | 437 | def test_basic_publish_connection_blocked(self): 438 | # Basic test checking that drain_events() is called 439 | # before publishing message and send_method() is called 440 | self.c._basic_publish('msg', 'ex', 'rkey') 441 | self.conn.drain_events.assert_called_once_with(timeout=0) 442 | self.c.send_method.assert_called_once_with( 443 | spec.Basic.Publish, 'Bssbb', 444 | (0, 'ex', 'rkey', False, False), 'msg', 445 | ) 446 | 447 | self.c.send_method.reset_mock() 448 | 449 | # Basic test checking that socket.timeout exception 450 | # is ignored and send_method() is called. 451 | self.conn.drain_events.side_effect = socket.timeout 452 | self.c._basic_publish('msg', 'ex', 'rkey') 453 | self.c.send_method.assert_called_once_with( 454 | spec.Basic.Publish, 'Bssbb', 455 | (0, 'ex', 'rkey', False, False), 'msg', 456 | ) 457 | 458 | def test_basic_publish_connection_blocked_not_supported(self): 459 | # Test veryfying that when server does not have 460 | # connection.blocked capability, drain_events() are not called 461 | self.conn.client_properties = { 462 | 'capabilities': { 463 | 'connection.blocked': False 464 | } 465 | } 466 | self.c._basic_publish('msg', 'ex', 'rkey') 467 | self.conn.drain_events.assert_not_called() 468 | self.c.send_method.assert_called_once_with( 469 | spec.Basic.Publish, 'Bssbb', 470 | (0, 'ex', 'rkey', False, False), 'msg', 471 | ) 472 | 473 | def test_basic_publish_connection_blocked_not_supported_missing(self): 474 | # Test veryfying that when server does not have 475 | # connection.blocked capability, drain_events() are not called 476 | self.conn.client_properties = { 477 | 'capabilities': {} 478 | } 479 | self.c._basic_publish('msg', 'ex', 'rkey') 480 | self.conn.drain_events.assert_not_called() 481 | self.c.send_method.assert_called_once_with( 482 | spec.Basic.Publish, 'Bssbb', 483 | (0, 'ex', 'rkey', False, False), 'msg', 484 | ) 485 | 486 | def test_basic_publish_connection_blocked_no_capabilities(self): 487 | # Test veryfying that when server does not have 488 | # support of capabilities, drain_events() are not called 489 | self.conn.client_properties = { 490 | } 491 | self.c._basic_publish('msg', 'ex', 'rkey') 492 | self.conn.drain_events.assert_not_called() 493 | self.c.send_method.assert_called_once_with( 494 | spec.Basic.Publish, 'Bssbb', 495 | (0, 'ex', 'rkey', False, False), 'msg', 496 | ) 497 | 498 | def test_basic_publish_confirm_callback(self): 499 | 500 | def wait_nack(method, *args, **kwargs): 501 | kwargs['callback'](spec.Basic.Nack) 502 | 503 | def wait_ack(method, *args, **kwargs): 504 | kwargs['callback'](spec.Basic.Ack) 505 | 506 | self.c._basic_publish = Mock(name='_basic_publish') 507 | self.c.wait = Mock(name='wait_nack', side_effect=wait_nack) 508 | 509 | with pytest.raises(MessageNacked): 510 | # when callback is called with spec.Basic.Nack it must raise 511 | # MessageNacked exception 512 | self.c.basic_publish_confirm(1, 2, arg=1) 513 | 514 | self.c.wait = Mock(name='wait_ack', side_effect=wait_ack) 515 | 516 | # when callback is called with spec.Basic.Ack 517 | # it must nost raise exception 518 | self.c.basic_publish_confirm(1, 2, arg=1) 519 | 520 | def test_basic_publish_connection_closed(self): 521 | self.c.collect() 522 | with pytest.raises(RecoverableConnectionError) as excinfo: 523 | self.c._basic_publish('msg', 'ex', 'rkey') 524 | assert 'basic_publish: connection closed' in str(excinfo.value) 525 | self.c.send_method.assert_not_called() 526 | 527 | def test_basic_qos(self): 528 | self.c.basic_qos(0, 123, False) 529 | self.c.send_method.assert_called_with( 530 | spec.Basic.Qos, 'lBb', (0, 123, False), 531 | wait=spec.Basic.QosOk, 532 | ) 533 | 534 | def test_basic_recover(self): 535 | self.c.basic_recover(requeue=True) 536 | self.c.send_method.assert_called_with( 537 | spec.Basic.Recover, 'b', (True,), 538 | ) 539 | 540 | def test_basic_recover_async(self): 541 | self.c.basic_recover_async(requeue=True) 542 | self.c.send_method.assert_called_with( 543 | spec.Basic.RecoverAsync, 'b', (True,), 544 | ) 545 | 546 | def test_basic_reject(self): 547 | self.c.basic_reject(123, requeue=True) 548 | self.c.send_method.assert_called_with( 549 | spec.Basic.Reject, 'Lb', (123, True), 550 | ) 551 | 552 | def test_on_basic_return(self): 553 | with pytest.raises(NotFound): 554 | self.c._on_basic_return(404, 'text', 'ex', 'rkey', 'msg') 555 | 556 | def test_on_basic_return__handled(self): 557 | with patch('amqp.channel.error_for_code') as error_for_code: 558 | callback = Mock(name='callback') 559 | self.c.events['basic_return'].add(callback) 560 | self.c._on_basic_return(404, 'text', 'ex', 'rkey', 'msg') 561 | callback.assert_called_with( 562 | error_for_code(), 'ex', 'rkey', 'msg', 563 | ) 564 | 565 | def test_tx_commit(self): 566 | self.c.tx_commit() 567 | self.c.send_method.assert_called_with( 568 | spec.Tx.Commit, wait=spec.Tx.CommitOk, 569 | ) 570 | 571 | def test_tx_rollback(self): 572 | self.c.tx_rollback() 573 | self.c.send_method.assert_called_with( 574 | spec.Tx.Rollback, wait=spec.Tx.RollbackOk, 575 | ) 576 | 577 | def test_tx_select(self): 578 | self.c.tx_select() 579 | self.c.send_method.assert_called_with( 580 | spec.Tx.Select, wait=spec.Tx.SelectOk, 581 | ) 582 | 583 | def test_confirm_select(self): 584 | self.c.confirm_select() 585 | self.c.send_method.assert_called_with( 586 | spec.Confirm.Select, 'b', (False,), 587 | wait=spec.Confirm.SelectOk, 588 | ) 589 | 590 | def test_on_basic_ack(self): 591 | callback = Mock(name='callback') 592 | self.c.events['basic_ack'].add(callback) 593 | self.c._on_basic_ack(123, True) 594 | callback.assert_called_with(123, True) 595 | 596 | def test_on_basic_nack(self): 597 | callback = Mock(name='callback') 598 | self.c.events['basic_nack'].add(callback) 599 | self.c._on_basic_nack(123, True) 600 | callback.assert_called_with(123, True) 601 | -------------------------------------------------------------------------------- /t/unit/test_exceptions.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | 3 | import pytest 4 | 5 | import amqp.exceptions 6 | from amqp.exceptions import AMQPError, error_for_code 7 | 8 | AMQP_EXCEPTIONS = ( 9 | 'ConnectionError', 'ChannelError', 10 | 'RecoverableConnectionError', 'IrrecoverableConnectionError', 11 | 'RecoverableChannelError', 'IrrecoverableChannelError', 12 | 'ConsumerCancelled', 'ContentTooLarge', 'NoConsumers', 13 | 'ConnectionForced', 'InvalidPath', 'AccessRefused', 'NotFound', 14 | 'ResourceLocked', 'PreconditionFailed', 'FrameError', 'FrameSyntaxError', 15 | 'InvalidCommand', 'ChannelNotOpen', 'UnexpectedFrame', 'ResourceError', 16 | 'NotAllowed', 'AMQPNotImplementedError', 'InternalError', 17 | ) 18 | 19 | 20 | class test_AMQPError: 21 | 22 | def test_str(self): 23 | assert str(AMQPError()) == '' 24 | x = AMQPError(method_sig=(50, 60)) 25 | assert str(x) == '(50, 60): (0) None' 26 | x = AMQPError('Test Exception') 27 | assert str(x) == 'Test Exception' 28 | 29 | @pytest.mark.parametrize("amqp_exception", AMQP_EXCEPTIONS) 30 | def test_str_subclass(self, amqp_exception): 31 | exp = f'<{amqp_exception}: unknown error>' 32 | exception_class = getattr(amqp.exceptions, amqp_exception) 33 | assert str(exception_class()) == exp 34 | 35 | 36 | class test_error_for_code: 37 | 38 | def test_unknown_error(self): 39 | default = Mock(name='default') 40 | x = error_for_code(2134214314, 't', 'm', default) 41 | default.assert_called_with('t', 'm', reply_code=2134214314) 42 | assert x is default() 43 | -------------------------------------------------------------------------------- /t/unit/test_method_framing.py: -------------------------------------------------------------------------------- 1 | from struct import pack 2 | from unittest.mock import Mock 3 | 4 | import pytest 5 | 6 | from amqp import spec 7 | from amqp.basic_message import Message 8 | from amqp.exceptions import UnexpectedFrame 9 | from amqp.method_framing import frame_handler, frame_writer 10 | 11 | 12 | class test_frame_handler: 13 | 14 | @pytest.fixture(autouse=True) 15 | def setup_conn(self): 16 | self.conn = Mock(name='connection') 17 | self.conn.bytes_recv = 0 18 | self.callback = Mock(name='callback') 19 | self.g = frame_handler(self.conn, self.callback) 20 | 21 | def test_header(self): 22 | buf = pack('>HH', 60, 51) 23 | assert self.g((1, 1, buf)) 24 | self.callback.assert_called_with(1, (60, 51), buf, None) 25 | assert self.conn.bytes_recv 26 | 27 | def test_header_message_empty_body(self): 28 | assert not self.g((1, 1, pack('>HH', *spec.Basic.Deliver))) 29 | self.callback.assert_not_called() 30 | 31 | with pytest.raises(UnexpectedFrame): 32 | self.g((1, 1, pack('>HH', *spec.Basic.Deliver))) 33 | 34 | m = Message() 35 | m.properties = {} 36 | buf = pack('>HxxQ', m.CLASS_ID, 0) 37 | buf += m._serialize_properties() 38 | assert self.g((2, 1, buf)) 39 | 40 | self.callback.assert_called() 41 | msg = self.callback.call_args[0][3] 42 | self.callback.assert_called_with( 43 | 1, msg.frame_method, msg.frame_args, msg, 44 | ) 45 | 46 | def test_header_message_content(self): 47 | assert not self.g((1, 1, pack('>HH', *spec.Basic.Deliver))) 48 | self.callback.assert_not_called() 49 | 50 | m = Message() 51 | m.properties = {} 52 | buf = pack('>HxxQ', m.CLASS_ID, 16) 53 | buf += m._serialize_properties() 54 | assert not self.g((2, 1, buf)) 55 | self.callback.assert_not_called() 56 | 57 | assert not self.g((3, 1, b'thequick')) 58 | self.callback.assert_not_called() 59 | 60 | assert self.g((3, 1, b'brownfox')) 61 | self.callback.assert_called() 62 | msg = self.callback.call_args[0][3] 63 | self.callback.assert_called_with( 64 | 1, msg.frame_method, msg.frame_args, msg, 65 | ) 66 | assert msg.body == b'thequickbrownfox' 67 | 68 | def test_heartbeat_frame(self): 69 | assert not self.g((8, 1, '')) 70 | self.callback.assert_not_called() 71 | assert self.conn.bytes_recv 72 | 73 | 74 | class test_frame_writer: 75 | 76 | @pytest.fixture(autouse=True) 77 | def setup_conn(self): 78 | self.connection = Mock(name='connection') 79 | self.transport = self.connection.Transport() 80 | self.connection.frame_max = 512 81 | self.connection.bytes_sent = 0 82 | self.g = frame_writer(self.connection, self.transport) 83 | self.write = self.transport.write 84 | 85 | def test_write_fast_header(self): 86 | frame = 1, 1, spec.Queue.Declare, b'x' * 30, None 87 | self.g(*frame) 88 | self.write.assert_called() 89 | 90 | def test_write_fast_content(self): 91 | msg = Message(body=b'y' * 10, content_type='utf-8') 92 | frame = 2, 1, spec.Basic.Publish, b'x' * 10, msg 93 | self.g(*frame) 94 | self.write.assert_called() 95 | assert 'content_encoding' not in msg.properties 96 | 97 | def test_write_slow_content(self): 98 | msg = Message(body=b'y' * 2048, content_type='utf-8') 99 | frame = 2, 1, spec.Basic.Publish, b'x' * 10, msg 100 | self.g(*frame) 101 | self.write.assert_called() 102 | assert 'content_encoding' not in msg.properties 103 | 104 | def test_write_zero_len_body(self): 105 | msg = Message(body=b'', content_type='application/octet-stream') 106 | frame = 2, 1, spec.Basic.Publish, b'x' * 10, msg 107 | self.g(*frame) 108 | self.write.assert_called() 109 | assert 'content_encoding' not in msg.properties 110 | 111 | def test_write_fast_unicode(self): 112 | msg = Message(body='\N{CHECK MARK}') 113 | frame = 2, 1, spec.Basic.Publish, b'x' * 10, msg 114 | self.g(*frame) 115 | self.write.assert_called() 116 | memory = self.write.call_args[0][0] 117 | assert isinstance(memory, memoryview) 118 | assert '\N{CHECK MARK}'.encode() in memory.tobytes() 119 | assert msg.properties['content_encoding'] == 'utf-8' 120 | 121 | def test_write_slow_unicode(self): 122 | msg = Message(body='y' * 2048 + '\N{CHECK MARK}') 123 | frame = 2, 1, spec.Basic.Publish, b'x' * 10, msg 124 | self.g(*frame) 125 | self.write.assert_called() 126 | memory = self.write.call_args[0][0] 127 | assert isinstance(memory, bytes) 128 | assert '\N{CHECK MARK}'.encode() in memory 129 | assert msg.properties['content_encoding'] == 'utf-8' 130 | 131 | def test_write_non_utf8(self): 132 | msg = Message(body='body', content_encoding='utf-16') 133 | frame = 2, 1, spec.Basic.Publish, b'x' * 10, msg 134 | self.g(*frame) 135 | self.write.assert_called() 136 | memory = self.write.call_args[0][0] 137 | assert isinstance(memory, memoryview) 138 | assert 'body'.encode('utf-16') in memory.tobytes() 139 | assert msg.properties['content_encoding'] == 'utf-16' 140 | 141 | def test_write_frame__fast__buffer_store_resize(self): 142 | """The buffer_store is resized when the connection's frame_max is increased.""" 143 | small_msg = Message(body='t') 144 | small_frame = 2, 1, spec.Basic.Publish, b'x' * 10, small_msg 145 | self.g(*small_frame) 146 | self.write.assert_called_once() 147 | write_arg = self.write.call_args[0][0] 148 | assert isinstance(write_arg, memoryview) 149 | assert len(write_arg) < self.connection.frame_max 150 | self.connection.reset_mock() 151 | 152 | # write a larger message to the same frame_writer after increasing frame_max 153 | large_msg = Message(body='t' * (self.connection.frame_max + 10)) 154 | large_frame = 2, 1, spec.Basic.Publish, b'x' * 10, large_msg 155 | original_frame_max = self.connection.frame_max 156 | self.connection.frame_max += 100 157 | self.g(*large_frame) 158 | self.write.assert_called_once() 159 | write_arg = self.write.call_args[0][0] 160 | assert isinstance(write_arg, memoryview) 161 | assert len(write_arg) > original_frame_max 162 | -------------------------------------------------------------------------------- /t/unit/test_platform.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import itertools 3 | import operator 4 | 5 | import pytest 6 | 7 | from amqp.platform import _linux_version_to_tuple 8 | 9 | 10 | def test_struct_argument_type(): 11 | from amqp.exceptions import FrameSyntaxError 12 | FrameSyntaxError() 13 | 14 | 15 | @pytest.mark.parametrize('s,expected', [ 16 | ('3.13.0-46-generic', (3, 13, 0)), 17 | ('3.19.43-1-amd64', (3, 19, 43)), 18 | ('4.4.34+', (4, 4, 34)), 19 | ('4.4.what', (4, 4, 0)), 20 | ('4.what.what', (4, 0, 0)), 21 | ('4.4.0-43-Microsoft', (4, 4, 0)), 22 | ]) 23 | def test_linux_version_to_tuple(s, expected): 24 | assert _linux_version_to_tuple(s) == expected 25 | 26 | 27 | def monkeypatch_platform(monkeypatch, sys_platform, platform_release): 28 | monkeypatch.setattr("sys.platform", sys_platform) 29 | 30 | def release(): 31 | return platform_release 32 | 33 | monkeypatch.setattr("platform.release", release) 34 | 35 | 36 | def test_tcp_opts_change(monkeypatch): 37 | monkeypatch_platform(monkeypatch, 'linux', '2.6.36-1-amd64') 38 | 39 | import amqp.platform 40 | importlib.reload(amqp.platform) 41 | old_linux = amqp.platform.KNOWN_TCP_OPTS 42 | 43 | monkeypatch_platform(monkeypatch, 'linux', '2.6.37-0-41-generic') 44 | importlib.reload(amqp.platform) 45 | new_linux = amqp.platform.KNOWN_TCP_OPTS 46 | 47 | monkeypatch_platform(monkeypatch, 'win32', '7') 48 | importlib.reload(amqp.platform) 49 | win = amqp.platform.KNOWN_TCP_OPTS 50 | 51 | monkeypatch_platform(monkeypatch, 'linux', '4.4.0-43-Microsoft') 52 | importlib.reload(amqp.platform) 53 | win_bash = amqp.platform.KNOWN_TCP_OPTS 54 | 55 | li = [old_linux, new_linux, win, win_bash] 56 | assert all(operator.ne(*i) for i in itertools.combinations(li, 2)) 57 | 58 | assert len(win) <= len(win_bash) < len(old_linux) < len(new_linux) 59 | -------------------------------------------------------------------------------- /t/unit/test_sasl.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import socket 3 | import sys 4 | from io import BytesIO 5 | from unittest.mock import Mock, call, patch 6 | 7 | import pytest 8 | 9 | from amqp import sasl 10 | from amqp.serialization import _write_table 11 | 12 | 13 | class test_SASL: 14 | def test_sasl_notimplemented(self): 15 | mech = sasl.SASL() 16 | with pytest.raises(NotImplementedError): 17 | mech.mechanism 18 | with pytest.raises(NotImplementedError): 19 | mech.start(None) 20 | 21 | def test_plain(self): 22 | username, password = 'foo', 'bar' 23 | mech = sasl.PLAIN(username, password) 24 | response = mech.start(None) 25 | assert isinstance(response, bytes) 26 | assert response.split(b'\0') == \ 27 | [b'', username.encode('utf-8'), password.encode('utf-8')] 28 | 29 | def test_plain_no_password(self): 30 | username, password = 'foo', None 31 | mech = sasl.PLAIN(username, password) 32 | response = mech.start(None) 33 | assert response == NotImplemented 34 | 35 | def test_amqplain(self): 36 | username, password = 'foo', 'bar' 37 | mech = sasl.AMQPLAIN(username, password) 38 | response = mech.start(None) 39 | assert isinstance(response, bytes) 40 | login_response = BytesIO() 41 | _write_table({b'LOGIN': username, b'PASSWORD': password}, 42 | login_response.write, []) 43 | expected_response = login_response.getvalue()[4:] 44 | assert response == expected_response 45 | 46 | def test_amqplain_no_password(self): 47 | username, password = 'foo', None 48 | mech = sasl.AMQPLAIN(username, password) 49 | response = mech.start(None) 50 | assert response == NotImplemented 51 | 52 | def test_gssapi_missing(self): 53 | gssapi = sys.modules.pop('gssapi', None) 54 | GSSAPI = sasl._get_gssapi_mechanism() 55 | with pytest.raises(NotImplementedError): 56 | GSSAPI() 57 | if gssapi is not None: 58 | sys.modules['gssapi'] = gssapi 59 | 60 | @contextlib.contextmanager 61 | def fake_gssapi(self): 62 | orig_gssapi = sys.modules.pop('gssapi', None) 63 | orig_gssapi_raw = sys.modules.pop('gssapi.raw', None) 64 | orig_gssapi_raw_misc = sys.modules.pop('gssapi.raw.misc', None) 65 | gssapi = sys.modules['gssapi'] = Mock() 66 | sys.modules['gssapi.raw'] = gssapi.raw 67 | sys.modules['gssapi.raw.misc'] = gssapi.raw.misc 68 | 69 | class GSSError(Exception): 70 | pass 71 | 72 | gssapi.raw.misc.GSSError = GSSError 73 | try: 74 | yield gssapi 75 | finally: 76 | if orig_gssapi is None: 77 | del sys.modules['gssapi'] 78 | else: 79 | sys.modules['gssapi'] = orig_gssapi 80 | if orig_gssapi_raw is None: 81 | del sys.modules['gssapi.raw'] 82 | else: 83 | sys.modules['gssapi.raw'] = orig_gssapi_raw 84 | if orig_gssapi_raw_misc is None: 85 | del sys.modules['gssapi.raw.misc'] 86 | else: 87 | sys.modules['gssapi.raw.misc'] = orig_gssapi_raw_misc 88 | 89 | def test_gssapi_rdns(self): 90 | with self.fake_gssapi() as gssapi, \ 91 | patch('socket.gethostbyaddr') as gethostbyaddr: 92 | connection = Mock() 93 | connection.transport.sock.getpeername.return_value = ('192.0.2.0', 94 | 5672) 95 | connection.transport.sock.family = socket.AF_INET 96 | gethostbyaddr.return_value = ('broker.example.org', (), ()) 97 | GSSAPI = sasl._get_gssapi_mechanism() 98 | 99 | mech = GSSAPI(rdns=True) 100 | mech.start(connection) 101 | 102 | connection.transport.sock.getpeername.assert_called_with() 103 | gethostbyaddr.assert_called_with('192.0.2.0') 104 | gssapi.Name.assert_called_with(b'amqp@broker.example.org', 105 | gssapi.NameType.hostbased_service) 106 | 107 | def test_gssapi_no_rdns(self): 108 | with self.fake_gssapi() as gssapi: 109 | connection = Mock() 110 | connection.transport.host = 'broker.example.org' 111 | GSSAPI = sasl._get_gssapi_mechanism() 112 | 113 | mech = GSSAPI() 114 | mech.start(connection) 115 | 116 | gssapi.Name.assert_called_with(b'amqp@broker.example.org', 117 | gssapi.NameType.hostbased_service) 118 | 119 | def test_gssapi_step_without_client_name(self): 120 | with self.fake_gssapi() as gssapi: 121 | context = Mock() 122 | context.step.return_value = b'secrets' 123 | name = Mock() 124 | gssapi.SecurityContext.return_value = context 125 | gssapi.Name.return_value = name 126 | connection = Mock() 127 | connection.transport.host = 'broker.example.org' 128 | GSSAPI = sasl._get_gssapi_mechanism() 129 | 130 | mech = GSSAPI() 131 | response = mech.start(connection) 132 | 133 | gssapi.SecurityContext.assert_called_with(name=name, creds=None) 134 | context.step.assert_called_with(None) 135 | assert response == b'secrets' 136 | 137 | def test_gssapi_step_with_client_name(self): 138 | with self.fake_gssapi() as gssapi: 139 | context = Mock() 140 | context.step.return_value = b'secrets' 141 | client_name, service_name, credentials = Mock(), Mock(), Mock() 142 | gssapi.SecurityContext.return_value = context 143 | gssapi.Credentials.return_value = credentials 144 | gssapi.Name.side_effect = [client_name, service_name] 145 | connection = Mock() 146 | connection.transport.host = 'broker.example.org' 147 | GSSAPI = sasl._get_gssapi_mechanism() 148 | 149 | mech = GSSAPI(client_name='amqp-client/client.example.org') 150 | response = mech.start(connection) 151 | gssapi.Name.assert_has_calls([ 152 | call(b'amqp-client/client.example.org'), 153 | call(b'amqp@broker.example.org', 154 | gssapi.NameType.hostbased_service)]) 155 | gssapi.Credentials.assert_called_with(name=client_name) 156 | gssapi.SecurityContext.assert_called_with(name=service_name, 157 | creds=credentials) 158 | context.step.assert_called_with(None) 159 | assert response == b'secrets' 160 | 161 | def test_external(self): 162 | mech = sasl.EXTERNAL() 163 | response = mech.start(None) 164 | assert isinstance(response, bytes) 165 | assert response == b'' 166 | -------------------------------------------------------------------------------- /t/unit/test_serialization.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | from datetime import datetime 3 | from decimal import Decimal 4 | from math import ceil 5 | from struct import pack 6 | 7 | import pytest 8 | 9 | from amqp.basic_message import Message 10 | from amqp.exceptions import FrameSyntaxError 11 | from amqp.serialization import GenericContent, _read_item, dumps, loads 12 | 13 | 14 | class _ANY: 15 | 16 | def __eq__(self, other): 17 | return other is not None 18 | 19 | def __ne__(self, other): 20 | return other is None 21 | 22 | 23 | class test_serialization: 24 | 25 | @pytest.mark.parametrize('descr,frame,expected,cast', [ 26 | ('S', b's8thequick', 'thequick', None), 27 | ('S', b'S\x00\x00\x00\x03\xc0\xc0\x00', b'\xc0\xc0\x00', None), 28 | ('x', b'x\x00\x00\x00\x09thequick\xffIGNORED', b'thequick\xff', None), 29 | ('b', b'b' + pack('>B', True), True, None), 30 | ('B', b'B' + pack('>b', 123), 123, None), 31 | ('U', b'U' + pack('>h', -321), -321, None), 32 | ('u', b'u' + pack('>H', 321), 321, None), 33 | ('i', b'i' + pack('>I', 1234), 1234, None), 34 | ('L', b'L' + pack('>q', -32451), -32451, None), 35 | ('l', b'l' + pack('>Q', 32451), 32451, None), 36 | ('f', b'f' + pack('>f', 33.3), 34.0, ceil), 37 | ]) 38 | def test_read_item(self, descr, frame, expected, cast): 39 | actual = _read_item(frame, 0)[0] 40 | actual = cast(actual) if cast else actual 41 | assert actual == expected 42 | 43 | def test_read_item_V(self): 44 | assert _read_item(b'V', 0)[0] is None 45 | 46 | def test_roundtrip(self): 47 | format = b'bobBlLbsbSTx' 48 | x = dumps(format, [ 49 | True, 32, False, 3415, 4513134, 13241923419, 50 | True, b'thequickbrownfox', False, 'jumpsoverthelazydog', 51 | datetime(2015, 3, 13, 10, 23), 52 | b'thequick\xff' 53 | ]) 54 | y = loads(format, x, 0) 55 | assert [ 56 | True, 32, False, 3415, 4513134, 13241923419, 57 | True, 'thequickbrownfox', False, 'jumpsoverthelazydog', 58 | datetime(2015, 3, 13, 10, 23), b'thequick\xff' 59 | ] == y[0] 60 | 61 | def test_int_boundaries(self): 62 | format = b'F' 63 | x = dumps(format, [ 64 | {'a': -2147483649, 'b': 2147483648}, # celery/celery#3121 65 | ]) 66 | y = loads(format, x, 0) 67 | assert y[0] == [{ 68 | 'a': -2147483649, 'b': 2147483648, # celery/celery#3121 69 | }] 70 | 71 | def test_loads_unknown_type(self): 72 | with pytest.raises(FrameSyntaxError): 73 | loads('y', 'asdsad', 0) 74 | 75 | def test_float(self): 76 | data = int(loads(b'fb', dumps(b'fb', [32.31, False]), 0)[0][0] * 100) 77 | assert(data == 3231) 78 | 79 | def test_table(self): 80 | table = { 81 | 'foo': 32, 82 | 'bar': 'baz', 83 | 'nil': None, 84 | 'array': [ 85 | 1, True, 'bar' 86 | ] 87 | } 88 | assert loads(b'F', dumps(b'F', [table]), 0)[0][0] == table 89 | 90 | def test_table__unknown_type(self): 91 | table = { 92 | 'foo': object(), 93 | 'bar': 'baz', 94 | 'nil': None, 95 | 'array': [ 96 | 1, True, 'bar' 97 | ] 98 | } 99 | with pytest.raises(FrameSyntaxError): 100 | dumps(b'F', [table]) 101 | 102 | def test_array(self): 103 | array = [ 104 | 'A', 1, True, 33.3, 105 | Decimal('55.5'), Decimal('-3.4'), 106 | datetime(2015, 3, 13, 10, 23), 107 | {'quick': 'fox', 'amount': 1}, 108 | [3, 'hens'], 109 | None, 110 | ] 111 | expected = list(array) 112 | expected[6] = _ANY() 113 | 114 | assert expected == loads('A', dumps('A', [array]), 0)[0][0] 115 | 116 | def test_array_unknown_type(self): 117 | with pytest.raises(FrameSyntaxError): 118 | dumps('A', [[object()]]) 119 | 120 | def test_bit_offset_adjusted_correctly(self): 121 | expected = [50, "quick", "fox", True, 122 | False, False, True, True, {"prop1": True}] 123 | buf = dumps('BssbbbbbF', expected) 124 | actual, _ = loads('BssbbbbbF', buf, 0) 125 | assert actual == expected 126 | 127 | def test_sixteen_bitflags(self): 128 | expected = [True, False] * 8 129 | format = 'b' * len(expected) 130 | buf = dumps(format, expected) 131 | actual, _ = loads(format, buf, 0) 132 | assert actual == expected 133 | 134 | 135 | class test_GenericContent: 136 | 137 | @pytest.fixture(autouse=True) 138 | def setup_content(self): 139 | self.g = GenericContent() 140 | 141 | def test_getattr(self): 142 | self.g.properties['foo'] = 30 143 | assert self.g.foo == 30 144 | with pytest.raises(AttributeError): 145 | self.g.bar 146 | 147 | def test_pickle(self): 148 | pickle.loads(pickle.dumps(self.g)) 149 | 150 | def test_load_properties(self): 151 | m = Message() 152 | m.properties = { 153 | 'content_type': 'application/json', 154 | 'content_encoding': 'utf-8', 155 | 'application_headers': { 156 | 'foo': 1, 157 | 'id': 'id#1', 158 | }, 159 | 'delivery_mode': 1, 160 | 'priority': 255, 161 | 'correlation_id': 'df31-142f-34fd-g42d', 162 | 'reply_to': 'cosmo', 163 | 'expiration': '2015-12-23', 164 | 'message_id': '3312', 165 | 'timestamp': 3912491234, 166 | 'type': 'generic', 167 | 'user_id': 'george', 168 | 'app_id': 'vandelay', 169 | 'cluster_id': 'NYC', 170 | } 171 | s = m._serialize_properties() 172 | m2 = Message() 173 | m2._load_properties(m2.CLASS_ID, s, 0) 174 | assert m2.properties == m.properties 175 | 176 | def test_load_properties__some_missing(self): 177 | m = Message() 178 | m.properties = { 179 | 'content_type': 'application/json', 180 | 'content_encoding': 'utf-8', 181 | 'delivery_mode': 1, 182 | 'correlation_id': 'df31-142f-34fd-g42d', 183 | 'reply_to': 'cosmo', 184 | 'expiration': '2015-12-23', 185 | 'message_id': '3312', 186 | 'type': None, 187 | 'app_id': None, 188 | 'cluster_id': None, 189 | } 190 | s = m._serialize_properties() 191 | m2 = Message() 192 | m2._load_properties(m2.CLASS_ID, s, 0) 193 | 194 | def test_inbound_header(self): 195 | m = Message() 196 | m.properties = { 197 | 'content_type': 'application/json', 198 | 'content_encoding': 'utf-8', 199 | } 200 | body = 'the quick brown fox' 201 | buf = b'\0' * 30 + pack('>HxxQ', m.CLASS_ID, len(body)) 202 | buf += m._serialize_properties() 203 | assert m.inbound_header(buf, offset=30) == 42 204 | assert m.body_size == len(body) 205 | assert m.properties['content_type'] == 'application/json' 206 | assert not m.ready 207 | 208 | def test_inbound_header__empty_body(self): 209 | m = Message() 210 | m.properties = {} 211 | buf = pack('>HxxQ', m.CLASS_ID, 0) 212 | buf += m._serialize_properties() 213 | assert m.inbound_header(buf, offset=0) == 12 214 | assert m.ready 215 | 216 | def test_inbound_body(self): 217 | m = Message() 218 | m.body_size = 16 219 | m.body_received = 8 220 | m._pending_chunks = [b'the', b'quick'] 221 | m.inbound_body(b'brown') 222 | assert not m.ready 223 | m.inbound_body(b'fox') 224 | assert m.ready 225 | assert m.body == b'thequickbrownfox' 226 | 227 | def test_inbound_body__no_chunks(self): 228 | m = Message() 229 | m.body_size = 16 230 | m.inbound_body('thequickbrownfox') 231 | assert m.ready 232 | -------------------------------------------------------------------------------- /t/unit/test_utils.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock, patch 2 | 3 | from amqp.utils import bytes_to_str, coro, get_logger, str_to_bytes 4 | 5 | 6 | class test_coro: 7 | 8 | def test_advances(self): 9 | @coro 10 | def x(): 11 | yield 1 12 | yield 2 13 | it = x() 14 | assert next(it) == 2 15 | 16 | 17 | class test_str_to_bytes: 18 | 19 | def test_from_unicode(self): 20 | assert isinstance(str_to_bytes('foo'), bytes) 21 | 22 | def test_from_bytes(self): 23 | assert isinstance(str_to_bytes(b'foo'), bytes) 24 | 25 | def test_supports_surrogates(self): 26 | bytes_with_surrogates = '\ud83d\ude4f'.encode('utf-8', 'surrogatepass') 27 | assert str_to_bytes('\ud83d\ude4f') == bytes_with_surrogates 28 | 29 | 30 | class test_bytes_to_str: 31 | 32 | def test_from_unicode(self): 33 | assert isinstance(bytes_to_str('foo'), str) 34 | 35 | def test_from_bytes(self): 36 | assert bytes_to_str(b'foo') 37 | 38 | def test_support_surrogates(self): 39 | assert bytes_to_str('\ud83d\ude4f') == '\ud83d\ude4f' 40 | 41 | 42 | class test_get_logger: 43 | 44 | def test_as_str(self): 45 | with patch('logging.getLogger') as getLogger: 46 | x = get_logger('foo.bar') 47 | getLogger.assert_called_with('foo.bar') 48 | assert x is getLogger() 49 | 50 | def test_as_logger(self): 51 | with patch('amqp.utils.NullHandler') as _NullHandler: 52 | m = Mock(name='logger') 53 | m.handlers = None 54 | x = get_logger(m) 55 | assert x is m 56 | x.addHandler.assert_called_with(_NullHandler()) 57 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | {3.8,3.9,3.10,3.11,3.12,3.13,pypy3}-unit 4 | {3.8,3.9,3.10,3.11,3.12,3.13,pypy3}-integration-rabbitmq 5 | flake8 6 | apicheck 7 | pydocstyle 8 | requires = 9 | tox-docker<=4.1 10 | requests<=2.31.0 11 | 12 | [testenv] 13 | deps= 14 | -r{toxinidir}/requirements/default.txt 15 | -r{toxinidir}/requirements/test.txt 16 | -r{toxinidir}/requirements/test-ci.txt 17 | 18 | apicheck,linkcheck: -r{toxinidir}/requirements/docs.txt 19 | flake8,pydocstyle: -r{toxinidir}/requirements/pkgutils.txt 20 | sitepackages = False 21 | recreate = False 22 | commands = 23 | unit: py.test -xv --cov=amqp --cov-report=xml --no-cov-on-fail t/unit {posargs} 24 | integration: py.test -xv -E rabbitmq t/integration {posargs:-n2} 25 | basepython = 26 | flake8,apicheck,linkcheck,pydocstyle: python3.13 27 | pypy3: pypy3.10 28 | 3.8: python3.8 29 | 3.9: python3.9 30 | 3.10: python3.10 31 | 3.11: python3.11 32 | 3.12: python3.12 33 | 3.13: python3.13 34 | install_command = python -m pip --disable-pip-version-check install {opts} {packages} 35 | allowlist_externals = * 36 | commands_pre = 37 | integration-rabbitmq: ./wait_for_rabbitmq.sh 38 | docker = 39 | integration-rabbitmq: rabbitmq 40 | dockerenv = 41 | PYAMQP_INTEGRATION_INSTANCE=1 42 | RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS=-rabbit tcp_listeners [5672] 43 | 44 | [docker:rabbitmq] 45 | image = rabbitmq:tls 46 | ports = 47 | 5672:5672/tcp 48 | 5671:5671/tcp 49 | healthcheck_cmd = /bin/bash -c 'rabbitmq-diagnostics ping -q' 50 | healthcheck_interval = 10 51 | healthcheck_timeout = 10 52 | healthcheck_retries = 30 53 | healthcheck_start_period = 5 54 | 55 | [testenv:apicheck] 56 | commands = 57 | sphinx-build -b apicheck -d {envtmpdir}/doctrees docs docs/_build/apicheck 58 | 59 | [testenv:linkcheck] 60 | commands = 61 | sphinx-build -W -b linkcheck -d {envtmpdir}/doctrees docs docs/_build/linkcheck 62 | 63 | [testenv:flake8] 64 | commands = 65 | flake8 {toxinidir}/amqp {toxinidir}/t 66 | 67 | [testenv:pydocstyle] 68 | commands = 69 | pydocstyle {toxinidir}/amqp 70 | -------------------------------------------------------------------------------- /wait_for_rabbitmq.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | containers=$(sudo docker ps -q | tail -n +1) 4 | 5 | for item in ${containers//\\n/} 6 | do 7 | env=$(sudo docker inspect -f '{{range $index, $value := .Config.Env}}{{$value}} {{end}}' $item); 8 | if [[ $env == *"PYAMQP_INTEGRATION_INSTANCE=1"* ]]; then 9 | grep -m1 'Server startup complete' <(sudo docker logs -f $item) 10 | sudo docker exec $item rabbitmqctl add_vhost gw0 11 | sudo docker exec $item rabbitmqctl set_permissions -p gw0 guest ".*" ".*" ".*" 12 | sudo docker exec $item rabbitmqctl add_vhost gw1 13 | sudo docker exec $item rabbitmqctl set_permissions -p gw1 guest ".*" ".*" ".*" 14 | fi 15 | done; 16 | --------------------------------------------------------------------------------