Skip to content

Commit c60c860

Browse files
committed
Merge branch 'main' of https://github.com/invertase/firebase-functions-python2 into firestore
2 parents d9c2502 + 08586c8 commit c60c860

File tree

6 files changed

+453
-16
lines changed

6 files changed

+453
-16
lines changed

src/firebase_functions/options.py

+36-14
Original file line numberDiff line numberDiff line change
@@ -437,34 +437,30 @@ def _required_apis(self) -> list[_manifest.ManifestRequiredApi]:
437437
]
438438

439439

440+
# TODO refactor Storage & Database options to use this base class.
440441
@_dataclasses.dataclass(frozen=True, kw_only=True)
441-
class PubSubOptions(RuntimeOptions):
442+
class EventHandlerOptions(RuntimeOptions):
442443
"""
443-
Options specific to Pub/Sub function types.
444+
Options specific to any event handling Cloud function.
444445
Internal use only.
445446
"""
446447

447-
retry: bool | None = None
448+
retry: bool | Expression[bool] | _util.Sentinel | None = None
448449
"""
449450
Whether failed executions should be delivered again.
450451
"""
451452

452-
topic: str
453-
"""
454-
The Pub/Sub topic to watch for message events.
455-
"""
456-
457453
def _endpoint(
458454
self,
459455
**kwargs,
460456
) -> _manifest.ManifestEndpoint:
461-
event_filters: _typing.Any = {
462-
"topic": self.topic,
463-
}
457+
assert kwargs["event_filters"] is not None
458+
assert kwargs["event_type"] is not None
459+
464460
event_trigger = _manifest.EventTrigger(
465-
eventType="google.cloud.pubsub.topic.v1.messagePublished",
466-
retry=False,
467-
eventFilters=event_filters,
461+
eventType=kwargs["event_type"],
462+
retry=self.retry if self.retry is not None else False,
463+
eventFilters=kwargs["event_filters"],
468464
)
469465

