Setting up the Python project for HTTP functions

The following examples assumes that you have Python 3.10 installed and you are using Linux as operating system.

Project structure

A minimal Python FunctionGraph project is typically structured as follows:

Project structure
/project-root
 ├─ src
 |  └─ index.py
 └─ requirements.txt

Step 1: Initialize the project

This project will use Flask, a popular Python web framework, to implement the HTTP function.

  1. Run the following commands to create a project folder.

    mkdir -p scratch-http-simple/src
    cd scratch-http-simple
    
  2. Run the following command to create a virtual environment for the project.

    # create a virtual environment named .venv in the project root directory
    python3 -m venv .venv
    
    # activate the virtual environment with the following command:
    source .venv/bin/activate
    
  3. create a file named requirements.txt in the project root directory and add the following line to specify Flask as a dependency.

    requirements.txt
    Flask==3.1.3
    waitress==3.0.2
    
  4. Run the following command to install the dependencies specified in requirements.txt.

    pip install -r requirements.txt
    

Step 2: Sample code

in the src folder, create a file named index.py and add the following code. For details about how to use this framework, see Flask’s guide .

Sample code:

src/index.py
import json

from flask import Flask, request, jsonify

# Create a Flask application instance.
app = Flask(__name__)


# Define a route /index that handles GET requests.
@app.route('/index', methods=['GET'])
def index_get():
    # Print the request path for debugging.
    print("***" + request.path + "***", flush=True)
    
    return jsonify(message="Hello from scratch-http sample!")
    
# Define a route /index that handles POST requests.
@app.route('/index', methods=['POST'])
def index():
    # Print the request path for debugging.
    print("***" + request.path + "***", flush=True)

    # Build response data.
    data = {
        "statusCode": 200,
        "isBase64Encoded": False,
        "body": json.dumps(request.path + " success"),
        "headers": {
            "Content-Type": "application/json"
        }
    }
    return jsonify(data)

# Main program entry
if __name__ == '__main__':
    # dev server:
    app.run(host="localhost", port=8000)
    
    # production server using waitress:
    #from waitress import serve
    #serve(app, host="0.0.0.0", port=8000)

Step 2a: Test the function locally

To test the function locally, run the following command in the project root directory:

python3 src/index.py

Then send a HTTP request to the function with the following command:

GET request
 curl -X GET localhost:8000/index
Result
{"message":"Hello from scratch-http sample!"}
POST request
  curl -X POST localhost:8000/index
Result
 {"body":"\"/index success\"","headers":{"Content-Type":"application/json"},"isBase64Encoded":false,"statusCode":200}

Step 3: Create the bootstrap file

Create a file named bootstrap in the project root directory and add the following code.

bootstrap
export PYTHONPATH=$RUNTIME_CODE_ROOT:$PYTHONPATH
/opt/function/runtime/python3.10/rtsp/python/bin/python3 $RUNTIME_CODE_ROOT/src/index.py

Step 4: Create package

To deploy the function to FunctionGraph, you need to create a zip package of the project. The Zip package should have following structure:

deployment zip file structure
/code.zip
 ├─ requirement 1          # pip third-party dependencies (optional)
 |  └─ ...
 ├─ src
 |  └─ index.py            # main function code file (mandatory)
 └─ bootstrap              # bootstrap file to start the function runtime (mandatory)

To create the zip package, you use makefile file as follows to include the necessary fields for FunctionGraph deployment.

Makefile
# Terraform backend configuration
BACKEND_CONFIG_BUCKET := "doc-samples-tf-backend"
BACKEND_CONFIG_KEY := "terraform_state/python/scratch-http-flask.tf"
BACKEND_CONFIG_REGION := "eu-de"
BACKEND_CONFIG_ENDPOINTS := "endpoints={s3=\"https://obs.eu-de.otc.t-systems.com\"}"

CURRENT_MAKEFILE := $(firstword $(MAKEFILE_LIST))

create_package:
  python3 ../../utils/createZip.py

tf_init:
  terraform -chdir=terraform \
    init \
    -backend-config=$(BACKEND_CONFIG_ENDPOINTS) \
    -backend-config="bucket=$(BACKEND_CONFIG_BUCKET)" \
    -backend-config="key=$(BACKEND_CONFIG_KEY)" \
    -backend-config="region=$(BACKEND_CONFIG_REGION)"

