├── .github
├── CONTRIBUTING.md
├── scripts
│ ├── generate_changelog.sh
│ └── publish_preflight_check.sh
└── workflows
│ ├── ci.yaml
│ └── release.yaml
├── .gitignore
├── .pylintrc
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── docs
├── generate.sh
└── theme
│ ├── devsite_sphinx_theme
│ ├── domainindex.html
│ ├── genindex.html
│ ├── layout.html
│ ├── page.html
│ ├── search.html
│ └── theme.conf
│ └── devsite_translator
│ ├── __init__.py
│ └── html.py
├── example
├── .firebaserc
├── .gitignore
├── firebase.json
├── functions
│ ├── .gitignore
│ ├── main.py
│ └── requirements.txt
└── index.html
├── mypy.ini
├── pyproject.toml
├── pytest.ini
├── samples
├── basic_alerts
│ ├── .firebaserc
│ ├── .gitignore
│ ├── __init__.py
│ ├── firebase.json
│ └── functions
│ │ ├── .gitignore
│ │ ├── main.py
│ │ └── requirements.txt
├── basic_db
│ ├── .firebaserc
│ ├── .gitignore
│ ├── __init__.py
│ ├── firebase.json
│ └── functions
│ │ ├── .gitignore
│ │ ├── main.py
│ │ └── requirements.txt
├── basic_eventarc
│ ├── .firebaserc
│ ├── .gitignore
│ ├── __init__.py
│ ├── firebase.json
│ └── functions
│ │ ├── .gitignore
│ │ ├── main.py
│ │ └── requirements.txt
├── basic_firestore
│ ├── .firebaserc
│ ├── .gitignore
│ ├── __init__.py
│ ├── firebase.json
│ └── functions
│ │ ├── .gitignore
│ │ ├── main.py
│ │ └── requirements.txt
├── basic_params
│ ├── .firebaserc
│ ├── .gitignore
│ ├── __init__.py
│ ├── firebase.json
│ └── functions
│ │ ├── .gitignore
│ │ ├── main.py
│ │ └── requirements.txt
├── basic_scheduler
│ ├── .firebaserc
│ ├── .gitignore
│ ├── __init__.py
│ ├── firebase.json
│ └── functions
│ │ ├── .gitignore
│ │ ├── main.py
│ │ └── requirements.txt
├── basic_storage
│ ├── .firebaserc
│ ├── .gitignore
│ ├── firebase.json
│ └── functions
│ │ ├── .gitignore
│ │ ├── __init__.py
│ │ ├── main.py
│ │ └── requirements.txt
├── basic_tasks
│ ├── .firebaserc
│ ├── .gitignore
│ ├── __init__.py
│ ├── firebase.json
│ └── functions
│ │ ├── .gitignore
│ │ ├── main.py
│ │ └── requirements.txt
├── basic_test_lab
│ ├── .firebaserc
│ ├── .gitignore
│ ├── __init__.py
│ ├── firebase.json
│ └── functions
│ │ ├── .gitignore
│ │ ├── main.py
│ │ └── requirements.txt
├── https_flask
│ ├── .firebaserc
│ ├── .gitignore
│ ├── __init__.py
│ ├── firebase.json
│ └── functions
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── main.py
│ │ └── requirements.txt
└── identity
│ ├── .firebaserc
│ ├── .gitignore
│ ├── __init__.py
│ ├── client
│ └── index.html
│ ├── firebase.json
│ └── functions
│ ├── .gitignore
│ ├── main.py
│ └── requirements.txt
├── setup.py
├── src
└── firebase_functions
│ ├── __init__.py
│ ├── alerts
│ ├── __init__.py
│ ├── app_distribution_fn.py
│ ├── billing_fn.py
│ ├── crashlytics_fn.py
│ └── performance_fn.py
│ ├── alerts_fn.py
│ ├── core.py
│ ├── db_fn.py
│ ├── eventarc_fn.py
│ ├── firestore_fn.py
│ ├── https_fn.py
│ ├── identity_fn.py
│ ├── logger.py
│ ├── options.py
│ ├── params.py
│ ├── private
│ ├── __init__.py
│ ├── _alerts_fn.py
│ ├── _identity_fn.py
│ ├── manifest.py
│ ├── path_pattern.py
│ ├── serving.py
│ ├── token_verifier.py
│ └── util.py
│ ├── pubsub_fn.py
│ ├── py.typed
│ ├── remote_config_fn.py
│ ├── scheduler_fn.py
│ ├── storage_fn.py
│ ├── tasks_fn.py
│ └── test_lab_fn.py
└── tests
├── firebase_config_test.json
├── test_db.py
├── test_eventarc_fn.py
├── test_firestore_fn.py
├── test_https_fn.py
├── test_identity_fn.py
├── test_init.py
├── test_logger.py
├── test_manifest.py
├── test_options.py
├── test_params.py
├── test_path_pattern.py
├── test_pubsub_fn.py
├── test_remote_config_fn.py
├── test_scheduler_fn.py
├── test_storage_fn.py
├── test_tasks_fn.py
├── test_test_lab_fn.py
└── test_util.py
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to contribute
2 |
3 | We'd love to accept your patches and contributions to this project. There are
4 | just a few small guidelines you need to follow.
5 |
6 | ## Contributor License Agreement
7 |
8 | Contributions to this project must be accompanied by a Contributor License
9 | Agreement. You (or your employer) retain the copyright to your contribution,
10 | this simply gives us permission to use and redistribute your contributions as
11 | part of the project. Head over to to see
12 | your current agreements on file or to sign a new one.
13 |
14 | You generally only need to submit a CLA once, so if you've already submitted one
15 | (even if it was for a different project), you probably don't need to do it
16 | again.
17 |
18 | ## Code reviews
19 |
20 | All submissions, including submissions by project members, require review. We
21 | use GitHub pull requests for this purpose. Consult [GitHub Help] for more
22 | information on using pull requests.
23 |
24 | ## Setup local environment
25 |
26 | Clone the project and run the following commands to setup your environment
27 |
28 | ```sh
29 | python3.11 -m venv venv
30 | source venv/bin/activate
31 | pip3 install --upgrade pip
32 | python3.11 -m pip install -e ".[dev]"
33 | ```
34 |
35 | (this also applies to setting up samples environment for each sample)
36 |
37 | ### Running tests
38 |
39 | Without coverage:
40 | ```bash
41 | python3.11 -m pytest
42 | ```
43 |
44 | With coverage:
45 | ```bash
46 | python3.11 -m pytest --cov=src --cov-report term --cov-report html --cov-report xml -vv
47 | ```
48 |
49 | ### Formatting code
50 |
51 | ```bash
52 | yapf -i -r -p .
53 | ```
54 |
55 | ### Running lints & type checking
56 |
57 | ```bash
58 | # Type checking
59 | python3.11 -m mypy .
60 | # Linting
61 | python3.11 -m pylint $(git ls-files '*.py')
62 | ```
63 |
64 | ### Generating Docs
65 |
66 | Prerequisites:
67 | - On OSX, install getopt:
68 | - `brew install gnu-getopt`
69 |
70 | ```sh
71 | ./docs/generate.sh --out=./docs/build/ --pypath=src/
72 | ```
73 |
74 | ### Deploying a sample for testing
75 |
76 | Example:
77 |
78 | ```sh
79 | cd samples/basic_https
80 | firebase deploy --only=functions
81 | ```
82 |
83 | Note to test your local changes of `firebase-functions` when deploying you should push your changes to a branch on GitHub and then locally in the `sample/*/requirements.txt` change `firebase-functions` dependency line to instead come from git, e.g. :
84 |
85 | ```
86 | git+https://github.com/YOUR_USERNAME/firebase-functions-python.git@YOURBRANCH#egg=firebase-functions
87 | ```
88 |
89 | [github help]: https://help.github.com/articles/about-pull-requests/
--------------------------------------------------------------------------------
/.github/scripts/generate_changelog.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Copyright 2023 Google Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | set -e
18 | set -u
19 |
20 | function printChangelog() {
21 | local TITLE=$1
22 | shift
23 | # Skip the sentinel value.
24 | local ENTRIES=("${@:2}")
25 | if [ ${#ENTRIES[@]} -ne 0 ]; then
26 | echo "### ${TITLE}"
27 | echo ""
28 | for ((i = 0; i < ${#ENTRIES[@]}; i++))
29 | do
30 | echo "* ${ENTRIES[$i]}"
31 | done
32 | echo ""
33 | fi
34 | }
35 |
36 | if [[ -z "${GITHUB_SHA}" ]]; then
37 | GITHUB_SHA="HEAD"
38 | fi
39 |
40 | LAST_TAG=`git describe --tags $(git rev-list --tags --max-count=1) 2> /dev/null` || true
41 | if [[ -z "${LAST_TAG}" ]]; then
42 | echo "[INFO] No tags found. Including all commits up to ${GITHUB_SHA}."
43 | VERSION_RANGE="${GITHUB_SHA}"
44 | else
45 | echo "[INFO] Last release tag: ${LAST_TAG}."
46 | COMMIT_SHA=`git show-ref -s ${LAST_TAG}`
47 | echo "[INFO] Last release commit: ${COMMIT_SHA}."
48 | VERSION_RANGE="${COMMIT_SHA}..${GITHUB_SHA}"
49 | echo "[INFO] Including all commits in the range ${VERSION_RANGE}."
50 | fi
51 |
52 | echo ""
53 |
54 | # Older versions of Bash (< 4.4) treat empty arrays as unbound variables, which triggers
55 | # errors when referencing them. Therefore we initialize each of these arrays with an empty
56 | # sentinel value, and later skip them.
57 | CHANGES=("")
58 | FIXES=("")
59 | FEATS=("")
60 | MISC=("")
61 |
62 | while read -r line
63 | do
64 | COMMIT_MSG=`echo ${line} | cut -d ' ' -f 2-`
65 | if [[ $COMMIT_MSG =~ ^change(\(.*\))?: ]]; then
66 | CHANGES+=("$COMMIT_MSG")
67 | elif [[ $COMMIT_MSG =~ ^fix(\(.*\))?: ]]; then
68 | FIXES+=("$COMMIT_MSG")
69 | elif [[ $COMMIT_MSG =~ ^refactor(\(.*\))?: ]]; then
70 | FIXES+=("$COMMIT_MSG")
71 | elif [[ $COMMIT_MSG =~ ^feat(\(.*\))?: ]]; then
72 | FEATS+=("$COMMIT_MSG")
73 | else
74 | MISC+=("${COMMIT_MSG}")
75 | fi
76 | done < <(git log ${VERSION_RANGE} --oneline)
77 |
78 | printChangelog "Breaking Changes" "${CHANGES[@]}"
79 | printChangelog "New Features" "${FEATS[@]}"
80 | printChangelog "Bug Fixes" "${FIXES[@]}"
--------------------------------------------------------------------------------
/.github/scripts/publish_preflight_check.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Copyright 2023 Google Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 |
18 | ###################################### Outputs #####################################
19 |
20 | # 1. version: The version of this release including the 'v' prefix (e.g. v1.2.3).
21 | # 2. changelog: Formatted changelog text for this release.
22 |
23 | ####################################################################################
24 |
25 | set -e
26 | set -u
27 |
28 | function echo_info() {
29 | local MESSAGE=$1
30 | echo "[INFO] ${MESSAGE}"
31 | }
32 |
33 | function echo_warn() {
34 | local MESSAGE=$1
35 | echo "[WARN] ${MESSAGE}"
36 | }
37 |
38 | function terminate() {
39 | echo ""
40 | echo_warn "--------------------------------------------"
41 | echo_warn "PREFLIGHT FAILED"
42 | echo_warn "--------------------------------------------"
43 | exit 1
44 | }
45 |
46 |
47 | echo_info "Starting publish preflight check..."
48 | echo_info "Git revision : ${GITHUB_SHA}"
49 | echo_info "Workflow triggered by : ${GITHUB_ACTOR}"
50 | echo_info "GitHub event : ${GITHUB_EVENT_NAME}"
51 |
52 |
53 | echo_info ""
54 | echo_info "--------------------------------------------"
55 | echo_info "Extracting release version"
56 | echo_info "--------------------------------------------"
57 | echo_info ""
58 |
59 | readonly INIT_FILE="src/firebase_functions/__init__.py"
60 | echo_info "Loading version from: ${INIT_FILE}"
61 |
62 | readonly RELEASE_VERSION=`grep "__version__" ${INIT_FILE} | awk '{print $3}' | tr -d \"` || true
63 | if [[ -z "${RELEASE_VERSION}" ]]; then
64 | echo_warn "Failed to extract release version from: ${INIT_FILE}"
65 | terminate
66 | fi
67 |
68 | if [[ ! "${RELEASE_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+([a-zA-Z0-9]+)?$ ]]; then
69 | echo_warn "Malformed release version string: ${RELEASE_VERSION}. Exiting."
70 | terminate
71 | fi
72 |
73 | echo_info "Extracted release version: ${RELEASE_VERSION}"
74 | echo "version=${RELEASE_VERSION}" >> "$GITHUB_OUTPUT"
75 |
76 |
77 | echo_info ""
78 | echo_info "--------------------------------------------"
79 | echo_info "Check release artifacts"
80 | echo_info "--------------------------------------------"
81 | echo_info ""
82 |
83 | if [[ ! -d dist ]]; then
84 | echo_warn "dist directory does not exist."
85 | terminate
86 | fi
87 |
88 | readonly BIN_DIST="dist/firebase_functions-${RELEASE_VERSION}-py3-none-any.whl"
89 | if [[ -f "${BIN_DIST}" ]]; then
90 | echo_info "Found binary distribution (bdist_wheel): ${BIN_DIST}"
91 | else
92 | echo_warn "Binary distribution ${BIN_DIST} not found."
93 | terminate
94 | fi
95 |
96 | readonly SRC_DIST="dist/firebase_functions-${RELEASE_VERSION}.tar.gz"
97 | if [[ -f "${SRC_DIST}" ]]; then
98 | echo_info "Found source distribution (sdist): ${SRC_DIST}"
99 | else
100 | echo_warn "Source distribution ${SRC_DIST} not found."
101 | terminate
102 | fi
103 |
104 | readonly ARTIFACT_COUNT=`ls dist/ | wc -l`
105 | if [[ $ARTIFACT_COUNT -ne 2 ]]; then
106 | echo_warn "Unexpected artifacts in the distribution directory."
107 | ls -l dist
108 | terminate
109 | fi
110 |
111 |
112 | echo_info ""
113 | echo_info "--------------------------------------------"
114 | echo_info "Checking previous releases"
115 | echo_info "--------------------------------------------"
116 | echo_info ""
117 |
118 | readonly PYPI_URL="https://pypi.org/pypi/firebase-functions/${RELEASE_VERSION}/json"
119 | readonly PYPI_STATUS=`curl -s -o /dev/null -L -w "%{http_code}" ${PYPI_URL}`
120 | if [[ $PYPI_STATUS -eq 404 ]]; then
121 | echo_info "Release version ${RELEASE_VERSION} not found in Pypi."
122 | elif [[ $PYPI_STATUS -eq 200 ]]; then
123 | echo_warn "Release version ${RELEASE_VERSION} already present in Pypi."
124 | terminate
125 | else
126 | echo_warn "Unexpected ${PYPI_STATUS} response from Pypi. Exiting."
127 | terminate
128 | fi
129 |
130 |
131 | echo_info ""
132 | echo_info "--------------------------------------------"
133 | echo_info "Checking release tag"
134 | echo_info "--------------------------------------------"
135 | echo_info ""
136 |
137 | echo_info "---< git fetch --depth=1 origin +refs/tags/*:refs/tags/* || true >---"
138 | git fetch --depth=1 origin +refs/tags/*:refs/tags/* || true
139 | echo ""
140 |
141 | readonly EXISTING_TAG=`git rev-parse -q --verify "refs/tags/${RELEASE_VERSION}"` || true
142 | if [[ -n "${EXISTING_TAG}" ]]; then
143 | echo_warn "Tag ${RELEASE_VERSION} already exists. Exiting."
144 | echo_warn "If the tag was created in a previous unsuccessful attempt, delete it and try again."
145 | echo_warn " $ git tag -d ${RELEASE_VERSION}"
146 | echo_warn " $ git push --delete origin ${RELEASE_VERSION}"
147 |
148 | readonly RELEASE_URL="https://github.com/firebase/firebase-functions-python/releases/tag/${RELEASE_VERSION}"
149 | echo_warn "Delete any corresponding releases at ${RELEASE_URL}."
150 | terminate
151 | fi
152 |
153 | echo_info "Tag ${RELEASE_VERSION} does not exist."
154 |
155 |
156 | echo_info ""
157 | echo_info "--------------------------------------------"
158 | echo_info "Generating changelog"
159 | echo_info "--------------------------------------------"
160 | echo_info ""
161 |
162 | echo_info "---< git fetch origin main --prune --unshallow >---"
163 | git fetch origin main --prune --unshallow
164 | echo ""
165 |
166 | echo_info "Generating changelog from history..."
167 | readonly CURRENT_DIR=$(dirname "$0")
168 | readonly CHANGELOG=`${CURRENT_DIR}/generate_changelog.sh`
169 | echo "$CHANGELOG"
170 |
171 | # Parse and preformat the text to handle multi-line output.
172 | # https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#example-of-a-multiline-string
173 | FILTERED_CHANGELOG=`echo "$CHANGELOG" | grep -v "[INFO]"`
174 | EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
175 | echo "changelog<<$EOF" >> "$GITHUB_OUTPUT"
176 | echo "$CHANGELOG" >> "$GITHUB_OUTPUT"
177 | echo "$EOF" >> "$GITHUB_OUTPUT"
178 |
179 |
180 | echo ""
181 | echo_info "--------------------------------------------"
182 | echo_info "PREFLIGHT SUCCESSFUL"
183 | echo_info "--------------------------------------------"
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - main
8 |
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | fail-fast: false
14 | matrix:
15 | python: ["3.10", "3.12"]
16 |
17 | steps:
18 | - uses: actions/checkout@v3
19 | - name: Set up Python ${{ matrix.python }}
20 | uses: actions/setup-python@v4
21 | with:
22 | python-version: ${{ matrix.python }}
23 | - name: Install dependencies
24 | run: |
25 | python${{ matrix.python }} -m venv venv
26 | source venv/bin/activate
27 | pip3 install --upgrade pip
28 | python${{ matrix.python }} -m pip install -e ".[dev]"
29 | - name: Test with pytest & coverage
30 | run: |
31 | source venv/bin/activate
32 | python${{ matrix.python }} -m pytest --cov=src --cov-report term --cov-report html --cov-report xml -vv
33 | # TODO requires activation for this repository on codecov website first.
34 | # - name: Upload coverage to Codecov
35 | # uses: codecov/codecov-action@v3
36 |
37 | lint:
38 | runs-on: ubuntu-latest
39 | steps:
40 | - uses: actions/checkout@v3
41 | - name: Set up Python
42 | uses: actions/setup-python@v4
43 | with:
44 | python-version: "3.10"
45 | - name: Install dependencies
46 | run: |
47 | python3.10 -m venv venv
48 | source venv/bin/activate
49 | pip3 install --upgrade pip
50 | python3.10 -m pip install -e ".[dev]"
51 | - name: Lint with pylint
52 | run: |
53 | source venv/bin/activate
54 | python3.10 -m pylint $(git ls-files '*.py')
55 | - name: Lint with mypy
56 | run: |
57 | source venv/bin/activate
58 | python3.10 -m mypy .
59 |
60 | docs:
61 | runs-on: ubuntu-latest
62 | steps:
63 | - uses: actions/checkout@v3
64 | - name: Set up Python
65 | uses: actions/setup-python@v4
66 | with:
67 | python-version: "3.10"
68 | - name: Install dependencies
69 | run: |
70 | python3.10 -m venv venv
71 | source venv/bin/activate
72 | pip3 install --upgrade pip
73 | python3.10 -m pip install -e ".[dev]"
74 | - name: Generate Reference Docs
75 | run: |
76 | source venv/bin/activate
77 | mkdir ./docs/build
78 | ./docs/generate.sh --out=./docs/build/ --pypath=src/
79 | - uses: actions/upload-artifact@v4
80 | name: Upload Docs Preview
81 | with:
82 | name: reference-docs
83 | path: ./docs/build/
84 |
85 | format:
86 | runs-on: ubuntu-latest
87 | steps:
88 | - uses: actions/checkout@v3
89 | - name: Set up Python
90 | uses: actions/setup-python@v4
91 | with:
92 | python-version: "3.10"
93 | - name: Install dependencies
94 | run: |
95 | python3.10 -m venv venv
96 | source venv/bin/activate
97 | pip3 install --upgrade pip
98 | python3.10 -m pip install -e ".[dev]"
99 | - name: Check Formatting
100 | run: |
101 | source venv/bin/activate
102 | yapf -d -r -p .
103 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2023 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 | name: Release Candidate
16 |
17 | on:
18 | pull_request:
19 | types: [opened, synchronize, closed]
20 | # Allow workflow to be triggered manually.
21 | workflow_dispatch:
22 |
23 | jobs:
24 | stage_release:
25 | # To publish a release, merge the release PR with the label 'release:publish'.
26 | # To stage a release without publishing it, manually invoke the workflow.
27 | # . or apply the 'release:stage' label to a PR.
28 | if: >
29 | (github.event.pull_request.merged && contains(github.event.pull_request.labels.*.name, 'release:publish')) ||
30 | github.event.workflow_dispatch ||
31 | (!github.event.pull_request.merged && contains(github.event.pull_request.labels.*.name, 'release:stage'))
32 |
33 | runs-on: ubuntu-latest
34 |
35 | steps:
36 | - name: Checkout source for staging
37 | uses: actions/checkout@v3
38 |
39 | - name: Set up Python
40 | uses: actions/setup-python@v4
41 | with:
42 | python-version: '3.10'
43 |
44 | - name: Install dependencies
45 | run: |
46 | pip install --upgrade pip
47 | python -m pip install -e ".[dev]"
48 |
49 | - name: Test with pytest & coverage
50 | run: |
51 | python -m pytest --cov=src --cov-report term --cov-report html --cov-report xml -vv
52 |
53 | # Build the Python Wheel and the source distribution.
54 | - name: Package release artifacts
55 | run: |
56 | python -m pip install setuptools wheel
57 | python setup.py bdist_wheel sdist
58 |
59 | # Attach the packaged artifacts to the workflow output. These can be manually
60 | # downloaded for later inspection if necessary.
61 | - name: Archive artifacts
62 | uses: actions/upload-artifact@v4
63 | with:
64 | name: dist
65 | path: dist/
66 |
67 | publish_release:
68 | needs: stage_release
69 |
70 | # Check whether the release should be published. We publish only when the trigger PR is
71 | # 1. merged
72 | # 2. to the main branch
73 | # 3. with the label 'release:publish', and
74 | # 4. the title prefix 'chore: Release '.
75 | if: >
76 | github.event.pull_request.merged &&
77 | github.ref == 'refs/heads/main' &&
78 | contains(github.event.pull_request.labels.*.name, 'release:publish') &&
79 | startsWith(github.event.pull_request.title, 'chore: Release ')
80 |
81 | runs-on: ubuntu-latest
82 |
83 | permissions:
84 | # Used to create a short-lived OIDC token which is given to PyPi to identify this workflow job
85 | # See: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings
86 | # and https://docs.pypi.org/trusted-publishers/using-a-publisher/
87 | id-token: write
88 | contents: write
89 |
90 | steps:
91 | - name: Checkout source for publish
92 | uses: actions/checkout@v4
93 |
94 | # Download the artifacts created by the stage_release job.
95 | - name: Download release candidates
96 | uses: actions/download-artifact@v4.1.7
97 | with:
98 | name: dist
99 | path: dist
100 |
101 | - name: Publish preflight check
102 | id: preflight
103 | run: ./.github/scripts/publish_preflight_check.sh
104 |
105 | # We pull this action from a custom fork of a contributor until
106 | # https://github.com/actions/create-release/pull/32 is merged. Also note that v1 of
107 | # this action does not support the "body" parameter.
108 | - name: Create release tag
109 | # Skip creating a release tag for prereleases
110 | if: (!contains(github.event.pull_request.labels.*.name, 'release:prerelease'))
111 | uses: fleskesvor/create-release@1a72e235c178bf2ae6c51a8ae36febc24568c5fe
112 | env:
113 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
114 | with:
115 | tag_name: ${{ steps.preflight.outputs.version }}
116 | release_name: Firebase Functions Python SDK ${{ steps.preflight.outputs.version }}
117 | body: ${{ steps.preflight.outputs.changelog }}
118 | draft: false
119 | prerelease: false
120 |
121 | - name: Publish to Pypi
122 | uses: pypa/gh-action-pypi-publish@release/v1
123 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 | pytest-coverage.txt
131 |
132 | # MacOS
133 | .DS_Store
134 |
135 | # reference docs
136 | doc/dist
137 |
138 | # IDE files
139 | .idea
140 | .vscode/*
141 | !.vscode/settings.json
142 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "python.linting.enabled": true,
3 | "python.linting.pylintEnabled": false,
4 | "python.formatting.provider": "yapf",
5 | "python.formatting.yapfArgs": [
6 | "--style",
7 | "{based_on_style: google, indent_width: 4}"
8 | ],
9 | "python.linting.pylintPath": "pylint",
10 | "python.envFile": "${workspaceFolder}/venv",
11 | "editor.formatOnSave": true,
12 | "python.linting.lintOnSave": true,
13 | "python.linting.mypyEnabled": true,
14 | "mypy.dmypyExecutable": "${workspaceFolder}/venv/bin/dmypy",
15 | "files.autoSave": "afterDelay",
16 | "python.testing.pytestArgs": [
17 | "tests"
18 | ],
19 | "python.testing.unittestEnabled": false,
20 | "python.testing.pytestEnabled": true
21 | }
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Cloud Functions for Firebase Python SDK (Public Preview)
2 |
3 | The [`firebase-functions`](https://pypi.org/project/firebase-functions/) package provides an SDK for defining Cloud Functions for Firebase in Python.
4 |
5 | Cloud Functions provides hosted, private, and scalable environment where you can run server code. The Firebase SDK for Cloud Functions integrates the Firebase platform by letting you write code that responds to events and invokes functionality exposed by other Firebase features.
6 |
7 | ## Learn more
8 |
9 | Learn more about the Firebase SDK for Cloud Functions in the [Firebase documentation](https://firebase.google.com/docs/functions/) or [check out our samples](https://github.com/firebase/functions-samples).
10 |
11 | Here are some resources to get help:
12 |
13 | - Start with the quickstart:
14 | - Go through the guide:
15 | - Read the full API reference:
16 | - Browse some examples:
17 |
18 | If the official documentation doesn't help, try asking through our official support channels:
19 |
20 | ## Usage
21 |
22 | ```python
23 | # functions/main.py
24 | from firebase_functions import db_fn
25 | from notify_users import api
26 |
27 | @db_fn.on_value_created(reference="/posts/{post_id}")
28 | def new_post(event):
29 | print(f"Received new post with ID: {event.params.get('post_id')}")
30 | return notifyUsers(event.data)
31 | ```
32 |
33 | ## Contributing
34 |
35 | To contribute a change, [check out the contributing guide](.github/CONTRIBUTING.md).
36 |
37 | ## License
38 |
39 | © Google, 2025. Licensed under [Apache License](LICENSE).
40 |
41 |
--------------------------------------------------------------------------------
/docs/generate.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Copyright 2022 Google Inc.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | #
16 | # generate.sh --- Produces HTML documentation from Python modules
17 | #
18 | # Arguments:
19 | # --out=path/to/copy/output
20 | # [--pypath=path/to/add/to/pythonpath]
21 | # [--sphinx-build=path/to/sphinx-build]
22 | # [--themepath=path/to/sphinx/themes]
23 | # TARGET
24 | #
25 | # Prerequisites:
26 | # Install the required Python modules:
27 | # pip install sphinx sphinxcontrib-napoleon
28 | # . On OSX, install getopt:
29 | # . :brew install gnu-getopt
30 | #
31 | # Example:
32 | # generate.sh \
33 | # --out=./docs/dist/
34 |
35 | SPHINXBIN=sphinx-build
36 | THEMEPATH=$(dirname $0)/theme
37 | THEMEPATH=$(realpath "$THEMEPATH")
38 |
39 | if [[ $(uname) == "Darwin" ]]; then
40 | TEMPARGS=$($(brew --prefix)/opt/gnu-getopt/bin/getopt -o o:p:b:t: --long out:,pypath:,sphinx-build:,themepath: -- "$@")
41 | else
42 | TEMPARGS=$(getopt -o o:p:b:t: --long out:,pypath:,sphinx-build:,themepath: -- "$@")
43 | getopt = getopt
44 | fi
45 | eval set -- "$TEMPARGS"
46 |
47 | while true; do
48 | case "$1" in
49 | -o | --out)
50 | OUTDIR=$(realpath "$2")
51 | shift 2
52 | ;;
53 | -p | --pypath)
54 | PYTHONPATH=$(realpath "$2"):"$PYTHONPATH"
55 | shift 2
56 | ;;
57 | -b | --sphinx-build)
58 | SPHINXBIN=$(realpath "$2")
59 | shift 2
60 | ;;
61 | -t | --themepath)
62 | THEMEPATH=$(realpath "$2")
63 | shift 2
64 | ;;
65 | --)
66 | shift
67 | break
68 | ;;
69 | *)
70 | echo Error
71 | exit 1
72 | ;;
73 | esac
74 | done
75 |
76 | TARGET="$1"
77 | PYTHONPATH="$THEMEPATH":"$PYTHONPATH"
78 |
79 | if [[ "$OUTDIR" == "" ]]; then
80 | echo Output directory not specified.
81 | exit 1
82 | fi
83 |
84 | TITLE="Firebase Python SDK for Cloud Functions"
85 | PY_MODULES='firebase_functions.core
86 | firebase_functions.alerts_fn
87 | firebase_functions.alerts.app_distribution_fn
88 | firebase_functions.alerts.billing_fn
89 | firebase_functions.alerts.crashlytics_fn
90 | firebase_functions.alerts.performance_fn
91 | firebase_functions.db_fn
92 | firebase_functions.eventarc_fn
93 | firebase_functions.firestore_fn
94 | firebase_functions.https_fn
95 | firebase_functions.identity_fn
96 | firebase_functions.logger
97 | firebase_functions.options
98 | firebase_functions.params
99 | firebase_functions.pubsub_fn
100 | firebase_functions.remote_config_fn
101 | firebase_functions.scheduler_fn
102 | firebase_functions.storage_fn
103 | firebase_functions.tasks_fn
104 | firebase_functions.test_lab_fn'
105 | DEVSITE_PATH='/docs/reference/functions/2nd-gen/python'
106 |
107 | #
108 | # Set up temporary project
109 | #
110 | PROJDIR=$(mktemp -d)
111 | echo Created project directory: "$PROJDIR"
112 | pushd "$PROJDIR" >/dev/null
113 | mkdir _build
114 |
115 | cat >conf.py <"$m".rst <index.rst <>index.rst
158 | done
159 |
160 | #
161 | # Run sphinx-build
162 | #
163 |
164 | echo Building HTML...
165 | echo "$PYTHONPATH"
166 | PYTHONPATH="$PYTHONPATH" "$SPHINXBIN" -W -v -b html . _build/
167 | if [ "$?" -ne 0 ]; then
168 | exit 1
169 | fi
170 |
171 | #
172 | # Copy output
173 | #
174 |
175 | echo Copying output to "$OUTDIR"...
176 | mkdir -p $OUTDIR
177 | for m in ${PY_MODULES}; do
178 | cp _build/"$m".html "$OUTDIR"
179 | done
180 | cp _build/index.html "$OUTDIR"
181 | cp _build/objects.inv "$OUTDIR"
182 |
183 | #
184 | # Create _toc.yaml
185 | #
186 |
187 | TOC="$OUTDIR"/_toc.yaml
188 | cat >"$TOC" <>"$TOC"
193 | echo " path: ${DEVSITE_PATH}/${m}" >>"$TOC"
194 | done
195 |
196 | popd >/dev/null
197 |
--------------------------------------------------------------------------------
/docs/theme/devsite_sphinx_theme/domainindex.html:
--------------------------------------------------------------------------------
1 | {# Empty template #}
--------------------------------------------------------------------------------
/docs/theme/devsite_sphinx_theme/genindex.html:
--------------------------------------------------------------------------------
1 | {# Empty template #}
--------------------------------------------------------------------------------
/docs/theme/devsite_sphinx_theme/layout.html:
--------------------------------------------------------------------------------
1 | {#
2 | basic/layout.html
3 | ~~~~~~~~~~~~~~~~~
4 |
5 | Master layout template for Sphinx themes.
6 |
7 | :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS.
8 | :license: BSD, see LICENSE for details.
9 | #}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | {%- block header %}{% endblock %}
21 |
22 | {%- block content %}
23 | {%- block document %}
24 | {% block body %} {% endblock %}
25 | {%- endblock %}
26 | {%- endblock %}
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/docs/theme/devsite_sphinx_theme/page.html:
--------------------------------------------------------------------------------
1 | {#
2 | basic/page.html
3 | ~~~~~~~~~~~~~~~
4 |
5 | Master template for simple pages.
6 |
7 | :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS.
8 | :license: BSD, see LICENSE for details.
9 | #}
10 | {%- extends "layout.html" %}
11 | {% block body %}
12 | {{ body }}
13 | {% endblock %}
14 |
--------------------------------------------------------------------------------
/docs/theme/devsite_sphinx_theme/search.html:
--------------------------------------------------------------------------------
1 | {# Empty template #}
--------------------------------------------------------------------------------
/docs/theme/devsite_sphinx_theme/theme.conf:
--------------------------------------------------------------------------------
1 | [theme]
2 | inherit = none
3 | stylesheet = none
4 | pygments_style = none
5 |
6 | [options]
7 |
--------------------------------------------------------------------------------
/docs/theme/devsite_translator/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/firebase-functions-python/436f4c7d8ad7fc81a5dc50e6a2c0eca3c538d397/docs/theme/devsite_translator/__init__.py
--------------------------------------------------------------------------------
/docs/theme/devsite_translator/html.py:
--------------------------------------------------------------------------------
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 | """Custom HTML translator for Firesite reference docs."""
15 |
16 | from sphinx.writers import html
17 |
18 | _DESCTYPE_NAMES = {
19 | 'class': 'Classes',
20 | 'data': 'Constants',
21 | 'function': 'Functions',
22 | 'method': 'Methods',
23 | 'attribute': 'Attributes',
24 | 'exception': 'Exceptions'
25 | }
26 |
27 | # Use the default translator for these node types
28 | _RENDER_WITH_DEFAULT = ['method', 'staticmethod', 'attribute']
29 |
30 |
31 | class FiresiteHTMLTranslator(html.HTMLTranslator):
32 | """Custom HTML translator that produces output suitable for Firesite.
33 |
34 | - Inserts H2 tags around object signatures
35 | - Uses tables instead of DLs
36 | - Inserts hidden H3 elements for right-side nav
37 | - Surrounds notes and warnings with \n\n')
115 |
116 | def visit_warning(self, node):
117 | self.body.append(self.starttag(node, 'aside', CLASS='caution'))
118 |
119 | def depart_warning(self, node):
120 | # pylint: disable=unused-argument
121 | self.body.append('\n\n')
122 |
--------------------------------------------------------------------------------
/example/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "python-functions-testing"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
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
67 |
--------------------------------------------------------------------------------
/example/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "functions": {
3 | "runtime": "python310",
4 | "ignore": [
5 | "venv",
6 | "__pycache__"
7 | ]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/example/functions/.gitignore:
--------------------------------------------------------------------------------
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__
14 |
--------------------------------------------------------------------------------
/example/functions/main.py:
--------------------------------------------------------------------------------
1 | """
2 | Example Firebase Functions written in Python
3 | """
4 | from firebase_functions import https_fn, options, params, pubsub_fn
5 | from firebase_admin import initialize_app
6 |
7 | initialize_app()
8 |
9 | options.set_global_options(
10 | region=options.SupportedRegion.EUROPE_WEST1,
11 | memory=options.MemoryOption.MB_128,
12 | min_instances=params.IntParam("MIN", default=3),
13 | )
14 |
15 |
16 | @https_fn.on_request()
17 | def onrequestexample(req: https_fn.Request) -> https_fn.Response:
18 | print("on request function data:", req.data)
19 | return https_fn.Response("Hello from https on request function example")
20 |
21 |
22 | @https_fn.on_call()
23 | def oncallexample(req: https_fn.CallableRequest):
24 | print("on call function data:", req)
25 | if req.data == "error_test":
26 | raise https_fn.HttpsError(
27 | https_fn.FunctionsErrorCode.INVALID_ARGUMENT,
28 | "This is a test",
29 | "This is some details of the test",
30 | )
31 | return "Hello from https on call function example"
32 |
33 |
34 | @pubsub_fn.on_message_published(topic="hello",)
35 | def onmessagepublishedexample(
36 | event: pubsub_fn.CloudEvent[pubsub_fn.MessagePublishedData]) -> None:
37 | print("Hello from pubsub event:", event)
38 |
--------------------------------------------------------------------------------
/example/functions/requirements.txt:
--------------------------------------------------------------------------------
1 | # firebase-functions-python
2 | ./../../
3 | # git+https://github.com/firebase/firebase-functions-python.git@main#egg=firebase-functions
4 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/mypy.ini:
--------------------------------------------------------------------------------
1 | [mypy]
2 | exclude = build|setup.py|venv
3 |
4 | # cloudevents package has no types
5 | ignore_missing_imports = True
6 | enable_incomplete_feature = Unpack
7 |
8 | [mypy-yaml.*]
9 | ignore_missing_imports = True
10 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.pytest.ini_options]
2 | pythonpath = [
3 | ".", "src/",
4 | ]
5 | [tool.coverage]
6 | [tool.coverage.run]
7 | omit = [
8 | '__init__.py',
9 | 'tests/*',
10 | '*/tests/*',
11 | ]
12 |
13 | [tool.coverage.report]
14 | skip_empty = true
15 | [tool.yapf]
16 | based_on_style = "google"
17 | indent_width = 4
18 | [tool.yapfignore]
19 | ignore_patterns = [
20 | "venv",
21 | "build",
22 | "dist",
23 | ]
24 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | pythonpath = src/ .
3 | log_cli = true
4 | log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)"
5 | log_cli_date_format = "%Y-%m-%d %H:%M:%S"
6 |
--------------------------------------------------------------------------------
/samples/basic_alerts/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "python-functions-testing"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/samples/basic_alerts/.gitignore:
--------------------------------------------------------------------------------
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
67 |
--------------------------------------------------------------------------------
/samples/basic_alerts/__init__.py:
--------------------------------------------------------------------------------
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
4 |
--------------------------------------------------------------------------------
/samples/basic_alerts/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "functions": [
3 | {
4 | "source": "functions",
5 | "codebase": "default",
6 | "ignore": [
7 | "venv"
8 | ]
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/samples/basic_alerts/functions/.gitignore:
--------------------------------------------------------------------------------
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__
14 |
--------------------------------------------------------------------------------
/samples/basic_alerts/functions/main.py:
--------------------------------------------------------------------------------
1 | """Cloud function samples for Firebase Alerts."""
2 |
3 | from firebase_functions import alerts_fn
4 | from firebase_functions.alerts import app_distribution_fn
5 | from firebase_functions.alerts import billing_fn
6 | from firebase_functions.alerts import crashlytics_fn
7 | from firebase_functions.alerts import performance_fn
8 |
9 |
10 | @alerts_fn.on_alert_published(
11 | alert_type=alerts_fn.AlertType.BILLING_PLAN_UPDATE)
12 | def onalertpublished(
13 | alert: alerts_fn.AlertEvent[alerts_fn.FirebaseAlertData[
14 | billing_fn.PlanUpdatePayload]]
15 | ) -> None:
16 | print(alert)
17 |
18 |
19 | @app_distribution_fn.on_in_app_feedback_published()
20 | def appdistributioninappfeedback(
21 | alert: app_distribution_fn.InAppFeedbackEvent) -> None:
22 | print(alert)
23 |
24 |
25 | @app_distribution_fn.on_new_tester_ios_device_published()
26 | def appdistributionnewrelease(
27 | alert: app_distribution_fn.NewTesterDeviceEvent) -> None:
28 | print(alert)
29 |
30 |
31 | @billing_fn.on_plan_automated_update_published()
32 | def billingautomatedplanupdate(
33 | alert: billing_fn.BillingPlanAutomatedUpdateEvent) -> None:
34 | print(alert)
35 |
36 |
37 | @billing_fn.on_plan_update_published()
38 | def billingplanupdate(alert: billing_fn.BillingPlanUpdateEvent) -> None:
39 | print(alert)
40 |
41 |
42 | @crashlytics_fn.on_new_fatal_issue_published()
43 | def crashlyticsnewfatalissue(
44 | alert: crashlytics_fn.CrashlyticsNewFatalIssueEvent) -> None:
45 | print(alert)
46 |
47 |
48 | @crashlytics_fn.on_new_nonfatal_issue_published()
49 | def crashlyticsnewnonfatalissue(
50 | alert: crashlytics_fn.CrashlyticsNewNonfatalIssueEvent) -> None:
51 | print(alert)
52 |
53 |
54 | @crashlytics_fn.on_new_anr_issue_published()
55 | def crashlyticsnewanrissue(
56 | alert: crashlytics_fn.CrashlyticsNewAnrIssueEvent) -> None:
57 | print(alert)
58 |
59 |
60 | @crashlytics_fn.on_regression_alert_published()
61 | def crashlyticsregression(
62 | alert: crashlytics_fn.CrashlyticsRegressionAlertEvent) -> None:
63 | print(alert)
64 |
65 |
66 | @crashlytics_fn.on_stability_digest_published()
67 | def crashlyticsstabilitydigest(
68 | alert: crashlytics_fn.CrashlyticsStabilityDigestEvent) -> None:
69 | print(alert)
70 |
71 |
72 | @crashlytics_fn.on_velocity_alert_published()
73 | def crashlyticsvelocity(
74 | alert: crashlytics_fn.CrashlyticsVelocityAlertEvent) -> None:
75 | print(alert)
76 |
77 |
78 | @performance_fn.on_threshold_alert_published()
79 | def performancethreshold(
80 | alert: performance_fn.PerformanceThresholdAlertEvent) -> None:
81 | print(alert)
82 |
--------------------------------------------------------------------------------
/samples/basic_alerts/functions/requirements.txt:
--------------------------------------------------------------------------------
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
9 |
--------------------------------------------------------------------------------
/samples/basic_db/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "python-functions-testing"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/samples/basic_db/.gitignore:
--------------------------------------------------------------------------------
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
67 |
--------------------------------------------------------------------------------
/samples/basic_db/__init__.py:
--------------------------------------------------------------------------------
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
4 |
--------------------------------------------------------------------------------
/samples/basic_db/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "functions": [
3 | {
4 | "source": "functions",
5 | "codebase": "default",
6 | "ignore": [
7 | "venv"
8 | ]
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/samples/basic_db/functions/.gitignore:
--------------------------------------------------------------------------------
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__
14 |
--------------------------------------------------------------------------------
/samples/basic_db/functions/main.py:
--------------------------------------------------------------------------------
1 | """
2 | Example Firebase Functions for RTDB written in Python
3 | """
4 | from firebase_functions import db_fn, options
5 | from firebase_admin import initialize_app
6 |
7 | initialize_app()
8 |
9 | options.set_global_options(region=options.SupportedRegion.EUROPE_WEST1)
10 |
11 |
12 | @db_fn.on_value_written(reference="hello/world")
13 | def onwriteexample(event: db_fn.Event[db_fn.Change]) -> None:
14 | print("Hello from db write event:", event)
15 |
16 |
17 | @db_fn.on_value_created(reference="hello/world")
18 | def oncreatedexample(event: db_fn.Event) -> None:
19 | print("Hello from db create event:", event)
20 |
21 |
22 | @db_fn.on_value_deleted(reference="hello/world")
23 | def ondeletedexample(event: db_fn.Event) -> None:
24 | print("Hello from db delete event:", event)
25 |
26 |
27 | @db_fn.on_value_updated(reference="hello/world")
28 | def onupdatedexample(event: db_fn.Event[db_fn.Change]) -> None:
29 | print("Hello from db updated event:", event)
30 |
--------------------------------------------------------------------------------
/samples/basic_db/functions/requirements.txt:
--------------------------------------------------------------------------------
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
9 |
--------------------------------------------------------------------------------
/samples/basic_eventarc/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "python-functions-testing"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/samples/basic_eventarc/.gitignore:
--------------------------------------------------------------------------------
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
67 |
--------------------------------------------------------------------------------
/samples/basic_eventarc/__init__.py:
--------------------------------------------------------------------------------
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
4 |
--------------------------------------------------------------------------------
/samples/basic_eventarc/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "functions": [
3 | {
4 | "source": "functions",
5 | "codebase": "default",
6 | "ignore": [
7 | "venv"
8 | ]
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/samples/basic_eventarc/functions/.gitignore:
--------------------------------------------------------------------------------
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__
14 |
--------------------------------------------------------------------------------
/samples/basic_eventarc/functions/main.py:
--------------------------------------------------------------------------------
1 | """Firebase Cloud Functions for Eventarc triggers example."""
2 | from firebase_functions import eventarc_fn
3 |
4 |
5 | @eventarc_fn.on_custom_event_published(
6 | event_type="firebase.extensions.storage-resize-images.v1.complete",)
7 | def onimageresize(event: eventarc_fn.CloudEvent) -> None:
8 | """
9 | Handle image resize events from the Firebase Storage Resize Images extension.
10 | https://extensions.dev/extensions/firebase/storage-resize-images
11 | """
12 | print("Received image resize completed event", event)
13 |
--------------------------------------------------------------------------------
/samples/basic_eventarc/functions/requirements.txt:
--------------------------------------------------------------------------------
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
9 |
--------------------------------------------------------------------------------
/samples/basic_firestore/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "python-functions-testing"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/samples/basic_firestore/.gitignore:
--------------------------------------------------------------------------------
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
67 |
--------------------------------------------------------------------------------
/samples/basic_firestore/__init__.py:
--------------------------------------------------------------------------------
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
4 |
--------------------------------------------------------------------------------
/samples/basic_firestore/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "functions": [
3 | {
4 | "source": "functions",
5 | "codebase": "default",
6 | "ignore": [
7 | "venv"
8 | ]
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/samples/basic_firestore/functions/.gitignore:
--------------------------------------------------------------------------------
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__
14 |
--------------------------------------------------------------------------------
/samples/basic_firestore/functions/main.py:
--------------------------------------------------------------------------------
1 | """
2 | Example Firebase Functions for Firestore written in Python
3 | """
4 | from firebase_functions import firestore_fn, options
5 | from firebase_admin import initialize_app
6 |
7 | initialize_app()
8 |
9 | options.set_global_options(region=options.SupportedRegion.EUROPE_WEST1)
10 |
11 |
12 | @firestore_fn.on_document_written(document="hello/{world}")
13 | def onfirestoredocumentwritten(
14 | event: firestore_fn.Event[firestore_fn.Change]) -> None:
15 | print("Hello from Firestore document write event:", event)
16 |
17 |
18 | @firestore_fn.on_document_created(document="hello/world")
19 | def onfirestoredocumentcreated(event: firestore_fn.Event) -> None:
20 | print("Hello from Firestore document create event:", event)
21 |
22 |
23 | @firestore_fn.on_document_deleted(document="hello/world")
24 | def onfirestoredocumentdeleted(event: firestore_fn.Event) -> None:
25 | print("Hello from Firestore document delete event:", event)
26 |
27 |
28 | @firestore_fn.on_document_updated(document="hello/world")
29 | def onfirestoredocumentupdated(
30 | event: firestore_fn.Event[firestore_fn.Change]) -> None:
31 | print("Hello from Firestore document updated event:", event)
32 |
--------------------------------------------------------------------------------
/samples/basic_firestore/functions/requirements.txt:
--------------------------------------------------------------------------------
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
9 |
--------------------------------------------------------------------------------
/samples/basic_params/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "python-functions-testing"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/samples/basic_params/.gitignore:
--------------------------------------------------------------------------------
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
67 |
--------------------------------------------------------------------------------
/samples/basic_params/__init__.py:
--------------------------------------------------------------------------------
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
4 |
--------------------------------------------------------------------------------
/samples/basic_params/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "functions": [
3 | {
4 | "source": "functions",
5 | "codebase": "default",
6 | "ignore": [
7 | "venv"
8 | ]
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/samples/basic_params/functions/.gitignore:
--------------------------------------------------------------------------------
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__
14 |
--------------------------------------------------------------------------------
/samples/basic_params/functions/main.py:
--------------------------------------------------------------------------------
1 | """
2 | Example Function params & inputs.
3 | """
4 | from firebase_functions import storage_fn, params
5 | from firebase_admin import initialize_app
6 |
7 | initialize_app()
8 |
9 | bucket = params.StringParam(
10 | "BUCKET",
11 | label="storage bucket",
12 | description="The bucket to resize images from.",
13 | input=params.ResourceInput(type=params.ResourceType.STORAGE_BUCKET),
14 | default=params.STORAGE_BUCKET,
15 | )
16 |
17 | output_path = params.StringParam(
18 | "OUTPUT_PATH",
19 | label="storage bucket output path",
20 | description=
21 | "The path of in the bucket where processed images will be stored.",
22 | input=params.TextInput(
23 | example="/images/processed",
24 | validation_regex=r"^\/.*$",
25 | validation_error_message=
26 | "Must be a valid path starting with a forward slash",
27 | ),
28 | default="/images/processed",
29 | )
30 |
31 | image_type = params.ListParam(
32 | "IMAGE_TYPE",
33 | label="convert image to preferred types",
34 | description="The image types you'd like your source image to convert to.",
35 | input=params.MultiSelectInput([
36 | params.SelectOption(value="jpeg", label="jpeg"),
37 | params.SelectOption(value="png", label="png"),
38 | params.SelectOption(value="webp", label="webp"),
39 | ]),
40 | default=["jpeg", "png"],
41 | )
42 |
43 | delete_original = params.BoolParam(
44 | "DELETE_ORIGINAL_FILE",
45 | label="delete the original file",
46 | description=
47 | "Do you want to automatically delete the original file from the Cloud Storage?",
48 | input=params.SelectInput([
49 | params.SelectOption(value=True, label="Delete on any resize attempt"),
50 | params.SelectOption(value=False, label="Don't delete"),
51 | ],),
52 | default=True,
53 | )
54 |
55 | image_resize_api_secret = params.SecretParam(
56 | "IMAGE_RESIZE_API_SECRET",
57 | label="image resize api secret",
58 | description="The fake secret key to use for the image resize API.",
59 | )
60 |
61 |
62 | @storage_fn.on_object_finalized(
63 | bucket=bucket,
64 | secrets=[image_resize_api_secret],
65 | )
66 | def resize_images(event: storage_fn.CloudEvent[storage_fn.StorageObjectData]):
67 | """
68 | This function will be triggered when a new object is created in the bucket.
69 | """
70 | print("Got a new image:", event)
71 | print("Selected image types:", image_type.value)
72 | print("Selected bucket resource:", bucket.value)
73 | print("Selected output location:", output_path.value)
74 | print("Testing a not so secret api key:", image_resize_api_secret.value)
75 | print("Should original images be deleted?:", delete_original.value)
76 | # TODO: Implement your image resize logic
77 |
--------------------------------------------------------------------------------
/samples/basic_params/functions/requirements.txt:
--------------------------------------------------------------------------------
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
9 |
--------------------------------------------------------------------------------
/samples/basic_scheduler/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "python-functions-testing"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/samples/basic_scheduler/.gitignore:
--------------------------------------------------------------------------------
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
67 |
--------------------------------------------------------------------------------
/samples/basic_scheduler/__init__.py:
--------------------------------------------------------------------------------
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
4 |
--------------------------------------------------------------------------------
/samples/basic_scheduler/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "functions": [
3 | {
4 | "source": "functions",
5 | "codebase": "default",
6 | "ignore": [
7 | "venv"
8 | ]
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/samples/basic_scheduler/functions/.gitignore:
--------------------------------------------------------------------------------
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__
14 |
--------------------------------------------------------------------------------
/samples/basic_scheduler/functions/main.py:
--------------------------------------------------------------------------------
1 | """Firebase Scheduled Cloud Functions example."""
2 |
3 | from firebase_functions import scheduler_fn
4 |
5 |
6 | @scheduler_fn.on_schedule(
7 | schedule="* * * * *",
8 | timezone=scheduler_fn.Timezone("America/Los_Angeles"),
9 | )
10 | def example(event: scheduler_fn.ScheduledEvent) -> None:
11 | print(event.job_name)
12 | print(event.schedule_time)
13 |
--------------------------------------------------------------------------------
/samples/basic_scheduler/functions/requirements.txt:
--------------------------------------------------------------------------------
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
9 |
--------------------------------------------------------------------------------
/samples/basic_storage/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "python-functions-testing"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/samples/basic_storage/.gitignore:
--------------------------------------------------------------------------------
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
67 |
--------------------------------------------------------------------------------
/samples/basic_storage/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "functions": [
3 | {
4 | "source": "functions",
5 | "codebase": "default"
6 | }
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/samples/basic_storage/functions/.gitignore:
--------------------------------------------------------------------------------
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__
14 |
--------------------------------------------------------------------------------
/samples/basic_storage/functions/__init__.py:
--------------------------------------------------------------------------------
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
4 |
--------------------------------------------------------------------------------
/samples/basic_storage/functions/main.py:
--------------------------------------------------------------------------------
1 | """
2 | Example Firebase Functions for Storage triggers.
3 | """
4 |
5 | from firebase_functions import storage_fn
6 | from firebase_functions.storage_fn import StorageObjectData, CloudEvent
7 | from firebase_admin import initialize_app
8 |
9 | initialize_app()
10 |
11 |
12 | @storage_fn.on_object_finalized()
13 | def onobjectfinalizedexample(event: CloudEvent[StorageObjectData]):
14 | """
15 | This function will be triggered when a new object is created in the bucket.
16 | """
17 | print(event)
18 |
19 |
20 | @storage_fn.on_object_archived()
21 | def onobjectarchivedexample(event: CloudEvent[StorageObjectData]):
22 | """
23 | This function will be triggered when an object is archived in the bucket.
24 | """
25 | print(event)
26 |
27 |
28 | @storage_fn.on_object_deleted()
29 | def onobjectdeletedexample(event: CloudEvent[StorageObjectData]):
30 | """
31 | This function will be triggered when an object is deleted in the bucket.
32 | """
33 | print(event)
34 |
35 |
36 | @storage_fn.on_object_metadata_updated()
37 | def onobjectmetadataupdatedexample(event: CloudEvent[StorageObjectData]):
38 | """
39 | This function will be triggered when an object's metadata is updated in the bucket.
40 | """
41 | print(event)
42 |
--------------------------------------------------------------------------------
/samples/basic_storage/functions/requirements.txt:
--------------------------------------------------------------------------------
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
9 |
--------------------------------------------------------------------------------
/samples/basic_tasks/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "python-functions-testing"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/samples/basic_tasks/.gitignore:
--------------------------------------------------------------------------------
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
67 |
--------------------------------------------------------------------------------
/samples/basic_tasks/__init__.py:
--------------------------------------------------------------------------------
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
4 |
--------------------------------------------------------------------------------
/samples/basic_tasks/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "functions": [
3 | {
4 | "source": "functions",
5 | "codebase": "default",
6 | "ignore": [
7 | "venv"
8 | ]
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/samples/basic_tasks/functions/.gitignore:
--------------------------------------------------------------------------------
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__
14 |
--------------------------------------------------------------------------------
/samples/basic_tasks/functions/main.py:
--------------------------------------------------------------------------------
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--.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.")
70 |
--------------------------------------------------------------------------------
/samples/basic_tasks/functions/requirements.txt:
--------------------------------------------------------------------------------
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
9 | google-cloud-tasks >= 2.13.1
--------------------------------------------------------------------------------
/samples/basic_test_lab/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "python-functions-testing"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/samples/basic_test_lab/.gitignore:
--------------------------------------------------------------------------------
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
67 |
--------------------------------------------------------------------------------
/samples/basic_test_lab/__init__.py:
--------------------------------------------------------------------------------
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
4 |
--------------------------------------------------------------------------------
/samples/basic_test_lab/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "functions": [
3 | {
4 | "source": "functions",
5 | "codebase": "default",
6 | "ignore": [
7 | "venv"
8 | ]
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/samples/basic_test_lab/functions/.gitignore:
--------------------------------------------------------------------------------
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__
14 |
--------------------------------------------------------------------------------
/samples/basic_test_lab/functions/main.py:
--------------------------------------------------------------------------------
1 | """Firebase Cloud Functions for Test Lab."""
2 | from firebase_functions.test_lab_fn import (
3 | CloudEvent,
4 | TestMatrixCompletedData,
5 | on_test_matrix_completed,
6 | )
7 |
8 |
9 | @on_test_matrix_completed()
10 | def testmatrixcompleted(event: CloudEvent[TestMatrixCompletedData]) -> None:
11 | print(f"Test Matrix ID: {event.data.test_matrix_id}")
12 | print(f"Test Matrix State: {event.data.state}")
13 | print(f"Test Matrix Outcome Summary: {event.data.outcome_summary}")
14 |
15 | print("Result Storage:")
16 | print(
17 | f" Tool Results History: {event.data.result_storage.tool_results_history}"
18 | )
19 | print(f" Results URI: {event.data.result_storage.results_uri}")
20 | print(f" GCS Path: {event.data.result_storage.gcs_path}")
21 | print(
22 | f" Tool Results Execution: {event.data.result_storage.tool_results_execution}"
23 | )
24 |
25 | print("Client Info:")
26 | print(f" Client: {event.data.client_info.client}")
27 | print(f" Details: {event.data.client_info.details}")
28 |
--------------------------------------------------------------------------------
/samples/basic_test_lab/functions/requirements.txt:
--------------------------------------------------------------------------------
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
9 |
--------------------------------------------------------------------------------
/samples/https_flask/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "python-functions-testing"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/samples/https_flask/.gitignore:
--------------------------------------------------------------------------------
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
67 |
--------------------------------------------------------------------------------
/samples/https_flask/__init__.py:
--------------------------------------------------------------------------------
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
4 |
--------------------------------------------------------------------------------
/samples/https_flask/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "functions": [
3 | {
4 | "source": "functions",
5 | "codebase": "default",
6 | "ignore": [
7 | "venv"
8 | ]
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/samples/https_flask/functions/.gitignore:
--------------------------------------------------------------------------------
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__
14 |
--------------------------------------------------------------------------------
/samples/https_flask/functions/README.md:
--------------------------------------------------------------------------------
1 | # Flask & Firebase Functions Example
2 |
3 | Getting started locally:
4 |
5 | ```bash
6 | python3.10 -m venv venv
7 | source venv/bin/activate
8 | pip3 install --upgrade pip
9 | python3.10 -m pip install -r requirements.txt
10 | ```
11 |
--------------------------------------------------------------------------------
/samples/https_flask/functions/main.py:
--------------------------------------------------------------------------------
1 | """
2 | Example Firebase Functions with Flask.
3 | """
4 |
5 | from flask import Flask
6 | from functions_wrapper import entrypoint
7 |
8 | from firebase_functions import https_fn
9 |
10 | app = Flask(__name__)
11 |
12 |
13 | @app.route("/hello")
14 | def hello():
15 | return "Hello!"
16 |
17 |
18 | @app.route("/world")
19 | def world():
20 | return "Hello World!"
21 |
22 |
23 | @https_fn.on_request()
24 | def httpsflaskexample(request):
25 | return entrypoint(app, request)
26 |
--------------------------------------------------------------------------------
/samples/https_flask/functions/requirements.txt:
--------------------------------------------------------------------------------
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
9 | functions_wrapper >= 1.0.1
10 |
--------------------------------------------------------------------------------
/samples/identity/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "python-functions-testing"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/samples/identity/.gitignore:
--------------------------------------------------------------------------------
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
67 |
--------------------------------------------------------------------------------
/samples/identity/__init__.py:
--------------------------------------------------------------------------------
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
4 |
--------------------------------------------------------------------------------
/samples/identity/client/index.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/samples/identity/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "functions": [
3 | {
4 | "source": "functions",
5 | "codebase": "default",
6 | "ignore": [
7 | "venv"
8 | ]
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/samples/identity/functions/.gitignore:
--------------------------------------------------------------------------------
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__
14 |
--------------------------------------------------------------------------------
/samples/identity/functions/main.py:
--------------------------------------------------------------------------------
1 | """Firebase Cloud Functions for blocking auth functions example."""
2 | from firebase_functions import identity_fn
3 |
4 |
5 | @identity_fn.before_user_created(
6 | id_token=True,
7 | access_token=True,
8 | refresh_token=True,
9 | )
10 | def beforeusercreated(
11 | event: identity_fn.AuthBlockingEvent
12 | ) -> identity_fn.BeforeCreateResponse | None:
13 | print(event)
14 | if not event.data.email:
15 | return None
16 | if "@cats.com" in event.data.email:
17 | return identity_fn.BeforeCreateResponse(display_name="Meow!",)
18 | if "@dogs.com" in event.data.email:
19 | return identity_fn.BeforeCreateResponse(display_name="Woof!",)
20 | return None
21 |
22 |
23 | @identity_fn.before_user_signed_in(
24 | id_token=True,
25 | access_token=True,
26 | refresh_token=True,
27 | )
28 | def beforeusersignedin(
29 | event: identity_fn.AuthBlockingEvent
30 | ) -> identity_fn.BeforeSignInResponse | None:
31 | print(event)
32 | if not event.data.email:
33 | return None
34 |
35 | if "@cats.com" in event.data.email:
36 | return identity_fn.BeforeSignInResponse(session_claims={"emoji": "🐈"})
37 |
38 | if "@dogs.com" in event.data.email:
39 | return identity_fn.BeforeSignInResponse(session_claims={"emoji": "🐕"})
40 |
41 | return None
42 |
--------------------------------------------------------------------------------
/samples/identity/functions/requirements.txt:
--------------------------------------------------------------------------------
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
9 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
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 | """
15 | Setup for Firebase Functions Python.
16 | """
17 | from os import path
18 | from setuptools import find_packages, setup
19 |
20 | install_requires = [
21 | 'flask>=2.1.2', 'functions-framework>=3.0.0', 'firebase-admin>=6.0.0',
22 | 'pyyaml>=6.0', 'typing-extensions>=4.4.0', 'cloudevents>=1.2.0,<2.0.0',
23 | 'flask-cors>=3.0.10', 'pyjwt[crypto]>=2.5.0', 'google-events==0.5.0',
24 | 'google-cloud-firestore>=2.11.0'
25 | ]
26 |
27 | dev_requires = [
28 | 'pytest>=7.1.2', 'setuptools>=63.4.2', 'pylint>=2.16.1',
29 | 'pytest-cov>=3.0.0', 'mypy>=1.0.0', 'sphinx>=6.1.3',
30 | 'sphinxcontrib-napoleon>=0.7', 'yapf>=0.32.0', 'toml>=0.10.2',
31 | 'google-cloud-tasks>=2.13.1'
32 | ]
33 |
34 | # Read in the package metadata per recommendations from:
35 | # https://packaging.python.org/guides/single-sourcing-package-version/
36 | init_path = path.join(path.dirname(path.abspath(__file__)), 'src',
37 | 'firebase_functions', '__init__.py')
38 | version = {}
39 | with open(init_path) as fp:
40 | exec(fp.read(), version) # pylint: disable=exec-used
41 |
42 | long_description = (
43 | 'The Firebase Functions Python SDK provides an SDK for defining'
44 | ' Cloud Functions for Firebase.')
45 |
46 | setup(
47 | name='firebase_functions',
48 | version=version['__version__'],
49 | description='Firebase Functions Python SDK',
50 | long_description=long_description,
51 | url='https://github.com/firebase/firebase-functions-python',
52 | author='Firebase Team',
53 | keywords=['firebase', 'functions', 'google', 'cloud'],
54 | license='Apache License 2.0',
55 | install_requires=install_requires,
56 | extras_require={'dev': dev_requires},
57 | packages=find_packages(where='src'),
58 | package_dir={'': 'src'},
59 | include_package_data=True,
60 | package_data={'firebase_functions': ['py.typed']},
61 | python_requires='>=3.10',
62 | classifiers=[
63 | 'Development Status :: 4 - Beta',
64 | 'Intended Audience :: Developers',
65 | 'Topic :: Software Development :: Build Tools',
66 | 'Programming Language :: Python :: 3.10',
67 | 'Programming Language :: Python :: 3.11',
68 | ],
69 | )
70 |
--------------------------------------------------------------------------------
/src/firebase_functions/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright 2025 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 | Firebase Functions for Python.
16 | """
17 |
18 | __version__ = "0.4.3"
19 |
--------------------------------------------------------------------------------
/src/firebase_functions/alerts/__init__.py:
--------------------------------------------------------------------------------
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 | """
15 | Cloud functions to handle events from Firebase Alerts.
16 | Subpackages give stronger typing to specific services which
17 | notify users via Firebase Alerts.
18 | """
19 |
20 | import dataclasses as _dataclasses
21 | import datetime as _dt
22 | import typing as _typing
23 |
24 | from firebase_functions.core import T
25 |
26 |
27 | @_dataclasses.dataclass(frozen=True)
28 | class FirebaseAlertData(_typing.Generic[T]):
29 | """
30 | The CloudEvent data emitted by Firebase Alerts.
31 | """
32 |
33 | create_time: _dt.datetime
34 | """
35 | The time the alert was created.
36 | """
37 |
38 | end_time: _dt.datetime | None
39 | """
40 | The time the alert ended. This is only set for alerts that have ended.
41 | """
42 |
43 | payload: T
44 | """
45 | Payload of the event, which includes the details of the specific alert.
46 | """
47 |
--------------------------------------------------------------------------------
/src/firebase_functions/alerts/billing_fn.py:
--------------------------------------------------------------------------------
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 billing events from Firebase Alerts.
17 | """
18 | import dataclasses as _dataclasses
19 | import functools as _functools
20 | import typing as _typing
21 | import cloudevents.http as _ce
22 | from firebase_functions.alerts import FirebaseAlertData
23 |
24 | import firebase_functions.private.util as _util
25 |
26 | from firebase_functions.core import T, CloudEvent
27 | from firebase_functions.options import BillingOptions
28 |
29 |
30 | @_dataclasses.dataclass(frozen=True)
31 | class PlanAutomatedUpdatePayload:
32 | """
33 | The internal payload object for billing plan automated updates.
34 | Payload is wrapped inside a `FirebaseAlertData` object.
35 | """
36 |
37 | billing_plan: str
38 | """
39 | A Firebase billing plan, e.g. "spark" or "blaze".
40 | """
41 |
42 | notification_type: str
43 | """
44 | The type of the notification, e.g. "upgrade_plan" or "downgrade_plan".
45 | """
46 |
47 |
48 | @_dataclasses.dataclass(frozen=True)
49 | class PlanUpdatePayload(PlanAutomatedUpdatePayload):
50 | """
51 | The internal payload object for billing plan updates.
52 | Payload is wrapped inside a `FirebaseAlertData` object.
53 | """
54 |
55 | principal_email: str
56 | """
57 | The email address of the person that triggered billing plan change.
58 | """
59 |
60 |
61 | @_dataclasses.dataclass(frozen=True)
62 | class BillingEvent(CloudEvent[FirebaseAlertData[T]]):
63 | """
64 | A custom CloudEvent for billing Firebase Alerts.
65 | """
66 |
67 | alert_type: str
68 | """
69 | The type of the alerts that got triggered.
70 | """
71 |
72 |
73 | BillingPlanUpdateEvent = BillingEvent[PlanUpdatePayload]
74 | """
75 | The type of the event for 'on_plan_update_published' functions.
76 | """
77 |
78 | BillingPlanAutomatedUpdateEvent = BillingEvent[PlanAutomatedUpdatePayload]
79 | """
80 | The type of the event for 'on_plan_automated_update_published' functions.
81 | """
82 |
83 | OnPlanUpdatePublishedCallable = _typing.Callable[[BillingPlanUpdateEvent], None]
84 | """
85 | The type of the callable for 'on_plan_update_published' functions.
86 | """
87 |
88 | OnPlanAutomatedUpdatePublishedCallable = _typing.Callable[
89 | [BillingPlanAutomatedUpdateEvent], None]
90 | """
91 | The type of the callable for 'on_plan_automated_update_published' functions.
92 | """
93 |
94 |
95 | @_util.copy_func_kwargs(BillingOptions)
96 | def on_plan_update_published(
97 | **kwargs
98 | ) -> _typing.Callable[[OnPlanUpdatePublishedCallable],
99 | OnPlanUpdatePublishedCallable]:
100 | """
101 | Event handler which triggers when a Firebase Alerts billing event is published.
102 |
103 | Example:
104 |
105 | .. code-block:: python
106 |
107 | import firebase_functions.alerts.billing_fn as billing_fn
108 |
109 | @billing_fn.on_plan_update_published()
110 | def example(alert: billing_fn.BillingPlanUpdateEvent) -> None:
111 | print(alert)
112 |
113 | :param \\*\\*kwargs: Options.
114 | :type \\*\\*kwargs: as :exc:`firebase_functions.options.BillingOptions`
115 | :rtype: :exc:`typing.Callable`
116 | \\[
117 | \\[ :exc:`firebase_functions.alerts.billing_fn.BillingPlanUpdateEvent` \\],
118 | `None`
119 | \\]
120 | A function that takes a BillingPlanUpdateEvent and returns None.
121 | """
122 | options = BillingOptions(**kwargs)
123 |
124 | def on_plan_update_published_inner_decorator(
125 | func: OnPlanUpdatePublishedCallable):
126 |
127 | @_functools.wraps(func)
128 | def on_plan_update_published_wrapped(raw: _ce.CloudEvent):
129 | from firebase_functions.private._alerts_fn import billing_event_from_ce
130 | func(billing_event_from_ce(raw))
131 |
132 | _util.set_func_endpoint_attr(
133 | on_plan_update_published_wrapped,
134 | options._endpoint(
135 | func_name=func.__name__,
136 | alert_type='billing.planUpdate',
137 | ),
138 | )
139 | return on_plan_update_published_wrapped
140 |
141 | return on_plan_update_published_inner_decorator
142 |
143 |
144 | @_util.copy_func_kwargs(BillingOptions)
145 | def on_plan_automated_update_published(
146 | **kwargs
147 | ) -> _typing.Callable[[OnPlanAutomatedUpdatePublishedCallable],
148 | OnPlanAutomatedUpdatePublishedCallable]:
149 | """
150 | Event handler which triggers when a Firebase Alerts billing event is published.
151 |
152 | Example:
153 |
154 | .. code-block:: python
155 |
156 | import firebase_functions.alerts.billing_fn as billing_fn
157 |
158 | @billing_fn.on_plan_automated_update_published()
159 | def example(alert: billing_fn.BillingPlanAutomatedUpdateEvent) -> None:
160 | print(alert)
161 |
162 | :param \\*\\*kwargs: Options.
163 | :type \\*\\*kwargs: as :exc:`firebase_functions.options.BillingOptions`
164 | :rtype: :exc:`typing.Callable`
165 | \\[
166 | \\[ :exc:`firebase_functions.alerts.billing_fn.BillingPlanAutomatedUpdateEvent` \\],
167 | `None`
168 | \\]
169 | A function that takes a BillingPlanUpdateEvent and returns None.
170 | """
171 | options = BillingOptions(**kwargs)
172 |
173 | def on_plan_automated_update_published_inner_decorator(
174 | func: OnPlanAutomatedUpdatePublishedCallable):
175 |
176 | @_functools.wraps(func)
177 | def on_plan_automated_update_published_wrapped(raw: _ce.CloudEvent):
178 | from firebase_functions.private._alerts_fn import billing_event_from_ce
179 | func(billing_event_from_ce(raw))
180 |
181 | _util.set_func_endpoint_attr(
182 | on_plan_automated_update_published_wrapped,
183 | options._endpoint(
184 | func_name=func.__name__,
185 | alert_type='billing.planAutomatedUpdate',
186 | ),
187 | )
188 | return on_plan_automated_update_published_wrapped
189 |
190 | return on_plan_automated_update_published_inner_decorator
191 |
--------------------------------------------------------------------------------
/src/firebase_functions/alerts/performance_fn.py:
--------------------------------------------------------------------------------
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 Firebase Performance Monitoring events from Firebase Alerts.
17 | """
18 |
19 | import dataclasses as _dataclasses
20 | import functools as _functools
21 | import typing as _typing
22 | import cloudevents.http as _ce
23 | from firebase_functions.alerts import FirebaseAlertData
24 |
25 | import firebase_functions.private.util as _util
26 |
27 | from firebase_functions.core import T, CloudEvent
28 | from firebase_functions.options import PerformanceOptions
29 |
30 |
31 | @_dataclasses.dataclass(frozen=True)
32 | class ThresholdAlertPayload:
33 | """
34 | The internal payload object for a performance threshold alert.
35 | Payload is wrapped inside a FirebaseAlertData object.
36 | """
37 |
38 | event_name: str
39 | """
40 | Name of the trace or network request this alert is for
41 | (e.g. my_custom_trace, firebase.com/api/123).
42 | """
43 |
44 | event_type: str
45 | """
46 | The resource type this alert is for (i.e. trace, network request,
47 | screen rendering, etc.).
48 | """
49 |
50 | metric_type: str
51 | """
52 | The metric type this alert is for (i.e. success rate,
53 | response time, duration, etc.).
54 | """
55 |
56 | num_samples: int
57 | """
58 | The number of events checked for this alert condition.
59 | """
60 |
61 | threshold_value: float
62 | """
63 | The threshold value of the alert condition without units (e.g. "75", "2.1").
64 | """
65 |
66 | threshold_unit: str
67 | """
68 | The unit for the alert threshold (e.g. "percent", "seconds").
69 | """
70 |
71 | violation_value: float | int
72 | """
73 | The value that violated the alert condition (e.g. "76.5", "3").
74 | """
75 |
76 | violation_unit: str
77 | """
78 | The unit for the violation value (e.g. "percent", "seconds").
79 | """
80 |
81 | investigate_uri: str
82 | """
83 | The link to Firebase Console to investigate more into this alert.
84 | """
85 |
86 | condition_percentile: float | int | None = None
87 | """
88 | The percentile of the alert condition, can be 0 if percentile
89 | is not applicable to the alert condition and omitted;
90 | range: [1, 100].
91 | """
92 |
93 | app_version: str | None = None
94 | """
95 | The app version this alert was triggered for, can be omitted
96 | if the alert is for a network request (because the alert was
97 | checked against data from all versions of app) or a web app
98 | (where the app is versionless).
99 | """
100 |
101 |
102 | @_dataclasses.dataclass(frozen=True)
103 | class PerformanceEvent(CloudEvent[FirebaseAlertData[T]]):
104 | """
105 | A custom CloudEvent for billing Firebase Alerts.
106 | """
107 |
108 | alert_type: str
109 | """
110 | The type of the alerts that got triggered.
111 | """
112 |
113 | app_id: str
114 | """
115 | The Firebase App ID that's associated with the alert.
116 | """
117 |
118 |
119 | PerformanceThresholdAlertEvent = PerformanceEvent[ThresholdAlertPayload]
120 | """
121 | The type of the event for 'on_threshold_alert_published' functions.
122 | """
123 |
124 | OnThresholdAlertPublishedCallable = _typing.Callable[
125 | [PerformanceThresholdAlertEvent], None]
126 | """
127 | The type of the callable for 'on_threshold_alert_published' functions.
128 | """
129 |
130 |
131 | @_util.copy_func_kwargs(PerformanceOptions)
132 | def on_threshold_alert_published(
133 | **kwargs
134 | ) -> _typing.Callable[[OnThresholdAlertPublishedCallable],
135 | OnThresholdAlertPublishedCallable]:
136 | """
137 | Event handler which runs every time a threshold alert is received.
138 |
139 | Example:
140 |
141 | .. code-block:: python
142 |
143 | import firebase_functions.alerts.performance_fn as performance_fn
144 |
145 | @performance_fn.on_threshold_alert_published()
146 | def example(alert: performance_fn.PerformanceThresholdAlertEvent) -> None:
147 | print(alert)
148 |
149 | :param \\*\\*kwargs: Options.
150 | :type \\*\\*kwargs: as :exc:`firebase_functions.options.PerformanceOptions`
151 | :rtype: :exc:`typing.Callable`
152 | \\[
153 | \\[ :exc:`firebase_functions.alerts.performance_fn.PerformanceThresholdAlertEvent` \\],
154 | `None`
155 | \\]
156 | A function that takes a PerformanceThresholdAlertEvent and returns None.
157 | """
158 | options = PerformanceOptions(**kwargs)
159 |
160 | def on_threshold_alert_published_inner_decorator(
161 | func: OnThresholdAlertPublishedCallable):
162 |
163 | @_functools.wraps(func)
164 | def on_threshold_alert_published_wrapped(raw: _ce.CloudEvent):
165 | from firebase_functions.private._alerts_fn import performance_event_from_ce
166 | func(performance_event_from_ce(raw))
167 |
168 | _util.set_func_endpoint_attr(
169 | on_threshold_alert_published_wrapped,
170 | options._endpoint(
171 | func_name=func.__name__,
172 | alert_type='performance.threshold',
173 | ),
174 | )
175 | return on_threshold_alert_published_wrapped
176 |
177 | return on_threshold_alert_published_inner_decorator
178 |
--------------------------------------------------------------------------------
/src/firebase_functions/alerts_fn.py:
--------------------------------------------------------------------------------
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 events from Firebase Alerts.
17 | """
18 |
19 | import dataclasses as _dataclasses
20 | import functools as _functools
21 | import typing as _typing
22 | import cloudevents.http as _ce
23 | from firebase_functions.alerts import FirebaseAlertData
24 |
25 | import firebase_functions.private.util as _util
26 |
27 | from firebase_functions.core import T, CloudEvent as _CloudEvent, _with_init
28 | from firebase_functions.options import FirebaseAlertOptions
29 |
30 | # Explicitly import AlertType to make it available in the public API.
31 | # pylint: disable=unused-import
32 | from firebase_functions.options import AlertType
33 |
34 |
35 | @_dataclasses.dataclass(frozen=True)
36 | class AlertEvent(_CloudEvent[T]):
37 | """
38 | A custom CloudEvent for Firebase Alerts (with custom extension attributes).
39 | """
40 |
41 | alert_type: str
42 | """
43 | The type of the alerts that got triggered.
44 | """
45 |
46 | app_id: str | None
47 | """
48 | The Firebase App ID that's associated with the alert. This is optional,
49 | and only present when the alert is targeting a specific Firebase App.
50 | """
51 |
52 |
53 | OnAlertPublishedEvent = AlertEvent[FirebaseAlertData[T]]
54 | """
55 | The type of the event for 'on_alert_published' functions.
56 | """
57 |
58 | OnAlertPublishedCallable = _typing.Callable[[OnAlertPublishedEvent], None]
59 | """
60 | The type of the callable for 'on_alert_published' functions.
61 | """
62 |
63 |
64 | @_util.copy_func_kwargs(FirebaseAlertOptions)
65 | def on_alert_published(
66 | **kwargs
67 | ) -> _typing.Callable[[OnAlertPublishedCallable], OnAlertPublishedCallable]:
68 | """
69 | Event handler that triggers when a Firebase Alerts event is published.
70 |
71 | Example:
72 |
73 | .. code-block:: python
74 |
75 | from firebase_functions import alerts_fn
76 |
77 | @alerts_fn.on_alert_published(
78 | alert_type=alerts_fn.AlertType.CRASHLYTICS_NEW_FATAL_ISSUE,
79 | )
80 | def example(alert: alerts_fn.AlertEvent[alerts_fn.FirebaseAlertData]) -> None:
81 | print(alert)
82 |
83 | :param \\*\\*kwargs: Options.
84 | :type \\*\\*kwargs: as :exc:`firebase_functions.options.FirebaseAlertOptions`
85 | :rtype: :exc:`typing.Callable`
86 | \\[ \\[ :exc:`firebase_functions.alerts_fn.AlertEvent` \\[
87 | :exc:`firebase_functions.alerts_fn.FirebaseAlertData` \\[
88 | :exc:`typing.Any` \\] \\] \\], `None` \\]
89 | A function that takes a AlertEvent and returns None.
90 | """
91 | options = FirebaseAlertOptions(**kwargs)
92 |
93 | def on_alert_published_inner_decorator(func: OnAlertPublishedCallable):
94 |
95 | @_functools.wraps(func)
96 | def on_alert_published_wrapped(raw: _ce.CloudEvent):
97 | from firebase_functions.private._alerts_fn import alerts_event_from_ce
98 | _with_init(func)(alerts_event_from_ce(raw))
99 |
100 | _util.set_func_endpoint_attr(
101 | on_alert_published_wrapped,
102 | options._endpoint(func_name=func.__name__),
103 | )
104 | return on_alert_published_wrapped
105 |
106 | return on_alert_published_inner_decorator
107 |
--------------------------------------------------------------------------------
/src/firebase_functions/core.py:
--------------------------------------------------------------------------------
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 | """
15 | Public code that is shared across modules.
16 | """
17 | import dataclasses as _dataclass
18 | import datetime as _datetime
19 | import typing as _typing
20 |
21 | from . import logger as _logger
22 |
23 | T = _typing.TypeVar("T")
24 |
25 |
26 | @_dataclass.dataclass(frozen=True)
27 | class CloudEvent(_typing.Generic[T]):
28 | """
29 | A CloudEvent is the base of a cross-platform format for encoding a serverless event.
30 | More information can be found at https://github.com/cloudevents/spec.
31 | """
32 |
33 | specversion: str
34 | """
35 | Version of the CloudEvents spec for this event.
36 | """
37 |
38 | id: str
39 | """
40 | A globally unique ID for this event.
41 | """
42 |
43 | source: str
44 | """
45 | The resource which published this event.
46 | """
47 |
48 | type: str
49 | """
50 | The type of event that this represents.
51 | """
52 |
53 | time: _datetime.datetime
54 | """
55 | When this event occurred.
56 | """
57 |
58 | data: T
59 | """
60 | Information about this specific event.
61 | """
62 |
63 | subject: str | None
64 | """
65 | The resource, provided by source, that this event relates to.
66 | """
67 |
68 |
69 | @_dataclass.dataclass(frozen=True)
70 | class Change(_typing.Generic[T]):
71 | """
72 | The Cloud Functions interface for events that change state, such as
73 | Realtime Database `on_value_written`.
74 | """
75 |
76 | before: T
77 | """
78 | The state of data before the change.
79 | """
80 |
81 | after: T
82 | """
83 | The state of data after the change.
84 | """
85 |
86 |
87 | _did_init = False
88 | _init_callback: _typing.Callable[[], _typing.Any] | None = None
89 |
90 |
91 | def init(callback: _typing.Callable[[], _typing.Any]) -> None:
92 | """
93 | Registers a function that should be run when in a production environment
94 | before executing any functions code.
95 | Calling this decorator more than once leads to undefined behavior.
96 | """
97 |
98 | global _did_init
99 | global _init_callback
100 |
101 | if _did_init:
102 | _logger.warn(
103 | "Setting init callback more than once. Only the most recent callback will be called"
104 | )
105 |
106 | _init_callback = callback
107 | _did_init = False
108 |
109 |
110 | def _with_init(
111 | fn: _typing.Callable[...,
112 | _typing.Any]) -> _typing.Callable[..., _typing.Any]:
113 | """
114 | A decorator that runs the init callback before running the decorated function.
115 | """
116 |
117 | def wrapper(*args, **kwargs):
118 | global _did_init
119 |
120 | if not _did_init:
121 | if _init_callback is not None:
122 | _init_callback()
123 | _did_init = True
124 |
125 | return fn(*args, **kwargs)
126 |
127 | return wrapper
128 |
--------------------------------------------------------------------------------
/src/firebase_functions/eventarc_fn.py:
--------------------------------------------------------------------------------
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 | """Cloud functions to handle Eventarc events."""
15 |
16 | # pylint: disable=protected-access
17 | import typing as _typing
18 | import functools as _functools
19 | import datetime as _dt
20 | import cloudevents.http as _ce
21 |
22 | import firebase_functions.options as _options
23 | import firebase_functions.private.util as _util
24 | from firebase_functions.core import CloudEvent, _with_init
25 |
26 |
27 | @_util.copy_func_kwargs(_options.EventarcTriggerOptions)
28 | def on_custom_event_published(
29 | **kwargs
30 | ) -> _typing.Callable[[_typing.Callable[[CloudEvent], None]], _typing.Callable[
31 | [CloudEvent], None]]:
32 | """
33 | Creates a handler for events published on the default event eventarc channel.
34 |
35 | Example:
36 |
37 | .. code-block:: python
38 |
39 | from firebase_functions import eventarc_fn
40 |
41 | @eventarc_fn.on_custom_event_published(
42 | event_type="firebase.extensions.storage-resize-images.v1.complete",
43 | )
44 | def onimageresize(event: eventarc_fn.CloudEvent) -> None:
45 | pass
46 |
47 | :param \\*\\*kwargs: Options.
48 | :type \\*\\*kwargs: as :exc:`firebase_functions.options.EventarcTriggerOptions`
49 | :rtype: :exc:`typing.Callable`
50 | \\[ \\[ :exc:`firebase_functions.core.CloudEvent` \\], `None` \\]
51 | A function that takes a CloudEvent and returns None.
52 | """
53 | options = _options.EventarcTriggerOptions(**kwargs)
54 |
55 | def on_custom_event_published_decorator(func: _typing.Callable[[CloudEvent],
56 | None]):
57 |
58 | @_functools.wraps(func)
59 | def on_custom_event_published_wrapped(raw: _ce.CloudEvent):
60 | event_attributes = raw._get_attributes()
61 | event_data: _typing.Any = raw.get_data()
62 | event_dict = {**event_data, **event_attributes}
63 | event: CloudEvent = CloudEvent(
64 | data=event_data,
65 | id=event_dict["id"],
66 | source=event_dict["source"],
67 | specversion=event_dict["specversion"],
68 | subject=event_dict["subject"]
69 | if "subject" in event_dict else None,
70 | time=_dt.datetime.strptime(
71 | event_dict["time"],
72 | "%Y-%m-%dT%H:%M:%S.%f%z",
73 | ),
74 | type=event_dict["type"],
75 | )
76 | _with_init(func)(event)
77 |
78 | _util.set_func_endpoint_attr(
79 | on_custom_event_published_wrapped,
80 | options._endpoint(func_name=func.__name__),
81 | )
82 | _util.set_required_apis_attr(
83 | on_custom_event_published_wrapped,
84 | options._required_apis(),
85 | )
86 | return on_custom_event_published_wrapped
87 |
88 | return on_custom_event_published_decorator
89 |
--------------------------------------------------------------------------------
/src/firebase_functions/logger.py:
--------------------------------------------------------------------------------
1 | """
2 | Logger module for Firebase Functions.
3 | """
4 |
5 | import enum as _enum
6 | import json as _json
7 | import sys as _sys
8 | import typing as _typing
9 | import typing_extensions as _typing_extensions
10 |
11 | # If encoding is not 'utf-8', change it to 'utf-8'.
12 | if _sys.stdout.encoding != "utf-8":
13 | _sys.stdout.reconfigure(encoding="utf-8") # type: ignore
14 | if _sys.stderr.encoding != "utf-8":
15 | _sys.stderr.reconfigure(encoding="utf-8") # type: ignore
16 |
17 |
18 | class LogSeverity(str, _enum.Enum):
19 | """
20 | `LogSeverity` indicates the detailed severity of the log entry. See
21 | `LogSeverity `.
22 | """
23 |
24 | DEBUG = "DEBUG"
25 | INFO = "INFO"
26 | NOTICE = "NOTICE"
27 | WARNING = "WARNING"
28 | ERROR = "ERROR"
29 | CRITICAL = "CRITICAL"
30 | ALERT = "ALERT"
31 | EMERGENCY = "EMERGENCY"
32 |
33 | def __str__(self) -> str:
34 | return self.value
35 |
36 |
37 | class LogEntry(_typing.TypedDict):
38 | """
39 | `LogEntry` represents a log entry.
40 | See `LogEntry `_.
41 | """
42 |
43 | severity: _typing_extensions.Required[LogSeverity]
44 | message: _typing_extensions.NotRequired[str]
45 |
46 |
47 | def _entry_from_args(severity: LogSeverity, *args, **kwargs) -> LogEntry:
48 | """
49 | Creates a `LogEntry` from the given arguments.
50 | """
51 |
52 | message: str = " ".join([
53 | value if isinstance(value, str) else _json.dumps(
54 | _remove_circular(value), ensure_ascii=False) for value in args
55 | ])
56 |
57 | other: _typing.Dict[str, _typing.Any] = {
58 | key: value if isinstance(value, str) else _remove_circular(value)
59 | for key, value in kwargs.items()
60 | }
61 |
62 | entry: _typing.Dict[str, _typing.Any] = {"severity": severity, **other}
63 | if message:
64 | entry["message"] = message
65 |
66 | return _typing.cast(LogEntry, entry)
67 |
68 |
69 | def _remove_circular(obj: _typing.Any,
70 | refs: _typing.Set[_typing.Any] | None = None):
71 | """
72 | Removes circular references from the given object and replaces them with "[CIRCULAR]".
73 | """
74 |
75 | if refs is None:
76 | refs = set()
77 |
78 | # Check if the object is already in the current recursion stack
79 | if id(obj) in refs:
80 | return "[CIRCULAR]"
81 |
82 | # For non-primitive objects, add the current object's id to the recursion stack
83 | if not isinstance(obj, (str, int, float, bool, type(None))):
84 | refs.add(id(obj))
85 |
86 | # Recursively process the object based on its type
87 | result: _typing.Any
88 | if isinstance(obj, dict):
89 | result = {
90 | key: _remove_circular(value, refs) for key, value in obj.items()
91 | }
92 | elif isinstance(obj, list):
93 | result = [_remove_circular(item, refs) for item in obj]
94 | elif isinstance(obj, tuple):
95 | result = tuple(_remove_circular(item, refs) for item in obj)
96 | else:
97 | result = obj
98 |
99 | # Remove the object's id from the recursion stack after processing
100 | if not isinstance(obj, (str, int, float, bool, type(None))):
101 | refs.remove(id(obj))
102 |
103 | return result
104 |
105 |
106 | def _get_write_file(severity: LogSeverity) -> _typing.TextIO:
107 | if severity == LogSeverity.ERROR:
108 | return _sys.stderr
109 | return _sys.stdout
110 |
111 |
112 | def write(entry: LogEntry) -> None:
113 | write_file = _get_write_file(entry["severity"])
114 | print(_json.dumps(_remove_circular(entry), ensure_ascii=False),
115 | file=write_file)
116 |
117 |
118 | def debug(*args, **kwargs) -> None:
119 | """
120 | Logs a debug message.
121 | """
122 | write(_entry_from_args(LogSeverity.DEBUG, *args, **kwargs))
123 |
124 |
125 | def log(*args, **kwargs) -> None:
126 | """
127 | Logs a log message.
128 | """
129 | write(_entry_from_args(LogSeverity.NOTICE, *args, **kwargs))
130 |
131 |
132 | def info(*args, **kwargs) -> None:
133 | """
134 | Logs an info message.
135 | """
136 | write(_entry_from_args(LogSeverity.INFO, *args, **kwargs))
137 |
138 |
139 | def warn(*args, **kwargs) -> None:
140 | """
141 | Logs a warning message.
142 | """
143 | write(_entry_from_args(LogSeverity.WARNING, *args, **kwargs))
144 |
145 |
146 | def error(*args, **kwargs) -> None:
147 | """
148 | Logs an error message.
149 | """
150 | write(_entry_from_args(LogSeverity.ERROR, *args, **kwargs))
151 |
--------------------------------------------------------------------------------
/src/firebase_functions/private/__init__.py:
--------------------------------------------------------------------------------
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 | """
15 | Firebase Functions for Python - Private/Internals
16 | """
17 |
--------------------------------------------------------------------------------
/src/firebase_functions/private/path_pattern.py:
--------------------------------------------------------------------------------
1 | # Copyright 2023 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 | """Path pattern matching utilities."""
15 |
16 | from enum import Enum
17 | import re
18 |
19 |
20 | def path_parts(path: str) -> list[str]:
21 | if not path or path == "" or path == "/":
22 | return []
23 | return path.strip("/").split("/")
24 |
25 |
26 | def join_path(base: str, child: str) -> str:
27 | return "/".join(path_parts(base) + path_parts(child))
28 |
29 |
30 | def trim_param(param: str) -> str:
31 | param_no_braces = param[1:-1]
32 | if "=" in param_no_braces:
33 | return param_no_braces[:param_no_braces.index("=")]
34 | return param_no_braces
35 |
36 |
37 | _WILDCARD_CAPTURE_REGEX = re.compile(r"{[^/{}]+}", re.IGNORECASE)
38 |
39 |
40 | class SegmentName(str, Enum):
41 | SEGMENT = "segment"
42 | SINGLE_CAPTURE = "single-capture"
43 | MULTI_CAPTURE = "multi-capture"
44 |
45 | def __str__(self) -> str:
46 | return self.value
47 |
48 |
49 | class PathSegment:
50 | """
51 | A segment of a path pattern.
52 | """
53 | name: SegmentName
54 | value: str
55 | trimmed: str
56 |
57 | def __str__(self):
58 | return self.value
59 |
60 | @property
61 | def is_single_segment_wildcard(self):
62 | pass
63 |
64 | @property
65 | def is_multi_segment_wildcard(self):
66 | pass
67 |
68 |
69 | class Segment(PathSegment):
70 | """
71 | A segment of a path pattern.
72 | """
73 |
74 | def __init__(self, value: str):
75 | self.value = value
76 | self.trimmed = value
77 | self.name = SegmentName.SEGMENT
78 |
79 | @property
80 | def is_single_segment_wildcard(self):
81 | return "*" in self.value and not self.is_multi_segment_wildcard
82 |
83 | @property
84 | def is_multi_segment_wildcard(self):
85 | return "**" in self.value
86 |
87 |
88 | class SingleCaptureSegment(PathSegment):
89 | """
90 | A segment of a path pattern that captures a single segment.
91 | """
92 | name = SegmentName.SINGLE_CAPTURE
93 |
94 | def __init__(self, value):
95 | self.value = value
96 | self.trimmed = trim_param(value)
97 |
98 | @property
99 | def is_single_segment_wildcard(self):
100 | return True
101 |
102 | @property
103 | def is_multi_segment_wildcard(self):
104 | return False
105 |
106 |
107 | class MultiCaptureSegment(PathSegment):
108 | """
109 | A segment of a path pattern that captures multiple segments.
110 | """
111 |
112 | name = SegmentName.MULTI_CAPTURE
113 |
114 | def __init__(self, value):
115 | self.value = value
116 | self.trimmed = trim_param(value)
117 |
118 | @property
119 | def is_single_segment_wildcard(self):
120 | return False
121 |
122 | @property
123 | def is_multi_segment_wildcard(self):
124 | return True
125 |
126 |
127 | class PathPattern:
128 | """
129 | Implements Eventarc's path pattern from the spec
130 | https://cloud.google.com/eventarc/docs/path-patterns
131 | """
132 | segments: list[PathSegment]
133 |
134 | def __init__(self, raw_path: str):
135 | normalized_path = raw_path.strip("/")
136 | self.raw = normalized_path
137 | self.segments = []
138 | self.init_path_segments(normalized_path)
139 |
140 | def init_path_segments(self, raw: str):
141 | parts = raw.split("/")
142 | for part in parts:
143 | segment: PathSegment | None = None
144 | capture = re.findall(_WILDCARD_CAPTURE_REGEX, part)
145 | if capture is not None and len(capture) == 1:
146 | if "**" in part:
147 | segment = MultiCaptureSegment(part)
148 | else:
149 | segment = SingleCaptureSegment(part)
150 | else:
151 | segment = Segment(part)
152 | self.segments.append(segment)
153 |
154 | @property
155 | def value(self) -> str:
156 | return self.raw
157 |
158 | @property
159 | def has_wildcards(self) -> bool:
160 | return any(segment.is_single_segment_wildcard or
161 | segment.is_multi_segment_wildcard
162 | for segment in self.segments)
163 |
164 | @property
165 | def has_captures(self) -> bool:
166 | return any(segment.name in (SegmentName.SINGLE_CAPTURE,
167 | SegmentName.MULTI_CAPTURE)
168 | for segment in self.segments)
169 |
170 | def extract_matches(self, path: str) -> dict[str, str]:
171 | matches: dict[str, str] = {}
172 | if not self.has_captures:
173 | return matches
174 | path_segments = path_parts(path)
175 | path_ndx = 0
176 | for segment_ndx in range(len(self.segments)):
177 | segment = self.segments[segment_ndx]
178 | remaining_segments = len(self.segments) - 1 - segment_ndx
179 | next_path_ndx = len(path_segments) - remaining_segments
180 | if segment.name == SegmentName.SINGLE_CAPTURE:
181 | matches[segment.trimmed] = path_segments[path_ndx]
182 | elif segment.name == SegmentName.MULTI_CAPTURE:
183 | matches[segment.trimmed] = "/".join(
184 | path_segments[path_ndx:next_path_ndx])
185 | path_ndx = next_path_ndx if segment.is_multi_segment_wildcard else path_ndx + 1
186 | return matches
187 |
--------------------------------------------------------------------------------
/src/firebase_functions/private/serving.py:
--------------------------------------------------------------------------------
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 | """
15 | Module used to serve Firebase functions locally and remotely.
16 | """
17 | # pylint: disable=protected-access
18 | import os
19 | import inspect
20 | import enum
21 | import yaml
22 | import importlib
23 | import sys
24 | from os import kill, getpid
25 | from signal import SIGTERM
26 |
27 | from flask import Flask
28 | from flask import Response
29 |
30 | from firebase_functions.private import manifest as _manifest
31 | from firebase_functions import params as _params, options as _options
32 | from firebase_functions.private import util as _util
33 |
34 |
35 | def get_functions():
36 | sys.path.insert(0, os.getcwd())
37 | spec = importlib.util.spec_from_file_location("main", "main.py")
38 | if spec is not None and spec.loader is not None:
39 | module = importlib.util.module_from_spec(spec)
40 | spec.loader.exec_module(module)
41 | else:
42 | raise FileNotFoundError(
43 | "Firebase Functions for Python could not find the main.py file in your project."
44 | )
45 | functions = inspect.getmembers(module, inspect.isfunction)
46 | firebase_functions = {}
47 | for entry in functions:
48 | if hasattr(entry[1], "__firebase_endpoint__"):
49 | name = entry[1].__firebase_endpoint__.entryPoint
50 | firebase_functions[name] = entry[1]
51 | return firebase_functions
52 |
53 |
54 | def to_spec(data: dict) -> dict:
55 |
56 | def convert_value(obj):
57 | if isinstance(obj, enum.Enum):
58 | return obj.value
59 | if isinstance(obj, dict):
60 | return to_spec(obj)
61 | if isinstance(obj, list):
62 | return list(map(convert_value, obj))
63 | return obj
64 |
65 | without_nones = dict(
66 | (k, convert_value(v)) for k, v in data.items() if v is not None)
67 | return without_nones
68 |
69 |
70 | def merge_required_apis(
71 | required_apis: list[_manifest.ManifestRequiredApi]
72 | ) -> list[_manifest.ManifestRequiredApi]:
73 | api_to_reasons: dict[str, list[str]] = {}
74 | for api_reason in required_apis:
75 | api = api_reason["api"]
76 | reason = api_reason["reason"]
77 | if api not in api_to_reasons:
78 | api_to_reasons[api] = []
79 |
80 | if reason not in api_to_reasons[api]:
81 | # Append unique reasons only
82 | api_to_reasons[api].append(reason)
83 |
84 | merged: list[_manifest.ManifestRequiredApi] = []
85 | for api, reasons in api_to_reasons.items():
86 | merged.append({"api": api, "reason": " ".join(reasons)})
87 |
88 | return merged
89 |
90 |
91 | def functions_as_yaml(functions: dict) -> str:
92 | endpoints: dict[str, _manifest.ManifestEndpoint] = {}
93 | required_apis: list[_manifest.ManifestRequiredApi] = []
94 | for name, function in functions.items():
95 | endpoint = function.__firebase_endpoint__
96 | endpoints[name] = endpoint
97 | if hasattr(function, "__required_apis"):
98 | for api in function.__required_apis:
99 | required_apis.append(api)
100 |
101 | required_apis = merge_required_apis(required_apis)
102 | manifest_stack = _manifest.ManifestStack(
103 | endpoints=endpoints,
104 | requiredAPIs=required_apis,
105 | params=list(_params._params.values()),
106 | )
107 | manifest_spec = _manifest.manifest_to_spec_dict(manifest_stack)
108 | manifest_spec_with_sentinels = to_spec(manifest_spec)
109 |
110 | def represent_sentinel(self, value):
111 | if value == _options.RESET_VALUE:
112 | return self.represent_scalar("tag:yaml.org,2002:null", "null")
113 | # Other sentinel types in the future can be added here.
114 | return self.represent_scalar("tag:yaml.org,2002:null", "null")
115 |
116 | yaml.add_representer(_util.Sentinel, represent_sentinel)
117 |
118 | return yaml.dump(manifest_spec_with_sentinels)
119 |
120 |
121 | def get_functions_yaml() -> Response:
122 | functions = get_functions()
123 | functions_yaml = functions_as_yaml(functions)
124 | return Response(functions_yaml, mimetype="text/yaml")
125 |
126 |
127 | def quitquitquit():
128 |
129 | def quit_after_close():
130 | kill(getpid(), SIGTERM)
131 |
132 | response = Response("OK", status=200)
133 | response.call_on_close(quit_after_close)
134 | return response
135 |
136 |
137 | def serve_admin() -> Flask:
138 | app = Flask(__name__)
139 | app.add_url_rule(
140 | "/__/functions.yaml",
141 | endpoint="functions.yaml",
142 | view_func=get_functions_yaml,
143 | )
144 |
145 | app.add_url_rule(
146 | "/__/quitquitquit",
147 | endpoint="quitquitquit",
148 | view_func=quitquitquit,
149 | )
150 |
151 | return app
152 |
153 |
154 | def main():
155 | if os.environ["ADMIN_PORT"] is not None:
156 | serve_admin().run(port=int(os.environ["ADMIN_PORT"]), debug=False)
157 |
158 |
159 | if __name__ == "__main__":
160 | main()
161 |
--------------------------------------------------------------------------------
/src/firebase_functions/pubsub_fn.py:
--------------------------------------------------------------------------------
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 | """
15 | Functions to handle events from Google Cloud Pub/Sub.
16 | """
17 | # pylint: disable=protected-access
18 | import dataclasses as _dataclasses
19 | import datetime as _dt
20 | import functools as _functools
21 | import typing as _typing
22 | import json as _json
23 | import base64 as _base64
24 | import cloudevents.http as _ce
25 |
26 | import firebase_functions.private.util as _util
27 |
28 | from firebase_functions.core import CloudEvent, T, _with_init
29 | from firebase_functions.options import PubSubOptions
30 |
31 |
32 | @_dataclasses.dataclass(frozen=True)
33 | class Message(_typing.Generic[T]):
34 | """
35 | Interface representing a Google Cloud Pub/Sub message.
36 | """
37 |
38 | message_id: str
39 | """
40 | Autogenerated ID that uniquely identifies this message.
41 | """
42 |
43 | publish_time: str
44 | """
45 | Time the message was published.
46 | """
47 |
48 | attributes: dict[str, str]
49 | """
50 | User-defined attributes published with the message, if any.
51 | """
52 |
53 | data: str
54 | """
55 | The data payload of this message object as a base64-encoded string.
56 | """
57 |
58 | ordering_key: str
59 | """
60 | User-defined key used to ensure ordering amongst messages with the same key.
61 | """
62 |
63 | @property
64 | def json(self) -> T | None:
65 | try:
66 | if self.data is not None:
67 | return _json.loads(_base64.b64decode(self.data).decode("utf-8"))
68 | else:
69 | return None
70 | except Exception as error:
71 | raise ValueError(
72 | f"Unable to parse Pub/Sub message data as JSON: {error}"
73 | ) from error
74 |
75 |
76 | @_dataclasses.dataclass(frozen=True)
77 | class MessagePublishedData(_typing.Generic[T]):
78 | """
79 | The interface published in a Pub/Sub publish subscription.
80 |
81 | 'T' Type representing `Message.data`'s JSON format.
82 | """
83 | message: Message[T]
84 | """
85 | Google Cloud Pub/Sub message.
86 | """
87 |
88 | subscription: str
89 | """
90 | A subscription resource.
91 | """
92 |
93 |
94 | _E1 = CloudEvent[MessagePublishedData[T]]
95 | _C1 = _typing.Callable[[_E1], None]
96 |
97 |
98 | def _message_handler(
99 | func: _C1,
100 | raw: _ce.CloudEvent,
101 | ) -> None:
102 | event_attributes = raw._get_attributes()
103 | event_data: _typing.Any = raw.get_data()
104 | event_dict = {"data": event_data, **event_attributes}
105 | data = event_dict["data"]
106 | message_dict = data["message"]
107 |
108 | # if no microseconds are present, we should set them to 0 to prevent parsing from failing
109 | if "." not in event_dict["time"]:
110 | event_dict["time"] = event_dict["time"].replace("Z", ".000000Z")
111 | if "." not in message_dict["publish_time"]:
112 | message_dict["publish_time"] = message_dict["publish_time"].replace(
113 | "Z", ".000000Z")
114 |
115 | time = _dt.datetime.strptime(
116 | event_dict["time"],
117 | "%Y-%m-%dT%H:%M:%S.%f%z",
118 | )
119 |
120 | publish_time = _dt.datetime.strptime(
121 | message_dict["publish_time"],
122 | "%Y-%m-%dT%H:%M:%S.%f%z",
123 | )
124 |
125 | # Convert the UTC string into a datetime object
126 | event_dict["time"] = time
127 | message_dict["publish_time"] = publish_time
128 |
129 | # Pop unnecessary keys from the message data
130 | # (we get these keys from the snake case alternatives that are provided)
131 | message_dict.pop("messageId", None)
132 | message_dict.pop("publishTime", None)
133 |
134 | # `orderingKey` doesn't come with a snake case alternative,
135 | # there is no `ordering_key` in the raw request.
136 | ordering_key = message_dict.pop("orderingKey", None)
137 |
138 | # Include empty attributes property if missing
139 | message_dict["attributes"] = message_dict.get("attributes", {})
140 |
141 | message: MessagePublishedData = MessagePublishedData(
142 | message=Message(
143 | **message_dict,
144 | ordering_key=ordering_key,
145 | ),
146 | subscription=data["subscription"],
147 | )
148 |
149 | event_dict["data"] = message
150 |
151 | event: CloudEvent[MessagePublishedData] = CloudEvent(
152 | data=event_dict["data"],
153 | id=event_dict["id"],
154 | source=event_dict["source"],
155 | specversion=event_dict["specversion"],
156 | subject=event_dict["subject"] if "subject" in event_dict else None,
157 | time=event_dict["time"],
158 | type=event_dict["type"],
159 | )
160 |
161 | _with_init(func)(event)
162 |
163 |
164 | @_util.copy_func_kwargs(PubSubOptions)
165 | def on_message_published(**kwargs) -> _typing.Callable[[_C1], _C1]:
166 | """
167 | Event handler that triggers on a message being published to a Pub/Sub topic.
168 |
169 | Example:
170 |
171 | .. code-block:: python
172 |
173 | @on_message_published(topic="hello-world")
174 | def example(event: CloudEvent[MessagePublishedData[object]]) -> None:
175 | pass
176 |
177 | :param \\*\\*kwargs: Pub/Sub options.
178 | :type \\*\\*kwargs: as :exc:`firebase_functions.options.PubSubOptions`
179 | :rtype: :exc:`typing.Callable`
180 | \\[ \\[ :exc:`firebase_functions.core.CloudEvent` \\[
181 | :exc:`firebase_functions.pubsub_fn.MessagePublishedData` \\[
182 | :exc:`typing.Any` \\] \\] \\], `None` \\]
183 | A function that takes a CloudEvent and returns ``None``.
184 | """
185 | options = PubSubOptions(**kwargs)
186 |
187 | def on_message_published_inner_decorator(func: _C1):
188 |
189 | @_functools.wraps(func)
190 | def on_message_published_wrapped(raw: _ce.CloudEvent):
191 | return _message_handler(func, raw)
192 |
193 | _util.set_func_endpoint_attr(
194 | on_message_published_wrapped,
195 | options._endpoint(func_name=func.__name__),
196 | )
197 | return on_message_published_wrapped
198 |
199 | return on_message_published_inner_decorator
200 |
--------------------------------------------------------------------------------
/src/firebase_functions/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/firebase-functions-python/436f4c7d8ad7fc81a5dc50e6a2c0eca3c538d397/src/firebase_functions/py.typed
--------------------------------------------------------------------------------
/src/firebase_functions/remote_config_fn.py:
--------------------------------------------------------------------------------
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, _with_init
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 | def __str__(self) -> str:
79 | return self.value
80 |
81 |
82 | class ConfigUpdateType(str, _enum.Enum):
83 | """
84 | What type of update was associated with the Remote Config template version.
85 | """
86 |
87 | REMOTE_CONFIG_UPDATE_TYPE_UNSPECIFIED = "REMOTE_CONFIG_UPDATE_TYPE_UNSPECIFIED"
88 | """
89 | Catch-all for unrecognized enum values.
90 | """
91 |
92 | INCREMENTAL_UPDATE = "INCREMENTAL_UPDATE"
93 | """
94 | A regular incremental update.
95 | """
96 |
97 | FORCED_UPDATE = "FORCED_UPDATE"
98 | """
99 | A forced update. The ETag was specified as "*" in an UpdateRemoteConfigRequest
100 | request or the "Force Update" button was pressed on the console.
101 | """
102 |
103 | ROLLBACK = "ROLLBACK"
104 | """
105 | A rollback to a previous Remote Config template.
106 | """
107 |
108 | def __str__(self) -> str:
109 | return self.value
110 |
111 |
112 | @_dataclasses.dataclass(frozen=True)
113 | class ConfigUpdateData:
114 | """
115 | The data within Firebase Remote Config update events.
116 | """
117 |
118 | version_number: int
119 | """
120 | The version number of the version's corresponding Remote Config template.
121 | """
122 |
123 | update_time: _dt.datetime
124 | """
125 | When the Remote Config template was written to the Remote Config server.
126 | """
127 |
128 | update_user: ConfigUser
129 | """
130 | Aggregation of all metadata fields about the account that performed the update.
131 | """
132 |
133 | description: str
134 | """
135 | The user-provided description of the corresponding Remote Config template.
136 | """
137 |
138 | update_origin: ConfigUpdateOrigin
139 | """
140 | Where the update action originated.
141 | """
142 |
143 | update_type: ConfigUpdateType
144 | """
145 | What type of update was made.
146 | """
147 |
148 | rollback_source: int | None = None
149 | """
150 | Only present if this version is the result of a rollback, and is
151 | the version number of the Remote Config template that was rolled back to.
152 | """
153 |
154 |
155 | _E1 = CloudEvent[ConfigUpdateData]
156 | _C1 = _typing.Callable[[_E1], None]
157 |
158 |
159 | def _config_handler(func: _C1, raw: _ce.CloudEvent) -> None:
160 | event_attributes = raw._get_attributes()
161 | event_data: _typing.Any = raw.get_data()
162 | event_dict = {**event_data, **event_attributes}
163 |
164 | config_data = ConfigUpdateData(
165 | version_number=event_data["versionNumber"],
166 | update_time=_dt.datetime.strptime(event_data["updateTime"],
167 | "%Y-%m-%dT%H:%M:%S.%f%z"),
168 | update_user=ConfigUser(
169 | name=event_data["updateUser"]["name"],
170 | email=event_data["updateUser"]["email"],
171 | image_url=event_data["updateUser"]["imageUrl"],
172 | ),
173 | description=event_data["description"],
174 | update_origin=ConfigUpdateOrigin(event_data["updateOrigin"]),
175 | update_type=ConfigUpdateType(event_data["updateType"]),
176 | rollback_source=event_data.get("rollbackSource", None),
177 | )
178 |
179 | event: CloudEvent[ConfigUpdateData] = CloudEvent(
180 | data=config_data,
181 | id=event_dict["id"],
182 | source=event_dict["source"],
183 | specversion=event_dict["specversion"],
184 | subject=event_dict["subject"] if "subject" in event_dict else None,
185 | time=_dt.datetime.strptime(
186 | event_dict["time"],
187 | "%Y-%m-%dT%H:%M:%S.%f%z",
188 | ),
189 | type=event_dict["type"],
190 | )
191 |
192 | _with_init(func)(event)
193 |
194 |
195 | @_util.copy_func_kwargs(EventHandlerOptions)
196 | def on_config_updated(**kwargs) -> _typing.Callable[[_C1], _C1]:
197 | """
198 | Event handler which triggers when data is updated in a Remote Config.
199 |
200 | Example:
201 |
202 | .. code-block:: python
203 |
204 | @on_config_updated()
205 | def example(event: CloudEvent[ConfigUpdateData]) -> None:
206 | pass
207 |
208 | :param \\*\\*kwargs: Pub/Sub options.
209 | :type \\*\\*kwargs: as :exc:`firebase_functions.options.EventHandlerOptions`
210 | :rtype: :exc:`typing.Callable`
211 | \\[ \\[ :exc:`firebase_functions.core.CloudEvent` \\[
212 | :exc:`firebase_functions.remote_config_fn.ConfigUpdateData` \\[
213 | :exc:`typing.Any` \\] \\] \\], `None` \\]
214 | A function that takes a CloudEvent and returns None.
215 | """
216 | options = EventHandlerOptions(**kwargs)
217 |
218 | def on_config_updated_inner_decorator(func: _C1):
219 |
220 | @_functools.wraps(func)
221 | def on_config_updated_wrapped(raw: _ce.CloudEvent):
222 | return _config_handler(func, raw)
223 |
224 | _util.set_func_endpoint_attr(
225 | on_config_updated_wrapped,
226 | options._endpoint(
227 | func_name=func.__name__,
228 | event_filters={},
229 | event_type="google.firebase.remoteconfig.remoteConfig.v1.updated"
230 | ),
231 | )
232 | return on_config_updated_wrapped
233 |
234 | return on_config_updated_inner_decorator
235 |
--------------------------------------------------------------------------------
/src/firebase_functions/scheduler_fn.py:
--------------------------------------------------------------------------------
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 | """Cloud functions to handle Schedule triggers."""
15 |
16 | import typing as _typing
17 | import dataclasses as _dataclasses
18 | import datetime as _dt
19 | import functools as _functools
20 |
21 | import firebase_functions.options as _options
22 | import firebase_functions.private.util as _util
23 | from functions_framework import logging as _logging
24 | from flask import (
25 | Request as _Request,
26 | Response as _Response,
27 | make_response as _make_response,
28 | )
29 |
30 | from firebase_functions.core import _with_init
31 | # Export for user convenience.
32 | # pylint: disable=unused-import
33 | from firebase_functions.options import Timezone
34 |
35 |
36 | @_dataclasses.dataclass(frozen=True)
37 | class ScheduledEvent:
38 | """
39 | A ``ScheduleEvent`` that is passed to the function handler.
40 | """
41 |
42 | job_name: str | None
43 | """
44 | The Cloud Scheduler job name.
45 | Populated via the ``X-CloudScheduler-JobName`` header.
46 | If invoked manually, this field is `None`.
47 | """
48 |
49 | schedule_time: _dt.datetime
50 | """
51 | For Cloud Scheduler jobs specified in the unix-cron format,
52 | this is the job schedule time in RFC3339 UTC "Zulu" format.
53 | Populated via the ``X-CloudScheduler-ScheduleTime`` header.
54 |
55 | If the schedule is manually triggered, this field is
56 | the function execution time.
57 | """
58 |
59 |
60 | _C = _typing.Callable[[ScheduledEvent], None]
61 |
62 |
63 | @_util.copy_func_kwargs(_options.ScheduleOptions)
64 | def on_schedule(**kwargs) -> _typing.Callable[[_C], _Response]:
65 | """
66 | Creates a handler for tasks sent to a Google Cloud Tasks queue.
67 | Requires a function that takes a ``CallableRequest``.
68 |
69 | Example:
70 |
71 | .. code-block:: python
72 |
73 | from firebase_functions import scheduler_fn
74 |
75 |
76 | @scheduler_fn.on_schedule(
77 | schedule="* * * * *",
78 | timezone=scheduler_fn.Timezone("America/Los_Angeles"),
79 | )
80 | def example(event: scheduler_fn.ScheduledEvent) -> None:
81 | print(event.job_name)
82 | print(event.schedule_time)
83 |
84 |
85 | :param \\*\\*kwargs: `ScheduleOptions` options.
86 | :type \\*\\*kwargs: as :exc:`firebase_functions.options.ScheduleOptions`
87 | :rtype: :exc:`typing.Callable`
88 | \\[ \\[ :exc:`firebase_functions.schedule_fn.ScheduledEvent` \\], :exc:`None` \\]
89 | A function that takes a ``ScheduledEvent`` and returns nothing.
90 | """
91 | options = _options.ScheduleOptions(**kwargs)
92 |
93 | def on_schedule_decorator(func: _C):
94 |
95 | @_functools.wraps(func)
96 | def on_schedule_wrapped(request: _Request) -> _Response:
97 | schedule_time: _dt.datetime
98 | schedule_time_str = request.headers.get(
99 | "X-CloudScheduler-ScheduleTime")
100 | if schedule_time_str is None:
101 | schedule_time = _dt.datetime.utcnow()
102 | else:
103 | schedule_time = _dt.datetime.strptime(
104 | schedule_time_str,
105 | "%Y-%m-%dT%H:%M:%S%z",
106 | )
107 | event = ScheduledEvent(
108 | job_name=request.headers.get("X-CloudScheduler-JobName"),
109 | schedule_time=schedule_time,
110 | )
111 | try:
112 | _with_init(func)(event)
113 | return _make_response()
114 | # Disable broad exceptions lint since we want to handle all exceptions.
115 | # pylint: disable=broad-except
116 | except Exception as exception:
117 | _logging.exception(exception)
118 | return _make_response(str(exception), 500)
119 |
120 | _util.set_func_endpoint_attr(
121 | on_schedule_wrapped,
122 | # pylint: disable=protected-access
123 | options._endpoint(func_name=func.__name__),
124 | )
125 | _util.set_required_apis_attr(
126 | on_schedule_wrapped,
127 | # pylint: disable=protected-access
128 | options._required_apis(),
129 | )
130 | return on_schedule_wrapped
131 |
132 | return on_schedule_decorator
133 |
--------------------------------------------------------------------------------
/src/firebase_functions/tasks_fn.py:
--------------------------------------------------------------------------------
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 | """Functions to handle Tasks enqueued with Google Cloud Tasks."""
15 |
16 | # pylint: disable=protected-access
17 | import typing as _typing
18 | import functools as _functools
19 | import dataclasses as _dataclasses
20 | import json as _json
21 |
22 | from flask import Request, Response, make_response as _make_response, jsonify as _jsonify
23 |
24 | import firebase_functions.core as _core
25 | import firebase_functions.options as _options
26 | import firebase_functions.private.util as _util
27 | from firebase_functions.https_fn import CallableRequest, HttpsError, FunctionsErrorCode
28 |
29 | from functions_framework import logging as _logging
30 |
31 | _C = _typing.Callable[[CallableRequest[_typing.Any]], _typing.Any]
32 | _C1 = _typing.Callable[[Request], Response]
33 | _C2 = _typing.Callable[[CallableRequest[_typing.Any]], _typing.Any]
34 |
35 |
36 | def _on_call_handler(func: _C2, request: Request) -> Response:
37 | try:
38 | if not _util.valid_on_call_request(request):
39 | _logging.error("Invalid request, unable to process.")
40 | raise HttpsError(FunctionsErrorCode.INVALID_ARGUMENT, "Bad Request")
41 | context: CallableRequest = CallableRequest(
42 | raw_request=request,
43 | data=_json.loads(request.data)["data"],
44 | )
45 |
46 | instance_id = request.headers.get("Firebase-Instance-ID-Token")
47 | if instance_id is not None:
48 | # Validating the token requires an http request, so we don't do it.
49 | # If the user wants to use it for something, it will be validated then.
50 | # Currently, the only real use case for this token is for sending
51 | # pushes with FCM. In that case, the FCM APIs will validate the token.
52 | context = _dataclasses.replace(
53 | context,
54 | instance_id_token=request.headers.get(
55 | "Firebase-Instance-ID-Token"),
56 | )
57 | result = _core._with_init(func)(context)
58 | return _jsonify(result=result)
59 | # Disable broad exceptions lint since we want to handle all exceptions here
60 | # and wrap as an HttpsError.
61 | # pylint: disable=broad-except
62 | except Exception as err:
63 | if not isinstance(err, HttpsError):
64 | _logging.error("Unhandled error: %s", err)
65 | err = HttpsError(FunctionsErrorCode.INTERNAL, "INTERNAL")
66 | status = err._http_error_code.status
67 | return _make_response(_jsonify(error=err._as_dict()), status)
68 |
69 |
70 | @_util.copy_func_kwargs(_options.TaskQueueOptions)
71 | def on_task_dispatched(**kwargs) -> _typing.Callable[[_C], Response]:
72 | """
73 | Creates a handler for tasks sent to a Google Cloud Tasks queue.
74 | Requires a function that takes a CallableRequest.
75 |
76 | Example:
77 |
78 | .. code-block:: python
79 |
80 | @tasks.on_task_dispatched()
81 | def example(request: tasks.CallableRequest) -> Any:
82 | return "Hello World"
83 |
84 | :param \\*\\*kwargs: TaskQueueOptions options.
85 | :type \\*\\*kwargs: as :exc:`firebase_functions.options.TaskQueueOptions`
86 | :rtype: :exc:`typing.Callable`
87 | \\[ \\[ :exc:`firebase_functions.https.CallableRequest` \\[
88 | :exc:`object` \\] \\], :exc:`object` \\]
89 | A function that takes a CallableRequest and returns an :exc:`object`.
90 | """
91 | options = _options.TaskQueueOptions(**kwargs)
92 |
93 | def on_task_dispatched_decorator(func: _C):
94 |
95 | @_functools.wraps(func)
96 | def on_task_dispatched_wrapped(request: Request) -> Response:
97 | return _on_call_handler(func, request)
98 |
99 | _util.set_func_endpoint_attr(
100 | on_task_dispatched_wrapped,
101 | options._endpoint(func_name=func.__name__),
102 | )
103 | _util.set_required_apis_attr(
104 | on_task_dispatched_wrapped,
105 | options._required_apis(),
106 | )
107 | return on_task_dispatched_wrapped
108 |
109 | return on_task_dispatched_decorator
110 |
--------------------------------------------------------------------------------
/tests/firebase_config_test.json:
--------------------------------------------------------------------------------
1 | {
2 | "storageBucket": "python-functions-testing.appspot.com"
3 | }
4 |
--------------------------------------------------------------------------------
/tests/test_db.py:
--------------------------------------------------------------------------------
1 | """
2 | Tests for the db module.
3 | """
4 |
5 | import unittest
6 | from unittest import mock
7 | from cloudevents.http import CloudEvent
8 | from firebase_functions import core, db_fn
9 |
10 |
11 | class TestDb(unittest.TestCase):
12 | """
13 | Tests for the db module.
14 | """
15 |
16 | def test_calls_init_function(self):
17 | hello = None
18 |
19 | @core.init
20 | def init():
21 | nonlocal hello
22 | hello = "world"
23 |
24 | func = mock.Mock(__name__="example_func")
25 | decorated_func = db_fn.on_value_created(reference="path")(func)
26 |
27 | event = CloudEvent(attributes={
28 | "specversion": "1.0",
29 | "id": "id",
30 | "source": "source",
31 | "subject": "subject",
32 | "type": "type",
33 | "time": "2024-04-10T12:00:00.000Z",
34 | "instance": "instance",
35 | "ref": "ref",
36 | "firebasedatabasehost": "firebasedatabasehost",
37 | "location": "location",
38 | },
39 | data={"delta": "delta"})
40 |
41 | decorated_func(event)
42 |
43 | self.assertEqual(hello, "world")
44 |
--------------------------------------------------------------------------------
/tests/test_eventarc_fn.py:
--------------------------------------------------------------------------------
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 | """Eventarc trigger function tests."""
15 | import unittest
16 | from unittest.mock import Mock
17 |
18 | from cloudevents.http import CloudEvent as _CloudEvent
19 |
20 | from firebase_functions import core
21 | from firebase_functions.core import CloudEvent
22 | from firebase_functions.eventarc_fn import on_custom_event_published
23 |
24 |
25 | class TestEventarcFn(unittest.TestCase):
26 | """
27 | Test Eventarc trigger functions.
28 | """
29 |
30 | def test_on_custom_event_published_decorator(self):
31 | """
32 | Tests the on_custom_event_published decorator functionality by checking
33 | that the __firebase_endpoint__ attribute is set properly.
34 | """
35 | func = Mock(__name__="example_func")
36 |
37 | decorated_func = on_custom_event_published(
38 | event_type="firebase.extensions.storage-resize-images.v1.complete",
39 | )(func)
40 |
41 | endpoint = getattr(decorated_func, "__firebase_endpoint__")
42 | self.assertIsNotNone(endpoint)
43 | self.assertIsNotNone(endpoint.eventTrigger)
44 | self.assertEqual(
45 | endpoint.eventTrigger["eventType"],
46 | "firebase.extensions.storage-resize-images.v1.complete",
47 | )
48 |
49 | def test_on_custom_event_published_wrapped(self):
50 | """
51 | Tests the wrapped function created by the on_custom_event_published
52 | decorator, ensuring that it correctly processes the raw event and calls
53 | the user-provided function with a properly formatted CloudEvent instance.
54 | """
55 | func = Mock(__name__="example_func")
56 | raw_event = _CloudEvent(
57 | attributes={
58 | "specversion": "1.0",
59 | "type": "firebase.extensions.storage-resize-images.v1.complete",
60 | "source": "https://example.com/testevent",
61 | "id": "1234567890",
62 | "subject": "test_subject",
63 | "time": "2023-03-11T13:25:37.403Z",
64 | },
65 | data={
66 | "some_key": "some_value",
67 | },
68 | )
69 |
70 | decorated_func = on_custom_event_published(
71 | event_type="firebase.extensions.storage-resize-images.v1.complete",
72 | )(func)
73 |
74 | decorated_func(raw_event)
75 |
76 | func.assert_called_once()
77 |
78 | event_arg = func.call_args.args[0]
79 | self.assertIsInstance(event_arg, CloudEvent)
80 | self.assertEqual(event_arg.data, {"some_key": "some_value"})
81 | self.assertEqual(event_arg.id, "1234567890")
82 | self.assertEqual(event_arg.source, "https://example.com/testevent")
83 | self.assertEqual(event_arg.specversion, "1.0")
84 | self.assertEqual(event_arg.subject, "test_subject")
85 | self.assertEqual(
86 | event_arg.type,
87 | "firebase.extensions.storage-resize-images.v1.complete",
88 | )
89 |
90 | def test_calls_init_function(self):
91 | hello = None
92 |
93 | @core.init
94 | def init():
95 | nonlocal hello
96 | hello = "world"
97 |
98 | func = Mock(__name__="example_func")
99 | raw_event = _CloudEvent(
100 | attributes={
101 | "specversion": "1.0",
102 | "type": "firebase.extensions.storage-resize-images.v1.complete",
103 | "source": "https://example.com/testevent",
104 | "id": "1234567890",
105 | "subject": "test_subject",
106 | "time": "2023-03-11T13:25:37.403Z",
107 | },
108 | data={
109 | "some_key": "some_value",
110 | },
111 | )
112 |
113 | decorated_func = on_custom_event_published(
114 | event_type="firebase.extensions.storage-resize-images.v1.complete",
115 | )(func)
116 |
117 | decorated_func(raw_event)
118 |
119 | self.assertEqual(hello, "world")
120 |
--------------------------------------------------------------------------------
/tests/test_firestore_fn.py:
--------------------------------------------------------------------------------
1 | """
2 | This module contains tests for the firestore_fn module.
3 | """
4 |
5 | import json
6 | from unittest import TestCase
7 | from unittest.mock import MagicMock, Mock, patch
8 |
9 | mocked_modules = {
10 | "google.cloud.firestore": MagicMock(),
11 | "google.cloud.firestore_v1": MagicMock(),
12 | "firebase_admin": MagicMock()
13 | }
14 |
15 |
16 | class TestFirestore(TestCase):
17 | """
18 | firestore_fn tests.
19 | """
20 |
21 | def test_firestore_endpoint_handler_calls_function_with_correct_args(self):
22 | with patch.dict("sys.modules", mocked_modules):
23 | from cloudevents.http import CloudEvent
24 | from firebase_functions.firestore_fn import _event_type_created_with_auth_context as event_type, \
25 | _firestore_endpoint_handler as firestore_endpoint_handler, AuthEvent
26 | from firebase_functions.private import path_pattern
27 |
28 | func = Mock(__name__="example_func")
29 |
30 | document_pattern = path_pattern.PathPattern("foo/{bar}")
31 | attributes = {
32 | "specversion":
33 | "1.0",
34 | "type":
35 | event_type,
36 | "source":
37 | "https://example.com/testevent",
38 | "time":
39 | "2023-03-11T13:25:37.403Z",
40 | "subject":
41 | "test_subject",
42 | "datacontenttype":
43 | "application/json",
44 | "location":
45 | "projects/project-id/databases/(default)/documents/foo/{bar}",
46 | "project":
47 | "project-id",
48 | "namespace":
49 | "(default)",
50 | "document":
51 | "foo/{bar}",
52 | "database":
53 | "projects/project-id/databases/(default)",
54 | "authtype":
55 | "unauthenticated",
56 | "authid":
57 | "foo"
58 | }
59 | raw_event = CloudEvent(attributes=attributes, data=json.dumps({}))
60 |
61 | firestore_endpoint_handler(func=func,
62 | event_type=event_type,
63 | document_pattern=document_pattern,
64 | raw=raw_event)
65 |
66 | func.assert_called_once()
67 |
68 | event = func.call_args.args[0]
69 | self.assertIsNotNone(event)
70 | self.assertIsInstance(event, AuthEvent)
71 | self.assertEqual(event.auth_type, "unauthenticated")
72 | self.assertEqual(event.auth_id, "foo")
73 |
74 | def test_calls_init_function(self):
75 | with patch.dict("sys.modules", mocked_modules):
76 | from firebase_functions import firestore_fn, core
77 | from cloudevents.http import CloudEvent
78 |
79 | func = Mock(__name__="example_func")
80 |
81 | hello = None
82 |
83 | @core.init
84 | def init():
85 | nonlocal hello
86 | hello = "world"
87 |
88 | attributes = {
89 | "specversion":
90 | "1.0",
91 | # pylint: disable=protected-access
92 | "type":
93 | firestore_fn._event_type_created,
94 | "source":
95 | "https://example.com/testevent",
96 | "time":
97 | "2023-03-11T13:25:37.403Z",
98 | "subject":
99 | "test_subject",
100 | "datacontenttype":
101 | "application/json",
102 | "location":
103 | "projects/project-id/databases/(default)/documents/foo/{bar}",
104 | "project":
105 | "project-id",
106 | "namespace":
107 | "(default)",
108 | "document":
109 | "foo/{bar}",
110 | "database":
111 | "projects/project-id/databases/(default)",
112 | "authtype":
113 | "unauthenticated",
114 | "authid":
115 | "foo"
116 | }
117 | raw_event = CloudEvent(attributes=attributes, data=json.dumps({}))
118 | decorated_func = firestore_fn.on_document_created(
119 | document="/foo/{bar}")(func)
120 |
121 | decorated_func(raw_event)
122 |
123 | self.assertEqual(hello, "world")
124 |
--------------------------------------------------------------------------------
/tests/test_https_fn.py:
--------------------------------------------------------------------------------
1 | """
2 | Tests for the https module.
3 | """
4 |
5 | import unittest
6 | from unittest.mock import Mock
7 | from flask import Flask, Request
8 | from werkzeug.test import EnvironBuilder
9 |
10 | from firebase_functions import core, https_fn
11 |
12 |
13 | class TestHttps(unittest.TestCase):
14 | """
15 | Tests for the http module.
16 | """
17 |
18 | def test_on_request_calls_init_function(self):
19 | app = Flask(__name__)
20 |
21 | hello = None
22 |
23 | @core.init
24 | def init():
25 | nonlocal hello
26 | hello = "world"
27 |
28 | func = Mock(__name__="example_func")
29 |
30 | with app.test_request_context("/"):
31 | environ = EnvironBuilder(
32 | method="POST",
33 | json={
34 | "data": {
35 | "test": "value"
36 | },
37 | },
38 | ).get_environ()
39 | request = Request(environ)
40 | decorated_func = https_fn.on_request()(func)
41 |
42 | decorated_func(request)
43 |
44 | self.assertEqual(hello, "world")
45 |
46 | def test_on_call_calls_init_function(self):
47 | app = Flask(__name__)
48 |
49 | hello = None
50 |
51 | @core.init
52 | def init():
53 | nonlocal hello
54 | hello = "world"
55 |
56 | func = Mock(__name__="example_func")
57 |
58 | with app.test_request_context("/"):
59 | environ = EnvironBuilder(
60 | method="POST",
61 | json={
62 | "data": {
63 | "test": "value"
64 | },
65 | },
66 | ).get_environ()
67 | request = Request(environ)
68 | decorated_func = https_fn.on_call()(func)
69 |
70 | decorated_func(request)
71 |
72 | self.assertEqual("world", hello)
73 |
--------------------------------------------------------------------------------
/tests/test_identity_fn.py:
--------------------------------------------------------------------------------
1 | """
2 | Identity function tests.
3 | """
4 |
5 | import unittest
6 | from unittest.mock import Mock, patch, MagicMock
7 | from flask import Flask, Request
8 | from werkzeug.test import EnvironBuilder
9 |
10 | from firebase_functions import core, identity_fn
11 |
12 | token_verifier_mock = MagicMock()
13 | token_verifier_mock.verify_auth_blocking_token = Mock(
14 | return_value={
15 | "user_record": {
16 | "uid": "uid",
17 | "metadata": {
18 | "creation_time": 0
19 | },
20 | "provider_data": []
21 | },
22 | "event_id": "event_id",
23 | "ip_address": "ip_address",
24 | "user_agent": "user_agent",
25 | "iat": 0
26 | })
27 | mocked_modules = {
28 | "firebase_functions.private.token_verifier": token_verifier_mock,
29 | }
30 |
31 |
32 | class TestIdentity(unittest.TestCase):
33 | """
34 | Identity function tests.
35 | """
36 |
37 | def test_calls_init_function(self):
38 | hello = None
39 |
40 | @core.init
41 | def init():
42 | nonlocal hello
43 | hello = "world"
44 |
45 | with patch.dict("sys.modules", mocked_modules):
46 | app = Flask(__name__)
47 |
48 | func = Mock(__name__="example_func",
49 | return_value=identity_fn.BeforeSignInResponse())
50 |
51 | with app.test_request_context("/"):
52 | environ = EnvironBuilder(
53 | method="POST",
54 | json={
55 | "data": {
56 | "jwt": "jwt"
57 | },
58 | },
59 | ).get_environ()
60 | request = Request(environ)
61 | decorated_func = identity_fn.before_user_signed_in()(func)
62 | decorated_func(request)
63 |
64 | self.assertEqual("world", hello)
65 |
--------------------------------------------------------------------------------
/tests/test_init.py:
--------------------------------------------------------------------------------
1 | """
2 | Test the init decorator.
3 | """
4 |
5 | import unittest
6 | from firebase_functions import core
7 |
8 |
9 | class TestInit(unittest.TestCase):
10 | """
11 | Test the init decorator.
12 | """
13 |
14 | def test_init_is_initialized(self):
15 |
16 | @core.init
17 | def fn():
18 | pass
19 |
20 | # pylint: disable=protected-access
21 | self.assertIsNotNone(core._init_callback)
22 | # pylint: disable=protected-access
23 | self.assertFalse(core._did_init)
24 |
--------------------------------------------------------------------------------
/tests/test_manifest.py:
--------------------------------------------------------------------------------
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 | """Manifest unit tests."""
15 |
16 | import firebase_functions.private.manifest as _manifest
17 | import firebase_functions.params as _params
18 |
19 | full_endpoint = _manifest.ManifestEndpoint(
20 | platform="gcfv2",
21 | region=["us-west1"],
22 | availableMemoryMb=512,
23 | timeoutSeconds=60,
24 | minInstances=1,
25 | maxInstances=3,
26 | concurrency=20,
27 | vpc={
28 | "connector": "aConnector",
29 | "egressSettings": "ALL_TRAFFIC",
30 | },
31 | serviceAccountEmail="root@",
32 | ingressSettings="ALLOW_ALL",
33 | labels={
34 | "hello": "world",
35 | },
36 | secretEnvironmentVariables=[{
37 | "key": "MY_SECRET"
38 | }],
39 | )
40 |
41 | full_endpoint_dict = {
42 | "platform": "gcfv2",
43 | "region": ["us-west1"],
44 | "availableMemoryMb": 512,
45 | "timeoutSeconds": 60,
46 | "minInstances": 1,
47 | "maxInstances": 3,
48 | "concurrency": 20,
49 | "vpc": {
50 | "connector": "aConnector",
51 | "egressSettings": "ALL_TRAFFIC",
52 | },
53 | "serviceAccountEmail": "root@",
54 | "ingressSettings": "ALLOW_ALL",
55 | "labels": {
56 | "hello": "world",
57 | },
58 | "secretEnvironmentVariables": [{
59 | "key": "MY_SECRET"
60 | }],
61 | }
62 |
63 | full_stack = _manifest.ManifestStack(
64 | endpoints={"test": full_endpoint},
65 | params=[
66 | _params.BoolParam("BOOL_TEST", default=False),
67 | _params.IntParam("INT_TEST", description="int_description"),
68 | _params._FloatParam("FLOAT_TEST", immutable=True),
69 | _params.SecretParam("SECRET_TEST"),
70 | _params.StringParam("STRING_TEST"),
71 | _params.ListParam("LIST_TEST", default=["1", "2", "3"]),
72 | ],
73 | requiredAPIs=[{
74 | "api": "test_api",
75 | "reason": "testing"
76 | }])
77 |
78 | full_stack_dict = {
79 | "specVersion": "v1alpha1",
80 | "endpoints": {
81 | "test": full_endpoint_dict
82 | },
83 | "params": [{
84 | "name": "BOOL_TEST",
85 | "type": "boolean",
86 | "default": False,
87 | }, {
88 | "name": "INT_TEST",
89 | "type": "int",
90 | "description": "int_description"
91 | }, {
92 | "name": "FLOAT_TEST",
93 | "type": "float",
94 | "immutable": True,
95 | }, {
96 | "name": "SECRET_TEST",
97 | "type": "secret"
98 | }, {
99 | "name": "STRING_TEST",
100 | "type": "string"
101 | }, {
102 | "default": ["1", "2", "3"],
103 | "name": "LIST_TEST",
104 | "type": "list"
105 | }],
106 | "requiredAPIs": [{
107 | "api": "test_api",
108 | "reason": "testing"
109 | }]
110 | }
111 |
112 |
113 | class TestManifestStack:
114 | """Stack unit tests."""
115 |
116 | def test_stack_to_dict(self):
117 | """Generic check that all ManifestStack values convert to dict."""
118 | stack_dict = _manifest.manifest_to_spec_dict(full_stack)
119 | assert (stack_dict == full_stack_dict
120 | ), "Generated manifest spec dict does not match expected dict."
121 |
122 |
123 | class TestManifestEndpoint:
124 | """Manifest unit tests."""
125 |
126 | def test_endpoint_to_dict(self):
127 | """Generic check that all ManifestEndpoint values convert to dict."""
128 | # pylint: disable=protected-access
129 | endpoint_dict = _manifest._dataclass_to_spec(full_endpoint)
130 | assert (endpoint_dict == full_endpoint_dict
131 | ), "Generated endpoint spec dict does not match expected dict."
132 |
133 | def test_endpoint_expressions(self):
134 | """Check Expression values convert to CEL strings."""
135 | max_param = _params.IntParam("MAX")
136 | expressions_test = _manifest.ManifestEndpoint(
137 | availableMemoryMb=_params.TernaryExpression(
138 | _params.BoolParam("LARGE_BOOL"), 1024, 256),
139 | minInstances=_params.StringParam("LARGE_STR").equals("yes").then(
140 | 6, 1),
141 | maxInstances=max_param.compare(">", 6).then(6, max_param),
142 | timeoutSeconds=_params.IntParam("WORLD"),
143 | concurrency=_params.IntParam("BAR"),
144 | vpc={"connector": _params.SecretParam("SECRET")})
145 | expressions_expected_dict = {
146 | "platform": "gcfv2",
147 | "region": [],
148 | "secretEnvironmentVariables": [],
149 | "availableMemoryMb": "{{ params.LARGE_BOOL ? 1024 : 256 }}",
150 | "minInstances": "{{ params.LARGE_STR == \"yes\" ? 6 : 1 }}",
151 | "maxInstances": "{{ params.MAX > 6 ? 6 : params.MAX }}",
152 | "timeoutSeconds": "{{ params.WORLD }}",
153 | "concurrency": "{{ params.BAR }}",
154 | "vpc": {
155 | "connector": "{{ params.SECRET }}"
156 | }
157 | }
158 | # pylint: disable=protected-access
159 | expressions_actual_dict = _manifest._dataclass_to_spec(expressions_test)
160 | assert (expressions_actual_dict == expressions_expected_dict
161 | ), "Generated endpoint spec dict does not match expected dict."
162 |
163 | def test_endpoint_nones(self):
164 | """Check all None values are removed."""
165 | expressions_test = _manifest.ManifestEndpoint(
166 | timeoutSeconds=None,
167 | minInstances=None,
168 | maxInstances=None,
169 | concurrency=None,
170 | )
171 | expressions_expected_dict = {
172 | "platform": "gcfv2",
173 | "region": [],
174 | "secretEnvironmentVariables": [],
175 | }
176 | # pylint: disable=protected-access
177 | expressions_actual_dict = _manifest._dataclass_to_spec(expressions_test)
178 | assert (expressions_actual_dict == expressions_expected_dict
179 | ), "Generated endpoint spec dict does not match expected dict."
180 |
--------------------------------------------------------------------------------
/tests/test_path_pattern.py:
--------------------------------------------------------------------------------
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 | """Path Pattern unit tests."""
15 |
16 | from unittest import TestCase
17 | from firebase_functions.private.path_pattern import path_parts, PathPattern, trim_param
18 |
19 |
20 | class TestPathUtilities(TestCase):
21 | """
22 | Tests for path utilities.
23 | """
24 |
25 | def test_path_parts(self):
26 | self.assertEqual(["foo", "bar", "baz"], path_parts("/foo/bar/baz"))
27 | self.assertEqual([], path_parts(""))
28 | self.assertEqual([], path_parts(None))
29 | self.assertEqual([], path_parts("/"))
30 |
31 |
32 | class TestPathPattern(TestCase):
33 | """
34 | Tests for PathPattern.
35 | """
36 |
37 | def test_trim_param(self):
38 | # trim a capture without equals
39 | self.assertEqual(trim_param("{something}"), "something")
40 | # trim a capture with equals
41 | self.assertEqual(trim_param("{something=*}"), "something")
42 |
43 | def test_extract_matches(self):
44 | # parse single-capture segments with leading slash
45 | pp = PathPattern("/messages/{a}/{b}/{c}")
46 | self.assertEqual(
47 | pp.extract_matches("messages/match_a/match_b/match_c"),
48 | {
49 | "a": "match_a",
50 | "b": "match_b",
51 | "c": "match_c",
52 | },
53 | )
54 |
55 | # parse single-capture segments without leading slash
56 | pp = PathPattern("messages/{a}/{b}/{c}")
57 | self.assertEqual(
58 | pp.extract_matches("messages/match_a/match_b/match_c"),
59 | {
60 | "a": "match_a",
61 | "b": "match_b",
62 | "c": "match_c",
63 | },
64 | )
65 |
66 | # parse without multi-capture segments
67 | pp = PathPattern("{a}/something/else/{b}/end/{c}")
68 | self.assertEqual(
69 | pp.extract_matches("match_a/something/else/match_b/end/match_c"),
70 | {
71 | "a": "match_a",
72 | "b": "match_b",
73 | "c": "match_c",
74 | },
75 | )
76 |
77 | # parse multi segment with params after
78 | pp = PathPattern("something/**/else/{a}/hello/{b}/world")
79 | self.assertEqual(
80 | pp.extract_matches(
81 | "something/is/a/thing/else/nothing/hello/user/world"),
82 | {
83 | "a": "nothing",
84 | "b": "user",
85 | },
86 | )
87 |
88 | # parse multi-capture segment with params after
89 | pp = PathPattern("something/{path=**}/else/{a}/hello/{b}/world")
90 | self.assertEqual(
91 | pp.extract_matches(
92 | "something/is/a/thing/else/nothing/hello/user/world"),
93 | {
94 | "path": "is/a/thing",
95 | "a": "nothing",
96 | "b": "user",
97 | },
98 | )
99 |
100 | # parse multi segment with params before
101 | pp = PathPattern("{a}/something/{b}/**/end")
102 | self.assertEqual(
103 | pp.extract_matches(
104 | "match_a/something/match_b/thing/else/nothing/hello/user/end"),
105 | {
106 | "a": "match_a",
107 | "b": "match_b",
108 | },
109 | )
110 |
111 | # parse multi-capture segment with params before
112 | pp = PathPattern("{a}/something/{b}/{path=**}/end")
113 | self.assertEqual(
114 | pp.extract_matches(
115 | "match_a/something/match_b/thing/else/nothing/hello/user/end"),
116 | {
117 | "a": "match_a",
118 | "b": "match_b",
119 | "path": "thing/else/nothing/hello/user",
120 | },
121 | )
122 |
123 | # parse multi segment with params before and after
124 | pp = PathPattern("{a}/something/**/{b}/end")
125 | self.assertEqual(
126 | pp.extract_matches(
127 | "match_a/something/thing/else/nothing/hello/user/match_b/end"),
128 | {
129 | "a": "match_a",
130 | "b": "match_b",
131 | },
132 | )
133 |
134 | # parse multi-capture segment with params before and after
135 | pp = PathPattern("{a}/something/{path=**}/{b}/end")
136 | self.assertEqual(
137 | pp.extract_matches(
138 | "match_a/something/thing/else/nothing/hello/user/match_b/end"),
139 | {
140 | "a": "match_a",
141 | "b": "match_b",
142 | "path": "thing/else/nothing/hello/user",
143 | },
144 | )
145 |
146 | pp = PathPattern("{a}-something-{b}-else-{c}")
147 | self.assertEqual(
148 | pp.extract_matches("match_a-something-match_b-else-match_c"),
149 | {},
150 | )
151 |
152 | pp = PathPattern("{a}")
153 | self.assertEqual(
154 | pp.extract_matches("match_a"),
155 | {
156 | "a": "match_a",
157 | },
158 | )
159 |
--------------------------------------------------------------------------------
/tests/test_pubsub_fn.py:
--------------------------------------------------------------------------------
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 | """PubSub function tests."""
15 | import unittest
16 | import datetime as _dt
17 | from unittest.mock import MagicMock
18 | from cloudevents.http import CloudEvent as _CloudEvent
19 |
20 | from firebase_functions import core
21 | from firebase_functions.pubsub_fn import (
22 | Message,
23 | MessagePublishedData,
24 | on_message_published,
25 | _message_handler,
26 | CloudEvent,
27 | )
28 |
29 |
30 | class TestPubSub(unittest.TestCase):
31 | """
32 | PubSub function tests.
33 | """
34 |
35 | def test_on_message_published_decorator(self):
36 | """
37 | Tests the on_message_published decorator functionality by checking that
38 | the _endpoint attribute is set properly.
39 | """
40 | func = MagicMock()
41 | func.__name__ = "testfn"
42 | decorated_func = on_message_published(topic="hello-world")(func)
43 | endpoint = getattr(decorated_func, "__firebase_endpoint__")
44 | self.assertIsNotNone(endpoint)
45 | self.assertIsNotNone(endpoint.eventTrigger)
46 | self.assertIsNotNone(endpoint.eventTrigger["eventType"])
47 | self.assertEqual("hello-world",
48 | endpoint.eventTrigger["eventFilters"]["topic"])
49 |
50 | def test_message_handler(self):
51 | """
52 | Tests the _message_handler function, ensuring that it correctly processes
53 | the raw event and calls the user-provided function with a properly
54 | formatted CloudEvent instance.
55 | """
56 | func = MagicMock()
57 | raw_event = _CloudEvent(
58 | attributes={
59 | "id": "test-message",
60 | "source": "https://example.com/pubsub",
61 | "specversion": "1.0",
62 | "time": "2023-03-11T13:25:37.403Z",
63 | "type": "com.example.pubsub.message",
64 | },
65 | data={
66 | "message": {
67 | "attributes": {
68 | "key": "value"
69 | },
70 | # {"test": "value"}
71 | "data": "eyJ0ZXN0IjogInZhbHVlIn0=",
72 | "message_id": "message-id-123",
73 | "publish_time": "2023-03-11T13:25:37.403Z",
74 | },
75 | "subscription": "my-subscription",
76 | },
77 | )
78 |
79 | _message_handler(func, raw_event)
80 | func.assert_called_once()
81 | event_arg = func.call_args.args[0]
82 | self.assertIsInstance(event_arg, CloudEvent)
83 | self.assertIsInstance(event_arg.data, MessagePublishedData)
84 | self.assertIsInstance(event_arg.data.message, Message)
85 | self.assertEqual(event_arg.data.message.message_id, "message-id-123")
86 | self.assertEqual(
87 | event_arg.data.message.publish_time,
88 | _dt.datetime.strptime(
89 | "2023-03-11T13:25:37.403Z",
90 | "%Y-%m-%dT%H:%M:%S.%f%z",
91 | ))
92 | self.assertDictEqual(event_arg.data.message.attributes,
93 | {"key": "value"})
94 | self.assertEqual(event_arg.data.message.data,
95 | "eyJ0ZXN0IjogInZhbHVlIn0=")
96 | self.assertIsNone(event_arg.data.message.ordering_key)
97 | self.assertEqual(event_arg.data.subscription, "my-subscription")
98 |
99 | def test_calls_init(self):
100 | hello = None
101 |
102 | @core.init
103 | def init():
104 | nonlocal hello
105 | hello = "world"
106 |
107 | func = MagicMock()
108 | raw_event = _CloudEvent(
109 | attributes={
110 | "id": "test-message",
111 | "source": "https://example.com/pubsub",
112 | "specversion": "1.0",
113 | "time": "2023-03-11T13:25:37.403Z",
114 | "type": "com.example.pubsub.message",
115 | },
116 | data={
117 | "message": {
118 | "attributes": {
119 | "key": "value"
120 | },
121 | "data": "eyJ0ZXN0IjogInZhbHVlIn0=",
122 | "message_id": "message-id-123",
123 | "publish_time": "2023-03-11T13:25:37.403Z",
124 | },
125 | "subscription": "my-subscription",
126 | },
127 | )
128 |
129 | _message_handler(func, raw_event)
130 |
131 | self.assertEqual("world", hello)
132 |
133 | def test_datetime_without_mircroseconds_doesnt_throw(self):
134 | time = "2023-03-11T13:25:37Z"
135 | raw_event = _CloudEvent(
136 | attributes={
137 | "id": "test-message",
138 | "source": "https://example.com/pubsub",
139 | "specversion": "1.0",
140 | "time": time,
141 | "type": "com.example.pubsub.message",
142 | },
143 | data={
144 | "message": {
145 | "attributes": {
146 | "key": "value"
147 | },
148 | "data": "eyJ0ZXN0IjogInZhbHVlIn0=",
149 | "message_id": "message-id-123",
150 | "publish_time": time,
151 | },
152 | "subscription": "my-subscription",
153 | },
154 | )
155 | try:
156 | _message_handler(lambda _: None, raw_event)
157 | # pylint: disable=broad-except
158 | except Exception:
159 | self.fail(
160 | "Datetime without microseconds should not throw an exception")
161 |
--------------------------------------------------------------------------------
/tests/test_remote_config_fn.py:
--------------------------------------------------------------------------------
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 | """Remote Config function tests."""
15 | import unittest
16 | from unittest.mock import MagicMock
17 | from cloudevents.http import CloudEvent as _CloudEvent
18 |
19 | from firebase_functions.remote_config_fn import (
20 | CloudEvent,
21 | ConfigUser,
22 | ConfigUpdateData,
23 | ConfigUpdateOrigin,
24 | ConfigUpdateType,
25 | on_config_updated,
26 | _config_handler,
27 | )
28 |
29 |
30 | class TestRemoteConfig(unittest.TestCase):
31 | """
32 | Remote Config function tests.
33 | """
34 |
35 | def test_on_config_updated_decorator(self):
36 | """
37 | Tests the on_config_updated decorator functionality by checking
38 | that the __firebase_endpoint__ attribute is set properly.
39 | """
40 | func = MagicMock()
41 | func.__name__ = "testfn"
42 | decorated_func = on_config_updated()(func)
43 | endpoint = getattr(decorated_func, "__firebase_endpoint__")
44 | self.assertIsNotNone(endpoint)
45 | self.assertIsNotNone(endpoint.eventTrigger)
46 | self.assertIsNotNone(endpoint.eventTrigger["eventType"])
47 |
48 | def test_config_handler(self):
49 | """
50 | Tests the _config_handler function, ensuring that it correctly processes
51 | the raw event and calls the user-provided function with a properly
52 | formatted CloudEvent instance.
53 | """
54 | func = MagicMock()
55 | raw_event = _CloudEvent(
56 | attributes={
57 | "specversion": "1.0",
58 | "type": "com.example.someevent",
59 | "source": "https://example.com/someevent",
60 | "id": "A234-1234-1234",
61 | "time": "2023-03-11T13:25:37.403Z",
62 | },
63 | data={
64 | "versionNumber": 42,
65 | "updateTime": "2023-03-11T13:25:37.403Z",
66 | "updateUser": {
67 | "name": "John Doe",
68 | "email": "johndoe@example.com",
69 | "imageUrl": "https://example.com/image.jpg"
70 | },
71 | "description": "Test update",
72 | "updateOrigin": "CONSOLE",
73 | "updateType": "INCREMENTAL_UPDATE",
74 | "rollbackSource": 41
75 | })
76 |
77 | _config_handler(func, raw_event)
78 |
79 | func.assert_called_once()
80 |
81 | event_arg = func.call_args.args[0]
82 | self.assertIsInstance(event_arg, CloudEvent)
83 | self.assertIsInstance(event_arg.data, ConfigUpdateData)
84 | self.assertIsInstance(event_arg.data.update_user, ConfigUser)
85 | self.assertEqual(event_arg.data.version_number, 42)
86 | self.assertEqual(event_arg.data.update_origin,
87 | ConfigUpdateOrigin.CONSOLE)
88 | self.assertEqual(event_arg.data.update_type,
89 | ConfigUpdateType.INCREMENTAL_UPDATE)
90 | self.assertEqual(event_arg.data.rollback_source, 41)
91 |
--------------------------------------------------------------------------------
/tests/test_scheduler_fn.py:
--------------------------------------------------------------------------------
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 | """Scheduler function tests."""
15 | import unittest
16 | from unittest.mock import Mock
17 | from datetime import datetime
18 | from flask import Request, Flask
19 | from werkzeug.test import EnvironBuilder
20 | from firebase_functions import scheduler_fn, core
21 |
22 |
23 | class TestScheduler(unittest.TestCase):
24 | """
25 | Scheduler function tests.
26 | """
27 |
28 | def test_on_schedule_decorator(self):
29 | """
30 | Tests the on_schedule decorator functionality by checking
31 | that the __firebase_endpoint__ attribute is set properly.
32 | """
33 |
34 | schedule = "* * * * *"
35 | tz = "America/Los_Angeles"
36 | example_func = Mock(__name__="example_func")
37 | decorated_func = scheduler_fn.on_schedule(
38 | schedule="* * * * *",
39 | timezone=scheduler_fn.Timezone(tz))(example_func)
40 | endpoint = getattr(decorated_func, "__firebase_endpoint__")
41 |
42 | self.assertIsNotNone(endpoint)
43 | self.assertIsNotNone(endpoint.scheduleTrigger)
44 | self.assertEqual(endpoint.scheduleTrigger.get("schedule"), schedule)
45 | self.assertEqual(endpoint.scheduleTrigger.get("timeZone"), tz)
46 |
47 | def test_on_schedule_call(self):
48 | """
49 | Tests to ensure the decorated function is called correctly
50 | with appropriate ScheduledEvent object and returns a 200
51 | status code if successful.
52 | """
53 |
54 | with Flask(__name__).test_request_context("/"):
55 | environ = EnvironBuilder(
56 | headers={
57 | "X-CloudScheduler-JobName": "example-job",
58 | "X-CloudScheduler-ScheduleTime": "2023-04-13T12:00:00-07:00"
59 | }).get_environ()
60 | mock_request = Request(environ)
61 | example_func = Mock(__name__="example_func")
62 | decorated_func = scheduler_fn.on_schedule(
63 | schedule="* * * * *")(example_func)
64 | response = decorated_func(mock_request)
65 |
66 | self.assertEqual(response.status_code, 200)
67 | example_func.assert_called_once_with(
68 | scheduler_fn.ScheduledEvent(
69 | job_name="example-job",
70 | schedule_time=datetime(
71 | 2023,
72 | 4,
73 | 13,
74 | 12,
75 | 0,
76 | tzinfo=scheduler_fn.Timezone("America/Los_Angeles"),
77 | ),
78 | ))
79 |
80 | def test_on_schedule_call_with_no_headers(self):
81 | """
82 | Tests to ensure that if the function is called manually
83 | then the ScheduledEvent object is populated with the
84 | current time and the job_name is None.
85 | """
86 |
87 | with Flask(__name__).test_request_context("/"):
88 | environ = EnvironBuilder().get_environ()
89 | mock_request = Request(environ)
90 | example_func = Mock(__name__="example_func")
91 | decorated_func = scheduler_fn.on_schedule(
92 | schedule="* * * * *")(example_func)
93 | response = decorated_func(mock_request)
94 |
95 | self.assertEqual(response.status_code, 200)
96 | self.assertEqual(example_func.call_count, 1)
97 | self.assertIsNone(example_func.call_args[0][0].job_name)
98 | self.assertIsNotNone(example_func.call_args[0][0].schedule_time)
99 |
100 | def test_on_schedule_call_with_exception(self):
101 | """
102 | Tests to ensure exceptions in the users handler are handled
103 | caught and returns a 500 status code.
104 | """
105 |
106 | with Flask(__name__).test_request_context("/"):
107 | environ = EnvironBuilder(
108 | headers={
109 | "X-CloudScheduler-JobName": "example-job",
110 | "X-CloudScheduler-ScheduleTime": "2023-04-13T12:00:00-07:00"
111 | }).get_environ()
112 | mock_request = Request(environ)
113 | example_func = Mock(__name__="example_func",
114 | side_effect=Exception("Test exception"))
115 | decorated_func = scheduler_fn.on_schedule(
116 | schedule="* * * * *")(example_func)
117 | response = decorated_func(mock_request)
118 |
119 | self.assertEqual(response.status_code, 500)
120 | self.assertEqual(response.data, b"Test exception")
121 |
122 | def test_calls_init(self):
123 | hello = None
124 |
125 | @core.init
126 | def init():
127 | nonlocal hello
128 | hello = "world"
129 |
130 | with Flask(__name__).test_request_context("/"):
131 | environ = EnvironBuilder().get_environ()
132 | mock_request = Request(environ)
133 | example_func = Mock(__name__="example_func")
134 | decorated_func = scheduler_fn.on_schedule(
135 | schedule="* * * * *")(example_func)
136 | decorated_func(mock_request)
137 |
138 | self.assertEqual("world", hello)
139 |
--------------------------------------------------------------------------------
/tests/test_storage_fn.py:
--------------------------------------------------------------------------------
1 | """
2 | Tests for the storage function.
3 | """
4 |
5 | import unittest
6 | from unittest.mock import Mock
7 |
8 | from firebase_functions import core, storage_fn
9 | from cloudevents.http import CloudEvent
10 |
11 |
12 | class TestStorage(unittest.TestCase):
13 | """
14 | Storage function tests.
15 | """
16 |
17 | def test_calls_init(self):
18 | hello = None
19 |
20 | @core.init
21 | def init():
22 | nonlocal hello
23 | hello = "world"
24 |
25 | func = Mock(__name__="example_func")
26 | event = CloudEvent(attributes={
27 | "source": "source",
28 | "type": "type"
29 | },
30 | data={
31 | "bucket": "bucket",
32 | "generation": "generation",
33 | "id": "id",
34 | "metageneration": "metageneration",
35 | "name": "name",
36 | "size": "size",
37 | "storageClass": "storageClass",
38 | })
39 |
40 | decorated_func = storage_fn.on_object_archived(bucket="bucket")(func)
41 | decorated_func(event)
42 |
43 | self.assertEqual("world", hello)
44 |
--------------------------------------------------------------------------------
/tests/test_tasks_fn.py:
--------------------------------------------------------------------------------
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 | """Task Queue function tests."""
15 | import unittest
16 |
17 | from unittest.mock import MagicMock, Mock
18 | from flask import Flask, Request
19 | from werkzeug.test import EnvironBuilder
20 |
21 | from firebase_functions import core
22 | from firebase_functions.tasks_fn import on_task_dispatched, CallableRequest
23 |
24 |
25 | class TestTasks(unittest.TestCase):
26 | """
27 | Task Queue function tests.
28 | """
29 |
30 | def test_on_task_dispatched_decorator(self):
31 | """
32 | Tests the on_task_dispatched decorator functionality by checking
33 | that the __firebase_endpoint__ attribute is set properly.
34 | """
35 |
36 | func = MagicMock()
37 | func.__name__ = "testfn"
38 | decorated_func = on_task_dispatched()(func)
39 | endpoint = getattr(decorated_func, "__firebase_endpoint__")
40 | self.assertIsNotNone(endpoint)
41 | self.assertIsNotNone(endpoint.taskQueueTrigger)
42 |
43 | def test_task_handler(self):
44 | """
45 | Test the proper execution of the task handler created by the on_task_dispatched
46 | decorator. This test will create a Flask app, apply the on_task_dispatched
47 | decorator to the example function, inject a request, and then ensure that a
48 | correct response is generated.
49 | """
50 | app = Flask(__name__)
51 |
52 | @on_task_dispatched()
53 | def example(request: CallableRequest[object]) -> str:
54 | self.assertEqual(request.data, {"test": "value"})
55 | return "Hello World"
56 |
57 | with app.test_request_context("/"):
58 | environ = EnvironBuilder(
59 | method="POST",
60 | json={
61 | "data": {
62 | "test": "value"
63 | },
64 | },
65 | ).get_environ()
66 | request = Request(environ)
67 | response = example(request)
68 | self.assertEqual(response.status_code, 200)
69 | self.assertEqual(
70 | response.get_data(as_text=True),
71 | '{"result":"Hello World"}\n',
72 | )
73 |
74 | def test_calls_init(self):
75 | hello = None
76 |
77 | @core.init
78 | def init():
79 | nonlocal hello
80 | hello = "world"
81 |
82 | app = Flask(__name__)
83 |
84 | func = Mock(__name__="example_func")
85 |
86 | with app.test_request_context("/"):
87 | environ = EnvironBuilder(
88 | method="POST",
89 | json={
90 | "data": {
91 | "test": "value"
92 | },
93 | },
94 | ).get_environ()
95 | request = Request(environ)
96 | decorated_func = on_task_dispatched()(func)
97 | decorated_func(request)
98 |
99 | self.assertEqual("world", hello)
100 |
--------------------------------------------------------------------------------
/tests/test_test_lab_fn.py:
--------------------------------------------------------------------------------
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 | """Test Lab function tests."""
15 | import unittest
16 | from unittest.mock import MagicMock, Mock
17 | from cloudevents.http import CloudEvent as _CloudEvent
18 |
19 | from firebase_functions import core
20 | from firebase_functions.test_lab_fn import (
21 | CloudEvent,
22 | TestMatrixCompletedData,
23 | TestState,
24 | OutcomeSummary,
25 | ResultStorage,
26 | ClientInfo,
27 | on_test_matrix_completed,
28 | _event_handler,
29 | )
30 |
31 |
32 | class TestTestLab(unittest.TestCase):
33 | """
34 | Test Lab function tests.
35 | """
36 |
37 | def test_on_test_matrix_completed_decorator(self):
38 | """
39 | Tests the on_test_matrix_completed decorator functionality by checking
40 | that the __firebase_endpoint__ attribute is set properly.
41 | """
42 | func = MagicMock()
43 | func.__name__ = "testfn"
44 | decorated_func = on_test_matrix_completed()(func)
45 | endpoint = getattr(decorated_func, "__firebase_endpoint__")
46 | self.assertIsNotNone(endpoint)
47 | self.assertIsNotNone(endpoint.eventTrigger)
48 | self.assertIsNotNone(endpoint.eventTrigger["eventType"])
49 |
50 | def test_event_handler(self):
51 | """
52 | Tests the _event_handler function, ensuring that it correctly processes
53 | the raw event and calls the user-provided function with a properly
54 | formatted CloudEvent instance.
55 | """
56 | func = MagicMock()
57 | raw_event = _CloudEvent(
58 | attributes={
59 | "specversion": "1.0",
60 | "type": "com.example.someevent",
61 | "source": "https://example.com/someevent",
62 | "id": "A234-1234-1234",
63 | "time": "2023-03-11T13:25:37.403Z",
64 | },
65 | data={
66 | "createTime": "2023-03-11T13:25:37.403Z",
67 | "state": "FINISHED",
68 | "invalidMatrixDetails": "Some details",
69 | "outcomeSummary": "SUCCESS",
70 | "resultStorage": {
71 | "toolResultsHistory":
72 | "projects/123/histories/456",
73 | "resultsUri":
74 | "https://example.com/results",
75 | "gcsPath":
76 | "gs://bucket/path/to/somewhere",
77 | "toolResultsExecution":
78 | "projects/123/histories/456/executions/789",
79 | },
80 | "clientInfo": {
81 | "client": "gcloud",
82 | },
83 | "testMatrixId": "testmatrix-123",
84 | })
85 |
86 | _event_handler(func, raw_event)
87 |
88 | func.assert_called_once()
89 |
90 | event_arg = func.call_args.args[0]
91 | self.assertIsInstance(event_arg, CloudEvent)
92 | self.assertIsInstance(event_arg.data, TestMatrixCompletedData)
93 | self.assertIsInstance(event_arg.data.result_storage, ResultStorage)
94 | self.assertIsInstance(event_arg.data.client_info, ClientInfo)
95 | self.assertEqual(event_arg.data.state, TestState.FINISHED)
96 | self.assertEqual(event_arg.data.outcome_summary, OutcomeSummary.SUCCESS)
97 | self.assertEqual(event_arg.data.test_matrix_id, "testmatrix-123")
98 |
99 | def test_calls_init(self):
100 | hello = None
101 |
102 | @core.init
103 | def init():
104 | nonlocal hello
105 | hello = "world"
106 |
107 | func = Mock(__name__="example_func")
108 | raw_event = _CloudEvent(
109 | attributes={
110 | "specversion": "1.0",
111 | "type": "com.example.someevent",
112 | "source": "https://example.com/someevent",
113 | "id": "A234-1234-1234",
114 | "time": "2023-03-11T13:25:37.403Z",
115 | },
116 | data={
117 | "createTime": "2023-03-11T13:25:37.403Z",
118 | "state": "FINISHED",
119 | "invalidMatrixDetails": "Some details",
120 | "outcomeSummary": "SUCCESS",
121 | "resultStorage": {
122 | "toolResultsHistory":
123 | "projects/123/histories/456",
124 | "resultsUri":
125 | "https://example.com/results",
126 | "gcsPath":
127 | "gs://bucket/path/to/somewhere",
128 | "toolResultsExecution":
129 | "projects/123/histories/456/executions/789",
130 | },
131 | "clientInfo": {
132 | "client": "gcloud",
133 | },
134 | "testMatrixId": "testmatrix-123",
135 | })
136 |
137 | decorated_func = on_test_matrix_completed()(func)
138 | decorated_func(raw_event)
139 |
140 | func.assert_called_once()
141 |
142 | self.assertEqual("world", hello)
143 |
--------------------------------------------------------------------------------