HTTP Google Cloud Functions in Python

1. Introduction

Python is a popular open source programming language used by data scientists, web application developers, systems administrators and more.

Google Cloud Functions is an event-driven serverless compute platform. Cloud Functions allows you to write your code without worrying about provisioning resources or scaling to handle changing requirements.

There are two types of Cloud Functions:

  • HTTP functions respond to HTTP requests. You'll build one in this codelab.
  • Background functions are triggered by events, like a message being published to Cloud Pub/Sub or a file being uploaded to Cloud Storage. We don't address this in this lab, but you can read more in the documentation.

d59d66e678290f9e.png

This codelab will walk you through creating your own Cloud Function in Python.

What you will build

In this codelab, you will publish a Cloud Function that, when invoked via HTTP, will display the "Python Powered" logo.

27ed676bc761938b.png

What you will learn

  • How to write an HTTP Cloud Function in Python.
  • How to write an HTTP Cloud Function in Python which takes arguments.
  • How to test an HTTP Cloud Function in Python.
  • How to run a local Python HTTP server to try the function.

2. Setup and Requirements

Self-paced environment setup

  1. Sign in to Cloud Console and create a new project or reuse an existing one. (If you don't already have a Gmail or G Suite account, you must create one.)

WvZ4Lcm6k-4AOA_v1j9HE6MO5-TJvI3TlGp4PNpqf_gbPLt9pq3c0ZiMOtT4bZZ1urP6tVOzAsTdpQ9OLYDcvvhpbdHcqqLRoQ7JFy-tCyKyE8gn9BtOwo8T3CLjOoTVL17Czok8-A

RQVrJ6Mj_SCQFipYRkdxqlWmokfg9D05dd5oq8qWaF_ci584t_xHW6M5-8fGR5i5JIUkx2r1G_5Ue630biVrQmSLKf_s2OiU9dnc4PZD1VnWSJf2AzZwzZP8UYiI__zqurLdj_7Qvw

ARsF00OMeFtN4MASqu6GKhjNAf8ANuL29fBrkJ6qnh3DrxdXFAftS-klvH5WNZ8XculywaNSnNZk3WoUP0d5UBQstpHXKgiWGJ3Wox3MVsL5TVhhSWt66U8yBfZBmNRksXds-_6wdA

Remember the project ID, a unique name across all Google Cloud projects (the name above has already been taken and will not work for you, sorry!). It will be referred to later in this codelab as PROJECT_ID.

  1. Next, you'll need to enable billing in Cloud Console in order to use Google Cloud resources.

Running through this codelab shouldn't cost much, if anything at all. Be sure to to follow any instructions in the "Cleaning up" section which advises you how to shut down resources so you don't incur billing beyond this tutorial. New users of Google Cloud are eligible for the $300USD Free Trial program.

Start Cloud Shell

While Google Cloud can be operated remotely from your laptop, in this codelab you will be using Google Cloud Shell, a command line environment running in the Cloud.

Activate Cloud Shell

  1. From the Cloud Console, click Activate Cloud Shell U3KqmcaywP9RcSjMDokVtfw7D3-7w-bFzhVEwwLx4kib1pcVJyZTc1AiQ5uf0x263MlnH23MO7OUTXsXwDdEjvsQsosC_kmZ4xmoFaVUMdTUeD6817oEW6G_cCw7ZzXSKL1z1L0PHQ.

HMjAm7ml5v7v4KhMG9Bptnp1MUiRh9c4M0K95kDG5vOQNCcMgNcGRqi9Z1JFIUpXzduJkQQai7o4T117GgcAx4VvJtm81L3EMO3tSFdo50BSfSh2uaQzMjP6eSLfgEzKVtwFbvWqig

If you've never started Cloud Shell before, you're presented with an intermediate screen (below the fold) describing what it is. If that's the case, click Continue (and you won't ever see it again). Here's what that one-time screen looks like:

oT-m4j50PqHO4zJIeW-_AG7XGlu76LUvukyZtWZBb9HIVZ-3PllJ103_NHIrhalNmfHT-2lCJFYJsFUTcCCdSe58ziWkSlBjbpfyN_O0UtoOyQCSL8DgHkYTFV9Txx_osxLryhtayg

It should only take a few moments to provision and connect to Cloud Shell.

je6hS0p4KcC104UEGhKzzcXCjYPcfIWl9u8JYW9pf4ZrsP7xFLT0Ua2XhbPV__h1fpA0Th4YOCWO5RdUtIH9uFUCCBGiplw-Q-d8PgCwvzPx1Ix6dUNu0NRAU9rFHh0-QKohgkhg5w

This virtual machine is loaded with all the development tools you need. It offers a persistent 5GB home directory and runs in Google Cloud, greatly enhancing network performance and authentication. Much, if not all, of your work in this codelab can be done with simply a browser or your Chromebook.

Once connected to Cloud Shell, you should see that you are already authenticated and that the project is already set to your project ID.

  1. Run the following command in Cloud Shell to confirm that you are authenticated:
gcloud auth list

Command output

 Credentialed Accounts
