Skip to content

began addding cloudevents #1

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
51 changes: 20 additions & 31 deletions examples/cloud_run_cloudevents/README.md
Original file line number Diff line number Diff line change
@@ -1,52 +1,41 @@
# Deploying a CloudEvent function to Cloud Run with the Functions Framework

This sample uses the [Cloud Events SDK](https://github.com/cloudevents/sdk-python) to send and receive a CloudEvent on Cloud Run.

## How to run this locally

Build the Docker image:

```commandline
docker build --tag ff_example .
docker build -t ff_example .
```

Run the image and bind the correct ports:

```commandline
docker run -p:8080:8080 ff_example
docker run --rm -p 8080:8080 -e PORT=8080 ff_example
```

Send an event to the container:

```python
from cloudevents.sdk import converters
from cloudevents.sdk import marshaller
from cloudevents.sdk.converters import structured
from cloudevents.sdk.event import v1
from cloudevents.http import CloudEvent, to_structured_http
import requests
import json

def run_structured(event, url):
http_marshaller = marshaller.NewDefaultHTTPMarshaller()
structured_headers, structured_data = http_marshaller.ToRequest(
event, converters.TypeStructured, json.dumps
)
print("structured CloudEvent")
print(structured_data.getvalue())

response = requests.post(url,
headers=structured_headers,
data=structured_data.getvalue())
response.raise_for_status()

event = (
v1.Event()
.SetContentType("application/json")
.SetData('{"name":"john"}')
.SetEventID("my-id")
.SetSource("from-galaxy-far-far-away")
.SetEventTime("tomorrow")
.SetEventType("cloudevent.greet.you")
)

run_structured(event, "http://0.0.0.0:8080/")

# Create event
attributes = {
"Content-Type": "application/json",
"source": "from-galaxy-far-far-away",
"type": "cloudevent.greet.you"
}
data = {"name":"john"}

event = CloudEvent(attributes, data)
print(event)

# Send event
headers, data = to_structured_http(event)
response = requests.post("http:/localhost:8080/", headers=headers, data=data)
response.raise_for_status()
```
4 changes: 3 additions & 1 deletion examples/cloud_run_cloudevents/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@


def hello(cloudevent):
print("Received event with ID: %s" % cloudevent.EventID(), file=sys.stdout, flush=True)
msg = f"Received event with ID: {cloudevent['id']}"
print(msg, file=sys.stdout, flush=True)
return msg
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"click>=7.0,<8.0",
"watchdog>=0.10.0",
"gunicorn>=19.2.0,<21.0; platform_system!='Windows'",
"cloudevents<1.0",
"cloudevents>=1.0",
],
entry_points={
"console_scripts": [
Expand Down
67 changes: 24 additions & 43 deletions src/functions_framework/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@
import sys
import types

import cloudevents.sdk
import cloudevents.sdk.event
import cloudevents.sdk.event.v1
import cloudevents.sdk.marshaller
from cloudevents.http import CloudEvent, from_http
from cloudevents.sdk.converters import is_binary, is_structured

import flask
import werkzeug

Expand Down Expand Up @@ -80,46 +79,27 @@ def view_func(path):
return view_func


def _get_cloudevent_version():
return cloudevents.sdk.event.v1.Event()


def _run_legacy_event(function, request):
event_data = request.get_json()
if not event_data:
flask.abort(400)
print('_run_legacy_event', function, request)
event_object = _Event(**event_data)
data = event_object.data
context = Context(**event_object.context)
function(data, context)


def _run_binary_cloudevent(function, request, cloudevent_def):
data = io.BytesIO(request.get_data())
http_marshaller = cloudevents.sdk.marshaller.NewDefaultHTTPMarshaller()
event = http_marshaller.FromRequest(
cloudevent_def, request.headers, data, json.load
)

function(event)


def _run_structured_cloudevent(function, request, cloudevent_def):
data = io.StringIO(request.get_data(as_text=True))
m = cloudevents.sdk.marshaller.NewDefaultHTTPMarshaller()
event = m.FromRequest(cloudevent_def, request.headers, data, json.loads)
def _run_cloudevent(function, request):
data = request.get_data(as_text=True)
event = from_http(data, request.headers)
function(event)


def _get_event_type(request):
if (
request.headers.get("ce-type")
and request.headers.get("ce-specversion")
and request.headers.get("ce-source")
and request.headers.get("ce-id")
):
if is_binary(request.headers):
return _EventType.CLOUDEVENT_BINARY
elif request.headers.get("Content-Type") == "application/cloudevents+json":
elif is_structured(request.headers):
return _EventType.CLOUDEVENT_STRUCTURED
else:
return _EventType.LEGACY
Expand All @@ -145,19 +125,14 @@ def view_func(path):

def _cloudevent_view_func_wrapper(function, request):
def view_func(path):
cloudevent_def = _get_cloudevent_version()
event_type = _get_event_type(request)
if event_type == _EventType.CLOUDEVENT_STRUCTURED:
_run_structured_cloudevent(function, request, cloudevent_def)
elif event_type == _EventType.CLOUDEVENT_BINARY:
_run_binary_cloudevent(function, request, cloudevent_def)
else:
try:
_run_cloudevent(function, request)
except:
flask.abort(
400,
description="Function was defined with FUNCTION_SIGNATURE_TYPE=cloudevent "
" but it did not receive a cloudevent as a request.",
)

return "OK"

return view_func
Expand Down Expand Up @@ -258,10 +233,13 @@ def create_app(target=None, source=None, signature_type=None):
werkzeug.routing.Rule("/", defaults={"path": ""}, endpoint="run")
)
app.url_map.add(werkzeug.routing.Rule("/robots.txt", endpoint="error"))
app.url_map.add(werkzeug.routing.Rule("/favicon.ico", endpoint="error"))
app.url_map.add(werkzeug.routing.Rule(
"/favicon.ico", endpoint="error"))
app.url_map.add(werkzeug.routing.Rule("/<path:path>", endpoint="run"))
app.view_functions["run"] = _http_view_func_wrapper(function, flask.request)
app.view_functions["error"] = lambda: flask.abort(404, description="Not Found")
app.view_functions["run"] = _http_view_func_wrapper(
function, flask.request)
app.view_functions["error"] = lambda: flask.abort(
404, description="Not Found")
app.after_request(read_request)
elif signature_type == "event" or signature_type == "cloudevent":
app.url_map.add(
Expand All @@ -276,11 +254,13 @@ def create_app(target=None, source=None, signature_type=None):
)

# Add a dummy endpoint for GET /
app.url_map.add(werkzeug.routing.Rule("/", endpoint="get", methods=["GET"]))
app.url_map.add(werkzeug.routing.Rule(
"/", endpoint="get", methods=["GET"]))
app.view_functions["get"] = lambda: ""

# Add the view functions
app.view_functions["event"] = _event_view_func_wrapper(function, flask.request)
app.view_functions["event"] = _event_view_func_wrapper(
function, flask.request)
app.view_functions["cloudevent"] = _cloudevent_view_func_wrapper(
function, flask.request
)
Expand Down Expand Up @@ -314,7 +294,8 @@ def __init__(self, target=None, source=None, signature_type=None):

def __call__(self, *args, **kwargs):
if not self.app:
self.app = create_app(self.target, self.source, self.signature_type)
self.app = create_app(
self.target, self.source, self.signature_type)
return self.app(*args, **kwargs)


Expand Down
1 change: 1 addition & 0 deletions test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{'attributes': {'specversion': '0.3', 'id': 'my-id', 'source': 'from-galaxy-far-far-away', 'type': 'cloudevent.greet.you', 'time': 'tomorrow', 'content-type': 'application/json'}, 'data': '{"name": "john"}'}
66 changes: 27 additions & 39 deletions tests/test_cloudevent_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@
import json
import pathlib

import cloudevents.sdk
import cloudevents.sdk.event.v1
import cloudevents.sdk.event.v03
import cloudevents.sdk.marshaller
from cloudevents.http import CloudEvent, from_http, to_binary_http, to_structured_http

import pytest

from functions_framework import LazyWSGIApp, create_app, exceptions
Expand All @@ -33,29 +31,30 @@

@pytest.fixture
def cloudevent_1_0():
event = (
cloudevents.sdk.event.v1.Event()
.SetContentType("application/json")
.SetData('{"name":"john"}')
.SetEventID("my-id")
.SetSource("from-galaxy-far-far-away")
.SetEventTime("tomorrow")
.SetEventType("cloudevent.greet.you")
)
attributes = {
"Content-Type": "application/json",
"id": "my-id",
"source": "from-galaxy-far-far-away",
"time": "tomorrow",
"type": "cloudevent.greet.you"
}
data = {"name": "john"}
event = CloudEvent(attributes, data)
return event


@pytest.fixture
def cloudevent_0_3():
event = (
cloudevents.sdk.event.v03.Event()
.SetContentType("application/json")
.SetData('{"name":"john"}')
.SetEventID("my-id")
.SetSource("from-galaxy-far-far-away")
.SetEventTime("tomorrow")
.SetEventType("cloudevent.greet.you")
)
attributes = {
"Content-Type": "application/json",
"id": "my-id",
"source": "from-galaxy-far-far-away",
"time": "tomorrow",
"type": "cloudevent.greet.you",
"specversion": "0.3"
}
data = {"name": "john"}
event = CloudEvent(attributes, data)
return event


Expand All @@ -64,13 +63,9 @@ def test_event_1_0(cloudevent_1_0):
target = "function"

client = create_app(target, source, "cloudevent").test_client()
structured_headers, structured_data = to_structured_http(cloudevent_1_0)

m = cloudevents.sdk.marshaller.NewDefaultHTTPMarshaller()
structured_headers, structured_data = m.ToRequest(
cloudevent_1_0, cloudevents.sdk.converters.TypeStructured, json.dumps
)

resp = client.post("/", headers=structured_headers, data=structured_data.getvalue())
resp = client.post("/", headers=structured_headers, data=structured_data)
assert resp.status_code == 200
assert resp.data == b"OK"

Expand All @@ -81,12 +76,8 @@ def test_binary_event_1_0(cloudevent_1_0):

client = create_app(target, source, "cloudevent").test_client()

m = cloudevents.sdk.marshaller.NewDefaultHTTPMarshaller()

binary_headers, binary_data = m.ToRequest(
cloudevent_1_0, cloudevents.sdk.converters.TypeBinary, json.dumps
)

binary_headers, binary_data = to_binary_http(cloudevent_1_0)
print(binary_headers, binary_data)
resp = client.post("/", headers=binary_headers, data=binary_data)

assert resp.status_code == 200
Expand All @@ -99,12 +90,9 @@ def test_event_0_3(cloudevent_0_3):

client = create_app(target, source, "cloudevent").test_client()

m = cloudevents.sdk.marshaller.NewDefaultHTTPMarshaller()
structured_headers, structured_data = m.ToRequest(
cloudevent_0_3, cloudevents.sdk.converters.TypeStructured, json.dumps
)
structured_headers, structured_data = to_structured_http(cloudevent_0_3)

resp = client.post("/", headers=structured_headers, data=structured_data.getvalue())
resp = client.post("/", headers=structured_headers, data=structured_data)
assert resp.status_code == 200
assert resp.data == b"OK"

Expand Down
6 changes: 4 additions & 2 deletions tests/test_event_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ def test_non_legacy_event_fails():
target = "function"

client = create_app(target, source, "event").test_client()
resp = client.post("/", headers=structured_headers, data=structured_data.getvalue())
resp = client.post("/", headers=structured_headers,
data=structured_data.decode())
assert resp.status_code == 400
assert resp.data != b"OK"

Expand Down Expand Up @@ -157,7 +158,8 @@ def test_invalid_function_definition_multiple_entry_points():
create_app(target, source, "event")

assert re.match(
"File .* is expected to contain a function named function", str(excinfo.value)
"File .* is expected to contain a function named function", str(
excinfo.value)
)


Expand Down
11 changes: 6 additions & 5 deletions tests/test_functions/cloudevents/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ def function(cloudevent):
HTTP status code indicating whether valid event was sent or not.

"""

valid_event = (
cloudevent.EventID() == "my-id"
and cloudevent.Data() == '{"name":"john"}'
and cloudevent.Source() == "from-galaxy-far-far-away"
and cloudevent.EventTime() == "tomorrow"
and cloudevent.EventType() == "cloudevent.greet.you"
cloudevent.data == {"name": "john"}
and cloudevent['id'] == "my-id"
and cloudevent['source'] == "from-galaxy-far-far-away"
and cloudevent['time'] == "tomorrow"
and cloudevent['type'] == "cloudevent.greet.you"
)

if not valid_event:
Expand Down
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ envlist = py{35,36,37,38}-{ubuntu-latest,macos-latest,windows-latest},lint
usedevelop = true
deps =
docker
cloudevents
pytest-cov
pytest-integration
pretend
Expand Down