babelqueue 0.1.0__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,33 @@
1
+ """BabelQueue — Polyglot Queues, Simplified.
2
+
3
+ The framework-agnostic Python core: the canonical wire-envelope codec, contracts,
4
+ and dead-letter helpers. Framework adapters (Celery, Django, ...) build on this.
5
+
6
+ from babelqueue import EnvelopeCodec
7
+
8
+ payload = EnvelopeCodec.make("urn:babel:orders:created", {"order_id": 1042})
9
+ body = EnvelopeCodec.encode(payload) # send `body` over Redis/RabbitMQ
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from . import dead_letter
15
+ from .codec import SCHEMA_VERSION, SOURCE_LANG, EnvelopeCodec
16
+ from .contracts import HasTraceId, PolyglotMessage
17
+ from .exceptions import BabelQueueError, UnknownUrnError
18
+ from .routing import UnknownUrnStrategy
19
+
20
+ __version__ = "0.1.0"
21
+
22
+ __all__ = [
23
+ "EnvelopeCodec",
24
+ "SCHEMA_VERSION",
25
+ "SOURCE_LANG",
26
+ "PolyglotMessage",
27
+ "HasTraceId",
28
+ "UnknownUrnStrategy",
29
+ "BabelQueueError",
30
+ "UnknownUrnError",
31
+ "dead_letter",
32
+ "__version__",
33
+ ]
babelqueue/codec.py ADDED
@@ -0,0 +1,96 @@
1
+ """The canonical BabelQueue wire envelope — the single Python implementation.
2
+
3
+ The shape is frozen as ``{job, trace_id, data, meta, attempts}`` (schema_version 1)
4
+ so a Python service interoperates byte-for-byte with the PHP/Laravel, Go, ... SDKs.
5
+ The ``job`` field carries the message URN (never a class name); ``trace_id`` is a
6
+ cross-service correlation id preserved across every hop. Pure stdlib — no deps.
7
+
8
+ Full spec: https://babelqueue.com
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import time
15
+ import uuid
16
+ from typing import Any, Dict, Mapping, Optional
17
+
18
+ from .exceptions import BabelQueueError
19
+
20
+ SCHEMA_VERSION = 1
21
+ SOURCE_LANG = "python"
22
+
23
+
24
+ class EnvelopeCodec:
25
+ """Builds, encodes and decodes the canonical envelope."""
26
+
27
+ SCHEMA_VERSION = SCHEMA_VERSION
28
+ SOURCE_LANG = SOURCE_LANG
29
+
30
+ @staticmethod
31
+ def make(
32
+ urn: str,
33
+ data: Mapping[str, Any],
34
+ *,
35
+ queue: str = "default",
36
+ trace_id: Optional[str] = None,
37
+ ) -> Dict[str, Any]:
38
+ """Build the canonical envelope for a ``(urn, data)`` pair.
39
+
40
+ ``trace_id`` is reused when given (trace continuation), otherwise a fresh
41
+ UUID is minted. ``attempts`` starts at 0 and is the top-level transport
42
+ counter (kept out of the immutable ``meta`` block).
43
+ """
44
+ urn = (urn or "").strip()
45
+ if not urn:
46
+ raise BabelQueueError(
47
+ "A polyglot message must expose a stable, non-empty URN so consumers "
48
+ "can identify it without any class name."
49
+ )
50
+
51
+ trace_id = (trace_id or "").strip() or str(uuid.uuid4())
52
+
53
+ return {
54
+ "job": urn,
55
+ "trace_id": trace_id,
56
+ "data": dict(data),
57
+ "meta": {
58
+ "id": str(uuid.uuid4()),
59
+ "queue": queue,
60
+ "lang": SOURCE_LANG,
61
+ "schema_version": SCHEMA_VERSION,
62
+ "created_at": int(time.time() * 1000),
63
+ },
64
+ "attempts": 0,
65
+ }
66
+
67
+ @staticmethod
68
+ def from_message(message: Any, queue: str = "default") -> Dict[str, Any]:
69
+ """Build the envelope from a message object (see :class:`PolyglotMessage`).
70
+
71
+ If the message also exposes ``get_babel_trace_id()`` (see
72
+ :class:`HasTraceId`) and returns a non-empty value, that trace id is reused.
73
+ """
74
+ get_trace = getattr(message, "get_babel_trace_id", None)
75
+ trace_id = get_trace() if callable(get_trace) else None
76
+
77
+ return EnvelopeCodec.make(
78
+ message.get_babel_urn(),
79
+ message.to_payload(),
80
+ queue=queue,
81
+ trace_id=trace_id,
82
+ )
83
+
84
+ @staticmethod
85
+ def encode(envelope: Mapping[str, Any]) -> str:
86
+ """Encode the envelope as compact UTF-8 JSON (unescaped unicode/slashes)."""
87
+ return json.dumps(envelope, ensure_ascii=False, separators=(",", ":"))
88
+
89
+ @staticmethod
90
+ def decode(raw: str) -> Dict[str, Any]:
91
+ """Decode a raw JSON body; returns ``{}`` for malformed/non-object input."""
92
+ try:
93
+ decoded = json.loads(raw)
94
+ except (ValueError, TypeError):
95
+ return {}
96
+ return decoded if isinstance(decoded, dict) else {}
@@ -0,0 +1,31 @@
1
+ """Typed, duck-typing-friendly contracts for polyglot messages.
2
+
3
+ These mirror the other SDKs' contracts so a typed message class can be checked,
4
+ but the codec also accepts plain ``(urn, data)`` — Python does not require a class.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, Mapping, Optional, Protocol, runtime_checkable
10
+
11
+
12
+ @runtime_checkable
13
+ class PolyglotMessage(Protocol):
14
+ """A producible message: a stable URN plus a pure, JSON-serialisable payload."""
15
+
16
+ def get_babel_urn(self) -> str:
17
+ """The message URN (e.g. ``urn:babel:orders:created``), never a class name."""
18
+ ...
19
+
20
+ def to_payload(self) -> Mapping[str, Any]:
21
+ """The pure, JSON-serialisable payload carried under the envelope ``data``."""
22
+ ...
23
+
24
+
25
+ @runtime_checkable
26
+ class HasTraceId(Protocol):
27
+ """Optional: lets a message continue an existing distributed trace."""
28
+
29
+ def get_babel_trace_id(self) -> Optional[str]:
30
+ """An inherited trace id to reuse, or ``None`` to mint a new one."""
31
+ ...
@@ -0,0 +1,39 @@
1
+ """Builds the additive ``dead_letter`` block attached when a message is
2
+ dead-lettered. Pure: returns an annotated copy; the original identity
3
+ (trace_id, meta.id, data) is preserved. Because the field is additive and
4
+ optional, the envelope stays at schema_version 1. See https://babelqueue.com.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import time
10
+ from typing import Any, Dict, Mapping, Optional
11
+
12
+ from .codec import SOURCE_LANG
13
+
14
+
15
+ def annotate(
16
+ envelope: Mapping[str, Any],
17
+ reason: str,
18
+ original_queue: str,
19
+ attempts: int,
20
+ *,
21
+ error: Optional[str] = None,
22
+ exception: Optional[str] = None,
23
+ lang: str = SOURCE_LANG,
24
+ ) -> Dict[str, Any]:
25
+ """Return a copy of ``envelope`` with a ``dead_letter`` block.
26
+
27
+ ``reason`` is one of ``failed`` | ``unknown_urn`` | ``poison``.
28
+ """
29
+ result = dict(envelope)
30
+ result["dead_letter"] = {
31
+ "reason": reason,
32
+ "error": error,
33
+ "exception": exception,
34
+ "failed_at": int(time.time() * 1000),
35
+ "original_queue": original_queue,
36
+ "attempts": attempts,
37
+ "lang": lang,
38
+ }
39
+ return result
@@ -0,0 +1,11 @@
1
+ """Exception hierarchy for BabelQueue."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class BabelQueueError(Exception):
7
+ """Base for every recoverable BabelQueue error (bad config, empty URN, ...)."""
8
+
9
+
10
+ class UnknownUrnError(BabelQueueError):
11
+ """A consumed message carries a URN with no mapped handler (strategy "fail")."""
babelqueue/py.typed ADDED
File without changes
babelqueue/routing.py ADDED
@@ -0,0 +1,14 @@
1
+ """Unknown-URN strategy names, shared with every other SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class UnknownUrnStrategy:
7
+ """What a consumer does with a message whose URN has no mapped handler."""
8
+
9
+ FAIL = "fail"
10
+ DELETE = "delete"
11
+ RELEASE = "release"
12
+ DEAD_LETTER = "dead_letter"
13
+
14
+ ALL = (FAIL, DELETE, RELEASE, DEAD_LETTER)
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: babelqueue
3
+ Version: 0.1.0
4
+ Summary: Polyglot Queues, Simplified — the Python core: the canonical BabelQueue wire-envelope codec, contracts and dead-letter helpers.
5
+ Project-URL: Homepage, https://babelqueue.com
6
+ Project-URL: Source, https://github.com/BabelQueue/babelqueue-python
7
+ Project-URL: Issues, https://github.com/BabelQueue/babelqueue-python/issues
8
+ Project-URL: Changelog, https://github.com/BabelQueue/babelqueue-python/blob/main/CHANGELOG.md
9
+ Author-email: Muhammet Şafak <info@muhammetsafak.com.tr>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: envelope,json,messaging,microservices,polyglot,queue
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Libraries
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.9
25
+ Provides-Extra: amqp
26
+ Requires-Dist: pika>=1.3; extra == 'amqp'
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=7; extra == 'dev'
29
+ Provides-Extra: redis
30
+ Requires-Dist: redis>=4; extra == 'redis'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # BabelQueue for Python
34
+
35
+ [![CI](https://github.com/BabelQueue/babelqueue-python/actions/workflows/ci.yml/badge.svg)](https://github.com/BabelQueue/babelqueue-python/actions/workflows/ci.yml)
36
+ [![PyPI](https://img.shields.io/pypi/v/babelqueue.svg)](https://pypi.org/project/babelqueue/)
37
+ [![Python](https://img.shields.io/pypi/pyversions/babelqueue.svg)](https://pypi.org/project/babelqueue/)
38
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
39
+
40
+ > **Polyglot Queues, Simplified.** Read and write the canonical BabelQueue message
41
+ > envelope from Python — so your Python services (AI/ML, data processing, …)
42
+ > exchange messages with Laravel, Symfony, Go, .NET and Node over one strict JSON
43
+ > format, on the broker you already run.
44
+
45
+ This is the framework-agnostic **Python core**: the wire-envelope codec,
46
+ contracts, and dead-letter helpers — **zero runtime dependencies** (standard
47
+ library only). The full standard is documented at
48
+ **[babelqueue.com](https://babelqueue.com)**.
49
+
50
+ ## Installation
51
+
52
+ ```bash
53
+ pip install babelqueue
54
+ ```
55
+
56
+ Requires Python `>=3.9`.
57
+
58
+ ## Usage
59
+
60
+ ```python
61
+ from babelqueue import EnvelopeCodec
62
+
63
+ # Produce — build the canonical envelope and publish the JSON to your broker.
64
+ envelope = EnvelopeCodec.make("urn:babel:orders:created", {"order_id": 1042})
65
+ body = EnvelopeCodec.encode(envelope) # -> UTF-8 JSON string
66
+ # redis.rpush("queues:orders", body) / channel.basic_publish(body=body, ...)
67
+
68
+ # Consume — decode a message produced by ANY BabelQueue SDK.
69
+ incoming = EnvelopeCodec.decode(body)
70
+ urn = incoming["job"] # "urn:babel:orders:created"
71
+ data = incoming["data"] # {"order_id": 1042}
72
+ trace_id = incoming["trace_id"] # correlate across services
73
+ ```
74
+
75
+ The envelope is identical to every other SDK's:
76
+
77
+ ```json
78
+ {
79
+ "job": "urn:babel:orders:created",
80
+ "trace_id": "…",
81
+ "data": { "order_id": 1042 },
82
+ "meta": { "id": "…", "queue": "default", "lang": "python", "schema_version": 1, "created_at": 1749132727000 },
83
+ "attempts": 0
84
+ }
85
+ ```
86
+
87
+ ### Typed messages (optional)
88
+
89
+ ```python
90
+ from babelqueue import EnvelopeCodec, PolyglotMessage
91
+
92
+ class OrderCreated: # structurally a PolyglotMessage
93
+ def __init__(self, order_id: int):
94
+ self.order_id = order_id
95
+ def get_babel_urn(self) -> str:
96
+ return "urn:babel:orders:created"
97
+ def to_payload(self) -> dict:
98
+ return {"order_id": self.order_id}
99
+
100
+ envelope = EnvelopeCodec.from_message(OrderCreated(1042), queue="orders")
101
+ ```
102
+
103
+ Continue an existing trace by adding `get_babel_trace_id(self) -> str | None`
104
+ (see `HasTraceId`), or pass `trace_id=` to `EnvelopeCodec.make`.
105
+
106
+ ### Dead-letter
107
+
108
+ ```python
109
+ from babelqueue import dead_letter
110
+
111
+ dlq = dead_letter.annotate(envelope, "failed", "orders", attempts=3, error="boom")
112
+ # publish `EnvelopeCodec.encode(dlq)` to the "orders.dlq" queue
113
+ ```
114
+
115
+ ## What's here vs. coming
116
+
117
+ - **Now (this package):** the codec, contracts, dead-letter and unknown-URN
118
+ helpers, plus the shared conformance fixtures. Bring your own broker client.
119
+ - **Next (planned):** a built-in runtime — `BabelQueue(broker_url=...)` with an
120
+ `@app.handler("urn:…")` decorator over `redis`/`pika` — and **Celery** / **Django**
121
+ adapters. Install via extras (`babelqueue[redis]`, `babelqueue[celery]`, …).
122
+
123
+ ## Testing
124
+
125
+ ```bash
126
+ pip install -e ".[dev]"
127
+ pytest
128
+ # (or, dependency-free) python -m unittest discover -s tests
129
+ ```
130
+
131
+ ## License
132
+
133
+ MIT © Muhammet Şafak. See [LICENSE](LICENSE).
@@ -0,0 +1,11 @@
1
+ babelqueue/__init__.py,sha256=7h4_jURrYdOcIIul38ck2FRFjipvMyXRJkTpFw466i4,940
2
+ babelqueue/codec.py,sha256=l54YTPC9ScieUqjQh-eEoQ3oEq20JoSmy2Q00zebynQ,3218
3
+ babelqueue/contracts.py,sha256=zl7137t2agSdKnGjUDxajSXR8xLQTaAsqWEY69zDG0A,1030
4
+ babelqueue/dead_letter.py,sha256=BbNJF6E9BsLom6iOsVTOABbKQwLRWLztsaxmyebmqK0,1112
5
+ babelqueue/exceptions.py,sha256=R6p4tPmRRYbEUTvf9baBdmDvvyKhl5xubqk-NGhhD-w,325
6
+ babelqueue/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ babelqueue/routing.py,sha256=WRTEKrBWd9Tm0BVLm_axwNLA1F5_ljRQyFaGiCuhmB8,351
8
+ babelqueue-0.1.0.dist-info/METADATA,sha256=o6Q5Mns9r0tzbD2efLj37gQTIhOLkQRCgFwLWM-1SuA,4895
9
+ babelqueue-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
10
+ babelqueue-0.1.0.dist-info/licenses/LICENSE,sha256=OJMGANMbwEJV5fkqnS8MDTiZJp9ZizRpOo4HYWR0m6E,1072
11
+ babelqueue-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Muhammet Şafak
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.