You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@flink.apache.org by tz...@apache.org on 2020/03/26 14:27:38 UTC

[flink-statefun] branch release-2.0 updated: [FLINK-16783] Add Python docker-compose based example

This is an automated email from the ASF dual-hosted git repository.

tzulitai pushed a commit to branch release-2.0
in repository https://gitbox.apache.org/repos/asf/flink-statefun.git


The following commit(s) were added to refs/heads/release-2.0 by this push:
     new a558a01  [FLINK-16783] Add Python docker-compose based example
a558a01 is described below

commit a558a01849b6b0f61983745e54916b4a12346c80
Author: Igal Shilman <ig...@gmail.com>
AuthorDate: Wed Mar 25 22:12:37 2020 +0100

    [FLINK-16783] Add Python docker-compose based example
    
    This closes #74.
---
 .../statefun-python-greeter/Dockerfile             |  22 +++
 .../statefun-python-greeter/README.md              |  32 ++++
 .../statefun-python-greeter/build-example.sh       |  40 +++++
 .../statefun-python-greeter/docker-compose.yml     |  73 +++++++++
 .../statefun-python-greeter/generator/Dockerfile   |  30 ++++
 .../generator/event-generator.py                   | 104 +++++++++++++
 .../generator/messages_pb2.py                      | 172 +++++++++++++++++++++
 .../statefun-python-greeter/greeter/Dockerfile     |  34 ++++
 .../statefun-python-greeter/greeter/greeter.py     |  82 ++++++++++
 .../statefun-python-greeter/greeter/messages.proto |  44 ++++++
 .../greeter/messages_pb2.py                        | 172 +++++++++++++++++++++
 .../greeter/requirements.txt                       |  22 +++
 .../statefun-python-greeter/module.yaml            |  54 +++++++
 13 files changed, 881 insertions(+)

