mq-bridge-py-basic 0.3.0__tar.gz → 0.3.1__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 (203) hide show
  1. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/.github/workflows/publish-node.yml +13 -7
  2. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/Cargo.lock +17 -4
  3. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/Cargo.toml +2 -2
  4. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/PKG-INFO +94 -7
  5. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/README.md +23 -1
  6. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/benches/performance_bench.rs +1 -1
  7. mq_bridge_py_basic-0.3.1/bindings-common/Cargo.toml +19 -0
  8. mq_bridge_py_basic-0.3.1/bindings-common/src/lib.rs +308 -0
  9. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/mq_bridge/__init__.py +2 -0
  10. mq_bridge_py_basic-0.3.1/mq_bridge/__init__.pyi +227 -0
  11. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/mq_bridge/config.pyi +2 -1
  12. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/python/mq-bridge-py/Cargo.toml +1 -0
  13. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/python/mq-bridge-py/README.md +93 -6
  14. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/python/mq-bridge-py/analysis/bench_http_native.py +7 -9
  15. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/python/mq-bridge-py/example.py +2 -2
  16. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/python/mq-bridge-py/examples/bench_memory.py +2 -2
  17. mq_bridge_py_basic-0.3.1/python/mq-bridge-py/examples/consume_pull.py +39 -0
  18. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/python/mq-bridge-py/examples/json_route.py +2 -2
  19. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/python/mq-bridge-py/examples/raw_route.py +2 -2
  20. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/python/mq-bridge-py/scripts/gen_config_types.py +1 -1
  21. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/python/mq-bridge-py/src/lib.rs +420 -164
  22. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/python/mq-bridge-py/tests/test_routes.py +108 -3
  23. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/canonical_message.rs +33 -0
  24. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/grpc.rs +17 -2
  25. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/kafka.rs +31 -6
  26. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/mqtt.rs +8 -2
  27. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/models.rs +19 -0
  28. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/traits.rs +15 -0
  29. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/kafka.rs +3 -3
  30. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/route.rs +35 -10
  31. mq_bridge_py_basic-0.3.0/mq_bridge/__init__.pyi +0 -146
  32. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  33. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  34. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  35. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/.github/workflows/benchmark.yml +0 -0
  36. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/.github/workflows/ci.yml +0 -0
  37. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/.github/workflows/ibm-mq.yml +0 -0
  38. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/.github/workflows/node.yml +0 -0
  39. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/.github/workflows/publish-python.yml +0 -0
  40. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/.github/workflows/python.yml +0 -0
  41. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/.github/workflows/release.yml +0 -0
  42. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/.github/workflows/test-matrix.yml +0 -0
  43. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/.gitignore +0 -0
  44. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/ARCHITECTURE.md +0 -0
  45. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/CONFIGURATION.md +0 -0
  46. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/CONTRIBUTING.md +0 -0
  47. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/LICENSE +0 -0
  48. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/LICENSE.python +0 -0
  49. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/benches/router_bench.rs +0 -0
  50. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/build.rs +0 -0
  51. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/claude.md +0 -0
  52. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/examples/ipc_worker_queue.yaml +0 -0
  53. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/mq-bridge.schema.json +0 -0
  54. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/mq_bridge/config.py +0 -0
  55. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/mq_bridge/py.typed +0 -0
  56. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/package-lock.json +0 -0
  57. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/pyproject.toml +0 -0
  58. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/python/mq-bridge-py/LICENSE.python +0 -0
  59. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/python/mq-bridge-py/examples/bench_memory.yaml +0 -0
  60. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/python/mq-bridge-py/examples/memory.yaml +0 -0
  61. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/python/mq-bridge-py/pyproject-basic.toml +0 -0
  62. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/python/mq-bridge-py/tests/test_config_types.py +0 -0
  63. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/python/mq-bridge-py/tests/test_packaging.py +0 -0
  64. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/python/mq-bridge-py/tests/test_performance_smoke.py +0 -0
  65. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/python/mq-bridge-py/tests/test_public_api.py +0 -0
  66. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/python/mq-bridge-py/uv.lock +0 -0
  67. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/scripts/analysis/HTTP_STREAM_ENDPOINT_PLAN.md +0 -0
  68. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/scripts/analysis/benches/metadata_bench.rs +0 -0
  69. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/scripts/analysis/debug_ibm_mq_tls.rs +0 -0
  70. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/scripts/analysis/http/analyze_xctrace_time_profile.py +0 -0
  71. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/scripts/analysis/http/http_flamegraph.py +0 -0
  72. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/scripts/analysis/http/mq_bridge_http_profile.rs +0 -0
  73. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/scripts/analysis/http/run_http_ladder.sh +0 -0
  74. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/scripts/httparena/README.md +0 -0
  75. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/scripts/httparena/frameworks/mq-bridge-py/Dockerfile +0 -0
  76. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/scripts/httparena/frameworks/mq-bridge-py/meta.json +0 -0
  77. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/scripts/httparena/frameworks/mq-bridge-py/server.py +0 -0
  78. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/scripts/techempower/Python/mq-bridge-py/README.md +0 -0
  79. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/scripts/techempower/Python/mq-bridge-py/benchmark_config.json +0 -0
  80. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/scripts/techempower/Python/mq-bridge-py/mq-bridge-py.dockerfile +0 -0
  81. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/scripts/techempower/Python/mq-bridge-py/server.py +0 -0
  82. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/scripts/techempower/README.md +0 -0
  83. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/scripts/techempower/postgres.yml +0 -0
  84. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/scripts/techempower/seed.sql +0 -0
  85. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/scripts/techempower/verify.sh +0 -0
  86. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/scripts/the-benchmarker/README.md +0 -0
  87. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/scripts/the-benchmarker/python/mq-bridge-py/config.yaml +0 -0
  88. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/scripts/the-benchmarker/python/mq-bridge-py/pyproject.toml +0 -0
  89. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/scripts/the-benchmarker/python/mq-bridge-py/server.py +0 -0
  90. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/command_handler.rs +0 -0
  91. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/connection_registry.rs +0 -0
  92. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/amqp.rs +0 -0
  93. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/aws.rs +0 -0
  94. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/fanout.rs +0 -0
  95. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/file.rs +0 -0
  96. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/grpc.proto +0 -0
  97. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/http.rs +0 -0
  98. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/http_stream.rs +0 -0
  99. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/ibm_mq.rs +0 -0
  100. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/memory/endpoint.rs +0 -0
  101. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/memory/ipc_unix.rs +0 -0
  102. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/memory/ipc_windows.rs +0 -0
  103. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/memory/memory_transport.rs +0 -0
  104. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/memory/mod.rs +0 -0
  105. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/memory/transport.rs +0 -0
  106. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/mod.rs +0 -0
  107. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/mongodb.rs +0 -0
  108. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/nats.rs +0 -0
  109. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/null.rs +0 -0
  110. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/reader.rs +0 -0
  111. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/response.rs +0 -0
  112. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/sled.rs +0 -0
  113. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/sqlx.rs +0 -0
  114. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/static_endpoint.rs +0 -0
  115. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/stream_buffer.rs +0 -0
  116. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/switch.rs +0 -0
  117. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/websocket.rs +0 -0
  118. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/endpoints/zeromq.rs +0 -0
  119. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/errors.rs +0 -0
  120. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/event_handler.rs +0 -0
  121. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/event_store.rs +0 -0
  122. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/extensions.rs +0 -0
  123. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/lib.rs +0 -0
  124. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/middleware/buffer.rs +0 -0
  125. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/middleware/cookie_jar.rs +0 -0
  126. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/middleware/deduplication.rs +0 -0
  127. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/middleware/delay.rs +0 -0
  128. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/middleware/dlq.rs +0 -0
  129. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/middleware/limiter.rs +0 -0
  130. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/middleware/metrics.rs +0 -0
  131. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/middleware/mod.rs +0 -0
  132. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/middleware/random_panic.rs +0 -0
  133. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/middleware/retry.rs +0 -0
  134. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/middleware/weak_join.rs +0 -0
  135. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/outcomes.rs +0 -0
  136. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/publisher.rs +0 -0
  137. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/response.rs +0 -0
  138. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/route.rs +0 -0
  139. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/test_utils.rs +0 -0
  140. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/src/type_handler.rs +0 -0
  141. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/README.md +0 -0
  142. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/armature_integration.rs +0 -0
  143. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/amqp.rs +0 -0
  144. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/aws.rs +0 -0
  145. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/config.toml +0 -0
  146. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/docker-compose/amqp.yml +0 -0
  147. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/docker-compose/aws.yml +0 -0
  148. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/docker-compose/ibm-mq-certs/client.crl +0 -0
  149. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/docker-compose/ibm-mq-certs/client.kdb +0 -0
  150. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/docker-compose/ibm-mq-certs/client.p12 +0 -0
  151. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/docker-compose/ibm-mq-certs/client.rdb +0 -0
  152. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/docker-compose/ibm-mq-certs/client.sth +0 -0
  153. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/docker-compose/ibm_mq.yml +0 -0
  154. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/docker-compose/ibm_mq_tls.yml +0 -0
  155. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/docker-compose/kafka-tls.yml +0 -0
  156. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/docker-compose/kafka.yml +0 -0
  157. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/docker-compose/mariadb.yml +0 -0
  158. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/docker-compose/mongodb-replica.yml +0 -0
  159. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/docker-compose/mongodb-tls.yml +0 -0
  160. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/docker-compose/mongodb.yml +0 -0
  161. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/docker-compose/mosquitto.conf +0 -0
  162. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/docker-compose/mosquitto_performance.conf +0 -0
  163. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/docker-compose/mosquitto_persistence.conf +0 -0
  164. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/docker-compose/mq_init.mqsc +0 -0
  165. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/docker-compose/mq_init_tls.mqsc +0 -0
  166. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/docker-compose/mqtt.yml +0 -0
  167. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/docker-compose/mqtt_performance.yml +0 -0
  168. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/docker-compose/mysql.yml +0 -0
  169. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/docker-compose/nats.yml +0 -0
  170. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/docker-compose/postgres.yml +0 -0
  171. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/file.rs +0 -0
  172. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/grpc.rs +0 -0
  173. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/grpc_tls.rs +0 -0
  174. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/http.rs +0 -0
  175. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/http_tls.rs +0 -0
  176. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/ibm_mq.rs +0 -0
  177. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/ibm_mq_tls.rs +0 -0
  178. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/ipc.rs +0 -0
  179. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/kafka_tls.rs +0 -0
  180. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/logic_test.rs +0 -0
  181. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/mariadb.rs +0 -0
  182. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/memory.rs +0 -0
  183. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/mod.rs +0 -0
  184. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/mongodb.rs +0 -0
  185. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/mongodb_raw.rs +0 -0
  186. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/mongodb_tls.rs +0 -0
  187. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/mqtt.rs +0 -0
  188. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/mysql.rs +0 -0
  189. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/nats.rs +0 -0
  190. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/performance_static.rs +0 -0
  191. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/postgres.rs +0 -0
  192. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/scripts/gen_certs.sh +0 -0
  193. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/sqlite.rs +0 -0
  194. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/tls_helpers.rs +0 -0
  195. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/websocket.rs +0 -0
  196. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration/zeromq.rs +0 -0
  197. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/integration_test.rs +0 -0
  198. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/memory_leak_test.rs +0 -0
  199. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/ref_test.rs +0 -0
  200. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/sqlite_test.rs +0 -0
  201. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/tls_example.rs +0 -0
  202. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/unit_tests.md +0 -0
  203. {mq_bridge_py_basic-0.3.0 → mq_bridge_py_basic-0.3.1}/tests/websocket_test.rs +0 -0
