Skip to content

Commit 81e2ef3

Browse files
committed
Merge branch 'main' of https://github.com/invertase/firebase-functions-python2 into remote-config
2 parents 8beb6bf + db81f7b commit 81e2ef3

File tree

15 files changed

+623
-15
lines changed

15 files changed

+623
-15
lines changed

mypy.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ exclude = build|setup.py|venv
33

44
# cloudevents package has no types
55
ignore_missing_imports = True
6+
enable_incomplete_feature = Unpack
67

78
[mypy-yaml.*]
89
ignore_missing_imports = True

samples/basic_tasks/.firebaserc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"projects": {
3+
"default": "python-functions-testing"
4+
}
5+
}

samples/basic_tasks/.gitignore

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
firebase-debug.log*
8+
firebase-debug.*.log*
9+
10+
# Firebase cache
11+
.firebase/
12+
13+
# Firebase config
14+
15+
# Uncomment this if you'd like others to create their own Firebase project.
16+
# For a team working on the same Firebase project(s), it is recommended to leave
17+
# it commented so all members can deploy to the same project(s) in .firebaserc.
18+
# .firebaserc
19+
20+
# Runtime data
21+
pids
22+
*.pid
23+
*.seed
24+
*.pid.lock
25+
26+
# Directory for instrumented libs generated by jscoverage/JSCover
27+
lib-cov
28+
29+
# Coverage directory used by tools like istanbul
30+
coverage
31+
32+
# nyc test coverage
33+
.nyc_output
34+
35+
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
36+
.grunt
37+
38+
# Bower dependency directory (https://bower.io/)
39+
bower_components
40+
41+
# node-waf configuration
42+
.lock-wscript
43+
44+
# Compiled binary addons (http://nodejs.org/api/addons.html)
45+
build/Release
46+
47+
# Dependency directories
48+
node_modules/
49+
50+
# Optional npm cache directory
51+
.npm
52+
53+
# Optional eslint cache
54+
.eslintcache
55+
56+
# Optional REPL history
57+
.node_repl_history
58+
59+
# Output of 'npm pack'
60+
*.tgz
61+
62+
# Yarn Integrity file
63+
.yarn-integrity
64+
65+
# dotenv environment variables file
66+
.env

samples/basic_tasks/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Required to avoid a 'duplicate modules' mypy error
2+
# in monorepos that have multiple main.py files.
3+
# https://github.com/python/mypy/issues/4008

samples/basic_tasks/firebase.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"functions": [
3+
{
4+
"source": "functions",
5+
"codebase": "default",
6+
"ignore": [
7+
"venv"
8+
]
9+
}
10+
]
11+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# pyenv
2+
.python-version
3+
4+
# Installer logs
5+
pip-log.txt
6+
pip-delete-this-directory.txt
7+
8+
# Environments
9+
.env
10+
.venv
11+
venv/
12+
venv.bak/
13+
__pycache__