tf_plan: 
  if [ ! -f "terraform/.terraform.lock.hcl" ]; then \
    $(MAKE) -f $(CURRENT_MAKEFILE) tf_init; \
  fi
  terraform -chdir=terraform \
    plan \
    -var-file="variables.tfvars" 

tf_apply: create_package
  if [ ! -f "terraform/.terraform.lock.hcl" ]; then \
    $(MAKE) -f $(CURRENT_MAKEFILE) tf_init; \
  fi
  terraform -chdir=terraform \
    apply -auto-approve \
    -var-file="variables.tfvars"

tf_destroy:
  terraform -chdir=terraform \
    destroy -auto-approve \
    -var-file="variables.tfvars"

test_deployed_post:
  # getting Token for authentication from Username/Password...
  $(eval OTC_X_AUTH_TOKEN := $(shell ../../utils/tokenFromUsername.sh))
  # getting the Function URN from terraform output...
  $(eval MY_FUNCTION_URN := $(shell terraform -chdir=terraform output -raw MY_FUNCTION_URN))
  # calling the deployed function via FunctionGraph API...
  @curl -X POST \
   -H "Content-Type: application/json" \
   -H "x-auth-token: $(OTC_X_AUTH_TOKEN)" \
   -d @./resources/apig_post_index.json \
   https://functiongraph.$(OTC_SDK_REGION).otc.t-systems.com/v2/$(OTC_SDK_PROJECTID)/fgs/functions/$(MY_FUNCTION_URN):latest/invocations
  @echo "" 
  # finished

test_deployed_get:
  # getting Token for authentication from Username/Password...
  $(eval OTC_X_AUTH_TOKEN := $(shell ../../utils/tokenFromUsername.sh))
  # getting the Function URN from terraform output...
  $(eval MY_FUNCTION_URN := $(shell terraform -chdir=terraform output -raw MY_FUNCTION_URN))
  # calling the deployed function via FunctionGraph API...
  @curl -X POST \
   -H "Content-Type: application/json" \
   -H "x-auth-token: $(OTC_X_AUTH_TOKEN)" \
   -d @./resources/apig_get_index.json \
   https://functiongraph.$(OTC_SDK_REGION).otc.t-systems.com/v2/$(OTC_SDK_PROJECTID)/fgs/functions/$(MY_FUNCTION_URN):latest/invocations
  @echo "" 
  # finished


test_deployed_apig_post:
  # get the API Gateway URL from terraform output
  $(eval API_GATEWAY_TRIGGER_URL := $(shell terraform -chdir=terraform output -raw API_GATEWAY_TRIGGER_URL))
  @echo "Using API Gateway URL: $(API_GATEWAY_TRIGGER_URL)"
  # execute a curl request against the deployed function via API Gateway
  curl -X POST \
    -H 'Content-Type: application/json' \
    -d @./resources/apig_post_index.json \
    $(API_GATEWAY_TRIGGER_URL)/index
    
test_deployed_apig_get:
  # get the API Gateway URL from terraform output
  $(eval API_GATEWAY_TRIGGER_URL := $(shell terraform -chdir=terraform output -raw API_GATEWAY_TRIGGER_URL))
  @echo "Using API Gateway URL: $(API_GATEWAY_TRIGGER_URL)"
  # execute a curl request against the deployed function via API Gateway
  curl -X GET \
     -H 'Content-Type: application/json' \
     $(API_GATEWAY_TRIGGER_URL)/index



.PHONY: create_package tf_init tf_plan tf_apply tf_destroy test_deployed

To create the zip package, run the following command in the project root directory:

make create_package

This will create a zip package named code.zip in the dist directory.

Step 5: Deploy to FunctionGraph

In FunctionGraph console, create a function with the following parameters:

  • Create with: Create from scratch

  • Function Type: HTTP Function

  • Region: select the region where you want to create the function

  • Function Name: scratch-http-simple

leave the other parameters with default values, and click Create Function to create the function.

In the Code tab of the created function, click Upload -> local Zip to upload the generated zip package (e.g., code.zip) to FunctionGraph.

