From 7f58e3e1a7be544b3de1ddf13eb6022c15185c0e Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 10 Jul 2023 19:19:44 +0200 Subject: [PATCH 001/108] chore(deps): update pypa/gh-action-pypi-publish digest to 54d67ed (#256) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fd5ccc6a..0dced000 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: - name: Build distributions run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@110f54a3871763056757c3e203635d4c5711439f # master + uses: pypa/gh-action-pypi-publish@54d67ed3c50a769c633f7db8063c9e634709c1b0 # master with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} From 2e04cc28ae028b8facc85dbdf738e2b8076dbbf7 Mon Sep 17 00:00:00 2001 From: Gareth Date: Mon, 10 Jul 2023 16:30:27 -0700 Subject: [PATCH 002/108] =?UTF-8?q?fix:=20reduce=20gunicorn=20concurrency?= =?UTF-8?q?=20to=20at=20most=204=20*=20maximum=20available=20cor=E2=80=A6?= =?UTF-8?q?=20(#259)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/functions_framework/_http/gunicorn.py | 4 +++- tests/test_http.py | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/functions_framework/_http/gunicorn.py b/src/functions_framework/_http/gunicorn.py index f522b67f..3a9c545b 100644 --- a/src/functions_framework/_http/gunicorn.py +++ b/src/functions_framework/_http/gunicorn.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os + import gunicorn.app.base @@ -20,7 +22,7 @@ def __init__(self, app, host, port, debug, **options): self.options = { "bind": "%s:%s" % (host, port), "workers": 1, - "threads": 1024, + "threads": (os.cpu_count() or 1) * 4, "timeout": 0, "loglevel": "error", "limit_request_line": 0, diff --git a/tests/test_http.py b/tests/test_http.py index 3414aaf6..fbfac9d2 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os import platform import sys @@ -97,7 +98,7 @@ def test_gunicorn_application(debug): assert gunicorn_app.options == { "bind": "%s:%s" % (host, port), "workers": 1, - "threads": 1024, + "threads": os.cpu_count() * 4, "timeout": 0, "loglevel": "error", "limit_request_line": 0, @@ -105,7 +106,7 @@ def test_gunicorn_application(debug): assert gunicorn_app.cfg.bind == ["1.2.3.4:1234"] assert gunicorn_app.cfg.workers == 1 - assert gunicorn_app.cfg.threads == 1024 + assert gunicorn_app.cfg.threads == os.cpu_count() * 4 assert gunicorn_app.cfg.timeout == 0 assert gunicorn_app.load() == app From 671f2cbf1b98b6a10eb70c0b6c0a66610acc85a6 Mon Sep 17 00:00:00 2001 From: Kenneth Rosario Date: Wed, 12 Jul 2023 11:54:55 -0700 Subject: [PATCH 003/108] chore: run buildpack integration test on pull request (#255) --- .github/workflows/buildpack-integration-test.yml | 16 ++++++++++++---- tests/conformance/prerun.sh | 11 +++++------ 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/.github/workflows/buildpack-integration-test.yml b/.github/workflows/buildpack-integration-test.yml index 78c56996..ec9fbf3d 100644 --- a/.github/workflows/buildpack-integration-test.yml +++ b/.github/workflows/buildpack-integration-test.yml @@ -4,14 +4,18 @@ on: push: branches: - master + pull_request: workflow_dispatch: + # Runs every day on 12:00 AM PST + schedule: + - cron: "0 0 * * *" # Declare default permissions as read only. permissions: read-all jobs: python37: - uses: GoogleCloudPlatform/functions-framework-conformance/.github/workflows/buildpack-integration-test.yml@v1.8.0 + uses: GoogleCloudPlatform/functions-framework-conformance/.github/workflows/buildpack-integration-test.yml@v1.8.4 with: http-builder-source: 'tests/conformance' http-builder-target: 'write_http_declarative' @@ -19,9 +23,10 @@ jobs: cloudevent-builder-target: 'write_cloud_event_declarative' prerun: 'tests/conformance/prerun.sh ${{ github.sha }}' builder-runtime: 'python37' + builder-runtime-version: '3.7' start-delay: 5 python38: - uses: GoogleCloudPlatform/functions-framework-conformance/.github/workflows/buildpack-integration-test.yml@v1.8.0 + uses: GoogleCloudPlatform/functions-framework-conformance/.github/workflows/buildpack-integration-test.yml@v1.8.4 with: http-builder-source: 'tests/conformance' http-builder-target: 'write_http_declarative' @@ -29,9 +34,10 @@ jobs: cloudevent-builder-target: 'write_cloud_event_declarative' prerun: 'tests/conformance/prerun.sh ${{ github.sha }}' builder-runtime: 'python38' + builder-runtime-version: '3.8' start-delay: 5 python39: - uses: GoogleCloudPlatform/functions-framework-conformance/.github/workflows/buildpack-integration-test.yml@v1.8.0 + uses: GoogleCloudPlatform/functions-framework-conformance/.github/workflows/buildpack-integration-test.yml@v1.8.4 with: http-builder-source: 'tests/conformance' http-builder-target: 'write_http_declarative' @@ -39,9 +45,10 @@ jobs: cloudevent-builder-target: 'write_cloud_event_declarative' prerun: 'tests/conformance/prerun.sh ${{ github.sha }}' builder-runtime: 'python39' + builder-runtime-version: '3.9' start-delay: 5 python310: - uses: GoogleCloudPlatform/functions-framework-conformance/.github/workflows/buildpack-integration-test.yml@v1.8.0 + uses: GoogleCloudPlatform/functions-framework-conformance/.github/workflows/buildpack-integration-test.yml@v1.8.4 with: http-builder-source: 'tests/conformance' http-builder-target: 'write_http_declarative' @@ -49,4 +56,5 @@ jobs: cloudevent-builder-target: 'write_cloud_event_declarative' prerun: 'tests/conformance/prerun.sh ${{ github.sha }}' builder-runtime: 'python310' + builder-runtime-version: '3.10' start-delay: 5 \ No newline at end of file diff --git a/tests/conformance/prerun.sh b/tests/conformance/prerun.sh index b46f0b51..c37fe62b 100755 --- a/tests/conformance/prerun.sh +++ b/tests/conformance/prerun.sh @@ -7,15 +7,14 @@ set -e FRAMEWORK_VERSION=$1 -if [ -z "${FRAMEWORK_VERSION}" ] - then - echo "Functions Framework version required as first parameter" - exit 1 +if [ -z "${FRAMEWORK_VERSION}" ]; then + echo "Functions Framework version required as first parameter" + exit 1 fi SCRIPT_DIR=$(realpath $(dirname $0)) cd $SCRIPT_DIR -echo "git+https://github.com/GoogleCloudPlatform/functions-framework-python@$FRAMEWORK_VERSION#egg=functions-framework" > requirements.txt -cat requirements.txt \ No newline at end of file +echo "git+https://github.com/GoogleCloudPlatform/functions-framework-python@$FRAMEWORK_VERSION#egg=functions-framework" >requirements.txt +cat requirements.txt From 854509a1e6926495c382e63bd0c3a0117d80b859 Mon Sep 17 00:00:00 2001 From: Gareth Date: Wed, 12 Jul 2023 13:16:50 -0700 Subject: [PATCH 004/108] chore: add conformance test coverage for typed function signature (#254) --- .github/workflows/conformance.yml | 32 +++++++++++++++---------------- tests/conformance/main.py | 22 +++++++++++---------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 5ea928c5..8ebf4e42 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -1,5 +1,9 @@ name: Python Conformance CI -on: [push, pull_request] +on: + push: + branches: + - 'master' + pull_request: # Declare default permissions as read only. permissions: read-all @@ -39,67 +43,61 @@ jobs: - name: Setup Go uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1 with: - go-version: '1.16' + go-version: '1.20' - name: Run HTTP conformance tests - uses: GoogleCloudPlatform/functions-framework-conformance/action@1975792fb34ebbfa058d690666186d669d3a5977 # v1.8.0 + uses: GoogleCloudPlatform/functions-framework-conformance/action@5f2a796b58f099d749e70ecc83f531f6701c64af # v1.8.3 with: - version: 'v1.6.0' functionType: 'http' useBuildpacks: false validateMapping: false cmd: "'functions-framework --source tests/conformance/main.py --target write_http --signature-type http'" - name: Run event conformance tests - uses: GoogleCloudPlatform/functions-framework-conformance/action@1975792fb34ebbfa058d690666186d669d3a5977 # v1.8.0 + uses: GoogleCloudPlatform/functions-framework-conformance/action@5f2a796b58f099d749e70ecc83f531f6701c64af # v1.8.3 with: - version: 'v1.6.0' functionType: 'legacyevent' useBuildpacks: false validateMapping: true cmd: "'functions-framework --source tests/conformance/main.py --target write_legacy_event --signature-type event'" - name: Run CloudEvents conformance tests - uses: GoogleCloudPlatform/functions-framework-conformance/action@1975792fb34ebbfa058d690666186d669d3a5977 # v1.8.0 + uses: GoogleCloudPlatform/functions-framework-conformance/action@5f2a796b58f099d749e70ecc83f531f6701c64af # v1.8.3 with: - version: 'v1.6.0' functionType: 'cloudevent' useBuildpacks: false validateMapping: true cmd: "'functions-framework --source tests/conformance/main.py --target write_cloud_event --signature-type cloudevent'" - name: Run HTTP conformance tests declarative - uses: GoogleCloudPlatform/functions-framework-conformance/action@1975792fb34ebbfa058d690666186d669d3a5977 # v1.8.0 + uses: GoogleCloudPlatform/functions-framework-conformance/action@5f2a796b58f099d749e70ecc83f531f6701c64af # v1.8.3 with: - version: 'v1.6.0' functionType: 'http' useBuildpacks: false validateMapping: false cmd: "'functions-framework --source tests/conformance/main.py --target write_http_declarative'" - name: Run CloudEvents conformance tests declarative - uses: GoogleCloudPlatform/functions-framework-conformance/action@1975792fb34ebbfa058d690666186d669d3a5977 # v1.8.0 + uses: GoogleCloudPlatform/functions-framework-conformance/action@5f2a796b58f099d749e70ecc83f531f6701c64af # v1.8.3 with: - version: 'v1.6.0' functionType: 'cloudevent' useBuildpacks: false validateMapping: true cmd: "'functions-framework --source tests/conformance/main.py --target write_cloud_event_declarative'" - name: Run HTTP concurrency tests declarative - uses: GoogleCloudPlatform/functions-framework-conformance/action@1975792fb34ebbfa058d690666186d669d3a5977 # v1.8.0 + uses: GoogleCloudPlatform/functions-framework-conformance/action@5f2a796b58f099d749e70ecc83f531f6701c64af # v1.8.3 with: - version: 'v1.6.0' functionType: 'http' useBuildpacks: false validateConcurrency: true cmd: "'functions-framework --source tests/conformance/main.py --target write_http_declarative_concurrent'" - name: Run Typed tests declarative - uses: GoogleCloudPlatform/functions-framework-conformance/action@1975792fb34ebbfa058d690666186d669d3a5977 # v1.8.0 + uses: GoogleCloudPlatform/functions-framework-conformance/action@5f2a796b58f099d749e70ecc83f531f6701c64af # v1.8.3 with: - version: 'v1.6.0' functionType: 'http' + declarativeType: 'typed' useBuildpacks: false validateMapping: false - cmd: "'functions-framework --source tests/conformance/main.py --target write_typed_event_declarative'" + cmd: "'functions-framework --source tests/conformance/main.py --target typed_conformance_test'" diff --git a/tests/conformance/main.py b/tests/conformance/main.py index 057edaa7..4f164d62 100644 --- a/tests/conformance/main.py +++ b/tests/conformance/main.py @@ -8,15 +8,18 @@ filename = "function_output.json" -class ConformanceType: - json_request: str +class RawJson: + data: dict - def __init__(self, json_request: str) -> None: - self.json_request = json_request + def __init__(self, data): + self.data = data @staticmethod - def from_dict(obj: dict) -> "ConformanceType": - return ConformanceType(json.dumps(obj)) + def from_dict(obj: dict) -> "RawJson": + return RawJson(obj) + + def to_dict(self) -> dict: + return self.data def _write_output(content): @@ -66,7 +69,6 @@ def write_http_declarative_concurrent(request): return "OK", 200 -@functions_framework.typed(ConformanceType) -def write_typed_event_declarative(x): - _write_output(x.json_request) - return "OK" +@functions_framework.typed(RawJson) +def typed_conformance_test(x): + return RawJson({"payload": x.data}) From 3bed433de33abdf37f4decd32822dcde5f98f37a Mon Sep 17 00:00:00 2001 From: Kenneth Rosario Date: Fri, 14 Jul 2023 14:27:47 -0700 Subject: [PATCH 005/108] chore: Fix workflow duplication (#261) --- .github/workflows/lint.yml | 6 +++++- .github/workflows/unit.yml | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1c0abd0f..88af8d1e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,5 +1,9 @@ name: Python Lint CI -on: [push, pull_request] +on: + push: + branches: + - master + pull_request: permissions: contents: read diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 0aeb0e7c..dd40b16e 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -1,5 +1,9 @@ name: Python Unit CI -on: [push, pull_request] +on: + push: + branches: + - master + pull_request: permissions: contents: read From 8829aed1590f9092072660ac0773e3e7ba89edbf Mon Sep 17 00:00:00 2001 From: HKWinterhalter Date: Mon, 17 Jul 2023 17:09:31 -0700 Subject: [PATCH 006/108] ci: rename master branch to main (#263) * ci: rename master branch to main * ci: retarget buildpack integration tests to latest * retarget buildpack integration tests to conformance main --- .github/workflows/buildpack-integration-test.yml | 12 ++++++------ .github/workflows/codeql.yml | 4 ++-- .github/workflows/conformance.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/scorecard.yml | 2 +- .github/workflows/unit.yml | 2 +- README.md | 4 ++-- src/functions_framework/event_conversion.py | 2 +- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/buildpack-integration-test.yml b/.github/workflows/buildpack-integration-test.yml index ec9fbf3d..0c9e6eff 100644 --- a/.github/workflows/buildpack-integration-test.yml +++ b/.github/workflows/buildpack-integration-test.yml @@ -3,7 +3,7 @@ name: Buildpack Integration Test on: push: branches: - - master + - main pull_request: workflow_dispatch: # Runs every day on 12:00 AM PST @@ -15,7 +15,7 @@ permissions: read-all jobs: python37: - uses: GoogleCloudPlatform/functions-framework-conformance/.github/workflows/buildpack-integration-test.yml@v1.8.4 + uses: GoogleCloudPlatform/functions-framework-conformance/.github/workflows/buildpack-integration-test.yml@main with: http-builder-source: 'tests/conformance' http-builder-target: 'write_http_declarative' @@ -26,7 +26,7 @@ jobs: builder-runtime-version: '3.7' start-delay: 5 python38: - uses: GoogleCloudPlatform/functions-framework-conformance/.github/workflows/buildpack-integration-test.yml@v1.8.4 + uses: GoogleCloudPlatform/functions-framework-conformance/.github/workflows/buildpack-integration-test.yml@main with: http-builder-source: 'tests/conformance' http-builder-target: 'write_http_declarative' @@ -37,7 +37,7 @@ jobs: builder-runtime-version: '3.8' start-delay: 5 python39: - uses: GoogleCloudPlatform/functions-framework-conformance/.github/workflows/buildpack-integration-test.yml@v1.8.4 + uses: GoogleCloudPlatform/functions-framework-conformance/.github/workflows/buildpack-integration-test.yml@main with: http-builder-source: 'tests/conformance' http-builder-target: 'write_http_declarative' @@ -48,7 +48,7 @@ jobs: builder-runtime-version: '3.9' start-delay: 5 python310: - uses: GoogleCloudPlatform/functions-framework-conformance/.github/workflows/buildpack-integration-test.yml@v1.8.4 + uses: GoogleCloudPlatform/functions-framework-conformance/.github/workflows/buildpack-integration-test.yml@main with: http-builder-source: 'tests/conformance' http-builder-target: 'write_http_declarative' @@ -57,4 +57,4 @@ jobs: prerun: 'tests/conformance/prerun.sh ${{ github.sha }}' builder-runtime: 'python310' builder-runtime-version: '3.10' - start-delay: 5 \ No newline at end of file + start-delay: 5 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 33614b00..dd1d1d77 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -13,10 +13,10 @@ name: "CodeQL" on: push: - branches: ["master"] + branches: ["main"] pull_request: # The branches below must be a subset of the branches above - branches: ["master"] + branches: ["main"] schedule: - cron: "0 0 * * 1" diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 8ebf4e42..b892a3ea 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -2,7 +2,7 @@ name: Python Conformance CI on: push: branches: - - 'master' + - 'main' pull_request: # Declare default permissions as read only. diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 88af8d1e..fc00bc6f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,7 +2,7 @@ name: Python Lint CI on: push: branches: - - master + - main pull_request: permissions: contents: read diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0dced000..519f287b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: - name: Build distributions run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@54d67ed3c50a769c633f7db8063c9e634709c1b0 # master + uses: pypa/gh-action-pypi-publish@54d67ed3c50a769c633f7db8063c9e634709c1b0 # main with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index e8b1f04c..0c24297e 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -8,7 +8,7 @@ on: schedule: - cron: '0 */12 * * *' push: - branches: [ "master" ] + branches: [ "main" ] workflow_dispatch: # Declare default permissions as read only. diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index dd40b16e..381c28b0 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -2,7 +2,7 @@ name: Python Unit CI on: push: branches: - - master + - main pull_request: permissions: contents: read diff --git a/README.md b/README.md index ce094471..0433028d 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ def hello_cloud_event(cloud_event): print(f"Received event with ID: {cloud_event['id']} and data {cloud_event.data}") ``` -> Your function is passed a single [CloudEvent](https://github.com/cloudevents/sdk-python/blob/master/cloudevents/sdk/event/v1.py) parameter. +> Your function is passed a single [CloudEvent](https://github.com/cloudevents/sdk-python/blob/main/cloudevents/sdk/event/v1.py) parameter. Run the following command to run `hello_cloud_event` target locally: @@ -302,7 +302,7 @@ After you've written your function, you can simply deploy it from your local mac ### Cloud Run/Cloud Run on GKE -Once you've written your function and added the Functions Framework to your `requirements.txt` file, all that's left is to create a container image. [Check out the Cloud Run quickstart](https://cloud.google.com/run/docs/quickstarts/build-and-deploy) for Python to create a container image and deploy it to Cloud Run. You'll write a `Dockerfile` when you build your container. This `Dockerfile` allows you to specify exactly what goes into your container (including custom binaries, a specific operating system, and more). [Here is an example `Dockerfile` that calls Functions Framework.](https://github.com/GoogleCloudPlatform/functions-framework-python/blob/master/examples/cloud_run_http) +Once you've written your function and added the Functions Framework to your `requirements.txt` file, all that's left is to create a container image. [Check out the Cloud Run quickstart](https://cloud.google.com/run/docs/quickstarts/build-and-deploy) for Python to create a container image and deploy it to Cloud Run. You'll write a `Dockerfile` when you build your container. This `Dockerfile` allows you to specify exactly what goes into your container (including custom binaries, a specific operating system, and more). [Here is an example `Dockerfile` that calls Functions Framework.](https://github.com/GoogleCloudPlatform/functions-framework-python/blob/main/examples/cloud_run_http) If you want even more control over the environment, you can [deploy your container image to Cloud Run on GKE](https://cloud.google.com/run/docs/quickstarts/prebuilt-deploy-gke). With Cloud Run on GKE, you can run your function on a GKE cluster, which gives you additional control over the environment (including use of GPU-based instances, longer timeouts and more). diff --git a/src/functions_framework/event_conversion.py b/src/functions_framework/event_conversion.py index 06e5a812..0e67cdaa 100644 --- a/src/functions_framework/event_conversion.py +++ b/src/functions_framework/event_conversion.py @@ -27,7 +27,7 @@ # Maps background/legacy event types to their equivalent CloudEvent types. # For more info on event mappings see -# https://github.com/GoogleCloudPlatform/functions-framework-conformance/blob/master/docs/mapping.md +# https://github.com/GoogleCloudPlatform/functions-framework-conformance/blob/main/docs/mapping.md _BACKGROUND_TO_CE_TYPE = { "google.pubsub.topic.publish": "google.cloud.pubsub.topic.v1.messagePublished", "providers/cloud.pubsub/eventTypes/topic.publish": "google.cloud.pubsub.topic.v1.messagePublished", From a7076a63665842bc97315b360863901b16a5819f Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 21 Aug 2023 17:54:31 +0200 Subject: [PATCH 007/108] chore(deps): update all non-major dependencies (#257) --- .github/workflows/codeql.yml | 10 +++++----- .github/workflows/conformance.yml | 22 +++++++++++----------- .github/workflows/dependency-review.yml | 6 +++--- .github/workflows/lint.yml | 6 +++--- .github/workflows/release.yml | 6 +++--- .github/workflows/scorecard.yml | 8 ++++---- .github/workflows/unit.yml | 6 +++--- 7 files changed, 32 insertions(+), 32 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index dd1d1d77..da547011 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@128a63446a954579617e875aaab7d2978154e969 # v2.4.0 + uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1 with: disable-sudo: true egress-policy: block @@ -53,11 +53,11 @@ jobs: objects.githubusercontent.com:443 - name: Checkout repository - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@83f0fe6c4988d98a455712a27f0255212bba9bd4 # v2.3.6 + uses: github/codeql-action/init@a09933a12a80f87b87005513f0abb1494c27a716 # v2.21.4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -67,7 +67,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@83f0fe6c4988d98a455712a27f0255212bba9bd4 # v2.3.6 + uses: github/codeql-action/autobuild@a09933a12a80f87b87005513f0abb1494c27a716 # v2.21.4 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -80,6 +80,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@83f0fe6c4988d98a455712a27f0255212bba9bd4 # v2.3.6 + uses: github/codeql-action/analyze@a09933a12a80f87b87005513f0abb1494c27a716 # v2.21.4 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index b892a3ea..637a20d4 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -16,7 +16,7 @@ jobs: python: ['3.7', '3.8', '3.9', '3.10', '3.11'] steps: - name: Harden Runner - uses: step-security/harden-runner@128a63446a954579617e875aaab7d2978154e969 # v2.4.0 + uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1 with: disable-sudo: true egress-policy: block @@ -30,10 +30,10 @@ jobs: storage.googleapis.com:443 - name: Checkout code - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - name: Setup Python - uses: actions/setup-python@bd6b4b6205c4dbad673328db7b31b7fab9e241c0 # v4.6.1 + uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 with: python-version: ${{ matrix.python }} @@ -41,12 +41,12 @@ jobs: run: python -m pip install -e . - name: Setup Go - uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1 + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: go-version: '1.20' - name: Run HTTP conformance tests - uses: GoogleCloudPlatform/functions-framework-conformance/action@5f2a796b58f099d749e70ecc83f531f6701c64af # v1.8.3 + uses: GoogleCloudPlatform/functions-framework-conformance/action@64d4646c26d11281faf5e8813dd0fc8a9551cac3 # v1.8.5 with: functionType: 'http' useBuildpacks: false @@ -54,7 +54,7 @@ jobs: cmd: "'functions-framework --source tests/conformance/main.py --target write_http --signature-type http'" - name: Run event conformance tests - uses: GoogleCloudPlatform/functions-framework-conformance/action@5f2a796b58f099d749e70ecc83f531f6701c64af # v1.8.3 + uses: GoogleCloudPlatform/functions-framework-conformance/action@64d4646c26d11281faf5e8813dd0fc8a9551cac3 # v1.8.5 with: functionType: 'legacyevent' useBuildpacks: false @@ -62,7 +62,7 @@ jobs: cmd: "'functions-framework --source tests/conformance/main.py --target write_legacy_event --signature-type event'" - name: Run CloudEvents conformance tests - uses: GoogleCloudPlatform/functions-framework-conformance/action@5f2a796b58f099d749e70ecc83f531f6701c64af # v1.8.3 + uses: GoogleCloudPlatform/functions-framework-conformance/action@64d4646c26d11281faf5e8813dd0fc8a9551cac3 # v1.8.5 with: functionType: 'cloudevent' useBuildpacks: false @@ -70,7 +70,7 @@ jobs: cmd: "'functions-framework --source tests/conformance/main.py --target write_cloud_event --signature-type cloudevent'" - name: Run HTTP conformance tests declarative - uses: GoogleCloudPlatform/functions-framework-conformance/action@5f2a796b58f099d749e70ecc83f531f6701c64af # v1.8.3 + uses: GoogleCloudPlatform/functions-framework-conformance/action@64d4646c26d11281faf5e8813dd0fc8a9551cac3 # v1.8.5 with: functionType: 'http' useBuildpacks: false @@ -78,7 +78,7 @@ jobs: cmd: "'functions-framework --source tests/conformance/main.py --target write_http_declarative'" - name: Run CloudEvents conformance tests declarative - uses: GoogleCloudPlatform/functions-framework-conformance/action@5f2a796b58f099d749e70ecc83f531f6701c64af # v1.8.3 + uses: GoogleCloudPlatform/functions-framework-conformance/action@64d4646c26d11281faf5e8813dd0fc8a9551cac3 # v1.8.5 with: functionType: 'cloudevent' useBuildpacks: false @@ -86,7 +86,7 @@ jobs: cmd: "'functions-framework --source tests/conformance/main.py --target write_cloud_event_declarative'" - name: Run HTTP concurrency tests declarative - uses: GoogleCloudPlatform/functions-framework-conformance/action@5f2a796b58f099d749e70ecc83f531f6701c64af # v1.8.3 + uses: GoogleCloudPlatform/functions-framework-conformance/action@64d4646c26d11281faf5e8813dd0fc8a9551cac3 # v1.8.5 with: functionType: 'http' useBuildpacks: false @@ -94,7 +94,7 @@ jobs: cmd: "'functions-framework --source tests/conformance/main.py --target write_http_declarative_concurrent'" - name: Run Typed tests declarative - uses: GoogleCloudPlatform/functions-framework-conformance/action@5f2a796b58f099d749e70ecc83f531f6701c64af # v1.8.3 + uses: GoogleCloudPlatform/functions-framework-conformance/action@64d4646c26d11281faf5e8813dd0fc8a9551cac3 # v1.8.5 with: functionType: 'http' declarativeType: 'typed' diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index ee6bdbec..799874eb 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@128a63446a954579617e875aaab7d2978154e969 # v2.4.0 + uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1 with: disable-sudo: true egress-policy: block @@ -25,6 +25,6 @@ jobs: api.github.com:443 github.com:443 - name: 'Checkout Repository' - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - name: 'Dependency Review' - uses: actions/dependency-review-action@1360a344ccb0ab6e9475edef90ad2f46bf8003b1 # v3.0.6 + uses: actions/dependency-review-action@f6fff72a3217f580d5afd49a46826795305b63c7 # v3.0.8 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index fc00bc6f..e53049e5 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@128a63446a954579617e875aaab7d2978154e969 # v2.4.0 + uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1 with: disable-sudo: true egress-policy: block @@ -21,9 +21,9 @@ jobs: github.com:443 pypi.org:443 - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - name: Setup Python - uses: actions/setup-python@bd6b4b6205c4dbad673328db7b31b7fab9e241c0 # v4.6.1 + uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 - name: Install tox run: python -m pip install tox - name: Lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 519f287b..89d66fa1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,16 +13,16 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@128a63446a954579617e875aaab7d2978154e969 # v2.4.0 + uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - name: Checkout - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 with: ref: ${{ github.event.release.tag_name }} - name: Install Python - uses: actions/setup-python@bd6b4b6205c4dbad673328db7b31b7fab9e241c0 # v4.6.1 + uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 - name: Install build dependencies run: python -m pip install -U setuptools build wheel - name: Build distributions diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 0c24297e..386dd69c 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@128a63446a954579617e875aaab7d2978154e969 # v2.4.0 + uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1 with: disable-sudo: true egress-policy: block @@ -44,12 +44,12 @@ jobs: rekor.sigstore.dev:443 - name: "Checkout code" - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@80e868c13c90f172d68d1f4501dee99e2479f7af # v2.1.3 + uses: ossf/scorecard-action@08b4669551908b1024bb425080c797723083c031 # v2.2.0 with: results_file: results.sarif results_format: sarif @@ -61,6 +61,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@83f0fe6c4988d98a455712a27f0255212bba9bd4 # v2.3.6 + uses: github/codeql-action/upload-sarif@a09933a12a80f87b87005513f0abb1494c27a716 # v2.21.4 with: sarif_file: results.sarif diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 381c28b0..9f98dea7 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -16,7 +16,7 @@ jobs: runs-on: ${{ matrix.platform }} steps: - name: Harden Runner - uses: step-security/harden-runner@128a63446a954579617e875aaab7d2978154e969 # v2.4.0 + uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1 with: disable-sudo: true egress-policy: block @@ -29,9 +29,9 @@ jobs: registry-1.docker.io:443 - name: Checkout - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - name: Use Python ${{ matrix.python }} - uses: actions/setup-python@bd6b4b6205c4dbad673328db7b31b7fab9e241c0 # v4.6.1 + uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 with: python-version: ${{ matrix.python }} - name: Install tox From 049e031d802cc6af35784b12b1d41e85781339ea Mon Sep 17 00:00:00 2001 From: Kenneth Rosario Date: Tue, 22 Aug 2023 11:06:16 -0700 Subject: [PATCH 008/108] chre: Update scorecard.yml with missing endpoint (#268) * chre: Update scorecard.yml with missing endpoint * Add wildcard endpoint --- .github/workflows/scorecard.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 386dd69c..d6a0d834 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -36,12 +36,12 @@ jobs: api.securityscorecards.dev:443 auth.docker.io:443 bestpractices.coreinfrastructure.org:443 - fulcio.sigstore.dev:443 github.com:443 index.docker.io:443 oss-fuzz-build-logs.storage.googleapis.com:443 sigstore-tuf-root.storage.googleapis.com:443 - rekor.sigstore.dev:443 + *.sigstore.dev:443 + - name: "Checkout code" uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 From 45aed538b5e39655318c7841457399fa3376ceaf Mon Sep 17 00:00:00 2001 From: Jonathan Ballet Date: Tue, 29 Aug 2023 01:28:46 +0200 Subject: [PATCH 009/108] feat: initial typing of the public API (#248) * Initial typing of the public API This adds type annotations for the 2 main decorators of the library. Closes: https://github.com/GoogleCloudPlatform/functions-framework-python/issues/190 * fix typing * Remove zip_safe flag, as it's not needed anymore. * run black * fix import order --------- Co-authored-by: Gareth --- README.md | 6 ++++-- setup.py | 1 + src/functions_framework/__init__.py | 10 +++++++--- src/functions_framework/py.typed | 0 tests/test_typing.py | 16 ++++++++++++++++ tox.ini | 2 ++ 6 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 src/functions_framework/py.typed create mode 100644 tests/test_typing.py diff --git a/README.md b/README.md index 0433028d..b32b2e3e 100644 --- a/README.md +++ b/README.md @@ -59,10 +59,11 @@ functions-framework==3.* Create an `main.py` file with the following contents: ```python +import flask import functions_framework @functions_framework.http -def hello(request): +def hello(request: flask.Request) -> flask.typing.ResponseReturnValue: return "Hello world!" ``` @@ -98,9 +99,10 @@ Create an `main.py` file with the following contents: ```python import functions_framework +from cloudevents.http.event import CloudEvent @functions_framework.cloud_event -def hello_cloud_event(cloud_event): +def hello_cloud_event(cloud_event: CloudEvent) -> None: print(f"Received event with ID: {cloud_event['id']} and data {cloud_event.data}") ``` diff --git a/setup.py b/setup.py index 9e8aedab..0a98faef 100644 --- a/setup.py +++ b/setup.py @@ -46,6 +46,7 @@ ], keywords="functions-framework", packages=find_packages(where="src"), + package_data={"functions_framework": ["py.typed"]}, namespace_packages=["google", "google.cloud"], package_dir={"": "src"}, python_requires=">=3.5, <4", diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index d4575b57..8d47670c 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -23,13 +23,14 @@ import types from inspect import signature -from typing import Type +from typing import Callable, Type import cloudevents.exceptions as cloud_exceptions import flask import werkzeug from cloudevents.http import from_http, is_binary +from cloudevents.http.event import CloudEvent from functions_framework import _function_registry, _typed_event, event_conversion from functions_framework.background_event import BackgroundEvent @@ -45,6 +46,9 @@ _CLOUDEVENT_MIME_TYPE = "application/cloudevents+json" +CloudEventFunction = Callable[[CloudEvent], None] +HTTPFunction = Callable[[flask.Request], flask.typing.ResponseReturnValue] + class _LoggingHandler(io.TextIOWrapper): """Logging replacement for stdout and stderr in GCF Python 3.7.""" @@ -59,7 +63,7 @@ def write(self, out): return self.stderr.write(json.dumps(payload) + "\n") -def cloud_event(func): +def cloud_event(func: CloudEventFunction) -> CloudEventFunction: """Decorator that registers cloudevent as user function signature type.""" _function_registry.REGISTRY_MAP[ func.__name__ @@ -99,7 +103,7 @@ def wrapper(*args, **kwargs): return _typed -def http(func): +def http(func: HTTPFunction) -> HTTPFunction: """Decorator that registers http as user function signature type.""" _function_registry.REGISTRY_MAP[ func.__name__ diff --git a/src/functions_framework/py.typed b/src/functions_framework/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_typing.py b/tests/test_typing.py new file mode 100644 index 00000000..279cd636 --- /dev/null +++ b/tests/test_typing.py @@ -0,0 +1,16 @@ +import typing + +if typing.TYPE_CHECKING: # pragma: no cover + import flask + + from cloudevents.http.event import CloudEvent + + import functions_framework + + @functions_framework.http + def hello(request: flask.Request) -> flask.typing.ResponseReturnValue: + return "Hello world!" + + @functions_framework.cloud_event + def hello_cloud_event(cloud_event: CloudEvent) -> None: + print(f"Received event: id={cloud_event['id']} and data={cloud_event.data}") diff --git a/tox.ini b/tox.ini index 0fe3dba6..e8c555b5 100644 --- a/tox.ini +++ b/tox.ini @@ -19,8 +19,10 @@ deps = black twine isort + mypy commands = black --check src tests setup.py conftest.py --exclude tests/test_functions/background_load_error/main.py isort -c src tests setup.py conftest.py + mypy tests/test_typing.py python setup.py --quiet sdist bdist_wheel twine check dist/* From 31907158995a1cda7d735291779fc029fea23c11 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 6 Sep 2023 03:26:24 +0200 Subject: [PATCH 010/108] chore(deps): update pypa/gh-action-pypi-publish digest to 8cdc2ab (#271) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 89d66fa1..40fc39b6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: - name: Build distributions run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@54d67ed3c50a769c633f7db8063c9e634709c1b0 # main + uses: pypa/gh-action-pypi-publish@8cdc2ab67c943c5edf5fd9ae1995546b4b550602 # main with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} From 46780dac88c8dfe715babe89f792d08e9ca482e7 Mon Sep 17 00:00:00 2001 From: David Laban Date: Wed, 6 Sep 2023 18:27:02 +0100 Subject: [PATCH 011/108] fix: don't exit on reload if there is a syntax error (#214) * fix: don't exit on reload if there is a syntax error (#213) * style: reformat using black * test: robustness to syntax error in debug mode startup --- src/functions_framework/__init__.py | 23 +++++++++++++++++++++-- tests/test_functions.py | 13 +++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index 8d47670c..ece4f446 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -357,11 +357,30 @@ def handle_none(rv): # Execute the module, within the application context with _app.app_context(): - spec.loader.exec_module(source_module) + try: + spec.loader.exec_module(source_module) + function = _function_registry.get_user_function( + source, source_module, target + ) + except Exception as e: + if werkzeug.serving.is_running_from_reloader(): + # When reloading, print out the error immediately, but raise + # it later so the debugger or server can handle it. + import traceback + + traceback.print_exc() + err = e + + def function(*_args, **_kwargs): + raise err from None + + else: + # When not reloading, raise the error immediately so the + # command fails. + raise e from None # Get the configured function signature type signature_type = _function_registry.get_func_signature_type(target, signature_type) - function = _function_registry.get_user_function(source, source_module, target) _configure_app(_app, function, signature_type) diff --git a/tests/test_functions.py b/tests/test_functions.py index 81860cae..f0bd7793 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -323,6 +323,19 @@ def test_invalid_function_definition_function_syntax_error(): ) +def test_invalid_function_definition_function_syntax_robustness_with_debug(monkeypatch): + monkeypatch.setattr( + functions_framework.werkzeug.serving, "is_running_from_reloader", lambda: True + ) + source = TEST_FUNCTIONS_DIR / "background_load_error" / "main.py" + target = "function" + + client = create_app(target, source).test_client() + + resp = client.get("/") + assert resp.status_code == 500 + + def test_invalid_function_definition_missing_dependency(): source = TEST_FUNCTIONS_DIR / "background_missing_dependency" / "main.py" target = "function" From b58361056acaaaf6b5185fde6adfcd374bc4bb1b Mon Sep 17 00:00:00 2001 From: HKWinterhalter Date: Wed, 6 Sep 2023 11:51:16 -0700 Subject: [PATCH 012/108] chore: update blunderbuss.yml (#273) --- .github/blunderbuss.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/blunderbuss.yml b/.github/blunderbuss.yml index 33581568..ffa474e5 100644 --- a/.github/blunderbuss.yml +++ b/.github/blunderbuss.yml @@ -1,9 +1,7 @@ assign_prs: - - KaylaNguyen - HKWinterhalter - janell-chen assign_issues: - - KaylaNguyen - HKWinterhalter - janell-chen From 8e904c531233f440466774cfb400cd1e587ac6f2 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 8 Sep 2023 02:40:29 +0200 Subject: [PATCH 013/108] chore(deps): update all non-major dependencies (#272) --- .github/workflows/codeql.yml | 8 ++++---- .github/workflows/conformance.yml | 2 +- .github/workflows/dependency-review.yml | 4 ++-- .github/workflows/lint.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/scorecard.yml | 4 ++-- .github/workflows/unit.yml | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index da547011..6a368480 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,11 +53,11 @@ jobs: objects.githubusercontent.com:443 - name: Checkout repository - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@a09933a12a80f87b87005513f0abb1494c27a716 # v2.21.4 + uses: github/codeql-action/init@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.21.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -67,7 +67,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@a09933a12a80f87b87005513f0abb1494c27a716 # v2.21.4 + uses: github/codeql-action/autobuild@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.21.5 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -80,6 +80,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@a09933a12a80f87b87005513f0abb1494c27a716 # v2.21.4 + uses: github/codeql-action/analyze@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.21.5 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 637a20d4..c7f4352f 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -30,7 +30,7 @@ jobs: storage.googleapis.com:443 - name: Checkout code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - name: Setup Python uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 799874eb..707087cd 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -25,6 +25,6 @@ jobs: api.github.com:443 github.com:443 - name: 'Checkout Repository' - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - name: 'Dependency Review' - uses: actions/dependency-review-action@f6fff72a3217f580d5afd49a46826795305b63c7 # v3.0.8 + uses: actions/dependency-review-action@6c5ccdad469c9f8a2996bfecaec55a631a347034 # v3.1.0 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e53049e5..1add91f0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,7 +21,7 @@ jobs: github.com:443 pypi.org:443 - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - name: Setup Python uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 - name: Install tox diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 40fc39b6..2ec287c8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - name: Checkout - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: ref: ${{ github.event.release.tag_name }} - name: Install Python diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index d6a0d834..e07ddedd 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -44,7 +44,7 @@ jobs: - name: "Checkout code" - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: persist-credentials: false @@ -61,6 +61,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@a09933a12a80f87b87005513f0abb1494c27a716 # v2.21.4 + uses: github/codeql-action/upload-sarif@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.21.5 with: sarif_file: results.sarif diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 9f98dea7..c09b01c9 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -29,7 +29,7 @@ jobs: registry-1.docker.io:443 - name: Checkout - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - name: Use Python ${{ matrix.python }} uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 with: From cbc0d0ac715dfa3b1cea8b9830bd5cd048f524c3 Mon Sep 17 00:00:00 2001 From: HKWinterhalter Date: Wed, 27 Sep 2023 15:26:37 -0700 Subject: [PATCH 014/108] chore(deps): Remove gunicorn version upper bound (#276) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0a98faef..e3f92e27 100644 --- a/setup.py +++ b/setup.py @@ -54,7 +54,7 @@ "flask>=1.0,<3.0", "click>=7.0,<9.0", "watchdog>=1.0.0", - "gunicorn>=19.2.0,<21.0; platform_system!='Windows'", + "gunicorn>=19.2.0; platform_system!='Windows'", "cloudevents>=1.2.0,<2.0.0", ], entry_points={ From 5f4bd3348a0c8baae4e0f6379111a00b34448e68 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 12 Oct 2023 22:34:42 +0200 Subject: [PATCH 015/108] chore(deps): update all non-major dependencies (#278) --- .github/workflows/codeql.yml | 8 ++++---- .github/workflows/conformance.yml | 18 +++++++++--------- .github/workflows/dependency-review.yml | 2 +- .github/workflows/lint.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- .github/workflows/scorecard.yml | 6 +++--- .github/workflows/unit.yml | 4 ++-- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6a368480..881bdc8b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 with: disable-sudo: true egress-policy: block @@ -57,7 +57,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.21.5 + uses: github/codeql-action/init@d90b8d79de6dc1f58e83a1499aa58d6c93dc28de # v2.22.2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -67,7 +67,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.21.5 + uses: github/codeql-action/autobuild@d90b8d79de6dc1f58e83a1499aa58d6c93dc28de # v2.22.2 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -80,6 +80,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.21.5 + uses: github/codeql-action/analyze@d90b8d79de6dc1f58e83a1499aa58d6c93dc28de # v2.22.2 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index c7f4352f..a7ed3ebe 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -16,7 +16,7 @@ jobs: python: ['3.7', '3.8', '3.9', '3.10', '3.11'] steps: - name: Harden Runner - uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 with: disable-sudo: true egress-policy: block @@ -33,7 +33,7 @@ jobs: uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - name: Setup Python - uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 + uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1 with: python-version: ${{ matrix.python }} @@ -46,7 +46,7 @@ jobs: go-version: '1.20' - name: Run HTTP conformance tests - uses: GoogleCloudPlatform/functions-framework-conformance/action@64d4646c26d11281faf5e8813dd0fc8a9551cac3 # v1.8.5 + uses: GoogleCloudPlatform/functions-framework-conformance/action@72a4f36b10f1c6435ab1a86a9ea24bda464cc262 # v1.8.6 with: functionType: 'http' useBuildpacks: false @@ -54,7 +54,7 @@ jobs: cmd: "'functions-framework --source tests/conformance/main.py --target write_http --signature-type http'" - name: Run event conformance tests - uses: GoogleCloudPlatform/functions-framework-conformance/action@64d4646c26d11281faf5e8813dd0fc8a9551cac3 # v1.8.5 + uses: GoogleCloudPlatform/functions-framework-conformance/action@72a4f36b10f1c6435ab1a86a9ea24bda464cc262 # v1.8.6 with: functionType: 'legacyevent' useBuildpacks: false @@ -62,7 +62,7 @@ jobs: cmd: "'functions-framework --source tests/conformance/main.py --target write_legacy_event --signature-type event'" - name: Run CloudEvents conformance tests - uses: GoogleCloudPlatform/functions-framework-conformance/action@64d4646c26d11281faf5e8813dd0fc8a9551cac3 # v1.8.5 + uses: GoogleCloudPlatform/functions-framework-conformance/action@72a4f36b10f1c6435ab1a86a9ea24bda464cc262 # v1.8.6 with: functionType: 'cloudevent' useBuildpacks: false @@ -70,7 +70,7 @@ jobs: cmd: "'functions-framework --source tests/conformance/main.py --target write_cloud_event --signature-type cloudevent'" - name: Run HTTP conformance tests declarative - uses: GoogleCloudPlatform/functions-framework-conformance/action@64d4646c26d11281faf5e8813dd0fc8a9551cac3 # v1.8.5 + uses: GoogleCloudPlatform/functions-framework-conformance/action@72a4f36b10f1c6435ab1a86a9ea24bda464cc262 # v1.8.6 with: functionType: 'http' useBuildpacks: false @@ -78,7 +78,7 @@ jobs: cmd: "'functions-framework --source tests/conformance/main.py --target write_http_declarative'" - name: Run CloudEvents conformance tests declarative - uses: GoogleCloudPlatform/functions-framework-conformance/action@64d4646c26d11281faf5e8813dd0fc8a9551cac3 # v1.8.5 + uses: GoogleCloudPlatform/functions-framework-conformance/action@72a4f36b10f1c6435ab1a86a9ea24bda464cc262 # v1.8.6 with: functionType: 'cloudevent' useBuildpacks: false @@ -86,7 +86,7 @@ jobs: cmd: "'functions-framework --source tests/conformance/main.py --target write_cloud_event_declarative'" - name: Run HTTP concurrency tests declarative - uses: GoogleCloudPlatform/functions-framework-conformance/action@64d4646c26d11281faf5e8813dd0fc8a9551cac3 # v1.8.5 + uses: GoogleCloudPlatform/functions-framework-conformance/action@72a4f36b10f1c6435ab1a86a9ea24bda464cc262 # v1.8.6 with: functionType: 'http' useBuildpacks: false @@ -94,7 +94,7 @@ jobs: cmd: "'functions-framework --source tests/conformance/main.py --target write_http_declarative_concurrent'" - name: Run Typed tests declarative - uses: GoogleCloudPlatform/functions-framework-conformance/action@64d4646c26d11281faf5e8813dd0fc8a9551cac3 # v1.8.5 + uses: GoogleCloudPlatform/functions-framework-conformance/action@72a4f36b10f1c6435ab1a86a9ea24bda464cc262 # v1.8.6 with: functionType: 'http' declarativeType: 'typed' diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 707087cd..4349019e 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 with: disable-sudo: true egress-policy: block diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1add91f0..56410fc1 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 with: disable-sudo: true egress-policy: block @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - name: Setup Python - uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 + uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1 - name: Install tox run: python -m pip install tox - name: Lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2ec287c8..dbda667c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs @@ -22,7 +22,7 @@ jobs: with: ref: ${{ github.event.release.tag_name }} - name: Install Python - uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 + uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1 - name: Install build dependencies run: python -m pip install -U setuptools build wheel - name: Build distributions diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index e07ddedd..d13e3d2a 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 with: disable-sudo: true egress-policy: block @@ -49,7 +49,7 @@ jobs: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@08b4669551908b1024bb425080c797723083c031 # v2.2.0 + uses: ossf/scorecard-action@483ef80eb98fb506c348f7d62e28055e49fe2398 # v2.3.0 with: results_file: results.sarif results_format: sarif @@ -61,6 +61,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.21.5 + uses: github/codeql-action/upload-sarif@d90b8d79de6dc1f58e83a1499aa58d6c93dc28de # v2.22.2 with: sarif_file: results.sarif diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index c09b01c9..6c67bd37 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -16,7 +16,7 @@ jobs: runs-on: ${{ matrix.platform }} steps: - name: Harden Runner - uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 with: disable-sudo: true egress-policy: block @@ -31,7 +31,7 @@ jobs: - name: Checkout uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - name: Use Python ${{ matrix.python }} - uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 + uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1 with: python-version: ${{ matrix.python }} - name: Install tox From 39a1bc5de91b19f9a44fc19d685d92fc60aa9194 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 12 Oct 2023 23:12:41 +0200 Subject: [PATCH 016/108] chore(deps): update actions/checkout action to v4 (#279) --- .github/workflows/codeql.yml | 2 +- .github/workflows/conformance.yml | 2 +- .github/workflows/dependency-review.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/scorecard.yml | 2 +- .github/workflows/unit.yml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 881bdc8b..f6f405f2 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: objects.githubusercontent.com:443 - name: Checkout repository - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index a7ed3ebe..9998536d 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -30,7 +30,7 @@ jobs: storage.googleapis.com:443 - name: Checkout code - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - name: Setup Python uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 4349019e..77efd86a 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -25,6 +25,6 @@ jobs: api.github.com:443 github.com:443 - name: 'Checkout Repository' - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - name: 'Dependency Review' uses: actions/dependency-review-action@6c5ccdad469c9f8a2996bfecaec55a631a347034 # v3.1.0 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 56410fc1..c58f585c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,7 +21,7 @@ jobs: github.com:443 pypi.org:443 - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - name: Setup Python uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1 - name: Install tox diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dbda667c..1ebe76fb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - name: Checkout - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: ref: ${{ github.event.release.tag_name }} - name: Install Python diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index d13e3d2a..f099b350 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -44,7 +44,7 @@ jobs: - name: "Checkout code" - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: persist-credentials: false diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 6c67bd37..dbadb281 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -29,7 +29,7 @@ jobs: registry-1.docker.io:443 - name: Checkout - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - name: Use Python ${{ matrix.python }} uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1 with: From b247895c3e855b2da6367bd8cc6c39d6f1674bf1 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 19 Oct 2023 20:20:03 +0200 Subject: [PATCH 017/108] chore(deps): update pypa/gh-action-pypi-publish digest to 79739dc (#277) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1ebe76fb..2a85f561 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: - name: Build distributions run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@8cdc2ab67c943c5edf5fd9ae1995546b4b550602 # main + uses: pypa/gh-action-pypi-publish@79739dc2f2bf6bcfd21ecf9af9f06bd643dbeeae # main with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} From b22e1d171feb0263775e2a4fa32c580b499bdced Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 1 Nov 2023 07:21:47 +0100 Subject: [PATCH 018/108] chore(deps): update all non-major dependencies (#285) --- .github/workflows/codeql.yml | 8 ++++---- .github/workflows/conformance.yml | 2 +- .github/workflows/dependency-review.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/scorecard.yml | 6 +++--- .github/workflows/unit.yml | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f6f405f2..4c8e2f92 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,11 +53,11 @@ jobs: objects.githubusercontent.com:443 - name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@d90b8d79de6dc1f58e83a1499aa58d6c93dc28de # v2.22.2 + uses: github/codeql-action/init@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -67,7 +67,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@d90b8d79de6dc1f58e83a1499aa58d6c93dc28de # v2.22.2 + uses: github/codeql-action/autobuild@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -80,6 +80,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@d90b8d79de6dc1f58e83a1499aa58d6c93dc28de # v2.22.2 + uses: github/codeql-action/analyze@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 9998536d..2647410b 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -30,7 +30,7 @@ jobs: storage.googleapis.com:443 - name: Checkout code - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Setup Python uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 77efd86a..088ebccf 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -25,6 +25,6 @@ jobs: api.github.com:443 github.com:443 - name: 'Checkout Repository' - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: 'Dependency Review' uses: actions/dependency-review-action@6c5ccdad469c9f8a2996bfecaec55a631a347034 # v3.1.0 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c58f585c..a504c985 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,7 +21,7 @@ jobs: github.com:443 pypi.org:443 - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Setup Python uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1 - name: Install tox diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2a85f561..65779b28 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - name: Checkout - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: ref: ${{ github.event.release.tag_name }} - name: Install Python diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index f099b350..7130693e 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -44,12 +44,12 @@ jobs: - name: "Checkout code" - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@483ef80eb98fb506c348f7d62e28055e49fe2398 # v2.3.0 + uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 with: results_file: results.sarif results_format: sarif @@ -61,6 +61,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@d90b8d79de6dc1f58e83a1499aa58d6c93dc28de # v2.22.2 + uses: github/codeql-action/upload-sarif@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 with: sarif_file: results.sarif diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index dbadb281..f9cd712b 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -29,7 +29,7 @@ jobs: registry-1.docker.io:443 - name: Checkout - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Use Python ${{ matrix.python }} uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1 with: From 366ca3f27d15c5566c39eeb164ba6228e2b2cfd3 Mon Sep 17 00:00:00 2001 From: Chi Zhang Date: Wed, 1 Nov 2023 13:55:46 -0700 Subject: [PATCH 019/108] ci: Update python matrix (#275) Signed-off-by: GitHub authored-by: chizhg --- .github/workflows/conformance.yml | 2 +- .github/workflows/unit.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 2647410b..886b65f7 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] steps: - name: Harden Runner uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index f9cd712b..de31a760 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -11,7 +11,7 @@ jobs: test: strategy: matrix: - python: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: From 6b9e9b56f2f364e9a19fd434d88a0fbe22808515 Mon Sep 17 00:00:00 2001 From: HKWinterhalter Date: Mon, 27 Nov 2023 15:04:58 -0800 Subject: [PATCH 020/108] docs: Fix broken Flask Request link in README.md (#286) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b32b2e3e..a234586c 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ def hello(request: flask.Request) -> flask.typing.ResponseReturnValue: return "Hello world!" ``` -> Your function is passed a single parameter, `(request)`, which is a Flask [`Request`](http://flask.pocoo.org/docs/1.0/api/#flask.Request) object. +> Your function is passed a single parameter, `(request)`, which is a Flask [`Request`](https://flask.palletsprojects.com/en/3.0.x/api/#flask.Request) object. Run the following command: From dcc9a25bad86d91024ee758b9748b584d4ca833c Mon Sep 17 00:00:00 2001 From: HKWinterhalter Date: Mon, 27 Nov 2023 16:08:56 -0800 Subject: [PATCH 021/108] chore: Fix file name reference in Dockerfile (#287) --- examples/cloud_run_cloud_events/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/cloud_run_cloud_events/Dockerfile b/examples/cloud_run_cloud_events/Dockerfile index 1bae67aa..08f45150 100644 --- a/examples/cloud_run_cloud_events/Dockerfile +++ b/examples/cloud_run_cloud_events/Dockerfile @@ -12,7 +12,7 @@ COPY . . # Install production dependencies. RUN pip install gunicorn cloudevents functions-framework RUN pip install -r requirements.txt -RUN chmod +x send_cloudevent.py +RUN chmod +x send_cloud_event.py # Run the web service on container startup. CMD ["functions-framework", "--target=hello", "--signature-type=cloudevent"] From 2de849407ec2cf9953b98a2cb34944c438ce39fe Mon Sep 17 00:00:00 2001 From: HKWinterhalter Date: Tue, 28 Nov 2023 14:42:34 -0800 Subject: [PATCH 022/108] chore(deps): update flask dependency range to include v3 (#288) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | flask ([changelog](https://flask.palletsprojects.com/changes/)) | `>=1.0,<3.0` -> `>=1.0,<4.0` | [![age](https://developer.mend.io/api/mc/badges/age/pypi/flask/3.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/flask/3.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/flask/2.3.3/3.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/flask/2.3.3/3.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e3f92e27..77d45587 100644 --- a/setup.py +++ b/setup.py @@ -51,7 +51,7 @@ package_dir={"": "src"}, python_requires=">=3.5, <4", install_requires=[ - "flask>=1.0,<3.0", + "flask>=1.0,<4.0", "click>=7.0,<9.0", "watchdog>=1.0.0", "gunicorn>=19.2.0; platform_system!='Windows'", From c85dc595d9fc1b698df02d15392d243e50dd707b Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 15:09:12 -0800 Subject: [PATCH 023/108] chore(main): release 3.5.0 (#264) * chore(main): release 3.5.0 * Update CHANGELOG.md to include Flask 3 support message * Update CHANGELOG.md to include Flask 3 support message --------- Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: HKWinterhalter --- CHANGELOG.md | 22 ++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9016fc49..32fe2c35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,28 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.5.0](https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v3.4.0...v3.5.0) (2023-11-28) + + +### Features + +* initial typing of the public API ([#248](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/248)) ([45aed53](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/45aed538b5e39655318c7841457399fa3376ceaf)) + + +### Bug Fixes + +* don't exit on reload if there is a syntax error ([#214](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/214)) ([46780da](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/46780dac88c8dfe715babe89f792d08e9ca482e7)) +* reduce gunicorn concurrency to at most 4 * maximum available corâ€Ļ ([#259](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/259)) ([2e04cc2](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/2e04cc28ae028b8facc85dbdf738e2b8076dbbf7)) + + +### Documentation + +* Fix broken Flask Request link in README.md ([#286](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/286)) ([6b9e9b5](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/6b9e9b56f2f364e9a19fd434d88a0fbe22808515)) + +### Dependencies + +* Include support for Flask 3 + ## [3.4.0](https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v3.3.0...v3.4.0) (2023-05-24) diff --git a/setup.py b/setup.py index 77d45587..51a3e27c 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( name="functions-framework", - version="3.4.0", + version="3.5.0", description="An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team.", long_description=long_description, long_description_content_type="text/markdown", From b6181e5c90fb142e59f8231ab9cdb24e7c789d9f Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 4 Dec 2023 23:28:25 +0100 Subject: [PATCH 024/108] chore(deps): update pypa/gh-action-pypi-publish digest to 2f6f737 (#294) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 65779b28..3d8bf13e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: - name: Build distributions run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@79739dc2f2bf6bcfd21ecf9af9f06bd643dbeeae # main + uses: pypa/gh-action-pypi-publish@2f6f737ca5f74c637829c0f5c3acd0e29ea5e8bf # main with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} From 85230e418bdfbb8feea101fcaaa995e017cc118e Mon Sep 17 00:00:00 2001 From: Kenneth Rosario Date: Thu, 18 Jan 2024 11:15:40 -0800 Subject: [PATCH 025/108] chore: fix unit test failure (#304) --- tests/test_samples.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_samples.py b/tests/test_samples.py index 65cee7d0..d76d7796 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -24,7 +24,7 @@ def test_cloud_run_http(self): self.stop_all_containers(client) TAG = "cloud_run_http" - client.images.build(path=str(EXAMPLES_DIR / "cloud_run_http"), tag={TAG}) + client.images.build(path=str(EXAMPLES_DIR / "cloud_run_http"), tag=TAG) container = client.containers.run(image=TAG, detach=True, ports={8080: 8080}) timeout = 10 success = False From 8b5c37465d8acb38debe5a2658f17ea956a23079 Mon Sep 17 00:00:00 2001 From: HKWinterhalter Date: Thu, 18 Jan 2024 11:25:49 -0800 Subject: [PATCH 026/108] chore: Update setup.py classifiers (#303) * chore: Update setup.py classifiers Update development status and python versions * chore: Update setup.py classifiers Update development status and python version classifiers --- setup.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 51a3e27c..3c2c2090 100644 --- a/setup.py +++ b/setup.py @@ -33,16 +33,15 @@ author="Google LLC", author_email="googleapis-packages@google.com", classifiers=[ - "Development Status :: 3 - Alpha", + "Development Status :: 5 - Production/Stable ", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ], keywords="functions-framework", packages=find_packages(where="src"), From 95931fa69de280e13b973c4ab6c3e8791fbae9d1 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 18 Jan 2024 23:50:22 +0100 Subject: [PATCH 027/108] chore(deps): update all non-major dependencies (#295) Co-authored-by: HKWinterhalter --- .github/workflows/codeql.yml | 8 ++++---- .github/workflows/conformance.yml | 4 ++-- .github/workflows/dependency-review.yml | 4 ++-- .github/workflows/lint.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- .github/workflows/scorecard.yml | 4 ++-- .github/workflows/unit.yml | 4 ++-- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 4c8e2f92..0ca62fbe 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 + uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 with: disable-sudo: true egress-policy: block @@ -57,7 +57,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 + uses: github/codeql-action/init@4759df8df70c5ebe7042c3029bbace20eee13edd # v2.23.1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -67,7 +67,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 + uses: github/codeql-action/autobuild@4759df8df70c5ebe7042c3029bbace20eee13edd # v2.23.1 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -80,6 +80,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 + uses: github/codeql-action/analyze@4759df8df70c5ebe7042c3029bbace20eee13edd # v2.23.1 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 886b65f7..79944511 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -16,7 +16,7 @@ jobs: python: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] steps: - name: Harden Runner - uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 + uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 with: disable-sudo: true egress-policy: block @@ -33,7 +33,7 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Setup Python - uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1 + uses: actions/setup-python@b64ffcaf5b410884ad320a9cfac8866006a109aa # v4.8.0 with: python-version: ${{ matrix.python }} diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 088ebccf..439f3e0b 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 + uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 with: disable-sudo: true egress-policy: block @@ -27,4 +27,4 @@ jobs: - name: 'Checkout Repository' uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: 'Dependency Review' - uses: actions/dependency-review-action@6c5ccdad469c9f8a2996bfecaec55a631a347034 # v3.1.0 + uses: actions/dependency-review-action@c74b580d73376b7750d3d2a50bfb8adc2c937507 # v3.1.5 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a504c985..8bac6754 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 + uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 with: disable-sudo: true egress-policy: block @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Setup Python - uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1 + uses: actions/setup-python@b64ffcaf5b410884ad320a9cfac8866006a109aa # v4.8.0 - name: Install tox run: python -m pip install tox - name: Lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3d8bf13e..3bf8af3f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 + uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs @@ -22,7 +22,7 @@ jobs: with: ref: ${{ github.event.release.tag_name }} - name: Install Python - uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1 + uses: actions/setup-python@b64ffcaf5b410884ad320a9cfac8866006a109aa # v4.8.0 - name: Install build dependencies run: python -m pip install -U setuptools build wheel - name: Build distributions diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 7130693e..e3a43a73 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 + uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 with: disable-sudo: true egress-policy: block @@ -61,6 +61,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 + uses: github/codeql-action/upload-sarif@4759df8df70c5ebe7042c3029bbace20eee13edd # v2.23.1 with: sarif_file: results.sarif diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index de31a760..934497be 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -16,7 +16,7 @@ jobs: runs-on: ${{ matrix.platform }} steps: - name: Harden Runner - uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 + uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 with: disable-sudo: true egress-policy: block @@ -31,7 +31,7 @@ jobs: - name: Checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Use Python ${{ matrix.python }} - uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1 + uses: actions/setup-python@b64ffcaf5b410884ad320a9cfac8866006a109aa # v4.8.0 with: python-version: ${{ matrix.python }} - name: Install tox From ee1687adf0ea362e8c70dc4897bd47efc61970b2 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 18 Jan 2024 23:54:56 +0100 Subject: [PATCH 028/108] chore(deps): update github/codeql-action action to v3 (#300) --- .github/workflows/codeql.yml | 6 +++--- .github/workflows/scorecard.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0ca62fbe..234262ef 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -57,7 +57,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@4759df8df70c5ebe7042c3029bbace20eee13edd # v2.23.1 + uses: github/codeql-action/init@0b21cf2492b6b02c465a3e5d7c473717ad7721ba # v3.23.1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -67,7 +67,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@4759df8df70c5ebe7042c3029bbace20eee13edd # v2.23.1 + uses: github/codeql-action/autobuild@0b21cf2492b6b02c465a3e5d7c473717ad7721ba # v3.23.1 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -80,6 +80,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@4759df8df70c5ebe7042c3029bbace20eee13edd # v2.23.1 + uses: github/codeql-action/analyze@0b21cf2492b6b02c465a3e5d7c473717ad7721ba # v3.23.1 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index e3a43a73..de9b2418 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -61,6 +61,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@4759df8df70c5ebe7042c3029bbace20eee13edd # v2.23.1 + uses: github/codeql-action/upload-sarif@0b21cf2492b6b02c465a3e5d7c473717ad7721ba # v3.23.1 with: sarif_file: results.sarif From 5f66695f5635263f431d9670fa29e16162bebcb8 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 18 Jan 2024 23:59:50 +0100 Subject: [PATCH 029/108] chore(deps): update actions/setup-go action to v5 (#298) Co-authored-by: HKWinterhalter --- .github/workflows/conformance.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 79944511..03d6b0c6 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -41,7 +41,7 @@ jobs: run: python -m pip install -e . - name: Setup Go - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 with: go-version: '1.20' From b03ef3951a03bcefed87a4af8268237ee1400f0d Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 19 Jan 2024 00:05:31 +0100 Subject: [PATCH 030/108] chore(deps): update actions/setup-python action to v5 (#299) --- .github/workflows/conformance.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/unit.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 03d6b0c6..db33ef01 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -33,7 +33,7 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Setup Python - uses: actions/setup-python@b64ffcaf5b410884ad320a9cfac8866006a109aa # v4.8.0 + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 with: python-version: ${{ matrix.python }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8bac6754..90971b86 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Setup Python - uses: actions/setup-python@b64ffcaf5b410884ad320a9cfac8866006a109aa # v4.8.0 + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 - name: Install tox run: python -m pip install tox - name: Lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3bf8af3f..85a8637c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: with: ref: ${{ github.event.release.tag_name }} - name: Install Python - uses: actions/setup-python@b64ffcaf5b410884ad320a9cfac8866006a109aa # v4.8.0 + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 - name: Install build dependencies run: python -m pip install -U setuptools build wheel - name: Build distributions diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 934497be..0d0ed72b 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -31,7 +31,7 @@ jobs: - name: Checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Use Python ${{ matrix.python }} - uses: actions/setup-python@b64ffcaf5b410884ad320a9cfac8866006a109aa # v4.8.0 + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 with: python-version: ${{ matrix.python }} - name: Install tox From e17555304cfaab6494f4a97e0df6d6f9249de4e2 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 19 Jan 2024 00:15:09 +0100 Subject: [PATCH 031/108] chore(deps): update pypa/gh-action-pypi-publish digest to c12cc61 (#297) Co-authored-by: HKWinterhalter --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 85a8637c..79deaf96 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: - name: Build distributions run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@2f6f737ca5f74c637829c0f5c3acd0e29ea5e8bf # main + uses: pypa/gh-action-pypi-publish@c12cc61414480c03e10ea76e2a0a1a17d6c764e2 # main with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} From 3a0cdb8ba1065510d8ca0f2bcabd484cd3ffaaf6 Mon Sep 17 00:00:00 2001 From: June <121135073+KAKAinGoog@users.noreply.github.com> Date: Wed, 14 Feb 2024 13:42:43 -0800 Subject: [PATCH 032/108] feat: avoid long running process when request timeout (#309) Previously function framework use 0 timeout which is actually "no timeout" restrction. This was causing a problem that when user provides a request timeout to Cloud function, process will still continue and consume resources. In this fix, timeout is enabled; default timeout settings is 5 min, same as Cloud run. To make sure timeout settings will be respected, default settings switched from multi-threads to multi-workers. However, user is still allowed to customize workers/threads by assigning env var. But user need to note that timeout won't work when #thread > 1. --- src/functions_framework/__init__.py | 12 ++++++------ src/functions_framework/_http/gunicorn.py | 7 ++++--- src/functions_framework/_typed_event.py | 6 +++--- tests/test_http.py | 12 ++++++------ 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index ece4f446..8c23e5c0 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -65,9 +65,9 @@ def write(self, out): def cloud_event(func: CloudEventFunction) -> CloudEventFunction: """Decorator that registers cloudevent as user function signature type.""" - _function_registry.REGISTRY_MAP[ - func.__name__ - ] = _function_registry.CLOUDEVENT_SIGNATURE_TYPE + _function_registry.REGISTRY_MAP[func.__name__] = ( + _function_registry.CLOUDEVENT_SIGNATURE_TYPE + ) @functools.wraps(func) def wrapper(*args, **kwargs): @@ -105,9 +105,9 @@ def wrapper(*args, **kwargs): def http(func: HTTPFunction) -> HTTPFunction: """Decorator that registers http as user function signature type.""" - _function_registry.REGISTRY_MAP[ - func.__name__ - ] = _function_registry.HTTP_SIGNATURE_TYPE + _function_registry.REGISTRY_MAP[func.__name__] = ( + _function_registry.HTTP_SIGNATURE_TYPE + ) @functools.wraps(func) def wrapper(*args, **kwargs): diff --git a/src/functions_framework/_http/gunicorn.py b/src/functions_framework/_http/gunicorn.py index 3a9c545b..009a06b7 100644 --- a/src/functions_framework/_http/gunicorn.py +++ b/src/functions_framework/_http/gunicorn.py @@ -21,14 +21,15 @@ class GunicornApplication(gunicorn.app.base.BaseApplication): def __init__(self, app, host, port, debug, **options): self.options = { "bind": "%s:%s" % (host, port), - "workers": 1, - "threads": (os.cpu_count() or 1) * 4, - "timeout": 0, + "workers": os.environ.get("WORKERS", (os.cpu_count() or 1) * 4), + "threads": os.environ.get("THREADS", 1), + "timeout": os.environ.get("CLOUD_RUN_TIMEOUT_SECONDS", 300), "loglevel": "error", "limit_request_line": 0, } self.options.update(options) self.app = app + super().__init__() def load_config(self): diff --git a/src/functions_framework/_typed_event.py b/src/functions_framework/_typed_event.py index 40e715ae..413c8f05 100644 --- a/src/functions_framework/_typed_event.py +++ b/src/functions_framework/_typed_event.py @@ -48,9 +48,9 @@ def register_typed_event(decorator_type, func): ) _function_registry.INPUT_TYPE_MAP[func.__name__] = input_type - _function_registry.REGISTRY_MAP[ - func.__name__ - ] = _function_registry.TYPED_SIGNATURE_TYPE + _function_registry.REGISTRY_MAP[func.__name__] = ( + _function_registry.TYPED_SIGNATURE_TYPE + ) """ Checks whether the response type of the typed function has a to_dict method""" diff --git a/tests/test_http.py b/tests/test_http.py index fbfac9d2..0a46fbea 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -97,17 +97,17 @@ def test_gunicorn_application(debug): assert gunicorn_app.app == app assert gunicorn_app.options == { "bind": "%s:%s" % (host, port), - "workers": 1, - "threads": os.cpu_count() * 4, - "timeout": 0, + "workers": os.cpu_count() * 4, + "threads": 1, + "timeout": 300, "loglevel": "error", "limit_request_line": 0, } assert gunicorn_app.cfg.bind == ["1.2.3.4:1234"] - assert gunicorn_app.cfg.workers == 1 - assert gunicorn_app.cfg.threads == os.cpu_count() * 4 - assert gunicorn_app.cfg.timeout == 0 + assert gunicorn_app.cfg.workers == os.cpu_count() * 4 + assert gunicorn_app.cfg.threads == 1 + assert gunicorn_app.cfg.timeout == 300 assert gunicorn_app.load() == app From c5c28b61ff829b8defdfe78ec32a4d3ce691de66 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 20 Feb 2024 22:20:26 +0100 Subject: [PATCH 033/108] chore(deps): update all non-major dependencies (#305) --- .github/workflows/codeql.yml | 8 ++++---- .github/workflows/conformance.yml | 2 +- .github/workflows/dependency-review.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/scorecard.yml | 4 ++-- .github/workflows/unit.yml | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 234262ef..64f617b3 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: disable-sudo: true egress-policy: block @@ -57,7 +57,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@0b21cf2492b6b02c465a3e5d7c473717ad7721ba # v3.23.1 + uses: github/codeql-action/init@379614612a29c9e28f31f39a59013eb8012a51f0 # v3.24.3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -67,7 +67,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@0b21cf2492b6b02c465a3e5d7c473717ad7721ba # v3.23.1 + uses: github/codeql-action/autobuild@379614612a29c9e28f31f39a59013eb8012a51f0 # v3.24.3 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -80,6 +80,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@0b21cf2492b6b02c465a3e5d7c473717ad7721ba # v3.23.1 + uses: github/codeql-action/analyze@379614612a29c9e28f31f39a59013eb8012a51f0 # v3.24.3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index db33ef01..a6760afa 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -16,7 +16,7 @@ jobs: python: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] steps: - name: Harden Runner - uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: disable-sudo: true egress-policy: block diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 439f3e0b..58c377c2 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: disable-sudo: true egress-policy: block diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 90971b86..e24e995e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: disable-sudo: true egress-policy: block diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 79deaf96..06388579 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index de9b2418..09ed0cbf 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: disable-sudo: true egress-policy: block @@ -61,6 +61,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@0b21cf2492b6b02c465a3e5d7c473717ad7721ba # v3.23.1 + uses: github/codeql-action/upload-sarif@379614612a29c9e28f31f39a59013eb8012a51f0 # v3.24.3 with: sarif_file: results.sarif diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 0d0ed72b..268abfc1 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -16,7 +16,7 @@ jobs: runs-on: ${{ matrix.platform }} steps: - name: Harden Runner - uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: disable-sudo: true egress-policy: block From 8bda09cd8e9addde17b51c28f70238e888e809be Mon Sep 17 00:00:00 2001 From: nifflets <5343516+nifflets@users.noreply.github.com> Date: Mon, 29 Apr 2024 09:39:19 -0700 Subject: [PATCH 034/108] chore: Fix unit tests (#321) * Fix unit tests Workaround for https://github.com/actions/setup-python/issues/696. macos-latest recently was updated to point to macos-14, which does not support Python 3.7, 3.8, 3.9 on ARM. * Update unit.yml * Update unit.yml --- .github/workflows/unit.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 268abfc1..3a7e1342 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -13,6 +13,22 @@ jobs: matrix: python: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] platform: [ubuntu-latest, macos-latest, windows-latest] + # Python <= 3.9 is not available on macos-14 + # Workaround for https://github.com/actions/setup-python/issues/696 + exclude: + - platform: macos-latest + python: '3.9' + - platform: macos-latest + python: '3.8' + - platform: macos-latest + python: '3.7' + include: + - platform: macos-latest + python: '3.9' + - platform: macos-13 + python: '3.8' + - platform: macos-13 + python: '3.7' runs-on: ${{ matrix.platform }} steps: - name: Harden Runner From 5fc2e46bd899ecd4d542fd587c8ad14ad8a1eba9 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 29 Apr 2024 18:44:15 +0200 Subject: [PATCH 035/108] chore(deps): update actions/dependency-review-action action to v4 (#306) Co-authored-by: Matthew Robertson --- .github/workflows/dependency-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 58c377c2..87b7e199 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -27,4 +27,4 @@ jobs: - name: 'Checkout Repository' uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: 'Dependency Review' - uses: actions/dependency-review-action@c74b580d73376b7750d3d2a50bfb8adc2c937507 # v3.1.5 + uses: actions/dependency-review-action@5bbc3ba658137598168acb2ab73b21c432dd411b # v4.2.5 From 59fdb1e392241bc7bff6b738eebad5f1887ab8a8 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 29 Apr 2024 19:07:19 +0200 Subject: [PATCH 036/108] chore(deps): update all non-major dependencies (#314) Co-authored-by: Matthew Robertson --- .github/workflows/codeql.yml | 8 ++++---- .github/workflows/conformance.yml | 4 ++-- .github/workflows/dependency-review.yml | 2 +- .github/workflows/lint.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- .github/workflows/scorecard.yml | 4 ++-- .github/workflows/unit.yml | 4 ++-- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 64f617b3..8be2b791 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,11 +53,11 @@ jobs: objects.githubusercontent.com:443 - name: Checkout repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@379614612a29c9e28f31f39a59013eb8012a51f0 # v3.24.3 + uses: github/codeql-action/init@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -67,7 +67,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@379614612a29c9e28f31f39a59013eb8012a51f0 # v3.24.3 + uses: github/codeql-action/autobuild@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -80,6 +80,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@379614612a29c9e28f31f39a59013eb8012a51f0 # v3.24.3 + uses: github/codeql-action/analyze@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index a6760afa..80ad8afb 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -30,10 +30,10 @@ jobs: storage.googleapis.com:443 - name: Checkout code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Setup Python - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 with: python-version: ${{ matrix.python }} diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 87b7e199..a79373ca 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -25,6 +25,6 @@ jobs: api.github.com:443 github.com:443 - name: 'Checkout Repository' - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: 'Dependency Review' uses: actions/dependency-review-action@5bbc3ba658137598168acb2ab73b21c432dd411b # v4.2.5 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e24e995e..d560d95f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,9 +21,9 @@ jobs: github.com:443 pypi.org:443 - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Setup Python - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 - name: Install tox run: python -m pip install tox - name: Lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 06388579..63b9f7f8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,11 +18,11 @@ jobs: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 with: ref: ${{ github.event.release.tag_name }} - name: Install Python - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 - name: Install build dependencies run: python -m pip install -U setuptools build wheel - name: Build distributions diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 09ed0cbf..150d7e49 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -44,7 +44,7 @@ jobs: - name: "Checkout code" - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 with: persist-credentials: false @@ -61,6 +61,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@379614612a29c9e28f31f39a59013eb8012a51f0 # v3.24.3 + uses: github/codeql-action/upload-sarif@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3 with: sarif_file: results.sarif diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 3a7e1342..f396ca2c 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -45,9 +45,9 @@ jobs: registry-1.docker.io:443 - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Use Python ${{ matrix.python }} - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 with: python-version: ${{ matrix.python }} - name: Install tox From dfc50594be5e5365ec31c8e1417d49085ff36ce3 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 15:00:46 -0700 Subject: [PATCH 037/108] chore(main): release 3.6.0 (#311) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32fe2c35..23665c2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.6.0](https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v3.5.0...v3.6.0) (2024-04-29) + + +### Features + +* avoid long running process when request timeout ([#309](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/309)) ([3a0cdb8](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/3a0cdb8ba1065510d8ca0f2bcabd484cd3ffaaf6)) + ## [3.5.0](https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v3.4.0...v3.5.0) (2023-11-28) diff --git a/setup.py b/setup.py index 3c2c2090..6ee9902f 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( name="functions-framework", - version="3.5.0", + version="3.6.0", description="An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team.", long_description=long_description, long_description_content_type="text/markdown", From 662bf4ced9aa52efe774662a0f0f496d3d3534fc Mon Sep 17 00:00:00 2001 From: nifflets <5343516+nifflets@users.noreply.github.com> Date: Tue, 7 May 2024 13:54:12 -0700 Subject: [PATCH 038/108] feat: Add execution id (#320) * feat: Add execution id Adds an execution id for each request. When the LOG_EXECUTION_ID env var is set, the execution id will be included in logs. --- setup.py | 1 + src/functions_framework/__init__.py | 37 ++- src/functions_framework/execution_id.py | 156 ++++++++++ tests/test_execution_id.py | 343 ++++++++++++++++++++++ tests/test_functions/execution_id/main.py | 33 +++ tests/test_view_functions.py | 2 +- 6 files changed, 570 insertions(+), 2 deletions(-) create mode 100644 src/functions_framework/execution_id.py create mode 100644 tests/test_execution_id.py create mode 100644 tests/test_functions/execution_id/main.py diff --git a/setup.py b/setup.py index 6ee9902f..d4a9fea5 100644 --- a/setup.py +++ b/setup.py @@ -55,6 +55,7 @@ "watchdog>=1.0.0", "gunicorn>=19.2.0; platform_system!='Windows'", "cloudevents>=1.2.0,<2.0.0", + "Werkzeug>=0.14,<4.0.0", ], entry_points={ "console_scripts": [ diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index 8c23e5c0..7474c01e 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -17,6 +17,8 @@ import io import json import logging +import logging.config +import os import os.path import pathlib import sys @@ -32,7 +34,12 @@ from cloudevents.http import from_http, is_binary from cloudevents.http.event import CloudEvent -from functions_framework import _function_registry, _typed_event, event_conversion +from functions_framework import ( + _function_registry, + _typed_event, + event_conversion, + execution_id, +) from functions_framework.background_event import BackgroundEvent from functions_framework.exceptions import ( EventConversionException, @@ -129,6 +136,7 @@ def setup_logging(): def _http_view_func_wrapper(function, request): + @execution_id.set_execution_context(request, _enable_execution_id_logging()) @functools.wraps(function) def view_func(path): return function(request._get_current_object()) @@ -143,6 +151,7 @@ def _run_cloud_event(function, request): def _typed_event_func_wrapper(function, request, inputType: Type): + @execution_id.set_execution_context(request, _enable_execution_id_logging()) def view_func(path): try: data = request.get_json() @@ -163,6 +172,7 @@ def view_func(path): def _cloud_event_view_func_wrapper(function, request): + @execution_id.set_execution_context(request, _enable_execution_id_logging()) def view_func(path): ce_exception = None event = None @@ -198,6 +208,7 @@ def view_func(path): def _event_view_func_wrapper(function, request): + @execution_id.set_execution_context(request, _enable_execution_id_logging()) def view_func(path): if event_conversion.is_convertable_cloud_event(request): # Convert this CloudEvent to the equivalent background event data and context. @@ -332,6 +343,9 @@ def create_app(target=None, source=None, signature_type=None): source_module, spec = _function_registry.load_function_module(source) + if _enable_execution_id_logging(): + _configure_app_execution_id_logging() + # Create the application _app = flask.Flask(target, template_folder=template_folder) _app.register_error_handler(500, crash_handler) @@ -355,6 +369,7 @@ def handle_none(rv): sys.stderr = _LoggingHandler("ERROR", sys.stderr) setup_logging() + _app.wsgi_app = execution_id.WsgiMiddleware(_app.wsgi_app) # Execute the module, within the application context with _app.app_context(): try: @@ -411,6 +426,26 @@ def __call__(self, *args, **kwargs): return self.app(*args, **kwargs) +def _configure_app_execution_id_logging(): + # Logging needs to be configured before app logger is accessed + logging.config.dictConfig( + { + "version": 1, + "handlers": { + "wsgi": { + "class": "logging.StreamHandler", + "stream": "ext://functions_framework.execution_id.logging_stream", + }, + }, + "root": {"level": "INFO", "handlers": ["wsgi"]}, + } + ) + + +def _enable_execution_id_logging(): + return os.environ.get("LOG_EXECUTION_ID") + + app = LazyWSGIApp() diff --git a/src/functions_framework/execution_id.py b/src/functions_framework/execution_id.py new file mode 100644 index 00000000..2b106531 --- /dev/null +++ b/src/functions_framework/execution_id.py @@ -0,0 +1,156 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import contextlib +import functools +import io +import json +import logging +import random +import re +import string +import sys + +import flask + +from werkzeug.local import LocalProxy + +_EXECUTION_ID_LENGTH = 12 +_EXECUTION_ID_CHARSET = string.digits + string.ascii_letters +_LOGGING_API_LABELS_FIELD = "logging.googleapis.com/labels" +_LOGGING_API_SPAN_ID_FIELD = "logging.googleapis.com/spanId" +_TRACE_CONTEXT_REGEX_PATTERN = re.compile( + r"^(?P[\w\d]+)/(?P\d+);o=(?P[01])$" +) +EXECUTION_ID_REQUEST_HEADER = "Function-Execution-Id" +TRACE_CONTEXT_REQUEST_HEADER = "X-Cloud-Trace-Context" + +logger = logging.getLogger(__name__) + + +class ExecutionContext: + def __init__(self, execution_id=None, span_id=None): + self.execution_id = execution_id + self.span_id = span_id + + +def _get_current_context(): + return ( + flask.g.execution_id_context + if flask.has_request_context() and "execution_id_context" in flask.g + else None + ) + + +def _set_current_context(context): + if flask.has_request_context(): + flask.g.execution_id_context = context + + +def _generate_execution_id(): + return "".join( + _EXECUTION_ID_CHARSET[random.randrange(len(_EXECUTION_ID_CHARSET))] + for _ in range(_EXECUTION_ID_LENGTH) + ) + + +# Middleware to add execution id to request header if one does not already exist +class WsgiMiddleware: + def __init__(self, wsgi_app): + self.wsgi_app = wsgi_app + + def __call__(self, environ, start_response): + execution_id = ( + environ.get("HTTP_FUNCTION_EXECUTION_ID") or _generate_execution_id() + ) + environ["HTTP_FUNCTION_EXECUTION_ID"] = execution_id + return self.wsgi_app(environ, start_response) + + +# Sets execution id and span id for the request +def set_execution_context(request, enable_id_logging=False): + if enable_id_logging: + stdout_redirect = contextlib.redirect_stdout( + LoggingHandlerAddExecutionId(sys.stdout) + ) + stderr_redirect = contextlib.redirect_stderr( + LoggingHandlerAddExecutionId(sys.stderr) + ) + else: + stdout_redirect = contextlib.nullcontext() + stderr_redirect = contextlib.nullcontext() + + def decorator(view_function): + @functools.wraps(view_function) + def wrapper(*args, **kwargs): + trace_context = re.match( + _TRACE_CONTEXT_REGEX_PATTERN, + request.headers.get(TRACE_CONTEXT_REQUEST_HEADER, ""), + ) + execution_id = request.headers.get(EXECUTION_ID_REQUEST_HEADER) + span_id = trace_context.group("span_id") if trace_context else None + _set_current_context(ExecutionContext(execution_id, span_id)) + + with stderr_redirect, stdout_redirect: + return view_function(*args, **kwargs) + + return wrapper + + return decorator + + +@LocalProxy +def logging_stream(): + return LoggingHandlerAddExecutionId(stream=flask.logging.wsgi_errors_stream) + + +class LoggingHandlerAddExecutionId(io.TextIOWrapper): + def __new__(cls, stream=sys.stdout): + if isinstance(stream, LoggingHandlerAddExecutionId): + return stream + else: + return super(LoggingHandlerAddExecutionId, cls).__new__(cls) + + def __init__(self, stream=sys.stdout): + io.TextIOWrapper.__init__(self, io.StringIO()) + self.stream = stream + + def write(self, contents): + if contents == "\n": + return + current_context = _get_current_context() + if current_context is None: + self.stream.write(contents + "\n") + self.stream.flush() + return + try: + execution_id = current_context.execution_id + span_id = current_context.span_id + payload = json.loads(contents) + if not isinstance(payload, dict): + payload = {"message": contents} + except json.JSONDecodeError: + if len(contents) > 0 and contents[-1] == "\n": + contents = contents[:-1] + payload = {"message": contents} + if execution_id: + payload[_LOGGING_API_LABELS_FIELD] = payload.get( + _LOGGING_API_LABELS_FIELD, {} + ) + payload[_LOGGING_API_LABELS_FIELD]["execution_id"] = execution_id + if span_id: + payload[_LOGGING_API_SPAN_ID_FIELD] = span_id + self.stream.write(json.dumps(payload)) + self.stream.write("\n") + self.stream.flush() diff --git a/tests/test_execution_id.py b/tests/test_execution_id.py new file mode 100644 index 00000000..bfddfc3c --- /dev/null +++ b/tests/test_execution_id.py @@ -0,0 +1,343 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import asyncio +import json +import pathlib +import re +import sys + +from functools import partial +from unittest.mock import Mock + +import pretend +import pytest + +from functions_framework import create_app, execution_id + +TEST_FUNCTIONS_DIR = pathlib.Path(__file__).resolve().parent / "test_functions" +TEST_EXECUTION_ID = "test_execution_id" +TEST_SPAN_ID = "123456" + + +def test_user_function_can_retrieve_execution_id_from_header(): + source = TEST_FUNCTIONS_DIR / "execution_id" / "main.py" + target = "function" + client = create_app(target, source).test_client() + resp = client.post( + "/", + headers={ + "Function-Execution-Id": TEST_EXECUTION_ID, + "Content-Type": "application/json", + }, + ) + + assert resp.get_json()["execution_id"] == TEST_EXECUTION_ID + + +def test_uncaught_exception_in_user_function_sets_execution_id(capsys, monkeypatch): + monkeypatch.setenv("LOG_EXECUTION_ID", "True") + source = TEST_FUNCTIONS_DIR / "execution_id" / "main.py" + target = "error" + app = create_app(target, source) + client = app.test_client() + resp = client.post( + "/", + headers={ + "Function-Execution-Id": TEST_EXECUTION_ID, + "Content-Type": "application/json", + }, + ) + assert resp.status_code == 500 + record = capsys.readouterr() + assert f'"execution_id": "{TEST_EXECUTION_ID}"' in record.err + + +def test_print_from_user_function_sets_execution_id(capsys, monkeypatch): + monkeypatch.setenv("LOG_EXECUTION_ID", "True") + source = TEST_FUNCTIONS_DIR / "execution_id" / "main.py" + target = "print_message" + app = create_app(target, source) + client = app.test_client() + client.post( + "/", + headers={ + "Function-Execution-Id": TEST_EXECUTION_ID, + "Content-Type": "application/json", + }, + json={"message": "some-message"}, + ) + record = capsys.readouterr() + assert f'"execution_id": "{TEST_EXECUTION_ID}"' in record.out + assert '"message": "some-message"' in record.out + + +def test_log_from_user_function_sets_execution_id(capsys, monkeypatch): + monkeypatch.setenv("LOG_EXECUTION_ID", "True") + source = TEST_FUNCTIONS_DIR / "execution_id" / "main.py" + target = "log_message" + app = create_app(target, source) + client = app.test_client() + client.post( + "/", + headers={ + "Function-Execution-Id": TEST_EXECUTION_ID, + "Content-Type": "application/json", + }, + json={"message": json.dumps({"custom-field": "some-message"})}, + ) + record = capsys.readouterr() + assert f'"execution_id": "{TEST_EXECUTION_ID}"' in record.err + assert '"custom-field": "some-message"' in record.err + + +def test_user_function_can_retrieve_generated_execution_id(monkeypatch): + monkeypatch.setattr( + execution_id, "_generate_execution_id", lambda: TEST_EXECUTION_ID + ) + source = TEST_FUNCTIONS_DIR / "execution_id" / "main.py" + target = "function" + client = create_app(target, source).test_client() + resp = client.post( + "/", + headers={ + "Content-Type": "application/json", + }, + ) + + assert resp.get_json()["execution_id"] == TEST_EXECUTION_ID + + +def test_does_not_set_execution_id_when_not_enabled(capsys): + source = TEST_FUNCTIONS_DIR / "execution_id" / "main.py" + target = "print_message" + app = create_app(target, source) + client = app.test_client() + client.post( + "/", + headers={ + "Function-Execution-Id": TEST_EXECUTION_ID, + "Content-Type": "application/json", + }, + json={"message": "some-message"}, + ) + record = capsys.readouterr() + assert f'"execution_id": "{TEST_EXECUTION_ID}"' not in record.out + assert "some-message" in record.out + + +def test_generate_execution_id(): + expected_matching_regex = "^[0-9a-zA-Z]{12}$" + actual_execution_id = execution_id._generate_execution_id() + + match = re.match(expected_matching_regex, actual_execution_id).group(0) + assert match == actual_execution_id + + +@pytest.mark.parametrize( + "headers,expected_execution_id,expected_span_id", + [ + ( + { + "X-Cloud-Trace-Context": f"TRACE_ID/{TEST_SPAN_ID};o=1", + "Function-Execution-Id": TEST_EXECUTION_ID, + }, + TEST_EXECUTION_ID, + TEST_SPAN_ID, + ), + ( + { + "X-Cloud-Trace-Context": f"TRACE_ID/{TEST_SPAN_ID};o=1", + "Function-Execution-Id": TEST_EXECUTION_ID, + }, + TEST_EXECUTION_ID, + TEST_SPAN_ID, + ), + ({}, None, None), + ( + { + "X-Cloud-Trace-Context": "malformed trace context string", + "Function-Execution-Id": TEST_EXECUTION_ID, + }, + TEST_EXECUTION_ID, + None, + ), + ], +) +def test_set_execution_context( + headers, expected_execution_id, expected_span_id, monkeypatch +): + request = pretend.stub(headers=headers) + + def view_func(): + pass + + monkeypatch.setattr( + execution_id, "_generate_execution_id", lambda: TEST_EXECUTION_ID + ) + mock_g = Mock() + monkeypatch.setattr(execution_id.flask, "g", mock_g) + monkeypatch.setattr(execution_id.flask, "has_request_context", lambda: True) + execution_id.set_execution_context(request)(view_func)() + + assert mock_g.execution_id_context.span_id == expected_span_id + assert mock_g.execution_id_context.execution_id == expected_execution_id + + +@pytest.mark.parametrize( + "log_message,expected_log_json", + [ + ("text message", {"message": "text message"}), + ( + json.dumps({"custom-field1": "value1", "custom-field2": "value2"}), + {"custom-field1": "value1", "custom-field2": "value2"}, + ), + ("[]", {"message": "[]"}), + ], +) +def test_log_handler(monkeypatch, log_message, expected_log_json, capsys): + log_handler = execution_id.LoggingHandlerAddExecutionId(stream=sys.stdout) + monkeypatch.setattr( + execution_id, + "_get_current_context", + lambda: execution_id.ExecutionContext( + span_id=TEST_SPAN_ID, execution_id=TEST_EXECUTION_ID + ), + ) + expected_log_json.update( + { + "logging.googleapis.com/labels": { + "execution_id": TEST_EXECUTION_ID, + }, + "logging.googleapis.com/spanId": TEST_SPAN_ID, + } + ) + + log_handler.write(log_message) + record = capsys.readouterr() + assert json.loads(record.out) == expected_log_json + assert json.loads(record.out) == expected_log_json + + +def test_log_handler_without_context_logs_unmodified(monkeypatch, capsys): + log_handler = execution_id.LoggingHandlerAddExecutionId(stream=sys.stdout) + monkeypatch.setattr( + execution_id, + "_get_current_context", + lambda: None, + ) + expected_message = "log message\n" + + log_handler.write("log message") + record = capsys.readouterr() + assert record.out == expected_message + + +def test_log_handler_ignores_newlines(monkeypatch, capsys): + log_handler = execution_id.LoggingHandlerAddExecutionId(stream=sys.stdout) + monkeypatch.setattr( + execution_id, + "_get_current_context", + lambda: execution_id.ExecutionContext( + span_id=TEST_SPAN_ID, execution_id=TEST_EXECUTION_ID + ), + ) + + log_handler.write("\n") + record = capsys.readouterr() + assert record.out == "" + + +def test_log_handler_does_not_nest(): + log_handler_1 = execution_id.LoggingHandlerAddExecutionId(stream=sys.stdout) + log_handler_2 = execution_id.LoggingHandlerAddExecutionId(log_handler_1) + + assert log_handler_1 == log_handler_2 + + +def test_log_handler_omits_empty_execution_context(monkeypatch, capsys): + log_handler = execution_id.LoggingHandlerAddExecutionId(stream=sys.stdout) + monkeypatch.setattr( + execution_id, + "_get_current_context", + lambda: execution_id.ExecutionContext(span_id=None, execution_id=None), + ) + expected_json = { + "message": "some message", + } + + log_handler.write("some message") + record = capsys.readouterr() + assert json.loads(record.out) == expected_json + + +@pytest.mark.asyncio +async def test_maintains_execution_id_for_concurrent_requests(monkeypatch, capsys): + monkeypatch.setenv("LOG_EXECUTION_ID", "True") + monkeypatch.setattr( + execution_id, + "_generate_execution_id", + Mock(side_effect=("test-execution-id-1", "test-execution-id-2")), + ) + + expected_logs = ( + { + "message": "message1", + "logging.googleapis.com/labels": {"execution_id": "test-execution-id-1"}, + }, + { + "message": "message2", + "logging.googleapis.com/labels": {"execution_id": "test-execution-id-2"}, + }, + { + "message": "message1", + "logging.googleapis.com/labels": {"execution_id": "test-execution-id-1"}, + }, + { + "message": "message2", + "logging.googleapis.com/labels": {"execution_id": "test-execution-id-2"}, + }, + ) + + source = TEST_FUNCTIONS_DIR / "execution_id" / "main.py" + target = "sleep" + client = create_app(target, source).test_client() + loop = asyncio.get_event_loop() + response1 = loop.run_in_executor( + None, + partial( + client.post, + "/", + headers={ + "Content-Type": "application/json", + }, + json={"message": "message1"}, + ), + ) + response2 = loop.run_in_executor( + None, + partial( + client.post, + "/", + headers={ + "Content-Type": "application/json", + }, + json={"message": "message2"}, + ), + ) + await asyncio.wait((response1, response2)) + record = capsys.readouterr() + logs = record.err.strip().split("\n") + logs_as_json = tuple(json.loads(log) for log in logs) + + assert logs_as_json == expected_logs diff --git a/tests/test_functions/execution_id/main.py b/tests/test_functions/execution_id/main.py new file mode 100644 index 00000000..72d1eaff --- /dev/null +++ b/tests/test_functions/execution_id/main.py @@ -0,0 +1,33 @@ +import logging +import time + +logger = logging.getLogger(__name__) + + +def print_message(request): + json = request.get_json(silent=True) + print(json.get("message")) + return "success", 200 + + +def log_message(request): + json = request.get_json(silent=True) + logger.info(json.get("message")) + return "success", 200 + + +def function(request): + return {"execution_id": request.headers.get("Function-Execution-Id")} + + +def error(request): + return 1 / 0 + + +def sleep(request): + json = request.get_json(silent=True) + message = json.get("message") + logger.info(message) + time.sleep(1) + logger.info(message) + return "success", 200 diff --git a/tests/test_view_functions.py b/tests/test_view_functions.py index f69b2155..a32fe9e4 100644 --- a/tests/test_view_functions.py +++ b/tests/test_view_functions.py @@ -25,7 +25,7 @@ def test_http_view_func_wrapper(): function = pretend.call_recorder(lambda request: "Hello") request_object = pretend.stub() - local_proxy = pretend.stub(_get_current_object=lambda: request_object) + local_proxy = pretend.stub(_get_current_object=lambda: request_object, headers={}) view_func = functions_framework._http_view_func_wrapper(function, local_proxy) view_func("/some/path") From 2e7de92e5f9cd83f01222eb06385d66fe0211777 Mon Sep 17 00:00:00 2001 From: nifflets <5343516+nifflets@users.noreply.github.com> Date: Mon, 13 May 2024 11:36:19 -0700 Subject: [PATCH 039/108] feat: support disabling execution id logging (#325) * feat: support disabling execution id logging * Update test_execution_id.py * Update __init__.py * Update __init__.py --- src/functions_framework/__init__.py | 5 +++- tests/test_execution_id.py | 46 ++++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index 7474c01e..df4683cb 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -443,7 +443,10 @@ def _configure_app_execution_id_logging(): def _enable_execution_id_logging(): - return os.environ.get("LOG_EXECUTION_ID") + # Based on distutils.util.strtobool + truthy_values = ("y", "yes", "t", "true", "on", "1") + env_var_value = os.environ.get("LOG_EXECUTION_ID") + return env_var_value in truthy_values app = LazyWSGIApp() diff --git a/tests/test_execution_id.py b/tests/test_execution_id.py index bfddfc3c..c650ee31 100644 --- a/tests/test_execution_id.py +++ b/tests/test_execution_id.py @@ -46,7 +46,7 @@ def test_user_function_can_retrieve_execution_id_from_header(): def test_uncaught_exception_in_user_function_sets_execution_id(capsys, monkeypatch): - monkeypatch.setenv("LOG_EXECUTION_ID", "True") + monkeypatch.setenv("LOG_EXECUTION_ID", "true") source = TEST_FUNCTIONS_DIR / "execution_id" / "main.py" target = "error" app = create_app(target, source) @@ -64,7 +64,7 @@ def test_uncaught_exception_in_user_function_sets_execution_id(capsys, monkeypat def test_print_from_user_function_sets_execution_id(capsys, monkeypatch): - monkeypatch.setenv("LOG_EXECUTION_ID", "True") + monkeypatch.setenv("LOG_EXECUTION_ID", "true") source = TEST_FUNCTIONS_DIR / "execution_id" / "main.py" target = "print_message" app = create_app(target, source) @@ -83,7 +83,7 @@ def test_print_from_user_function_sets_execution_id(capsys, monkeypatch): def test_log_from_user_function_sets_execution_id(capsys, monkeypatch): - monkeypatch.setenv("LOG_EXECUTION_ID", "True") + monkeypatch.setenv("LOG_EXECUTION_ID", "true") source = TEST_FUNCTIONS_DIR / "execution_id" / "main.py" target = "log_message" app = create_app(target, source) @@ -136,6 +136,44 @@ def test_does_not_set_execution_id_when_not_enabled(capsys): assert "some-message" in record.out +def test_does_not_set_execution_id_when_env_var_is_false(capsys, monkeypatch): + monkeypatch.setenv("LOG_EXECUTION_ID", "false") + source = TEST_FUNCTIONS_DIR / "execution_id" / "main.py" + target = "print_message" + app = create_app(target, source) + client = app.test_client() + client.post( + "/", + headers={ + "Function-Execution-Id": TEST_EXECUTION_ID, + "Content-Type": "application/json", + }, + json={"message": "some-message"}, + ) + record = capsys.readouterr() + assert f'"execution_id": "{TEST_EXECUTION_ID}"' not in record.out + assert "some-message" in record.out + + +def test_does_not_set_execution_id_when_env_var_is_not_bool_like(capsys, monkeypatch): + monkeypatch.setenv("LOG_EXECUTION_ID", "maybe") + source = TEST_FUNCTIONS_DIR / "execution_id" / "main.py" + target = "print_message" + app = create_app(target, source) + client = app.test_client() + client.post( + "/", + headers={ + "Function-Execution-Id": TEST_EXECUTION_ID, + "Content-Type": "application/json", + }, + json={"message": "some-message"}, + ) + record = capsys.readouterr() + assert f'"execution_id": "{TEST_EXECUTION_ID}"' not in record.out + assert "some-message" in record.out + + def test_generate_execution_id(): expected_matching_regex = "^[0-9a-zA-Z]{12}$" actual_execution_id = execution_id._generate_execution_id() @@ -283,7 +321,7 @@ def test_log_handler_omits_empty_execution_context(monkeypatch, capsys): @pytest.mark.asyncio async def test_maintains_execution_id_for_concurrent_requests(monkeypatch, capsys): - monkeypatch.setenv("LOG_EXECUTION_ID", "True") + monkeypatch.setenv("LOG_EXECUTION_ID", "true") monkeypatch.setattr( execution_id, "_generate_execution_id", From f08757a17267d768e4c3ca4c6979f2a7db25e83c Mon Sep 17 00:00:00 2001 From: jrmfg <117788025+jrmfg@users.noreply.github.com> Date: Thu, 16 May 2024 12:41:34 -0700 Subject: [PATCH 040/108] feat: restore gunicorn worker default configs from 3.5.0 (#326) * feat: restore defaults present < 3.6.0, but retain customizability * revert the test, too * also restore this assert :) --------- Co-authored-by: Jeremy Fehr --- src/functions_framework/_http/gunicorn.py | 6 +++--- tests/test_http.py | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/functions_framework/_http/gunicorn.py b/src/functions_framework/_http/gunicorn.py index 009a06b7..050f766c 100644 --- a/src/functions_framework/_http/gunicorn.py +++ b/src/functions_framework/_http/gunicorn.py @@ -21,9 +21,9 @@ class GunicornApplication(gunicorn.app.base.BaseApplication): def __init__(self, app, host, port, debug, **options): self.options = { "bind": "%s:%s" % (host, port), - "workers": os.environ.get("WORKERS", (os.cpu_count() or 1) * 4), - "threads": os.environ.get("THREADS", 1), - "timeout": os.environ.get("CLOUD_RUN_TIMEOUT_SECONDS", 300), + "workers": os.environ.get("WORKERS", 1), + "threads": os.environ.get("THREADS", (os.cpu_count() or 1) * 4), + "timeout": os.environ.get("CLOUD_RUN_TIMEOUT_SECONDS", 0), "loglevel": "error", "limit_request_line": 0, } diff --git a/tests/test_http.py b/tests/test_http.py index 0a46fbea..fbfac9d2 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -97,17 +97,17 @@ def test_gunicorn_application(debug): assert gunicorn_app.app == app assert gunicorn_app.options == { "bind": "%s:%s" % (host, port), - "workers": os.cpu_count() * 4, - "threads": 1, - "timeout": 300, + "workers": 1, + "threads": os.cpu_count() * 4, + "timeout": 0, "loglevel": "error", "limit_request_line": 0, } assert gunicorn_app.cfg.bind == ["1.2.3.4:1234"] - assert gunicorn_app.cfg.workers == os.cpu_count() * 4 - assert gunicorn_app.cfg.threads == 1 - assert gunicorn_app.cfg.timeout == 300 + assert gunicorn_app.cfg.workers == 1 + assert gunicorn_app.cfg.threads == os.cpu_count() * 4 + assert gunicorn_app.cfg.timeout == 0 assert gunicorn_app.load() == app From 0ecd98507b4ee458e2aea858119255f9f1dc1cea Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 16 May 2024 21:57:01 +0200 Subject: [PATCH 041/108] chore(deps): update all non-major dependencies (#322) Co-authored-by: jrmfg <117788025+jrmfg@users.noreply.github.com> --- .github/workflows/codeql.yml | 10 +++++----- .github/workflows/conformance.yml | 6 +++--- .github/workflows/dependency-review.yml | 6 +++--- .github/workflows/lint.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- .github/workflows/scorecard.yml | 8 ++++---- .github/workflows/unit.yml | 4 ++-- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 8be2b791..f2ba168f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 with: disable-sudo: true egress-policy: block @@ -53,11 +53,11 @@ jobs: objects.githubusercontent.com:443 - name: Checkout repository - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3 + uses: github/codeql-action/init@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.25.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -67,7 +67,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3 + uses: github/codeql-action/autobuild@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.25.5 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -80,6 +80,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3 + uses: github/codeql-action/analyze@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.25.5 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 80ad8afb..9369f779 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -16,7 +16,7 @@ jobs: python: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] steps: - name: Harden Runner - uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 with: disable-sudo: true egress-policy: block @@ -30,7 +30,7 @@ jobs: storage.googleapis.com:443 - name: Checkout code - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Setup Python uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 @@ -41,7 +41,7 @@ jobs: run: python -m pip install -e . - name: Setup Go - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 + uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 with: go-version: '1.20' diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index a79373ca..29b80a11 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 with: disable-sudo: true egress-policy: block @@ -25,6 +25,6 @@ jobs: api.github.com:443 github.com:443 - name: 'Checkout Repository' - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: 'Dependency Review' - uses: actions/dependency-review-action@5bbc3ba658137598168acb2ab73b21c432dd411b # v4.2.5 + uses: actions/dependency-review-action@0c155c5e8556a497adf53f2c18edabf945ed8e70 # v4.3.2 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d560d95f..efbce442 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 with: disable-sudo: true egress-policy: block @@ -21,7 +21,7 @@ jobs: github.com:443 pypi.org:443 - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Setup Python uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 - name: Install tox diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 63b9f7f8..3eea6984 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,12 +13,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - name: Checkout - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 with: ref: ${{ github.event.release.tag_name }} - name: Install Python diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 150d7e49..6b89ddbc 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 with: disable-sudo: true egress-policy: block @@ -44,12 +44,12 @@ jobs: - name: "Checkout code" - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 + uses: ossf/scorecard-action@dc50aa9510b46c811795eb24b2f1ba02a914e534 # v2.3.3 with: results_file: results.sarif results_format: sarif @@ -61,6 +61,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3 + uses: github/codeql-action/upload-sarif@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.25.5 with: sarif_file: results.sarif diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index f396ca2c..7943f75a 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -32,7 +32,7 @@ jobs: runs-on: ${{ matrix.platform }} steps: - name: Harden Runner - uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 with: disable-sudo: true egress-policy: block @@ -45,7 +45,7 @@ jobs: registry-1.docker.io:443 - name: Checkout - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Use Python ${{ matrix.python }} uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 with: From 3d4389229cfa0bc9bf29783a9f831525a2d1aaea Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 16 May 2024 22:03:34 +0200 Subject: [PATCH 042/108] chore(deps): update pypa/gh-action-pypi-publish digest to 699cd61 (#313) Co-authored-by: jrmfg <117788025+jrmfg@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3eea6984..9c11e448 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: - name: Build distributions run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@c12cc61414480c03e10ea76e2a0a1a17d6c764e2 # main + uses: pypa/gh-action-pypi-publish@699cd6103f50bf5c3b2f070c70712d109c168e6c # main with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} From fff38ae6ecb1054bad676900216663050e6edf10 Mon Sep 17 00:00:00 2001 From: jrmfg <117788025+jrmfg@users.noreply.github.com> Date: Thu, 16 May 2024 13:34:44 -0700 Subject: [PATCH 043/108] fix: update scorecard.yml (#327) needed for security scorecard --- .github/workflows/scorecard.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 6b89ddbc..dde198ab 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -36,6 +36,7 @@ jobs: api.securityscorecards.dev:443 auth.docker.io:443 bestpractices.coreinfrastructure.org:443 + bestpractices.dev:443 github.com:443 index.docker.io:443 oss-fuzz-build-logs.storage.googleapis.com:443 From 2601975285386fc573de8033381edc99527ef3c9 Mon Sep 17 00:00:00 2001 From: Jeremy Fehr <117788025+jrmfg@users.noreply.github.com> Date: Fri, 17 May 2024 13:11:33 -0700 Subject: [PATCH 044/108] feat: (opt-in): terminate handling of work when the request has already timed out (#328) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Overhead-free (or at least very cheap). The “timeout” gunicorn config means drastically different things for sync and non-sync workers: > Workers silent for more than this many seconds are killed and restarted. > > Value is a positive number or 0. Setting it to 0 has the effect of > infinite timeouts by disabling timeouts for all workers entirely. > > Generally, the default of thirty seconds should suffice. Only set this > noticeably higher if you’re sure of the repercussions for sync workers. > For the non sync workers it just means that the worker process is still > communicating and is not tied to the length of time required to handle a > single request. So. For cases where threads = 1 (user set or our defaults), we’ll use the sync worker and let the regular timeout functionality do its thing. For cases where threads > 1, we’re using the gthread worker, and timeout means something completely different and not really user-observable. So we’ll leave the communication timeout (default gunicorn “timeout”) at 30 seconds, but create our own gthread-derived worker class to use instead, which terminates request handling (with no mind to gunicorn’s “graceful shutdown” config), to emulate GCFv1. The arbiter spawns these workers, so we have to maintain some sort of global timeout state for us to read in our custom gthread worker. In the future, we should consider letting the user adjust the graceful shutdown seconds. But the default of 30 seems like it’s worked fine historically, so it’s hard to argue for changing it. IIUC, this means that on gen 2, there’s a small behavior difference for the sync workers compared to gen 1, in that gen 2 sync worker workloads will get an extra 30 seconds of timeout to gracefully shut down. I don’t think monkeying with this config and opting-in to sync workers is very common, though, so let’s not worry about it here; everyone should be on the gthread path outlined above. * fix tests * small test fixes give up on coverage support for things that are tested in different processes, or in gthread, because it looks like pytest-cov gave up on support for these, where as coverage has out-of-the-box support * format * isort everything * skip tests on mac there's something test-specific about how mac pickles functions for execution in multiprocessing.Process which is causing problems. it seems somewhere in the innards of flask and gunicorn and macos... since this feature is opt-in anyway, let's just skip testing darwin. * sort tuple of dicts in async tests before asserting causes flakes sometimes in workflows * use double-quotes * also skip tests on windows - this is all built for gunicorn, there's no value adding it for windows anyway * skip import on windows * easy stuff * add a few tests for sync worker timeouts these shouldn't have changed with this commit --- .../send_cloud_event.py | 2 +- playground/main.py | 16 ++ src/functions_framework/_http/gunicorn.py | 42 ++- src/functions_framework/exceptions.py | 6 +- src/functions_framework/request_timeout.py | 42 +++ tests/test_execution_id.py | 3 +- tests/test_functions/timeout/main.py | 12 + tests/test_timeouts.py | 265 ++++++++++++++++++ tox.ini | 1 + 9 files changed, 381 insertions(+), 8 deletions(-) create mode 100644 playground/main.py create mode 100644 src/functions_framework/request_timeout.py create mode 100644 tests/test_functions/timeout/main.py create mode 100644 tests/test_timeouts.py diff --git a/examples/cloud_run_cloud_events/send_cloud_event.py b/examples/cloud_run_cloud_events/send_cloud_event.py index b523c31a..c08b8f93 100644 --- a/examples/cloud_run_cloud_events/send_cloud_event.py +++ b/examples/cloud_run_cloud_events/send_cloud_event.py @@ -13,9 +13,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from cloudevents.http import CloudEvent, to_structured import requests +from cloudevents.http import CloudEvent, to_structured # Create a cloudevent using https://github.com/cloudevents/sdk-python # Note we only need source and type because the cloudevents constructor by diff --git a/playground/main.py b/playground/main.py new file mode 100644 index 00000000..b974ddbc --- /dev/null +++ b/playground/main.py @@ -0,0 +1,16 @@ +import logging +import time + +import functions_framework + +logger = logging.getLogger(__name__) + + +@functions_framework.http +def main(request): + timeout = 2 + for _ in range(timeout * 10): + time.sleep(0.1) + logger.info("logging message after timeout elapsed") + return "Hello, world!" + diff --git a/src/functions_framework/_http/gunicorn.py b/src/functions_framework/_http/gunicorn.py index 050f766c..92cad90e 100644 --- a/src/functions_framework/_http/gunicorn.py +++ b/src/functions_framework/_http/gunicorn.py @@ -1,4 +1,4 @@ -# Copyright 2020 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,17 +16,43 @@ import gunicorn.app.base +from gunicorn.workers.gthread import ThreadWorker + +from ..request_timeout import ThreadingTimeout + +# global for use in our custom gthread worker; the gunicorn arbiter spawns these +# and it's not possible to inject (and self.timeout means something different to +# async workers!) +# set/managed in gunicorn application init for test-friendliness +TIMEOUT_SECONDS = None + class GunicornApplication(gunicorn.app.base.BaseApplication): def __init__(self, app, host, port, debug, **options): + threads = int(os.environ.get("THREADS", (os.cpu_count() or 1) * 4)) + + global TIMEOUT_SECONDS + TIMEOUT_SECONDS = int(os.environ.get("CLOUD_RUN_TIMEOUT_SECONDS", 0)) + self.options = { "bind": "%s:%s" % (host, port), - "workers": os.environ.get("WORKERS", 1), - "threads": os.environ.get("THREADS", (os.cpu_count() or 1) * 4), - "timeout": os.environ.get("CLOUD_RUN_TIMEOUT_SECONDS", 0), - "loglevel": "error", + "workers": int(os.environ.get("WORKERS", 1)), + "threads": threads, + "loglevel": os.environ.get("GUNICORN_LOG_LEVEL", "error"), "limit_request_line": 0, } + + if ( + TIMEOUT_SECONDS > 0 + and threads > 1 + and (os.environ.get("THREADED_TIMEOUT_ENABLED", "False").lower() == "true") + ): # pragma: no cover + self.options["worker_class"] = ( + "functions_framework._http.gunicorn.GThreadWorkerWithTimeoutSupport" + ) + else: + self.options["timeout"] = TIMEOUT_SECONDS + self.options.update(options) self.app = app @@ -38,3 +64,9 @@ def load_config(self): def load(self): return self.app + + +class GThreadWorkerWithTimeoutSupport(ThreadWorker): # pragma: no cover + def handle_request(self, req, conn): + with ThreadingTimeout(TIMEOUT_SECONDS): + super(GThreadWorkerWithTimeoutSupport, self).handle_request(req, conn) diff --git a/src/functions_framework/exceptions.py b/src/functions_framework/exceptions.py index 671a28a4..81b9f8f0 100644 --- a/src/functions_framework/exceptions.py +++ b/src/functions_framework/exceptions.py @@ -1,4 +1,4 @@ -# Copyright 2020 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -35,3 +35,7 @@ class MissingTargetException(FunctionsFrameworkException): class EventConversionException(FunctionsFrameworkException): pass + + +class RequestTimeoutException(FunctionsFrameworkException): + pass diff --git a/src/functions_framework/request_timeout.py b/src/functions_framework/request_timeout.py new file mode 100644 index 00000000..ed34f66e --- /dev/null +++ b/src/functions_framework/request_timeout.py @@ -0,0 +1,42 @@ +import ctypes +import logging +import threading + +from .exceptions import RequestTimeoutException + +logger = logging.getLogger(__name__) + + +class ThreadingTimeout(object): # pragma: no cover + def __init__(self, seconds): + self.seconds = seconds + self.target_tid = threading.current_thread().ident + self.timer = None + + def __enter__(self): + self.timer = threading.Timer(self.seconds, self._raise_exc) + self.timer.start() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.timer.cancel() + if exc_type is RequestTimeoutException: + logger.warning( + "Request handling exceeded {0} seconds timeout; terminating request handling...".format( + self.seconds + ), + exc_info=(exc_type, exc_val, exc_tb), + ) + return False + + def _raise_exc(self): + ret = ctypes.pythonapi.PyThreadState_SetAsyncExc( + ctypes.c_long(self.target_tid), ctypes.py_object(RequestTimeoutException) + ) + if ret == 0: + raise ValueError("Invalid thread ID {}".format(self.target_tid)) + elif ret > 1: + ctypes.pythonapi.PyThreadState_SetAsyncExc( + ctypes.c_long(self.target_tid), None + ) + raise SystemError("PyThreadState_SetAsyncExc failed") diff --git a/tests/test_execution_id.py b/tests/test_execution_id.py index c650ee31..a2601817 100644 --- a/tests/test_execution_id.py +++ b/tests/test_execution_id.py @@ -378,4 +378,5 @@ async def test_maintains_execution_id_for_concurrent_requests(monkeypatch, capsy logs = record.err.strip().split("\n") logs_as_json = tuple(json.loads(log) for log in logs) - assert logs_as_json == expected_logs + sort_key = lambda d: d["message"] + assert sorted(logs_as_json, key=sort_key) == sorted(expected_logs, key=sort_key) diff --git a/tests/test_functions/timeout/main.py b/tests/test_functions/timeout/main.py new file mode 100644 index 00000000..09efeb88 --- /dev/null +++ b/tests/test_functions/timeout/main.py @@ -0,0 +1,12 @@ +import logging +import time + +logger = logging.getLogger(__name__) + + +def function(request): + # sleep for 1200 total ms (1.2 sec) + for _ in range(12): + time.sleep(0.1) + logger.info("some extra logging message") + return "success", 200 diff --git a/tests/test_timeouts.py b/tests/test_timeouts.py new file mode 100644 index 00000000..9637de67 --- /dev/null +++ b/tests/test_timeouts.py @@ -0,0 +1,265 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pathlib +import socket +import time + +from multiprocessing import Process + +import pytest +import requests + +ff_gunicorn = pytest.importorskip("functions_framework._http.gunicorn") + + +from functions_framework import create_app + +TEST_FUNCTIONS_DIR = pathlib.Path(__file__).resolve().parent / "test_functions" +TEST_HOST = "0.0.0.0" +TEST_PORT = "8080" + + +@pytest.fixture(autouse=True) +def run_around_tests(): + # the test samples test also listens on 8080, so let's be good stewards of + # the port and make sure it's free + _wait_for_no_listen(TEST_HOST, TEST_PORT) + yield + _wait_for_no_listen(TEST_HOST, TEST_PORT) + + +@pytest.mark.skipif("platform.system() == 'Windows'") +@pytest.mark.skipif("platform.system() == 'Darwin'") +@pytest.mark.slow_integration_test +def test_no_timeout_allows_request_processing_to_finish(): + source = TEST_FUNCTIONS_DIR / "timeout" / "main.py" + target = "function" + + app = create_app(target, source) + + options = {} + + gunicorn_app = ff_gunicorn.GunicornApplication( + app, TEST_HOST, TEST_PORT, False, **options + ) + + gunicorn_p = Process(target=gunicorn_app.run) + gunicorn_p.start() + + _wait_for_listen(TEST_HOST, TEST_PORT) + + result = requests.get("http://{}:{}/".format(TEST_HOST, TEST_PORT)) + + gunicorn_p.terminate() + gunicorn_p.join() + + assert result.status_code == 200 + + +@pytest.mark.skipif("platform.system() == 'Windows'") +@pytest.mark.skipif("platform.system() == 'Darwin'") +@pytest.mark.slow_integration_test +def test_timeout_but_not_threaded_timeout_enabled_does_not_kill(monkeypatch): + monkeypatch.setenv("CLOUD_RUN_TIMEOUT_SECONDS", "1") + monkeypatch.setenv("THREADED_TIMEOUT_ENABLED", "false") + source = TEST_FUNCTIONS_DIR / "timeout" / "main.py" + target = "function" + + app = create_app(target, source) + + options = {} + + gunicorn_app = ff_gunicorn.GunicornApplication( + app, TEST_HOST, TEST_PORT, False, **options + ) + + gunicorn_p = Process(target=gunicorn_app.run) + gunicorn_p.start() + + _wait_for_listen(TEST_HOST, TEST_PORT) + + result = requests.get("http://{}:{}/".format(TEST_HOST, TEST_PORT)) + + gunicorn_p.terminate() + gunicorn_p.join() + + assert result.status_code == 200 + + +@pytest.mark.skipif("platform.system() == 'Windows'") +@pytest.mark.skipif("platform.system() == 'Darwin'") +@pytest.mark.slow_integration_test +def test_timeout_and_threaded_timeout_enabled_kills(monkeypatch): + monkeypatch.setenv("CLOUD_RUN_TIMEOUT_SECONDS", "1") + monkeypatch.setenv("THREADED_TIMEOUT_ENABLED", "true") + source = TEST_FUNCTIONS_DIR / "timeout" / "main.py" + target = "function" + + app = create_app(target, source) + + options = {} + + gunicorn_app = ff_gunicorn.GunicornApplication( + app, TEST_HOST, TEST_PORT, False, **options + ) + + gunicorn_p = Process(target=gunicorn_app.run) + gunicorn_p.start() + + _wait_for_listen(TEST_HOST, TEST_PORT) + + result = requests.get("http://{}:{}/".format(TEST_HOST, TEST_PORT)) + + gunicorn_p.terminate() + gunicorn_p.join() + + # Any exception raised in execution is a 500 error. Cloud Functions 1st gen and + # 2nd gen/Cloud Run infrastructure doing the timeout will return a 408 (gen 1) + # or 504 (gen 2/CR) at the infrastructure layer when request timeouts happen, + # and this code will only be available to the user in logs. + assert result.status_code == 500 + + +@pytest.mark.skipif("platform.system() == 'Windows'") +@pytest.mark.skipif("platform.system() == 'Darwin'") +@pytest.mark.slow_integration_test +def test_timeout_and_threaded_timeout_enabled_but_timeout_not_exceeded_doesnt_kill( + monkeypatch, +): + monkeypatch.setenv("CLOUD_RUN_TIMEOUT_SECONDS", "2") + monkeypatch.setenv("THREADED_TIMEOUT_ENABLED", "true") + source = TEST_FUNCTIONS_DIR / "timeout" / "main.py" + target = "function" + + app = create_app(target, source) + + options = {} + + gunicorn_app = ff_gunicorn.GunicornApplication( + app, TEST_HOST, TEST_PORT, False, **options + ) + + gunicorn_p = Process(target=gunicorn_app.run) + gunicorn_p.start() + + _wait_for_listen(TEST_HOST, TEST_PORT) + + result = requests.get("http://{}:{}/".format(TEST_HOST, TEST_PORT)) + + gunicorn_p.terminate() + gunicorn_p.join() + + assert result.status_code == 200 + + +@pytest.mark.skipif("platform.system() == 'Windows'") +@pytest.mark.skipif("platform.system() == 'Darwin'") +@pytest.mark.slow_integration_test +def test_timeout_sync_worker_kills_on_timeout( + monkeypatch, +): + monkeypatch.setenv("CLOUD_RUN_TIMEOUT_SECONDS", "1") + monkeypatch.setenv("WORKERS", 2) + monkeypatch.setenv("THREADS", 1) + source = TEST_FUNCTIONS_DIR / "timeout" / "main.py" + target = "function" + + app = create_app(target, source) + + options = {} + + gunicorn_app = ff_gunicorn.GunicornApplication( + app, TEST_HOST, TEST_PORT, False, **options + ) + + gunicorn_p = Process(target=gunicorn_app.run) + gunicorn_p.start() + + _wait_for_listen(TEST_HOST, TEST_PORT) + + result = requests.get("http://{}:{}/".format(TEST_HOST, TEST_PORT)) + + gunicorn_p.terminate() + gunicorn_p.join() + + assert result.status_code == 500 + + +@pytest.mark.skipif("platform.system() == 'Windows'") +@pytest.mark.skipif("platform.system() == 'Darwin'") +@pytest.mark.slow_integration_test +def test_timeout_sync_worker_does_not_kill_if_less_than_timeout( + monkeypatch, +): + monkeypatch.setenv("CLOUD_RUN_TIMEOUT_SECONDS", "2") + monkeypatch.setenv("WORKERS", 2) + monkeypatch.setenv("THREADS", 1) + source = TEST_FUNCTIONS_DIR / "timeout" / "main.py" + target = "function" + + app = create_app(target, source) + + options = {} + + gunicorn_app = ff_gunicorn.GunicornApplication( + app, TEST_HOST, TEST_PORT, False, **options + ) + + gunicorn_p = Process(target=gunicorn_app.run) + gunicorn_p.start() + + _wait_for_listen(TEST_HOST, TEST_PORT) + + result = requests.get("http://{}:{}/".format(TEST_HOST, TEST_PORT)) + + gunicorn_p.terminate() + gunicorn_p.join() + + assert result.status_code == 200 + + +@pytest.mark.skip +def _wait_for_listen(host, port, timeout=10): + # Used in tests to make sure that the gunicorn app has booted and is + # listening before sending a test request + start_time = time.perf_counter() + while True: + try: + with socket.create_connection((host, port), timeout=timeout): + break + except OSError as ex: + time.sleep(0.01) + if time.perf_counter() - start_time >= timeout: + raise TimeoutError( + "Waited too long for port {} on host {} to start accepting " + "connections.".format(port, host) + ) from ex + + +@pytest.mark.skip +def _wait_for_no_listen(host, port, timeout=10): + # Used in tests to make sure that the port is actually free after + # the process binding to it should have been killed + start_time = time.perf_counter() + while True: + try: + with socket.create_connection((host, port), timeout=timeout): + time.sleep(0.01) + if time.perf_counter() - start_time >= timeout: + raise TimeoutError( + "Waited too long for port {} on host {} to stop accepting " + "connections.".format(port, host) + ) + except OSError as ex: + break diff --git a/tox.ini b/tox.ini index e8c555b5..6ba6d3b4 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ envlist = py{35,36,37,38,39,310}-{ubuntu-latest,macos-latest,windows-latest},lin usedevelop = true deps = docker + pytest-asyncio pytest-cov pytest-integration pretend From 04c1fdc4185b8a97eb46d72b6432d32d5d70dffc Mon Sep 17 00:00:00 2001 From: Jeremy Fehr <117788025+jrmfg@users.noreply.github.com> Date: Fri, 17 May 2024 13:20:22 -0700 Subject: [PATCH 045/108] fix: Update scorecard.yml (#329) --- .github/workflows/scorecard.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index dde198ab..9f84e511 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -33,6 +33,7 @@ jobs: allowed-endpoints: > api.github.com:443 api.osv.dev:443 + api.scorecard.dev:443 api.securityscorecards.dev:443 auth.docker.io:443 bestpractices.coreinfrastructure.org:443 From 0ebea7d818a6dd62aa98df813eed6a79948d6b84 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 22 May 2024 09:48:27 -0700 Subject: [PATCH 046/108] chore(main): release 3.7.0 (#324) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 16 ++++++++++++++++ setup.py | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23665c2e..5f0e13a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.7.0](https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v3.6.0...v3.7.0) (2024-05-17) + + +### Features + +* (opt-in): terminate handling of work when the request has already timed out ([#328](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/328)) ([2601975](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/2601975285386fc573de8033381edc99527ef3c9)) +* Add execution id ([#320](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/320)) ([662bf4c](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/662bf4ced9aa52efe774662a0f0f496d3d3534fc)) +* restore gunicorn worker default configs from 3.5.0 ([#326](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/326)) ([f08757a](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/f08757a17267d768e4c3ca4c6979f2a7db25e83c)) +* support disabling execution id logging ([#325](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/325)) ([2e7de92](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/2e7de92e5f9cd83f01222eb06385d66fe0211777)) + + +### Bug Fixes + +* update scorecard.yml ([#327](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/327)) ([fff38ae](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/fff38ae6ecb1054bad676900216663050e6edf10)) +* Update scorecard.yml ([#329](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/329)) ([04c1fdc](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/04c1fdc4185b8a97eb46d72b6432d32d5d70dffc)) + ## [3.6.0](https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v3.5.0...v3.6.0) (2024-04-29) diff --git a/setup.py b/setup.py index d4a9fea5..5e529845 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( name="functions-framework", - version="3.6.0", + version="3.7.0", description="An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team.", long_description=long_description, long_description_content_type="text/markdown", From 02472e7315d0fd642db26441b3cb21f799906739 Mon Sep 17 00:00:00 2001 From: Jeremy Fehr <117788025+jrmfg@users.noreply.github.com> Date: Wed, 22 May 2024 10:25:28 -0700 Subject: [PATCH 047/108] fix: add www.bestpractices.dev:443 to scorecard (#330) * fix: update scorecard.yml * fix: update scorecard.yml --- .github/workflows/scorecard.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 9f84e511..b7ed460a 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -38,6 +38,7 @@ jobs: auth.docker.io:443 bestpractices.coreinfrastructure.org:443 bestpractices.dev:443 + www.bestpractices.dev:443 github.com:443 index.docker.io:443 oss-fuzz-build-logs.storage.googleapis.com:443 From 94763d83fb931c16682acbc978c094de9f6b1aea Mon Sep 17 00:00:00 2001 From: HKWinterhalter Date: Sun, 2 Jun 2024 23:47:03 -0700 Subject: [PATCH 048/108] chore: Update blunderbuss.yml (#333) * Update blunderbuss.yml * chore: Update blunderbuss.yml * chore: Update blunderbuss.yml Remove assignees - to be replaced with other mechanism --- .github/blunderbuss.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/blunderbuss.yml b/.github/blunderbuss.yml index ffa474e5..8b137891 100644 --- a/.github/blunderbuss.yml +++ b/.github/blunderbuss.yml @@ -1,7 +1 @@ -assign_prs: - - HKWinterhalter - - janell-chen -assign_issues: - - HKWinterhalter - - janell-chen From d1d0753b6ea0dcc4222e28fc61002ac563b54cac Mon Sep 17 00:00:00 2001 From: nifflets <5343516+nifflets@users.noreply.github.com> Date: Tue, 11 Jun 2024 10:33:25 -0700 Subject: [PATCH 049/108] feat: Set default logging level to align with Flask's defaults (#336) --- src/functions_framework/__init__.py | 2 +- tests/test_functions/execution_id/main.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index df4683cb..22fbf44c 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -437,7 +437,7 @@ def _configure_app_execution_id_logging(): "stream": "ext://functions_framework.execution_id.logging_stream", }, }, - "root": {"level": "INFO", "handlers": ["wsgi"]}, + "root": {"level": "WARNING", "handlers": ["wsgi"]}, } ) diff --git a/tests/test_functions/execution_id/main.py b/tests/test_functions/execution_id/main.py index 72d1eaff..f6677603 100644 --- a/tests/test_functions/execution_id/main.py +++ b/tests/test_functions/execution_id/main.py @@ -12,7 +12,7 @@ def print_message(request): def log_message(request): json = request.get_json(silent=True) - logger.info(json.get("message")) + logger.warning(json.get("message")) return "success", 200 @@ -27,7 +27,7 @@ def error(request): def sleep(request): json = request.get_json(silent=True) message = json.get("message") - logger.info(message) + logger.warning(message) time.sleep(1) - logger.info(message) + logger.warning(message) return "success", 200 From 7ba78506745c06acb0da39e31e4927dbbd50a07a Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 25 Jun 2024 18:26:58 +0200 Subject: [PATCH 050/108] chore(deps): update all non-major dependencies (#335) --- .github/workflows/codeql.yml | 10 +++++----- .github/workflows/conformance.yml | 4 ++-- .github/workflows/dependency-review.yml | 6 +++--- .github/workflows/lint.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- .github/workflows/scorecard.yml | 6 +++--- .github/workflows/unit.yml | 4 ++-- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f2ba168f..1159d924 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 + uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1 with: disable-sudo: true egress-policy: block @@ -53,11 +53,11 @@ jobs: objects.githubusercontent.com:443 - name: Checkout repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.25.5 + uses: github/codeql-action/init@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -67,7 +67,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.25.5 + uses: github/codeql-action/autobuild@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -80,6 +80,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.25.5 + uses: github/codeql-action/analyze@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 9369f779..a7305644 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -16,7 +16,7 @@ jobs: python: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] steps: - name: Harden Runner - uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 + uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1 with: disable-sudo: true egress-policy: block @@ -30,7 +30,7 @@ jobs: storage.googleapis.com:443 - name: Checkout code - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup Python uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 29b80a11..47acd65b 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 + uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1 with: disable-sudo: true egress-policy: block @@ -25,6 +25,6 @@ jobs: api.github.com:443 github.com:443 - name: 'Checkout Repository' - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: 'Dependency Review' - uses: actions/dependency-review-action@0c155c5e8556a497adf53f2c18edabf945ed8e70 # v4.3.2 + uses: actions/dependency-review-action@72eb03d02c7872a771aacd928f3123ac62ad6d3a # v4.3.3 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index efbce442..244db273 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 + uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1 with: disable-sudo: true egress-policy: block @@ -21,7 +21,7 @@ jobs: github.com:443 pypi.org:443 - - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup Python uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 - name: Install tox diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9c11e448..3fb61679 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,12 +13,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 + uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - name: Checkout - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: ref: ${{ github.event.release.tag_name }} - name: Install Python diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index b7ed460a..fb1d6581 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 + uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1 with: disable-sudo: true egress-policy: block @@ -47,7 +47,7 @@ jobs: - name: "Checkout code" - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: persist-credentials: false @@ -64,6 +64,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.25.5 + uses: github/codeql-action/upload-sarif@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10 with: sarif_file: results.sarif diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 7943f75a..332d563d 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -32,7 +32,7 @@ jobs: runs-on: ${{ matrix.platform }} steps: - name: Harden Runner - uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 + uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1 with: disable-sudo: true egress-policy: block @@ -45,7 +45,7 @@ jobs: registry-1.docker.io:443 - name: Checkout - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Use Python ${{ matrix.python }} uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 with: From fb82fb66c79a0591bd8d882bda51425c68a880dc Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 25 Jun 2024 18:31:33 +0200 Subject: [PATCH 051/108] chore(deps): update pypa/gh-action-pypi-publish digest to ec4db0b (#334) Co-authored-by: Jeremy Fehr <117788025+jrmfg@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3fb61679..710d3ca8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: - name: Build distributions run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@699cd6103f50bf5c3b2f070c70712d109c168e6c # main + uses: pypa/gh-action-pypi-publish@ec4db0b4ddc65acdf4bff5fa45ac92d78b56bdf0 # main with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} From 102bc8ab9dbd3926b8791bb9fbedd68762fdd16e Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:12:51 -0700 Subject: [PATCH 052/108] chore(main): release 3.8.0 (#332) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: Jeremy Fehr <117788025+jrmfg@users.noreply.github.com> --- CHANGELOG.md | 12 ++++++++++++ setup.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f0e13a1..f9209989 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.8.0](https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v3.7.0...v3.8.0) (2024-06-25) + + +### Features + +* Set default logging level to align with Flask's defaults ([#336](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/336)) ([d1d0753](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/d1d0753b6ea0dcc4222e28fc61002ac563b54cac)) + + +### Bug Fixes + +* add www.bestpractices.dev:443 to scorecard ([#330](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/330)) ([02472e7](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/02472e7315d0fd642db26441b3cb21f799906739)) + ## [3.7.0](https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v3.6.0...v3.7.0) (2024-05-17) diff --git a/setup.py b/setup.py index 5e529845..6cfa3fec 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( name="functions-framework", - version="3.7.0", + version="3.8.0", description="An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team.", long_description=long_description, long_description_content_type="text/markdown", From d622f137e8a2419fc487c867d67e12d0204b586b Mon Sep 17 00:00:00 2001 From: Jeremy Fehr <117788025+jrmfg@users.noreply.github.com> Date: Fri, 26 Jul 2024 15:40:18 -0700 Subject: [PATCH 053/108] fix: upgrade gunicorn to 22 to fix CVE-2024-1135 (#341) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6cfa3fec..ff13955c 100644 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ "flask>=1.0,<4.0", "click>=7.0,<9.0", "watchdog>=1.0.0", - "gunicorn>=19.2.0; platform_system!='Windows'", + "gunicorn>=22.0.0; platform_system!='Windows'", "cloudevents>=1.2.0,<2.0.0", "Werkzeug>=0.14,<4.0.0", ], From 432acc188f4a8809a7cd225e070df97629cb6a74 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Fri, 26 Jul 2024 15:46:10 -0700 Subject: [PATCH 054/108] chore(main): release 3.8.1 (#342) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9209989..515ce06e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.8.1](https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v3.8.0...v3.8.1) (2024-07-26) + + +### Bug Fixes + +* upgrade gunicorn to 22 to fix CVE-2024-1135 ([#341](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/341)) ([d622f13](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/d622f137e8a2419fc487c867d67e12d0204b586b)) + ## [3.8.0](https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v3.7.0...v3.8.0) (2024-06-25) diff --git a/setup.py b/setup.py index ff13955c..e89e6296 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( name="functions-framework", - version="3.8.0", + version="3.8.1", description="An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team.", long_description=long_description, long_description_content_type="text/markdown", From 0f29362e5710de07388cf72889214a5e00931219 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Sat, 27 Jul 2024 01:48:52 +0200 Subject: [PATCH 055/108] chore(deps): update all non-major dependencies (#339) Co-authored-by: Jeremy Fehr <117788025+jrmfg@users.noreply.github.com> --- .github/workflows/codeql.yml | 8 ++++---- .github/workflows/conformance.yml | 6 +++--- .github/workflows/dependency-review.yml | 4 ++-- .github/workflows/lint.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- .github/workflows/scorecard.yml | 6 +++--- .github/workflows/unit.yml | 4 ++-- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1159d924..12fc1e5d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1 + uses: step-security/harden-runner@0d381219ddf674d61a7572ddd19d7941e271515c # v2.9.0 with: disable-sudo: true egress-policy: block @@ -57,7 +57,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10 + uses: github/codeql-action/init@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -67,7 +67,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10 + uses: github/codeql-action/autobuild@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -80,6 +80,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10 + uses: github/codeql-action/analyze@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index a7305644..6b345d29 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -16,7 +16,7 @@ jobs: python: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] steps: - name: Harden Runner - uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1 + uses: step-security/harden-runner@0d381219ddf674d61a7572ddd19d7941e271515c # v2.9.0 with: disable-sudo: true egress-policy: block @@ -33,7 +33,7 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup Python - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 with: python-version: ${{ matrix.python }} @@ -41,7 +41,7 @@ jobs: run: python -m pip install -e . - name: Setup Go - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version: '1.20' diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 47acd65b..e85dd0e7 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1 + uses: step-security/harden-runner@0d381219ddf674d61a7572ddd19d7941e271515c # v2.9.0 with: disable-sudo: true egress-policy: block @@ -27,4 +27,4 @@ jobs: - name: 'Checkout Repository' uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: 'Dependency Review' - uses: actions/dependency-review-action@72eb03d02c7872a771aacd928f3123ac62ad6d3a # v4.3.3 + uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 244db273..6e36e9f2 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1 + uses: step-security/harden-runner@0d381219ddf674d61a7572ddd19d7941e271515c # v2.9.0 with: disable-sudo: true egress-policy: block @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup Python - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 - name: Install tox run: python -m pip install tox - name: Lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 710d3ca8..de3548af 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1 + uses: step-security/harden-runner@0d381219ddf674d61a7572ddd19d7941e271515c # v2.9.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs @@ -22,7 +22,7 @@ jobs: with: ref: ${{ github.event.release.tag_name }} - name: Install Python - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 - name: Install build dependencies run: python -m pip install -U setuptools build wheel - name: Build distributions diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index fb1d6581..54031979 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1 + uses: step-security/harden-runner@0d381219ddf674d61a7572ddd19d7941e271515c # v2.9.0 with: disable-sudo: true egress-policy: block @@ -52,7 +52,7 @@ jobs: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@dc50aa9510b46c811795eb24b2f1ba02a914e534 # v2.3.3 + uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 with: results_file: results.sarif results_format: sarif @@ -64,6 +64,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10 + uses: github/codeql-action/upload-sarif@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 with: sarif_file: results.sarif diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 332d563d..a0bbb3df 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -32,7 +32,7 @@ jobs: runs-on: ${{ matrix.platform }} steps: - name: Harden Runner - uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1 + uses: step-security/harden-runner@0d381219ddf674d61a7572ddd19d7941e271515c # v2.9.0 with: disable-sudo: true egress-policy: block @@ -47,7 +47,7 @@ jobs: - name: Checkout uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Use Python ${{ matrix.python }} - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 with: python-version: ${{ matrix.python }} - name: Install tox From 6bc8e5a760c74307cac7735580999629bbd42c03 Mon Sep 17 00:00:00 2001 From: Jeremy Fehr <117788025+jrmfg@users.noreply.github.com> Date: Fri, 26 Jul 2024 16:52:42 -0700 Subject: [PATCH 056/108] chore: remove personal local testing folder (#340) --- playground/main.py | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 playground/main.py diff --git a/playground/main.py b/playground/main.py deleted file mode 100644 index b974ddbc..00000000 --- a/playground/main.py +++ /dev/null @@ -1,16 +0,0 @@ -import logging -import time - -import functions_framework - -logger = logging.getLogger(__name__) - - -@functions_framework.http -def main(request): - timeout = 2 - for _ in range(timeout * 10): - time.sleep(0.1) - logger.info("logging message after timeout elapsed") - return "Hello, world!" - From 7196e9f5b862c76a21cfe851332cd387c6e6a0ed Mon Sep 17 00:00:00 2001 From: Jeremy Fehr <117788025+jrmfg@users.noreply.github.com> Date: Wed, 18 Sep 2024 09:02:23 -0700 Subject: [PATCH 057/108] chore: Update blunderbuss.yml (#344) --- .github/blunderbuss.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/blunderbuss.yml b/.github/blunderbuss.yml index 8b137891..e913bef1 100644 --- a/.github/blunderbuss.yml +++ b/.github/blunderbuss.yml @@ -1 +1,9 @@ +assign_prs: + - HKwinterhalter + - nifflets + - liuyunnnn +assign_issues: + - HKwinterhalter + - nifflets + - liuyunnnn From b01ea7d396f0faab69ccae6a3f7c307ab31a4e4c Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 19 Sep 2024 22:39:00 +0200 Subject: [PATCH 058/108] chore(deps): update all non-major dependencies (#343) --- .github/workflows/codeql.yml | 8 ++++---- .github/workflows/conformance.yml | 4 ++-- .github/workflows/dependency-review.yml | 2 +- .github/workflows/lint.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- .github/workflows/scorecard.yml | 4 ++-- .github/workflows/unit.yml | 4 ++-- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 12fc1e5d..6b1d2ac3 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@0d381219ddf674d61a7572ddd19d7941e271515c # v2.9.0 + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: disable-sudo: true egress-policy: block @@ -57,7 +57,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 + uses: github/codeql-action/init@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -67,7 +67,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 + uses: github/codeql-action/autobuild@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -80,6 +80,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 + uses: github/codeql-action/analyze@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 6b345d29..f2d5fcdf 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -16,7 +16,7 @@ jobs: python: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] steps: - name: Harden Runner - uses: step-security/harden-runner@0d381219ddf674d61a7572ddd19d7941e271515c # v2.9.0 + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: disable-sudo: true egress-policy: block @@ -33,7 +33,7 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup Python - uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 + uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 with: python-version: ${{ matrix.python }} diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index e85dd0e7..fb1a582f 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@0d381219ddf674d61a7572ddd19d7941e271515c # v2.9.0 + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: disable-sudo: true egress-policy: block diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6e36e9f2..cb570fc7 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@0d381219ddf674d61a7572ddd19d7941e271515c # v2.9.0 + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: disable-sudo: true egress-policy: block @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup Python - uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 + uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 - name: Install tox run: python -m pip install tox - name: Lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index de3548af..87e51a08 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@0d381219ddf674d61a7572ddd19d7941e271515c # v2.9.0 + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs @@ -22,7 +22,7 @@ jobs: with: ref: ${{ github.event.release.tag_name }} - name: Install Python - uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 + uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 - name: Install build dependencies run: python -m pip install -U setuptools build wheel - name: Build distributions diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 54031979..a25d4bf0 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@0d381219ddf674d61a7572ddd19d7941e271515c # v2.9.0 + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: disable-sudo: true egress-policy: block @@ -64,6 +64,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 + uses: github/codeql-action/upload-sarif@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 with: sarif_file: results.sarif diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index a0bbb3df..27c9a1e1 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -32,7 +32,7 @@ jobs: runs-on: ${{ matrix.platform }} steps: - name: Harden Runner - uses: step-security/harden-runner@0d381219ddf674d61a7572ddd19d7941e271515c # v2.9.0 + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: disable-sudo: true egress-policy: block @@ -47,7 +47,7 @@ jobs: - name: Checkout uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Use Python ${{ matrix.python }} - uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 + uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 with: python-version: ${{ matrix.python }} - name: Install tox From 2b51b1b5af0b6dad8b50c6292ca42379721e1840 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 24 Sep 2024 19:10:02 +0200 Subject: [PATCH 059/108] chore(deps): update pypa/gh-action-pypi-publish digest to 897895f (#338) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 87e51a08..5bcee5e6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: - name: Build distributions run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@ec4db0b4ddc65acdf4bff5fa45ac92d78b56bdf0 # main + uses: pypa/gh-action-pypi-publish@897895f1e160c830e369f9779632ebc134688e1b # main with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} From 578894b9e60593e06c2ba4ca19f4571906ea8163 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 2 Oct 2024 01:05:54 +0200 Subject: [PATCH 060/108] chore(deps): update pypa/gh-action-pypi-publish digest to f760068 (#347) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5bcee5e6..93806d9a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: - name: Build distributions run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@897895f1e160c830e369f9779632ebc134688e1b # main + uses: pypa/gh-action-pypi-publish@f7600683efdcb7656dec5b29656edb7bc586e597 # main with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} From 35204161e9d98e23dc220517fcf5b3c0c687914f Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 4 Oct 2024 01:15:52 +0200 Subject: [PATCH 061/108] chore(deps): update all non-major dependencies (#348) --- .github/workflows/codeql.yml | 8 ++++---- .github/workflows/conformance.yml | 2 +- .github/workflows/dependency-review.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/scorecard.yml | 4 ++-- .github/workflows/unit.yml | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6b1d2ac3..cff560f1 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,11 +53,11 @@ jobs: objects.githubusercontent.com:443 - name: Checkout repository - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 + uses: github/codeql-action/init@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -67,7 +67,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 + uses: github/codeql-action/autobuild@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -80,6 +80,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 + uses: github/codeql-action/analyze@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index f2d5fcdf..0d86d788 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -30,7 +30,7 @@ jobs: storage.googleapis.com:443 - name: Checkout code - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Setup Python uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index fb1a582f..ee09082a 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -25,6 +25,6 @@ jobs: api.github.com:443 github.com:443 - name: 'Checkout Repository' - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: 'Dependency Review' uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index cb570fc7..718a5ebc 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,7 +21,7 @@ jobs: github.com:443 pypi.org:443 - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Setup Python uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 - name: Install tox diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 93806d9a..69165b1b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: ref: ${{ github.event.release.tag_name }} - name: Install Python diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index a25d4bf0..e6da4669 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -47,7 +47,7 @@ jobs: - name: "Checkout code" - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: persist-credentials: false @@ -64,6 +64,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 + uses: github/codeql-action/upload-sarif@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 with: sarif_file: results.sarif diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 27c9a1e1..463340a5 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -45,7 +45,7 @@ jobs: registry-1.docker.io:443 - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Use Python ${{ matrix.python }} uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 with: From 57c8701a7e266bea3ebb7c3825b2b13f1e8576ac Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 7 Nov 2024 01:22:58 +0100 Subject: [PATCH 062/108] chore(deps): update pypa/gh-action-pypi-publish digest to 1f5d4ec (#351) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 69165b1b..9b5609a5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: - name: Build distributions run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@f7600683efdcb7656dec5b29656edb7bc586e597 # main + uses: pypa/gh-action-pypi-publish@1f5d4ec244f65dce93685ee3e98e77123f090866 # main with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} From d5ac3d8d01fdb71f7454a0433e586f1eb4a0f6fe Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Thu, 14 Nov 2024 08:32:17 +1100 Subject: [PATCH 063/108] fix: remove unused import (#349) --- src/functions_framework/_function_registry.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/functions_framework/_function_registry.py b/src/functions_framework/_function_registry.py index f266ee82..2214b5fd 100644 --- a/src/functions_framework/_function_registry.py +++ b/src/functions_framework/_function_registry.py @@ -16,7 +16,6 @@ import sys import types -from re import T from typing import Type from functions_framework.exceptions import ( From 0279c8daf526fc01e63cbc6381403a19fb9b4781 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:40:29 -0800 Subject: [PATCH 064/108] chore(main): release 3.8.2 (#353) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 515ce06e..e2438f81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.8.2](https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v3.8.1...v3.8.2) (2024-11-13) + + +### Bug Fixes + +* remove unused import ([#349](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/349)) ([d5ac3d8](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/d5ac3d8d01fdb71f7454a0433e586f1eb4a0f6fe)) + ## [3.8.1](https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v3.8.0...v3.8.1) (2024-07-26) diff --git a/setup.py b/setup.py index e89e6296..14f0b106 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( name="functions-framework", - version="3.8.1", + version="3.8.2", description="An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team.", long_description=long_description, long_description_content_type="text/markdown", From 6a1dfbc20b66809d6d5f2e467fcdd3849756b0e7 Mon Sep 17 00:00:00 2001 From: James Ma Date: Tue, 3 Dec 2024 09:23:38 -0800 Subject: [PATCH 065/108] Update README.md (#355) --- README.md | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index a234586c..d564ad92 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,8 @@ Python functions -- brought to you by the Google Cloud Functions team. The Functions Framework lets you write lightweight functions that run in many different environments, including: -* [Google Cloud Functions](https://cloud.google.com/functions/) +* [Google Cloud Run Functions](https://cloud.google.com/functions/) * Your local development machine -* [Cloud Run and Cloud Run for Anthos](https://cloud.google.com/run/) * [Knative](https://github.com/knative/)-based environments The framework allows you to go from: @@ -294,7 +293,7 @@ https://cloud.google.com/functions/docs/tutorials/pubsub#functions_helloworld_pu ## Run your function on serverless platforms -### Google Cloud Functions +### Google Cloud Run functions This Functions Framework is based on the [Python Runtime on Google Cloud Functions](https://cloud.google.com/functions/docs/concepts/python-runtime). @@ -302,12 +301,6 @@ On Cloud Functions, using the Functions Framework is not necessary: you don't ne After you've written your function, you can simply deploy it from your local machine using the `gcloud` command-line tool. [Check out the Cloud Functions quickstart](https://cloud.google.com/functions/docs/quickstart). -### Cloud Run/Cloud Run on GKE - -Once you've written your function and added the Functions Framework to your `requirements.txt` file, all that's left is to create a container image. [Check out the Cloud Run quickstart](https://cloud.google.com/run/docs/quickstarts/build-and-deploy) for Python to create a container image and deploy it to Cloud Run. You'll write a `Dockerfile` when you build your container. This `Dockerfile` allows you to specify exactly what goes into your container (including custom binaries, a specific operating system, and more). [Here is an example `Dockerfile` that calls Functions Framework.](https://github.com/GoogleCloudPlatform/functions-framework-python/blob/main/examples/cloud_run_http) - -If you want even more control over the environment, you can [deploy your container image to Cloud Run on GKE](https://cloud.google.com/run/docs/quickstarts/prebuilt-deploy-gke). With Cloud Run on GKE, you can run your function on a GKE cluster, which gives you additional control over the environment (including use of GPU-based instances, longer timeouts and more). - ### Container environments based on Knative Cloud Run and Cloud Run on GKE both implement the [Knative Serving API](https://www.knative.dev/docs/). The Functions Framework is designed to be compatible with Knative environments. Just build and deploy your container to a Knative environment. @@ -325,10 +318,10 @@ You can configure the Functions Framework using command-line flags or environmen | `--source` | `FUNCTION_SOURCE` | The path to the file containing your function. Default: `main.py` (in the current working directory) | | `--debug` | `DEBUG` | A flag that allows to run functions-framework to run in debug mode, including live reloading. Default: `False` | -## Enable Google Cloud Function Events +## Enable Google Cloud Run function Events The Functions Framework can unmarshall incoming -Google Cloud Functions [event](https://cloud.google.com/functions/docs/concepts/events-triggers#events) payloads to `event` and `context` objects. +Google Cloud Run functions [event](https://cloud.google.com/functions/docs/concepts/events-triggers#events) payloads to `event` and `context` objects. These will be passed as arguments to your function when it receives a request. Note that your function must use the `event`-style function signature: From 0b521ab15328f8aa96ff70e706f06223d134c62d Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 18 Dec 2024 20:01:27 +0100 Subject: [PATCH 066/108] chore(deps): update pypa/gh-action-pypi-publish digest to 916e576 (#354) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9b5609a5..815c6308 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: - name: Build distributions run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@1f5d4ec244f65dce93685ee3e98e77123f090866 # main + uses: pypa/gh-action-pypi-publish@916e57631f04a497e4bec0e29e80684e45b4305e # main with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} From 1d389748f924ee022cba995edced0a1a920c0d04 Mon Sep 17 00:00:00 2001 From: HKWinterhalter Date: Mon, 17 Mar 2025 15:53:13 -0700 Subject: [PATCH 067/108] chore: remove all assignees from blunderbuss.yml (#362) * chore: remove all assignees from blunderbuss.yml --- .github/blunderbuss.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/blunderbuss.yml b/.github/blunderbuss.yml index e913bef1..8b137891 100644 --- a/.github/blunderbuss.yml +++ b/.github/blunderbuss.yml @@ -1,9 +1 @@ -assign_prs: - - HKwinterhalter - - nifflets - - liuyunnnn -assign_issues: - - HKwinterhalter - - nifflets - - liuyunnnn From c6eab2fecb913d5bab1d5dbd6ba2e34b7d6cf9b9 Mon Sep 17 00:00:00 2001 From: nifflets <5343516+nifflets@users.noreply.github.com> Date: Wed, 19 Mar 2025 11:24:18 -0700 Subject: [PATCH 068/108] fix: Update minimum required version of Flask to 2.0 (#356) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 14f0b106..016c4e3e 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ package_dir={"": "src"}, python_requires=">=3.5, <4", install_requires=[ - "flask>=1.0,<4.0", + "flask>=2.0,<4.0", "click>=7.0,<9.0", "watchdog>=1.0.0", "gunicorn>=22.0.0; platform_system!='Windows'", From 9348c87cf05eae3726d041f26e43db586951cebb Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Tue, 6 May 2025 13:54:32 -0400 Subject: [PATCH 069/108] docs: Add a development guide (#359) --- DEVELOPMENT.md | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 DEVELOPMENT.md diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 00000000..f6966656 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,55 @@ +# Development + +Quickstart for functions-framework maintainers. + +## Environment setup + +Install [tox](https://pypi.org/p/tox), the test runner: + +``` +$ python -m pip install -U tox +``` + +## Running linting + +Linting can be run with: + +``` +$ python -m tox -e lint +``` + +## Running tests + +All tests can be run with: + +``` +$ python -m tox +``` + +Tests for the current Python version can be run with: + +``` +$ python -m tox -e py +``` + +Tests for a specific Python version can be run with: + +``` +$ python -m tox -e py3.12 +``` + +A specific test file (e.g. `tests/test_cli.py`) can be run with: + +``` +$ python -m tox -e py -- tests/test_cli.py +``` + +A specific test in the file (e.g. `test_cli_no_arguements` in `tests/test_cli.py`) can be run with: + +``` +$ python -m tox -e py -- tests/test_cli.py::test_cli_no_arguments +``` + +## Releasing + +Releases are triggered via the [Release Please](https://github.com/apps/release-please) app, which in turn kicks off the [Release to PyPI](https://github.com/GoogleCloudPlatform/functions-framework-python/blob/main/.github/workflows/release.yml) workflow. From c0fa420970d36d57adceaf70bbaea784e427e594 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Tue, 6 May 2025 13:54:49 -0400 Subject: [PATCH 070/108] fix: Update test suite for EOL Python versions (#360) --- .github/workflows/conformance.yml | 10 ++++++++-- .github/workflows/unit.yml | 15 +++++++++++++-- tox.ini | 24 ++++++++++++++++++++++-- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 0d86d788..4e066990 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -10,10 +10,16 @@ permissions: read-all jobs: build: - runs-on: ubuntu-latest strategy: matrix: - python: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] + python: ['3.9', '3.10', '3.11', '3.12'] + platform: [ubuntu-latest] + include: + - platform: ubuntu-22.04 + python: '3.8' + - platform: ubuntu-22.04 + python: '3.7' + runs-on: ${{ matrix.platform }} steps: - name: Harden Runner uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 463340a5..404b5159 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -1,5 +1,5 @@ name: Python Unit CI -on: +on: push: branches: - main @@ -13,8 +13,9 @@ jobs: matrix: python: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] platform: [ubuntu-latest, macos-latest, windows-latest] - # Python <= 3.9 is not available on macos-14 + # Python <= 3.9 is not available on macos-latest # Workaround for https://github.com/actions/setup-python/issues/696 + # Python <= 3.8 is not available on ubuntu-latest exclude: - platform: macos-latest python: '3.9' @@ -22,6 +23,10 @@ jobs: python: '3.8' - platform: macos-latest python: '3.7' + - platform: ubuntu-latest + python: '3.8' + - platform: ubuntu-latest + python: '3.7' include: - platform: macos-latest python: '3.9' @@ -29,6 +34,10 @@ jobs: python: '3.8' - platform: macos-13 python: '3.7' + - platform: ubuntu-22.04 + python: '3.8' + - platform: ubuntu-22.04 + python: '3.7' runs-on: ${{ matrix.platform }} steps: - name: Harden Runner @@ -39,7 +48,9 @@ jobs: allowed-endpoints: > auth.docker.io:443 files.pythonhosted.org:443 + api.github.com:443 github.com:443 + objects.githubusercontent.com:443 production.cloudflare.docker.com:443 pypi.org:443 registry-1.docker.io:443 diff --git a/tox.ini b/tox.ini index 6ba6d3b4..71550e76 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,24 @@ [tox] -envlist = py{35,36,37,38,39,310}-{ubuntu-latest,macos-latest,windows-latest},lint +envlist = + lint + py312-ubuntu-latest + py312-macos-latest + py312-windows-latest + py311-ubuntu-latest + py311-macos-latest + py311-windows-latest + py310-ubuntu-latest + py310-macos-latest + py310-windows-latest + py39-ubuntu-latest + py39-macos-13 + py39-windows-latest + py38-ubuntu-22.04 + py38-macos-13 + py38-windows-latest + py37-ubuntu-22.04 + py37-macos-13 + py37-windows-latest [testenv] usedevelop = true @@ -21,9 +40,10 @@ deps = twine isort mypy + build commands = black --check src tests setup.py conftest.py --exclude tests/test_functions/background_load_error/main.py isort -c src tests setup.py conftest.py mypy tests/test_typing.py - python setup.py --quiet sdist bdist_wheel + python -m build twine check dist/* From 4c44d08c5ebb304e80a3adc3f8e6d150987b29bb Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Wed, 14 May 2025 14:35:46 -0400 Subject: [PATCH 071/108] fix: Switch to `pyproject.toml` based builds (#365) * fix: Switch to `pyproject.toml` based builds * chore: Add newly supported Python versions (3.11 & 3.12) * fix: remove `setup.py` from linting steps * chore: disable Python 3.12 conformance tests --- .../workflows/buildpack-integration-test.yml | 26 +++++++ pyproject.toml | 57 +++++++++++++++ setup.py | 69 ------------------- tox.ini | 4 +- 4 files changed, 85 insertions(+), 71 deletions(-) create mode 100644 pyproject.toml delete mode 100644 setup.py diff --git a/.github/workflows/buildpack-integration-test.yml b/.github/workflows/buildpack-integration-test.yml index 0c9e6eff..234c24ef 100644 --- a/.github/workflows/buildpack-integration-test.yml +++ b/.github/workflows/buildpack-integration-test.yml @@ -58,3 +58,29 @@ jobs: builder-runtime: 'python310' builder-runtime-version: '3.10' start-delay: 5 + python311: + uses: GoogleCloudPlatform/functions-framework-conformance/.github/workflows/buildpack-integration-test.yml@main + with: + http-builder-source: 'tests/conformance' + http-builder-target: 'write_http_declarative' + cloudevent-builder-source: 'tests/conformance' + cloudevent-builder-target: 'write_cloud_event_declarative' + prerun: 'tests/conformance/prerun.sh ${{ github.sha }}' + builder-runtime: 'python311' + builder-runtime-version: '3.11' + start-delay: 5 +# Python 3.12 conformance tests are disabled due to the buildpack defaulting to +# Ubuntu 18.04, which has no Python 3.12 version, and being unable to specify +# the OS/stack via the conformance test configuration +# +# python312: +# uses: GoogleCloudPlatform/functions-framework-conformance/.github/workflows/buildpack-integration-test.yml@main +# with: +# http-builder-source: 'tests/conformance' +# http-builder-target: 'write_http_declarative' +# cloudevent-builder-source: 'tests/conformance' +# cloudevent-builder-target: 'write_cloud_event_declarative' +# prerun: 'tests/conformance/prerun.sh ${{ github.sha }}' +# builder-runtime: 'python312' +# builder-runtime-version: '3.12' +# start-delay: 5 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..5f25ecf8 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,57 @@ +[project] +name = "functions-framework" +version = "3.8.2" +description = "An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team." +readme = "README.md" +requires-python = ">=3.5, <4" +# Once we drop support for Python 3.7 and 3.8, this can become +# license = "Apache-2.0" +license = {text = "Apache-2.0"} +authors = [ + { name = "Google LLC", email = "googleapis-packages@google.com" } +] +maintainers = [ + { name = "Google LLC", email = "googleapis-packages@google.com" } +] +keywords = ["functions-framework"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dependencies = [ + "flask>=2.0,<4.0", + "click>=7.0,<9.0", + "watchdog>=1.0.0", + "gunicorn>=22.0.0; platform_system!='Windows'", + "cloudevents>=1.2.0,<2.0.0", + "Werkzeug>=0.14,<4.0.0", +] + +[project.urls] +Homepage = "https://github.com/googlecloudplatform/functions-framework-python" + +[project.scripts] +ff = "functions_framework._cli:_cli" +functions-framework = "functions_framework._cli:_cli" +functions_framework = "functions_framework._cli:_cli" +functions-framework-python = "functions_framework._cli:_cli" +functions_framework_python = "functions_framework._cli:_cli" + +[build-system] +requires = ["setuptools>=61.0.0"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +functions_framework = ["py.typed"] + +[tool.setuptools.package-dir] +"" = "src" diff --git a/setup.py b/setup.py deleted file mode 100644 index 016c4e3e..00000000 --- a/setup.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from io import open -from os import path - -from setuptools import find_packages, setup - -here = path.abspath(path.dirname(__file__)) - -# Get the long description from the README file -with open(path.join(here, "README.md"), encoding="utf-8") as f: - long_description = f.read() - -setup( - name="functions-framework", - version="3.8.2", - description="An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team.", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/googlecloudplatform/functions-framework-python", - author="Google LLC", - author_email="googleapis-packages@google.com", - classifiers=[ - "Development Status :: 5 - Production/Stable ", - "Intended Audience :: Developers", - "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - ], - keywords="functions-framework", - packages=find_packages(where="src"), - package_data={"functions_framework": ["py.typed"]}, - namespace_packages=["google", "google.cloud"], - package_dir={"": "src"}, - python_requires=">=3.5, <4", - install_requires=[ - "flask>=2.0,<4.0", - "click>=7.0,<9.0", - "watchdog>=1.0.0", - "gunicorn>=22.0.0; platform_system!='Windows'", - "cloudevents>=1.2.0,<2.0.0", - "Werkzeug>=0.14,<4.0.0", - ], - entry_points={ - "console_scripts": [ - "ff=functions_framework._cli:_cli", - "functions-framework=functions_framework._cli:_cli", - "functions_framework=functions_framework._cli:_cli", - "functions-framework-python=functions_framework._cli:_cli", - "functions_framework_python=functions_framework._cli:_cli", - ] - }, -) diff --git a/tox.ini b/tox.ini index 71550e76..1599608c 100644 --- a/tox.ini +++ b/tox.ini @@ -42,8 +42,8 @@ deps = mypy build commands = - black --check src tests setup.py conftest.py --exclude tests/test_functions/background_load_error/main.py - isort -c src tests setup.py conftest.py + black --check src tests conftest.py --exclude tests/test_functions/background_load_error/main.py + isort -c src tests conftest.py mypy tests/test_typing.py python -m build twine check dist/* From f57a0163d0b20eac4a77d5776262a3942780df9a Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Fri, 16 May 2025 11:26:56 -0700 Subject: [PATCH 072/108] chore(main): release 3.8.3 (#363) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 14 ++++++++++++++ pyproject.toml | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2438f81..8231717d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.8.3](https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v3.8.2...v3.8.3) (2025-05-14) + + +### Bug Fixes + +* Switch to `pyproject.toml` based builds ([#365](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/365)) ([4c44d08](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/4c44d08c5ebb304e80a3adc3f8e6d150987b29bb)) +* Update minimum required version of Flask to 2.0 ([#356](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/356)) ([c6eab2f](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/c6eab2fecb913d5bab1d5dbd6ba2e34b7d6cf9b9)) +* Update test suite for EOL Python versions ([#360](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/360)) ([c0fa420](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/c0fa420970d36d57adceaf70bbaea784e427e594)) + + +### Documentation + +* Add a development guide ([#359](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/359)) ([9348c87](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/9348c87cf05eae3726d041f26e43db586951cebb)) + ## [3.8.2](https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v3.8.1...v3.8.2) (2024-11-13) diff --git a/pyproject.toml b/pyproject.toml index 5f25ecf8..c160562f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "functions-framework" -version = "3.8.2" +version = "3.8.3" description = "An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team." readme = "README.md" requires-python = ">=3.5, <4" From 3fdeaa0dd799553f92ebcef919e8755763a166c5 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Sat, 17 May 2025 00:39:34 +0200 Subject: [PATCH 073/108] chore(deps): update pypa/gh-action-pypi-publish digest to db8f07d (#358) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 815c6308..443c7ee8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: - name: Build distributions run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@916e57631f04a497e4bec0e29e80684e45b4305e # main + uses: pypa/gh-action-pypi-publish@db8f07d3871a0a180efa06b95d467625c19d5d5f # main with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} From 0ab5f03f5f47f96903197c4e5419f7ac1b238f3c Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Sat, 17 May 2025 00:45:23 +0200 Subject: [PATCH 074/108] chore(deps): update all non-major dependencies (#352) --- .github/workflows/codeql.yml | 10 +++++----- .github/workflows/conformance.yml | 10 +++++----- .github/workflows/dependency-review.yml | 6 +++--- .github/workflows/lint.yml | 6 +++--- .github/workflows/release.yml | 6 +++--- .github/workflows/scorecard.yml | 8 ++++---- .github/workflows/unit.yml | 6 +++--- 7 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index cff560f1..46f6a4d1 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 with: disable-sudo: true egress-policy: block @@ -53,11 +53,11 @@ jobs: objects.githubusercontent.com:443 - name: Checkout repository - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 + uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -67,7 +67,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 + uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -80,6 +80,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 + uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 4e066990..9dde6036 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -22,7 +22,7 @@ jobs: runs-on: ${{ matrix.platform }} steps: - name: Harden Runner - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 with: disable-sudo: true egress-policy: block @@ -36,10 +36,10 @@ jobs: storage.googleapis.com:443 - name: Checkout code - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Python - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ matrix.python }} @@ -47,9 +47,9 @@ jobs: run: python -m pip install -e . - name: Setup Go - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: - go-version: '1.20' + go-version: '1.24' - name: Run HTTP conformance tests uses: GoogleCloudPlatform/functions-framework-conformance/action@72a4f36b10f1c6435ab1a86a9ea24bda464cc262 # v1.8.6 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index ee09082a..5baf3aa0 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 with: disable-sudo: true egress-policy: block @@ -25,6 +25,6 @@ jobs: api.github.com:443 github.com:443 - name: 'Checkout Repository' - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: 'Dependency Review' - uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4 + uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 718a5ebc..4fa8dff2 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 with: disable-sudo: true egress-policy: block @@ -21,9 +21,9 @@ jobs: github.com:443 pypi.org:443 - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Python - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - name: Install tox run: python -m pip install tox - name: Lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 443c7ee8..269f14c7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,16 +13,16 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - name: Checkout - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.release.tag_name }} - name: Install Python - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - name: Install build dependencies run: python -m pip install -U setuptools build wheel - name: Build distributions diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index e6da4669..72b82523 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 with: disable-sudo: true egress-policy: block @@ -47,12 +47,12 @@ jobs: - name: "Checkout code" - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 + uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 with: results_file: results.sarif results_format: sarif @@ -64,6 +64,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 + uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 with: sarif_file: results.sarif diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 404b5159..e16d4de8 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -41,7 +41,7 @@ jobs: runs-on: ${{ matrix.platform }} steps: - name: Harden Runner - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 with: disable-sudo: true egress-policy: block @@ -56,9 +56,9 @@ jobs: registry-1.docker.io:443 - name: Checkout - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Use Python ${{ matrix.python }} - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ matrix.python }} - name: Install tox From cc2b9b584fcc3daaa2762ae62a3ce1277a488a1c Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 9 Jun 2025 13:24:43 -0700 Subject: [PATCH 075/108] fix: Pin cloudevent sdk version to support python3.7. (#373) * fix: Pin cloudevent sdk version to support python3.7. * Pin cloudevent version on examples. * Pin cloudevent versions. --- examples/cloud_run_cloud_events/requirements.txt | 2 +- examples/cloud_run_decorator/requirements.txt | 1 + examples/cloud_run_event/requirements.txt | 1 + examples/cloud_run_http/requirements.txt | 1 + examples/docker-compose/requirements.txt | 1 + examples/skaffold/requirements.txt | 1 + pyproject.toml | 10 ++++------ 7 files changed, 10 insertions(+), 7 deletions(-) diff --git a/examples/cloud_run_cloud_events/requirements.txt b/examples/cloud_run_cloud_events/requirements.txt index 0a7427c7..43d925f1 100644 --- a/examples/cloud_run_cloud_events/requirements.txt +++ b/examples/cloud_run_cloud_events/requirements.txt @@ -1,3 +1,3 @@ # Optionally include additional dependencies here -cloudevents>=1.2.0 +cloudevents==1.11.0 # Pin version - last version compatible w/ python3.7 requests diff --git a/examples/cloud_run_decorator/requirements.txt b/examples/cloud_run_decorator/requirements.txt index 33c5f99f..3f8c88a5 100644 --- a/examples/cloud_run_decorator/requirements.txt +++ b/examples/cloud_run_decorator/requirements.txt @@ -1 +1,2 @@ # Optionally include additional dependencies here +cloudevents==1.11.0 # Pin version - last version compatible w/ python3.7 diff --git a/examples/cloud_run_event/requirements.txt b/examples/cloud_run_event/requirements.txt index 33c5f99f..3f8c88a5 100644 --- a/examples/cloud_run_event/requirements.txt +++ b/examples/cloud_run_event/requirements.txt @@ -1 +1,2 @@ # Optionally include additional dependencies here +cloudevents==1.11.0 # Pin version - last version compatible w/ python3.7 diff --git a/examples/cloud_run_http/requirements.txt b/examples/cloud_run_http/requirements.txt index 33c5f99f..3f8c88a5 100644 --- a/examples/cloud_run_http/requirements.txt +++ b/examples/cloud_run_http/requirements.txt @@ -1 +1,2 @@ # Optionally include additional dependencies here +cloudevents==1.11.0 # Pin version - last version compatible w/ python3.7 diff --git a/examples/docker-compose/requirements.txt b/examples/docker-compose/requirements.txt index 3601409f..c856b8d8 100644 --- a/examples/docker-compose/requirements.txt +++ b/examples/docker-compose/requirements.txt @@ -1 +1,2 @@ # Add any Python requirements here +cloudevents==1.11.0 # Pin version - last version compatible w/ python3.7 diff --git a/examples/skaffold/requirements.txt b/examples/skaffold/requirements.txt index 3601409f..c856b8d8 100644 --- a/examples/skaffold/requirements.txt +++ b/examples/skaffold/requirements.txt @@ -1 +1,2 @@ # Add any Python requirements here +cloudevents==1.11.0 # Pin version - last version compatible w/ python3.7 diff --git a/pyproject.toml b/pyproject.toml index c160562f..fa001304 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,12 +6,10 @@ readme = "README.md" requires-python = ">=3.5, <4" # Once we drop support for Python 3.7 and 3.8, this can become # license = "Apache-2.0" -license = {text = "Apache-2.0"} -authors = [ - { name = "Google LLC", email = "googleapis-packages@google.com" } -] +license = { text = "Apache-2.0" } +authors = [{ name = "Google LLC", email = "googleapis-packages@google.com" }] maintainers = [ - { name = "Google LLC", email = "googleapis-packages@google.com" } + { name = "Google LLC", email = "googleapis-packages@google.com" }, ] keywords = ["functions-framework"] classifiers = [ @@ -29,7 +27,7 @@ dependencies = [ "click>=7.0,<9.0", "watchdog>=1.0.0", "gunicorn>=22.0.0; platform_system!='Windows'", - "cloudevents>=1.2.0,<2.0.0", + "cloudevents>=1.2.0,<=1.11.0", # Must support python 3.7 "Werkzeug>=0.14,<4.0.0", ] From 37e0bf764ff24ebb82ba18bcac1bee6b03cecb13 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 10 Jun 2025 12:26:22 -0700 Subject: [PATCH 076/108] fix(ci): specify python version in tox environment (#375) * fix(ci): specify python version in tox environment * Enable shell for github workflows. --- .github/workflows/unit.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index e16d4de8..32b34fdb 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -64,4 +64,8 @@ jobs: - name: Install tox run: python -m pip install tox - name: Test - run: python -m tox -e py-${{ matrix.platform }} + shell: bash + run: | + # Remove dots from python version string, i.e. 3.10 -> 310 + PY_VERSION=$(echo "${{ matrix.python }}" | sed 's/\.//g') + python -m tox -e py${PY_VERSION}-${{ matrix.platform }} From 49f698517a06d0e47b2aadc2d603e0b193770440 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 10 Jun 2025 12:44:30 -0700 Subject: [PATCH 077/108] feat: add support for async functions (#364) * feat: Introduce functions_framework.aio submodule that support async function execution. * Remove httpx. * Update pyproject.toml to include extra async package. * Update test deps. * Improve test coverage. * Make linter happy. * Fix test harness to support py37. * Remove version filter in tox file. * Remove dependency-groups in pyproject.toml for now. * Use py3.8 compatible types. * Fix more incompatibility with python38 * Pin cloudevent sdk to python37 compatible version. * Fix more py37 incompatibility. * fix: Prevent test_aio.py collection errors on Python 3.7 Add pytest_ignore_collect hook to skip test_aio.py entirely on Python 3.7 to prevent ImportError during test collection. The previous approach using only pytest_collection_modifyitems was too late in the process - the error occurred when pytest tried to import the module before skip markers could be applied. Both hooks are marked as safe to remove when Python 3.7 support is dropped. * style: Apply black formatting to conftest.py * fix: Use modern pytest collection_path parameter and return None - Replace deprecated 'path' parameter with 'collection_path' in pytest_ignore_collect - Return None instead of False to let pytest use default behavior - This should fix the issue where pytest was collecting tests from .tox/.pkg/ * fix: Skip tests parametrized with None on Python 3.7 Simplify the check to just skip any test parametrized with None value. On Python 3.7, create_asgi_app is always None due to the conditional import, so this catches all async-related parametrized tests. * fix: Replace asyncio.to_thread with Python 3.8 compatible code Use asyncio.get_event_loop().run_in_executor() instead of asyncio.to_thread() for Python 3.8 compatibility. Added TODO comments to switch back when Python 3.8 support is dropped. * fix: Improve async test detection for Python 3.7 - Use a list of async keywords (async, asgi, aio, starlette) - Check for these keywords in test names, file paths, and parameters - This catches more async-related tests including those with "aio" prefix * fix: Handle Flask vs Starlette redirect behavior differences - Remove unnecessary follow_redirects=True from Starlette TestClient - Make test_http_function_request_url_empty_path aware of framework differences - Starlette TestClient normalizes empty path "" to "/" while Flask preserves it - Test now expects appropriate behavior for each framework * fix: Exclude aio module from coverage on Python 3.7 Add special coverage configuration for Python 3.7 that excludes the aio module since it requires Python 3.8+ due to Starlette dependency. This prevents coverage failures on Python 3.7. * fix: Simplify conftest.py. * fix: Use full environment names for py37 coverage exclusion The tox environment names in GitHub Actions include the OS suffix (e.g., py37-ubuntu-22.04), so we need to match the full names. * fix: Explicitly list each py37 environment for coverage exclusion - List py37-ubuntu-22.04 and py37-macos-13 explicitly - Place py37 settings before general windows-latest setting - This should properly exclude aio module from coverage on Python 3.7 * fix: Add Python 3.7 specific coverage configuration - Create .coveragerc-py37 to exclude aio module from coverage on Python 3.7 - Use --cov-config flag to specify this file for py37 environments only - This prevents the aio module exclusion from affecting Python 3.8+ tests --- .coveragerc-py37 | 10 + conftest.py | 50 +++ pyproject.toml | 6 +- setup.py | 72 ++++ src/functions_framework/aio/__init__.py | 250 ++++++++++++++ tests/test_aio.py | 190 +++++++++++ tests/test_cloud_event_functions.py | 71 ++-- tests/test_decorator_functions.py | 87 ++++- tests/test_functions.py | 323 +++++++++++------- .../cloud_events/async_empty_data.py | 38 +++ .../test_functions/cloud_events/async_main.py | 40 +++ .../decorators/async_decorator.py | 98 ++++++ .../http_check_env/async_main.py | 36 ++ .../http_request_check/async_main.py | 40 +++ .../http_streaming/async_main.py | 46 +++ .../test_functions/http_trigger/async_main.py | 48 +++ .../http_trigger_sleep/async_main.py | 33 ++ .../http_with_import/async_main.py | 29 ++ tests/test_typing.py | 12 + tox.ini | 9 + 20 files changed, 1316 insertions(+), 172 deletions(-) create mode 100644 .coveragerc-py37 create mode 100644 setup.py create mode 100644 src/functions_framework/aio/__init__.py create mode 100644 tests/test_aio.py create mode 100644 tests/test_functions/cloud_events/async_empty_data.py create mode 100644 tests/test_functions/cloud_events/async_main.py create mode 100644 tests/test_functions/decorators/async_decorator.py create mode 100644 tests/test_functions/http_check_env/async_main.py create mode 100644 tests/test_functions/http_request_check/async_main.py create mode 100644 tests/test_functions/http_streaming/async_main.py create mode 100644 tests/test_functions/http_trigger/async_main.py create mode 100644 tests/test_functions/http_trigger_sleep/async_main.py create mode 100644 tests/test_functions/http_with_import/async_main.py diff --git a/.coveragerc-py37 b/.coveragerc-py37 new file mode 100644 index 00000000..13be2ea1 --- /dev/null +++ b/.coveragerc-py37 @@ -0,0 +1,10 @@ +[run] +# Coverage configuration specifically for Python 3.7 environments +# Excludes the aio module which requires Python 3.8+ (Starlette dependency) +# This file is only used by py37-* tox environments +omit = + */functions_framework/aio/* + */.tox/* + */tests/* + */venv/* + */.venv/* \ No newline at end of file diff --git a/conftest.py b/conftest.py index 21572fda..f72314ed 100644 --- a/conftest.py +++ b/conftest.py @@ -42,3 +42,53 @@ def isolate_logging(): sys.stderr = sys.__stderr__ logging.shutdown() reload(logging) + + +# Safe to remove when we drop Python 3.7 support +def pytest_ignore_collect(collection_path, config): + """Ignore async test files on Python 3.7 since Starlette requires Python 3.8+""" + if sys.version_info >= (3, 8): + return None + + # Skip test_aio.py entirely on Python 3.7 + if collection_path.name == "test_aio.py": + return True + + return None + + +# Safe to remove when we drop Python 3.7 support +def pytest_collection_modifyitems(config, items): + """Skip async-related tests on Python 3.7 since Starlette requires Python 3.8+""" + if sys.version_info >= (3, 8): + return + + skip_async = pytest.mark.skip( + reason="Async features require Python 3.8+ (Starlette dependency)" + ) + + # Keywords that indicate async-related tests + async_keywords = ["async", "asgi", "aio", "starlette"] + + for item in items: + skip_test = False + + if hasattr(item, "callspec") and hasattr(item.callspec, "params"): + for param_name, param_value in item.callspec.params.items(): + # Check if test has fixtures with async-related parameters + if isinstance(param_value, str) and any( + keyword in param_value.lower() for keyword in async_keywords + ): + skip_test = True + break + # Skip tests parametrized with None (create_asgi_app on Python 3.7) + if param_value is None: + skip_test = True + break + + # Skip tests that explicitly test async functionality + if any(keyword in item.name.lower() for keyword in async_keywords): + skip_test = True + + if skip_test: + item.add_marker(skip_async) diff --git a/pyproject.toml b/pyproject.toml index fa001304..3a631b5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "functions-framework" version = "3.8.3" description = "An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team." readme = "README.md" -requires-python = ">=3.5, <4" +requires-python = ">=3.7, <4" # Once we drop support for Python 3.7 and 3.8, this can become # license = "Apache-2.0" license = { text = "Apache-2.0" } @@ -29,11 +29,15 @@ dependencies = [ "gunicorn>=22.0.0; platform_system!='Windows'", "cloudevents>=1.2.0,<=1.11.0", # Must support python 3.7 "Werkzeug>=0.14,<4.0.0", + "httpx>=0.24.1", ] [project.urls] Homepage = "https://github.com/googlecloudplatform/functions-framework-python" +[project.optional-dependencies] +async = ["starlette>=0.37.0,<1.0.0; python_version>='3.8'"] + [project.scripts] ff = "functions_framework._cli:_cli" functions-framework = "functions_framework._cli:_cli" diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..1c35d39b --- /dev/null +++ b/setup.py @@ -0,0 +1,72 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from io import open +from os import path + +from setuptools import find_packages, setup + +here = path.abspath(path.dirname(__file__)) + +# Get the long description from the README file +with open(path.join(here, "README.md"), encoding="utf-8") as f: + long_description = f.read() + +setup( + name="functions-framework", + version="3.8.2", + description="An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team.", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/googlecloudplatform/functions-framework-python", + author="Google LLC", + author_email="googleapis-packages@google.com", + classifiers=[ + "Development Status :: 5 - Production/Stable ", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + ], + keywords="functions-framework", + packages=find_packages(where="src"), + package_data={"functions_framework": ["py.typed"]}, + namespace_packages=["google", "google.cloud"], + package_dir={"": "src"}, + python_requires=">=3.5, <4", + install_requires=[ + "flask>=1.0,<4.0", + "click>=7.0,<9.0", + "watchdog>=1.0.0", + "gunicorn>=22.0.0; platform_system!='Windows'", + "cloudevents>=1.2.0,<2.0.0", + "Werkzeug>=0.14,<4.0.0", + ], + extras_require={ + "async": ["starlette>=0.37.0,<1.0.0"], + }, + entry_points={ + "console_scripts": [ + "ff=functions_framework._cli:_cli", + "functions-framework=functions_framework._cli:_cli", + "functions_framework=functions_framework._cli:_cli", + "functions-framework-python=functions_framework._cli:_cli", + "functions_framework_python=functions_framework._cli:_cli", + ] + }, +) diff --git a/src/functions_framework/aio/__init__.py b/src/functions_framework/aio/__init__.py new file mode 100644 index 00000000..832d6818 --- /dev/null +++ b/src/functions_framework/aio/__init__.py @@ -0,0 +1,250 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import functools +import inspect +import os + +from typing import Any, Awaitable, Callable, Dict, Tuple, Union + +from cloudevents.http import from_http +from cloudevents.http.event import CloudEvent + +from functions_framework import _function_registry +from functions_framework.exceptions import ( + FunctionsFrameworkException, + MissingSourceException, +) + +try: + from starlette.applications import Starlette + from starlette.exceptions import HTTPException + from starlette.requests import Request + from starlette.responses import JSONResponse, Response + from starlette.routing import Route +except ImportError: + raise FunctionsFrameworkException( + "Starlette is not installed. Install the framework with the 'async' extra: " + "pip install functions-framework[async]" + ) + +HTTPResponse = Union[ + Response, # Functions can return a full Starlette Response object + str, # Str returns are wrapped in Response(result) + Dict[Any, Any], # Dict returns are wrapped in JSONResponse(result) + Tuple[Any, int], # Flask-style (content, status_code) supported + None, # None raises HTTPException +] + +_FUNCTION_STATUS_HEADER_FIELD = "X-Google-Status" +_CRASH = "crash" + +CloudEventFunction = Callable[[CloudEvent], Union[None, Awaitable[None]]] +HTTPFunction = Callable[[Request], Union[HTTPResponse, Awaitable[HTTPResponse]]] + + +def cloud_event(func: CloudEventFunction) -> CloudEventFunction: + """Decorator that registers cloudevent as user function signature type.""" + _function_registry.REGISTRY_MAP[func.__name__] = ( + _function_registry.CLOUDEVENT_SIGNATURE_TYPE + ) + if inspect.iscoroutinefunction(func): + + @functools.wraps(func) + async def async_wrapper(*args, **kwargs): + return await func(*args, **kwargs) + + return async_wrapper + + @functools.wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + return wrapper + + +def http(func: HTTPFunction) -> HTTPFunction: + """Decorator that registers http as user function signature type.""" + _function_registry.REGISTRY_MAP[func.__name__] = ( + _function_registry.HTTP_SIGNATURE_TYPE + ) + + if inspect.iscoroutinefunction(func): + + @functools.wraps(func) + async def async_wrapper(*args, **kwargs): + return await func(*args, **kwargs) + + return async_wrapper + + @functools.wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + return wrapper + + +async def _crash_handler(request, exc): + headers = {_FUNCTION_STATUS_HEADER_FIELD: _CRASH} + return Response(f"Internal Server Error: {exc}", status_code=500, headers=headers) + + +def _http_func_wrapper(function, is_async): + @functools.wraps(function) + async def handler(request): + if is_async: + result = await function(request) + else: + # TODO: Use asyncio.to_thread when we drop Python 3.8 support + # Python 3.8 compatible version of asyncio.to_thread + loop = asyncio.get_event_loop() + result = await loop.run_in_executor(None, function, request) + if isinstance(result, str): + return Response(result) + elif isinstance(result, dict): + return JSONResponse(result) + elif isinstance(result, tuple) and len(result) == 2: + # Support Flask-style tuple response + content, status_code = result + return Response(content, status_code=status_code) + elif result is None: + raise HTTPException(status_code=500, detail="No response returned") + else: + return result + + return handler + + +def _cloudevent_func_wrapper(function, is_async): + @functools.wraps(function) + async def handler(request): + data = await request.body() + + try: + event = from_http(request.headers, data) + except Exception as e: + raise HTTPException( + 400, detail=f"Bad Request: Got CloudEvent exception: {repr(e)}" + ) + if is_async: + await function(event) + else: + # TODO: Use asyncio.to_thread when we drop Python 3.8 support + # Python 3.8 compatible version of asyncio.to_thread + loop = asyncio.get_event_loop() + await loop.run_in_executor(None, function, event) + return Response("OK") + + return handler + + +async def _handle_not_found(request: Request): + raise HTTPException(status_code=404, detail="Not Found") + + +def create_asgi_app(target=None, source=None, signature_type=None): + """Create an ASGI application for the function. + + Args: + target: The name of the target function to invoke + source: The source file containing the function + signature_type: The signature type of the function + ('http', 'event', 'cloudevent', or 'typed') + + Returns: + A Starlette ASGI application instance + """ + target = _function_registry.get_function_target(target) + source = _function_registry.get_function_source(source) + + if not os.path.exists(source): + raise MissingSourceException( + f"File {source} that is expected to define function doesn't exist" + ) + + source_module, spec = _function_registry.load_function_module(source) + spec.loader.exec_module(source_module) + function = _function_registry.get_user_function(source, source_module, target) + signature_type = _function_registry.get_func_signature_type(target, signature_type) + + is_async = inspect.iscoroutinefunction(function) + routes = [] + if signature_type == _function_registry.HTTP_SIGNATURE_TYPE: + http_handler = _http_func_wrapper(function, is_async) + routes.append( + Route( + "/", + endpoint=http_handler, + methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH"], + ), + ) + routes.append(Route("/robots.txt", endpoint=_handle_not_found, methods=["GET"])) + routes.append( + Route("/favicon.ico", endpoint=_handle_not_found, methods=["GET"]) + ) + routes.append( + Route( + "/{path:path}", + http_handler, + methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH"], + ) + ) + elif signature_type == _function_registry.CLOUDEVENT_SIGNATURE_TYPE: + cloudevent_handler = _cloudevent_func_wrapper(function, is_async) + routes.append(Route("/{path:path}", cloudevent_handler, methods=["POST"])) + routes.append(Route("/", cloudevent_handler, methods=["POST"])) + elif signature_type == _function_registry.TYPED_SIGNATURE_TYPE: + raise FunctionsFrameworkException( + f"ASGI server does not support typed events (signature type: '{signature_type}'). " + ) + elif signature_type == _function_registry.BACKGROUNDEVENT_SIGNATURE_TYPE: + raise FunctionsFrameworkException( + f"ASGI server does not support legacy background events (signature type: '{signature_type}'). " + "Use 'cloudevent' signature type instead." + ) + else: + raise FunctionsFrameworkException( + f"Unsupported signature type for ASGI server: {signature_type}" + ) + + exception_handlers = { + 500: _crash_handler, + } + app = Starlette(routes=routes, exception_handlers=exception_handlers) + return app + + +class LazyASGIApp: + """ + Wrap the ASGI app in a lazily initialized wrapper to prevent initialization + at import-time + """ + + def __init__(self, target=None, source=None, signature_type=None): + self.target = target + self.source = source + self.signature_type = signature_type + + self.app = None + self._app_initialized = False + + async def __call__(self, scope, receive, send): + if not self._app_initialized: + self.app = create_asgi_app(self.target, self.source, self.signature_type) + self._app_initialized = True + await self.app(scope, receive, send) + + +app = LazyASGIApp() diff --git a/tests/test_aio.py b/tests/test_aio.py new file mode 100644 index 00000000..cf69479a --- /dev/null +++ b/tests/test_aio.py @@ -0,0 +1,190 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pathlib +import re +import sys +import tempfile + +from unittest.mock import Mock, call + +if sys.version_info >= (3, 8): + from unittest.mock import AsyncMock + +import pytest + +from functions_framework import exceptions +from functions_framework.aio import ( + LazyASGIApp, + _cloudevent_func_wrapper, + _http_func_wrapper, + create_asgi_app, +) + +TEST_FUNCTIONS_DIR = pathlib.Path(__file__).resolve().parent / "test_functions" + + +def test_import_error_without_starlette(monkeypatch): + import builtins + + original_import = builtins.__import__ + + def mock_import(name, *args, **kwargs): + if name.startswith("starlette"): + raise ImportError(f"No module named '{name}'") + return original_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", mock_import) + + # Remove the module from sys.modules to force re-import + if "functions_framework.aio" in sys.modules: + del sys.modules["functions_framework.aio"] + + with pytest.raises(exceptions.FunctionsFrameworkException) as excinfo: + import functions_framework.aio + + assert "Starlette is not installed" in str(excinfo.value) + assert "pip install functions-framework[async]" in str(excinfo.value) + + +def test_invalid_function_definition_missing_function_file(): + source = TEST_FUNCTIONS_DIR / "missing_function_file" / "main.py" + target = "function" + + with pytest.raises(exceptions.MissingSourceException) as excinfo: + create_asgi_app(target, source) + + assert re.match( + r"File .* that is expected to define function doesn't exist", str(excinfo.value) + ) + + +def test_asgi_typed_signature_not_supported(): + source = TEST_FUNCTIONS_DIR / "typed_events" / "typed_event.py" + target = "function_typed" + + with pytest.raises(exceptions.FunctionsFrameworkException) as excinfo: + create_asgi_app(target, source, "typed") + + assert "ASGI server does not support typed events (signature type: 'typed')" in str( + excinfo.value + ) + + +def test_asgi_background_event_not_supported(): + source = TEST_FUNCTIONS_DIR / "background_trigger" / "main.py" + target = "function" + + with pytest.raises(exceptions.FunctionsFrameworkException) as excinfo: + create_asgi_app(target, source, "event") + + assert ( + "ASGI server does not support legacy background events (signature type: 'event')" + in str(excinfo.value) + ) + assert "Use 'cloudevent' signature type instead" in str(excinfo.value) + + +@pytest.mark.asyncio +async def test_lazy_asgi_app(monkeypatch): + actual_app = AsyncMock() + create_asgi_app_mock = Mock(return_value=actual_app) + monkeypatch.setattr("functions_framework.aio.create_asgi_app", create_asgi_app_mock) + + # Test that it's lazy + target, source, signature_type = "func", "source.py", "http" + lazy_app = LazyASGIApp(target, source, signature_type) + + assert lazy_app.app is None + assert lazy_app._app_initialized is False + + # Mock ASGI call parameters + scope = {"type": "http", "method": "GET", "path": "/"} + receive = AsyncMock() + send = AsyncMock() + + # Test that it's initialized when called + await lazy_app(scope, receive, send) + + assert lazy_app.app is actual_app + assert lazy_app._app_initialized is True + assert create_asgi_app_mock.call_count == 1 + assert create_asgi_app_mock.call_args == call(target, source, signature_type) + + # Verify the app was called + actual_app.assert_called_once_with(scope, receive, send) + + # Test that subsequent calls use the same app + create_asgi_app_mock.reset_mock() + actual_app.reset_mock() + + await lazy_app(scope, receive, send) + + assert create_asgi_app_mock.call_count == 0 # Should not create app again + actual_app.assert_called_once_with(scope, receive, send) # Should be called again + + +@pytest.mark.asyncio +async def test_http_func_wrapper_json_response(): + async def http_func(request): + return {"message": "hello", "count": 42} + + wrapper = _http_func_wrapper(http_func, is_async=True) + + request = Mock() + response = await wrapper(request) + + assert response.__class__.__name__ == "JSONResponse" + assert b'"message":"hello"' in response.body + assert b'"count":42' in response.body + + +@pytest.mark.asyncio +async def test_http_func_wrapper_sync_function(): + def sync_http_func(request): + return "sync response" + + wrapper = _http_func_wrapper(sync_http_func, is_async=False) + + request = Mock() + response = await wrapper(request) + + assert response.__class__.__name__ == "Response" + assert response.body == b"sync response" + + +@pytest.mark.asyncio +async def test_cloudevent_func_wrapper_sync_function(): + called_with_event = None + + def sync_cloud_event(event): + nonlocal called_with_event + called_with_event = event + + wrapper = _cloudevent_func_wrapper(sync_cloud_event, is_async=False) + + request = Mock() + request.body = AsyncMock( + return_value=b'{"specversion": "1.0", "type": "test.event", "source": "test-source", "id": "123", "data": {"test": "data"}}' + ) + request.headers = {"content-type": "application/cloudevents+json"} + + response = await wrapper(request) + + assert response.body == b"OK" + assert response.status_code == 200 + + assert called_with_event is not None + assert called_with_event["type"] == "test.event" + assert called_with_event["source"] == "test-source" diff --git a/tests/test_cloud_event_functions.py b/tests/test_cloud_event_functions.py index 691fe388..2e7c281d 100644 --- a/tests/test_cloud_event_functions.py +++ b/tests/test_cloud_event_functions.py @@ -13,13 +13,25 @@ # limitations under the License. import json import pathlib +import sys import pytest -from cloudevents.http import CloudEvent, to_binary, to_structured +from cloudevents import conversion as ce_conversion +from cloudevents.http import CloudEvent + +if sys.version_info >= (3, 8): + from starlette.testclient import TestClient as StarletteTestClient +else: + StarletteTestClient = None from functions_framework import create_app +if sys.version_info >= (3, 8): + from functions_framework.aio import create_asgi_app +else: + create_asgi_app = None + TEST_FUNCTIONS_DIR = pathlib.Path(__file__).resolve().parent / "test_functions" TEST_DATA_DIR = pathlib.Path(__file__).resolve().parent / "test_data" @@ -89,57 +101,63 @@ def background_event(): return json.load(f) -@pytest.fixture -def client(): - source = TEST_FUNCTIONS_DIR / "cloud_events" / "main.py" +@pytest.fixture(params=["main.py", "async_main.py"]) +def client(request): + source = TEST_FUNCTIONS_DIR / "cloud_events" / request.param target = "function" - return create_app(target, source, "cloudevent").test_client() + if not request.param.startswith("async_"): + return create_app(target, source, "cloudevent").test_client() + app = create_asgi_app(target, source, "cloudevent") + return StarletteTestClient(app) -@pytest.fixture -def empty_client(): - source = TEST_FUNCTIONS_DIR / "cloud_events" / "empty_data.py" +@pytest.fixture(params=["empty_data.py", "async_empty_data.py"]) +def empty_client(request): + source = TEST_FUNCTIONS_DIR / "cloud_events" / request.param target = "function" - return create_app(target, source, "cloudevent").test_client() + if not request.param.startswith("async_"): + return create_app(target, source, "cloudevent").test_client() + app = create_asgi_app(target, source, "cloudevent") + return StarletteTestClient(app) @pytest.fixture -def converted_background_event_client(): +def converted_background_event_client(request): source = TEST_FUNCTIONS_DIR / "cloud_events" / "converted_background_event.py" target = "function" return create_app(target, source, "cloudevent").test_client() def test_event(client, cloud_event_1_0): - headers, data = to_structured(cloud_event_1_0) + headers, data = ce_conversion.to_structured(cloud_event_1_0) resp = client.post("/", headers=headers, data=data) assert resp.status_code == 200 - assert resp.data == b"OK" + assert resp.text == "OK" def test_binary_event(client, cloud_event_1_0): - headers, data = to_binary(cloud_event_1_0) + headers, data = ce_conversion.to_binary(cloud_event_1_0) resp = client.post("/", headers=headers, data=data) assert resp.status_code == 200 - assert resp.data == b"OK" + assert resp.text == "OK" def test_event_0_3(client, cloud_event_0_3): - headers, data = to_structured(cloud_event_0_3) + headers, data = ce_conversion.to_structured(cloud_event_0_3) resp = client.post("/", headers=headers, data=data) assert resp.status_code == 200 - assert resp.data == b"OK" + assert resp.text == "OK" def test_binary_event_0_3(client, cloud_event_0_3): - headers, data = to_binary(cloud_event_0_3) + headers, data = ce_conversion.to_binary(cloud_event_0_3) resp = client.post("/", headers=headers, data=data) assert resp.status_code == 200 - assert resp.data == b"OK" + assert resp.text == "OK" @pytest.mark.parametrize("specversion", ["0.3", "1.0"]) @@ -156,7 +174,7 @@ def test_cloud_event_missing_required_binary_fields( resp = client.post("/", headers=invalid_headers, json=data_payload) assert resp.status_code == 400 - assert "MissingRequiredFields" in resp.get_data().decode() + assert "MissingRequiredFields" in resp.text @pytest.mark.parametrize("specversion", ["0.3", "1.0"]) @@ -174,7 +192,7 @@ def test_cloud_event_missing_required_structured_fields( resp = client.post("/", headers=headers, json=invalid_data) assert resp.status_code == 400 - assert "MissingRequiredFields" in resp.data.decode() + assert "MissingRequiredFields" in resp.text def test_invalid_fields_binary(client, create_headers_binary, data_payload): @@ -183,7 +201,7 @@ def test_invalid_fields_binary(client, create_headers_binary, data_payload): resp = client.post("/", headers=headers, json=data_payload) assert resp.status_code == 400 - assert "InvalidRequiredFields" in resp.data.decode() + assert "InvalidRequiredFields" in resp.text def test_unparsable_cloud_event(client): @@ -191,7 +209,7 @@ def test_unparsable_cloud_event(client): resp = client.post("/", headers=headers, data="") assert resp.status_code == 400 - assert "Bad Request" in resp.data.decode() + assert "Bad Request" in resp.text @pytest.mark.parametrize("specversion", ["0.3", "1.0"]) @@ -200,7 +218,7 @@ def test_empty_data_binary(empty_client, create_headers_binary, specversion): resp = empty_client.post("/", headers=headers, json="") assert resp.status_code == 200 - assert resp.get_data() == b"OK" + assert resp.text == "OK" @pytest.mark.parametrize("specversion", ["0.3", "1.0"]) @@ -211,7 +229,7 @@ def test_empty_data_structured(empty_client, specversion, create_structured_data resp = empty_client.post("/", headers=headers, json=data) assert resp.status_code == 200 - assert resp.get_data() == b"OK" + assert resp.text == "OK" @pytest.mark.parametrize("specversion", ["0.3", "1.0"]) @@ -220,7 +238,7 @@ def test_no_mime_type_structured(empty_client, specversion, create_structured_da resp = empty_client.post("/", headers={}, json=data) assert resp.status_code == 200 - assert resp.get_data() == b"OK" + assert resp.text == "OK" def test_background_event(converted_background_event_client, background_event): @@ -228,5 +246,6 @@ def test_background_event(converted_background_event_client, background_event): "/", headers={}, json=background_event ) + print(resp.text) assert resp.status_code == 200 - assert resp.get_data() == b"OK" + assert resp.text == "OK" diff --git a/tests/test_decorator_functions.py b/tests/test_decorator_functions.py index e8c9bc70..435aa815 100644 --- a/tests/test_decorator_functions.py +++ b/tests/test_decorator_functions.py @@ -12,13 +12,27 @@ # See the License for the specific language governing permissions and # limitations under the License. import pathlib +import sys import pytest -from cloudevents.http import CloudEvent, to_binary, to_structured +from cloudevents import conversion as ce_conversion +from cloudevents.http import CloudEvent + +# Conditional import for Starlette +if sys.version_info >= (3, 8): + from starlette.testclient import TestClient as StarletteTestClient +else: + StarletteTestClient = None from functions_framework import create_app +# Conditional import for async functionality +if sys.version_info >= (3, 8): + from functions_framework.aio import create_asgi_app +else: + create_asgi_app = None + TEST_FUNCTIONS_DIR = pathlib.Path(__file__).resolve().parent / "test_functions" # Python 3.5: ModuleNotFoundError does not exist @@ -28,18 +42,24 @@ _ModuleNotFoundError = ImportError -@pytest.fixture -def cloud_event_decorator_client(): - source = TEST_FUNCTIONS_DIR / "decorators" / "decorator.py" +@pytest.fixture(params=["decorator.py", "async_decorator.py"]) +def cloud_event_decorator_client(request): + source = TEST_FUNCTIONS_DIR / "decorators" / request.param target = "function_cloud_event" - return create_app(target, source).test_client() + if not request.param.startswith("async_"): + return create_app(target, source).test_client() + app = create_asgi_app(target, source) + return StarletteTestClient(app) -@pytest.fixture -def http_decorator_client(): - source = TEST_FUNCTIONS_DIR / "decorators" / "decorator.py" +@pytest.fixture(params=["decorator.py", "async_decorator.py"]) +def http_decorator_client(request): + source = TEST_FUNCTIONS_DIR / "decorators" / request.param target = "function_http" - return create_app(target, source).test_client() + if not request.param.startswith("async_"): + return create_app(target, source).test_client() + app = create_asgi_app(target, source) + return StarletteTestClient(app) @pytest.fixture @@ -56,14 +76,55 @@ def cloud_event_1_0(): def test_cloud_event_decorator(cloud_event_decorator_client, cloud_event_1_0): - headers, data = to_structured(cloud_event_1_0) + headers, data = ce_conversion.to_structured(cloud_event_1_0) resp = cloud_event_decorator_client.post("/", headers=headers, data=data) - assert resp.status_code == 200 - assert resp.data == b"OK" + assert resp.text == "OK" def test_http_decorator(http_decorator_client): resp = http_decorator_client.post("/my_path", json={"mode": "path"}) assert resp.status_code == 200 - assert resp.data == b"/my_path" + assert resp.text == "/my_path" + + +def test_aio_sync_cloud_event_decorator(cloud_event_1_0): + """Test aio decorator with sync cloud event function.""" + source = TEST_FUNCTIONS_DIR / "decorators" / "async_decorator.py" + target = "function_cloud_event_sync" + + app = create_asgi_app(target, source) + client = StarletteTestClient(app) + + headers, data = ce_conversion.to_structured(cloud_event_1_0) + resp = client.post("/", headers=headers, data=data) + assert resp.status_code == 200 + assert resp.text == "OK" + + +def test_aio_sync_http_decorator(): + source = TEST_FUNCTIONS_DIR / "decorators" / "async_decorator.py" + target = "function_http_sync" + + app = create_asgi_app(target, source) + client = StarletteTestClient(app) + + resp = client.post("/my_path?mode=path") + assert resp.status_code == 200 + assert resp.text == "/my_path" + + resp = client.post("/other_path") + assert resp.status_code == 200 + assert resp.text == "sync response" + + +def test_aio_http_dict_response(): + source = TEST_FUNCTIONS_DIR / "decorators" / "async_decorator.py" + target = "function_http_dict_response" + + app = create_asgi_app(target, source) + client = StarletteTestClient(app) + + resp = client.post("/") + assert resp.status_code == 200 + assert resp.json() == {"message": "hello", "count": 42, "success": True} diff --git a/tests/test_functions.py b/tests/test_functions.py index f0bd7793..9107dc68 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -12,19 +12,31 @@ # See the License for the specific language governing permissions and # limitations under the License. -import io import json import pathlib import re +import sys import time import pretend import pytest +# Conditional import for Starlette +if sys.version_info >= (3, 8): + from starlette.testclient import TestClient as StarletteTestClient +else: + StarletteTestClient = None + import functions_framework from functions_framework import LazyWSGIApp, create_app, errorhandler, exceptions +# Conditional import for async functionality +if sys.version_info >= (3, 8): + from functions_framework.aio import create_asgi_app +else: + create_asgi_app = None + TEST_FUNCTIONS_DIR = pathlib.Path.cwd() / "tests" / "test_functions" @@ -72,127 +84,181 @@ def create_ce_headers(): } -def test_http_function_executes_success(): - source = TEST_FUNCTIONS_DIR / "http_trigger" / "main.py" +@pytest.fixture(params=["main.py", "async_main.py"]) +def http_trigger_client(request): + source = TEST_FUNCTIONS_DIR / "http_trigger" / request.param target = "function" + if not request.param.startswith("async_"): + return create_app(target, source).test_client() + app = create_asgi_app(target, source) + return StarletteTestClient(app, raise_server_exceptions=False) - client = create_app(target, source).test_client() - - resp = client.post("/my_path", json={"mode": "SUCCESS"}) - assert resp.status_code == 200 - assert resp.data == b"success" - -def test_http_function_executes_failure(): - source = TEST_FUNCTIONS_DIR / "http_trigger" / "main.py" +@pytest.fixture(params=["main.py", "async_main.py"]) +def http_request_check_client(request): + source = TEST_FUNCTIONS_DIR / "http_request_check" / request.param target = "function" - - client = create_app(target, source).test_client() - - resp = client.get("/", json={"mode": "FAILURE"}) - assert resp.status_code == 400 - assert resp.data == b"failure" + if not request.param.startswith("async_"): + return create_app(target, source).test_client() + app = create_asgi_app(target, source) + return StarletteTestClient( + app, + # Override baseurl to use localhost instead of default http://testserver. + base_url="http://localhost", + ) -def test_http_function_executes_throw(): - source = TEST_FUNCTIONS_DIR / "http_trigger" / "main.py" +@pytest.fixture(params=["main.py", "async_main.py"]) +def http_check_env_client(request): + source = TEST_FUNCTIONS_DIR / "http_check_env" / request.param target = "function" + if not request.param.startswith("async_"): + return create_app(target, source).test_client() + app = create_asgi_app(target, source) + return StarletteTestClient(app) - client = create_app(target, source).test_client() - resp = client.put("/", json={"mode": "THROW"}) - assert resp.status_code == 500 +@pytest.fixture(params=["main.py", "async_main.py"]) +def http_trigger_sleep_client(request): + source = TEST_FUNCTIONS_DIR / "http_trigger_sleep" / request.param + target = "function" + if not request.param.startswith("async_"): + return create_app(target, source).test_client() + app = create_asgi_app(target, source) + return StarletteTestClient(app) -def test_http_function_request_url_empty_path(): - source = TEST_FUNCTIONS_DIR / "http_request_check" / "main.py" +@pytest.fixture(params=["main.py", "async_main.py"]) +def http_with_import_client(request): + source = TEST_FUNCTIONS_DIR / "http_with_import" / request.param target = "function" + if not request.param.startswith("async_"): + return create_app(target, source).test_client() + app = create_asgi_app(target, source) + return StarletteTestClient(app) - client = create_app(target, source).test_client() - resp = client.get("", json={"mode": "url"}) - assert resp.status_code == 308 - assert resp.location == "http://localhost/" +@pytest.fixture(params=["sync", "async"]) +def http_method_check_client(request): + source = TEST_FUNCTIONS_DIR / "http_method_check" / "main.py" + target = "function" + if not request.param == "async": + return create_app(target, source).test_client() + app = create_asgi_app(target, source) + return StarletteTestClient(app) -def test_http_function_request_url_slash(): - source = TEST_FUNCTIONS_DIR / "http_request_check" / "main.py" +@pytest.fixture(params=["sync", "async"]) +def module_is_correct_client(request): + source = TEST_FUNCTIONS_DIR / "module_is_correct" / "main.py" target = "function" + if not request.param == "async": + return create_app(target, source).test_client() + app = create_asgi_app(target, source) + return StarletteTestClient(app) - client = create_app(target, source).test_client() - resp = client.get("/", json={"mode": "url"}) - assert resp.status_code == 200 - assert resp.data == b"http://localhost/" +@pytest.fixture(params=["sync", "async"]) +def returns_none_client(request): + source = TEST_FUNCTIONS_DIR / "returns_none" / "main.py" + target = "function" + if not request.param == "async": + return create_app(target, source).test_client() + app = create_asgi_app(target, source) + return StarletteTestClient(app) -def test_http_function_rquest_url_path(): - source = TEST_FUNCTIONS_DIR / "http_request_check" / "main.py" +@pytest.fixture(params=["sync", "async"]) +def relative_imports_client(request): + source = TEST_FUNCTIONS_DIR / "relative_imports" / "main.py" target = "function" + if not request.param == "async": + return create_app(target, source).test_client() + app = create_asgi_app(target, source) + return StarletteTestClient(app) - client = create_app(target, source).test_client() - resp = client.get("/my_path", json={"mode": "url"}) +def test_http_function_executes_success(http_trigger_client): + resp = http_trigger_client.post("/my_path", json={"mode": "SUCCESS"}) assert resp.status_code == 200 - assert resp.data == b"http://localhost/my_path" + assert resp.text == "success" -def test_http_function_request_path_slash(): - source = TEST_FUNCTIONS_DIR / "http_request_check" / "main.py" - target = "function" +def test_http_function_executes_failure(http_trigger_client): + resp = http_trigger_client.post("/", json={"mode": "FAILURE"}) + assert resp.status_code == 400 + assert resp.text == "failure" - client = create_app(target, source).test_client() - resp = client.get("/", json={"mode": "path"}) - assert resp.status_code == 200 - assert resp.data == b"/" +def test_http_function_executes_throw(http_trigger_client): + resp = http_trigger_client.put("/", json={"mode": "THROW"}) + assert resp.status_code == 500 -def test_http_function_request_path_path(): - source = TEST_FUNCTIONS_DIR / "http_request_check" / "main.py" - target = "function" +def test_http_function_request_url_empty_path(http_request_check_client): + # Starlette TestClient normalizes empty path "" to "/" before making the request, + # while Flask preserves the empty path and lets the server handle the redirect + if StarletteTestClient and isinstance( + http_request_check_client, StarletteTestClient + ): + # Starlette TestClient converts "" to "/" so we get a direct 200 response + resp = http_request_check_client.post("", json={"mode": "url"}) + assert resp.status_code == 200 + assert resp.text == "http://localhost/" + else: + # Flask returns a 308 redirect from empty path to "/" + resp = http_request_check_client.post("", json={"mode": "url"}) + assert resp.status_code == 308 + assert resp.location == "http://localhost/" + + +def test_http_function_request_url_slash(http_request_check_client): + resp = http_request_check_client.post("/", json={"mode": "url"}) + assert resp.status_code == 200 + assert resp.text == "http://localhost/" - client = create_app(target, source).test_client() - resp = client.get("/my_path", json={"mode": "path"}) +def test_http_function_rquest_url_path(http_request_check_client): + resp = http_request_check_client.post("/my_path", json={"mode": "url"}) assert resp.status_code == 200 - assert resp.data == b"/my_path" + assert resp.text == "http://localhost/my_path" -def test_http_function_check_env_function_target(): - source = TEST_FUNCTIONS_DIR / "http_check_env" / "main.py" - target = "function" +def test_http_function_request_path_slash(http_request_check_client): + resp = http_request_check_client.post("/", json={"mode": "path"}) + assert resp.status_code == 200 + assert resp.text == "/" - client = create_app(target, source).test_client() - resp = client.post("/", json={"mode": "FUNCTION_TARGET"}) +def test_http_function_request_path_path(http_request_check_client): + resp = http_request_check_client.post("/my_path", json={"mode": "path"}) assert resp.status_code == 200 - assert resp.data == b"function" + assert resp.text == "/my_path" -def test_http_function_check_env_function_signature_type(): - source = TEST_FUNCTIONS_DIR / "http_check_env" / "main.py" - target = "function" - - client = create_app(target, source).test_client() - - resp = client.post("/", json={"mode": "FUNCTION_SIGNATURE_TYPE"}) +def test_http_function_check_env_function_target(http_check_env_client): + resp = http_check_env_client.post("/", json={"mode": "FUNCTION_TARGET"}) assert resp.status_code == 200 - assert resp.data == b"http" + # Use .content for StarletteTestClient, .data for Flask test client (both return bytes) + data = getattr(resp, "content", getattr(resp, "data", None)) + assert data == b"function" -def test_http_function_execution_time(): - source = TEST_FUNCTIONS_DIR / "http_trigger_sleep" / "main.py" - target = "function" +def test_http_function_check_env_function_signature_type(http_check_env_client): + resp = http_check_env_client.post("/", json={"mode": "FUNCTION_SIGNATURE_TYPE"}) + assert resp.status_code == 200 + assert resp.text == "http" - client = create_app(target, source).test_client() +def test_http_function_execution_time(http_trigger_sleep_client): start_time = time.time() - resp = client.get("/", json={"mode": "1000"}) + resp = http_trigger_sleep_client.post("/", json={"mode": "1000"}) execution_time_sec = time.time() - start_time assert resp.status_code == 200 - assert resp.data == b"OK" + assert resp.text == "OK" + # Check that the execution time is roughly correct (allowing some buffer) + assert execution_time_sec > 0.9 def test_background_function_executes(background_event_client, background_json): @@ -268,7 +334,8 @@ def test_invalid_function_definition_missing_function_file(): ) -def test_invalid_function_definition_multiple_entry_points(): +@pytest.mark.parametrize("create_app", [create_app, create_asgi_app]) +def test_invalid_function_definition_multiple_entry_points(create_app): source = TEST_FUNCTIONS_DIR / "background_multiple_entry_points" / "main.py" target = "function" @@ -281,7 +348,8 @@ def test_invalid_function_definition_multiple_entry_points(): ) -def test_invalid_function_definition_multiple_entry_points_invalid_function(): +@pytest.mark.parametrize("create_app", [create_app, create_asgi_app]) +def test_invalid_function_definition_multiple_entry_points_invalid_function(create_app): source = TEST_FUNCTIONS_DIR / "background_multiple_entry_points" / "main.py" target = "invalidFunction" @@ -294,7 +362,8 @@ def test_invalid_function_definition_multiple_entry_points_invalid_function(): ) -def test_invalid_function_definition_multiple_entry_points_not_a_function(): +@pytest.mark.parametrize("create_app", [create_app, create_asgi_app]) +def test_invalid_function_definition_multiple_entry_points_not_a_function(create_app): source = TEST_FUNCTIONS_DIR / "background_multiple_entry_points" / "main.py" target = "notAFunction" @@ -308,7 +377,8 @@ def test_invalid_function_definition_multiple_entry_points_not_a_function(): ) -def test_invalid_function_definition_function_syntax_error(): +@pytest.mark.parametrize("create_app", [create_app, create_asgi_app]) +def test_invalid_function_definition_function_syntax_error(create_app): source = TEST_FUNCTIONS_DIR / "background_load_error" / "main.py" target = "function" @@ -336,7 +406,8 @@ def test_invalid_function_definition_function_syntax_robustness_with_debug(monke assert resp.status_code == 500 -def test_invalid_function_definition_missing_dependency(): +@pytest.mark.parametrize("create_app", [create_app, create_asgi_app]) +def test_invalid_function_definition_missing_dependency(create_app): source = TEST_FUNCTIONS_DIR / "background_missing_dependency" / "main.py" target = "function" @@ -346,7 +417,8 @@ def test_invalid_function_definition_missing_dependency(): assert "No module named 'nonexistentpackage'" in str(excinfo.value) -def test_invalid_configuration(): +@pytest.mark.parametrize("create_app", [create_app, create_asgi_app]) +def test_invalid_configuration(create_app): with pytest.raises(exceptions.InvalidConfigurationException) as excinfo: create_app(None, None, None) @@ -356,7 +428,8 @@ def test_invalid_configuration(): ) -def test_invalid_signature_type(): +@pytest.mark.parametrize("create_app", [create_app, create_asgi_app]) +def test_invalid_signature_type(create_app): source = TEST_FUNCTIONS_DIR / "http_trigger" / "main.py" target = "function" @@ -382,54 +455,39 @@ def test_http_function_flask_render_template(): ) -def test_http_function_with_import(): - source = TEST_FUNCTIONS_DIR / "http_with_import" / "main.py" - target = "function" - - client = create_app(target, source).test_client() - - resp = client.get("/") +def test_http_function_with_import(http_with_import_client): + resp = http_with_import_client.get("/") assert resp.status_code == 200 - assert resp.data == b"Hello" + assert resp.text == "Hello" @pytest.mark.parametrize( - "method, data", + "method, text", [ - ("get", b"GET"), - ("head", b""), # body will be empty - ("post", b"POST"), - ("put", b"PUT"), - ("delete", b"DELETE"), - ("options", b"OPTIONS"), - ("trace", b"TRACE"), - ("patch", b"PATCH"), + ("get", "GET"), + ("head", ""), # body will be empty + ("post", "POST"), + ("put", "PUT"), + ("delete", "DELETE"), + ("options", "OPTIONS"), + # ("trace", "TRACE"), # unsupported in httpx + ("patch", "PATCH"), ], ) -def test_http_function_all_methods(method, data): - source = TEST_FUNCTIONS_DIR / "http_method_check" / "main.py" - target = "function" - - client = create_app(target, source).test_client() - - resp = getattr(client, method)("/") +def test_http_function_all_methods(http_method_check_client, method, text): + resp = getattr(http_method_check_client, method)("/") assert resp.status_code == 200 - assert resp.data == data + assert resp.text == text @pytest.mark.parametrize("path", ["robots.txt", "favicon.ico"]) -def test_error_paths(path): - source = TEST_FUNCTIONS_DIR / "http_trigger" / "main.py" - target = "function" - - client = create_app(target, source).test_client() - - resp = client.get("/{}".format(path)) +def test_error_paths(http_trigger_client, path): + resp = http_trigger_client.get("/{}".format(path)) assert resp.status_code == 404 - assert b"Not Found" in resp.data + assert "Not Found" in resp.text @pytest.mark.parametrize( @@ -473,12 +531,8 @@ def function(): pass -def test_class_in_main_is_in_right_module(): - source = TEST_FUNCTIONS_DIR / "module_is_correct" / "main.py" - target = "function" - - client = create_app(target, source).test_client() - resp = client.get("/") +def test_class_in_main_is_in_right_module(module_is_correct_client): + resp = module_is_correct_client.get("/") assert resp.status_code == 200 @@ -493,12 +547,8 @@ def test_flask_current_app_is_available(): assert resp.status_code == 200 -def test_function_returns_none(): - source = TEST_FUNCTIONS_DIR / "returns_none" / "main.py" - target = "function" - - client = create_app(target, source).test_client() - resp = client.get("/") +def test_function_returns_none(returns_none_client): + resp = returns_none_client.get("/") assert resp.status_code == 500 @@ -515,6 +565,20 @@ def test_function_returns_stream(): assert resp.data.decode("utf-8") == "1.0\n3.0\n6.0\n10.0\n" +def test_async_function_returns_stream(): + source = TEST_FUNCTIONS_DIR / "http_streaming" / "async_main.py" + target = "function" + + client = StarletteTestClient(create_asgi_app(target, source)) + + collected_response = "" + with client.stream("POST", "/", content="1\n2\n3\n4\n") as resp: + assert resp.status_code == 200 + for text in resp.iter_text(): + collected_response += text + assert collected_response == "1.0\n3.0\n6.0\n10.0\n" + + def test_legacy_function_check_env(monkeypatch): source = TEST_FUNCTIONS_DIR / "http_check_env" / "main.py" target = "function" @@ -633,12 +697,7 @@ def tests_cloud_to_background_event_client_invalid_source( assert resp.status_code == 500 -def test_relative_imports(): - source = TEST_FUNCTIONS_DIR / "relative_imports" / "main.py" - target = "function" - - client = create_app(target, source).test_client() - - resp = client.get("/") +def test_relative_imports(relative_imports_client): + resp = relative_imports_client.get("/") assert resp.status_code == 200 - assert resp.data == b"success" + assert resp.text == "success" diff --git a/tests/test_functions/cloud_events/async_empty_data.py b/tests/test_functions/cloud_events/async_empty_data.py new file mode 100644 index 00000000..afc94c99 --- /dev/null +++ b/tests/test_functions/cloud_events/async_empty_data.py @@ -0,0 +1,38 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Function used to test handling CloudEvent (async) functions.""" +from starlette.exceptions import HTTPException + + +async def function(cloud_event): + """Test Event function that checks to see if a valid CloudEvent was sent. + + The function returns 200 if it received the expected event, otherwise 500. + + Args: + cloud_event: A CloudEvent as defined by https://github.com/cloudevents/sdk-python. + + Returns: + HTTP status code indicating whether valid event was sent or not. + + """ + valid_event = ( + cloud_event["id"] == "my-id" + and cloud_event["source"] == "from-galaxy-far-far-away" + and cloud_event["type"] == "cloud_event.greet.you" + ) + + if not valid_event: + raise HTTPException(status_code=500, detail="Something went wrong internally.") diff --git a/tests/test_functions/cloud_events/async_main.py b/tests/test_functions/cloud_events/async_main.py new file mode 100644 index 00000000..7e9b5423 --- /dev/null +++ b/tests/test_functions/cloud_events/async_main.py @@ -0,0 +1,40 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Function used to test handling CloudEvent (async) functions.""" +from starlette.exceptions import HTTPException + + +async def function(cloud_event): + """Test Event function that checks to see if a valid CloudEvent was sent. + + The function returns 200 if it received the expected event, otherwise 500. + + Args: + cloud_event: A CloudEvent as defined by https://github.com/cloudevents/sdk-python. + + Returns: + HTTP status code indicating whether valid event was sent or not. + + """ + valid_event = ( + cloud_event["id"] == "my-id" + and cloud_event.data == {"name": "john"} + and cloud_event["source"] == "from-galaxy-far-far-away" + and cloud_event["type"] == "cloud_event.greet.you" + and cloud_event["time"] == "2020-08-16T13:58:54.471765" + ) + + if not valid_event: + raise HTTPException(status_code=500, detail="Something went wrong internally.") diff --git a/tests/test_functions/decorators/async_decorator.py b/tests/test_functions/decorators/async_decorator.py new file mode 100644 index 00000000..0c0db7e4 --- /dev/null +++ b/tests/test_functions/decorators/async_decorator.py @@ -0,0 +1,98 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Function used to test handling functions using decorators.""" +from starlette.exceptions import HTTPException + +import functions_framework.aio + + +@functions_framework.aio.cloud_event +async def function_cloud_event(cloud_event): + """Test Event function that checks to see if a valid CloudEvent was sent. + + The function returns 200 if it received the expected event, otherwise 500. + + Args: + cloud_event: A CloudEvent as defined by https://github.com/cloudevents/sdk-python. + + Returns: + HTTP status code indicating whether valid event was sent or not. + """ + valid_event = ( + cloud_event["id"] == "my-id" + and cloud_event.data == {"name": "john"} + and cloud_event["source"] == "from-galaxy-far-far-away" + and cloud_event["type"] == "cloud_event.greet.you" + and cloud_event["time"] == "2020-08-16T13:58:54.471765" + ) + + if not valid_event: + raise HTTPException(500) + + +@functions_framework.aio.http +async def function_http(request): + """Test function which returns the requested element of the HTTP request. + + Name of the requested HTTP request element is provided in the 'mode' field in + the incoming JSON document. + + Args: + request: The HTTP request which triggered this function. Must contain name + of the requested HTTP request element in the 'mode' field in JSON document + in request body. + + Returns: + Value of the requested HTTP request element, or 'Bad Request' status in case + of unrecognized incoming request. + """ + data = await request.json() + mode = data["mode"] + if mode == "path": + return request.url.path + else: + raise HTTPException(400) + + +@functions_framework.aio.cloud_event +def function_cloud_event_sync(cloud_event): + """Test sync CloudEvent function with aio decorator.""" + valid_event = ( + cloud_event["id"] == "my-id" + and cloud_event.data == {"name": "john"} + and cloud_event["source"] == "from-galaxy-far-far-away" + and cloud_event["type"] == "cloud_event.greet.you" + and cloud_event["time"] == "2020-08-16T13:58:54.471765" + ) + + if not valid_event: + raise HTTPException(500) + + +@functions_framework.aio.http +def function_http_sync(request): + """Test sync HTTP function with aio decorator.""" + # Use query params since they're accessible synchronously + mode = request.query_params.get("mode") + if mode == "path": + return request.url.path + else: + return "sync response" + + +@functions_framework.aio.http +def function_http_dict_response(request): + """Test sync HTTP function returning dict with aio decorator.""" + return {"message": "hello", "count": 42, "success": True} diff --git a/tests/test_functions/http_check_env/async_main.py b/tests/test_functions/http_check_env/async_main.py new file mode 100644 index 00000000..dd91faec --- /dev/null +++ b/tests/test_functions/http_check_env/async_main.py @@ -0,0 +1,36 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Function used in Worker tests of environment variables setup.""" +import os + +X_GOOGLE_FUNCTION_NAME = "gcf-function" +X_GOOGLE_ENTRY_POINT = "function" +HOME = "/tmp" + + +async def function(request): + """Test function which returns the requested environment variable value. + + Args: + request: The HTTP request which triggered this function. Must contain name + of the requested environment variable in the 'mode' field in JSON document + in request body. + + Returns: + Value of the requested environment variable. + """ + data = await request.json() + name = data.get("mode") + return os.environ[name] diff --git a/tests/test_functions/http_request_check/async_main.py b/tests/test_functions/http_request_check/async_main.py new file mode 100644 index 00000000..bf0e7ce5 --- /dev/null +++ b/tests/test_functions/http_request_check/async_main.py @@ -0,0 +1,40 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Function used in Worker tests of HTTP request contents.""" + + +async def function(request): + """Test function which returns the requested element of the HTTP request. + + Name of the requested HTTP request element is provided in the 'mode' field in + the incoming JSON document. + + Args: + request: The HTTP request which triggered this function. Must contain name + of the requested HTTP request element in the 'mode' field in JSON document + in request body. + + Returns: + Value of the requested HTTP request element, or 'Bad Request' status in case + of unrecognized incoming request. + """ + data = await request.json() + mode = data.get("mode") + if mode == "path": + return request.url.path + elif mode == "url": + return str(request.url) + else: + return "invalid request", 400 diff --git a/tests/test_functions/http_streaming/async_main.py b/tests/test_functions/http_streaming/async_main.py new file mode 100644 index 00000000..1db2a7b9 --- /dev/null +++ b/tests/test_functions/http_streaming/async_main.py @@ -0,0 +1,46 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Async function used in Worker tests of handling HTTP functions.""" + +import asyncio + +from starlette.responses import StreamingResponse + + +async def function(request): + """Test async HTTP function that reads a stream of integers and returns a stream + providing the sum of values read so far. + + Args: + request: The HTTP request which triggered this function. Must contain a + stream of new line separated integers. + + Returns: + A Starlette StreamingResponse. + """ + print("INVOKED THE ASYNC STREAM FUNCTION!!!") + + body = await request.body() + body_str = body.decode("utf-8") + lines = body_str.strip().split("\n") if body_str.strip() else [] + + def generate(): + sum_so_far = 0 + for line in lines: + if line.strip(): + sum_so_far += float(line) + yield (str(sum_so_far) + "\n").encode("utf-8") + + return StreamingResponse(generate()) diff --git a/tests/test_functions/http_trigger/async_main.py b/tests/test_functions/http_trigger/async_main.py new file mode 100644 index 00000000..0e487d52 --- /dev/null +++ b/tests/test_functions/http_trigger/async_main.py @@ -0,0 +1,48 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Function used in Worker tests of handling HTTP functions.""" + +from starlette.exceptions import HTTPException +from starlette.responses import Response + + +async def function(request): + """Test HTTP function whose behavior depends on the given mode. + + The function returns a success, a failure, or throws an exception, depending + on the given mode. + + Args: + request: The HTTP request which triggered this function. Must contain name + of the requested mode in the 'mode' field in JSON document in request + body. + + Returns: + Value and status code defined for the given mode. + + Raises: + Exception: Thrown when requested in the incoming mode specification. + """ + data = await request.json() + mode = data.get("mode") + print("Mode: " + mode) + if mode == "SUCCESS": + return "success", 200 + elif mode == "FAILURE": + raise HTTPException(status_code=400, detail="failure") + elif mode == "THROW": + raise Exception("omg") + else: + return "invalid request", 400 diff --git a/tests/test_functions/http_trigger_sleep/async_main.py b/tests/test_functions/http_trigger_sleep/async_main.py new file mode 100644 index 00000000..fe77be1e --- /dev/null +++ b/tests/test_functions/http_trigger_sleep/async_main.py @@ -0,0 +1,33 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Async function used in Worker tests of function execution time.""" +import asyncio + + +async def function(request): + """Async test function which sleeps for the given number of seconds. + + The test verifies that it gets the response from the function only after the + given number of seconds. + + Args: + request: The HTTP request which triggered this function. Must contain the + requested number of seconds in the 'mode' field in JSON document in + request body. + """ + payload = await request.json() + sleep_sec = int(payload.get("mode")) / 1000.0 + await asyncio.sleep(sleep_sec) + return "OK" diff --git a/tests/test_functions/http_with_import/async_main.py b/tests/test_functions/http_with_import/async_main.py new file mode 100644 index 00000000..75a1dcac --- /dev/null +++ b/tests/test_functions/http_with_import/async_main.py @@ -0,0 +1,29 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Function used in Worker tests of handling HTTP functions.""" + +from foo import bar + + +async def function(request): + """Test HTTP function which imports from another file + + Args: + request: The HTTP request which triggered this function. + + Returns: + The imported return value and status code defined for the given mode. + """ + return bar diff --git a/tests/test_typing.py b/tests/test_typing.py index 279cd636..0ca90b47 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -14,3 +14,15 @@ def hello(request: flask.Request) -> flask.typing.ResponseReturnValue: @functions_framework.cloud_event def hello_cloud_event(cloud_event: CloudEvent) -> None: print(f"Received event: id={cloud_event['id']} and data={cloud_event.data}") + + from starlette.requests import Request + + import functions_framework.aio + + @functions_framework.aio.http + async def hello_async(request: Request) -> str: + return "Hello world!" + + @functions_framework.aio.cloud_event + async def hello_cloud_event_async(cloud_event: CloudEvent) -> None: + print(f"Received event: id={cloud_event['id']} and data={cloud_event.data}") diff --git a/tox.ini b/tox.ini index 1599608c..fd3e38a6 100644 --- a/tox.ini +++ b/tox.ini @@ -24,12 +24,19 @@ envlist = usedevelop = true deps = docker + httpx pytest-asyncio pytest-cov pytest-integration pretend +extras = + async setenv = PYTESTARGS = --cov=functions_framework --cov-branch --cov-report term-missing --cov-fail-under=100 + # Python 3.7: Use .coveragerc-py37 to exclude aio module from coverage since it requires Python 3.8+ (Starlette dependency) + py37-ubuntu-22.04: PYTESTARGS = --cov=functions_framework --cov-config=.coveragerc-py37 --cov-branch --cov-report term-missing --cov-fail-under=100 + py37-macos-13: PYTESTARGS = --cov=functions_framework --cov-config=.coveragerc-py37 --cov-branch --cov-report term-missing --cov-fail-under=100 + py37-windows-latest: PYTESTARGS = windows-latest: PYTESTARGS = commands = pytest {env:PYTESTARGS} {posargs} @@ -41,6 +48,8 @@ deps = isort mypy build +extras = + async commands = black --check src tests conftest.py --exclude tests/test_functions/background_load_error/main.py isort -c src tests conftest.py From 268acf121015bf2a5592715e4cfe582f9d236ff8 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 12 Jun 2025 15:41:35 -0700 Subject: [PATCH 078/108] feat: add flag to run functions framework in asgi stack (#376) * feat: add ASGI server support for async functions - Add uvicorn and uvicorn-worker to async optional dependencies - Refactor gunicorn.py with BaseGunicornApplication for shared config - Add UvicornApplication class for ASGI apps - Add StarletteApplication in asgi.py for development mode - Update HTTPServer to auto-detect Flask (WSGI) vs other (ASGI) apps - Add --gateway CLI flag to choose between wsgi and asgi - Update test_http.py to use Flask instance in tests * fix: apply black and isort formatting to source files * test: add comprehensive tests for ASGI server support - Add tests for HTTPServer ASGI/WSGI auto-detection - Add tests for StarletteApplication and UvicornApplication - Add tests for CLI --gateway flag functionality - Add integration tests for async functions with ASGI - Ensure 100% code coverage for new ASGI features - Apply black and isort formatting to test files * fix: skip async test files on Python 3.7 * fix: exclude async code from Python 3.7 coverage * fix: Install uvicorn on windows. * fix: unable to use reload in starlette. * fix: add missing endpoint parameter to Route constructors in ASGI * feat: add async conformance tests with ASGI gateway * fix: set UvicornWorker class before parent init and update tests * fix: update asgi tests to remove reload option * feat: add async-specific conformance tests for ASGI mode * fix: apply black formatting to async files * fix: disable validateMapping for CloudEvent tests in ASGI mode ASGI mode does not support automatic conversion from legacy events to CloudEvents, so validateMapping must be false for CloudEvent conformance tests. * fix: avoid mutating options dict in Gunicorn applications Create a copy of the options dict before modifying it to prevent side effects when the same options dict is reused elsewhere. This could cause issues with timeout tests. * fix: add pragma comments for Python 3.7 coverage and fix options handling - Add pragma: no cover comments for ASGI-specific code paths that won't execute in Python 3.7 - Fix options dict handling to use consistent variable names to avoid confusion * fix: revert to separate GunicornApplication and UvicornApplication classes Remove the BaseGunicornApplication abstraction as it was causing issues with the timeout mechanism. Each class now independently extends gunicorn.app.base.BaseApplication, which is cleaner and avoids the problems we were seeing with shared state and options handling. * refactor: restore GunicornApplication to match main branch Remove unnecessary changes to GunicornApplication class, keeping only the UvicornApplication addition for ASGI support. * chore: Untrack uv.lock * chore: rename confirmance test (asgi) github workflow * chore: cleanup .gitignore. * chore: clean up unncessary comments. --- .coveragerc-py37 | 13 ++- .github/workflows/conformance-asgi.yml | 91 ++++++++++++++++ .gitignore | 1 + conftest.py | 4 +- pyproject.toml | 6 +- src/functions_framework/_cli.py | 17 ++- src/functions_framework/_http/__init__.py | 37 +++++-- src/functions_framework/_http/asgi.py | 43 ++++++++ src/functions_framework/_http/gunicorn.py | 25 +++++ src/functions_framework/aio/__init__.py | 8 +- tests/conformance/async_main.py | 59 +++++++++++ tests/test_asgi.py | 120 ++++++++++++++++++++++ tests/test_cli.py | 21 ++++ tests/test_functions.py | 2 - tests/test_http.py | 119 ++++++++++++++++++++- 15 files changed, 544 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/conformance-asgi.yml create mode 100644 src/functions_framework/_http/asgi.py create mode 100644 tests/conformance/async_main.py create mode 100644 tests/test_asgi.py diff --git a/.coveragerc-py37 b/.coveragerc-py37 index 13be2ea1..fb6dbb6e 100644 --- a/.coveragerc-py37 +++ b/.coveragerc-py37 @@ -4,7 +4,18 @@ # This file is only used by py37-* tox environments omit = */functions_framework/aio/* + */functions_framework/_http/asgi.py */.tox/* */tests/* */venv/* - */.venv/* \ No newline at end of file + */.venv/* + +[report] +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain about async-specific imports and code + from functions_framework.aio import + from functions_framework._http.asgi import + from functions_framework._http.gunicorn import UvicornApplication \ No newline at end of file diff --git a/.github/workflows/conformance-asgi.yml b/.github/workflows/conformance-asgi.yml new file mode 100644 index 00000000..c69d1862 --- /dev/null +++ b/.github/workflows/conformance-asgi.yml @@ -0,0 +1,91 @@ +name: Python Conformance CI (asgi) +on: + push: + branches: + - 'main' + pull_request: + +# Declare default permissions as read only. +permissions: read-all + +jobs: + build: + strategy: + matrix: + python: ['3.8', '3.9', '3.10', '3.11', '3.12'] + platform: [ubuntu-latest] + runs-on: ${{ matrix.platform }} + steps: + - name: Harden Runner + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 + with: + disable-sudo: true + egress-policy: block + allowed-endpoints: > + api.github.com:443 + files.pythonhosted.org:443 + github.com:443 + objects.githubusercontent.com:443 + proxy.golang.org:443 + pypi.org:443 + storage.googleapis.com:443 + + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: ${{ matrix.python }} + + - name: Install the framework with async extras + run: python -m pip install -e .[async] + + - name: Setup Go + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version: '1.24' + + - name: Run HTTP conformance tests + uses: GoogleCloudPlatform/functions-framework-conformance/action@72a4f36b10f1c6435ab1a86a9ea24bda464cc262 # v1.8.6 + with: + functionType: 'http' + useBuildpacks: false + validateMapping: false + cmd: "'functions-framework --source tests/conformance/async_main.py --target write_http --signature-type http --gateway asgi'" + + - name: Run CloudEvents conformance tests + uses: GoogleCloudPlatform/functions-framework-conformance/action@72a4f36b10f1c6435ab1a86a9ea24bda464cc262 # v1.8.6 + with: + functionType: 'cloudevent' + useBuildpacks: false + validateMapping: false + cmd: "'functions-framework --source tests/conformance/async_main.py --target write_cloud_event --signature-type cloudevent --gateway asgi'" + + - name: Run HTTP conformance tests declarative + uses: GoogleCloudPlatform/functions-framework-conformance/action@72a4f36b10f1c6435ab1a86a9ea24bda464cc262 # v1.8.6 + with: + functionType: 'http' + useBuildpacks: false + validateMapping: false + cmd: "'functions-framework --source tests/conformance/async_main.py --target write_http_declarative --gateway asgi'" + + - name: Run CloudEvents conformance tests declarative + uses: GoogleCloudPlatform/functions-framework-conformance/action@72a4f36b10f1c6435ab1a86a9ea24bda464cc262 # v1.8.6 + with: + functionType: 'cloudevent' + useBuildpacks: false + validateMapping: false + cmd: "'functions-framework --source tests/conformance/async_main.py --target write_cloud_event_declarative --gateway asgi'" + + - name: Run HTTP concurrency tests declarative + uses: GoogleCloudPlatform/functions-framework-conformance/action@72a4f36b10f1c6435ab1a86a9ea24bda464cc262 # v1.8.6 + with: + functionType: 'http' + useBuildpacks: false + validateConcurrency: true + cmd: "'functions-framework --source tests/conformance/async_main.py --target write_http_declarative_concurrent --gateway asgi'" + + # Note: Event (legacy) and Typed tests are not supported in ASGI mode + # Note: validateMapping is set to false for CloudEvent tests because ASGI mode + # does not support automatic conversion from legacy events to CloudEvents \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8b5379fe..967d4513 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ dist/ function_output.json serverlog_stderr.txt serverlog_stdout.txt +venv/ diff --git a/conftest.py b/conftest.py index f72314ed..1d17e9bf 100644 --- a/conftest.py +++ b/conftest.py @@ -50,8 +50,8 @@ def pytest_ignore_collect(collection_path, config): if sys.version_info >= (3, 8): return None - # Skip test_aio.py entirely on Python 3.7 - if collection_path.name == "test_aio.py": + # Skip test_aio.py and test_asgi.py entirely on Python 3.7 + if collection_path.name in ["test_aio.py", "test_asgi.py"]: return True return None diff --git a/pyproject.toml b/pyproject.toml index 3a631b5d..350d8997 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,11 @@ dependencies = [ Homepage = "https://github.com/googlecloudplatform/functions-framework-python" [project.optional-dependencies] -async = ["starlette>=0.37.0,<1.0.0; python_version>='3.8'"] +async = [ + "starlette>=0.37.0,<1.0.0; python_version>='3.8'", + "uvicorn>=0.18.0,<1.0.0; python_version>='3.8'", + "uvicorn-worker>=0.2.0,<1.0.0; python_version>='3.8'" +] [project.scripts] ff = "functions_framework._cli:_cli" diff --git a/src/functions_framework/_cli.py b/src/functions_framework/_cli.py index 773dd4cd..e27b5446 100644 --- a/src/functions_framework/_cli.py +++ b/src/functions_framework/_cli.py @@ -32,6 +32,19 @@ @click.option("--host", envvar="HOST", type=click.STRING, default="0.0.0.0") @click.option("--port", envvar="PORT", type=click.INT, default=8080) @click.option("--debug", envvar="DEBUG", is_flag=True) -def _cli(target, source, signature_type, host, port, debug): - app = create_app(target, source, signature_type) +@click.option( + "--gateway", + envvar="GATEWAY", + type=click.Choice(["wsgi", "asgi"]), + default="wsgi", + help="Server gateway interface type (wsgi for sync, asgi for async)", +) +def _cli(target, source, signature_type, host, port, debug, gateway): + if gateway == "asgi": # pragma: no cover + from functions_framework.aio import create_asgi_app + + app = create_asgi_app(target, source, signature_type) + else: + app = create_app(target, source, signature_type) + create_server(app, debug).run(host, port) diff --git a/src/functions_framework/_http/__init__.py b/src/functions_framework/_http/__init__.py index ca9b0f5c..fa2cbc09 100644 --- a/src/functions_framework/_http/__init__.py +++ b/src/functions_framework/_http/__init__.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from flask import Flask + from functions_framework._http.flask import FlaskApplication @@ -21,15 +23,30 @@ def __init__(self, app, debug, **options): self.debug = debug self.options = options - if self.debug: - self.server_class = FlaskApplication - else: - try: - from functions_framework._http.gunicorn import GunicornApplication - - self.server_class = GunicornApplication - except ImportError as e: + if isinstance(app, Flask): + if self.debug: self.server_class = FlaskApplication + else: + try: + from functions_framework._http.gunicorn import GunicornApplication + + self.server_class = GunicornApplication + except ImportError as e: + self.server_class = FlaskApplication + else: # pragma: no cover + if self.debug: + from functions_framework._http.asgi import StarletteApplication + + self.server_class = StarletteApplication + else: + try: + from functions_framework._http.gunicorn import UvicornApplication + + self.server_class = UvicornApplication + except ImportError as e: + from functions_framework._http.asgi import StarletteApplication + + self.server_class = StarletteApplication def run(self, host, port): http_server = self.server_class( @@ -38,5 +55,5 @@ def run(self, host, port): http_server.run() -def create_server(wsgi_app, debug, **options): - return HTTPServer(wsgi_app, debug, **options) +def create_server(app, debug, **options): + return HTTPServer(app, debug, **options) diff --git a/src/functions_framework/_http/asgi.py b/src/functions_framework/_http/asgi.py new file mode 100644 index 00000000..083ffc2e --- /dev/null +++ b/src/functions_framework/_http/asgi.py @@ -0,0 +1,43 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import uvicorn + + +class StarletteApplication: + """A Starlette application that uses Uvicorn for direct serving (development mode).""" + + def __init__(self, app, host, port, debug, **options): + """Initialize the Starlette application. + + Args: + app: The ASGI application to serve + host: The host to bind to + port: The port to bind to + debug: Whether to run in debug mode + **options: Additional options to pass to Uvicorn + """ + self.app = app + self.host = host + self.port = port + self.debug = debug + + self.options = { + "log_level": "debug" if debug else "error", + } + self.options.update(options) + + def run(self): + """Run the Uvicorn server directly.""" + uvicorn.run(self.app, host=self.host, port=int(self.port), **self.options) diff --git a/src/functions_framework/_http/gunicorn.py b/src/functions_framework/_http/gunicorn.py index 92cad90e..745ce2f8 100644 --- a/src/functions_framework/_http/gunicorn.py +++ b/src/functions_framework/_http/gunicorn.py @@ -70,3 +70,28 @@ class GThreadWorkerWithTimeoutSupport(ThreadWorker): # pragma: no cover def handle_request(self, req, conn): with ThreadingTimeout(TIMEOUT_SECONDS): super(GThreadWorkerWithTimeoutSupport, self).handle_request(req, conn) + + +class UvicornApplication(gunicorn.app.base.BaseApplication): + """Gunicorn application for ASGI apps using Uvicorn workers.""" + + def __init__(self, app, host, port, debug, **options): + self.options = { + "bind": "%s:%s" % (host, port), + "workers": int(os.environ.get("WORKERS", 1)), + "worker_class": "uvicorn_worker.UvicornWorker", + "timeout": int(os.environ.get("CLOUD_RUN_TIMEOUT_SECONDS", 0)), + "loglevel": os.environ.get("GUNICORN_LOG_LEVEL", "error"), + "limit_request_line": 0, + } + self.options.update(options) + self.app = app + + super().__init__() + + def load_config(self): + for key, value in self.options.items(): + self.cfg.set(key, value) + + def load(self): + return self.app diff --git a/src/functions_framework/aio/__init__.py b/src/functions_framework/aio/__init__.py index 832d6818..21f12754 100644 --- a/src/functions_framework/aio/__init__.py +++ b/src/functions_framework/aio/__init__.py @@ -197,14 +197,16 @@ def create_asgi_app(target=None, source=None, signature_type=None): routes.append( Route( "/{path:path}", - http_handler, + endpoint=http_handler, methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH"], ) ) elif signature_type == _function_registry.CLOUDEVENT_SIGNATURE_TYPE: cloudevent_handler = _cloudevent_func_wrapper(function, is_async) - routes.append(Route("/{path:path}", cloudevent_handler, methods=["POST"])) - routes.append(Route("/", cloudevent_handler, methods=["POST"])) + routes.append( + Route("/{path:path}", endpoint=cloudevent_handler, methods=["POST"]) + ) + routes.append(Route("/", endpoint=cloudevent_handler, methods=["POST"])) elif signature_type == _function_registry.TYPED_SIGNATURE_TYPE: raise FunctionsFrameworkException( f"ASGI server does not support typed events (signature type: '{signature_type}'). " diff --git a/tests/conformance/async_main.py b/tests/conformance/async_main.py new file mode 100644 index 00000000..2a7b30a1 --- /dev/null +++ b/tests/conformance/async_main.py @@ -0,0 +1,59 @@ +import asyncio +import json + +from cloudevents.http import to_json + +import functions_framework.aio + +filename = "function_output.json" + + +class RawJson: + data: dict + + def __init__(self, data): + self.data = data + + @staticmethod + def from_dict(obj: dict) -> "RawJson": + return RawJson(obj) + + def to_dict(self) -> dict: + return self.data + + +def _write_output(content): + with open(filename, "w") as f: + f.write(content) + + +async def write_http(request): + json_data = await request.json() + _write_output(json.dumps(json_data)) + return "OK", 200 + + +async def write_cloud_event(cloud_event): + _write_output(to_json(cloud_event).decode()) + + +@functions_framework.aio.http +async def write_http_declarative(request): + json_data = await request.json() + _write_output(json.dumps(json_data)) + return "OK", 200 + + +@functions_framework.aio.cloud_event +async def write_cloud_event_declarative(cloud_event): + _write_output(to_json(cloud_event).decode()) + + +@functions_framework.aio.http +async def write_http_declarative_concurrent(request): + await asyncio.sleep(1) + return "OK", 200 + + +# Note: Typed events are not supported in ASGI mode yet +# Legacy event functions are also not supported in ASGI mode diff --git a/tests/test_asgi.py b/tests/test_asgi.py new file mode 100644 index 00000000..cd117bd3 --- /dev/null +++ b/tests/test_asgi.py @@ -0,0 +1,120 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys + +import flask +import pretend +import pytest + +import functions_framework._http + +try: + from starlette.applications import Starlette +except ImportError: + pass + + +def test_httpserver_detects_asgi_app(): + flask_app = flask.Flask("test") + flask_wrapper = functions_framework._http.HTTPServer(flask_app, debug=True) + assert flask_wrapper.server_class.__name__ == "FlaskApplication" + + starlette_app = Starlette(routes=[]) + starlette_wrapper = functions_framework._http.HTTPServer(starlette_app, debug=True) + assert starlette_wrapper.server_class.__name__ == "StarletteApplication" + + +@pytest.mark.skipif("platform.system() == 'Windows'") +def test_httpserver_production_asgi(): + starlette_app = Starlette(routes=[]) + wrapper = functions_framework._http.HTTPServer(starlette_app, debug=False) + assert wrapper.server_class.__name__ == "UvicornApplication" + + +def test_starlette_application_init(): + from functions_framework._http.asgi import StarletteApplication + + app = pretend.stub() + host = "1.2.3.4" + port = "5678" + + # Test debug mode + starlette_app = StarletteApplication(app, host, port, debug=True, custom="value") + assert starlette_app.app == app + assert starlette_app.host == host + assert starlette_app.port == port + assert starlette_app.debug is True + assert starlette_app.options["log_level"] == "debug" + assert starlette_app.options["custom"] == "value" + + # Test production mode + starlette_app = StarletteApplication(app, host, port, debug=False) + assert starlette_app.options["log_level"] == "error" + + +@pytest.mark.skipif("platform.system() == 'Windows'") +def test_uvicorn_application_init(): + from functions_framework._http.gunicorn import UvicornApplication + + app = pretend.stub() + host = "1.2.3.4" + port = "1234" + + uvicorn_app = UvicornApplication(app, host, port, debug=False) + assert uvicorn_app.app == app + assert uvicorn_app.options["worker_class"] == "uvicorn_worker.UvicornWorker" + assert uvicorn_app.options["bind"] == "1.2.3.4:1234" + assert uvicorn_app.load() == app + + +def test_httpserver_fallback_on_import_error(monkeypatch): + starlette_app = Starlette(routes=[]) + + monkeypatch.setitem(sys.modules, "functions_framework._http.gunicorn", None) + + wrapper = functions_framework._http.HTTPServer(starlette_app, debug=False) + assert wrapper.server_class.__name__ == "StarletteApplication" + + +def test_starlette_application_run(monkeypatch): + uvicorn_run_calls = [] + + def mock_uvicorn_run(app, **kwargs): + uvicorn_run_calls.append((app, kwargs)) + + uvicorn_stub = pretend.stub(run=mock_uvicorn_run) + monkeypatch.setitem(sys.modules, "uvicorn", uvicorn_stub) + + # Clear and re-import to get fresh module with mocked uvicorn + if "functions_framework._http.asgi" in sys.modules: + del sys.modules["functions_framework._http.asgi"] + + from functions_framework._http.asgi import StarletteApplication + + app = pretend.stub() + host = "1.2.3.4" + port = "5678" + + starlette_app = StarletteApplication(app, host, port, debug=True, custom="value") + starlette_app.run() + + assert len(uvicorn_run_calls) == 1 + assert uvicorn_run_calls[0][0] == app + assert uvicorn_run_calls[0][1] == { + "host": host, + "port": int(port), + "log_level": "debug", + "custom": "value", + } diff --git a/tests/test_cli.py b/tests/test_cli.py index 7613b649..17445d11 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import sys + import pretend import pytest @@ -103,3 +105,22 @@ def test_cli(monkeypatch, args, env, create_app_calls, run_calls): assert result.exit_code == 0 assert create_app.calls == create_app_calls assert wsgi_server.run.calls == run_calls + + +def test_asgi_cli(monkeypatch): + asgi_server = pretend.stub(run=pretend.call_recorder(lambda *a, **kw: None)) + asgi_app = pretend.stub() + + create_asgi_app = pretend.call_recorder(lambda *a, **kw: asgi_app) + aio_module = pretend.stub(create_asgi_app=create_asgi_app) + monkeypatch.setitem(sys.modules, "functions_framework.aio", aio_module) + + create_server = pretend.call_recorder(lambda *a, **kw: asgi_server) + monkeypatch.setattr(functions_framework._cli, "create_server", create_server) + + runner = CliRunner() + result = runner.invoke(_cli, ["--target", "foo", "--gateway", "asgi"]) + + assert result.exit_code == 0 + assert create_asgi_app.calls == [pretend.call("foo", None, "http")] + assert asgi_server.run.calls == [pretend.call("0.0.0.0", 8080)] diff --git a/tests/test_functions.py b/tests/test_functions.py index 9107dc68..534f4a88 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -21,7 +21,6 @@ import pretend import pytest -# Conditional import for Starlette if sys.version_info >= (3, 8): from starlette.testclient import TestClient as StarletteTestClient else: @@ -31,7 +30,6 @@ from functions_framework import LazyWSGIApp, create_app, errorhandler, exceptions -# Conditional import for async functionality if sys.version_info >= (3, 8): from functions_framework.aio import create_asgi_app else: diff --git a/tests/test_http.py b/tests/test_http.py index fbfac9d2..df9d4c6c 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -16,6 +16,7 @@ import platform import sys +import flask import pretend import pytest @@ -45,7 +46,7 @@ def test_create_server(monkeypatch, debug): ], ) def test_httpserver(monkeypatch, debug, gunicorn_missing, expected): - app = pretend.stub() + app = flask.Flask("test") http_server = pretend.stub(run=pretend.call_recorder(lambda: None)) server_classes = { "flask": pretend.call_recorder(lambda *a, **kw: http_server), @@ -133,3 +134,119 @@ def test_flask_application(debug): assert app.run.calls == [ pretend.call(host, port, debug=debug, a=options["a"], b=options["b"]), ] + + +@pytest.mark.parametrize( + "debug, uvicorn_missing, expected", + [ + (True, False, "starlette"), + (False, False, "uvicorn" if platform.system() != "Windows" else "starlette"), + (True, True, "starlette"), + (False, True, "starlette"), + ], +) +def test_httpserver_asgi(monkeypatch, debug, uvicorn_missing, expected): + app = pretend.stub() + http_server = pretend.stub(run=pretend.call_recorder(lambda: None)) + server_classes = { + "starlette": pretend.call_recorder(lambda *a, **kw: http_server), + "uvicorn": pretend.call_recorder(lambda *a, **kw: http_server), + } + options = {"a": pretend.stub(), "b": pretend.stub()} + + from functions_framework._http import asgi + + monkeypatch.setattr(asgi, "StarletteApplication", server_classes["starlette"]) + + if uvicorn_missing or platform.system() == "Windows": + monkeypatch.setitem(sys.modules, "functions_framework._http.gunicorn", None) + else: + from functions_framework._http import gunicorn + + monkeypatch.setattr(gunicorn, "UvicornApplication", server_classes["uvicorn"]) + + wrapper = functions_framework._http.HTTPServer(app, debug, **options) + + assert wrapper.app == app + assert wrapper.server_class == server_classes[expected] + assert wrapper.options == options + + host = pretend.stub() + port = pretend.stub() + + wrapper.run(host, port) + + assert wrapper.server_class.calls == [ + pretend.call(app, host, port, debug, **options) + ] + assert http_server.run.calls == [pretend.call()] + + +@pytest.mark.skipif("platform.system() == 'Windows'") +def test_uvicorn_application(): + app = pretend.stub() + host = "1.2.3.4" + port = "1234" + options = {} + + import functions_framework._http.gunicorn + + uvicorn_app = functions_framework._http.gunicorn.UvicornApplication( + app, host, port, debug=False, **options + ) + + assert uvicorn_app.app == app + assert uvicorn_app.options == { + "bind": "%s:%s" % (host, port), + "workers": 1, + "timeout": 0, + "loglevel": "error", + "limit_request_line": 0, + "worker_class": "uvicorn_worker.UvicornWorker", + } + + assert uvicorn_app.cfg.bind == ["1.2.3.4:1234"] + assert uvicorn_app.cfg.workers == 1 + assert uvicorn_app.cfg.timeout == 0 + assert uvicorn_app.load() == app + + +@pytest.mark.parametrize("debug", [True, False]) +def test_starlette_application(monkeypatch, debug): + uvicorn_run = pretend.call_recorder(lambda *a, **kw: None) + uvicorn_stub = pretend.stub(run=uvicorn_run) + monkeypatch.setitem(sys.modules, "uvicorn", uvicorn_stub) + + # Clear and re-import to get fresh module with mocked uvicorn + if "functions_framework._http.asgi" in sys.modules: + del sys.modules["functions_framework._http.asgi"] + + from functions_framework._http.asgi import StarletteApplication + + app = pretend.stub() + host = "1.2.3.4" + port = "5678" + options = {"custom": "value"} + + starlette_app = StarletteApplication(app, host, port, debug, **options) + + assert starlette_app.app == app + assert starlette_app.host == host + assert starlette_app.port == port + assert starlette_app.debug == debug + assert starlette_app.options == { + "log_level": "debug" if debug else "error", + "custom": "value", + } + + starlette_app.run() + + assert uvicorn_run.calls == [ + pretend.call( + app, + host=host, + port=int(port), + log_level="debug" if debug else "error", + custom="value", + ) + ] From 1123eeac8cedae23af8980a928f01f5ad100d9de Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 17 Jun 2025 14:04:37 -0700 Subject: [PATCH 079/108] feat: add execution_id support for async stack (#377) * feat: add execution_id support for async stack - Add contextvars support to execution_id.py for async-safe context storage - Create AsgiMiddleware class to inject execution_id into ASGI requests - Add set_execution_context_async decorator for both sync and async functions - Update LoggingHandlerAddExecutionId to support both Flask g and contextvars - Integrate execution_id support in aio/__init__.py with proper exception handling - Add comprehensive async tests matching sync test functionality - Follow Starlette best practices for exception handling The implementation enables automatic execution_id injection and logging for async functions when LOG_EXECUTION_ID=true, matching the existing sync stack behavior. * refactor: move exception logging to crash handler for cleaner code - Remove try/catch blocks from wrapper functions - Centralize exception logging in _crash_handler - Extract execution_id directly from request headers in crash handler - Temporarily set context when logging exceptions to ensure execution_id is included - This approach is cleaner and more similar to Flask's centralized exception handling * refactor: improve code organization based on feedback - Move imports to top of file instead of inside functions - Extract common header parsing logic into _extract_context_from_headers helper - Reduce code duplication between sync and async decorators - Add comment explaining why crash handler needs to extract context from headers - This addresses the context reset issue where decorators clean up before exception handlers run * fix: preserve execution context for exception handlers - Don't reset context on exception, only on successful completion - This allows exception handlers to access execution_id naturally - Simplify crash handler since context is now available - Rely on Python's automatic contextvar cleanup when task completes - Each request runs in its own task, so no risk of context leakage This is more correct and follows the principle that context should be available throughout the entire request lifecycle, including error handling. * style: apply black and isort formatting - Format code with black for consistent style - Sort imports with isort for better organization - All linting checks now pass * refactor: clean up async tests and remove redundant comments * chore: remove uv.lock from version control * style: fix black formatting * fix: skip async execution_id tests on Python 3.7 * refactor: reuse _enable_execution_id_logging from main module * chore: more cleanup. * test: remove unnecessary pragma no cover for sync_wrapper * test: improve coverage by removing unnecessary pragma no cover annotations * style: fix black formatting * style: fix isort import ordering * test: add back pragma no cover for genuinely hard-to-test edge cases * refactor: simplify async decorator by removing dead code branch * fix: exclude async-specific code from py37 coverage The AsgiMiddleware class and set_execution_context_async function in execution_id.py require Python 3.8+ due to async dependencies. These are now excluded from coverage calculations in Python 3.7 environments. * fix: improve async execution ID context propagation using contextvars - Use contextvars.copy_context() to properly propagate execution context in async functions - Implement AsyncExecutionIdHandler to handle JSON logging with execution_id - Redirect logging output from stderr to stdout for consistency - Add build dependency to dev dependencies - Update tests to reflect new logging output location * feat: Add execution ID logging for async functions Refactors the async logging implementation to align with the sync version, ensuring consistent execution ID logging across both stacks. * chore: clean up impl. * refactor: define custom exception handling middleware to avoid duplicate log of traceback. * style: run black * chore: clean up code a little more. * fix: propagate context in ce fns. * style: more nits. * chore: remove unncessary debug flag. * fix: respond to PR comments * fix: respond to more PR comments --- .coveragerc-py37 | 6 +- conftest.py | 8 +- pyproject.toml | 10 + src/functions_framework/aio/__init__.py | 117 +++++- src/functions_framework/execution_id.py | 125 +++++- tests/test_aio.py | 4 + tests/test_execution_id.py | 1 + tests/test_execution_id_async.py | 365 ++++++++++++++++++ .../test_functions/execution_id/async_main.py | 62 +++ 9 files changed, 665 insertions(+), 33 deletions(-) create mode 100644 tests/test_execution_id_async.py create mode 100644 tests/test_functions/execution_id/async_main.py diff --git a/.coveragerc-py37 b/.coveragerc-py37 index fb6dbb6e..b1c98d23 100644 --- a/.coveragerc-py37 +++ b/.coveragerc-py37 @@ -18,4 +18,8 @@ exclude_lines = # Don't complain about async-specific imports and code from functions_framework.aio import from functions_framework._http.asgi import - from functions_framework._http.gunicorn import UvicornApplication \ No newline at end of file + from functions_framework._http.gunicorn import UvicornApplication + + # Exclude async-specific classes and functions in execution_id.py + class AsgiMiddleware: + def set_execution_context_async \ No newline at end of file diff --git a/conftest.py b/conftest.py index 1d17e9bf..257f60d4 100644 --- a/conftest.py +++ b/conftest.py @@ -50,8 +50,12 @@ def pytest_ignore_collect(collection_path, config): if sys.version_info >= (3, 8): return None - # Skip test_aio.py and test_asgi.py entirely on Python 3.7 - if collection_path.name in ["test_aio.py", "test_asgi.py"]: + # Skip test_aio.py, test_asgi.py, and test_execution_id_async.py entirely on Python 3.7 + if collection_path.name in [ + "test_aio.py", + "test_asgi.py", + "test_execution_id_async.py", + ]: return True return None diff --git a/pyproject.toml b/pyproject.toml index 350d8997..2b6e0639 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,3 +61,13 @@ functions_framework = ["py.typed"] [tool.setuptools.package-dir] "" = "src" + +[dependency-groups] +dev = [ + "black>=23.3.0", + "build>=1.1.1", + "isort>=5.11.5", + "pretend>=1.0.9", + "pytest>=7.4.4", + "pytest-asyncio>=0.21.2", +] diff --git a/src/functions_framework/aio/__init__.py b/src/functions_framework/aio/__init__.py index 21f12754..4245f2d1 100644 --- a/src/functions_framework/aio/__init__.py +++ b/src/functions_framework/aio/__init__.py @@ -13,16 +13,24 @@ # limitations under the License. import asyncio +import contextvars import functools import inspect +import logging +import logging.config import os +import traceback from typing import Any, Awaitable, Callable, Dict, Tuple, Union from cloudevents.http import from_http from cloudevents.http.event import CloudEvent -from functions_framework import _function_registry +from functions_framework import ( + _enable_execution_id_logging, + _function_registry, + execution_id, +) from functions_framework.exceptions import ( FunctionsFrameworkException, MissingSourceException, @@ -31,6 +39,7 @@ try: from starlette.applications import Starlette from starlette.exceptions import HTTPException + from starlette.middleware import Middleware from starlette.requests import Request from starlette.responses import JSONResponse, Response from starlette.routing import Route @@ -96,29 +105,27 @@ def wrapper(*args, **kwargs): return wrapper -async def _crash_handler(request, exc): - headers = {_FUNCTION_STATUS_HEADER_FIELD: _CRASH} - return Response(f"Internal Server Error: {exc}", status_code=500, headers=headers) - - -def _http_func_wrapper(function, is_async): +def _http_func_wrapper(function, is_async, enable_id_logging=False): + @execution_id.set_execution_context_async(enable_id_logging) @functools.wraps(function) async def handler(request): if is_async: result = await function(request) else: # TODO: Use asyncio.to_thread when we drop Python 3.8 support - # Python 3.8 compatible version of asyncio.to_thread loop = asyncio.get_event_loop() - result = await loop.run_in_executor(None, function, request) + ctx = contextvars.copy_context() + result = await loop.run_in_executor(None, ctx.run, function, request) if isinstance(result, str): return Response(result) elif isinstance(result, dict): return JSONResponse(result) elif isinstance(result, tuple) and len(result) == 2: - # Support Flask-style tuple response content, status_code = result - return Response(content, status_code=status_code) + if isinstance(content, dict): + return JSONResponse(content, status_code=status_code) + else: + return Response(content, status_code=status_code) elif result is None: raise HTTPException(status_code=500, detail="No response returned") else: @@ -127,7 +134,8 @@ async def handler(request): return handler -def _cloudevent_func_wrapper(function, is_async): +def _cloudevent_func_wrapper(function, is_async, enable_id_logging=False): + @execution_id.set_execution_context_async(enable_id_logging) @functools.wraps(function) async def handler(request): data = await request.body() @@ -142,9 +150,9 @@ async def handler(request): await function(event) else: # TODO: Use asyncio.to_thread when we drop Python 3.8 support - # Python 3.8 compatible version of asyncio.to_thread loop = asyncio.get_event_loop() - await loop.run_in_executor(None, function, event) + ctx = contextvars.copy_context() + await loop.run_in_executor(None, ctx.run, function, event) return Response("OK") return handler @@ -154,6 +162,64 @@ async def _handle_not_found(request: Request): raise HTTPException(status_code=404, detail="Not Found") +def _configure_app_execution_id_logging(): + logging.config.dictConfig( + { + "version": 1, + "handlers": { + "asgi": { + "class": "logging.StreamHandler", + "stream": "ext://functions_framework.execution_id.logging_stream", + }, + }, + "root": {"level": "INFO", "handlers": ["asgi"]}, + } + ) + + +class ExceptionHandlerMiddleware: + def __init__(self, app): + self.app = app + + async def __call__(self, scope, receive, send): + if scope["type"] != "http": # pragma: no cover + await self.app(scope, receive, send) + return + + try: + await self.app(scope, receive, send) + except Exception as exc: + logger = logging.getLogger() + tb_lines = traceback.format_exception(type(exc), exc, exc.__traceback__) + tb_text = "".join(tb_lines) + + path = scope.get("path", "/") + method = scope.get("method", "GET") + error_msg = f"Exception on {path} [{method}]\n{tb_text}".rstrip() + + logger.error(error_msg) + + headers = [ + [b"content-type", b"text/plain"], + [_FUNCTION_STATUS_HEADER_FIELD.encode(), _CRASH.encode()], + ] + + await send( + { + "type": "http.response.start", + "status": 500, + "headers": headers, + } + ) + await send( + { + "type": "http.response.body", + "body": b"Internal Server Error", + } + ) + # Don't re-raise to prevent starlette from printing traceback again + + def create_asgi_app(target=None, source=None, signature_type=None): """Create an ASGI application for the function. @@ -175,6 +241,11 @@ def create_asgi_app(target=None, source=None, signature_type=None): ) source_module, spec = _function_registry.load_function_module(source) + + enable_id_logging = _enable_execution_id_logging() + if enable_id_logging: + _configure_app_execution_id_logging() + spec.loader.exec_module(source_module) function = _function_registry.get_user_function(source, source_module, target) signature_type = _function_registry.get_func_signature_type(target, signature_type) @@ -182,7 +253,7 @@ def create_asgi_app(target=None, source=None, signature_type=None): is_async = inspect.iscoroutinefunction(function) routes = [] if signature_type == _function_registry.HTTP_SIGNATURE_TYPE: - http_handler = _http_func_wrapper(function, is_async) + http_handler = _http_func_wrapper(function, is_async, enable_id_logging) routes.append( Route( "/", @@ -202,7 +273,9 @@ def create_asgi_app(target=None, source=None, signature_type=None): ) ) elif signature_type == _function_registry.CLOUDEVENT_SIGNATURE_TYPE: - cloudevent_handler = _cloudevent_func_wrapper(function, is_async) + cloudevent_handler = _cloudevent_func_wrapper( + function, is_async, enable_id_logging + ) routes.append( Route("/{path:path}", endpoint=cloudevent_handler, methods=["POST"]) ) @@ -221,10 +294,14 @@ def create_asgi_app(target=None, source=None, signature_type=None): f"Unsupported signature type for ASGI server: {signature_type}" ) - exception_handlers = { - 500: _crash_handler, - } - app = Starlette(routes=routes, exception_handlers=exception_handlers) + app = Starlette( + routes=routes, + middleware=[ + Middleware(ExceptionHandlerMiddleware), + Middleware(execution_id.AsgiMiddleware), + ], + ) + return app diff --git a/src/functions_framework/execution_id.py b/src/functions_framework/execution_id.py index 2b106531..df412187 100644 --- a/src/functions_framework/execution_id.py +++ b/src/functions_framework/execution_id.py @@ -13,7 +13,9 @@ # limitations under the License. import contextlib +import contextvars import functools +import inspect import io import json import logging @@ -38,6 +40,9 @@ logger = logging.getLogger(__name__) +# Context variable for async execution context +execution_context_var = contextvars.ContextVar("execution_context", default=None) + class ExecutionContext: def __init__(self, execution_id=None, span_id=None): @@ -46,7 +51,10 @@ def __init__(self, execution_id=None, span_id=None): def _get_current_context(): - return ( + context = execution_context_var.get() + if context is not None: + return context + return ( # pragma: no cover flask.g.execution_id_context if flask.has_request_context() and "execution_id_context" in flask.g else None @@ -54,6 +62,8 @@ def _get_current_context(): def _set_current_context(context): + execution_context_var.set(context) + # Also set in Flask context if available for sync if flask.has_request_context(): flask.g.execution_id_context = context @@ -65,6 +75,18 @@ def _generate_execution_id(): ) +def _extract_context_from_headers(headers): + """Extract execution context from request headers.""" + trace_context = re.match( + _TRACE_CONTEXT_REGEX_PATTERN, + headers.get(TRACE_CONTEXT_REQUEST_HEADER, ""), + ) + execution_id = headers.get(EXECUTION_ID_REQUEST_HEADER) + span_id = trace_context.group("span_id") if trace_context else None + + return ExecutionContext(execution_id, span_id) + + # Middleware to add execution id to request header if one does not already exist class WsgiMiddleware: def __init__(self, wsgi_app): @@ -78,8 +100,42 @@ def __call__(self, environ, start_response): return self.wsgi_app(environ, start_response) -# Sets execution id and span id for the request +class AsgiMiddleware: + def __init__(self, app): + self.app = app + + async def __call__(self, scope, receive, send): + if scope["type"] == "http": # pragma: no branch + execution_id_header = b"function-execution-id" + execution_id = None + + for name, value in scope.get("headers", []): + if name.lower() == execution_id_header: + execution_id = value.decode("latin-1") + break + + if not execution_id: + execution_id = _generate_execution_id() + new_headers = list(scope.get("headers", [])) + new_headers.append( + (execution_id_header, execution_id.encode("latin-1")) + ) + scope["headers"] = new_headers + + await self.app(scope, receive, send) + + def set_execution_context(request, enable_id_logging=False): + """Decorator for Flask/WSGI handlers that sets execution context. + + Takes request object at decoration time (Flask pattern where request is available + via thread-local context when decorator is applied). + + Usage: + @set_execution_context(request, enable_id_logging=True) + def view_func(path): + ... + """ if enable_id_logging: stdout_redirect = contextlib.redirect_stdout( LoggingHandlerAddExecutionId(sys.stdout) @@ -94,22 +150,71 @@ def set_execution_context(request, enable_id_logging=False): def decorator(view_function): @functools.wraps(view_function) def wrapper(*args, **kwargs): - trace_context = re.match( - _TRACE_CONTEXT_REGEX_PATTERN, - request.headers.get(TRACE_CONTEXT_REQUEST_HEADER, ""), - ) - execution_id = request.headers.get(EXECUTION_ID_REQUEST_HEADER) - span_id = trace_context.group("span_id") if trace_context else None - _set_current_context(ExecutionContext(execution_id, span_id)) + context = _extract_context_from_headers(request.headers) + _set_current_context(context) with stderr_redirect, stdout_redirect: - return view_function(*args, **kwargs) + result = view_function(*args, **kwargs) + return result return wrapper return decorator +def set_execution_context_async(enable_id_logging=False): + """Decorator for ASGI/async handlers that sets execution context. + + Unlike set_execution_context which takes request at decoration time (Flask pattern), + this expects the decorated function to receive request as its first parameter (ASGI pattern). + + Usage: + @set_execution_context_async(enable_id_logging=True) + async def handler(request, *args, **kwargs): + ... + """ + if enable_id_logging: + stdout_redirect = contextlib.redirect_stdout( + LoggingHandlerAddExecutionId(sys.stdout) + ) + stderr_redirect = contextlib.redirect_stderr( + LoggingHandlerAddExecutionId(sys.stderr) + ) + else: + stdout_redirect = contextlib.nullcontext() + stderr_redirect = contextlib.nullcontext() + + def decorator(func): + @functools.wraps(func) + async def async_wrapper(request, *args, **kwargs): + context = _extract_context_from_headers(request.headers) + token = execution_context_var.set(context) + + with stderr_redirect, stdout_redirect: + result = await func(request, *args, **kwargs) + + execution_context_var.reset(token) + return result + + @functools.wraps(func) + def sync_wrapper(request, *args, **kwargs): + context = _extract_context_from_headers(request.headers) + token = execution_context_var.set(context) + + with stderr_redirect, stdout_redirect: + result = func(request, *args, **kwargs) + + execution_context_var.reset(token) + return result + + if inspect.iscoroutinefunction(func): + return async_wrapper + else: + return sync_wrapper + + return decorator + + @LocalProxy def logging_stream(): return LoggingHandlerAddExecutionId(stream=flask.logging.wsgi_errors_stream) diff --git a/tests/test_aio.py b/tests/test_aio.py index cf69479a..4f34c279 100644 --- a/tests/test_aio.py +++ b/tests/test_aio.py @@ -143,6 +143,8 @@ async def http_func(request): wrapper = _http_func_wrapper(http_func, is_async=True) request = Mock() + request.headers = Mock() + request.headers.get = Mock(return_value="") response = await wrapper(request) assert response.__class__.__name__ == "JSONResponse" @@ -158,6 +160,8 @@ def sync_http_func(request): wrapper = _http_func_wrapper(sync_http_func, is_async=False) request = Mock() + request.headers = Mock() + request.headers.get = Mock(return_value="") response = await wrapper(request) assert response.__class__.__name__ == "Response" diff --git a/tests/test_execution_id.py b/tests/test_execution_id.py index a2601817..b8c5b9f0 100644 --- a/tests/test_execution_id.py +++ b/tests/test_execution_id.py @@ -223,6 +223,7 @@ def view_func(): monkeypatch.setattr( execution_id, "_generate_execution_id", lambda: TEST_EXECUTION_ID ) + mock_g = Mock() monkeypatch.setattr(execution_id.flask, "g", mock_g) monkeypatch.setattr(execution_id.flask, "has_request_context", lambda: True) diff --git a/tests/test_execution_id_async.py b/tests/test_execution_id_async.py new file mode 100644 index 00000000..01e638a1 --- /dev/null +++ b/tests/test_execution_id_async.py @@ -0,0 +1,365 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import asyncio +import json +import pathlib +import re + +from functools import partial +from unittest.mock import Mock + +import pytest + +from starlette.testclient import TestClient + +from functions_framework import execution_id +from functions_framework.aio import create_asgi_app + +TEST_FUNCTIONS_DIR = pathlib.Path(__file__).resolve().parent / "test_functions" +TEST_EXECUTION_ID = "test_execution_id" +TEST_SPAN_ID = "123456" + + +def test_user_function_can_retrieve_execution_id_from_header(): + source = TEST_FUNCTIONS_DIR / "execution_id" / "async_main.py" + target = "async_function" + app = create_asgi_app(target, source) + client = TestClient(app) + resp = client.post( + "/", + headers={ + "Function-Execution-Id": TEST_EXECUTION_ID, + "Content-Type": "application/json", + }, + ) + + assert resp.json()["execution_id"] == TEST_EXECUTION_ID + + +def test_uncaught_exception_in_user_function_sets_execution_id(capsys, monkeypatch): + monkeypatch.setenv("LOG_EXECUTION_ID", "true") + source = TEST_FUNCTIONS_DIR / "execution_id" / "async_main.py" + target = "async_error" + app = create_asgi_app(target, source) + # Don't raise server exceptions so we can capture the logs + client = TestClient(app, raise_server_exceptions=False) + resp = client.post( + "/", + headers={ + "Function-Execution-Id": TEST_EXECUTION_ID, + "Content-Type": "application/json", + }, + ) + assert resp.status_code == 500 + record = capsys.readouterr() + assert f'"execution_id": "{TEST_EXECUTION_ID}"' in record.err + assert '"logging.googleapis.com/labels"' in record.err + assert "ZeroDivisionError" in record.err + + +def test_print_from_user_function_sets_execution_id(capsys, monkeypatch): + monkeypatch.setenv("LOG_EXECUTION_ID", "true") + source = TEST_FUNCTIONS_DIR / "execution_id" / "async_main.py" + target = "async_print_message" + app = create_asgi_app(target, source) + client = TestClient(app) + client.post( + "/", + headers={ + "Function-Execution-Id": TEST_EXECUTION_ID, + "Content-Type": "application/json", + }, + json={"message": "some-message"}, + ) + record = capsys.readouterr() + assert f'"execution_id": "{TEST_EXECUTION_ID}"' in record.out + assert '"message": "some-message"' in record.out + + +def test_log_from_user_function_sets_execution_id(capsys, monkeypatch): + monkeypatch.setenv("LOG_EXECUTION_ID", "true") + source = TEST_FUNCTIONS_DIR / "execution_id" / "async_main.py" + target = "async_log_message" + app = create_asgi_app(target, source) + client = TestClient(app) + client.post( + "/", + headers={ + "Function-Execution-Id": TEST_EXECUTION_ID, + "Content-Type": "application/json", + }, + json={"message": json.dumps({"custom-field": "some-message"})}, + ) + record = capsys.readouterr() + assert f'"execution_id": "{TEST_EXECUTION_ID}"' in record.err + assert '"custom-field": "some-message"' in record.err + assert '"logging.googleapis.com/labels"' in record.err + + +def test_user_function_can_retrieve_generated_execution_id(monkeypatch): + monkeypatch.setattr( + execution_id, "_generate_execution_id", lambda: TEST_EXECUTION_ID + ) + source = TEST_FUNCTIONS_DIR / "execution_id" / "async_main.py" + target = "async_function" + app = create_asgi_app(target, source) + client = TestClient(app) + resp = client.post( + "/", + headers={ + "Content-Type": "application/json", + }, + ) + + assert resp.json()["execution_id"] == TEST_EXECUTION_ID + + +def test_does_not_set_execution_id_when_not_enabled(capsys): + source = TEST_FUNCTIONS_DIR / "execution_id" / "async_main.py" + target = "async_print_message" + app = create_asgi_app(target, source) + client = TestClient(app) + client.post( + "/", + headers={ + "Function-Execution-Id": TEST_EXECUTION_ID, + "Content-Type": "application/json", + }, + json={"message": "some-message"}, + ) + record = capsys.readouterr() + assert f'"execution_id": "{TEST_EXECUTION_ID}"' not in record.out + assert "some-message" in record.out + + +def test_does_not_set_execution_id_when_env_var_is_false(capsys, monkeypatch): + monkeypatch.setenv("LOG_EXECUTION_ID", "false") + source = TEST_FUNCTIONS_DIR / "execution_id" / "async_main.py" + target = "async_print_message" + app = create_asgi_app(target, source) + client = TestClient(app) + client.post( + "/", + headers={ + "Function-Execution-Id": TEST_EXECUTION_ID, + "Content-Type": "application/json", + }, + json={"message": "some-message"}, + ) + record = capsys.readouterr() + assert f'"execution_id": "{TEST_EXECUTION_ID}"' not in record.out + assert "some-message" in record.out + + +def test_does_not_set_execution_id_when_env_var_is_not_bool_like(capsys, monkeypatch): + monkeypatch.setenv("LOG_EXECUTION_ID", "maybe") + source = TEST_FUNCTIONS_DIR / "execution_id" / "async_main.py" + target = "async_print_message" + app = create_asgi_app(target, source) + client = TestClient(app) + client.post( + "/", + headers={ + "Function-Execution-Id": TEST_EXECUTION_ID, + "Content-Type": "application/json", + }, + json={"message": "some-message"}, + ) + record = capsys.readouterr() + assert f'"execution_id": "{TEST_EXECUTION_ID}"' not in record.out + assert "some-message" in record.out + + +def test_generate_execution_id(): + expected_matching_regex = "^[0-9a-zA-Z]{12}$" + actual_execution_id = execution_id._generate_execution_id() + + match = re.match(expected_matching_regex, actual_execution_id).group(0) + assert match == actual_execution_id + + +@pytest.mark.parametrize( + "headers,expected_execution_id,expected_span_id,should_generate", + [ + ( + { + "X-Cloud-Trace-Context": f"TRACE_ID/{TEST_SPAN_ID};o=1", + "Function-Execution-Id": TEST_EXECUTION_ID, + }, + TEST_EXECUTION_ID, + TEST_SPAN_ID, + False, + ), + ({}, None, None, True), # Middleware will generate an ID + ( + { + "X-Cloud-Trace-Context": "malformed trace context string", + "Function-Execution-Id": TEST_EXECUTION_ID, + }, + TEST_EXECUTION_ID, + None, + False, + ), + ], +) +def test_set_execution_context_headers( + headers, expected_execution_id, expected_span_id, should_generate +): + source = TEST_FUNCTIONS_DIR / "execution_id" / "async_main.py" + target = "async_trace_test" + app = create_asgi_app(target, source) + client = TestClient(app) + + resp = client.post("/", headers=headers) + + result = resp.json() + if should_generate: + # When no execution ID is provided, middleware generates one + assert result.get("execution_id") is not None + assert len(result.get("execution_id")) == 12 # Generated IDs are 12 chars + else: + assert result.get("execution_id") == expected_execution_id + assert result.get("span_id") == expected_span_id + + +@pytest.mark.asyncio +async def test_maintains_execution_id_for_concurrent_requests(monkeypatch, capsys): + monkeypatch.setenv("LOG_EXECUTION_ID", "true") + + expected_logs = ( + { + "message": "message1", + "logging.googleapis.com/labels": {"execution_id": "test-execution-id-1"}, + }, + { + "message": "message2", + "logging.googleapis.com/labels": {"execution_id": "test-execution-id-2"}, + }, + { + "message": "message1", + "logging.googleapis.com/labels": {"execution_id": "test-execution-id-1"}, + }, + { + "message": "message2", + "logging.googleapis.com/labels": {"execution_id": "test-execution-id-2"}, + }, + ) + + source = TEST_FUNCTIONS_DIR / "execution_id" / "async_main.py" + target = "async_sleep" + app = create_asgi_app(target, source) + client = TestClient(app) + loop = asyncio.get_event_loop() + response1 = loop.run_in_executor( + None, + partial( + client.post, + "/", + headers={ + "Content-Type": "application/json", + "Function-Execution-Id": "test-execution-id-1", + }, + json={"message": "message1"}, + ), + ) + response2 = loop.run_in_executor( + None, + partial( + client.post, + "/", + headers={ + "Content-Type": "application/json", + "Function-Execution-Id": "test-execution-id-2", + }, + json={"message": "message2"}, + ), + ) + await asyncio.wait((response1, response2)) + record = capsys.readouterr() + logs = record.err.strip().split("\n") + logs_as_json = tuple(json.loads(log) for log in logs) + + sort_key = lambda d: d["message"] + assert sorted(logs_as_json, key=sort_key) == sorted(expected_logs, key=sort_key) + + +def test_async_decorator_with_sync_function(): + def sync_func(request): + return {"status": "ok"} + + wrapped = execution_id.set_execution_context_async(enable_id_logging=False)( + sync_func + ) + + request = Mock() + request.headers = Mock() + request.headers.get = Mock(return_value="") + + result = wrapped(request) + + assert result == {"status": "ok"} + + +def test_sync_cloudevent_function_has_execution_context(monkeypatch, capsys): + """Test that sync CloudEvent functions can access execution context.""" + monkeypatch.setenv("LOG_EXECUTION_ID", "true") + + source = TEST_FUNCTIONS_DIR / "execution_id" / "async_main.py" + target = "sync_cloudevent_with_context" + app = create_asgi_app(target, source, signature_type="cloudevent") + client = TestClient(app) + + response = client.post( + "/", + headers={ + "ce-specversion": "1.0", + "ce-type": "com.example.test", + "ce-source": "test-source", + "ce-id": "test-id", + "Function-Execution-Id": TEST_EXECUTION_ID, + "Content-Type": "application/json", + }, + json={"message": "test"}, + ) + + assert response.status_code == 200 + assert response.text == "OK" + + record = capsys.readouterr() + assert f"Execution ID in sync CloudEvent: {TEST_EXECUTION_ID}" in record.err + assert "No execution context in sync CloudEvent function!" not in record.err + + +def test_cloudevent_returns_500(capsys, monkeypatch): + monkeypatch.setenv("LOG_EXECUTION_ID", "true") + source = TEST_FUNCTIONS_DIR / "execution_id" / "async_main.py" + target = "async_cloudevent_error" + app = create_asgi_app(target, source, signature_type="cloudevent") + client = TestClient(app, raise_server_exceptions=False) + resp = client.post( + "/", + headers={ + "ce-specversion": "1.0", + "ce-type": "com.example.test", + "ce-source": "test-source", + "ce-id": "test-id", + "Function-Execution-Id": TEST_EXECUTION_ID, + "Content-Type": "application/json", + }, + ) + assert resp.status_code == 500 + record = capsys.readouterr() + assert f'"execution_id": "{TEST_EXECUTION_ID}"' in record.err + assert '"logging.googleapis.com/labels"' in record.err + assert "ValueError" in record.err diff --git a/tests/test_functions/execution_id/async_main.py b/tests/test_functions/execution_id/async_main.py new file mode 100644 index 00000000..7149e7fb --- /dev/null +++ b/tests/test_functions/execution_id/async_main.py @@ -0,0 +1,62 @@ +import asyncio +import logging + +from functions_framework import execution_id + +logger = logging.getLogger(__name__) + + +async def async_print_message(request): + json = await request.json() + print(json.get("message")) + return {"status": "success"}, 200 + + +async def async_log_message(request): + json = await request.json() + logger.warning(json.get("message")) + return {"status": "success"}, 200 + + +async def async_function(request): + return {"execution_id": request.headers.get("Function-Execution-Id")} + + +async def async_error(request): + return 1 / 0 + + +async def async_sleep(request): + json = await request.json() + message = json.get("message") + logger.warning(message) + await asyncio.sleep(1) + logger.warning(message) + return {"status": "success"}, 200 + + +async def async_trace_test(request): + context = execution_id._get_current_context() + return { + "execution_id": context.execution_id if context else None, + "span_id": context.span_id if context else None, + } + + +def sync_function_in_async_context(request): + return { + "execution_id": request.headers.get("Function-Execution-Id"), + "type": "sync", + } + + +def sync_cloudevent_with_context(cloud_event): + context = execution_id._get_current_context() + if context: + logger.info(f"Execution ID in sync CloudEvent: {context.execution_id}") + else: + logger.error("No execution context in sync CloudEvent function!") + + +async def async_cloudevent_error(cloudevent): + raise ValueError("This is a test error") From 42b7fdd153f2360e3e2fc92a74d0674f424a99e4 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 18 Jun 2025 10:23:17 -0700 Subject: [PATCH 080/108] refactor: rename GATEWAY flag to FUNCTION_GATEWAY. (#379) --- src/functions_framework/_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/functions_framework/_cli.py b/src/functions_framework/_cli.py index e27b5446..ec2474d4 100644 --- a/src/functions_framework/_cli.py +++ b/src/functions_framework/_cli.py @@ -34,7 +34,7 @@ @click.option("--debug", envvar="DEBUG", is_flag=True) @click.option( "--gateway", - envvar="GATEWAY", + envvar="FUNCTION_GATEWAY", type=click.Choice(["wsgi", "asgi"]), default="wsgi", help="Server gateway interface type (wsgi for sync, asgi for async)", From a576a8f28a6029fc5b5ab0725d2aa9c6c5f4304f Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 18 Jun 2025 10:25:22 -0700 Subject: [PATCH 081/108] fix: set default log level for asgi logger to WARNING to match default python behavior (#381) * fix: set default log level for asgi logger to WARNING to match default python behavior. * fix: fix broken test. --- src/functions_framework/aio/__init__.py | 2 +- tests/test_functions/execution_id/async_main.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/functions_framework/aio/__init__.py b/src/functions_framework/aio/__init__.py index 4245f2d1..e30b5f99 100644 --- a/src/functions_framework/aio/__init__.py +++ b/src/functions_framework/aio/__init__.py @@ -172,7 +172,7 @@ def _configure_app_execution_id_logging(): "stream": "ext://functions_framework.execution_id.logging_stream", }, }, - "root": {"level": "INFO", "handlers": ["asgi"]}, + "root": {"level": "WARNING", "handlers": ["asgi"]}, } ) diff --git a/tests/test_functions/execution_id/async_main.py b/tests/test_functions/execution_id/async_main.py index 7149e7fb..4485e3f4 100644 --- a/tests/test_functions/execution_id/async_main.py +++ b/tests/test_functions/execution_id/async_main.py @@ -53,7 +53,7 @@ def sync_function_in_async_context(request): def sync_cloudevent_with_context(cloud_event): context = execution_id._get_current_context() if context: - logger.info(f"Execution ID in sync CloudEvent: {context.execution_id}") + logger.warning(f"Execution ID in sync CloudEvent: {context.execution_id}") else: logger.error("No execution context in sync CloudEvent function!") From 58deaf1e819c73ee38294f4dc8837bac0c533d35 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 23 Jun 2025 08:54:16 -0700 Subject: [PATCH 082/108] refactor: replace --gateway flag with --asgi boolean flag (#383) * refactor: replace --gateway flag with --asgi boolean flag Simplify the CLI by replacing `--gateway asgi` with `--asgi`. The new flag is more intuitive as WSGI remains the default and ASGI is opt-in. Also updates the environment variable to FUNCTION_USE_ASGI for clarity. * fix: update conformance tests to use --asgi flag --- .github/workflows/conformance-asgi.yml | 10 +++++----- src/functions_framework/_cli.py | 13 ++++++------- tests/test_cli.py | 2 +- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/.github/workflows/conformance-asgi.yml b/.github/workflows/conformance-asgi.yml index c69d1862..a62fcb71 100644 --- a/.github/workflows/conformance-asgi.yml +++ b/.github/workflows/conformance-asgi.yml @@ -52,7 +52,7 @@ jobs: functionType: 'http' useBuildpacks: false validateMapping: false - cmd: "'functions-framework --source tests/conformance/async_main.py --target write_http --signature-type http --gateway asgi'" + cmd: "'functions-framework --source tests/conformance/async_main.py --target write_http --signature-type http --asgi'" - name: Run CloudEvents conformance tests uses: GoogleCloudPlatform/functions-framework-conformance/action@72a4f36b10f1c6435ab1a86a9ea24bda464cc262 # v1.8.6 @@ -60,7 +60,7 @@ jobs: functionType: 'cloudevent' useBuildpacks: false validateMapping: false - cmd: "'functions-framework --source tests/conformance/async_main.py --target write_cloud_event --signature-type cloudevent --gateway asgi'" + cmd: "'functions-framework --source tests/conformance/async_main.py --target write_cloud_event --signature-type cloudevent --asgi'" - name: Run HTTP conformance tests declarative uses: GoogleCloudPlatform/functions-framework-conformance/action@72a4f36b10f1c6435ab1a86a9ea24bda464cc262 # v1.8.6 @@ -68,7 +68,7 @@ jobs: functionType: 'http' useBuildpacks: false validateMapping: false - cmd: "'functions-framework --source tests/conformance/async_main.py --target write_http_declarative --gateway asgi'" + cmd: "'functions-framework --source tests/conformance/async_main.py --target write_http_declarative --asgi'" - name: Run CloudEvents conformance tests declarative uses: GoogleCloudPlatform/functions-framework-conformance/action@72a4f36b10f1c6435ab1a86a9ea24bda464cc262 # v1.8.6 @@ -76,7 +76,7 @@ jobs: functionType: 'cloudevent' useBuildpacks: false validateMapping: false - cmd: "'functions-framework --source tests/conformance/async_main.py --target write_cloud_event_declarative --gateway asgi'" + cmd: "'functions-framework --source tests/conformance/async_main.py --target write_cloud_event_declarative --asgi'" - name: Run HTTP concurrency tests declarative uses: GoogleCloudPlatform/functions-framework-conformance/action@72a4f36b10f1c6435ab1a86a9ea24bda464cc262 # v1.8.6 @@ -84,7 +84,7 @@ jobs: functionType: 'http' useBuildpacks: false validateConcurrency: true - cmd: "'functions-framework --source tests/conformance/async_main.py --target write_http_declarative_concurrent --gateway asgi'" + cmd: "'functions-framework --source tests/conformance/async_main.py --target write_http_declarative_concurrent --asgi'" # Note: Event (legacy) and Typed tests are not supported in ASGI mode # Note: validateMapping is set to false for CloudEvent tests because ASGI mode diff --git a/src/functions_framework/_cli.py b/src/functions_framework/_cli.py index ec2474d4..c2ba9f4b 100644 --- a/src/functions_framework/_cli.py +++ b/src/functions_framework/_cli.py @@ -33,14 +33,13 @@ @click.option("--port", envvar="PORT", type=click.INT, default=8080) @click.option("--debug", envvar="DEBUG", is_flag=True) @click.option( - "--gateway", - envvar="FUNCTION_GATEWAY", - type=click.Choice(["wsgi", "asgi"]), - default="wsgi", - help="Server gateway interface type (wsgi for sync, asgi for async)", + "--asgi", + envvar="FUNCTION_USE_ASGI", + is_flag=True, + help="Use ASGI server for function execution", ) -def _cli(target, source, signature_type, host, port, debug, gateway): - if gateway == "asgi": # pragma: no cover +def _cli(target, source, signature_type, host, port, debug, asgi): + if asgi: # pragma: no cover from functions_framework.aio import create_asgi_app app = create_asgi_app(target, source, signature_type) diff --git a/tests/test_cli.py b/tests/test_cli.py index 17445d11..4e5a0a08 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -119,7 +119,7 @@ def test_asgi_cli(monkeypatch): monkeypatch.setattr(functions_framework._cli, "create_server", create_server) runner = CliRunner() - result = runner.invoke(_cli, ["--target", "foo", "--gateway", "asgi"]) + result = runner.invoke(_cli, ["--target", "foo", "--asgi"]) assert result.exit_code == 0 assert create_asgi_app.calls == [pretend.call("foo", None, "http")] From 2de6eec6fae132b8b1fb41e7024a2260a05bc072 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 22 Jul 2025 11:51:04 -0700 Subject: [PATCH 083/108] fix: resolve CI failures for egress policies and Python 3.7 buildpack support (#388) * fix: add GitHub Actions CDN to egress allowlist The conformance workflow was failing with ECONNREFUSED errors when trying to download Python binaries from GitHub releases. This was caused by the harden-runner egress policy blocking connections to the GitHub Actions CDN IP addresses. Added *.actions.githubusercontent.com:443 to the allowed endpoints to fix Python setup for all versions (3.7, 3.8, etc). * fix: remove Python 3.7 from buildpack integration tests Google Cloud Buildpacks dropped Python 3.7 support for Ubuntu 22.04. The version is not available in their runtime manifest. Note: Functions Framework still supports Python 3.7, which is tested in unit and conformance tests using GitHub Actions with Ubuntu 20.04. * fix: use correct domain for GitHub release assets The Python binaries are actually hosted on release-assets.githubusercontent.com, not *.actions.githubusercontent.com * fix: add release-assets domain to unit and conformance-asgi workflows The same ECONNREFUSED issue was affecting multiple workflows with harden-runner egress policies --- .github/workflows/buildpack-integration-test.yml | 11 ----------- .github/workflows/conformance-asgi.yml | 1 + .github/workflows/conformance.yml | 1 + .github/workflows/unit.yml | 1 + 4 files changed, 3 insertions(+), 11 deletions(-) diff --git a/.github/workflows/buildpack-integration-test.yml b/.github/workflows/buildpack-integration-test.yml index 234c24ef..2c028fa9 100644 --- a/.github/workflows/buildpack-integration-test.yml +++ b/.github/workflows/buildpack-integration-test.yml @@ -14,17 +14,6 @@ on: permissions: read-all jobs: - python37: - uses: GoogleCloudPlatform/functions-framework-conformance/.github/workflows/buildpack-integration-test.yml@main - with: - http-builder-source: 'tests/conformance' - http-builder-target: 'write_http_declarative' - cloudevent-builder-source: 'tests/conformance' - cloudevent-builder-target: 'write_cloud_event_declarative' - prerun: 'tests/conformance/prerun.sh ${{ github.sha }}' - builder-runtime: 'python37' - builder-runtime-version: '3.7' - start-delay: 5 python38: uses: GoogleCloudPlatform/functions-framework-conformance/.github/workflows/buildpack-integration-test.yml@main with: diff --git a/.github/workflows/conformance-asgi.yml b/.github/workflows/conformance-asgi.yml index a62fcb71..904a7773 100644 --- a/.github/workflows/conformance-asgi.yml +++ b/.github/workflows/conformance-asgi.yml @@ -29,6 +29,7 @@ jobs: proxy.golang.org:443 pypi.org:443 storage.googleapis.com:443 + release-assets.githubusercontent.com:443 - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 9dde6036..7d10b8af 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -34,6 +34,7 @@ jobs: proxy.golang.org:443 pypi.org:443 storage.googleapis.com:443 + release-assets.githubusercontent.com:443 - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 32b34fdb..28ed5b1e 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -54,6 +54,7 @@ jobs: production.cloudflare.docker.com:443 pypi.org:443 registry-1.docker.io:443 + release-assets.githubusercontent.com:443 - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 From 82ba117e1309c52e6ba8c11c5d8cb22f253c256d Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 22 Jul 2025 12:20:03 -0700 Subject: [PATCH 084/108] refactor: move async dependencies from optional to direct (#386) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This PR moves starlette, uvicorn, and uvicorn-worker from optional dependencies to direct dependencies, simplifying the installation process for users who need async functionality. ## Impact Analysis - **Installation size increase: < 1MB** (specifically 816KB, ~3.5% increase) - Base installation: 23MB → With async deps: 24MB - Removes the need for `pip install functions-framework[async]` - Maintains Python 3.8+ requirement for these dependencies ## Changes - Move async dependencies to direct dependencies in `pyproject.toml` - Remove `[async]` extra dependency configuration - Update imports to be direct instead of conditional - Remove "Starlette is not installed" error messages - Update tests to reflect direct dependency availability ## Test Plan All existing tests pass with 100% coverage. The async functionality remains unchanged, just the installation method is simplified. --- .github/workflows/conformance-asgi.yml | 4 ++-- pyproject.toml | 10 +++------- src/functions_framework/aio/__init__.py | 19 ++++++------------- tests/test_aio.py | 23 ----------------------- tests/test_asgi.py | 7 ++----- tox.ini | 4 ---- 6 files changed, 13 insertions(+), 54 deletions(-) diff --git a/.github/workflows/conformance-asgi.yml b/.github/workflows/conformance-asgi.yml index 904a7773..8a61cd4c 100644 --- a/.github/workflows/conformance-asgi.yml +++ b/.github/workflows/conformance-asgi.yml @@ -39,8 +39,8 @@ jobs: with: python-version: ${{ matrix.python }} - - name: Install the framework with async extras - run: python -m pip install -e .[async] + - name: Install the framework + run: python -m pip install -e . - name: Setup Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 diff --git a/pyproject.toml b/pyproject.toml index 2b6e0639..9aa34365 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,18 +30,14 @@ dependencies = [ "cloudevents>=1.2.0,<=1.11.0", # Must support python 3.7 "Werkzeug>=0.14,<4.0.0", "httpx>=0.24.1", + "starlette>=0.37.0,<1.0.0; python_version>='3.8'", + "uvicorn>=0.18.0,<1.0.0; python_version>='3.8'", + "uvicorn-worker>=0.2.0,<1.0.0; python_version>='3.8'", ] [project.urls] Homepage = "https://github.com/googlecloudplatform/functions-framework-python" -[project.optional-dependencies] -async = [ - "starlette>=0.37.0,<1.0.0; python_version>='3.8'", - "uvicorn>=0.18.0,<1.0.0; python_version>='3.8'", - "uvicorn-worker>=0.2.0,<1.0.0; python_version>='3.8'" -] - [project.scripts] ff = "functions_framework._cli:_cli" functions-framework = "functions_framework._cli:_cli" diff --git a/src/functions_framework/aio/__init__.py b/src/functions_framework/aio/__init__.py index e30b5f99..8e5f9dc7 100644 --- a/src/functions_framework/aio/__init__.py +++ b/src/functions_framework/aio/__init__.py @@ -25,6 +25,12 @@ from cloudevents.http import from_http from cloudevents.http.event import CloudEvent +from starlette.applications import Starlette +from starlette.exceptions import HTTPException +from starlette.middleware import Middleware +from starlette.requests import Request +from starlette.responses import JSONResponse, Response +from starlette.routing import Route from functions_framework import ( _enable_execution_id_logging, @@ -36,19 +42,6 @@ MissingSourceException, ) -try: - from starlette.applications import Starlette - from starlette.exceptions import HTTPException - from starlette.middleware import Middleware - from starlette.requests import Request - from starlette.responses import JSONResponse, Response - from starlette.routing import Route -except ImportError: - raise FunctionsFrameworkException( - "Starlette is not installed. Install the framework with the 'async' extra: " - "pip install functions-framework[async]" - ) - HTTPResponse = Union[ Response, # Functions can return a full Starlette Response object str, # Str returns are wrapped in Response(result) diff --git a/tests/test_aio.py b/tests/test_aio.py index 4f34c279..e7533b1d 100644 --- a/tests/test_aio.py +++ b/tests/test_aio.py @@ -35,29 +35,6 @@ TEST_FUNCTIONS_DIR = pathlib.Path(__file__).resolve().parent / "test_functions" -def test_import_error_without_starlette(monkeypatch): - import builtins - - original_import = builtins.__import__ - - def mock_import(name, *args, **kwargs): - if name.startswith("starlette"): - raise ImportError(f"No module named '{name}'") - return original_import(name, *args, **kwargs) - - monkeypatch.setattr(builtins, "__import__", mock_import) - - # Remove the module from sys.modules to force re-import - if "functions_framework.aio" in sys.modules: - del sys.modules["functions_framework.aio"] - - with pytest.raises(exceptions.FunctionsFrameworkException) as excinfo: - import functions_framework.aio - - assert "Starlette is not installed" in str(excinfo.value) - assert "pip install functions-framework[async]" in str(excinfo.value) - - def test_invalid_function_definition_missing_function_file(): source = TEST_FUNCTIONS_DIR / "missing_function_file" / "main.py" target = "function" diff --git a/tests/test_asgi.py b/tests/test_asgi.py index cd117bd3..e5b97e60 100644 --- a/tests/test_asgi.py +++ b/tests/test_asgi.py @@ -18,12 +18,9 @@ import pretend import pytest -import functions_framework._http +from starlette.applications import Starlette -try: - from starlette.applications import Starlette -except ImportError: - pass +import functions_framework._http def test_httpserver_detects_asgi_app(): diff --git a/tox.ini b/tox.ini index fd3e38a6..cb0873b6 100644 --- a/tox.ini +++ b/tox.ini @@ -29,8 +29,6 @@ deps = pytest-cov pytest-integration pretend -extras = - async setenv = PYTESTARGS = --cov=functions_framework --cov-branch --cov-report term-missing --cov-fail-under=100 # Python 3.7: Use .coveragerc-py37 to exclude aio module from coverage since it requires Python 3.8+ (Starlette dependency) @@ -48,8 +46,6 @@ deps = isort mypy build -extras = - async commands = black --check src tests conftest.py --exclude tests/test_functions/background_load_error/main.py isort -c src tests conftest.py From ef48e70ee21432a5c7ff014e064b8424254ef289 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 22 Jul 2025 17:50:07 -0700 Subject: [PATCH 085/108] feat: auto-detect ASGI mode for @aio decorated functions (#387) ## Summary - Implement automatic ASGI mode detection for functions decorated with `@aio.http` or `@aio.cloud_event` - The approach creates a Flask app first, loads the module within its context, then checks if ASGI is needed - This results in an unused Flask app for ASGI functions, but we accept this memory overhead as a trade-off - The `--asgi` CLI flag still works and skips the Flask app creation for optimization ## Implementation Details - Added `ASGI_FUNCTIONS` set to `_function_registry.py` to track functions that require ASGI - Updated `@aio.http` and `@aio.cloud_event` decorators to register functions in `ASGI_FUNCTIONS` - Modified `create_app()` to auto-detect ASGI requirements after module loading: 1. Always creates a Flask app first 2. Loads the user module within Flask app context 3. Checks if target function is in `ASGI_FUNCTIONS` registry 4. If ASGI is needed, delegates to `create_asgi_app_from_module()` - The `--asgi` CLI flag continues to work, bypassing Flask app creation entirely for performance ## Trade-offs - **Memory overhead**: ASGI functions will have an unused Flask app instance created during auto-detection - **Accepted trade-off**: This avoids loading modules twice which could cause side effects - **Optimization available**: Users can still use `--asgi` flag to skip Flask app creation entirely ## Test plan - [x] Added tests to verify decorators register functions in `ASGI_FUNCTIONS` - [x] Added CLI tests to verify auto-detection works for `@aio` decorated functions - [x] Added CLI tests to verify regular functions still use Flask/WSGI mode - [x] Added proper test isolation with registry cleanup fixtures - [x] All existing tests pass - [x] Linting passes --- .coveragerc-py37 | 9 ++-- src/functions_framework/__init__.py | 28 +++++++++++ src/functions_framework/_cli.py | 5 +- src/functions_framework/_function_registry.py | 4 ++ src/functions_framework/aio/__init__.py | 30 ++++++++++++ tests/test_cli.py | 39 +++++++++++++++ tests/test_decorator_functions.py | 47 +++++++++++++++++++ tests/test_function_registry.py | 16 +++++++ tests/test_functions.py | 2 +- 9 files changed, 170 insertions(+), 10 deletions(-) diff --git a/.coveragerc-py37 b/.coveragerc-py37 index b1c98d23..efb63fec 100644 --- a/.coveragerc-py37 +++ b/.coveragerc-py37 @@ -12,14 +12,11 @@ omit = [report] exclude_lines = - # Have to re-enable the standard pragma pragma: no cover - - # Don't complain about async-specific imports and code from functions_framework.aio import from functions_framework._http.asgi import from functions_framework._http.gunicorn import UvicornApplication - - # Exclude async-specific classes and functions in execution_id.py class AsgiMiddleware: - def set_execution_context_async \ No newline at end of file + def set_execution_context_async + return create_asgi_app_from_module + app = create_asgi_app\(target, source, signature_type\) \ No newline at end of file diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index 22fbf44c..31169f4c 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -327,6 +327,16 @@ def crash_handler(e): def create_app(target=None, source=None, signature_type=None): + """Create an app for the function. + + Args: + target: The name of the target function to invoke + source: The source file containing the function + signature_type: The signature type of the function + + Returns: + A Flask WSGI app or Starlette ASGI app depending on function decorators + """ target = _function_registry.get_function_target(target) source = _function_registry.get_function_source(source) @@ -370,6 +380,7 @@ def handle_none(rv): setup_logging() _app.wsgi_app = execution_id.WsgiMiddleware(_app.wsgi_app) + # Execute the module, within the application context with _app.app_context(): try: @@ -394,6 +405,23 @@ def function(*_args, **_kwargs): # command fails. raise e from None + use_asgi = target in _function_registry.ASGI_FUNCTIONS + if use_asgi: + # This function needs ASGI, delegate to create_asgi_app + # Note: @aio decorators only register functions in ASGI_FUNCTIONS when the + # module is imported. We can't know if a function uses @aio until after + # we load the module. + # + # To avoid loading modules twice, we always create a Flask app first, load the + # module within its context, then check if ASGI is needed. This results in an + # unused Flask app for ASGI functions, but we accept this memory overhead as a + # trade-off. + from functions_framework.aio import create_asgi_app_from_module + + return create_asgi_app_from_module( + target, source, signature_type, source_module, spec + ) + # Get the configured function signature type signature_type = _function_registry.get_func_signature_type(target, signature_type) diff --git a/src/functions_framework/_cli.py b/src/functions_framework/_cli.py index c2ba9f4b..48455ea6 100644 --- a/src/functions_framework/_cli.py +++ b/src/functions_framework/_cli.py @@ -16,7 +16,7 @@ import click -from functions_framework import create_app +from functions_framework import _function_registry, create_app from functions_framework._http import create_server @@ -39,11 +39,10 @@ help="Use ASGI server for function execution", ) def _cli(target, source, signature_type, host, port, debug, asgi): - if asgi: # pragma: no cover + if asgi: from functions_framework.aio import create_asgi_app app = create_asgi_app(target, source, signature_type) else: app = create_app(target, source, signature_type) - create_server(app, debug).run(host, port) diff --git a/src/functions_framework/_function_registry.py b/src/functions_framework/_function_registry.py index 2214b5fd..1f08c794 100644 --- a/src/functions_framework/_function_registry.py +++ b/src/functions_framework/_function_registry.py @@ -40,6 +40,10 @@ # Keys are the user function name, values are the type of the function input INPUT_TYPE_MAP = {} +# ASGI_FUNCTIONS stores function names that require ASGI mode. +# Functions decorated with @aio.http or @aio.cloud_event are added here. +ASGI_FUNCTIONS = set() + def get_user_function(source, source_module, target): """Returns user function, raises exception for invalid function.""" diff --git a/src/functions_framework/aio/__init__.py b/src/functions_framework/aio/__init__.py index 8e5f9dc7..a56fe942 100644 --- a/src/functions_framework/aio/__init__.py +++ b/src/functions_framework/aio/__init__.py @@ -62,6 +62,7 @@ def cloud_event(func: CloudEventFunction) -> CloudEventFunction: _function_registry.REGISTRY_MAP[func.__name__] = ( _function_registry.CLOUDEVENT_SIGNATURE_TYPE ) + _function_registry.ASGI_FUNCTIONS.add(func.__name__) if inspect.iscoroutinefunction(func): @functools.wraps(func) @@ -82,6 +83,7 @@ def http(func: HTTPFunction) -> HTTPFunction: _function_registry.REGISTRY_MAP[func.__name__] = ( _function_registry.HTTP_SIGNATURE_TYPE ) + _function_registry.ASGI_FUNCTIONS.add(func.__name__) if inspect.iscoroutinefunction(func): @@ -213,6 +215,29 @@ async def __call__(self, scope, receive, send): # Don't re-raise to prevent starlette from printing traceback again +def create_asgi_app_from_module(target, source, signature_type, source_module, spec): + """Create an ASGI application from an already-loaded module. + + Args: + target: The name of the target function to invoke + source: The source file containing the function + signature_type: The signature type of the function + source_module: The already-loaded module + spec: The module spec + + Returns: + A Starlette ASGI application instance + """ + enable_id_logging = _enable_execution_id_logging() + if enable_id_logging: # pragma: no cover + _configure_app_execution_id_logging() + + function = _function_registry.get_user_function(source, source_module, target) + signature_type = _function_registry.get_func_signature_type(target, signature_type) + + return _create_asgi_app_with_function(function, signature_type, enable_id_logging) + + def create_asgi_app(target=None, source=None, signature_type=None): """Create an ASGI application for the function. @@ -243,6 +268,11 @@ def create_asgi_app(target=None, source=None, signature_type=None): function = _function_registry.get_user_function(source, source_module, target) signature_type = _function_registry.get_func_signature_type(target, signature_type) + return _create_asgi_app_with_function(function, signature_type, enable_id_logging) + + +def _create_asgi_app_with_function(function, signature_type, enable_id_logging): + """Create an ASGI app with the given function and signature type.""" is_async = inspect.iscoroutinefunction(function) routes = [] if signature_type == _function_registry.HTTP_SIGNATURE_TYPE: diff --git a/tests/test_cli.py b/tests/test_cli.py index 4e5a0a08..75c93f20 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os +import pathlib import sys import pretend @@ -20,9 +22,30 @@ from click.testing import CliRunner import functions_framework +import functions_framework._function_registry as _function_registry from functions_framework._cli import _cli +# Conditional import for Starlette (Python 3.8+) +if sys.version_info >= (3, 8): + from starlette.applications import Starlette +else: + Starlette = None + + +@pytest.fixture(autouse=True) +def clean_registries(): + """Clean up both REGISTRY_MAP and ASGI_FUNCTIONS registries.""" + original_registry_map = _function_registry.REGISTRY_MAP.copy() + original_asgi = _function_registry.ASGI_FUNCTIONS.copy() + _function_registry.REGISTRY_MAP.clear() + _function_registry.ASGI_FUNCTIONS.clear() + yield + _function_registry.REGISTRY_MAP.clear() + _function_registry.REGISTRY_MAP.update(original_registry_map) + _function_registry.ASGI_FUNCTIONS.clear() + _function_registry.ASGI_FUNCTIONS.update(original_asgi) + def test_cli_no_arguments(): runner = CliRunner() @@ -124,3 +147,19 @@ def test_asgi_cli(monkeypatch): assert result.exit_code == 0 assert create_asgi_app.calls == [pretend.call("foo", None, "http")] assert asgi_server.run.calls == [pretend.call("0.0.0.0", 8080)] + + +def test_cli_auto_detects_asgi_decorator(): + """Test that CLI auto-detects @aio decorated functions without --asgi flag.""" + # Use the actual async_decorator.py test file which has @aio.http decorated functions + test_functions_dir = pathlib.Path(__file__).parent / "test_functions" / "decorators" + source = test_functions_dir / "async_decorator.py" + + # Call create_app without any asgi flag - should auto-detect + app = functions_framework.create_app(target="function_http", source=str(source)) + + # Verify it created a Starlette app (ASGI) + assert isinstance(app, Starlette) + + # Verify the function was registered in ASGI_FUNCTIONS + assert "function_http" in _function_registry.ASGI_FUNCTIONS diff --git a/tests/test_decorator_functions.py b/tests/test_decorator_functions.py index 435aa815..3a6e5e99 100644 --- a/tests/test_decorator_functions.py +++ b/tests/test_decorator_functions.py @@ -19,6 +19,8 @@ from cloudevents import conversion as ce_conversion from cloudevents.http import CloudEvent +import functions_framework._function_registry as registry + # Conditional import for Starlette if sys.version_info >= (3, 8): from starlette.testclient import TestClient as StarletteTestClient @@ -35,6 +37,21 @@ TEST_FUNCTIONS_DIR = pathlib.Path(__file__).resolve().parent / "test_functions" + +@pytest.fixture(autouse=True) +def clean_registries(): + """Clean up both REGISTRY_MAP and ASGI_FUNCTIONS registries.""" + original_registry_map = registry.REGISTRY_MAP.copy() + original_asgi = registry.ASGI_FUNCTIONS.copy() + registry.REGISTRY_MAP.clear() + registry.ASGI_FUNCTIONS.clear() + yield + registry.REGISTRY_MAP.clear() + registry.REGISTRY_MAP.update(original_registry_map) + registry.ASGI_FUNCTIONS.clear() + registry.ASGI_FUNCTIONS.update(original_asgi) + + # Python 3.5: ModuleNotFoundError does not exist try: _ModuleNotFoundError = ModuleNotFoundError @@ -128,3 +145,33 @@ def test_aio_http_dict_response(): resp = client.post("/") assert resp.status_code == 200 assert resp.json() == {"message": "hello", "count": 42, "success": True} + + +def test_aio_decorators_register_asgi_functions(): + """Test that @aio decorators add function names to ASGI_FUNCTIONS registry.""" + from functions_framework.aio import cloud_event, http + + @http + async def test_http_func(request): + return "test" + + @cloud_event + async def test_cloud_event_func(event): + pass + + assert "test_http_func" in registry.ASGI_FUNCTIONS + assert "test_cloud_event_func" in registry.ASGI_FUNCTIONS + + assert registry.REGISTRY_MAP["test_http_func"] == "http" + assert registry.REGISTRY_MAP["test_cloud_event_func"] == "cloudevent" + + @http + def test_http_sync(request): + return "sync" + + @cloud_event + def test_cloud_event_sync(event): + pass + + assert "test_http_sync" in registry.ASGI_FUNCTIONS + assert "test_cloud_event_sync" in registry.ASGI_FUNCTIONS diff --git a/tests/test_function_registry.py b/tests/test_function_registry.py index e3ae3c7e..5b517cdc 100644 --- a/tests/test_function_registry.py +++ b/tests/test_function_registry.py @@ -13,9 +13,25 @@ # limitations under the License. import os +import pytest + from functions_framework import _function_registry +@pytest.fixture(autouse=True) +def clean_registries(): + """Clean up both REGISTRY_MAP and ASGI_FUNCTIONS registries.""" + original_registry_map = _function_registry.REGISTRY_MAP.copy() + original_asgi = _function_registry.ASGI_FUNCTIONS.copy() + _function_registry.REGISTRY_MAP.clear() + _function_registry.ASGI_FUNCTIONS.clear() + yield + _function_registry.REGISTRY_MAP.clear() + _function_registry.REGISTRY_MAP.update(original_registry_map) + _function_registry.ASGI_FUNCTIONS.clear() + _function_registry.ASGI_FUNCTIONS.update(original_asgi) + + def test_get_function_signature(): test_cases = [ { diff --git a/tests/test_functions.py b/tests/test_functions.py index 534f4a88..bafb5e8b 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -495,7 +495,7 @@ def test_error_paths(http_trigger_client, path): def test_lazy_wsgi_app(monkeypatch, target, source, signature_type): actual_app_stub = pretend.stub() wsgi_app = pretend.call_recorder(lambda *a, **kw: actual_app_stub) - create_app = pretend.call_recorder(lambda *a: wsgi_app) + create_app = pretend.call_recorder(lambda *a, **kw: wsgi_app) monkeypatch.setattr(functions_framework, "create_app", create_app) # Test that it's lazy From d5ac6b4329683993a15d05a19ffdc97c57e04759 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 16:04:49 -0700 Subject: [PATCH 086/108] chore(main): release 3.9.0 (#374) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 18 ++++++++++++++++++ pyproject.toml | 2 +- setup.py | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8231717d..465d5c1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.9.0](https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v3.8.3...v3.9.0) (2025-07-23) + + +### Features + +* add execution_id support for async stack ([#377](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/377)) ([1123eea](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/1123eeac8cedae23af8980a928f01f5ad100d9de)) +* add flag to run functions framework in asgi stack ([#376](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/376)) ([268acf1](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/268acf121015bf2a5592715e4cfe582f9d236ff8)) +* add support for async functions ([#364](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/364)) ([49f6985](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/49f698517a06d0e47b2aadc2d603e0b193770440)) +* auto-detect ASGI mode for [@aio](https://github.com/aio) decorated functions ([#387](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/387)) ([ef48e70](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/ef48e70ee21432a5c7ff014e064b8424254ef289)) + + +### Bug Fixes + +* **ci:** specify python version in tox environment ([#375](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/375)) ([37e0bf7](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/37e0bf764ff24ebb82ba18bcac1bee6b03cecb13)) +* Pin cloudevent sdk version to support python3.7. ([#373](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/373)) ([cc2b9b5](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/cc2b9b584fcc3daaa2762ae62a3ce1277a488a1c)) +* resolve CI failures for egress policies and Python 3.7 buildpack support ([#388](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/388)) ([2de6eec](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/2de6eec6fae132b8b1fb41e7024a2260a05bc072)) +* set default log level for asgi logger to WARNING to match default python behavior ([#381](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/381)) ([a576a8f](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/a576a8f28a6029fc5b5ab0725d2aa9c6c5f4304f)) + ## [3.8.3](https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v3.8.2...v3.8.3) (2025-05-14) diff --git a/pyproject.toml b/pyproject.toml index 9aa34365..2fcbabbc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "functions-framework" -version = "3.8.3" +version = "3.9.0" description = "An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team." readme = "README.md" requires-python = ">=3.7, <4" diff --git a/setup.py b/setup.py index 1c35d39b..1dd17d7a 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( name="functions-framework", - version="3.8.2", + version="3.9.0", description="An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team.", long_description=long_description, long_description_content_type="text/markdown", From 26fb101d556a39262855a304c1ac351856ce4b61 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 24 Jul 2025 10:28:56 -0700 Subject: [PATCH 087/108] fix: remove unused httpx dependency (#389) httpx was accidentally added in #364 but is not used anywhere in the codebase. Removing it reduces our dependency count from 28 to 20 packages. Impact: - Removes 8 unnecessary packages (httpx + 7 transitive deps) - Reduces installation size by ~2MB - Keeps only the actually needed async dependencies --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2fcbabbc..a7c1778a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,6 @@ dependencies = [ "gunicorn>=22.0.0; platform_system!='Windows'", "cloudevents>=1.2.0,<=1.11.0", # Must support python 3.7 "Werkzeug>=0.14,<4.0.0", - "httpx>=0.24.1", "starlette>=0.37.0,<1.0.0; python_version>='3.8'", "uvicorn>=0.18.0,<1.0.0; python_version>='3.8'", "uvicorn-worker>=0.2.0,<1.0.0; python_version>='3.8'", From a1c557c26588ebeacd6852ddbce9c7866006bc39 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 24 Jul 2025 19:29:35 +0200 Subject: [PATCH 088/108] chore(deps): update pypa/gh-action-pypi-publish digest to e9ccbe5 (#369) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 269f14c7..77207e54 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: - name: Build distributions run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@db8f07d3871a0a180efa06b95d467625c19d5d5f # main + uses: pypa/gh-action-pypi-publish@e9ccbe5a211ba3e8363f472cae362b56b104e796 # main with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} From 0c643782d30d7295c10a3ad5ffd1e5f448bfd16f Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 24 Jul 2025 19:52:44 +0200 Subject: [PATCH 089/108] chore(deps): update all non-major dependencies (#370) * chore(deps): update all non-major dependencies * keep cloudevents version --------- Co-authored-by: Andras Kerekes --- .github/workflows/codeql.yml | 8 ++++---- .github/workflows/conformance-asgi.yml | 12 ++++++------ .github/workflows/conformance.yml | 16 ++++++++-------- .github/workflows/dependency-review.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/scorecard.yml | 6 +++--- .github/workflows/unit.yml | 2 +- pyproject.toml | 2 +- 9 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 46f6a4d1..270f890c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: disable-sudo: true egress-policy: block @@ -57,7 +57,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 + uses: github/codeql-action/init@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.29.4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -67,7 +67,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 + uses: github/codeql-action/autobuild@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.29.4 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -80,6 +80,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 + uses: github/codeql-action/analyze@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.29.4 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/conformance-asgi.yml b/.github/workflows/conformance-asgi.yml index 8a61cd4c..b3f3601d 100644 --- a/.github/workflows/conformance-asgi.yml +++ b/.github/workflows/conformance-asgi.yml @@ -17,7 +17,7 @@ jobs: runs-on: ${{ matrix.platform }} steps: - name: Harden Runner - uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: disable-sudo: true egress-policy: block @@ -48,7 +48,7 @@ jobs: go-version: '1.24' - name: Run HTTP conformance tests - uses: GoogleCloudPlatform/functions-framework-conformance/action@72a4f36b10f1c6435ab1a86a9ea24bda464cc262 # v1.8.6 + uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 with: functionType: 'http' useBuildpacks: false @@ -56,7 +56,7 @@ jobs: cmd: "'functions-framework --source tests/conformance/async_main.py --target write_http --signature-type http --asgi'" - name: Run CloudEvents conformance tests - uses: GoogleCloudPlatform/functions-framework-conformance/action@72a4f36b10f1c6435ab1a86a9ea24bda464cc262 # v1.8.6 + uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 with: functionType: 'cloudevent' useBuildpacks: false @@ -64,7 +64,7 @@ jobs: cmd: "'functions-framework --source tests/conformance/async_main.py --target write_cloud_event --signature-type cloudevent --asgi'" - name: Run HTTP conformance tests declarative - uses: GoogleCloudPlatform/functions-framework-conformance/action@72a4f36b10f1c6435ab1a86a9ea24bda464cc262 # v1.8.6 + uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 with: functionType: 'http' useBuildpacks: false @@ -72,7 +72,7 @@ jobs: cmd: "'functions-framework --source tests/conformance/async_main.py --target write_http_declarative --asgi'" - name: Run CloudEvents conformance tests declarative - uses: GoogleCloudPlatform/functions-framework-conformance/action@72a4f36b10f1c6435ab1a86a9ea24bda464cc262 # v1.8.6 + uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 with: functionType: 'cloudevent' useBuildpacks: false @@ -80,7 +80,7 @@ jobs: cmd: "'functions-framework --source tests/conformance/async_main.py --target write_cloud_event_declarative --asgi'" - name: Run HTTP concurrency tests declarative - uses: GoogleCloudPlatform/functions-framework-conformance/action@72a4f36b10f1c6435ab1a86a9ea24bda464cc262 # v1.8.6 + uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 with: functionType: 'http' useBuildpacks: false diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 7d10b8af..9eedb0a2 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -22,7 +22,7 @@ jobs: runs-on: ${{ matrix.platform }} steps: - name: Harden Runner - uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: disable-sudo: true egress-policy: block @@ -53,7 +53,7 @@ jobs: go-version: '1.24' - name: Run HTTP conformance tests - uses: GoogleCloudPlatform/functions-framework-conformance/action@72a4f36b10f1c6435ab1a86a9ea24bda464cc262 # v1.8.6 + uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 with: functionType: 'http' useBuildpacks: false @@ -61,7 +61,7 @@ jobs: cmd: "'functions-framework --source tests/conformance/main.py --target write_http --signature-type http'" - name: Run event conformance tests - uses: GoogleCloudPlatform/functions-framework-conformance/action@72a4f36b10f1c6435ab1a86a9ea24bda464cc262 # v1.8.6 + uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 with: functionType: 'legacyevent' useBuildpacks: false @@ -69,7 +69,7 @@ jobs: cmd: "'functions-framework --source tests/conformance/main.py --target write_legacy_event --signature-type event'" - name: Run CloudEvents conformance tests - uses: GoogleCloudPlatform/functions-framework-conformance/action@72a4f36b10f1c6435ab1a86a9ea24bda464cc262 # v1.8.6 + uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 with: functionType: 'cloudevent' useBuildpacks: false @@ -77,7 +77,7 @@ jobs: cmd: "'functions-framework --source tests/conformance/main.py --target write_cloud_event --signature-type cloudevent'" - name: Run HTTP conformance tests declarative - uses: GoogleCloudPlatform/functions-framework-conformance/action@72a4f36b10f1c6435ab1a86a9ea24bda464cc262 # v1.8.6 + uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 with: functionType: 'http' useBuildpacks: false @@ -85,7 +85,7 @@ jobs: cmd: "'functions-framework --source tests/conformance/main.py --target write_http_declarative'" - name: Run CloudEvents conformance tests declarative - uses: GoogleCloudPlatform/functions-framework-conformance/action@72a4f36b10f1c6435ab1a86a9ea24bda464cc262 # v1.8.6 + uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 with: functionType: 'cloudevent' useBuildpacks: false @@ -93,7 +93,7 @@ jobs: cmd: "'functions-framework --source tests/conformance/main.py --target write_cloud_event_declarative'" - name: Run HTTP concurrency tests declarative - uses: GoogleCloudPlatform/functions-framework-conformance/action@72a4f36b10f1c6435ab1a86a9ea24bda464cc262 # v1.8.6 + uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 with: functionType: 'http' useBuildpacks: false @@ -101,7 +101,7 @@ jobs: cmd: "'functions-framework --source tests/conformance/main.py --target write_http_declarative_concurrent'" - name: Run Typed tests declarative - uses: GoogleCloudPlatform/functions-framework-conformance/action@72a4f36b10f1c6435ab1a86a9ea24bda464cc262 # v1.8.6 + uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 with: functionType: 'http' declarativeType: 'typed' diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 5baf3aa0..02009c72 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: disable-sudo: true egress-policy: block diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4fa8dff2..1c99b0f7 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: disable-sudo: true egress-policy: block diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 77207e54..6bb76655 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 72b82523..23b0c7c0 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: disable-sudo: true egress-policy: block @@ -52,7 +52,7 @@ jobs: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 + uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 with: results_file: results.sarif results_format: sarif @@ -64,6 +64,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 + uses: github/codeql-action/upload-sarif@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.29.4 with: sarif_file: results.sarif diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 28ed5b1e..90e9e915 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -41,7 +41,7 @@ jobs: runs-on: ${{ matrix.platform }} steps: - name: Harden Runner - uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: disable-sudo: true egress-policy: block diff --git a/pyproject.toml b/pyproject.toml index a7c1778a..4a00ace8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ "click>=7.0,<9.0", "watchdog>=1.0.0", "gunicorn>=22.0.0; platform_system!='Windows'", - "cloudevents>=1.2.0,<=1.11.0", # Must support python 3.7 + "cloudevents>=1.2.0,<=1.11.0", # Must support python 3.7 "Werkzeug>=0.14,<4.0.0", "starlette>=0.37.0,<1.0.0; python_version>='3.8'", "uvicorn>=0.18.0,<1.0.0; python_version>='3.8'", From 9e625c9a79cf9a0a35778adad285c20be7535b8f Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 24 Jul 2025 11:07:58 -0700 Subject: [PATCH 090/108] chore(main): release 3.9.1 (#390) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ pyproject.toml | 2 +- setup.py | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 465d5c1b..f7be45f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.9.1](https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v3.9.0...v3.9.1) (2025-07-24) + + +### Bug Fixes + +* remove unused httpx dependency ([#389](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/389)) ([26fb101](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/26fb101d556a39262855a304c1ac351856ce4b61)) + ## [3.9.0](https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v3.8.3...v3.9.0) (2025-07-23) diff --git a/pyproject.toml b/pyproject.toml index 4a00ace8..968ed607 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "functions-framework" -version = "3.9.0" +version = "3.9.1" description = "An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team." readme = "README.md" requires-python = ">=3.7, <4" diff --git a/setup.py b/setup.py index 1dd17d7a..e1467ced 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( name="functions-framework", - version="3.9.0", + version="3.9.1", description="An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team.", long_description=long_description, long_description_content_type="text/markdown", From 1b6c428d559991134240473f6622f8759a3360d5 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 24 Jul 2025 13:28:25 -0700 Subject: [PATCH 091/108] fix: increase start delay for ASGI conformance tests to address flaky failures (#391) The ASGI conformance tests are failing sporadically with connection refused errors, where the server appears to not be ready when the test client attempts to connect. Hypothesis: The default 1-second start delay may be insufficient for Uvicorn/ASGI server startup in CI environments. This change increases startDelay to 5 seconds to match the buildpack integration tests, which have been running reliably. Note: The GitHub Action expects startDelay (camelCase) as an input parameter, which it then passes to the conformance test client as -start-delay flag. This is an attempt to diagnose and fix the intermittent failures - further investigation may be needed if the issue persists. --- .github/workflows/conformance-asgi.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/conformance-asgi.yml b/.github/workflows/conformance-asgi.yml index b3f3601d..69f2b215 100644 --- a/.github/workflows/conformance-asgi.yml +++ b/.github/workflows/conformance-asgi.yml @@ -54,6 +54,7 @@ jobs: useBuildpacks: false validateMapping: false cmd: "'functions-framework --source tests/conformance/async_main.py --target write_http --signature-type http --asgi'" + startDelay: 5 - name: Run CloudEvents conformance tests uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 @@ -62,6 +63,7 @@ jobs: useBuildpacks: false validateMapping: false cmd: "'functions-framework --source tests/conformance/async_main.py --target write_cloud_event --signature-type cloudevent --asgi'" + startDelay: 5 - name: Run HTTP conformance tests declarative uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 @@ -70,6 +72,7 @@ jobs: useBuildpacks: false validateMapping: false cmd: "'functions-framework --source tests/conformance/async_main.py --target write_http_declarative --asgi'" + startDelay: 5 - name: Run CloudEvents conformance tests declarative uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 @@ -78,6 +81,7 @@ jobs: useBuildpacks: false validateMapping: false cmd: "'functions-framework --source tests/conformance/async_main.py --target write_cloud_event_declarative --asgi'" + startDelay: 5 - name: Run HTTP concurrency tests declarative uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 @@ -86,6 +90,7 @@ jobs: useBuildpacks: false validateConcurrency: true cmd: "'functions-framework --source tests/conformance/async_main.py --target write_http_declarative_concurrent --asgi'" + startDelay: 5 # Note: Event (legacy) and Typed tests are not supported in ASGI mode # Note: validateMapping is set to false for CloudEvent tests because ASGI mode From f1dc285f07f7f5daa398d1a510573b4d15117332 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 10:55:45 -0700 Subject: [PATCH 092/108] chore(main): release 3.9.2 (#392) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ pyproject.toml | 2 +- setup.py | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7be45f0..73ed6d36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.9.2](https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v3.9.1...v3.9.2) (2025-07-24) + + +### Bug Fixes + +* increase start delay for ASGI conformance tests to address flaky failures ([#391](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/391)) ([1b6c428](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/1b6c428d559991134240473f6622f8759a3360d5)) + ## [3.9.1](https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v3.9.0...v3.9.1) (2025-07-24) diff --git a/pyproject.toml b/pyproject.toml index 968ed607..19912786 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "functions-framework" -version = "3.9.1" +version = "3.9.2" description = "An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team." readme = "README.md" requires-python = ">=3.7, <4" diff --git a/setup.py b/setup.py index e1467ced..3ba48c93 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( name="functions-framework", - version="3.9.1", + version="3.9.2", description="An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team.", long_description=long_description, long_description_content_type="text/markdown", From 9b37f85f6c37078119c2ea3cc91e6b3c00954a8c Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 4 Aug 2025 13:27:44 -0700 Subject: [PATCH 093/108] fix(ci): Add release-assets.githubusercontent.com to allowed endpoints (#394) The CodeQL workflow was failing because the `step-security/harden-runner` was blocking egress traffic to the endpoint used for downloading the CodeQL bundle. The download from `https://github.com/github/codeql-action/releases` redirects to `release-assets.githubusercontent.com`. This change adds `release-assets.githubusercontent.com` to the list of allowed endpoints to resolve the `ECONNREFUSED` error and removes previous incorrect attempts. --- .github/workflows/codeql.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 270f890c..e34c1f9b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -51,6 +51,7 @@ jobs: github.com:443 pypi.org:443 objects.githubusercontent.com:443 + release-assets.githubusercontent.com:443 - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 From a07b1e4f81d84424189488022e5f3d59c06014cc Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 4 Aug 2025 13:39:47 -0700 Subject: [PATCH 094/108] feat: Add async and streaming examples (#393) Adds two new examples to demonstrate asynchronous and streaming capabilities: - `cloud_run_async`: Shows how to create basic asynchronous HTTP and CloudEvent functions. - `cloud_run_streaming_http`: Shows how to create both synchronous and asynchronous streaming HTTP functions. The examples include instructions for running locally with the `functions-framework` CLI and deploying to Cloud Run with `gcloud`. --- examples/README.md | 2 + examples/cloud_run_async/README.md | 68 +++++++++++++++++++ examples/cloud_run_async/main.py | 36 ++++++++++ examples/cloud_run_async/requirements.txt | 4 ++ examples/cloud_run_async/send_cloud_event.py | 38 +++++++++++ examples/cloud_run_streaming_http/README.md | 65 ++++++++++++++++++ examples/cloud_run_streaming_http/main.py | 59 ++++++++++++++++ .../cloud_run_streaming_http/requirements.txt | 1 + 8 files changed, 273 insertions(+) create mode 100644 examples/cloud_run_async/README.md create mode 100644 examples/cloud_run_async/main.py create mode 100644 examples/cloud_run_async/requirements.txt create mode 100644 examples/cloud_run_async/send_cloud_event.py create mode 100644 examples/cloud_run_streaming_http/README.md create mode 100644 examples/cloud_run_streaming_http/main.py create mode 100644 examples/cloud_run_streaming_http/requirements.txt diff --git a/examples/README.md b/examples/README.md index 7960a743..47b7c398 100644 --- a/examples/README.md +++ b/examples/README.md @@ -5,6 +5,8 @@ * [`cloud_run_http`](./cloud_run_http/) - Deploying an HTTP function to [Cloud Run](http://cloud.google.com/run) with the Functions Framework * [`cloud_run_event`](./cloud_run_event/) - Deploying a CloudEvent function to [Cloud Run](http://cloud.google.com/run) with the Functions Framework * [`cloud_run_cloud_events`](cloud_run_cloud_events/) - Deploying a [CloudEvent](https://github.com/cloudevents/sdk-python) function to [Cloud Run](http://cloud.google.com/run) with the Functions Framework +* [`cloud_run_async`](./cloud_run_async/) - Deploying asynchronous HTTP and CloudEvent functions to [Cloud Run](http://cloud.google.com/run) with the Functions Framework +* [`cloud_run_streaming_http`](./cloud_run_streaming_http/) - Deploying streaming HTTP functions to [Cloud Run](http://cloud.google.com/run) with the Functions Framework ## Development Tools * [`docker-compose`](./docker-compose) - diff --git a/examples/cloud_run_async/README.md b/examples/cloud_run_async/README.md new file mode 100644 index 00000000..81de2747 --- /dev/null +++ b/examples/cloud_run_async/README.md @@ -0,0 +1,68 @@ +# Deploying async functions to Cloud Run + +This sample shows how to deploy asynchronous functions to [Cloud Run functions](https://cloud.google.com/functions) with the Functions Framework. It includes examples for both HTTP and CloudEvent functions, which can be found in the `main.py` file. + +## Dependencies + +Install the dependencies for this example: + +```sh +pip install -r requirements.txt +``` + +## Running locally + +### HTTP Function + +To run the HTTP function locally, use the `functions-framework` command: + +```sh +functions-framework --target=hello_async_http +``` + +Then, send a request to it from another terminal: + +```sh +curl localhost:8080 +# Output: Hello, async world! +``` + +### CloudEvent Function + +To run the CloudEvent function, specify the target and set the signature type: + +```sh +functions-framework --target=hello_async_cloudevent --signature-type=cloudevent +``` + +Then, in another terminal, send a sample CloudEvent using the provided script: + +```sh +python send_cloud_event.py +``` + +## Deploying to Cloud Run + +You can deploy these functions to Cloud Run using the `gcloud` CLI. + +### HTTP Function + +```sh +gcloud run deploy async-http-function \ + --source . \ + --function hello_async_http \ + --base-image python312 \ + --region +``` + +### CloudEvent Function + +```sh +gcloud run deploy async-cloudevent-function \ + --source . \ + --function hello_async_cloudevent \ + --base-image python312 \ + --region +``` + +After deploying, you can invoke the CloudEvent function by sending an HTTP POST request with a CloudEvent payload to its URL. diff --git a/examples/cloud_run_async/main.py b/examples/cloud_run_async/main.py new file mode 100644 index 00000000..40062a95 --- /dev/null +++ b/examples/cloud_run_async/main.py @@ -0,0 +1,36 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import functions_framework.aio + +@functions_framework.aio.http +async def hello_async_http(request): + """ + An async HTTP function. + Args: + request (starlette.requests.Request): The request object. + Returns: + The response text, or an instance of any Starlette response class + (e.g. `starlette.responses.Response`). + """ + return "Hello, async world!" + +@functions_framework.aio.cloud_event +async def hello_async_cloudevent(cloud_event): + """ + An async CloudEvent function. + Args: + cloud_event (cloudevents.http.CloudEvent): The CloudEvent object. + """ + print(f"Received event with ID: {cloud_event['id']} and data {cloud_event.data}") diff --git a/examples/cloud_run_async/requirements.txt b/examples/cloud_run_async/requirements.txt new file mode 100644 index 00000000..b862cce2 --- /dev/null +++ b/examples/cloud_run_async/requirements.txt @@ -0,0 +1,4 @@ +functions-framework>=3.9.2,<4.0.0 + +# For testing +httpx<=0.28.1 diff --git a/examples/cloud_run_async/send_cloud_event.py b/examples/cloud_run_async/send_cloud_event.py new file mode 100644 index 00000000..714bc719 --- /dev/null +++ b/examples/cloud_run_async/send_cloud_event.py @@ -0,0 +1,38 @@ +#!/usr/local/bin/python + +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import asyncio +import httpx + +from cloudevents.http import CloudEvent, to_structured + + +async def main(): + attributes = { + "Content-Type": "application/json", + "source": "from-galaxy-far-far-away", + "type": "cloudevent.greet.you", + } + data = {"name": "john"} + + event = CloudEvent(attributes, data) + + headers, data = to_structured(event) + + async with httpx.AsyncClient() as client: + await client.post("http://localhost:8080/", headers=headers, data=data) + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/cloud_run_streaming_http/README.md b/examples/cloud_run_streaming_http/README.md new file mode 100644 index 00000000..839b09c7 --- /dev/null +++ b/examples/cloud_run_streaming_http/README.md @@ -0,0 +1,65 @@ +# Deploying streaming functions to Cloud Run + +This sample shows how to deploy streaming functions to [Cloud Run](http://cloud.google.com/run) with the Functions Framework. The `main.py` file contains examples for both synchronous and asynchronous streaming. + +## Dependencies + +Install the dependencies for this example: + +```sh +pip install -r requirements.txt +``` + +## Running locally + +### Synchronous Streaming + +To run the synchronous streaming function locally: + +```sh +functions-framework --target=hello_stream +``` + +Then, send a request to it from another terminal: + +```sh +curl localhost:8080 +``` + +### Asynchronous Streaming + +To run the asynchronous streaming function locally: + +```sh +functions-framework --target=hello_stream_async +``` + +Then, send a request to it from another terminal: + +```sh +curl localhost:8080 +``` + +## Deploying to Cloud Run + +You can deploy these functions to Cloud Run using the `gcloud` CLI. + +### Synchronous Streaming + +```sh +gcloud run deploy streaming-function \ + --source . \ + --function hello_stream \ + --base-image python312 \ + --region +``` + +### Asynchronous Streaming + +```sh +gcloud run deploy streaming-async-function \ + --source . \ + --function hello_stream_async \ + --base-image python312 \ + --region +``` diff --git a/examples/cloud_run_streaming_http/main.py b/examples/cloud_run_streaming_http/main.py new file mode 100644 index 00000000..5010b1a3 --- /dev/null +++ b/examples/cloud_run_streaming_http/main.py @@ -0,0 +1,59 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +import asyncio +import functions_framework +import functions_framework.aio +from starlette.responses import StreamingResponse + +# Helper function for the synchronous streaming example. +def slow_numbers(minimum, maximum): + yield '
    ' + for number in range(minimum, maximum + 1): + yield '
  • %d
  • ' % number + time.sleep(0.5) + yield '
' + +@functions_framework.http +def hello_stream(request): + """ + A synchronous HTTP function that streams a response. + Args: + request (flask.Request): The request object. + Returns: + A generator, which will be streamed as the response. + """ + generator = slow_numbers(1, 10) + return generator, {'Content-Type': 'text/html'} + +# Helper function for the asynchronous streaming example. +async def slow_numbers_async(minimum, maximum): + yield '
    ' + for number in range(minimum, maximum + 1): + yield '
  • %d
  • ' % number + await asyncio.sleep(0.5) + yield '
' + +@functions_framework.aio.http +async def hello_stream_async(request): + """ + An asynchronous HTTP function that streams a response. + Args: + request (starlette.requests.Request): The request object. + Returns: + A starlette.responses.StreamingResponse. + """ + generator = slow_numbers_async(1, 10) + return StreamingResponse(generator, media_type='text/html') diff --git a/examples/cloud_run_streaming_http/requirements.txt b/examples/cloud_run_streaming_http/requirements.txt new file mode 100644 index 00000000..83e77d02 --- /dev/null +++ b/examples/cloud_run_streaming_http/requirements.txt @@ -0,0 +1 @@ +functions-framework>=3.9.2,<4.0.0 From 3597a56e82958d97ee915f564d074e3faa4efb56 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 10 Nov 2025 16:20:51 +0000 Subject: [PATCH 095/108] chore(deps): update all non-major dependencies (#400) --- .github/workflows/codeql.yml | 10 +++++----- .github/workflows/conformance-asgi.yml | 6 +++--- .github/workflows/conformance.yml | 6 +++--- .github/workflows/dependency-review.yml | 6 +++--- .github/workflows/lint.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- .github/workflows/scorecard.yml | 8 ++++---- .github/workflows/unit.yml | 4 ++-- pyproject.toml | 2 +- 9 files changed, 25 insertions(+), 25 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e34c1f9b..e5e575fb 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 with: disable-sudo: true egress-policy: block @@ -54,11 +54,11 @@ jobs: release-assets.githubusercontent.com:443 - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.29.4 + uses: github/codeql-action/init@5d5cd550d3e189c569da8f16ea8de2d821c9bf7a # v3.31.2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -68,7 +68,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.29.4 + uses: github/codeql-action/autobuild@5d5cd550d3e189c569da8f16ea8de2d821c9bf7a # v3.31.2 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -81,6 +81,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.29.4 + uses: github/codeql-action/analyze@5d5cd550d3e189c569da8f16ea8de2d821c9bf7a # v3.31.2 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/conformance-asgi.yml b/.github/workflows/conformance-asgi.yml index 69f2b215..ee568faa 100644 --- a/.github/workflows/conformance-asgi.yml +++ b/.github/workflows/conformance-asgi.yml @@ -17,7 +17,7 @@ jobs: runs-on: ${{ matrix.platform }} steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 with: disable-sudo: true egress-policy: block @@ -32,7 +32,7 @@ jobs: release-assets.githubusercontent.com:443 - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - name: Setup Python uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 @@ -45,7 +45,7 @@ jobs: - name: Setup Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: - go-version: '1.24' + go-version: '1.25' - name: Run HTTP conformance tests uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 9eedb0a2..34f997a6 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -22,7 +22,7 @@ jobs: runs-on: ${{ matrix.platform }} steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 with: disable-sudo: true egress-policy: block @@ -37,7 +37,7 @@ jobs: release-assets.githubusercontent.com:443 - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - name: Setup Python uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 @@ -50,7 +50,7 @@ jobs: - name: Setup Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: - go-version: '1.24' + go-version: '1.25' - name: Run HTTP conformance tests uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 02009c72..efa405f8 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 with: disable-sudo: true egress-policy: block @@ -25,6 +25,6 @@ jobs: api.github.com:443 github.com:443 - name: 'Checkout Repository' - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - name: 'Dependency Review' - uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1 + uses: actions/dependency-review-action@40c09b7dc99638e5ddb0bfd91c1673effc064d8a # v4.8.1 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1c99b0f7..e9088c6a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 with: disable-sudo: true egress-policy: block @@ -21,7 +21,7 @@ jobs: github.com:443 pypi.org:443 - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - name: Setup Python uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - name: Install tox diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6bb76655..c209a7ed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,12 +13,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 with: ref: ${{ github.event.release.tag_name }} - name: Install Python diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 23b0c7c0..6fdac794 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 with: disable-sudo: true egress-policy: block @@ -47,12 +47,12 @@ jobs: - name: "Checkout code" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 with: results_file: results.sarif results_format: sarif @@ -64,6 +64,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.29.4 + uses: github/codeql-action/upload-sarif@5d5cd550d3e189c569da8f16ea8de2d821c9bf7a # v3.31.2 with: sarif_file: results.sarif diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 90e9e915..20f24675 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -41,7 +41,7 @@ jobs: runs-on: ${{ matrix.platform }} steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 with: disable-sudo: true egress-policy: block @@ -57,7 +57,7 @@ jobs: release-assets.githubusercontent.com:443 - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - name: Use Python ${{ matrix.python }} uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: diff --git a/pyproject.toml b/pyproject.toml index 19912786..27368e37 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ "click>=7.0,<9.0", "watchdog>=1.0.0", "gunicorn>=22.0.0; platform_system!='Windows'", - "cloudevents>=1.2.0,<=1.11.0", # Must support python 3.7 + "cloudevents>=1.12.0,<=1.12.0", # Must support python 3.7 "Werkzeug>=0.14,<4.0.0", "starlette>=0.37.0,<1.0.0; python_version>='3.8'", "uvicorn>=0.18.0,<1.0.0; python_version>='3.8'", From ef45fae46896b50a7a3e0e5c3cb4813519e3cb76 Mon Sep 17 00:00:00 2001 From: Maeve <167252720+maemayve@users.noreply.github.com> Date: Mon, 10 Nov 2025 10:13:59 -0800 Subject: [PATCH 096/108] fix: remove Python 3.7 test execution (#402) Python 3.7 is a decomissioned runtime. Tests are failing because of language features not supported by this python version. Disable these tests rather than backfixing. --- .coveragerc-py37 | 22 ---------------------- .github/workflows/conformance.yml | 2 -- .github/workflows/unit.yml | 10 +--------- tox.ini | 7 ------- 4 files changed, 1 insertion(+), 40 deletions(-) delete mode 100644 .coveragerc-py37 diff --git a/.coveragerc-py37 b/.coveragerc-py37 deleted file mode 100644 index efb63fec..00000000 --- a/.coveragerc-py37 +++ /dev/null @@ -1,22 +0,0 @@ -[run] -# Coverage configuration specifically for Python 3.7 environments -# Excludes the aio module which requires Python 3.8+ (Starlette dependency) -# This file is only used by py37-* tox environments -omit = - */functions_framework/aio/* - */functions_framework/_http/asgi.py - */.tox/* - */tests/* - */venv/* - */.venv/* - -[report] -exclude_lines = - pragma: no cover - from functions_framework.aio import - from functions_framework._http.asgi import - from functions_framework._http.gunicorn import UvicornApplication - class AsgiMiddleware: - def set_execution_context_async - return create_asgi_app_from_module - app = create_asgi_app\(target, source, signature_type\) \ No newline at end of file diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 34f997a6..6c14136e 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -17,8 +17,6 @@ jobs: include: - platform: ubuntu-22.04 python: '3.8' - - platform: ubuntu-22.04 - python: '3.7' runs-on: ${{ matrix.platform }} steps: - name: Harden Runner diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 20f24675..96a880d2 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -11,7 +11,7 @@ jobs: test: strategy: matrix: - python: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] + python: ['3.8', '3.9', '3.10', '3.11', '3.12'] platform: [ubuntu-latest, macos-latest, windows-latest] # Python <= 3.9 is not available on macos-latest # Workaround for https://github.com/actions/setup-python/issues/696 @@ -21,23 +21,15 @@ jobs: python: '3.9' - platform: macos-latest python: '3.8' - - platform: macos-latest - python: '3.7' - platform: ubuntu-latest python: '3.8' - - platform: ubuntu-latest - python: '3.7' include: - platform: macos-latest python: '3.9' - platform: macos-13 python: '3.8' - - platform: macos-13 - python: '3.7' - platform: ubuntu-22.04 python: '3.8' - - platform: ubuntu-22.04 - python: '3.7' runs-on: ${{ matrix.platform }} steps: - name: Harden Runner diff --git a/tox.ini b/tox.ini index cb0873b6..2e36e689 100644 --- a/tox.ini +++ b/tox.ini @@ -16,9 +16,6 @@ envlist = py38-ubuntu-22.04 py38-macos-13 py38-windows-latest - py37-ubuntu-22.04 - py37-macos-13 - py37-windows-latest [testenv] usedevelop = true @@ -31,10 +28,6 @@ deps = pretend setenv = PYTESTARGS = --cov=functions_framework --cov-branch --cov-report term-missing --cov-fail-under=100 - # Python 3.7: Use .coveragerc-py37 to exclude aio module from coverage since it requires Python 3.8+ (Starlette dependency) - py37-ubuntu-22.04: PYTESTARGS = --cov=functions_framework --cov-config=.coveragerc-py37 --cov-branch --cov-report term-missing --cov-fail-under=100 - py37-macos-13: PYTESTARGS = --cov=functions_framework --cov-config=.coveragerc-py37 --cov-branch --cov-report term-missing --cov-fail-under=100 - py37-windows-latest: PYTESTARGS = windows-latest: PYTESTARGS = commands = pytest {env:PYTESTARGS} {posargs} From 2cf966f037d83fe0d8e69cee8d41ff238db79164 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 10:20:01 -0800 Subject: [PATCH 097/108] chore(main): release 3.10.0 (#395) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 13 +++++++++++++ pyproject.toml | 2 +- setup.py | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73ed6d36..b817ff60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.10.0](https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v3.9.2...v3.10.0) (2025-11-10) + + +### Features + +* Add async and streaming examples ([#393](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/393)) ([a07b1e4](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/a07b1e4f81d84424189488022e5f3d59c06014cc)) + + +### Bug Fixes + +* **ci:** Add release-assets.githubusercontent.com to allowed endpoints ([#394](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/394)) ([9b37f85](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/9b37f85f6c37078119c2ea3cc91e6b3c00954a8c)) +* remove Python 3.7 test execution ([#402](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/402)) ([ef45fae](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/ef45fae46896b50a7a3e0e5c3cb4813519e3cb76)) + ## [3.9.2](https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v3.9.1...v3.9.2) (2025-07-24) diff --git a/pyproject.toml b/pyproject.toml index 27368e37..b187260f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "functions-framework" -version = "3.9.2" +version = "3.10.0" description = "An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team." readme = "README.md" requires-python = ">=3.7, <4" diff --git a/setup.py b/setup.py index 3ba48c93..28d0ea11 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( name="functions-framework", - version="3.9.2", + version="3.10.0", description="An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team.", long_description=long_description, long_description_content_type="text/markdown", From 8d74a7b9ecead58df77208d6e4717419a9aa9447 Mon Sep 17 00:00:00 2001 From: Maeve <167252720+maemayve@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:39:44 -0800 Subject: [PATCH 098/108] fix: Correct cloudevents dependency to allow 1.11.0 (#405) Python 3.7 is no longer supported so don't allow versions prior to 1.11.0 but allow 1.11.0 as Python 3.8 is still supported by the framework. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b187260f..6d98287c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ "click>=7.0,<9.0", "watchdog>=1.0.0", "gunicorn>=22.0.0; platform_system!='Windows'", - "cloudevents>=1.12.0,<=1.12.0", # Must support python 3.7 + "cloudevents>=1.11.0,<=1.12.0", # Must support python 3.8 "Werkzeug>=0.14,<4.0.0", "starlette>=0.37.0,<1.0.0; python_version>='3.8'", "uvicorn>=0.18.0,<1.0.0; python_version>='3.8'", From 43e63f6f847d89eeb018add028fb6222f2fac38c Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Fri, 13 Feb 2026 14:48:58 -0800 Subject: [PATCH 099/108] fix: pin lint tool versions to avoid surprise breakages (#413) --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 2e36e689..6b866c51 100644 --- a/tox.ini +++ b/tox.ini @@ -34,10 +34,10 @@ commands = pytest {env:PYTESTARGS} {posargs} [testenv:lint] basepython=python3 deps = - black + black>=25,<26 twine - isort - mypy + isort>=5,<6 + mypy>=1,<2 build commands = black --check src tests conftest.py --exclude tests/test_functions/background_load_error/main.py From 0a3f09e6695dbf9c350940fee1eaf15ea4632e84 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Fri, 13 Feb 2026 15:23:26 -0800 Subject: [PATCH 100/108] chore: configure dependabot to use fix commit type when updating depedencies (#411) Today, release-please doesn't trigger a release for commits with chore: commit type. We'd like to cut a release when updating dependencies that impacts the packaged artifact, so we'll instruct dependabot to use fix commit type instead of chore(deps) it uses today. --- .github/renovate.json | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/.github/renovate.json b/.github/renovate.json index 32ac90d6..f7ad311a 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,15 +1,29 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["group:allNonMajor", "schedule:monthly"], + "extends": [ + "group:allNonMajor", + "schedule:monthly" + ], + "semanticCommits": "enabled", "packageRules": [ { "description": "Create a PR whenever there is a new major version", - "matchUpdateTypes": [ - "major" - ] + "matchUpdateTypes": ["major"] + }, + { + "description": "Use releasable commit type for runtime dependency updates", + "matchManagers": ["pep621", "pip_setup"], + "matchDepTypes": ["dependencies"], + "semanticCommitType": "fix", + "semanticCommitScope": "deps" + }, + { + "description": "Keep development-only dependency updates non-releasable", + "matchManagers": ["pep621"], + "matchDepTypes": ["dependency-groups"], + "semanticCommitType": "chore", + "semanticCommitScope": "deps" } ], - "ignorePaths": [ - "examples/**" - ] + "ignorePaths": ["examples/**"] } From f5fd4ddd55bb4b11fd93abff389f3035261330fb Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 17 Feb 2026 18:41:29 +0000 Subject: [PATCH 101/108] chore(deps): update actions/setup-go action to v6 (#410) Co-authored-by: Daniel Lee --- .github/workflows/conformance-asgi.yml | 2 +- .github/workflows/conformance.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/conformance-asgi.yml b/.github/workflows/conformance-asgi.yml index ee568faa..a67d8c42 100644 --- a/.github/workflows/conformance-asgi.yml +++ b/.github/workflows/conformance-asgi.yml @@ -43,7 +43,7 @@ jobs: run: python -m pip install -e . - name: Setup Go - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: go-version: '1.25' diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 6c14136e..9604ebe9 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -46,7 +46,7 @@ jobs: run: python -m pip install -e . - name: Setup Go - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: go-version: '1.25' From e90a9890a0c9b9affc768b7e631e68fd05944c4f Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 17 Feb 2026 19:06:25 +0000 Subject: [PATCH 102/108] chore(deps): update actions/checkout action to v6 (#408) --- .github/workflows/codeql.yml | 2 +- .github/workflows/conformance-asgi.yml | 2 +- .github/workflows/conformance.yml | 2 +- .github/workflows/dependency-review.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/scorecard.yml | 2 +- .github/workflows/unit.yml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e5e575fb..c3114a39 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -54,7 +54,7 @@ jobs: release-assets.githubusercontent.com:443 - name: Checkout repository - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/conformance-asgi.yml b/.github/workflows/conformance-asgi.yml index a67d8c42..453ec9e3 100644 --- a/.github/workflows/conformance-asgi.yml +++ b/.github/workflows/conformance-asgi.yml @@ -32,7 +32,7 @@ jobs: release-assets.githubusercontent.com:443 - name: Checkout code - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Python uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 9604ebe9..a40d7325 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -35,7 +35,7 @@ jobs: release-assets.githubusercontent.com:443 - name: Checkout code - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Python uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index efa405f8..0362f060 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -25,6 +25,6 @@ jobs: api.github.com:443 github.com:443 - name: 'Checkout Repository' - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: 'Dependency Review' uses: actions/dependency-review-action@40c09b7dc99638e5ddb0bfd91c1673effc064d8a # v4.8.1 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e9088c6a..0704d917 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,7 +21,7 @@ jobs: github.com:443 pypi.org:443 - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Python uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - name: Install tox diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c209a7ed..18a4bc6e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - name: Checkout - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.release.tag_name }} - name: Install Python diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 6fdac794..3688db76 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -47,7 +47,7 @@ jobs: - name: "Checkout code" - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 96a880d2..37d8f81f 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -49,7 +49,7 @@ jobs: release-assets.githubusercontent.com:443 - name: Checkout - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Use Python ${{ matrix.python }} uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: From 80fdae75cdb2d4d389a17f690a1ab8401b8d3916 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 12:29:55 -0800 Subject: [PATCH 103/108] chore(main): release 3.10.1 (#406) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 8 ++++++++ pyproject.toml | 2 +- setup.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b817ff60..14e609c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.10.1](https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v3.10.0...v3.10.1) (2026-02-17) + + +### Bug Fixes + +* Correct cloudevents dependency to allow 1.11.0 ([#405](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/405)) ([8d74a7b](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/8d74a7b9ecead58df77208d6e4717419a9aa9447)) +* pin lint tool versions to avoid surprise breakages ([#413](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/413)) ([43e63f6](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/43e63f6f847d89eeb018add028fb6222f2fac38c)) + ## [3.10.0](https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v3.9.2...v3.10.0) (2025-11-10) diff --git a/pyproject.toml b/pyproject.toml index 6d98287c..972e7e52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "functions-framework" -version = "3.10.0" +version = "3.10.1" description = "An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team." readme = "README.md" requires-python = ">=3.7, <4" diff --git a/setup.py b/setup.py index 28d0ea11..db6479ea 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( name="functions-framework", - version="3.10.0", + version="3.10.1", description="An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team.", long_description=long_description, long_description_content_type="text/markdown", From 6de54973227faab57429d3fcfa33ced4df6c7b65 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 17 Feb 2026 20:37:00 +0000 Subject: [PATCH 104/108] chore(deps): update all non-major dependencies (#407) --- .github/workflows/codeql.yml | 8 ++++---- .github/workflows/conformance-asgi.yml | 14 +++++++------- .github/workflows/conformance.yml | 18 +++++++++--------- .github/workflows/dependency-review.yml | 4 ++-- .github/workflows/lint.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/scorecard.yml | 4 ++-- .github/workflows/unit.yml | 2 +- 8 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c3114a39..fe4c7c54 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 + uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 with: disable-sudo: true egress-policy: block @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@5d5cd550d3e189c569da8f16ea8de2d821c9bf7a # v3.31.2 + uses: github/codeql-action/init@f5c2471be782132e47a6e6f9c725e56730d6e9a3 # v3.32.3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -68,7 +68,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@5d5cd550d3e189c569da8f16ea8de2d821c9bf7a # v3.31.2 + uses: github/codeql-action/autobuild@f5c2471be782132e47a6e6f9c725e56730d6e9a3 # v3.32.3 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -81,6 +81,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@5d5cd550d3e189c569da8f16ea8de2d821c9bf7a # v3.31.2 + uses: github/codeql-action/analyze@f5c2471be782132e47a6e6f9c725e56730d6e9a3 # v3.32.3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/conformance-asgi.yml b/.github/workflows/conformance-asgi.yml index 453ec9e3..ccb10a5c 100644 --- a/.github/workflows/conformance-asgi.yml +++ b/.github/workflows/conformance-asgi.yml @@ -17,7 +17,7 @@ jobs: runs-on: ${{ matrix.platform }} steps: - name: Harden Runner - uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 + uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 with: disable-sudo: true egress-policy: block @@ -45,10 +45,10 @@ jobs: - name: Setup Go uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: - go-version: '1.25' + go-version: '1.26' - name: Run HTTP conformance tests - uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 + uses: GoogleCloudPlatform/functions-framework-conformance/action@403fda9e6e176aae87646aace9bed075cee8e7fd # v1.8.8 with: functionType: 'http' useBuildpacks: false @@ -57,7 +57,7 @@ jobs: startDelay: 5 - name: Run CloudEvents conformance tests - uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 + uses: GoogleCloudPlatform/functions-framework-conformance/action@403fda9e6e176aae87646aace9bed075cee8e7fd # v1.8.8 with: functionType: 'cloudevent' useBuildpacks: false @@ -66,7 +66,7 @@ jobs: startDelay: 5 - name: Run HTTP conformance tests declarative - uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 + uses: GoogleCloudPlatform/functions-framework-conformance/action@403fda9e6e176aae87646aace9bed075cee8e7fd # v1.8.8 with: functionType: 'http' useBuildpacks: false @@ -75,7 +75,7 @@ jobs: startDelay: 5 - name: Run CloudEvents conformance tests declarative - uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 + uses: GoogleCloudPlatform/functions-framework-conformance/action@403fda9e6e176aae87646aace9bed075cee8e7fd # v1.8.8 with: functionType: 'cloudevent' useBuildpacks: false @@ -84,7 +84,7 @@ jobs: startDelay: 5 - name: Run HTTP concurrency tests declarative - uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 + uses: GoogleCloudPlatform/functions-framework-conformance/action@403fda9e6e176aae87646aace9bed075cee8e7fd # v1.8.8 with: functionType: 'http' useBuildpacks: false diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index a40d7325..92f084c9 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -20,7 +20,7 @@ jobs: runs-on: ${{ matrix.platform }} steps: - name: Harden Runner - uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 + uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 with: disable-sudo: true egress-policy: block @@ -48,10 +48,10 @@ jobs: - name: Setup Go uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: - go-version: '1.25' + go-version: '1.26' - name: Run HTTP conformance tests - uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 + uses: GoogleCloudPlatform/functions-framework-conformance/action@403fda9e6e176aae87646aace9bed075cee8e7fd # v1.8.8 with: functionType: 'http' useBuildpacks: false @@ -59,7 +59,7 @@ jobs: cmd: "'functions-framework --source tests/conformance/main.py --target write_http --signature-type http'" - name: Run event conformance tests - uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 + uses: GoogleCloudPlatform/functions-framework-conformance/action@403fda9e6e176aae87646aace9bed075cee8e7fd # v1.8.8 with: functionType: 'legacyevent' useBuildpacks: false @@ -67,7 +67,7 @@ jobs: cmd: "'functions-framework --source tests/conformance/main.py --target write_legacy_event --signature-type event'" - name: Run CloudEvents conformance tests - uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 + uses: GoogleCloudPlatform/functions-framework-conformance/action@403fda9e6e176aae87646aace9bed075cee8e7fd # v1.8.8 with: functionType: 'cloudevent' useBuildpacks: false @@ -75,7 +75,7 @@ jobs: cmd: "'functions-framework --source tests/conformance/main.py --target write_cloud_event --signature-type cloudevent'" - name: Run HTTP conformance tests declarative - uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 + uses: GoogleCloudPlatform/functions-framework-conformance/action@403fda9e6e176aae87646aace9bed075cee8e7fd # v1.8.8 with: functionType: 'http' useBuildpacks: false @@ -83,7 +83,7 @@ jobs: cmd: "'functions-framework --source tests/conformance/main.py --target write_http_declarative'" - name: Run CloudEvents conformance tests declarative - uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 + uses: GoogleCloudPlatform/functions-framework-conformance/action@403fda9e6e176aae87646aace9bed075cee8e7fd # v1.8.8 with: functionType: 'cloudevent' useBuildpacks: false @@ -91,7 +91,7 @@ jobs: cmd: "'functions-framework --source tests/conformance/main.py --target write_cloud_event_declarative'" - name: Run HTTP concurrency tests declarative - uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 + uses: GoogleCloudPlatform/functions-framework-conformance/action@403fda9e6e176aae87646aace9bed075cee8e7fd # v1.8.8 with: functionType: 'http' useBuildpacks: false @@ -99,7 +99,7 @@ jobs: cmd: "'functions-framework --source tests/conformance/main.py --target write_http_declarative_concurrent'" - name: Run Typed tests declarative - uses: GoogleCloudPlatform/functions-framework-conformance/action@c7b9c8798fb35e454f76da185a40547ee55c784e # v1.8.7 + uses: GoogleCloudPlatform/functions-framework-conformance/action@403fda9e6e176aae87646aace9bed075cee8e7fd # v1.8.8 with: functionType: 'http' declarativeType: 'typed' diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 0362f060..5dbfda76 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 + uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 with: disable-sudo: true egress-policy: block @@ -27,4 +27,4 @@ jobs: - name: 'Checkout Repository' uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: 'Dependency Review' - uses: actions/dependency-review-action@40c09b7dc99638e5ddb0bfd91c1673effc064d8a # v4.8.1 + uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0704d917..6ca5a4ac 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 + uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 with: disable-sudo: true egress-policy: block diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 18a4bc6e..2caddcba 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 + uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 3688db76..412aedc9 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 + uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 with: disable-sudo: true egress-policy: block @@ -64,6 +64,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@5d5cd550d3e189c569da8f16ea8de2d821c9bf7a # v3.31.2 + uses: github/codeql-action/upload-sarif@f5c2471be782132e47a6e6f9c725e56730d6e9a3 # v3.32.3 with: sarif_file: results.sarif diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 37d8f81f..c94865e2 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -33,7 +33,7 @@ jobs: runs-on: ${{ matrix.platform }} steps: - name: Harden Runner - uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 + uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 with: disable-sudo: true egress-policy: block From b41ee77d6fb61a9e0a76f17d561a221e50fe788a Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 17 Feb 2026 15:23:16 -0800 Subject: [PATCH 105/108] fix: remove macos-13 from test matrix (runner retired) (#414) --- .github/workflows/unit.yml | 2 -- tox.ini | 1 - 2 files changed, 3 deletions(-) diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index c94865e2..adab381d 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -26,8 +26,6 @@ jobs: include: - platform: macos-latest python: '3.9' - - platform: macos-13 - python: '3.8' - platform: ubuntu-22.04 python: '3.8' runs-on: ${{ matrix.platform }} diff --git a/tox.ini b/tox.ini index 6b866c51..0e69c33d 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,6 @@ envlist = py39-macos-13 py39-windows-latest py38-ubuntu-22.04 - py38-macos-13 py38-windows-latest [testenv] From 715ba9a4a8075eed73d78c28209763a7eb67f01f Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 17 Jun 2026 10:57:21 -0700 Subject: [PATCH 106/108] fix(ci): update allowed endpoints for harden-runner (#427) --- .github/workflows/dependency-review.yml | 2 ++ .github/workflows/unit.yml | 1 + 2 files changed, 3 insertions(+) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 5dbfda76..bcf29c0b 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -22,7 +22,9 @@ jobs: disable-sudo: true egress-policy: block allowed-endpoints: > + api.deps.dev:443 api.github.com:443 + api.securityscorecards.dev:443 github.com:443 - name: 'Checkout Repository' uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index adab381d..2901f5f3 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -42,6 +42,7 @@ jobs: github.com:443 objects.githubusercontent.com:443 production.cloudflare.docker.com:443 + production.cloudfront.docker.com:443 pypi.org:443 registry-1.docker.io:443 release-assets.githubusercontent.com:443 From c6501715bb57348ead817ee90c18622b3c1c31ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=BCller?= <63948181+SpielerNogard@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:13:27 +0200 Subject: [PATCH 107/108] fix(deps): bump starlette to >=1.0.1 on Python 3.10+ to fix PYSEC-2026-161 (#423) Starlette <=1.0.0 is vulnerable to a missing Host header validation that poisons request.url.path and bypasses path-based security checks (GHSA-86qp-5c8j-p5mr / PYSEC-2026-161). The fix only landed in 1.0.1, which requires Python >=3.10. Constraint is split by interpreter version so Python 3.8/3.9 users keep the existing 0.x line (no upstream fix available) while Python 3.10+ pulls the patched 1.x line. Co-authored-by: Daniel Lee --- pyproject.toml | 3 ++- setup.py | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 972e7e52..04baad0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,8 @@ dependencies = [ "gunicorn>=22.0.0; platform_system!='Windows'", "cloudevents>=1.11.0,<=1.12.0", # Must support python 3.8 "Werkzeug>=0.14,<4.0.0", - "starlette>=0.37.0,<1.0.0; python_version>='3.8'", + "starlette>=0.37.0,<1.0.0; python_version>='3.8' and python_version<'3.10'", + "starlette>=1.0.1,<2.0.0; python_version>='3.10'", "uvicorn>=0.18.0,<1.0.0; python_version>='3.8'", "uvicorn-worker>=0.2.0,<1.0.0; python_version>='3.8'", ] diff --git a/setup.py b/setup.py index db6479ea..41244a6c 100644 --- a/setup.py +++ b/setup.py @@ -58,7 +58,10 @@ "Werkzeug>=0.14,<4.0.0", ], extras_require={ - "async": ["starlette>=0.37.0,<1.0.0"], + "async": [ + "starlette>=0.37.0,<1.0.0; python_version<'3.10'", + "starlette>=1.0.1,<2.0.0; python_version>='3.10'", + ], }, entry_points={ "console_scripts": [ From d13d9aa4666b553e0e435813ab799c6f43e96bd8 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 11:17:02 -0700 Subject: [PATCH 108/108] chore(main): release 3.10.2 (#417) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 9 +++++++++ pyproject.toml | 2 +- setup.py | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14e609c4..20d13326 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.10.2](https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v3.10.1...v3.10.2) (2026-06-17) + + +### Bug Fixes + +* **ci:** update allowed endpoints for harden-runner ([#427](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/427)) ([715ba9a](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/715ba9a4a8075eed73d78c28209763a7eb67f01f)) +* **deps:** bump starlette to >=1.0.1 on Python 3.10+ to fix PYSEC-2026-161 ([#423](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/423)) ([c650171](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/c6501715bb57348ead817ee90c18622b3c1c31ab)) +* remove macos-13 from test matrix (runner retired) ([#414](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/414)) ([b41ee77](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/b41ee77d6fb61a9e0a76f17d561a221e50fe788a)) + ## [3.10.1](https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v3.10.0...v3.10.1) (2026-02-17) diff --git a/pyproject.toml b/pyproject.toml index 04baad0e..cb6549fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "functions-framework" -version = "3.10.1" +version = "3.10.2" description = "An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team." readme = "README.md" requires-python = ">=3.7, <4" diff --git a/setup.py b/setup.py index 41244a6c..10dfee0d 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( name="functions-framework", - version="3.10.1", + version="3.10.2", description="An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team.", long_description=long_description, long_description_content_type="text/markdown",