babelqueue 0.3.0__tar.gz → 0.4.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.
Files changed (35) hide show
  1. {babelqueue-0.3.0 → babelqueue-0.4.0}/CHANGELOG.md +14 -1
  2. {babelqueue-0.3.0 → babelqueue-0.4.0}/PKG-INFO +1 -1
  3. {babelqueue-0.3.0 → babelqueue-0.4.0}/pyproject.toml +1 -1
  4. {babelqueue-0.3.0 → babelqueue-0.4.0}/src/babelqueue/__init__.py +1 -1
  5. {babelqueue-0.3.0 → babelqueue-0.4.0}/src/babelqueue/codec.py +33 -0
  6. babelqueue-0.4.0/tests/conformance/fixtures/invalid-missing-urn.json +14 -0
  7. babelqueue-0.4.0/tests/conformance/fixtures/invalid-unknown-schema-version.json +15 -0
  8. babelqueue-0.4.0/tests/conformance/fixtures/unicode-and-numbers.json +20 -0
  9. babelqueue-0.4.0/tests/conformance/fixtures/urn-alias.json +15 -0
  10. babelqueue-0.4.0/tests/conformance/manifest.json +71 -0
  11. babelqueue-0.4.0/tests/conformance/schema/message-envelope.schema.json +110 -0
  12. babelqueue-0.4.0/tests/fixtures/dead-lettered.json +24 -0
  13. babelqueue-0.4.0/tests/fixtures/order-created.json +15 -0
  14. babelqueue-0.4.0/tests/test_conformance.py +58 -0
  15. {babelqueue-0.3.0 → babelqueue-0.4.0}/.github/workflows/ci.yml +0 -0
  16. {babelqueue-0.3.0 → babelqueue-0.4.0}/.github/workflows/release.yml +0 -0
  17. {babelqueue-0.3.0 → babelqueue-0.4.0}/.gitignore +0 -0
  18. {babelqueue-0.3.0 → babelqueue-0.4.0}/LICENSE +0 -0
  19. {babelqueue-0.3.0 → babelqueue-0.4.0}/README.md +0 -0
  20. {babelqueue-0.3.0 → babelqueue-0.4.0}/src/babelqueue/app.py +0 -0
  21. {babelqueue-0.3.0 → babelqueue-0.4.0}/src/babelqueue/contracts.py +0 -0
  22. {babelqueue-0.3.0 → babelqueue-0.4.0}/src/babelqueue/dead_letter.py +0 -0
  23. {babelqueue-0.3.0 → babelqueue-0.4.0}/src/babelqueue/exceptions.py +0 -0
  24. {babelqueue-0.3.0 → babelqueue-0.4.0}/src/babelqueue/pika_transport.py +0 -0
  25. {babelqueue-0.3.0 → babelqueue-0.4.0}/src/babelqueue/py.typed +0 -0
  26. {babelqueue-0.3.0 → babelqueue-0.4.0}/src/babelqueue/redis_transport.py +0 -0
  27. {babelqueue-0.3.0 → babelqueue-0.4.0}/src/babelqueue/routing.py +0 -0
  28. {babelqueue-0.3.0 → babelqueue-0.4.0}/src/babelqueue/transport.py +0 -0
  29. {babelqueue-0.3.0/tests → babelqueue-0.4.0/tests/conformance}/fixtures/dead-lettered.json +0 -0
  30. {babelqueue-0.3.0/tests → babelqueue-0.4.0/tests/conformance}/fixtures/order-created.json +0 -0
  31. {babelqueue-0.3.0 → babelqueue-0.4.0}/tests/test_app.py +0 -0
  32. {babelqueue-0.3.0 → babelqueue-0.4.0}/tests/test_codec.py +0 -0
  33. {babelqueue-0.3.0 → babelqueue-0.4.0}/tests/test_dead_letter.py +0 -0
  34. {babelqueue-0.3.0 → babelqueue-0.4.0}/tests/test_pika_transport.py +0 -0
  35. {babelqueue-0.3.0 → babelqueue-0.4.0}/tests/test_redis_transport.py +0 -0
