├── pamqp ├── py.typed ├── __init__.py ├── heartbeat.py ├── body.py ├── constants.py ├── common.py ├── header.py ├── exceptions.py ├── frame.py ├── base.py ├── encode.py └── decode.py ├── docs ├── genindex.rst ├── requirements.txt ├── common.rst ├── frame.rst ├── decode.rst ├── encode.rst ├── header.rst ├── heartbeat.rst ├── body.rst ├── commands.rst ├── base.rst ├── exceptions.rst ├── _static │ └── css │ │ └── custom.css ├── conf.py ├── index.rst ├── Makefile └── changelog.rst ├── setup.py ├── MANIFEST.in ├── .readthedocs.yaml ├── .gitignore ├── .codeclimate.yml ├── .editorconfig ├── CONTRIBUTING.md ├── .github └── workflows │ ├── deploy.yaml │ └── testing.yaml ├── tests ├── test_tag_uri_scheme.py ├── test_encode_decode.py ├── test_frame_unmarshaling_errors.py ├── test_frame_marshaling.py ├── test_command_argument_errors.py ├── test_encoding.py └── test_decoding.py ├── LICENSE ├── setup.cfg ├── README.rst └── codegen └── extensions.xml /pamqp/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/genindex.rst: -------------------------------------------------------------------------------- 1 | Index 2 | ===== 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | setuptools.setup() 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | include pamqp/py.typed 4 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx==2.4.4 2 | sphinx-autodoc-typehints 3 | sphinx-material==0.0.23 4 | typed_ast 5 | -------------------------------------------------------------------------------- /docs/common.rst: -------------------------------------------------------------------------------- 1 | pamqp.common 2 | ============ 3 | .. automodule:: pamqp.common 4 | :members: 5 | :undoc-members: 6 | -------------------------------------------------------------------------------- /docs/frame.rst: -------------------------------------------------------------------------------- 1 | pamqp.frame 2 | =========== 3 | .. automodule:: pamqp.frame 4 | :members: 5 | :member-order: bysource 6 | -------------------------------------------------------------------------------- /docs/decode.rst: -------------------------------------------------------------------------------- 1 | pamqp.decode 2 | ============ 3 | .. automodule:: pamqp.decode 4 | :members: 5 | :member-order: bysource 6 | -------------------------------------------------------------------------------- /docs/encode.rst: -------------------------------------------------------------------------------- 1 | pamqp.encode 2 | ============ 3 | .. automodule:: pamqp.encode 4 | :members: 5 | :member-order: bysource 6 | -------------------------------------------------------------------------------- /docs/header.rst: -------------------------------------------------------------------------------- 1 | pamqp.header 2 | ============ 3 | .. automodule:: pamqp.header 4 | :members: 5 | :member-order: bysource 6 | 7 | -------------------------------------------------------------------------------- /docs/heartbeat.rst: -------------------------------------------------------------------------------- 1 | pamqp.heartbeat 2 | =============== 3 | .. automodule:: pamqp.heartbeat 4 | :members: 5 | :member-order: bysource 6 | -------------------------------------------------------------------------------- /docs/body.rst: -------------------------------------------------------------------------------- 1 | pamqp.body 2 | ========== 3 | .. automodule:: pamqp.body 4 | :members: 5 | :special-members: 6 | :member-order: bysource 7 | -------------------------------------------------------------------------------- /docs/commands.rst: -------------------------------------------------------------------------------- 1 | pamqp.commands 2 | ============== 3 | 4 | .. automodule:: pamqp.commands 5 | :members: 6 | :special-members: 7 | :member-order: bysource 8 | -------------------------------------------------------------------------------- /docs/base.rst: -------------------------------------------------------------------------------- 1 | pamqp.base 2 | ========== 3 | 4 | .. automodule:: pamqp.base 5 | :members: 6 | :special-members: 7 | :inherited-members: 8 | :member-order: bysource 9 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | build: 3 | os: ubuntu-22.04 4 | tools: 5 | python: "3.12" 6 | sphinx: 7 | configuration: docs/conf.py 8 | python: 9 | install: 10 | - requirements: docs/requirements.txt 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .mypy_cache 3 | .vscode 4 | codegen/amqp-rabbitmq-0.9.1.json 5 | codegen/amqp0-9-1.xml 6 | *.pyc 7 | build 8 | dist 9 | docs/_build 10 | atlassian-ide-plugin.xml 11 | .DS_Store 12 | pamqp.egg-info 13 | env 14 | env2.7 15 | env3 16 | .coverage 17 | coverage.xml 18 | xunit.xml 19 | coverage 20 | -------------------------------------------------------------------------------- /pamqp/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """AMQP Specifications and Classes""" 3 | __author__ = 'Gavin M. Roy' 4 | __email__ = 'gavinmroy@gmail.com' 5 | __since__ = '2011-09-23' 6 | __version__ = version = '3.3.0' 7 | 8 | __all__ = [ 9 | 'body', 'decode', 'commands', 'constants', 'encode', 'exceptions', 'frame', 10 | 'header', 'heartbeat' 11 | ] 12 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | languages: 2 | Python: true 3 | exclude_paths: 4 | - codegen/* 5 | - docs/* 6 | - tests/* 7 | - tools/* 8 | checks: 9 | argument-count: 10 | enabled: false 11 | file-lines: 12 | enabled: false 13 | similar-code: 14 | enabled: false 15 | method-complexity: 16 | config: 17 | threshold: 10 18 | return-statements: 19 | config: 20 | threshold: 10 21 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | 9 | # 4 space indentation 10 | [*.py] 11 | indent_style = space 12 | indent_size = 4 13 | 14 | # 2 space indentation 15 | [*.yml] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [bootstrap] 20 | indent_style = space 21 | indent_size = 2 22 | -------------------------------------------------------------------------------- /docs/exceptions.rst: -------------------------------------------------------------------------------- 1 | pamqp.exceptions 2 | ================ 3 | 4 | The :py:mod:`pamqp.exceptions` module is auto-generated, created by the ``tools/codegen.py`` application. 5 | 6 | :py:mod:`pamqp.exceptions` implements AMQP exceptions as Python exceptions so that client libraries can raise these exceptions as is appropriate without having to implement their own extensions for AMQP protocol related issues. 7 | 8 | .. automodule:: pamqp.exceptions 9 | :members: 10 | :member-order: bysource 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | To get setup in the environment and run the tests, take the following steps: 4 | 5 | ```sh 6 | python3 -m venv env 7 | source env/bin/activate 8 | pip install -e '.[testing]' 9 | 10 | flake8 11 | coverage run && coverage report 12 | ``` 13 | 14 | Please format your code contributions with the ``yapf`` formatter: 15 | 16 | ```sh 17 | yapf -i --recursive --style=pep8 pamqp 18 | ``` 19 | 20 | ## Test Coverage 21 | 22 | Pull requests that make changes or additions that are not covered by tests 23 | will likely be closed without review. 24 | -------------------------------------------------------------------------------- /pamqp/heartbeat.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | AMQP Heartbeat Frame, used to create new Heartbeat frames for sending to a peer 4 | 5 | """ 6 | import struct 7 | 8 | from pamqp import constants 9 | 10 | 11 | class Heartbeat(object): 12 | """Heartbeat frame object mapping class. AMQP Heartbeat frames are mapped 13 | on to this class for a common access structure to the attributes/data 14 | values. 15 | 16 | """ 17 | name: str = 'Heartbeat' 18 | value = struct.pack('>BHI', constants.FRAME_HEARTBEAT, 0, 0) + \ 19 | constants.FRAME_END_CHAR 20 | 21 | @classmethod 22 | def marshal(cls) -> bytes: 23 | """Return the binary frame content""" 24 | return cls.value 25 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Deployment 2 | on: 3 | push: 4 | branches-ignore: ["*"] 5 | tags: ["*"] 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-latest 9 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') && github.repository == 'gmr/pamqp' 10 | container: python:3.10-alpine 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v1 14 | - name: Install wheel 15 | run: pip3 install wheel 16 | - name: Build package 17 | run: python3 setup.py sdist bdist_wheel 18 | - name: Publish package 19 | uses: pypa/gh-action-pypi-publish@master 20 | with: 21 | user: __token__ 22 | password: ${{ secrets.PYPI_PASSWORD }} 23 | -------------------------------------------------------------------------------- /docs/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | .md-content { 2 | margin-right: 2em; 3 | } 4 | .md-main__inner { 5 | padding: 0; 6 | } 7 | .navheader { 8 | padding-left: 1em; 9 | } 10 | .md-sidebar--secondary { 11 | display: none; 12 | } 13 | .viewcode-link { 14 | font-size: .75em; 15 | margin-left: .5em; 16 | } 17 | .caption-text { 18 | font-style: oblique; 19 | } 20 | code.descname { 21 | background: transparent; 22 | box-shadow: none; 23 | font-size: 100% !important; 24 | } 25 | code.descname { 26 | background: transparent; 27 | box-shadow: none; 28 | font-size: 100% !important; 29 | font-weight: bold; 30 | } 31 | code.descclassname { 32 | background: transparent; 33 | box-shadow: none; 34 | font-size: 100% !important; 35 | font-weight: bold; 36 | } 37 | .rst-versions { 38 | font-size: 1.75em; 39 | } 40 | -------------------------------------------------------------------------------- /tests/test_tag_uri_scheme.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pamqp import commands 4 | 5 | # Tag uri examples from https://en.wikipedia.org/wiki/Tag_URI_scheme 6 | TAG_URIS = [ 7 | 'tag:timothy@hpl.hp.com,2001:web/externalHome', 8 | 'tag:sandro@w3.org,2004-05:Sandro', 9 | 'tag:my-ids.com,2001-09-15:TimKindberg:presentations:UBath2004-05-19', 10 | 'tag:blogger.com,1999:blog-555', 11 | 'tag:yaml.org,2002:int#section1' 12 | ] 13 | 14 | 15 | class TagUriScheme(unittest.TestCase): 16 | 17 | def test_tag_uri_scheme_tag1(self): 18 | commands.Exchange.Declare(exchange=TAG_URIS[0]) 19 | 20 | def test_tag_uri_scheme_tag2(self): 21 | commands.Exchange.Declare(exchange=TAG_URIS[1]) 22 | 23 | def test_tag_uri_scheme_tag3(self): 24 | commands.Exchange.Declare(exchange=TAG_URIS[2]) 25 | 26 | def test_tag_uri_scheme_tag4(self): 27 | commands.Exchange.Declare(exchange=TAG_URIS[3]) 28 | -------------------------------------------------------------------------------- /pamqp/body.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | The :py:mod:`pamqp.body` module contains the :py:class:`Body` class which is 4 | used when unmarshalling body frames. When dealing with content frames, the 5 | message body will be returned from the library as an instance of the body 6 | class. 7 | 8 | """ 9 | 10 | 11 | class ContentBody: 12 | """ContentBody carries the value for an AMQP message body frame 13 | 14 | :param value: The value for the ContentBody frame 15 | 16 | """ 17 | name = 'ContentBody' 18 | 19 | def __init__(self, value: bytes): 20 | """Create a new instance of a ContentBody object""" 21 | self.value = value 22 | 23 | def __len__(self) -> int: 24 | """Return the length of the content body value""" 25 | return len(self.value) if self.value else 0 26 | 27 | def marshal(self) -> bytes: 28 | """Return the marshaled content body. This method is here for API 29 | compatibility, there is no special marshaling for the payload in a 30 | content frame. 31 | 32 | """ 33 | return self.value 34 | 35 | def unmarshal(self, data: bytes) -> None: 36 | """Apply the data to the object. This method is here for API 37 | compatibility, there is no special unmarshalling for the payload in a 38 | content frame. 39 | 40 | :param data: The content body data from the frame 41 | 42 | """ 43 | self.value = data 44 | -------------------------------------------------------------------------------- /.github/workflows/testing.yaml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | on: 3 | push: 4 | branches: ["*"] 5 | paths-ignore: 6 | - 'docs/**' 7 | - '*.md' 8 | - '*.rst' 9 | tags-ignore: ["*"] 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python: 16 | - "3.7" 17 | - "3.8" 18 | - "3.9" 19 | - "3.10" 20 | - "3.11" 21 | - "3.12" 22 | container: 23 | image: python:${{ matrix.python }}-alpine 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@v1 27 | 28 | - name: Setup environment 29 | run: apk --update add gcc libpq make musl-dev linux-headers alpine-conf 30 | 31 | - name: Set the timezone 32 | run: setup-timezone -z America/New_York 33 | 34 | - name: Install testing dependencies 35 | run: pip3 install -e '.[testing]' 36 | 37 | - name: Create build directory 38 | run: mkdir build 39 | 40 | - name: Run flake8 tests 41 | run: flake8 --output build/flake8.txt --tee 42 | 43 | - name: Run tests 44 | run: coverage run && coverage report && coverage xml 45 | 46 | - name: Upload Coverage 47 | uses: codecov/codecov-action@v1.0.2 48 | if: github.event_name == 'push' && github.repository == 'gmr/pamqp' 49 | with: 50 | token: ${{secrets.CODECOV_TOKEN}} 51 | file: build/coverage.xml 52 | flags: unittests 53 | -------------------------------------------------------------------------------- /tests/test_encode_decode.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | import datetime 3 | import unittest 4 | 5 | from pamqp import decode, encode 6 | 7 | 8 | class EncodeDecodeTests(unittest.TestCase): 9 | 10 | def test_encode_decode_field_table_long_keys(self): 11 | """Encoding and decoding a field_table with too long keys.""" 12 | # second key is 126 A's + \N{PILE OF POO} 13 | data = {'A' * 256: 1, 14 | ((b'A' * 128) + b'\xf0\x9f\x92\xa9').decode('utf-8'): 2} 15 | encoded = encode.field_table(data) 16 | decoded = decode.field_table(encoded)[1] 17 | self.assertIn('A' * 128, decoded) 18 | 19 | def test_timestamp_with_dst(self): 20 | # this test assumes the system is set up using a northern hemisphere 21 | # timesone with DST (America/New_York as per github CI is fine) 22 | data = datetime.datetime(2006, 5, 21, 16, 30, 10, 23 | tzinfo=datetime.timezone.utc) 24 | encoded = encode.timestamp(data) 25 | decoded = decode.timestamp(encoded)[1] 26 | self.assertEqual(decoded, data) 27 | 28 | def test_timestamp_without_timezone(self): 29 | naive = datetime.datetime(2006, 5, 21, 16, 30, 10) 30 | aware = datetime.datetime(2006, 5, 21, 16, 30, 10, 31 | tzinfo=datetime.timezone.utc) 32 | encoded = encode.timestamp(naive) 33 | decoded = decode.timestamp(encoded)[1] 34 | self.assertEqual(decoded, aware) 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2022 Gavin M. Roy 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | * Neither the name of the copyright holder nor the names of its contributors may 13 | be used to endorse or promote products derived from this software without 14 | specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 19 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 20 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 23 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 24 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 25 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import pkg_resources 4 | import sphinx_material 5 | 6 | html_theme = 'sphinx_material' 7 | html_theme_path = sphinx_material.html_theme_path() 8 | html_context = sphinx_material.get_html_context() 9 | html_sidebars = { 10 | "**": ["globaltoc.html", "searchbox.html"] 11 | } 12 | html_theme_options = { 13 | 'base_url': 'http://pamqp.readthedocs.io', 14 | 'repo_url': 'https://github.com/gmr/pamqp/', 15 | 'repo_name': 'pamqp', 16 | 'html_minify': True, 17 | 'css_minify': True, 18 | 'nav_title': 'pamqp', 19 | 'globaltoc_depth': 1, 20 | 'theme_color': 'fc6600', 21 | 'color_primary': 'grey', 22 | 'color_accent': 'orange', 23 | 'version_dropdown': False 24 | } 25 | html_static_path = ['_static'] 26 | html_css_files = [ 27 | 'css/custom.css' 28 | ] 29 | 30 | master_doc = 'index' 31 | project = 'pamqp' 32 | release = version = pkg_resources.get_distribution(project).version 33 | copyright = '2011-{}, Gavin M. Roy'.format(datetime.date.today().year) 34 | 35 | extensions = [ 36 | 'sphinx.ext.autodoc', 37 | 'sphinx_autodoc_typehints', 38 | 'sphinx.ext.intersphinx', 39 | 'sphinx.ext.viewcode', 40 | 'sphinx_material' 41 | ] 42 | 43 | set_type_checking_flag = True 44 | typehints_fully_qualified = True 45 | always_document_param_types = True 46 | typehints_document_rtype = True 47 | 48 | templates_path = ['_templates'] 49 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 50 | intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} 51 | 52 | autodoc_default_options = { 53 | 'autodoc_typehints': 'description', 54 | 'special-members': ('__contains__,__eq__,__getitem__,' 55 | '__iter__,__len__'), 56 | } 57 | -------------------------------------------------------------------------------- /pamqp/constants.py: -------------------------------------------------------------------------------- 1 | # Auto-generated, do not edit this file. 2 | import re 3 | 4 | # AMQP Protocol Frame Prefix 5 | AMQP = b'AMQP' 6 | 7 | # AMQP Protocol Version 8 | VERSION = (0, 9, 1) 9 | 10 | # RabbitMQ Defaults 11 | DEFAULT_HOST = 'localhost' 12 | DEFAULT_PORT = 5672 13 | DEFAULT_USER = 'guest' 14 | DEFAULT_PASS = 'guest' 15 | DEFAULT_VHOST = '/' 16 | 17 | # AMQP Constants 18 | FRAME_METHOD = 1 19 | FRAME_HEADER = 2 20 | FRAME_BODY = 3 21 | FRAME_HEARTBEAT = 8 22 | FRAME_MIN_SIZE = 4096 23 | FRAME_END = 206 24 | # Indicates that the method completed successfully. This reply code is reserved 25 | # for future use - the current protocol design does not use positive 26 | # confirmation and reply codes are sent only in case of an error. 27 | REPLY_SUCCESS = 200 28 | 29 | # Not included in the spec XML or JSON files. 30 | FRAME_END_CHAR = b'\xce' 31 | FRAME_HEADER_SIZE = 7 32 | FRAME_MAX_SIZE = 131072 33 | 34 | # AMQP data types 35 | DATA_TYPES = [ 36 | 'bit', # single bit 37 | 'long', # 32-bit integer 38 | 'longlong', # 64-bit integer 39 | 'longstr', # long string 40 | 'octet', # single octet 41 | 'short', # 16-bit integer 42 | 'shortstr', # short string (max. 256 characters) 43 | 'table', # field table 44 | 'timestamp' # 64-bit timestamp 45 | ] 46 | 47 | # AMQP domains 48 | DOMAINS = { 49 | 'channel-id': 'longstr', 50 | 'class-id': 'short', 51 | 'consumer-tag': 'shortstr', 52 | 'delivery-tag': 'longlong', 53 | 'destination': 'shortstr', 54 | 'duration': 'longlong', 55 | 'exchange-name': 'shortstr', 56 | 'method-id': 'short', 57 | 'no-ack': 'bit', 58 | 'no-local': 'bit', 59 | 'offset': 'longlong', 60 | 'path': 'shortstr', 61 | 'peer-properties': 'table', 62 | 'queue-name': 'shortstr', 63 | 'redelivered': 'bit', 64 | 'reference': 'longstr', 65 | 'reject-code': 'short', 66 | 'reject-text': 'shortstr', 67 | 'reply-code': 'short', 68 | 'reply-text': 'shortstr', 69 | 'security-token': 'longstr' 70 | } 71 | 72 | # AMQP domain patterns 73 | DOMAIN_REGEX = { 74 | 'exchange-name': re.compile(r'^[a-zA-Z0-9-_.:@#,/+ ]*$'), 75 | 'queue-name': re.compile(r'^[a-zA-Z0-9-_.:@#,/+ ]*$') 76 | } 77 | 78 | # Other constants 79 | DEPRECATION_WARNING = 'This command is deprecated in AMQP 0-9-1' 80 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = pamqp 3 | version = attr: pamqp.__version__ 4 | description = RabbitMQ Focused AMQP low-level library 5 | long_description = file: README.rst 6 | long_description_content_type = text/x-rst; charset=UTF-8 7 | license = BSD 3-Clause License 8 | license-file = LICENSE 9 | home-page = https://github.com/gmr/pamqp 10 | project_urls = 11 | Bug Tracker = https://github.com/gmr/pamqp/issues 12 | Documentation = https://pamqp.readthedocs.io 13 | Source Code = https://github.com/gmr/pamqp/ 14 | author = Gavin M. Roy 15 | author_email = gavinmroy@gmail.com 16 | classifiers = 17 | Development Status :: 5 - Production/Stable 18 | Intended Audience :: Developers 19 | License :: OSI Approved :: BSD License 20 | Natural Language :: English 21 | Operating System :: OS Independent 22 | Programming Language :: Python :: 3 23 | Programming Language :: Python :: 3.7 24 | Programming Language :: Python :: 3.8 25 | Programming Language :: Python :: 3.9 26 | Programming Language :: Python :: 3.10 27 | Programming Language :: Python :: 3.11 28 | Programming Language :: Python :: 3.12 29 | Programming Language :: Python :: 3 :: Only 30 | Topic :: Communications 31 | Topic :: Internet 32 | Topic :: Software Development 33 | Typing :: Typed 34 | requires-dist = setuptools 35 | keywords = 36 | amqp 37 | rabbitmq 38 | 39 | [options] 40 | include_package_data = True 41 | packages = 42 | pamqp 43 | python_requires = >=3.7 44 | zip_safe = true 45 | 46 | [options.extras_require] 47 | codegen = 48 | lxml 49 | requests 50 | yapf 51 | testing = 52 | coverage 53 | flake8 54 | flake8-comprehensions 55 | flake8-deprecated 56 | flake8-import-order 57 | flake8-print 58 | flake8-quotes 59 | flake8-rst-docstrings 60 | flake8-tuple 61 | yapf 62 | 63 | [bdist_wheel] 64 | universal = 1 65 | 66 | [build_sphinx] 67 | all-files = 1 68 | 69 | [coverage:run] 70 | branch = True 71 | command_line = -m unittest discover tests --verbose 72 | data_file = build/.coverage 73 | 74 | [coverage:report] 75 | show_missing = True 76 | include = 77 | pamqp/* 78 | omit = 79 | tests/*.py 80 | 81 | [coverage:html] 82 | directory = build/coverage 83 | 84 | [coverage:xml] 85 | output = build/coverage.xml 86 | 87 | [flake8] 88 | application-import-names = pamqp 89 | exclude = bak,build,docs,env,tools 90 | import-order-style = google 91 | ignore = RST306 92 | rst-directives = deprecated 93 | rst-roles = attr,class,const,data,exc,func,meth,mod,obj,py:class,py:mod 94 | -------------------------------------------------------------------------------- /pamqp/common.py: -------------------------------------------------------------------------------- 1 | """ 2 | Common type aliases and classes. 3 | 4 | """ 5 | import datetime 6 | import decimal 7 | import struct 8 | import typing 9 | 10 | FieldArray = typing.List['FieldValue'] 11 | """A data structure for holding an array of field values.""" 12 | 13 | FieldTable = typing.Dict[str, 'FieldValue'] 14 | """Field tables are data structures that contain packed name-value pairs. 15 | 16 | The name-value pairs are encoded as short string defining the name, and octet 17 | defining the values type and then the value itself. The valid field types for 18 | tables are an extension of the native integer, bit, string, and timestamp 19 | types, and are shown in the grammar. Multi-octet integer fields are always 20 | held in network byte order. 21 | 22 | Guidelines for implementers: 23 | 24 | - Field names MUST start with a letter, '$' or '#' and may continue with 25 | letters, `$` or `#`, digits, or underlines, to a maximum length of 128 26 | characters. 27 | - The server SHOULD validate field names and upon receiving an invalid field 28 | name, it SHOULD signal a connection exception with reply code 503 29 | (syntax error). 30 | - Decimal values are not intended to support floating point values, but rather 31 | fixed-point business values such as currency rates and amounts. They are 32 | encoded as an octet representing the number of places followed by a long 33 | signed integer. The 'decimals' octet is not signed. 34 | - Duplicate fields are illegal. The behaviour of a peer with respect to a 35 | table containing duplicate fields is undefined. 36 | 37 | """ 38 | 39 | FieldValue = typing.Union[bool, bytes, bytearray, decimal.Decimal, FieldArray, 40 | FieldTable, float, int, None, str, datetime.datetime] 41 | """Defines valid field values for a :const:`FieldTable` and a 42 | :const:`FieldValue` 43 | 44 | """ 45 | 46 | Arguments = typing.Optional[FieldTable] 47 | """Defines an AMQP method arguments argument data type""" 48 | 49 | 50 | class Struct: 51 | """Simple object for getting to the struct objects for 52 | :mod:`pamqp.decode` / :mod:`pamqp.encode`. 53 | 54 | """ 55 | byte = struct.Struct('B') 56 | double = struct.Struct('>d') 57 | float = struct.Struct('>f') 58 | integer = struct.Struct('>I') 59 | uint = struct.Struct('>i') 60 | long_long_int = struct.Struct('>q') 61 | short_short_int = struct.Struct('>b') 62 | short_short_uint = struct.Struct('>B') 63 | timestamp = struct.Struct('>Q') 64 | long = struct.Struct('>l') 65 | ulong = struct.Struct('>L') 66 | short = struct.Struct('>h') 67 | ushort = struct.Struct('>H') 68 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | pamqp 2 | ===== 3 | pamqp is a low level AMQP 0-9-1 frame encoding and decoding library for Python 3. 4 | 5 | pamqp is not a end-user client library for talking to RabbitMQ but rather is used 6 | by client libraries for marshaling and unmarshaling AMQP frames. 7 | 8 | |Version| |License| 9 | 10 | Issues 11 | ------ 12 | Please report any issues to the Github repo at `https://github.com/gmr/pamqp/issues `_ 13 | 14 | Source 15 | ------ 16 | pamqp source is available on Github at `https://github.com/gmr/pamqp `_ 17 | 18 | Installation 19 | ------------ 20 | pamqp is available from the `Python Package Index `_ but should generally be installed as a dependency from a client library. 21 | 22 | Documentation 23 | ------------- 24 | .. toctree:: 25 | :maxdepth: 1 26 | 27 | base 28 | body 29 | commands 30 | common 31 | decode 32 | encode 33 | exceptions 34 | frame 35 | header 36 | heartbeat 37 | changelog 38 | genindex 39 | 40 | License 41 | ------- 42 | 43 | Copyright (c) 2011-2024 Gavin M. Roy 44 | All rights reserved. 45 | 46 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 47 | 48 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 49 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 50 | * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 51 | 52 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 53 | 54 | .. |Version| image:: https://img.shields.io/pypi/v/pamqp.svg? 55 | :target: https://pypi.python.org/pypi/pamqp 56 | :alt: Package Version 57 | 58 | .. |License| image:: https://img.shields.io/pypi/l/pamqp.svg? 59 | :target: https://github.com/gmr/pamqp/blob/master/LICENSE 60 | :alt: BSD 61 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pamqp 2 | ===== 3 | pamqp is a low level AMQP 0-9-1 frame encoding and decoding library for Python 3. 4 | 5 | pamqp is not a end-user client library for talking to RabbitMQ but rather is 6 | used by client libraries for marshaling and unmarshaling AMQP frames. 7 | 8 | |Version| |Status| |Coverage| |License| |Maintainability| |Downloads| 9 | 10 | Documentation 11 | ------------- 12 | https://pamqp.readthedocs.io 13 | 14 | Python Versions Supported 15 | ------------------------- 16 | 3.7+ 17 | 18 | License 19 | ------- 20 | Copyright (c) 2011-2024 Gavin M. Roy 21 | All rights reserved. 22 | 23 | Redistribution and use in source and binary forms, with or without modification, 24 | are permitted provided that the following conditions are met: 25 | 26 | * Redistributions of source code must retain the above copyright notice, this 27 | list of conditions and the following disclaimer. 28 | * Redistributions in binary form must reproduce the above copyright notice, 29 | this list of conditions and the following disclaimer in the documentation 30 | and/or other materials provided with the distribution. 31 | * Neither the name of the copyright holder nor the names of its contributors may 32 | be used to endorse or promote products derived from this software without 33 | specific prior written permission. 34 | 35 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 36 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 37 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 38 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 39 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 40 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 41 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 42 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 43 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 44 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 45 | 46 | .. |Version| image:: https://img.shields.io/pypi/v/pamqp.svg? 47 | :target: https://pypi.python.org/pypi/pamqp 48 | 49 | .. |Status| image:: https://github.com/gmr/pamqp/workflows/Testing/badge.svg? 50 | :target: https://github.com/gmr/pamqp/actions?workflow=Testing 51 | :alt: Build Status 52 | 53 | .. |Coverage| image:: https://img.shields.io/codecov/c/github/gmr/pamqp.svg? 54 | :target: https://codecov.io/github/gmr/pamqp?branch=master 55 | 56 | .. |License| image:: https://img.shields.io/pypi/l/pamqp.svg? 57 | :target: https://pamqp.readthedocs.org 58 | 59 | .. |Maintainability| image:: https://api.codeclimate.com/v1/badges/9efbb0957abb036254a1/maintainability 60 | :target: https://codeclimate.com/github/gmr/pamqp 61 | 62 | .. |Downloads| image:: https://img.shields.io/pypi/dm/pamqp 63 | :target: https://pypi.org/project/pamqp/ 64 | -------------------------------------------------------------------------------- /tests/test_frame_unmarshaling_errors.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import struct 3 | import unittest 4 | 5 | from pamqp import constants, exceptions, frame 6 | 7 | 8 | class TestCase(unittest.TestCase): 9 | 10 | def test_invalid_protocol_header(self): 11 | with self.assertRaises(exceptions.UnmarshalingException) as err: 12 | frame.unmarshal(b'AMQP\x00\x00\t') 13 | self.assertTrue(str(err).startswith( 14 | "Could not unmarshal " 15 | 'frame: Data did not match the ProtocolHeader format')) 16 | 17 | def test_invalid_frame_header(self): 18 | frame_data = struct.pack('>BI', 255, 0) 19 | with self.assertRaises(exceptions.UnmarshalingException) as err: 20 | frame.unmarshal(frame_data) 21 | self.assertEqual( 22 | str(err), 'Could not unmarshal Unknown frame: No frame size') 23 | 24 | def test_frame_with_no_length(self): 25 | frame_data = (b'\x01\x00\x01\x00\x00\x00\x00\x00<\x00P\x00\x00\x00\x00' 26 | b'\x00\x00\x00\x01\x00\xce') 27 | with self.assertRaises(exceptions.UnmarshalingException) as err: 28 | frame.unmarshal(frame_data) 29 | self.assertEqual( 30 | str(err), 'Could not unmarshal Unknown frame: No frame size') 31 | 32 | def test_frame_malformed_length(self): 33 | frame_data = (b'\x01\x00\x01\x00\x00\x00\x0c\x00<\x00P\x00\x00\x00\x00' 34 | b'\x00\x00\x00\xce') 35 | with self.assertRaises(exceptions.UnmarshalingException) as err: 36 | frame.unmarshal(frame_data) 37 | self.assertEqual( 38 | str(err), 39 | 'Could not unmarshal Unknown frame: Not all data received') 40 | 41 | def test_frame_malformed_end_byte(self): 42 | frame_data = (b'\x01\x00\x01\x00\x00\x00\r\x00<\x00P\x00\x00\x00\x00' 43 | b'\x00\x00\x00\x01\x00\x00') 44 | with self.assertRaises(exceptions.UnmarshalingException) as err: 45 | frame.unmarshal(frame_data) 46 | self.assertEqual( 47 | str(err), 48 | 'Could not unmarshal Unknown frame: Last byte error') 49 | 50 | def test_malformed_frame_content(self): 51 | payload = struct.pack('>HxxQ', 8192, 32768) 52 | frame_value = b''.join([struct.pack('>BHI', 5, 0, len(payload)), 53 | payload, constants.FRAME_END_CHAR]) 54 | with self.assertRaises(exceptions.UnmarshalingException) as err: 55 | frame.unmarshal(frame_value) 56 | self.assertEqual( 57 | str(err), 58 | 'Could not unmarshal Unknown frame: Unknown frame type: 5') 59 | 60 | def test_invalid_method_frame_index(self): 61 | payload = struct.pack('>L', 42949) 62 | frame_value = b''.join([struct.pack('>BHI', 1, 0, len(payload)), 63 | payload, constants.FRAME_END_CHAR]) 64 | with self.assertRaises(exceptions.UnmarshalingException) as err: 65 | frame.unmarshal(frame_value) 66 | self.assertEqual( 67 | str(err), 68 | ('Could not unmarshal Unknown frame: ' 69 | 'Unknown method index: 42949')) 70 | 71 | def test_invalid_method_frame_content(self): 72 | payload = struct.pack('>L', 0x000A0029) 73 | frame_value = b''.join([struct.pack('>BHI', 1, 0, len(payload)), 74 | payload, constants.FRAME_END_CHAR]) 75 | with self.assertRaises(exceptions.UnmarshalingException) as err: 76 | frame.unmarshal(frame_value) 77 | self.assertTrue(str(err).startswith( 78 | 'Could not unmarshal L', 0x000A0029) 82 | frame_value = b''.join([struct.pack('>BHI', 2, 0, len(payload)), 83 | payload, constants.FRAME_END_CHAR]) 84 | with self.assertRaises(exceptions.UnmarshalingException) as err: 85 | frame.unmarshal(frame_value) 86 | self.assertTrue(str(err).startswith( 87 | 'Could not unmarshal ContentHeader frame:')) 88 | -------------------------------------------------------------------------------- /codegen/extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Returned when RabbitMQ sends back with 'basic.return' when a 7 | 'mandatory' message cannot be delivered to any queue. 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | The reason the connection was blocked. 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | The queue name identifies the queue within the vhost. In methods where the queue 29 | name may be blank, and that has no specific significance, this refers to the 30 | 'current' queue for the channel, meaning the last queue that the client declared 31 | on the channel. If the client did not declare a queue, and the method needs a 32 | queue name, this will result in a 502 (syntax error) channel exception. 33 | 34 | 35 | 36 | 37 | 38 | 39 | The number of messages pending in the queue. 40 | 41 | 42 | 43 | 44 | 45 | If set, the server will not respond to the method. The client 46 | should not wait for a reply method. If the server could not 47 | complete the method it will raise a channel or connection exception. 48 | 49 | 50 | 51 | 52 | 53 | Deprecated value that must be zero. 54 | 55 | 56 | 57 | 58 | Deprecated value that must be empty. 59 | 60 | 61 | 62 | 63 | Deprecated value that must be False. 64 | 65 | 66 | 68 | 69 | Used when negotiating a connection on an out-of-band channel. Do 70 | not use, must be zero. 71 | 72 | 73 | 74 | 75 | Deprecated value that must be False. 76 | 77 | 78 | 79 | 80 | Deprecated value that must be empty. 81 | 82 | 83 | 84 | 85 | Do not use, must be zero. 86 | 87 | 88 | 89 | 90 | Deprecated value that must be empty. 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /tests/test_frame_marshaling.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | import datetime 3 | import unittest 4 | import uuid 5 | 6 | from pamqp import body, commands, frame, header, heartbeat 7 | 8 | 9 | class MarshalingTests(unittest.TestCase): 10 | def test_protocol_header(self): 11 | expectation = b'AMQP\x00\x00\t\x01' 12 | response = frame.marshal(header.ProtocolHeader(), 0) 13 | self.assertEqual(response, expectation, 14 | 'ProtocolHeader did not match expectation') 15 | 16 | def test_basic_ack(self): 17 | expectation = (b'\x01\x00\x01\x00\x00\x00\r\x00<\x00P\x00\x00\x00' 18 | b'\x00\x00\x00\x00\x01\x00\xce') 19 | frame_obj = commands.Basic.Ack(1, False) 20 | response = frame.marshal(frame_obj, 1) 21 | self.assertEqual(response, expectation, 22 | 'Basic.Ack did not match expectation') 23 | 24 | def test_basic_cancel(self): 25 | expectation = (b'\x01\x00\x01\x00\x00\x00\r\x00<\x00\x1e\x07' 26 | b'ctag1.0\x00\xce') 27 | frame_obj = commands.Basic.Cancel('ctag1.0', False) 28 | response = frame.marshal(frame_obj, 1) 29 | self.assertEqual(response, expectation, 30 | 'Basic.Cancel did not match expectation') 31 | 32 | def test_basic_cancelok(self): 33 | expectation = (b'\x01\x00\x01\x00\x00\x00\x0c\x00<\x00\x1f\x07' 34 | b'ctag1.0\xce') 35 | frame_obj = commands.Basic.CancelOk(consumer_tag='ctag1.0') 36 | response = frame.marshal(frame_obj, 1) 37 | self.assertEqual(response, expectation, 38 | 'Basic.Cancel did not match expectation') 39 | 40 | def test_basic_consume(self): 41 | expectation = (b'\x01\x00\x01\x00\x00\x00\x15\x00<\x00\x14\x00' 42 | b'\x00\x03bar\x05ctag0\x00\x00\x00\x00\x00\xce') 43 | frame_obj = commands.Basic.Consume(0, 'bar', 'ctag0', False, 44 | False, False, False) 45 | response = frame.marshal(frame_obj, 1) 46 | self.assertEqual(response, expectation, 47 | 'Basic.Consume did not match expectation') 48 | 49 | def test_heartbeat(self): 50 | expectation = b'\x08\x00\x00\x00\x00\x00\x00\xce' 51 | response = frame.marshal(heartbeat.Heartbeat(), 0) 52 | self.assertEqual(response, expectation, 53 | 'Heartbeat did not match expectation') 54 | 55 | def test_content_body(self): 56 | value = str(uuid.uuid4()).encode('utf-8') 57 | expectation = b'\x03\x00\x01\x00\x00\x00$' + value + b'\xce' 58 | self.assertEqual(frame.marshal(body.ContentBody(value), 1), 59 | expectation) 60 | 61 | def test_content_header(self): 62 | expectation = (b'\x02\x00\x01\x00\x00\x00\x0e\x00<\x00\x00\x00\x00' 63 | b'\x00\x00\x00\x00\x00\n\x00\x00\xce') 64 | self.assertEqual(frame.marshal(header.ContentHeader(body_size=10), 1), 65 | expectation) 66 | 67 | def test_content_header_with_basic_properties(self): 68 | props = commands.Basic.Properties( 69 | app_id='unittest', 70 | content_type='application/json', 71 | content_encoding='bzip2', 72 | correlation_id='d146482a-42dd-4b8b-a620-63d62ef686f3', 73 | delivery_mode=2, 74 | expiration='100', 75 | headers={'foo': 'Test ✈'}, 76 | message_id='4b5baed7-66e3-49da-bfe4-20a9651e0db4', 77 | message_type='foo', 78 | priority=10, 79 | reply_to='q1', 80 | timestamp=datetime.datetime(2019, 12, 19, 23, 29, 00, 81 | tzinfo=datetime.timezone.utc)) 82 | expectation = (b'\x02\x00\x01\x00\x00\x00\xa2\x00<\x00\x00\x00\x00\x00' 83 | b'\x00\x00\x00\x00\n\xff\xe8\x10application/json\x05' 84 | b'bzip2\x00\x00\x00\x11\x03fooS\x00\x00\x00\x08Test ' 85 | b'\xe2\x9c\x88\x02\n$d146482a-42dd-4b8b-a620-63d62ef68' 86 | b'6f3\x02q1\x03100$4b5baed7-66e3-49da-bfe4-20a9651e0db4' 87 | b'\x00\x00\x00\x00]\xfc\x07\xbc\x03foo\x08unittest\xce') 88 | self.assertEqual(frame.marshal(header.ContentHeader(0, 10, props), 1), 89 | expectation) 90 | 91 | def test_unknown_frame_type(self): 92 | with self.assertRaises(ValueError): 93 | frame.marshal(self, 1) 94 | -------------------------------------------------------------------------------- /pamqp/header.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | AMQP Header Class Definitions 4 | 5 | For encoding AMQP Header frames into binary AMQP stream data and decoding AMQP 6 | binary data into AMQP Header frames. 7 | 8 | """ 9 | import struct 10 | import typing 11 | 12 | from pamqp import commands, constants, decode 13 | 14 | BasicProperties = typing.Optional[commands.Basic.Properties] 15 | 16 | 17 | class ProtocolHeader: 18 | """Class that represents the AMQP Protocol Header""" 19 | name = 'ProtocolHeader' 20 | 21 | def __init__(self, 22 | major_version: int = constants.VERSION[0], 23 | minor_version: int = constants.VERSION[1], 24 | revision: int = constants.VERSION[2]): 25 | """Construct a Protocol Header frame object for the specified AMQP 26 | version. 27 | 28 | :param major_version: The AMQP major version (``0``) 29 | :param minor_version: The AMQP major version (``9``) 30 | :param revision: The AMQP major version (``1``) 31 | 32 | """ 33 | self.major_version = major_version 34 | self.minor_version = minor_version 35 | self.revision = revision 36 | 37 | def marshal(self) -> bytes: 38 | """Return the full AMQP wire protocol frame data representation of the 39 | ProtocolHeader frame. 40 | 41 | """ 42 | return constants.AMQP + struct.pack('BBBB', 0, self.major_version, 43 | self.minor_version, self.revision) 44 | 45 | def unmarshal(self, data: bytes) -> int: 46 | """Dynamically decode the frame data applying the values to the method 47 | object by iterating through the attributes in order and decoding them. 48 | 49 | :param data: The frame value to unpack 50 | :raises: ValueError 51 | 52 | """ 53 | try: 54 | (self.major_version, self.minor_version, 55 | self.revision) = struct.unpack('BBB', data[5:8]) 56 | except struct.error: 57 | raise ValueError( 58 | 'Could not unpack protocol header from {!r}'.format(data)) 59 | return 8 60 | 61 | 62 | class ContentHeader: 63 | """Represent a content header frame 64 | 65 | A Content Header frame is received after a Basic.Deliver or Basic.GetOk 66 | frame and has the data and properties for the Content Body frames that 67 | follow. 68 | 69 | """ 70 | name = 'ContentHeader' 71 | 72 | def __init__(self, 73 | weight: int = 0, 74 | body_size: int = 0, 75 | properties: typing.Optional[BasicProperties] = None): 76 | """Initialize the Exchange.DeleteOk class 77 | 78 | Weight is unused and must be `0` 79 | 80 | :param weight: The unused content weight field 81 | :param body_size: The size in bytes of the message body 82 | :param properties: The message properties 83 | 84 | """ 85 | self.class_id = None 86 | self.weight = weight 87 | self.body_size = body_size 88 | self.properties = properties or commands.Basic.Properties() 89 | 90 | def marshal(self) -> bytes: 91 | """Return the AMQP binary encoded value of the frame""" 92 | return struct.pack('>HxxQ', commands.Basic.frame_id, 93 | self.body_size) + self.properties.marshal() 94 | 95 | def unmarshal(self, data: bytes) -> None: 96 | """Dynamically decode the frame data applying the values to the method 97 | object by iterating through the attributes in order and decoding them. 98 | 99 | :param data: The raw frame data to unmarshal 100 | 101 | """ 102 | self.class_id, self.weight, self.body_size = struct.unpack( 103 | '>HHQ', data[0:12]) 104 | offset, flags = self._get_flags(data[12:]) 105 | self.properties.unmarshal(flags, data[12 + offset:]) 106 | 107 | @staticmethod 108 | def _get_flags(data: bytes) -> typing.Tuple[int, int]: 109 | """Decode the flags from the data returning the bytes consumed and 110 | flags. 111 | 112 | """ 113 | bytes_consumed, flags, flagword_index = 0, 0, 0 114 | while True: 115 | consumed, partial_flags = decode.short_int(data) 116 | bytes_consumed += consumed 117 | flags |= (partial_flags << (flagword_index * 16)) 118 | if not partial_flags & 1: # pragma: nocover 119 | break 120 | flagword_index += 1 # pragma: nocover 121 | return bytes_consumed, flags 122 | -------------------------------------------------------------------------------- /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 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 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 " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pamqp.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pamqp.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/pamqp" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pamqp" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /pamqp/exceptions.py: -------------------------------------------------------------------------------- 1 | # Auto-generated, do not edit this file. 2 | 3 | 4 | class PAMQPException(Exception): 5 | """Base exception for all pamqp specific exceptions.""" 6 | 7 | 8 | class UnmarshalingException(PAMQPException): 9 | """Raised when a frame is not able to be unmarshaled.""" 10 | 11 | def __str__(self) -> str: # pragma: nocover 12 | return 'Could not unmarshal {} frame: {}'.format( 13 | self.args[0], self.args[1]) 14 | 15 | 16 | class AMQPError(PAMQPException): 17 | """Base exception for all AMQP errors.""" 18 | 19 | 20 | class AMQPSoftError(AMQPError): 21 | """Base exception for all AMQP soft errors.""" 22 | 23 | 24 | class AMQPHardError(AMQPError): 25 | """Base exception for all AMQP hard errors.""" 26 | 27 | 28 | class AMQPContentTooLarge(AMQPSoftError): 29 | """ 30 | The client attempted to transfer content larger than the server could 31 | accept at the present time. The client may retry at a later time. 32 | 33 | """ 34 | name = 'CONTENT-TOO-LARGE' 35 | value = 311 36 | 37 | 38 | class AMQPNoRoute(AMQPSoftError): 39 | """ 40 | Returned when RabbitMQ sends back with 'basic.return' when a 'mandatory' 41 | message cannot be delivered to any queue. 42 | 43 | """ 44 | name = 'NO-ROUTE' 45 | value = 312 46 | 47 | 48 | class AMQPNoConsumers(AMQPSoftError): 49 | """ 50 | When the exchange cannot deliver to a consumer when the immediate flag is 51 | set. As a result of pending data on the queue or the absence of any 52 | consumers of the queue. 53 | 54 | """ 55 | name = 'NO-CONSUMERS' 56 | value = 313 57 | 58 | 59 | class AMQPAccessRefused(AMQPSoftError): 60 | """ 61 | The client attempted to work with a server entity to which it has no access 62 | due to security settings. 63 | 64 | """ 65 | name = 'ACCESS-REFUSED' 66 | value = 403 67 | 68 | 69 | class AMQPNotFound(AMQPSoftError): 70 | """ 71 | The client attempted to work with a server entity that does not exist. 72 | 73 | """ 74 | name = 'NOT-FOUND' 75 | value = 404 76 | 77 | 78 | class AMQPResourceLocked(AMQPSoftError): 79 | """ 80 | The client attempted to work with a server entity to which it has no access 81 | because another client is working with it. 82 | 83 | """ 84 | name = 'RESOURCE-LOCKED' 85 | value = 405 86 | 87 | 88 | class AMQPPreconditionFailed(AMQPSoftError): 89 | """ 90 | The client requested a method that was not allowed because some 91 | precondition failed. 92 | 93 | """ 94 | name = 'PRECONDITION-FAILED' 95 | value = 406 96 | 97 | 98 | class AMQPConnectionForced(AMQPHardError): 99 | """ 100 | An operator intervened to close the connection for some reason. The client 101 | may retry at some later date. 102 | 103 | """ 104 | name = 'CONNECTION-FORCED' 105 | value = 320 106 | 107 | 108 | class AMQPInvalidPath(AMQPHardError): 109 | """ 110 | The client tried to work with an unknown virtual host. 111 | 112 | """ 113 | name = 'INVALID-PATH' 114 | value = 402 115 | 116 | 117 | class AMQPFrameError(AMQPHardError): 118 | """ 119 | The sender sent a malformed frame that the recipient could not decode. This 120 | strongly implies a programming error in the sending peer. 121 | 122 | """ 123 | name = 'FRAME-ERROR' 124 | value = 501 125 | 126 | 127 | class AMQPSyntaxError(AMQPHardError): 128 | """ 129 | The sender sent a frame that contained illegal values for one or more 130 | fields. This strongly implies a programming error in the sending peer. 131 | 132 | """ 133 | name = 'SYNTAX-ERROR' 134 | value = 502 135 | 136 | 137 | class AMQPCommandInvalid(AMQPHardError): 138 | """ 139 | The client sent an invalid sequence of frames, attempting to perform an 140 | operation that was considered invalid by the server. This usually implies a 141 | programming error in the client. 142 | 143 | """ 144 | name = 'COMMAND-INVALID' 145 | value = 503 146 | 147 | 148 | class AMQPChannelError(AMQPHardError): 149 | """ 150 | The client attempted to work with a channel that had not been correctly 151 | opened. This most likely indicates a fault in the client layer. 152 | 153 | """ 154 | name = 'CHANNEL-ERROR' 155 | value = 504 156 | 157 | 158 | class AMQPUnexpectedFrame(AMQPHardError): 159 | """ 160 | The peer sent a frame that was not expected, usually in the context of a 161 | content header and body. This strongly indicates a fault in the peer's 162 | content processing. 163 | 164 | """ 165 | name = 'UNEXPECTED-FRAME' 166 | value = 505 167 | 168 | 169 | class AMQPResourceError(AMQPHardError): 170 | """ 171 | The server could not complete the method because it lacked sufficient 172 | resources. This may be due to the client creating too many of some type of 173 | entity. 174 | 175 | """ 176 | name = 'RESOURCE-ERROR' 177 | value = 506 178 | 179 | 180 | class AMQPNotAllowed(AMQPHardError): 181 | """ 182 | The client tried to work with some entity in a manner that is prohibited by 183 | the server, due to security settings or by some other criteria. 184 | 185 | """ 186 | name = 'NOT-ALLOWED' 187 | value = 530 188 | 189 | 190 | class AMQPNotImplemented(AMQPHardError): 191 | """ 192 | The client tried to use functionality that is not implemented in the 193 | server. 194 | 195 | """ 196 | name = 'NOT-IMPLEMENTED' 197 | value = 540 198 | 199 | 200 | class AMQPInternalError(AMQPHardError): 201 | """ 202 | The server could not complete the method because of an internal error. The 203 | server may require intervention by an operator in order to resume normal 204 | operations. 205 | 206 | """ 207 | name = 'INTERNAL-ERROR' 208 | value = 541 209 | 210 | 211 | # AMQP Error code to class mapping 212 | CLASS_MAPPING = { 213 | 311: AMQPContentTooLarge, 214 | 312: AMQPNoRoute, 215 | 313: AMQPNoConsumers, 216 | 403: AMQPAccessRefused, 217 | 404: AMQPNotFound, 218 | 405: AMQPResourceLocked, 219 | 406: AMQPPreconditionFailed, 220 | 320: AMQPConnectionForced, 221 | 402: AMQPInvalidPath, 222 | 501: AMQPFrameError, 223 | 502: AMQPSyntaxError, 224 | 503: AMQPCommandInvalid, 225 | 504: AMQPChannelError, 226 | 505: AMQPUnexpectedFrame, 227 | 506: AMQPResourceError, 228 | 530: AMQPNotAllowed, 229 | 540: AMQPNotImplemented, 230 | 541: AMQPInternalError 231 | } 232 | -------------------------------------------------------------------------------- /pamqp/frame.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """Manage the marshaling and unmarshaling of AMQP frames 3 | 4 | unmarshal will turn a raw AMQP byte stream into the appropriate AMQP objects 5 | from the specification file. 6 | 7 | marshal will take an object created from the specification file and turn it 8 | into a raw byte stream. 9 | 10 | """ 11 | import logging 12 | import struct 13 | import typing 14 | 15 | from pamqp import (base, body, commands, common, constants, decode, exceptions, 16 | header, heartbeat) 17 | 18 | LOGGER = logging.getLogger(__name__) 19 | UNMARSHAL_FAILURE = 0, 0, None 20 | 21 | FrameTypes = typing.Union[base.Frame, body.ContentBody, header.ContentHeader, 22 | header.ProtocolHeader, heartbeat.Heartbeat] 23 | 24 | 25 | def marshal(frame_value: FrameTypes, channel_id: int) -> bytes: 26 | """Marshal a frame to be sent over the wire. 27 | 28 | :raises: ValueError 29 | 30 | """ 31 | if isinstance(frame_value, header.ProtocolHeader): 32 | return frame_value.marshal() 33 | elif isinstance(frame_value, base.Frame): 34 | return _marshal_method_frame(frame_value, channel_id) 35 | elif isinstance(frame_value, header.ContentHeader): 36 | return _marshal_content_header_frame(frame_value, channel_id) 37 | elif isinstance(frame_value, body.ContentBody): 38 | return _marshal_content_body_frame(frame_value, channel_id) 39 | elif isinstance(frame_value, heartbeat.Heartbeat): 40 | return frame_value.marshal() 41 | raise ValueError('Could not determine frame type: {}'.format(frame_value)) 42 | 43 | 44 | def unmarshal(data_in: bytes) -> typing.Tuple[int, int, FrameTypes]: 45 | """Takes in binary data and maps builds the appropriate frame type, 46 | returning a frame object. 47 | 48 | :returns: tuple of bytes consumed, channel, and a frame object 49 | :raises: exceptions.UnmarshalingException 50 | 51 | """ 52 | try: # Look to see if it's a protocol header frame 53 | value = _unmarshal_protocol_header_frame(data_in) 54 | except ValueError as error: 55 | raise exceptions.UnmarshalingException(header.ProtocolHeader, error) 56 | else: 57 | if value: 58 | return 8, 0, value 59 | 60 | frame_type, channel_id, frame_size = frame_parts(data_in) 61 | 62 | # Heartbeats do not have frame length indicators 63 | if frame_type == constants.FRAME_HEARTBEAT and frame_size == 0: 64 | return 8, channel_id, heartbeat.Heartbeat() 65 | 66 | if not frame_size: 67 | raise exceptions.UnmarshalingException('Unknown', 'No frame size') 68 | 69 | byte_count = constants.FRAME_HEADER_SIZE + frame_size + 1 70 | if byte_count > len(data_in): 71 | raise exceptions.UnmarshalingException('Unknown', 72 | 'Not all data received') 73 | 74 | if data_in[byte_count - 1] != constants.FRAME_END: 75 | raise exceptions.UnmarshalingException('Unknown', 'Last byte error') 76 | frame_data = data_in[constants.FRAME_HEADER_SIZE:byte_count - 1] 77 | if frame_type == constants.FRAME_METHOD: 78 | return byte_count, channel_id, _unmarshal_method_frame(frame_data) 79 | elif frame_type == constants.FRAME_HEADER: 80 | return byte_count, channel_id, _unmarshal_header_frame(frame_data) 81 | elif frame_type == constants.FRAME_BODY: 82 | return byte_count, channel_id, _unmarshal_body_frame(frame_data) 83 | raise exceptions.UnmarshalingException( 84 | 'Unknown', 'Unknown frame type: {}'.format(frame_type)) 85 | 86 | 87 | def frame_parts(data: bytes) -> typing.Tuple[int, int, typing.Optional[int]]: 88 | """Attempt to decode a low-level frame, returning frame parts""" 89 | try: # Get the Frame Type, Channel Number and Frame Size 90 | return struct.unpack('>BHI', data[0:constants.FRAME_HEADER_SIZE]) 91 | except struct.error: # Did not receive a full frame 92 | return UNMARSHAL_FAILURE 93 | 94 | 95 | def _marshal(frame_type: int, channel_id: int, payload: bytes) -> bytes: 96 | """Marshal the low-level AMQ frame""" 97 | return b''.join([ 98 | struct.pack('>BHI', frame_type, channel_id, len(payload)), payload, 99 | constants.FRAME_END_CHAR 100 | ]) 101 | 102 | 103 | def _marshal_content_body_frame(value: body.ContentBody, 104 | channel_id: int) -> bytes: 105 | """Marshal as many content body frames as needed to transmit the content""" 106 | return _marshal(constants.FRAME_BODY, channel_id, value.marshal()) 107 | 108 | 109 | def _marshal_content_header_frame(value: header.ContentHeader, 110 | channel_id: int) -> bytes: 111 | """Marshal a content header frame""" 112 | return _marshal(constants.FRAME_HEADER, channel_id, value.marshal()) 113 | 114 | 115 | def _marshal_method_frame(value: base.Frame, channel_id: int) -> bytes: 116 | """Marshal a method frame""" 117 | return _marshal(constants.FRAME_METHOD, channel_id, 118 | common.Struct.integer.pack(value.index) + value.marshal()) 119 | 120 | 121 | def _unmarshal_protocol_header_frame(data_in: bytes) \ 122 | -> typing.Optional[header.ProtocolHeader]: 123 | """Attempt to unmarshal a protocol header frame 124 | 125 | The ProtocolHeader is abbreviated in size and functionality compared to 126 | the rest of the frame types, so return UNMARSHAL_ERROR doesn't apply 127 | as cleanly since we don't have all of the attributes to return even 128 | regardless of success or failure. 129 | 130 | :raises: ValueError 131 | 132 | """ 133 | if data_in[0:4] == constants.AMQP: # Do the first four bytes match? 134 | frame = header.ProtocolHeader() 135 | frame.unmarshal(data_in) 136 | return frame 137 | return None 138 | 139 | 140 | def _unmarshal_method_frame(frame_data: bytes) -> base.Frame: 141 | """Attempt to unmarshal a method frame 142 | 143 | :raises: pamqp.exceptions.UnmarshalingException 144 | 145 | """ 146 | bytes_used, method_index = decode.long_int(frame_data[0:4]) 147 | try: 148 | method = commands.INDEX_MAPPING[method_index]() 149 | except KeyError: 150 | raise exceptions.UnmarshalingException( 151 | 'Unknown', 'Unknown method index: {}'.format(str(method_index))) 152 | try: 153 | method.unmarshal(frame_data[bytes_used:]) 154 | except struct.error as error: 155 | raise exceptions.UnmarshalingException(method, error) 156 | return method 157 | 158 | 159 | def _unmarshal_header_frame(frame_data: bytes) -> header.ContentHeader: 160 | """Attempt to unmarshal a header frame 161 | 162 | :raises: pamqp.exceptions.UnmarshalingException 163 | 164 | """ 165 | content_header = header.ContentHeader() 166 | try: 167 | content_header.unmarshal(frame_data) 168 | except struct.error as error: 169 | raise exceptions.UnmarshalingException('ContentHeader', error) 170 | return content_header 171 | 172 | 173 | def _unmarshal_body_frame(frame_data: bytes) -> body.ContentBody: 174 | """Attempt to unmarshal a body frame""" 175 | content_body = body.ContentBody(b'') 176 | content_body.unmarshal(frame_data) 177 | return content_body 178 | -------------------------------------------------------------------------------- /pamqp/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base classes for the representation of frames and data structures. 3 | 4 | """ 5 | import logging 6 | import struct 7 | import typing 8 | 9 | from pamqp import common, decode, encode 10 | 11 | LOGGER = logging.getLogger(__name__) 12 | 13 | 14 | class _AMQData: 15 | """Base class for AMQ methods and properties for encoding and decoding""" 16 | __annotations__: typing.Dict = {} 17 | __slots__: typing.List = [] 18 | name = '_AMQData' 19 | 20 | def __contains__(self, item: str) -> bool: 21 | """Return if the item is in the attribute list""" 22 | return item in self.__slots__ 23 | 24 | def __getitem__(self, item: str) -> common.FieldValue: 25 | """Return an attribute as if it were a dict 26 | 27 | :param item: The key to use to retrieve the value 28 | :rtype: :const:`pamqp.common.FieldValue` 29 | :raises: KeyError 30 | 31 | """ 32 | return getattr(self, item) 33 | 34 | def __iter__(self) \ 35 | -> typing.Generator[typing.Tuple[str, common.FieldValue], 36 | None, None]: 37 | """Iterate the attributes and values as key, value pairs 38 | 39 | :rtype: (:class:`str`, :const:`pamqp.common.FieldValue`) 40 | 41 | """ 42 | for attribute in self.__slots__: 43 | yield attribute, getattr(self, attribute) 44 | 45 | def __len__(self) -> int: 46 | """Return the length of the attribute list""" 47 | return len(self.__slots__) 48 | 49 | def __repr__(self) -> str: 50 | """Return the representation of the frame object""" 51 | return '<{} object at {}>'.format(self.name, hex(id(self))) 52 | 53 | @classmethod 54 | def amqp_type(cls, attr: str) -> str: 55 | """Return the AMQP data type for an attribute 56 | 57 | :param attr: The attribute name 58 | 59 | """ 60 | return getattr(cls, '_' + attr) 61 | 62 | @classmethod 63 | def attributes(cls) -> list: 64 | """Return the list of attributes""" 65 | return cls.__slots__ 66 | 67 | 68 | class Frame(_AMQData): 69 | """Base Class for AMQ Methods for encoding and decoding""" 70 | frame_id = 0 71 | index = 0 72 | synchronous = False 73 | valid_responses: typing.List = [] 74 | 75 | def marshal(self) -> bytes: 76 | """Dynamically encode the frame by taking the list of attributes and 77 | encode them item by item getting the value form the object attribute 78 | and the data type from the class attribute. 79 | 80 | """ 81 | self.validate() 82 | byte, offset, output, processing_bitset = -1, 0, [], False 83 | for argument in self.__slots__: 84 | data_type = self.amqp_type(argument) 85 | if not processing_bitset and data_type == 'bit': 86 | byte, offset, processing_bitset = 0, 0, True 87 | data_value = getattr(self, argument, 0) 88 | if processing_bitset: 89 | if data_type != 'bit': 90 | processing_bitset = False 91 | output.append(encode.octet(byte)) 92 | else: 93 | byte = encode.bit(data_value, byte, offset) 94 | offset += 1 95 | if offset == 8: # pragma: nocover 96 | output.append(encode.octet(byte)) 97 | processing_bitset = False 98 | continue # pragma: nocover 99 | output.append(encode.by_type(data_value, data_type)) 100 | if processing_bitset: 101 | output.append(encode.octet(byte)) 102 | return b''.join(output) 103 | 104 | def unmarshal(self, data: bytes) -> None: 105 | """Dynamically decode the frame data applying the values to the method 106 | object by iterating through the attributes in order and decoding them. 107 | 108 | :param data: The raw AMQP frame data 109 | 110 | """ 111 | offset, processing_bitset = 0, False 112 | for argument in self.__slots__: 113 | data_type = self.amqp_type(argument) 114 | if offset == 7 and processing_bitset: # pragma: nocover 115 | data = data[1:] 116 | offset = 0 117 | if processing_bitset and data_type != 'bit': 118 | offset = 0 119 | processing_bitset = False 120 | data = data[1:] 121 | consumed, value = decode.by_type(data, data_type, offset) 122 | if data_type == 'bit': 123 | offset += 1 124 | processing_bitset = True 125 | consumed = 0 126 | setattr(self, argument, value) 127 | if consumed: 128 | data = data[consumed:] 129 | 130 | def validate(self) -> None: 131 | """Validate the frame data ensuring all domains or attributes adhere 132 | to the protocol specification. 133 | 134 | :raises: ValueError 135 | 136 | """ 137 | 138 | 139 | class BasicProperties(_AMQData): 140 | """Provide a base object that marshals and unmarshals the Basic.Properties 141 | object values. 142 | 143 | """ 144 | flags: typing.Dict[str, int] = {} 145 | name = 'BasicProperties' 146 | 147 | def __eq__(self, other: object) -> bool: 148 | if not isinstance(other, BasicProperties): 149 | raise NotImplementedError 150 | return all( 151 | getattr(self, k, None) == getattr(other, k, None) 152 | for k in self.__slots__) 153 | 154 | def encode_property(self, name: str, value: common.FieldValue) -> bytes: 155 | """Encode a single property value 156 | 157 | :param name: The name of the property to encode 158 | :param value: The property to encode 159 | :type value: :const:`pamqp.common.FieldValue` 160 | :raises: TypeError 161 | 162 | """ 163 | return encode.by_type(value, self.amqp_type(name)) 164 | 165 | def marshal(self) -> bytes: 166 | """Take the Basic.Properties data structure and marshal it into the 167 | data structure needed for the ContentHeader. 168 | 169 | """ 170 | flags = 0 171 | parts = [] 172 | for property_name in self.__slots__: 173 | property_value = getattr(self, property_name) 174 | if property_value is not None and property_value != '': 175 | flags = flags | self.flags[property_name] 176 | parts.append( 177 | self.encode_property(property_name, property_value)) 178 | flag_pieces = [] 179 | while True: 180 | remainder = flags >> 16 181 | partial_flags = flags & 0xFFFE 182 | if remainder != 0: # pragma: nocover 183 | partial_flags |= 1 184 | flag_pieces.append(struct.pack('>H', partial_flags)) 185 | flags = remainder 186 | if not flags: # pragma: nocover 187 | break 188 | return b''.join(flag_pieces + parts) 189 | 190 | def unmarshal(self, flags: int, data: bytes) -> None: 191 | """Dynamically decode the frame data applying the values to the method 192 | object by iterating through the attributes in order and decoding them. 193 | 194 | """ 195 | for property_name in self.__slots__: 196 | if flags & self.flags[property_name]: 197 | data_type = getattr(self.__class__, '_' + property_name) 198 | consumed, value = decode.by_type(data, data_type) 199 | setattr(self, property_name, value) 200 | data = data[consumed:] 201 | 202 | def validate(self) -> None: 203 | """Validate the frame data ensuring all domains or attributes adhere 204 | to the protocol specification. 205 | 206 | :raises: ValueError 207 | 208 | """ 209 | if self.cluster_id != '': 210 | raise ValueError('cluster_id must be empty') 211 | if self.delivery_mode is not None and self.delivery_mode not in [1, 2]: 212 | raise ValueError('Invalid delivery_mode value: {}'.format( 213 | self.delivery_mode)) 214 | -------------------------------------------------------------------------------- /tests/test_command_argument_errors.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import uuid 3 | 4 | from pamqp import commands 5 | 6 | 7 | class ArgumentErrorsTestCase(unittest.TestCase): 8 | 9 | def test_basic_consume_queue_length(self): 10 | with self.assertRaises(ValueError): 11 | commands.Basic.Consume(queue=str.ljust('A', 257)) 12 | 13 | def test_basic_consume_queue_characters(self): 14 | with self.assertRaises(ValueError): 15 | commands.Basic.Consume(queue='*') 16 | 17 | def test_basic_consume_ticket(self): 18 | with self.assertRaises(ValueError): 19 | commands.Basic.Consume(ticket=46 & 2) # Just ahead of me 20 | 21 | def test_basic_deliver_exchange_length(self): 22 | with self.assertRaises(ValueError): 23 | commands.Basic.Deliver('ctag0', 1, False, str.ljust('A', 128), 'k') 24 | 25 | def test_basic_deliver_exchange_characters(self): 26 | with self.assertRaises(ValueError): 27 | commands.Basic.Deliver('ctag0', 1, False, '*', 'k') 28 | 29 | def test_basic_get_queue_length(self): 30 | with self.assertRaises(ValueError): 31 | commands.Basic.Get(queue=str.ljust('A', 257)) 32 | 33 | def test_basic_get_queue_characters(self): 34 | with self.assertRaises(ValueError): 35 | commands.Basic.Get(queue='*') 36 | 37 | def test_get_ticket(self): 38 | with self.assertRaises(ValueError): 39 | commands.Basic.Get(ticket=46 & 2) 40 | 41 | def test_basic_getempty_cluster_id(self): 42 | with self.assertRaises(ValueError): 43 | commands.Basic.GetEmpty(cluster_id='See my shadow changing') 44 | 45 | def test_basic_getok_exchange_length(self): 46 | with self.assertRaises(ValueError): 47 | commands.Basic.GetOk(1, False, str.ljust('A', 128), 'k', 0) 48 | 49 | def test_basic_getok_exchange_characters(self): 50 | with self.assertRaises(ValueError): 51 | commands.Basic.GetOk(1, False, '*', 'k', 0) 52 | 53 | def test_basic_properties_cluster_id(self): 54 | with self.assertRaises(ValueError): 55 | commands.Basic.Properties(cluster_id='See my shadow changing') 56 | 57 | def test_basic_publish_exchange_length(self): 58 | with self.assertRaises(ValueError): 59 | commands.Basic.Publish(exchange=str.ljust('A', 128)) 60 | 61 | def test_basic_publish_exchange_characters(self): 62 | with self.assertRaises(ValueError): 63 | commands.Basic.Publish(exchange='*') 64 | 65 | def test_basic_publish_ticket(self): 66 | with self.assertRaises(ValueError): 67 | commands.Basic.Publish(ticket=46 & 2) 68 | 69 | def test_basic_return_exchange_length(self): 70 | with self.assertRaises(ValueError): 71 | commands.Basic.Return(404, 'Not Found', str.ljust('A', 128), 'k') 72 | 73 | def test_basic_return_exchange_characters(self): 74 | with self.assertRaises(ValueError): 75 | commands.Basic.Return(404, 'Not Found', '*', 'k') 76 | 77 | def test_connection_open_vhost(self): 78 | with self.assertRaises(ValueError): 79 | commands.Connection.Open(str.ljust('A', 128)) 80 | 81 | def test_connection_open_capabilities(self): 82 | with self.assertRaises(ValueError): 83 | commands.Connection.Open(capabilities=str(uuid.uuid4())) 84 | 85 | def test_connection_open_insist(self): 86 | with self.assertRaises(ValueError): 87 | commands.Connection.Open(insist=True) 88 | 89 | def test_connection_openok_known_hosts(self): 90 | with self.assertRaises(ValueError): 91 | commands.Connection.OpenOk(known_hosts=str(uuid.uuid4())) 92 | 93 | def test_channel_open_out_of_band(self): 94 | with self.assertRaises(ValueError): 95 | commands.Channel.Open(out_of_band=str(uuid.uuid4())) 96 | 97 | def test_channel_openok_channel_id(self): 98 | with self.assertRaises(ValueError): 99 | commands.Channel.OpenOk(channel_id=str(uuid.uuid4())) 100 | 101 | def test_exchange_declare_ticket(self): 102 | with self.assertRaises(ValueError): 103 | commands.Exchange.Declare(ticket=46 & 2) # Just ahead of me 104 | 105 | def test_exchange_declare_exchange_length(self): 106 | with self.assertRaises(ValueError): 107 | commands.Exchange.Declare(exchange=str.ljust('A', 128)) 108 | 109 | def test_exchange_declare_exchange_characters(self): 110 | with self.assertRaises(ValueError): 111 | commands.Exchange.Declare(exchange='***') 112 | 113 | def test_exchange_delete_ticket(self): 114 | with self.assertRaises(ValueError): 115 | commands.Exchange.Delete(ticket=46 & 2) 116 | 117 | def test_exchange_delete_exchange_length(self): 118 | with self.assertRaises(ValueError): 119 | commands.Exchange.Delete(exchange=str.ljust('A', 128)) 120 | 121 | def test_exchange_delete_exchange_characters(self): 122 | with self.assertRaises(ValueError): 123 | commands.Exchange.Delete(exchange='***') 124 | 125 | def test_exchange_bind_ticket(self): 126 | with self.assertRaises(ValueError): 127 | commands.Exchange.Bind(ticket=46 & 2) 128 | 129 | def test_exchange_bind_destination_length(self): 130 | with self.assertRaises(ValueError): 131 | commands.Exchange.Bind(destination=''.rjust(32768, '*')) 132 | 133 | def test_exchange_bind_destination_pattern(self): 134 | with self.assertRaises(ValueError): 135 | commands.Exchange.Bind(destination='Fortysix & 2') 136 | 137 | def test_exchange_bind_source_length(self): 138 | with self.assertRaises(ValueError): 139 | commands.Exchange.Bind(source=''.rjust(32768, '*')) 140 | 141 | def test_exchange_bind_source_pattern(self): 142 | with self.assertRaises(ValueError): 143 | commands.Exchange.Bind(source='Fortysix & 2') 144 | 145 | def test_exchange_unbind_ticket(self): 146 | with self.assertRaises(ValueError): 147 | commands.Exchange.Unbind(ticket=46 & 2) 148 | 149 | def test_exchange_unbind_destination_length(self): 150 | with self.assertRaises(ValueError): 151 | commands.Exchange.Unbind(destination=''.rjust(32768, '*')) 152 | 153 | def test_exchange_unbind_destination_pattern(self): 154 | with self.assertRaises(ValueError): 155 | commands.Exchange.Unbind(destination='Fortysix & 2') 156 | 157 | def test_exchange_unbind_source_length(self): 158 | with self.assertRaises(ValueError): 159 | commands.Exchange.Unbind(source=''.rjust(32768, '*')) 160 | 161 | def test_exchange_unbind_source_pattern(self): 162 | with self.assertRaises(ValueError): 163 | commands.Exchange.Unbind(source='Fortysix & 2') 164 | 165 | def test_queue_declare_ticket(self): 166 | with self.assertRaises(ValueError): 167 | commands.Queue.Declare(ticket=46 & 2) 168 | 169 | def test_queue_declare_queue_length(self): 170 | with self.assertRaises(ValueError): 171 | commands.Queue.Declare(queue=str.ljust('A', 257)) 172 | 173 | def test_queue_declare_queue_characters(self): 174 | with self.assertRaises(ValueError): 175 | commands.Queue.Declare(queue='***') 176 | 177 | def test_queue_declare_queue_with_valid_name(self): 178 | self.assertIsNotNone(commands.Queue.Declare(queue='foo')) 179 | 180 | def test_queue_declareok_queue_length(self): 181 | with self.assertRaises(ValueError): 182 | commands.Queue.DeclareOk(str.ljust('A', 257), 0, 0) 183 | 184 | def test_queue_declareok_queue_characters(self): 185 | with self.assertRaises(ValueError): 186 | commands.Queue.DeclareOk('***', 0, 0) 187 | 188 | def test_queue_delete_ticket(self): 189 | with self.assertRaises(ValueError): 190 | commands.Queue.Delete(ticket=46 & 2) 191 | 192 | def test_queue_delete_queue_length(self): 193 | with self.assertRaises(ValueError): 194 | commands.Queue.Delete(queue=str.ljust('A', 257)) 195 | 196 | def test_queue_delete_queue_characters(self): 197 | with self.assertRaises(ValueError): 198 | commands.Queue.Delete(queue='***') 199 | 200 | def test_queue_bind_ticket(self): 201 | with self.assertRaises(ValueError): 202 | commands.Queue.Bind(ticket=46 & 2) 203 | 204 | def test_queue_bind_queue_length(self): 205 | with self.assertRaises(ValueError): 206 | commands.Queue.Bind(queue=str.ljust('A', 257), exchange='B') 207 | 208 | def test_queue_bind_queue_characters(self): 209 | with self.assertRaises(ValueError): 210 | commands.Queue.Bind(queue='***', exchange='B') 211 | 212 | def test_queue_bind_exchange_length(self): 213 | with self.assertRaises(ValueError): 214 | commands.Queue.Bind(exchange=str.ljust('A', 128), queue='B') 215 | 216 | def test_queue_bind_exchange_characters(self): 217 | with self.assertRaises(ValueError): 218 | commands.Queue.Bind(exchange='***', queue='B') 219 | 220 | def test_queue_unbind_ticket(self): 221 | with self.assertRaises(ValueError): 222 | commands.Queue.Unbind(ticket=46 & 2) 223 | 224 | def test_queue_unbind_queue_length(self): 225 | with self.assertRaises(ValueError): 226 | commands.Queue.Unbind(queue=str.ljust('A', 257), exchange='B') 227 | 228 | def test_queue_unbind_queue_characters(self): 229 | with self.assertRaises(ValueError): 230 | commands.Queue.Unbind(queue='***', exchange='B') 231 | 232 | def test_queue_unbind_exchange_length(self): 233 | with self.assertRaises(ValueError): 234 | commands.Queue.Unbind(exchange=str.ljust('A', 128), queue='B') 235 | 236 | def test_queue_unbind_exchange_characters(self): 237 | with self.assertRaises(ValueError): 238 | commands.Queue.Unbind(exchange='***', queue='B') 239 | 240 | def test_queue_purge_ticket(self): 241 | with self.assertRaises(ValueError): 242 | commands.Queue.Purge(ticket=46 & 2) 243 | 244 | def test_queue_purge_queue_length(self): 245 | with self.assertRaises(ValueError): 246 | commands.Queue.Purge(queue=str.ljust('A', 257)) 247 | 248 | def test_queue_purge_queue_characters(self): 249 | with self.assertRaises(ValueError): 250 | commands.Queue.Purge(queue='***') 251 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 3.3.0 (2024-01-12) 5 | ------------------ 6 | - Allow space character in exchange and queue names (#47) 7 | - Convert AMQP timestamp property to handle milliseconds (#48 - `david1155 `_) 8 | - Remove internal must be false check to support RabbitMQ Tracing (#50 - `hari01584 `_) 9 | - Remove usage of deprecated datetime.utcfromtimestamp (#52 - `decaz `_) 10 | 11 | 3.2.1 (2022-09-07) 12 | ------------------ 13 | - Add wheel to distribution format (#43) 14 | 15 | 3.2.0 (2022-06-27) 16 | ------------------ 17 | - Allow long-str to fall back to bytes in case of UnicodeDecodeError (AMQP 1.0 interop) (#40 - `dmaone `_) 18 | - DOMAIN_REGEX enhanced to fulfill tag uri scheme for exchange and queue names. (#42 - `deschmih `_) 19 | 20 | 3.1.0 (2022-01-10) 21 | ------------------ 22 | - Add implicit UTC timezone behavior to the AMQP Basic.Properties timestamp value. (#37 - `RemiCardona `_) 23 | - Add support for short-short-int and short-short-uint. (#33 - `michal800106 `_) 24 | 25 | 3.0.1 (2020-08-07) 26 | ------------------ 27 | - Fix an issue with `Basic.Reject` `requeue=False` always being set to `True` (#29 - `eandersson `_) 28 | 29 | 3.0.0 (2020-08-04) 30 | ------------------ 31 | - Fix unsigned short-int encoding and decoding to use the correct amqp field designation of ``B`` instad of ``b`` (#27) 32 | - Fix timestamp encoding/decoding tests to work when the timezone is set on the host machine (#26) 33 | 34 | 3.0.0a6 (2020-03-19) 35 | -------------------- 36 | - `pamqp.commands.Basic.QoS.globally` renamed back to :attr:`pamqp.commands.Basic.QoS.global_` 37 | - Refactored codegen to put params in the class level docstring not ``__init__`` 38 | - Added new :meth:`pamqp.frame.Frame.validate()` method 39 | - Changed validation to ignore attributes with a value of `None` 40 | - Changed default value behaviors to only use default values if one is specified, instead of by data type. 41 | - Overwrote AMQP spec for queue name max-length to match documented RabbitMQ value (#24) 42 | - Updated documentation in codegen output 43 | - Added strict validation of `pamqp.commands.Basic.Properties.delivery_mode` to ensure it's ``0`` or ``1`` 44 | - Fixed codegen with respect to applying extension data over base spec data 45 | 46 | 3.0.0a5 (2020-03-11) 47 | -------------------- 48 | - Rename `pamqp.frame._frame_parts` to :meth:`pamqp.frame.frame_parts` (#15 again) 49 | - `pamqp.commands.Basic.QoS.global_` renamed to :attr:`pamqp.commands.Basic.QoS.globally` 50 | - Removed mypy checking due to errors in mypy and recursive type aliases 51 | - Added `pamqp/py.typed` for PEP-561 compatibility (#21 - `michael-k `_) 52 | 53 | 3.0.0a4 (2020-01-01) 54 | -------------------- 55 | - Refactor codegen.py 56 | - Revert the behaviors added in 3.0.0a2 with regard to documented defaults and `None` 57 | - Use `amqp0-9-1.extended.xml` instead of `amqp-0-9-1.xml` to get the documentation for RabbitMQ added classes/methods 58 | - Add strict value checking for deprecated values 59 | - Remove empty ``__init__`` functions from method classes 60 | 61 | 3.0.0a3 (2019-12-31) 62 | -------------------- 63 | - Make comparison of Basic.Properties against other object types raise `NotImplementedError` 64 | - Return test coverage to 100% 65 | 66 | 3.0.0a2 (2019-12-31) 67 | -------------------- 68 | - Added mypy as part of the test pipeline and made updates based upon its findings. 69 | - Added length checking and regex checking for values specified in AMQP spec 70 | - Fixed some of the type annotations added in 3.0.0a0 71 | - Fixed some of the documentation and label usage in `pamqp.commands` 72 | - Removed redundant inline documentation in `pamqp.commands` 73 | - Updated default values to only reflect defaults specified in the XML and JSON specs. If no default is specified, the value will now be `None`. 74 | 75 | 3.0.0a1 (2019-12-19) 76 | -------------------- 77 | - Revert the removal of `pamqp.body.ContentBody.name` 78 | 79 | 3.0.0a0 (2019-12-16) 80 | -------------------- 81 | - Update to support Python 3.6+ only 82 | - Add typing annotations to all modules, callables, and classes 83 | - Moved exceptions from `pamqp.specification` to `pamqp.exceptions` 84 | - Moved constants from `pamqp.specification` to `pamqp.constants` 85 | - Moved base classes out of `pamqp.specification` to `pamqp.base` 86 | - Changed the structure of nested classes for AMQP Commands (Classes & Methods) in `pamqp.specification` to functions in `pamqp.commands` 87 | - Renamed `pamqp.specification.ERRORS` to `pamqp.exceptions.CLASS_MAPPING` 88 | - Remove convenience exports of `pamqp.headers.ContentHeader` and `pamqp.header.ProtocolHeader` 89 | - pamqp.body.ContentBody.value now only supports `bytes` 90 | - Changed `pamqp.decode.timestamp` to return a `datetime.datetime` instance instead of `time.struct_time`. 91 | - Updated `pamqp.encode.support_deprecated_rabbitmq()` to allow for toggling support. 92 | - Changed `pamqp.encode.timestamp` to only support `datetime.datetime` and `time.struct_time` values, dropping epoch (`int`) support. 93 | - Removed `pamqp.frame.BasicProperties.to_dict()` in favor of behavior allowing for `dict(pamqp.frame.BasicProperties)` 94 | - Optimized `pamqp.heartbeat.Heartbeat` to marshal the static frame value as a predefined class attribute. 95 | - Add support for `Connection.UpdateSecret` and `Connection.UpdateSecretOk`. 96 | - Removed the ability to unset a `Basic.Property` by invoking `del properties[key]` 97 | - Removed the deprecated `pamqp.codec` sub-package 98 | 99 | 2.3.0 (2019-04-18) 100 | ------------------ 101 | - Add :py:func:`pamqp.encode.support_deprecated_rabbitmq` function to limit data types available when encoding field-tables for older RabbitMQ versions. 102 | 103 | 2.2.0 (2019-04-18) 104 | ------------------ 105 | - Change :py:meth:`pamqp.encode.timestamp` to allow for numeric/epoch timestamps (#14 - `mosquito `_) 106 | - Change :py:meth:`pamqp.frame.frame_parts` to a public method (#15 - `mosquito `_) 107 | - Cleanup of code to pass configured flake8 tests 108 | - Add support for 8-bit unsigned integer values in :py:meth:`pamqp.encode.table_integer` 109 | 110 | 2.1.0 (2018-12-28) 111 | ------------------ 112 | - Change raising a DeprecationWarning exception to using warnings.warn for deprecated AMQP methods (#13 - `dzen `_) 113 | 114 | 2.0.0 (2018-09-11) 115 | ------------------ 116 | - **Change Python versions supported to 2.7 and 3.4+** 117 | - **Always decode field table keys as strings (#6)** 118 | - This may be a breaking change means in Python3 keys will always be type str for short strings. This includes frame 119 | values and field table values. 120 | - In Python 2.7 if a short-string (key, frame field value, etc) has UTF-8 characters in it, it will be a `unicode` object. 121 | - Combine test coverage across all Python versions 122 | - Fix range for signed short integer (#7) 123 | - Fix guards for usage of unsigned short usage in `pamqp.encode` (#7) 124 | - Fix encoding and decoding of unsigned short (#7) 125 | - Add support for unsigned short integer and long integer in field tables (#10) 126 | - Address edge case of small value in long type (#8) 127 | - Address long string encoding inconsistency (#9) 128 | - Cleanup unicode object & conditionals in py3 (#9) 129 | - Add `pamqp.exceptions.PAMQPException` as a base class for pamqp specific exceptions (#4) 130 | - Fix decoding of void values in a field table or array 131 | 132 | 1.6.1 (2015-02-05) 133 | ------------------ 134 | - Fix the encoding guard for unsigned short integers to be 65535 [rabbitpy #62] 135 | 136 | 1.6.0 (2014-12-12) 137 | ------------------ 138 | - Remove UTF-8 encoding from byte_array (#2) 139 | - Fix AMQP Field Tables / `Basic.Properties` headers behavior: 140 | - Field names per spec should not exceed 128 bytes 141 | - long-strings should not be utf-8 encoded (only short-strings *boggle*) 142 | - Ensure that field table long strings are not coerced to UTF-8 as specified in AMQP 0-9-1 143 | If a string is passed in as a long string in a field table and it contains UTF-8 characters it will be UTF-8 encoded 144 | - Move AMQP Methods in specification.py to slotted classes 145 | - Change `Basic.Properties` to a slotted class 146 | - Instead of class level attributes with the same name as obj attributes, prefix class attributes for data types with an underscore 147 | - Add new class method type() for `Basic.Properties` for accessing data type 148 | - Add new class method type() for AMQP methods for accessing data type 149 | - Change `Basic.Properties.attributes` to `Basic.Properties.attributes()`, returning the list of slotted attributes 150 | - Fix a typo for booleans in the method mapping for table decoding 151 | - `Frame.__getitem__` will now raise a KeyError instead of None for an invalid attribute 152 | - `PropertiesBase` no longer checks to see if an attribute is set for contains 153 | - Adds new specification tests 154 | - More efficiently handle the frame end character in Python 3 155 | 156 | 1.5.0 (2014-11-05) 157 | ------------------ 158 | - Cleanup how UTF-8 is handled in decoding strings 159 | - Ensure that field tables (headers property, etc) can use keys with utf-8 data 160 | - Address missing and mis-aligned AMQP-0-9-1 field table decoding with the field type indicators from the RabbitMQ protocol errata page 161 | - Fix a encoding by type bug introduced with 1.4 having to do with bytearrays 162 | - Be explicit about needing a class id in the ContentHeader 163 | - Update the tests to reflect the unicode changes 164 | - Clean up the tests 165 | 166 | 1.4.0 (2014-11-04) 167 | ------------------ 168 | - Fix a long standing bug for non-specified responses for RabbitMQ AMQP extensions 169 | - Refactor adding bytearrays and recoding complexity 170 | - Add bytearray support (#1 and gmr/rabbitpy#48) 171 | - Change encode/decode type errors from ValueError to TypeError exceptions 172 | - Remove separate codecs for Python 2 & 3 173 | - Move codecs from `pamqp.codec.encode` and `pamqp.codec.decode` to `pamqp.encode` and `pamqp.decode` 174 | - Deprecate pamqp.codec 175 | - Remove weird imports from top level __init__.py, not sure what I was thinking there 176 | - Clean up codegen a bit to make it more PYTHON3 compatible 177 | - Update codegen/include for new codec and PYTHON2/PYTHON3 behavior 178 | - Update documentation 179 | - Distribution updates: 180 | - Let travis upload to pypi 181 | - Add wheel distribution 182 | - Update supported python versions 183 | - Update classifiers 184 | 185 | 1.3.1 (2014-02-14) 186 | ------------------ 187 | - Fix encoding of long-long-integers 188 | 189 | 1.3.0 (2014-01-17) 190 | ------------------ 191 | - Remove support for short strings in field tables 192 | 193 | 1.2.4 (2013-12-22) 194 | ------------------ 195 | - Add short-short-int support 196 | 197 | 1.2.3 (2013-12-22) 198 | ------------------ 199 | - Fix distribution requirements 200 | 201 | 1.2.2 (2013-12-22) 202 | ------------------ 203 | - Add decimal data type support 204 | 205 | 1.2.1 (2013-07-29) 206 | ------------------ 207 | - Fix Confirm.Select definition 208 | 209 | 1.2.0 (2013-07-08) 210 | ------------------ 211 | - Add support for Connection.Blocked, Connection.Unblocked 212 | - Add documentation to specification.py in the codegen process 213 | 214 | 1.1.3 (2013-03-27) 215 | ------------------ 216 | - Fix exception creation 217 | 218 | 1.1.2 (2013-03-27) 219 | ------------------ 220 | - Add Confirm.Select, Confirm.SelectOk 221 | 222 | 1.1.1 (2013-03-22) 223 | ------------------ 224 | - Remove debugging print statements (eek) 225 | 226 | 1.1.0 (2013-03-21) 227 | ------------------ 228 | - Add Python 3.3 support 229 | 230 | 1.0.1 (2012-10-02) 231 | ------------------ 232 | - Address Unicode issues 233 | - Add void support in table arrays 234 | 235 | 1.0.0 (2012-09-24) 236 | ------------------ 237 | - Initial version 238 | -------------------------------------------------------------------------------- /pamqp/encode.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Functions for encoding data of various types including field tables and arrays 4 | 5 | """ 6 | import calendar 7 | import datetime 8 | import decimal as _decimal 9 | import logging 10 | import struct 11 | import time 12 | import typing 13 | 14 | from pamqp import common 15 | 16 | LOGGER = logging.getLogger(__name__) 17 | 18 | DEPRECATED_RABBITMQ_SUPPORT = False 19 | """Toggle to support older versions of RabbitMQ.""" 20 | 21 | 22 | def support_deprecated_rabbitmq(enabled: bool = True) -> None: 23 | """Toggle the data types available in field-tables 24 | 25 | If called with `True`, than RabbitMQ versions, the field-table integer 26 | types will not support the full AMQP spec. 27 | 28 | :param enabled: Specify if deprecated RabbitMQ versions are supported 29 | 30 | """ 31 | global DEPRECATED_RABBITMQ_SUPPORT 32 | 33 | DEPRECATED_RABBITMQ_SUPPORT = enabled 34 | 35 | 36 | def by_type(value: common.FieldValue, data_type: str) -> bytes: 37 | """Takes a value of any type and tries to encode it with the specified 38 | encoder. 39 | 40 | :param value: The value to encode 41 | :type value: :const:`pamqp.common.FieldValue` 42 | :param data_type: The data type name to use for encoding 43 | :raises TypeError: when the :data:`data_type` is unknown 44 | 45 | """ 46 | try: 47 | return METHODS[str(data_type)](value) 48 | except KeyError: 49 | raise TypeError('Unknown type: {}'.format(value)) 50 | 51 | 52 | def bit(value: int, byte: int, position: int) -> int: 53 | """Encode a bit value 54 | 55 | :param value: Value to encode 56 | :param byte: The byte to apply the value to 57 | :param position: The position in the byte to set the bit on 58 | 59 | """ 60 | return byte | (value << position) 61 | 62 | 63 | def boolean(value: bool) -> bytes: 64 | """Encode a boolean value 65 | 66 | :param value: Value to encode 67 | :raises TypeError: when the value is not the correct type 68 | 69 | """ 70 | if not isinstance(value, bool): 71 | raise TypeError('bool required, received {}'.format(type(value))) 72 | return common.Struct.short_short_uint.pack(int(value)) 73 | 74 | 75 | def byte_array(value: bytearray) -> bytes: 76 | """Encode a byte array value 77 | 78 | :param value: Value to encode 79 | :raises TypeError: when the value is not the correct type 80 | 81 | """ 82 | if not isinstance(value, bytearray): 83 | raise TypeError('bytearray required, received {}'.format(type(value))) 84 | return common.Struct.integer.pack(len(value)) + value 85 | 86 | 87 | def decimal(value: _decimal.Decimal) -> bytes: 88 | """Encode a decimal.Decimal value 89 | 90 | :param value: Value to encode 91 | :raises TypeError: when the value is not the correct type 92 | 93 | """ 94 | if not isinstance(value, _decimal.Decimal): 95 | raise TypeError('decimal.Decimal required, received {}'.format( 96 | type(value))) 97 | tmp = str(value) 98 | if '.' in tmp: 99 | decimals = len(tmp.split('.')[-1]) 100 | value = value.normalize() 101 | raw = int(value * (_decimal.Decimal(10)**decimals)) 102 | return struct.pack('>Bi', decimals, raw) 103 | return struct.pack('>Bi', 0, int(value)) 104 | 105 | 106 | def double(value: float) -> bytes: 107 | """Encode a floating point value as a double 108 | 109 | :param value: Value to encode 110 | :raises TypeError: when the value is not the correct type 111 | 112 | """ 113 | if not isinstance(value, float): 114 | raise TypeError('float required, received {}'.format(type(value))) 115 | return common.Struct.double.pack(value) 116 | 117 | 118 | def floating_point(value: float) -> bytes: 119 | """Encode a floating point value 120 | 121 | :param value: Value to encode 122 | :raises TypeError: when the value is not the correct type 123 | 124 | """ 125 | if not isinstance(value, float): 126 | raise TypeError('float required, received {}'.format(type(value))) 127 | return common.Struct.float.pack(value) 128 | 129 | 130 | def long_int(value: int) -> bytes: 131 | """Encode a long integer 132 | 133 | :param value: Value to encode 134 | :raises TypeError: when the value is not the correct type or outside the 135 | acceptable range for the data type 136 | 137 | """ 138 | if not isinstance(value, int): 139 | raise TypeError('int required, received {}'.format(type(value))) 140 | elif not (-2147483648 <= value <= 2147483647): 141 | raise TypeError('Long integer range: -2147483648 to 2147483647') 142 | return common.Struct.long.pack(value) 143 | 144 | 145 | def long_uint(value: int) -> bytes: 146 | """Encode a long unsigned integer 147 | 148 | :param value: Value to encode 149 | :raises TypeError: when the value is not the correct type or outside the 150 | acceptable range for the data type 151 | 152 | """ 153 | if not isinstance(value, int): 154 | raise TypeError('int required, received {}'.format(type(value))) 155 | elif not (0 <= value <= 4294967295): 156 | raise TypeError('Long unsigned-integer range: 0 to 4294967295') 157 | return common.Struct.ulong.pack(value) 158 | 159 | 160 | def long_long_int(value: int) -> bytes: 161 | """Encode a long-long int 162 | 163 | :param value: Value to encode 164 | :raises TypeError: when the value is not the correct type or outside the 165 | acceptable range for the data type 166 | 167 | """ 168 | if not isinstance(value, int): 169 | raise TypeError('int required, received {}'.format(type(value))) 170 | elif not (-9223372036854775808 <= value <= 9223372036854775807): 171 | raise TypeError('long-long integer range: ' 172 | '-9223372036854775808 to 9223372036854775807') 173 | return common.Struct.long_long_int.pack(value) 174 | 175 | 176 | def long_string(value: str) -> bytes: 177 | """Encode a "long string" 178 | 179 | :param value: Value to encode 180 | :raises TypeError: when the value is not the correct type 181 | 182 | """ 183 | return _string(common.Struct.integer, value) 184 | 185 | 186 | def octet(value: int) -> bytes: 187 | """Encode an octet value 188 | 189 | :param value: Value to encode 190 | :raises TypeError: when the value is not the correct type 191 | 192 | """ 193 | if not isinstance(value, int): 194 | raise TypeError('int required, received {}'.format(type(value))) 195 | return common.Struct.byte.pack(value) 196 | 197 | 198 | def short_int(value: int) -> bytes: 199 | """Encode a short integer 200 | 201 | :param value: Value to encode 202 | :raises TypeError: when the value is not the correct type or outside the 203 | acceptable range for the data type 204 | 205 | """ 206 | if not isinstance(value, int): 207 | raise TypeError('int required, received {}'.format(type(value))) 208 | elif not (-32768 <= value <= 32767): 209 | raise TypeError('Short integer range: -32678 to 32767') 210 | return common.Struct.short.pack(value) 211 | 212 | 213 | def short_uint(value: int) -> bytes: 214 | """Encode an unsigned short integer 215 | 216 | :param value: Value to encode 217 | :raises TypeError: when the value is not the correct type or outside the 218 | acceptable range for the data type 219 | 220 | """ 221 | if not isinstance(value, int): 222 | raise TypeError('int required, received {}'.format(type(value))) 223 | elif not (0 <= value <= 65535): 224 | raise TypeError('Short unsigned integer range: 0 to 65535') 225 | return common.Struct.ushort.pack(value) 226 | 227 | 228 | def short_string(value: str) -> bytes: 229 | """ Encode a string 230 | 231 | :param value: Value to encode 232 | :raises TypeError: when the value is not the correct type 233 | 234 | """ 235 | return _string(common.Struct.byte, value) 236 | 237 | 238 | def timestamp(value: typing.Union[datetime.datetime, time.struct_time]) \ 239 | -> bytes: 240 | """Encode a datetime.datetime object or time.struct_time 241 | 242 | :param value: Value to encode 243 | :raises TypeError: when the value is not the correct type 244 | 245 | """ 246 | if isinstance(value, datetime.datetime): 247 | if value.tzinfo is None or value.tzinfo.utcoffset(value) is None: 248 | # assume datetime object is UTC 249 | value = value.replace(tzinfo=datetime.timezone.utc) 250 | return common.Struct.timestamp.pack(int(value.timestamp())) 251 | if isinstance(value, time.struct_time): 252 | return common.Struct.timestamp.pack(calendar.timegm(value)) 253 | raise TypeError( 254 | 'datetime.datetime or time.struct_time required, received {}'.format( 255 | type(value))) 256 | 257 | 258 | def field_array(value: common.FieldArray) -> bytes: 259 | """Encode a field array from a list of values 260 | 261 | :param value: Value to encode 262 | :type value: :const:`pamqp.common.FieldArray` 263 | :raises TypeError: when the value is not the correct type 264 | 265 | """ 266 | if not isinstance(value, list): 267 | raise TypeError('list of values required, received {}'.format( 268 | type(value))) 269 | data = [] 270 | for item in value: 271 | data.append(encode_table_value(item)) 272 | output = b''.join(data) 273 | return common.Struct.integer.pack(len(output)) + output 274 | 275 | 276 | def field_table(value: common.FieldTable) -> bytes: 277 | """Encode a field table from a dict 278 | 279 | :param value: Value to encode 280 | :type value: :const:`pamqp.common.FieldTable` 281 | :raises TypeError: when the value is not the correct type 282 | 283 | """ 284 | if not value: # If there is no value, return a standard 4 null bytes 285 | return common.Struct.integer.pack(0) 286 | elif not isinstance(value, dict): 287 | raise TypeError('dict required, received {}'.format(type(value))) 288 | data = [] 289 | for key, value in sorted(value.items()): 290 | if len(key) > 128: # field names have 128 char max 291 | LOGGER.warning('Truncating key %s to 128 bytes', key) 292 | key = key[0:128] 293 | data.append(short_string(key)) 294 | try: 295 | data.append(encode_table_value(value)) 296 | except TypeError as err: 297 | raise TypeError('{} error: {}/'.format(key, err)) 298 | output = b''.join(data) 299 | return common.Struct.integer.pack(len(output)) + output 300 | 301 | 302 | def table_integer(value: int) -> bytes: 303 | """Determines the best type of numeric type to encode value as, preferring 304 | the smallest data size first. 305 | 306 | :param value: Value to encode 307 | :raises TypeError: when the value is not the correct type or outside the 308 | acceptable range for the data type 309 | 310 | """ 311 | if DEPRECATED_RABBITMQ_SUPPORT: 312 | return _deprecated_table_integer(value) 313 | if -128 <= value <= 127: 314 | return b'b' + octet(value) 315 | elif -32768 <= value <= 32767: 316 | return b's' + short_int(value) 317 | elif 0 <= value <= 65535: 318 | return b'u' + short_uint(value) 319 | elif -2147483648 <= value <= 2147483647: 320 | return b'I' + long_int(value) 321 | elif 0 <= value <= 4294967295: 322 | return b'i' + long_uint(value) 323 | elif -9223372036854775808 <= value <= 9223372036854775807: 324 | return b'l' + long_long_int(value) 325 | raise TypeError('Unsupported numeric value: {}'.format(value)) 326 | 327 | 328 | def _deprecated_table_integer(value: int) -> bytes: 329 | """Determines the best type of numeric type to encode value as, preferring 330 | the smallest data size first, supporting versions of RabbitMQ < 3.6 331 | 332 | :param value: Value to encode 333 | :raises TypeError: when the value is not the correct type or outside the 334 | acceptable range for the data type 335 | 336 | """ 337 | if -128 <= value <= 127: 338 | return b'b' + octet(value) 339 | elif -32768 <= value <= 32767: 340 | return b's' + short_int(value) 341 | elif -2147483648 <= value <= 2147483647: 342 | return b'I' + long_int(value) 343 | elif -9223372036854775808 <= value <= 9223372036854775807: 344 | return b'l' + long_long_int(value) 345 | raise TypeError('Unsupported numeric value: {}'.format(value)) 346 | 347 | 348 | def _string(encoder: struct.Struct, value: str) -> bytes: 349 | """Reduce a small amount of duplication in string handling 350 | 351 | :raises: TypeError 352 | 353 | """ 354 | if not isinstance(value, str): 355 | raise TypeError('str required, received {}'.format(type(value))) 356 | temp = value.encode('utf-8') 357 | return encoder.pack(len(temp)) + temp 358 | 359 | 360 | def encode_table_value( 361 | value: typing.Union[common.FieldArray, common.FieldTable, 362 | common.FieldValue] 363 | ) -> bytes: 364 | """Takes a value of any type and tries to encode it with the proper encoder 365 | 366 | :param value: Value to encode 367 | :type value: :const:`pamqp.common.FieldArray` or 368 | :const:`pamqp.common.FieldTable` or 369 | :const:`pamqp.common.FieldValue` 370 | :raises TypeError: when the type of the value is not supported 371 | 372 | """ 373 | if isinstance(value, bool): 374 | return b't' + boolean(value) 375 | elif isinstance(value, int): 376 | return table_integer(value) 377 | elif isinstance(value, _decimal.Decimal): 378 | return b'D' + decimal(value) 379 | elif isinstance(value, float): 380 | return b'f' + floating_point(value) 381 | elif isinstance(value, str): 382 | return b'S' + long_string(value) 383 | elif isinstance(value, (datetime.datetime, time.struct_time)): 384 | return b'T' + timestamp(value) 385 | elif isinstance(value, dict): 386 | return b'F' + field_table(value) 387 | elif isinstance(value, list): 388 | return b'A' + field_array(value) 389 | elif isinstance(value, bytearray): 390 | return b'x' + byte_array(value) 391 | elif value is None: 392 | return b'V' 393 | raise TypeError('Unknown type: {} ({!r})'.format(type(value), value)) 394 | 395 | 396 | METHODS = { 397 | 'bytearray': byte_array, 398 | 'double': double, 399 | 'field_array': field_array, 400 | 'long': long_uint, 401 | 'longlong': long_long_int, 402 | 'longstr': long_string, 403 | 'octet': octet, 404 | 'short': short_uint, 405 | 'shortstr': short_string, 406 | 'table': field_table, 407 | 'timestamp': timestamp, 408 | 'void': lambda _: None, 409 | } 410 | -------------------------------------------------------------------------------- /pamqp/decode.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Functions for decoding data of various types including field tables and arrays 4 | 5 | """ 6 | import datetime 7 | import decimal as _decimal 8 | import typing 9 | 10 | from pamqp import common 11 | 12 | 13 | def by_type(value: bytes, 14 | data_type: str, 15 | offset: int = 0) -> typing.Tuple[int, common.FieldValue]: 16 | """Decodes values using the specified type 17 | 18 | :param value: The binary value to decode 19 | :param data_type: The data type name of the value 20 | :param offset: The starting position of the data in the byte stream 21 | :rtype: :class:`tuple` (:class:`int`, :const:`pamqp.common.FieldValue`) 22 | :raises ValueError: when the data type is unknown 23 | 24 | """ 25 | if data_type == 'bit': 26 | return bit(value, offset) 27 | decoder = METHODS.get(data_type) 28 | if decoder is None: 29 | raise ValueError('Unknown type: {}'.format(data_type)) 30 | return decoder(value) 31 | 32 | 33 | def bit(value: bytes, position: int) -> typing.Tuple[int, bool]: 34 | """Decode a bit value, returning bytes consumed and the value. 35 | 36 | :param value: The binary value to decode 37 | :param position: The position in the byte of the bit value 38 | :rtype: :class:`tuple` (:class:`int`, :class:`bool`) 39 | :raises ValueError: when the binary data can not be unpacked 40 | 41 | """ 42 | bit_buffer = common.Struct.byte.unpack_from(value)[0] 43 | try: 44 | return 0, (bit_buffer & (1 << position)) != 0 45 | except TypeError: 46 | raise ValueError('Could not unpack bit value') 47 | 48 | 49 | def boolean(value: bytes) -> typing.Tuple[int, bool]: 50 | """Decode a boolean value, returning bytes consumed and the value. 51 | 52 | :param value: The binary value to decode 53 | :rtype: :class:`tuple` (:class:`int`, :class:`bool`) 54 | :raises ValueError: when the binary data can not be unpacked 55 | 56 | """ 57 | try: 58 | return 1, bool(common.Struct.byte.unpack_from(value[0:1])[0]) 59 | except TypeError: 60 | raise ValueError('Could not unpack boolean value') 61 | 62 | 63 | def byte_array(value: bytes) -> typing.Tuple[int, bytearray]: 64 | """Decode a byte_array value, returning bytes consumed and the value. 65 | 66 | :param value: The binary value to decode 67 | :rtype: :class:`tuple` (:class:`int`, :class:`bytearray`) 68 | :raises ValueError: when the binary data can not be unpacked 69 | 70 | """ 71 | try: 72 | length = common.Struct.integer.unpack(value[0:4])[0] 73 | return length + 4, bytearray(value[4:length + 4]) 74 | except TypeError: 75 | raise ValueError('Could not unpack byte array value') 76 | 77 | 78 | def decimal(value: bytes) -> typing.Tuple[int, _decimal.Decimal]: 79 | """Decode a decimal value, returning bytes consumed and the value. 80 | 81 | :param value: The binary value to decode 82 | :rtype: :class:`tuple` (:class:`int`, :class:`decimal.Decimal`) 83 | :raises ValueError: when the binary data can not be unpacked 84 | 85 | """ 86 | try: 87 | decimals = common.Struct.byte.unpack(value[0:1])[0] 88 | raw = common.Struct.integer.unpack(value[1:5])[0] 89 | return 5, _decimal.Decimal(raw) * (_decimal.Decimal(10)**-decimals) 90 | except TypeError: 91 | raise ValueError('Could not unpack decimal value') 92 | 93 | 94 | def double(value: bytes) -> typing.Tuple[int, float]: 95 | """Decode a double value, returning bytes consumed and the value. 96 | 97 | :param value: The binary value to decode 98 | :rtype: :class:`tuple` (:class:`int`, :class:`float`) 99 | :raises ValueError: when the binary data can not be unpacked 100 | 101 | """ 102 | try: 103 | return 8, common.Struct.double.unpack_from(value)[0] 104 | except TypeError: 105 | raise ValueError('Could not unpack double value') 106 | 107 | 108 | def floating_point(value: bytes) -> typing.Tuple[int, float]: 109 | """Decode a floating point value, returning bytes consumed and the value. 110 | 111 | :param value: The binary value to decode 112 | :rtype: :class:`tuple` (:class:`int`, :class:`float`) 113 | :raises ValueError: when the binary data can not be unpacked 114 | 115 | """ 116 | try: 117 | return 4, common.Struct.float.unpack_from(value)[0] 118 | except TypeError: 119 | raise ValueError('Could not unpack floating point value') 120 | 121 | 122 | def long_int(value: bytes) -> typing.Tuple[int, int]: 123 | """Decode a long integer value, returning bytes consumed and the value. 124 | 125 | :param value: The binary value to decode 126 | :rtype: :class:`tuple` (:class:`int`, :class:`int`) 127 | :raises ValueError: when the binary data can not be unpacked 128 | 129 | """ 130 | try: 131 | return 4, common.Struct.long.unpack(value[0:4])[0] 132 | except TypeError: 133 | raise ValueError('Could not unpack long integer value') 134 | 135 | 136 | def long_uint(value: bytes) -> typing.Tuple[int, int]: 137 | """Decode an unsigned long integer value, returning bytes consumed and 138 | the value. 139 | 140 | :param value: The binary value to decode 141 | :rtype: :class:`tuple` (:class:`int`, :class:`int`) 142 | :raises ValueError: when the binary data can not be unpacked 143 | 144 | """ 145 | try: 146 | return 4, common.Struct.ulong.unpack(value[0:4])[0] 147 | except TypeError: 148 | raise ValueError('Could not unpack unsigned long integer value') 149 | 150 | 151 | def long_long_int(value: bytes) -> typing.Tuple[int, int]: 152 | """Decode a long-long integer value, returning bytes consumed and the 153 | value. 154 | 155 | :param value: The binary value to decode 156 | :rtype: :class:`tuple` (:class:`int`, :class:`int`) 157 | :raises ValueError: when the binary data can not be unpacked 158 | 159 | """ 160 | try: 161 | return 8, common.Struct.long_long_int.unpack(value[0:8])[0] 162 | except TypeError: 163 | raise ValueError('Could not unpack long-long integer value') 164 | 165 | 166 | def long_str(value: bytes) -> typing.Tuple[int, typing.Union[str, bytes]]: 167 | """Decode a string value, returning bytes consumed and the value. 168 | 169 | :param value: The binary value to decode 170 | :rtype: :class:`tuple` (:class:`int`, :class:`str`) 171 | :raises ValueError: when the binary data can not be unpacked 172 | 173 | """ 174 | try: 175 | length = common.Struct.integer.unpack(value[0:4])[0] 176 | return length + 4, value[4:length + 4].decode('utf-8') 177 | except TypeError: 178 | raise ValueError('Could not unpack long string value') 179 | except UnicodeDecodeError: 180 | return length + 4, value[4:length + 4] 181 | 182 | 183 | def octet(value: bytes) -> typing.Tuple[int, int]: 184 | """Decode an octet value, returning bytes consumed and the value. 185 | 186 | :param value: The binary value to decode 187 | :rtype: :class:`tuple` (:class:`int`, :class:`int`) 188 | :raises ValueError: when the binary data can not be unpacked 189 | 190 | """ 191 | try: 192 | return 1, common.Struct.byte.unpack(value[0:1])[0] 193 | except TypeError: 194 | raise ValueError('Could not unpack octet value') 195 | 196 | 197 | def short_int(value: bytes) -> typing.Tuple[int, int]: 198 | """Decode a short integer value, returning bytes consumed and the value. 199 | 200 | :param value: The binary value to decode 201 | :rtype: :class:`tuple` (:class:`int`, :class:`int`) 202 | :raises ValueError: when the binary data can not be unpacked 203 | 204 | """ 205 | try: 206 | return 2, common.Struct.short.unpack_from(value[0:2])[0] 207 | except TypeError: 208 | raise ValueError('Could not unpack short integer value') 209 | 210 | 211 | def short_uint(value: bytes) -> typing.Tuple[int, int]: 212 | """Decode an unsigned short integer value, returning bytes consumed and 213 | the value. 214 | 215 | :param value: The binary value to decode 216 | :rtype: :class:`tuple` (:class:`int`, :class:`int`) 217 | :raises ValueError: when the binary data can not be unpacked 218 | 219 | """ 220 | try: 221 | return 2, common.Struct.ushort.unpack_from(value[0:2])[0] 222 | except TypeError: 223 | raise ValueError('Could not unpack unsigned short integer value') 224 | 225 | 226 | def short_short_int(value: bytes) -> typing.Tuple[int, int]: 227 | """Decode a short-short integer value, returning bytes consumed and the 228 | value. 229 | 230 | :param value: The binary value to decode 231 | :rtype: :class:`tuple` (:class:`int`, :class:`int`) 232 | :raises ValueError: when the binary data can not be unpacked 233 | 234 | """ 235 | try: 236 | return 1, common.Struct.short_short_int.unpack_from(value[0:1])[0] 237 | except TypeError: 238 | raise ValueError('Could not unpack short-short integer value') 239 | 240 | 241 | def short_short_uint(value: bytes) -> typing.Tuple[int, int]: 242 | """Decode a unsigned short-short integer value, returning bytes consumed 243 | and the value. 244 | 245 | :param value: The binary value to decode 246 | :rtype: :class:`tuple` (:class:`int`, :class:`int`) 247 | :raises ValueError: when the binary data can not be unpacked 248 | 249 | """ 250 | try: 251 | return 1, common.Struct.short_short_uint.unpack_from(value[0:1])[0] 252 | except TypeError: 253 | raise ValueError('Could not unpack unsigned short-short integer value') 254 | 255 | 256 | def short_str(value: bytes) -> typing.Tuple[int, str]: 257 | """Decode a string value, returning bytes consumed and the value. 258 | 259 | :param value: The binary value to decode 260 | :rtype: :class:`tuple` (:class:`int`, :class:`str`) 261 | :raises ValueError: when the binary data can not be unpacked 262 | 263 | """ 264 | try: 265 | length = common.Struct.byte.unpack(value[0:1])[0] 266 | return length + 1, value[1:length + 1].decode('utf-8') 267 | except TypeError: 268 | raise ValueError('Could not unpack short string value') 269 | 270 | 271 | def timestamp(value: bytes) -> typing.Tuple[int, datetime.datetime]: 272 | """Decode a timestamp value, returning bytes consumed and the value. 273 | 274 | :param value: The binary value to decode 275 | :rtype: :class:`tuple` (:class:`int`, :class:`datetime.datetime`) 276 | :raises ValueError: when the binary data can not be unpacked 277 | 278 | """ 279 | try: 280 | temp = common.Struct.timestamp.unpack(value[0:8]) 281 | ts_value = temp[0] 282 | 283 | # Anything above the year 2106 is likely milliseconds 284 | if ts_value > 0xFFFFFFFF: 285 | ts_value /= 1000.0 286 | 287 | return 8, datetime.datetime.fromtimestamp(ts_value, 288 | tz=datetime.timezone.utc) 289 | except TypeError: 290 | raise ValueError('Could not unpack timestamp value') 291 | 292 | 293 | def embedded_value(value: bytes) -> typing.Tuple[int, common.FieldValue]: 294 | """Dynamically decode a value based upon the starting byte 295 | 296 | :param value: The binary value to decode 297 | :rtype: :class:`tuple` (:class:`int`, :const:`pamqp.common.FieldValue`) 298 | :raises ValueError: when the binary data can not be unpacked 299 | 300 | """ 301 | if not value: 302 | return 0, None 303 | try: 304 | bytes_consumed, temp = TABLE_MAPPING[value[0:1]](value[1:]) 305 | except KeyError: 306 | raise ValueError('Unknown type: {!r}'.format(value[:1])) 307 | return bytes_consumed + 1, temp 308 | 309 | 310 | def field_array(value: bytes) -> typing.Tuple[int, common.FieldArray]: 311 | """Decode a field array value, returning bytes consumed and the value. 312 | 313 | :param value: The binary value to decode 314 | :rtype: :class:`tuple` (:class:`int`, :const:`pamqp.common.FieldArray`) 315 | :raises ValueError: when the binary data can not be unpacked 316 | 317 | """ 318 | try: 319 | length = common.Struct.integer.unpack(value[0:4])[0] 320 | offset = 4 321 | data = [] 322 | field_array_end = offset + length 323 | while offset < field_array_end: 324 | consumed, result = embedded_value(value[offset:]) 325 | offset += consumed 326 | data.append(result) 327 | return offset, data 328 | except TypeError: 329 | raise ValueError('Could not unpack data') 330 | 331 | 332 | def field_table(value: bytes) -> typing.Tuple[int, common.FieldTable]: 333 | """Decode a field array value, returning bytes consumed and the value. 334 | 335 | :param value: The binary value to decode 336 | :rtype: :class:`tuple` (:class:`int`, :const:`pamqp.common.FieldTable`) 337 | :raises ValueError: when the binary data can not be unpacked 338 | 339 | """ 340 | try: 341 | length = common.Struct.integer.unpack(value[0:4])[0] 342 | offset = 4 343 | data = {} 344 | field_table_end = offset + length 345 | while offset < field_table_end: 346 | key_length = common.Struct.byte.unpack_from(value, offset)[0] 347 | offset += 1 348 | key = value[offset:offset + key_length].decode('utf-8') 349 | offset += key_length 350 | consumed, result = embedded_value(value[offset:]) 351 | offset += consumed 352 | data[key] = result 353 | return field_table_end, data 354 | except TypeError: 355 | raise ValueError('Could not unpack data') 356 | 357 | 358 | def void(_: bytes) -> typing.Tuple[int, None]: 359 | """Return a void, no data to decode 360 | 361 | :param _: The empty bytes object to ignore 362 | :rtype: :class:`tuple` (:class:`int`, :const:`None`) 363 | 364 | """ 365 | return 0, None 366 | 367 | 368 | METHODS = { 369 | 'array': field_array, 370 | 'bit': bit, 371 | 'boolean': boolean, 372 | 'byte_array': byte_array, 373 | 'decimal': decimal, 374 | 'double': double, 375 | 'float': floating_point, 376 | 'long': long_uint, 377 | 'longlong': long_long_int, 378 | 'longstr': long_str, 379 | 'octet': octet, 380 | 'short': short_uint, 381 | 'shortstr': short_str, 382 | 'table': field_table, 383 | 'timestamp': timestamp, 384 | 'void': void, 385 | } # Define a data type mapping to methods for by_type() 386 | 387 | # See https://www.rabbitmq.com/amqp-0-9-1-errata.html 388 | TABLE_MAPPING = { 389 | b't': boolean, 390 | b'b': short_short_int, 391 | b'B': short_short_uint, 392 | b's': short_int, 393 | b'u': short_uint, 394 | b'I': long_int, 395 | b'i': long_uint, 396 | b'l': long_long_int, 397 | b'L': long_long_int, 398 | b'f': floating_point, 399 | b'd': double, 400 | b'D': decimal, 401 | b'S': long_str, 402 | b'A': field_array, 403 | b'T': timestamp, 404 | b'F': field_table, 405 | b'V': void, 406 | b'\x00': void, # While not documented, have seen this in the wild 407 | b'x': byte_array, 408 | } # Define a mapping for use in `field_array()` and `field_table()` 409 | -------------------------------------------------------------------------------- /tests/test_encoding.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | import datetime 3 | import decimal 4 | import unittest 5 | 6 | from pamqp import encode 7 | 8 | 9 | class MarshalingTests(unittest.TestCase): 10 | def test_encode_bool_wrong_type(self): 11 | self.assertRaises(TypeError, encode.boolean, 'hi') 12 | 13 | def test_encode_bool_false(self): 14 | self.assertEqual(encode.boolean(False), b'\x00') 15 | 16 | def test_encode_bool_true(self): 17 | self.assertEqual(encode.boolean(True), b'\x01') 18 | 19 | def test_encode_byte_array(self): 20 | self.assertEqual(encode.byte_array(bytearray([65, 66, 67])), 21 | b'\x00\x00\x00\x03ABC') 22 | 23 | def test_encode_byte_array_wrong_type(self): 24 | self.assertRaises(TypeError, encode.byte_array, b'ABC') 25 | 26 | def test_encode_decimal_wrong_type(self): 27 | self.assertRaises(TypeError, encode.decimal, 3.141597) 28 | 29 | def test_encode_decimal(self): 30 | self.assertEqual(encode.decimal(decimal.Decimal('3.14159')), 31 | b'\x05\x00\x04\xcb/') 32 | 33 | def test_encode_decimal_whole(self): 34 | self.assertEqual(encode.decimal(decimal.Decimal('314159')), 35 | b'\x00\x00\x04\xcb/') 36 | 37 | def test_encode_double_invalid_value(self): 38 | self.assertRaises(TypeError, encode.double, '1234') 39 | 40 | def test_encode_double(self): 41 | self.assertEqual(encode.double(float(3.14159)), 42 | b'@\t!\xf9\xf0\x1b\x86n') 43 | 44 | def test_encode_floating_point_type(self): 45 | self.assertRaises(TypeError, encode.floating_point, '1234') 46 | 47 | def test_encode_float(self): 48 | self.assertEqual(encode.floating_point(float(3.14159)), b'@I\x0f\xd0') 49 | 50 | def test_encode_long_int_wrong_type(self): 51 | self.assertRaises(TypeError, encode.long_int, 3.141597) 52 | 53 | def test_encode_table_integer_bad_value_error(self): 54 | self.assertRaises(TypeError, encode.long_int, 9223372036854775808) 55 | 56 | def test_encode_long_int(self): 57 | self.assertEqual(encode.long_int(2147483647), 58 | b'\x7f\xff\xff\xff') 59 | 60 | def test_encode_long_int_error(self): 61 | self.assertRaises(TypeError, encode.long_int, 21474836449) 62 | 63 | def test_encode_long_uint(self): 64 | self.assertEqual(encode.long_uint(4294967295), 65 | b'\xff\xff\xff\xff') 66 | 67 | def test_encode_long_uint_error(self): 68 | self.assertRaises(TypeError, encode.long_uint, 4294967296) 69 | 70 | def test_encode_long_uint_wrong_type(self): 71 | self.assertRaises(TypeError, encode.long_uint, 3.141597) 72 | 73 | def test_encode_long_long_int_wrong_type(self): 74 | self.assertRaises(TypeError, encode.long_long_int, 3.141597) 75 | 76 | def test_encode_long_long_int_error(self): 77 | self.assertRaises(TypeError, encode.long_long_int, 78 | 9223372036854775808) 79 | 80 | def test_encode_octet(self): 81 | self.assertEqual(encode.octet(1), b'\x01') 82 | 83 | def test_encode_octet_error(self): 84 | self.assertRaises(TypeError, encode.octet, 'hi') 85 | 86 | def test_encode_short_wrong_type(self): 87 | self.assertRaises(TypeError, encode.short_int, 3.141597) 88 | 89 | def test_encode_short(self): 90 | self.assertEqual(encode.short_int(32767), b'\x7f\xff') 91 | 92 | def test_encode_short_error(self): 93 | self.assertRaises(TypeError, encode.short_int, 32768) 94 | 95 | def test_encode_short_uint(self): 96 | self.assertEqual(encode.short_uint(65535), b'\xff\xff') 97 | 98 | def test_encode_short_uint_error(self): 99 | self.assertRaises(TypeError, encode.short_uint, 65536) 100 | 101 | def test_encode_short_uint_type_error(self): 102 | self.assertRaises(TypeError, encode.short_uint, 'hello') 103 | 104 | def test_encode_table_integer_error(self): 105 | self.assertRaises(TypeError, encode.table_integer, 9223372036854775808) 106 | 107 | def test_encode_short_string(self): 108 | self.assertEqual(encode.short_string('Hello'), b'\x05Hello') 109 | 110 | def test_encode_short_string_error(self): 111 | self.assertRaises(TypeError, encode.short_string, 32768) 112 | 113 | def test_encode_short_string_utf8_python3(self): 114 | self.assertEqual(encode.short_string('🐰'), b'\x04\xf0\x9f\x90\xb0') 115 | 116 | def test_encode_long_string(self): 117 | self.assertEqual(encode.long_string('0123456789'), 118 | b'\x00\x00\x00\n0123456789') 119 | 120 | def test_encode_long_string_bytes(self): 121 | self.assertEqual(encode.long_string('rabbitmq'), 122 | b'\x00\x00\x00\x08rabbitmq') 123 | 124 | def test_encode_long_string_utf8_python3(self): 125 | self.assertEqual(encode.long_string('🐰'), 126 | b'\x00\x00\x00\x04\xf0\x9f\x90\xb0') 127 | 128 | def test_encode_long_string_error(self): 129 | self.assertRaises(TypeError, encode.long_string, 100) 130 | 131 | def test_encode_timestamp_from_datetime(self): 132 | self.assertEqual( 133 | encode.timestamp(datetime.datetime( 134 | 2006, 11, 21, 16, 30, 10, tzinfo=datetime.timezone.utc)), 135 | b'\x00\x00\x00\x00Ec)\x92') 136 | 137 | def test_encode_timestamp_from_struct_time(self): 138 | value = encode.timestamp( 139 | datetime.datetime( 140 | 2006, 11, 21, 16, 30, 10, 141 | tzinfo=datetime.timezone.utc).timetuple()) 142 | self.assertEqual(value, b'\x00\x00\x00\x00Ec)\x92') 143 | 144 | def test_encode_timestamp_error(self): 145 | self.assertRaises(TypeError, encode.timestamp, 'hi') 146 | 147 | def test_encode_field_array(self): 148 | expectation = (b'\x00\x00\x00:b\x01u\xaf\xc8I\x02bZ\x00S\x00\x00\x00' 149 | b'\x04TestT\x00\x00\x00\x00Ec)\x92I\xbb\x9a\xca\x00D' 150 | b'\x02\x00\x00\x01:f@H\xf5\xc3i\xc4e5\xffl\x80\x00\x00' 151 | b'\x00\x00\x00\x00\x08') 152 | data = [ 153 | 1, 45000, 40000000, 'Test', 154 | datetime.datetime( 155 | 2006, 11, 21, 16, 30, 10, tzinfo=datetime.timezone.utc), 156 | -1147483648, 157 | decimal.Decimal('3.14'), 3.14, 158 | 3294967295, -9223372036854775800 159 | ] 160 | self.assertEqual(encode.field_array(data), expectation) 161 | 162 | def test_encode_field_array_error(self): 163 | self.assertRaises(TypeError, encode.field_array, 'hi') 164 | 165 | def test_encode_field_table_empty(self): 166 | self.assertEqual(encode.field_table(None), b'\x00\x00\x00\x00') 167 | 168 | def test_encode_field_table_type_error(self): 169 | self.assertRaises(TypeError, encode.field_table, [1, 2, 3]) 170 | 171 | def test_encode_field_table_object(self): 172 | self.assertRaises(TypeError, encode.field_table, 173 | {'key': encode.field_table}) 174 | 175 | def test_encode_field_table(self): 176 | expectation = (b"\x00\x00\x04'\x08arrayvalA\x00\x00\x00\x06b\x01b" 177 | b'\x02b\x03\x07boolvalt\x01\tbytearrayx\x00\x00\x00' 178 | b'\x03AAA\x06decvalD\x02\x00\x00\x01:\x07dictvalF\x00' 179 | b'\x00\x00\x0c\x03fooS\x00\x00\x00\x03bar\x08floatval' 180 | b'f@H\xf5\xc3\x06intvalb\x01\x07longstrS\x00\x00\x03t' 181 | b'000000000000000000000000000000000000000000000000000' 182 | b'011111111111111111111111111111111111111111111111111' 183 | b'112222222222222222222222222222222222222222222222222' 184 | b'222111111111111111111111111111111111111111111111111' 185 | b'111122222222222222222222222222222222222222222222222' 186 | b'222221111111111111111111111111111111111111111111111' 187 | b'111111222222222222222222222222222222222222222222222' 188 | b'222222211111111111111111111111111111111111111111111' 189 | b'111111112222222222222222222222222222222222222222222' 190 | b'222222222111111111111111111111111111111111111111111' 191 | b'111111111122222222222222222222222222222222222222222' 192 | b'222222222221111111111111111111111111111111111111111' 193 | b'111111111111222222222222222222222222222222222222222' 194 | b'222222222222211111111111111111111111111111111111111' 195 | b'111111111111112222222222222222222222222222222222222' 196 | b'222222222222222111111111111111111111111111111111111' 197 | b'111111111111111100000000000000000000000000000000000' 198 | b'00000000000000000\x07longvalI6e&U\x04noneV\x06strva' 199 | b'lS\x00\x00\x00\x04Test\x0ctimestampvalT\x00\x00\x00' 200 | b'\x00Ec)\x92') 201 | data = { 202 | 'intval': 1, 203 | 'strval': 'Test', 204 | 'boolval': True, 205 | 'timestampval': datetime.datetime( 206 | 2006, 11, 21, 16, 30, 10, tzinfo=datetime.timezone.utc), 207 | 'decval': decimal.Decimal('3.14'), 208 | 'floatval': 3.14, 209 | 'longval': 912598613, 210 | 'dictval': { 211 | 'foo': 'bar' 212 | }, 213 | 'arrayval': [1, 2, 3], 214 | 'none': None, 215 | 'bytearray': bytearray((65, 65, 65)), 216 | 'longstr': ('0000000000000000000000000000000000000000000000000' 217 | '0001111111111111111111111111111111111111111111111' 218 | '1111112222222222222222222222222222222222222222222' 219 | '2222222221111111111111111111111111111111111111111' 220 | '1111111111112222222222222222222222222222222222222' 221 | '2222222222222221111111111111111111111111111111111' 222 | '1111111111111111112222222222222222222222222222222' 223 | '2222222222222222222221111111111111111111111111111' 224 | '1111111111111111111111112222222222222222222222222' 225 | '2222222222222222222222222221111111111111111111111' 226 | '1111111111111111111111111111112222222222222222222' 227 | '2222222222222222222222222222222221111111111111111' 228 | '1111111111111111111111111111111111112222222222222' 229 | '2222222222222222222222222222222222222221111111111' 230 | '1111111111111111111111111111111111111111112222222' 231 | '2222222222222222222222222222222222222222222221111' 232 | '1111111111111111111111111111111111111111111111110' 233 | '0000000000000000000000000000000000000000000000000' 234 | '00') 235 | } 236 | self.assertEqual(encode.field_table(data), expectation) 237 | 238 | def test_encode_by_type_field_array(self): 239 | expectation = (b'\x00\x00\x008b\x01sBhu\xaf\xc8S\x00\x00\x00\x04TestT' 240 | b'\x00\x00\x00\x00Ec)\x92I\xbb\x9a\xca\x00D\x02\x00' 241 | b'\x00\x01:f@H\xf5\xc3i\xc4e5\xffl\x80\x00\x00\x00\x00' 242 | b'\x00\x00\x08') 243 | data = [ 244 | 1, 17000, 45000, 'Test', 245 | datetime.datetime( 246 | 2006, 11, 21, 16, 30, 10, tzinfo=datetime.timezone.utc), 247 | -1147483648, 248 | decimal.Decimal('3.14'), 3.14, 249 | 3294967295, -9223372036854775800 250 | ] 251 | self.assertEqual(encode.by_type(data, 'field_array'), expectation) 252 | 253 | def test_encode_by_type_byte_array(self): 254 | self.assertEqual(encode.by_type(bytearray((65, 66, 67)), 'bytearray'), 255 | b'\x00\x00\x00\x03ABC') 256 | 257 | def test_encode_by_type_double(self): 258 | self.assertEqual(encode.by_type(float(4294967295), 'double'), 259 | b'A\xef\xff\xff\xff\xe0\x00\x00') 260 | 261 | def test_encode_by_type_long_uint(self): 262 | self.assertEqual(encode.by_type(4294967295, 'long'), 263 | b'\xff\xff\xff\xff') 264 | 265 | def test_encode_by_type_long_long_int(self): 266 | self.assertEqual(encode.by_type(9223372036854775800, 'longlong'), 267 | b'\x7f\xff\xff\xff\xff\xff\xff\xf8') 268 | 269 | def test_encode_by_type_long_str(self): 270 | self.assertEqual(encode.by_type('0123456789', 'longstr'), 271 | b'\x00\x00\x00\n0123456789') 272 | 273 | def test_encode_by_type_none(self): 274 | self.assertEqual(encode.by_type(None, 'void'), None) 275 | 276 | def test_encode_by_type_octet(self): 277 | self.assertEqual(encode.by_type(1, 'octet'), b'\x01') 278 | 279 | def test_encode_by_type_short(self): 280 | self.assertEqual(encode.by_type(32767, 'short'), b'\x7f\xff') 281 | 282 | def test_encode_by_type_timestamp(self): 283 | self.assertEqual( 284 | encode.by_type( 285 | datetime.datetime(2006, 11, 21, 16, 30, 10, 286 | tzinfo=datetime.timezone.utc), 'timestamp'), 287 | b'\x00\x00\x00\x00Ec)\x92') 288 | 289 | def test_encode_by_type_field_table(self): 290 | expectation = (b'\x00\x00\x04B\x08arrayvalA\x00\x00\x00\x08b\x01s\x10' 291 | b'`u\xa4\x10\x07boolvalt\x01\x06decvalD\x02\x00\x00\x01' 292 | b':\x07dictvalF\x00\x00\x00\x0c\x03fooS\x00\x00\x00\x03' 293 | b'bar\x08floatvalf@H\xf5\xc3\x07longstrS\x00\x00\x03t00' 294 | b'00000000000000000000000000000000000000000000000000111' 295 | b'11111111111111111111111111111111111111111111111112222' 296 | b'22222222222222222222222222222222222222222222222211111' 297 | b'11111111111111111111111111111111111111111111111222222' 298 | b'22222222222222222222222222222222222222222222221111111' 299 | b'11111111111111111111111111111111111111111111122222222' 300 | b'22222222222222222222222222222222222222222222111111111' 301 | b'11111111111111111111111111111111111111111112222222222' 302 | b'22222222222222222222222222222222222222222211111111111' 303 | b'11111111111111111111111111111111111111111222222222222' 304 | b'22222222222222222222222222222222222222221111111111111' 305 | b'11111111111111111111111111111111111111122222222222222' 306 | b'22222222222222222222222222222222222222111111111111111' 307 | b'11111111111111111111111111111111111112222222222222222' 308 | b'22222222222222222222222222222222222211111111111111111' 309 | b'11111111111111111111111111111111111000000000000000000' 310 | b'0000000000000000000000000000000000\x07longvalI6e&U' 311 | b'\x06s32intI\xc4e6\x00\x06s64intl7\x82\xda\xce\x9d\x90' 312 | b'\x00\x00\x06strvalS\x00\x00\x00\x04Test\x0ctimestamp' 313 | b'valT\x00\x00\x00\x00Ec)\x92\x06u16ints \x00\x06u32int' 314 | b'i\xeek(\x00\x05u8bitb ') 315 | data = { 316 | 'u8bit': 32, 317 | 'u16int': 8192, 318 | 's32int': -1000000000, 319 | 'u32int': 4000000000, 320 | 's64int': 4000000000000000000, 321 | 'strval': 'Test', 322 | 'boolval': True, 323 | 'timestampval': datetime.datetime( 324 | 2006, 11, 21, 16, 30, 10, tzinfo=datetime.timezone.utc), 325 | 'decval': decimal.Decimal('3.14'), 326 | 'floatval': 3.14, 327 | 'longval': 912598613, 328 | 'dictval': { 329 | 'foo': 'bar' 330 | }, 331 | 'arrayval': [1, 4192, 42000], 332 | 'longstr': ('0000000000000000000000000000000000000000000000000' 333 | '0001111111111111111111111111111111111111111111111' 334 | '1111112222222222222222222222222222222222222222222' 335 | '2222222221111111111111111111111111111111111111111' 336 | '1111111111112222222222222222222222222222222222222' 337 | '2222222222222221111111111111111111111111111111111' 338 | '1111111111111111112222222222222222222222222222222' 339 | '2222222222222222222221111111111111111111111111111' 340 | '1111111111111111111111112222222222222222222222222' 341 | '2222222222222222222222222221111111111111111111111' 342 | '1111111111111111111111111111112222222222222222222' 343 | '2222222222222222222222222222222221111111111111111' 344 | '1111111111111111111111111111111111112222222222222' 345 | '2222222222222222222222222222222222222221111111111' 346 | '1111111111111111111111111111111111111111112222222' 347 | '2222222222222222222222222222222222222222222221111' 348 | '1111111111111111111111111111111111111111111111110' 349 | '0000000000000000000000000000000000000000000000000' 350 | '00') 351 | } 352 | self.assertEqual(encode.by_type(data, 'table'), expectation) 353 | 354 | def test_encode_by_type_error(self): 355 | self.assertRaises(TypeError, encode.by_type, 12345.12434, 'foo') 356 | 357 | 358 | class EncodeTableIntegerTestCase(unittest.TestCase): 359 | 360 | def setUp(self): 361 | encode.support_deprecated_rabbitmq(False) 362 | 363 | def tearDown(self): 364 | encode.support_deprecated_rabbitmq(False) 365 | 366 | def test_table_integer(self): 367 | tests = { 368 | 'short-short': (32, b'b '), 369 | 'short': (1024, b's\x04\x00'), 370 | 'short-negative': (-1024, b's\xfc\x00'), 371 | 'short-unsigned': (32768, b'u\x80\x00'), 372 | 'long': (65536, b'I\x00\x01\x00\x00'), 373 | 'long-negative': (65536, b'I\x00\x01\x00\x00'), 374 | 'long-unsigned': (4294967295, b'i\xff\xff\xff\xff'), 375 | 'long-long': (9223372036854775805, 376 | b'l\x7f\xff\xff\xff\xff\xff\xff\xfd'), 377 | } 378 | for key, value in tests.items(): 379 | result = encode.table_integer(value[0]) 380 | self.assertEqual(result, value[1], 381 | 'encode {} mismatch ({!r} != {!r})'.format( 382 | key, result, value[1])) 383 | 384 | def test_deprecated_table_integer(self): 385 | self.assertFalse(encode.DEPRECATED_RABBITMQ_SUPPORT) 386 | encode.support_deprecated_rabbitmq(True) 387 | self.assertTrue(encode.DEPRECATED_RABBITMQ_SUPPORT) 388 | tests = { 389 | 'short-short': (32, b'b '), 390 | 'short': (1024, b's\x04\x00'), 391 | 'short-negative': (-1024, b's\xfc\x00'), 392 | 'long': (65536, b'I\x00\x01\x00\x00'), 393 | 'long-negative': (65536, b'I\x00\x01\x00\x00'), 394 | 'long-long': (2147483648, b'l\x00\x00\x00\x00\x80\x00\x00\x00'), 395 | } 396 | for key, value in tests.items(): 397 | result = encode.table_integer(value[0]) 398 | self.assertEqual( 399 | result, value[1], 400 | 'encode {} mismatch of {!r} ({!r} != {!r})'.format( 401 | key, value[0], result, value[1])) 402 | 403 | def test_too_large_int(self): 404 | self.assertFalse(encode.DEPRECATED_RABBITMQ_SUPPORT) 405 | with self.assertRaises(TypeError): 406 | encode.table_integer(9223372036854775809) 407 | encode.support_deprecated_rabbitmq(True) 408 | self.assertTrue(encode.DEPRECATED_RABBITMQ_SUPPORT) 409 | with self.assertRaises(TypeError): 410 | encode.table_integer(9223372036854775809) 411 | -------------------------------------------------------------------------------- /tests/test_decoding.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | import datetime 3 | import decimal 4 | import struct 5 | import unittest 6 | 7 | from pamqp import decode 8 | 9 | PLATFORM_32BIT = (struct.calcsize('P') * 8) == 32 10 | PLATFORM_64BIT = (struct.calcsize('P') * 8) == 64 11 | 12 | 13 | class CodecDecodeTests(unittest.TestCase): 14 | FIELD_ARR = (b'\x00\x00\x009b\x01u\xaf\xc8S\x00\x00\x00\x08Test \xe2\x9c' 15 | b'\x88T\x00\x00\x00\x00Ec)\x92I\xbb\x9a\xca\x00D\x02\x00\x00' 16 | b'\x01:f@H\xf5\xc3i\xc4e5\xffl\x80\x00\x00\x00\x00\x00\x00' 17 | b'\x08') 18 | FIELD_ARR_VALUE = [ 19 | 1, 45000, 'Test ✈', 20 | datetime.datetime(2006, 11, 21, 16, 30, 10, 21 | tzinfo=datetime.timezone.utc), -1147483648, 22 | decimal.Decimal('3.14'), 3.14, 3294967295, -9223372036854775800 23 | ] 24 | FIELD_TBL = ( 25 | b'\x00\x00\x00\x99\x08arrayvalA\x00\x00\x00\x06b\x01b\x02b\x03\x07' 26 | b'boolvalt\x01\x06decvalD\x02\x00\x00\x01:\x07dictvalF\x00\x00\x00' 27 | b'\r\x04f\xe2\x9c\x89S\x00\x00\x00\x03\xe2\x9c\x90\x08floatvalf@H' 28 | b'\xf5\xc3\x06intvalb\x01\x07longvalI6e&U\x06strvalS\x00\x00\x00\x08' 29 | b'Test \xe2\x9c\x88\x0ctimestampvalT\x00\x00\x00\x00Ec)\x92\x04\xf0' 30 | b'\x9f\x90\xb0V' 31 | ) 32 | FIELD_TBL_VALUE = { 33 | 'intval': 1, 34 | 'strval': 'Test ✈', 35 | 'boolval': True, 36 | 'timestampval': datetime.datetime(2006, 11, 21, 16, 30, 10, 37 | tzinfo=datetime.timezone.utc), 38 | 'decval': decimal.Decimal('3.14'), 39 | '🐰': None, 40 | 'floatval': 3.14, 41 | 'longval': 912598613, 42 | 'dictval': { 43 | 'f✉': '✐' 44 | }, 45 | 'arrayval': [1, 2, 3] 46 | } 47 | 48 | def test_decode_by_type_invalid_data_type(self): 49 | self.assertRaises(ValueError, decode.by_type, b'Z\x00', b'foobar') 50 | 51 | def test_decode_bit_bytes_consumed(self): 52 | self.assertEqual(decode.bit(b'\xff', 4)[0], 0) 53 | 54 | def test_decode_invalid_value(self): 55 | self.assertRaises(ValueError, decode.bit, b'\xff', None) 56 | 57 | def test_decode_bit_on(self): 58 | self.assertTrue(decode.bit(b'\xff', 4)[1]) 59 | 60 | def test_decode_bit_off(self): 61 | self.assertFalse(decode.bit(b'\x0f', 4)[1]) 62 | 63 | def test_decode_boolean_bytes_consumed(self): 64 | self.assertEqual(decode.boolean(b'\x01')[0], 1) 65 | 66 | def test_decode_boolean_false(self): 67 | self.assertFalse(decode.boolean(b'\x00')[1]) 68 | 69 | def test_decode_boolean_false_data_type(self): 70 | self.assertIsInstance(decode.boolean(b'\x00')[1], bool) 71 | 72 | def test_decode_boolean_invalid_value(self): 73 | self.assertRaises(ValueError, decode.boolean, None) 74 | 75 | def test_decode_boolean_true(self): 76 | self.assertTrue(decode.boolean(b'\x01')[1]) 77 | 78 | def test_decode_boolean_true_data_type(self): 79 | self.assertIsInstance(decode.boolean(b'\x01')[1], bool) 80 | 81 | def test_decode_byte_array(self): 82 | self.assertEqual(decode.byte_array(b'\x00\x00\x00\x03ABC'), 83 | (7, bytearray([65, 66, 67]))) 84 | 85 | def test_decode_byte_array_invalid_value(self): 86 | self.assertRaises(ValueError, decode.byte_array, None) 87 | 88 | def test_decode_decimal_value_bytes_consumed(self): 89 | value = b'\x05\x00\x04\xcb/' 90 | self.assertEqual(decode.decimal(value)[0], len(value)) 91 | 92 | def test_decode_decimal_value_data_type(self): 93 | value = b'\x05\x00\x04\xcb/' 94 | self.assertIsInstance(decode.decimal(value)[1], decimal.Decimal) 95 | 96 | def test_decode_decimal_value(self): 97 | value = b'\x05\x00\x04\xcb/' 98 | self.assertEqual(round(float(decode.decimal(value)[1]), 5), 99 | round(float(decimal.Decimal('3.14159')), 5)) 100 | 101 | def test_decode_decimal_invalid_value(self): 102 | self.assertRaises(ValueError, decode.decimal, False) 103 | 104 | def test_decode_double_value(self): 105 | value = b'@\t!\xf9\xf0\x1b\x86n' 106 | self.assertEqual(round(decode.double(value)[1], 5), 107 | round(float(3.14159), 5)) 108 | 109 | def test_decode_double_invalid_value(self): 110 | self.assertRaises(ValueError, decode.double, 123) 111 | 112 | def test_decode_embedded_value_null(self): 113 | self.assertEqual(decode.embedded_value(b'\00')[1], None) 114 | 115 | def test_decode_embedded_value_invalid_data(self): 116 | self.assertRaises(ValueError, decode.embedded_value, b'Z\x00') 117 | 118 | def test_decode_floating_point_bytes_consumed(self): 119 | value = b'@I\x0f\xd0' 120 | self.assertEqual(decode.floating_point(value)[0], 4) 121 | 122 | def test_decode_floating_point_data_type(self): 123 | value = b'@I\x0f\xd0' 124 | self.assertIsInstance(decode.floating_point(value)[1], float) 125 | 126 | def test_decode_floating_point_invalid_value(self): 127 | self.assertRaises(ValueError, decode.floating_point, False) 128 | 129 | def test_decode_floating_point_value(self): 130 | value = b'@I\x0f\xd0' 131 | self.assertEqual(round(decode.floating_point(value)[1], 5), 132 | round(float(3.14159), 5)) 133 | 134 | def test_decode_long_int_bytes_consumed(self): 135 | value = b'\x7f\xff\xff\xff' 136 | self.assertEqual(decode.long_int(value)[0], 4) 137 | 138 | def test_decode_long_int_data_type(self): 139 | value = b'\x7f\xff\xff\xff' 140 | self.assertIsInstance(decode.long_int(value)[1], int) 141 | 142 | def test_decode_long_int_invalid_value(self): 143 | self.assertRaises(ValueError, decode.long_int, None) 144 | 145 | def test_decode_long_int_value(self): 146 | value = b'\x7f\xff\xff\xff' 147 | self.assertEqual(decode.long_int(value)[1], 2147483647) 148 | 149 | def test_decode_long_long_int_bytes_consumed(self): 150 | value = b'\x7f\xff\xff\xff\xff\xff\xff\xf8' 151 | self.assertEqual(decode.long_long_int(value)[0], 8) 152 | 153 | @unittest.skipIf(PLATFORM_32BIT, 'Skipped on 32-bit platforms') 154 | def test_decode_long_long_int_data_type_64bit(self): 155 | value = b'\x7f\xff\xff\xff\xff\xff\xff\xf8' 156 | self.assertIsInstance(decode.long_long_int(value)[1], int) 157 | 158 | @unittest.skipIf(PLATFORM_64BIT, 'Skipped on 64-bit platforms') 159 | def test_decode_long_long_int_data_type_32bit(self): 160 | value = b'\x7f\xff\xff\xff\xff\xff\xff\xf8' 161 | self.assertIsInstance(decode.long_long_int(value)[1], int) 162 | 163 | def test_decode_long_long_int_invalid_value(self): 164 | self.assertRaises(ValueError, decode.long_long_int, None) 165 | 166 | def test_decode_long_long_int_value(self): 167 | value = b'\x7f\xff\xff\xff\xff\xff\xff\xf8' 168 | self.assertEqual(decode.long_long_int(value)[1], 9223372036854775800) 169 | 170 | def test_decode_long_str_bytes_consumed(self): 171 | value = b'\x00\x00\x00\n0123456789' 172 | self.assertEqual(decode.long_str(value)[0], 14) 173 | 174 | def test_decode_long_str_data_type(self): 175 | value = b'\x00\x00\x00\n0123456789' 176 | self.assertIsInstance(decode.long_str(value)[1], str) 177 | 178 | def test_decode_long_str_data_type_unicode(self): 179 | value = b'\0\0\0\x0c\xd8\xa7\xd8\xae\xd8\xaa\xd8\xa8\xd8\xa7\xd8\xb1' 180 | self.assertIsInstance(decode.long_str(value)[1], str) 181 | 182 | def test_decode_long_str_data_type_non_unicode(self): 183 | value = b'\x00\x00\x00\x01\xff' 184 | self.assertIsInstance(decode.long_str(value)[1], bytes) 185 | 186 | def test_decode_long_str_invalid_value(self): 187 | self.assertRaises(ValueError, decode.long_str, None) 188 | 189 | def test_decode_long_str_value(self): 190 | value = b'\x00\x00\x00\n0123456789' 191 | self.assertEqual(decode.long_str(value)[1], '0123456789') 192 | 193 | def test_decode_octet_bytes_consumed(self): 194 | value = b'\xff' 195 | self.assertEqual(decode.octet(value)[0], 1) 196 | 197 | def test_decode_octet_data_type(self): 198 | value = b'\xff' 199 | self.assertIsInstance(decode.octet(value)[1], int) 200 | 201 | def test_decode_octet_invalid_value(self): 202 | self.assertRaises(ValueError, decode.octet, None) 203 | 204 | def test_decode_octet_value(self): 205 | value = b'\xff' 206 | self.assertEqual(decode.octet(value)[1], 255) 207 | 208 | def test_decode_short_int_bytes_consumed(self): 209 | value = b'\x7f\xff' 210 | self.assertEqual(decode.short_int(value)[0], 2) 211 | 212 | def test_decode_short_int_data_type(self): 213 | value = b'\x7f\xff' 214 | self.assertIsInstance(decode.short_int(value)[0], int) 215 | 216 | def test_decode_short_int_invalid_value(self): 217 | self.assertRaises(ValueError, decode.short_int, None) 218 | 219 | def test_decode_short_int_value(self): 220 | value = b'\x7f\xff' 221 | self.assertEqual(decode.short_int(value)[1], 32767) 222 | 223 | def test_decode_short_short_int_data_type(self): 224 | self.assertIsInstance(decode.short_short_int(b'\xff')[0], int) 225 | 226 | def test_decode_short_short_int_invalid_value(self): 227 | self.assertRaises(ValueError, decode.short_short_int, None) 228 | 229 | def test_decode_short_short_uint_value(self): 230 | self.assertEqual(decode.short_short_uint(b'\xff')[1], 255) 231 | 232 | def test_decode_short_short_uint_invalid_value(self): 233 | self.assertRaises(ValueError, decode.short_short_uint, None) 234 | 235 | def test_decode_short_str_bytes_consumed(self): 236 | self.assertEqual(decode.short_str(b'\n0123456789')[0], 11) 237 | 238 | def test_decode_short_str_data_type(self): 239 | self.assertIsInstance(decode.short_str(b'\n0123456789')[1], str) 240 | 241 | def test_decode_short_str_invalid_value(self): 242 | self.assertRaises(ValueError, decode.short_str, None) 243 | 244 | def test_decode_short_str_value(self): 245 | self.assertEqual(decode.short_str(b'\n0123456789')[1], '0123456789') 246 | 247 | def test_decode_timestamp_bytes_consumed(self): 248 | self.assertEqual(decode.timestamp(b'\x00\x00\x00\x00Ec)\x92')[0], 8) 249 | 250 | def test_decode_timestamp_data_type(self): 251 | self.assertIsInstance( 252 | decode.timestamp(b'\x00\x00\x00\x00Ec)\x92')[1], datetime.datetime) 253 | 254 | def test_decode_timestamp_invalid_value(self): 255 | self.assertRaises(ValueError, decode.timestamp, None) 256 | 257 | def test_decode_timestamp_value(self): 258 | self.assertEqual( 259 | decode.timestamp(b'\x00\x00\x00\x00Ec)\x92')[1], 260 | datetime.datetime(2006, 11, 21, 16, 30, 10, 261 | tzinfo=datetime.timezone.utc)) 262 | 263 | def test_decode_field_array_bytes_consumed(self): 264 | self.assertEqual( 265 | decode.field_array(self.FIELD_ARR)[0], len(self.FIELD_ARR)) 266 | 267 | def test_decode_field_array_data_type(self): 268 | self.assertIsInstance(decode.field_array(self.FIELD_ARR)[1], list) 269 | 270 | def test_decode_field_array_invalid_value(self): 271 | self.assertRaises(ValueError, decode.field_array, None) 272 | 273 | def test_decode_field_array_value(self): 274 | value = decode.field_array(self.FIELD_ARR)[1] 275 | for position in range(0, len(value)): 276 | if isinstance(value[position], float): 277 | self.assertAlmostEqual( 278 | round(value[position], 3), 279 | round(self.FIELD_ARR_VALUE[position], 3)) 280 | else: 281 | self.assertEqual(value[position], 282 | self.FIELD_ARR_VALUE[position]) 283 | 284 | def test_decode_field_table_bytes_consumed(self): 285 | self.assertEqual( 286 | decode.field_table(self.FIELD_TBL)[0], len(self.FIELD_TBL)) 287 | 288 | def test_decode_field_table_data_type(self): 289 | self.assertIsInstance(decode.field_table(self.FIELD_TBL)[1], dict) 290 | 291 | def test_decode_field_table_invalid_value(self): 292 | self.assertRaises(ValueError, decode.field_table, None) 293 | 294 | def test_decode_field_table_value(self): 295 | value = decode.field_table(self.FIELD_TBL)[1] 296 | for key in self.FIELD_TBL_VALUE.keys(): 297 | if isinstance(value[key], float): 298 | self.assertAlmostEqual(round(value[key], 3), 299 | round(self.FIELD_TBL_VALUE[key], 3)) 300 | else: 301 | self.assertEqual(value[key], self.FIELD_TBL_VALUE[key]) 302 | 303 | def test_decode_by_type_bit_bytes_consumed(self): 304 | self.assertEqual(decode.by_type(b'\xff', 'bit', 4)[0], 0) 305 | 306 | def test_decode_by_type_invalid_value(self): 307 | self.assertRaises(ValueError, decode.by_type, b'\xff', 'bit', None) 308 | 309 | def test_decode_by_type_bit_on(self): 310 | self.assertTrue(decode.by_type(b'\xff', 'bit', 4)[1]) 311 | 312 | def test_decode_by_type_bit_off(self): 313 | self.assertFalse(decode.by_type(b'\x0f', 'bit', 4)[1]) 314 | 315 | def test_decode_by_type_boolean_bytes_consumed(self): 316 | self.assertEqual(decode.by_type(b'\x01', 'boolean')[0], 1) 317 | 318 | def test_decode_by_type_boolean_false(self): 319 | self.assertFalse(decode.by_type(b'\x00', 'boolean')[1]) 320 | 321 | def test_decode_by_type_boolean_false_data_type(self): 322 | self.assertIsInstance(decode.by_type(b'\x00', 'boolean')[1], bool) 323 | 324 | def test_decode_by_type_boolean_invalid_value(self): 325 | self.assertRaises(ValueError, decode.by_type, None, 'boolean') 326 | 327 | def test_decode_by_type_boolean_true(self): 328 | self.assertTrue(decode.by_type(b'\x01', 'boolean')[1]) 329 | 330 | def test_decode_by_type_boolean_true_data_type(self): 331 | self.assertIsInstance(decode.by_type(b'\x01', 'boolean')[1], bool) 332 | 333 | def test_decode_by_type_byte_array_bytes_consumed(self): 334 | value = b'\x00\x00\x00\t123456789' 335 | self.assertEqual(decode.by_type(value, 'byte_array')[0], 13) 336 | 337 | def test_decode_by_type_byte_array_data_type(self): 338 | value = b'\x00\x00\x00\t123456789' 339 | self.assertIsInstance( 340 | decode.by_type(value, 'byte_array')[1], bytearray) 341 | 342 | def test_decode_by_type_byte_array_value(self): 343 | value = b'\x00\x00\x00\t123456789' 344 | self.assertEqual( 345 | decode.by_type(value, 'byte_array')[1], bytearray(b'123456789')) 346 | 347 | def test_decode_by_type_decimal_bytes_consumed(self): 348 | value = b'\x05\x00\x04\xcb/' 349 | self.assertEqual(decode.by_type(value, 'decimal')[0], len(value)) 350 | 351 | def test_decode_by_type_decimal_data_type(self): 352 | self.assertIsInstance( 353 | decode.by_type(b'\x05\x00\x04\xcb/', 'decimal')[1], 354 | decimal.Decimal) 355 | 356 | def test_decode_by_type_decimal_value(self): 357 | self.assertEqual( 358 | round(float(decode.by_type(b'\x05\x00\x04\xcb/', 'decimal')[1]), 359 | 5), round(float(decimal.Decimal('3.14159')), 5)) 360 | 361 | def test_decode_by_type_decimal_invalid_value(self): 362 | self.assertRaises(ValueError, decode.by_type, False, 'decimal') 363 | 364 | def test_decode_by_type_double_data_type(self): 365 | value = b'C\x0f\xd8\x91\x14\xb9\xc3\x98' 366 | self.assertIsInstance(decode.by_type(value, 'double')[1], float) 367 | 368 | def test_decode_by_type_double_bytes_consumed(self): 369 | value = b'C\x0f\xd8\x91\x14\xb9\xc3\x98' 370 | self.assertEqual(decode.by_type(value, 'double')[0], 8) 371 | 372 | def test_decode_by_type_double_value(self): 373 | value = b'C\x0f\xd8\x91\x14\xb9\xc3\x98' 374 | self.assertEqual( 375 | decode.by_type(value, 'double')[1], 1120480238450803.0) 376 | 377 | def test_decode_by_type_floating_point_data_type(self): 378 | self.assertIsInstance(decode.by_type(b'@I\x0f\xd0', 'float')[1], float) 379 | 380 | def test_decode_by_type_float_bytes_consumed(self): 381 | self.assertEqual(decode.by_type(b'@I\x0f\xd0', 'float')[0], 4) 382 | 383 | def test_decode_by_type_floating_point_invalid_value(self): 384 | self.assertRaises(ValueError, decode.by_type, False, 'float') 385 | 386 | def test_decode_by_type_floating_point_value(self): 387 | value = b'@I\x0f\xd0' 388 | self.assertEqual(round(decode.by_type(value, 'float')[1], 5), 389 | round(float(3.14159), 5)) 390 | 391 | def test_decode_by_type_long_bytes_consumed(self): 392 | value = b'\x7f\xff\xff\xff' 393 | self.assertEqual(decode.by_type(value, 'long')[0], 4) 394 | 395 | def test_decode_by_type_long_data_type(self): 396 | value = b'\x7f\xff\xff\xff' 397 | self.assertIsInstance(decode.by_type(value, 'long')[1], int) 398 | 399 | def test_decode_by_type_long_invalid_value(self): 400 | self.assertRaises(ValueError, decode.by_type, None, 'long') 401 | 402 | def test_decode_by_type_long_value(self): 403 | value = b'\x7f\xff\xff\xff' 404 | self.assertEqual(decode.by_type(value, 'long')[1], 2147483647) 405 | 406 | def test_decode_by_type_long_long_bytes_consumed(self): 407 | value = b'\x7f\xff\xff\xff\xff\xff\xff\xf8' 408 | self.assertEqual(decode.by_type(value, 'longlong')[0], 8) 409 | 410 | @unittest.skipIf(PLATFORM_64BIT, 'Skipped on 64-bit platforms') 411 | def test_decode_by_type_long_long_data_type_32bit(self): 412 | value = b'\x7f\xff\xff\xff\xff\xff\xff\xf8' 413 | self.assertIsInstance(decode.by_type(value, 'longlong')[1], int) 414 | 415 | @unittest.skipIf(PLATFORM_32BIT, 'Skipped on 32-bit platforms') 416 | def test_decode_by_type_long_long_data_type_64bit(self): 417 | value = b'\x7f\xff\xff\xff\xff\xff\xff\xf8' 418 | self.assertIsInstance(decode.by_type(value, 'longlong')[1], int) 419 | 420 | def test_decode_by_type_long_long_invalid_value(self): 421 | self.assertRaises(ValueError, decode.by_type, None, 'longlong') 422 | 423 | def test_decode_by_type_long_long_value(self): 424 | value = b'\x7f\xff\xff\xff\xff\xff\xff\xf8' 425 | self.assertEqual( 426 | decode.by_type(value, 'longlong')[1], 9223372036854775800) 427 | 428 | def test_decode_by_type_longstr_bytes_consumed(self): 429 | value = b'\x00\x00\x00\n0123456789' 430 | self.assertEqual(decode.by_type(value, 'longstr')[0], 14) 431 | 432 | def test_decode_by_type_longstr_data_type_with_unicode(self): 433 | value = b'\x00\x00\x00\x08Test \xe2\x9c\x88' 434 | self.assertIsInstance(decode.by_type(value, 'longstr')[1], str) 435 | 436 | def test_decode_by_type_longstr_data_type(self): 437 | value = b'\x00\x00\x00\n0123456789' 438 | self.assertIsInstance(decode.by_type(value, 'longstr')[1], str) 439 | 440 | def test_decode_by_type_longstr_invalid_value(self): 441 | self.assertRaises(ValueError, decode.by_type, None, 'longstr') 442 | 443 | def test_decode_by_type_longstr_value(self): 444 | value = b'\x00\x00\x00\n0123456789' 445 | self.assertEqual(decode.by_type(value, 'longstr')[1], '0123456789') 446 | 447 | def test_decode_by_type_octet_bytes_consumed(self): 448 | self.assertEqual(decode.by_type(b'\xff', 'octet')[0], 1) 449 | 450 | def test_decode_by_type_octet_data_type(self): 451 | self.assertIsInstance(decode.by_type(b'\xff', 'octet')[1], int) 452 | 453 | def test_decode_by_type_octet_invalid_value(self): 454 | self.assertRaises(ValueError, decode.by_type, None, 'octet') 455 | 456 | def test_decode_by_type_octet_value(self): 457 | self.assertEqual(decode.by_type(b'\xff', 'octet')[1], 255) 458 | 459 | def test_decode_by_type_short_bytes_consumed(self): 460 | value = b'\x7f\xff' 461 | self.assertEqual(decode.by_type(value, 'short')[0], 2) 462 | 463 | def test_decode_by_type_short_data_type(self): 464 | value = b'\x7f\xff' 465 | self.assertIsInstance(decode.by_type(value, 'short')[1], int) 466 | 467 | def test_decode_by_type_short_invalid_value(self): 468 | self.assertRaises(ValueError, decode.by_type, None, 'short') 469 | 470 | def test_decode_by_type_short_value(self): 471 | value = b'\x7f\xff' 472 | self.assertEqual(decode.by_type(value, 'short')[1], 32767) 473 | 474 | def test_decode_by_type_timestamp_bytes_consumed(self): 475 | value = b'\x00\x00\x00\x00Ec)\x92' 476 | self.assertEqual(decode.by_type(value, 'timestamp')[0], 8) 477 | 478 | def test_decode_by_type_timestamp_data_type(self): 479 | value = b'\x00\x00\x00\x00Ec)\x92' 480 | self.assertIsInstance( 481 | decode.by_type(value, 'timestamp')[1], datetime.datetime) 482 | 483 | def test_decode_by_type_timestamp_invalid_value(self): 484 | self.assertRaises(ValueError, decode.by_type, None, 'timestamp') 485 | 486 | def test_decode_by_type_timestamp_value(self): 487 | value = b'\x00\x00\x00\x00Ec)\x92' 488 | self.assertEqual( 489 | decode.by_type(value, 'timestamp')[1], 490 | datetime.datetime(2006, 11, 21, 16, 30, 10, 491 | tzinfo=datetime.timezone.utc)) 492 | 493 | def test_decode_by_type_void(self): 494 | self.assertIsNone(decode.by_type(b'', 'void')[1]) 495 | 496 | def test_decode_by_type_field_array_bytes_consumed(self): 497 | self.assertEqual( 498 | decode.by_type(self.FIELD_ARR, 'array')[0], len(self.FIELD_ARR)) 499 | 500 | def test_decode_by_type_field_array_data_type(self): 501 | self.assertIsInstance(decode.by_type(self.FIELD_ARR, 'array')[1], list) 502 | 503 | def test_decode_by_type_field_array_invalid_value(self): 504 | self.assertRaises(ValueError, decode.by_type, None, 'array') 505 | 506 | def test_decode_by_type_field_array_value(self): 507 | value = decode.by_type(self.FIELD_ARR, 'array')[1] 508 | for position in range(0, len(value)): 509 | if isinstance(value[position], float): 510 | self.assertAlmostEqual( 511 | round(value[position], 3), 512 | round(self.FIELD_ARR_VALUE[position], 3)) 513 | else: 514 | self.assertEqual( 515 | value[position], self.FIELD_ARR_VALUE[position]) 516 | 517 | def test_decode_by_type_field_table_bytes_consumed(self): 518 | self.assertEqual( 519 | decode.by_type(self.FIELD_TBL, 'table')[0], len(self.FIELD_TBL)) 520 | 521 | def test_decode_by_type_field_table_data_type(self): 522 | self.assertIsInstance(decode.by_type(self.FIELD_TBL, 'table')[1], dict) 523 | 524 | def test_decode_by_type_field_table_invalid_value(self): 525 | self.assertRaises(ValueError, decode.by_type, None, 'table') 526 | 527 | def test_decode_by_type_field_table_value(self): 528 | value = decode.by_type(self.FIELD_TBL, 'table')[1] 529 | for key in self.FIELD_TBL_VALUE.keys(): 530 | if isinstance(value[key], float): 531 | self.assertAlmostEqual( 532 | round(value[key], 3), round(self.FIELD_TBL_VALUE[key], 3)) 533 | else: 534 | self.assertEqual(value[key], self.FIELD_TBL_VALUE[key]) 535 | 536 | def test_decode_embedded_value_empty_bytes_consumed(self): 537 | self.assertEqual(decode.embedded_value(b'')[0], 0) 538 | 539 | def test_decode_embedded_value_empty_value(self): 540 | self.assertEqual(decode.embedded_value(b'')[1], None) 541 | 542 | def test_decode_embedded_value_decimal_bytes_consumed(self): 543 | value = b'D\x05\x00\x04\xcb/' 544 | self.assertEqual(decode.embedded_value(value)[0], len(value)) 545 | 546 | def test_decode_embedded_value_decimal_data_type(self): 547 | value = b'D\x05\x00\x04\xcb/' 548 | self.assertIsInstance( 549 | decode.embedded_value(value)[1], decimal.Decimal) 550 | 551 | def test_decode_embedded_value_decimal_value(self): 552 | value = b'D\x05\x00\x04\xcb/' 553 | self.assertEqual(round(float(decode.embedded_value(value)[1]), 5), 554 | round(float(decimal.Decimal('3.14159')), 5)) 555 | 556 | def test_decode_embedded_value_double_bytes_consumed(self): 557 | value = b'dC\x0f\xd8\x91\x14\xb9\xc3\x98' 558 | self.assertEqual(decode.embedded_value(value)[0], len(value)) 559 | 560 | def test_decode_embedded_value_double_data_type(self): 561 | value = b'dC\x0f\xd8\x91\x14\xb9\xc3\x98' 562 | self.assertIsInstance(decode.embedded_value(value)[1], float) 563 | 564 | def test_decode_embedded_value_double_value(self): 565 | value = b'dC\x0f\xd8\x91\x14\xb9\xc3\x98' 566 | self.assertEqual(decode.embedded_value(value)[1], 1120480238450803.0) 567 | 568 | def test_decode_embedded_value_long_bytes_consumed(self): 569 | value = b'I\x7f\xff\xff\xff' 570 | self.assertEqual(decode.embedded_value(value)[0], 5) 571 | 572 | def test_decode_embedded_value_long_data_type(self): 573 | value = b'I\x7f\xff\xff\xff' 574 | self.assertIsInstance(decode.embedded_value(value)[1], int) 575 | 576 | def test_decode_embedded_value_long_value(self): 577 | value = b'I\x7f\xff\xff\xff' 578 | self.assertEqual(decode.embedded_value(value)[1], 2147483647) 579 | 580 | def test_decode_embedded_value_long_uint_bytes_consumed(self): 581 | value = b'i\xff\xff\xff\xff' 582 | self.assertEqual(decode.embedded_value(value)[0], 5) 583 | 584 | def test_decode_embedded_value_long_uint_data_type(self): 585 | value = b'i\xff\xff\xff\xff' 586 | self.assertIsInstance(decode.embedded_value(value)[1], int) 587 | 588 | def test_decode_embedded_value_long_uint_value(self): 589 | value = b'i\xff\xff\xff\xff' 590 | self.assertEqual(decode.embedded_value(value)[1], 4294967295) 591 | 592 | def test_decode_embedded_value_long_long_bytes_consumed(self): 593 | value = b'l\x7f\xff\xff\xff\xff\xff\xff\xf8' 594 | self.assertEqual(decode.embedded_value(value)[0], 9) 595 | 596 | @unittest.skipIf(PLATFORM_32BIT, 'Skipped on 32-bit platforms') 597 | def test_decode_embedded_value_long_long_data_type_64bit(self): 598 | value = b'l\x7f\xff\xff\xff\xff\xff\xff\xf8' 599 | self.assertIsInstance(decode.embedded_value(value)[1], int) 600 | 601 | @unittest.skipIf(PLATFORM_64BIT, 'Skipped on 64-bit platforms') 602 | def test_decode_embedded_value_long_long_data_type_32bit(self): 603 | value = b'l\x7f\xff\xff\xff\xff\xff\xff\xf8' 604 | self.assertIsInstance(decode.embedded_value(value)[1], int) 605 | 606 | def test_decode_embedded_value_long_long_value(self): 607 | value = b'l\x7f\xff\xff\xff\xff\xff\xff\xf8' 608 | self.assertEqual(decode.embedded_value(value)[1], 9223372036854775800) 609 | 610 | def test_decode_embedded_value_longstr_bytes_consumed(self): 611 | value = b'S\x00\x00\x00\n0123456789' 612 | self.assertEqual(decode.embedded_value(value)[0], 15) 613 | 614 | def test_decode_embedded_value_longstr_data_type(self): 615 | value = b'S\x00\x00\x00\n0123456789' 616 | self.assertIsInstance(decode.embedded_value(value)[1], str) 617 | 618 | def test_decode_embedded_value_byte_array_data(self): 619 | value = b'x\x00\x00\x00\x03ABC' 620 | self.assertEqual( 621 | decode.embedded_value(value)[1], bytearray([65, 66, 67])) 622 | 623 | def test_decode_embedded_value_byte_array_data_type(self): 624 | value = b'x\x00\x00\x00\x03ABC' 625 | self.assertIsInstance(decode.embedded_value(value)[1], bytearray) 626 | 627 | def test_decode_embedded_value_longstr_data_type_unicode(self): 628 | value = (b'S\x00\x00\x00\x0c\xd8\xa7\xd8\xae\xd8\xaa\xd8\xa8\xd8' 629 | b'\xa7\xd8\xb1') 630 | self.assertIsInstance(decode.embedded_value(value)[1], str) 631 | 632 | def test_decode_embedded_value_longstr_value(self): 633 | value = b'S\x00\x00\x00\n0123456789' 634 | self.assertEqual(decode.embedded_value(value)[1], '0123456789') 635 | 636 | def test_decode_embedded_value_short_bytes_consumed(self): 637 | value = b's\x7f\xff' 638 | self.assertEqual(decode.embedded_value(value)[0], 3) 639 | 640 | def test_decode_embedded_value_short_short_bytes_consumed(self): 641 | self.assertEqual(decode.embedded_value(b'b\xff')[0], 2) 642 | 643 | def test_decode_embedded_value_short_short_data_type(self): 644 | self.assertIsInstance(decode.embedded_value(b'b\xff')[1], int) 645 | 646 | def test_decode_embedded_value_short_short_value(self): 647 | self.assertEqual(decode.embedded_value(b'B\xff')[1], 255) 648 | 649 | def test_decode_embedded_value_short_data_type(self): 650 | value = b's\x7f\xff' 651 | self.assertIsInstance(decode.embedded_value(value)[1], int) 652 | 653 | def test_decode_embedded_value_short_value(self): 654 | self.assertEqual(decode.embedded_value(b's\x7f\xff')[1], 32767) 655 | 656 | def test_decode_embedded_value_short_uint_data_type(self): 657 | self.assertIsInstance(decode.embedded_value(b'u\xff\xff')[1], int) 658 | 659 | def test_decode_embedded_value_short_uint_value(self): 660 | self.assertEqual(decode.embedded_value(b'u\xff\xff')[1], 65535) 661 | 662 | def test_decode_embedded_value_timestamp_bytes_consumed(self): 663 | value = b'T\x00\x00\x00\x00Ec)\x92' 664 | self.assertEqual(decode.embedded_value(value)[0], 9) 665 | 666 | def test_decode_embedded_value_timestamp_data_type(self): 667 | value = b'T\x00\x00\x00\x00Ec)\x92' 668 | self.assertIsInstance( 669 | decode.embedded_value(value)[1], datetime.datetime) 670 | 671 | def test_decode_embedded_value_timestamp_value(self): 672 | value = b'T\x00\x00\x00\x00Ec)\x92' 673 | self.assertEqual( 674 | decode.embedded_value(value)[1], 675 | datetime.datetime(2006, 11, 21, 16, 30, 10, 676 | tzinfo=datetime.timezone.utc)) 677 | 678 | def test_decode_embedded_value_field_array_bytes_consumed(self): 679 | self.assertEqual( 680 | decode.embedded_value(b'A' + self.FIELD_ARR)[0], 681 | len(b'A' + self.FIELD_ARR)) 682 | 683 | def test_decode_embedded_value_field_array_data_type(self): 684 | self.assertIsInstance( 685 | decode.embedded_value(b'A' + self.FIELD_ARR)[1], list) 686 | 687 | def test_decode_embedded_value_field_array_value(self): 688 | value = decode.embedded_value(b'A' + self.FIELD_ARR)[1] 689 | for position in range(0, len(value)): 690 | if isinstance(value[position], float): 691 | self.assertAlmostEqual( 692 | round(value[position], 3), 693 | round(self.FIELD_ARR_VALUE[position], 3)) 694 | else: 695 | self.assertEqual(value[position], 696 | self.FIELD_ARR_VALUE[position]) 697 | 698 | def test_decode_embedded_value_field_table_bytes_consumed(self): 699 | self.assertEqual( 700 | decode.embedded_value(b'F' + self.FIELD_TBL)[0], 701 | len(b'F' + self.FIELD_TBL)) 702 | 703 | def test_decode_embedded_value_field_table_data_type(self): 704 | self.assertIsInstance( 705 | decode.embedded_value(b'F' + self.FIELD_TBL)[1], dict) 706 | 707 | def test_decode_embedded_value_field_table_value(self): 708 | value = decode.embedded_value(b'F' + self.FIELD_TBL)[1] 709 | for key in self.FIELD_TBL_VALUE.keys(): 710 | if isinstance(value[key], float): 711 | self.assertAlmostEqual(round(value[key], 3), 712 | round(self.FIELD_TBL_VALUE[key], 3)) 713 | else: 714 | self.assertEqual(value[key], self.FIELD_TBL_VALUE[key]) 715 | 716 | def test_decode_embedded_value_void_consumed(self): 717 | self.assertEqual(decode.embedded_value(b'V')[0], 1) 718 | 719 | def test_decode_embedded_value_void_value(self): 720 | self.assertIsNone(decode.embedded_value(b'V')[1]) 721 | 722 | def test_field_embedded_value_field_table_keys(self): 723 | value = decode.embedded_value(b'F' + self.FIELD_TBL)[1] 724 | self.assertListEqual(sorted(value.keys()), 725 | sorted(self.FIELD_TBL_VALUE.keys())) 726 | 727 | def test_decode_large_timestamp_bytes_consumed(self): 728 | dt = datetime.datetime(2107, 1, 1, 0, 0, tzinfo=datetime.timezone.utc) 729 | large_timestamp_bytes = struct.pack('>Q', int(dt.timestamp() * 1000)) 730 | self.assertEqual(decode.timestamp(large_timestamp_bytes)[0], 8) 731 | 732 | def test_decode_large_timestamp_data_type(self): 733 | dt = datetime.datetime(2107, 1, 1, 0, 0, tzinfo=datetime.timezone.utc) 734 | large_timestamp_bytes = struct.pack('>Q', int(dt.timestamp() * 1000)) 735 | self.assertIsInstance(decode.timestamp(large_timestamp_bytes)[1], 736 | datetime.datetime) 737 | 738 | def test_decode_large_timestamp_value(self): 739 | dt = datetime.datetime(2107, 1, 1, 0, 0, tzinfo=datetime.timezone.utc) 740 | large_timestamp_bytes = struct.pack('>Q', 741 | int(dt.timestamp() * 1000)) 742 | self.assertEqual(decode.timestamp(large_timestamp_bytes)[1], dt) 743 | --------------------------------------------------------------------------------