diff --git a/statefun-examples/statefun-python-greeter/Dockerfile b/statefun-examples/statefun-python-greeter/Dockerfile
new file mode 100644
index 0000000..9f3a803
--- /dev/null
+++ b/statefun-examples/statefun-python-greeter/Dockerfile
@@ -0,0 +1,22 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+FROM statefun
+
+RUN mkdir -p /opt/statefun/modules/greeter
+ADD module.yaml /opt/statefun/modules/greeter
+
+
diff --git a/statefun-examples/statefun-python-greeter/README.md b/statefun-examples/statefun-python-greeter/README.md
new file mode 100644
index 0000000..aee4826
--- /dev/null
+++ b/statefun-examples/statefun-python-greeter/README.md
@@ -0,0 +1,32 @@
+# The Greeter Example
+
+This is a simple example that runs a simple stateful function that accepts requests from a Kafka ingress,
+and then responds by sending greeting responses to a Kafka egress. It demonstrates the primitive building blocks
+of a Stateful Functions applications, such as ingresses, handling state in functions,
+and sending messages to egresses.
+
+
+## Building the example
+
+1) Make sure that you have built the Python distribution
+   To build the distribution
+    -  `cd statefun-python-sdk/`
+    -  `./build-distribution.sh`
+    
+2) Run `./build-example.sh` 
+
+## Running the example
+
+To run the example:
+
+```
+./build-example.sh
+docker-compose up -d
+```
+
+Then, to see the example in actions, see what comes out of the topic `greetings`:
+
+```
+docker-compose logs -f event-generator 
+```
+
diff --git a/statefun-examples/statefun-python-greeter/build-example.sh b/statefun-examples/statefun-python-greeter/build-example.sh
new file mode 100755
index 0000000..9440c5d
--- /dev/null
+++ b/statefun-examples/statefun-python-greeter/build-example.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# clean
+rm -f apache_flink_statefun-*-py3-none-any.whl
+rm -rf __pycache__
+
+# copy the whl distribution
+cp ../../statefun-python-sdk/dist/apache_flink_statefun-*-py3-none-any.whl greeter/apache_flink_statefun-snapshot-py3-none-any.whl 2>/dev/null
+rc=$?
+if [[ ${rc} -ne 0 ]]; then
+    echo "Failed copying the whl distribution, please build the Python distribution first."
+    echo "To build the distribution:"
+    echo "  goto to statefun-python-sdk/"
+    echo "  call ./build-distribution.sh"
+    exit 1;
+fi
+
+# build
+
+docker-compose build
+
+rm -f greeter/apache_flink_statefun-*-py3-none-any.whl
+
+echo "Done. To start the example run: docker-compose up"
+
diff --git a/statefun-examples/statefun-python-greeter/docker-compose.yml b/statefun-examples/statefun-python-greeter/docker-compose.yml
new file mode 100644
index 0000000..501e36b
--- /dev/null
+++ b/statefun-examples/statefun-python-greeter/docker-compose.yml
@@ -0,0 +1,73 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+version: "2.1"
+services:
+  zookeeper:
+    image: wurstmeister/zookeeper
+    ports:
+      - "2181:2181"
+  kafka-broker:
+    image: wurstmeister/kafka:2.12-2.0.1
+    ports:
+      - "9092:9092"
+    environment:
+      HOSTNAME_COMMAND: "route -n | awk '/UG[ \t]/{print $$2}'"
+      KAFKA_CREATE_TOPICS: "names:1:1,greetings:1:1"
+      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
+    depends_on:
+      - zookeeper
+    volumes:
+      - /var/run/docker.sock:/var/run/docker.sock
+  master:
+    build:
+      context: .
+    expose:
+      - "6123"
+    ports:
+      - "8081:8081"
+    environment:
+      - ROLE=master
+      - MASTER_HOST=master
+    volumes:
+      - ./checkpoint-dir:/checkpoint-dir
+  worker:
+    build:
+      context: .
+    expose:
+      - "6121"
+      - "6122"
+    depends_on:
+      - master
+      - kafka-broker
+    links:
+      - "master:master"
+      - "kafka-broker:kafka-broker"
+    environment:
+      - ROLE=worker
+      - MASTER_HOST=master
+    volumes:
+      - ./checkpoint-dir:/checkpoint-dir
+  python-worker:
+    build:
+      context: ./greeter
+    expose:
+      - "8000"
+  event-generator:
+    build:
+      context: generator
+      dockerfile: Dockerfile
+    depends_on:
+      - kafka-broker
diff --git a/statefun-examples/statefun-python-greeter/generator/Dockerfile b/statefun-examples/statefun-python-greeter/generator/Dockerfile
new file mode 100644
index 0000000..c3cf87c
--- /dev/null
+++ b/statefun-examples/statefun-python-greeter/generator/Dockerfile
@@ -0,0 +1,30 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+FROM python:3.7-alpine
+
+RUN mkdir -p /app
+WORKDIR /app
+
+RUN pip install protobuf
+RUN pip install kafka-python
+
+COPY event-generator.py /app
+COPY messages_pb2.py /app
+
+CMD ["python", "/app/event-generator.py"]
+
+
diff --git a/statefun-examples/statefun-python-greeter/generator/event-generator.py b/statefun-examples/statefun-python-greeter/generator/event-generator.py
new file mode 100644
index 0000000..385ef7d
--- /dev/null
+++ b/statefun-examples/statefun-python-greeter/generator/event-generator.py
@@ -0,0 +1,104 @@
+################################################################################
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+# limitations under the License.
+################################################################################
+
+import signal
+import sys
+import time
+import threading
+
+import random
+
+from kafka.errors import NoBrokersAvailable
+
+from messages_pb2 import GreetRequest, GreetResponse
+
+from kafka import KafkaProducer
+from kafka import KafkaConsumer
+
+KAFKA_BROKER = "kafka-broker:9092"
+NAMES = ["Jerry", "George", "Elaine", "Kramer", "Newman", "Frank"]
+
+
+def random_requests():
+    """Generate infinite sequence of random GreetRequests."""
+    while True:
+        request = GreetRequest()
+        request.name = random.choice(NAMES)
+        yield request
+
+
+def produce():
+    if len(sys.argv) == 2:
+        delay_seconds = int(sys.argv[1])
+    else:
+        delay_seconds = 1
+    producer = KafkaProducer(bootstrap_servers=[KAFKA_BROKER])
+    for request in random_requests():
+        key = request.name.encode('utf-8')
+        val = request.SerializeToString()
+        producer.send(topic='names', key=key, value=val)
+        producer.flush()
+        time.sleep(delay_seconds)
+
+
+def consume():
+    consumer = KafkaConsumer(
+        'greetings',
+        bootstrap_servers=[KAFKA_BROKER],
+        auto_offset_reset='earliest',
+        group_id='event-gen')
+    for message in consumer:
+        response = GreetResponse()
+        response.ParseFromString(message.value)
+        print("%s:\t%s" % (response.name, response.greeting), flush=True)
+
+
+def handler(number, frame):
+    sys.exit(0)
+
+
+def safe_loop(fn):
+    while True:
+        try:
+            fn()
+        except SystemExit:
+            print("Good bye!")
+            return
+        except NoBrokersAvailable:
+            time.sleep(2)
+            continue
+        except Exception as e:
+            print(e)
+            return
+
+
+def main():
+    signal.signal(signal.SIGTERM, handler)
+
+    producer = threading.Thread(target=safe_loop, args=[produce])
+    producer.start()
+
+    consumer = threading.Thread(target=safe_loop, args=[consume])
+    consumer.start()
+
+    producer.join()
+    consumer.join()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/statefun-examples/statefun-python-greeter/generator/messages_pb2.py b/statefun-examples/statefun-python-greeter/generator/messages_pb2.py
new file mode 100644
index 0000000..bce4d24
--- /dev/null
+++ b/statefun-examples/statefun-python-greeter/generator/messages_pb2.py
@@ -0,0 +1,172 @@
+# -*- coding: utf-8 -*-
+################################################################################
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+# limitations under the License.
+################################################################################
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: messages.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+  name='messages.proto',
+  package='example',
+  syntax='proto3',
+  serialized_options=None,
+  serialized_pb=_b('\n\x0emessages.proto\x12\x07\x65xample\"\x1c\n\x0cGreetRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"/\n\rGreetResponse\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x10\n\x08greeting\x18\x02 \x01(\t\"\x19\n\tSeenCount\x12\x0c\n\x04seen\x18\x01 \x01(\x03\x62\x06proto3')
+)
+
+
+
+
+_GREETREQUEST = _descriptor.Descriptor(
+  name='GreetRequest',
+  full_name='example.GreetRequest',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='name', full_name='example.GreetRequest.name', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=27,
+  serialized_end=55,
+)
+
+
+_GREETRESPONSE = _descriptor.Descriptor(
+  name='GreetResponse',
+  full_name='example.GreetResponse',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='name', full_name='example.GreetResponse.name', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='greeting', full_name='example.GreetResponse.greeting', index=1,
+      number=2, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=57,
+  serialized_end=104,
+)
+
+
+_SEENCOUNT = _descriptor.Descriptor(
+  name='SeenCount',
+  full_name='example.SeenCount',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='seen', full_name='example.SeenCount.seen', index=0,
+      number=1, type=3, cpp_type=2, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=106,
+  serialized_end=131,
+)
+
+DESCRIPTOR.message_types_by_name['GreetRequest'] = _GREETREQUEST
+DESCRIPTOR.message_types_by_name['GreetResponse'] = _GREETRESPONSE
+DESCRIPTOR.message_types_by_name['SeenCount'] = _SEENCOUNT
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+GreetRequest = _reflection.GeneratedProtocolMessageType('GreetRequest', (_message.Message,), dict(
+  DESCRIPTOR = _GREETREQUEST,
+  __module__ = 'messages_pb2'
+  # @@protoc_insertion_point(class_scope:example.GreetRequest)
+  ))
+_sym_db.RegisterMessage(GreetRequest)
+
+GreetResponse = _reflection.GeneratedProtocolMessageType('GreetResponse', (_message.Message,), dict(
+  DESCRIPTOR = _GREETRESPONSE,
+  __module__ = 'messages_pb2'
+  # @@protoc_insertion_point(class_scope:example.GreetResponse)
+  ))
+_sym_db.RegisterMessage(GreetResponse)
+
+SeenCount = _reflection.GeneratedProtocolMessageType('SeenCount', (_message.Message,), dict(
+  DESCRIPTOR = _SEENCOUNT,
+  __module__ = 'messages_pb2'
+  # @@protoc_insertion_point(class_scope:example.SeenCount)
+  ))
+_sym_db.RegisterMessage(SeenCount)
+
+
+# @@protoc_insertion_point(module_scope)
diff --git a/statefun-examples/statefun-python-greeter/greeter/Dockerfile b/statefun-examples/statefun-python-greeter/greeter/Dockerfile
new file mode 100644
index 0000000..d313fdd
--- /dev/null
+++ b/statefun-examples/statefun-python-greeter/greeter/Dockerfile
@@ -0,0 +1,34 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+FROM python:3.7-alpine
+
+RUN mkdir -p /app
+WORKDIR /app
+
+COPY apache_flink_statefun-snapshot-py3-none-any.whl /app
+RUN pip install apache_flink_statefun-snapshot-py3-none-any.whl
+
+COPY requirements.txt /app
+RUN pip install -r requirements.txt
+
+COPY greeter.py /app
+COPY messages_pb2.py /app
+
+EXPOSE 8000
+
+CMD ["gunicorn", "-b", "0.0.0.0:8000", "-w 4", "greeter:app"]
+
diff --git a/statefun-examples/statefun-python-greeter/greeter/greeter.py b/statefun-examples/statefun-python-greeter/greeter/greeter.py
new file mode 100644
index 0000000..bc56a12
--- /dev/null
+++ b/statefun-examples/statefun-python-greeter/greeter/greeter.py
@@ -0,0 +1,82 @@
+################################################################################
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+# limitations under the License.
+################################################################################
+from messages_pb2 import SeenCount, GreetRequest, GreetResponse
+
+from statefun import StatefulFunctions
+from statefun import RequestReplyHandler
+from statefun import kafka_egress_record
+
+functions = StatefulFunctions()
+
+
+@functions.bind("example/greeter")
+def greet(context, greet_request: GreetRequest):
+    state = context.state('seen_count').unpack(SeenCount)
+    if not state:
+        state = SeenCount()
+        state.seen = 1
+    else:
+        state.seen += 1
+    context.state('seen_count').pack(state)
+
+    response = compute_greeting(greet_request.name, state.seen)
+
+    egress_message = kafka_egress_record(topic="greetings", key=greet_request.name, value=response)
+    context.pack_and_send_egress("example/greets", egress_message)
+
+
+def compute_greeting(name, seen):
+    """
+    Compute a personalized greeting, based on the number of times this @name had been seen before.
+    """
+    templates = ["", "Welcome %s", "Nice to see you again %s", "Third time is a charm %s"]
+    if seen < len(templates):
+        greeting = templates[seen] % name
+    else:
+        greeting = "Nice to see you at the %d-nth time %s!" % (seen, name)
+
+    response = GreetResponse()
+    response.name = name
+    response.greeting = greeting
+
+    return response
+
+
+handler = RequestReplyHandler(functions)
+
+#
+# Serve the endpoint
+#
+
+from flask import request
+from flask import make_response
+from flask import Flask
+
+app = Flask(__name__)
+
+
+@app.route('/statefun', methods=['POST'])
+def handle():
+    response_data = handler(request.data)
+    response = make_response(response_data)
+    response.headers.set('Content-Type', 'application/octet-stream')
+    return response
+
+
+if __name__ == "__main__":
+    app.run()
diff --git a/statefun-examples/statefun-python-greeter/greeter/messages.proto b/statefun-examples/statefun-python-greeter/greeter/messages.proto
new file mode 100644
index 0000000..3dc1a50
--- /dev/null
+++ b/statefun-examples/statefun-python-greeter/greeter/messages.proto
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+syntax = "proto3";
+
+// protoc *.proto --python_out=.
+
+package example;
+
+// External request sent by a user who wants to be greeted
+message GreetRequest {
+    // The name of the user to greet
+    string name = 1;
+}
+
+// A customized response sent to the user
+message GreetResponse {
+    // The name of the user being greeted
+    string name = 1;
+    // The users customized greeting
+    string greeting = 2;
+}
+
+// An internal message used to store state
+message SeenCount {
+    // The number of times a users has been seen so far
+    int64 seen = 1;
+}
+
+
diff --git a/statefun-examples/statefun-python-greeter/greeter/messages_pb2.py b/statefun-examples/statefun-python-greeter/greeter/messages_pb2.py
new file mode 100644
index 0000000..bce4d24
--- /dev/null
+++ b/statefun-examples/statefun-python-greeter/greeter/messages_pb2.py
@@ -0,0 +1,172 @@
+# -*- coding: utf-8 -*-
+################################################################################
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+# limitations under the License.
+################################################################################
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: messages.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+  name='messages.proto',
+  package='example',
+  syntax='proto3',
+  serialized_options=None,
+  serialized_pb=_b('\n\x0emessages.proto\x12\x07\x65xample\"\x1c\n\x0cGreetRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"/\n\rGreetResponse\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x10\n\x08greeting\x18\x02 \x01(\t\"\x19\n\tSeenCount\x12\x0c\n\x04seen\x18\x01 \x01(\x03\x62\x06proto3')
+)
+
+
+
+
+_GREETREQUEST = _descriptor.Descriptor(
+  name='GreetRequest',
+  full_name='example.GreetRequest',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='name', full_name='example.GreetRequest.name', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=27,
+  serialized_end=55,
+)
+
+
+_GREETRESPONSE = _descriptor.Descriptor(
+  name='GreetResponse',
+  full_name='example.GreetResponse',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='name', full_name='example.GreetResponse.name', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='greeting', full_name='example.GreetResponse.greeting', index=1,
+      number=2, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=57,
+  serialized_end=104,
+)
+
+
+_SEENCOUNT = _descriptor.Descriptor(
+  name='SeenCount',
+  full_name='example.SeenCount',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='seen', full_name='example.SeenCount.seen', index=0,
+      number=1, type=3, cpp_type=2, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=106,
+  serialized_end=131,
+)
+
+DESCRIPTOR.message_types_by_name['GreetRequest'] = _GREETREQUEST
+DESCRIPTOR.message_types_by_name['GreetResponse'] = _GREETRESPONSE
+DESCRIPTOR.message_types_by_name['SeenCount'] = _SEENCOUNT
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+GreetRequest = _reflection.GeneratedProtocolMessageType('GreetRequest', (_message.Message,), dict(
+  DESCRIPTOR = _GREETREQUEST,
+  __module__ = 'messages_pb2'
+  # @@protoc_insertion_point(class_scope:example.GreetRequest)
+  ))
+_sym_db.RegisterMessage(GreetRequest)
+
+GreetResponse = _reflection.GeneratedProtocolMessageType('GreetResponse', (_message.Message,), dict(
+  DESCRIPTOR = _GREETRESPONSE,
+  __module__ = 'messages_pb2'
+  # @@protoc_insertion_point(class_scope:example.GreetResponse)
+  ))
+_sym_db.RegisterMessage(GreetResponse)
+
+SeenCount = _reflection.GeneratedProtocolMessageType('SeenCount', (_message.Message,), dict(
+  DESCRIPTOR = _SEENCOUNT,
+  __module__ = 'messages_pb2'
+  # @@protoc_insertion_point(class_scope:example.SeenCount)
+  ))
+_sym_db.RegisterMessage(SeenCount)
+
+
+# @@protoc_insertion_point(module_scope)
diff --git a/statefun-examples/statefun-python-greeter/greeter/requirements.txt b/statefun-examples/statefun-python-greeter/greeter/requirements.txt
new file mode 100644
index 0000000..fcbc07c
--- /dev/null
+++ b/statefun-examples/statefun-python-greeter/greeter/requirements.txt
@@ -0,0 +1,22 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+apache-flink-statefun
+flask==1.1.1
+gunicorn==20.0.4
+
+
+
diff --git a/statefun-examples/statefun-python-greeter/module.yaml b/statefun-examples/statefun-python-greeter/module.yaml
new file mode 100644
index 0000000..146e947
--- /dev/null
+++ b/statefun-examples/statefun-python-greeter/module.yaml
@@ -0,0 +1,54 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+version: "1.0"
+module:
+  meta:
+    type: remote
+  spec:
+    functions:
+      - function:
+          meta:
+            kind: http
+            type: example/greeter
+          spec:
+            endpoint: http://python-worker:8000/statefun
+            states:
+              - seen_count
+            maxNumBatchRequests: 500
+            timeout: 2min
+    ingresses:
+      - ingress:
+          meta:
+            type: statefun.kafka.io/routable-protobuf-ingress
+            id: example/names
+          spec:
+            address: kafka-broker:9092
+            consumerGroupId: my-group-id
+            topics:
+              - topic: names
+                typeUrl: com.googleapis/example.GreetRequest
+                targets:
+                  - example/greeter
+    egresses:
+      - egress:
+          meta:
+            type: statefun.kafka.io/generic-egress
+            id: example/greets
+          spec:
+            address: kafka-broker:9092
+            deliverySemantic:
+              type: exactly-once
+              transactionTimeoutMillis: 100000
+