ACTIVE  ACCOUNT
*       <my_account>@<my_domain.com>

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
gcloud config list project

Command output

[core]
project = <PROJECT_ID>

If it is not, you can set it with this command:

gcloud config set project <PROJECT_ID>

Command output

Updated property [core/project].

Ensure the Cloud Functions and Cloud Build APIs are enabled

Run the following command from Cloud Shell to make sure the Cloud Functions and Cloud Build APIs are enabled:

gcloud services enable cloudfunctions.googleapis.com cloudbuild.googleapis.com

Note: Cloud Build will be called by the gcloud functions deploy command and will automatically build your code into a container image.

Download and setup the code

  1. From the Cloud Shell terminal, use curl to download a zip with the code for this codelab:
curl -LO https://github.com/GoogleCloudPlatform/python-docs-samples/archive/master.zip
  1. Use unzip to unpack the code. This unpacks a directory (python-docs-samples-master), which contains sample Python code for cloud.google.com.
unzip master.zip
  1. Change to the directory containing the code for this codelab:
cd python-docs-samples-master/codelabs/functions/python_powered

Check out the content of the python_powered directory:

ls

You should get the following files:

app.py  main.py  python_powered.jpg  test_main.py

3. Introducing HTTP Cloud Functions in Python

HTTP Cloud Functions in Python are written as regular Python functions. The function must accept a single argument, which is usually named request.

main.py

def hello_world(request):
    """HTTP Cloud Function.
    Args:
        request (flask.Request): The request object.
        <http://flask.pocoo.org/docs/1.0/api/#flask.Request>
    Returns:
        The response text, or any set of values that can be turned into a
        Response object using `make_response`
        <http://flask.pocoo.org/docs/1.0/api/#flask.Flask.make_response>.
    """
    return "Hello World!"

You can open this file in Cloud Shell by opening the editor (click the pencil icon at the top right of Cloud Shell) then using the file tree on the left side of the editor to open the file, python-docs-samples-master/codelabs/functions/python_powered/main.py.

Let's deploy this function as an HTTP Cloud Function! Make sure you cd python-docs-samples-master/codelabs/functions/python_powered/, then you can deploy it using the function name and gcloud functions deploy. This may take a minute or two.

gcloud functions deploy hello_world \
  --runtime python38 \
  --trigger-http \
  --allow-unauthenticated

The --allow-unauthenticated deploy option enables you to reach the function without authentication.

Command output:

Deploying function (may take a while - up to 2 minutes)...done.
availableMemoryMb: 256
entryPoint: hello_world
httpsTrigger:
  url: https://REGION-PROJECT_ID.cloudfunctions.net/hello_world
...

To test the hello_world function, you can click the httpsTrigger.url URL displayed in the command output above. You can also programmatically retrieve the URL and call the function with the following commands:

URL=$(gcloud functions describe hello_world --format "value(httpsTrigger.url)")
curl -w "\n" $URL

You should get the following result:

Hello World!

4. Writing a Python HTTP Cloud Function which takes arguments

Functions are more useful when they can take arguments. We'll define a new function, hello_name, which says "Hello World!" by default, but can accept a name argument which causes it to say "Hello _____!" depending on the value of name.

main.py

import flask


def hello_name(request):
    """HTTP Cloud Function.
    Args:
        request (flask.Request): The request object.
        <http://flask.pocoo.org/docs/1.0/api/#flask.Request>
    Returns:
        The response text, or any set of values that can be turned into a
        Response object using `make_response`
        <http://flask.pocoo.org/docs/1.0/api/#flask.Flask.make_response>.
    """
    request_args = request.args

    if request_args and "name" in request_args:
        name = request_args["name"]
    else:
        name = "World"
    return "Hello {}!".format(flask.escape(name))

Again, let's deploy this new function as an HTTP Cloud Function! Make sure you cd python-docs-samples-master/codelabs/functions/python_powered/, then you can deploy it using the function name and gcloud functions deploy. Note that this command is slightly different than the one you ran before: we're deploying hello_name, not hello_world.

gcloud functions deploy hello_name \
  --runtime python38 \
  --trigger-http \
  --allow-unauthenticated

The --allow-unauthenticated deploy option enables you to reach the function without authentication.

Command output:

Deploying function (may take a while - up to 2 minutes)...done.
availableMemoryMb: 256
entryPoint: hello_name
httpsTrigger:
  url: https://REGION-PROJECT_ID.cloudfunctions.net/hello_name
...

To test the hello_name function, you can click the httpsTrigger.url URL displayed in the command output above. You can also programmatically retrieve the URL and call the function with the following commands:

URL=$(gcloud functions describe hello_name --format "value(httpsTrigger.url)")
curl -w "\n" $URL

You should get the default result:

Hello World!

You're getting the default result because the name argument isn't set. To set it, add a URL parameter name=YOURNAME to the URL. Call it like so, replacing YOURNAME with your name:

curl -w "\n" $URL?name=YOURNAME

This time, you'll get your custom response:

Hello YOURNAME!

The next step is to add some tests to make sure your functions continue to work.

5. Writing tests

