Skip to content

feat: eventarc function support #61

New issue

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

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

Already on GitHub? Sign in to your account

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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/generate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ TITLE="Firebase Python SDK for Cloud Functions"
PY_MODULES='firebase_functions
firebase_functions.core
firebase_functions.db_fn
firebase_functions.eventarc_fn
firebase_functions.https_fn
firebase_functions.options
firebase_functions.params
Expand Down
5 changes: 5 additions & 0 deletions samples/basic_eventarc/.firebaserc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"projects": {
"default": "python-functions-testing"
}
}
66 changes: 66 additions & 0 deletions samples/basic_eventarc/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
firebase-debug.log*
firebase-debug.*.log*

# Firebase cache
.firebase/

# Firebase config

# Uncomment this if you'd like others to create their own Firebase project.
# For a team working on the same Firebase project(s), it is recommended to leave
# it commented so all members can deploy to the same project(s) in .firebaserc.
# .firebaserc

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env
3 changes: 3 additions & 0 deletions samples/basic_eventarc/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Required to avoid a 'duplicate modules' mypy error
# in monorepos that have multiple main.py files.
# https://github.com/python/mypy/issues/4008
11 changes: 11 additions & 0 deletions samples/basic_eventarc/firebase.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"functions": [
{
"source": "functions",
"codebase": "default",
"ignore": [
"venv"
]
}
]
}
13 changes: 13 additions & 0 deletions samples/basic_eventarc/functions/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# pyenv
.python-version

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Environments
.env
.venv
venv/
venv.bak/
__pycache__
12 changes: 12 additions & 0 deletions samples/basic_eventarc/functions/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Firebase Cloud Functions for Eventarc triggers example."""
from firebase_functions import eventarc_fn


@eventarc_fn.on_custom_event_published(
event_type="firebase.extensions.storage-resize-images.v1.complete",)
def onimageresize(event: eventarc_fn.CloudEvent) -> None:
"""
Handle image resize events from the Firebase Storage Resize Images extension.
https://extensions.dev/extensions/firebase/storage-resize-images
"""
print("Received image resize completed event", event)
8 changes: 8 additions & 0 deletions samples/basic_eventarc/functions/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Not published yet,
# firebase-functions-python >= 0.0.1
# so we use a relative path during development:
./../../../
# Or switch to git ref for deployment testing:
# git+https://github.com/firebase/firebase-functions-python.git@main#egg=firebase-functions

firebase-admin >= 6.0.1
88 changes: 88 additions & 0 deletions src/firebase_functions/eventarc_fn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Copyright 2022 Google Inc.
#
# 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.
"""Cloud functions to handle Eventarc events."""

# pylint: disable=protected-access
import typing as _typing
import functools as _functools
import datetime as _dt
import cloudevents.http as _ce

import firebase_functions.options as _options
import firebase_functions.private.util as _util
from firebase_functions.core import CloudEvent


@_util.copy_func_kwargs(_options.EventarcTriggerOptions)
def on_custom_event_published(
**kwargs
) -> _typing.Callable[[_typing.Callable[[CloudEvent], None]], _typing.Callable[
[CloudEvent], None]]:
"""
Creates a handler for events published on the default event eventarc channel.

Example:

.. code-block:: python

from firebase_functions import eventarc_fn

@eventarc_fn.on_custom_event_published(
event_type="firebase.extensions.storage-resize-images.v1.complete",
)
def onimageresize(event: eventarc_fn.CloudEvent) -> None:
pass

:param \\*\\*kwargs: Options.
:type \\*\\*kwargs: as :exc:`firebase_functions.options.EventarcTriggerOptions`
:rtype: :exc:`typing.Callable`
\\[ \\[ :exc:`firebase_functions.core.CloudEvent` \\], `None` \\]
A function that takes a CloudEvent and returns None.
"""
options = _options.EventarcTriggerOptions(**kwargs)

def on_custom_event_published_decorator(func: _typing.Callable[[CloudEvent],
None]):

@_functools.wraps(func)
def on_custom_event_published_wrapped(raw: _ce.CloudEvent):
event_attributes = raw._get_attributes()
event_data: _typing.Any = raw.get_data()
event_dict = {**event_data, **event_attributes}
event: CloudEvent = CloudEvent(
data=event_data,
id=event_dict["id"],
source=event_dict["source"],
specversion=event_dict["specversion"],
subject=event_dict["subject"]
if "subject" in event_dict else None,
time=_dt.datetime.strptime(
event_dict["time"],
"%Y-%m-%dT%H:%M:%S.%f%z",
),
type=event_dict["type"],
)
func(event)

_util.set_func_endpoint_attr(
on_custom_event_published_wrapped,
options._endpoint(func_name=func.__name__),
)
_util.set_required_apis_attr(
on_custom_event_published_wrapped,
options._required_apis(),
)
return on_custom_event_published_wrapped

return on_custom_event_published_decorator
60 changes: 60 additions & 0 deletions src/firebase_functions/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,66 @@ def _endpoint(
**kwargs, event_filters=event_filters, event_type=event_type))))


@_dataclasses.dataclass(frozen=True, kw_only=True)
class EventarcTriggerOptions(EventHandlerOptions):
"""
Options that can be set on an Eventarc trigger.
Internal use only.
"""

event_type: str
"""
Type of the event to trigger on.
"""

channel: str | None = None
"""
ID of the channel. Can be either:
* fully qualified channel resource name:
`projects/{project}/locations/{location}/channels/{channel-id}`
* partial resource name with location and channel ID, in which case
the runtime project ID of the function will be used:
`locations/{location}/channels/{channel-id}`
* partial channel ID, in which case the runtime project ID of the
function and `us-central1` as location will be used:
`{channel-id}`

