DefStream
Synchronous event/message processing with pluggable broker adapters
Just def.
Let workers do the work.
You don't need to await this.
DefStream is a synchronous task-processing library that consumes broker messages
and executes ordinary Python task methods in a thread or process pool.
Configure execution, acknowledgements, failure policies, and monitoring without
async / await.
- Task definitions: declare a subscription and handler in a
Runclass. - Parallel execution: choose
ThreadPoolorProcessPool. - Broker choice: RabbitMQ and Redis Streams implementations are included; install only the clients you need.
- Request/reply: receive task return values through RabbitMQ RPC.
- Operations: optionally enable HTTP health checks, Prometheus metrics, a dashboard, and a task console.
- Input/output contracts: define schemas and runtime validators and expose AsyncAPI documentation.
Contents
- Installation
- Quick start
- Tasks and message handling
- Execution pools and shutdown
- Lifecycle hooks
- Request/reply contracts and validation
- Health checks and task console
- Logging
- Broker selection and operational considerations
- RabbitMQ usage
- Redis usage
- Examples and license
Logging
DefStream records messages through standard logging without configuring handlers,
formatters, or levels. Select the sources your application needs. The samples
output only the Sample and DefStream logger hierarchies, replacing their
print() calls with logging. The Worker's existing startup banner is still
printed regardless of logging configuration.
from logging import DEBUG, Filter, Formatter, StreamHandler, getLogger
sources = (Filter('Sample'), Filter('DefStream'))
handler = StreamHandler()
handler.setFormatter(Formatter(
'%(asctime)s %(processName)s %(name)s %(levelname)s %(message)s'
))
handler.addFilter(lambda record: any(source.filter(record) for source in sources))
root = getLogger()
root.setLevel(DEBUG)
root.addHandler(handler)
logger = getLogger('Sample')
logger.info('Application started')
Apply filters to the handler, not the root logger. Filter('DefStream')
includes DefStream and names below DefStream., but not DefStreamExtra.
This configuration is an example for a standalone application; it does not change
output from other handlers already installed. To receive only broker logs, select
DefStream.Broker.Implements.RabbitMQ or DefStream.Broker.Implements.Redis
instead of DefStream.
- The class member
loggerusesDefStream.Worker,DefStream.Run,DefStream.Executor,DefStream.Pool,DefStream.ThreadPool, andDefStream.ProcessPool. - Shared Pool methods also use the concrete pool's logger member. User subclasses
inherit the default name and can explicitly override
logger. - Inspector, Server, and RequestHandler share
DefStream.Inspector. HTTP request receipt is DEBUG; response status and transmission results are INFO. Extension-specific logs useDefStream.Inspector.Extensions.<extension name>. - Normal per-message processing is DEBUG, lifecycle events are INFO, events requiring attention are WARNING, and processing failures are ERROR. Bodies, results, and credentials are excluded from the default log context.
- Without any handlers, Python's
lastResortmay write WARNING and higher to stderr. Do not put sensitive values in user exception messages: failure tracebacks include those messages.
ProcessPool uses spawn child processes, so parent handlers are not automatically
inherited. To receive child logs, apply the same configuration in an importable
PoolSetup implementation and pass it through ProcessPool(..., setups=...).
That configuration cannot capture DEBUG and INFO logs emitted before the child
setup hook runs. In existing applications, choose the initialization location
carefully to avoid adding the same handlers repeatedly.
Installation
Requires Python 3.9 or newer. Prepare the broker server you intend to use.
python -m pip install "DefStream[RabbitMQ]"
Choose Redis or both adapters as needed:
python -m pip install "DefStream[Redis]"
python -m pip install "DefStream[RabbitMQ,Redis]"
For the core alone, use python -m pip install DefStream.
Both broker implementations are included in the DefStream distribution, but the default
installation has no broker client dependencies. The RabbitMQ extra installs pika>=1.3.2;
the Redis extra installs redis>=5.1.1.
| Installation choice | Python import path | Guide |
|---|---|---|
DefStream |
DefStream |
This document |
DefStream[RabbitMQ] |
DefStream.Broker.Implements.RabbitMQ |
RabbitMQ usage |
DefStream[Redis] |
DefStream.Broker.Implements.Redis |
Redis usage |
Separate releases of DefStream-RabbitMQ and DefStream-Redis are discontinued,
and their projects are scheduled for deletion. Backward compatibility with those
packages is not supported. Install the merged package in a new environment using
the commands above. To migrate an existing environment, first run
python -m pip uninstall DefStream-RabbitMQ DefStream-Redis, then reinstall the
merged DefStream. Do not install them together: they provide files at the same import paths.
For an existing environment, use
python -m pip install --force-reinstall "DefStream[RabbitMQ,Redis]",
selecting extras for the brokers you actually use.
Quick start
These examples assume a local RabbitMQ server at 127.0.0.1:5672.
Save the following three files in the same directory. The guest credentials
are for local examples only; use a dedicated account and environment variables
or a secret store in production.
1. Define a task: tasks.py
from DefStream.Broker.Implements.RabbitMQ import Reject, Subscription
from DefStream.Worker import Run
class Add(
Run,
parameters=Subscription(
queue='defstream.example.add',
qos=4,
error='defstream.example.add.error',
),
on_error=Reject(),
):
def run(
self,
a: int,
b: int,
):
result = a + b
print(result)
return result
parameters selects the task's queue. A body of {'a': 10, 'b': 20} becomes
run(a=10, b=20). This example rejects failures instead of retrying them and
declares an error queue to receive rejected messages.
2. Run the Worker: worker.py
Worker accepts DefStream.Broker.Connection directly.
The connection contract only requires connect() and close(). The worker checks
for GetConsumer support internally when it runs. A broker that cannot create
a consumer raises TypeError; no worker-specific connection type is needed.
from signal import SIGINT, signal
from DefStream.Broker.Implements.RabbitMQ import Configuration, Connection
from DefStream.Worker import ThreadPool, Worker, WorkerSetup, declare
from tasks import Add
class Prepare(WorkerSetup):
def __call__(
self,
worker: Worker,
) -> None:
for task in worker.tasks:
declare(
task=task,
broker=worker.broker,
)
if __name__ == '__main__':
worker = Worker(
Connection(Configuration(
host='127.0.0.1',
port=5672,
username='guest',
password='guest',
)),
ThreadPool(4),
setups=Prepare(),
)
worker.add(Add)
signal(
SIGINT,
lambda *_: worker.stop(),
)
worker.run()
python worker.py
The Worker opens its broker connection, runs Prepare to declare the queue
and error queue, then starts consuming. Defining a Run does not register it:
call worker.add() explicitly.
3. Send messages: client.py
Run this in another terminal while the Worker is running.
from DefStream.Broker.Implements.RabbitMQ import Configuration, Connection
from DefStream.Worker import emit, request
from tasks import Add
connection = Connection(Configuration(
host='127.0.0.1',
port=5672,
username='guest',
password='guest',
))
try:
connection.connect()
message_id = emit(
{'a': 10, 'b': 20},
task=Add,
broker=connection,
)
print(message_id)
result = request(
{'a': 10, 'b': 20},
task=Add,
broker=connection,
timeout=5,
)
print(result)
finally:
connection.close()
python client.py
emit() and request() each publish a separate task. This example performs
the addition twice; request() returns 30. The request timeout is in seconds
and does not cancel work that has already been delivered.
Tasks and message handling
Task-directed API
| Function | Purpose | Connection ownership |
|---|---|---|
declare(task=..., broker=...) |
Declare task queues and subscription resources | Caller connects and closes |
emit(body, task=..., broker=...) |
Publish a one-way message to a task | Caller connects and closes |
request(body, task=..., broker=..., timeout=5) |
Request a task result synchronously | Caller connects and closes; RabbitMQ only |
Import these functions from DefStream.Worker. They do not connect or close
the supplied connection. Neither emit() nor request() declares destinations.
Call declare() in the receiving Worker's setup or a separate provisioning step.
Reuse a connection for multiple publications on its owning thread.
All three helpers accept the common Connection type and check declaration,
emission, or RPC support internally. Unsupported operations raise NotImplementedError.
The task helper and connection method previously named send are now named emit.
Custom brokers should use Emittable.emit instead of TaskSender.send for task emission.
Low-level Sendable.send() and RPC Callable.call() on queues, exchanges, and streams
remain unchanged.
Task bodies must be mappings with string keys. Return values must be serializable by the adapter. A Worker cannot register multiple tasks for the same queue: routing selects tasks by queue, not by a type field in the message.
Task priorities
For RabbitMQ classic queues, define both the default message priority and queue
max_priority in the RabbitMQ Subscription supplied as the task's parameters.
There is no separate priority setting on Run.
declare() reads these settings to provision the priority queue.
from DefStream.Broker.Implements.RabbitMQ import Subscription
from DefStream.Worker import (
Run,
declare,
request,
emit,
)
class Add(
Run,
parameters=Subscription(
queue='defstream.example.priority.add',
max_priority=5,
priority=2,
),
):
def run(
self,
a: int,
b: int,
):
return a + b
# broker is a RabbitMQ Connection connected by the caller.
declare(
task=Add,
broker=broker,
)
emit(
{'a': 10, 'b': 20},
task=Add,
broker=broker,
)
# RPC requires a separately running Worker.
result = request(
{'a': 10, 'b': 20},
task=Add,
broker=broker,
priority=5,
)
- Omitting the call's
priorityor passingNoneuses the task default. An explicit0overrides the default with the lowest priority. - Setting only the task default sets the queue maximum to RabbitMQ's limit of
255. Setting only the maximum leaves the default asNone; without a per-call value either, the message priority property is omitted. - Values above the task maximum, negative values, non-integers, and
boolare rejected. - Specifying priority only at call time validates only the broker range
0..255. The caller must provision a priority queue and respect its actual maximum. - Child tasks inherit settings and may override them with valid values, but cannot clear them.
- Requesting priorities with Redis raises
NotImplementedError. Calls without priorities retain existing Redis behavior. - Priority affects delivery of waiting messages within the same queue. It does not preempt delivered or running tasks or guarantee execution and completion order.
Success, failures, and retries
- ACK follows successful execution and completion hooks. RabbitMQ RPC also publishes the reply before ACK.
- Failures use the task's
on_errorpolicy. The default isRequeue(); chooseReject()explicitly if needed. - Both policies can be imported from
DefStream.Broker.Policies. - Rejection is broker-specific: RabbitMQ dead-letters to a configured DLQ or discards the message; Redis acknowledges it for the group while preserving stream history.
- There is no automatic retry-count limit. Choose a policy that avoids endless redelivery of permanently invalid messages.
- Publication and ACK are not one transaction. Make tasks idempotent and safe to execute more than once.
The common resource operations below apply when accessing a broker directly. Not every resource supports every operation.
Use isinstance or issubclass with GetExchange, GetQueue, GetStream,
or GetConsumer to check whether a connection provides exchange(), queue(),
stream(), or consumer(), respectively. Import these contracts from DefStream.Broker.
| Method | Purpose | Timeout unit |
|---|---|---|
send(body) |
Publish a message | Adapter configuration |
call(body, timeout=5) |
Synchronous RPC | Seconds |
pop(timeout=1000) |
Receive one message | Milliseconds |
read(offset=0, timeout=1000) |
Read stream history and new messages | Milliseconds |
For pop() and read(), None or 0 waits indefinitely.
Settle directly consumed processing events with ack(), nack(), or reject().
Redis history reads are separate from consumer-group processing and cannot be ACKed.
RabbitMQ stream reads are ACKed automatically when iteration advances.
Execution pools and shutdown
| Pool | Typical use | Considerations |
|---|---|---|
ThreadPool(4) |
I/O-bound tasks | Synchronize state shared across execution threads |
ProcessPool(4) |
CPU-bound tasks | Classes, inputs, results, and hooks must be picklable |
Import both pools from DefStream.Worker. Without an explicit size, they use
the CPU count. Capacity includes submitted, unfinished tasks; there is no
unbounded waiting queue. A full pool causes the Worker to NACK for redelivery.
RabbitMQ's per-task Subscription.qos limits unacknowledged deliveries, not pool size.
With ProcessPool, define classes and hooks at importable module scope and
create and run the Worker inside if __name__ == '__main__':.
Never pass broker connections or events to child processes.
worker.stop() stops receiving new work, waits for active tasks and message
settlement, then closes resources. There is currently no forced termination or
shutdown timeout. Register an application SIGINT handler as above for graceful
Ctrl+C shutdown. Each Worker, its connection, and its pool belong to a single
run: do not share them with another Worker or restart them.
Lifecycle hooks
All hook interfaces are exported from DefStream.Worker.
| Scope | Hooks | Arguments and execution location |
|---|---|---|
| Task | RunSetup, RunComplete, RunError |
Context; context and result; context and exception. Inside the pool |
| Pool | PoolSetup, PoolCleanup |
No arguments. Inside each execution thread/process |
| Worker | WorkerSetup, WorkerCleanup |
Worker instance. Parent connection-owning thread |
Declare task hooks with setups=, completes=, and errors= on the Run class.
Pass setups= and cleanups= to Worker or pool constructors.
Each option accepts one hook or an ordered sequence of hooks.
Inspect call arguments through Context.args and Context.kwargs.
Pool execution environments start lazily and run their setup and cleanup once per environment. Create and close environment-specific resources, such as DB connections, there. Worker setup runs after broker connection and before subscription; cleanup runs after pool shutdown and before consumer/broker closure. Cleanup must tolerate partial initialization. Hooks are not guaranteed after forced termination or process crashes.
RunError is execution-error code; on_error is the message-settlement policy.
Running an error hook does not turn failure into success. If a completion hook
publishes another task, create and close a separate connection in that hook's
thread/process. A later failure does not roll back messages already published.
Request/reply contracts and validation
Descriptor describes data structure; Validator validates and transforms
actual values. Connect both to Specification to document and validate tasks.
Place the following examples in the same file, in order.
1. Describe a schema with Descriptor
from DefStream.Descriptor import DumpSchema, Integer, Object, Properties, Schema
request_schema = Object(
description='Addition request',
properties=Properties(
a=Integer('First operand'),
b=Integer('Second operand'),
),
)
response_schema = Integer('Sum')
print(DumpSchema(Schema(
'AddRequest',
request_schema,
)))
Object describes an object, Properties its fields, and Integer an integer.
Fields such as Integer are required by default; use required=False to mark
an optional field. Use String, Number, Boolean, and Array for other types
and nested structures. Wrap a value in Schema before passing it to
DumpSchema() to obtain a plain JSON Schema dictionary.
This documents a and b as required integers but does not validate messages
automatically. Required fields, defaults, and ranges need corresponding
validation rules if they must also be enforced at runtime.
2. Validate values with Validator
from DefStream.Validator import Validator
from DefStream.Validator.Patterns import IsInteger, IsObject
from DefStream.Validator.Patterns.Object import IsMappingOf, IsRequiredIn
request_validator = Validator(
IsObject(
IsRequiredIn(
'a',
'b',
),
IsMappingOf({
'a': IsInteger(),
'b': IsInteger(),
}),
),
)
response_validator = Validator(IsInteger())
print(request_validator({'a': 10, 'b': 20}))
Validatorapplies patterns in order and returns the final value.IsObjectchecks for thedicttype, then runs its nested required-key and field-type rules.IsRequiredInraisesValueErrorfor missing keys;IsIntegerraisesTypeErrorfor wrong types, including the string'10'and booleanTrue.IsMappingOfvalidates/transforms configured fields and may update the dictionary in place. Pass a copy to preserve the original. It does not remove or reject unconfigured extra keys. Extra keys that do not match task arguments make the call fail; implement additional restrictions if needed.
3. Combine conversion and validation
To accept numeric strings, use an explicit conversion pattern instead of
IsInteger. This independent example converts a string to an integer and then
checks that it is non-negative.
from DefStream.Validator import Validator
from DefStream.Validator.Patterns import IsGreaterEqualTo, ToInteger
non_negative_integer = Validator(
ToInteger(),
IsGreaterEqualTo(0),
)
print(non_negative_integer('10'))
The result is integer 10, not a string. Invalid strings and negative values
raise ValueError. ToInteger handles Python numeric-literal strings and
differs from strict type checking. This converter does not change the strict
request validator above. To apply it to a field, set that field's value in
IsMappingOf to non_negative_integer.
4. Connect task request and response contracts
Replace the earlier Add with the following class using the schemas and
validators defined above. Once registered with the Worker, inputs are validated
before run(), and the return value before completion hooks.
from DefStream.Broker.Implements.RabbitMQ import Reject, Subscription
from DefStream.Worker import Run, Specification
class Add(
Run,
parameters=Subscription(queue='defstream.example.validated-add'),
on_error=Reject(),
request=Specification(
schema=request_schema,
validator=request_validator,
example={'a': 10, 'b': 20},
),
response=Specification(
schema=response_schema,
validator=response_validator,
example=30,
),
name='add',
description='Add two integers',
):
def run(
self,
a: int,
b: int,
):
return a + b
schemais documentation metadata: it accepts JSON Schema mappings, Descriptor values/conditions, and dataclass types, but does not itself validate values or instantiate models.validatoris a callable validation/transformation function. Its returned value is used for execution. A request validator must still return a mapping with string keys.- Plain JSON Schema dictionaries and custom validation functions can also be passed as
schemaandvalidator. exampleis documentation data and is not automatically executed or validated.formatdefaults toapplication/json. It changes documented media types, not broker codecs.- Run's
nameis used as both the AsyncAPI Operation key and the Metrics task key. When omitted,module.__qualname__is used; Run'sdescriptionappears on the AsyncAPI Operation. Explicit names must match[A-Za-z0-9_.-]+; whitespace and Korean characters are not allowed. Names are case-sensitive and preserved exactly as provided. - Execution order: setup hooks → request validation →
run()→ response validation → completion hooks → reply/ACK.
Validation exceptions are task failures and follow error hooks and the on_error
policy. The example's Reject() avoids endlessly retrying invalid inputs.
To preserve failures in a DLQ, also configure Subscription(error=...) and
provision it through declare(). Schemas and examples appear in Inspector's
AsyncAPI and console; custom Validator patterns are not automatically
reverse-engineered into JSON Schema.
Health checks and task console
Inspector is optional and uses a Python standard-library HTTP server.
Configure it before starting the Worker and pass Worker(..., inspector=inspector).
This snippet reuses the Connection and Configuration imports from worker.py.
from DefStream.Inspector import Inspector
from DefStream.Inspector.Extensions import (
Console,
Metrics,
Ping,
Specifications,
Status,
)
def create_connection():
return Connection(Configuration(
host='127.0.0.1',
port=5672,
username='guest',
password='guest',
))
inspector = Inspector(
host='127.0.0.1',
port=9999,
exts=(
Ping('/ping'),
Metrics('/metrics'),
Status('/status'),
Specifications('/asyncapi.json'),
Console(
'/console',
broker=create_connection,
title='DefStream',
),
),
)
| Path | Purpose |
|---|---|
/ping |
Check subscription readiness and consumer-loop responsiveness: 200 when healthy, 503 during startup/shutdown or when unresponsive |
/metrics |
Prometheus metrics with the defstream_ prefix |
/status |
Successes, failures, throughput, and duration; register with exactly one Metrics |
/asyncapi.json |
Registered task request/reply contracts as AsyncAPI JSON |
/console |
Browse tasks and schemas, send JSON messages, and execute RPC on supported brokers |
There are no default routes. Metrics describe one Worker and reset on restart. Execution counts include retries and hooks, not unique messages or ACKs. The dashboard provides recent 1-second and 60-second windows, not long-term storage. Declaring a response contract does not add RPC support to Redis.
Restrict access. The console example above publishes real tasks without
authentication. For authentication, pass a random token of at least 16 characters
using Console(token=...) and enter it in the browser. Load the token from an
environment variable or secret store. Other built-in endpoints remain
unauthenticated: bind to loopback or a trusted management network, and use an
HTTPS reverse proxy with additional access control for remote access.
Inspector has no built-in TLS and is not a public web server.
The console's connection factory must return a new connection on every call,
never the Worker's connection.
Console's broker parameter uses the DefStream.Broker.BrokerFactory contract.
Its __call__() -> Connection takes no arguments and returns a new connection.
Functions such as create_connection above and lambdas remain valid without changes.
For a class-based factory, you can inherit BrokerFactory and implement __call__.
RabbitMQ usage
Requires Python 3.9 or newer and a reachable RabbitMQ server; uses Pika.
Send and receive queue messages
This example uses a local RabbitMQ server's guest account. Use a dedicated
account and environment variables or a secret store in production.
It creates defstream.example.rabbitmq and leaves the queue in place.
from DefStream.Broker.Implements.RabbitMQ import Configuration, Connection
connection = Connection(Configuration(
host='127.0.0.1',
port=5672,
username='guest',
password='guest',
))
try:
connection.connect()
connection.create_queue('defstream.example.rabbitmq')
queue = connection.queue('defstream.example.rabbitmq')
queue.send({'message': 'Hello, DefStream!'})
event = queue.pop(timeout=1000)
if event is not None:
print(event.body)
event.ack()
finally:
connection.close()
The default codec serializes messages as JSON.
The pop() timeout is in milliseconds; a positive timeout returns None
if no delivery arrives. None or 0 waits indefinitely.
Call ack() on success, nack() for redelivery, or reject() to reject.
Rejected messages go to the broker-configured dead-letter destination or are
discarded when none is configured.
Logging
Connection, Consumer, Queue, Exchange, Stream, and Event expose a class
member logger named DefStream.Broker.Implements.RabbitMQ.ClassName. Subclasses
inherit that logger unless they explicitly override it. The library adds no handlers
(including NullHandler), levels, or filters; propagation and logging.lastResort
keep their standard behavior.
Lifecycle and resource changes use INFO, per-message/RPC/settlement tracing uses DEBUG, expected RPC anomalies use WARNING, and internal I/O or codec failures use ERROR. Payloads, results, headers, credentials, exception text, and tracebacks are omitted. Exceptions still propagate; application-side logging may duplicate a library record. A publish return does not mean consumer processing completed, and an RPC response timeout does not cancel delivered work.
The Python samples configure a root DEBUG console handler only in their main entry
points. Its filter accepts Filter('Sample') or
Filter('DefStream.Broker.Implements.RabbitMQ'), including dotted descendants only;
Pika and core logs are excluded. Applications configure their own handlers, levels,
and filters at a chosen ancestor (DefStream, DefStream.Broker, etc.) or class.
Connection configuration
| Option | Meaning |
|---|---|
host, port |
RabbitMQ address and AMQP port |
username, password |
Credentials |
vhost |
Virtual host; defaults to / |
ssl |
Enable TLS; defaults to False |
timeout |
Blocked-connection timeout, in milliseconds |
heartbeat |
The current implementation passes int(heartbeat * 2 / 1000) seconds to Pika |
encode, decode |
Message codecs; JSON by default |
timeout is not a whole-operation deadline.
Configure and service connection heartbeats appropriately for task and idle periods.
Use connections and their resources only on the owning thread; do not
share them across threads or processes.
Process tasks with a Worker
This executable example provisions a task queue and error queue, then consumes tasks.
from signal import SIGINT, signal
from DefStream.Broker.Implements.RabbitMQ import (
Configuration,
Connection,
Reject,
Subscription,
)
from DefStream.Worker import Run, ThreadPool, Worker, WorkerSetup, declare
class Add(
Run,
parameters=Subscription(
queue='defstream.example.rabbitmq.add',
qos=4,
error='defstream.example.rabbitmq.add.error',
),
on_error=Reject(),
):
def run(
self,
a: int,
b: int,
):
return a + b
class Prepare(WorkerSetup):
def __call__(
self,
worker: Worker,
) -> None:
for task in worker.tasks:
declare(
task=task,
broker=worker.broker,
)
if __name__ == '__main__':
worker = Worker(
Connection(Configuration(
host='127.0.0.1',
port=5672,
username='guest',
password='guest',
)),
ThreadPool(4),
setups=Prepare(),
)
worker.add(Add)
signal(
SIGINT,
lambda *_: worker.stop(),
)
worker.run()
Send {'a': 10, 'b': 20} to the same queue from another process.
The Worker ACKs after execution and completion hooks finish, and applies
the selected failure policy on error. Omitting on_error defaults to Requeue().
Subscription options:
queue: task queue name.qos: this subscription's unacknowledged-delivery limit. The consumer default is 1; 0 means unlimited. This is separate from shared Worker pool capacity.error: error queue to create and bind throughdeclare(); a DLQ for rejected messages.offset: integer offset for RabbitMQ stream subscriptions.max_priority: maximum priority for a classic task queue; an integer in1..255orNone.priority: default message priority; an integer in0..max_priorityorNone. Setting only this value setsmax_priorityto255.
Constructing a Subscription or calling consumer.subs() does not declare queues.
Task declare() provisions ordinary durable queues and an optional DLQ,
not stream queues or Exchange bindings. Incompatible existing queue options
fail explicitly; queues are not automatically deleted and recreated.
Worker task priorities
Publish tasks with DefStream.Worker.emit or connection.emit(body, parameters).
The task publication API is emit. Low-level send() and RPC call() on queues, exchanges,
and streams remain unchanged.
Set the default priority and queue max_priority together in the task's
Subscription. There is no separate priority setting on Run.
class PriorityAdd(
Add,
parameters=Subscription(
queue='defstream.example.rabbitmq.priority.add',
qos=1,
max_priority=5,
priority=2,
),
):
pass
- The Worker API
declare(task=PriorityAdd, broker=connection)declares the queue withx-max-priority=5while preserving existing DLQ settings. emit(..., task=PriorityAdd, broker=connection)andrequest(..., task=PriorityAdd, broker=connection)use the task default of2. Passpriority=0or another valid value to override it for an individual call.- Setting only the default sets the queue maximum to
255. Setting only the maximum leaves the default asNone. Without a per-call value either, the AMQP priority property is omitted. - Worker calls validate against the task's effective maximum. If priority is
specified only at call time, only the
0..255range is validated; the caller must provision a priority queue beforehand. - The existing low-level
Queue.send,Exchange.send, andQueue.callalso acceptpriority=. These APIs do not receive a task definition and therefore do not validate against a task-specific maximum. They accept integers in0..255and rejectbool. - Larger maximum values increase resource costs; configure only the levels you need.
- Task definitions declaring the same queue must use the same maximum. Redeclaring an ordinary queue or a queue with a different maximum propagates a RabbitMQ error. Queues are not automatically deleted. Switch to a new queue, or preserve pending messages before safely recreating the queue and migrating producers and consumers together.
- Higher values are delivered first, but prefetched messages and running tasks are not preempted. Redelivery and multiple consumers prevent guarantees about completion order, and lower-priority messages can wait indefinitely. Streams and quorum queues are outside the scope of this feature.
Synchronous RPC
Call a queue served by a running Worker to receive the result of Run.run().
Use the following between connect() and close() in the direct-connection example.
result = connection.queue('defstream.example.rabbitmq.add').call(
{'a': 10, 'b': 20},
timeout=5,
)
print(result)
timeoutis in seconds. Missing replies raiseTimeoutError. This does not cancel delivered work or strictly bound total connection and cleanup time.- Each call owns a dedicated connection and temporary reply queue, cleaned up on exit.
- Server rejection raises
DefStream.Broker.RemoteError. Requeue can retry without sending a final error response. - Reply publication and request ACK are not atomic. Handle duplicate execution with RPC too.
Applications sharing a Run class can also use the core helper
request(body, task=Add, broker=connection, timeout=5).
Exchanges and streams
connection.exchange(name)providessend()andcall().event=selects the routing key. Declare the Exchange, queues, and bindings first.- Exchange RPC routed to several queues can execute at every destination and returns only the first matching reply. Use a single destination for ordinary RPC.
- Configure routing with
create_exchange(),bind_exchange_to_queue(), andbind_exchange_to_exchange(). Correspondingdelete_...orunbind_...methods are available. - Use
create_stream()to provision a stream queue, then accessconnection.stream(name)withsend()andread(offset=0, timeout=1000). The read timeout is in milliseconds. Read events are ACKed automatically when iteration advances. delete_queue(),delete_stream(), anddelete_exchange()remove resources; use them only for resources owned by your application.
Operational considerations
- Design idempotent tasks for at-least-once delivery.
- Unlimited requeue can repeatedly execute permanently invalid messages. Plan DLQ and retry policies.
- Execute long tasks in the Worker's pool rather than on the consumer connection.
- When publishing from task hooks, create a separate producer connection inside the hook's thread/process.
worker.stop()waits for in-flight tasks and ACKs before closing the connection and pool. There is no forced termination or shutdown timeout.
Redis usage
Requires Python 3.9 or newer and Redis 6.2 or newer. Uses redis-py Streams, not Pub/Sub or list-based queues.
Send and receive messages
This example assumes a local Redis server at 127.0.0.1:6379.
It creates the defstream.example.redis stream and DefStream group and leaves
them in place.
from DefStream.Broker.Implements.Redis import Configuration, Connection
connection = Connection(Configuration(
host='127.0.0.1',
port=6379,
))
try:
connection.connect()
connection.create_queue(
'defstream.example.redis',
group='DefStream',
)
queue = connection.queue(
'defstream.example.redis',
group='DefStream',
)
message_id = queue.send({'message': 'Hello, DefStream!'})
print(message_id)
event = queue.pop(timeout=1000)
if event is not None:
print(event.id, event.body)
event.ack()
finally:
connection.close()
connect() verifies connectivity with PING and propagates failures.
The default codec stores the entire message as UTF-8 JSON in a single body
stream field. Legacy field-per-value streams are not compatible.
Set custom codecs through Configuration(encode=..., decode=...).
Connection configuration
| Option | Default and purpose |
|---|---|
host, port |
127.0.0.1, 6379 |
username, password |
Credentials; password-only authentication is supported |
secure |
Enable TLS; defaults to False |
database |
Database number; defaults to 0 |
type |
ConnectionType.Pool or ConnectionType.Cluster |
max |
Maximum connections; defaults to 10 |
timeout |
Connect/socket timeout in seconds; defaults to 5 |
Import ConnectionType from the adapter package.
The default Pool mode uses redis-py's connection pool.
Cluster mode uses Redis Cluster and requires database=0.
Read production credentials from environment variables or a secret store.
Logging
Connection, Consumer, Queue, and Event each use a class-level logger named
DefStream.Broker.Implements.Redis.<ClassName>. Records propagate to the parent
DefStream logger and the root logger. The library does not configure handlers
or logging levels; configure output destinations and filters in your application.
Records exclude message bodies and credentials, and existing exceptions still
propagate to callers after an error is logged. Since callers may log the same
failure again, keep traceback logging at the boundary responsible for that failure.
The examples configure output only at their execution entry points, selecting
the Sample and Redis logger hierarchies at DEBUG level.
Consumer groups and history
connection.queue(name, group='DefStream')andconnection.stream(name, group='DefStream')return the same stream-backedQueuetype.create_queue()andcreate_stream()prepare the stream and group. Publishing/subscribing can also create the required resources lazily.- New groups start at
0-0, so messages published before subscription can be consumed. - Consumers in the same group share messages; separate groups each receive them. Preserve the group name to continue an existing deployment's position and pending state.
pop(timeout=1000)waits for one group delivery. Its timeout is in milliseconds. A positive timeout returnsNonewhen no delivery arrives;Noneor0waits indefinitely.
Read stream history without changing group state using this snippet between
connect() and close() in the direct-connection example:
for event in connection.stream('defstream.example.redis').read(
offset=0,
timeout=1000,
):
print(event.id, event.body)
read() reads existing history and new messages with a total millisecond timeout.
None or 0 tails indefinitely. Close the iterator to stop reading.
History events are not group deliveries and cannot be ACKed or requeued.
A string stream ID as offset resumes after that ID.
Process tasks with a Worker
This executable example prepares a task stream and group, then consumes tasks.
from signal import SIGINT, signal
from DefStream.Broker.Policies import Reject
from DefStream.Broker.Implements.Redis import Configuration, Connection, Subscription
from DefStream.Worker import Run, ThreadPool, Worker, WorkerSetup, declare
class Add(
Run,
parameters=Subscription(
queue='defstream.example.redis.add',
group='DefStream',
),
on_error=Reject(),
):
def run(
self,
a: int,
b: int,
):
print(a + b)
class Prepare(WorkerSetup):
def __call__(
self,
worker: Worker,
) -> None:
for task in worker.tasks:
declare(
task=task,
broker=worker.broker,
)
if __name__ == '__main__':
worker = Worker(
Connection(Configuration(
host='127.0.0.1',
port=6379,
)),
ThreadPool(4),
setups=Prepare(),
)
worker.add(Add)
signal(
SIGINT,
lambda *_: worker.stop(),
)
worker.run()
Send {'a': 10, 'b': 20} to the same stream from another process.
The Worker ACKs after execution and completion hooks finish; it does not return
the result as an RPC reply. The default failure policy is Requeue() if
on_error is omitted; this example selects Reject().
You can also use the task API emit(body, task=Add, broker=connection);
the caller must connect and close the connection.
The task helper and connection.send(body, parameters) have been renamed to emit.
Use connection.emit(body, parameters).
Low-level queue.send(body) remains unchanged.
ACK, rejection, and retries
| Operation | Effect |
|---|---|
ack() |
XACK for the group; preserves stream history |
reject() / Reject() |
XACK for the group; no built-in DLQ; preserves history |
nack() / Requeue() |
Append a new message and ACK the original pending entry |
Requeue uses an atomic single-key Lua script, but the copy has a new stream ID. The new message is visible to every group, not only the failing group. Use separate retry streams if retries need group isolation. There is no automatic retry-count limit or built-in error stream.
Direct consumer users can configure:
| Option | Meaning |
|---|---|
connection.consumer(qos=1) |
Total local unacknowledged-delivery limit across subscriptions |
reclaim_idle=60000 |
Idle milliseconds before pending-entry reclamation; defaults to 60 seconds |
reclaim_idle=None |
Disable automatic pending recovery |
The default Worker uses the default consumer settings. Another consumer may reclaim a long task while it is still running; plan task duration and recovery together. Shutdown leaves unfinished deliveries pending, without ACK, for later recovery.
Operational considerations
- Delivery is at least once. Make side effects safe to execute more than once.
- ACK/rejection preserves history; retries add entries. Manage retention externally and do not indiscriminately trim pending entries.
delete_queue()anddelete_stream()delete the stream key itself. Do not remove shared streams/groups in cleanup code without ownership.- Connection, decoding, and handler failures propagate; there is no automatic reconnection.
- Exchanges and RPC are not supported. Do not use
request()or console RPC. - Redis Cluster is implemented, but current integration tests cover standalone Redis.
- The Worker's connection and pool belong to one run.
worker.stop()waits for tasks and closes resources.
Broker selection and operational considerations
| Feature | RabbitMQ | Redis Streams |
|---|---|---|
| Queue messaging | Supported | Streams and consumer groups |
| Exchange routing | Supported | Not supported |
| Stream history reads | Supported | Supported |
| Task RPC | Supported | Not supported |
| Error destination | Explicitly declared DLQ | No built-in destination |
- Use RabbitMQ connections/resources only on their owning thread, not from task threads sharing the consumer connection.
- Redis requires 6.2 or newer. Consumers in one group share deliveries; different groups each receive the stream.
- Redis requeue appends a copy and ACKs the original, making the new message visible to other groups too.
- Redis reclaims pending entries after 60 seconds of idle time by default. Another consumer may reclaim an active long task.
Direct consumer users can adjust
reclaim_idleor disable it withNone; the default Worker uses the default setting. - Redis ACK/rejection preserves history. Manage retention externally and avoid removing pending entries.
- Do not assume automatic network reconnection or exactly-once execution. Design application restart, idempotency, retry, and error-queue policies.
License
DefStream is licensed under the GNU General Public License version 3. You may use, modify, and redistribute the software under its terms. Distributed modified versions must preserve the same license and provide the corresponding source code. The software is provided without warranty.
Metadata
Release files for DefStream 0.1.17
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 | |
|---|---|---|---|---|
| defstream-0.1.17.tar.gz | 268.2 kB | Without provenance | Details |
Built distributions (wheels)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| defstream-0.1.17-60-py3-none-any.whl | Python 3 | none | any | Details |
| defstream-0.1.17-58-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 496.4 kB
Release files / defstream-0.1.17.tar.gz
| Download URL | defstream-0.1.17.tar.gz |
|---|---|
| Size | 268.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
fbd609a7724f5cf351d9e540084fea5fba337d99169ef121aa5d204a8bc6da1a
|
|
BLAKE2b-256 checksum How to use checksums |
04955c9f9e122fab5cc3ede8c316ebc98a9ad89a04fc0d6b16b888878e3f649a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Release files / defstream-0.1.17-60-py3-none-any.whl
| Download URL | defstream-0.1.17-60-py3-none-any.whl |
|---|---|
| Size | 114.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
11cc49506b80ae4492fc1222e4bcc9132f060fa4e2d295f8704ccea7f110ed42
|
|
BLAKE2b-256 checksum How to use checksums |
81358adf12dbaf8ccff8de06c234a9619f9339e080a00fb3fe7a3b2a2a02e72d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Release files / defstream-0.1.17-58-py3-none-any.whl
| Download URL | defstream-0.1.17-58-py3-none-any.whl |
|---|---|
| Size | 114.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
79a402d00350023de6d99b1adc73c3b6788a20142595badaab2386bdac3a63bd
|
|
BLAKE2b-256 checksum How to use checksums |
304d479be1789b00cccc3af3de860a90d8bf99837d9365abe896fca0c512d950
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Security for DefStream 0.1.17
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A 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.