Skip to main content

FranzMQ

FranzMQ is a structured MQTT communication library for edge and cloud applications. It builds on paho-mqtt and introduces typed payloads, hierarchical topics, priority-based callbacks, and a command/acknowledge pattern -- all with optional ISA-95 topic modeling and TLS auto-configuration.

Features

  • Typed payloads using Python dataclasses with automatic JSON encoding/decoding
  • Priority-based concurrent callbacks for message handling
  • Command/acknowledge pattern for confirmed request-response over MQTT
  • Class-based topic definitions for type-safe, hierarchical topic construction
  • ISA-95 topic modeling for enterprise-ready messaging structures
  • Pinned-key authentication — the identity key is the credential, no CA
  • MQTT 5 with broker reason codes surfaced as exceptions
  • MQTT-based logging with seamless integration

Installation

pip install franzmq

Quick Start

from franzmq import Client, Topic, Metric

client = Client.autocreate_and_connect(client_id="my-client")

topic = Topic(payload_type=Metric, node_id="my-client", context=("sensor", "temperature"))
metric = Metric(value=22.5)

client.publish(topic, metric)

Topics

FranzMQ topics follow the structure {prefix}/{version}/{_PayloadType}/{node_id}/{context...}.

node_id is the identity the message is published under — the node or machine the record belongs to. It is required and always sits at level 4, directly after the payload type: brokers that enforce an identity rule match that level against the authenticated client, and every hop that re-mounts a record rewrites only the path after it. Subscription filters may use + there to span nodes.

Basic Topic

from franzmq import Topic, Metric

topic = Topic(payload_type=Metric, node_id="machine-1", context=("sensor", "temperature"))
# example/v1/_Metric/machine-1/sensor/temperature

ISA-95 Topic

For enterprise-level communication with ISA-95 hierarchy levels:

from franzmq import Topic, Metric, Isa95Topic, Isa95Fields

basic_topic = Topic(payload_type=Metric, node_id="machine-1", context=("sensor", "temperature"))

isa95_fields = Isa95Fields(
    enterprise="ent1",
    site="s1",
    area="a1",
    production_line="pl1",
    work_cell="wc1",
    origin_id="origin1"
)
isa95_topic = Isa95Topic.from_topic(basic_topic, isa95_fields)
# example/v1-isa95/ent1/s1/a1/pl1/wc1/origin1/_Metric/sensor/temperature

Typed Payloads

All messages use structured dataclasses that encode/decode automatically to/from JSON. The following payload types are included:

Payload Purpose
Metric Timestamped measurement values
Log Structured log entries (level, message, module, etc.)
ServiceDetails Service registration with type and metadata
Cmd Command with correlation ID and expiration
Ack Acknowledgement with result code and message

Custom payloads extend the Payload base class:

from dataclasses import dataclass
from franzmq import Payload

@dataclass
class SensorReading(Payload):
    sensor_id: str
    value: float
    unit: str

Callback System

Subscribe to topics and register callbacks with optional priority. Callbacks receive a single message: Message argument containing the decoded topic and payload.

from franzmq import Message

def on_metric(message: Message):
    print(f"Received: {message.payload.value} on {message.topic}")

client.subscribe(topic, qos=1, callback=on_metric, priority=10)

Callbacks are ordered by descending priority (higher numbers run first). Callbacks with the same priority are executed concurrently in separate threads.

Command/Acknowledge Pattern

FranzMQ supports request-response semantics over MQTT. One command gets one acknowledgement, carrying the broker's own result codes.

Flow

Sender                          Receiver
  |                               |
  |-- Cmd (correlation_id) ------>|
  |                               | (check expiration)
  |                               | (execute callback)
  |<-- Ack (result_code) ---------|
  |                               |

The sender waits until the command's own expiry. A command that expires before it is executed is acked 498 without running the callback.

Result codes

Code Meaning
200 Done
409 Conflict — the request contradicts current state
422 Invalid — the request could not be understood
498 Expired before execution
500 Internal error, including an exception in the callback

Sending commands

publish_command subscribes to the ack topic, publishes the command, waits for the ack, and returns it.

from franzmq import Client, Topic, Cmd, Ack

client = Client.autocreate_and_connect(client_id="sender")

cmd_topic = Topic(
    prefix="myproject",
    payload_type=Cmd,
    node_id="device1",
    context=("device1", "settings")
)

ack = client.publish_command(
    topic=cmd_topic,
    command={"enabled": True, "interval_ms": 500},
    validity_duration=30.0,
)

if ack.result_code >= 500:
    raise Exception(f"Command failed: {ack.message}")

Receiving commands

subscribe_to_command handles expiry and acknowledgement automatically. The callback receives a Message and returns a result code.

from franzmq import Client, Topic, Cmd, Message

client = Client.autocreate_and_connect(client_id="receiver")

cmd_topic = Topic(
    prefix="myproject",
    payload_type=Cmd,
    node_id="device1",
    context=("device1", "settings")
)

