Skip to content

Commit 0b35bed

Browse files
authored
Merge branch 'main' into @invertase/feat-support-exception-logging
2 parents 6d7e2ab + 08c0fd4 commit 0b35bed

8 files changed

Lines changed: 153 additions & 32 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ on:
66
branches:
77
- main
88

9+
permissions:
10+
contents: read
11+
912
jobs:
1013
test:
1114
runs-on: ubuntu-latest
@@ -15,9 +18,11 @@ jobs:
1518
python: ["3.10", "3.12"]
1619

1720
steps:
18-
- uses: actions/checkout@v3
21+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
22+
with:
23+
persist-credentials: false
1924
- name: Install uv
20-
uses: astral-sh/setup-uv@v3
25+
uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3.2.4
2126
with:
2227
enable-cache: true
2328
- name: Set up Python ${{ matrix.python }}
@@ -35,9 +40,11 @@ jobs:
3540
lint:
3641
runs-on: ubuntu-latest
3742
steps:
38-
- uses: actions/checkout@v3
43+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
44+
with:
45+
persist-credentials: false
3946
- name: Install uv
40-
uses: astral-sh/setup-uv@v3
47+
uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3.2.4
4148
with:
4249
enable-cache: true
4350
- name: Set up Python
@@ -55,9 +62,11 @@ jobs:
5562
docs:
5663
runs-on: ubuntu-latest
5764
steps:
58-
- uses: actions/checkout@v3
65+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
66+
with:
67+
persist-credentials: false
5968
- name: Install uv
60-
uses: astral-sh/setup-uv@v3
69+
uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3.2.4
6170
with:
6271
enable-cache: true
6372
- name: Set up Python
@@ -69,7 +78,7 @@ jobs:
6978
run: |
7079
mkdir ./docs/build
7180
uv run ./docs/generate.sh --out=./docs/build/ --pypath=src/
72-
- uses: actions/upload-artifact@v4
81+
- uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
7382
name: Upload Docs Preview
7483
with:
7584
name: reference-docs
@@ -78,9 +87,11 @@ jobs:
7887
format:
7988
runs-on: ubuntu-latest
8089
steps:
81-
- uses: actions/checkout@v3
90+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
91+
with:
92+
persist-credentials: false
8293
- name: Install uv
83-
uses: astral-sh/setup-uv@v3
94+
uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3.2.4
8495
with:
8596
enable-cache: true
8697
- name: Set up Python
@@ -90,4 +101,4 @@ jobs:
90101
uv sync
91102
- name: Check Formatting with ruff
92103
run: |
93-
uv run ruff format --check .
104+
uv run ruff format --check .

.github/workflows/release.yaml

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ on:
2020
# Allow workflow to be triggered manually.
2121
workflow_dispatch:
2222

23+
permissions:
24+
contents: read
25+
2326
jobs:
2427
stage_release:
2528
# To publish a release, merge the release PR with the label 'release:publish'.
@@ -34,15 +37,17 @@ jobs:
3437

3538
steps:
3639
- name: Checkout source for staging
37-
uses: actions/checkout@v3
40+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
41+
with:
42+
persist-credentials: false
3843

3944
- name: Set up Python
40-
uses: actions/setup-python@v4
45+
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
4146
with:
4247
python-version: '3.10'
4348

4449
- name: Install uv
45-
uses: astral-sh/setup-uv@v3
50+
uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3.2.4
4651
with:
4752
enable-cache: true
4853

@@ -62,7 +67,7 @@ jobs:
6267
# Attach the packaged artifacts to the workflow output. These can be manually
6368
# downloaded for later inspection if necessary.
6469
- name: Archive artifacts
65-
uses: actions/upload-artifact@v4
70+
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
6671
with:
6772
name: dist
6873
path: dist/
@@ -92,11 +97,13 @@ jobs:
9297

9398
steps:
9499
- name: Checkout source for publish
95-
uses: actions/checkout@v4
100+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
101+
with:
102+
persist-credentials: false
96103

