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.
Files changed (114) hide show
  1. rabbitmq_amqp_python_client-2.0.0b2/AGENTS.md +46 -0
  2. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/PKG-INFO +4 -1
  3. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/README.md +2 -0
  4. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/pyproject.toml +2 -1
  5. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/session.py +15 -3
  6. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/wire/encoding.py +50 -7
  7. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/integration/test_consumer_integration.py +9 -1
  8. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/fake_broker.py +3 -1
  9. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_session.py +82 -1
  10. rabbitmq_amqp_python_client-2.0.0b2/tests/unit/test_wire_fuzz.py +188 -0
  11. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/certs/ca.p12 +0 -0
  12. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/certs/ca_certificate.pem +0 -0
  13. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/certs/ca_key.pem +0 -0
  14. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/certs/client_localhost.p12 +0 -0
  15. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/certs/client_localhost_certificate.pem +0 -0
  16. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/certs/client_localhost_key.pem +0 -0
  17. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/certs/server_localhost.p12 +0 -0
  18. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/certs/server_localhost_certificate.pem +0 -0
  19. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/certs/server_localhost_key.pem +0 -0
  20. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/ubuntu/advanced.config +0 -0
  21. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/ubuntu/enabled_plugins +0 -0
  22. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/ubuntu/gha-log-check.sh +0 -0
  23. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/ubuntu/gha-setup.sh +0 -0
  24. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/ubuntu/log/.gitkeep +0 -0
  25. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.ci/ubuntu/rabbitmq.conf +0 -0
  26. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.dockerignore +0 -0
  27. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.github/release.yml +0 -0
  28. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.github/workflows/build-test.yaml +0 -0
  29. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.github/workflows/publish-pypi.yaml +0 -0
  30. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.github/workflows/publish_release.yaml +0 -0
  31. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/.gitignore +0 -0
  32. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/Makefile +0 -0
  33. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/create_tag.sh +0 -0
  34. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/README.md +0 -0
  35. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/auto_reconnection.py +0 -0
  36. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/basic_connection.py +0 -0
  37. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/console_application.py +0 -0
  38. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/consumer_example.py +0 -0
  39. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/consumer_timeout_example.py +0 -0
  40. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/delayed_retry_context_example.py +0 -0
  41. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/delayed_retry_example.py +0 -0
  42. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/direct_reply_to_example.py +0 -0
  43. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/docker/reliable/Dockerfile +0 -0
  44. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/docker/reliable/README.md +0 -0
  45. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/management_example.py +0 -0
  46. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/performance_test.py +0 -0
  47. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/presettled_consumer_example.py +0 -0
  48. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/publisher_example.py +0 -0
  49. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/quorum_single_active_consumer.py +0 -0
  50. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/rejection_reason_example.py +0 -0
  51. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/reliable.py +0 -0
  52. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/stream_filtering.py +0 -0
  53. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/docs/examples/tls_connection.py +0 -0
  54. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/__init__.py +0 -0
  55. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/connection.py +0 -0
  56. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/constants.py +0 -0
  57. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/consumer.py +0 -0
  58. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/exceptions.py +0 -0
  59. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/link.py +0 -0
  60. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/logging_utils.py +0 -0
  61. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/management.py +0 -0
  62. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/publisher.py +0 -0
  63. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/reconnection.py +0 -0
  64. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/wire/__init__.py +0 -0
  65. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/wire/delivery_state.py +0 -0
  66. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/wire/frames.py +0 -0
  67. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/wire/message.py +0 -0
  68. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/wire/performatives.py +0 -0
  69. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/rabbitmq_amqp_python_client/wire/sasl.py +0 -0
  70. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/__init__.py +0 -0
  71. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/integration/__init__.py +0 -0
  72. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/integration/conftest.py +0 -0
  73. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/integration/test_client_interpolation_integration.py +0 -0
  74. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/integration/test_client_stream_interpolation_integration.py +0 -0
  75. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/integration/test_connection_integration.py +0 -0
  76. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/integration/test_console_application_integration.py +0 -0
  77. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/integration/test_management_integration.py +0 -0
  78. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/integration/test_performance_test_integration.py +0 -0
  79. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/integration/test_publisher_integration.py +0 -0
  80. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/integration/test_reconnection_integration.py +0 -0
  81. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/integration/test_session_link_integration.py +0 -0
  82. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/integration/test_tls_integration.py +0 -0
  83. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/__init__.py +0 -0
  84. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/conftest.py +0 -0
  85. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/resources/amqp10_validation/empty_message.amqp +0 -0
  86. {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
  87. {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
  88. {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
  89. {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
  90. {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
  91. {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
  92. {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
  93. {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
  94. {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
  95. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/resources/amqp10_validation/shovel_annotations.amqp +0 -0
  96. {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
  97. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/resources/amqp10_validation/uuid_message.amqp +0 -0
  98. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_amqp10_validation.py +0 -0
  99. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_connection.py +0 -0
  100. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_console_application.py +0 -0
  101. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_consumer.py +0 -0
  102. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_delivery_state.py +0 -0
  103. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_encoding.py +0 -0
  104. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_frames.py +0 -0
  105. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_link.py +0 -0
  106. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_management.py +0 -0
  107. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_management_builders.py +0 -0
  108. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_message.py +0 -0
  109. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_package_exports.py +0 -0
  110. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_performance_test.py +0 -0
  111. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_performatives.py +0 -0
  112. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_publisher.py +0 -0
  113. {rabbitmq_amqp_python_client-2.0.0b1 → rabbitmq_amqp_python_client-2.0.0b2}/tests/unit/test_reconnection.py +0 -0
  114. {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.1
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.1"
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
- if performative.delivery_id is not None:
433
- self._next_incoming_id = performative.delivery_id + 1
434
- self._received_transfer = True
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
- return self._read_body(self.read_code())
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
- return chr(int(self._unpack(">I", 4)))
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
- return raw.decode("utf-8")
679
- return Symbol(raw.decode("ascii"))
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
- result[key] = self.read_value()
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
- if message.body_as_string() in poisoned:
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=0,
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 == 5
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))