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.
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/Cargo.lock +4 -4
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/Cargo.toml +1 -1
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/PKG-INFO +1 -1
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/file/mod.rs +11 -2
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/mod.rs +83 -1
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/object_store.rs +220 -16
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/.github/dependabot.yml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/.github/workflows/benchmark.yml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/.github/workflows/ci.yml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/.github/workflows/ibm-mq.yml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/.github/workflows/node.yml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/.github/workflows/publish-node.yml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/.github/workflows/publish-python.yml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/.github/workflows/python.yml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/.github/workflows/release.yml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/.github/workflows/test-matrix.yml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/.gitignore +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/CHANGELOG.md +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/CLAUDE.md +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/CONTRIBUTING.md +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/LICENSE +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/LICENSE.python +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/README.md +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/SECURITY.md +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/benches/ETL_BENCHMARKS.md +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/benches/performance_bench.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/benches/router_bench.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/benches/zeromq_backends.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/bindings-common/Cargo.toml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/bindings-common/src/lib.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/bindings-common/src/logging.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/build.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/deny.toml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/docs/ARCHITECTURE.md +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/docs/CONFIGURATION.md +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/docs/DELIVERY.md +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/docs/EXTENDING.md +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/docs/PLUGINS.md +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/docs/REFERENCE.md +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/examples/ipc_worker_queue.yaml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/mq-bridge.schema.json +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/mq_bridge/__init__.py +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/mq_bridge/__init__.pyi +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/mq_bridge/config.py +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/mq_bridge/config.pyi +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/mq_bridge/plugin_packaging.py +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/mq_bridge/py.typed +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/package-lock.json +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/pyproject.toml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/Cargo.toml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/LICENSE.python +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/README.md +0 -0
- {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
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/example.py +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/examples/bench_memory.py +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/examples/bench_memory.yaml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/examples/consume_pull.py +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/examples/dlt_source.py +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/examples/json_route.py +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/examples/memory.yaml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/examples/raw_route.py +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/pyproject-basic.toml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/pyproject-reduced.toml +0 -0
- {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
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/src/lib.rs +0 -0
- {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
- {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
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/tests/test_logging.py +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/tests/test_packaging.py +0 -0
- {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
- {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
- {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
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/tests/test_routes.py +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/uv.lock +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/analysis/benches/metadata_bench.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/analysis/debug_ibm_mq_tls.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/analysis/http/analyze_xctrace_time_profile.py +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/analysis/http/http_flamegraph.py +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/analysis/http/mq_bridge_http_profile.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/analysis/http/run_http_ladder.sh +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/ci/format_bench_output.sh +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/httparena/README.md +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/httparena/frameworks/mq-bridge-py/Dockerfile +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/httparena/frameworks/mq-bridge-py/meta.json +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/httparena/frameworks/mq-bridge-py/server.py +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/techempower/Python/mq-bridge-py/README.md +0 -0
- {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
- {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
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/techempower/Python/mq-bridge-py/server.py +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/techempower/README.md +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/techempower/postgres.yml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/techempower/seed.sql +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/techempower/verify.sh +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/scripts/the-benchmarker/README.md +0 -0
- {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
- {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
- {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
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/canonical_message.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/checkpoint.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/command_handler.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/amqp.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/aws.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/clickhouse.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/file/tests.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/grpc.proto +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/grpc.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/http/mod.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/http/publisher.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/http/stream.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/http/tests.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/ibm_mq.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/kafka.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/memory/endpoint.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/memory/framed.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/memory/ipc_unix.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/memory/ipc_windows.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/memory/memory_transport.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/memory/mod.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/memory/transport.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/mongodb/consumer.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/mongodb/dedup.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/mongodb/mod.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/mongodb/publisher.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/mongodb/readers.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/mongodb/tests.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/mqtt.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/nats.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/poll.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/postgres/VENDORED.md +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/postgres/mod.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/postgres/pgoutput/decoder.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/postgres/pgoutput/messages.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/postgres/pgoutput/mod.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/postgres/pgoutput/registry.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/postgres/pgoutput/values.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/postgres/replication.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/postgres/state.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/redis_streams.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/sled.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/sqlx/dedup.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/sqlx/mod.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/sqlx/tests.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/structural/fanout.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/structural/mod.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/structural/null.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/structural/reader.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/structural/request.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/structural/response.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/structural/static_endpoint.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/structural/stream_buffer.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/structural/switch.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/websocket.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/zeromq/codec.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/zeromq/mod.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/zeromq/omq.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/endpoints/zeromq/zmq.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/errors.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/event_handler.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/event_store.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/extensions.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/lib.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/buffer.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/compression.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/cookie_jar.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/deduplication.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/delay.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/dlq.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/encryption.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/id.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/limiter.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/metrics.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/mod.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/random_panic.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/retry.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/transform/coerce.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/transform/compiled.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/transform/error.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/transform/mod.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/transform/path.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/transform/schema.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/transform/tests.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/middleware/weak_join.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/models/builders.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/models/defaults.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/models/secrets.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/models/serde_support.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/models/tests.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/models.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/outcomes.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/plugin/conformance.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/plugin/endpoint.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/plugin/message.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/plugin/middleware.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/plugin/mod.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/plugin/sdk.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/plugin/test_support.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/publisher.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/response.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/route.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/support/base64_engine.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/support/compression.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/support/connection_registry.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/support/crypto.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/support/crypto_envelope.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/support/interpolation.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/support/mod.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/support/plugin_abi.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/support/source_ranges.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/test_utils.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/traits.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/src/type_handler.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/README.md +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/armature_integration.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/amqp.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/aws.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/clickhouse.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/config.toml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/amqp.yml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/aws.yml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/clickhouse.yml +0 -0
- {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
- {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
- {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
- {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
- {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
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/ibm_mq.yml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/ibm_mq_tls.yml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/kafka-tls.yml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/kafka.yml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/mariadb.yml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/mongodb-replica.yml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/mongodb-tls.yml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/mongodb.yml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/mosquitto.conf +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/mosquitto_performance.conf +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/mosquitto_persistence.conf +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/mq_init.mqsc +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/mq_init_tls.mqsc +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/mqtt.yml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/mqtt_performance.yml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/mysql.yml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/nats.yml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/object_store.yml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/postgres.yml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/postgres_cdc.yml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/docker-compose/redis.yml +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/file.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/grpc.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/grpc_tls.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/http.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/http_tls.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/ibm_mq.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/ibm_mq_tls.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/ipc.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/kafka.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/kafka_tls.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/logic_test.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/mariadb.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/memory.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/mod.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/mongodb.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/mongodb_raw.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/mongodb_tls.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/mqtt.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/mysql.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/nats.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/object_store.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/performance_static.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/postgres.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/postgres_cdc.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/redis_streams.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/route.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/scripts/gen_certs.sh +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/sqlite.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/tls_helpers.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/websocket.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration/zeromq.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/integration_test.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/memory_leak_test.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/ref_test.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/reference_docs_test.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/sqlite_test.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/tls_example.rs +0 -0
- {mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/tests/unit_tests.md +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
3490
|
+
version = "0.4.4"
|
|
3491
3491
|
dependencies = [
|
|
3492
3492
|
"anyhow",
|
|
3493
3493
|
"async-trait",
|
|
@@ -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 =
|
|
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 = (
|
|
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(
|
|
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
|
-
|
|
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
|
|
134
|
-
|
|
135
|
-
|
|
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(
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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(
|
|
358
|
-
|
|
409
|
+
return Err(classify_put_error(
|
|
410
|
+
error,
|
|
411
|
+
format!("object-store put idempotent '{key}'"),
|
|
359
412
|
));
|
|
360
413
|
}
|
|
361
414
|
}
|
|
362
|
-
|
|
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());
|
|
File without changes
|
{mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/.github/ISSUE_TEMPLATE/feature_request.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/examples/bench_memory.py
RENAMED
|
File without changes
|
{mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/examples/bench_memory.yaml
RENAMED
|
File without changes
|
{mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/examples/consume_pull.py
RENAMED
|
File without changes
|
{mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/examples/dlt_source.py
RENAMED
|
File without changes
|
{mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/examples/json_route.py
RENAMED
|
File without changes
|
{mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/examples/memory.yaml
RENAMED
|
File without changes
|
{mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/examples/raw_route.py
RENAMED
|
File without changes
|
{mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/pyproject-basic.toml
RENAMED
|
File without changes
|
{mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/pyproject-reduced.toml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mq_bridge_py_basic-0.4.3 → mq_bridge_py_basic-0.4.4}/python/mq-bridge-py/tests/test_config_types.py
RENAMED
|
File without changes
|