97104
# Download the artifacts created by the stage_release job.
98105
- name: Download release candidates
99-
uses: actions/download-artifact@v4.1.7
106+
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
100107
with:
101108
name: dist
102109
path: dist
@@ -111,7 +118,7 @@ jobs:
111118
- name: Create release tag
112119
# Skip creating a release tag for prereleases
113120
if: (!contains(github.event.pull_request.labels.*.name, 'release:prerelease'))
114-
uses: fleskesvor/create-release@1a72e235c178bf2ae6c51a8ae36febc24568c5fe
121+
uses: fleskesvor/create-release@1a72e235c178bf2ae6c51a8ae36febc24568c5fe # zizmor: ignore[unpinned-uses]
115122
env:
116123
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
117124
with:
@@ -122,4 +129,4 @@ jobs:
122129
prerelease: false
123130

124131
- name: Publish to Pypi
125-
uses: pypa/gh-action-pypi-publish@release/v1
132+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ dependencies = [
2828
"cloudevents>=1.2.0,<2.0.0",
2929
"flask-cors>=3.0.10",
3030
"pyjwt[crypto]>=2.5.0",
31-
"google-events==0.5.0",
31+
"google-events>=0.5.0,<1.0.0",
3232
"google-cloud-firestore>=2.11.0",
3333
"tzdata>=2024.1",
3434
]