@@ -10,11 +10,12 @@ name: Publish Node Package
10
10
  # RHEL 9 (glibc 2.34), Debian 11/12 and Ubuntu 20.04+ — covering the priority
11
11
  # server targets. macOS/Windows build natively.
12
12
  #
13
- # Auth: token-or-OIDC. If the `NPM_TOKEN` secret is set it is used (needed for the
14
- # one-time bootstrap that creates the 5 platform packages, since npm has no
15
- # pending-publisher concept). Once all six packages exist and trusted publishing
16
- # is configured for them, remove the secret and publishing continues via OIDC
17
- # (id-token) with provenance no token.
13
+ # Auth: OIDC trusted publishing. The one-time bootstrap (which used an NPM_TOKEN
14
+ # secret to create the 5 platform packages, since npm has no pending-publisher
15
+ # concept) is done; all six packages exist with trusted publishing configured, so
16
+ # publishing runs via OIDC (id-token) with provenance no token. We deliberately
17
+ # do NOT read NPM_TOKEN: a present-but-expired secret would be sent to the registry
18
+ # and 401 instead of falling back to OIDC, so the credential is left out entirely.
18
19
 
19
20
  on:
20
21
  release:
@@ -67,6 +68,9 @@ jobs:
67
68
  env:
68
69
  CARGO_NET_RETRY: "5"