@@ -9,6 +9,17 @@ The envelope wire format is versioned separately by `meta.schema_version`
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.4.0] - 2026-06-06
13
+
14
+ ### Added
15
+ - `EnvelopeCodec.urn()` — resolve the URN (`job`, accepting `urn` as an alias).
16
+ - `EnvelopeCodec.accepts()` — consumer-side envelope validation (rejects empty URN,
17
+ unsupported `meta.schema_version`, blank `trace_id`, non-object `data`).
18
+ - Shared **cross-SDK conformance suite** under `tests/conformance/` (vendored from
19
+ the canonical `conformance/` set) plus a `test_conformance.py` runner.
20
+
21
+ ## [0.3.0] - 2026-06-06
22
+
12
23
  ### Added
13
24
  - **RabbitMQ transport** (`PikaTransport`, `amqp://`): durable queue, persistent
14
25
  delivery, `basic_get` + manual ack, and the contract AMQP properties (`type`=URN,
@@ -44,6 +55,8 @@ The envelope wire format is versioned separately by `meta.schema_version`
44
55
  - Pre-1.0: the public API may change before the `1.0.0` tag.
45
56
  - The core has **zero runtime dependencies** (standard library only); Python `>=3.9`.
46
57
 
47
- [Unreleased]: https://github.com/BabelQueue/babelqueue-python/compare/v0.2.0...HEAD
58
+ [Unreleased]: https://github.com/BabelQueue/babelqueue-python/compare/v0.4.0...HEAD
59
+ [0.4.0]: https://github.com/BabelQueue/babelqueue-python/compare/v0.3.0...v0.4.0
60
+ [0.3.0]: https://github.com/BabelQueue/babelqueue-python/compare/v0.2.0...v0.3.0
48
61
  [0.2.0]: https://github.com/BabelQueue/babelqueue-python/compare/v0.1.0...v0.2.0
49
62
  [0.1.0]: https://github.com/BabelQueue/babelqueue-python/releases/tag/v0.1.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: babelqueue
3
- Version: 0.3.0
3
+ Version: 0.4.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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "babelqueue"
7
- version = "0.3.0"
7
+ version = "0.4.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"
@@ -19,7 +19,7 @@ from .exceptions import BabelQueueError, UnknownUrnError
19
19
  from .routing import UnknownUrnStrategy
20
20
  from .transport import InMemoryTransport, ReceivedMessage, Transport
21
21
 
22
- __version__ = "0.3.0"
22
+ __version__ = "0.4.0"
23
23
 
24
24
  __all__ = [
25
25
  "BabelQueue",
@@ -94,3 +94,36 @@ class EnvelopeCodec:
94
94
  except (ValueError, TypeError):
95
95
  return {}
96
96
  return decoded if isinstance(decoded, dict) else {}
97
+
98
+ @staticmethod
99
+ def urn(envelope: Mapping[str, Any]) -> str:
100
+ """The message URN: canonical ``job``, with ``urn`` accepted as an alias."""
101
+ return str(envelope.get("job") or envelope.get("urn") or "")
102
+
103
+ @staticmethod
104
+ def accepts(envelope: Mapping[str, Any]) -> bool:
105
+ """Whether a consumer should accept this envelope (consumer-side validation).
106
+
107
+ Rejects messages with no URN, an unsupported ``meta.schema_version``, a
108
+ missing/blank ``trace_id``, or a non-object ``data`` / non-integer
109
+ ``attempts``. (Accepts the ``urn`` alias, unlike the producer JSON Schema.)
110
+ """
111
+ if EnvelopeCodec.urn(envelope) == "":
112
+ return False
113
+
114
+ meta = envelope.get("meta")
115
+ if not isinstance(meta, dict) or meta.get("schema_version") != SCHEMA_VERSION:
116
+ return False
117
+
118
+ if not isinstance(envelope.get("data"), dict):
119
+ return False
120
+
121
+ attempts = envelope.get("attempts")
122
+ if not isinstance(attempts, int) or isinstance(attempts, bool):
123
+ return False
124
+
125
+ trace_id = envelope.get("trace_id")
126
+ if not isinstance(trace_id, str) or trace_id == "":
127
+ return False
128
+
129
+ return True
@@ -0,0 +1,14 @@
1
+ {
2
+ "trace_id": "7b3f9c2a-e41d-4f88-9b2a-1c0d5e6f7a8b",
3
+ "data": {
4
+ "order_id": 1042
5
+ },
6
+ "meta": {
7
+ "id": "f1e2d3c4-b5a6-4789-90ab-cdef01234567",
8
+ "queue": "orders",
9
+ "lang": "php",
10
+ "schema_version": 1,
11
+ "created_at": 1749132727000
12
+ },
13
+ "attempts": 0
14
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "job": "urn:babel:orders:created",
3
+ "trace_id": "7b3f9c2a-e41d-4f88-9b2a-1c0d5e6f7a8b",
4
+ "data": {
5
+ "order_id": 1042
6
+ },
7
+ "meta": {
8
+ "id": "f1e2d3c4-b5a6-4789-90ab-cdef01234567",
9
+ "queue": "orders",
10
+ "lang": "php",
11
+ "schema_version": 2,
12
+ "created_at": 1749132727000
13
+ },
14
+ "attempts": 0
15
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "job": "urn:babel:catalog:item.indexed",
3
+ "trace_id": "3f7a1d2e-9b4c-4a8d-bc1e-0f5a6b7c8d90",
4
+ "data": {
5
+ "title": "Café — naïve ☕",
6
+ "qty": 7,
7
+ "price_cents": 1299,
8
+ "ratio": 0.5,
9
+ "active": true,
10
+ "note": null
11
+ },
12
+ "meta": {
13
+ "id": "b2c3d4e5-f607-4890-a1b2-c3d4e5f60718",
14
+ "queue": "catalog",
15
+ "lang": "python",
16
+ "schema_version": 1,
17
+ "created_at": 1749132727000
18
+ },
19
+ "attempts": 2
20
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "urn": "urn:babel:orders:created",
3
+ "trace_id": "9c1e0b44-7a2d-4e6f-8a10-2b3c4d5e6f70",
4
+ "data": {
5
+ "order_id": 1042
6
+ },
7
+ "meta": {
8
+ "id": "a1b2c3d4-e5f6-4789-90ab-cdef01234567",
9
+ "queue": "orders",
10
+ "lang": "go",
11
+ "schema_version": 1,
12
+ "created_at": 1749132727000
13
+ },
14
+ "attempts": 0
15
+ }
@@ -0,0 +1,71 @@
1
+ {
2
+ "schema_version": 1,
3
+ "description": "Cross-SDK conformance cases. Every BabelQueue SDK core must satisfy these against the canonical wire envelope. Per-message fields (meta.id, trace_id, meta.created_at) are intrinsically unique and are NOT asserted by value.",
4
+ "cases": [
5
+ {
6
+ "name": "order-created",
7
+ "file": "fixtures/order-created.json",
8
+ "valid": true,
9
+ "description": "A normal produced envelope.",
10
+ "expect": {
11
+ "urn": "urn:babel:orders:created",
12
+ "data": { "order_id": 1042 },
13
+ "attempts": 0,
14
+ "lang": "php",
15
+ "schema_version": 1
16
+ }
17
+ },
18
+ {
19
+ "name": "urn-alias",
20
+ "file": "fixtures/urn-alias.json",
21
+ "valid": true,
22
+ "description": "Consumers MUST accept 'urn' as an inbound alias for 'job'.",
23
+ "expect": {
24
+ "urn": "urn:babel:orders:created",
25
+ "data": { "order_id": 1042 },
26
+ "attempts": 0,
27
+ "lang": "go",
28
+ "schema_version": 1
29
+ }
30
+ },
31
+ {
32
+ "name": "dead-lettered",
33
+ "file": "fixtures/dead-lettered.json",
34
+ "valid": true,
35
+ "description": "A dead-lettered message: original preserved + additive dead_letter block.",
36
+ "expect": {
37
+ "urn": "urn:babel:orders:created",
38
+ "data": { "order_id": 1042 },
39
+ "attempts": 3,
40
+ "lang": "php",
41
+ "schema_version": 1,
42
+ "dead_letter": { "reason": "failed", "original_queue": "orders" }
43
+ }
44
+ },
45
+ {
46
+ "name": "unicode-and-numbers",
47
+ "file": "fixtures/unicode-and-numbers.json",
48
+ "valid": true,
49
+ "description": "UTF-8 strings, integers, an exact float, boolean and null round-trip identically.",
50
+ "expect": {
51
+ "urn": "urn:babel:catalog:item.indexed",
52
+ "data": { "title": "Café — naïve ☕", "qty": 7, "price_cents": 1299, "ratio": 0.5, "active": true, "note": null },
53
+ "attempts": 2,
54
+ "lang": "python",
55
+ "schema_version": 1
56
+ }
57
+ },
58
+ {
59
+ "name": "invalid-unknown-schema-version",
60
+ "file": "fixtures/invalid-unknown-schema-version.json",
61
+ "valid": false,
62
+ "reason": "meta.schema_version is not a version this SDK supports"
63
+ },
64
+ {
65
+ "name": "invalid-missing-urn",
66
+ "file": "fixtures/invalid-missing-urn.json",
67
+ "valid": false,
68
+ "reason": "no 'job' or 'urn' — the message has no identity"
69
+ }
70
+ ]
71
+ }
@@ -0,0 +1,110 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://babelqueue.com/contracts/message-envelope.schema.json",
4
+ "title": "BabelQueueMessageEnvelope",
5
+ "description": "The canonical, language-agnostic BabelQueue wire envelope (schema_version 1). This schema is authoritative alongside contracts/message-envelope.md.",
6
+ "type": "object",
7
+ "additionalProperties": true,
8
+ "required": ["job", "trace_id", "data", "meta", "attempts"],
9
+ "properties": {
10
+ "job": {
11
+ "type": "string",
12
+ "minLength": 1,
13
+ "description": "The message URN — language-agnostic identity. Canonical producer field name. Consumers also accept 'urn' as an inbound alias. Never a class name.",
14
+ "examples": ["urn:babel:orders:created", "urn:babel:orders:invoice.requested"]
15
+ },
16
+ "urn": {
17
+ "type": "string",
18
+ "minLength": 1,
19
+ "description": "Inbound alias for 'job'. Accepted by consumers for interoperability; producers SHOULD emit 'job'."
20
+ },
21
+ "trace_id": {
22
+ "type": "string",
23
+ "format": "uuid",
24
+ "description": "Cross-service correlation id. Generated by the first producer, preserved and forwarded unchanged by every SDK across every hop."
25
+ },
26
+ "data": {
27
+ "type": "object",
28
+ "description": "Pure, JSON-encodable business payload. No language-specific types. Numbers/time/binary per contracts/message-envelope.md section 6.",
29
+ "additionalProperties": true
30
+ },
31
+ "meta": {
32
+ "type": "object",
33
+ "description": "Producer-set, immutable descriptive metadata.",
34
+ "additionalProperties": true,
35
+ "required": ["id", "queue", "lang", "schema_version", "created_at"],
36
+ "properties": {
37
+ "id": {
38
+ "type": "string",
39
+ "format": "uuid",
40
+ "description": "Unique id for THIS message (distinct from trace_id)."
41
+ },
42
+ "queue": {
43
+ "type": "string",
44
+ "minLength": 1,
45
+ "description": "Logical queue name (not the broker key)."
46
+ },
47
+ "lang": {
48
+ "type": "string",
49
+ "enum": ["php", "go", "python", "java", "dotnet", "node"],
50
+ "description": "Producer language tag."
51
+ },
52
+ "schema_version": {
53
+ "type": "integer",
54
+ "const": 1,
55
+ "description": "Envelope schema version. Consumers MUST reject versions they do not support."
56
+ },
57
+ "created_at": {
58
+ "type": "integer",
59
+ "minimum": 0,
60
+ "description": "Production time as Unix epoch MILLISECONDS, UTC."
61
+ }
62
+ }
63
+ },
64
+ "attempts": {
65
+ "type": "integer",
66
+ "minimum": 0,
67
+ "description": "Top-level transport retry counter, mutated by the broker/worker. Deliberately kept OUT of the immutable meta block."
68
+ },
69
+ "dead_letter": {
70
+ "type": "object",
71
+ "description": "Optional. Present ONLY on messages sitting on a dead-letter queue (ADR-0009). Additive, so it does not change schema_version. Normal consumers ignore it.",
72
+ "additionalProperties": true,
73
+ "required": ["reason", "failed_at", "original_queue", "attempts"],
74
+ "properties": {
75
+ "reason": {
76
+ "type": "string",
77
+ "enum": ["failed", "unknown_urn", "poison"],
78
+ "description": "Why the message was dead-lettered."
79
+ },
80
+ "error": {
81
+ "type": ["string", "null"],
82
+ "description": "Human-readable error message, if any."
83
+ },
84
+ "exception": {
85
+ "type": ["string", "null"],
86
+ "description": "Producer-language exception/type name, if any (informational, language-specific)."
87
+ },
88
+ "failed_at": {
89
+ "type": "integer",
90
+ "minimum": 0,
91
+ "description": "Dead-letter time as Unix epoch milliseconds, UTC."
92
+ },
93
+ "original_queue": {
94
+ "type": "string",
95
+ "description": "The queue the message was consumed from before dead-lettering."
96
+ },
97
+ "attempts": {
98
+ "type": "integer",
99
+ "minimum": 0,
100
+ "description": "Delivery attempts made before dead-lettering."
101
+ },
102
+ "lang": {
103
+ "type": "string",
104
+ "enum": ["php", "go", "python", "java", "dotnet", "node"],
105
+ "description": "Language of the SDK that dead-lettered the message."
106
+ }
107
+ }
108
+ }
109
+ }
110
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "job": "urn:babel:orders:created",
3
+ "trace_id": "7b3f9c2a-e41d-4f88-9b2a-1c0d5e6f7a8b",
4
+ "data": {
5
+ "order_id": 1042
6
+ },
7
+ "meta": {
8
+ "id": "f1e2d3c4-b5a6-4789-90ab-cdef01234567",
9
+ "queue": "orders",
10
+ "lang": "php",
11
+ "schema_version": 1,
12
+ "created_at": 1749132727000
13
+ },
14
+ "attempts": 3,
15
+ "dead_letter": {
16
+ "reason": "failed",
17
+ "error": "Payment gateway timeout",
18
+ "exception": "App\\Exceptions\\GatewayTimeout",
19
+ "failed_at": 1749132730000,
20
+ "original_queue": "orders",
21
+ "attempts": 3,
22
+ "lang": "php"
23
+ }
24
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "job": "urn:babel:orders:created",
3
+ "trace_id": "7b3f9c2a-e41d-4f88-9b2a-1c0d5e6f7a8b",
4
+ "data": {
5
+ "order_id": 1042
6
+ },
7
+ "meta": {
8
+ "id": "f1e2d3c4-b5a6-4789-90ab-cdef01234567",
9
+ "queue": "orders",
10
+ "lang": "php",
11
+ "schema_version": 1,
12
+ "created_at": 1749132727000
13
+ },
14
+ "attempts": 0
15
+ }
@@ -0,0 +1,58 @@
1
+ """Runs the shared cross-SDK conformance suite (vendored under tests/conformance/).
2
+
3
+ The same manifest + fixtures are run by every BabelQueue SDK; passing here proves
4
+ this SDK reads/writes the canonical envelope identically to the others.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import unittest
11
+ from pathlib import Path
12
+
13
+ from babelqueue import EnvelopeCodec
14
+
15
+ SUITE = Path(__file__).parent / "conformance"
16
+ MANIFEST = json.loads((SUITE / "manifest.json").read_text(encoding="utf-8"))
17
+
18
+
19
+ class ConformanceTest(unittest.TestCase):
20
+ def test_suite_is_present(self) -> None:
21
+ self.assertEqual(MANIFEST["schema_version"], 1)
22
+ self.assertGreaterEqual(len(MANIFEST["cases"]), 6)
23
+
24
+ def test_cases(self) -> None:
25
+ for case in MANIFEST["cases"]:
26
+ with self.subTest(case=case["name"]):
27
+ raw = (SUITE / case["file"]).read_text(encoding="utf-8")
28
+ env = EnvelopeCodec.decode(raw)
29
+ self.assertNotEqual(env, {}, "fixture must decode")
30
+
31
+ if not case["valid"]:
32
+ self.assertFalse(
33
+ EnvelopeCodec.accepts(env),
34
+ f"{case['name']} must be rejected: {case.get('reason')}",
35
+ )
36
+ continue
37
+
38
+ expect = case["expect"]
39
+ self.assertTrue(EnvelopeCodec.accepts(env), f"{case['name']} must be accepted")
40
+ self.assertEqual(EnvelopeCodec.urn(env), expect["urn"])
41
+ self.assertEqual(env["attempts"], expect["attempts"])
42
+ self.assertEqual(env["meta"]["lang"], expect["lang"])
43
+ self.assertEqual(env["meta"]["schema_version"], expect["schema_version"])
44
+
45
+ if "data" in expect:
46
+ self.assertEqual(env["data"], expect["data"])
47
+
48
+ if "dead_letter" in expect:
49
+ for key, value in expect["dead_letter"].items():
50
+ self.assertEqual(env["dead_letter"][key], value)
51
+
52
+ # Per-message fields must be present (not asserted by value).
53
+ self.assertIn("id", env["meta"])
54
+ self.assertTrue(env["trace_id"])
55
+
56
+
57
+ if __name__ == "__main__":
58
+ unittest.main()
File without changes
File without changes
File without changes
File without changes