Creating a template

Found an error? Have a suggestion?Edit this page on GitHub

This tutorial teaches you how to create a simple generator template using a Python MQTT client. You'll use the AsyncAPI document and the template you develop to generate Python code. Additionally, you'll create template code with a reusable component to reuse the custom functionality you create and test your code using an MQTT client.

Suppose you can only sleep when the AC in your bedroom is set to 22 °C, and you can't sleep when the temperature drops or rises above that. You can install a smart monitor in your bedroom that keeps track of the temperature and notifies you to adjust it to your optimum temperature when it fluctuates. You will create a template to alert you when the bedroom's temperature fluctuates from 22 °C.

In this tutorial:

  • You'll use the Eclipse Mosquito MQTT broker, which you'll connect to subscribe and publish messages using an MQTT client.
  • You'll use Python Paho-MQTT as the MQTT client in this project.
  • You'll create a React template that will use the MQTT broker to allow you to monitor your bedroom's temperature and notify you when the temperature drops or rises above 22 °C.
  • Lastly, create a reusable component for the output code's sendTemperatureDrop and sendTemperatureRise functions.

Background context

There is a list of community maintained templates, but what if you do not find what you need? In that case, you'll create a user-defined template that generates custom output from the generator. Before you create the template, you'll need to have an AsyncAPI document that defines the properties you want to use in your template to test against. In this tutorial, you'll use the following template saved in the test/fixtures/asyncapi.yml file in your template project directory.

1
2asyncapi: 2.6.0
3
4info:
5  title: Temperature Service
6  version: 1.0.0
7  description: This service is in charge of processing all the events related to temperature.
8
9servers:
10  dev:
11    url: test.mosquitto.org
12    protocol: mqtt
13
14channels:
15  temperature/changed:
16    description: Updates the bedroom temperature in the database when the temperatures drops or goes up.
17    publish:
18      operationId: temperatureChange
19      message:
20        description: Message that is being sent when the temperature in the bedroom changes.
21        payload:
22          type: object
23          additionalProperties: false
24          properties:
25            temperatureId:
26              type: string
27components:
28  schemas:
29    temperatureId:
30      type: object
31      additionalProperties: false
32      properties:
33        temperatureId:
34          type: string
Remember
  • To generate code, use the AsyncAPI CLI. If you don't have the CLI installed, follow CLI installation guide.
  • If you are new to AsyncAPI Generator, check out the following docs: template development, which explains the minimum requirements for a template and possible features.

Overview of steps

  1. Create a new directory for your template named python-mqtt-client-template.
  2. Install the AsyncAPI CLI using the command npm install -g @asyncapi/cli.
  3. Create a new folder test/fixtures with a file named asyncapi.yml in your fixtures directory. This file is used to define the structure of your template. You can copy the above example and paste it in your asyncapi.yml document.
  4. Create a new file named package.json in your python-mqtt-client-template directory. This file is used to define the dependencies for your template.
  5. Create a new folder python-mqtt-client-template/template. Create a new file named index.js in your template directory. This file is used to define the logic for your template.
  6. Create a test.py file to validate the logic of your application. Don't worry about this file for now. The tutorial will tell you how to create it later.

Now your directory should look like this:

1python-mqtt-client-template 
2├── template
3   └── index.js
4├── test
5   └── fixtures
6       └── asyncapi.yml
7└── package.json

Lets break it down:

package.json file

The package.json file is used to define the dependencies for your template. Add the following code snippet to your package.json file:

1{
2  "name": "python-mqtt-client-template",
3  "version": "0.0.1",
4  "description": "A template that generates a Python MQTT client using MQTT.",
5  "generator": {
6    "renderer": "react",
7    "apiVersion": "v1",
8    "generator": ">=1.10.0 <2.0.0",
9    "supportedProtocols": ["mqtt"]
10  },
11  "dependencies": {
12    "@asyncapi/generator-react-sdk": "^0.2.25"
13  },
14  "devDependencies": {
15    "rimraf": "^5.0.0"
16  }
17}

Here's what is contained in the code snippet above:

  • name -the name of your template.
  • version - the current version of your template.
  • description - a description of what your template does.
  • generator - specify generator specific configuration.
    • renderer - can either be react or nunjucks. In this case the generator will pass your template to the react render engine to generate the output.
    • apiVersion - specifies which major version of the Parser-API your template will use.
    • generator - a string representing the generator version-range your template is compatible with.
    • supportedProtocols - A list that specifies which protocols are supported by your template.
  • dependencies - specifies which version of @asyncapi/generator-react-sdk should be used.

