varco-nats
NATS JetStream event bus backend for varco —
NatsEventBus built on nats-py.
varco_nats implements varco_core's AbstractEventBus, ChannelManager,
AbstractDeadLetterQueue and HealthCheck contracts on top of NATS JetStream —
the persistent, at-least-once layer of NATS (its analogue of Apache Kafka).
JetStream only. Core NATS at-most-once pub/sub is intentionally not exposed. If you need fire-and-forget delivery, use
varco_redis's Pub/Sub bus.
Installation
uv add varco-nats # or: pip install varco-nats
Requires a running NATS server with JetStream enabled (nats-server -js).
Quick start
from varco_nats import NatsEventBus, NatsEventBusSettings
from varco_core.event import BusEventProducer, EventConsumer, listen
class OrderPlacedEvent(Event):
__event_type__ = "order.placed"
order_id: str
config = NatsEventBusSettings(
servers="nats://localhost:4222",
durable_name="order-service", # the JetStream analogue of a Kafka group_id
)
async with NatsEventBus(config) as bus:
class OrderConsumer(EventConsumer):
@listen(OrderPlacedEvent, channel="orders")
async def on_placed(self, event: OrderPlacedEvent) -> None:
print(f"Order placed: {event.order_id}")
OrderConsumer().register_to(bus)
producer = BusEventProducer(bus)
await producer._produce(OrderPlacedEvent(order_id="abc"), channel="orders")
How channels map to NATS
Unlike Kafka — where each channel is a topic — NATS channels are subjects under a single JetStream stream's wildcard:
| varco concept | NATS concept |
|---|---|
stream (stream_name) |
one JetStream stream capturing {subject_prefix}.> |
channel "orders" |
subject {subject_prefix}.{channel_prefix}orders |
durable_name |
base name for durable consumers (≈ Kafka consumer group) |
CHANNEL_ALL |
local-only filter — opens no consumer |
The bus creates the backing stream automatically on start() when
auto_create_stream=True (the default).
Delivery semantics
NatsEventBus mirrors KafkaEventBus: JetStream redelivery is the
broker-level safety net, while handler-level retries are the job of
varco's @listen(retry_policy=..., dlq=...) machinery.
delivery_semantics |
Behaviour |
|---|---|
at_most_once |
Message acked before dispatch. Crash → message lost. No duplicates. |
at_least_once (default) |
Message acked after dispatch. Crash before ack → JetStream redelivers. A handler that merely raises (no crash) also redelivers (Plan 019 / RT2-B): the message is nak()ed for immediate redelivery, bounded by max_deliver. |
exactly_once |
As at_least_once + every publish carries Nats-Msg-Id = event.event_id, so JetStream drops producer-retry duplicates within duplicate_window. |
from varco_nats import NatsDeliverySemantics
config = NatsEventBusSettings(
servers="nats://localhost:4222",
delivery_semantics=NatsDeliverySemantics.EXACTLY_ONCE,
max_deliver=5, # redelivery budget — JetStream's own default is unlimited
)
⚠️ BREAKING (behaviour), Plan 019 / RT2-B: prior to this, a handler that
raised under AT_LEAST_ONCE/EXACTLY_ONCE was silently acked and never
redelivered — only a process crash triggered redelivery. Now a raising
handler nak()s the message for redelivery, up to max_deliver attempts
(default 5, env VARCO_NATS_MAX_DELIVER), after which it is term()ed
(never redelivered again) with a WARNING log. A non-idempotent handler
with no @listen(retry_policy=...) may now see repeat side-effects where
it previously saw exactly one delivery. A deserialization failure is always
term()ed regardless of delivery count — a poison payload can never
succeed on retry.
⚠️ ErrorPolicy.FIRE_FORGET opts OUT of redelivery. FIRE_FORGET
swallows a handler exception inside _dispatch before _on_message ever
sees it — the bus observes a "successful" dispatch and acks. Use the
default COLLECT_ALL or FAIL_FAST if you want AT_LEAST_ONCE redelivery
on handler failure.
ack_wait_seconds (previously dead configuration — see the env-var table
below) now actually reaches the JetStream consumer.
Configuration
All settings are read from environment variables with the VARCO_NATS_ prefix:
VARCO_NATS_SERVERS=nats://nats.internal:4222
VARCO_NATS_STREAM_NAME=orders-events
VARCO_NATS_SUBJECT_PREFIX=orders
VARCO_NATS_DURABLE_NAME=order-service
VARCO_NATS_DELIVERY_SEMANTICS=at_least_once
VARCO_NATS_CHANNEL_PREFIX=prod.
VARCO_NATS_ACK_WAIT_SECONDS=30.0 # now live — reaches the JetStream consumer (Plan 019 / RT2-B)
VARCO_NATS_MAX_DELIVER=5 # redelivery budget before term() (Plan 019 / RT2-B)
config = NatsEventBusSettings.from_env()
For structured connection/security config (TLS, user/password, token), use
NatsConnectionSettings with the NATS_ prefix:
NATS_SERVERS=nats://nats1:4222,nats://nats2:4222
NATS_SSL__CA_CERT=/etc/ssl/nats-ca.pem
NATS_AUTH__TYPE=basic
NATS_AUTH__USERNAME=alice
NATS_AUTH__PASSWORD=secret
from varco_nats import NatsConnectionSettings
conn = NatsConnectionSettings.from_env()
config = NatsEventBusSettings(connect_kwargs=conn.to_nats_kwargs())
Stream management
NatsStreamManager administers the backing JetStream stream. channel_exists()
implements the ChannelManager ABC's declared-or-present contract (Plan
019 / RT2-C): declare_channel(c) implies channel_exists(c) is True
until delete_channel(c), even with zero messages published — via a
process-local declaration registry layered on top of broker evidence
("does the subject currently carry a message?", still available separately
as channel_has_messages()):
from varco_nats import NatsStreamManager, NatsChannelManagerSettings
settings = NatsChannelManagerSettings(servers="nats://localhost:4222")
async with NatsStreamManager(settings) as manager:
await manager.declare_channel("orders") # ensures the stream + registers "orders"
exists = await manager.channel_exists("orders") # True — declared, even with 0 messages
has_data = await manager.channel_has_messages("orders") # NATS-only: old "carries a message?" predicate
channels = await manager.list_channels() # declared channels ∪ channels carrying messages
await manager.delete_channel("orders") # purge messages + discard from the registry
⚠️ BREAKING (behaviour), Plan 019 / RT2-C: channel_exists()/
list_channels() previously answered "does this channel currently carry a
message?" — a freshly declared, empty channel reported as not existing,
which violated the ChannelManager ABC's own documented contract. They now
answer "was this channel declared (by this manager instance) or does it
carry a message?", matching Kafka and Redis. The registry is
process-local — a fresh manager in another process reports False for
a channel declared elsewhere that has never carried a message (identical to
Redis's long-standing, documented limitation).
Dead letter queue
NatsDLQ stores exhausted events in a dedicated WorkQueue-retention
JetStream stream — so count() returns the exact pending-entry count:
from varco_nats import NatsDLQ
async with NatsDLQ(settings=NatsEventBusSettings()) as dlq:
# Usually wired automatically via @listen(dlq=dlq):
class OrderConsumer(EventConsumer):
@listen(
OrderPlacedEvent,
channel="orders",
retry_policy=RetryPolicy(max_attempts=3),
dlq=dlq,
)
async def on_order(self, event: OrderPlacedEvent) -> None: ...
# Relay:
entries = await dlq.pop_batch(limit=10)
for entry in entries:
await alert_ops(entry)
await dlq.ack(entry.entry_id)
Dependency injection (Providify)
from varco_nats.di import bootstrap
from varco_core.event import AbstractEventBus
container = bootstrap() # scans varco_nats
bus = await container.aget(AbstractEventBus) # NatsEventBus singleton
# ...
await container.ashutdown() # stops the bus via @PreDestroy
Install the DLQ explicitly when needed:
from varco_nats.dlq import NatsDLQConfiguration
from varco_core.event.dlq import AbstractDeadLetterQueue
await container.ainstall(NatsDLQConfiguration)
dlq = await container.aget(AbstractDeadLetterQueue)
Running tests
# Unit tests — no broker required (nats-py is faked)
uv run pytest varco_nats/tests/
# Integration tests — require Docker (a real NATS server is started)
uv run pytest varco_nats/tests/ -m integration
# or
VARCO_RUN_INTEGRATION=1 uv run pytest varco_nats/tests/
Migration 1.x → 2.0
The no-op @Configuration aliases (NatsEventBusConfiguration,
NatsChannelManagerConfiguration) were removed. Register the bus via scan:
# Before (1.x)
await container.ainstall(NatsEventBusConfiguration)
# After (2.0)
from varco_nats.di import bootstrap
bootstrap(container) # or: container.scan("varco_nats", recursive=True)
The opt-in NatsDLQConfiguration is unchanged — still await container.ainstall(...).
License
Apache-2.0
Metadata
Release files for varco-nats 3.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| varco_nats-3.2.0.tar.gz | 88.1 kB | With provenance | Details for varco_nats-3.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| varco_nats-3.2.0-py3-none-any.whl | Python 3 | none | any | Details for varco_nats-3.2.0-py3-none-any.whl |
Total release size: 153.3 kB
Release files / varco_nats-3.2.0.tar.gz
| Download URL | varco_nats-3.2.0.tar.gz |
|---|---|
| Size | 88.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f6e5ae9a98ce9097613c88e9159464eda48e8047de532830f1327bccd38f71ef
|
|
BLAKE2b-256 checksum How to use checksums |
b46e858df51de94efa1ff2ca02a389a7f326dbadb6867e8786e1d5b439c63832
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 12, 2026.
Transparency logRelease files / varco_nats-3.2.0-py3-none-any.whl
| Download URL | varco_nats-3.2.0-py3-none-any.whl |
|---|---|
| Size | 65.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9cad5b43c99a0e167da6826dca1a8ba2fd590ae28fe5070b792fb54ec2bf3641
|
|
BLAKE2b-256 checksum How to use checksums |
c4d26953d51ea13f18cca88bb37e222a86d1f0512c51b3d5b5c5da889aad0df1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 12, 2026.
Transparency logSecurity for varco-nats 3.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for varco_nats-3.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 12, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.