Python3 Functions

Writing Python functions with fission

Fission supports functions written in Python3.7+. In this usage guide we’ll cover how to set up and use a Python environment on Fission, write functions, and work with dependencies. We’ll also cover basic troubleshooting.

Before you start

We’ll assume you have Fission and Kubernetes setup. If not, head over to the installation guide.

Verify your Fission setup with:

fission version

Add the Python environment to your cluster

Fission language support is enabled by creating an Environment. An environment is the language-specific part of Fission. It has a container image in which your function will run.

fission environment create --name python --image fission/python-env

Note: If your function references external libraries, non-standard Python modules or has additional OS dependencies, you will need to create a custom image for your Python environment. You can skip to working with dependencies section to understand more. For most of the usual scenarios, the default environment will work without any modification.

Create a simple function in Python

Create a file named hello.py:

def main():
    return "Hello, world!"

Create a Fission function (this uploads the file to Fission on the cluster):

fission function create --name hello --env python --code hello.py 

Invoke this function through the Fission CLI:

$ fission function test --name hello
Hello, world!

You can also invoke this function by creating an HTTP trigger and making an HTTP request to the Fission router. Ensure you have your router’s address in the FISSION_ROUTER environment variable as this guide describes. Then,

$ fission route create --method GET --url /hello --function hello 

$ curl $FISSION_ROUTER/hello
Hello, world!

Function input and output interface

In this section we’ll describe the input and output interfaces of Python functions in Fission. Fission’s Python integration is built on the Flask framework. You can access HTTP requests and responses as you do in Flask. We’ll provide some examples below.

Accessing HTTP Requests

HTTP Headers

Write a simple headers.py with something like this:

from flask import request

def main():
    try:
        myHeader = request.headers['x-my-header']
    except KeyError:
        return "Header 'x-my-header' not found"
    return "The header's value is '%s'" % myHeader

Create that function, assign it a route, and invoke it with an HTTP header:

$ fission function create --name headers --env python --code headers.py

$ fission route create --url /headers --function headers

$ curl -H "X-My-Header: Hello" $FISSION_ROUTER/headers
The header's value is 'Hello'
Query parameters

HTTP Query parameters are the key-value pairs in a URL after the ?. They are also available through the request object:

Write a simple query.py with something like this:

from flask import request

def main():
    queryParam = request.args.get('myKey')
    return "Value for myKey: %s" % queryParam

Create that function, assign it a route, and invoke it with a query parameter:

$ fission function create --name query --env python --code query.py

$ fission route create --url /query --function query

$ curl $FISSION_ROUTER/query?myKey=myValue
Value for myKey: myValue
Body

HTTP POST and PUT requests can have a request body. Once again, youccan access this body through the request object.

For requests with a JSON Content-Type, you can directly get a parsed object with request.get_json() [docs].

For form-encoded requests ( application/x-www-form-urlencoded), use request.form.get('key') [docs].

For all other requests, use request.data [docs] to get the full request body as a string of bytes.

You can find the full docs on the request object in the flask docs.

Controlling HTTP Responses

The simplest way to return a response is to return a string. This implicitly says that your function succeeded with a status code of 200; the returned string becomes the body. However, you can control the response more closely using the Flask response object.

Setting Response Headers
import flask

def main():
    resp = flask.Response("Hello, world!")
    resp.headers['X-My-Response-Header'] = 'Something'
    return resp
Setting Status Codes
import flask

def main():
    resp = flask.Response("Hello, world!")
    resp.status_code = 200
    return resp
HTTP Redirects
import flask

def main():
    r = flask.redirect('/new-url', code=303)

    # Optional; set this to False to force a relative URL redirect.
    # Defaults to True, which converts the redirect to an absolute URL
    # that's only accessible within the cluster.
    r.autocorrect_location_header = False

    return r

Logging

from flask import current_app

def main():
    current_app.logger.info("This is a log message")
    return "Hello, world"

Working with dependencies

The examples above show simple one-file functions with no dependencies. You can package dependencies with your function, and even use Fission to download and package up the dependencies.

Using the Python environment with the builder

Fission supports builders, which are language-specific containers that know how to gather dependencies and build from a source zip file, into a deployment zip file.

To use a builder with your environment, create the environment with the –builder flag:

fission env create --name python --image fission/python-env --builder fission/python-builder

A function with dependencies

Let’s take a simple python function which has a dependency on the pyyaml module. We can specify the dependencies in requirements.txt and a simple command to build from source. The tree structure of directory and contents of the file would look like:

sourcepkg/
├── __init__.py
├── build.sh
├── requirements.txt
└── user.py

And the file contents:

  • user.py
import sys
import yaml

document = """
  a: 1
  b:
    c: 3
    d: 4
"""

def main():
    return yaml.dump(yaml.load(document))
  • requirements.txt
pyyaml
  • build.sh
#!/bin/sh
pip3 install -r ${SRC_PKG}/requirements.txt -t ${SRC_PKG} && cp -r ${SRC_PKG} ${DEPLOY_PKG}

Make sure the build.sh file is executable:

chmod +x build.sh
  • Archive these files:
$ zip -jr demo-src-pkg.zip sourcepkg/
  adding: __init__.py (stored 0%)
  adding: build.sh (deflated 24%)
  adding: requirements.txt (stored 0%)
  adding: user.py (deflated 25%)

Using the source archive creared in previous step, you can create a package in Fission:

$ fission package create --sourcearchive demo-src-pkg.zip --env python --buildcmd "./build.sh"
Package 'demo-src-pkg-zip-8lwt' created

Since we are working with a source package, we provided the build command. Once you create the package, the build process will start and you can see the build logs with the fission package info command:

$ fission pkg info --name demo-src-pkg-zip-8lwt
Name:        demo-src-pkg-zip-8lwt
Environment: python
Status:      succeeded
Build Logs:
Collecting pyyaml (from -r /packages/demo-src-pkg-zip-8lwt-v57qil/requirements.txt (line 1))
  Using cached PyYAML-3.12.tar.gz
Installing collected packages: pyyaml
  Running setup.py install for pyyaml: started
    Running setup.py install for pyyaml: finished with status 'done'
Successfully installed pyyaml-3.12

Using the package above you can create the function. Since this package is already associated with a source archive, an environment and a build command, you don’t need to provide these while creating a function from this package.

The only additional thing you’ll need to provide is the Function’s entrypoint:

$ fission fn create --name srcpy --pkg demo-src-pkg-zip-8lwt --entrypoint "user.main"
function 'srcpy' created

# Run the function:
$ fission fn test --name srcpy
a: 1
b: {c: 3, d: 4}

Modifying the runtime environment image

The base runtime image of the Python can also be modified to include dependencies. You can do this for dependencies that all your functions need, thus reducing the size of your function packages (and improving cold-start times).

First, get a copy of the Fission source, which includes the Python environment:

git clone https://github.com/fission/environments

Get to the Python environment:

cd environments/python

To add package dependencies, edit requirements.txt to add what you need, and rebuild this image as follows:

Next, build and push the container image. To push your image you’ll need access to a Docker registry. Let’s assume you have a DockerHub account called “USER”. (You could use any other registry too.)

docker build -t USER/python-env .
docker push USER/python-env

Now you can use this image as your function runtime. You can re-create the environment, pointing the runtime at this image:

fission env create --name python --image USER/python-env ...

Or just update it, if you already have an image:

fission env update --name python --image USER/python-env ...

After this, functions that have the env parameter set to “python” will use this new customized image for running the functions.