rabbitmq-amqp-python-client 2.0.0a1__tar.gz → 2.0.0b1__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 (112) hide show
  1. rabbitmq_amqp_python_client-2.0.0b1/.dockerignore +15 -0
  2. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/.github/workflows/publish-pypi.yaml +6 -7
  3. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/Makefile +1 -1
  4. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/PKG-INFO +16 -6
  5. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/README.md +15 -5
  6. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/docs/examples/README.md +5 -1
  7. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/docs/examples/auto_reconnection.py +1 -1
  8. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/docs/examples/basic_connection.py +3 -3
  9. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/docs/examples/console_application.py +166 -34
  10. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/docs/examples/consumer_example.py +2 -2
  11. rabbitmq_amqp_python_client-2.0.0b1/docs/examples/consumer_timeout_example.py +135 -0
  12. rabbitmq_amqp_python_client-2.0.0b1/docs/examples/delayed_retry_context_example.py +101 -0
  13. rabbitmq_amqp_python_client-2.0.0b1/docs/examples/delayed_retry_example.py +146 -0
  14. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/docs/examples/direct_reply_to_example.py +4 -4
  15. rabbitmq_amqp_python_client-2.0.0b1/docs/examples/docker/reliable/Dockerfile +45 -0
  16. rabbitmq_amqp_python_client-2.0.0b1/docs/examples/docker/reliable/README.md +60 -0
  17. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/docs/examples/management_example.py +3 -3
  18. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/docs/examples/performance_test.py +6 -6
  19. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/docs/examples/presettled_consumer_example.py +3 -3
  20. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/docs/examples/publisher_example.py +2 -2
  21. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/docs/examples/quorum_single_active_consumer.py +1 -1
  22. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/docs/examples/rejection_reason_example.py +2 -2
  23. rabbitmq_amqp_python_client-2.0.0b1/docs/examples/reliable.py +619 -0
  24. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/docs/examples/stream_filtering.py +2 -2
  25. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/docs/examples/tls_connection.py +1 -1
  26. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/pyproject.toml +1 -1
  27. {rabbitmq_amqp_python_client-2.0.0a1/src → rabbitmq_amqp_python_client-2.0.0b1/rabbitmq_amqp_python_client}/__init__.py +5 -1
  28. {rabbitmq_amqp_python_client-2.0.0a1/src → rabbitmq_amqp_python_client-2.0.0b1/rabbitmq_amqp_python_client}/constants.py +6 -0
  29. {rabbitmq_amqp_python_client-2.0.0a1/src → rabbitmq_amqp_python_client-2.0.0b1/rabbitmq_amqp_python_client}/consumer.py +331 -13
  30. {rabbitmq_amqp_python_client-2.0.0a1/src → rabbitmq_amqp_python_client-2.0.0b1/rabbitmq_amqp_python_client}/link.py +101 -20
  31. {rabbitmq_amqp_python_client-2.0.0a1/src → rabbitmq_amqp_python_client-2.0.0b1/rabbitmq_amqp_python_client}/logging_utils.py +4 -4
  32. {rabbitmq_amqp_python_client-2.0.0a1/src → rabbitmq_amqp_python_client-2.0.0b1/rabbitmq_amqp_python_client}/management.py +16 -0
  33. {rabbitmq_amqp_python_client-2.0.0a1/src → rabbitmq_amqp_python_client-2.0.0b1/rabbitmq_amqp_python_client}/wire/__init__.py +2 -0
  34. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/integration/test_client_interpolation_integration.py +3 -3
  35. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/integration/test_client_stream_interpolation_integration.py +4 -4
  36. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/integration/test_connection_integration.py +1 -1
  37. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/integration/test_consumer_integration.py +371 -9
  38. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/integration/test_management_integration.py +4 -4
  39. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/integration/test_performance_test_integration.py +2 -2
  40. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/integration/test_publisher_integration.py +3 -3
  41. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/integration/test_reconnection_integration.py +2 -2
  42. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/integration/test_session_link_integration.py +3 -3
  43. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/integration/test_tls_integration.py +1 -1
  44. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/conftest.py +3 -3
  45. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/fake_broker.py +4 -4
  46. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/test_amqp10_validation.py +3 -3
  47. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/test_connection.py +7 -7
  48. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/test_console_application.py +5 -5
  49. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/test_consumer.py +510 -11
  50. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/test_delivery_state.py +3 -3
  51. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/test_encoding.py +2 -2
  52. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/test_frames.py +3 -3
  53. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/test_link.py +4 -4
  54. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/test_management.py +4 -4
  55. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/test_management_builders.py +35 -2
  56. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/test_message.py +3 -3
  57. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/test_package_exports.py +3 -3
  58. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/test_performance_test.py +5 -5
  59. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/test_performatives.py +4 -4
  60. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/test_publisher.py +4 -4
  61. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/test_reconnection.py +10 -10
  62. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/test_sasl.py +3 -3
  63. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/test_session.py +4 -4
  64. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/.ci/certs/ca.p12 +0 -0
  65. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/.ci/certs/ca_certificate.pem +0 -0
  66. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/.ci/certs/ca_key.pem +0 -0
  67. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/.ci/certs/client_localhost.p12 +0 -0
  68. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/.ci/certs/client_localhost_certificate.pem +0 -0
  69. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/.ci/certs/client_localhost_key.pem +0 -0
  70. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/.ci/certs/server_localhost.p12 +0 -0
  71. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/.ci/certs/server_localhost_certificate.pem +0 -0
  72. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/.ci/certs/server_localhost_key.pem +0 -0
  73. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/.ci/ubuntu/advanced.config +0 -0
  74. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/.ci/ubuntu/enabled_plugins +0 -0
  75. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/.ci/ubuntu/gha-log-check.sh +0 -0
  76. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/.ci/ubuntu/gha-setup.sh +0 -0
  77. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/.ci/ubuntu/log/.gitkeep +0 -0
  78. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/.ci/ubuntu/rabbitmq.conf +0 -0
  79. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/.github/release.yml +0 -0
  80. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/.github/workflows/build-test.yaml +0 -0
  81. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/.github/workflows/publish_release.yaml +0 -0
  82. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/.gitignore +0 -0
  83. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/create_tag.sh +0 -0
  84. {rabbitmq_amqp_python_client-2.0.0a1/src → rabbitmq_amqp_python_client-2.0.0b1/rabbitmq_amqp_python_client}/connection.py +0 -0
  85. {rabbitmq_amqp_python_client-2.0.0a1/src → rabbitmq_amqp_python_client-2.0.0b1/rabbitmq_amqp_python_client}/exceptions.py +0 -0
  86. {rabbitmq_amqp_python_client-2.0.0a1/src → rabbitmq_amqp_python_client-2.0.0b1/rabbitmq_amqp_python_client}/publisher.py +0 -0
  87. {rabbitmq_amqp_python_client-2.0.0a1/src → rabbitmq_amqp_python_client-2.0.0b1/rabbitmq_amqp_python_client}/reconnection.py +0 -0
  88. {rabbitmq_amqp_python_client-2.0.0a1/src → rabbitmq_amqp_python_client-2.0.0b1/rabbitmq_amqp_python_client}/session.py +0 -0
  89. {rabbitmq_amqp_python_client-2.0.0a1/src → rabbitmq_amqp_python_client-2.0.0b1/rabbitmq_amqp_python_client}/wire/delivery_state.py +0 -0
  90. {rabbitmq_amqp_python_client-2.0.0a1/src → rabbitmq_amqp_python_client-2.0.0b1/rabbitmq_amqp_python_client}/wire/encoding.py +0 -0
  91. {rabbitmq_amqp_python_client-2.0.0a1/src → rabbitmq_amqp_python_client-2.0.0b1/rabbitmq_amqp_python_client}/wire/frames.py +0 -0
  92. {rabbitmq_amqp_python_client-2.0.0a1/src → rabbitmq_amqp_python_client-2.0.0b1/rabbitmq_amqp_python_client}/wire/message.py +0 -0
  93. {rabbitmq_amqp_python_client-2.0.0a1/src → rabbitmq_amqp_python_client-2.0.0b1/rabbitmq_amqp_python_client}/wire/performatives.py +0 -0
  94. {rabbitmq_amqp_python_client-2.0.0a1/src → rabbitmq_amqp_python_client-2.0.0b1/rabbitmq_amqp_python_client}/wire/sasl.py +0 -0
  95. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/__init__.py +0 -0
  96. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/integration/__init__.py +0 -0
  97. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/integration/conftest.py +0 -0
  98. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/integration/test_console_application_integration.py +0 -0
  99. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/__init__.py +0 -0
  100. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/resources/amqp10_validation/empty_message.amqp +0 -0
  101. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/resources/amqp10_validation/header_amqpvalue_message.amqp +0 -0
  102. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/resources/amqp10_validation/message_body_250.amqp +0 -0
  103. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/resources/amqp10_validation/message_body_700.amqp +0 -0
  104. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/resources/amqp10_validation/message_from_version_1_0_0.amqp +0 -0
  105. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/resources/amqp10_validation/message_random_application_properties_300.amqp +0 -0
  106. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/resources/amqp10_validation/message_random_application_properties_500.amqp +0 -0
  107. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/resources/amqp10_validation/message_random_application_properties_properties_900.amqp +0 -0
  108. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/resources/amqp10_validation/message_unicode_message.amqp +0 -0
  109. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/resources/amqp10_validation/nil_and_types.amqp +0 -0
  110. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/resources/amqp10_validation/shovel_annotations.amqp +0 -0
  111. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/resources/amqp10_validation/static_test_message_compare.amqp +0 -0
  112. {rabbitmq_amqp_python_client-2.0.0a1 → rabbitmq_amqp_python_client-2.0.0b1}/tests/unit/resources/amqp10_validation/uuid_message.amqp +0 -0
