From 86723eca6699bda25ba16f7cea4e6c5d4e779273 Mon Sep 17 00:00:00 2001 From: Joel Gerard Date: Fri, 5 Jun 2020 17:07:22 -0700 Subject: [PATCH 01/45] Ignore IDE files. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c0305632..eeea9ae6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ __pycache__/ build/ dist/ .coverage +.idea/ From b0cd71ac6a8ee886ec147060dd7a9cf94c7296b4 Mon Sep 17 00:00:00 2001 From: Joel Gerard Date: Sat, 6 Jun 2020 08:06:54 -0700 Subject: [PATCH 02/45] Use the test file directory as a basis instead of cwd. Allows tests to be run from anywhere and enables IDE debugger --- tests/test_functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_functions.py b/tests/test_functions.py index c6eccb91..9d3c358c 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os import pathlib import re import time @@ -23,8 +24,7 @@ from functions_framework import LazyWSGIApp, create_app, exceptions -TEST_FUNCTIONS_DIR = pathlib.Path.cwd() / "tests" / "test_functions" - +TEST_FUNCTIONS_DIR = pathlib.Path(os.path.dirname(os.path.abspath(__file__))) / "test_functions" # Python 3.5: ModuleNotFoundError does not exist try: From 64d8e3d0e4f25734b72f5060451afd1d9029fb23 Mon Sep 17 00:00:00 2001 From: joelgerard Date: Tue, 9 Jun 2020 11:46:37 -0700 Subject: [PATCH 03/45] Add support for Cloud Events. Rough draft. I will squash a bunch of these interim commits before submitting the PR. DO NOT SUBMIT --- src/functions_framework/__init__.py | 53 ++++++++++++++++++++++++----- tests/test_functions.py | 32 ++++++++++++++++- tests/test_functions/events/main.py | 44 ++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 10 deletions(-) create mode 100644 tests/test_functions/events/main.py diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index ce0e2fbf..1afdfa49 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -14,6 +14,8 @@ import functools import importlib.util +import io +import json import os.path import pathlib import sys @@ -31,6 +33,10 @@ ) from google.cloud.functions.context import Context + +from cloudevents.sdk.event import v1 +from cloudevents.sdk import marshaller + DEFAULT_SOURCE = os.path.realpath("./main.py") DEFAULT_SIGNATURE_TYPE = "http" @@ -60,9 +66,29 @@ def __init__( self.data = data +def _get_cloud_event_version(request): + headers = request.headers + # TODO: not 100% robust + if headers.get("Content-Type") == "application/cloudevents+json": + return v1.Event() + + return None + +def _convert_request_to_event(request, cloud_event_def): + # TODO: not 100% robust + m = marshaller.NewDefaultHTTPMarshaller() + data = io.StringIO(request.get_data(as_text=True)) + return m.FromRequest(cloud_event_def, request.headers, data, json.loads) + + def _http_view_func_wrapper(function, request): def view_func(path): - return function(request._get_current_object()) + # How do we preserve backwards compatibility? + cloud_event_def = _get_cloud_event_version(request._get_current_object()) + if cloud_event_def is None: + return function(request._get_current_object()) + else: + return function(_convert_request_to_event(request, cloud_event_def)) return view_func @@ -91,14 +117,23 @@ def view_func(path): ) function(data, context) else: - # This is a regular CloudEvent - event_data = request.get_json() - if not event_data: - flask.abort(400) - event_object = _Event(**event_data) - data = event_object.data - context = Context(**event_object.context) - function(data, context) + cloud_event_def = _get_cloud_event_version(request) + if cloud_event_def is None: + # This is a regular CloudEvent + event_data = request.get_json() + if not event_data: + flask.abort(400) + event_object = _Event(**event_data) + data = event_object.data + context = Context(**event_object.context) + function(data, context) + else: + # We have a bonafide event from the SDK. Let's use it. + # TODO(joelgerard): Starting to become a long fn. + event = _convert_request_to_event(request, cloud_event_def) + # TODO: Fix context + # context = Context(**event.context) + function(event) #, context) return "OK" diff --git a/tests/test_functions.py b/tests/test_functions.py index 9d3c358c..c11a7785 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -11,7 +11,7 @@ # 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 json import os import pathlib import re @@ -19,6 +19,10 @@ import pretend import pytest +from cloudevents.sdk import marshaller +from cloudevents.sdk.converters import structured +from cloudevents.sdk.event import v1 +from cloudevents.sdk import converters import functions_framework @@ -46,6 +50,32 @@ def background_json(tmpdir): } +def test_event(): + source = TEST_FUNCTIONS_DIR / "events" / "main.py" + target = "function" + + client = create_app(target, source, "event").test_client() + + event = ( + v1.Event() + .SetContentType("application/json") + .SetData('{"name":"john"}') + .SetEventID("my-id") + .SetSource("from-galaxy-far-far-away") + .SetEventTime("tomorrow") + .SetEventType("cloudevent.greet.you") + ) + m = marshaller.NewDefaultHTTPMarshaller() + structured_headers, structured_data = m.ToRequest( + event, converters.TypeStructured, json.dumps + ) + + resp = client.post("/", headers=structured_headers, + data=structured_data.getvalue()) + assert resp.status_code == 200 + assert resp.data == b"success" + + def test_http_function_executes_success(): source = TEST_FUNCTIONS_DIR / "http_trigger" / "main.py" target = "function" diff --git a/tests/test_functions/events/main.py b/tests/test_functions/events/main.py new file mode 100644 index 00000000..4ac22e5f --- /dev/null +++ b/tests/test_functions/events/main.py @@ -0,0 +1,44 @@ +# Copyright 2020 Google LLC +# +# Licensed 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. + +"""Function used in Worker tests of handling HTTP functions.""" +import io + +import flask +import json +from cloudevents.sdk import marshaller +from cloudevents.sdk.event import v1 + +def function(event): + """Test HTTP function whose behavior depends on the given mode. + + The function returns a success, a failure, or throws an exception, depending + on the given mode. + + Args: + request: The HTTP request which triggered this function. Must contain name + of the requested mode in the 'mode' field in JSON document in request + body. + + Returns: + Value and status code defined for the given mode. + + Raises: + Exception: Thrown when requested in the incoming mode specification. + """ + + if (event.EventID() == "my-id"): + return "success", 200 + else: + return "failure", 500 From bbf6d35658eb6498aec69509d1498ea94045c596 Mon Sep 17 00:00:00 2001 From: joelgerard Date: Wed, 10 Jun 2020 11:38:40 -0700 Subject: [PATCH 04/45] Return the functions return value. Test Cloud Events SDK 0.3. Add some error handling. Please see all the TODO questions before I finish off this PR. DO NOT SUBMIT --- src/functions_framework/__init__.py | 82 +++++++++++++++-------------- tests/test_functions.py | 40 +++++++++++--- tests/test_functions/events/main.py | 7 +-- 3 files changed, 78 insertions(+), 51 deletions(-) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index 1afdfa49..269ff6d2 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -68,31 +68,27 @@ def __init__( def _get_cloud_event_version(request): headers = request.headers - # TODO: not 100% robust + # TODO(joelgerard): Should there be further inspection of the payload for versioning considerations? if headers.get("Content-Type") == "application/cloudevents+json": - return v1.Event() - + return v1.Event() return None def _convert_request_to_event(request, cloud_event_def): - # TODO: not 100% robust - m = marshaller.NewDefaultHTTPMarshaller() - data = io.StringIO(request.get_data(as_text=True)) - return m.FromRequest(cloud_event_def, request.headers, data, json.loads) + try: + m = marshaller.NewDefaultHTTPMarshaller() + data = io.StringIO(request.get_data(as_text=True)) + return m.FromRequest(cloud_event_def, request.headers, data, json.loads) + except: + raise FunctionsFrameworkException("Content-Type header indicated a Cloud Event, " + "but it could not be parsed.") def _http_view_func_wrapper(function, request): def view_func(path): - # How do we preserve backwards compatibility? - cloud_event_def = _get_cloud_event_version(request._get_current_object()) - if cloud_event_def is None: - return function(request._get_current_object()) - else: - return function(_convert_request_to_event(request, cloud_event_def)) + return function(request._get_current_object()) return view_func - def _is_binary_cloud_event(request): return ( request.headers.get("ce-type") @@ -101,39 +97,47 @@ def _is_binary_cloud_event(request): and request.headers.get("ce-id") ) +# TODO(joelgerard): is this really legacy as I have stated? +def _run_binary_legacy_cloud_event(function, request): + # Support CloudEvents in binary content mode, with data being the + # whole request body and context attributes retrieved from request + # headers. + data = request.get_data() + context = Context( + eventId=request.headers.get("ce-eventId"), + timestamp=request.headers.get("ce-timestamp"), + eventType=request.headers.get("ce-eventType"), + resource=request.headers.get("ce-resource"), + ) + function(data, context) + +def _run_legacy_cloud_event(function, request): + # This is a regular CloudEvent + event_data = request.get_json() + if not event_data: + flask.abort(400) + event_object = _Event(**event_data) + data = event_object.data + context = Context(**event_object.context) + function(data, context) + +def _run_cloud_event(function, request, cloud_event_def): + event = _convert_request_to_event(request, cloud_event_def) + return function(event) def _event_view_func_wrapper(function, request): def view_func(path): if _is_binary_cloud_event(request): - # Support CloudEvents in binary content mode, with data being the - # whole request body and context attributes retrieved from request - # headers. - data = request.get_data() - context = Context( - eventId=request.headers.get("ce-eventId"), - timestamp=request.headers.get("ce-timestamp"), - eventType=request.headers.get("ce-eventType"), - resource=request.headers.get("ce-resource"), - ) - function(data, context) + _run_binary_legacy_cloud_event(function, request) else: cloud_event_def = _get_cloud_event_version(request) if cloud_event_def is None: - # This is a regular CloudEvent - event_data = request.get_json() - if not event_data: - flask.abort(400) - event_object = _Event(**event_data) - data = event_object.data - context = Context(**event_object.context) - function(data, context) + _run_legacy_cloud_event(function, request) else: - # We have a bonafide event from the SDK. Let's use it. - # TODO(joelgerard): Starting to become a long fn. - event = _convert_request_to_event(request, cloud_event_def) - # TODO: Fix context - # context = Context(**event.context) - function(event) #, context) + # We have a bonafide event from the Cloud Event SDK. Let's use it. + # TODO(joelgerard): Note that I return the function value. This seems better + # going forward rather than returning "OK". ?? + return _run_cloud_event(function, request, cloud_event_def) return "OK" diff --git a/tests/test_functions.py b/tests/test_functions.py index c11a7785..89156c42 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -22,6 +22,7 @@ from cloudevents.sdk import marshaller from cloudevents.sdk.converters import structured from cloudevents.sdk.event import v1 +from cloudevents.sdk.event import v03 from cloudevents.sdk import converters import functions_framework @@ -49,13 +50,7 @@ def background_json(tmpdir): "data": {"filename": str(tmpdir / "filename.txt"), "value": "some-value"}, } - -def test_event(): - source = TEST_FUNCTIONS_DIR / "events" / "main.py" - target = "function" - - client = create_app(target, source, "event").test_client() - +def _createTestEvent(): event = ( v1.Event() .SetContentType("application/json") @@ -65,6 +60,35 @@ def test_event(): .SetEventTime("tomorrow") .SetEventType("cloudevent.greet.you") ) + return event + +def test_event_1_0(): + source = TEST_FUNCTIONS_DIR / "events" / "main.py" + target = "function" + + client = create_app(target, source, "event").test_client() + event = _createTestEvent() + + m = marshaller.NewDefaultHTTPMarshaller() + structured_headers, structured_data = m.ToRequest( + event, converters.TypeStructured, json.dumps + ) + + resp = client.post("/", headers=structured_headers, + data=structured_data.getvalue()) + assert resp.status_code == 200 + + # TODO: Note this returns success not OK. Change? + assert resp.data == b"success" + + +def test_event_0_3(): + source = TEST_FUNCTIONS_DIR / "events" / "main.py" + target = "function" + + client = create_app(target, source, "event").test_client() + + event = _createTestEvent() m = marshaller.NewDefaultHTTPMarshaller() structured_headers, structured_data = m.ToRequest( event, converters.TypeStructured, json.dumps @@ -73,6 +97,8 @@ def test_event(): resp = client.post("/", headers=structured_headers, data=structured_data.getvalue()) assert resp.status_code == 200 + + # TODO: Note this returns success not OK. Change? assert resp.data == b"success" diff --git a/tests/test_functions/events/main.py b/tests/test_functions/events/main.py index 4ac22e5f..904dd517 100644 --- a/tests/test_functions/events/main.py +++ b/tests/test_functions/events/main.py @@ -27,17 +27,14 @@ def function(event): on the given mode. Args: - request: The HTTP request which triggered this function. Must contain name - of the requested mode in the 'mode' field in JSON document in request - body. + event: A Cloud event as defined by https://github.com/cloudevents/sdk-python. Returns: Value and status code defined for the given mode. - Raises: - Exception: Thrown when requested in the incoming mode specification. """ + # todo(joelgerard): Should probably check some of this as well. if (event.EventID() == "my-id"): return "success", 200 else: From 3bcb69d19d8b9585ff4aef6d3bbebac0fd47d5b9 Mon Sep 17 00:00:00 2001 From: joelgerard Date: Fri, 12 Jun 2020 16:54:18 -0700 Subject: [PATCH 05/45] Minor cleanup. Split test code. --- .gitignore | 1 - src/functions_framework/__init__.py | 81 ++++++------ tests/test_cloudevent_functions.py | 92 ++++++++++++++ tests/test_event_functions.py | 190 ++++++++++++++++++++++++++++ tests/test_functions.py | 176 +------------------------- tests/test_functions/events/main.py | 11 +- 6 files changed, 330 insertions(+), 221 deletions(-) create mode 100644 tests/test_cloudevent_functions.py create mode 100644 tests/test_event_functions.py diff --git a/.gitignore b/.gitignore index eeea9ae6..c0305632 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,3 @@ __pycache__/ build/ dist/ .coverage -.idea/ diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index 1afdfa49..5530ec5e 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -68,31 +68,27 @@ def __init__( def _get_cloud_event_version(request): headers = request.headers - # TODO: not 100% robust + # TODO(joelgerard): Should there be further inspection of the payload for versioning considerations? if headers.get("Content-Type") == "application/cloudevents+json": return v1.Event() - return None def _convert_request_to_event(request, cloud_event_def): - # TODO: not 100% robust - m = marshaller.NewDefaultHTTPMarshaller() - data = io.StringIO(request.get_data(as_text=True)) - return m.FromRequest(cloud_event_def, request.headers, data, json.loads) + try: + m = marshaller.NewDefaultHTTPMarshaller() + data = io.StringIO(request.get_data(as_text=True)) + return m.FromRequest(cloud_event_def, request.headers, data, json.loads) + except: + raise FunctionsFrameworkException("Content-Type header indicated a Cloud Event, " + "but it could not be parsed.") def _http_view_func_wrapper(function, request): def view_func(path): - # How do we preserve backwards compatibility? - cloud_event_def = _get_cloud_event_version(request._get_current_object()) - if cloud_event_def is None: - return function(request._get_current_object()) - else: - return function(_convert_request_to_event(request, cloud_event_def)) + return function(request._get_current_object()) return view_func - def _is_binary_cloud_event(request): return ( request.headers.get("ce-type") @@ -102,38 +98,47 @@ def _is_binary_cloud_event(request): ) +# TODO(joelgerard): is this really legacy as I have stated? +def _run_binary_legacy_cloud_event(function, request): + # Support CloudEvents in binary content mode, with data being the + # whole request body and context attributes retrieved from request + # headers. + data = request.get_data() + context = Context( + eventId=request.headers.get("ce-eventId"), + timestamp=request.headers.get("ce-timestamp"), + eventType=request.headers.get("ce-eventType"), + resource=request.headers.get("ce-resource"), + ) + function(data, context) + +def _run_legacy_event(function, request): + # This is a regular CloudEvent + event_data = request.get_json() + if not event_data: + flask.abort(400) + event_object = _Event(**event_data) + data = event_object.data + context = Context(**event_object.context) + function(data, context) + +def _run_cloud_event(function, request, cloud_event_def): + event = _convert_request_to_event(request, cloud_event_def) + function(event) + def _event_view_func_wrapper(function, request): def view_func(path): if _is_binary_cloud_event(request): - # Support CloudEvents in binary content mode, with data being the - # whole request body and context attributes retrieved from request - # headers. - data = request.get_data() - context = Context( - eventId=request.headers.get("ce-eventId"), - timestamp=request.headers.get("ce-timestamp"), - eventType=request.headers.get("ce-eventType"), - resource=request.headers.get("ce-resource"), - ) - function(data, context) + _run_binary_legacy_cloud_event(function, request) else: cloud_event_def = _get_cloud_event_version(request) if cloud_event_def is None: - # This is a regular CloudEvent - event_data = request.get_json() - if not event_data: - flask.abort(400) - event_object = _Event(**event_data) - data = event_object.data - context = Context(**event_object.context) - function(data, context) + _run_legacy_event(function, request) else: - # We have a bonafide event from the SDK. Let's use it. - # TODO(joelgerard): Starting to become a long fn. - event = _convert_request_to_event(request, cloud_event_def) - # TODO: Fix context - # context = Context(**event.context) - function(event) #, context) + # We have a bonafide event from the Cloud Event SDK. Let's use it. + # TODO(joelgerard): Note that I return the function value. This seems better + # going forward rather than returning "OK". ?? + _run_cloud_event(function, request, cloud_event_def) return "OK" diff --git a/tests/test_cloudevent_functions.py b/tests/test_cloudevent_functions.py new file mode 100644 index 00000000..98cba374 --- /dev/null +++ b/tests/test_cloudevent_functions.py @@ -0,0 +1,92 @@ +# Copyright 2020 Google LLC +# +# Licensed 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 json +import pathlib +import pytest +from cloudevents.sdk import marshaller +from cloudevents.sdk.event import v1 +from cloudevents.sdk import converters + +from functions_framework import LazyWSGIApp, create_app, exceptions + +TEST_FUNCTIONS_DIR = pathlib.Path(__file__).resolve().parent / "test_functions" + +# Python 3.5: ModuleNotFoundError does not exist +try: + _ModuleNotFoundError = ModuleNotFoundError +except: + _ModuleNotFoundError = ImportError + + +@pytest.fixture +def event_1_10(): + event = ( + v1.Event() + .SetContentType("application/json") + .SetData('{"name":"john"}') + .SetEventID("my-id") + .SetSource("from-galaxy-far-far-away") + .SetEventTime("tomorrow") + .SetEventType("cloudevent.greet.you") + ) + return event + + +@pytest.fixture +def event_0_3(): + event = ( + v1.Event() + .SetContentType("application/json") + .SetData('{"name":"john"}') + .SetEventID("my-id") + .SetSource("from-galaxy-far-far-away") + .SetEventTime("tomorrow") + .SetEventType("cloudevent.greet.you") + ) + return event + + +def test_event_1_0(event_1_10): + source = TEST_FUNCTIONS_DIR / "events" / "main.py" + target = "function" + + client = create_app(target, source, "event").test_client() + + m = marshaller.NewDefaultHTTPMarshaller() + structured_headers, structured_data = m.ToRequest( + event_1_10, converters.TypeStructured, json.dumps + ) + + resp = client.post("/", headers=structured_headers, + data=structured_data.getvalue()) + assert resp.status_code == 200 + assert resp.data == b"OK" + + + +def test_event_0_3(event_0_3): + source = TEST_FUNCTIONS_DIR / "events" / "main.py" + target = "function" + + client = create_app(target, source, "event").test_client() + + m = marshaller.NewDefaultHTTPMarshaller() + structured_headers, structured_data = m.ToRequest( + event_0_3, converters.TypeStructured, json.dumps + ) + + resp = client.post("/", headers=structured_headers, + data=structured_data.getvalue()) + assert resp.status_code == 200 + assert resp.data == b"OK" diff --git a/tests/test_event_functions.py b/tests/test_event_functions.py new file mode 100644 index 00000000..abad1414 --- /dev/null +++ b/tests/test_event_functions.py @@ -0,0 +1,190 @@ +# Copyright 2020 Google LLC +# +# Licensed 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 pathlib +import re +import pytest + +from functions_framework import LazyWSGIApp, create_app, exceptions + +TEST_FUNCTIONS_DIR = pathlib.Path(__file__).resolve().parent / "test_functions" + +# Python 3.5: ModuleNotFoundError does not exist +try: + _ModuleNotFoundError = ModuleNotFoundError +except: + _ModuleNotFoundError = ImportError + + +@pytest.fixture +def background_json(tmpdir): + return { + "context": { + "eventId": "some-eventId", + "timestamp": "some-timestamp", + "eventType": "some-eventType", + "resource": "some-resource", + }, + "data": {"filename": str(tmpdir / "filename.txt"), "value": "some-value"}, + } + + + + +def test_background_function_executes(background_json): + source = TEST_FUNCTIONS_DIR / "background_trigger" / "main.py" + target = "function" + + client = create_app(target, source, "event").test_client() + + resp = client.post("/", json=background_json) + assert resp.status_code == 200 + + +def test_background_function_supports_get(background_json): + source = TEST_FUNCTIONS_DIR / "background_trigger" / "main.py" + target = "function" + + client = create_app(target, source, "event").test_client() + + resp = client.get("/") + assert resp.status_code == 200 + + +def test_background_function_executes_entry_point_one(background_json): + source = TEST_FUNCTIONS_DIR / "background_multiple_entry_points" / "main.py" + target = "myFunctionFoo" + + client = create_app(target, source, "event").test_client() + + resp = client.post("/", json=background_json) + assert resp.status_code == 200 + + +def test_background_function_executes_entry_point_two(background_json): + source = TEST_FUNCTIONS_DIR / "background_multiple_entry_points" / "main.py" + target = "myFunctionBar" + + client = create_app(target, source, "event").test_client() + + resp = client.post("/", json=background_json) + assert resp.status_code == 200 + + +def test_multiple_calls(background_json): + source = TEST_FUNCTIONS_DIR / "background_multiple_entry_points" / "main.py" + target = "myFunctionFoo" + + client = create_app(target, source, "event").test_client() + + resp = client.post("/", json=background_json) + assert resp.status_code == 200 + resp = client.post("/", json=background_json) + assert resp.status_code == 200 + resp = client.post("/", json=background_json) + assert resp.status_code == 200 + + +def test_pubsub_payload(background_json): + source = TEST_FUNCTIONS_DIR / "background_trigger" / "main.py" + target = "function" + + client = create_app(target, source, "event").test_client() + + resp = client.post("/", json=background_json) + + assert resp.status_code == 200 + assert resp.data == b"OK" + + with open(background_json["data"]["filename"]) as f: + assert f.read() == '{{"entryPoint": "function", "value": "{}"}}'.format( + background_json["data"]["value"] + ) + + +def test_background_function_no_data(background_json): + source = TEST_FUNCTIONS_DIR / "background_trigger" / "main.py" + target = "function" + + client = create_app(target, source, "event").test_client() + + resp = client.post("/") + assert resp.status_code == 400 + + + + + +def test_invalid_function_definition_multiple_entry_points(): + source = TEST_FUNCTIONS_DIR / "background_multiple_entry_points" / "main.py" + target = "function" + + with pytest.raises(exceptions.MissingTargetException) as excinfo: + create_app(target, source, "event") + + assert re.match( + "File .* is expected to contain a function named function", str(excinfo.value) + ) + + +def test_invalid_function_definition_multiple_entry_points_invalid_function(): + source = TEST_FUNCTIONS_DIR / "background_multiple_entry_points" / "main.py" + target = "invalidFunction" + + with pytest.raises(exceptions.MissingTargetException) as excinfo: + create_app(target, source, "event") + + assert re.match( + "File .* is expected to contain a function named invalidFunction", + str(excinfo.value), + ) + + +def test_invalid_function_definition_multiple_entry_points_not_a_function(): + source = TEST_FUNCTIONS_DIR / "background_multiple_entry_points" / "main.py" + target = "notAFunction" + + with pytest.raises(exceptions.InvalidTargetTypeException) as excinfo: + create_app(target, source, "event") + + assert re.match( + "The function defined in file .* as notAFunction needs to be of type " + "function. Got: .*", + str(excinfo.value), + ) + + +def test_invalid_function_definition_function_syntax_error(): + source = TEST_FUNCTIONS_DIR / "background_load_error" / "main.py" + target = "function" + + with pytest.raises(SyntaxError) as excinfo: + create_app(target, source, "event") + + assert any( + ( + "invalid syntax" in str(excinfo.value), # Python <3.8 + "unmatched ')'" in str(excinfo.value), # Python >3.8 + ) + ) + + +def test_invalid_function_definition_missing_dependency(): + source = TEST_FUNCTIONS_DIR / "background_missing_dependency" / "main.py" + target = "function" + + with pytest.raises(_ModuleNotFoundError) as excinfo: + create_app(target, source, "event") + + assert "No module named 'nonexistentpackage'" in str(excinfo.value) + diff --git a/tests/test_functions.py b/tests/test_functions.py index c11a7785..41bbdae4 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -11,24 +11,17 @@ # 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 json -import os import pathlib import re import time import pretend import pytest -from cloudevents.sdk import marshaller -from cloudevents.sdk.converters import structured -from cloudevents.sdk.event import v1 -from cloudevents.sdk import converters - import functions_framework from functions_framework import LazyWSGIApp, create_app, exceptions -TEST_FUNCTIONS_DIR = pathlib.Path(os.path.dirname(os.path.abspath(__file__))) / "test_functions" +TEST_FUNCTIONS_DIR = pathlib.Path(__file__).resolve().parent / "test_functions" # Python 3.5: ModuleNotFoundError does not exist try: @@ -49,33 +42,6 @@ def background_json(tmpdir): "data": {"filename": str(tmpdir / "filename.txt"), "value": "some-value"}, } - -def test_event(): - source = TEST_FUNCTIONS_DIR / "events" / "main.py" - target = "function" - - client = create_app(target, source, "event").test_client() - - event = ( - v1.Event() - .SetContentType("application/json") - .SetData('{"name":"john"}') - .SetEventID("my-id") - .SetSource("from-galaxy-far-far-away") - .SetEventTime("tomorrow") - .SetEventType("cloudevent.greet.you") - ) - m = marshaller.NewDefaultHTTPMarshaller() - structured_headers, structured_data = m.ToRequest( - event, converters.TypeStructured, json.dumps - ) - - resp = client.post("/", headers=structured_headers, - data=structured_data.getvalue()) - assert resp.status_code == 200 - assert resp.data == b"success" - - def test_http_function_executes_success(): source = TEST_FUNCTIONS_DIR / "http_trigger" / "main.py" target = "function" @@ -199,85 +165,7 @@ def test_http_function_execution_time(): assert resp.data == b"OK" -def test_background_function_executes(background_json): - source = TEST_FUNCTIONS_DIR / "background_trigger" / "main.py" - target = "function" - - client = create_app(target, source, "event").test_client() - - resp = client.post("/", json=background_json) - assert resp.status_code == 200 - - -def test_background_function_supports_get(background_json): - source = TEST_FUNCTIONS_DIR / "background_trigger" / "main.py" - target = "function" - - client = create_app(target, source, "event").test_client() - - resp = client.get("/") - assert resp.status_code == 200 - - -def test_background_function_executes_entry_point_one(background_json): - source = TEST_FUNCTIONS_DIR / "background_multiple_entry_points" / "main.py" - target = "myFunctionFoo" - - client = create_app(target, source, "event").test_client() - - resp = client.post("/", json=background_json) - assert resp.status_code == 200 - - -def test_background_function_executes_entry_point_two(background_json): - source = TEST_FUNCTIONS_DIR / "background_multiple_entry_points" / "main.py" - target = "myFunctionBar" - - client = create_app(target, source, "event").test_client() - - resp = client.post("/", json=background_json) - assert resp.status_code == 200 - - -def test_multiple_calls(background_json): - source = TEST_FUNCTIONS_DIR / "background_multiple_entry_points" / "main.py" - target = "myFunctionFoo" - - client = create_app(target, source, "event").test_client() - - resp = client.post("/", json=background_json) - assert resp.status_code == 200 - resp = client.post("/", json=background_json) - assert resp.status_code == 200 - resp = client.post("/", json=background_json) - assert resp.status_code == 200 - - -def test_pubsub_payload(background_json): - source = TEST_FUNCTIONS_DIR / "background_trigger" / "main.py" - target = "function" - - client = create_app(target, source, "event").test_client() - - resp = client.post("/", json=background_json) - - assert resp.status_code == 200 - assert resp.data == b"OK" - - with open(background_json["data"]["filename"]) as f: - assert f.read() == '{{"entryPoint": "function", "value": "{}"}}'.format( - background_json["data"]["value"] - ) - -def test_background_function_no_data(background_json): - source = TEST_FUNCTIONS_DIR / "background_trigger" / "main.py" - target = "function" - - client = create_app(target, source, "event").test_client() - - resp = client.post("/") - assert resp.status_code == 400 def test_invalid_function_definition_missing_function_file(): @@ -292,68 +180,6 @@ def test_invalid_function_definition_missing_function_file(): ) -def test_invalid_function_definition_multiple_entry_points(): - source = TEST_FUNCTIONS_DIR / "background_multiple_entry_points" / "main.py" - target = "function" - - with pytest.raises(exceptions.MissingTargetException) as excinfo: - create_app(target, source, "event") - - assert re.match( - "File .* is expected to contain a function named function", str(excinfo.value) - ) - - -def test_invalid_function_definition_multiple_entry_points_invalid_function(): - source = TEST_FUNCTIONS_DIR / "background_multiple_entry_points" / "main.py" - target = "invalidFunction" - - with pytest.raises(exceptions.MissingTargetException) as excinfo: - create_app(target, source, "event") - - assert re.match( - "File .* is expected to contain a function named invalidFunction", - str(excinfo.value), - ) - - -def test_invalid_function_definition_multiple_entry_points_not_a_function(): - source = TEST_FUNCTIONS_DIR / "background_multiple_entry_points" / "main.py" - target = "notAFunction" - - with pytest.raises(exceptions.InvalidTargetTypeException) as excinfo: - create_app(target, source, "event") - - assert re.match( - "The function defined in file .* as notAFunction needs to be of type " - "function. Got: .*", - str(excinfo.value), - ) - - -def test_invalid_function_definition_function_syntax_error(): - source = TEST_FUNCTIONS_DIR / "background_load_error" / "main.py" - target = "function" - - with pytest.raises(SyntaxError) as excinfo: - create_app(target, source, "event") - - assert any( - ( - "invalid syntax" in str(excinfo.value), # Python <3.8 - "unmatched ')'" in str(excinfo.value), # Python >3.8 - ) - ) - - -def test_invalid_function_definition_missing_dependency(): - source = TEST_FUNCTIONS_DIR / "background_missing_dependency" / "main.py" - target = "function" - - with pytest.raises(_ModuleNotFoundError) as excinfo: - create_app(target, source, "event") - - assert "No module named 'nonexistentpackage'" in str(excinfo.value) def test_invalid_configuration(): diff --git a/tests/test_functions/events/main.py b/tests/test_functions/events/main.py index 4ac22e5f..a6196e86 100644 --- a/tests/test_functions/events/main.py +++ b/tests/test_functions/events/main.py @@ -27,18 +27,15 @@ def function(event): on the given mode. Args: - request: The HTTP request which triggered this function. Must contain name - of the requested mode in the 'mode' field in JSON document in request - body. + event: A Cloud event as defined by https://github.com/cloudevents/sdk-python. Returns: Value and status code defined for the given mode. - Raises: - Exception: Thrown when requested in the incoming mode specification. """ + # todo(joelgerard): Should probably check some of this as well. if (event.EventID() == "my-id"): - return "success", 200 + return 200 else: - return "failure", 500 + return 500 From 42ea896c7c0f2ba7fcc632f66c2c3c99ae9231d7 Mon Sep 17 00:00:00 2001 From: joelgerard Date: Mon, 15 Jun 2020 15:43:16 -0700 Subject: [PATCH 06/45] Clean up unused paths, split large test files into two, ensure functions DO NOT return a custom value. General tidy-up. Support binary functions. --- setup.py | 1 + src/functions_framework/__init__.py | 139 +++++++++++++---------- tests/test_cloudevent_functions.py | 27 ++++- tests/test_functions/cloudevents/main.py | 39 +++++++ tests/test_functions/events/main.py | 41 ------- tests/test_view_functions.py | 35 ------ 6 files changed, 138 insertions(+), 144 deletions(-) create mode 100644 tests/test_functions/cloudevents/main.py delete mode 100644 tests/test_functions/events/main.py diff --git a/setup.py b/setup.py index 2a16fe7f..7a2e34ce 100644 --- a/setup.py +++ b/setup.py @@ -52,6 +52,7 @@ "click>=7.0,<8.0", "watchdog>=0.10.0", "gunicorn>=19.2.0,<21.0; platform_system!='Windows'", + "cloudevents<=1.0", ], extras_require={"test": ["pytest", "tox"]}, entry_points={ diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index 5530ec5e..c65912ba 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -import functools +from enum import Enum import importlib.util import io import json @@ -40,6 +40,11 @@ DEFAULT_SOURCE = os.path.realpath("./main.py") DEFAULT_SIGNATURE_TYPE = "http" +class _EventType(Enum): + LEGACY = 1 + CLOUD_EVENT_BINARY = 2 + CLOUD_EVENT_TEXT = 3 + class _Event(object): """Event passed to background functions.""" @@ -65,55 +70,18 @@ def __init__( } self.data = data - -def _get_cloud_event_version(request): - headers = request.headers - # TODO(joelgerard): Should there be further inspection of the payload for versioning considerations? - if headers.get("Content-Type") == "application/cloudevents+json": - return v1.Event() - return None - -def _convert_request_to_event(request, cloud_event_def): - try: - m = marshaller.NewDefaultHTTPMarshaller() - data = io.StringIO(request.get_data(as_text=True)) - return m.FromRequest(cloud_event_def, request.headers, data, json.loads) - except: - raise FunctionsFrameworkException("Content-Type header indicated a Cloud Event, " - "but it could not be parsed.") - - def _http_view_func_wrapper(function, request): def view_func(path): return function(request._get_current_object()) return view_func -def _is_binary_cloud_event(request): - return ( - request.headers.get("ce-type") - and request.headers.get("ce-specversion") - and request.headers.get("ce-source") - and request.headers.get("ce-id") - ) +def _get_cloud_event_version(): + return v1.Event() -# TODO(joelgerard): is this really legacy as I have stated? -def _run_binary_legacy_cloud_event(function, request): - # Support CloudEvents in binary content mode, with data being the - # whole request body and context attributes retrieved from request - # headers. - data = request.get_data() - context = Context( - eventId=request.headers.get("ce-eventId"), - timestamp=request.headers.get("ce-timestamp"), - eventType=request.headers.get("ce-eventType"), - resource=request.headers.get("ce-resource"), - ) - function(data, context) def _run_legacy_event(function, request): - # This is a regular CloudEvent event_data = request.get_json() if not event_data: flask.abort(400) @@ -122,28 +90,80 @@ def _run_legacy_event(function, request): context = Context(**event_object.context) function(data, context) -def _run_cloud_event(function, request, cloud_event_def): - event = _convert_request_to_event(request, cloud_event_def) + +def _run_binary_cloud_event(function, request, cloud_event_def): + data = io.BytesIO(request.get_data()) + http_marshaller = marshaller.NewDefaultHTTPMarshaller() + event = http_marshaller.FromRequest( + cloud_event_def, request.headers, data, json.load) + + function(event) + + +def _run_text_cloud_event(function, request, cloud_event_def): + data = io.StringIO(request.get_data(as_text=True)) + m = marshaller.NewDefaultHTTPMarshaller() + event = m.FromRequest(cloud_event_def, request.headers, data, json.loads) 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"): + return _EventType.CLOUD_EVENT_BINARY + elif request.headers.get("Content-Type") == "application/cloudevents+json": + return _EventType.CLOUD_EVENT_TEXT + else: + return _EventType.LEGACY + + def _event_view_func_wrapper(function, request): def view_func(path): - if _is_binary_cloud_event(request): - _run_binary_legacy_cloud_event(function, request) + if _get_event_type(request) == _EventType.LEGACY: + _run_legacy_event(function, request) else: - cloud_event_def = _get_cloud_event_version(request) - if cloud_event_def is None: - _run_legacy_event(function, request) - else: - # We have a bonafide event from the Cloud Event SDK. Let's use it. - # TODO(joelgerard): Note that I return the function value. This seems better - # going forward rather than returning "OK". ?? - _run_cloud_event(function, request, cloud_event_def) + # here for defensive backwards compatibility in case we make a mistake in rollout. + raise InvalidConfigurationException("The FUNCTION_SIGNATURE_TYPE for this function is set to event " + "but no legacy event was given. If you are using CloudEvents set " + "FUNCTION_SIGNATURE_TYPE=cloudevent") + + + return "OK" + + return view_func + + +def _cloudevent_view_func_wrapper(function, request): + def view_func(path): + cloud_event_def = _get_cloud_event_version() + event_type = _get_event_type(request) + if event_type == _EventType.CLOUD_EVENT_TEXT: + _run_text_cloud_event(function, request, cloud_event_def) + elif event_type == _EventType.CLOUD_EVENT_BINARY: + _run_binary_cloud_event(function, request, cloud_event_def) + else: + raise InvalidConfigurationException("Function was defined with FUNCTION_SIGNATURE_TYPE=cloudevent " + " but it did not receive a cloudevent as a request.") return "OK" return view_func +def _setup_event_routes(app): + app.url_map.add( + werkzeug.routing.Rule( + "/", defaults={"path": ""}, endpoint="run", methods=["POST"] + ) + ) + app.url_map.add( + werkzeug.routing.Rule("/", endpoint="run", methods=["POST"]) + ) + + # Add a dummy endpoint for GET / + app.url_map.add(werkzeug.routing.Rule("/", endpoint="get", methods=["GET"])) + app.view_functions["get"] = lambda: "" def create_app(target=None, source=None, signature_type=None): # Get the configured function target @@ -220,18 +240,11 @@ def create_app(target=None, source=None, signature_type=None): app.view_functions["run"] = _http_view_func_wrapper(function, flask.request) app.view_functions["error"] = lambda: flask.abort(404, description="Not Found") elif signature_type == "event": - app.url_map.add( - werkzeug.routing.Rule( - "/", defaults={"path": ""}, endpoint="run", methods=["POST"] - ) - ) - app.url_map.add( - werkzeug.routing.Rule("/", endpoint="run", methods=["POST"]) - ) + _setup_event_routes(app) app.view_functions["run"] = _event_view_func_wrapper(function, flask.request) - # Add a dummy endpoint for GET / - app.url_map.add(werkzeug.routing.Rule("/", endpoint="get", methods=["GET"])) - app.view_functions["get"] = lambda: "" + elif signature_type == "cloudevent": + _setup_event_routes(app) + app.view_functions["run"] = _cloudevent_view_func_wrapper(function, flask.request) else: raise FunctionsFrameworkException( "Invalid signature type: {signature_type}".format( diff --git a/tests/test_cloudevent_functions.py b/tests/test_cloudevent_functions.py index 98cba374..c635f2af 100644 --- a/tests/test_cloudevent_functions.py +++ b/tests/test_cloudevent_functions.py @@ -16,6 +16,7 @@ import pytest from cloudevents.sdk import marshaller from cloudevents.sdk.event import v1 +from cloudevents.sdk.event import v03 from cloudevents.sdk import converters from functions_framework import LazyWSGIApp, create_app, exceptions @@ -46,7 +47,7 @@ def event_1_10(): @pytest.fixture def event_0_3(): event = ( - v1.Event() + v03.Event() .SetContentType("application/json") .SetData('{"name":"john"}') .SetEventID("my-id") @@ -58,10 +59,10 @@ def event_0_3(): def test_event_1_0(event_1_10): - source = TEST_FUNCTIONS_DIR / "events" / "main.py" + source = TEST_FUNCTIONS_DIR / "cloudevents" / "main.py" target = "function" - client = create_app(target, source, "event").test_client() + client = create_app(target, source, "cloudevent").test_client() m = marshaller.NewDefaultHTTPMarshaller() structured_headers, structured_data = m.ToRequest( @@ -73,13 +74,29 @@ def test_event_1_0(event_1_10): assert resp.status_code == 200 assert resp.data == b"OK" +def test_binary_event_1_0(event_1_10): + source = TEST_FUNCTIONS_DIR / "cloudevents" / "main.py" + target = "function" + + client = create_app(target, source, "cloudevent").test_client() + + m = marshaller.NewDefaultHTTPMarshaller() + + binary_headers, binary_data = m.ToRequest( + event_1_10, converters.TypeBinary, json.dumps) + + resp = client.post( + "/", headers=binary_headers, data=binary_data) + + assert resp.status_code == 200 + assert resp.data == b"OK" def test_event_0_3(event_0_3): - source = TEST_FUNCTIONS_DIR / "events" / "main.py" + source = TEST_FUNCTIONS_DIR / "cloudevents" / "main.py" target = "function" - client = create_app(target, source, "event").test_client() + client = create_app(target, source, "cloudevent").test_client() m = marshaller.NewDefaultHTTPMarshaller() structured_headers, structured_data = m.ToRequest( diff --git a/tests/test_functions/cloudevents/main.py b/tests/test_functions/cloudevents/main.py new file mode 100644 index 00000000..c3790cd7 --- /dev/null +++ b/tests/test_functions/cloudevents/main.py @@ -0,0 +1,39 @@ +# Copyright 2020 Google LLC +# +# Licensed 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. + +"""Function used to test handling Cloud Event functions.""" + + +def function(cloud_event): + """Test Event function that checks to see if a valid CloudEvent was sent. + + The function returns 200 if it received the expected event, otherwise 500. + + Args: + cloud_event: A Cloud event as defined by https://github.com/cloudevents/sdk-python. + + Returns: + HTTP status code indicating whether valid event was sent or not. + + """ + valid_event = cloud_event.EventID() == "my-id" and \ + cloud_event.Data() == '{"name":"john"}' and \ + cloud_event.Source() == "from-galaxy-far-far-away" and \ + cloud_event.EventTime() == "tomorrow" and \ + cloud_event.EventType() == "cloudevent.greet.you" + + if valid_event: + return 200 + else: + return 500 diff --git a/tests/test_functions/events/main.py b/tests/test_functions/events/main.py deleted file mode 100644 index a6196e86..00000000 --- a/tests/test_functions/events/main.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed 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. - -"""Function used in Worker tests of handling HTTP functions.""" -import io - -import flask -import json -from cloudevents.sdk import marshaller -from cloudevents.sdk.event import v1 - -def function(event): - """Test HTTP function whose behavior depends on the given mode. - - The function returns a success, a failure, or throws an exception, depending - on the given mode. - - Args: - event: A Cloud event as defined by https://github.com/cloudevents/sdk-python. - - Returns: - Value and status code defined for the given mode. - - """ - - # todo(joelgerard): Should probably check some of this as well. - if (event.EventID() == "my-id"): - return 200 - else: - return 500 diff --git a/tests/test_view_functions.py b/tests/test_view_functions.py index 51dad087..a9e13bb7 100644 --- a/tests/test_view_functions.py +++ b/tests/test_view_functions.py @@ -60,41 +60,6 @@ def test_event_view_func_wrapper(monkeypatch): ] -def test_binary_event_view_func_wrapper(monkeypatch): - data = pretend.stub() - request = pretend.stub( - headers={ - "ce-type": "something", - "ce-specversion": "something", - "ce-source": "something", - "ce-id": "something", - "ce-eventId": "some-eventId", - "ce-timestamp": "some-timestamp", - "ce-eventType": "some-eventType", - "ce-resource": "some-resource", - }, - get_data=lambda: data, - ) - - context_stub = pretend.stub() - context_class = pretend.call_recorder(lambda *a, **kw: context_stub) - monkeypatch.setattr(functions_framework, "Context", context_class) - function = pretend.call_recorder(lambda data, context: "Hello") - - view_func = functions_framework._event_view_func_wrapper(function, request) - view_func("/some/path") - - assert function.calls == [pretend.call(data, context_stub)] - assert context_class.calls == [ - pretend.call( - eventId="some-eventId", - timestamp="some-timestamp", - eventType="some-eventType", - resource="some-resource", - ) - ] - - def test_legacy_event_view_func_wrapper(monkeypatch): data = pretend.stub() json = { From a1dddbb60dfd0606a5643f77e1721dcfab16d70b Mon Sep 17 00:00:00 2001 From: joelgerard Date: Mon, 15 Jun 2020 16:56:47 -0700 Subject: [PATCH 07/45] Fix lint errors with black. --- src/functions_framework/__init__.py | 36 +++++++++++++++++++---------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index c65912ba..dc7e0589 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -40,6 +40,7 @@ DEFAULT_SOURCE = os.path.realpath("./main.py") DEFAULT_SIGNATURE_TYPE = "http" + class _EventType(Enum): LEGACY = 1 CLOUD_EVENT_BINARY = 2 @@ -70,6 +71,7 @@ def __init__( } self.data = data + def _http_view_func_wrapper(function, request): def view_func(path): return function(request._get_current_object()) @@ -95,7 +97,8 @@ def _run_binary_cloud_event(function, request, cloud_event_def): data = io.BytesIO(request.get_data()) http_marshaller = marshaller.NewDefaultHTTPMarshaller() event = http_marshaller.FromRequest( - cloud_event_def, request.headers, data, json.load) + cloud_event_def, request.headers, data, json.load + ) function(event) @@ -108,10 +111,12 @@ def _run_text_cloud_event(function, request, cloud_event_def): 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 ( + request.headers.get("ce-type") + and request.headers.get("ce-specversion") + and request.headers.get("ce-source") + and request.headers.get("ce-id") + ): return _EventType.CLOUD_EVENT_BINARY elif request.headers.get("Content-Type") == "application/cloudevents+json": return _EventType.CLOUD_EVENT_TEXT @@ -125,10 +130,11 @@ def view_func(path): _run_legacy_event(function, request) else: # here for defensive backwards compatibility in case we make a mistake in rollout. - raise InvalidConfigurationException("The FUNCTION_SIGNATURE_TYPE for this function is set to event " - "but no legacy event was given. If you are using CloudEvents set " - "FUNCTION_SIGNATURE_TYPE=cloudevent") - + raise InvalidConfigurationException( + "The FUNCTION_SIGNATURE_TYPE for this function is set to event " + "but no legacy event was given. If you are using CloudEvents set " + "FUNCTION_SIGNATURE_TYPE=cloudevent" + ) return "OK" @@ -144,13 +150,16 @@ def view_func(path): elif event_type == _EventType.CLOUD_EVENT_BINARY: _run_binary_cloud_event(function, request, cloud_event_def) else: - raise InvalidConfigurationException("Function was defined with FUNCTION_SIGNATURE_TYPE=cloudevent " - " but it did not receive a cloudevent as a request.") + raise InvalidConfigurationException( + "Function was defined with FUNCTION_SIGNATURE_TYPE=cloudevent " + " but it did not receive a cloudevent as a request." + ) return "OK" return view_func + def _setup_event_routes(app): app.url_map.add( werkzeug.routing.Rule( @@ -165,6 +174,7 @@ def _setup_event_routes(app): app.url_map.add(werkzeug.routing.Rule("/", endpoint="get", methods=["GET"])) app.view_functions["get"] = lambda: "" + def create_app(target=None, source=None, signature_type=None): # Get the configured function target target = target or os.environ.get("FUNCTION_TARGET", "") @@ -244,7 +254,9 @@ def create_app(target=None, source=None, signature_type=None): app.view_functions["run"] = _event_view_func_wrapper(function, flask.request) elif signature_type == "cloudevent": _setup_event_routes(app) - app.view_functions["run"] = _cloudevent_view_func_wrapper(function, flask.request) + app.view_functions["run"] = _cloudevent_view_func_wrapper( + function, flask.request + ) else: raise FunctionsFrameworkException( "Invalid signature type: {signature_type}".format( From 4407259d01b40dce7c4bf6be6d2a08d275260b08 Mon Sep 17 00:00:00 2001 From: joelgerard Date: Mon, 15 Jun 2020 16:56:47 -0700 Subject: [PATCH 08/45] Fix lint errors with black. --- src/functions_framework/__init__.py | 36 +++++++++++++++-------- tests/test_cloudevent_functions.py | 37 ++++++++++++------------ tests/test_event_functions.py | 6 ---- tests/test_functions.py | 6 +--- tests/test_functions/cloudevents/main.py | 12 ++++---- 5 files changed, 50 insertions(+), 47 deletions(-) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index c65912ba..dc7e0589 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -40,6 +40,7 @@ DEFAULT_SOURCE = os.path.realpath("./main.py") DEFAULT_SIGNATURE_TYPE = "http" + class _EventType(Enum): LEGACY = 1 CLOUD_EVENT_BINARY = 2 @@ -70,6 +71,7 @@ def __init__( } self.data = data + def _http_view_func_wrapper(function, request): def view_func(path): return function(request._get_current_object()) @@ -95,7 +97,8 @@ def _run_binary_cloud_event(function, request, cloud_event_def): data = io.BytesIO(request.get_data()) http_marshaller = marshaller.NewDefaultHTTPMarshaller() event = http_marshaller.FromRequest( - cloud_event_def, request.headers, data, json.load) + cloud_event_def, request.headers, data, json.load + ) function(event) @@ -108,10 +111,12 @@ def _run_text_cloud_event(function, request, cloud_event_def): 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 ( + request.headers.get("ce-type") + and request.headers.get("ce-specversion") + and request.headers.get("ce-source") + and request.headers.get("ce-id") + ): return _EventType.CLOUD_EVENT_BINARY elif request.headers.get("Content-Type") == "application/cloudevents+json": return _EventType.CLOUD_EVENT_TEXT @@ -125,10 +130,11 @@ def view_func(path): _run_legacy_event(function, request) else: # here for defensive backwards compatibility in case we make a mistake in rollout. - raise InvalidConfigurationException("The FUNCTION_SIGNATURE_TYPE for this function is set to event " - "but no legacy event was given. If you are using CloudEvents set " - "FUNCTION_SIGNATURE_TYPE=cloudevent") - + raise InvalidConfigurationException( + "The FUNCTION_SIGNATURE_TYPE for this function is set to event " + "but no legacy event was given. If you are using CloudEvents set " + "FUNCTION_SIGNATURE_TYPE=cloudevent" + ) return "OK" @@ -144,13 +150,16 @@ def view_func(path): elif event_type == _EventType.CLOUD_EVENT_BINARY: _run_binary_cloud_event(function, request, cloud_event_def) else: - raise InvalidConfigurationException("Function was defined with FUNCTION_SIGNATURE_TYPE=cloudevent " - " but it did not receive a cloudevent as a request.") + raise InvalidConfigurationException( + "Function was defined with FUNCTION_SIGNATURE_TYPE=cloudevent " + " but it did not receive a cloudevent as a request." + ) return "OK" return view_func + def _setup_event_routes(app): app.url_map.add( werkzeug.routing.Rule( @@ -165,6 +174,7 @@ def _setup_event_routes(app): app.url_map.add(werkzeug.routing.Rule("/", endpoint="get", methods=["GET"])) app.view_functions["get"] = lambda: "" + def create_app(target=None, source=None, signature_type=None): # Get the configured function target target = target or os.environ.get("FUNCTION_TARGET", "") @@ -244,7 +254,9 @@ def create_app(target=None, source=None, signature_type=None): app.view_functions["run"] = _event_view_func_wrapper(function, flask.request) elif signature_type == "cloudevent": _setup_event_routes(app) - app.view_functions["run"] = _cloudevent_view_func_wrapper(function, flask.request) + app.view_functions["run"] = _cloudevent_view_func_wrapper( + function, flask.request + ) else: raise FunctionsFrameworkException( "Invalid signature type: {signature_type}".format( diff --git a/tests/test_cloudevent_functions.py b/tests/test_cloudevent_functions.py index c635f2af..acf6847a 100644 --- a/tests/test_cloudevent_functions.py +++ b/tests/test_cloudevent_functions.py @@ -34,12 +34,12 @@ def event_1_10(): event = ( v1.Event() - .SetContentType("application/json") - .SetData('{"name":"john"}') - .SetEventID("my-id") - .SetSource("from-galaxy-far-far-away") - .SetEventTime("tomorrow") - .SetEventType("cloudevent.greet.you") + .SetContentType("application/json") + .SetData('{"name":"john"}') + .SetEventID("my-id") + .SetSource("from-galaxy-far-far-away") + .SetEventTime("tomorrow") + .SetEventType("cloudevent.greet.you") ) return event @@ -48,12 +48,12 @@ def event_1_10(): def event_0_3(): event = ( v03.Event() - .SetContentType("application/json") - .SetData('{"name":"john"}') - .SetEventID("my-id") - .SetSource("from-galaxy-far-far-away") - .SetEventTime("tomorrow") - .SetEventType("cloudevent.greet.you") + .SetContentType("application/json") + .SetData('{"name":"john"}') + .SetEventID("my-id") + .SetSource("from-galaxy-far-far-away") + .SetEventTime("tomorrow") + .SetEventType("cloudevent.greet.you") ) return event @@ -69,11 +69,11 @@ def test_event_1_0(event_1_10): event_1_10, converters.TypeStructured, json.dumps ) - resp = client.post("/", headers=structured_headers, - data=structured_data.getvalue()) + resp = client.post("/", headers=structured_headers, data=structured_data.getvalue()) assert resp.status_code == 200 assert resp.data == b"OK" + def test_binary_event_1_0(event_1_10): source = TEST_FUNCTIONS_DIR / "cloudevents" / "main.py" target = "function" @@ -83,10 +83,10 @@ def test_binary_event_1_0(event_1_10): m = marshaller.NewDefaultHTTPMarshaller() binary_headers, binary_data = m.ToRequest( - event_1_10, converters.TypeBinary, json.dumps) + event_1_10, converters.TypeBinary, json.dumps + ) - resp = client.post( - "/", headers=binary_headers, data=binary_data) + resp = client.post("/", headers=binary_headers, data=binary_data) assert resp.status_code == 200 assert resp.data == b"OK" @@ -103,7 +103,6 @@ def test_event_0_3(event_0_3): event_0_3, converters.TypeStructured, json.dumps ) - resp = client.post("/", headers=structured_headers, - data=structured_data.getvalue()) + resp = client.post("/", headers=structured_headers, data=structured_data.getvalue()) assert resp.status_code == 200 assert resp.data == b"OK" diff --git a/tests/test_event_functions.py b/tests/test_event_functions.py index abad1414..72dab200 100644 --- a/tests/test_event_functions.py +++ b/tests/test_event_functions.py @@ -39,8 +39,6 @@ def background_json(tmpdir): } - - def test_background_function_executes(background_json): source = TEST_FUNCTIONS_DIR / "background_trigger" / "main.py" target = "function" @@ -122,9 +120,6 @@ def test_background_function_no_data(background_json): assert resp.status_code == 400 - - - def test_invalid_function_definition_multiple_entry_points(): source = TEST_FUNCTIONS_DIR / "background_multiple_entry_points" / "main.py" target = "function" @@ -187,4 +182,3 @@ def test_invalid_function_definition_missing_dependency(): create_app(target, source, "event") assert "No module named 'nonexistentpackage'" in str(excinfo.value) - diff --git a/tests/test_functions.py b/tests/test_functions.py index 41bbdae4..4eebb6ed 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -42,6 +42,7 @@ def background_json(tmpdir): "data": {"filename": str(tmpdir / "filename.txt"), "value": "some-value"}, } + def test_http_function_executes_success(): source = TEST_FUNCTIONS_DIR / "http_trigger" / "main.py" target = "function" @@ -165,9 +166,6 @@ def test_http_function_execution_time(): assert resp.data == b"OK" - - - def test_invalid_function_definition_missing_function_file(): source = TEST_FUNCTIONS_DIR / "missing_function_file" / "main.py" target = "functions" @@ -180,8 +178,6 @@ def test_invalid_function_definition_missing_function_file(): ) - - def test_invalid_configuration(): with pytest.raises(exceptions.InvalidConfigurationException) as excinfo: create_app(None, None, None) diff --git a/tests/test_functions/cloudevents/main.py b/tests/test_functions/cloudevents/main.py index c3790cd7..0581282a 100644 --- a/tests/test_functions/cloudevents/main.py +++ b/tests/test_functions/cloudevents/main.py @@ -27,11 +27,13 @@ def function(cloud_event): HTTP status code indicating whether valid event was sent or not. """ - valid_event = cloud_event.EventID() == "my-id" and \ - cloud_event.Data() == '{"name":"john"}' and \ - cloud_event.Source() == "from-galaxy-far-far-away" and \ - cloud_event.EventTime() == "tomorrow" and \ - cloud_event.EventType() == "cloudevent.greet.you" + valid_event = ( + cloud_event.EventID() == "my-id" + and cloud_event.Data() == '{"name":"john"}' + and cloud_event.Source() == "from-galaxy-far-far-away" + and cloud_event.EventTime() == "tomorrow" + and cloud_event.EventType() == "cloudevent.greet.you" + ) if valid_event: return 200 From d40f4a09907a679d735148eb36cee7ad8717f42f Mon Sep 17 00:00:00 2001 From: joelgerard Date: Tue, 16 Jun 2020 09:50:30 -0700 Subject: [PATCH 09/45] Update setup.py Co-authored-by: Dustin Ingram --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7a2e34ce..2509ca22 100644 --- a/setup.py +++ b/setup.py @@ -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", ], extras_require={"test": ["pytest", "tox"]}, entry_points={ From 1d83d3c9c40a60a48c00b1d8af05d964254d2b63 Mon Sep 17 00:00:00 2001 From: joelgerard Date: Tue, 16 Jun 2020 09:50:41 -0700 Subject: [PATCH 10/45] Update tests/test_cloudevent_functions.py Co-authored-by: Dustin Ingram --- tests/test_cloudevent_functions.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_cloudevent_functions.py b/tests/test_cloudevent_functions.py index acf6847a..0f2e5990 100644 --- a/tests/test_cloudevent_functions.py +++ b/tests/test_cloudevent_functions.py @@ -14,10 +14,8 @@ import json import pathlib import pytest -from cloudevents.sdk import marshaller -from cloudevents.sdk.event import v1 -from cloudevents.sdk.event import v03 -from cloudevents.sdk import converters +import cloudevents.sdk +import cloudevents.sdk.event from functions_framework import LazyWSGIApp, create_app, exceptions From 7c46f60d4527461ee65b8912bc18d470f247a15f Mon Sep 17 00:00:00 2001 From: joelgerard Date: Tue, 16 Jun 2020 09:50:58 -0700 Subject: [PATCH 11/45] Update tests/test_cloudevent_functions.py Co-authored-by: Dustin Ingram --- tests/test_cloudevent_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cloudevent_functions.py b/tests/test_cloudevent_functions.py index 0f2e5990..397c8c8b 100644 --- a/tests/test_cloudevent_functions.py +++ b/tests/test_cloudevent_functions.py @@ -78,7 +78,7 @@ def test_binary_event_1_0(event_1_10): client = create_app(target, source, "cloudevent").test_client() - m = marshaller.NewDefaultHTTPMarshaller() + m = cloudevents.sdk.marshaller.NewDefaultHTTPMarshaller() binary_headers, binary_data = m.ToRequest( event_1_10, converters.TypeBinary, json.dumps From 8832081f2a7b317f8d39c141fa1492e8eaf9a9ca Mon Sep 17 00:00:00 2001 From: joelgerard Date: Tue, 16 Jun 2020 09:51:09 -0700 Subject: [PATCH 12/45] Update src/functions_framework/__init__.py Co-authored-by: Dustin Ingram --- src/functions_framework/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index dc7e0589..3aa1ae5d 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -105,7 +105,7 @@ def _run_binary_cloud_event(function, request, cloud_event_def): def _run_text_cloud_event(function, request, cloud_event_def): data = io.StringIO(request.get_data(as_text=True)) - m = marshaller.NewDefaultHTTPMarshaller() + m = cloudevent.sdk.marshaller.NewDefaultHTTPMarshaller() event = m.FromRequest(cloud_event_def, request.headers, data, json.loads) function(event) From 7a163316c8498880379b9576627aa2f91288886e Mon Sep 17 00:00:00 2001 From: joelgerard Date: Tue, 16 Jun 2020 09:51:36 -0700 Subject: [PATCH 13/45] Update tests/test_functions/cloudevents/main.py Co-authored-by: Dustin Ingram --- tests/test_functions/cloudevents/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_functions/cloudevents/main.py b/tests/test_functions/cloudevents/main.py index 0581282a..928e13e1 100644 --- a/tests/test_functions/cloudevents/main.py +++ b/tests/test_functions/cloudevents/main.py @@ -15,7 +15,7 @@ """Function used to test handling Cloud Event functions.""" -def function(cloud_event): +def function(cloudevent): """Test Event function that checks to see if a valid CloudEvent was sent. The function returns 200 if it received the expected event, otherwise 500. From 16a6858a509f8327546053cde781c89942bf807b Mon Sep 17 00:00:00 2001 From: joelgerard Date: Tue, 16 Jun 2020 10:23:13 -0700 Subject: [PATCH 14/45] Clearer imports. --- src/functions_framework/__init__.py | 10 +++++----- tests/test_cloudevent_functions.py | 18 ++++++++++-------- tests/test_functions/cloudevents/main.py | 2 +- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index 3aa1ae5d..370f1a85 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -34,8 +34,8 @@ from google.cloud.functions.context import Context -from cloudevents.sdk.event import v1 -from cloudevents.sdk import marshaller +import cloudevents.sdk.event +import cloudevents.sdk DEFAULT_SOURCE = os.path.realpath("./main.py") DEFAULT_SIGNATURE_TYPE = "http" @@ -80,7 +80,7 @@ def view_func(path): def _get_cloud_event_version(): - return v1.Event() + return cloudevents.sdk .event.v1.Event() def _run_legacy_event(function, request): @@ -95,7 +95,7 @@ def _run_legacy_event(function, request): def _run_binary_cloud_event(function, request, cloud_event_def): data = io.BytesIO(request.get_data()) - http_marshaller = marshaller.NewDefaultHTTPMarshaller() + http_marshaller = cloudevents.sdk .marshaller.NewDefaultHTTPMarshaller() event = http_marshaller.FromRequest( cloud_event_def, request.headers, data, json.load ) @@ -105,7 +105,7 @@ def _run_binary_cloud_event(function, request, cloud_event_def): def _run_text_cloud_event(function, request, cloud_event_def): data = io.StringIO(request.get_data(as_text=True)) - m = cloudevent.sdk.marshaller.NewDefaultHTTPMarshaller() + m = cloudevents.sdk.marshaller.NewDefaultHTTPMarshaller() event = m.FromRequest(cloud_event_def, request.headers, data, json.loads) function(event) diff --git a/tests/test_cloudevent_functions.py b/tests/test_cloudevent_functions.py index 397c8c8b..c94a174f 100644 --- a/tests/test_cloudevent_functions.py +++ b/tests/test_cloudevent_functions.py @@ -15,7 +15,9 @@ import pathlib import pytest import cloudevents.sdk -import cloudevents.sdk.event +import cloudevents.sdk.event.v1 +import cloudevents.sdk.event.v03 +import cloudevents.sdk.marshaller from functions_framework import LazyWSGIApp, create_app, exceptions @@ -31,7 +33,7 @@ @pytest.fixture def event_1_10(): event = ( - v1.Event() + cloudevents.sdk.event.v1.Event() .SetContentType("application/json") .SetData('{"name":"john"}') .SetEventID("my-id") @@ -45,7 +47,7 @@ def event_1_10(): @pytest.fixture def event_0_3(): event = ( - v03.Event() + cloudevents.sdk.event.v03.Event() .SetContentType("application/json") .SetData('{"name":"john"}') .SetEventID("my-id") @@ -62,9 +64,9 @@ def test_event_1_0(event_1_10): client = create_app(target, source, "cloudevent").test_client() - m = marshaller.NewDefaultHTTPMarshaller() + m = cloudevents.sdk.marshaller.NewDefaultHTTPMarshaller() structured_headers, structured_data = m.ToRequest( - event_1_10, converters.TypeStructured, json.dumps + event_1_10, cloudevents.sdk.converters.TypeStructured, json.dumps ) resp = client.post("/", headers=structured_headers, data=structured_data.getvalue()) @@ -81,7 +83,7 @@ def test_binary_event_1_0(event_1_10): m = cloudevents.sdk.marshaller.NewDefaultHTTPMarshaller() binary_headers, binary_data = m.ToRequest( - event_1_10, converters.TypeBinary, json.dumps + event_1_10, cloudevents.sdk.converters.TypeBinary, json.dumps ) resp = client.post("/", headers=binary_headers, data=binary_data) @@ -96,9 +98,9 @@ def test_event_0_3(event_0_3): client = create_app(target, source, "cloudevent").test_client() - m = marshaller.NewDefaultHTTPMarshaller() + m = cloudevents.sdk.marshaller.NewDefaultHTTPMarshaller() structured_headers, structured_data = m.ToRequest( - event_0_3, converters.TypeStructured, json.dumps + event_0_3, cloudevents.sdk.converters.TypeStructured, json.dumps ) resp = client.post("/", headers=structured_headers, data=structured_data.getvalue()) diff --git a/tests/test_functions/cloudevents/main.py b/tests/test_functions/cloudevents/main.py index 928e13e1..0581282a 100644 --- a/tests/test_functions/cloudevents/main.py +++ b/tests/test_functions/cloudevents/main.py @@ -15,7 +15,7 @@ """Function used to test handling Cloud Event functions.""" -def function(cloudevent): +def function(cloud_event): """Test Event function that checks to see if a valid CloudEvent was sent. The function returns 200 if it received the expected event, otherwise 500. From bdbf5e25b8b2f052b334f385c370d10974a83405 Mon Sep 17 00:00:00 2001 From: joelgerard Date: Tue, 16 Jun 2020 10:24:19 -0700 Subject: [PATCH 15/45] don't factor out routes. --- src/functions_framework/__init__.py | 44 ++++++++++++++--------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index 370f1a85..a56edbab 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -80,7 +80,7 @@ def view_func(path): def _get_cloud_event_version(): - return cloudevents.sdk .event.v1.Event() + return cloudevents.sdk.event.v1.Event() def _run_legacy_event(function, request): @@ -95,7 +95,7 @@ def _run_legacy_event(function, request): def _run_binary_cloud_event(function, request, cloud_event_def): data = io.BytesIO(request.get_data()) - http_marshaller = cloudevents.sdk .marshaller.NewDefaultHTTPMarshaller() + http_marshaller = cloudevents.sdk.marshaller.NewDefaultHTTPMarshaller() event = http_marshaller.FromRequest( cloud_event_def, request.headers, data, json.load ) @@ -160,21 +160,6 @@ def view_func(path): return view_func -def _setup_event_routes(app): - app.url_map.add( - werkzeug.routing.Rule( - "/", defaults={"path": ""}, endpoint="run", methods=["POST"] - ) - ) - app.url_map.add( - werkzeug.routing.Rule("/", endpoint="run", methods=["POST"]) - ) - - # Add a dummy endpoint for GET / - app.url_map.add(werkzeug.routing.Rule("/", endpoint="get", methods=["GET"])) - app.view_functions["get"] = lambda: "" - - def create_app(target=None, source=None, signature_type=None): # Get the configured function target target = target or os.environ.get("FUNCTION_TARGET", "") @@ -249,12 +234,25 @@ def create_app(target=None, source=None, signature_type=None): app.url_map.add(werkzeug.routing.Rule("/", endpoint="run")) app.view_functions["run"] = _http_view_func_wrapper(function, flask.request) app.view_functions["error"] = lambda: flask.abort(404, description="Not Found") - elif signature_type == "event": - _setup_event_routes(app) - app.view_functions["run"] = _event_view_func_wrapper(function, flask.request) - elif signature_type == "cloudevent": - _setup_event_routes(app) - app.view_functions["run"] = _cloudevent_view_func_wrapper( + elif signature_type == "event" or signature_type == "cloudevent": + app.url_map.add( + werkzeug.routing.Rule( + "/", defaults={"path": ""}, endpoint=signature_type, methods=["POST"] + ) + ) + app.url_map.add( + werkzeug.routing.Rule( + "/", endpoint=signature_type, methods=["POST"] + ) + ) + + # Add a dummy endpoint for 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["cloudevent"] = _cloudevent_view_func_wrapper( function, flask.request ) else: From ea2e28cc0bc296a0ba2f91e2e9e2b336bb16693c Mon Sep 17 00:00:00 2001 From: joelgerard Date: Tue, 16 Jun 2020 11:21:55 -0700 Subject: [PATCH 16/45] Add a TODO for testing the different combinations of events and signature types. --- tests/test_view_functions.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/test_view_functions.py b/tests/test_view_functions.py index a9e13bb7..a8aa7df5 100644 --- a/tests/test_view_functions.py +++ b/tests/test_view_functions.py @@ -88,3 +88,39 @@ def test_legacy_event_view_func_wrapper(monkeypatch): resource="some-resource", ) ] + +# TODO(#57): This test needs to be updated or moved to integration tests once the +# event adapter is working. +# def test_binary_event_view_func_wrapper(monkeypatch): +# data = pretend.stub() +# request = pretend.stub( +# headers={ +# "ce-type": "something", +# "ce-specversion": "something", +# "ce-source": "something", +# "ce-id": "something", +# "ce-eventId": "some-eventId", +# "ce-timestamp": "some-timestamp", +# "ce-eventType": "some-eventType", +# "ce-resource": "some-resource", +# }, +# get_data=lambda: data, +# ) +# +# context_stub = pretend.stub() +# context_class = pretend.call_recorder(lambda *a, **kw: context_stub) +# monkeypatch.setattr(functions_framework, "Context", context_class) +# function = pretend.call_recorder(lambda data, context: "Hello") +# +# view_func = functions_framework._event_view_func_wrapper(function, request) +# view_func("/some/path") +# +# assert function.calls == [pretend.call(data, context_stub)] +# assert context_class.calls == [ +# pretend.call( +# eventId="some-eventId", +# timestamp="some-timestamp", +# eventType="some-eventType", +# resource="some-resource", +# ) +# ] \ No newline at end of file From 9e8d874e84d52c80ab4db06d0179baa2e9e51254 Mon Sep 17 00:00:00 2001 From: joelgerard Date: Tue, 16 Jun 2020 12:57:46 -0700 Subject: [PATCH 17/45] Add cloudevent as a signature type in the argument list. --- src/functions_framework/_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/functions_framework/_cli.py b/src/functions_framework/_cli.py index 4fe6e427..b619aa92 100644 --- a/src/functions_framework/_cli.py +++ b/src/functions_framework/_cli.py @@ -26,7 +26,7 @@ @click.option( "--signature-type", envvar="FUNCTION_SIGNATURE_TYPE", - type=click.Choice(["http", "event"]), + type=click.Choice(["http", "event","cloudevent"]), default="http", ) @click.option("--host", envvar="HOST", type=click.STRING, default="0.0.0.0") From 06ffc2d0941dcb2cf85b6052087a7ff16b5738ef Mon Sep 17 00:00:00 2001 From: joelgerard Date: Tue, 16 Jun 2020 14:31:19 -0700 Subject: [PATCH 18/45] Clarify import. --- src/functions_framework/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index a56edbab..97dcaa5d 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -33,9 +33,9 @@ ) from google.cloud.functions.context import Context - -import cloudevents.sdk.event import cloudevents.sdk +import cloudevents.sdk.event +import cloudevents.sdk.event.v1 DEFAULT_SOURCE = os.path.realpath("./main.py") DEFAULT_SIGNATURE_TYPE = "http" From ecb0b6101c29228b11e263ef17db6e964a7e6c65 Mon Sep 17 00:00:00 2001 From: joelgerard Date: Tue, 16 Jun 2020 14:31:19 -0700 Subject: [PATCH 19/45] Clarify import. --- src/functions_framework/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index a56edbab..5db53e55 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -33,9 +33,10 @@ ) from google.cloud.functions.context import Context - -import cloudevents.sdk.event import cloudevents.sdk +import cloudevents.sdk.event +import cloudevents.sdk.event.v1 +import cloudevents.sdk.marshaller DEFAULT_SOURCE = os.path.realpath("./main.py") DEFAULT_SIGNATURE_TYPE = "http" From 209a8d66ebf418aeeb21b7e030073177b4f26d29 Mon Sep 17 00:00:00 2001 From: joelgerard Date: Tue, 16 Jun 2020 14:54:51 -0700 Subject: [PATCH 20/45] A sample that shows how to use a CloudEvent. --- examples/cloud_events/Dockerfile | 17 +++++++++++++++++ examples/cloud_events/main.py | 22 ++++++++++++++++++++++ examples/cloud_events/requirements.txt | 1 + 3 files changed, 40 insertions(+) create mode 100644 examples/cloud_events/Dockerfile create mode 100644 examples/cloud_events/main.py create mode 100644 examples/cloud_events/requirements.txt diff --git a/examples/cloud_events/Dockerfile b/examples/cloud_events/Dockerfile new file mode 100644 index 00000000..0e5e1b99 --- /dev/null +++ b/examples/cloud_events/Dockerfile @@ -0,0 +1,17 @@ +# Use the official Python image. +# https://hub.docker.com/_/python +FROM python:3.7-slim + +# Copy local code to the container image. +ENV APP_HOME /app +WORKDIR $APP_HOME +COPY . . + +RUN apt-get update && apt-get install -y git + +# Install production dependencies. +RUN pip install gunicorn cloudevents functions-framework +RUN pip install -r requirements.txt + +# Run the web service on container startup. +CMD exec functions-framework --target=hello --signature-type=cloudevent diff --git a/examples/cloud_events/main.py b/examples/cloud_events/main.py new file mode 100644 index 00000000..9ec57f62 --- /dev/null +++ b/examples/cloud_events/main.py @@ -0,0 +1,22 @@ +# Copyright 2020 Google LLC +# +# Licensed 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. + +# This sample creates a function that accepts a Cloud Event per +# https://github.com/cloudevents/sdk-python +import sys + +def hello(cloud_event): + print("Received event with ID: %s" % cloud_event.EventID(), file=sys.stdout, flush=True) + return 200 + diff --git a/examples/cloud_events/requirements.txt b/examples/cloud_events/requirements.txt new file mode 100644 index 00000000..33c5f99f --- /dev/null +++ b/examples/cloud_events/requirements.txt @@ -0,0 +1 @@ +# Optionally include additional dependencies here From f573a1e8503bcdce5a91571455a433a17597726a Mon Sep 17 00:00:00 2001 From: joelgerard Date: Tue, 16 Jun 2020 14:59:20 -0700 Subject: [PATCH 21/45] In the case of a sig type / event type mismatch throw a 400 --- src/functions_framework/__init__.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index 5db53e55..0d006b43 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -131,11 +131,11 @@ def view_func(path): _run_legacy_event(function, request) else: # here for defensive backwards compatibility in case we make a mistake in rollout. - raise InvalidConfigurationException( - "The FUNCTION_SIGNATURE_TYPE for this function is set to event " + werkzeug.abort(400, "The FUNCTION_SIGNATURE_TYPE for this function is set to event " "but no legacy event was given. If you are using CloudEvents set " "FUNCTION_SIGNATURE_TYPE=cloudevent" - ) + ) + return "OK" @@ -151,8 +151,7 @@ def view_func(path): elif event_type == _EventType.CLOUD_EVENT_BINARY: _run_binary_cloud_event(function, request, cloud_event_def) else: - raise InvalidConfigurationException( - "Function was defined with FUNCTION_SIGNATURE_TYPE=cloudevent " + werkzeug.abort(400, "Function was defined with FUNCTION_SIGNATURE_TYPE=cloudevent " " but it did not receive a cloudevent as a request." ) From 7212640f41cada4f4fc4449d38f4ed8dab897d81 Mon Sep 17 00:00:00 2001 From: joelgerard Date: Tue, 16 Jun 2020 15:53:15 -0700 Subject: [PATCH 22/45] Update the docs to use CloudEvent sig type instead of Event sig type. Note that I wrote the "Event" type is deprecated. Not sure if this is accurate. --- README.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8a580c40..417a7b02 100644 --- a/README.md +++ b/README.md @@ -129,27 +129,30 @@ You can configure the Functions Framework using command-line flags or environmen | `--host` | `HOST` | The host on which the Functions Framework listens for requests. Default: `0.0.0.0` | | `--port` | `PORT` | The port on which the Functions Framework listens for requests. Default: `8080` | | `--target` | `FUNCTION_TARGET` | The name of the exported function to be invoked in response to requests. Default: `function` | -| `--signature-type` | `FUNCTION_SIGNATURE_TYPE` | The signature used when writing your function. Controls unmarshalling rules and determines which arguments are used to invoke your function. Default: `http`; accepted values: `http` or `event` | +| `--signature-type` | `FUNCTION_SIGNATURE_TYPE` | The signature used when writing your function. Controls unmarshalling rules and determines which arguments are used to invoke your function. Default: `http`; accepted values: `http` or `cloudevent`. Note: the`event` signature type is legacy and deprecated. | | `--source` | `FUNCTION_SOURCE` | The path to the file containing your function. Default: `main.py` (in the current working directory) | | `--debug` | `DEBUG` | A flag that allows to run functions-framework to run in debug mode, including live reloading. Default: `False` | # Enable CloudEvents -The Functions Framework can unmarshall incoming [CloudEvents](http://cloudevents.io) payloads to `data` and `context` objects. These will be passed as arguments to your function when it receives a request. Note that your function must use the event-style function signature: +The Functions Framework can unmarshall incoming [CloudEvents](http://cloudevents.io) payloads to a Cloud Event object. +In this case, you can create a function that accepts a single argument, `event`, e.g.: + ```python -def hello(data, context): - print(data) - print(context) +def hello(cloud_event): + print("Received event with ID: %s" % cloud_event.EventID()) + return 200 ``` -To enable automatic unmarshalling, set the function signature type to `event` using the `--signature-type` command-line flag or the `FUNCTION_SIGNATURE_TYPE` environment variable. By default, the HTTP signature type will be used and automatic event unmarshalling will be disabled. +To enable automatic unmarshalling, set the function signature type to `cloudevent` using the `--signature-type` command-line flag or the `FUNCTION_SIGNATURE_TYPE` environment variable. By default, the HTTP signature type will be used and automatic event unmarshalling will be disabled. For more details on this signature type, check out the Google Cloud Functions documentation on [background functions](https://cloud.google.com/functions/docs/writing/background#cloud_pubsub_example). # Advanced Examples -More advanced guides can be found in the [`examples/`](./examples/) directory. +More advanced guides can be found in the [`examples/`](./examples/) directory. You can also find examples +on using the CloudEvent Python SDK [here](https://github.com/cloudevents/sdk-python). # Contributing From 974d52ba1c1c4e5e97e09828da0082cc09627515 Mon Sep 17 00:00:00 2001 From: joelgerard Date: Tue, 16 Jun 2020 15:56:32 -0700 Subject: [PATCH 23/45] Lint fixes. --- src/functions_framework/__init__.py | 15 +++++++++------ src/functions_framework/_cli.py | 2 +- tests/test_view_functions.py | 3 ++- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index 0d006b43..a393cabc 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -131,11 +131,12 @@ def view_func(path): _run_legacy_event(function, request) else: # here for defensive backwards compatibility in case we make a mistake in rollout. - werkzeug.abort(400, "The FUNCTION_SIGNATURE_TYPE for this function is set to event " + werkzeug.abort( + 400, + "The FUNCTION_SIGNATURE_TYPE for this function is set to event " "but no legacy event was given. If you are using CloudEvents set " - "FUNCTION_SIGNATURE_TYPE=cloudevent" - ) - + "FUNCTION_SIGNATURE_TYPE=cloudevent", + ) return "OK" @@ -151,8 +152,10 @@ def view_func(path): elif event_type == _EventType.CLOUD_EVENT_BINARY: _run_binary_cloud_event(function, request, cloud_event_def) else: - werkzeug.abort(400, "Function was defined with FUNCTION_SIGNATURE_TYPE=cloudevent " - " but it did not receive a cloudevent as a request." + werkzeug.abort( + 400, + "Function was defined with FUNCTION_SIGNATURE_TYPE=cloudevent " + " but it did not receive a cloudevent as a request.", ) return "OK" diff --git a/src/functions_framework/_cli.py b/src/functions_framework/_cli.py index b619aa92..663ea50f 100644 --- a/src/functions_framework/_cli.py +++ b/src/functions_framework/_cli.py @@ -26,7 +26,7 @@ @click.option( "--signature-type", envvar="FUNCTION_SIGNATURE_TYPE", - type=click.Choice(["http", "event","cloudevent"]), + type=click.Choice(["http", "event", "cloudevent"]), default="http", ) @click.option("--host", envvar="HOST", type=click.STRING, default="0.0.0.0") diff --git a/tests/test_view_functions.py b/tests/test_view_functions.py index a8aa7df5..6f31242a 100644 --- a/tests/test_view_functions.py +++ b/tests/test_view_functions.py @@ -89,6 +89,7 @@ def test_legacy_event_view_func_wrapper(monkeypatch): ) ] + # TODO(#57): This test needs to be updated or moved to integration tests once the # event adapter is working. # def test_binary_event_view_func_wrapper(monkeypatch): @@ -123,4 +124,4 @@ def test_legacy_event_view_func_wrapper(monkeypatch): # eventType="some-eventType", # resource="some-resource", # ) -# ] \ No newline at end of file +# ] From 9b87a6e834c3a2b5ebe451ff9eb1b5c98268ce3d Mon Sep 17 00:00:00 2001 From: joelgerard Date: Tue, 16 Jun 2020 16:09:04 -0700 Subject: [PATCH 24/45] Tests for checking correct event type corresponds to correct function sig. Fixed abort import error. --- src/functions_framework/__init__.py | 4 ++-- tests/test_cloudevent_functions.py | 11 +++++++++++ tests/test_event_functions.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index a393cabc..db7f2c21 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -131,7 +131,7 @@ def view_func(path): _run_legacy_event(function, request) else: # here for defensive backwards compatibility in case we make a mistake in rollout. - werkzeug.abort( + werkzeug.exceptions.abort( 400, "The FUNCTION_SIGNATURE_TYPE for this function is set to event " "but no legacy event was given. If you are using CloudEvents set " @@ -152,7 +152,7 @@ def view_func(path): elif event_type == _EventType.CLOUD_EVENT_BINARY: _run_binary_cloud_event(function, request, cloud_event_def) else: - werkzeug.abort( + werkzeug.exceptions.abort( 400, "Function was defined with FUNCTION_SIGNATURE_TYPE=cloudevent " " but it did not receive a cloudevent as a request.", diff --git a/tests/test_cloudevent_functions.py b/tests/test_cloudevent_functions.py index c94a174f..0914408f 100644 --- a/tests/test_cloudevent_functions.py +++ b/tests/test_cloudevent_functions.py @@ -106,3 +106,14 @@ def test_event_0_3(event_0_3): resp = client.post("/", headers=structured_headers, data=structured_data.getvalue()) assert resp.status_code == 200 assert resp.data == b"OK" + + +def test_non_cloud_event_(): + source = TEST_FUNCTIONS_DIR / "cloudevents" / "main.py" + target = "function" + + client = create_app(target, source, "cloudevent").test_client() + + resp = client.post("/", json="{not_event}") + assert resp.status_code == 400 + assert resp.data != b"OK" diff --git a/tests/test_event_functions.py b/tests/test_event_functions.py index 72dab200..e2303464 100644 --- a/tests/test_event_functions.py +++ b/tests/test_event_functions.py @@ -11,9 +11,13 @@ # 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 json import pathlib import re import pytest +import cloudevents.sdk +import cloudevents.sdk.event.v1 +import cloudevents.sdk.marshaller from functions_framework import LazyWSGIApp, create_app, exceptions @@ -39,6 +43,30 @@ def background_json(tmpdir): } +def test_non_legacy_event_fails(): + cloud_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") + ) + m = cloudevents.sdk.marshaller.NewDefaultHTTPMarshaller() + structured_headers, structured_data = m.ToRequest( + cloud_event, cloudevents.sdk.converters.TypeStructured, json.dumps + ) + + source = TEST_FUNCTIONS_DIR / "background_trigger" / "main.py" + target = "function" + + client = create_app(target, source, "event").test_client() + resp = client.post("/", headers=structured_headers, data=structured_data.getvalue()) + assert resp.status_code == 400 + assert resp.data != b"OK" + + def test_background_function_executes(background_json): source = TEST_FUNCTIONS_DIR / "background_trigger" / "main.py" target = "function" From 8d33033aca320912b720e25544688a3bf0049f07 Mon Sep 17 00:00:00 2001 From: joelgerard Date: Tue, 16 Jun 2020 16:16:52 -0700 Subject: [PATCH 25/45] Sort imports. --- examples/cloud_events/main.py | 2 +- src/functions_framework/__init__.py | 12 ++++++------ tests/test_cloudevent_functions.py | 3 ++- tests/test_event_functions.py | 3 ++- tests/test_functions.py | 1 + 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/examples/cloud_events/main.py b/examples/cloud_events/main.py index 9ec57f62..6ba78b21 100644 --- a/examples/cloud_events/main.py +++ b/examples/cloud_events/main.py @@ -16,7 +16,7 @@ # https://github.com/cloudevents/sdk-python import sys + def hello(cloud_event): print("Received event with ID: %s" % cloud_event.EventID(), file=sys.stdout, flush=True) return 200 - diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index db7f2c21..2af76fe8 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from enum import Enum import importlib.util import io import json @@ -21,6 +20,12 @@ import sys import types +from enum import Enum + +import cloudevents.sdk +import cloudevents.sdk.event +import cloudevents.sdk.event.v1 +import cloudevents.sdk.marshaller import flask import werkzeug @@ -33,11 +38,6 @@ ) from google.cloud.functions.context import Context -import cloudevents.sdk -import cloudevents.sdk.event -import cloudevents.sdk.event.v1 -import cloudevents.sdk.marshaller - DEFAULT_SOURCE = os.path.realpath("./main.py") DEFAULT_SIGNATURE_TYPE = "http" diff --git a/tests/test_cloudevent_functions.py b/tests/test_cloudevent_functions.py index 0914408f..592029ea 100644 --- a/tests/test_cloudevent_functions.py +++ b/tests/test_cloudevent_functions.py @@ -13,11 +13,12 @@ # limitations under the License. import json import pathlib -import pytest + import cloudevents.sdk import cloudevents.sdk.event.v1 import cloudevents.sdk.event.v03 import cloudevents.sdk.marshaller +import pytest from functions_framework import LazyWSGIApp, create_app, exceptions diff --git a/tests/test_event_functions.py b/tests/test_event_functions.py index e2303464..8f07dcfe 100644 --- a/tests/test_event_functions.py +++ b/tests/test_event_functions.py @@ -14,10 +14,11 @@ import json import pathlib import re -import pytest + import cloudevents.sdk import cloudevents.sdk.event.v1 import cloudevents.sdk.marshaller +import pytest from functions_framework import LazyWSGIApp, create_app, exceptions diff --git a/tests/test_functions.py b/tests/test_functions.py index 4eebb6ed..792a646e 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -17,6 +17,7 @@ import pretend import pytest + import functions_framework from functions_framework import LazyWSGIApp, create_app, exceptions From c297097e8295b91250f47feb36be664678170e1f Mon Sep 17 00:00:00 2001 From: joelgerard Date: Wed, 17 Jun 2020 10:29:27 -0700 Subject: [PATCH 26/45] Remove old example. --- examples/README.md | 2 +- examples/cloud_run_event/Dockerfile | 15 --------------- examples/cloud_run_event/main.py | 17 ----------------- examples/cloud_run_event/requirements.txt | 1 - 4 files changed, 1 insertion(+), 34 deletions(-) delete mode 100644 examples/cloud_run_event/Dockerfile delete mode 100644 examples/cloud_run_event/main.py delete mode 100644 examples/cloud_run_event/requirements.txt diff --git a/examples/README.md b/examples/README.md index 33ce2a76..64215444 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,4 +1,4 @@ # Python Functions Frameworks Examples * [`cloud_run_http`](./cloud_run_http/) - Deploying an HTTP function to [Cloud Run](http://cloud.google.com/run) with the Functions Framework -* [`cloud_run_event`](./cloud_run_event/) - Deploying a CloudEvent function to [Cloud Run](http://cloud.google.com/run) with the Functions Framework +* [`cloud_events`](./cloud_events/) - Deploying a CloudEvent function to [Cloud Run](http://cloud.google.com/run) with the Functions Framework diff --git a/examples/cloud_run_event/Dockerfile b/examples/cloud_run_event/Dockerfile deleted file mode 100644 index 6b31c042..00000000 --- a/examples/cloud_run_event/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -# Use the official Python image. -# https://hub.docker.com/_/python -FROM python:3.7-slim - -# Copy local code to the container image. -ENV APP_HOME /app -WORKDIR $APP_HOME -COPY . . - -# Install production dependencies. -RUN pip install gunicorn functions-framework -RUN pip install -r requirements.txt - -# Run the web service on container startup. -CMD exec functions-framework --target=hello --signature_type=event diff --git a/examples/cloud_run_event/main.py b/examples/cloud_run_event/main.py deleted file mode 100644 index 7ae454c4..00000000 --- a/examples/cloud_run_event/main.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed 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. - - -def hello(data, context): - pass diff --git a/examples/cloud_run_event/requirements.txt b/examples/cloud_run_event/requirements.txt deleted file mode 100644 index 33c5f99f..00000000 --- a/examples/cloud_run_event/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -# Optionally include additional dependencies here From 5176b8cfe967306ce95979422eb1bd0ce7824cdd Mon Sep 17 00:00:00 2001 From: joelgerard Date: Wed, 17 Jun 2020 10:36:24 -0700 Subject: [PATCH 27/45] Readme to explain how to run the sample locally. --- examples/cloud_events/README.md | 51 +++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 examples/cloud_events/README.md diff --git a/examples/cloud_events/README.md b/examples/cloud_events/README.md new file mode 100644 index 00000000..7d944f26 --- /dev/null +++ b/examples/cloud_events/README.md @@ -0,0 +1,51 @@ +# Python Functions Frameworks Cloud Event Sample +This sample uses the [Cloud Events SDK](https://github.com/cloudevents/sdk-python) to receive an event. +## How to run this locally +Build the docker image. + +```commandline +docker build --tag ff_example . +``` + +Run the image and bind the correct ports. + +```commandline +docker run -p:8080: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 +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/") + +``` From f499790df9b3a5fe72fddc08ef8138dda39b3649 Mon Sep 17 00:00:00 2001 From: joelgerard Date: Wed, 17 Jun 2020 10:46:36 -0700 Subject: [PATCH 28/45] Rename cloud_event to cloudevent --- README.md | 4 +-- examples/README.md | 2 +- .../{cloud_events => cloudevents}/Dockerfile | 0 .../{cloud_events => cloudevents}/README.md | 0 .../{cloud_events => cloudevents}/main.py | 4 +-- .../requirements.txt | 0 src/functions_framework/__init__.py | 28 +++++++++---------- tests/test_cloudevent_functions.py | 2 +- tests/test_event_functions.py | 4 +-- tests/test_functions/cloudevents/main.py | 14 +++++----- 10 files changed, 29 insertions(+), 29 deletions(-) rename examples/{cloud_events => cloudevents}/Dockerfile (100%) rename examples/{cloud_events => cloudevents}/README.md (100%) rename examples/{cloud_events => cloudevents}/main.py (85%) rename examples/{cloud_events => cloudevents}/requirements.txt (100%) diff --git a/README.md b/README.md index 417a7b02..475d9837 100644 --- a/README.md +++ b/README.md @@ -140,8 +140,8 @@ In this case, you can create a function that accepts a single argument, `event`, ```python -def hello(cloud_event): - print("Received event with ID: %s" % cloud_event.EventID()) +def hello(cloudevent): + print("Received event with ID: %s" % cloudevent.EventID()) return 200 ``` diff --git a/examples/README.md b/examples/README.md index 64215444..717c89a1 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,4 +1,4 @@ # Python Functions Frameworks Examples * [`cloud_run_http`](./cloud_run_http/) - Deploying an HTTP function to [Cloud Run](http://cloud.google.com/run) with the Functions Framework -* [`cloud_events`](./cloud_events/) - Deploying a CloudEvent function to [Cloud Run](http://cloud.google.com/run) with the Functions Framework +* [`cloudevents`](./cloudevents/) - Deploying a CloudEvent function to [Cloud Run](http://cloud.google.com/run) with the Functions Framework diff --git a/examples/cloud_events/Dockerfile b/examples/cloudevents/Dockerfile similarity index 100% rename from examples/cloud_events/Dockerfile rename to examples/cloudevents/Dockerfile diff --git a/examples/cloud_events/README.md b/examples/cloudevents/README.md similarity index 100% rename from examples/cloud_events/README.md rename to examples/cloudevents/README.md diff --git a/examples/cloud_events/main.py b/examples/cloudevents/main.py similarity index 85% rename from examples/cloud_events/main.py rename to examples/cloudevents/main.py index 6ba78b21..3ea4b8fb 100644 --- a/examples/cloud_events/main.py +++ b/examples/cloudevents/main.py @@ -17,6 +17,6 @@ import sys -def hello(cloud_event): - print("Received event with ID: %s" % cloud_event.EventID(), file=sys.stdout, flush=True) +def hello(cloudevent): + print("Received event with ID: %s" % cloudevent.EventID(), file=sys.stdout, flush=True) return 200 diff --git a/examples/cloud_events/requirements.txt b/examples/cloudevents/requirements.txt similarity index 100% rename from examples/cloud_events/requirements.txt rename to examples/cloudevents/requirements.txt diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index 2af76fe8..c433d4c0 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -44,8 +44,8 @@ class _EventType(Enum): LEGACY = 1 - CLOUD_EVENT_BINARY = 2 - CLOUD_EVENT_TEXT = 3 + CLOUDEVENT_BINARY = 2 + CLOUDEVENT_TEXT = 3 class _Event(object): @@ -80,7 +80,7 @@ def view_func(path): return view_func -def _get_cloud_event_version(): +def _get_cloudevent_version(): return cloudevents.sdk.event.v1.Event() @@ -94,20 +94,20 @@ def _run_legacy_event(function, request): function(data, context) -def _run_binary_cloud_event(function, request, cloud_event_def): +def _run_binary_cloudevent(function, request, cloudevent_def): data = io.BytesIO(request.get_data()) http_marshaller = cloudevents.sdk.marshaller.NewDefaultHTTPMarshaller() event = http_marshaller.FromRequest( - cloud_event_def, request.headers, data, json.load + cloudevent_def, request.headers, data, json.load ) function(event) -def _run_text_cloud_event(function, request, cloud_event_def): +def _run_text_cloudevent(function, request, cloudevent_def): data = io.StringIO(request.get_data(as_text=True)) m = cloudevents.sdk.marshaller.NewDefaultHTTPMarshaller() - event = m.FromRequest(cloud_event_def, request.headers, data, json.loads) + event = m.FromRequest(cloudevent_def, request.headers, data, json.loads) function(event) @@ -118,9 +118,9 @@ def _get_event_type(request): and request.headers.get("ce-source") and request.headers.get("ce-id") ): - return _EventType.CLOUD_EVENT_BINARY + return _EventType.CLOUDEVENT_BINARY elif request.headers.get("Content-Type") == "application/cloudevents+json": - return _EventType.CLOUD_EVENT_TEXT + return _EventType.CLOUDEVENT_TEXT else: return _EventType.LEGACY @@ -145,12 +145,12 @@ def view_func(path): def _cloudevent_view_func_wrapper(function, request): def view_func(path): - cloud_event_def = _get_cloud_event_version() + cloudevent_def = _get_cloudevent_version() event_type = _get_event_type(request) - if event_type == _EventType.CLOUD_EVENT_TEXT: - _run_text_cloud_event(function, request, cloud_event_def) - elif event_type == _EventType.CLOUD_EVENT_BINARY: - _run_binary_cloud_event(function, request, cloud_event_def) + if event_type == _EventType.CLOUDEVENT_TEXT: + _run_text_cloudevent(function, request, cloudevent_def) + elif event_type == _EventType.CLOUDEVENT_BINARY: + _run_binary_cloudevent(function, request, cloudevent_def) else: werkzeug.exceptions.abort( 400, diff --git a/tests/test_cloudevent_functions.py b/tests/test_cloudevent_functions.py index 592029ea..7768a00e 100644 --- a/tests/test_cloudevent_functions.py +++ b/tests/test_cloudevent_functions.py @@ -109,7 +109,7 @@ def test_event_0_3(event_0_3): assert resp.data == b"OK" -def test_non_cloud_event_(): +def test_non_cloudevent_(): source = TEST_FUNCTIONS_DIR / "cloudevents" / "main.py" target = "function" diff --git a/tests/test_event_functions.py b/tests/test_event_functions.py index 8f07dcfe..7b274672 100644 --- a/tests/test_event_functions.py +++ b/tests/test_event_functions.py @@ -45,7 +45,7 @@ def background_json(tmpdir): def test_non_legacy_event_fails(): - cloud_event = ( + cloudevent = ( cloudevents.sdk.event.v1.Event() .SetContentType("application/json") .SetData('{"name":"john"}') @@ -56,7 +56,7 @@ def test_non_legacy_event_fails(): ) m = cloudevents.sdk.marshaller.NewDefaultHTTPMarshaller() structured_headers, structured_data = m.ToRequest( - cloud_event, cloudevents.sdk.converters.TypeStructured, json.dumps + cloudevent, cloudevents.sdk.converters.TypeStructured, json.dumps ) source = TEST_FUNCTIONS_DIR / "background_trigger" / "main.py" diff --git a/tests/test_functions/cloudevents/main.py b/tests/test_functions/cloudevents/main.py index 0581282a..91b792dc 100644 --- a/tests/test_functions/cloudevents/main.py +++ b/tests/test_functions/cloudevents/main.py @@ -15,24 +15,24 @@ """Function used to test handling Cloud Event functions.""" -def function(cloud_event): +def function(cloudevent): """Test Event function that checks to see if a valid CloudEvent was sent. The function returns 200 if it received the expected event, otherwise 500. Args: - cloud_event: A Cloud event as defined by https://github.com/cloudevents/sdk-python. + cloudevent: A Cloud event as defined by https://github.com/cloudevents/sdk-python. Returns: HTTP status code indicating whether valid event was sent or not. """ valid_event = ( - cloud_event.EventID() == "my-id" - and cloud_event.Data() == '{"name":"john"}' - and cloud_event.Source() == "from-galaxy-far-far-away" - and cloud_event.EventTime() == "tomorrow" - and cloud_event.EventType() == "cloudevent.greet.you" + 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" ) if valid_event: From 973beafd0fdbeb10cf556d2a11cb4a64f06bdce4 Mon Sep 17 00:00:00 2001 From: joelgerard Date: Wed, 17 Jun 2020 10:46:52 -0700 Subject: [PATCH 29/45] For legacy docs, add a notice to the new docs. --- examples/cloud_run_event/README.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 examples/cloud_run_event/README.md diff --git a/examples/cloud_run_event/README.md b/examples/cloud_run_event/README.md new file mode 100644 index 00000000..336cc291 --- /dev/null +++ b/examples/cloud_run_event/README.md @@ -0,0 +1,3 @@ +# Events Example (legacy) +This sample directory used to refer to a mechanism for function event handling. +This is been retired in favor of Cloud Events. Please see [this example](../cloudevents). \ No newline at end of file From 30b92591fd429438e402fa970acea36868599458 Mon Sep 17 00:00:00 2001 From: joelgerard Date: Wed, 17 Jun 2020 10:48:54 -0700 Subject: [PATCH 30/45] There is no 1.1 event type. --- tests/test_cloudevent_functions.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_cloudevent_functions.py b/tests/test_cloudevent_functions.py index 7768a00e..74a4530e 100644 --- a/tests/test_cloudevent_functions.py +++ b/tests/test_cloudevent_functions.py @@ -32,7 +32,7 @@ @pytest.fixture -def event_1_10(): +def event_1_0(): event = ( cloudevents.sdk.event.v1.Event() .SetContentType("application/json") @@ -59,7 +59,7 @@ def event_0_3(): return event -def test_event_1_0(event_1_10): +def test_event_1_0(event_1_0): source = TEST_FUNCTIONS_DIR / "cloudevents" / "main.py" target = "function" @@ -67,7 +67,7 @@ def test_event_1_0(event_1_10): m = cloudevents.sdk.marshaller.NewDefaultHTTPMarshaller() structured_headers, structured_data = m.ToRequest( - event_1_10, cloudevents.sdk.converters.TypeStructured, json.dumps + event_1_0, cloudevents.sdk.converters.TypeStructured, json.dumps ) resp = client.post("/", headers=structured_headers, data=structured_data.getvalue()) @@ -75,7 +75,7 @@ def test_event_1_0(event_1_10): assert resp.data == b"OK" -def test_binary_event_1_0(event_1_10): +def test_binary_event_1_0(event_1_0): source = TEST_FUNCTIONS_DIR / "cloudevents" / "main.py" target = "function" @@ -84,7 +84,7 @@ def test_binary_event_1_0(event_1_10): m = cloudevents.sdk.marshaller.NewDefaultHTTPMarshaller() binary_headers, binary_data = m.ToRequest( - event_1_10, cloudevents.sdk.converters.TypeBinary, json.dumps + event_1_0, cloudevents.sdk.converters.TypeBinary, json.dumps ) resp = client.post("/", headers=binary_headers, data=binary_data) From 2532cecf502ea230ec4553a55816f77eb1387b65 Mon Sep 17 00:00:00 2001 From: joelgerard Date: Wed, 17 Jun 2020 13:23:19 -0700 Subject: [PATCH 31/45] use the term cloudevent rather than event everywhere where we are talking about a CloudEvent to disambiguate these signature types. --- tests/test_cloudevent_functions.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_cloudevent_functions.py b/tests/test_cloudevent_functions.py index 74a4530e..17e6f23c 100644 --- a/tests/test_cloudevent_functions.py +++ b/tests/test_cloudevent_functions.py @@ -32,7 +32,7 @@ @pytest.fixture -def event_1_0(): +def cloudevent_1_0(): event = ( cloudevents.sdk.event.v1.Event() .SetContentType("application/json") @@ -46,7 +46,7 @@ def event_1_0(): @pytest.fixture -def event_0_3(): +def cloudevent_0_3(): event = ( cloudevents.sdk.event.v03.Event() .SetContentType("application/json") @@ -59,7 +59,7 @@ def event_0_3(): return event -def test_event_1_0(event_1_0): +def test_event_1_0(cloudevent_1_0): source = TEST_FUNCTIONS_DIR / "cloudevents" / "main.py" target = "function" @@ -67,7 +67,7 @@ def test_event_1_0(event_1_0): m = cloudevents.sdk.marshaller.NewDefaultHTTPMarshaller() structured_headers, structured_data = m.ToRequest( - event_1_0, cloudevents.sdk.converters.TypeStructured, json.dumps + cloudevent_1_0, cloudevents.sdk.converters.TypeStructured, json.dumps ) resp = client.post("/", headers=structured_headers, data=structured_data.getvalue()) @@ -75,7 +75,7 @@ def test_event_1_0(event_1_0): assert resp.data == b"OK" -def test_binary_event_1_0(event_1_0): +def test_binary_event_1_0(cloudevent_1_0): source = TEST_FUNCTIONS_DIR / "cloudevents" / "main.py" target = "function" @@ -84,7 +84,7 @@ def test_binary_event_1_0(event_1_0): m = cloudevents.sdk.marshaller.NewDefaultHTTPMarshaller() binary_headers, binary_data = m.ToRequest( - event_1_0, cloudevents.sdk.converters.TypeBinary, json.dumps + cloudevent_1_0, cloudevents.sdk.converters.TypeBinary, json.dumps ) resp = client.post("/", headers=binary_headers, data=binary_data) @@ -93,7 +93,7 @@ def test_binary_event_1_0(event_1_0): assert resp.data == b"OK" -def test_event_0_3(event_0_3): +def test_event_0_3(cloudevent_0_3): source = TEST_FUNCTIONS_DIR / "cloudevents" / "main.py" target = "function" @@ -101,7 +101,7 @@ def test_event_0_3(event_0_3): m = cloudevents.sdk.marshaller.NewDefaultHTTPMarshaller() structured_headers, structured_data = m.ToRequest( - event_0_3, cloudevents.sdk.converters.TypeStructured, json.dumps + cloudevent_0_3, cloudevents.sdk.converters.TypeStructured, json.dumps ) resp = client.post("/", headers=structured_headers, data=structured_data.getvalue()) From 7928d5ddbc3995124880c334ea8612dd4bdd2040 Mon Sep 17 00:00:00 2001 From: joelgerard Date: Wed, 17 Jun 2020 15:33:50 -0700 Subject: [PATCH 32/45] Update examples/cloudevents/README.md Co-authored-by: Dustin Ingram --- examples/cloudevents/README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/cloudevents/README.md b/examples/cloudevents/README.md index 7d944f26..a03cc6a6 100644 --- a/examples/cloudevents/README.md +++ b/examples/cloudevents/README.md @@ -1,7 +1,8 @@ -# Python Functions Frameworks Cloud Event Sample -This sample uses the [Cloud Events SDK](https://github.com/cloudevents/sdk-python) to receive an event. +# 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. +Build the Docker image: ```commandline docker build --tag ff_example . From a38b480f287383f19bb91f76498e4a6cfd28ac7f Mon Sep 17 00:00:00 2001 From: joelgerard Date: Wed, 17 Jun 2020 15:34:17 -0700 Subject: [PATCH 33/45] Update examples/cloudevents/README.md Co-authored-by: Dustin Ingram --- examples/cloudevents/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/cloudevents/README.md b/examples/cloudevents/README.md index a03cc6a6..7a21a3fa 100644 --- a/examples/cloudevents/README.md +++ b/examples/cloudevents/README.md @@ -14,7 +14,7 @@ Run the image and bind the correct ports. docker run -p:8080:8080 ff_example ``` -Send an event to the container. +Send an event to the container: ```python from cloudevents.sdk import converters From 154de1c4a1cafcbf9666fdca3b0698a916d7b5e9 Mon Sep 17 00:00:00 2001 From: joelgerard Date: Wed, 17 Jun 2020 15:34:27 -0700 Subject: [PATCH 34/45] Update examples/cloudevents/README.md Co-authored-by: Dustin Ingram --- examples/cloudevents/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/cloudevents/README.md b/examples/cloudevents/README.md index 7a21a3fa..0f3c8fe0 100644 --- a/examples/cloudevents/README.md +++ b/examples/cloudevents/README.md @@ -8,7 +8,7 @@ Build the Docker image: docker build --tag ff_example . ``` -Run the image and bind the correct ports. +Run the image and bind the correct ports: ```commandline docker run -p:8080:8080 ff_example From 2a92d9c2cd088b57c122f740f85bdd52c3d78258 Mon Sep 17 00:00:00 2001 From: joelgerard Date: Wed, 17 Jun 2020 15:34:51 -0700 Subject: [PATCH 35/45] Update examples/cloudevents/main.py Co-authored-by: Dustin Ingram --- examples/cloudevents/main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/cloudevents/main.py b/examples/cloudevents/main.py index 3ea4b8fb..94b2734a 100644 --- a/examples/cloudevents/main.py +++ b/examples/cloudevents/main.py @@ -19,4 +19,3 @@ def hello(cloudevent): print("Received event with ID: %s" % cloudevent.EventID(), file=sys.stdout, flush=True) - return 200 From a2b3c67914c0d38dd99be086147fa7af7410f4ed Mon Sep 17 00:00:00 2001 From: joelgerard Date: Wed, 17 Jun 2020 15:35:28 -0700 Subject: [PATCH 36/45] Update tests/test_view_functions.py Co-authored-by: Dustin Ingram --- tests/test_view_functions.py | 37 ------------------------------------ 1 file changed, 37 deletions(-) diff --git a/tests/test_view_functions.py b/tests/test_view_functions.py index 6f31242a..a9e13bb7 100644 --- a/tests/test_view_functions.py +++ b/tests/test_view_functions.py @@ -88,40 +88,3 @@ def test_legacy_event_view_func_wrapper(monkeypatch): resource="some-resource", ) ] - - -# TODO(#57): This test needs to be updated or moved to integration tests once the -# event adapter is working. -# def test_binary_event_view_func_wrapper(monkeypatch): -# data = pretend.stub() -# request = pretend.stub( -# headers={ -# "ce-type": "something", -# "ce-specversion": "something", -# "ce-source": "something", -# "ce-id": "something", -# "ce-eventId": "some-eventId", -# "ce-timestamp": "some-timestamp", -# "ce-eventType": "some-eventType", -# "ce-resource": "some-resource", -# }, -# get_data=lambda: data, -# ) -# -# context_stub = pretend.stub() -# context_class = pretend.call_recorder(lambda *a, **kw: context_stub) -# monkeypatch.setattr(functions_framework, "Context", context_class) -# function = pretend.call_recorder(lambda data, context: "Hello") -# -# view_func = functions_framework._event_view_func_wrapper(function, request) -# view_func("/some/path") -# -# assert function.calls == [pretend.call(data, context_stub)] -# assert context_class.calls == [ -# pretend.call( -# eventId="some-eventId", -# timestamp="some-timestamp", -# eventType="some-eventType", -# resource="some-resource", -# ) -# ] From dbc44a3f7c3c13bb294278530ae810c6ba474962 Mon Sep 17 00:00:00 2001 From: joelgerard Date: Wed, 17 Jun 2020 15:43:24 -0700 Subject: [PATCH 37/45] Add legacy event back to docs. --- README.md | 12 ++++++++++-- examples/cloud_run_event/README.md | 3 +-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 475d9837..b09e2539 100644 --- a/README.md +++ b/README.md @@ -129,14 +129,14 @@ You can configure the Functions Framework using command-line flags or environmen | `--host` | `HOST` | The host on which the Functions Framework listens for requests. Default: `0.0.0.0` | | `--port` | `PORT` | The port on which the Functions Framework listens for requests. Default: `8080` | | `--target` | `FUNCTION_TARGET` | The name of the exported function to be invoked in response to requests. Default: `function` | -| `--signature-type` | `FUNCTION_SIGNATURE_TYPE` | The signature used when writing your function. Controls unmarshalling rules and determines which arguments are used to invoke your function. Default: `http`; accepted values: `http` or `cloudevent`. Note: the`event` signature type is legacy and deprecated. | +| `--signature-type` | `FUNCTION_SIGNATURE_TYPE` | The signature used when writing your function. Controls unmarshalling rules and determines which arguments are used to invoke your function. Default: `http`; accepted values: `http` or `cloudevent`. Note: the`event` signature type is legacy; `cloudevent` is preferred. | | `--source` | `FUNCTION_SOURCE` | The path to the file containing your function. Default: `main.py` (in the current working directory) | | `--debug` | `DEBUG` | A flag that allows to run functions-framework to run in debug mode, including live reloading. Default: `False` | # Enable CloudEvents The Functions Framework can unmarshall incoming [CloudEvents](http://cloudevents.io) payloads to a Cloud Event object. -In this case, you can create a function that accepts a single argument, `event`, e.g.: +In this case, you can create a function that accepts a single argument, `cloudevent`, e.g.: ```python @@ -147,6 +147,14 @@ def hello(cloudevent): To enable automatic unmarshalling, set the function signature type to `cloudevent` using the `--signature-type` command-line flag or the `FUNCTION_SIGNATURE_TYPE` environment variable. By default, the HTTP signature type will be used and automatic event unmarshalling will be disabled. +Note, you can still use the legacy `event` function signature although in this case the function signature is slightly different, i.e.: + +```python +def hello(data, context): + print(data) + print(context) +``` + For more details on this signature type, check out the Google Cloud Functions documentation on [background functions](https://cloud.google.com/functions/docs/writing/background#cloud_pubsub_example). # Advanced Examples diff --git a/examples/cloud_run_event/README.md b/examples/cloud_run_event/README.md index 336cc291..8785fb8d 100644 --- a/examples/cloud_run_event/README.md +++ b/examples/cloud_run_event/README.md @@ -1,3 +1,2 @@ # Events Example (legacy) -This sample directory used to refer to a mechanism for function event handling. -This is been retired in favor of Cloud Events. Please see [this example](../cloudevents). \ No newline at end of file +This example demonstrates how to write an event function. This will be retired in favor of Cloud Events. Please see [this example](../cloudevents). \ No newline at end of file From eafa7811591d90847e9cca9789cb682b9150f4d7 Mon Sep 17 00:00:00 2001 From: joelgerard Date: Wed, 17 Jun 2020 15:43:29 -0700 Subject: [PATCH 38/45] Add legacy event back to docs. --- examples/README.md | 5 ++++- examples/cloud_run_event/Dockerfile | 15 +++++++++++++++ examples/cloud_run_event/main.py | 17 +++++++++++++++++ examples/cloud_run_event/requirements.txt | 1 + 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 examples/cloud_run_event/Dockerfile create mode 100644 examples/cloud_run_event/main.py create mode 100644 examples/cloud_run_event/requirements.txt diff --git a/examples/README.md b/examples/README.md index 717c89a1..69ee3f97 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,4 +1,7 @@ # Python Functions Frameworks Examples * [`cloud_run_http`](./cloud_run_http/) - Deploying an HTTP function to [Cloud Run](http://cloud.google.com/run) with the Functions Framework -* [`cloudevents`](./cloudevents/) - Deploying a CloudEvent function to [Cloud Run](http://cloud.google.com/run) with the Functions Framework +* [`cloud_events`](./cloud_events/) - Deploying a CloudEvent function to [Cloud Run](http://cloud.google.com/run) with the Functions Framework +* [`cloud_run_event`](./cloud_run_event/) - Deploying a legacy Event function to [Cloud Run](http://cloud.google.com/run) with the Functions Framework + + diff --git a/examples/cloud_run_event/Dockerfile b/examples/cloud_run_event/Dockerfile new file mode 100644 index 00000000..6b31c042 --- /dev/null +++ b/examples/cloud_run_event/Dockerfile @@ -0,0 +1,15 @@ +# Use the official Python image. +# https://hub.docker.com/_/python +FROM python:3.7-slim + +# Copy local code to the container image. +ENV APP_HOME /app +WORKDIR $APP_HOME +COPY . . + +# Install production dependencies. +RUN pip install gunicorn functions-framework +RUN pip install -r requirements.txt + +# Run the web service on container startup. +CMD exec functions-framework --target=hello --signature_type=event diff --git a/examples/cloud_run_event/main.py b/examples/cloud_run_event/main.py new file mode 100644 index 00000000..7ae454c4 --- /dev/null +++ b/examples/cloud_run_event/main.py @@ -0,0 +1,17 @@ +# Copyright 2020 Google LLC +# +# Licensed 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. + + +def hello(data, context): + pass diff --git a/examples/cloud_run_event/requirements.txt b/examples/cloud_run_event/requirements.txt new file mode 100644 index 00000000..33c5f99f --- /dev/null +++ b/examples/cloud_run_event/requirements.txt @@ -0,0 +1 @@ +# Optionally include additional dependencies here From 6ce6f63cf02413d7aaecd2712382e2a7a9e8aa19 Mon Sep 17 00:00:00 2001 From: joelgerard Date: Wed, 17 Jun 2020 15:44:01 -0700 Subject: [PATCH 39/45] Use abort from flask for consistency and fix return in event test. --- src/functions_framework/__init__.py | 8 ++++---- tests/test_functions/cloudevents/main.py | 7 +++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index c433d4c0..cebe07c5 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -131,9 +131,9 @@ def view_func(path): _run_legacy_event(function, request) else: # here for defensive backwards compatibility in case we make a mistake in rollout. - werkzeug.exceptions.abort( + flask.abort( 400, - "The FUNCTION_SIGNATURE_TYPE for this function is set to event " + description="The FUNCTION_SIGNATURE_TYPE for this function is set to event " "but no legacy event was given. If you are using CloudEvents set " "FUNCTION_SIGNATURE_TYPE=cloudevent", ) @@ -152,9 +152,9 @@ def view_func(path): elif event_type == _EventType.CLOUDEVENT_BINARY: _run_binary_cloudevent(function, request, cloudevent_def) else: - werkzeug.exceptions.abort( + flask.abort( 400, - "Function was defined with FUNCTION_SIGNATURE_TYPE=cloudevent " + description="Function was defined with FUNCTION_SIGNATURE_TYPE=cloudevent " " but it did not receive a cloudevent as a request.", ) diff --git a/tests/test_functions/cloudevents/main.py b/tests/test_functions/cloudevents/main.py index 91b792dc..f2fdb6f3 100644 --- a/tests/test_functions/cloudevents/main.py +++ b/tests/test_functions/cloudevents/main.py @@ -13,6 +13,7 @@ # limitations under the License. """Function used to test handling Cloud Event functions.""" +import flask def function(cloudevent): @@ -35,7 +36,5 @@ def function(cloudevent): and cloudevent.EventType() == "cloudevent.greet.you" ) - if valid_event: - return 200 - else: - return 500 + if not valid_event: + flask.abort(500) From c2ba64d51d0840fb9c934e10ced9e7c229ae8e7f Mon Sep 17 00:00:00 2001 From: joelgerard Date: Wed, 17 Jun 2020 16:30:37 -0700 Subject: [PATCH 40/45] update docs and error messages to better mirror the other runtimes. --- README.md | 43 +++++++++++++++++++++-------- examples/README.md | 5 ++-- examples/cloud_run_event/README.md | 5 ++-- src/functions_framework/__init__.py | 2 +- 4 files changed, 38 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index b09e2539..ac039de6 100644 --- a/README.md +++ b/README.md @@ -129,14 +129,41 @@ You can configure the Functions Framework using command-line flags or environmen | `--host` | `HOST` | The host on which the Functions Framework listens for requests. Default: `0.0.0.0` | | `--port` | `PORT` | The port on which the Functions Framework listens for requests. Default: `8080` | | `--target` | `FUNCTION_TARGET` | The name of the exported function to be invoked in response to requests. Default: `function` | -| `--signature-type` | `FUNCTION_SIGNATURE_TYPE` | The signature used when writing your function. Controls unmarshalling rules and determines which arguments are used to invoke your function. Default: `http`; accepted values: `http` or `cloudevent`. Note: the`event` signature type is legacy; `cloudevent` is preferred. | +| `--signature-type` | `FUNCTION_SIGNATURE_TYPE` | The signature used when writing your function. Controls unmarshalling rules and determines which arguments are used to invoke your function. Default: `http`; accepted values: `http` or `event` or `cloudevent` | | `--source` | `FUNCTION_SOURCE` | The path to the file containing your function. Default: `main.py` (in the current working directory) | | `--debug` | `DEBUG` | A flag that allows to run functions-framework to run in debug mode, including live reloading. Default: `False` | + +# Enable Google Cloud Functions Events + +The Functions Framework can unmarshall incoming +Google Cloud Functions [event](https://cloud.google.com/functions/docs/concepts/events-triggers#events) payloads to `data` and `context` objects. +These will be passed as arguments to your function when it receives a request. +Note that your function must use the `event`-style function signature: + + +```python +def hello(data, context): + print(data) + print(context) +``` + +To enable automatic unmarshalling, set the function signature type to `event` +using a command-line flag or an environment variable. By default, the HTTP +signature will be used and automatic event unmarshalling will be disabled. + +For more details on this signature type, check out the Google Cloud Functions +documentation on +[background functions](https://cloud.google.com/functions/docs/writing/background#cloud_pubsub_example). + +See the [running example](examples/cloud_run_event). + # Enable CloudEvents -The Functions Framework can unmarshall incoming [CloudEvents](http://cloudevents.io) payloads to a Cloud Event object. -In this case, you can create a function that accepts a single argument, `cloudevent`, e.g.: +The Functions Framework can unmarshall incoming +[CloudEvents](http://cloudevents.io) payloads to a `cloudevent` object. +It will be passed as an argument to your function when it receives a request. +Note that your function must use the `cloudevent`-style function signature ```python @@ -147,15 +174,7 @@ def hello(cloudevent): To enable automatic unmarshalling, set the function signature type to `cloudevent` using the `--signature-type` command-line flag or the `FUNCTION_SIGNATURE_TYPE` environment variable. By default, the HTTP signature type will be used and automatic event unmarshalling will be disabled. -Note, you can still use the legacy `event` function signature although in this case the function signature is slightly different, i.e.: - -```python -def hello(data, context): - print(data) - print(context) -``` - -For more details on this signature type, check out the Google Cloud Functions documentation on [background functions](https://cloud.google.com/functions/docs/writing/background#cloud_pubsub_example). +See the [running example](examples/cloudevents). # Advanced Examples diff --git a/examples/README.md b/examples/README.md index 69ee3f97..8938a2ad 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,7 +1,8 @@ # Python Functions Frameworks Examples * [`cloud_run_http`](./cloud_run_http/) - Deploying an HTTP function to [Cloud Run](http://cloud.google.com/run) with the Functions Framework -* [`cloud_events`](./cloud_events/) - Deploying a CloudEvent function to [Cloud Run](http://cloud.google.com/run) with the Functions Framework -* [`cloud_run_event`](./cloud_run_event/) - Deploying a legacy Event function to [Cloud Run](http://cloud.google.com/run) with the Functions Framework +* [`cloud_run_event`](./cloud_run_event/) - Deploying a [Google Cloud Functions Event](https://cloud.google.com/functions/docs/concepts/events-triggers#events) function to [Cloud Run](http://cloud.google.com/run) with the Functions Framework +* [`cloudevents`](./cloudevents/) - Deploying a [CloudEvent](https://github.com/cloudevents/sdk-python) function to [Cloud Run](http://cloud.google.com/run) with the Functions Framework + diff --git a/examples/cloud_run_event/README.md b/examples/cloud_run_event/README.md index 8785fb8d..7d3e0a78 100644 --- a/examples/cloud_run_event/README.md +++ b/examples/cloud_run_event/README.md @@ -1,2 +1,3 @@ -# Events Example (legacy) -This example demonstrates how to write an event function. This will be retired in favor of Cloud Events. Please see [this example](../cloudevents). \ No newline at end of file +# Google Cloud Functions Events Example +This example demonstrates how to write an event function. Note that you can also use [CloudEvents](https://github.com/cloudevents/sdk-python) +([example](../cloudevents)), which is a different construct. \ No newline at end of file diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index cebe07c5..79c6bfb5 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -134,7 +134,7 @@ def view_func(path): flask.abort( 400, description="The FUNCTION_SIGNATURE_TYPE for this function is set to event " - "but no legacy event was given. If you are using CloudEvents set " + "but no Google Cloud Functions Event was given. If you are using CloudEvents set " "FUNCTION_SIGNATURE_TYPE=cloudevent", ) From 6889f54512946d406cfc380fd44e64e736237013 Mon Sep 17 00:00:00 2001 From: joelgerard Date: Thu, 18 Jun 2020 09:15:08 -0700 Subject: [PATCH 41/45] Minor fixes to docs w.r.t. naming. --- README.md | 4 ++-- examples/README.md | 2 +- examples/{cloudevents => cloud_run_cloudevents}/Dockerfile | 0 examples/{cloudevents => cloud_run_cloudevents}/README.md | 0 examples/{cloudevents => cloud_run_cloudevents}/main.py | 0 .../{cloudevents => cloud_run_cloudevents}/requirements.txt | 0 examples/cloud_run_event/README.md | 2 +- 7 files changed, 4 insertions(+), 4 deletions(-) rename examples/{cloudevents => cloud_run_cloudevents}/Dockerfile (100%) rename examples/{cloudevents => cloud_run_cloudevents}/README.md (100%) rename examples/{cloudevents => cloud_run_cloudevents}/main.py (100%) rename examples/{cloudevents => cloud_run_cloudevents}/requirements.txt (100%) diff --git a/README.md b/README.md index ac039de6..b4d59c53 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ See the [running example](examples/cloud_run_event). # Enable CloudEvents The Functions Framework can unmarshall incoming -[CloudEvents](http://cloudevents.io) payloads to a `cloudevent` object. +[CloudEvent](http://cloudevents.io) payloads to a `cloudevent` object. It will be passed as an argument to your function when it receives a request. Note that your function must use the `cloudevent`-style function signature @@ -174,7 +174,7 @@ def hello(cloudevent): To enable automatic unmarshalling, set the function signature type to `cloudevent` using the `--signature-type` command-line flag or the `FUNCTION_SIGNATURE_TYPE` environment variable. By default, the HTTP signature type will be used and automatic event unmarshalling will be disabled. -See the [running example](examples/cloudevents). +See the [running example](examples/cloud_run_cloudevents). # Advanced Examples diff --git a/examples/README.md b/examples/README.md index 8938a2ad..12b755a5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,7 +2,7 @@ * [`cloud_run_http`](./cloud_run_http/) - Deploying an HTTP function to [Cloud Run](http://cloud.google.com/run) with the Functions Framework * [`cloud_run_event`](./cloud_run_event/) - Deploying a [Google Cloud Functions Event](https://cloud.google.com/functions/docs/concepts/events-triggers#events) function to [Cloud Run](http://cloud.google.com/run) with the Functions Framework -* [`cloudevents`](./cloudevents/) - Deploying a [CloudEvent](https://github.com/cloudevents/sdk-python) function to [Cloud Run](http://cloud.google.com/run) with the Functions Framework +* [`cloud_run_cloudevents`](./cloud_run_cloudevents/) - Deploying a [CloudEvent](https://github.com/cloudevents/sdk-python) function to [Cloud Run](http://cloud.google.com/run) with the Functions Framework diff --git a/examples/cloudevents/Dockerfile b/examples/cloud_run_cloudevents/Dockerfile similarity index 100% rename from examples/cloudevents/Dockerfile rename to examples/cloud_run_cloudevents/Dockerfile diff --git a/examples/cloudevents/README.md b/examples/cloud_run_cloudevents/README.md similarity index 100% rename from examples/cloudevents/README.md rename to examples/cloud_run_cloudevents/README.md diff --git a/examples/cloudevents/main.py b/examples/cloud_run_cloudevents/main.py similarity index 100% rename from examples/cloudevents/main.py rename to examples/cloud_run_cloudevents/main.py diff --git a/examples/cloudevents/requirements.txt b/examples/cloud_run_cloudevents/requirements.txt similarity index 100% rename from examples/cloudevents/requirements.txt rename to examples/cloud_run_cloudevents/requirements.txt diff --git a/examples/cloud_run_event/README.md b/examples/cloud_run_event/README.md index 7d3e0a78..62d34cca 100644 --- a/examples/cloud_run_event/README.md +++ b/examples/cloud_run_event/README.md @@ -1,3 +1,3 @@ # Google Cloud Functions Events Example This example demonstrates how to write an event function. Note that you can also use [CloudEvents](https://github.com/cloudevents/sdk-python) -([example](../cloudevents)), which is a different construct. \ No newline at end of file +([example](../cloud_run_cloudevents)), which is a different construct. \ No newline at end of file From 271e976307d8f09aa13db9bffdf0bce5df99cf7d Mon Sep 17 00:00:00 2001 From: joelgerard Date: Thu, 18 Jun 2020 13:00:55 -0700 Subject: [PATCH 42/45] Update src/functions_framework/__init__.py Co-authored-by: Dustin Ingram --- src/functions_framework/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index 79c6bfb5..7c26bc81 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -42,7 +42,7 @@ DEFAULT_SIGNATURE_TYPE = "http" -class _EventType(Enum): +class _EventType(enum.Enum): LEGACY = 1 CLOUDEVENT_BINARY = 2 CLOUDEVENT_TEXT = 3 From 7cb1d88abf7d0e1ca5d197e18191c3c630461645 Mon Sep 17 00:00:00 2001 From: joelgerard Date: Thu, 18 Jun 2020 13:03:11 -0700 Subject: [PATCH 43/45] Fix enum per reviewer suggestion. --- src/functions_framework/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index 7c26bc81..b44ca2ea 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import enum import importlib.util import io import json @@ -20,8 +21,6 @@ import sys import types -from enum import Enum - import cloudevents.sdk import cloudevents.sdk.event import cloudevents.sdk.event.v1 From 2e34e8b5da2c386692838504f44d91f7b047a977 Mon Sep 17 00:00:00 2001 From: joelgerard Date: Fri, 19 Jun 2020 13:35:16 -0700 Subject: [PATCH 44/45] Rename text event => strucuture event. --- examples/README.md | 3 --- src/functions_framework/__init__.py | 10 +++++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/examples/README.md b/examples/README.md index 12b755a5..fc027bfe 100644 --- a/examples/README.md +++ b/examples/README.md @@ -3,6 +3,3 @@ * [`cloud_run_http`](./cloud_run_http/) - Deploying an HTTP function to [Cloud Run](http://cloud.google.com/run) with the Functions Framework * [`cloud_run_event`](./cloud_run_event/) - Deploying a [Google Cloud Functions Event](https://cloud.google.com/functions/docs/concepts/events-triggers#events) function to [Cloud Run](http://cloud.google.com/run) with the Functions Framework * [`cloud_run_cloudevents`](./cloud_run_cloudevents/) - Deploying a [CloudEvent](https://github.com/cloudevents/sdk-python) function to [Cloud Run](http://cloud.google.com/run) with the Functions Framework - - - diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index b44ca2ea..5f5422a7 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -44,7 +44,7 @@ class _EventType(enum.Enum): LEGACY = 1 CLOUDEVENT_BINARY = 2 - CLOUDEVENT_TEXT = 3 + CLOUDEVENT_STRUCTURED = 3 class _Event(object): @@ -103,7 +103,7 @@ def _run_binary_cloudevent(function, request, cloudevent_def): function(event) -def _run_text_cloudevent(function, request, cloudevent_def): +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) @@ -119,7 +119,7 @@ def _get_event_type(request): ): return _EventType.CLOUDEVENT_BINARY elif request.headers.get("Content-Type") == "application/cloudevents+json": - return _EventType.CLOUDEVENT_TEXT + return _EventType.CLOUDEVENT_STRUCTURED else: return _EventType.LEGACY @@ -146,8 +146,8 @@ 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_TEXT: - _run_text_cloudevent(function, request, cloudevent_def) + 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: From c138d201f3208dc9f2b1498202c86c5369b1c6a9 Mon Sep 17 00:00:00 2001 From: joelgerard Date: Wed, 15 Jul 2020 14:59:03 -0700 Subject: [PATCH 45/45] Conformance tests --- conformance_tests/Dockerfile | 21 ++++++++++++++++++++ conformance_tests/cloud_event_test.py | 23 ++++++++++++++++++++++ conformance_tests/legacy_event_test.py | 22 +++++++++++++++++++++ conformance_tests/run_conformance_tests.sh | 10 ++++++++++ conformance_tests/run_tests.sh | 0 5 files changed, 76 insertions(+) create mode 100644 conformance_tests/Dockerfile create mode 100644 conformance_tests/cloud_event_test.py create mode 100644 conformance_tests/legacy_event_test.py create mode 100755 conformance_tests/run_conformance_tests.sh create mode 100644 conformance_tests/run_tests.sh diff --git a/conformance_tests/Dockerfile b/conformance_tests/Dockerfile new file mode 100644 index 00000000..be4d721a --- /dev/null +++ b/conformance_tests/Dockerfile @@ -0,0 +1,21 @@ +# Builds the conformance tester and runs it. +# To build this file, ensure you are in the root directory, i.e. one level up from here, and run +# $ docker build -t conformance -f conformance_tests/Dockerfile . +FROM golang:1.14.5 AS builder +WORKDIR /src +RUN apt-get update && apt-get install -y git +RUN go version +RUN git clone https://github.com/GoogleCloudPlatform/functions-framework-conformance.git +RUN cd /src/functions-framework-conformance/client && go build + +FROM python:3.7-slim +ENV APP_HOME /app +WORKDIR $APP_HOME +COPY . . +COPY --from=builder /src/functions-framework-conformance/client/client . + +RUN pip install gunicorn +RUN pip install /app + + +CMD exec ./client -cmd "functions-framework --target=hello --signature-type=cloudevent --source=conformance_tests/cloud_event_test.py" -type=cloudevent diff --git a/conformance_tests/cloud_event_test.py b/conformance_tests/cloud_event_test.py new file mode 100644 index 00000000..99185be3 --- /dev/null +++ b/conformance_tests/cloud_event_test.py @@ -0,0 +1,23 @@ +# Copyright 2020 Google LLC +# +# Licensed 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 cloudevents.sdk import converters +from cloudevents.sdk import marshaller +from cloudevents.sdk.converters import structured + +def hello(cloudevent): + m = marshaller.NewHTTPMarshaller([structured.NewJSONHTTPCloudEventConverter()]) + headers, body = m.ToRequest(cloudevent, converters.TypeStructured, lambda x: x) + text_file = open("function_output.json", "w") + text_file.write(body.getvalue().decode("utf-8")) + text_file.close() diff --git a/conformance_tests/legacy_event_test.py b/conformance_tests/legacy_event_test.py new file mode 100644 index 00000000..c96f015d --- /dev/null +++ b/conformance_tests/legacy_event_test.py @@ -0,0 +1,22 @@ +# Copyright 2020 Google LLC +# +# Licensed 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 marshal + + +def hello(data, context): + bytes = marshal.dumps(data) + redata = marshal.loads(bytes) + text_file = open("/app/function_output.json", "w") + n = text_file.write(str(redata)) + text_file.close() diff --git a/conformance_tests/run_conformance_tests.sh b/conformance_tests/run_conformance_tests.sh new file mode 100755 index 00000000..d02a2ae8 --- /dev/null +++ b/conformance_tests/run_conformance_tests.sh @@ -0,0 +1,10 @@ +cd .. +dockerFile=conformance_tests/Dockerfile + +if [ ! -f "$dockerFile" ]; then + echo "$dockerFile not in expected location" + exit 1 +fi + +docker build -t conformance -f conformance_tests/Dockerfile . +docker run -p 8080:8080 -t conformance diff --git a/conformance_tests/run_tests.sh b/conformance_tests/run_tests.sh new file mode 100644 index 00000000..e69de29b