Skip to content

Commit a6c664a

Browse files
committed
Release 1.0.0: public API SemVer-stable; core + Celery/Django adapters
1 parent e645d2e commit a6c664a

8 files changed

Lines changed: 128 additions & 11 deletions

File tree

.github/workflows/ci.yml

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,24 @@ jobs:
3232
- name: Run tests
3333
run: pytest
3434

35+
lint:
36+
name: Static analysis (ruff + mypy)
37+
runs-on: ubuntu-latest
38+
steps:
39+
- uses: actions/checkout@v5
40+
- name: Setup Python
41+
uses: actions/setup-python@v5
42+
with:
43+
python-version: '3.12'
44+
- name: Install (dev + all adapters for type context)
45+
run: |
46+
python -m pip install --upgrade pip
47+
pip install -e ".[dev,celery,django,redis,amqp]"
48+
- name: Ruff
49+
run: ruff check src tests
50+
- name: Mypy
51+
run: mypy
52+
3553
integration:
3654
name: Redis integration
3755
runs-on: ubuntu-latest
@@ -62,16 +80,16 @@ jobs:
6280
with:
6381
python-version: '3.12'
6482

65-
- name: Install (with redis + amqp extras)
83+
- name: Install (all adapters — full coverage with brokers)
6684
run: |
6785
python -m pip install --upgrade pip
68-
pip install -e ".[redis,amqp,dev]"
86+
pip install -e ".[redis,amqp,celery,django,dev]"
6987
70-
- name: Run tests (Redis + RabbitMQ transports included)
88+
- name: Run full suite with coverage gate (>=90%)
7189
env:
7290
BABELQUEUE_TEST_REDIS: redis://localhost:6379/0
7391
BABELQUEUE_TEST_AMQP: amqp://guest:guest@localhost:5672/
74-
run: pytest
92+
run: pytest --cov=babelqueue --cov-report=term-missing --cov-fail-under=90
7593

7694
conformance:
7795
name: Conformance suite in sync
@@ -85,3 +103,16 @@ jobs:
85103
diff -ru "$RUNNER_TEMP/conformance/fixtures" "tests/conformance/fixtures"
86104
diff -ru "$RUNNER_TEMP/conformance/schema" "tests/conformance/schema"
87105
echo "Vendored conformance is in sync with the canonical suite."
106+
107+
ci-green:
108+
name: CI green
109+
runs-on: ubuntu-latest
110+
needs: [test, lint, integration, conformance]
111+
if: ${{ always() }}
112+
steps:
113+
- name: Fail if any required job did not pass
114+
run: |
115+
if ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}; then
116+
echo "A required job failed or was cancelled."
117+
exit 1
118+
fi

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ dist/
99
.ruff_cache/
1010
.venv/
1111
venv/
12+
.coverage
13+
coverage.xml
14+
htmlcov/

CHANGELOG.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,22 @@ The envelope wire format is versioned separately by `meta.schema_version`
99

1010
## [Unreleased]
1111

