babelqueue 1.19.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.19.0 → babelqueue-1.21.0}/.github/workflows/ci.yml +35 -8
- babelqueue-1.21.0/.github/workflows/release.yml +66 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/CHANGELOG.md +57 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/PKG-INFO +26 -1
- {babelqueue-1.19.0 → babelqueue-1.21.0}/README.md +25 -0
- babelqueue-1.21.0/conformance-known-gaps.json +84 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/pyproject.toml +1 -1
- babelqueue-1.21.0/scripts/check_conformance_results.py +160 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/__init__.py +1 -1
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/_envelope_shape.py +3 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/app.py +10 -6
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/artemis_transport.py +3 -3
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/asb_transport.py +3 -3
- babelqueue-1.21.0/src/babelqueue/codec.py +341 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/exceptions.py +15 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/kafka_transport.py +3 -3
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/pulsar_transport.py +13 -8
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/redrive.py +2 -2
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/sqs_transport.py +7 -4
- babelqueue-1.21.0/tests/conformance/CONFORMANCE_VERSION +1 -0
- babelqueue-1.21.0/tests/conformance/fixtures/blank-ascii-job-urn-fallback.json +16 -0
- babelqueue-1.21.0/tests/conformance/fixtures/dead-letter-roundtrip.json +23 -0
- babelqueue-1.21.0/tests/conformance/fixtures/forbidden-key-job-and-urn.json +16 -0
- babelqueue-1.21.0/tests/conformance/fixtures/forbidden-key-job-nonascii-space-and-urn.json +16 -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/meta-ext-vendor-roundtrip.json +27 -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.21.0/tests/conformance/fixtures/urn-only-roundtrip.json +15 -0
- babelqueue-1.21.0/tests/conformance/manifest.json +1863 -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.19.0 → babelqueue-1.21.0}/tests/test_codec.py +5 -7
- babelqueue-1.21.0/tests/test_conformance.py +295 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/test_kafka_transport.py +1 -1
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/test_pulsar_transport.py +16 -2
- babelqueue-1.21.0/tests/test_reencode_regression.py +186 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/test_schema_conformance.py +4 -2
- babelqueue-1.19.0/.github/workflows/release.yml +0 -46
- babelqueue-1.19.0/src/babelqueue/codec.py +0 -214
- babelqueue-1.19.0/tests/conformance/CONFORMANCE_VERSION +0 -1
- babelqueue-1.19.0/tests/conformance/manifest.json +0 -675
- babelqueue-1.19.0/tests/test_behaviour_conformance.py +0 -195
- babelqueue-1.19.0/tests/test_conformance.py +0 -58
- {babelqueue-1.19.0 → babelqueue-1.21.0}/.github/FUNDING.yml +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/.github/dependabot.yml +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/.gitignore +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/LICENSE +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/celery.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/contracts.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/dead_letter.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/django/__init__.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/django/apps.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/django/management/__init__.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/django/management/commands/__init__.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/django/management/commands/babelqueue_worker.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/gdpr.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/headers.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/idempotency.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/otel.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/outbox.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/pika_transport.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/py.typed +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/redis_transport.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/replay.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/routing.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/schema.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/src/babelqueue/transport.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/conformance/fixtures/data-array-rejected.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/conformance/fixtures/dead-lettered.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/conformance/fixtures/duplicate-keys-last-wins.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/conformance/fixtures/empty-data-roundtrip.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/conformance/fixtures/forbidden-key-meta-attempts.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/conformance/fixtures/forbidden-key-meta-max-retries.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/conformance/fixtures/forbidden-key-meta-source.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/conformance/fixtures/forbidden-key-meta-ts.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/conformance/fixtures/forbidden-key-timestamp.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/conformance/fixtures/invalid-missing-urn.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/conformance/fixtures/invalid-unknown-schema-version.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/conformance/fixtures/invalid-wrong-case-job.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/conformance/fixtures/order-created.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/conformance/fixtures/unicode-and-numbers.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/conformance/fixtures/unknown-lang.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/conformance/fixtures/unknown-meta-roundtrip.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/conformance/fixtures/unknown-toplevel-roundtrip.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/conformance/fixtures/urn-alias.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/conformance/fixtures/wrong-case-keys-roundtrip.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/conformance/schema/message-envelope.schema.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/fakes/confluent_kafka/__init__.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/fakes/confluent_kafka/admin.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/fixtures/dead-lettered.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/fixtures/order-created.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/poison-trigger-set.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/test_app.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/test_artemis_conformance.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/test_artemis_transport.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/test_asb_conformance.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/test_asb_transport.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/test_celery.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/test_dead_letter.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/test_django.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/test_gdpr.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/test_headers.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/test_idempotency.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/test_kafka_conformance.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/test_kafka_integration.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/test_kafka_integration_logic.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/test_otel.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/test_outbox.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/test_overhead.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/test_pika_transport.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/test_pulsar_conformance.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/test_redis_transport.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/test_redrive.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/test_replay.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/test_schema.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/test_shutdown.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.21.0}/tests/test_sqs_conformance.py +0 -0
- {babelqueue-1.19.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,63 @@ 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
|
+
|
|
34
|
+
## [1.20.0] - 2026-10-17
|
|
35
|
+
|
|
36
|
+
Conformance suite 1.4.0, [ADR-0031](https://babelqueue.com). The envelope is unchanged
|
|
37
|
+
(`schema_version: 1`); this is a MINOR with one deliberate behaviour change (encode rejects).
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
- `EnvelopeCodec.make(urn, data, *, queue, trace_id, meta=None)`: a producer `meta` write API. The entries
|
|
41
|
+
land in `meta` next to the core keys (recommended shape `meta.ext.<vendor>`). A collision with a core
|
|
42
|
+
meta key or a forbidden key raises the new `EnvelopeKeyError` (subclass of `BabelQueueError`, with a
|
|
43
|
+
`pointer` attribute). `_envelope_shape.CORE_META_KEYS` is the single copy of the core key list.
|
|
44
|
+
- `decode` folds the `urn` alias into `job` (ADR-0031 (c)): a `job` that is missing, empty or only ASCII
|
|
45
|
+
whitespace (`" \t\n\v\f\r"`; a U+2003 `job` is NOT blank) falls back to `urn` silently; when both carry
|
|
46
|
+
a value, `job` wins and `urn` is dropped with a warning naming `/urn`. `accepts()` / `urn()` follow the
|
|
47
|
+
same rule. The decoded envelope never keeps a `urn` key.
|
|
48
|
+
- Tests: the runner now implements the 1.4.0 fields (`expect_preserved` on `forbidden_keys`;
|
|
49
|
+
`expect_absent_after_reencode`, `apply_dead_letter`, `expect_dead_letter` on `roundtrip`, the latter via
|
|
50
|
+
`dead_letter.annotate`) plus the `extension_write` and `encode_reject` sections, with a runner
|
|
51
|
+
self-test proving violations are reported.
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
- **Behaviour change: `EnvelopeCodec.encode` raises `EnvelopeKeyError`** for an envelope carrying a
|
|
55
|
+
forbidden key (message-envelope.md section 10) or a top-level `urn`, instead of dropping it with a
|
|
56
|
+
warning (policy K-15 (a)). Nothing is written. `decode` still warns and drops, so re-encoding a decoded
|
|
57
|
+
envelope is safe. Code that built an envelope from a raw legacy body and relied on encode to clean it
|
|
58
|
+
must decode it first (`EnvelopeCodec.encode(EnvelopeCodec.decode(raw))`).
|
|
59
|
+
- `test_all_five_contract_keys_are_covered` is a subset check (the suite adds `/urn` cases).
|
|
60
|
+
- Docs only: the note in the previous release that the Go reference's case-insensitive envelope-key read "is tightened separately" is now closed — Go core 1.14.0 reads envelope keys exactly (case-sensitive, last duplicate wins), so every SDK now decides poison on the same exact-case keys.
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
- Raw-body re-encode paths now decode before they encode, so the new `encode` rejection cannot strand a
|
|
64
|
+
legacy body: the runtime's raw-body RELEASE re-publish (`app.py`), and the SQS and Pulsar attempts
|
|
65
|
+
reconciliation (Kafka, Azure Service Bus and Artemis already decoded first). A body with a legacy
|
|
66
|
+
`timestamp` or only a `urn` alias is re-emitted normalised (forbidden key dropped and warned, `urn`
|
|
67
|
+
folded into `job`); unknown top-level and `meta` keys, including `meta.ext`, are preserved.
|
|
68
|
+
|
|
12
69
|
## [1.19.0] - 2026-10-12
|
|
13
70
|
|
|
14
71
|
### Changed
|
|
@@ -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, …)
|
|
@@ -106,6 +108,29 @@ The envelope is identical to every other SDK's:
|
|
|
106
108
|
}
|
|
107
109
|
```
|
|
108
110
|
|
|
111
|
+
### Extension metadata, forbidden keys and the `urn` alias (1.20.0, ADR-0031)
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
# Producer meta: extra keys next to the core meta keys (recommended shape: meta.ext.<vendor>).
|
|
115
|
+
envelope = EnvelopeCodec.make(
|
|
116
|
+
"urn:babel:orders:created", {"order_id": 1042},
|
|
117
|
+
queue="orders", meta={"ext": {"acme": {"tier": "gold"}}},
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
- `meta=` is keyword-only. A key that collides with a core meta key (`id`, `queue`, `lang`,
|
|
122
|
+
`schema_version`, `created_at`) or with a forbidden key (`max_retries`, `attempts`, `source`, `ts`)
|
|
123
|
+
raises `EnvelopeKeyError` (a `BabelQueueError`; its `pointer` names the key, e.g. `/meta/ts`).
|
|
124
|
+
- Unknown top-level and `meta` keys of an inbound message survive a decode -> `attempts + 1` -> encode
|
|
125
|
+
hop (retry, redrive, dead-letter), so a vendor `meta.ext` block is never lost.
|
|
126
|
+
- `EnvelopeCodec.encode` **rejects** an envelope that carries a forbidden key (top-level `timestamp`;
|
|
127
|
+
`meta.max_retries`, `meta.attempts`, `meta.source`, `meta.ts`) or the inbound-only top-level `urn`
|
|
128
|
+
alias: it raises `EnvelopeKeyError` instead of dropping the key with a warning. `decode` is unchanged
|
|
129
|
+
(it still warns and drops), so a decoded envelope can always be re-encoded.
|
|
130
|
+
- `decode` folds `urn` into `job`: a `job` that is missing, empty or only ASCII whitespace (space, `\t`,
|
|
131
|
+
`\n`, `\v`, `\f`, `\r`) falls back to `urn`; when both carry a value, `job` wins and `urn` is dropped
|
|
132
|
+
with a warning naming `/urn`. The decoded envelope never keeps a `urn` key.
|
|
133
|
+
|
|
109
134
|
### Typed messages (optional)
|
|
110
135
|
|
|
111
136
|
```python
|
|
@@ -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, …)
|
|
@@ -52,6 +54,29 @@ The envelope is identical to every other SDK's:
|
|
|
52
54
|
}
|
|
53
55
|
```
|
|
54
56
|
|
|
57
|
+
### Extension metadata, forbidden keys and the `urn` alias (1.20.0, ADR-0031)
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
# Producer meta: extra keys next to the core meta keys (recommended shape: meta.ext.<vendor>).
|
|
61
|
+
envelope = EnvelopeCodec.make(
|
|
62
|
+
"urn:babel:orders:created", {"order_id": 1042},
|
|
63
|
+
queue="orders", meta={"ext": {"acme": {"tier": "gold"}}},
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- `meta=` is keyword-only. A key that collides with a core meta key (`id`, `queue`, `lang`,
|
|
68
|
+
`schema_version`, `created_at`) or with a forbidden key (`max_retries`, `attempts`, `source`, `ts`)
|
|
69
|
+
raises `EnvelopeKeyError` (a `BabelQueueError`; its `pointer` names the key, e.g. `/meta/ts`).
|
|
70
|
+
- Unknown top-level and `meta` keys of an inbound message survive a decode -> `attempts + 1` -> encode
|
|
71
|
+
hop (retry, redrive, dead-letter), so a vendor `meta.ext` block is never lost.
|
|
72
|
+
- `EnvelopeCodec.encode` **rejects** an envelope that carries a forbidden key (top-level `timestamp`;
|
|
73
|
+
`meta.max_retries`, `meta.attempts`, `meta.source`, `meta.ts`) or the inbound-only top-level `urn`
|
|
74
|
+
alias: it raises `EnvelopeKeyError` instead of dropping the key with a warning. `decode` is unchanged
|
|
75
|
+
(it still warns and drops), so a decoded envelope can always be re-encoded.
|
|
76
|
+
- `decode` folds `urn` into `job`: a `job` that is missing, empty or only ASCII whitespace (space, `\t`,
|
|
77
|
+
`\n`, `\v`, `\f`, `\r`) falls back to `urn`; when both carry a value, `job` wins and `urn` is dropped
|
|
78
|
+
with a warning naming `/urn`. The decoded envelope never keeps a `urn` key.
|
|
79
|
+
|
|
55
80
|
### Typed messages (optional)
|
|
56
81
|
|
|
57
82
|
```python
|
|
@@ -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:]))
|
|
@@ -51,6 +51,9 @@ _META = {
|
|
|
51
51
|
"schema_version": _INTEGER,
|
|
52
52
|
"created_at": _INTEGER,
|
|
53
53
|
}
|
|
54
|
+
#: The core ``meta`` keys of message-envelope.md section 4 (the single copy; the producer ``meta``
|
|
55
|
+
#: write API of the codec rejects a collision with any of them, ADR-0031 (b)).
|
|
56
|
+
CORE_META_KEYS = frozenset(_META)
|
|
54
57
|
_DEAD_LETTER = {
|
|
55
58
|
"reason": _STRING,
|
|
56
59
|
"error": _STRING,
|
|
@@ -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
|
|
@@ -458,8 +458,9 @@ class BabelQueue:
|
|
|
458
458
|
self._stop.wait(self._rewind_pause)
|
|
459
459
|
|
|
460
460
|
def _release_unprocessed(self, received: ReceivedMessage) -> None:
|
|
461
|
-
"""Release a message whose handler never completed (shutdown): unchanged body
|
|
462
|
-
|
|
461
|
+
"""Release a message whose handler never completed (shutdown): unchanged body
|
|
462
|
+
(normalised only if it carries a forbidden key or a ``urn`` alias), no backoff, and the
|
|
463
|
+
runtime adds no ``attempts`` increment. A broker that counts
|
|
463
464
|
deliveries still counts it: on SQS the release bumps ``ApproximateReceiveCount``, so the
|
|
464
465
|
redelivered envelope reads one more attempt (and a ``RedrivePolicy`` sees it too)."""
|
|
465
466
|
self._release(received, received.body, 0.0)
|
|
@@ -578,12 +579,15 @@ _UNSUPPORTED_SCHEMA_ROUTE_PREFIX = "babelqueue:unsupported-schema-version:"
|
|
|
578
579
|
|
|
579
580
|
def _without_forbidden_keys(body: str) -> str:
|
|
580
581
|
"""``body`` unchanged, unless it carries a forbidden envelope key (message-envelope.md §10,
|
|
581
|
-
e.g. from a legacy producer) — then it is re-encoded without it (warned by the codec), so a
|
|
582
|
+
e.g. from a legacy producer) or a ``urn`` alias — then it is re-encoded without it (warned by the codec), so a
|
|
582
583
|
raw-body re-publish never re-emits a key the decode boundary reported as dropped."""
|
|
583
584
|
parsed = parse_envelope(body)
|
|
584
|
-
|
|
585
|
+
# ``urn`` is not a §10 key but ``encode`` rejects it and ADR-0031 (a)/(c) forbid re-emitting it.
|
|
586
|
+
if not parsed or not (has_forbidden_keys(parsed) or "urn" in parsed):
|
|
585
587
|
return body
|
|
586
|
-
|
|
588
|
+
# ``decode`` (not the raw parse) so the dropped keys — and a top-level ``urn`` alias, folded
|
|
589
|
+
# into ``job`` — are gone before ``encode``, which rejects them (ADR-0031 (c)).
|
|
590
|
+
return EnvelopeCodec.encode(EnvelopeCodec.decode_lenient(body))
|
|
587
591
|
|
|
588
592
|
|
|
589
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
|