Step 6: Test the Function

Test GET /index

Create a test event with the following parameters:

  • Event Name: test-event-get

  • Event Template: API Gateway (Dedicated)

apig_get_index.json
{
  "body": "",
  "requestContext": {
      "apiId": "bc1dcffd-aa35-474d-897c-d53425a4c08e",
      "requestId": "11cdcdcf33949dc6d722640a13091c77",
      "stage": "RELEASE"
  },
  "queryStringParameters": {
      "responseType": "application/json",
      "name": "John"
  },
  "httpMethod": "GET",
  "pathParameters": {
  },
  "headers": {
      "accept-language": "q=0.5,en-US;q=0.3,en;q=0.2",
      "accept-encoding": "gzip, deflate, br",
      "x-forwarded-port": "443",
      "x-forwarded-for": "103.218.216.98",
      "accept": "application/json",
      "upgrade-insecure-requests": "1",
      "host": "host",
      "x-forwarded-proto": "https",
      "pragma": "no-cache",
      "cache-control": "no-cache",
      "x-real-ip": "103.218.216.98",
      "user-agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:57.0) Gecko/20100101 Firefox/57.0"
  },
  "path": "/index",
  "isBase64Encoded": false
}

Click Test to execute the function with the test event created, and you should see the following output in the Execution Result section

Execution Result GET
{
   "body": "eyJtZXNzYWdlIjoiSGVsbG8gZnJvbSBzY3JhdGNoLWh0dHAgc2FtcGxlISJ9Cg==",
   "headers": {
      "Content-Length": [
            "46"
      ],
      "Content-Type": [
            "application/json"
      ],
      "Date": [
            "Mon, 21 Sep 2026 09:44:27 GMT"
      ],
      "Server": [
            "Werkzeug/3.1.8 Python/3.10.0"
      ]
   },
   "statusCode": 200,
   "isBase64Encoded": true
}

The body in the output is base64 encoded. After decoding, you should see the following content:

{"message":"Hello from scratch-http sample!"}

Test POST /index

Create a test event with the following parameters:

  • Event Name: test-event-post

  • Event Template: API Gateway (Dedicated)

apig_post_index.json
{
  "body": "{\"name\": \"Hello World\"}",
  "requestContext": {
      "apiId": "bc1dcffd-aa35-474d-897c-d53425a4c08e",
      "requestId": "11cdcdcf33949dc6d722640a13091c77",
      "stage": "RELEASE"
  },
  "queryStringParameters": {
      "responseType": "application/json"
  },
  "httpMethod": "POST",
  "pathParameters": {},
  "headers": {
      "accept-language": "q=0.5,en-US;q=0.3,en;q=0.2",
      "accept-encoding": "gzip, deflate, br",
      "x-forwarded-port": "443",
      "x-forwarded-for": "103.218.216.98",
      "accept": "application/json",
      "upgrade-insecure-requests": "1",
      "host": "host",
      "x-forwarded-proto": "https",
      "pragma": "no-cache",
      "cache-control": "no-cache",
      "x-real-ip": "103.218.216.98",
      "user-agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:57.0) Gecko/20100101 Firefox/57.0"
  },
  "path": "/index",
  "isBase64Encoded": false
}

Click Test to execute the function with the test event created, and you should see the following output in the Execution Result section

Execution Result POST
{
   "body": "eyJib2R5IjoiXCIvaW5kZXggc3VjY2Vzc1wiIiwiaGVhZGVycyI6eyJDb250ZW50LVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIn0sImlzQmFzZTY0RW5jb2RlZCI6ZmFsc2UsInN0YXR1c0NvZGUiOjIwMH0K",
   "headers": {
      "Content-Length": [
            "117"
      ],
      "Content-Type": [
            "application/json"
      ],
      "Date": [
            "Mon, 21 Sep 2026 09:46:43 GMT"
      ],
      "Server": [
            "Werkzeug/3.1.8 Python/3.10.0"
      ]
   },
   "statusCode": 200,
   "isBase64Encoded": true
}

The body in the output is base64 encoded. After decoding, you should see the following content:

{"body":"\"/index success\"","headers":{"Content-Type":"application/json"},"isBase64Encoded":false,"statusCode":200}