Navigate to the **python-mqtt-client-template directory. Run the command npm install on your terminal to install the dependencies specified in package.json.

index.js file

The index.js file is used to define the logic for your template. Inside the template folder, create an index.js file and add the code snippet below:

1//1
2import { File } from '@asyncapi/generator-react-sdk'
3//2
4export default function ({ asyncapi }) {
5//3
6  return <File name="client.py">{asyncapi.info().title()}</File>
7}

The code snippet above does the following:

  1. Import the generator-react-sdk dependency.
  2. The asyncapi argument is an instance of the AsyncAPI Parser. It will allow you to access the content of the AsyncAPI document in your template using helper functions.
  3. The asyncapi.info().title() is using the info() helper function to return the info object from the AsyncAPI document illustrated in the code snippet below:
1info:
2  title: Temperature Service
3  version: 1.0.0
4  description: This service is in charge of processing all the events related to temperature.

The asyncapi.info().title() returns Temperature Service.

Test using AsyncAPI CLI

To see this in action, navigate to the python-mqtt-client-template directory. Then, run asyncapi generate fromTemplate test/fixtures/asyncapi.yml ./ -o test/project command on your terminal. If successful, you'll see the message below on your terminal:

1Generation in progress. Keep calm and wait a bit... done
2Check out your shiny new generated files at test/project.

Navigating to the test/project directory. You should see a client.py file; the only content is Temperature Service.

Let's break down the previous command:

  • asyncapi generate fromTemplate is how you use AsyncAPI generator via the AsyncAPI CLI.
  • test/fixtures/asyncapi.yml points to your AsyncAPI document.
  • ./ specifies the location of your template.
  • -o specifies where to output the result.

Creating a template

You will create an MQTT-supported template that will generate a Python client from the template and the AsyncAPI document above.

In this section, you'll:

  1. Write the MQTT client code.
  2. Write code to test the client works.
  3. Update the template to use the client code.
  4. Setup a script to help you run this code.
  5. Template your code.

1. Create the client

Here is the sample code to be pasted in the client.py you generated above running the asyncapi generate fromTemplate test/fixtures/asyncapi.yml ./ -o test/project command. It uses the paho-mqtt package.

1# 1
2import paho.mqtt.client as mqtt
3# 2
4mqttBroker = "test.mosquitto.org"
5
6class TemperatureServiceClient:
7    def __init__(self):
8      # 3 
9        self.client = mqtt.Client()
10      # 4
11        self.client.connect(mqttBroker)
12
13
14    def sendTemperatureChange(self, id):
15      # 5
16        topic = "temperature/changed"
17      # 6 
18        self.client.publish(topic, id)

Make sure you have the Paho-MQTT library installed. You can install it using pip with the pip install paho-mqtt==1.6.1 command. Please note that this tutorial is based on Paho-MQTT version 1.6.1. The Paho-MQTT library has since been updated to version 2.0.0, which includes changes that are not covered in this tutorial. To ensure compatibility and to follow along without any issues, please install version 1.6.1 of the Paho-MQTT library.

Let's break down the previous code snippet:

  1. Imports the MQTT module from the Paho package, which provides the MQTT client functionality.
  2. Assigns the MQTT broker address test.mosquitto.org to the variable MQTT broker. This specifies the location where the MQTT client will connect to.
  3. Defines an instance of the MQTT client object. This object will be used to establish a connection with the MQTT broker and perform MQTT operations.
  4. Defines that on client instance creation, it connects to the broker.
  5. The sendTemperatureChange is a function the client user invokes to publish a message to the broker, and its specific topic.

In summary, this code sets up an MQTT client using the Paho-MQTT library. It connects to the test.mosquitto.org MQTT broker, and the sendTemperatureChange() method publishes temperature change information to the temperature/changed topic whenever called.

2. Test the client

You'll interact with the Temperature Service using the client module you created above. You'll create an instance of the client using client = TemperatureServiceClient() and then use client.sendTemperatureChange function to publish messages that Temperature Service is subscribed to. Create a test/project/test.py file in your project and add the code snippet below:

Now your directory should look like this:

