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-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.
Run the following commands to create a project folder.
mkdir -p scratch-http-simple/src cd scratch-http-simple
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
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
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:
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:
curl -X GET localhost:8000/index
{"message":"Hello from scratch-http sample!"}
curl -X POST localhost:8000/index
{"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.
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:
/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.
# 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)
{
"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
{
"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)
{
"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
{
"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}