HTTP Cloud Functions in Python are tested using the unittest module from the standard library. There is no need to run an emulator or other simulation to test your function—just normal Python code.

Here is what a test looks like for the hello_world and hello_name functions:

test_main.py

import unittest
import unittest.mock

import main


class TestHello(unittest.TestCase):
    def test_hello_world(self):
        req = unittest.mock.Mock()

        # Call tested function
        assert main.hello_world(req) == "Hello World!"

    def test_hello_name_no_name(self):
        req = unittest.mock.Mock(args={})

        # Call tested function
        assert main.hello_name(req) == "Hello World!"

    def test_hello_name_with_name(self):
        name = "test"
        req = unittest.mock.Mock(args={"name": name})

        # Call tested function
        assert main.hello_name(req) == "Hello {}!".format(name)
  1. Python tests are written the same way as other Python files. They start with a set of imports, then define classes and functions.
  2. The test declaration is of the form class TestHello(TestCase). It must be a class that inherits from unittest.TestCase.
  3. The test class has methods, each of which must start with test_, which represent individual test cases.
  4. Each test case tests one of our functions by mocking the request parameter (i.e., replacing it with a fake object with the specific data we require for the test).
  5. After invoking each function, the test checks the HTTP response to be sure it was what we were expecting.

As main.py depends on flask, make sure the Flask framework is installed in your test environment:

python3 -m pip install --user flask

Installing Flask outputs a result similar to the following:

Collecting flask
...
Successfully installed Jinja2-2.11.2 MarkupSafe-1.1.1 Werkzeug-1.0.1 flask-1.1.2 ...

To run these tests locally, cd to the directory with the files you're testing then use the python3 -m unittest command:

python3 -m unittest
...
----------------------------------------------------------------------
Ran 3 tests in 0.001s

OK

Next, you'll create a new function which returns the ‘Python Powered' logo.

6. Writing the ‘Python Powered' HTTP Cloud Function

Let's make the "Hello World!" function a bit more entertaining by printing the ‘Python Powered' image for every request:

27ed676bc761938b.png

The following listing shows the code to make it happen. You can see this code in Cloud Shell at python-docs-samples-master/codelabs/functions/python_powered/main.py. After the code block, there are notes about each portion.

main.py

import os

import flask


def python_powered(request):
    """HTTP Cloud Function.
    Args:
        request (flask.Request): The request object.
        <http://flask.pocoo.org/docs/1.0/api/#flask.Request>
    Returns:
        The response file, a JPG image that says "Python Powered"
    """
    return flask.send_from_directory(os.getcwd(), "python_powered.jpg", mimetype="image/jpg")
  1. The file starts with an import statement from flask of a function we'll use to send files as responses.
  2. Next is the python_powered function declaration.
  3. The function calls the send_from_directory function with a filename to send (python_powered.jpg) and a mimetype.
  4. If there was no error when writing the response, the function returns normally.

Deploy this function as you did the "Hello World!" function from before, using gcloud functions deploy and the name of the function, python_powered:

gcloud functions deploy python_powered \
  --runtime python38 \
  --trigger-http \
  --allow-unauthenticated

The --allow-unauthenticated deploy option enables you to reach the function without authentication.

To test the function, visit the function's URL, which again is displayed in the gcloud functions deploy command output in your browser. If everything is working correctly, you will see the ‘Python Powered' logo in your browser!

Next, you'll create an app so that you can run your function locally and try it in a browser.

7. Running the function locally

You can run an HTTP function locally by creating an HTTP server and calling your function in a route.

You can write an HTTP server for your function in the same directory as your function. Create a file named app.py with the following contents:

app.py

import flask

import main


app = flask.Flask(__name__)


@app.route("/")
def index():
    return main.python_powered(flask.request)
  1. This file imports python_powered from your function file, main.py, and additional resources from flask , a Python web framework.
  2. It creates a Flask application
  3. It registers a route at the base URL which is handled with a function named index().
  4. The index() function then calls our python_powered function, passing it the current request.

Make sure the Flask framework is installed in your development environment:

python3 -m pip install --user flask

Installing Flask outputs a result similar to the following:

Collecting flask
...
Successfully installed Jinja2-2.11.2 MarkupSafe-1.1.1 Werkzeug-1.0.1 flask-1.1.2 ...

To run this application locally, run the following command:

python3 -m flask run --port 8080

Now use the Cloud Shell Web Preview to test the server in your browser. In Cloud Shell, click the "Web preview" button and select "Preview on port 8080":

61ed695d77b2b647.png

Cloud Shell opens the preview URL on its proxy service in a new browser window. The web preview restricts access over HTTPS to your user account only. If everything is working properly, you should see the ‘Python Powered' logo!

8. Cleaning up

Cloud Functions pricing is based on how often your function is invoked, including a free tier for functions that don't run often.

Once you're done testing your Cloud Functions, you can delete them using gcloud:

gcloud functions delete hello_world --quiet
gcloud functions delete hello_name --quiet
gcloud functions delete python_powered --quiet

You can also delete the functions from the Cloud Console UI.

We hope you enjoy using Cloud Functions in Python!