src/firebase_functions/firestore_fn.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ def _firestore_endpoint_handler(
221221
func(database_event_with_auth_context)
222222
else:
223223
# mypy cannot infer that the event type is correct, hence the cast
224-
_typing.cast(_C1 | _C2, func)(database_event)
224+
_typing.cast(_C1 | _C2, func)(database_event) # type: ignore[arg-type]
225225

226226

227227
@_util.copy_func_kwargs(FirestoreOptions)

src/firebase_functions/options.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -254,10 +254,11 @@ class RuntimeOptions:
254254
to the value "gcf_gen1"
255255
"""
256256

257-
vpc_connector: str | _util.Sentinel | None = None
257+
vpc_connector: str | Expression[str] | _util.Sentinel | None = None
258258
"""
259259
Connect function to specified VPC connector.
260-
A value of ``RESET_VALUE`` removes the VPC connector.
260+
Accepts a plain string, an ``Expression[str]`` (e.g. a ``StringParam`` or
261+
ternary expression), or ``RESET_VALUE`` to remove the VPC connector.
261262
"""
262263

263264
vpc_connector_egress_settings: VpcEgressSetting | _util.Sentinel | None = None
@@ -1254,8 +1255,8 @@ def set_global_options(
12541255
max_instances: int | Expression[int] | _util.Sentinel | None = None,
12551256
concurrency: int | Expression[int] | _util.Sentinel | None = None,
12561257
cpu: int | _typing.Literal["gcf_gen1"] | _util.Sentinel = "gcf_gen1",
1257-
vpc_connector: str | None = None,
1258-
vpc_connector_egress_settings: VpcEgressSetting | None = None,
1258+
vpc_connector: str | Expression[str] | _util.Sentinel | None = None,
1259+
vpc_connector_egress_settings: VpcEgressSetting | _util.Sentinel | None = None,
12591260
service_account: str | _util.Sentinel | None = None,
12601261
ingress: IngressSetting | _util.Sentinel | None = None,
12611262
labels: dict[str, str] | None = None,

tests/conftest.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Copyright 2026 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+
"""
15+
Pytest configuration and shared fixtures for the test suite.
16+
"""
17+
18+
import pytest
19+
20+
from firebase_functions import params
21+
22+
# pylint: disable=protected-access
23+
24+
25+
@pytest.fixture(autouse=True)
26+
def _cleanup_params():
27+
"""Clear the global params registry so each test runs with a clean state."""
28+
params._params.clear()
29+
yield
30+
params._params.clear()

tests/test_options.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
Options unit tests.
1616
"""
1717

18+
import pytest
1819
from pytest import raises
1920

2021
from firebase_functions import alerts_fn, https_fn, options, params
@@ -41,6 +42,15 @@ def asamplefunctionpreserved(_):
4142
return "hello world"
4243

4344

45+
@pytest.fixture(autouse=True)
46+
def _cleanup_global_options():
47+
"""Reset global options so each test runs with a clean state."""
48+
original_options = options._GLOBAL_OPTIONS
49+
options._GLOBAL_OPTIONS = options.RuntimeOptions()
50+
yield
51+
options._GLOBAL_OPTIONS = original_options
52+
53+
4454
def test_set_global_options():
4555
"""
4656
Testing if setting a global option internally change the values.
@@ -64,6 +74,30 @@ def test_global_options_merged_with_provider_options():
6474
)
6575

6676

77+
@pytest.mark.parametrize(
78+
"vpc_connector_expr_factory",
79+
[
80+
lambda: params.StringParam("VPC_CONNECTOR"),
81+
lambda: params.BoolParam("USE_VPC").equals(True).then("my-vpc", ""),
82+
],
83+
)
84+
def test_set_global_options_accepts_vpc_connector_expression(vpc_connector_expr_factory):
85+
vpc_connector_expr = vpc_connector_expr_factory()
86+
options.set_global_options(vpc_connector=vpc_connector_expr)
87+
88+
assert options._GLOBAL_OPTIONS.vpc_connector == vpc_connector_expr, (
89+
"global vpc_connector expression was not stored"
90+
)
91+
92+
https_options = options.HttpsOptions()
93+
endpoint = https_options._endpoint(func_name="test_vpc_global")
94+
95+
assert endpoint.vpc is not None, "vpc block was not set on endpoint"
96+
assert endpoint.vpc["connector"] == str(vpc_connector_expr), (
97+
"global vpc_connector Expression[str] was not applied to the endpoint"
98+
)
99+
100+
67101
def test_https_options_removes_cors():
68102
"""
69103
Testing _HttpsOptions strips out the 'cors' property when converted to a dict.
@@ -206,6 +240,48 @@ def test_invoker_with_no_element_throws():
206240
options.HttpsOptions(invoker=[])._endpoint(func_name="test")
207241

208242

243+
@pytest.mark.parametrize(
244+
"vpc_connector_expr_factory",
245+
[
246+
lambda: params.StringParam("VPC_CONNECTOR"),
247+
lambda: params.BoolParam("USE_VPC").equals(True).then("my-vpc", ""),
248+
],
249+
)
250+
def test_vpc_connector_accepts_expression(vpc_connector_expr_factory):
251+
vpc_connector_expr = vpc_connector_expr_factory()
252+
https_options = options.HttpsOptions(vpc_connector=vpc_connector_expr)
253+
https_options_dict = https_options._asdict_with_global_options()
254+
255+
# The options dict should contain the CEL string representation for the expression.
256+
assert https_options_dict["vpc_connector"] == str(vpc_connector_expr), (
257+
"vpc_connector expression was not converted to CEL string"
258+
)
259+
260+
# The generated endpoint should map the resolved vpc_connector into the vpc block.
261+
endpoint = https_options._endpoint(func_name="test_vpc")
262+
assert endpoint.vpc is not None, "vpc block was not set on endpoint"
263+
assert endpoint.vpc["connector"] == str(vpc_connector_expr), (
264+
"vpc connector was not set from vpc_connector Expression[str]"
265+
)
266+
267+
268+
def test_vpc_connector_expression_with_egress_settings():
269+
vpc_connector_expr = params.StringParam("VPC_CONNECTOR")
270+
https_options = options.HttpsOptions(
271+
vpc_connector=vpc_connector_expr,
272+
vpc_connector_egress_settings=options.VpcEgressSetting.ALL_TRAFFIC,
273+
)
274+
275+
endpoint = https_options._endpoint(func_name="test_vpc_egress")
276+
assert endpoint.vpc is not None, "vpc block was not set on endpoint"
277+
assert endpoint.vpc["connector"] == str(vpc_connector_expr), (
278+
"vpc connector was not set from vpc_connector Expression[str]"
279+
)
280+
assert endpoint.vpc.get("egressSettings") == options.VpcEgressSetting.ALL_TRAFFIC.value, (
281+
"egressSettings was not set alongside an Expression[str] vpc_connector"
282+
)
283+
284+
209285
def _assert_alert_endpoint_options(endpoint, expected_alert_type, expect_app_id: str | None = None):
210286
assert endpoint.region == ["europe-west1"]
211287
assert endpoint.maxInstances == 1

uv.lock

Lines changed: 4 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)