mq-bridge-py-basic 0.4.3__tar.gz → 0.4.4__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 (288) hide show
  1. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/Cargo.lock +4 -4
  2. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/Cargo.toml +1 -1
  3. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/PKG-INFO +1 -1
  4. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/file/mod.rs +11 -2
  5. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/mod.rs +83 -1
  6. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/object_store.rs +220 -16
  7. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  8. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  9. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  10. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/.github/dependabot.yml +0 -0
  11. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/.github/workflows/benchmark.yml +0 -0
  12. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/.github/workflows/ci.yml +0 -0
  13. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/.github/workflows/ibm-mq.yml +0 -0
  14. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/.github/workflows/node.yml +0 -0
  15. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/.github/workflows/publish-node.yml +0 -0
  16. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/.github/workflows/publish-python.yml +0 -0
  17. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/.github/workflows/python.yml +0 -0
  18. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/.github/workflows/release.yml +0 -0
  19. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/.github/workflows/test-matrix.yml +0 -0
  20. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/.gitignore +0 -0
  21. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/CHANGELOG.md +0 -0
  22. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/CLAUDE.md +0 -0
  23. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/CONTRIBUTING.md +0 -0
  24. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/LICENSE +0 -0
  25. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/LICENSE.python +0 -0
  26. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/README.md +0 -0
  27. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/SECURITY.md +0 -0
  28. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/benches/ETL_BENCHMARKS.md +0 -0
  29. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/benches/performance_bench.rs +0 -0
  30. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/benches/router_bench.rs +0 -0
  31. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/benches/zeromq_backends.rs +0 -0
  32. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/bindings-common/Cargo.toml +0 -0
  33. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/bindings-common/src/lib.rs +0 -0
  34. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/bindings-common/src/logging.rs +0 -0
  35. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/build.rs +0 -0
  36. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/deny.toml +0 -0
  37. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/docs/ARCHITECTURE.md +0 -0
  38. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/docs/CONFIGURATION.md +0 -0
  39. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/docs/DELIVERY.md +0 -0
  40. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/docs/EXTENDING.md +0 -0
  41. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/docs/PLUGINS.md +0 -0
  42. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/docs/REFERENCE.md +0 -0
  43. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/examples/ipc_worker_queue.yaml +0 -0
  44. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/mq-bridge.schema.json +0 -0
  45. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/mq_bridge/__init__.py +0 -0
  46. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/mq_bridge/__init__.pyi +0 -0
  47. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/mq_bridge/config.py +0 -0
  48. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/mq_bridge/config.pyi +0 -0
  49. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/mq_bridge/plugin_packaging.py +0 -0
  50. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/mq_bridge/py.typed +0 -0
  51. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/package-lock.json +0 -0
  52. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/pyproject.toml +0 -0
  53. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/Cargo.toml +0 -0
  54. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/LICENSE.python +0 -0
  55. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/README.md +0 -0
  56. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/analysis/bench_http_native.py +0 -0
  57. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/example.py +0 -0
  58. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/examples/bench_memory.py +0 -0
  59. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/examples/bench_memory.yaml +0 -0
  60. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/examples/consume_pull.py +0 -0
  61. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/examples/dlt_source.py +0 -0
  62. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/examples/json_route.py +0 -0
  63. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/examples/memory.yaml +0 -0
  64. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/examples/raw_route.py +0 -0
  65. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/pyproject-basic.toml +0 -0
  66. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/pyproject-reduced.toml +0 -0
  67. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/scripts/gen_config_types.py +0 -0
  68. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/src/lib.rs +0 -0
  69. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/tests/test_config_types.py +0 -0
  70. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/tests/test_custom_endpoint.py +0 -0
  71. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/tests/test_logging.py +0 -0
  72. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/tests/test_packaging.py +0 -0
  73. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/tests/test_performance_smoke.py +0 -0
  74. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/tests/test_plugin_package.py +0 -0
  75. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/tests/test_public_api.py +0 -0
  76. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/tests/test_routes.py +0 -0
  77. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/uv.lock +0 -0
  78. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/analysis/benches/metadata_bench.rs +0 -0
  79. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/analysis/debug_ibm_mq_tls.rs +0 -0
  80. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/analysis/http/analyze_xctrace_time_profile.py +0 -0
  81. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/analysis/http/http_flamegraph.py +0 -0
  82. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/analysis/http/mq_bridge_http_profile.rs +0 -0
  83. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/analysis/http/run_http_ladder.sh +0 -0
  84. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/ci/format_bench_output.sh +0 -0
  85. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/httparena/README.md +0 -0
  86. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/httparena/frameworks/mq-bridge-py/Dockerfile +0 -0
  87. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/httparena/frameworks/mq-bridge-py/meta.json +0 -0
  88. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/httparena/frameworks/mq-bridge-py/server.py +0 -0
  89. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/techempower/Python/mq-bridge-py/README.md +0 -0
  90. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/techempower/Python/mq-bridge-py/benchmark_config.json +0 -0
  91. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/techempower/Python/mq-bridge-py/mq-bridge-py.dockerfile +0 -0
  92. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/techempower/Python/mq-bridge-py/server.py +0 -0
  93. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/techempower/README.md +0 -0
  94. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/techempower/postgres.yml +0 -0
  95. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/techempower/seed.sql +0 -0
  96. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/techempower/verify.sh +0 -0
  97. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/the-benchmarker/README.md +0 -0
  98. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/the-benchmarker/python/mq-bridge-py/config.yaml +0 -0
  99. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/the-benchmarker/python/mq-bridge-py/pyproject.toml +0 -0
  100. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/the-benchmarker/python/mq-bridge-py/server.py +0 -0
  101. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/canonical_message.rs +0 -0
  102. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/checkpoint.rs +0 -0
  103. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/command_handler.rs +0 -0
  104. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/amqp.rs +0 -0
  105. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/aws.rs +0 -0
  106. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/clickhouse.rs +0 -0
  107. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/file/tests.rs +0 -0
  108. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/grpc.proto +0 -0
  109. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/grpc.rs +0 -0
  110. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/http/mod.rs +0 -0
  111. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/http/publisher.rs +0 -0
  112. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/http/stream.rs +0 -0
  113. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/http/tests.rs +0 -0
  114. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/ibm_mq.rs +0 -0
  115. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/kafka.rs +0 -0
  116. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/memory/endpoint.rs +0 -0
  117. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/memory/framed.rs +0 -0
  118. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/memory/ipc_unix.rs +0 -0
  119. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/memory/ipc_windows.rs +0 -0
  120. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/memory/memory_transport.rs +0 -0
  121. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/memory/mod.rs +0 -0
  122. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/memory/transport.rs +0 -0
  123. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/mongodb/consumer.rs +0 -0
  124. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/mongodb/dedup.rs +0 -0
  125. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/mongodb/mod.rs +0 -0
  126. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/mongodb/publisher.rs +0 -0
  127. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/mongodb/readers.rs +0 -0
  128. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/mongodb/tests.rs +0 -0
  129. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/mqtt.rs +0 -0
  130. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/nats.rs +0 -0
  131. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/poll.rs +0 -0
  132. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/postgres/VENDORED.md +0 -0
  133. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/postgres/mod.rs +0 -0
  134. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/postgres/pgoutput/decoder.rs +0 -0
  135. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/postgres/pgoutput/messages.rs +0 -0
  136. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/postgres/pgoutput/mod.rs +0 -0
  137. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/postgres/pgoutput/registry.rs +0 -0
  138. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/postgres/pgoutput/values.rs +0 -0
  139. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/postgres/replication.rs +0 -0
  140. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/postgres/state.rs +0 -0
  141. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/redis_streams.rs +0 -0
  142. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/sled.rs +0 -0
  143. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/sqlx/dedup.rs +0 -0
  144. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/sqlx/mod.rs +0 -0
  145. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/sqlx/tests.rs +0 -0
  146. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/structural/fanout.rs +0 -0
  147. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/structural/mod.rs +0 -0
  148. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/structural/null.rs +0 -0
  149. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/structural/reader.rs +0 -0
  150. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/structural/request.rs +0 -0
  151. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/structural/response.rs +0 -0
  152. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/structural/static_endpoint.rs +0 -0
  153. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/structural/stream_buffer.rs +0 -0
  154. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/structural/switch.rs +0 -0
  155. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/websocket.rs +0 -0
  156. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/zeromq/codec.rs +0 -0
  157. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/zeromq/mod.rs +0 -0
  158. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/zeromq/omq.rs +0 -0
  159. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/zeromq/zmq.rs +0 -0
  160. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/errors.rs +0 -0
  161. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/event_handler.rs +0 -0
  162. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/event_store.rs +0 -0
  163. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/extensions.rs +0 -0
  164. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/lib.rs +0 -0
  165. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/buffer.rs +0 -0
  166. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/compression.rs +0 -0
  167. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/cookie_jar.rs +0 -0
  168. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/deduplication.rs +0 -0
  169. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/delay.rs +0 -0
  170. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/dlq.rs +0 -0
  171. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/encryption.rs +0 -0
  172. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/id.rs +0 -0
  173. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/limiter.rs +0 -0
  174. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/metrics.rs +0 -0
  175. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/mod.rs +0 -0
  176. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/random_panic.rs +0 -0
  177. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/retry.rs +0 -0
  178. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/transform/coerce.rs +0 -0
  179. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/transform/compiled.rs +0 -0
  180. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/transform/error.rs +0 -0
  181. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/transform/mod.rs +0 -0
  182. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/transform/path.rs +0 -0
  183. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/transform/schema.rs +0 -0
  184. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/transform/tests.rs +0 -0
  185. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/weak_join.rs +0 -0
  186. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/models/builders.rs +0 -0
  187. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/models/defaults.rs +0 -0
  188. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/models/secrets.rs +0 -0
  189. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/models/serde_support.rs +0 -0
  190. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/models/tests.rs +0 -0
  191. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/models.rs +0 -0
  192. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/outcomes.rs +0 -0
  193. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/plugin/conformance.rs +0 -0
  194. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/plugin/endpoint.rs +0 -0
  195. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/plugin/message.rs +0 -0
  196. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/plugin/middleware.rs +0 -0
  197. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/plugin/mod.rs +0 -0
  198. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/plugin/sdk.rs +0 -0
  199. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/plugin/test_support.rs +0 -0
  200. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/publisher.rs +0 -0
  201. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/response.rs +0 -0
  202. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/route.rs +0 -0
  203. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/support/base64_engine.rs +0 -0
  204. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/support/compression.rs +0 -0
  205. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/support/connection_registry.rs +0 -0
  206. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/support/crypto.rs +0 -0
  207. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/support/crypto_envelope.rs +0 -0
  208. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/support/interpolation.rs +0 -0
  209. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/support/mod.rs +0 -0
  210. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/support/plugin_abi.rs +0 -0
  211. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/support/source_ranges.rs +0 -0
  212. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/test_utils.rs +0 -0
  213. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/traits.rs +0 -0
  214. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/type_handler.rs +0 -0
  215. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/README.md +0 -0
  216. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/armature_integration.rs +0 -0
  217. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/amqp.rs +0 -0
  218. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/aws.rs +0 -0
  219. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/clickhouse.rs +0 -0
  220. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/config.toml +0 -0
  221. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/amqp.yml +0 -0
  222. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/aws.yml +0 -0
  223. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/clickhouse.yml +0 -0
  224. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/ibm-mq-certs/client.crl +0 -0
  225. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/ibm-mq-certs/client.kdb +0 -0
  226. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/ibm-mq-certs/client.p12 +0 -0
  227. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/ibm-mq-certs/client.rdb +0 -0
  228. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/ibm-mq-certs/client.sth +0 -0
  229. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/ibm_mq.yml +0 -0
  230. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/ibm_mq_tls.yml +0 -0
  231. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/kafka-tls.yml +0 -0
  232. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/kafka.yml +0 -0
  233. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/mariadb.yml +0 -0
  234. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/mongodb-replica.yml +0 -0
  235. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/mongodb-tls.yml +0 -0
  236. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/mongodb.yml +0 -0
  237. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/mosquitto.conf +0 -0
  238. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/mosquitto_performance.conf +0 -0
  239. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/mosquitto_persistence.conf +0 -0
  240. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/mq_init.mqsc +0 -0
  241. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/mq_init_tls.mqsc +0 -0
  242. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/mqtt.yml +0 -0
  243. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/mqtt_performance.yml +0 -0
  244. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/mysql.yml +0 -0
  245. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/nats.yml +0 -0
  246. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/object_store.yml +0 -0
  247. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/postgres.yml +0 -0
  248. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/postgres_cdc.yml +0 -0
  249. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/redis.yml +0 -0
  250. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/file.rs +0 -0
  251. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/grpc.rs +0 -0
  252. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/grpc_tls.rs +0 -0
  253. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/http.rs +0 -0
  254. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/http_tls.rs +0 -0
  255. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/ibm_mq.rs +0 -0
  256. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/ibm_mq_tls.rs +0 -0
  257. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/ipc.rs +0 -0
  258. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/kafka.rs +0 -0
  259. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/kafka_tls.rs +0 -0
  260. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/logic_test.rs +0 -0
  261. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/mariadb.rs +0 -0
  262. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/memory.rs +0 -0
  263. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/mod.rs +0 -0
  264. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/mongodb.rs +0 -0
  265. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/mongodb_raw.rs +0 -0
  266. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/mongodb_tls.rs +0 -0
  267. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/mqtt.rs +0 -0
  268. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/mysql.rs +0 -0
  269. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/nats.rs +0 -0
  270. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/object_store.rs +0 -0
  271. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/performance_static.rs +0 -0
  272. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/postgres.rs +0 -0
  273. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/postgres_cdc.rs +0 -0
  274. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/redis_streams.rs +0 -0
  275. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/route.rs +0 -0
  276. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/scripts/gen_certs.sh +0 -0
  277. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/sqlite.rs +0 -0
  278. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/tls_helpers.rs +0 -0
  279. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/websocket.rs +0 -0
  280. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/zeromq.rs +0 -0
  281. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration_test.rs +0 -0
  282. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/memory_leak_test.rs +0 -0
  283. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/ref_test.rs +0 -0
  284. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/reference_docs_test.rs +0 -0
  285. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/sqlite_test.rs +0 -0
  286. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/tls_example.rs +0 -0
  287. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/unit_tests.md +0 -0
  288. {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/websocket_test.rs +0 -0
@@ -3348,7 +3348,7 @@ dependencies = [
3348
3348
 
3349
3349
  [[package]]
3350
3350
  name = "mq-bridge"
3351
- version = "0.4.3"
3351
+ version = "0.4.4"
3352
3352
  dependencies = [
3353
3353
  "aes-gcm",
3354
3354
  "anyhow",
@@ -3431,7 +3431,7 @@ dependencies = [
3431
3431
 
3432
3432
  [[package]]
3433
3433
  name = "mq-bridge-bindings-common"
3434
- version = "0.4.3"
3434
+ version = "0.4.4"
3435
3435
  dependencies = [
3436
3436
  "anyhow",
3437
3437
  "fast-uuid-v7",
@@ -3445,7 +3445,7 @@ dependencies = [
3445
3445
 
3446
3446
  [[package]]
3447
3447
  name = "mq-bridge-node"
3448
- version = "0.4.3"
3448
+ version = "0.4.4"
3449
3449
  dependencies = [
3450
3450
  "anyhow",
3451
3451
  "async-trait",
@@ -3487,7 +3487,7 @@ dependencies = [
3487
3487
 
3488
3488
  [[package]]
3489
3489
  name = "mq-bridge-py"
3490
- version = "0.4.3"
3490
+ version = "0.4.4"
3491
3491
  dependencies = [
3492
3492
  "anyhow",
3493
3493
  "async-trait",
@@ -16,7 +16,7 @@ members = ["bindings-common", "python/mq-bridge-py"]
16
16
  resolver = "2"
17
17
 
18
18
  [workspace.package]
19
- version = "0.4.3"
19
+ version = "0.4.4"
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.4.3
3
+ Version: 0.4.4
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: License :: OSI Approved :: MIT License
6
6
  Classifier: Programming Language :: Python :: 3
@@ -2056,11 +2056,20 @@ impl FileConsumer {
2056
2056
  }
2057
2057
 
2058
2058
  pub async fn new(config: &FileConfig) -> anyhow::Result<Self> {
2059
+ Self::new_with_source_metadata(config, config.source_metadata).await
2060
+ }
2061
+
2062
+ /// `source_metadata` is the effective flag: the route enables it for an idempotent
2063
+ /// output even when the input config leaves it unset.
2064
+ pub async fn new_with_source_metadata(
2065
+ config: &FileConfig,
2066
+ source_metadata: bool,
2067
+ ) -> anyhow::Result<Self> {
2059
2068
  let startup_open_error = probe_source_path(&config.path)?;
2060
2069
  let mut consumer = Self::new_backend(config).await?;
2061
2070
  consumer.path = config.path.clone();
2062
2071
  consumer.startup_open_error = startup_open_error;
2063
- consumer.source_metadata = config.source_metadata;
2072
+ consumer.source_metadata = source_metadata;
2064
2073
  // `consume` always reads from byte 0, so its record index is reproducible and two
2065
2074
  // runs deliberately produce the same names — that is what makes the sink idempotent.
2066
2075
  // `subscribe` starts at the current end and `group_subscribe` resumes at a stored
@@ -2068,7 +2077,7 @@ impl FileConsumer {
2068
2077
  // numbered. Without an epoch those names would collide and the sink would discard
2069
2078
  // the new records as already-covered; with one they stay distinct and ordered, at
2070
2079
  // the cost of cross-restart deduplication.
2071
- consumer.run_epoch = (config.source_metadata
2080
+ consumer.run_epoch = (source_metadata
2072
2081
  && !matches!(&config.mode, None | Some(FileConsumerMode::Consume { .. })))
2073
2082
  .then(next_run_epoch);
2074
2083
  Ok(consumer)
@@ -1017,6 +1017,10 @@ async fn run_http_inline_response_fast_path(
1017
1017
  Ok(true)
1018
1018
  }
1019
1019
 
1020
+ /// `_source_metadata` is honoured by the types [`supports_source_metadata`] admits and by the
1021
+ /// NATS and AMQP consumers, which stamp provenance that is not a replay position. The other
1022
+ /// branches ignore the flag. Startup only rejects an input for missing replay positions when
1023
+ /// `source_metadata_required` is set.
1020
1024
  async fn create_base_consumer(
1021
1025
  route_name: &str,
1022
1026
  endpoint: &Endpoint,
@@ -1092,7 +1096,9 @@ async fn create_base_consumer(
1092
1096
  redis_streams::RedisStreamsConsumer::new(&config).await?,
1093
1097
  ))
1094
1098
  }
1095
- EndpointType::File(cfg) => Ok(boxed(file::FileConsumer::new(cfg).await?)),
1099
+ EndpointType::File(cfg) => Ok(boxed(
1100
+ file::FileConsumer::new_with_source_metadata(cfg, _source_metadata).await?,
1101
+ )),
1096
1102
  #[cfg(feature = "object-store")]
1097
1103
  EndpointType::ObjectStore(cfg) => {
1098
1104
  Ok(boxed(object_store::ObjectStoreConsumer::new(cfg).await?))
@@ -2107,6 +2113,82 @@ mod tests {
2107
2113
  .contains("requires an input that carries a replay position"));
2108
2114
  }
2109
2115
 
2116
+ /// The route derives `source_metadata` from an idempotent output, so a plain `file`
2117
+ /// input must be stamped without the caller setting the flag on the source config.
2118
+ #[tokio::test]
2119
+ async fn file_input_is_stamped_when_the_output_is_idempotent() {
2120
+ let dir = tempfile::tempdir().unwrap();
2121
+ let input = dir.path().join("orders.jsonl");
2122
+ std::fs::write(&input, "{\"id\":1}\n{\"id\":2}\n").unwrap();
2123
+
2124
+ let file = Endpoint::new(EndpointType::File(crate::models::FileConfig::new(
2125
+ input.to_string_lossy(),
2126
+ )));
2127
+ let mut consumer =
2128
+ create_consumer_from_route_with_source_metadata("idempotent-route", &file, true)
2129
+ .await
2130
+ .unwrap();
2131
+
2132
+ let batch = consumer.receive_batch(10).await.unwrap();
2133
+ assert_eq!(batch.messages.len(), 2, "file input produced no rows");
2134
+ for (index, message) in batch.messages.iter().enumerate() {
2135
+ assert_eq!(
2136
+ message
2137
+ .metadata
2138
+ .get("mqb.src.file_path")
2139
+ .map(String::as_str),
2140
+ Some(input.to_string_lossy().as_ref())
2141
+ );
2142
+ // The record index is the replay position, so it has to be the row's own index.
2143
+ assert_eq!(
2144
+ message
2145
+ .metadata
2146
+ .get("mqb.src.file_record")
2147
+ .map(String::as_str),
2148
+ Some(index.to_string().as_str())
2149
+ );
2150
+ // `consume` reproduces its record index, so it carries no epoch.
2151
+ assert!(!message.metadata.contains_key("mqb.src.file_epoch"));
2152
+ assert!(crate::support::source_ranges::SourcePosition::from_message(message).is_ok());
2153
+ }
2154
+ }
2155
+
2156
+ /// The epoch reads the same derived flag: a `group_subscribe` input restarts its record
2157
+ /// index, so without one the second run's names would collide with the first run's.
2158
+ #[tokio::test]
2159
+ async fn derived_source_metadata_also_gives_group_subscribe_a_run_epoch() {
2160
+ let dir = tempfile::tempdir().unwrap();
2161
+ let input = dir.path().join("orders.jsonl");
2162
+ std::fs::write(&input, "{\"id\":1}\n").unwrap();
2163
+
2164
+ let file = Endpoint::new(EndpointType::File(crate::models::FileConfig {
2165
+ mode: Some(crate::models::FileConsumerMode::GroupSubscribe {
2166
+ group_id: "derived-epoch".to_string(),
2167
+ read_from_tail: false,
2168
+ }),
2169
+ ..crate::models::FileConfig::new(input.to_string_lossy())
2170
+ }));
2171
+
2172
+ // Two runs of the same input: both restart the record index at 0, so only the epoch
2173
+ // keeps the second run's records from being named like the first run's.
2174
+ let mut positions = Vec::new();
2175
+ for _ in 0..2 {
2176
+ let mut consumer =
2177
+ create_consumer_from_route_with_source_metadata("idempotent-route", &file, true)
2178
+ .await
2179
+ .unwrap();
2180
+ let batch = consumer.receive_batch(10).await.unwrap();
2181
+ let first = batch.messages.first().expect("file input produced no rows");
2182
+ assert!(first.metadata.contains_key("mqb.src.file_epoch"));
2183
+ positions
2184
+ .push(crate::support::source_ranges::SourcePosition::from_message(first).unwrap());
2185
+ }
2186
+
2187
+ assert_ne!(positions[0].source, positions[1].source);
2188
+ // A later run reads later records, so its objects must sort after the earlier run's.
2189
+ assert!(positions[0].source < positions[1].source);
2190
+ }
2191
+
2110
2192
  #[tokio::test]
2111
2193
  async fn test_fanout_publisher_integration() {
2112
2194
  let ep1 = Endpoint::new_memory("fanout_1", 10);
@@ -31,7 +31,7 @@ use crate::endpoints::file::{encode_record, parse_delimiter, parse_message};
31
31
  use crate::models::{Compression, FileFormat, ObjectStoreConfig};
32
32
  #[cfg(feature = "encryption")]
33
33
  use crate::support::crypto::Crypto;
34
- use crate::support::source_ranges::{finalized_name, CoveredRanges};
34
+ use crate::support::source_ranges::{finalized_name, parse_finalized_name, CoveredRanges};
35
35
  use crate::traits::{
36
36
  BoxFuture, ConsumerError, MessageConsumer, MessageDisposition, MessagePublisher,
37
37
  PublisherError, ReceivedBatch, SentBatch,
@@ -127,18 +127,53 @@ async fn recover_idempotent_object_ranges(
127
127
  base: &ObjPath,
128
128
  extension: &str,
129
129
  ) -> anyhow::Result<CoveredRanges> {
130
- let mut names = Vec::new();
130
+ // Parsed as the listing streams: a bucket holding a long history of parts would otherwise
131
+ // materialise every key at once, and only the merged ranges are worth keeping.
132
+ let mut covered = CoveredRanges::default();
131
133
  let mut stream = store.list(Some(base));
132
134
  while let Some(meta) = stream.next().await {
133
- let key = meta?.location.to_string();
134
- if let Some(name) = key.rsplit('/').next() {
135
- names.push(name.to_string());
135
+ let location = meta?.location;
136
+ // The listing is recursive, but this sink writes its parts flat under `base`. A
137
+ // parseable name below a nested prefix is another sink's part, and counting it here
138
+ // would mark ranges covered that were never written to this prefix.
139
+ let Some(mut parts) = location.prefix_match(base) else {
140
+ continue;
141
+ };
142
+ let Some(name) = parts.next() else {
143
+ continue;
144
+ };
145
+ if parts.next().is_some() {
146
+ continue;
147
+ }
148
+ if let Some((source, start, end)) = parse_finalized_name(name.as_ref(), extension) {
149
+ // `parse_finalized_name` already guarantees a valid range.
150
+ covered.insert(source, start, end)?;
136
151
  }
137
152
  }
138
- Ok(CoveredRanges::from_finalized_names(
139
- names.iter().map(String::as_str),
140
- extension,
141
- ))
153
+ Ok(covered)
154
+ }
155
+
156
+ /// Classifies a failed PUT. A denied credential or a store that does not implement the
157
+ /// requested mode does not become writable by trying again, so those stop the route with the
158
+ /// reason instead of retrying forever. `PutMode::Create` on a store with conditional put
159
+ /// disabled lands here as `NotImplemented`, which is how an idempotent sink reports that the
160
+ /// bucket cannot give it the write-once guarantee it is built on.
161
+ fn classify_put_error(error: ObjectStoreError, context: String) -> PublisherError {
162
+ let permanent = matches!(
163
+ error,
164
+ ObjectStoreError::NotImplemented { .. }
165
+ | ObjectStoreError::NotSupported { .. }
166
+ | ObjectStoreError::PermissionDenied { .. }
167
+ | ObjectStoreError::Unauthenticated { .. }
168
+ | ObjectStoreError::InvalidPath { .. }
169
+ | ObjectStoreError::UnknownConfigurationKey { .. }
170
+ );
171
+ let error = anyhow!(error).context(context);
172
+ if permanent {
173
+ PublisherError::NonRetryable(error)
174
+ } else {
175
+ PublisherError::Retryable(error)
176
+ }
142
177
  }
143
178
 
144
179
  /// Splits a fetched object into record slices on `delimiter`, dropping a trailing empty
@@ -322,7 +357,20 @@ impl ObjectStorePublisher {
322
357
  &self,
323
358
  messages: Vec<CanonicalMessage>,
324
359
  ) -> Result<SentBatch, PublisherError> {
325
- let mut covered = self.covered_ranges.lock().await;
360
+ // The lock guards the shared range map, not the work done with it. Held across the
361
+ // encode and the PUT it would serialise the whole worker pool onto one in-flight
362
+ // object — a sink that publishes sequentially, which is what naming by source range
363
+ // exists to make unnecessary. A snapshot is a handful of merged ranges per partition,
364
+ // so cloning it costs far less than what it lets run concurrently.
365
+ //
366
+ // The route enqueues each message once, so batches in flight together hold disjoint
367
+ // ranges and cannot race here at all. A re-sent batch repeats a range exactly, and an
368
+ // identical range is an identical name, which `PutMode::Create` turns into a no-op.
369
+ // Only a source that redelivers offsets still in flight — a Kafka rebalance mid-batch —
370
+ // could produce two runs that overlap *partially*; those name two different objects and
371
+ // the overlap would be written twice. Claiming the range under the lock before the PUT
372
+ // would close that window, at the cost of unwinding the claim on every failure path.
373
+ let covered = self.covered_ranges.lock().await.clone();
326
374
  let runs = covered
327
375
  .uncovered_runs(messages)
328
376
  .map_err(PublisherError::NonRetryable)?;
@@ -333,6 +381,10 @@ impl ObjectStorePublisher {
333
381
  let mut body = Vec::new();
334
382
  for mut message in run.messages {
335
383
  message.strip_source_metadata();
384
+ // Unlike the ordinary path this cannot report `Partial` and hand one bad record
385
+ // to a DLQ: the object is named for the whole range, so skipping a record inside
386
+ // it would mark offsets covered that were never written. The batch fails whole
387
+ // instead. Runs already PUT stay written and are skipped on the resend.
336
388
  let bytes = encode_record(&message, &self.format)
337
389
  .map_err(|error| PublisherError::NonRetryable(anyhow!(error)))?;
338
390
  body.extend_from_slice(&bytes);
@@ -354,12 +406,15 @@ impl ObjectStorePublisher {
354
406
  {
355
407
  Ok(_) | Err(ObjectStoreError::AlreadyExists { .. }) => {}
356
408
  Err(error) => {
357
- return Err(PublisherError::Retryable(
358
- anyhow!(error).context(format!("object-store put idempotent '{key}'")),
409
+ return Err(classify_put_error(
410
+ error,
411
+ format!("object-store put idempotent '{key}'"),
359
412
  ));
360
413
  }
361
414
  }
362
- covered
415
+ self.covered_ranges
416
+ .lock()
417
+ .await
363
418
  .insert(run.source, run.start, run.end)
364
419
  .map_err(PublisherError::NonRetryable)?;
365
420
  }
@@ -421,9 +476,7 @@ impl MessagePublisher for ObjectStorePublisher {
421
476
  self.store
422
477
  .put(&key, PutPayload::from(body))
423
478
  .await
424
- .map_err(|e| {
425
- PublisherError::Retryable(anyhow!(e).context(format!("object-store put '{key}'")))
426
- })?;
479
+ .map_err(|e| classify_put_error(e, format!("object-store put '{key}'")))?;
427
480
  trace!(key = %key, "Wrote object to object store");
428
481
  if failed.is_empty() {
429
482
  Ok(SentBatch::Ack)
@@ -1010,6 +1063,119 @@ mod tests {
1010
1063
  message
1011
1064
  }
1012
1065
 
1066
+ /// A store whose `put_opts` parks on a barrier, so a put can only complete once as many
1067
+ /// puts are in flight as the barrier expects. A publisher that serialises its writes
1068
+ /// never gets there and hangs, which is what turns "slower" into a failing test.
1069
+ #[derive(Debug)]
1070
+ struct BarrierStore {
1071
+ inner: InMemory,
1072
+ puts: tokio::sync::Barrier,
1073
+ }
1074
+
1075
+ impl BarrierStore {
1076
+ fn new(concurrent_puts: usize) -> Self {
1077
+ Self {
1078
+ inner: InMemory::new(),
1079
+ puts: tokio::sync::Barrier::new(concurrent_puts),
1080
+ }
1081
+ }
1082
+ }
1083
+
1084
+ impl std::fmt::Display for BarrierStore {
1085
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1086
+ write!(f, "BarrierStore")
1087
+ }
1088
+ }
1089
+
1090
+ #[async_trait]
1091
+ impl ObjectStore for BarrierStore {
1092
+ async fn put_opts(
1093
+ &self,
1094
+ location: &ObjPath,
1095
+ payload: PutPayload,
1096
+ opts: PutOptions,
1097
+ ) -> object_store::Result<object_store::PutResult> {
1098
+ self.puts.wait().await;
1099
+ self.inner.put_opts(location, payload, opts).await
1100
+ }
1101
+
1102
+ async fn put_multipart_opts(
1103
+ &self,
1104
+ location: &ObjPath,
1105
+ opts: object_store::PutMultipartOptions,
1106
+ ) -> object_store::Result<Box<dyn object_store::MultipartUpload>> {
1107
+ self.inner.put_multipart_opts(location, opts).await
1108
+ }
1109
+
1110
+ async fn get_opts(
1111
+ &self,
1112
+ location: &ObjPath,
1113
+ options: object_store::GetOptions,
1114
+ ) -> object_store::Result<object_store::GetResult> {
1115
+ self.inner.get_opts(location, options).await
1116
+ }
1117
+
1118
+ fn delete_stream(
1119
+ &self,
1120
+ locations: futures::stream::BoxStream<'static, object_store::Result<ObjPath>>,
1121
+ ) -> futures::stream::BoxStream<'static, object_store::Result<ObjPath>> {
1122
+ self.inner.delete_stream(locations)
1123
+ }
1124
+
1125
+ fn list(
1126
+ &self,
1127
+ prefix: Option<&ObjPath>,
1128
+ ) -> futures::stream::BoxStream<'static, object_store::Result<object_store::ObjectMeta>>
1129
+ {
1130
+ self.inner.list(prefix)
1131
+ }
1132
+
1133
+ async fn list_with_delimiter(
1134
+ &self,
1135
+ prefix: Option<&ObjPath>,
1136
+ ) -> object_store::Result<object_store::ListResult> {
1137
+ self.inner.list_with_delimiter(prefix).await
1138
+ }
1139
+
1140
+ async fn copy_opts(
1141
+ &self,
1142
+ from: &ObjPath,
1143
+ to: &ObjPath,
1144
+ options: object_store::CopyOptions,
1145
+ ) -> object_store::Result<()> {
1146
+ self.inner.copy_opts(from, to, options).await
1147
+ }
1148
+ }
1149
+
1150
+ /// The idempotent path must not trade the worker pool for its ordering guarantee. Naming
1151
+ /// objects by source range is what makes concurrent writes safe, so the range bookkeeping
1152
+ /// may only be locked around the map itself — never around the encode and the PUT, which
1153
+ /// would leave an ordered sink publishing one object at a time.
1154
+ #[tokio::test]
1155
+ async fn idempotent_sends_are_not_serialised_by_the_covered_range_lock() {
1156
+ let store = Arc::new(BarrierStore::new(2));
1157
+ let mut publisher = test_publisher(store);
1158
+ publisher.idempotency = true;
1159
+ let other = publisher.clone();
1160
+
1161
+ // Disjoint offset runs, so both batches genuinely write and neither is skipped.
1162
+ let first = tokio::spawn(async move {
1163
+ publisher
1164
+ .send_batch_idempotent(vec![kafka_message(0)])
1165
+ .await
1166
+ });
1167
+ let second =
1168
+ tokio::spawn(async move { other.send_batch_idempotent(vec![kafka_message(1)]).await });
1169
+
1170
+ let sends = async {
1171
+ first.await.unwrap().unwrap();
1172
+ second.await.unwrap().unwrap();
1173
+ };
1174
+ tokio::time::timeout(Duration::from_secs(10), sends)
1175
+ .await
1176
+ .expect("both puts must be in flight at once; the covered-range lock is being held across the put");
1177
+ }
1178
+
1013
1179
  /// The guarantee the idempotent sink exists to make, and the one the whole ordering
1014
1180
  /// thread turns on: object order is *source* order even when batches are written
1015
1181
  /// concurrently and out of order. Dispatches the batches in reverse and lets them race,
@@ -1109,6 +1275,44 @@ mod tests {
1109
1275
  );
1110
1276
  }
1111
1277
 
1278
+ /// `list` is recursive, so a sink whose base is a *sub*prefix of ours shows up in our
1279
+ /// listing with a perfectly parseable part name. Counting it would mark that range
1280
+ /// covered here and silently drop the records that were never written under our prefix.
1281
+ #[tokio::test]
1282
+ async fn recovery_ignores_parseable_parts_under_a_nested_prefix() {
1283
+ let store: Arc<dyn ObjectStore> = Arc::new(InMemory::new());
1284
+ let nested = ObjPath::from(format!(
1285
+ "data/other/{}",
1286
+ part_key(0, 1, "jsonl").filename().unwrap()
1287
+ ));
1288
+ store
1289
+ .put(&nested, PutPayload::from("someone else's part"))
1290
+ .await
1291
+ .unwrap();
1292
+
1293
+ let base = ObjPath::from("data");
1294
+ let covered = recover_idempotent_object_ranges(store.as_ref(), &base, "jsonl")
1295
+ .await
1296
+ .unwrap();
1297
+
1298
+ let mut publisher = test_publisher(store.clone());
1299
+ publisher.idempotency = true;
1300
+ publisher.covered_ranges = Arc::new(Mutex::new(covered));
1301
+ publisher
1302
+ .send_batch(vec![kafka_message(0), kafka_message(1)])
1303
+ .await
1304
+ .unwrap();
1305
+
1306
+ let written = store
1307
+ .get(&part_key(0, 1, "jsonl"))
1308
+ .await
1309
+ .expect("the nested part must not count as coverage")
1310
+ .bytes()
1311
+ .await
1312
+ .unwrap();
1313
+ assert!(!written.is_empty());
1314
+ }
1315
+
1112
1316
  #[tokio::test]
1113
1317
  async fn idempotent_sink_does_not_overwrite_an_existing_range_object() {
1114
1318
  let store: Arc<dyn ObjectStore> = Arc::new(InMemory::new());