rabbitmq-amqp-python-client 2.0.0b1__tar.gz → 2.0.0b2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rabbitmq_amqp_python_client-2.0.0b2/AGENTS.md +46 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/PKG-INFO +4 -1
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/README.md +2 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/pyproject.toml +2 -1
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/session.py +15 -3
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/wire/encoding.py +50 -7
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/integration/test_consumer_integration.py +9 -1
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/fake_broker.py +3 -1
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_session.py +82 -1
- rabbitmq_amqp_python_client-2.0.0b2/tests/unit/test_wire_fuzz.py +188 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/certs/ca.p12 +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/certs/ca_certificate.pem +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/certs/ca_key.pem +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/certs/client_localhost.p12 +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/certs/client_localhost_certificate.pem +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/certs/client_localhost_key.pem +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/certs/server_localhost.p12 +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/certs/server_localhost_certificate.pem +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/certs/server_localhost_key.pem +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/ubuntu/advanced.config +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/ubuntu/enabled_plugins +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/ubuntu/gha-log-check.sh +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/ubuntu/gha-setup.sh +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/ubuntu/log/.gitkeep +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/ubuntu/rabbitmq.conf +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.dockerignore +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.github/release.yml +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.github/workflows/build-test.yaml +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.github/workflows/publish-pypi.yaml +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.github/workflows/publish_release.yaml +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.gitignore +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/Makefile +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/create_tag.sh +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/README.md +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/auto_reconnection.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/basic_connection.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/console_application.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/consumer_example.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/consumer_timeout_example.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/delayed_retry_context_example.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/delayed_retry_example.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/direct_reply_to_example.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/docker/reliable/Dockerfile +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/docker/reliable/README.md +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/management_example.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/performance_test.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/presettled_consumer_example.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/publisher_example.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/quorum_single_active_consumer.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/rejection_reason_example.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/reliable.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/stream_filtering.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/tls_connection.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/__init__.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/connection.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/constants.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/consumer.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/exceptions.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/link.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/logging_utils.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/management.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/publisher.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/reconnection.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/wire/__init__.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/wire/delivery_state.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/wire/frames.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/wire/message.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/wire/performatives.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/wire/sasl.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/__init__.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/integration/__init__.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/integration/conftest.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/integration/test_client_interpolation_integration.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/integration/test_client_stream_interpolation_integration.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/integration/test_connection_integration.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/integration/test_console_application_integration.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/integration/test_management_integration.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/integration/test_performance_test_integration.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/integration/test_publisher_integration.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/integration/test_reconnection_integration.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/integration/test_session_link_integration.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/integration/test_tls_integration.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/__init__.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/conftest.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/resources/amqp10_validation/empty_message.amqp +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/resources/amqp10_validation/header_amqpvalue_message.amqp +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/resources/amqp10_validation/message_body_250.amqp +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/resources/amqp10_validation/message_body_700.amqp +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/resources/amqp10_validation/message_from_version_1_0_0.amqp +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/resources/amqp10_validation/message_random_application_properties_300.amqp +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/resources/amqp10_validation/message_random_application_properties_500.amqp +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/resources/amqp10_validation/message_random_application_properties_properties_900.amqp +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/resources/amqp10_validation/message_unicode_message.amqp +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/resources/amqp10_validation/nil_and_types.amqp +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/resources/amqp10_validation/shovel_annotations.amqp +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/resources/amqp10_validation/static_test_message_compare.amqp +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/resources/amqp10_validation/uuid_message.amqp +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_amqp10_validation.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_connection.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_console_application.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_consumer.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_delivery_state.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_encoding.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_frames.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_link.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_management.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_management_builders.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_message.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_package_exports.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_performance_test.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_performatives.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_publisher.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_reconnection.py +0 -0
- {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_sasl.py +0 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Guidance for AI coding agents working in this repository.
|
|
4
|
+
|
|
5
|
+
## Project
|
|
6
|
+
|
|
7
|
+
RabbitMQ AMQP 1.0 Python client (version 2.x), a native Python implementation
|
|
8
|
+
with no dependency on `qpid-proton`. See `README.md` for install instructions
|
|
9
|
+
and a quick-start example, and `README.md#project-layout` for how the code is
|
|
10
|
+
organized (`wire/` is the protocol layer; `connection.py`, `session.py`,
|
|
11
|
+
`link.py`, `management.py`, `publisher.py`, `consumer.py`, `reconnection.py`
|
|
12
|
+
build the public API on top of it).
|
|
13
|
+
|
|
14
|
+
## Setup
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
make install
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Common commands
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
make format # ruff format, then ruff check --fix
|
|
24
|
+
make lint # ruff check and ruff format --check
|
|
25
|
+
make typecheck # mypy rabbitmq_amqp_python_client, in strict mode
|
|
26
|
+
make test-unit # the unit suite; no broker needed
|
|
27
|
+
make test-integration # the integration suite; needs a broker on localhost:5672
|
|
28
|
+
make test # both suites
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Run `make lint` and `make typecheck` before considering a change done.
|
|
32
|
+
`make test-unit` needs no broker; `make test-integration` needs one running
|
|
33
|
+
on `localhost:5672`.
|
|
34
|
+
|
|
35
|
+
## Contributing
|
|
36
|
+
|
|
37
|
+
Every contribution — bug fix or feature — must include one of the following:
|
|
38
|
+
|
|
39
|
+
- A **failing test case** that reproduces the issue and passes once the fix
|
|
40
|
+
is applied (add it under `tests/unit` or `tests/integration`, matching the
|
|
41
|
+
existing style in that directory).
|
|
42
|
+
- A **small, runnable example** that clearly reproduces the issue, in the
|
|
43
|
+
style of the scripts under `docs/examples/`.
|
|
44
|
+
|
|
45
|
+
Do not open a change without one of these — it's how reviewers (human or
|
|
46
|
+
agent) confirm the problem is real and that the fix actually addresses it.
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: rabbitmq-amqp-python-client
|
|
3
|
-
Version: 2.0.0-beta.
|
|
3
|
+
Version: 2.0.0-beta.2
|
|
4
4
|
Summary: A from-scratch AMQP 1.0 client for RabbitMQ, built directly against the wire protocol.
|
|
5
5
|
Author: RabbitMQ Team
|
|
6
6
|
License: Apache-2.0
|
|
7
7
|
Requires-Python: >=3.10
|
|
8
8
|
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: hypothesis>=6.100; extra == 'dev'
|
|
9
10
|
Requires-Dist: mypy>=2.3.1; extra == 'dev'
|
|
10
11
|
Requires-Dist: pika>=1.4.2; extra == 'dev'
|
|
11
12
|
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
@@ -18,6 +19,8 @@ Description-Content-Type: text/markdown
|
|
|
18
19
|
|
|
19
20
|
RabbitMQ AMQP 1.0 Python client version 2.x for RabbitMQ 4.x.
|
|
20
21
|
|
|
22
|
+
Client is tested and validated against RabbitMQ 4.3.0
|
|
23
|
+
|
|
21
24
|
This client version built from scratch.</br>
|
|
22
25
|
The primary reason for this rewrite is to remove the dependency on the `qpid-proton` C library present in version 1.0, providing a fully native Python client, see [here](https://github.com/rabbitmq/rabbitmq-amqp-python-client/discussions/123).
|
|
23
26
|
This client is inspired by the Java and .NET AMQP 1.0 clients, aiming to deliver the same behavior and user experience across ecosystems.
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
RabbitMQ AMQP 1.0 Python client version 2.x for RabbitMQ 4.x.
|
|
4
4
|
|
|
5
|
+
Client is tested and validated against RabbitMQ 4.3.0
|
|
6
|
+
|
|
5
7
|
This client version built from scratch.</br>
|
|
6
8
|
The primary reason for this rewrite is to remove the dependency on the `qpid-proton` C library present in version 1.0, providing a fully native Python client, see [here](https://github.com/rabbitmq/rabbitmq-amqp-python-client/discussions/123).
|
|
7
9
|
This client is inspired by the Java and .NET AMQP 1.0 clients, aiming to deliver the same behavior and user experience across ecosystems.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "rabbitmq-amqp-python-client"
|
|
7
|
-
version = "2.0.0-beta.
|
|
7
|
+
version = "2.0.0-beta.2"
|
|
8
8
|
description = "A from-scratch AMQP 1.0 client for RabbitMQ, built directly against the wire protocol."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -16,6 +16,7 @@ dependencies = []
|
|
|
16
16
|
dev = [
|
|
17
17
|
"pytest>=9.1.1",
|
|
18
18
|
"pytest-cov>=5.0",
|
|
19
|
+
"hypothesis>=6.100",
|
|
19
20
|
"ruff>=0.16.5",
|
|
20
21
|
"mypy>=2.3.1",
|
|
21
22
|
"pika>=1.4.2",
|
|
@@ -38,6 +38,11 @@ DEFAULT_WINDOW = 0x7FFFFFFF
|
|
|
38
38
|
#: Highest link handle the spec allows.
|
|
39
39
|
MAX_HANDLE = 0xFFFFFFFF
|
|
40
40
|
|
|
41
|
+
#: Transfer-ids are 32-bit RFC-1982 serial numbers: a peer may start theirs at
|
|
42
|
+
#: an arbitrary value (RabbitMQ deliberately starts near this boundary) and it
|
|
43
|
+
#: wraps back to 0 rather than growing past it.
|
|
44
|
+
TRANSFER_ID_MODULUS = 0x1_0000_0000
|
|
45
|
+
|
|
41
46
|
DEFAULT_BEGIN_TIMEOUT_SECONDS = 5.0
|
|
42
47
|
DEFAULT_END_TIMEOUT_SECONDS = 5.0
|
|
43
48
|
|
|
@@ -429,9 +434,16 @@ class Session:
|
|
|
429
434
|
|
|
430
435
|
def _on_transfer(self, performative: Transfer, payload: bytes) -> None:
|
|
431
436
|
with self._cond:
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
437
|
+
# ``delivery-id`` and the implicit transfer-id next-incoming-id
|
|
438
|
+
# tracks are independent counters that may start at different
|
|
439
|
+
# values (RabbitMQ starts the latter near the 32-bit boundary
|
|
440
|
+
# specifically to catch clients that conflate the two — see
|
|
441
|
+
# rabbit_amqp_session.erl's comment on next_outgoing_id vs
|
|
442
|
+
# outgoing_delivery_id). Every transfer *frame* — including a
|
|
443
|
+
# fragment with no delivery-id of its own — advances it by one.
|
|
444
|
+
if self._next_incoming_id is not None:
|
|
445
|
+
self._next_incoming_id = (self._next_incoming_id + 1) % TRANSFER_ID_MODULUS
|
|
446
|
+
self._received_transfer = True
|
|
435
447
|
self._incoming_used += 1
|
|
436
448
|
replenish = self._incoming_used >= max(1, self._incoming_window // 2)
|
|
437
449
|
link = self._link_for_handle(performative.handle)
|
|
@@ -92,6 +92,21 @@ _INT16_MIN, _INT16_MAX = -0x8000, 0x7FFF
|
|
|
92
92
|
_INT32_MIN, _INT32_MAX = -0x80000000, 0x7FFFFFFF
|
|
93
93
|
_INT64_MIN, _INT64_MAX = -0x8000000000000000, 0x7FFFFFFFFFFFFFFF
|
|
94
94
|
|
|
95
|
+
#: Hard cap on a list/map/array's declared element count. A constant-width
|
|
96
|
+
#: element (``null``, ``boolean``, ``uint0``, ``ulong0``) costs zero further
|
|
97
|
+
#: bytes, so an ``array`` of one of those can claim billions of elements from
|
|
98
|
+
#: a handful of wire bytes; without this cap, decoding one would force an
|
|
99
|
+
#: allocation many orders of magnitude larger than the frame that requested it.
|
|
100
|
+
MAX_COMPOUND_COUNT = 1_000_000
|
|
101
|
+
|
|
102
|
+
#: Hard cap on how deeply values may nest (a described type wrapping another,
|
|
103
|
+
#: or a list/map/array containing one). Every level costs only one byte on
|
|
104
|
+
#: the wire (e.g. a run of described-type constructors, 0x00), so a small
|
|
105
|
+
#: malicious frame can otherwise recurse past Python's call-stack limit and
|
|
106
|
+
#: crash the process with an uncaught ``RecursionError`` instead of a
|
|
107
|
+
#: ``ProtocolError``. No real AMQP message nests anywhere near this deep.
|
|
108
|
+
MAX_DECODE_DEPTH = 100
|
|
109
|
+
|
|
95
110
|
|
|
96
111
|
class Symbol(str):
|
|
97
112
|
"""A ``str`` that encodes as an AMQP ``symbol`` (ASCII) instead of a ``string``."""
|
|
@@ -575,11 +590,12 @@ class Decoder:
|
|
|
575
590
|
position: Index of the first byte to read.
|
|
576
591
|
"""
|
|
577
592
|
|
|
578
|
-
__slots__ = ("_data", "_position")
|
|
593
|
+
__slots__ = ("_data", "_position", "_depth")
|
|
579
594
|
|
|
580
595
|
def __init__(self, data: bytes | bytearray | memoryview, position: int = 0) -> None:
|
|
581
596
|
self._data = bytes(data)
|
|
582
597
|
self._position = position
|
|
598
|
+
self._depth = 0
|
|
583
599
|
|
|
584
600
|
@property
|
|
585
601
|
def position(self) -> int:
|
|
@@ -608,8 +624,19 @@ class Decoder:
|
|
|
608
624
|
return self.read(1)[0]
|
|
609
625
|
|
|
610
626
|
def read_value(self) -> Any:
|
|
611
|
-
"""Read one complete value, constructor included.
|
|
612
|
-
|
|
627
|
+
"""Read one complete value, constructor included.
|
|
628
|
+
|
|
629
|
+
Raises:
|
|
630
|
+
ProtocolError: If reading the value would nest deeper than
|
|
631
|
+
:data:`MAX_DECODE_DEPTH`.
|
|
632
|
+
"""
|
|
633
|
+
self._depth += 1
|
|
634
|
+
try:
|
|
635
|
+
if self._depth > MAX_DECODE_DEPTH:
|
|
636
|
+
raise ProtocolError(f"AMQP value nesting exceeds the {MAX_DECODE_DEPTH}-level limit")
|
|
637
|
+
return self._read_body(self.read_code())
|
|
638
|
+
finally:
|
|
639
|
+
self._depth -= 1
|
|
613
640
|
|
|
614
641
|
def _unpack(self, fmt: str, size: int) -> Any:
|
|
615
642
|
return struct.unpack(fmt, self.read(size))[0]
|
|
@@ -650,7 +677,11 @@ class Decoder:
|
|
|
650
677
|
if code == CODE_DOUBLE:
|
|
651
678
|
return float(self._unpack(">d", 8))
|
|
652
679
|
if code == CODE_CHAR:
|
|
653
|
-
|
|
680
|
+
code_point = int(self._unpack(">I", 4))
|
|
681
|
+
try:
|
|
682
|
+
return chr(code_point)
|
|
683
|
+
except (ValueError, OverflowError) as error:
|
|
684
|
+
raise ProtocolError(f"AMQP char 0x{code_point:x} is not a valid Unicode scalar value") from error
|
|
654
685
|
if code == CODE_TIMESTAMP:
|
|
655
686
|
return Timestamp(self._unpack(">q", 8))
|
|
656
687
|
if code == CODE_UUID:
|
|
@@ -675,8 +706,14 @@ class Decoder:
|
|
|
675
706
|
if code in (CODE_VBIN8, CODE_VBIN32):
|
|
676
707
|
return raw
|
|
677
708
|
if code in (CODE_STR8, CODE_STR32):
|
|
678
|
-
|
|
679
|
-
|
|
709
|
+
try:
|
|
710
|
+
return raw.decode("utf-8")
|
|
711
|
+
except UnicodeDecodeError as error:
|
|
712
|
+
raise ProtocolError(f"AMQP string is not valid UTF-8: {error}") from error
|
|
713
|
+
try:
|
|
714
|
+
return Symbol(raw.decode("ascii"))
|
|
715
|
+
except UnicodeDecodeError as error:
|
|
716
|
+
raise ProtocolError(f"AMQP symbol is not valid ASCII: {error}") from error
|
|
680
717
|
|
|
681
718
|
def _read_compound(self, code: int, size: int, count: int) -> Any:
|
|
682
719
|
# `size` is a hint used only to reject a declared length that could
|
|
@@ -690,6 +727,8 @@ class Decoder:
|
|
|
690
727
|
end = self._position + size - width
|
|
691
728
|
if end > len(self._data):
|
|
692
729
|
raise ProtocolError(f"truncated AMQP compound value: declared size {size} overruns the buffer")
|
|
730
|
+
if count > MAX_COMPOUND_COUNT:
|
|
731
|
+
raise ProtocolError(f"AMQP compound declares {count} elements, exceeding the {MAX_COMPOUND_COUNT} limit")
|
|
693
732
|
if code in (CODE_ARRAY8, CODE_ARRAY32):
|
|
694
733
|
element_code = self.read_code()
|
|
695
734
|
return [self._read_body(element_code) for _ in range(count)]
|
|
@@ -699,7 +738,11 @@ class Decoder:
|
|
|
699
738
|
result: dict[Any, Any] = {}
|
|
700
739
|
for _ in range(count // 2):
|
|
701
740
|
key = self.read_value()
|
|
702
|
-
|
|
741
|
+
value = self.read_value()
|
|
742
|
+
try:
|
|
743
|
+
result[key] = value
|
|
744
|
+
except TypeError as error:
|
|
745
|
+
raise ProtocolError(f"AMQP map key of type {type(key).__name__} is not hashable") from error
|
|
703
746
|
return result
|
|
704
747
|
return [self.read_value() for _ in range(count)]
|
|
705
748
|
|
|
@@ -342,7 +342,15 @@ class TestRaisingHandler:
|
|
|
342
342
|
|
|
343
343
|
def fail_on_some(data):
|
|
344
344
|
message = decode(data)
|
|
345
|
-
|
|
345
|
+
# ``decode`` is patched on the shared Message class, so it also sees
|
|
346
|
+
# the connection's own management-response traffic, whose bodies are
|
|
347
|
+
# maps rather than strings; only this test's own deliveries can be
|
|
348
|
+
# judged against ``poisoned``.
|
|
349
|
+
try:
|
|
350
|
+
body = message.body_as_string()
|
|
351
|
+
except TypeError:
|
|
352
|
+
return message
|
|
353
|
+
if body in poisoned:
|
|
346
354
|
raise ProtocolError("simulated undecodable delivery")
|
|
347
355
|
return message
|
|
348
356
|
|
|
@@ -69,6 +69,7 @@ class FakeBroker:
|
|
|
69
69
|
auto_settle=True,
|
|
70
70
|
sasl_layer=True,
|
|
71
71
|
receiver_flow_properties=None,
|
|
72
|
+
next_outgoing_id=0,
|
|
72
73
|
):
|
|
73
74
|
self._sock = sock
|
|
74
75
|
self._sock.settimeout(5.0)
|
|
@@ -87,6 +88,7 @@ class FakeBroker:
|
|
|
87
88
|
self.auto_settle = auto_settle
|
|
88
89
|
self.sasl_layer = sasl_layer
|
|
89
90
|
self.receiver_flow_properties = receiver_flow_properties
|
|
91
|
+
self.next_outgoing_id = next_outgoing_id
|
|
90
92
|
|
|
91
93
|
self.sasl_init: SaslInit | None = None
|
|
92
94
|
self.remote_open: Open | None = None
|
|
@@ -262,7 +264,7 @@ class FakeBroker:
|
|
|
262
264
|
channel,
|
|
263
265
|
Begin(
|
|
264
266
|
remote_channel=channel,
|
|
265
|
-
next_outgoing_id=
|
|
267
|
+
next_outgoing_id=self.next_outgoing_id,
|
|
266
268
|
incoming_window=DEFAULT_WINDOW,
|
|
267
269
|
outgoing_window=DEFAULT_WINDOW,
|
|
268
270
|
handle_max=self.handle_max,
|
|
@@ -241,7 +241,88 @@ class TestWindows:
|
|
|
241
241
|
link = _LinkDouble()
|
|
242
242
|
handle = opened.allocate_handle(link)
|
|
243
243
|
opened.handle_frame(Transfer(handle=handle, delivery_id=4, delivery_tag=b"t"), b"body")
|
|
244
|
-
assert opened._next_incoming_id ==
|
|
244
|
+
assert opened._next_incoming_id == 1
|
|
245
|
+
|
|
246
|
+
def test_next_incoming_id_is_independent_of_the_wire_delivery_id(self, session):
|
|
247
|
+
"""delivery-id and the implicit transfer-id are separate counters (#141).
|
|
248
|
+
|
|
249
|
+
RabbitMQ starts a session's real transfer-id count wherever it likes
|
|
250
|
+
and starts delivery-id at 0 regardless, so next-incoming-id must
|
|
251
|
+
advance once per transfer *frame* and never adopt the frame's
|
|
252
|
+
delivery-id, however small or large that happens to be.
|
|
253
|
+
"""
|
|
254
|
+
_broker, _connection, opened = session
|
|
255
|
+
link = _LinkDouble()
|
|
256
|
+
handle = opened.allocate_handle(link)
|
|
257
|
+
opened.handle_frame(Transfer(handle=handle, delivery_id=0, delivery_tag=b"a"), b"body")
|
|
258
|
+
opened.handle_frame(Transfer(handle=handle, delivery_id=1, delivery_tag=b"b"), b"body")
|
|
259
|
+
opened.handle_frame(Transfer(handle=handle, delivery_id=2, delivery_tag=b"c"), b"body")
|
|
260
|
+
assert opened._next_incoming_id == 3
|
|
261
|
+
|
|
262
|
+
def test_next_incoming_id_continues_from_the_peers_arbitrary_starting_point(self, connect):
|
|
263
|
+
"""Reproduces #141: RabbitMQ begins a session with next-outgoing-id set
|
|
264
|
+
near the 32-bit boundary rather than 0. A client that derives
|
|
265
|
+
next-incoming-id from the small, independently-numbered delivery-id
|
|
266
|
+
instead of counting frames sends a FLOW the broker rejects with
|
|
267
|
+
``amqp:session:window-violation`` (its next-incoming-id "leads" the
|
|
268
|
+
broker's real next-outgoing-id).
|
|
269
|
+
"""
|
|
270
|
+
broker, connection = connect(broker_kwargs={"next_outgoing_id": 0xFFFFFFFC})
|
|
271
|
+
opened = connection.open_session()
|
|
272
|
+
link = _LinkDouble()
|
|
273
|
+
handle = opened.allocate_handle(link)
|
|
274
|
+
assert opened._next_incoming_id == 0xFFFFFFFC
|
|
275
|
+
opened.handle_frame(Transfer(handle=handle, delivery_id=0, delivery_tag=b"a"), b"body")
|
|
276
|
+
assert opened._next_incoming_id == 0xFFFFFFFD
|
|
277
|
+
opened.handle_frame(Transfer(handle=handle, delivery_id=1, delivery_tag=b"b"), b"body")
|
|
278
|
+
assert opened._next_incoming_id == 0xFFFFFFFE
|
|
279
|
+
opened.send_flow()
|
|
280
|
+
_channel, performative, _payload = broker.wait_for(Flow)
|
|
281
|
+
assert performative.next_incoming_id == 0xFFFFFFFE
|
|
282
|
+
|
|
283
|
+
def test_next_incoming_id_wraps_at_the_32_bit_boundary(self, connect):
|
|
284
|
+
"""The transfer-id is an RFC-1982 serial number: it wraps to 0 rather
|
|
285
|
+
than overflowing ``uint32``, which would otherwise blow up encoding
|
|
286
|
+
the next FLOW frame.
|
|
287
|
+
"""
|
|
288
|
+
broker, connection = connect(broker_kwargs={"next_outgoing_id": 0xFFFFFFFE})
|
|
289
|
+
opened = connection.open_session()
|
|
290
|
+
link = _LinkDouble()
|
|
291
|
+
handle = opened.allocate_handle(link)
|
|
292
|
+
for delivery_id in range(3):
|
|
293
|
+
opened.handle_frame(Transfer(handle=handle, delivery_id=delivery_id, delivery_tag=b"t"), b"body")
|
|
294
|
+
assert opened._next_incoming_id == 1
|
|
295
|
+
opened.send_flow()
|
|
296
|
+
_channel, performative, _payload = broker.wait_for(Flow)
|
|
297
|
+
assert performative.next_incoming_id == 1
|
|
298
|
+
|
|
299
|
+
def test_next_incoming_id_advances_once_per_fragment_of_a_multi_frame_delivery(self, session):
|
|
300
|
+
"""A fragmented delivery still consumes one transfer-id per frame (#141).
|
|
301
|
+
|
|
302
|
+
RabbitMQ's own session bookkeeping increments its outgoing transfer-id
|
|
303
|
+
once per TRANSFER *frame* it sends, continuation frames included, not
|
|
304
|
+
once per delivery (see rabbit_amqp_session.erl's comment contrasting
|
|
305
|
+
next_outgoing_id with outgoing_delivery_id). Live-broker testing
|
|
306
|
+
confirmed this: fragmenting one delivery into 7 frames advanced the
|
|
307
|
+
broker's real transfer count by 7, and a client that only advanced
|
|
308
|
+
next-incoming-id on the delivery's final frame — as the ``more`` flag
|
|
309
|
+
might tempt one to do — would fall behind and eventually trip the
|
|
310
|
+
same ``amqp:session:window-violation`` this fix prevents, just from
|
|
311
|
+
under- rather than over-counting.
|
|
312
|
+
"""
|
|
313
|
+
_broker, _connection, opened = session
|
|
314
|
+
link = _LinkDouble()
|
|
315
|
+
handle = opened.allocate_handle(link)
|
|
316
|
+
# Only the first frame of a delivery carries delivery-id; continuation
|
|
317
|
+
# frames (more=True) carry none, exactly as a real fragmented transfer
|
|
318
|
+
# arrives on the wire.
|
|
319
|
+
opened.handle_frame(Transfer(handle=handle, delivery_id=0, delivery_tag=b"a", more=True), b"frag-1")
|
|
320
|
+
assert opened._next_incoming_id == 1
|
|
321
|
+
opened.handle_frame(Transfer(handle=handle, more=True), b"frag-2")
|
|
322
|
+
assert opened._next_incoming_id == 2
|
|
323
|
+
opened.handle_frame(Transfer(handle=handle, more=False), b"frag-3")
|
|
324
|
+
assert opened._next_incoming_id == 3
|
|
325
|
+
assert [payload for _performative, payload in link.frames] == [b"frag-1", b"frag-2", b"frag-3"]
|
|
245
326
|
|
|
246
327
|
def test_the_incoming_window_is_replenished_once_half_consumed(self, connect):
|
|
247
328
|
broker, connection = connect()
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Property-based (Hypothesis) fuzz tests for the AMQP 1.0 wire codec.
|
|
2
|
+
|
|
3
|
+
The rest of ``tests/unit`` is example-based: every case is a value someone
|
|
4
|
+
(human or AI) thought to write down. These tests instead check invariants
|
|
5
|
+
that must hold for *any* input, which is the only way to exercise byte
|
|
6
|
+
patterns nobody thought to hand-pick:
|
|
7
|
+
|
|
8
|
+
* **Round trip** - ``decode_value(encode_value(x)) == x`` for arbitrary,
|
|
9
|
+
possibly deeply nested Python values.
|
|
10
|
+
* **Crash resistance** - decoding arbitrary or mutated-but-otherwise-valid
|
|
11
|
+
bytes must only ever raise :class:`ProtocolError`. Any other exception
|
|
12
|
+
means a hostile or buggy peer can crash the client instead of having its
|
|
13
|
+
frame rejected.
|
|
14
|
+
|
|
15
|
+
Requires the ``hypothesis`` dev dependency (``pip install -e ".[dev]"``).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import math
|
|
21
|
+
import struct
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
import pytest
|
|
25
|
+
from hypothesis import given, settings
|
|
26
|
+
from hypothesis import strategies as st
|
|
27
|
+
|
|
28
|
+
from rabbitmq_amqp_python_client.exceptions import ProtocolError
|
|
29
|
+
from rabbitmq_amqp_python_client.wire import encoding as enc
|
|
30
|
+
from rabbitmq_amqp_python_client.wire.frames import FRAME_TYPE_AMQP, FRAME_TYPE_SASL, decode_frame_body
|
|
31
|
+
|
|
32
|
+
# --- Strategies for values `encode_value`/`decode_value` round-trip ---
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _wrapped_int(wrapper: type, low: int, high: int) -> st.SearchStrategy[Any]:
|
|
36
|
+
return st.integers(min_value=low, max_value=high).map(wrapper)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
_ASCII_SYMBOL_TEXT = st.text(alphabet=st.characters(min_codepoint=0x21, max_codepoint=0x7E), max_size=32)
|
|
40
|
+
|
|
41
|
+
_SCALARS = st.one_of(
|
|
42
|
+
st.none(),
|
|
43
|
+
st.booleans(),
|
|
44
|
+
st.integers(min_value=-0x80000000, max_value=0x7FFFFFFF), # plain int -> AMQP int
|
|
45
|
+
st.integers(min_value=-0x8000000000000000, max_value=0x7FFFFFFFFFFFFFFF), # plain int -> AMQP long
|
|
46
|
+
_wrapped_int(enc.Ubyte, 0, 0xFF),
|
|
47
|
+
_wrapped_int(enc.Byte, -0x80, 0x7F),
|
|
48
|
+
_wrapped_int(enc.Ushort, 0, 0xFFFF),
|
|
49
|
+
_wrapped_int(enc.Short, -0x8000, 0x7FFF),
|
|
50
|
+
_wrapped_int(enc.Uint, 0, 0xFFFFFFFF),
|
|
51
|
+
_wrapped_int(enc.Int, -0x80000000, 0x7FFFFFFF),
|
|
52
|
+
_wrapped_int(enc.Ulong, 0, 0xFFFFFFFFFFFFFFFF),
|
|
53
|
+
_wrapped_int(enc.Long, -0x8000000000000000, 0x7FFFFFFFFFFFFFFF),
|
|
54
|
+
_wrapped_int(enc.Timestamp, -0x8000000000000000, 0x7FFFFFFFFFFFFFFF),
|
|
55
|
+
_ASCII_SYMBOL_TEXT.map(enc.Symbol),
|
|
56
|
+
st.characters(max_codepoint=0x10FFFF).map(enc.Char),
|
|
57
|
+
st.integers(min_value=0, max_value=0xFFFFFFFF).map(
|
|
58
|
+
lambda bits: enc.Float(struct.unpack(">f", struct.pack(">I", bits))[0])
|
|
59
|
+
),
|
|
60
|
+
st.floats(allow_nan=True, allow_infinity=True).map(enc.Double),
|
|
61
|
+
st.text(max_size=32),
|
|
62
|
+
st.binary(max_size=32),
|
|
63
|
+
st.uuids(),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
_VALUES = st.recursive(
|
|
67
|
+
_SCALARS,
|
|
68
|
+
lambda children: st.one_of(
|
|
69
|
+
st.lists(children, max_size=5),
|
|
70
|
+
st.dictionaries(st.text(max_size=16), children, max_size=5),
|
|
71
|
+
),
|
|
72
|
+
max_leaves=20,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _values_equal(left: Any, right: Any) -> bool:
|
|
77
|
+
"""Compare decoded/original values, treating NaN as equal to itself."""
|
|
78
|
+
if isinstance(left, float) and isinstance(right, float) and math.isnan(left) and math.isnan(right):
|
|
79
|
+
return True
|
|
80
|
+
if isinstance(left, list) and isinstance(right, list):
|
|
81
|
+
return len(left) == len(right) and all(_values_equal(a, b) for a, b in zip(left, right, strict=True))
|
|
82
|
+
if isinstance(left, dict) and isinstance(right, dict):
|
|
83
|
+
return left.keys() == right.keys() and all(_values_equal(left[key], right[key]) for key in left)
|
|
84
|
+
return bool(left == right)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@given(value=_VALUES)
|
|
88
|
+
@settings(max_examples=300, deadline=None)
|
|
89
|
+
def test_round_trip_preserves_arbitrary_values(value: Any) -> None:
|
|
90
|
+
encoded = enc.encode_value(value)
|
|
91
|
+
decoded = enc.decode_value(encoded)
|
|
92
|
+
assert _values_equal(decoded, value), f"round trip mismatch: {value!r} -> {decoded!r}"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# --- Crash resistance: a hostile/corrupt peer must only ever get a ProtocolError ---
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@given(data=st.binary(max_size=256))
|
|
99
|
+
@settings(max_examples=500, deadline=None)
|
|
100
|
+
def test_decode_value_never_leaks_a_non_protocol_error(data: bytes) -> None:
|
|
101
|
+
try:
|
|
102
|
+
enc.decode_value(data)
|
|
103
|
+
except ProtocolError:
|
|
104
|
+
pass
|
|
105
|
+
except Exception as exc: # noqa: BLE001 - intentionally broad: this is the property under test
|
|
106
|
+
pytest.fail(f"decode_value raised {type(exc).__name__} instead of ProtocolError for {data!r}: {exc!r}")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@given(value=_VALUES, mutations=st.lists(st.tuples(st.integers(min_value=0), st.integers(0, 255)), max_size=6))
|
|
110
|
+
@settings(max_examples=300, deadline=None)
|
|
111
|
+
def test_decode_value_never_leaks_a_non_protocol_error_on_mutated_input(
|
|
112
|
+
value: Any, mutations: list[tuple[int, int]]
|
|
113
|
+
) -> None:
|
|
114
|
+
"""Byte-flip a validly encoded value: more likely than raw random bytes to
|
|
115
|
+
reach the compound/variable-width/described decode paths."""
|
|
116
|
+
encoded = bytearray(enc.encode_value(value))
|
|
117
|
+
if not encoded:
|
|
118
|
+
return
|
|
119
|
+
for index, byte_value in mutations:
|
|
120
|
+
encoded[index % len(encoded)] = byte_value
|
|
121
|
+
try:
|
|
122
|
+
enc.decode_value(bytes(encoded))
|
|
123
|
+
except ProtocolError:
|
|
124
|
+
pass
|
|
125
|
+
except Exception as exc: # noqa: BLE001 - intentionally broad: this is the property under test
|
|
126
|
+
pytest.fail(
|
|
127
|
+
f"decode_value raised {type(exc).__name__} instead of ProtocolError for {bytes(encoded)!r}: {exc!r}"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@given(frame_type=st.sampled_from([FRAME_TYPE_AMQP, FRAME_TYPE_SASL]), body=st.binary(max_size=256))
|
|
132
|
+
@settings(max_examples=500, deadline=None)
|
|
133
|
+
def test_decode_frame_body_never_leaks_a_non_protocol_error(frame_type: int, body: bytes) -> None:
|
|
134
|
+
try:
|
|
135
|
+
decode_frame_body(frame_type, body)
|
|
136
|
+
except ProtocolError:
|
|
137
|
+
pass
|
|
138
|
+
except Exception as exc: # noqa: BLE001 - intentionally broad: this is the property under test
|
|
139
|
+
pytest.fail(f"decode_frame_body raised {type(exc).__name__} instead of ProtocolError for {body!r}: {exc!r}")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# --- Regression tests for the two resource-exhaustion bugs this fuzzing found ---
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class TestDecoderResourceLimits:
|
|
146
|
+
"""A hostile peer must not be able to turn a small frame into unbounded work.
|
|
147
|
+
|
|
148
|
+
These reproduce two bugs found while writing the properties above:
|
|
149
|
+
`_read_compound` did not cap an array's declared element count against
|
|
150
|
+
anything but the buffer, and `read_value`'s recursion had no depth limit.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
def test_array_of_null_rejects_a_count_that_would_allocate_millions_of_elements(self):
|
|
154
|
+
# 10 bytes on the wire; MAX_COMPOUND_COUNT + 1 declared `null` elements
|
|
155
|
+
# that (before the fix) cost zero further bytes each, so the array
|
|
156
|
+
# would happily build a multi-million-entry list from this alone.
|
|
157
|
+
count = enc.MAX_COMPOUND_COUNT + 1
|
|
158
|
+
header = struct.pack(">II", 5, count) # size = count-field width + constructor byte
|
|
159
|
+
data = bytes((enc.CODE_ARRAY32,)) + header + bytes((enc.CODE_NULL,))
|
|
160
|
+
with pytest.raises(ProtocolError, match="exceeding"):
|
|
161
|
+
enc.decode_value(data)
|
|
162
|
+
|
|
163
|
+
def test_array_of_null_at_the_count_limit_still_decodes(self):
|
|
164
|
+
count = enc.MAX_COMPOUND_COUNT
|
|
165
|
+
header = struct.pack(">II", 5, count)
|
|
166
|
+
data = bytes((enc.CODE_ARRAY32,)) + header + bytes((enc.CODE_NULL,))
|
|
167
|
+
assert enc.decode_value(data) == [None] * count
|
|
168
|
+
|
|
169
|
+
def test_a_long_chain_of_described_types_is_rejected_before_the_stack_overflows(self):
|
|
170
|
+
# Each 0x00 opens another described-type level; without a depth limit
|
|
171
|
+
# this recurses through `read_value` until CPython raises an uncaught
|
|
172
|
+
# RecursionError instead of a ProtocolError.
|
|
173
|
+
chain = bytes((enc.CODE_DESCRIBED,)) * 4000 + enc.NULL_BYTES * 2
|
|
174
|
+
with pytest.raises(ProtocolError, match="nesting"):
|
|
175
|
+
enc.decode_value(chain)
|
|
176
|
+
|
|
177
|
+
def test_nesting_well_under_the_depth_limit_still_round_trips(self):
|
|
178
|
+
value: Any = None
|
|
179
|
+
for _ in range(40):
|
|
180
|
+
value = enc.Described(1, value)
|
|
181
|
+
assert enc.decode_value(enc.encode_value(value)) == value
|
|
182
|
+
|
|
183
|
+
def test_nesting_well_over_the_depth_limit_is_rejected(self):
|
|
184
|
+
value: Any = None
|
|
185
|
+
for _ in range(200):
|
|
186
|
+
value = enc.Described(1, value)
|
|
187
|
+
with pytest.raises(ProtocolError, match="nesting"):
|
|
188
|
+
enc.decode_value(enc.encode_value(value))
|
{rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/certs/ca.p12
RENAMED
|
File without changes
|
|
File without changes
|
{rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/certs/ca_key.pem
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/ubuntu/gha-setup.sh
RENAMED
|
File without changes
|
{rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/ubuntu/log/.gitkeep
RENAMED
|
File without changes
|
{rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/ubuntu/rabbitmq.conf
RENAMED
|
File without changes
|
|
File without changes
|
{rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.github/release.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/__init__.py
RENAMED
|
File without changes
|
{rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/conftest.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_link.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_sasl.py
RENAMED
|
File without changes
|