API Reference

Gourd.subscribe(topic)

A decorator that registers a function as a handler for an MQTT topic. When a message arrives on a matching topic, Gourd calls the function with a GourdMessage argument.

@app.subscribe('sensors/temperature')
def handle_temp(message):
    app.log.info(f'Temperature: {message.text}')

Registering the handler also subscribes to the topic on the broker — you don't need to call anything else.

Wildcard Patterns

MQTT supports two wildcard characters in topic filters:

Wildcard Meaning Example pattern Matches Does not match
+ Single level (one segment between /) sensors/+/temp sensors/room1/temp sensors/room1/humidity, sensors/a/b/temp
# Multi-level (parent and all subtopics; must be last) sensors/# sensors, sensors/room1, sensors/room1/temp other/topic
# alone Everything # any topic

Malformed patterns (e.g. foo+bar, home/#/temp) raise ValueError at match time.

Multiple Handlers

You can register multiple functions for the same topic. They are called in registration order. An exception in one handler does not prevent others from running.

@app.subscribe('sensors/temperature')
def log_temp(message):
    app.log.info(message.text)

@app.subscribe('sensors/temperature')
def store_temp(message):
    db.insert(message.payload)

One Handler for Multiple Topics

Stack decorators to register the same function for multiple topics:

@app.subscribe('sensors/temperature')
@app.subscribe('sensors/humidity')
def handle_sensor(message):
    app.log.info(f'{message.topic}: {message.text}')

Gourd.thread(*args, **kwargs)

A decorator factory that registers a function to be started in a background thread when the app runs. Any arguments are passed to the function when the thread starts. Threads run as daemons.

@app.thread()
def poll_sensor():
    while True:
        value = read_sensor()
        app.publish('sensors/temperature', str(value))
        time.sleep(10)

Pass arguments that should be forwarded to the function:

@app.thread('sensors/temperature', interval=10)
def poll_sensor(topic, interval=5):
    while True:
        app.publish(topic, str(read_sensor()))
        time.sleep(interval)

Threads are started when run_forever() or loop_start() is called — not at decoration time. This means you can safely use @app.thread() at module level without worrying about when the MQTT connection is established.


Gourd.publish(topic, payload=None, *, qos=None, **kwargs)

Publishes a message to the MQTT broker.

app.publish('lights/kitchen', 'ON')
app.publish('lights/kitchen', 'ON', retain=True)
app.publish('lights/kitchen', None, retain=True)  # delete retained message

Gourd.run_forever()

Connects to the broker and runs the MQTT loop in the current thread until interrupted.

if __name__ == '__main__':
    app.run_forever()

This is the normal way to run a Gourd app. It handles KeyboardInterrupt (Ctrl-C) gracefully and triggers clean shutdown via the registered atexit handler.

The gourd CLI calls run_forever() for you when you use the gourd module:app command.


Gourd.loop_start() / Gourd.loop_stop()

Runs the MQTT loop in a background thread, returning immediately so the calling thread can do other work.

app.loop_start()

try:
    while True:
        value = read_sensor()
        app.publish('sensors/temperature', str(value))
        time.sleep(5)
except KeyboardInterrupt:
    pass
finally:
    app.loop_stop()

Gourd.connect()

Connects to the MQTT broker. Called automatically by run_forever() and loop_start().

Only call this directly if you are managing the paho-mqtt loop yourself.


GourdMessage

The object passed to every subscriber function. It wraps the underlying paho-mqtt message with convenient properties.

Properties

message.topicstr

The exact topic the message arrived on (not the wildcard pattern used to subscribe).

@app.subscribe('sensors/+/temp')
def handle(message):
    print(message.topic)  # e.g. "sensors/room1/temp"

message.payload — original MQTT payload type (typically bytes)

The raw payload from paho-mqtt, unchanged.

message.textstr

The payload decoded as UTF-8 text. For byte payloads, the payload is decoded as UTF-8 and leading and trailing whitespace is stripped. If a byte payload is not valid UTF-8, accessing message.text raises UnicodeDecodeError.

Use message.text only when the payload is known to be UTF-8 text. For arbitrary or binary payloads, use message.payload instead. message.jsondict | None

If the payload is a valid UTF-8 encoded JSON object (starts with { and ends with }), this is the parsed result. Otherwise it is None.

Important: message.json returns None when the payload is not valid UTF-8, is not valid JSON, or is not a JSON object — it never raises an exception. None is falsy, so use if message.json to check whether parsing succeeded.

@app.subscribe('sensors/#')
def handle(message):
    if message.json:
        temp = message.json.get('celsius')
    else:
        # plain string payload or undecodable payload
        raw = message.text

All other attributes delegate to the underlying paho.mqtt.client.MQTTMessage, including qos, retain, mid, timestamp, etc.


app.log

A standard Python logging.Logger instance. It is pre-configured to send log output to both the console and the MQTT debug topic ({mqtt_topic}/debug by default, where mqtt_topic defaults to {app_name}/{hostname}).

app.log.debug('detailed trace')
app.log.info('normal status')
app.log.warning('unexpected but handled')
app.log.error('something failed')

See Configuration — MQTT Logging for options.