@@ -0,0 +1,15 @@
1
+ .git
2
+ .github
3
+ .venv
4
+ .mypy_cache
5
+ .pytest_cache
6
+ .ruff_cache
7
+ .idea
8
+ .claude
9
+ __pycache__
10
+ *.pyc
11
+ dist
12
+ build
13
+ *.egg-info
14
+ tests
15
+ .ci
@@ -25,14 +25,13 @@ jobs:
25
25
  id-token: write
26
26
  steps:
27
27
  - uses: actions/checkout@v3
28
- - name: build with poetry
29
- env:
30
- REPO: ${{ inputs.pypi_repository_url }}
28
+ - uses: actions/setup-python@v5
29
+ with:
30
+ python-version: "3.12"
31
+ - name: build
31
32
  run: |
32
- curl -sSL https://install.python-poetry.org | python3 -
33
- export PATH="/home/runner/.local/bin:$PATH"
34
- poetry config repositories.test-pypi $REPO
35
- poetry build
33
+ python3 -m pip install build
34
+ python3 -m build
36
35
  # retrieve your distributions here
37
36
  - name: Publish package distributions to PyPI
38
37
  uses: pypa/gh-action-pypi-publish@release/v1
@@ -21,7 +21,7 @@ lint:
21
21
  $(PYTHON) -m ruff format --check .
22
22
 
23
23
  typecheck:
24
- $(PYTHON) -m mypy src
24
+ $(PYTHON) -m mypy rabbitmq_amqp_python_client
25
25
  MYPYPATH=. $(PYTHON) -m mypy docs/examples
26
26
 
27
27
  build:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: rabbitmq-amqp-python-client
3
- Version: 2.0.0a1
3
+ Version: 2.0.0-beta.1
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
@@ -18,13 +18,23 @@ Description-Content-Type: text/markdown
18
18
 