If not specified, the default Firebase channel will be used:
`projects/{project}/locations/us-central1/channels/firebase`
"""

filters: dict[str, str] | None = None
"""
Eventarc event exact match filter.
"""

def _endpoint(
self,
**kwargs,
) -> _manifest.ManifestEndpoint:
event_filters = {} if self.filters is None else self.filters
endpoint = _manifest.ManifestEndpoint(**_typing.cast(
_typing.Dict,
_dataclasses.asdict(super()._endpoint(
**kwargs,
event_filters=event_filters,
event_type=self.event_type,
))))
assert endpoint.eventTrigger is not None
channel = (self.channel if self.channel is not None else
"locations/us-central1/channels/firebase")
endpoint.eventTrigger["channel"] = channel
return endpoint

def _required_apis(self) -> list[_manifest.ManifestRequiredApi]:
return [
_manifest.ManifestRequiredApi(
api="eventarcpublishing.googleapis.com",
reason="Needed for custom event functions",
)
]


@_dataclasses.dataclass(frozen=True, kw_only=True)
class ScheduleOptions(RuntimeOptions):
"""
Expand Down
85 changes: 85 additions & 0 deletions tests/test_eventarc_fn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Copyright 2022 Google Inc.
#
# 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.
"""Eventarc trigger function tests."""
import unittest
from unittest.mock import Mock
from cloudevents.http import CloudEvent as _CloudEvent
from firebase_functions.core import CloudEvent
from firebase_functions.eventarc_fn import on_custom_event_published


class TestEventarcFn(unittest.TestCase):
"""
Test Eventarc trigger functions.
"""

def test_on_custom_event_published_decorator(self):
"""
Tests the on_custom_event_published decorator functionality by checking
that the __firebase_endpoint__ attribute is set properly.
"""
func = Mock(__name__="example_func")

decorated_func = on_custom_event_published(
event_type="firebase.extensions.storage-resize-images.v1.complete",
)(func)

endpoint = getattr(decorated_func, "__firebase_endpoint__")
self.assertIsNotNone(endpoint)
self.assertIsNotNone(endpoint.eventTrigger)
self.assertEqual(
endpoint.eventTrigger["eventType"],
"firebase.extensions.storage-resize-images.v1.complete",
)

def test_on_custom_event_published_wrapped(self):
"""
Tests the wrapped function created by the on_custom_event_published
decorator, ensuring that it correctly processes the raw event and calls
the user-provided function with a properly formatted CloudEvent instance.
"""
func = Mock(__name__="example_func")
raw_event = _CloudEvent(
attributes={
"specversion": "1.0",
"type": "firebase.extensions.storage-resize-images.v1.complete",
"source": "https://example.com/testevent",
"id": "1234567890",
"subject": "test_subject",
"time": "2023-03-11T13:25:37.403Z",
},
data={
"some_key": "some_value",
},
)

decorated_func = on_custom_event_published(
event_type="firebase.extensions.storage-resize-images.v1.complete",
)(func)

decorated_func(raw_event)

func.assert_called_once()

event_arg = func.call_args.args[0]
self.assertIsInstance(event_arg, CloudEvent)
self.assertEqual(event_arg.data, {"some_key": "some_value"})
self.assertEqual(event_arg.id, "1234567890")
self.assertEqual(event_arg.source, "https://example.com/testevent")
self.assertEqual(event_arg.specversion, "1.0")
self.assertEqual(event_arg.subject, "test_subject")
self.assertEqual(
event_arg.type,
"firebase.extensions.storage-resize-images.v1.complete",
)