Python Template

This template allows you to code functions using Python 3.9 and supports synchronous and asynchronous tasks.

Synchronous Function Support

The skeleton of this template looks like this:
def handle(req):
    return {
        "status_code": 200,
        "body": req.body.decode('utf-8')
    }

Asynchronous Function Support:

Additionally, this template supports asynchronous functions for more complex operations. Here's an example of an asynchronous function:
import asyncio
async def handle_async(req): 
  # Asynchronous operation, such as waiting for an external API response 
  await asyncio.sleep(1)
  return {
    "status_code": 200,
    "body": f"Asynchronous response: {req.body.decode('utf-8')}" } 

These functions (synchronous and asynchronous) echoes back your request body message with a 200 status code.

Request Object

The request object (req) is the only parameter passed to your handle function and is an instance of the Event class. It has five properties:
  • method: A string (str) that contains the HTTP method of the request. Its value can be POST, GET, PUT, PATCH or DELETE.
  • body: A bytes object containing the body of the request. If this is plain text, you may want to convert it to a string using req.body.decode('utf-8') to make it easier to work with.
  • headers: This is a werkzeug.EnvironHeaders object that contains all the request headers. Read the documentation of this type of objects to know how to use it.
  • query: A werkzeug.ImmutableMultiDict object that contains the query parameters you sent in the URL. This is a special kind of dictionary with extended capabilities that makes it convenient for working with query parameters.
  • path: A string (str) object containing the URL path used to invoke your function. Note that this will be just the path that you append after the /invoke or /async-invoke endpoint.

Response Object

The response object is a dictionary returned by the handle function that will be used to build the HTTP response for your request. This dictionary can have the following keys:
  • status_code: An integer (int) representing the status code of the response. It must be a valid status code greater than or equal to 200. Defaults to 200.
  • headers: A dictionary (dict) containing the headers to return.
  • body: The body of the response. If a Content-Type header has not been explicitly set, it will be automatically determined by the body type:
    • If the body is a dict, the Content-Type will be set to application/json.
    • Otherwise, the body will be the string representation of the value and the content type will be set to plain/text.

None of these values is required. In fact, you can omit the return statement to send a 200 empty response.

Libraries

Adding third-party Python dependencies is not allowed at the moment, although this will change in the future. For now, you can use the Python's standard library and the following pre-installed dependencies (this list will grow):
  • requests: Provides a convenient and easy way to make HTTP/1.1 requests.
  • Paho-mqtt: The Eclipse Paho MQTT Python client.

SWX Package

The SWX package allows some operations using the Altair IoT Studio API:
  • Get an access token.
Note: This package is in a very early stage of development and only offers a very limited number of features.

Secrets

You can access your Secrets using the two built-in functions in the secrets package:
def get(secret_name: str) -> str
Returns the value of the given secret. If the secret is not found, an empty string will be returned.
def exists(secret_name: str) -> bool
Returns a boolean value indicating if the given secret is found.
Example:
import function.secrets as secrets 

def handle(req):
   # ... 
    if secrets.exists("my_password"):
        my_secret = secrets.get("my_password") 
          # ...
    # ... 

Logging

import logging
log = logging.getLogger("my_logger")
log.setLevel(logging.INFO)
log.error("ERROR")
log.info("INFO")
log.debug("DEBUG")  # This line wont' be logged because Logging Level is INFO
You can also use the following as anything you write to stderr will be saved as a log entry.
import sys
print("Footer",file=sys.stderr)

Examples

Here are some simple examples to illustrate how you can write your own functions using Python.

Adder
This is a really simple function that gets a comma-separated list of numbers in the request body and return the sum of them (or an error if the input format is invalid).
def handle(req):
    # Gets function input
    body = req.body.decode("utf-8")

    # Parses input as a comma-separated list of numbers
    numbers = body.split(',')

    try:
        # Tries to convert all values to numbers
        numbers = [float(i) for i in numbers]
        response_msg = sum(numbers)
        status_code = 200
    except Exception:
        # Throws an error if something is not a number
        response_msg = "Invalid input format!"
        status_code = 400

    return {
        "status_code": status_code,
        "body": response_msg,
    }
MQTT Publishers
The following example shows how to set up an MQTT client to publish the payload of the request to the functions/Python topic.
import paho.mqtt.client as mqtt

MQTT_BROKER = "test.mosquitto.org"
MQTT_PORT = 1883

def handle(req):
    body = req.body.decode('utf-8')

    client = mqtt.Client()
    client.connect(MQTT_BROKER, MQTT_PORT, 60)
    client.loop start ()
    msg_info = client.publish("functions/python", body, qos=0)
    msg_info.wait_for_publish()

    return {
        "status_code": 200,
        "body": "Message published!"
    }
Update a Thing's Property
The following example shows how to update a Thing's Property using the swx package to deal with token request and revocation.
import requests 
from swx.auth.token import get_token 
 
API_URL = 'https://api.swx.altairone.com' 
SPACE = 'examples'   # Change it to your Space name 
 
# We'll use a Thing credentials to request the access token 
CATEGORY = '<A-CATEGORY-NAME>' 
THING_ID = '<A-THING-ID>' 
CLIENT_ID = '<A-CLIENT-ID>' 

 
 
def handle(req): 

  # Get the client secret from the Secrets Storage.
  # This avoids hardcoding the secret in the code.
  client_secret = secrets.get("my_secret_name")


    # Request token with the Thing's credentials and 'thing.update' scope. 
    # Token will be automatically revoked at the end of the context manager block. 
    with get_token(CLIENT_ID,  client_secret, ["thing.update"]) as token: 
        url = f"{API_URL}/spaces/{SPACE}/categories/{CATEGORY}/things/{THING_ID}/properties" 
        requests.put(url, 
                     headers={"Authorization": f"Bearer {token.access_token}"}, 
                     json={"temperature": 27}) 
 
    return { 
        "status_code": 200, 
        "body": "Property updated!" 
    }