69
70
  CARGO_NET_GIT_FETCH_WITH_CLI: "true"
71
+ # curl's HTTP/2 multiplexing flakes on Windows runners ("[55] Failed
72
+ # sending data to the peer"); force HTTP/1.1 so fetches are reliable.
73
+ CARGO_HTTP_MULTIPLEXING: "false"
70
74
  run: >-
71
75
  npx napi build --platform --release --strip
72
76
  --target ${{ matrix.target }}
@@ -129,6 +133,9 @@ jobs:
129
133
  env:
130
134
  CARGO_NET_RETRY: "5"
131
135
  CARGO_NET_GIT_FETCH_WITH_CLI: "true"
136
+ # curl's HTTP/2 multiplexing flakes on Windows runners ("[55] Failed
137
+ # sending data to the peer"); force HTTP/1.1 so fetches are reliable.
138
+ CARGO_HTTP_MULTIPLEXING: "false"
132
139
  run: >-
133
140
  npx napi build --platform --release --strip
134
141
  --target ${{ matrix.target }}
@@ -220,8 +227,7 @@ jobs:
220
227
  - name: Publish platform packages and root
221
228
  working-directory: node/mq-bridge-node
222
229
  env:
223
- # Empty when the secret is unset -> npm falls back to OIDC.
224
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
230
+ # No NODE_AUTH_TOKEN: publishing is OIDC-only (trusted publishing).
225
231
  NPM_CONFIG_PROVENANCE: "true"
226
232
  run: |
227
233
  set -e