1python-mqtt-client-template
2├── template
3   └── index.js
4└── test
5    ├── fixtures
6       └── asyncapi.yml
7    └── project
8        ├── client.py
9        └── test.py
1from client import TemperatureServiceClient
2from random import randrange
3import time
4
5client = TemperatureServiceClient()
6
7id_length = 8
8min_value = 10**(id_length-1)  # Minimum value with 8 digits (e.g., 10000000)
9max_value = 10**id_length - 1  # Maximum value with 8 digits (e.g., 99999999)
10
11while True:
12    randomId = randrange(min_value, max_value + 1)
13    client.sendTemperatureChange(randomId)
14    print("New temperature detected " + str(randomId) + " sent to temperature/changed")
15    time.sleep(1)
16

Run the code above in your terminal using the command python test.py. You should see output similar to the snippet below logged on your terminal:

1New temperature detected 64250266 sent to temperature/changed
2New temperature detected 36947728 sent to temperature/changed
3New temperature detected 72955029 sent to temperature/changed

To make sure your test.py and client code works check if the broker really receives temperature-related messages. You can do it using an MQTT CLI using docker. Run the command docker run hivemq/mqtt-cli sub -t temperature/changed -h test.mosquitto.org in your terminal. It will download the image if you don't have it locally, then the CLI will connect to the broker, subscribe to the temperature/changed topic and then output the temperature ids on the terminal.

3. Update the template with client code

Open index.js and copy the content of client.py and replace {asyncapi.info().title()} with it. It should look like the code snippet below now:

1import { File } from '@asyncapi/generator-react-sdk';
2
3export default function ({ asyncapi }) {
4  return (
5    <File name="client.py">
6      {`import paho.mqtt.client as mqtt
7
8mqttBroker = "test.mosquitto.org"
9
10class TemperatureServiceClient:
11    def __init__(self):
12        self.client = mqtt.Client()
13        self.client.connect(mqttBroker)
14
15
16    def sendTemperatureChange(self, id):
17        topic = "temperature/changed"
18        self.client.publish(topic, id)`}
19    </File>
20  )
21}

4. Write script to run the test code

In package.json you can have the scripts property that you invoke by calling npm run <your_script>. Add these scripts to package.json:

1    "scripts": {
2        "test:clean": "rimraf test/project/client.py",
3        "test:generate": "asyncapi generate fromTemplate test/fixtures/asyncapi.yml ./ --output test/project --force-write",
4        "test:start": "python test/project/test.py",
5        "test": "npm run test:clean && npm run test:generate && npm run test:start"
6    }

The 4 scripts above do the following:

  1. test:clean: This script uses the rimraf package to remove the old version of the file test/project/client.py every time you run your test.
  2. test:generate: This script uses the AsyncAPI CLI to generate a new version of client.py.
  3. test:start: This script runs the python code using client.py.
  4. test: This script runs all the other scripts in proper order.

Run npm test on your terminal to ensure everything works as expected.

5. Template your code

5a. Add parameters to the configuration file

You often have different runtime environments in programming, e.g., development and production. You will use different servers to spin both of these instances. You'll have two broker versions, one for production and the other for development. You have defined a dev server in the AsyncAPI document:

1servers:
2  dev:
3    url: test.mosquitto.org
4    protocol: mqtt

This will allow you to also define the broker you will use in production in the servers section above. Therefore, we can template the code mqttBroker = 'test.mosquitto.org' in index.js so the value is populated dynamically at runtime depending on the specified server environment.

The generator has a parameters object used to define parameters you use to dynamically modify your template code at runtime. It also supports the server parameter that defines the server configuration value. Navigate to package.json and add the snippet below:

1    "generator": {
2        # ...(redacted for brevity)
3        "parameters": {
4            "server": {
5              "description": "The server you want to use in the code.",
6              "required": true
7            }
8        }
9    }

"required": true: makes the parameter mandatory and once user forgets to add it to the cli command, a proper error message is yielded. You'll pass the server to be used to generate your code using --param server=dev in the AsyncAPI CLI command. Failure to which you'll get an error:

Generator Error: This template requires the following missing params: server.

Update your test:generate script in package.json to include the server param

"test:generate": "asyncapi generate fromTemplate test/fixtures/asyncapi.yml ./ --output test/project --force-write --param server=dev"

You can now replace the static broker from mqttBroker = 'test.mosquitto.org' to mqttBroker = "${asyncapi.servers().get(params.server).url()}" in index.js.

Now the template code looks like this:

1import { File } from '@asyncapi/generator-react-sdk';
2
3// notice that now the template not only gets the instance of parsed AsyncAPI document but also the parameters
4export default function ({ asyncapi, params }) {
5 
6  return (
7    <File name="client.py">
8      {`import paho.mqtt.client as mqtt
9
10mqttBroker = "${asyncapi.servers().get(params.server).url()}"
11
12class TemperatureServiceClient:
13    def __init__(self):
14        self.client = mqtt.Client()
15        self.client.connect(mqttBroker)
16
17
18    def sendTemperatureChange(self, id):
19        topic = "temperature/changed"
20        self.client.publish(topic, id)`}
21    </File>
22  )
23}

Run npm test to validate that your code still works as expected.

5b. Templating index.js with React

Python takes indentation very seriously, and our generated output will be Python code. We, therefore, need to make sure the indentation in index.js looks right so the generated code is indented correctly. After templating the code in index.js, it will look like the following code snippet:

1// 1
2import { File, Text } from '@asyncapi/generator-react-sdk'
3export default function ({ asyncapi, params }) {
4  return (
5    <File name="client.py">
6    // 2
7      <Text newLines={2}>import paho.mqtt.client as mqtt</Text>
8    // 3
9      <Text newLines={2}>mqttBroker = "{asyncapi.servers().get(params.server).url()}"</Text>
10    // 4
11      <Text newLines={2}>class {asyncapi.info().title().replaceAll(' ', '')}Client:</Text>
12    // 5
13      <Text indent={2} newLines={2}>
14        {`def __init__(self):
15            self.client = mqtt.Client()
16            self.client.connect(mqttBroker)`}
17      </Text>
18      </File>
19  )
20}
  1. Import the Text component that will wrap strings so they are indented properly in the output. Your import statement should now look like this: import { File, Text } from '@asyncapi/generator-react-sdk'.
  2. When the Paho module import is rendered in client.py file, it will add two extra new lines.
  3. The broker url is templated in a Text component removing the $ from the string template.
  4. Dynamically get the class name TemperatureServiceClient from the AsyncAPI document from the info object using the Parser API using the code: asyncapi.info().title() . It will return Temperature Service, then remove the spaces and add Client as a suffix.
  5. There is no templating needed in the __init__ function, there is only hardcoded information.

If you're on the fence about which templating engine you should use in your template, check out the React render engine and nunjucks render engine documentation. In the next section, you'll refactor your template to use React.

5c. Creating a reusable component

Suppose you have two channels, one to watch if the temperature drop below 22 °C and one to check if the temperature is above 22 °C, the generated output code would look like this:

1import paho.mqtt.client as mqtt
2
3mqttBroker = "test.mosquitto.org"
4
5class TemperatureServiceClient:
6
7  def __init__(self):
8              self.client = mqtt.Client()
9              self.client.connect(mqttBroker)
10
11  def sendTemperatureDrop(self, id):
12          topic = "temperature/dropped"
13          self.client.publish(topic, id)
14  def sendTemperatureRise(self, id):
15          topic = "temperature/risen"
16          self.client.publish(topic, id)
17

You'll then need to template to dynamically generate sendTemperatureDrop and sendTemperatureRise functions in the generated code based off the AsyncAPI document content. The goal is to write template code that returns functions for channels that the Temperature Service application is subscribed to. The template code to generate these functions will look like this:

1<Text newLines={2}>
2  <TopicFunction channels={asyncapi.channels().filterByReceive()} />
3</Text>

It's recommended to put reusable components outside the template directory in a new directory called components. You'll create a component that will dynamically generate functions in the output for as many channels as there are in your AsyncAPI document that contains a publish operation. Add the following code in the python-mqtt-client-template/components/TopicFunction.js file, after creating the python-mqtt-client-template/components/ directory:

1/*
2 * This component returns a block of functions that user can use to send messages to specific topic.
3 * As input it requires a list of Channel models from the parsed AsyncAPI document
4 */
5export function TopicFunction({ channels }) {
6  const topicsDetails = getTopics(channels)
7  let functions = ''
8
9  topicsDetails.forEach((t) => {
10    functions += `def send${t.name}(self, id):
11        topic = "${t.topic}"
12        self.client.publish(topic, id)\n`
13  })
14
15  return functions
16}
17
18/*
19 * This function returns a list of objects, one for each channel with two properties, name and topic
20 * name - holds information about the operationId provided in the AsyncAPI document
21 * topic - holds information about the address of the topic
22 *
23 * As input it requires a list of Channel models from the parsed AsyncAPI document
24 */
25function getTopics(channels) {
26  const channelsCanSendTo = channels
27  let topicsDetails = []
28
29  channelsCanSendTo.forEach((ch) => {
30    const topic = {}
31    const operationId = ch.operations().filterByReceive()[0].id()
32    topic.name = operationId.charAt(0).toUpperCase() + operationId.slice(1)
33    topic.topic = ch.address()
34
35    topicsDetails.push(topic)
36  })
37
38  return topicsDetails
39}

{ channels }: the TopicFunction component accepts a custom prop called channels and in your template code getTopics(channels): Returns a list of objects, one for each channel with two properties; name and topic. The name holds information about the operationId provided in the AsyncAPI document while the topic holds information about the address of the topic.

Import the TopicFunction component in your template code in index.js and add the template code to generate the functions to topics that the Temperature Service application is subscribed to. In your case, the final version of your template code should look like this:

1import { File, Text } from '@asyncapi/generator-react-sdk'
2import { TopicFunction } from '../components/TopicFunction'
3
4export default function ({ asyncapi, params }) {
5  return (
6    <File name="client.py">
7      <Text newLines={2}>import paho.mqtt.client as mqtt</Text>
8
9      <Text newLines={2}>mqttBroker = "{asyncapi.servers().get(params.server).url()}"</Text>
10
11      <Text newLines={2}>class {asyncapi.info().title().replaceAll(' ', '')}Client:</Text>
12
13      <Text indent={2} newLines={2}>
14        {`def __init__(self):
15            self.client = mqtt.Client()
16            self.client.connect(mqttBroker)`}
17      </Text>
18
19      <Text indent={2}>
20        <TopicFunction channels={asyncapi.channels().filterByReceive()} />
21      </Text>
22    </File>
23  )
24}
25

Now your directory should look like this:

1python-mqtt-client-template
2├── components
3   └── TopicFunction.js
4├── template
5   └── index.js
6└── test
7    ├── fixtures
8       └── asyncapi.yml
9    └── project
10        ├── client.py
11        └── test.py

Run npm test on your terminal to ensure everything works as expected.

In the next section, you'll add another channel to asyncapi.yml file called temperature/dropped and temperature/risen then run the template again to make sure it still works as expected.

5d. Update AsyncAPI document

Update the AsyncAPI document to use two channels:

1channels:
2  temperature/dropped:
3    description:  Notifies the user when the temperature drops past a certain point.
4    publish:
5      operationId: temperatureDrop
6      message:
7        description: Message that is being sent when the temperature drops past a certain point.
8        payload:
9          type: object
10          additionalProperties: false
11          properties:
12            temperatureId:
13              type: string
14
15  temperature/risen:
16    description: Notifies the user when the temperature rises past a certain point.
17    publish:
18      operationId: temperatureRise
19      message:
20        description: Message that is being sent when the temperature rises past a certain point.
21        payload:
22          type: object
23          additionalProperties: false
24          properties:
25            temperatureId:
26              type: string

And update your test script in test.py to test the two functions as below:

1    client.sendTemperatureDrop(randomId)
2    print("Temperature drop detected " + str(randomId) + " sent to temperature/dropped")
3    client.sendTemperatureRise(randomId)
4    print("Temperature rise detected " + str(randomId) + " sent to temperature/risen")

Run npm test to validate that everything works as expected. You should see logs similar to the snippet below in your terminal:

1Temperature drop detected 49040460 sent to temperature/dropped
2Temperature rise detected 49040460 sent to temperature/risen
3Temperature drop detected 66943992 sent to temperature/dropped
4Temperature rise detected 66943992 sent to temperature/risen

Where to go from here?

Great job completing this tutorial! You have learnt how to use an AsyncAPI file to create a Python MQTT template and used it with the Paho-MQTT library in Python to connect to an MQTT broker and publish messages.😃

If you want to tinker with a completed template and see what it would look like in production, check out the Paho-MQTT template. You can also check out the accompanying article about creating MQTT client code.

You can also check out the MQTT beginners guide tutorial to learn more about asynchronous messaging using MQTT.

Was this helpful?
Help us improve the docs by adding your contribution.
OR
Github:AsyncAPICreate Issue on GitHub