19
19
  RabbitMQ AMQP 1.0 Python client version 2.x for RabbitMQ 4.x.
20
20
 
21
- This client version built from scratch. The primary reason for this rewrite is to remove the dependency on the `qpid-proton` C library present in version 1, providing a fully native Python client.
21
+ This client version built from scratch.</br>
22
+ 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).
22
23
  This client is inspired by the Java and .NET AMQP 1.0 clients, aiming to deliver the same behavior and user experience across ecosystems.
23
24
 
24
25
  The client is different from 1.0 and not compatible, so read the documentation before updating the client.
25
26
 
26
27
 
27
- ## Installation
28
+ ## Install
29
+
30
+ The client is distributed via [`PIP`](https://pypi.org/project/rabbitmq-amqp-python-client/):
31
+ ```bash
32
+ pip install rabbitmq-amqp-python-client==2.0.0ax
33
+ ```
34
+
35
+
36
+
37
+ ## Build from source
28
38
 
29
39
  ```sh
30
40
  python3 -m venv .venv
@@ -42,7 +52,7 @@ pip install -e ".[dev]"
42
52
  ## Quick start
43
53
 
44
54
  ```python
45
- from src import Connection, ConnectionParameters, Context, Message
55
+ from rabbitmq_amqp_python_client import Connection, ConnectionParameters, Context, Message
46
56
 
47
57
  connection = Connection(ConnectionParameters()) # localhost:5672, guest/guest
48
58
  try:
@@ -84,7 +94,7 @@ PYTHONPATH=. .venv/bin/python docs/examples/basic_connection.py
84
94
  make install # create .venv and install the package with its dev extras
85
95
  make format # ruff format, then ruff check --fix
86
96
  make lint # ruff check and ruff format --check
87
- make typecheck # mypy src, in strict mode
97
+ make typecheck # mypy rabbitmq_amqp_python_client, in strict mode
88
98
  make test-unit # the unit suite; no broker needed
89
99
  make test-integration # the integration suite; needs a broker on localhost:5672
90
100
  make test # both suites
@@ -98,7 +108,7 @@ MYPYPATH=. .venv/bin/python -m mypy docs/examples/*.py
98
108
 
99
109
  ## Project layout
100
110
 
101
- `src/` holds the client: `wire/` is the protocol
111
+ `rabbitmq_amqp_python_client/` holds the client: `wire/` is the protocol
102
112
  layer (type codec, frames, performatives, messages, SASL) and knows nothing
103
113
  above itself, and `connection.py`, `session.py`, `link.py`, `management.py`,
104
114
  `publisher.py`, `consumer.py` and `reconnection.py` build the public API on top
@@ -2,13 +2,23 @@
2
2
 
3
3
  RabbitMQ AMQP 1.0 Python client version 2.x for RabbitMQ 4.x.
4
4
 
5
- This client version built from scratch. The primary reason for this rewrite is to remove the dependency on the `qpid-proton` C library present in version 1, providing a fully native Python client.
5
+ This client version built from scratch.</br>
6
+ 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).
6
7
  This client is inspired by the Java and .NET AMQP 1.0 clients, aiming to deliver the same behavior and user experience across ecosystems.
7
8
 
8
9
  The client is different from 1.0 and not compatible, so read the documentation before updating the client.
9
10
 
10
11
 
11
- ## Installation
12
+ ## Install
13
+
14
+ The client is distributed via [`PIP`](https://pypi.org/project/rabbitmq-amqp-python-client/):
15
+ ```bash
16
+ pip install rabbitmq-amqp-python-client==2.0.0ax
17
+ ```
18
+
19
+
20
+
21
+ ## Build from source
12
22
 
13
23
  ```sh
14
24
  python3 -m venv .venv
@@ -26,7 +36,7 @@ pip install -e ".[dev]"
26
36
  ## Quick start
27
37
 
28
38
  ```python
29
- from src import Connection, ConnectionParameters, Context, Message
39
+ from rabbitmq_amqp_python_client import Connection, ConnectionParameters, Context, Message
30
40
 
31
41
  connection = Connection(ConnectionParameters()) # localhost:5672, guest/guest
32
42
  try:
@@ -68,7 +78,7 @@ PYTHONPATH=. .venv/bin/python docs/examples/basic_connection.py
68
78
  make install # create .venv and install the package with its dev extras
69
79
  make format # ruff format, then ruff check --fix
70
80
  make lint # ruff check and ruff format --check
71
- make typecheck # mypy src, in strict mode
81
+ make typecheck # mypy rabbitmq_amqp_python_client, in strict mode
72
82
  make test-unit # the unit suite; no broker needed
73
83
  make test-integration # the integration suite; needs a broker on localhost:5672
74
84
  make test # both suites
@@ -82,7 +92,7 @@ MYPYPATH=. .venv/bin/python -m mypy docs/examples/*.py
82
92
 
83
93
  ## Project layout
84
94
 
85
- `src/` holds the client: `wire/` is the protocol
95
+ `rabbitmq_amqp_python_client/` holds the client: `wire/` is the protocol
86
96
  layer (type codec, frames, performatives, messages, SASL) and knows nothing
87
97
  above itself, and `connection.py`, `session.py`, `link.py`, `management.py`,
88
98
  `publisher.py`, `consumer.py` and `reconnection.py` build the public API on top
@@ -18,11 +18,15 @@ one layer at a time, and the rest build on them.
18
18
  | [`publisher_example.py`](publisher_example.py) | The three targets a publisher can have — a queue, an exchange plus a routing key, and none at all. The anonymous publisher sends to two different addresses over one link by setting `properties.to` per message, and a publish to an unbound routing key shows a `RELEASED` outcome, which is not an error. |
19
19
  | [`consumer_example.py`](consumer_example.py) | A `message_handler` that accepts and counts every delivery, `unsettled_message_count` while it does, and `pause()`/`unpause()` as flow control: messages published while paused stay queued and arrive the moment credit is restored. |
20
20
  | [`auto_reconnection.py`](auto_reconnection.py) | A connection surviving a forced socket drop: publishers and consumers built before the drop keep working afterwards, and `RecoveryConfiguration(topology=True)` re-declares the topology the broker lost. |
21
- | [`console_application.py`](console_application.py) | The whole client end to end — declare, publish, consume, report — as a scriptable smoke test with meaningful exit codes. `--help` lists its options. |
21
+ | [`console_application.py`](console_application.py) | The whole client end to end — declare, publish, consume, report — as a scriptable smoke test with meaningful exit codes. `--help` lists its options, every one of which also has an `AMQP_*` environment-variable override; see [`docker/`](docker/) for running it as a container configured entirely that way. |
22
+ | [`reliable.py`](reliable.py) | Modeled on [rabbitmq-amqp-go-client's own `reliable` example](https://github.com/rabbitmq/rabbitmq-amqp-go-client/tree/main/docs/examples/reliable): a publisher and a consumer built once on a quorum queue and left running while a broker disconnect/reconnect can happen at any point without either being rebuilt, with every publish outcome classified and periodic stats logged throughout. Configured entirely through environment variables, no command-line flags; see [`docker/reliable/`](docker/reliable/) for running it as a container. |
22
23
  | [`performance_test.py`](performance_test.py) | A throughput/latency generator: many publishers and consumers on one or more connections, with periodic rate and latency reporting. |
23
24
  | [`presettled_consumer_example.py`](presettled_consumer_example.py) | At-most-once consumption: `settle_strategy(ConsumerSettleStrategy.PRESETTLED)` attaches with `snd-settle-mode = settled`, so the broker settles every delivery itself, the handler never touches its `Context`, and `unsettled_message_count` stays at `0` for the consumer's whole life — including across a forced disconnect that auto-reconnection recovers from. |
24
25
  | [`direct_reply_to_example.py`](direct_reply_to_example.py) | Request/reply without a dedicated reply queue: `settle_strategy(ConsumerSettleStrategy.DIRECT_REPLY_TO)` attaches to no queue at all and reads back a broker-generated pseudo-queue address from `consumer.queue`; a request naming it as `properties.reply_to` gets answered by an ordinary queue-bound consumer publishing to that address. The pseudo-queue is session-scoped, so a forced disconnect and auto-reconnection leave the requester with a brand new address, which the script re-reads before sending its second request. |
25
26
  | [`rejection_reason_example.py`](rejection_reason_example.py) | Why a publish was refused: a quorum queue with `max_length(5)` and the `reject-publish` overflow strategy returns a `REJECTED` outcome carrying `RejectionDetails.reason` and `.rejected_by_queue` (needs RabbitMQ 4.3+; the script says so when the broker supplies neither). |
26
27
  | [`quorum_single_active_consumer.py`](quorum_single_active_consumer.py) | Single active consumer notifications on a quorum queue (needs RabbitMQ 4.3+): two consumers on a queue declared with `single_active_consumer(True)` each register a `single_active_consumer_state_changed` handler, and the broker tells one of them it is active and the other that it is standby. Only the active one receives messages; closing it promotes the standby one, which then starts receiving. |
28
+ | [`delayed_retry_example.py`](delayed_retry_example.py) | Delaying a redelivery on a quorum queue (needs RabbitMQ 4.3+): a queue declared with `delayed_retry_type(QuorumQueueDelayedRetryType.FAILED)`, `delayed_retry_min(...)` and `delayed_retry_max(...)` holds a `context.discard()`-rejected message back for that window before redelivering it, while a `context.requeue()`-released one — a *returned*, not a *failed*, delivery, so `FAILED` does not cover it — comes back immediately. |
29
+ | [`delayed_retry_context_example.py`](delayed_retry_context_example.py) | Per-message delayed retry (needs RabbitMQ 4.3+): `context.delayed_retry(delay)` stamps an `x-opt-delivery-time` annotation on one redelivery, holding it back for that message alone — no queue-level `x-delayed-retry-*` configuration required at all, unlike `delayed_retry_example.py` above. |
30
+ | [`consumer_timeout_example.py`](consumer_timeout_example.py) | Consumer timeout (needs RabbitMQ 4.3+): `quorum().consumer_timeout(...)` sets the `rabbitmq:consumer-timeout` attach property, and `quorum().on_delivery_release(handler)` is called with a `TimeoutContext` when the broker force-releases a delivery held past that timeout — accepting it there is what lifts the broker's block on further delivery to that consumer. |
27
31
  | [`stream_filtering.py`](stream_filtering.py) | Reading a stream queue from a chosen offset and narrowing what arrives (needs RabbitMQ 4.2+ for the SQL filter): the same batch of messages is read back three times from `offset(FIRST)` — once with `filter().subject(...).property("region", ...)`, once with the equivalent `filter().sql(...)`, and once with the cheap, probabilistic `filter_values(...)` bloom filter, whose publisher side is just an `x-stream-filter-value` message annotation. The script reports whether the broker really enforces the SQL filter. |
28
32
  | [`tls_connection.py`](tls_connection.py) | TLS transport and certificate-based authentication (needs the broker's TLS listener enabled, `.ci/ubuntu/rabbitmq.conf`): one-way TLS with the broker's certificate verified against `.ci/certs/ca_certificate.pem`, then mutual TLS where a client certificate authenticates via SASL EXTERNAL (`sasl_external=True`) instead of a username/password, and finally — clearly separated and explicitly discouraged — a connection with verification disabled entirely. |
@@ -27,7 +27,7 @@ import socket
27
27
  import time
28
28
  import uuid
29
29
 
30
- from src import (
30
+ from rabbitmq_amqp_python_client import (
31
31
  AMQPError,
32
32
  Connection,
33
33
  ConnectionParameters,
@@ -4,12 +4,12 @@ Run against a local broker::
4
4
 
5
5
  PYTHONPATH=. .venv/bin/python docs/examples/basic_connection.py
6
6
 
7
- Constructing a :class:`~src.Connection` dials the TCP
7
+ Constructing a :class:`~rabbitmq_amqp_python_client.Connection` dials the TCP
8
8
  socket, exchanges the protocol header, runs SASL and the ``open`` handshake, and
9
9
  starts the frame reader — all before ``__init__`` returns. There is no separate
10
10
  ``connect()`` step: a constructed connection is already ``OPEN`` and usable.
11
11
 
12
- The default :class:`~src.ConnectionParameters` describe
12
+ The default :class:`~rabbitmq_amqp_python_client.ConnectionParameters` describe
13
13
  exactly what a stock broker offers — ``localhost:5672``, ``guest``/``guest``, the
14
14
  ``/`` virtual host — so the no-argument form below is the shortest connection
15
15
  this client can make. What the broker sent back in its own ``open`` is readable
@@ -24,7 +24,7 @@ from __future__ import annotations
24
24
 
25
25
  import logging
26
26
 
27
- from src import Connection, ConnectionParameters
27
+ from rabbitmq_amqp_python_client import Connection, ConnectionParameters
28
28
 
29
29
  logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-7s %(name)s: %(message)s")
30
30
  logger = logging.getLogger("example")
@@ -10,12 +10,21 @@ both confirmed and consumed, so this doubles as a scriptable smoke test::
10
10
  python3 docs/examples/console_application.py --messages 500 --queue-type quorum
11
11
  python3 docs/examples/console_application.py --help
12
12
 
13
+ Every flag also has an ``AMQP_*`` environment variable that sets its default
14
+ (a flag given on the command line always wins); see each flag's ``--help``
15
+ text for its variable's name. That is what lets docs/examples/docker/Dockerfile
16
+ run this script as a container configured entirely through ``docker run -e``,
17
+ with nothing broker-specific baked into the image::
18
+
19
+ docker build -f docs/examples/docker/Dockerfile -t console-application .
20
+ docker run --rm -e AMQP_HOST=rabbitmq -e AMQP_MESSAGES=500 console-application
21
+
13
22
  This program adds nothing to the client's public surface: it is built entirely
14
- out of :class:`~src.Connection`,
15
- :class:`~src.Management`,
16
- :class:`~src.Publisher`,
17
- :class:`~src.Consumer` and
18
- :class:`~src.RecoveryConfiguration`. It owns exactly one
23
+ out of :class:`~rabbitmq_amqp_python_client.Connection`,
24
+ :class:`~rabbitmq_amqp_python_client.Management`,
25
+ :class:`~rabbitmq_amqp_python_client.Publisher`,
26
+ :class:`~rabbitmq_amqp_python_client.Consumer` and
27
+ :class:`~rabbitmq_amqp_python_client.RecoveryConfiguration`. It owns exactly one
19
28
  connection: it is a correctness/smoke-test tool, not a load generator.
20
29
 
21
30
  Exit codes:
@@ -33,6 +42,7 @@ from __future__ import annotations
33
42
 
34
43
  import argparse
35
44
  import logging
45
+ import os
36
46
  import ssl
37
47
  import sys
38
48
  import threading
@@ -44,12 +54,15 @@ from pathlib import Path
44
54
  from typing import TYPE_CHECKING, TextIO
45
55
 
46
56
  # Make the client importable when this example is run straight from a checkout,
47
- # with neither the package installed nor PYTHONPATH set.
48
- _SOURCE_ROOT = Path(__file__).resolve().parents[2]
49
- if _SOURCE_ROOT.is_dir() and str(_SOURCE_ROOT) not in sys.path:
57
+ # with neither the package installed nor PYTHONPATH set. Guarded by a length
58
+ # check because a containerized copy of this script (see docs/examples/docker/)
59
+ # lives too shallow in the image's filesystem for a third parent to exist.
60
+ _RESOLVED_PARENTS = Path(__file__).resolve().parents
61
+ _SOURCE_ROOT = _RESOLVED_PARENTS[2] if len(_RESOLVED_PARENTS) > 2 else None
62
+ if _SOURCE_ROOT is not None and _SOURCE_ROOT.is_dir() and str(_SOURCE_ROOT) not in sys.path:
50
63
  sys.path.insert(0, str(_SOURCE_ROOT))
51
64
 
52
- from src import ( # noqa: E402 - after the sys.path bootstrap above
65
+ from rabbitmq_amqp_python_client import ( # noqa: E402 - after the sys.path bootstrap above
53
66
  AMQPError,
54
67
  Connection,
55
68
  ConnectionParameters,
@@ -64,7 +77,7 @@ from src import ( # noqa: E402 - after the sys.path bootstrap above
64
77
  )
65
78
 
66
79
  if TYPE_CHECKING:
67
- from src.connection import ConnectionState
80
+ from rabbitmq_amqp_python_client.connection import ConnectionState
68
81
 
69
82
  # --- exit codes (§7) ----------------------------------------------------
70
83
 
@@ -93,6 +106,79 @@ QUEUE_TYPES = (QUEUE_TYPE_CLASSIC, QUEUE_TYPE_QUORUM, QUEUE_TYPE_STREAM)
93
106
  #: Prefix of the queue name generated when ``--queue`` is not given.
94
107
  GENERATED_QUEUE_PREFIX = "console-app-"
95
108
 
109
+ # --- environment overrides (§2) -----------------------------------------
110
+ #
111
+ # Every flag below also reads an ``AMQP_*`` environment variable as its
112
+ # default, before argparse's own default applies; a flag given on the command
113
+ # line always wins. This is what lets docs/examples/docker/Dockerfile
114
+ # configure a run entirely through ``docker run -e``, with no broker detail
115
+ # baked into the image.
116
+
117
+ _ENV_TRUE_VALUES = frozenset({"1", "true", "yes", "on"})
118
+ _ENV_FALSE_VALUES = frozenset({"0", "false", "no", "off"})
119
+
120
+
121
+ def _env_str(name: str, default: str) -> str:
122
+ """Return the string environment variable ``name``, or ``default`` if unset."""
123
+ return os.environ.get(name, default)
124
+
125
+
126
+ def _env_optional_int(name: str) -> int | None:
127
+ """Return ``name`` parsed as an int, or ``None`` if unset.
128
+
129
+ Raises:
130
+ OptionsError: If ``name`` is set but is not a valid integer.
131
+ """
132
+ value = os.environ.get(name)
133
+ if value is None:
134
+ return None
135
+ try:
136
+ return int(value)
137
+ except ValueError:
138
+ raise OptionsError(f"{name}={value!r} is not a valid integer") from None
139
+
140
+
141
+ def _env_int(name: str, default: int) -> int:
142
+ """Return ``name`` parsed as an int, or ``default`` if unset."""
143
+ value = _env_optional_int(name)
144
+ return default if value is None else value
145
+
146
+
147
+ def _env_float(name: str, default: float) -> float:
148
+ """Return ``name`` parsed as a float, or ``default`` if unset.
149
+
150
+ Raises:
151
+ OptionsError: If ``name`` is set but is not a valid float.
152
+ """
153
+ value = os.environ.get(name)
154
+ if value is None:
155
+ return default
156
+ try:
157
+ return float(value)
158
+ except ValueError:
159
+ raise OptionsError(f"{name}={value!r} is not a valid float") from None
160
+
161
+
162
+ def _env_bool(name: str, default: bool) -> bool:
163
+ """Return the boolean ``name`` is set to, or ``default`` if unset.
164
+
165
+ Accepts ``1``/``true``/``yes``/``on`` and ``0``/``false``/``no``/``off``,
166
+ case-insensitively.
167
+
168
+ Raises:
169
+ OptionsError: If ``name`` is set to anything else.
170
+ """
171
+ value = os.environ.get(name)
172
+ if value is None:
173
+ return default
174
+ normalized = value.strip().lower()
175
+ if normalized in _ENV_TRUE_VALUES:
176
+ return True
177
+ if normalized in _ENV_FALSE_VALUES:
178
+ return False
179
+ raise OptionsError(f"{name}={value!r} is not a valid boolean (true/false, yes/no, on/off, 1/0)")
180
+
181
+
96
182
  # --- timing -------------------------------------------------------------
97
183
 
98
184
  #: How long the drain wait blocks per poll before re-reading the counters.
@@ -169,8 +255,19 @@ def build_parser() -> argparse.ArgumentParser:
169
255
 
170
256
  Returns:
171
257
  A parser whose defaults match :class:`Options`', except for ``--queue``,
172
- which is generated per run by :func:`parse_args`.
258
+ which is generated per run by :func:`parse_args`. Every default is first
259
+ read from its ``AMQP_*`` environment variable, falling back to
260
+ :class:`Options`' own default when that variable is unset.
261
+
262
+ Raises:
263
+ OptionsError: If an ``AMQP_*`` environment variable is set to a value
264
+ its flag cannot accept — the same rejection :func:`parse_args`
265
+ raises for a bad command-line value, just discovered earlier.
173
266
  """
267
+ queue_type_default = _env_str("AMQP_QUEUE_TYPE", QUEUE_TYPE_CLASSIC)
268
+ if queue_type_default not in QUEUE_TYPES:
269
+ raise OptionsError(f"AMQP_QUEUE_TYPE={queue_type_default!r} must be one of {', '.join(QUEUE_TYPES)}")
270
+
174
271
  parser = argparse.ArgumentParser(
175
272
  prog="console_application.py",
176
273
  description="Publish and consume messages against one RabbitMQ queue, then report what happened.",
@@ -179,52 +276,87 @@ def build_parser() -> argparse.ArgumentParser:
179
276
  "-n",
180
277
  "--messages",
181
278
  type=int,
182
- default=DEFAULT_MESSAGE_COUNT,
183
- help="how many messages to publish (default: %(default)s); 0 is rejected",
279
+ default=_env_int("AMQP_MESSAGES", DEFAULT_MESSAGE_COUNT),
280
+ help="how many messages to publish (default: %(default)s, env: AMQP_MESSAGES); 0 is rejected",
184
281
  )
185
282
  parser.add_argument(
186
283
  "--queue-type",
187
284
  choices=QUEUE_TYPES,
188
- default=QUEUE_TYPE_CLASSIC,
189
- help="which queue sub-builder declares the queue (default: %(default)s)",
285
+ default=queue_type_default,
286
+ help="which queue sub-builder declares the queue (default: %(default)s, env: AMQP_QUEUE_TYPE)",
287
+ )
288
+ parser.add_argument(
289
+ "--queue",
290
+ default=_env_str("AMQP_QUEUE", ""),
291
+ help="queue name (default: a generated console-app-* name, env: AMQP_QUEUE)",
292
+ )
293
+ parser.add_argument(
294
+ "--keep-queue",
295
+ action="store_true",
296
+ default=_env_bool("AMQP_KEEP_QUEUE", False),
297
+ help="do not delete the queue during teardown (env: AMQP_KEEP_QUEUE)",
190
298
  )
191
- parser.add_argument("--queue", default="", help="queue name (default: a generated console-app-* name)")
192
- parser.add_argument("--keep-queue", action="store_true", help="do not delete the queue during teardown")
193
299
  parser.add_argument(
194
300
  "--consume-timeout",
195
301
  type=float,
196
- default=DEFAULT_CONSUME_TIMEOUT_SECONDS,
197
- help="seconds to wait for consumption to catch up (default: %(default)s)",
302
+ default=_env_float("AMQP_CONSUME_TIMEOUT", DEFAULT_CONSUME_TIMEOUT_SECONDS),
303
+ help="seconds to wait for consumption to catch up (default: %(default)s, env: AMQP_CONSUME_TIMEOUT)",
198
304
  )
199
305
  parser.add_argument(
200
306
  "--publish-timeout",
201
307
  type=float,
202
- default=DEFAULT_PUBLISH_TIMEOUT_SECONDS,
203
- help="per-call publish timeout, in seconds (default: %(default)s)",
308
+ default=_env_float("AMQP_PUBLISH_TIMEOUT", DEFAULT_PUBLISH_TIMEOUT_SECONDS),
309
+ help="per-call publish timeout, in seconds (default: %(default)s, env: AMQP_PUBLISH_TIMEOUT)",
204
310
  )
205
311
  parser.add_argument(
206
312
  "--stats-interval",
207
313
  type=float,
208
- default=DEFAULT_STATS_INTERVAL_SECONDS,
209
- help="seconds between periodic stats blocks (default: %(default)s)",
314
+ default=_env_float("AMQP_STATS_INTERVAL", DEFAULT_STATS_INTERVAL_SECONDS),
315
+ help="seconds between periodic stats blocks (default: %(default)s, env: AMQP_STATS_INTERVAL)",
316
+ )
317
+ parser.add_argument(
318
+ "--host",
319
+ default=_env_str("AMQP_HOST", "localhost"),
320
+ help="broker host (default: %(default)s, env: AMQP_HOST)",
321
+ )
322
+ parser.add_argument(
323
+ "--port",
324
+ type=int,
325
+ default=_env_optional_int("AMQP_PORT"),
326
+ help="broker port (default: 5672, or 5671 with --tls; env: AMQP_PORT)",
327
+ )
328
+ parser.add_argument(
329
+ "--user",
330
+ default=_env_str("AMQP_USER", "guest"),
331
+ help="SASL PLAIN username (default: %(default)s, env: AMQP_USER)",
332
+ )
333
+ parser.add_argument(
334
+ "--password",
335
+ default=_env_str("AMQP_PASSWORD", "guest"),
336
+ help="SASL PLAIN password (default: %(default)s, env: AMQP_PASSWORD)",
337
+ )
338
+ parser.add_argument(
339
+ "--vhost",
340
+ default=_env_str("AMQP_VHOST", "/"),
341
+ help="virtual host (default: %(default)s, env: AMQP_VHOST)",
342
+ )
343
+ parser.add_argument(
344
+ "--tls",
345
+ action="store_true",
346
+ default=_env_bool("AMQP_TLS", False),
347
+ help="wrap the connection in TLS (env: AMQP_TLS)",
210
348
  )
211
- parser.add_argument("--host", default="localhost", help="broker host (default: %(default)s)")
212
- parser.add_argument("--port", type=int, default=None, help="broker port (default: 5672, or 5671 with --tls)")
213
- parser.add_argument("--user", default="guest", help="SASL PLAIN username (default: %(default)s)")
214
- parser.add_argument("--password", default="guest", help="SASL PLAIN password (default: %(default)s)")
215
- parser.add_argument("--vhost", default="/", help="virtual host (default: %(default)s)")
216
- parser.add_argument("--tls", action="store_true", help="wrap the connection in TLS")
217
349
  parser.add_argument(
218
350
  "--recovery",
219
351
  action=argparse.BooleanOptionalAction,
220
- default=True,
221
- help="auto-reconnect after an unexpected disconnect (default: enabled)",
352
+ default=_env_bool("AMQP_RECOVERY", True),
353
+ help="auto-reconnect after an unexpected disconnect (default: enabled, env: AMQP_RECOVERY)",
222
354
  )
223
355
  parser.add_argument(
224
356
  "--recovery-topology",
225
357
  action=argparse.BooleanOptionalAction,
226
- default=False,
227
- help="redeclare recorded topology after a reconnect (default: disabled)",
358
+ default=_env_bool("AMQP_RECOVERY_TOPOLOGY", False),
359
+ help="redeclare recorded topology after a reconnect (default: disabled, env: AMQP_RECOVERY_TOPOLOGY)",
228
360
  )
229
361
  return parser
230
362
 
@@ -475,7 +607,7 @@ def map_queue_type(options: Options, specification: QueueSpecification) -> Queue
475
607
  specification: The builder from ``management.queue(name)``.
476
608
 
477
609
  Returns:
478
- The same builder, ready to :meth:`~src.QueueSpecification.declare`.
610
+ The same builder, ready to :meth:`~rabbitmq_amqp_python_client.QueueSpecification.declare`.
479
611
 
480
612
  Raises:
481
613
  OptionsError: If ``queue_type`` is not one of :data:`QUEUE_TYPES`.
@@ -8,7 +8,7 @@ A consumer is a receiver link plus a ``message_handler``. The link is granted
8
8
  ``initial_credits`` when it attaches, and the client re-grants one credit every
9
9
  time a delivery is settled, so at most that many messages are ever outstanding.
10
10
  The handler runs on the client's delivery thread and is handed a
11
- :class:`~src.Context` alongside the message; calling
11
+ :class:`~rabbitmq_amqp_python_client.Context` alongside the message; calling
12
12
  ``context.accept()`` settles the delivery as ``accepted``, which is what tells
13
13
  the broker it may drop it. ``discard()`` and ``requeue()`` are the other two
14
14
  choices, and doing nothing leaves the delivery unsettled — visible as
@@ -28,7 +28,7 @@ import logging
28
28
  import queue
29
29
  import uuid
30
30
 
31
- from src import (
31
+ from rabbitmq_amqp_python_client import (
32
32
  Connection,
33
33
  ConnectionParameters,
34
34
  Context,
@@ -0,0 +1,135 @@
1
+ """Consumer timeout: ``rabbitmq:consumer-timeout`` and ``on_delivery_release`` (step_130).
2
+
3
+ Run against a local RabbitMQ 4.3+ broker::
4
+
5
+ PYTHONPATH=. .venv/bin/python docs/examples/consumer_timeout_example.py
6
+
7
+ RabbitMQ limits how long a consumer may hold a delivery unsettled before the
8
+ broker takes it back and blocks further delivery to that link, until the
9
+ client acknowledges the release. This client surfaces that as
10
+ ``quorum().consumer_timeout(...)`` (the ``rabbitmq:consumer-timeout`` attach
11
+ property) plus ``quorum().on_delivery_release(handler)``, which is called once
12
+ per delivery the broker force-releases this way — ``handler(context, message)``
13
+ is given a :class:`TimeoutContext`, whose only valid settlement is
14
+ ``accept()``, and calling it is what lifts the broker's block.
15
+
16
+ This script attaches a consumer with a short ``consumer_timeout`` and an
17
+ ``on_delivery_release`` handler that logs the release and accepts it,
18
+ publishes one message whose handler deliberately sleeps past that timeout on
19
+ its first delivery (settling normally afterward), observes the release fire
20
+ and unlock the consumer, then publishes a second message and confirms it
21
+ still arrives before closing everything down.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import logging
27
+ import time
28
+ import uuid
29
+
30
+ from rabbitmq_amqp_python_client import (
31
+ Connection,
32
+ ConnectionParameters,
33
+ ConsumerError,
34
+ Context,
35
+ Message,
36
+ QueueType,
37
+ TimeoutContext,
38
+ )
39
+
40
+ #: The consumer's own rabbitmq:consumer-timeout, in milliseconds and seconds.
41
+ CONSUMER_TIMEOUT_MS = 3_000
42
+ CONSUMER_TIMEOUT_SECONDS = CONSUMER_TIMEOUT_MS / 1_000
43
+
44
+ #: How long the example waits for something it expects to happen.
45
+ TIMEOUT_SECONDS = 20.0
46
+
47
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-7s %(name)s: %(message)s")
48
+ logger = logging.getLogger("example")
49
+
50
+
51
+ def consumer_timeout() -> None:
52
+ """Show a timed-out delivery released by the broker, and the consumer unlocked afterward."""
53
+ connection = Connection(ConnectionParameters(container_id=f"example-consumer-timeout-{uuid.uuid4().hex[:8]}"))
54
+ name = f"example-consumer-timeout-{uuid.uuid4().hex[:8]}"
55
+ try:
56
+ connection.management().queue(name).type(QueueType.QUORUM).declare()
57
+ logger.info("declared quorum queue %r", name)
58
+
59
+ released: list[tuple[TimeoutContext, Message]] = []
60
+ second_message: list[Message] = []
61
+ timeout_triggered = False
62
+
63
+ def on_delivery_release(context: TimeoutContext, message: Message) -> None:
64
+ logger.info(
65
+ "broker released %r past the %.1fs consumer-timeout; accepting",
66
+ message.body_as_string(),
67
+ CONSUMER_TIMEOUT_SECONDS,
68
+ )
69
+ context.accept()
70
+ released.append((context, message))
71
+
72
+ def on_message(context: Context, message: Message) -> None:
73
+ nonlocal timeout_triggered
74
+ if not timeout_triggered:
75
+ timeout_triggered = True
76
+ logger.info(
77
+ "holding %r unsettled for %.1fs to trigger the consumer-timeout",
78
+ message.body_as_string(),
79
+ CONSUMER_TIMEOUT_SECONDS + 2,
80
+ )
81
+ time.sleep(CONSUMER_TIMEOUT_SECONDS + 2)
82
+ try:
83
+ context.accept()
84
+ except ConsumerError:
85
+ logger.info("as expected: the broker had already released this delivery")
86
+ return
87
+ context.accept()
88
+ second_message.append(message)
89
+ logger.info("second message %r arrived and was accepted normally", message.body_as_string())
90
+
91
+ consumer = (
92
+ connection.consumer_builder()
93
+ .queue(name)
94
+ .quorum()
95
+ .consumer_timeout(CONSUMER_TIMEOUT_MS)
96
+ .on_delivery_release(on_delivery_release)
97
+ .builder()
98
+ .message_handler(on_message)
99
+ .initial_credits(1)
100
+ .build()
101
+ )
102
+ logger.info("consumer attached with rabbitmq:consumer-timeout=%dms", CONSUMER_TIMEOUT_MS)
103
+
104
+ try:
105
+ publisher = connection.publisher_builder().queue(name).build()
106
+ try:
107
+ publisher.publish(Message("held-too-long"), timeout=TIMEOUT_SECONDS)
108
+
109
+ deadline = time.monotonic() + TIMEOUT_SECONDS
110
+ while not released and time.monotonic() < deadline:
111
+ time.sleep(0.05)
112
+ if not released:
113
+ raise TimeoutError(f"the broker never released the timed-out delivery within {TIMEOUT_SECONDS:g}s")
114
+ logger.info("on_delivery_release fired; the broker's block on this link should now be lifted")
115
+
116
+ publisher.publish(Message("second"), timeout=TIMEOUT_SECONDS)
117
+ deadline = time.monotonic() + TIMEOUT_SECONDS
118
+ while not second_message and time.monotonic() < deadline:
119
+ time.sleep(0.05)
120
+ if not second_message:
121
+ raise TimeoutError("the consumer was never unlocked: the second message never arrived")
122
+ logger.info("confirmed: the consumer is unlocked and receiving normally again")
123
+ finally:
124
+ publisher.close()
125
+ finally:
126
+ consumer.close()
127
+
128
+ connection.management().queue(name).delete()
129
+ logger.info("deleted the queue %r", name)
130
+ finally:
131
+ connection.close()
132
+
133
+
134
+ if __name__ == "__main__":
135
+ consumer_timeout()