├── .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 | --------------------------------------------------------------------------------