babelqueue 1.20.0__tar.gz → 1.21.0__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.
- {babelqueue-1.20.0 → babelqueue-1.21.0}/.github/workflows/ci.yml +35 -8
- babelqueue-1.21.0/.github/workflows/release.yml +66 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/CHANGELOG.md +22 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/PKG-INFO +3 -1
- {babelqueue-1.20.0 → babelqueue-1.21.0}/README.md +2 -0
- babelqueue-1.21.0/conformance-known-gaps.json +84 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/pyproject.toml +1 -1
- babelqueue-1.21.0/scripts/check_conformance_results.py +160 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/__init__.py +1 -1
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/app.py +2 -2
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/artemis_transport.py +3 -3
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/asb_transport.py +3 -3
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/codec.py +80 -28
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/kafka_transport.py +3 -3
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/pulsar_transport.py +4 -4
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/redrive.py +2 -2
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/sqs_transport.py +3 -3
- babelqueue-1.21.0/tests/conformance/CONFORMANCE_VERSION +1 -0
- babelqueue-1.21.0/tests/conformance/fixtures/job-type-array-urn-fallback.json +18 -0
- babelqueue-1.21.0/tests/conformance/fixtures/job-type-bool-urn-fallback.json +16 -0
- babelqueue-1.21.0/tests/conformance/fixtures/job-type-empty-string-urn-fallback.json +16 -0
- babelqueue-1.21.0/tests/conformance/fixtures/job-type-missing-job-array-urn.json +17 -0
- babelqueue-1.21.0/tests/conformance/fixtures/job-type-null-no-urn.json +15 -0
- babelqueue-1.21.0/tests/conformance/fixtures/job-type-null-urn-fallback.json +16 -0
- babelqueue-1.21.0/tests/conformance/fixtures/job-type-number-blank-urn.json +16 -0
- babelqueue-1.21.0/tests/conformance/fixtures/job-type-number-nonstring-urn.json +16 -0
- babelqueue-1.21.0/tests/conformance/fixtures/job-type-number-urn-fallback.json +16 -0
- babelqueue-1.21.0/tests/conformance/fixtures/job-type-object-urn-fallback.json +18 -0
- babelqueue-1.21.0/tests/conformance/fixtures/trim-blank-job-cr.json +16 -0
- babelqueue-1.21.0/tests/conformance/fixtures/trim-blank-job-ff.json +16 -0
- babelqueue-1.21.0/tests/conformance/fixtures/trim-blank-job-lf.json +16 -0
- babelqueue-1.21.0/tests/conformance/fixtures/trim-blank-job-space.json +16 -0
- babelqueue-1.21.0/tests/conformance/fixtures/trim-blank-job-tab.json +16 -0
- babelqueue-1.21.0/tests/conformance/fixtures/trim-blank-job-vt.json +16 -0
- babelqueue-1.21.0/tests/conformance/fixtures/trim-blank-trace-id-cr.json +15 -0
- babelqueue-1.21.0/tests/conformance/fixtures/trim-blank-trace-id-ff.json +15 -0
- babelqueue-1.21.0/tests/conformance/fixtures/trim-blank-trace-id-lf.json +15 -0
- babelqueue-1.21.0/tests/conformance/fixtures/trim-blank-trace-id-space.json +15 -0
- babelqueue-1.21.0/tests/conformance/fixtures/trim-blank-trace-id-tab.json +15 -0
- babelqueue-1.21.0/tests/conformance/fixtures/trim-blank-trace-id-vt.json +15 -0
- babelqueue-1.21.0/tests/conformance/fixtures/trim-job-and-urn-both-blank.json +16 -0
- babelqueue-1.21.0/tests/conformance/fixtures/trim-reemit-job-ascii-padded.json +15 -0
- babelqueue-1.21.0/tests/conformance/fixtures/trim-reemit-job-unicode-padded.json +15 -0
- babelqueue-1.21.0/tests/conformance/fixtures/trim-reemit-trace-id-padded.json +15 -0
- babelqueue-1.21.0/tests/conformance/fixtures/trim-unicode-job-emsp.json +15 -0
- babelqueue-1.21.0/tests/conformance/fixtures/trim-unicode-job-ideographic.json +15 -0
- babelqueue-1.21.0/tests/conformance/fixtures/trim-unicode-job-nbsp.json +15 -0
- babelqueue-1.21.0/tests/conformance/fixtures/trim-urn-only-ascii-blank.json +15 -0
- babelqueue-1.21.0/tests/conformance/fixtures/trim-urn-only-unicode-space.json +15 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/conformance/manifest.json +900 -0
- babelqueue-1.21.0/tests/conformance/schema/results.example.json +60 -0
- babelqueue-1.21.0/tests/conformance/schema/results.schema.json +109 -0
- babelqueue-1.21.0/tests/test_behaviour_conformance.py +1266 -0
- babelqueue-1.21.0/tests/test_conformance.py +295 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_kafka_transport.py +1 -1
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_pulsar_transport.py +16 -2
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_schema_conformance.py +4 -2
- babelqueue-1.20.0/.github/workflows/release.yml +0 -46
- babelqueue-1.20.0/tests/conformance/CONFORMANCE_VERSION +0 -1
- babelqueue-1.20.0/tests/test_behaviour_conformance.py +0 -513
- babelqueue-1.20.0/tests/test_conformance.py +0 -58
- {babelqueue-1.20.0 → babelqueue-1.21.0}/.github/FUNDING.yml +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/.github/dependabot.yml +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/.gitignore +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/LICENSE +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/_envelope_shape.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/celery.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/contracts.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/dead_letter.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/django/__init__.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/django/apps.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/django/management/__init__.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/django/management/commands/__init__.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/django/management/commands/babelqueue_worker.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/exceptions.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/gdpr.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/headers.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/idempotency.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/otel.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/outbox.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/pika_transport.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/py.typed +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/redis_transport.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/replay.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/routing.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/schema.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/src/babelqueue/transport.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/conformance/fixtures/blank-ascii-job-urn-fallback.json +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/conformance/fixtures/data-array-rejected.json +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/conformance/fixtures/dead-letter-roundtrip.json +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/conformance/fixtures/dead-lettered.json +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/conformance/fixtures/duplicate-keys-last-wins.json +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/conformance/fixtures/empty-data-roundtrip.json +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/conformance/fixtures/forbidden-key-job-and-urn.json +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/conformance/fixtures/forbidden-key-job-nonascii-space-and-urn.json +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/conformance/fixtures/forbidden-key-meta-attempts.json +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/conformance/fixtures/forbidden-key-meta-max-retries.json +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/conformance/fixtures/forbidden-key-meta-source.json +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/conformance/fixtures/forbidden-key-meta-ts.json +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/conformance/fixtures/forbidden-key-timestamp.json +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/conformance/fixtures/invalid-missing-urn.json +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/conformance/fixtures/invalid-unknown-schema-version.json +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/conformance/fixtures/invalid-wrong-case-job.json +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/conformance/fixtures/meta-ext-vendor-roundtrip.json +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/conformance/fixtures/order-created.json +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/conformance/fixtures/unicode-and-numbers.json +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/conformance/fixtures/unknown-lang.json +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/conformance/fixtures/unknown-meta-roundtrip.json +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/conformance/fixtures/unknown-toplevel-roundtrip.json +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/conformance/fixtures/urn-alias.json +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/conformance/fixtures/urn-only-roundtrip.json +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/conformance/fixtures/wrong-case-keys-roundtrip.json +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/conformance/schema/message-envelope.schema.json +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/fakes/confluent_kafka/__init__.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/fakes/confluent_kafka/admin.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/fixtures/dead-lettered.json +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/fixtures/order-created.json +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/poison-trigger-set.json +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_app.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_artemis_conformance.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_artemis_transport.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_asb_conformance.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_asb_transport.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_celery.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_codec.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_dead_letter.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_django.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_gdpr.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_headers.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_idempotency.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_kafka_conformance.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_kafka_integration.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_kafka_integration_logic.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_otel.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_outbox.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_overhead.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_pika_transport.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_pulsar_conformance.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_redis_transport.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_redrive.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_reencode_regression.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_replay.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_schema.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_shutdown.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_sqs_conformance.py +0 -0
- {babelqueue-1.20.0 → babelqueue-1.21.0}/tests/test_sqs_transport.py +0 -0
|
@@ -160,18 +160,45 @@ jobs:
|
|
|
160
160
|
run: pytest tests/test_kafka_integration.py -v
|
|
161
161
|
|
|
162
162
|
conformance:
|
|
163
|
-
name: Conformance suite in sync
|
|
163
|
+
name: Conformance suite in sync + results.json
|
|
164
164
|
runs-on: ubuntu-latest
|
|
165
|
+
env:
|
|
166
|
+
# The pinned conformance release (ADR-0032). Bump together with tests/conformance/.
|
|
167
|
+
BQ_CONFORMANCE_VERSION: 1.5.0
|
|
168
|
+
# sha256 of that release's fetch.sh: the script runs on the runner, so it is pinned rather
|
|
169
|
+
# than trusted from a mutable tag. Bump together with BQ_CONFORMANCE_VERSION.
|
|
170
|
+
BQ_FETCH_SH_SHA256: 8bb6ba567b288fb0ac71a70fd17041b2503df119e01998d76fdad372570bf849
|
|
165
171
|
steps:
|
|
166
172
|
- uses: actions/checkout@v7
|
|
167
|
-
|
|
173
|
+
|
|
174
|
+
- name: Setup Python
|
|
175
|
+
uses: actions/setup-python@v7
|
|
176
|
+
with:
|
|
177
|
+
python-version: '3.12'
|
|
178
|
+
|
|
179
|
+
- name: Verify the vendored suite against the release asset
|
|
168
180
|
run: |
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
181
|
+
# fetch.sh comes from the pinned tag; it downloads babelqueue-conformance-<v>.tar.gz,
|
|
182
|
+
# verifies it against the release SHA256SUMS before unpacking, then diffs the vendor dir.
|
|
183
|
+
curl -fsSL -o "$RUNNER_TEMP/fetch.sh" \
|
|
184
|
+
"https://raw.githubusercontent.com/BabelQueue/conformance/v${BQ_CONFORMANCE_VERSION}/fetch.sh"
|
|
185
|
+
echo "${BQ_FETCH_SH_SHA256} $RUNNER_TEMP/fetch.sh" | sha256sum -c -
|
|
186
|
+
bash "$RUNNER_TEMP/fetch.sh" --check "$BQ_CONFORMANCE_VERSION" tests/conformance
|
|
187
|
+
test "$(tr -d '[:space:]' < tests/conformance/CONFORMANCE_VERSION)" = "$BQ_CONFORMANCE_VERSION"
|
|
188
|
+
echo "Vendored conformance is in sync with the v${BQ_CONFORMANCE_VERSION} release asset."
|
|
189
|
+
|
|
190
|
+
- name: Install
|
|
191
|
+
run: |
|
|
192
|
+
python -m pip install --upgrade pip
|
|
193
|
+
pip install -e ".[dev,celery,django,otel]" opentelemetry-sdk jsonschema
|
|
194
|
+
|
|
195
|
+
- name: Run the conformance suite and write results.json
|
|
196
|
+
env:
|
|
197
|
+
BQ_CONFORMANCE_RESULTS: ${{ runner.temp }}/results.json
|
|
198
|
+
run: pytest -q tests/test_conformance.py
|
|
199
|
+
|
|
200
|
+
- name: Validate results.json (schema, bindings, sections)
|
|
201
|
+
run: python scripts/check_conformance_results.py "$RUNNER_TEMP/results.json" tests/conformance
|
|
175
202
|
|
|
176
203
|
ci-green:
|
|
177
204
|
name: CI green
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ['v*']
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
release:
|
|
12
|
+
name: Build, test & publish to PyPI
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
# Recommended for PyPI Trusted Publishing — configure a "pypi" environment +
|
|
15
|
+
# a trusted publisher on PyPI (repo BabelQueue/babelqueue-python, this workflow).
|
|
16
|
+
environment: pypi
|
|
17
|
+
env:
|
|
18
|
+
# The pinned conformance release (ADR-0032). Keep identical to ci.yml.
|
|
19
|
+
BQ_CONFORMANCE_VERSION: 1.5.0
|
|
20
|
+
BQ_FETCH_SH_SHA256: 8bb6ba567b288fb0ac71a70fd17041b2503df119e01998d76fdad372570bf849
|
|
21
|
+
permissions:
|
|
22
|
+
id-token: write # PyPI Trusted Publishing (OIDC) — no API token needed
|
|
23
|
+
contents: write # create the GitHub release
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v7
|
|
26
|
+
|
|
27
|
+
- name: Setup Python
|
|
28
|
+
uses: actions/setup-python@v7
|
|
29
|
+
with:
|
|
30
|
+
python-version: '3.12'
|
|
31
|
+
|
|
32
|
+
- name: Verify the vendored suite against the release asset
|
|
33
|
+
run: |
|
|
34
|
+
curl -fsSL -o "$RUNNER_TEMP/fetch.sh" \
|
|
35
|
+
"https://raw.githubusercontent.com/BabelQueue/conformance/v${BQ_CONFORMANCE_VERSION}/fetch.sh"
|
|
36
|
+
echo "${BQ_FETCH_SH_SHA256} $RUNNER_TEMP/fetch.sh" | sha256sum -c -
|
|
37
|
+
bash "$RUNNER_TEMP/fetch.sh" --check "$BQ_CONFORMANCE_VERSION" tests/conformance
|
|
38
|
+
test "$(tr -d '[:space:]' < tests/conformance/CONFORMANCE_VERSION)" = "$BQ_CONFORMANCE_VERSION"
|
|
39
|
+
|
|
40
|
+
- name: Install & test (writes results.json)
|
|
41
|
+
env:
|
|
42
|
+
BQ_CONFORMANCE_RESULTS: ${{ runner.temp }}/results.json
|
|
43
|
+
run: |
|
|
44
|
+
python -m pip install --upgrade pip build
|
|
45
|
+
pip install -e ".[dev,celery,django,otel]" opentelemetry-sdk jsonschema
|
|
46
|
+
pytest
|
|
47
|
+
test -s "$BQ_CONFORMANCE_RESULTS" # the conformance run must have produced results.json
|
|
48
|
+
|
|
49
|
+
- name: Validate results.json (schema, bindings, sections)
|
|
50
|
+
run: python scripts/check_conformance_results.py "$RUNNER_TEMP/results.json" tests/conformance
|
|
51
|
+
|
|
52
|
+
- name: Build distributions
|
|
53
|
+
run: python -m build
|
|
54
|
+
|
|
55
|
+
- name: Publish to PyPI
|
|
56
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
57
|
+
# Or, with a token instead of trusted publishing:
|
|
58
|
+
# with:
|
|
59
|
+
# password: ${{ secrets.PYPI_API_TOKEN }}
|
|
60
|
+
|
|
61
|
+
- name: Create GitHub release
|
|
62
|
+
uses: softprops/action-gh-release@v3
|
|
63
|
+
with:
|
|
64
|
+
generate_release_notes: true
|
|
65
|
+
files: ${{ runner.temp }}/results.json
|
|
66
|
+
fail_on_unmatched_files: true
|
|
@@ -9,6 +9,28 @@ The envelope wire format is versioned separately by `meta.schema_version`
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [1.21.0] - 2026-10-19
|
|
13
|
+
|
|
14
|
+
Implements ADR-0032 (conformance packaging, results format, ASCII trim and the non-string `job` rule).
|
|
15
|
+
The envelope stays at `schema_version: 1`.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- **`EnvelopeCodec.decode` raises `BabelQueueError` for a JSON object with neither a valid string `job` nor
|
|
19
|
+
a valid string `urn`** (ADR-0032 section 6). Malformed or non-object input still yields `{}`. The runtime
|
|
20
|
+
dispatch, the transports' attempts reconciliation and `redrive` use the new `decode_lenient`, so such a
|
|
21
|
+
body still takes the poison / pass-through path.
|
|
22
|
+
- A non-string `job` (`None`, number, boolean, object, array, falsy or not) is ignored with a warning and a
|
|
23
|
+
valid string `urn` takes over.
|
|
24
|
+
- Trim and blank checks in `make` (`urn`, `trace_id`) and `accepts` (`trace_id`) use the closed ASCII set
|
|
25
|
+
(space, `\t`, `\n`, `\v`, `\f`, `\r`); `str.strip()` Unicode whitespace is no longer trimmed
|
|
26
|
+
(ADR-0032 section 5). The set is exported as `codec.ASCII_WHITESPACE` and the transports import it.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- `EnvelopeCodec.decode_lenient`.
|
|
30
|
+
- Conformance runner covers the v1.5.0 sections and writes `results.json` (ADR-0032 section 2).
|
|
31
|
+
`conformance-known-gaps.json` and `scripts/check_conformance_results.py` gate it in CI and release: the
|
|
32
|
+
SQS `delay` and Kafka `backoff` tier cases run and record honest `fail`s until they are implemented.
|
|
33
|
+
|
|
12
34
|
## [1.20.0] - 2026-10-17
|
|
13
35
|
|
|
14
36
|
Conformance suite 1.4.0, [ADR-0031](https://babelqueue.com). The envelope is unchanged
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: babelqueue
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.21.0
|
|
4
4
|
Summary: Polyglot Queues, Simplified — the Python core: the canonical BabelQueue wire-envelope codec, contracts and dead-letter helpers.
|
|
5
5
|
Project-URL: Homepage, https://babelqueue.com
|
|
6
6
|
Project-URL: Source, https://github.com/BabelQueue/babelqueue-python
|
|
@@ -58,6 +58,8 @@ Description-Content-Type: text/markdown
|
|
|
58
58
|
[](https://pypi.org/project/babelqueue/)
|
|
59
59
|
[](https://pypi.org/project/babelqueue/)
|
|
60
60
|
[](LICENSE)
|
|
61
|
+
[](https://babelqueue.com/compatibility/)
|
|
62
|
+
[](https://babelqueue.com/compatibility/)
|
|
61
63
|
|
|
62
64
|
> **Polyglot Queues, Simplified.** Read and write the canonical BabelQueue message
|
|
63
65
|
> envelope from Python — so your Python services (AI/ML, data processing, …)
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
[](https://pypi.org/project/babelqueue/)
|
|
5
5
|
[](https://pypi.org/project/babelqueue/)
|
|
6
6
|
[](LICENSE)
|
|
7
|
+
[](https://babelqueue.com/compatibility/)
|
|
8
|
+
[](https://babelqueue.com/compatibility/)
|
|
7
9
|
|
|
8
10
|
> **Polyglot Queues, Simplified.** Read and write the canonical BabelQueue message
|
|
9
11
|
> envelope from Python — so your Python services (AI/ML, data processing, …)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"gaps": [
|
|
3
|
+
{
|
|
4
|
+
"section": "delay",
|
|
5
|
+
"case": "standard-1s",
|
|
6
|
+
"reason": "Python SqsTransport has no per-message DelaySeconds publish path (broker-bindings section 3.4, ADR-0012); not implemented in this core, tracked in the r1 follow-up plan"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"section": "delay",
|
|
10
|
+
"case": "standard-60s",
|
|
11
|
+
"reason": "Python SqsTransport has no per-message DelaySeconds publish path (broker-bindings section 3.4, ADR-0012); not implemented in this core, tracked in the r1 follow-up plan"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"section": "delay",
|
|
15
|
+
"case": "standard-900s-is-the-cap",
|
|
16
|
+
"reason": "Python SqsTransport has no per-message DelaySeconds publish path (broker-bindings section 3.4, ADR-0012); not implemented in this core, tracked in the r1 follow-up plan"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"section": "delay",
|
|
20
|
+
"case": "standard-901s-rejected",
|
|
21
|
+
"reason": "Python SqsTransport has no per-message DelaySeconds publish path (broker-bindings section 3.4, ADR-0012); not implemented in this core, tracked in the r1 follow-up plan"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"section": "delay",
|
|
25
|
+
"case": "standard-3600s-rejected",
|
|
26
|
+
"reason": "Python SqsTransport has no per-message DelaySeconds publish path (broker-bindings section 3.4, ADR-0012); not implemented in this core, tracked in the r1 follow-up plan"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"section": "delay",
|
|
30
|
+
"case": "fifo-per-message-delay-rejected",
|
|
31
|
+
"reason": "Python SqsTransport has no per-message DelaySeconds publish path (broker-bindings section 3.4, ADR-0012); not implemented in this core, tracked in the r1 follow-up plan"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"section": "backoff",
|
|
35
|
+
"case": "below-first-tier",
|
|
36
|
+
"reason": "Python KafkaTransport has no retry-topic tier routing (broker-bindings sections 6.4/6.5, ADR-0015); not implemented in this core, tracked in the r1 follow-up plan"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"section": "backoff",
|
|
40
|
+
"case": "exactly-first-tier",
|
|
41
|
+
"reason": "Python KafkaTransport has no retry-topic tier routing (broker-bindings sections 6.4/6.5, ADR-0015); not implemented in this core, tracked in the r1 follow-up plan"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"section": "backoff",
|
|
45
|
+
"case": "just-above-first-tier",
|
|
46
|
+
"reason": "Python KafkaTransport has no retry-topic tier routing (broker-bindings sections 6.4/6.5, ADR-0015); not implemented in this core, tracked in the r1 follow-up plan"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"section": "backoff",
|
|
50
|
+
"case": "exactly-third-tier",
|
|
51
|
+
"reason": "Python KafkaTransport has no retry-topic tier routing (broker-bindings sections 6.4/6.5, ADR-0015); not implemented in this core, tracked in the r1 follow-up plan"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"section": "backoff",
|
|
55
|
+
"case": "exactly-largest-tier",
|
|
56
|
+
"reason": "Python KafkaTransport has no retry-topic tier routing (broker-bindings sections 6.4/6.5, ADR-0015); not implemented in this core, tracked in the r1 follow-up plan"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"section": "backoff",
|
|
60
|
+
"case": "above-largest-tier-rejected",
|
|
61
|
+
"reason": "Python KafkaTransport has no retry-topic tier routing (broker-bindings sections 6.4/6.5, ADR-0015); not implemented in this core, tracked in the r1 follow-up plan"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"section": "backoff",
|
|
65
|
+
"case": "no-tiers-configured-rejected",
|
|
66
|
+
"reason": "Python KafkaTransport has no retry-topic tier routing (broker-bindings sections 6.4/6.5, ADR-0015); not implemented in this core, tracked in the r1 follow-up plan"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"section": "backoff",
|
|
70
|
+
"case": "retry_republish/first-retry",
|
|
71
|
+
"reason": "Python KafkaTransport has no retry-topic tier routing (broker-bindings sections 6.4/6.5, ADR-0015); not implemented in this core, tracked in the r1 follow-up plan"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"section": "backoff",
|
|
75
|
+
"case": "retry_republish/third-retry-picks-larger-tier",
|
|
76
|
+
"reason": "Python KafkaTransport has no retry-topic tier routing (broker-bindings sections 6.4/6.5, ADR-0015); not implemented in this core, tracked in the r1 follow-up plan"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"section": "backoff",
|
|
80
|
+
"case": "retry_republish/no-tiers-configured-rejected",
|
|
81
|
+
"reason": "Python KafkaTransport has no retry-topic tier routing (broker-bindings sections 6.4/6.5, ADR-0015); not implemented in this core, tracked in the r1 follow-up plan"
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "babelqueue"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.21.0"
|
|
8
8
|
description = "Polyglot Queues, Simplified — the Python core: the canonical BabelQueue wire-envelope codec, contracts and dead-letter helpers."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Completeness gate for a conformance ``results.json`` (ADR-0032 section 3 / Compliance).
|
|
3
|
+
|
|
4
|
+
Usage: python scripts/check_conformance_results.py <results.json> <conformance-dir> [src-dir]
|
|
5
|
+
|
|
6
|
+
Known gaps: a ``fail`` is accepted only when ``conformance-known-gaps.json`` (repo root) lists its
|
|
7
|
+
section and case; a listed case that reports ``pass`` is a stale gap and fails the gate, as does
|
|
8
|
+
a ``skip`` or a missing result for a listed case, and a gap naming a case the manifest lacks.
|
|
9
|
+
|
|
10
|
+
Shared by ``ci.yml`` and ``release.yml`` so a tag cannot publish an asset CI would have refused.
|
|
11
|
+
It is deliberately independent of the test runner: the carried bindings are re-derived from the
|
|
12
|
+
package modules and compared for *set equality* with ``results.bindings`` (a missing binding and
|
|
13
|
+
a surplus one both fail), then the sections are checked against the manifest scopes.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import re
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
import jsonschema
|
|
24
|
+
|
|
25
|
+
# Transport module -> binding (manifest scope). Keep in step with tests/test_conformance.py.
|
|
26
|
+
TRANSPORT_BINDINGS = {
|
|
27
|
+
"redis_transport": "redis",
|
|
28
|
+
"pika_transport": "amqp",
|
|
29
|
+
"sqs_transport": "sqs",
|
|
30
|
+
"asb_transport": "asb",
|
|
31
|
+
"pulsar_transport": "pulsar",
|
|
32
|
+
"kafka_transport": "kafka",
|
|
33
|
+
"artemis_transport": "artemis",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def derive_bindings(src: Path) -> set:
|
|
38
|
+
bindings = set()
|
|
39
|
+
for path in sorted(src.glob("*_transport.py")):
|
|
40
|
+
if path.stem not in TRANSPORT_BINDINGS:
|
|
41
|
+
raise SystemExit(f"transport module {path.name} has no binding mapping")
|
|
42
|
+
bindings.add(TRANSPORT_BINDINGS[path.stem])
|
|
43
|
+
otel = src / "otel.py"
|
|
44
|
+
if otel.is_file() and "traceparent" in otel.read_text(encoding="utf-8").lower():
|
|
45
|
+
bindings.add("otel")
|
|
46
|
+
return bindings
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
GAPS_FILE = Path(__file__).resolve().parent.parent / "conformance-known-gaps.json"
|
|
50
|
+
|
|
51
|
+
#: A reason must cite the contract section and the follow-up plan, so a gap is never anonymous.
|
|
52
|
+
REASON_PATTERN = re.compile(r"section[s]? \d")
|
|
53
|
+
REASON_PLAN = "r1 follow-up plan"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def check_gap_entry(entry: dict, manifest: dict) -> list:
|
|
57
|
+
"""Problems with one known-gap entry against the manifest (empty when it is well formed)."""
|
|
58
|
+
problems = []
|
|
59
|
+
section, case, reason = entry["section"], entry["case"], entry["reason"]
|
|
60
|
+
definition = manifest["sections"].get(section)
|
|
61
|
+
if definition is None:
|
|
62
|
+
problems.append(f"{section}/{case}: unknown section")
|
|
63
|
+
elif definition["scope"] == "core":
|
|
64
|
+
# Only a broker-scoped feature may be a documented gap; the core envelope never is.
|
|
65
|
+
problems.append(f"{section}/{case}: a core-scope section cannot have a known gap")
|
|
66
|
+
if not REASON_PATTERN.search(reason) or REASON_PLAN not in reason:
|
|
67
|
+
problems.append(f'{section}/{case}: reason must cite a contract "section N" and the "{REASON_PLAN}"')
|
|
68
|
+
return problems
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def load_gaps(path: Path, errors: list, manifest: dict) -> dict:
|
|
72
|
+
"""``(section, case) -> reason`` from the known-gaps file; malformed entries are errors."""
|
|
73
|
+
if not path.is_file():
|
|
74
|
+
return {}
|
|
75
|
+
document = json.loads(path.read_text(encoding="utf-8"))
|
|
76
|
+
gaps: dict = {}
|
|
77
|
+
entry_problems: list = []
|
|
78
|
+
entries = document.get("gaps") if isinstance(document, dict) else None
|
|
79
|
+
if not isinstance(entries, list):
|
|
80
|
+
errors.append(f'{path.name}: top level must be {{"gaps": [...]}}')
|
|
81
|
+
return gaps
|
|
82
|
+
for index, entry in enumerate(entries):
|
|
83
|
+
if not isinstance(entry, dict) or not all(
|
|
84
|
+
isinstance(entry.get(key), str) and entry[key].strip() for key in ("section", "case", "reason")
|
|
85
|
+
):
|
|
86
|
+
errors.append(f"{path.name}: gaps[{index}] needs non-empty string section, case and reason")
|
|
87
|
+
continue
|
|
88
|
+
key = (entry["section"], entry["case"])
|
|
89
|
+
entry_problems.extend(check_gap_entry(entry, manifest))
|
|
90
|
+
if key in gaps:
|
|
91
|
+
errors.append(f"{path.name}: duplicate gap {key[0]}/{key[1]}")
|
|
92
|
+
gaps[key] = entry["reason"]
|
|
93
|
+
errors.extend(f"{path.name}: {problem}" for problem in entry_problems)
|
|
94
|
+
return gaps
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def main(argv: list) -> int:
|
|
98
|
+
if len(argv) not in (2, 3):
|
|
99
|
+
print(__doc__, file=sys.stderr)
|
|
100
|
+
return 2
|
|
101
|
+
results_path, suite = Path(argv[0]), Path(argv[1])
|
|
102
|
+
src = Path(argv[2]) if len(argv) == 3 else Path("src/babelqueue")
|
|
103
|
+
|
|
104
|
+
results = json.loads(results_path.read_text(encoding="utf-8"))
|
|
105
|
+
manifest = json.loads((suite / "manifest.json").read_text(encoding="utf-8"))
|
|
106
|
+
schema = json.loads((suite / "schema" / "results.schema.json").read_text(encoding="utf-8"))
|
|
107
|
+
|
|
108
|
+
errors = []
|
|
109
|
+
try:
|
|
110
|
+
jsonschema.Draft202012Validator(schema, format_checker=jsonschema.FormatChecker()).validate(results)
|
|
111
|
+
except jsonschema.ValidationError as exc:
|
|
112
|
+
errors.append(f"results.json does not match results.schema.json: {exc.message}")
|
|
113
|
+
|
|
114
|
+
carried = derive_bindings(src)
|
|
115
|
+
listed = set(results.get("bindings", []))
|
|
116
|
+
for name in sorted(carried - listed):
|
|
117
|
+
errors.append(f'binding "{name}" is carried by the package but missing from results.bindings')
|
|
118
|
+
for name in sorted(listed - carried):
|
|
119
|
+
errors.append(f'results.bindings lists "{name}", which the package does not carry')
|
|
120
|
+
|
|
121
|
+
sections = results.get("sections", {})
|
|
122
|
+
for name, definition in manifest["sections"].items():
|
|
123
|
+
in_scope = definition["scope"] == "core" or definition["scope"] in listed
|
|
124
|
+
if in_scope and name not in sections:
|
|
125
|
+
errors.append(f'in-scope section "{name}" (scope {definition["scope"]}) is missing')
|
|
126
|
+
if not in_scope and name in sections:
|
|
127
|
+
errors.append(f'section "{name}" (scope {definition["scope"]}) must be absent')
|
|
128
|
+
|
|
129
|
+
gaps = load_gaps(GAPS_FILE, errors, manifest)
|
|
130
|
+
for (section, case_id), _reason in sorted(gaps.items()):
|
|
131
|
+
outcome = sections.get(section, {}).get(case_id)
|
|
132
|
+
if outcome is None:
|
|
133
|
+
errors.append(f"{section}/{case_id}: listed as a known gap but absent from results.json")
|
|
134
|
+
elif outcome.get("status") == "pass":
|
|
135
|
+
errors.append(f"{section}/{case_id}: stale gap (listed in {GAPS_FILE.name} but passes); remove it")
|
|
136
|
+
elif outcome.get("status") == "skip":
|
|
137
|
+
errors.append(f"{section}/{case_id}: a known gap must be reported as fail, not skip")
|
|
138
|
+
|
|
139
|
+
failed = []
|
|
140
|
+
for section, cases in sections.items():
|
|
141
|
+
for case_id, outcome in cases.items():
|
|
142
|
+
if outcome.get("status") == "fail" and (section, case_id) not in gaps:
|
|
143
|
+
failed.append(f"{section}/{case_id}")
|
|
144
|
+
errors.extend(f"{case}: fail (not a known gap)" for case in failed)
|
|
145
|
+
|
|
146
|
+
if errors:
|
|
147
|
+
print("\n".join(errors), file=sys.stderr)
|
|
148
|
+
return 1
|
|
149
|
+
known = sum(
|
|
150
|
+
1
|
|
151
|
+
for section, cases in sections.items()
|
|
152
|
+
for case_id, outcome in cases.items()
|
|
153
|
+
if outcome.get("status") == "fail" and (section, case_id) in gaps
|
|
154
|
+
)
|
|
155
|
+
print(f"results.json complete: bindings={sorted(listed)}, sections={len(sections)}, fail={known} (all known gaps)")
|
|
156
|
+
return 0
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
if __name__ == "__main__":
|
|
160
|
+
sys.exit(main(sys.argv[1:]))
|
|
@@ -284,7 +284,7 @@ class BabelQueue:
|
|
|
284
284
|
# §3.7/§4.7/§5.7/§6.7/§7.7): the raw body goes to the DLQ byte-identical, so there is
|
|
285
285
|
# no envelope to read here.
|
|
286
286
|
gated = received.route_urn.startswith(_UNSUPPORTED_SCHEMA_ROUTE_PREFIX)
|
|
287
|
-
envelope = {} if gated else EnvelopeCodec.
|
|
287
|
+
envelope = {} if gated else EnvelopeCodec.decode_lenient(received.body)
|
|
288
288
|
urn = str(envelope.get("job") or envelope.get("urn") or "")
|
|
289
289
|
if received.route_urn and envelope:
|
|
290
290
|
# Transport-supplied routing override; body untouched. Only for a body that
|
|
@@ -587,7 +587,7 @@ def _without_forbidden_keys(body: str) -> str:
|
|
|
587
587
|
return body
|
|
588
588
|
# ``decode`` (not the raw parse) so the dropped keys — and a top-level ``urn`` alias, folded
|
|
589
589
|
# into ``job`` — are gone before ``encode``, which rejects them (ADR-0031 (c)).
|
|
590
|
-
return EnvelopeCodec.encode(EnvelopeCodec.
|
|
590
|
+
return EnvelopeCodec.encode(EnvelopeCodec.decode_lenient(body))
|
|
591
591
|
|
|
592
592
|
|
|
593
593
|
def _handler_wants_envelope(fn: Handler) -> bool:
|
|
@@ -37,7 +37,7 @@ import re
|
|
|
37
37
|
from typing import Any, Dict, Optional
|
|
38
38
|
from urllib.parse import quote
|
|
39
39
|
|
|
40
|
-
from .codec import SCHEMA_VERSION, EnvelopeCodec, parse_envelope
|
|
40
|
+
from .codec import ASCII_WHITESPACE, SCHEMA_VERSION, EnvelopeCodec, parse_envelope
|
|
41
41
|
from .transport import ReceivedMessage, Transport, truncate_declared_version
|
|
42
42
|
|
|
43
43
|
_logger = logging.getLogger("babelqueue.artemis")
|
|
@@ -45,7 +45,7 @@ _logger = logging.getLogger("babelqueue.artemis")
|
|
|
45
45
|
#: A blank ``bq_schema_version``: empty or only ASCII whitespace (space, \t, \n, \v, \f, \r) — the
|
|
46
46
|
#: same set in every SDK (GR-5). Deliberately not ``str.strip()``, which also strips U+001C-U+001F,
|
|
47
47
|
#: U+0085, U+00A0, U+2028, U+3000 ...
|
|
48
|
-
_BLANK_ASCII_PATTERN = re.compile(
|
|
48
|
+
_BLANK_ASCII_PATTERN = re.compile("[" + re.escape(ASCII_WHITESPACE) + "]*")
|
|
49
49
|
#: Prefix of the reserved ``route_urn`` a version-gated message is delivered under. It is not a
|
|
50
50
|
#: ``urn:babel:`` URN, so no handler can legitimately own it; the runtime dead-letters it
|
|
51
51
|
#: directly (never retried, never decoded; the DLQ record is the incoming raw body). The
|
|
@@ -188,7 +188,7 @@ class ArtemisTransport(Transport):
|
|
|
188
188
|
return body
|
|
189
189
|
if dc <= 0:
|
|
190
190
|
return body
|
|
191
|
-
env = EnvelopeCodec.
|
|
191
|
+
env = EnvelopeCodec.decode_lenient(body)
|
|
192
192
|
if not env or dc <= int(env.get("attempts", 0) or 0):
|
|
193
193
|
return body
|
|
194
194
|
env["attempts"] = dc
|
|
@@ -28,7 +28,7 @@ import re
|
|
|
28
28
|
from typing import Any, Dict, Optional
|
|
29
29
|
from urllib.parse import quote, urlsplit
|
|
30
30
|
|
|
31
|
-
from .codec import SCHEMA_VERSION, EnvelopeCodec, parse_envelope
|
|
31
|
+
from .codec import ASCII_WHITESPACE, SCHEMA_VERSION, EnvelopeCodec, parse_envelope
|
|
32
32
|
from .transport import ReceivedMessage, Transport, truncate_declared_version
|
|
33
33
|
|
|
34
34
|
_logger = logging.getLogger("babelqueue.asb")
|
|
@@ -36,7 +36,7 @@ _logger = logging.getLogger("babelqueue.asb")
|
|
|
36
36
|
#: A blank ``bq-schema-version``: empty or only ASCII whitespace (space, \t, \n, \v, \f, \r) — the
|
|
37
37
|
#: same set in every SDK (GR-5). Deliberately not ``str.strip()``, which also strips U+001C-U+001F,
|
|
38
38
|
#: U+0085, U+00A0, U+2028, U+3000 ...
|
|
39
|
-
_BLANK_ASCII_PATTERN = re.compile(
|
|
39
|
+
_BLANK_ASCII_PATTERN = re.compile("[" + re.escape(ASCII_WHITESPACE) + "]*")
|
|
40
40
|
#: Prefix of the reserved ``route_urn`` a version-gated message is delivered under. It is not a
|
|
41
41
|
#: ``urn:babel:`` URN, so no handler can legitimately own it; the runtime dead-letters it
|
|
42
42
|
#: directly (never retried, never decoded; the DLQ record is the incoming raw body). The
|
|
@@ -149,7 +149,7 @@ class AsbTransport(Transport):
|
|
|
149
149
|
if dc <= 1:
|
|
150
150
|
return body
|
|
151
151
|
native = dc - 1
|
|
152
|
-
env = EnvelopeCodec.
|
|
152
|
+
env = EnvelopeCodec.decode_lenient(body)
|
|
153
153
|
if not env or native <= int(env.get("attempts", 0)):
|
|
154
154
|
return body
|
|
155
155
|
env["attempts"] = native
|