def handle_settings(message: Message) -> int:
    settings = message.payload.command
    apply_settings(settings)
    return 200  # success

client.subscribe_to_command(
    topic=cmd_topic,
    callback=handle_settings,
    qos=1,
)

The callback can return:

  • None -- treated as 200 (success)
  • An int result code
  • A (int, str) tuple of (result_code, message)

Commands for the same topic are executed sequentially via an internal queue.

Custom command payloads

Extend Cmd for typed command payloads:

from dataclasses import dataclass, field
from franzmq import Cmd

@dataclass
class DeviceSettingsCmd(Cmd):
    command: dict = field(default_factory=dict)

Then use DeviceSettingsCmd as the topic's payload_type.

Class-Based Topic Definitions

For projects with many topics, use TopicBase and classproperty to define hierarchical topic trees:

from franzmq import TopicBase, classproperty, Metric
from franzmq.data_contracts.base import ServiceDetails

class DeviceTopic(TopicBase):
    prefix = "myproject"
    version = "v1"
    node_id = "device1"
    context = ()

    @classproperty
    def State(cls):
        return cls._topic(["state"], payload_type=ServiceDetails)

    @classproperty
    def Temperature(cls):
        return cls._topic(["temperature"], payload_type=Metric)

Access topics as class attributes:

DeviceTopic.State        # myproject/v1/_ServiceDetails/device1/state
DeviceTopic.Temperature  # myproject/v1/_Metric/device1/temperature

node_id may be set on the class (as above) or passed per topic: cls._topic(["temperature"], payload_type=Metric, node_id="device2").

Nested hierarchies use _parent_class_name and _prefix to compose topic paths from parent classes.

Logging over MQTT

Enable MQTT-based logging by calling:

import logging

client.configure_mqtt_logger(level=logging.INFO)

Auto Configuration via Environment Variables

Uses python-decouple for environment configuration.

Variable Required Default Description
MACHINE_KEY Yes -- Path to the ed25519 identity key — the credential
NODE_ID No client id Identity: MQTT username, client id, and level 4 of every topic
MQTT_IP No broker Broker hostname
MQTT_PORT No 1883 Broker port
MQTT_SESSION_EXPIRY No never expires Seconds the broker keeps the session after a disconnect

Authentication

The broker authenticates by pinned key, not by a certificate authority. Each client has an ed25519 private key; on connect it presents a certificate minted from that key in-process, and the broker looks up the public key inside among the identities enrolled there. An unknown key is refused. Nothing validates the broker in return, because no authority exists to validate it against — trust runs the other way.

Generate a key with colca-keygen and enroll its public key at the node before the client's first connect.

Rejected publishes

At QoS ≥ 1 publish() waits for the PUBACK and raises PublishRejected when the broker answers with a failure reason code:

Code Meaning
0x90 topic name invalid — the broker does not know this contract
0x99 payload format invalid — the payload failed the contract schema
0x87 not authorized — no grant covers this topic
0x89 quota exceeded — the destination is draining
from franzmq.errors import PublishRejected

try:
    client.publish(topic, metric, qos=1)
except PublishRejected as err:
    logger.error("%s rejected: %s", err.topic, err.reason)

Only one QoS ≥ 1 publish is in flight at a time. That is deliberate: unbounded in-flight QoS-1 lets a reconnect replay an unacked message after a newer one is already on the wire, and it is what makes a reason code attributable to the message that earned it. Pass wait=False for fire-and-forget.

License

MIT License

Release files for franzmq 0.6.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for franzmq 0.6.1
File Size Uploaded
franzmq-0.6.1.tar.gz 32.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for franzmq 0.6.1
File Interpreter ABI Platform
franzmq-0.6.1-py3-none-any.whl Python 3 none any Details

Total release size: 58.3 kB

Release files / franzmq-0.6.1.tar.gz

Download URL franzmq-0.6.1.tar.gz
Size 32.8 kB
Tags Source
SHA-256 checksum
How to use checksums
09fb42f25cc39cbe85c88aaaf701ccd6b3a341ffa9eff1672f9b1fadc8f0501f
BLAKE2b-256 checksum
How to use checksums
3591f2563f0cfd8a5bb2fd1c8700f7c4086c27e2d9a6244ddba23c7a870e67e4
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 Aug 17, 2026.

Transparency log

Release files / franzmq-0.6.1-py3-none-any.whl

Download URL franzmq-0.6.1-py3-none-any.whl
Size 25.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
50a85111ea08ac4c7cabef68d28e466ada3d85da7e0b14afd93932f63b6901fa
BLAKE2b-256 checksum
How to use checksums
a2ee0dccd254d7a67b96f3ed9254caf5bc20b7cffe31f097c9affc675e5595e3
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 Aug 17, 2026.

Transparency log

Release history Release notifications | RSS feed

0.6.4

2 release files

0.6.3

2 release files

0.6.2

2 release files

This release

0.6.1 This release

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.1

2 release files

0.4.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page