Skip to content

Update the handler to match style of golang-http-template #16

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
*.pyc
build
163 changes: 144 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,157 @@
# python-flask-template
OpenFaaS Python Flask Templates
=============================================

Python OpenFaaS template with Flask
The Python Flask templates that make use of the incubator project [of-watchdog](https://github.com/openfaas-incubator/of-watchdog).

To try this out with either Python 2.7 or Python 3.6:

```bash
faas template pull https://github.com/openfaas-incubator/python-flask-template
faas new --list
Languages available as templates:
Templates available in this repository:
- python27-flask
- python3-flask
```
- python3-flask-armhf
- python3-http
- python3-http-armhf

Generate a function with one of the languages:
Notes:
- To build and deploy a function for Raspberry Pi or ARMv7 in general, use the language templates ending in *-armhf*

```bash
faas new --lang python3-flask myfunction
mv myfunction.yml stack.yml
## Downloading the templates
```
$ faas template pull https://github.com/openfaas-incubator/python-flask-template
```

Followed by the usual flow:
# Using the python27-flask/python3-flask templates
Create a new function
```
$ faas new --lang python27-flask <fn-name>
```
Build, push, and deploy
```
$ faas up -f <fn-name>.yml
```
Test the new function
```
$ echo -n content | faas invoke <fn-name>
```

# Using the python3-http templates
Create a new function
```
$ faas new --lang python3-http <fn-name>
```
Build, push, and deploy
```
$ faas up -f <fn-name>.yml
```
Set your OpenFaaS gateway URL. For example:
```
faas build \
&& faas deploy
&& faas list --verbose
$ OPENFAAS_URL=http://127.0.0.1:8080
```
Test the new function
```
$ curl -i $OPENFAAS_URL/function/<fn-name>
```

## Event and Context Data
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an "official doc" where the contract with the watchdog is explained?
The goal is to know which field is mandatory, and which is not

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @pcorbel how is this question related to the Python template?

The function handler is passed two arguments, *event* and *context*.

*event* contains data about the request, including:
- body
- headers
- method
- query
- path

*context* contains basic information about the function, including:
- hostname

## Response Bodies
By default, the template will automatically attempt to set the correct Content-Type header for you based on the type of response.

# Wait a couple of seconds then:
For example, returning a dict object type will automatically attach the header `Content-Type: application/json` and returning a string type will automatically attach the `Content-Type: text/html, charset=utf-8` for you.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we mention flask given that this is not visible to the user? the template will?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

..


echo -n content | faas invoke myfunction
## Example usage
### Custom status codes and response bodies
Successful response status code and JSON response body
```python
def handle(event, context):
return {
"statusCode": 200,
"body": {
"key": "value"
}
}
```
Successful response status code and string response body
```python
def handle(event, context):
return {
"statusCode": 201,
"body": "Object successfully created"
}
```
Failure response status code and JSON error message
```python
def handle(event, context):
return {
"statusCode": 400,
"body": {
"error": "Bad request"
}
}
```
### Custom Response Headers
Setting custom response headers
```python
def handle(event, context):
return {
"statusCode": 200,
"body": {
"key": "value"
},
"headers": {
"Location": "https://www.example.com/"
}
}
```
### Accessing Event Data
Accessing request body
```python
def handle(event, context):
return {
"statusCode": 200,
"body": "You said: " + str(event.body)
}
```
Accessing request method
```python
def handle(event, context):
if event.method == 'GET':
return {
"statusCode": 200,
"body": "GET request"
}
else:
return {
"statusCode": 405,
"body": "Method not allowed"
}
```
Accessing request query string arguments
```python
def handle(event, context):
return {
"statusCode": 200,
"body": {
"name": event.query['name']
}
}
```
Accessing request headers
```python
def handle(event, context):
return {
"statusCode": 200,
"body": {
"content-type-received": event.headers['Content-Type']
}
}
```
48 changes: 48 additions & 0 deletions template/python3-http-armhf/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
FROM armhf/python:3.6-alpine
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stock image 'library/python:3.6-alpine' is compatible with ARM.
So we can use

FROM python:3.6-alpine

and if the image is build on ARM, the ARM version will be pulled.
Tested on Raspberry pi OK 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I had no idea about that! You've tested it on the Raspberry Pi already?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I've tested and it ran successfully

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tried this and it failed. How were you able to run the image on your Raspberry Pi? Did you have to make any other changes?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried what / what failed? Give steps please so we can try too.

Copy link
Member

@alexellis alexellis Mar 3, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to work for me on armhf:

docker run -ti python:3.6-alpine /bin/sh

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried building the template with python:3.6-alpine and deploying it on the rpi, but the watchdog fails with the message Error reading stdout: EOF.


ARG ADDITIONAL_PACKAGE
# Alternatively use ADD https:// (which will not be cached by Docker builder)
RUN apk --no-cache add curl ${ADDITIONAL_PACKAGE} \
&& echo "Pulling watchdog binary from Github." \
&& curl -sSLf https://github.com/openfaas-incubator/of-watchdog/releases/download/0.4.6/of-watchdog-armhf > /usr/bin/fwatchdog \
&& chmod +x /usr/bin/fwatchdog \
&& apk del curl --no-cache

# Add non root user
RUN addgroup -S app && adduser app -S -G app
RUN chown app /home/app

USER app

ENV PATH=$PATH:/home/app/.local/bin

WORKDIR /home/app/

COPY index.py .
COPY requirements.txt .
USER root
RUN pip install -r requirements.txt
USER app

RUN mkdir -p function
RUN touch ./function/__init__.py
WORKDIR /home/app/function/
COPY function/requirements.txt .
RUN pip install --user -r requirements.txt

WORKDIR /home/app/

USER root
COPY function function
RUN chown -R app:app ./
USER app

# Set up of-watchdog for HTTP mode
ENV fprocess="python index.py"
ENV cgi_headers="true"
ENV mode="http"
ENV upstream_url="http://127.0.0.1:5000"

HEALTHCHECK --interval=5s CMD [ -e /tmp/.lock ] || exit 1

CMD ["fwatchdog"]
6 changes: 6 additions & 0 deletions template/python3-http-armhf/function/handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
def handle(event, context):
# TODO implement
return {
"statusCode": 200,
"body": "Hello from OpenFaaS!"
}
Empty file.
69 changes: 69 additions & 0 deletions template/python3-http-armhf/index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env python
from flask import Flask, request, jsonify
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No header?
I think it would be a good practice to put

#!/usr/bin/env python
# -*- coding: utf-8 -*-

everywhere

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unfamiliar with this so I'll take a look into it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We haven't done this elsewhere so please propose it generically @pcorbel in a separate issue.

from waitress import serve
import os

from function import handler

app = Flask(__name__)

class Event:
def __init__(self):
self.body = request.get_data()
self.headers = request.headers
self.method = request.method
self.query = request.args
self.path = request.path
Copy link

@dschulten dschulten Apr 4, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also ad request.files and request.form - they are needed for multipart/form-data requests. The request.files attribute is an ImmutableMultiDict of FileStorage (which is a file-ish ducktype), and form is an ImmutableMultiDict of strings which represent the textual multiparts which have no filename in their content-disposition. Maybe make a plain dictionary of lists from the MultiDict, to abstract away the Flask api.

That would make my pull request obsolete.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That won't be added at this time, but feel free to propose it as an issue.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


class Context:
def __init__(self):
self.hostname = os.environ['HOSTNAME']

def format_status_code(resp):
if 'statusCode' in resp:
return resp['statusCode']

return 200

def format_body(resp):
if 'body' not in resp:
return ""
elif type(resp['body']) == dict:
return jsonify(resp['body'])
else:
return str(resp['body'])

def format_headers(resp):
if 'headers' not in resp:
return []
elif type(resp['headers']) == dict:
headers = []
for key in resp['headers'].keys():
header_tuple = (key, resp['headers'][key])
headers.append(header_tuple)
return headers

return resp['headers']

def format_response(resp):
if resp == None:
return ('', 200)

statusCode = format_status_code(resp)
body = format_body(resp)
headers = format_headers(resp)

return (body, statusCode, headers)

@app.route('/', defaults={'path': ''}, methods=['GET', 'PUT', 'POST', 'PATCH', 'DELETE'])
@app.route('/<path:path>', methods=['GET', 'PUT', 'POST', 'PATCH', 'DELETE'])
def call_handler(path):
event = Event()
context = Context()
response_data = handler.handle(event, context)

resp = format_response(response_data)
return resp

if __name__ == '__main__':
serve(app, host='0.0.0.0', port=5000)
2 changes: 2 additions & 0 deletions template/python3-http-armhf/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
flask
waitress
14 changes: 14 additions & 0 deletions template/python3-http-armhf/template.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
language: python3-http-armhf
fprocess: python index.py
build_options:
- name: dev
packages:
- make
- automake
- gcc
- g++
- subversion
- python3-dev
- musl-dev
- libffi-dev
- git
48 changes: 48 additions & 0 deletions template/python3-http/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
FROM python:3.6-alpine

ARG ADDITIONAL_PACKAGE
# Alternatively use ADD https:// (which will not be cached by Docker builder)
RUN apk --no-cache add curl ${ADDITIONAL_PACKAGE} \

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a user, I'd like to be able to install build dependencies and have them removed after pip installs dependencies.

Is that possible?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some build dependencies generate .so libraries which are needed in the deployment / runtime container.

&& echo "Pulling watchdog binary from Github." \
&& curl -sSLf https://github.com/openfaas-incubator/of-watchdog/releases/download/0.4.6/of-watchdog > /usr/bin/fwatchdog \
&& chmod +x /usr/bin/fwatchdog \
&& apk del curl --no-cache

# Add non root user
RUN addgroup -S app && adduser app -S -G app
RUN chown app /home/app

USER app

ENV PATH=$PATH:/home/app/.local/bin

WORKDIR /home/app/

COPY index.py .
COPY requirements.txt .
USER root
RUN pip install -r requirements.txt
USER app

RUN mkdir -p function
RUN touch ./function/__init__.py
WORKDIR /home/app/function/
COPY function/requirements.txt .
RUN pip install --user -r requirements.txt

WORKDIR /home/app/

USER root
COPY function function
RUN chown -R app:app ./
USER app

# Set up of-watchdog for HTTP mode
ENV fprocess="python index.py"
ENV cgi_headers="true"
ENV mode="http"
ENV upstream_url="http://127.0.0.1:5000"

HEALTHCHECK --interval=5s CMD [ -e /tmp/.lock ] || exit 1

CMD ["fwatchdog"]
6 changes: 6 additions & 0 deletions template/python3-http/function/handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
def handle(event, context):
# TODO implement
return {
"statusCode": 200,
"body": "Hello from OpenFaaS!"
}
Empty file.
Loading