samples/basic_tasks/functions/main.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Firebase Cloud Functions for Tasks."""
2+
3+
import datetime
4+
import json
5+
6+
from firebase_admin import initialize_app
7+
from google.cloud import tasks_v2
8+
from firebase_functions import tasks_fn, https_fn
9+
from firebase_functions.options import SupportedRegion, RetryConfig, RateLimits
10+
11+
app = initialize_app()
12+
13+
14+
# Once this function is deployed, a Task Queue will be created with the name
15+
# `on_task_dispatched_example`. You can then enqueue tasks to this queue by
16+
# calling the `enqueue_task` function.
17+
@tasks_fn.on_task_dispatched(
18+
retry_config=RetryConfig(max_attempts=5),
19+
rate_limits=RateLimits(max_concurrent_dispatches=10),
20+
region=SupportedRegion.US_CENTRAL1,
21+
)
22+
def ontaskdispatchedexample(req: tasks_fn.CallableRequest):
23+
"""
24+
The endpoint which will be executed by the enqueued task.
25+
"""
26+
print(req.data)
27+
28+
29+
# To enqueue a task, you can use the following function.
30+
# e.g.
31+
# curl -X POST -H "Content-Type: application/json" \
32+
# -d '{"data": "Hello World!"}' \
33+
# https://enqueue-task-<projectHash>-<region>.a.run.app\
34+
@https_fn.on_request()
35+
def enqueuetask(req: https_fn.Request) -> https_fn.Response:
36+
"""
37+
Enqueues a task to the queue `on_task_dispatched_function`.
38+
"""
39+
client = tasks_v2.CloudTasksClient()
40+
41+
# The URL of the `on_task_dispatched_function` function.
42+
# Must be set to the URL of the deployed function.
43+
44+
url = req.json.get("url") if req.json else None
45+
46+
body = {"data": req.json}
47+
48+
task: tasks_v2.Task = tasks_v2.Task(
49+
**{
50+
"http_request": {
51+
"http_method": tasks_v2.HttpMethod.POST,
52+
"url": url,
53+
"headers": {
54+
"Content-type": "application/json"
55+
},
56+
"body": json.dumps(body).encode(),
57+
},
58+
"schedule_time":
59+
datetime.datetime.utcnow() + datetime.timedelta(minutes=1),
60+
})
61+
62+
parent = client.queue_path(
63+
app.project_id,
64+
SupportedRegion.US_CENTRAL1,
65+
"ontaskdispatchedexample2",
66+
)
67+
68+
client.create_task(request={"parent": parent, "task": task})
69+
return https_fn.Response("Task enqueued.")
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Not published yet,
2+
# firebase-functions-python >= 0.0.1
3+
# so we use a relative path during development:
4+
./../../../
5+
# Or switch to git ref for deployment testing:
6+
# git+https://github.com/firebase/firebase-functions-python.git@main#egg=firebase-functions
7+
8+
firebase-admin >= 6.0.1

src/firebase_functions/options.py

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,62 @@ class SupportedRegion(str, _enum.Enum):
9999
US_WEST1 = "us-west1"
100100

101101

102+
@_dataclasses.dataclass(frozen=True)
103+
class RateLimits():
104+
"""
105+
How congestion control should be applied to the function.
106+
"""
107+
max_concurrent_dispatches: int | Expression[
108+
int] | _util.Sentinel | None = None
109+
"""
110+
The maximum number of requests that can be outstanding at a time.
111+
If left unspecified, will default to 1000.
112+
"""
113+
114+
max_dispatches_per_second: int | Expression[
115+
int] | _util.Sentinel | None = None
116+
"""
117+
The maximum number of requests that can be invoked per second.
118+
If left unspecified, will default to 500.
119+
"""
120+
121+
122+
@_dataclasses.dataclass(frozen=True)
123+
class RetryConfig():
124+
"""
125+
How a task should be retried in the event of a non-2xx return.
126+
"""
127+
128+
max_attempts: int | Expression[int] | _util.Sentinel | None = None
129+
"""
130+
The maximum number of times a request should be attempted.
131+
If left unspecified, will default to 3.
132+
"""
133+
134+
max_retry_seconds: int | Expression[int] | _util.Sentinel | None = None
135+
"""
136+
The maximum amount of time for retrying failed task.
137+
If left unspecified will retry indefinitely.
138+
"""
139+
140+
max_backoff_seconds: int | Expression[int] | _util.Sentinel | None = None
141+
"""
142+
The maximum amount of time to wait between attempts.
143+
If left unspecified will default to 1hr.
144+
"""
145+
146+
max_doublings: int | Expression[int] | _util.Sentinel | None = None
147+
"""
148+
The maximum number of times to double the backoff between
149+
retries. If left unspecified will default to 16.
150+
"""
151+
152+
min_backoff_seconds: int | Expression[int] | _util.Sentinel | None = None
153+
"""
154+
The minimum time to wait between attempts.
155+
"""
156+
157+
102158
@_dataclasses.dataclass(frozen=True, kw_only=True)
103159
class RuntimeOptions:
104160
"""
@@ -126,7 +182,7 @@ class RuntimeOptions:
126182
The minimum timeout for a gen 2 function is 1s. The maximum timeout for a
127183
function depends on the type of function: Event handling functions have a
128184
maximum timeout of 540s (9 minutes). HTTPS and callable functions have a
129-
maximum timeout of 36,00s (1 hour). Task queue functions have a maximum
185+
maximum timeout of 3,600s (1 hour). Task queue functions have a maximum
130186
timeout of 1,800s (30 minutes)
131187
"""
132188

@@ -318,6 +374,69 @@ def convert_secret(
318374
return endpoint
319375

320376

377+
@_dataclasses.dataclass(frozen=True, kw_only=True)
378+
class TaskQueueOptions(RuntimeOptions):
379+
"""
380+
Options specific to Tasks function types.
381+
"""
382+
383+
retry_config: RetryConfig | None = None
384+
"""
385+
How a task should be retried in the event of a non-2xx return.
386+
"""
387+
388+
rate_limits: RateLimits | None = None
389+
"""
390+
How congestion control should be applied to the function.
391+
"""
392+
393+
invoker: str | list[str] | _typing.Literal["private"] | None = None
394+
"""
395+
Who can enqueue tasks for this function.
396+
397+
Note:
398+
If left unspecified, only service accounts which have
399+
`roles/cloudtasks.enqueuer` and `roles/cloudfunctions.invoker`
400+
will have permissions.
401+
"""
402+
403+
def _endpoint(
404+
self,
405+
**kwargs,
406+
) -> _manifest.ManifestEndpoint:
407+
rate_limits: _manifest.RateLimits | None = _manifest.RateLimits(
408+
maxConcurrentDispatches=self.rate_limits.max_concurrent_dispatches,
409+
maxDispatchesPerSecond=self.rate_limits.max_dispatches_per_second,
410+
) if self.rate_limits is not None else None
411+
412+
retry_config: _manifest.RetryConfig | None = _manifest.RetryConfig(
413+
maxAttempts=self.retry_config.max_attempts,
414+
maxRetrySeconds=self.retry_config.max_retry_seconds,
415+
maxBackoffSeconds=self.retry_config.max_backoff_seconds,
416+
maxDoublings=self.retry_config.max_doublings,
417+
minBackoffSeconds=self.retry_config.min_backoff_seconds,
418+
) if self.retry_config is not None else None
419+
420+
kwargs_merged = {
421+
**_dataclasses.asdict(super()._endpoint(**kwargs)),
422+
"taskQueueTrigger":
423+
_manifest.TaskQueueTrigger(
424+
rateLimits=rate_limits,
425+
retryConfig=retry_config,
426+
),
427+
}
428+
return _manifest.ManifestEndpoint(
429+
**_typing.cast(_typing.Dict, kwargs_merged))
430+
431+
def _required_apis(self) -> list[_manifest.ManifestRequiredApi]:
432+
return [
433+
_manifest.ManifestRequiredApi(
434+
api="cloudtasks.googleapis.com",
435+
reason="Needed for task queue functions",
436+
)
437+
]
438+
439+
321440
# TODO refactor Storage & Database options to use this base class.
322441
@_dataclasses.dataclass(frozen=True, kw_only=True)
323442
class EventHandlerOptions(RuntimeOptions):

src/firebase_functions/private/manifest.py

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,38 @@ class EventTrigger(_typing.TypedDict):
6666

6767

6868
class RetryConfig(_typing.TypedDict):
69-
retryCount: _typing_extensions.NotRequired[int | _params.Expression[int]]
70-
maxRetrySeconds: _typing_extensions.NotRequired[str |
71-
_params.Expression[str]]
72-
minBackoffSeconds: _typing_extensions.NotRequired[str |
73-
_params.Expression[str]]
74-
maxBackoffSeconds: _typing_extensions.NotRequired[str |
75-
_params.Expression[str]]
76-
maxDoublings: _typing_extensions.NotRequired[int | _params.Expression[int]]
69+
"""
70+
Retry configuration for a endpoint.
71+
"""
72+
maxAttempts: _typing_extensions.NotRequired[int | _params.Expression[int] |
73+
_util.Sentinel | None]
74+
maxRetrySeconds: _typing_extensions.NotRequired[int |
75+
_params.Expression[int] |
76+
_util.Sentinel | None]
77+
maxBackoffSeconds: _typing_extensions.NotRequired[int |
78+
_params.Expression[int] |
79+
_util.Sentinel | None]
80+
maxDoublings: _typing_extensions.NotRequired[int | _params.Expression[int] |
81+
_util.Sentinel | None]
82+
minBackoffSeconds: _typing_extensions.NotRequired[int |
83+
_params.Expression[int] |
84+
_util.Sentinel | None]
85+
86+
87+
class RateLimits(_typing.TypedDict):
88+
maxConcurrentDispatches: int | _params.Expression[
89+
int] | _util.Sentinel | None
90+
91+
maxDispatchesPerSecond: int | _params.Expression[int] | _util.Sentinel | None
92+
93+
94+
class TaskQueueTrigger(_typing.TypedDict):
95+
"""
96+
Trigger definitions for RPCs servers using the HTTP protocol defined at
97+
https://firebase.google.com/docs/functions/callable-reference
98+
"""
99+
retryConfig: RetryConfig | None
100+
rateLimits: RateLimits | None
77101

78102

79103
class ScheduleTrigger(_typing.TypedDict):
@@ -117,6 +141,7 @@ class ManifestEndpoint:
117141
eventTrigger: EventTrigger | None = None
118142
scheduleTrigger: ScheduleTrigger | None = None
119143
blockingTrigger: BlockingTrigger | None = None
144+
taskQueueTrigger: TaskQueueTrigger | None = None
120145

121146

122147
class ManifestRequiredApi(_typing.TypedDict):

0 commit comments

Comments
 (0)