470466
kwargs_merged = {
@@ -476,6 +472,32 @@ def _endpoint(
476472
**_typing.cast(_typing.Dict, kwargs_merged))
477473

478474

475+
@_dataclasses.dataclass(frozen=True, kw_only=True)
476+
class PubSubOptions(EventHandlerOptions):
477+
"""
478+
Options specific to Pub/Sub function types.
479+
Internal use only.
480+
"""
481+
482+
topic: str
483+
"""
484+
The Pub/Sub topic to watch for message events.
485+
"""
486+
487+
def _endpoint(
488+
self,
489+
**kwargs,
490+
) -> _manifest.ManifestEndpoint:
491+
event_filters: _typing.Any = {
492+
"topic": self.topic,
493+
}
494+
event_type = "google.cloud.pubsub.topic.v1.messagePublished"
495+
return _manifest.ManifestEndpoint(**_typing.cast(
496+
_typing.Dict,
497+
_dataclasses.asdict(super()._endpoint(
498+
**kwargs, event_filters=event_filters, event_type=event_type))))
499+
500+
479501
@_dataclasses.dataclass(frozen=True, kw_only=True)
480502
class StorageOptions(RuntimeOptions):
481503
"""

src/firebase_functions/private/manifest.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ class EventTrigger(_typing.TypedDict):
6161
str, str | _params.Expression[str]]]
6262
channel: _typing_extensions.NotRequired[str]
6363
eventType: _typing_extensions.Required[str]
64-
retry: _typing_extensions.Required[bool | _params.Expression[bool]]
64+
retry: _typing_extensions.Required[bool | _params.Expression[bool] |
65+
_util.Sentinel]
6566

6667

6768
class RetryConfig(_typing.TypedDict):

src/firebase_functions/pubsub_fn.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ def example(event: CloudEvent[MessagePublishedData[object]]) -> None:
171171
:type \\*\\*kwargs: as :exc:`firebase_functions.options.PubSubOptions`
172172
:rtype: :exc:`typing.Callable`
173173
\\[ \\[ :exc:`firebase_functions.core.CloudEvent` \\[
174-
:exc:`firebase_functions.pubsub.MessagePublishedData` \\[
174+
:exc:`firebase_functions.pubsub_fn.MessagePublishedData` \\[
175175
:exc:`typing.Any` \\] \\] \\], `None` \\]
176176
A function that takes a CloudEvent and returns None.
177177
"""
+228
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
# Copyright 2022 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
# pylint: disable=protected-access
15+
"""
16+
Cloud functions to handle Remote Config events.
17+
"""
18+
import dataclasses as _dataclasses
19+
import functools as _functools
20+
import datetime as _dt
21+
import typing as _typing
22+
import cloudevents.http as _ce
23+
import enum as _enum
24+
25+
import firebase_functions.private.util as _util
26+
27+
from firebase_functions.core import CloudEvent
28+
from firebase_functions.options import EventHandlerOptions
29+
30+
31+
@_dataclasses.dataclass(frozen=True)
32+
class ConfigUser:
33+
"""
34+
All the fields associated with the person/service account that wrote a Remote Config template.
35+
"""
36+
37+
name: str
38+
"""
39+
Display name.
40+
"""
41+
42+
email: str
43+
"""
44+
Email address.
45+
"""
46+
47+
image_url: str
48+
"""
49+
Image URL.
50+
"""
51+
52+
53+
class ConfigUpdateOrigin(str, _enum.Enum):
54+
"""
55+
Where the Remote Config update action originated.
56+
"""
57+
58+
REMOTE_CONFIG_UPDATE_ORIGIN_UNSPECIFIED = "REMOTE_CONFIG_UPDATE_ORIGIN_UNSPECIFIED"
59+
"""
60+
Catch-all for unrecognized values.
61+
"""
62+
63+
CONSOLE = "CONSOLE"
64+
"""
65+
The update came from the Firebase UI.
66+
"""
67+
68+
REST_API = "REST_API"
69+
"""
70+
The update came from the Remote Config REST API.
71+
"""
72+
73+
ADMIN_SDK_NODE = "ADMIN_SDK_NODE"
74+
"""
75+
The update came from the Firebase Admin Node SDK.
76+
"""
77+
78+
79+
class ConfigUpdateType(str, _enum.Enum):
80+
"""
81+
What type of update was associated with the Remote Config template version.
82+
"""
83+
84+
REMOTE_CONFIG_UPDATE_TYPE_UNSPECIFIED = "REMOTE_CONFIG_UPDATE_TYPE_UNSPECIFIED"
85+
"""
86+
Catch-all for unrecognized enum values.
87+
"""
88+
89+
INCREMENTAL_UPDATE = "INCREMENTAL_UPDATE"
90+
"""
91+
A regular incremental update.
92+
"""
93+
94+
FORCED_UPDATE = "FORCED_UPDATE"
95+
"""
96+
A forced update. The ETag was specified as "*" in an UpdateRemoteConfigRequest
97+
request or the "Force Update" button was pressed on the console.
98+
"""
99+
100+
ROLLBACK = "ROLLBACK"
101+
"""
102+
A rollback to a previous Remote Config template.
103+
"""
104+
105+
106+
@_dataclasses.dataclass(frozen=True)
107+
class ConfigUpdateData:
108+
"""
109+
The data within Firebase Remote Config update events.
110+
"""
111+
112+
version_number: int
113+
"""
114+
The version number of the version's corresponding Remote Config template.
115+
"""
116+
117+
update_time: _dt.datetime
118+
"""
119+
When the Remote Config template was written to the Remote Config server.
120+
"""
121+
122+
update_user: ConfigUser
123+
"""
124+
Aggregation of all metadata fields about the account that performed the update.
125+
"""
126+
127+
description: str
128+
"""
129+
The user-provided description of the corresponding Remote Config template.
130+
"""
131+
132+
update_origin: ConfigUpdateOrigin
133+
"""
134+
Where the update action originated.
135+
"""
136+
137+
update_type: ConfigUpdateType
138+
"""
139+
What type of update was made.
140+
"""
141+
142+
rollback_source: int | None = None
143+
"""
144+
Only present if this version is the result of a rollback, and will be
145+
the version number of the Remote Config template that was rolled-back to.
146+
"""
147+
148+
149+
_E1 = CloudEvent[ConfigUpdateData]
150+
_C1 = _typing.Callable[[_E1], None]
151+
152+
153+
def _config_handler(func: _C1, raw: _ce.CloudEvent) -> None:
154+
event_attributes = raw._get_attributes()
155+
event_data: _typing.Any = raw.get_data()
156+
event_dict = {**event_data, **event_attributes}
157+
158+
config_data = ConfigUpdateData(
159+
version_number=event_data["versionNumber"],
160+
update_time=_dt.datetime.strptime(event_data["updateTime"],
161+
"%Y-%m-%dT%H:%M:%S.%f%z"),
162+
update_user=ConfigUser(
163+
name=event_data["updateUser"]["name"],
164+
email=event_data["updateUser"]["email"],
165+
image_url=event_data["updateUser"]["imageUrl"],
166+
),
167+
description=event_data["description"],
168+
update_origin=ConfigUpdateOrigin(event_data["updateOrigin"]),
169+
update_type=ConfigUpdateType(event_data["updateType"]),
170+
rollback_source=event_data.get("rollbackSource", None),
171+
)
172+
173+
event: CloudEvent[ConfigUpdateData] = CloudEvent(
174+
data=config_data,
175+
id=event_dict["id"],
176+
source=event_dict["source"],
177+
specversion=event_dict["specversion"],
178+
subject=event_dict["subject"] if "subject" in event_dict else None,
179+
time=_dt.datetime.strptime(
180+
event_dict["time"],
181+
"%Y-%m-%dT%H:%M:%S.%f%z",
182+
),
183+
type=event_dict["type"],
184+
)
185+
186+
func(event)
187+
188+
189+
@_util.copy_func_kwargs(EventHandlerOptions)
190+
def on_config_updated(**kwargs) -> _typing.Callable[[_C1], _C1]:
191+
"""
192+
Event handler which triggers when data is updated in a Remote Config.
193+
194+
Example:
195+
196+
.. code-block:: python
197+
198+
@on_config_updated()
199+
def example(event: CloudEvent[ConfigUpdateData]) -> None:
200+
pass
201+
202+
:param \\*\\*kwargs: Pub/Sub options.
203+
:type \\*\\*kwargs: as :exc:`firebase_functions.options.EventHandlerOptions`
204+
:rtype: :exc:`typing.Callable`
205+
\\[ \\[ :exc:`firebase_functions.core.CloudEvent` \\[
206+
:exc:`firebase_functions.remote_config_fn.ConfigUpdateData` \\[
207+
:exc:`typing.Any` \\] \\] \\], `None` \\]
208+
A function that takes a CloudEvent and returns None.
209+
"""
210+
options = EventHandlerOptions(**kwargs)
211+
212+
def on_config_updated_inner_decorator(func: _C1):
213+
214+
@_functools.wraps(func)
215+
def on_config_updated_wrapped(raw: _ce.CloudEvent):
216+
return _config_handler(func, raw)
217+
218+
_util.set_func_endpoint_attr(
219+
on_config_updated_wrapped,
220+
options._endpoint(
221+
func_name=func.__name__,
222+
event_filters={},
223+
event_type="google.firebase.remoteconfig.remoteConfig.v1.updated"
224+
),
225+
)
226+
return on_config_updated_wrapped
227+
228+
return on_config_updated_inner_decorator

0 commit comments

Comments
 (0)