@@ -3082,7 +3082,7 @@ dependencies = [
3082
3082
 
3083
3083
  [[package]]
3084
3084
  name = "mq-bridge"
3085
- version = "0.3.0"
3085
+ version = "0.3.1"
3086
3086
  dependencies = [
3087
3087
  "anyhow",
3088
3088
  "arc-swap",
@@ -3147,20 +3147,32 @@ dependencies = [
3147
3147
  "zeromq",
3148
3148
  ]
3149
3149
 
3150
+ [[package]]
3151
+ name = "mq-bridge-bindings-common"
3152
+ version = "0.3.1"
3153
+ dependencies = [
3154
+ "anyhow",
3155
+ "fast-uuid-v7",
3156
+ "mq-bridge",
3157
+ "serde",
3158
+ "serde_yaml_ng",
3159
+ "tokio",
3160
+ ]
3161
+
3150
3162
  [[package]]
3151
3163
  name = "mq-bridge-node"
3152
- version = "0.3.0"
3164
+ version = "0.3.1"
3153
3165
  dependencies = [
3154
3166
  "anyhow",
3155
3167
  "async-trait",
3156
3168
  "bytes",
3157
3169
  "fast-uuid-v7",
3158
3170
  "mq-bridge",
3171
+ "mq-bridge-bindings-common",
3159
3172
  "napi",
3160
3173
  "napi-build",
3161
3174
  "napi-derive",
3162
3175
  "schemars",
3163
- "serde",
3164
3176
  "serde_json",
3165
3177
  "serde_yaml_ng",
3166
3178
  "tokio",
@@ -3169,13 +3181,14 @@ dependencies = [
3169
3181
 
3170
3182
  [[package]]
3171
3183
  name = "mq-bridge-py"
3172
- version = "0.3.0"
3184
+ version = "0.3.1"
3173
3185
  dependencies = [
3174
3186
  "anyhow",
3175
3187
  "async-trait",
3176
3188
  "bytes",
3177
3189
  "fast-uuid-v7",
3178
3190
  "mq-bridge",
3191
+ "mq-bridge-bindings-common",
3179
3192
  "pyo3",
3180
3193
  "rustls 0.23.40",
3181
3194
  "schemars",
@@ -12,11 +12,11 @@ keywords = ["messaging", "kafka", "nats", "amqp", "cqrs"]
12
12
  categories = ["asynchronous", "network-programming"]
13
13
 
14
14
  [workspace]
15
- members = ["python/mq-bridge-py"]
15
+ members = ["bindings-common", "python/mq-bridge-py"]
16
16
  resolver = "2"
17
17
 
18
18
  [workspace.package]
19
- version = "0.3.0"
19
+ version = "0.3.1"
20
20
 
21
21
  [workspace.dependencies]
22
22
  anyhow = "1.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mq-bridge-py-basic
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: License :: OSI Approved :: MIT License
6
6
  Classifier: Programming Language :: Python :: 3
@@ -38,12 +38,14 @@ Memory and file endpoints are always present in both packages. Use `mq-bridge-py
38
38
 
39
39
  The public API stays close to mq-bridge itself:
40
40
 
41
- - `Route.from_yaml(path, name)` loads one named route from a YAML file
42
- - `Route.from_yaml_str(text, name)` / `Route.from_config(mapping, name)` build a route from an in-memory YAML string or a Python `dict`, no file required
41
+ - `Route.from_file(path, name=None)` loads a route from a YAML/JSON file. The three constructors differ only by source: `from_file` (path), `from_str` (in-memory YAML/JSON string), `from_config` (Python `dict`)
42
+ - The `name` is optional: pass it to pick one entry out of a `routes:`/`publishers:` document, or omit it to treat the whole config as a single bare route/endpoint body
43
43
  - `Route.with_handler(...)` attaches a raw `Message` handler, with lazy `json()`/`text()` readers and `with_json()`/`with_payload()` response helpers
44
44
  - `Route.add_handler(kind, ...)` uses mq-bridge's `kind` dispatch and delivers decoded JSON
45
45
  - `RetryableError` and `NonRetryableError` let Python handlers signal retry intent
46
- - `Publisher.from_yaml(path, name)` (plus `from_yaml_str` / `from_config`) loads one named publisher
46
+ - `Publisher.from_file(path, name=None)` (plus `from_str` / `from_config`) builds a publisher endpoint
47
+
48
+ `from_yaml` / `from_yaml_str` remain as deprecated aliases for `from_file` / `from_str`.
47
49
  - `Publisher.send_json(...)` and `Publisher.request_json(...)` serialize Python JSON values in Rust
48
50
 
49
51
  The Python surface is synchronous and blocking. Tokio, broker I/O, routing, and batching all stay in Rust.
@@ -53,10 +55,10 @@ The Python surface is synchronous and blocking. Tokio, broker I/O, routing, and
53
55
  `mq-bridge-app` can create and test route and endpoint JSON/YAML through its UI.
54
56
  It does not replace your Python code or handlers, but it is useful when you want
55
57
  a known-good connection and route shape before pasting the configuration into
56
- Python. Load the generated config with `Route.from_config`, `Route.from_yaml`,
57
- `Publisher.from_config`, or `Publisher.from_yaml`.
58
+ Python. Load the generated config with `Route.from_config`, `Route.from_file`,
59
+ `Publisher.from_config`, or `Publisher.from_file`.
58
60
 
59
- For the `from_config` / `from_yaml_str` mappings, `mq_bridge.config` ships
61
+ For the `from_config` / `from_str` mappings, `mq_bridge.config` ships
60
62
  `TypedDict` definitions so editors autocomplete the config keys (`input`,
61
63
  `output`, `batch_size`, every transport config, middleware, …):
62
64
 
@@ -117,6 +119,91 @@ with Route.from_config(config, "orders_route").with_handler(handle):
117
119
  Configuration/connection errors surface from `start()` itself, not from a
118
120
  background thread. `run()` remains available for the blocking single-route case.
119
121
 
122
+ ## Pull-based consumer
123
+
124
+ `Route` is push-based: you attach a handler and the route drives it. When you
125
+ instead want to **pull** messages on your own schedule — e.g. to feed a
126
+ generator-style sink such as a [`dlt`](https://dlthub.com) resource — use
127
+ `Consumer`. It wraps any input endpoint and hands batches back to Python:
128
+
129
+ ```python
130
+ from mq_bridge import Consumer
131
+
132
+ consumer = Consumer.from_config({"nats": {"subject": "orders", "url": "nats://localhost:4222"}})
133
+
134
+ while not consumer.exhausted:
135
+ batch = consumer.poll(max=500, timeout_ms=1000) # [] on timeout
136
+ if not batch:
137
+ continue
138
+ for message in batch:
139
+ handle(message.json())
140
+ consumer.commit() # ack only after handling
141
+ ```
142
+
143
+ `poll()` receives up to `max` messages **without** acknowledging them;
144
+ `commit()` acks every batch returned since the last commit, advancing the
145
+ consumer offset (or removing them from the queue). Committing only after the
146
+ downstream write succeeds gives at-least-once delivery: a crash before `commit()`
147
+ re-delivers the batch. `poll()` returns `[]` once `timeout_ms` elapses with
148
+ nothing received (omit it to block until a message arrives), and sets
149
+ `exhausted` once a bounded source (e.g. a file) is fully drained — streaming
150
+ brokers never set it.
151
+
152
+ > **You must call `commit()` — it is not optional.** It is the only thing that
153
+ > tells the broker a batch is done. If you keep polling without committing:
154
+ > - the consumer offset never advances, so every message is **re-delivered** on
155
+ > the next run (and you reprocess from the start);
156
+ > - most brokers stop sending once their unacknowledged/prefetch window fills, so
157
+ > `poll()` eventually **stalls** and returns nothing;
158
+ > - the uncommitted batches are held in memory pending their ack, so the process
159
+ > **grows unbounded**.
160
+ >
161
+ > Commit after each batch you have durably handled (as in the loops above). If a
162
+ > batch fails downstream, simply *don't* commit it — it will be redelivered.
163
+
164
+ `consumer.status()` returns a snapshot dict (`healthy`, `target`, `pending`,
165
+ `capacity`, `error`, `details`). `pending` is the broker backlog/lag where the
166
+ transport reports it — Kafka offset lag, AMQP queue depth, NATS JetStream
167
+ `num_pending` — so `pending == 0` is a precise "caught up" check for a bounded
168
+ drain; it is `None` where the broker exposes no backlog (core NATS, MQTT), where
169
+ you fall back to a `timeout_ms` that returns `[]`. It's a point-in-time snapshot,
170
+ not a guarantee.
171
+
172
+ `consumer.close()` releases the broker connection; it's idempotent, and `poll()`
173
+ /`status()` raise afterwards. Python is garbage-collected, so close explicitly
174
+ (or use the context-manager form, which closes on exit) rather than relying on
175
+ the object being collected:
176
+
177
+ ```python
178
+ with Consumer.from_config(cfg) as consumer:
179
+ batch = consumer.poll(max=500, timeout_ms=1000)
180
+ ...
181
+ consumer.commit()
182
+ # connection released here
183
+ ```
184
+
185
+ The endpoint config decides durability exactly as a route input does: a
186
+ consumer-group config resumes from the last commit, a subscriber config receives
187
+ only new messages. `Consumer.from_file` / `from_str` accept the same shapes,
188
+ plus a named entry under a `consumers:` document section.
189
+
190
+ As a `dlt` resource this is a few lines:
191
+
192
+ ```python
193
+ import dlt
194
+ from mq_bridge import Consumer
195
+
196
+ @dlt.resource(name="orders")
197
+ def orders():
198
+ consumer = Consumer.from_config({"nats": {"subject": "orders", "url": "nats://localhost:4222"}})
199
+ while not consumer.exhausted:
200
+ batch = consumer.poll(max=500, timeout_ms=1000)
201
+ if not batch:
202
+ break # nothing more pending this run
203
+ yield [m.json() for m in batch]
204
+ consumer.commit()
205
+ ```
206
+
120
207
  ## Tuning (environment variables)
121
208
 
122
209
  These knobs are read from the environment at startup:
@@ -20,6 +20,28 @@
20
20
  It is not only a forwarder. A route can transform, filter, fan out, retry, rate-limit, deduplicate, or turn a request into a response before the message reaches the next system. The core is built on Tokio and keeps the transport details at the edge, so application code can mostly work with `CanonicalMessage`s and handlers.
21
21
 
22
22
 
23
+ ## Language Bindings
24
+
25
+ `mq-bridge` is a Rust library, but the same engine ships as native bindings for **Python** and **Node.js**. The Tokio runtime, broker I/O, routing, and batching all stay in Rust; the binding is a thin layer for handlers and configuration.
26
+
27
+ | Language | Package | Install |
28
+ | :--- | :--- | :--- |
29
+ | Rust | [`mq-bridge`](https://crates.io/crates/mq-bridge) | `cargo add mq-bridge` |
30
+ | Python | [`mq-bridge-py`](python/mq-bridge-py/README.md) ([PyPI](https://pypi.org/project/mq-bridge-py/)) | `pip install mq-bridge-py` |
31
+ | Node.js | [`mq-bridge`](node/mq-bridge-node/README.md) ([npm](https://www.npmjs.com/package/mq-bridge)) | `npm install mq-bridge` |
32
+
33
+ The constructor names are kept aligned across languages, so a config loader reads the same in either binding (Python uses `snake_case`, Node uses `camelCase`):
34
+
35
+ - `Route.from_file` / `Route.fromFile` — load a route from a YAML/JSON file
36
+ - `Route.from_str` / `Route.fromStr` — load from an in-memory YAML/JSON string
37
+ - `Route.from_config` / `Route.fromConfig` — load from a dict / JS object
38
+ - the matching `Publisher.*` constructors build a publisher endpoint
39
+
40
+ The `name` argument is optional in both: pass it to select one entry from a `routes:`/`publishers:` document, or omit it to treat the config as a single bare route/endpoint body.
41
+
42
+ The Python binding also holds up well under load on the third-party [http-arena.com](https://www.http-arena.com/#sort=rps:-1&q=python) requests-per-second HTTP benchmark (live leaderboard — rankings shift over time). See [the Python analysis notes](python/mq-bridge-py/README.md#analysis) for the local HTTP comparison harness.
43
+
44
+
23
45
  ## Architecture
24
46
 
25
47
  See [ARCHITECTURE.md](ARCHITECTURE.md) for a detailed overview of the internal design, extensibility, and usage patterns.
@@ -161,7 +183,7 @@ https://github.com/marcomq/mq-bridge-app
161
183
 
162
184
  `mq-bridge-app` can be used to create and test route and endpoint configurations through a UI. The generated JSON/YAML can then be copied into an application and loaded by the Rust library or the Python bindings.
163
185
 
164
- This does not replace application code or handlers, but it is useful when you want a known-good connection and route shape before pasting the configuration into code. For Python projects, routes and publishers are commonly loaded from JSON/YAML with `Route.from_config`, `Route.from_yaml`, `Publisher.from_config`, or `Publisher.from_yaml`.
186
+ This does not replace application code or handlers, but it is useful when you want a known-good connection and route shape before pasting the configuration into code. For Python and Node projects, routes and publishers are commonly loaded from JSON/YAML with `Route.from_file`/`fromFile` (a file path) and `Route.from_str`/`fromStr` (a JSON/YAML string), while `Route.from_config`/`fromConfig` takes an already-parsed dict/JS object. The matching `Publisher.*` constructors work the same way.
165
187
 
166
188
  ### Programmatic Handlers
167
189
 
@@ -190,7 +190,7 @@ pub mod kafka_helper {
190
190
  group_id: Some("perf_test_group_kafka".to_string()),
191
191
  topic: Some(topic.to_string()),
192
192
  producer_options: Some(vec![
193
- ("queue.buffering.max.ms".to_string(), "50".to_string()), // Linger for 50ms to batch messages
193
+ ("queue.buffering.max.ms".to_string(), "1".to_string()), // Small linger; send_batch already enqueues a burst
194
194
  ("acks".to_string(), "1".to_string()), // Wait for leader ack, a good balance
195
195
  ("compression.type".to_string(), "snappy".to_string()), // Use snappy compression
196
196
  ]),
@@ -0,0 +1,19 @@
1
+ [package]
2
+ name = "mq-bridge-bindings-common"
3
+ version.workspace = true
4
+ edition = "2021"
5
+ publish = false
6
+ description = "Shared config-loading and message helpers for the mq-bridge Python and Node bindings"
7
+ license = "MIT"
8
+ repository = "https://github.com/marcomq/mq-bridge"
9
+
10
+ [lib]
11
+ doctest = false
12
+
13
+ [dependencies]
14
+ anyhow.workspace = true
15
+ fast-uuid-v7.workspace = true
16
+ mq-bridge = { path = "..", default-features = false }
17
+ serde = { workspace = true, features = ["derive"] }
18
+ serde_yaml_ng.workspace = true
19
+ tokio = { workspace = true, features = ["rt-multi-thread"] }
@@ -0,0 +1,308 @@
1
+ //! Shared helpers for the mq-bridge language bindings (Python and Node).
2
+ //!
3
+ //! Both bindings used to carry their own copies of the config-document loading
4
+ //! and route/publisher resolution logic. Those copies drifted (array-publisher
5
+ //! support, default-name format, and where the `config:` root gets unwrapped all
6
+ //! diverged), so the logic lives here once. Each binding keeps only its
7
+ //! `#[pyo3]` / `#[napi]` wrappers and maps these `anyhow::Result`s to its native
8
+ //! error type at the boundary.
9
+
10
+ use std::collections::HashMap;
11
+ use std::fs;
12
+ use std::path::Path;
13
+
14
+ use anyhow::Context;
15
+ use mq_bridge::models::{Endpoint, PublisherConfig};
16
+ use mq_bridge::Route;
17
+ use serde::Deserialize;
18
+ use tokio::runtime::{Builder, Runtime};
19
+
20
+ /// A parsed `routes:` + `publishers:` configuration document.
21
+ pub struct ConfigDocument {
22
+ pub routes: HashMap<String, Route>,
23
+ pub publishers: PublisherConfig,
24
+ }
25
+
26
+ /// One entry of a `publishers:` array (`- name: ... endpoint: ...`).
27
+ #[derive(Debug, Deserialize)]
28
+ struct NamedPublisher {
29
+ name: String,
30
+ endpoint: Endpoint,
31
+ }
32
+
33
+ /// Build the multi-threaded Tokio runtime the bindings drive their transports
34
+ /// on.
35
+ pub fn build_runtime() -> anyhow::Result<Runtime> {
36
+ Builder::new_multi_thread()
37
+ .enable_all()
38
+ .build()
39
+ .context("failed to build Tokio runtime")
40
+ }
41
+
42
+ /// Read and parse a YAML/JSON config file, unwrapping a `config:` export root
43
+ /// if present.
44
+ pub fn load_config_value(path: &Path) -> anyhow::Result<serde_yaml_ng::Value> {
45
+ let raw = fs::read_to_string(path)
46
+ .with_context(|| format!("failed to read config from {}", path.display()))?;
47
+ let value = serde_yaml_ng::from_str(&raw).context("failed to parse YAML config")?;
48
+ Ok(unwrap_config_root(value))
49
+ }
50
+
51
+ /// Load a named route from a config file.
52
+ pub fn load_named_route(path: &Path, name: Option<&str>) -> anyhow::Result<Route> {
53
+ named_route_from_value(load_config_value(path)?, name)
54
+ }
55
+
56
+ /// Resolve a route from a config value. When `name` is `Some`, look it up in a
57
+ /// `routes:` document (falling back to a single-route body). When `name` is
58
+ /// `None`, the value must be a single bare route body. A `config:` export root
59
+ /// is unwrapped first, so file, string, and mapping inputs behave identically.
60
+ pub fn named_route_from_value(
61
+ value: serde_yaml_ng::Value,
62
+ name: Option<&str>,
63
+ ) -> anyhow::Result<Route> {
64
+ let value = unwrap_config_root(value);
65
+ if let Some(name) = name {
66
+ if let Ok(document) = load_document_from_value(value.clone()) {
67
+ if let Some(route) = document.routes.get(name).cloned() {
68
+ return Ok(route);
69
+ }
70
+ }
71
+ }
72
+
73
+ serde_yaml_ng::from_value(value).with_context(|| match name {
74
+ Some(name) => format!(
75
+ "No route named '{name}' found, and the config could not be parsed as a single route"
76
+ ),
77
+ None => "config could not be parsed as a single route body".to_string(),
78
+ })
79
+ }
80
+
81
+ /// Load a named publisher endpoint from a config file.
82
+ pub fn load_named_publisher(path: &Path, name: Option<&str>) -> anyhow::Result<Endpoint> {
83
+ named_publisher_from_value(load_config_value(path)?, name)
84
+ }
85
+
86
+ /// Resolve a publisher endpoint from a config value. Mirrors
87
+ /// [`named_route_from_value`]: name lookup in a `publishers:` document with a
88
+ /// single-endpoint-body fallback, and a `config:` root unwrapped first.
89
+ pub fn named_publisher_from_value(
90
+ value: serde_yaml_ng::Value,
91
+ name: Option<&str>,
92
+ ) -> anyhow::Result<Endpoint> {
93
+ let value = unwrap_config_root(value);
94
+ if let Some(name) = name {
95
+ if let Ok(document) = load_document_from_value(value.clone()) {
96
+ if let Some(endpoint) = document.publishers.get(name).cloned() {
97
+ return Ok(endpoint);
98
+ }
99
+ }
100
+ }
101
+
102
+ serde_yaml_ng::from_value(value).with_context(|| match name {
103
+ Some(name) => format!(
104
+ "No publisher named '{name}' found, and the config could not be parsed as a single publisher endpoint"
105
+ ),
106
+ None => "config could not be parsed as a single endpoint body".to_string(),
107
+ })
108
+ }
109
+
110
+ /// Load a named consumer endpoint from a config file.
111
+ pub fn load_named_consumer(path: &Path, name: Option<&str>) -> anyhow::Result<Endpoint> {
112
+ named_consumer_from_value(load_config_value(path)?, name)
113
+ }
114
+
115
+ /// Resolve a consumer (input) endpoint from a config value. Mirrors
116
+ /// [`named_publisher_from_value`]: when `name` is `Some`, look it up in a
117
+ /// `consumers:` section; otherwise (or on a miss) parse the value as a single
118
+ /// bare endpoint body. A `config:` export root is unwrapped first.
119
+ pub fn named_consumer_from_value(
120
+ value: serde_yaml_ng::Value,
121
+ name: Option<&str>,
122
+ ) -> anyhow::Result<Endpoint> {
123
+ let value = unwrap_config_root(value);
124
+ if let Some(name) = name {
125
+ if let Some(map) = value.as_mapping() {
126
+ if let Some(section) = map.get(serde_yaml_ng::Value::String("consumers".to_string())) {
127
+ if let Ok(consumers) =
128
+ serde_yaml_ng::from_value::<HashMap<String, Endpoint>>(section.clone())
129
+ {
130
+ if let Some(endpoint) = consumers.get(name) {
131
+ return Ok(endpoint.clone());
132
+ }
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ serde_yaml_ng::from_value(value).with_context(|| match name {
139
+ Some(name) => format!(
140
+ "No consumer named '{name}' found, and the config could not be parsed as a single consumer endpoint"
141
+ ),
142
+ None => "config could not be parsed as a single endpoint body".to_string(),
143
+ })
144
+ }
145
+
146
+ /// Parse a value that may be a `{routes, publishers}` document, a bare
147
+ /// `{name: route}` map, or (without those sections) a plain route map.
148
+ pub fn load_document_from_value(value: serde_yaml_ng::Value) -> anyhow::Result<ConfigDocument> {
149
+ let value = unwrap_config_root(value);
150
+ let section_key = |name: &str| serde_yaml_ng::Value::String(name.to_string());
151
+ let routes_key = section_key("routes");
152
+ let publishers_key = section_key("publishers");
153
+
154
+ if let Some(map) = value.as_mapping() {
155
+ if map.contains_key(&routes_key) || map.contains_key(&publishers_key) {
156
+ let routes = map
157
+ .get(&routes_key)
158
+ .map_or_else(
159
+ || Ok(HashMap::new()),
160
+ |section| serde_yaml_ng::from_value(section.clone()),
161
+ )
162
+ .context("failed to parse 'routes' section")?;
163
+ let publishers = map.get(&publishers_key).map_or_else(
164
+ || Ok(PublisherConfig::new()),
165
+ |section| parse_publishers_section(section.clone()),
166
+ )?;
167
+ return Ok(ConfigDocument { routes, publishers });
168
+ }
169
+ }
170
+
171
+ let routes = serde_yaml_ng::from_value(value).context("failed to parse YAML as a route map")?;
172
+ Ok(ConfigDocument {
173
+ routes,
174
+ publishers: PublisherConfig::new(),
175
+ })
176
+ }
177
+
178
+ /// Parse a `publishers:` section, accepting either a `{name: endpoint}` map or a
179
+ /// `[{name, endpoint}]` array.
180
+ pub fn parse_publishers_section(value: serde_yaml_ng::Value) -> anyhow::Result<PublisherConfig> {
181
+ match value {
182
+ serde_yaml_ng::Value::Mapping(_) => {
183
+ serde_yaml_ng::from_value(value).context("failed to parse 'publishers' section")
184
+ }
185
+ serde_yaml_ng::Value::Sequence(_) => {
186
+ let entries: Vec<NamedPublisher> = serde_yaml_ng::from_value(value)
187
+ .context("failed to parse 'publishers' array section")?;
188
+ let mut publishers = PublisherConfig::new();
189
+ for entry in entries {
190
+ if publishers.contains_key(&entry.name) {
191
+ return Err(anyhow::anyhow!(
192
+ "duplicate publisher name '{}' in 'publishers' array section",
193
+ entry.name
194
+ ));
195
+ }
196
+ publishers.insert(entry.name, entry.endpoint);
197
+ }
198
+ Ok(publishers)
199
+ }
200
+ other => Err(anyhow::anyhow!(
201
+ "failed to parse 'publishers' section: expected a map or array, got {other:?}"
202
+ )),
203
+ }
204
+ }
205
+
206
+ /// Strip a `config:` export root if present (e.g. an `mq-bridge-app` export),
207
+ /// returning the inner document. Idempotent.
208
+ fn unwrap_config_root(value: serde_yaml_ng::Value) -> serde_yaml_ng::Value {
209
+ if let Some(map) = value.as_mapping() {
210
+ let config_key = serde_yaml_ng::Value::String("config".to_string());
211
+ if let Some(config) = map.get(&config_key) {
212
+ return config.clone();
213
+ }
214
+ }
215
+ value
216
+ }
217
+
218
+ /// Map an optional name from a binding boundary to `None` when missing or empty,
219
+ /// so callers can omit it (or pass `""`) to mean "no name".
220
+ pub fn normalize_name(name: Option<&str>) -> Option<&str> {
221
+ name.filter(|n| !n.is_empty())
222
+ }
223
+
224
+ /// Generated identity for a route built without an explicit name.
225
+ pub fn default_route_name() -> String {
226
+ format!("route-{}", fast_uuid_v7::gen_id())
227
+ }
228
+
229
+ #[cfg(test)]
230
+ mod tests {
231
+ use super::*;
232
+
233
+ // An mq-bridge-app export: a `config:` root wrapping a document whose
234
+ // `publishers:` section is an array (the two shapes that had drifted
235
+ // between the bindings).
236
+ const EXPORT: &str = r#"
237
+ config:
238
+ publishers:
239
+ - name: incoming
240
+ endpoint:
241
+ memory:
242
+ topic: orders
243
+ capacity: 16
244
+ routes:
245
+ orders_route:
246
+ input:
247
+ memory:
248
+ topic: orders
249
+ capacity: 16
250
+ output:
251
+ response: {}
252
+ "#;
253
+
254
+ fn yaml(s: &str) -> serde_yaml_ng::Value {
255
+ serde_yaml_ng::from_str(s).unwrap()
256
+ }
257
+
258
+ #[test]
259
+ fn publishers_section_accepts_array_and_map() {
260
+ let array = yaml("- name: out\n endpoint:\n memory:\n topic: t\n");
261
+ assert!(parse_publishers_section(array).unwrap().contains_key("out"));
262
+
263
+ let map = yaml("out:\n memory:\n topic: t\n");
264
+ assert!(parse_publishers_section(map).unwrap().contains_key("out"));
265
+ }
266
+
267
+ #[test]
268
+ fn named_lookups_unwrap_config_export_root() {
269
+ // The `config:` root is unwrapped, the array publishers parsed, and the
270
+ // named entries resolved.
271
+ let route = named_route_from_value(yaml(EXPORT), Some("orders_route"));
272
+ assert!(route.is_ok(), "route: {:?}", route.err());
273
+
274
+ let publisher = named_publisher_from_value(yaml(EXPORT), Some("incoming"));
275
+ assert!(publisher.is_ok(), "publisher: {:?}", publisher.err());
276
+ }
277
+
278
+ #[test]
279
+ fn document_exposes_routes_and_publishers() {
280
+ let doc = load_document_from_value(unwrap_config_root(yaml(EXPORT))).unwrap();
281
+ assert!(doc.routes.contains_key("orders_route"));
282
+ assert!(doc.publishers.contains_key("incoming"));
283
+ }
284
+
285
+ #[test]
286
+ fn named_consumer_resolves_section_and_bare_body() {
287
+ // Named lookup in a `consumers:` section.
288
+ let doc = yaml("consumers:\n orders:\n memory:\n topic: t\n capacity: 16\n");
289
+ assert!(named_consumer_from_value(doc, Some("orders")).is_ok());
290
+
291
+ // Bare single-endpoint body (no name).
292
+ let bare = yaml("memory:\n topic: t\n capacity: 16\n");
293
+ assert!(named_consumer_from_value(bare, None).is_ok());
294
+
295
+ // A `config:` export root is unwrapped before the `consumers:` lookup.
296
+ let export = yaml(
297
+ "config:\n consumers:\n orders:\n memory:\n topic: t\n capacity: 16\n",
298
+ );
299
+ assert!(named_consumer_from_value(export, Some("orders")).is_ok());
300
+ }
301
+
302
+ #[test]
303
+ fn normalize_name_treats_empty_as_none() {
304
+ assert_eq!(normalize_name(Some("x")), Some("x"));
305
+ assert_eq!(normalize_name(Some("")), None);
306
+ assert_eq!(normalize_name(None), None);
307
+ }
308
+ }
@@ -1,4 +1,5 @@
1
1
  from ._mq_bridge import (
2
+ Consumer,
2
3
  MemoryDrainer,
3
4
  Message,
4
5
  NonRetryableError,
@@ -10,6 +11,7 @@ from ._mq_bridge import (
10
11
  )
11
12
 
12
13
  __all__ = [
14
+ "Consumer",
13
15
  "MemoryDrainer",
14
16
  "Message",
15
17
  "NonRetryableError",