12+
## [1.0.0] - 2026-06-07
13+
14+
**1.0.0 — the public API is now SemVer-stable**: breaking changes require a MAJOR,
15+
following the deprecation policy. The wire envelope is unchanged
16+
(`schema_version: 1`); the core + Celery/Django adapters ship together. Full
17+
reference at [babelqueue.com](https://babelqueue.com).
18+
19+
### Internal
20+
- CI adds **ruff** + **mypy** static analysis and a **>=90% coverage gate**
21+
(`pytest --cov --cov-fail-under=90`, run in the broker-backed job so the Redis /
22+
RabbitMQ transports count). Type-safety fix in `redis_transport` (str-narrow the
23+
BLMOVE reply) surfaced by mypy — no behaviour change.
24+
- **GR-8 latency benchmark** (`tests/test_overhead.py`) — asserts the envelope
25+
encode/decode path adds **≤2%** over plain-JSON serialization vs a conservative
26+
750µs broker round-trip.
27+
1228
## [0.5.0] - 2026-06-06
1329

1430
### Added
@@ -68,7 +84,8 @@ The envelope wire format is versioned separately by `meta.schema_version`
6884
- Pre-1.0: the public API may change before the `1.0.0` tag.
6985
- The core has **zero runtime dependencies** (standard library only); Python `>=3.9`.
7086

71-
[Unreleased]: https://github.com/BabelQueue/babelqueue-python/compare/v0.5.0...HEAD
87+
[Unreleased]: https://github.com/BabelQueue/babelqueue-python/compare/v1.0.0...HEAD
88+
[1.0.0]: https://github.com/BabelQueue/babelqueue-python/compare/v0.5.0...v1.0.0
7289
[0.5.0]: https://github.com/BabelQueue/babelqueue-python/compare/v0.4.0...v0.5.0
7390
[0.4.0]: https://github.com/BabelQueue/babelqueue-python/compare/v0.3.0...v0.4.0
7491
[0.3.0]: https://github.com/BabelQueue/babelqueue-python/compare/v0.2.0...v0.3.0

pyproject.toml

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "babelqueue"
7-
version = "0.5.0"
7+
version = "1.0.0"
88
description = "Polyglot Queues, Simplified — the Python core: the canonical BabelQueue wire-envelope codec, contracts and dead-letter helpers."
99
readme = "README.md"
1010
requires-python = ">=3.9"
@@ -33,7 +33,7 @@ redis = ["redis>=4"]
3333
amqp = ["pika>=1.3"]
3434
celery = ["celery>=5"]
3535
django = ["django>=4.2"]
36-
dev = ["pytest>=7"]
36+
dev = ["pytest>=7", "pytest-cov>=4", "mypy>=1.8", "ruff>=0.5"]
3737

3838
[project.urls]
3939
Homepage = "https://babelqueue.com"
@@ -43,3 +43,21 @@ Changelog = "https://github.com/BabelQueue/babelqueue-python/blob/main/CHANGELOG
4343

4444
[tool.hatch.build.targets.wheel]
4545
packages = ["src/babelqueue"]
46+
47+
[tool.ruff]
48+
target-version = "py39"
49+
line-length = 100
50+
51+
[tool.mypy]
52+
# Lowest target mypy accepts; the package itself still supports Python 3.9 at
53+
# runtime (requires-python >=3.9) and uses only 3.9-compatible typing.
54+
python_version = "3.10"
55+
files = ["src/babelqueue"]
56+
ignore_missing_imports = true
57+
58+
[tool.coverage.run]
59+
source = ["babelqueue"]
60+
61+
[tool.coverage.report]
62+
show_missing = true
63+
exclude_lines = ["pragma: no cover", "raise NotImplementedError", "if TYPE_CHECKING:"]

src/babelqueue/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from .routing import UnknownUrnStrategy
2020
from .transport import InMemoryTransport, ReceivedMessage, Transport
2121

22-
__version__ = "0.5.0"
22+
__version__ = "1.0.0"
2323

2424
__all__ = [
2525
"BabelQueue",

src/babelqueue/redis_transport.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,15 @@ def publish(self, queue: str, body: str) -> None:
3636
self._redis.rpush(queue, body)
3737

3838
def pop(self, queue: str, timeout: float = 1.0) -> Optional[ReceivedMessage]:
39-
body = self._redis.blmove(queue, self._processing(queue), timeout, "LEFT", "RIGHT")
39+
# redis-py types the BLMOVE timeout as int, but Redis accepts a float
40+
# (sub-second) timeout; passing it through is correct at runtime.
41+
body = self._redis.blmove(queue, self._processing(queue), timeout, "LEFT", "RIGHT") # type: ignore[arg-type]
4042
if body is None:
4143
return None
42-
return ReceivedMessage(body=body, queue=queue, handle=body)
44+
# decode_responses=True yields str; the guard satisfies the type checker
45+
# (and is a harmless safety net otherwise).
46+
text = body if isinstance(body, str) else body.decode()
47+
return ReceivedMessage(body=text, queue=queue, handle=text)
4348

4449
def ack(self, message: ReceivedMessage) -> None:
4550
self._redis.lrem(self._processing(message.queue), 1, message.handle)

src/babelqueue/transport.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from abc import ABC, abstractmethod
1010
from collections import defaultdict, deque
11-
from dataclasses import dataclass, field
11+
from dataclasses import dataclass
1212
from typing import Any, Deque, Dict, Optional
1313

1414
from .exceptions import BabelQueueError

tests/test_overhead.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""GR-8 budget: the envelope encode/decode path must add no more than 2% over plain
2+
JSON serialization (the baseline a publisher already pays), measured against a
3+
conservative broker round-trip. Pure CPU — no broker — so the gate is stable and
4+
environment-independent in CI. Same methodology + reference as every other SDK.
5+
"""
6+
7+
import json
8+
import time
9+
from typing import Callable
10+
11+
from babelqueue import EnvelopeCodec
12+
13+
# Conservative networked broker publish+consume round-trip (ns). Local loopback
14+
# Redis measures ~300µs; production brokers (networked/persistent, RabbitMQ with
15+
# confirms) are commonly >=0.5-2ms, so 750µs is conservative.
16+
REFERENCE_BROKER_ROUNDTRIP_NS = 750_000
17+
18+
_DATA = {"order_id": 1042, "amount": 99.9, "currency": "USD", "note": "café ☕"}
19+
20+
21+
def _ns_per_op(fn: Callable[[], None]) -> float:
22+
for _ in range(5_000): # warm up
23+
fn()
24+
iterations = 50_000
25+
start = time.perf_counter_ns()
26+
for _ in range(iterations):
27+
fn()
28+
return (time.perf_counter_ns() - start) / iterations
29+
30+
31+
def test_codec_overhead_within_budget() -> None:
32+
def envelope() -> None:
33+
EnvelopeCodec.decode(EnvelopeCodec.encode(EnvelopeCodec.make("urn:babel:orders:created", _DATA)))
34+
35+
def bare() -> None:
36+
json.loads(json.dumps(_DATA))
37+
38+
marginal = max(0.0, _ns_per_op(envelope) - _ns_per_op(bare))
39+
overhead = marginal / REFERENCE_BROKER_ROUNDTRIP_NS * 100
40+
41+
assert overhead <= 2.0, (
42+
f"codec overhead {overhead:.2f}% exceeds the 2% GR-8 budget (marginal {marginal:.0f} ns)"
43+
)

0 commit comments

Comments
 (0)