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