diff --git a/.github/blunderbuss.yml b/.github/blunderbuss.yml index bf218bbc3f9..f298284000e 100644 --- a/.github/blunderbuss.yml +++ b/.github/blunderbuss.yml @@ -16,12 +16,6 @@ # Updates should be made to both assign_issues_by & assign_prs_by sections ### assign_issues_by: - # DEE teams - - labels: - - "api: people-and-planet-ai" - to: - - davidcavazos - # AppEco teams - labels: - "api: cloudsql" @@ -57,21 +51,6 @@ assign_issues_by: to: - GoogleCloudPlatform/api-bigquery - # AppEco individuals - - labels: - - "api: aml-ai" - to: - - nickcook - - labels: - - "api: bigquery" - to: - - shollyman - - labels: - - "api: datascienceonramp" - to: - - leahecole - - bradmiro - # Self-service teams - labels: - "api: asset" @@ -120,27 +99,10 @@ assign_issues_by: to: - GoogleCloudPlatform/googleapi-dataplex - # Self-service individuals - - labels: - - "api: auth" - to: - - arithmetic1728 - - labels: - - "api: appengine" - to: - - jinglundong - - ### # Updates should be made to both assign_issues_by & assign_prs_by sections ### assign_prs_by: - # DEE teams - - labels: - - "api: people-and-planet-ai" - to: - - davidcavazos - # AppEco teams - labels: - "api: cloudsql" @@ -170,17 +132,6 @@ assign_prs_by: to: - GoogleCloudPlatform/cloud-dpes-composer - # AppEco individuals - - labels: - - "api: bigquery" - to: - - shollyman - - labels: - - "api: datascienceonramp" - to: - - leahecole - - bradmiro - # Self-service teams - labels: - "api: asset" @@ -235,16 +186,3 @@ assign_prs_by: - "api: connectgateway" to: - GoogleCloudPlatform/connectgateway - # Self-service individuals - - labels: - - "api: auth" - to: - - arithmetic1728 - - labels: - - "api: appengine" - to: - - jinglundong - -### -# Updates should be made to both assign_issues_by & assign_prs_by sections -### diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml index bf76c480c47..1403afe718c 100644 --- a/.github/sync-repo-settings.yaml +++ b/.github/sync-repo-settings.yaml @@ -43,7 +43,6 @@ branchProtectionRules: # List of required status check contexts that must pass for commits to be accepted to matching branches. requiredStatusCheckContexts: - "Kokoro CI - Lint" - - "Kokoro CI - Python 2.7 (App Engine Standard Only)" - "Kokoro CI - Python 3.9" - "Kokoro CI - Python 3.13" - "cla/google" diff --git a/.gitignore b/.gitignore index bcb6b89f6ff..80cf8846a58 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,8 @@ env/ .idea .env* **/venv -**/noxfile.py \ No newline at end of file +**/noxfile.py + +# Auth Local secrets file +auth/custom-credentials/okta/custom-credentials-okta-secrets.json +auth/custom-credentials/aws/custom-credentials-aws-secrets.json diff --git a/.kokoro/docker/Dockerfile b/.kokoro/docker/Dockerfile index ba9af12a933..c37e7f091e2 100644 --- a/.kokoro/docker/Dockerfile +++ b/.kokoro/docker/Dockerfile @@ -110,33 +110,68 @@ RUN curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - \ && rm -rf /var/lib/apt/lists/* \ && rm -f /var/cache/apt/archives/*.deb -COPY fetch_gpg_keys.sh /tmp -# Install the desired versions of Python. -RUN set -ex \ - && export GNUPGHOME="$(mktemp -d)" \ - && echo "disable-ipv6" >> "${GNUPGHOME}/dirmngr.conf" \ - && /tmp/fetch_gpg_keys.sh \ - && for PYTHON_VERSION in 2.7.18 3.7.17 3.8.20 3.9.20 3.10.15 3.11.10 3.12.7 3.13.0; do \ - wget --no-check-certificate -O python-${PYTHON_VERSION}.tar.xz "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz" \ - && wget --no-check-certificate -O python-${PYTHON_VERSION}.tar.xz.asc "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz.asc" \ - && gpg --batch --verify python-${PYTHON_VERSION}.tar.xz.asc python-${PYTHON_VERSION}.tar.xz \ - && rm -r python-${PYTHON_VERSION}.tar.xz.asc \ - && mkdir -p /usr/src/python-${PYTHON_VERSION} \ - && tar -xJC /usr/src/python-${PYTHON_VERSION} --strip-components=1 -f python-${PYTHON_VERSION}.tar.xz \ - && rm python-${PYTHON_VERSION}.tar.xz \ - && cd /usr/src/python-${PYTHON_VERSION} \ - && ./configure \ - --enable-shared \ - # This works only on Python 2.7 and throws a warning on every other - # version, but seems otherwise harmless. - --enable-unicode=ucs4 \ - --with-system-ffi \ - --without-ensurepip \ - && make -j$(nproc) \ - && make install \ - && ldconfig \ +# From https://www.python.org/downloads/metadata/sigstore/ +# Starting with Python 3.14, Sigstore is the only method of signing and verification of release artifacts. +RUN LATEST_VERSION="2.6.1" && \ + wget "https://github.com/sigstore/cosign/releases/download/v${LATEST_VERSION}/cosign_${LATEST_VERSION}_amd64.deb" && \ + dpkg -i cosign_${LATEST_VERSION}_amd64.deb && \ + rm cosign_${LATEST_VERSION}_amd64.deb + +ARG PYTHON_VERSIONS="3.7.17 3.8.20 3.9.23 3.10.18 3.11.13 3.12.11 3.13.8 3.14.0" + +SHELL ["/bin/bash", "-c"] + +RUN set -eux; \ + # Define the required associative arrays completely. + declare -A PYTHON_IDENTITIES; \ + PYTHON_IDENTITIES=(\ + [3.7]="nad@python.org" \ + [3.8]="lukasz@langa.pl" \ + [3.9]="lukasz@langa.pl" \ + [3.10]="pablogsal@python.org" \ + [3.11]="pablogsal@python.org" \ + [3.12]="thomas@python.org" \ + [3.13]="thomas@python.org" \ + [3.14]="hugo@python.org" \ + ); \ + declare -A PYTHON_ISSUERS; \ + PYTHON_ISSUERS=(\ + [3.7]="https://github.com/login/oauth" \ + [3.8]="https://github.com/login/oauth" \ + [3.9]="https://github.com/login/oauth" \ + [3.10]="https://accounts.google.com" \ + [3.11]="https://accounts.google.com" \ + [3.12]="https://accounts.google.com" \ + [3.13]="https://accounts.google.com" \ + [3.14]="https://github.com/login/oauth" \ + ); \ + \ + for VERSION in $PYTHON_VERSIONS; do \ + # 1. Define VERSION_GROUP (e.g., 3.14 from 3.14.0) + VERSION_GROUP="$(echo "${VERSION}" | cut -d . -f 1,2)"; \ + \ + # 2. Look up IDENTITY and ISSUER using the defined VERSION_GROUP + IDENTITY="${PYTHON_IDENTITIES[$VERSION_GROUP]}"; \ + ISSUER="${PYTHON_ISSUERS[$VERSION_GROUP]}"; \ + \ + wget --quiet -O python-${VERSION}.tar.xz "https://www.python.org/ftp/python/${VERSION}/Python-$VERSION.tar.xz" \ + && wget --quiet -O python-${VERSION}.tar.xz.sigstore "https://www.python.org/ftp/python/${VERSION}/Python-$VERSION.tar.xz.sigstore" \ + # Verify the Python tarball signature with cosign. + && cosign verify-blob python-${VERSION}.tar.xz \ + --certificate-oidc-issuer "${ISSUER}" \ + --certificate-identity "${IDENTITY}" \ + --bundle python-${VERSION}.tar.xz.sigstore \ + && mkdir -p /usr/src/python-${VERSION} \ + && tar -xJC /usr/src/python-${VERSION} --strip-components=1 -f python-${VERSION}.tar.xz \ + && rm python-${VERSION}.tar.xz \ + && cd /usr/src/python-${VERSION} \ + && ./configure \ + --enable-shared \ + --with-system-ffi \ + && make -j$(nproc) \ + && make install \ + && ldconfig \ ; done \ - && rm -rf "${GNUPGHOME}" \ && rm -rf /usr/src/python* \ && rm -rf ~/.cache/ @@ -158,6 +193,7 @@ RUN wget --no-check-certificate -O /tmp/get-pip-3-7.py 'https://bootstrap.pypa.i && [ "$(pip list |tac|tac| awk -F '[ ()]+' '$1 == "pip" { print $2; exit }')" = "$PYTHON_PIP_VERSION" ] # Ensure Pip for all python3 versions +RUN python3.14 /tmp/get-pip.py RUN python3.13 /tmp/get-pip.py RUN python3.12 /tmp/get-pip.py RUN python3.11 /tmp/get-pip.py @@ -175,6 +211,7 @@ RUN python3.10 -m pip RUN python3.11 -m pip RUN python3.12 -m pip RUN python3.13 -m pip +RUN python3.14 -m pip # Install "setuptools" for Python 3.12+ (see https://docs.python.org/3/whatsnew/3.12.html#distutils) RUN python3.12 -m pip install --no-cache-dir setuptools diff --git a/.kokoro/docker/fetch_gpg_keys.sh b/.kokoro/docker/fetch_gpg_keys.sh deleted file mode 100755 index 5b8dbbab1ed..00000000000 --- a/.kokoro/docker/fetch_gpg_keys.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash -# 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. - -# A script to fetch gpg keys with retry. - -function retry { - if [[ "${#}" -le 1 ]]; then - echo "Usage: ${0} retry_count commands.." - exit 1 - fi - local retries=${1} - local command="${@:2}" - until [[ "${retries}" -le 0 ]]; do - $command && return 0 - if [[ $? -ne 0 ]]; then - echo "command failed, retrying" - ((retries--)) - fi - done - return 1 -} - -# 2.7.17 (Benjamin Peterson) -retry 3 gpg --keyserver keyserver.ubuntu.com --recv-keys \ - C01E1CAD5EA2C4F0B8E3571504C367C218ADD4FF - -# 3.4.10, 3.5.9 (Larry Hastings) -retry 3 gpg --keyserver keyserver.ubuntu.com --recv-keys \ - 97FC712E4C024BBEA48A61ED3A5CA953F73C700D - -# 3.6.9, 3.7.5 (Ned Deily) -retry 3 gpg --keyserver keyserver.ubuntu.com --recv-keys \ - 0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D - -# 3.8.0, 3.9.0 (Ɓukasz Langa) -retry 3 gpg --keyserver keyserver.ubuntu.com --recv-keys \ - E3FF2839C048B25C084DEBE9B26995E310250568 - -# 3.10.x and 3.11.x (Pablo Galindo Salgado) -retry 3 gpg --keyserver keyserver.ubuntu.com --recv-keys \ - A035C8C19219BA821ECEA86B64E628F8D684696D - -# 3.12.x and 3.13.x source files and tags (Thomas Wouters) -retry 3 gpg --keyserver keyserver.ubuntu.com --recv-keys \ - A821E680E5FA6305 \ No newline at end of file diff --git a/.kokoro/python2.7/common.cfg b/.kokoro/python3.14/common.cfg similarity index 89% rename from .kokoro/python2.7/common.cfg rename to .kokoro/python3.14/common.cfg index ad2c8f64523..8d12e9ed952 100644 --- a/.kokoro/python2.7/common.cfg +++ b/.kokoro/python3.14/common.cfg @@ -1,4 +1,4 @@ -# Copyright 2019 Google LLC +# 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. @@ -44,11 +44,16 @@ action { # Specify which tests to run env_vars: { key: "RUN_TESTS_SESSION" - value: "py-2.7" + value: "py-3.14" } -# Declare build specific Cloud project. env_vars: { key: "BUILD_SPECIFIC_GCLOUD_PROJECT" - value: "python-docs-samples-tests" + value: "python-docs-samples-tests-314" +} + +# Number of test workers. +env_vars: { + key: "NUM_TEST_WORKERS" + value: "10" } diff --git a/.kokoro/python2.7/continuous.cfg b/.kokoro/python3.14/continuous.cfg similarity index 96% rename from .kokoro/python2.7/continuous.cfg rename to .kokoro/python3.14/continuous.cfg index cfbe29058c8..5753c38482a 100644 --- a/.kokoro/python2.7/continuous.cfg +++ b/.kokoro/python3.14/continuous.cfg @@ -1,4 +1,4 @@ -# Copyright 2020 Google LLC +# 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. diff --git a/.kokoro/python2.7/periodic.cfg b/.kokoro/python3.14/periodic.cfg similarity index 81% rename from .kokoro/python2.7/periodic.cfg rename to .kokoro/python3.14/periodic.cfg index 1921dd0a999..8a14abb05ef 100644 --- a/.kokoro/python2.7/periodic.cfg +++ b/.kokoro/python3.14/periodic.cfg @@ -1,4 +1,4 @@ -# Copyright 2020 Google LLC +# 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. @@ -20,3 +20,8 @@ env_vars: { value: ".kokoro/tests/run_tests.sh" } +# Tell Trampoline to upload the Docker image after successfull build. +env_vars: { + key: "TRAMPOLINE_IMAGE_UPLOAD" + value: "true" +} diff --git a/.kokoro/python2.7/presubmit.cfg b/.kokoro/python3.14/presubmit.cfg similarity index 96% rename from .kokoro/python2.7/presubmit.cfg rename to .kokoro/python3.14/presubmit.cfg index d74d307bbed..b8ecd3b0d15 100644 --- a/.kokoro/python2.7/presubmit.cfg +++ b/.kokoro/python3.14/presubmit.cfg @@ -1,4 +1,4 @@ -# Copyright 2019 Google LLC +# 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. diff --git a/.kokoro/tests/run_tests.sh b/.kokoro/tests/run_tests.sh index 1715decdce7..191b40b09e0 100755 --- a/.kokoro/tests/run_tests.sh +++ b/.kokoro/tests/run_tests.sh @@ -58,7 +58,7 @@ if [[ $* == *--only-diff-head* ]]; then fi fi -# Because Kokoro runs presubmit builds simalteneously, we often see +# Because Kokoro runs presubmit builds simultaneously, we often see # quota related errors. I think we can avoid this by changing the # order of tests to execute (e.g. reverse order for py-3.8 # build). Currently there's no easy way to do that with btlr, so we diff --git a/README.md b/README.md index e699be6032e..398102e8902 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ Python samples for [Google Cloud Platform products][cloud]. -[![Build Status][py-2.7-shield]][py-2.7-link] [![Build Status][py-3.9-shield]][py-3.9-link] [![Build Status][py-3.10-shield]][py-3.10-link] [![Build Status][py-3.11-shield]][py-3.11-link] [![Build Status][py-3.12-shield]][py-3.12-link] [![Build Status][py-3.13-shield]][py-3.13-link] - ## Google Cloud Samples Check out some of the samples found on this repository on the [Google Cloud Samples](https://cloud.google.com/docs/samples?l=python) page. @@ -66,16 +64,3 @@ Contributions welcome! See the [Contributing Guide](CONTRIBUTING.md). [cloud_python_setup]: https://cloud.google.com/python/setup [auth_command]: https://cloud.google.com/sdk/gcloud/reference/beta/auth/application-default/login [gcp_auth]: https://cloud.google.com/docs/authentication#projects_and_resources - -[py-2.7-shield]: https://storage.googleapis.com/cloud-devrel-public/python-docs-samples/badges/py-2.7.svg -[py-2.7-link]: https://storage.googleapis.com/cloud-devrel-public/python-docs-samples/badges/py-2.7.html -[py-3.9-shield]: https://storage.googleapis.com/cloud-devrel-public/python-docs-samples/badges/py-3.9.svg -[py-3.9-link]: https://storage.googleapis.com/cloud-devrel-public/python-docs-samples/badges/py-3.9.html -[py-3.10-shield]: https://storage.googleapis.com/cloud-devrel-public/python-docs-samples/badges/py-310.svg -[py-3.10-link]: https://storage.googleapis.com/cloud-devrel-public/python-docs-samples/badges/py-3.10.html -[py-3.11-shield]: https://storage.googleapis.com/cloud-devrel-public/python-docs-samples/badges/py-311.svg -[py-3.11-link]: https://storage.googleapis.com/cloud-devrel-public/python-docs-samples/badges/py-3.11.html -[py-3.12-shield]: https://storage.googleapis.com/cloud-devrel-public/python-docs-samples/badges/py-3.12.svg -[py-3.12-link]: https://storage.googleapis.com/cloud-devrel-public/python-docs-samples/badges/py-3.12.html -[py-3.13-shield]: https://storage.googleapis.com/cloud-devrel-public/python-docs-samples/badges/py-3.13.svg -[py-3.13-link]: https://storage.googleapis.com/cloud-devrel-public/python-docs-samples/badges/py-3.13.html diff --git a/appengine/flexible/README.md b/appengine/flexible/README.md index 0cc851a437e..8f6a03a894f 100644 --- a/appengine/flexible/README.md +++ b/appengine/flexible/README.md @@ -7,8 +7,6 @@ These are samples for using Python on Google App Engine Flexible Environment. These samples are typically referenced from the [docs](https://cloud.google.com/appengine/docs). -For code samples of Python version 3.7 and earlier, please check -https://github.com/GoogleCloudPlatform/python-docs-samples/tree/main/appengine/flexible_python37_and_earlier See our other [Google Cloud Platform github repos](https://github.com/GoogleCloudPlatform) for sample applications and scaffolding for other frameworks and use cases. diff --git a/appengine/flexible/django_cloudsql/noxfile_config.py b/appengine/flexible/django_cloudsql/noxfile_config.py index 30010ba672d..60e19bd8a96 100644 --- a/appengine/flexible/django_cloudsql/noxfile_config.py +++ b/appengine/flexible/django_cloudsql/noxfile_config.py @@ -22,7 +22,7 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - "ignored_versions": ["2.7", "3.7", "3.8", "3.10", "3.12", "3.13"], + "ignored_versions": ["2.7", "3.7", "3.8", "3.9", "3.10", "3.11"], # An envvar key for determining the project id to use. Change it # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a # build specific Cloud project. You can also use your own string diff --git a/appengine/flexible/django_cloudsql/requirements.txt b/appengine/flexible/django_cloudsql/requirements.txt index 5d64cd3b97f..da90b09edaa 100644 --- a/appengine/flexible/django_cloudsql/requirements.txt +++ b/appengine/flexible/django_cloudsql/requirements.txt @@ -1,6 +1,6 @@ -Django==5.2.8 +Django==6.0.1; python_version >= "3.12" gunicorn==23.0.0 -psycopg2-binary==2.9.10 +psycopg2-binary==2.9.11 django-environ==0.12.0 google-cloud-secret-manager==2.21.1 django-storages[google]==1.14.6 diff --git a/appengine/flexible/hello_world/app.yaml b/appengine/flexible/hello_world/app.yaml index ac38af83425..8a9b1e1763b 100644 --- a/appengine/flexible/hello_world/app.yaml +++ b/appengine/flexible/hello_world/app.yaml @@ -17,7 +17,8 @@ env: flex entrypoint: gunicorn -b :$PORT main:app runtime_config: - operating_system: ubuntu22 + operating_system: ubuntu24 + runtime_version: 3.12 # This sample incurs costs to run on the App Engine flexible environment. # The settings below are to reduce costs during testing and are not appropriate diff --git a/appengine/flexible/hello_world/requirements.txt b/appengine/flexible/hello_world/requirements.txt index 068ea0acdfc..bdb61ec2417 100644 --- a/appengine/flexible/hello_world/requirements.txt +++ b/appengine/flexible/hello_world/requirements.txt @@ -1,5 +1,2 @@ -Flask==3.0.3; python_version > '3.6' -Flask==2.3.3; python_version < '3.7' -Werkzeug==3.0.3; python_version > '3.6' -Werkzeug==2.3.8; python_version < '3.7' -gunicorn==23.0.0 \ No newline at end of file +Flask==3.0.3 +gunicorn==22.0.0 diff --git a/appengine/flexible/hello_world_django/app.yaml b/appengine/flexible/hello_world_django/app.yaml index 62b74a9c27e..85096c4adc4 100644 --- a/appengine/flexible/hello_world_django/app.yaml +++ b/appengine/flexible/hello_world_django/app.yaml @@ -17,4 +17,4 @@ env: flex entrypoint: gunicorn -b :$PORT project_name.wsgi runtime_config: - python_version: 3 + operating_system: "ubuntu24" diff --git a/appengine/flexible/hello_world_django/noxfile_config.py b/appengine/flexible/hello_world_django/noxfile_config.py index 196376e7023..692b834f789 100644 --- a/appengine/flexible/hello_world_django/noxfile_config.py +++ b/appengine/flexible/hello_world_django/noxfile_config.py @@ -22,7 +22,7 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - "ignored_versions": ["2.7", "3.7"], + "ignored_versions": ["2.7", "3.7", "3.8", "3.9", "3.10", "3.11"], # Old samples are opted out of enforcing Python type hints # All new samples should feature them "enforce_type_hints": False, diff --git a/appengine/flexible/hello_world_django/project_name/settings.py b/appengine/flexible/hello_world_django/project_name/settings.py index f8b93099d56..bd094b5f576 100644 --- a/appengine/flexible/hello_world_django/project_name/settings.py +++ b/appengine/flexible/hello_world_django/project_name/settings.py @@ -114,3 +114,5 @@ # https://docs.djangoproject.com/en/stable/howto/static-files/ STATIC_URL = "/static/" + +STATIC_ROOT = os.path.join(BASE_DIR, 'static') diff --git a/appengine/flexible/hello_world_django/project_name/urls.py b/appengine/flexible/hello_world_django/project_name/urls.py index 9a393bb42d2..7d3a1e0f315 100644 --- a/appengine/flexible/hello_world_django/project_name/urls.py +++ b/appengine/flexible/hello_world_django/project_name/urls.py @@ -13,12 +13,12 @@ # limitations under the License. from django.contrib import admin -from django.urls import include, path +from django.urls import path import helloworld.views urlpatterns = [ - path("admin/", include(admin.site.urls)), + path("admin/", admin.site.urls), path("", helloworld.views.index), ] diff --git a/appengine/flexible/hello_world_django/requirements.txt b/appengine/flexible/hello_world_django/requirements.txt index 564852cb740..a7f029a554d 100644 --- a/appengine/flexible/hello_world_django/requirements.txt +++ b/appengine/flexible/hello_world_django/requirements.txt @@ -1,2 +1,2 @@ -Django==5.2.5 +Django==6.0.1; python_version >= "3.12" gunicorn==23.0.0 diff --git a/appengine/flexible_python37_and_earlier/README.md b/appengine/flexible_python37_and_earlier/README.md deleted file mode 100644 index 41927a35c3d..00000000000 --- a/appengine/flexible_python37_and_earlier/README.md +++ /dev/null @@ -1,70 +0,0 @@ -## Google App Engine Flexible Environment Python Samples - -[![Open in Cloud Shell][shell_img]][shell_link] - -[shell_img]: http://gstatic.com/cloudssh/images/open-btn.png -[shell_link]: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&page=editor&open_in_editor=appengine/flexible_python37_and_earlier/README.md - -These are samples for using Python on Google App Engine Flexible Environment. These samples are typically referenced from the [docs](https://cloud.google.com/appengine/docs). - -See our other [Google Cloud Platform github repos](https://github.com/GoogleCloudPlatform) for sample applications and -scaffolding for other frameworks and use cases. - -## Run Locally - -Some samples have specific instructions. If there is a README in the sample folder, please refer to it for any additional steps required to run the sample. - -In general, the samples typically require: - -1. Install the [Google Cloud SDK](https://cloud.google.com/sdk/), including the [gcloud tool](https://cloud.google.com/sdk/gcloud/), and [gcloud app component](https://cloud.google.com/sdk/gcloud-app). - -2. Setup the gcloud tool. This provides authentication to Google Cloud APIs and services. - - ``` - gcloud init - ``` - -3. Clone this repo. - - ``` - git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git - cd python-docs-samples/appengine/flexible_python37_and_earlier - ``` - -4. Follow https://cloud.google.com/python/docs/setup to set up a Python development environment. Then run: - - ``` - pip install -r requirements.txt - python main.py - ``` - -5. Visit the application at [http://localhost:8080](http://localhost:8080). - - -## Deploying - -Some samples in this repositories may have special deployment instructions. Refer to the readme in the sample directory. - -1. Use the [Google Developers Console](https://console.developer.google.com) to create a project/app id. (App id and project id are identical) - -2. Setup the gcloud tool, if you haven't already. - - ``` - gcloud init - ``` - -3. Use gcloud to deploy your app. - - ``` - gcloud app deploy - ``` - -4. Congratulations! Your application is now live at `your-app-id.appspot.com` - -## Contributing changes - -* See [CONTRIBUTING.md](../../CONTRIBUTING.md) - -## Licensing - -* See [LICENSE](../../LICENSE) diff --git a/appengine/flexible_python37_and_earlier/analytics/README.md b/appengine/flexible_python37_and_earlier/analytics/README.md deleted file mode 100644 index d4fa88bef8b..00000000000 --- a/appengine/flexible_python37_and_earlier/analytics/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# Google Analytics Measurement Protocol sample for Google App Engine Flexible - -[![Open in Cloud Shell][shell_img]][shell_link] - -[shell_img]: http://gstatic.com/cloudssh/images/open-btn.png -[shell_link]: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&page=editor&open_in_editor=appengine/flexible_python37_and_earlier/analytics/README.md - -This sample demonstrates how to use the [Google Analytics Measurement Protocol](https://developers.google.com/analytics/devguides/collection/protocol/v1/) (or any other SQL server) on [Google App Engine Flexible Environment](https://cloud.google.com/appengine). - -## Setup - -Before you can run or deploy the sample, you will need to do the following: - -1. Create a Google Analytics Property and obtain the Tracking ID. - -2. Update the environment variables in in ``app.yaml`` with your Tracking ID. - -## Running locally - -Refer to the [top-level README](../README.md) for instructions on running and deploying. - -You will need to set the following environment variables via your shell before running the sample: - - $ export GA_TRACKING_ID=[your Tracking ID] - $ python main.py diff --git a/appengine/flexible_python37_and_earlier/analytics/app.yaml b/appengine/flexible_python37_and_earlier/analytics/app.yaml deleted file mode 100644 index 0f5590d7058..00000000000 --- a/appengine/flexible_python37_and_earlier/analytics/app.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2021 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. - -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - python_version: 3 - -#[START gae_flex_analytics_env_variables] -env_variables: - GA_TRACKING_ID: your-tracking-id -#[END gae_flex_analytics_env_variables] diff --git a/appengine/flexible_python37_and_earlier/analytics/main.py b/appengine/flexible_python37_and_earlier/analytics/main.py deleted file mode 100644 index c07ab9b4703..00000000000 --- a/appengine/flexible_python37_and_earlier/analytics/main.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright 2015 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. - -# [START gae_flex_analytics_track_event] -import logging -import os - -from flask import Flask -import requests - - -app = Flask(__name__) - - -# Environment variables are defined in app.yaml. -GA_TRACKING_ID = os.environ["GA_TRACKING_ID"] - - -def track_event(category, action, label=None, value=0): - data = { - "v": "1", # API Version. - "tid": GA_TRACKING_ID, # Tracking ID / Property ID. - # Anonymous Client Identifier. Ideally, this should be a UUID that - # is associated with particular user, device, or browser instance. - "cid": "555", - "t": "event", # Event hit type. - "ec": category, # Event category. - "ea": action, # Event action. - "el": label, # Event label. - "ev": value, # Event value, must be an integer - "ua": "Opera/9.80 (Windows NT 6.0) Presto/2.12.388 Version/12.14", - } - - response = requests.post("https://www.google-analytics.com/collect", data=data) - - # If the request fails, this will raise a RequestException. Depending - # on your application's needs, this may be a non-error and can be caught - # by the caller. - response.raise_for_status() - - -@app.route("/") -def track_example(): - track_event(category="Example", action="test action") - return "Event tracked." - - -@app.errorhandler(500) -def server_error(e): - logging.exception("An error occurred during a request.") - return ( - """ - An internal error occurred:
{}
- See logs for full stacktrace. - """.format( - e - ), - 500, - ) - - -if __name__ == "__main__": - # This is used when running locally. Gunicorn is used to run the - # application on Google App Engine. See entrypoint in app.yaml. - app.run(host="127.0.0.1", port=8080, debug=True) -# [END gae_flex_analytics_track_event] diff --git a/appengine/flexible_python37_and_earlier/analytics/main_test.py b/appengine/flexible_python37_and_earlier/analytics/main_test.py deleted file mode 100644 index 02914bda79d..00000000000 --- a/appengine/flexible_python37_and_earlier/analytics/main_test.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2016 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 re - -import pytest -import responses - - -@pytest.fixture -def app(monkeypatch): - monkeypatch.setenv("GA_TRACKING_ID", "1234") - - import main - - main.app.testing = True - return main.app.test_client() - - -@responses.activate -def test_tracking(app): - responses.add( - responses.POST, re.compile(r".*"), body="{}", content_type="application/json" - ) - - r = app.get("/") - - assert r.status_code == 200 - assert "Event tracked" in r.data.decode("utf-8") - - assert len(responses.calls) == 1 - request_body = responses.calls[0].request.body - assert "tid=1234" in request_body - assert "ea=test+action" in request_body diff --git a/appengine/flexible_python37_and_earlier/analytics/noxfile_config.py b/appengine/flexible_python37_and_earlier/analytics/noxfile_config.py deleted file mode 100644 index 1665dd736f8..00000000000 --- a/appengine/flexible_python37_and_earlier/analytics/noxfile_config.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2023 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. - -# Default TEST_CONFIG_OVERRIDE for python repos. - -# You can copy this file into your directory, then it will be imported from -# the noxfile.py. - -# The source of truth: -# https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/noxfile_config.py - -TEST_CONFIG_OVERRIDE = { - # You can opt out from the test for specific Python versions. - # Skipping for Python 3.9 due to pyarrow compilation failure. - "ignored_versions": ["2.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"], - # Old samples are opted out of enforcing Python type hints - # All new samples should feature them - "enforce_type_hints": False, - # An envvar key for determining the project id to use. Change it - # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a - # build specific Cloud project. You can also use your own string - # to use your own Cloud project. - "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", - # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', - # A dictionary you want to inject into your test. Don't put any - # secrets here. These values will override predefined values. - "envs": {}, -} diff --git a/appengine/flexible_python37_and_earlier/analytics/requirements-test.txt b/appengine/flexible_python37_and_earlier/analytics/requirements-test.txt deleted file mode 100644 index e89f6031ad7..00000000000 --- a/appengine/flexible_python37_and_earlier/analytics/requirements-test.txt +++ /dev/null @@ -1,3 +0,0 @@ -pytest==8.2.0 -responses==0.17.0; python_version < '3.7' -responses==0.23.1; python_version > '3.6' diff --git a/appengine/flexible_python37_and_earlier/analytics/requirements.txt b/appengine/flexible_python37_and_earlier/analytics/requirements.txt deleted file mode 100644 index 9bfb6dcc546..00000000000 --- a/appengine/flexible_python37_and_earlier/analytics/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -Flask==3.0.3; python_version > '3.6' -Flask==2.3.3; python_version < '3.7' -gunicorn==23.0.0 -requests[security]==2.31.0 -Werkzeug==3.0.3 diff --git a/appengine/flexible_python37_and_earlier/datastore/README.md b/appengine/flexible_python37_and_earlier/datastore/README.md deleted file mode 100644 index 5676c53aab9..00000000000 --- a/appengine/flexible_python37_and_earlier/datastore/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# Python Google Cloud Datastore sample for Google App Engine Flexible Environment - -[![Open in Cloud Shell][shell_img]][shell_link] - -[shell_img]: http://gstatic.com/cloudssh/images/open-btn.png -[shell_link]: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&page=editor&open_in_editor=appengine/flexible_python37_and_earlier/datastore/README.md - -This sample demonstrates how to use [Google Cloud Datastore](https://cloud.google.com/datastore/) on [Google App Engine Flexible Environment](https://cloud.google.com/appengine). - -## Setup - -Before you can run or deploy the sample, you will need to enable the Cloud Datastore API in the [Google Developers Console](https://console.developers.google.com/project/_/apiui/apiview/datastore/overview). - -## Running locally - -Refer to the [top-level README](../README.md) for instructions on running and deploying. - -When running locally, you can use the [Google Cloud SDK](https://cloud.google.com/sdk) to provide authentication to use Google Cloud APIs: - - $ gcloud init - -Starting your application: - - $ python main.py diff --git a/appengine/flexible_python37_and_earlier/datastore/app.yaml b/appengine/flexible_python37_and_earlier/datastore/app.yaml deleted file mode 100644 index ca76f83fc3b..00000000000 --- a/appengine/flexible_python37_and_earlier/datastore/app.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2021 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. - -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - python_version: 3 diff --git a/appengine/flexible_python37_and_earlier/datastore/main.py b/appengine/flexible_python37_and_earlier/datastore/main.py deleted file mode 100644 index ac1cec4ee5b..00000000000 --- a/appengine/flexible_python37_and_earlier/datastore/main.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright 2015 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 datetime -import logging -import socket - -from flask import Flask, request -from google.cloud import datastore - - -app = Flask(__name__) - - -def is_ipv6(addr): - """Checks if a given address is an IPv6 address.""" - try: - socket.inet_pton(socket.AF_INET6, addr) - return True - except OSError: - return False - - -# [START gae_flex_datastore_app] -@app.route("/") -def index(): - ds = datastore.Client() - - user_ip = request.remote_addr - - # Keep only the first two octets of the IP address. - if is_ipv6(user_ip): - user_ip = ":".join(user_ip.split(":")[:2]) - else: - user_ip = ".".join(user_ip.split(".")[:2]) - - entity = datastore.Entity(key=ds.key("visit")) - entity.update( - { - "user_ip": user_ip, - "timestamp": datetime.datetime.now(tz=datetime.timezone.utc), - } - ) - - ds.put(entity) - query = ds.query(kind="visit", order=("-timestamp",)) - - results = [] - for x in query.fetch(limit=10): - try: - results.append("Time: {timestamp} Addr: {user_ip}".format(**x)) - except KeyError: - print("Error with result format, skipping entry.") - - output = "Last 10 visits:\n{}".format("\n".join(results)) - - return output, 200, {"Content-Type": "text/plain; charset=utf-8"} - - -# [END gae_flex_datastore_app] - - -@app.errorhandler(500) -def server_error(e): - logging.exception("An error occurred during a request.") - return ( - """ - An internal error occurred:
{}
- See logs for full stacktrace. - """.format( - e - ), - 500, - ) - - -if __name__ == "__main__": - # This is used when running locally. Gunicorn is used to run the - # application on Google App Engine. See entrypoint in app.yaml. - app.run(host="127.0.0.1", port=8080, debug=True) diff --git a/appengine/flexible_python37_and_earlier/datastore/main_test.py b/appengine/flexible_python37_and_earlier/datastore/main_test.py deleted file mode 100644 index 6b17c44ca79..00000000000 --- a/appengine/flexible_python37_and_earlier/datastore/main_test.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2015 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 main - - -def test_index(): - main.app.testing = True - client = main.app.test_client() - - r = client.get("/", environ_base={"REMOTE_ADDR": "127.0.0.1"}) - assert r.status_code == 200 - assert "Last 10 visits" in r.data.decode("utf-8") diff --git a/appengine/flexible_python37_and_earlier/datastore/noxfile_config.py b/appengine/flexible_python37_and_earlier/datastore/noxfile_config.py deleted file mode 100644 index 1665dd736f8..00000000000 --- a/appengine/flexible_python37_and_earlier/datastore/noxfile_config.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2023 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. - -# Default TEST_CONFIG_OVERRIDE for python repos. - -# You can copy this file into your directory, then it will be imported from -# the noxfile.py. - -# The source of truth: -# https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/noxfile_config.py - -TEST_CONFIG_OVERRIDE = { - # You can opt out from the test for specific Python versions. - # Skipping for Python 3.9 due to pyarrow compilation failure. - "ignored_versions": ["2.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"], - # Old samples are opted out of enforcing Python type hints - # All new samples should feature them - "enforce_type_hints": False, - # An envvar key for determining the project id to use. Change it - # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a - # build specific Cloud project. You can also use your own string - # to use your own Cloud project. - "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", - # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', - # A dictionary you want to inject into your test. Don't put any - # secrets here. These values will override predefined values. - "envs": {}, -} diff --git a/appengine/flexible_python37_and_earlier/datastore/requirements-test.txt b/appengine/flexible_python37_and_earlier/datastore/requirements-test.txt deleted file mode 100644 index 15d066af319..00000000000 --- a/appengine/flexible_python37_and_earlier/datastore/requirements-test.txt +++ /dev/null @@ -1 +0,0 @@ -pytest==8.2.0 diff --git a/appengine/flexible_python37_and_earlier/datastore/requirements.txt b/appengine/flexible_python37_and_earlier/datastore/requirements.txt deleted file mode 100644 index ff3c9dcce0c..00000000000 --- a/appengine/flexible_python37_and_earlier/datastore/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -Flask==3.0.3; python_version > '3.6' -Flask==2.3.3; python_version < '3.7' -google-cloud-datastore==2.20.2 -gunicorn==23.0.0 -Werkzeug==3.0.3 diff --git a/appengine/flexible_python37_and_earlier/django_cloudsql/README.md b/appengine/flexible_python37_and_earlier/django_cloudsql/README.md deleted file mode 100644 index 60e3ff2f5e7..00000000000 --- a/appengine/flexible_python37_and_earlier/django_cloudsql/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# Getting started with Django on Google Cloud Platform on App Engine Flexible - -[![Open in Cloud Shell][shell_img]][shell_link] - -[shell_img]: http://gstatic.com/cloudssh/images/open-btn.png -[shell_link]: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&page=editor&open_in_editor=appengine/flexible_python37_and_earlier/django_cloudsql/README.md - -This repository is an example of how to run a [Django](https://www.djangoproject.com/) -app on Google App Engine Flexible Environment. It uses the -[Writing your first Django app](https://docs.djangoproject.com/en/stable/intro/tutorial01/) as the -example app to deploy. - - -# Tutorial -See our [Running Django in the App Engine Flexible Environment](https://cloud.google.com/python/django/flexible-environment) tutorial for instructions for setting up and deploying this sample application. - - -## Contributing changes - -* See [CONTRIBUTING.md](CONTRIBUTING.md) - - -## Licensing - -* See [LICENSE](LICENSE) diff --git a/appengine/flexible_python37_and_earlier/django_cloudsql/app.yaml b/appengine/flexible_python37_and_earlier/django_cloudsql/app.yaml deleted file mode 100644 index 7fcf498d62e..00000000000 --- a/appengine/flexible_python37_and_earlier/django_cloudsql/app.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2021 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. -# -# [START gaeflex_py_django_app_yaml] -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT mysite.wsgi - -beta_settings: - cloud_sql_instances: PROJECT_ID:REGION:INSTANCE_NAME - -runtime_config: - python_version: 3.7 -# [END gaeflex_py_django_app_yaml] diff --git a/appengine/flexible_python37_and_earlier/django_cloudsql/mysite/settings.py b/appengine/flexible_python37_and_earlier/django_cloudsql/mysite/settings.py deleted file mode 100644 index ab4d8e7d5e1..00000000000 --- a/appengine/flexible_python37_and_earlier/django_cloudsql/mysite/settings.py +++ /dev/null @@ -1,176 +0,0 @@ -# Copyright 2015 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 io -import os - -import environ -from google.cloud import secretmanager - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - -# [START gaeflex_py_django_secret_config] -env = environ.Env(DEBUG=(bool, False)) -env_file = os.path.join(BASE_DIR, ".env") - -if os.path.isfile(env_file): - # Use a local secret file, if provided - - env.read_env(env_file) -# [START_EXCLUDE] -elif os.getenv("TRAMPOLINE_CI", None): - # Create local settings if running with CI, for unit testing - - placeholder = ( - f"SECRET_KEY=a\n" - "GS_BUCKET_NAME=None\n" - f"DATABASE_URL=sqlite://{os.path.join(BASE_DIR, 'db.sqlite3')}" - ) - env.read_env(io.StringIO(placeholder)) -# [END_EXCLUDE] -elif os.environ.get("GOOGLE_CLOUD_PROJECT", None): - # Pull secrets from Secret Manager - project_id = os.environ.get("GOOGLE_CLOUD_PROJECT") - - client = secretmanager.SecretManagerServiceClient() - settings_name = os.environ.get("SETTINGS_NAME", "django_settings") - name = f"projects/{project_id}/secrets/{settings_name}/versions/latest" - payload = client.access_secret_version(name=name).payload.data.decode("UTF-8") - - env.read_env(io.StringIO(payload)) -else: - raise Exception("No local .env or GOOGLE_CLOUD_PROJECT detected. No secrets found.") -# [END gaeflex_py_django_secret_config] - -SECRET_KEY = env("SECRET_KEY") - -# SECURITY WARNING: don't run with debug turned on in production! -# Change this to "False" when you are ready for production -DEBUG = env("DEBUG") - -# SECURITY WARNING: App Engine's security features ensure that it is safe to -# have ALLOWED_HOSTS = ['*'] when the app is deployed. If you deploy a Django -# app not on App Engine, make sure to set an appropriate host here. -ALLOWED_HOSTS = ["*"] - -# Application definition - -INSTALLED_APPS = ( - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "django.contrib.staticfiles", - "polls", -) - -MIDDLEWARE = ( - "django.middleware.security.SecurityMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", -) - -ROOT_URLCONF = "mysite.urls" - -TEMPLATES = [ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [], - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "django.template.context_processors.debug", - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - ], - }, - }, -] - -WSGI_APPLICATION = "mysite.wsgi.application" - -# Database - -# [START gaeflex_py_django_database_config] -# Use django-environ to parse the connection string -DATABASES = {"default": env.db()} - -# If the flag as been set, configure to use proxy -if os.getenv("USE_CLOUD_SQL_AUTH_PROXY", None): - DATABASES["default"]["HOST"] = "127.0.0.1" - DATABASES["default"]["PORT"] = 5432 - -# [END gaeflex_py_django_database_config] - -# Use a in-memory sqlite3 database when testing in CI systems -if os.getenv("TRAMPOLINE_CI", None): - DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": os.path.join(BASE_DIR, "db.sqlite3"), - } - } - - -# Password validation - -AUTH_PASSWORD_VALIDATORS = [ - { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", # noqa: 501 - }, - { - "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", # noqa: 501 - }, - { - "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", # noqa: 501 - }, - { - "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", # noqa: 501 - }, -] - -# Internationalization -# https://docs.djangoproject.com/en/stable/topics/i18n/ - -LANGUAGE_CODE = "en-us" - -TIME_ZONE = "UTC" - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - -# Static files (CSS, JavaScript, Images) -# [START gaeflex_py_django_static_config] -# Define static storage via django-storages[google] -GS_BUCKET_NAME = env("GS_BUCKET_NAME") -STATIC_URL = "/static/" -DEFAULT_FILE_STORAGE = "storages.backends.gcloud.GoogleCloudStorage" -STATICFILES_STORAGE = "storages.backends.gcloud.GoogleCloudStorage" -GS_DEFAULT_ACL = "publicRead" -# [END gaeflex_py_django_static_config] - -# Default primary key field type -# https://docs.djangoproject.com/en/stable/ref/settings/#default-auto-field - -DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/appengine/flexible_python37_and_earlier/django_cloudsql/mysite/urls.py b/appengine/flexible_python37_and_earlier/django_cloudsql/mysite/urls.py deleted file mode 100644 index 62e72564fc2..00000000000 --- a/appengine/flexible_python37_and_earlier/django_cloudsql/mysite/urls.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2015 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. - -# [START gaeflex_py_django_local_static] -from django.conf import settings -from django.conf.urls.static import static -from django.contrib import admin -from django.urls import include, path - -urlpatterns = [ - path("", include("polls.urls")), - path("admin/", admin.site.urls), -] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) -# [END gaeflex_py_django_local_static] diff --git a/appengine/flexible_python37_and_earlier/django_cloudsql/mysite/wsgi.py b/appengine/flexible_python37_and_earlier/django_cloudsql/mysite/wsgi.py deleted file mode 100644 index 968cf994b60..00000000000 --- a/appengine/flexible_python37_and_earlier/django_cloudsql/mysite/wsgi.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2015 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 os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") - -application = get_wsgi_application() diff --git a/appengine/flexible_python37_and_earlier/django_cloudsql/noxfile_config.py b/appengine/flexible_python37_and_earlier/django_cloudsql/noxfile_config.py deleted file mode 100644 index a51f3680ad6..00000000000 --- a/appengine/flexible_python37_and_earlier/django_cloudsql/noxfile_config.py +++ /dev/null @@ -1,39 +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. - -# Default TEST_CONFIG_OVERRIDE for python repos. - -# You can copy this file into your directory, then it will be imported from -# the noxfile.py. - -# The source of truth: -# https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/noxfile_config.py - -TEST_CONFIG_OVERRIDE = { - # You can opt out from the test for specific Python versions. - # Skipping for Python 3.9 due to pyarrow compilation failure. - "ignored_versions": ["2.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"], - # Old samples are opted out of enforcing Python type hints - # All new samples should feature them - "enforce_type_hints": False, - # An envvar key for determining the project id to use. Change it - # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a - # build specific Cloud project. You can also use your own string - # to use your own Cloud project. - "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", - # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', - # A dictionary you want to inject into your test. Don't put any - # secrets here. These values will override predefined values. - "envs": {"DJANGO_SETTINGS_MODULE": "mysite.settings"}, -} diff --git a/appengine/flexible_python37_and_earlier/django_cloudsql/polls/__init__.py b/appengine/flexible_python37_and_earlier/django_cloudsql/polls/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/appengine/flexible_python37_and_earlier/django_cloudsql/polls/models.py b/appengine/flexible_python37_and_earlier/django_cloudsql/polls/models.py deleted file mode 100644 index 5d2bf302721..00000000000 --- a/appengine/flexible_python37_and_earlier/django_cloudsql/polls/models.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2015 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 django.db import models - - -class Question(models.Model): - question_text = models.CharField(max_length=200) - pub_date = models.DateTimeField("date published") - - -class Choice(models.Model): - question = models.ForeignKey(Question, on_delete=models.CASCADE) - choice_text = models.CharField(max_length=200) - votes = models.IntegerField(default=0) - - -# Create your models here. diff --git a/appengine/flexible_python37_and_earlier/django_cloudsql/polls/test_polls.py b/appengine/flexible_python37_and_earlier/django_cloudsql/polls/test_polls.py deleted file mode 100644 index 3ce4c624bbb..00000000000 --- a/appengine/flexible_python37_and_earlier/django_cloudsql/polls/test_polls.py +++ /dev/null @@ -1,22 +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 django.test import Client, TestCase # noqa: 401 - - -class PollViewTests(TestCase): - def test_index_view(self): - response = self.client.get("/") - assert response.status_code == 200 - assert "Hello, world" in str(response.content) diff --git a/appengine/flexible_python37_and_earlier/django_cloudsql/polls/views.py b/appengine/flexible_python37_and_earlier/django_cloudsql/polls/views.py deleted file mode 100644 index 262f571d568..00000000000 --- a/appengine/flexible_python37_and_earlier/django_cloudsql/polls/views.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright 2015 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 django.http import HttpResponse - - -def index(request): - return HttpResponse("Hello, world. You're at the polls index.") diff --git a/appengine/flexible_python37_and_earlier/django_cloudsql/requirements-test.txt b/appengine/flexible_python37_and_earlier/django_cloudsql/requirements-test.txt deleted file mode 100644 index 5e5d2c73a81..00000000000 --- a/appengine/flexible_python37_and_earlier/django_cloudsql/requirements-test.txt +++ /dev/null @@ -1,2 +0,0 @@ -pytest==8.2.0 -pytest-django==4.9.0 diff --git a/appengine/flexible_python37_and_earlier/django_cloudsql/requirements.txt b/appengine/flexible_python37_and_earlier/django_cloudsql/requirements.txt deleted file mode 100644 index 284290f2532..00000000000 --- a/appengine/flexible_python37_and_earlier/django_cloudsql/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -Django==5.2.5 -gunicorn==23.0.0 -psycopg2-binary==2.9.10 -django-environ==0.12.0 -google-cloud-secret-manager==2.21.1 -django-storages[google]==1.14.6 diff --git a/appengine/flexible_python37_and_earlier/hello_world/app.yaml b/appengine/flexible_python37_and_earlier/hello_world/app.yaml deleted file mode 100644 index 7aa7a47e159..00000000000 --- a/appengine/flexible_python37_and_earlier/hello_world/app.yaml +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2021 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. - -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - python_version: 3 - -# This sample incurs costs to run on the App Engine flexible environment. -# The settings below are to reduce costs during testing and are not appropriate -# for production use. For more information, see: -# https://cloud.google.com/appengine/docs/flexible/python/configuring-your-app-with-app-yaml -manual_scaling: - instances: 1 -resources: - cpu: 1 - memory_gb: 0.5 - disk_size_gb: 10 diff --git a/appengine/flexible_python37_and_earlier/hello_world/main.py b/appengine/flexible_python37_and_earlier/hello_world/main.py deleted file mode 100644 index eba195ed4fd..00000000000 --- a/appengine/flexible_python37_and_earlier/hello_world/main.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2015 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. - -# [START gae_flex_quickstart] -from flask import Flask - -app = Flask(__name__) - - -@app.route("/") -def hello(): - """Return a friendly HTTP greeting. - - Returns: - A string with the words 'Hello World!'. - """ - return "Hello World!" - - -if __name__ == "__main__": - # This is used when running locally only. When deploying to Google App - # Engine, a webserver process such as Gunicorn will serve the app. - app.run(host="127.0.0.1", port=8080, debug=True) -# [END gae_flex_quickstart] diff --git a/appengine/flexible_python37_and_earlier/hello_world/main_test.py b/appengine/flexible_python37_and_earlier/hello_world/main_test.py deleted file mode 100644 index a6049b094f9..00000000000 --- a/appengine/flexible_python37_and_earlier/hello_world/main_test.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2015 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 main - - -def test_index(): - main.app.testing = True - client = main.app.test_client() - - r = client.get("/") - assert r.status_code == 200 - assert "Hello World" in r.data.decode("utf-8") diff --git a/appengine/flexible_python37_and_earlier/hello_world/noxfile_config.py b/appengine/flexible_python37_and_earlier/hello_world/noxfile_config.py deleted file mode 100644 index 1665dd736f8..00000000000 --- a/appengine/flexible_python37_and_earlier/hello_world/noxfile_config.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2023 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. - -# Default TEST_CONFIG_OVERRIDE for python repos. - -# You can copy this file into your directory, then it will be imported from -# the noxfile.py. - -# The source of truth: -# https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/noxfile_config.py - -TEST_CONFIG_OVERRIDE = { - # You can opt out from the test for specific Python versions. - # Skipping for Python 3.9 due to pyarrow compilation failure. - "ignored_versions": ["2.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"], - # Old samples are opted out of enforcing Python type hints - # All new samples should feature them - "enforce_type_hints": False, - # An envvar key for determining the project id to use. Change it - # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a - # build specific Cloud project. You can also use your own string - # to use your own Cloud project. - "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", - # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', - # A dictionary you want to inject into your test. Don't put any - # secrets here. These values will override predefined values. - "envs": {}, -} diff --git a/appengine/flexible_python37_and_earlier/hello_world/requirements-test.txt b/appengine/flexible_python37_and_earlier/hello_world/requirements-test.txt deleted file mode 100644 index 15d066af319..00000000000 --- a/appengine/flexible_python37_and_earlier/hello_world/requirements-test.txt +++ /dev/null @@ -1 +0,0 @@ -pytest==8.2.0 diff --git a/appengine/flexible_python37_and_earlier/hello_world/requirements.txt b/appengine/flexible_python37_and_earlier/hello_world/requirements.txt deleted file mode 100644 index 055e4c6a13d..00000000000 --- a/appengine/flexible_python37_and_earlier/hello_world/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -Flask==3.0.3; python_version > '3.6' -Flask==3.0.3; python_version < '3.7' -gunicorn==23.0.0 -Werkzeug==3.0.3 diff --git a/appengine/flexible_python37_and_earlier/hello_world_django/.gitignore b/appengine/flexible_python37_and_earlier/hello_world_django/.gitignore deleted file mode 100644 index 49ef2557b16..00000000000 --- a/appengine/flexible_python37_and_earlier/hello_world_django/.gitignore +++ /dev/null @@ -1 +0,0 @@ -db.sqlite3 diff --git a/appengine/flexible_python37_and_earlier/hello_world_django/README.md b/appengine/flexible_python37_and_earlier/hello_world_django/README.md deleted file mode 100644 index d6705b131a3..00000000000 --- a/appengine/flexible_python37_and_earlier/hello_world_django/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# Django sample for Google App Engine Flexible Environment - -[![Open in Cloud Shell][shell_img]][shell_link] - -[shell_img]: http://gstatic.com/cloudssh/images/open-btn.png -[shell_link]: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&page=editor&open_in_editor=appengine/flexible_python37_and_earlier/hello_world_django/README.md - -This is a basic hello world [Django](https://www.djangoproject.com/) example -for [Google App Engine Flexible Environment](https://cloud.google.com/appengine). - -## Running locally - -You can run locally using django's `manage.py`: - - $ python manage.py runserver - -## Deployment & how the application runs on Google App Engine. - -Follow the standard deployment instructions in -[the top-level README](../README.md). Google App Engine runs the application -using [gunicorn](http://gunicorn.org/) as defined by `entrypoint` in -[`app.yaml`](app.yaml). You can use a different WSGI container if you want, as -long as it listens for web traffic on port `$PORT` and is declared in -[`requirements.txt`](requirements.txt). - -## How this was created - -To set up Python development environment, please follow -https://cloud.google.com/python/docs/setup. - -This project was created using standard Django commands: - - $ virtualenv env - $ source env/bin/activate - $ pip install django gunicorn - $ pip freeze > requirements.txt - $ django-admin startproject project_name - $ python manage.py startapp helloworld - -Then, we added a simple view in `hellworld.views`, added the app to -`project_name.settings.INSTALLED_APPS`, and finally added a URL rule to -`project_name.urls`. - -In order to deploy to Google App Engine, we created a simple -[`app.yaml`](app.yaml). - -## Database notice - -This sample project uses Django's default sqlite database. This isn't suitable -for production as your application can run multiple instances and each will -have a different sqlite database. Additionally, instance disks are ephemeral, -so data will not survive restarts. - -For production applications running on Google Cloud Platform, you have -the following options: - -* Use [Cloud SQL](https://cloud.google.com/sql), a fully-managed MySQL database. - There is a [Flask CloudSQL](../cloudsql) sample that should be straightforward - to adapt to Django. -* Use any database of your choice hosted on - [Google Compute Engine](https://cloud.google.com/compute). The - [Cloud Launcher](https://cloud.google.com/launcher/) can be used to easily - deploy common databases. -* Use third-party database services, or services hosted by other providers, - provided you have configured access. - diff --git a/appengine/flexible_python37_and_earlier/hello_world_django/helloworld/__init__.py b/appengine/flexible_python37_and_earlier/hello_world_django/helloworld/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/appengine/flexible_python37_and_earlier/hello_world_django/helloworld/views.py b/appengine/flexible_python37_and_earlier/hello_world_django/helloworld/views.py deleted file mode 100644 index 71c0106bda1..00000000000 --- a/appengine/flexible_python37_and_earlier/hello_world_django/helloworld/views.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python -# Copyright 2015 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 django.http import HttpResponse - - -def index(request): - return HttpResponse("Hello, World. This is Django running on Google App Engine") diff --git a/appengine/flexible_python37_and_earlier/hello_world_django/manage.py b/appengine/flexible_python37_and_earlier/hello_world_django/manage.py deleted file mode 100755 index c213c77eca6..00000000000 --- a/appengine/flexible_python37_and_earlier/hello_world_django/manage.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python -# Copyright 2021 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 os -import sys - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project_name.settings") - - from django.core.management import execute_from_command_line - - execute_from_command_line(sys.argv) diff --git a/appengine/flexible_python37_and_earlier/hello_world_django/project_name/__init__.py b/appengine/flexible_python37_and_earlier/hello_world_django/project_name/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/appengine/flexible_python37_and_earlier/hello_world_django/project_name/settings.py b/appengine/flexible_python37_and_earlier/hello_world_django/project_name/settings.py deleted file mode 100644 index f8b93099d56..00000000000 --- a/appengine/flexible_python37_and_earlier/hello_world_django/project_name/settings.py +++ /dev/null @@ -1,116 +0,0 @@ -# Copyright 2015 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. - -""" -Django settings for project_name project. - -Generated by 'django-admin startproject' using Django 1.8.4. - -For more information on this file, see -https://docs.djangoproject.com/en/stable/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/stable/ref/settings/ -""" - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -import os - -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/stable/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = "qgw!j*bpxo7g&o1ux-(2ph818ojfj(3c#-#*_8r^8&hq5jg$3@" - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = [] - - -# Application definition - -INSTALLED_APPS = ( - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "django.contrib.staticfiles", - "helloworld", -) - -MIDDLEWARE = ( - "django.middleware.security.SecurityMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", -) - -ROOT_URLCONF = "project_name.urls" - -TEMPLATES = [ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [], - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "django.template.context_processors.debug", - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - ], - }, - }, -] - -WSGI_APPLICATION = "project_name.wsgi.application" - - -# Database -# https://docs.djangoproject.com/en/stable/ref/settings/#databases - -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": os.path.join(BASE_DIR, "db.sqlite3"), - } -} - - -# Internationalization -# https://docs.djangoproject.com/en/stable/topics/i18n/ - -LANGUAGE_CODE = "en-us" - -TIME_ZONE = "UTC" - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/stable/howto/static-files/ - -STATIC_URL = "/static/" diff --git a/appengine/flexible_python37_and_earlier/hello_world_django/project_name/urls.py b/appengine/flexible_python37_and_earlier/hello_world_django/project_name/urls.py deleted file mode 100644 index 9a393bb42d2..00000000000 --- a/appengine/flexible_python37_and_earlier/hello_world_django/project_name/urls.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2015 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 django.contrib import admin -from django.urls import include, path - -import helloworld.views - - -urlpatterns = [ - path("admin/", include(admin.site.urls)), - path("", helloworld.views.index), -] diff --git a/appengine/flexible_python37_and_earlier/hello_world_django/project_name/wsgi.py b/appengine/flexible_python37_and_earlier/hello_world_django/project_name/wsgi.py deleted file mode 100644 index c069a496999..00000000000 --- a/appengine/flexible_python37_and_earlier/hello_world_django/project_name/wsgi.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2021 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. - -""" -WSGI config for project_name project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/stable/howto/deployment/wsgi/ -""" - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project_name.settings") - -application = get_wsgi_application() diff --git a/appengine/flexible_python37_and_earlier/hello_world_django/requirements-test.txt b/appengine/flexible_python37_and_earlier/hello_world_django/requirements-test.txt deleted file mode 100644 index 15d066af319..00000000000 --- a/appengine/flexible_python37_and_earlier/hello_world_django/requirements-test.txt +++ /dev/null @@ -1 +0,0 @@ -pytest==8.2.0 diff --git a/appengine/flexible_python37_and_earlier/hello_world_django/requirements.txt b/appengine/flexible_python37_and_earlier/hello_world_django/requirements.txt deleted file mode 100644 index 564852cb740..00000000000 --- a/appengine/flexible_python37_and_earlier/hello_world_django/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -Django==5.2.5 -gunicorn==23.0.0 diff --git a/appengine/flexible_python37_and_earlier/metadata/main.py b/appengine/flexible_python37_and_earlier/metadata/main.py deleted file mode 100644 index 9d1e320865a..00000000000 --- a/appengine/flexible_python37_and_earlier/metadata/main.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright 2015 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 logging - -from flask import Flask -import requests - - -logging.basicConfig(level=logging.INFO) -app = Flask(__name__) - - -# [START gae_flex_metadata] -METADATA_NETWORK_INTERFACE_URL = ( - "http://metadata/computeMetadata/v1/instance/network-interfaces/0/" - "access-configs/0/external-ip" -) - - -def get_external_ip(): - """Gets the instance's external IP address from the Compute Engine metadata - server. - - If the metadata server is unavailable, it assumes that the application is running locally. - - Returns: - The instance's external IP address, or the string 'localhost' if the IP address - is not available. - """ - try: - r = requests.get( - METADATA_NETWORK_INTERFACE_URL, - headers={"Metadata-Flavor": "Google"}, - timeout=2, - ) - return r.text - except requests.RequestException: - logging.info("Metadata server could not be reached, assuming local.") - return "localhost" - - -# [END gae_flex_metadata] - - -@app.route("/") -def index(): - """Serves a string with the instance's external IP address. - - Websocket connections must be made directly to this instance. - - Returns: - A formatted string containing the instance's external IP address. - """ - external_ip = get_external_ip() - return f"External IP: {external_ip}" - - -@app.errorhandler(500) -def server_error(e): - """Serves a formatted message on-error. - - Returns: - The error message and a code 500 status. - """ - logging.exception("An error occurred during a request.") - return ( - f"An internal error occurred:
{e}

See logs for full stacktrace.", - 500, - ) - - -if __name__ == "__main__": - # This is used when running locally. Gunicorn is used to run the - # application on Google App Engine. See entrypoint in app.yaml. - app.run(host="127.0.0.1", port=8080, debug=True) diff --git a/appengine/flexible_python37_and_earlier/metadata/main_test.py b/appengine/flexible_python37_and_earlier/metadata/main_test.py deleted file mode 100644 index 55d345d170d..00000000000 --- a/appengine/flexible_python37_and_earlier/metadata/main_test.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2023 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 main - - -def test_index(): - main.app.testing = True - client = main.app.test_client() - - external_ip = main.get_external_ip() - - r = client.get("/") - assert r.status_code == 200 - assert f"External IP: {external_ip}" in r.data.decode("utf-8") diff --git a/appengine/flexible_python37_and_earlier/metadata/noxfile_config.py b/appengine/flexible_python37_and_earlier/metadata/noxfile_config.py deleted file mode 100644 index 1665dd736f8..00000000000 --- a/appengine/flexible_python37_and_earlier/metadata/noxfile_config.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2023 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. - -# Default TEST_CONFIG_OVERRIDE for python repos. - -# You can copy this file into your directory, then it will be imported from -# the noxfile.py. - -# The source of truth: -# https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/noxfile_config.py - -TEST_CONFIG_OVERRIDE = { - # You can opt out from the test for specific Python versions. - # Skipping for Python 3.9 due to pyarrow compilation failure. - "ignored_versions": ["2.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"], - # Old samples are opted out of enforcing Python type hints - # All new samples should feature them - "enforce_type_hints": False, - # An envvar key for determining the project id to use. Change it - # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a - # build specific Cloud project. You can also use your own string - # to use your own Cloud project. - "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", - # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', - # A dictionary you want to inject into your test. Don't put any - # secrets here. These values will override predefined values. - "envs": {}, -} diff --git a/appengine/flexible_python37_and_earlier/metadata/requirements-test.txt b/appengine/flexible_python37_and_earlier/metadata/requirements-test.txt deleted file mode 100644 index 15d066af319..00000000000 --- a/appengine/flexible_python37_and_earlier/metadata/requirements-test.txt +++ /dev/null @@ -1 +0,0 @@ -pytest==8.2.0 diff --git a/appengine/flexible_python37_and_earlier/metadata/requirements.txt b/appengine/flexible_python37_and_earlier/metadata/requirements.txt deleted file mode 100644 index 9bfb6dcc546..00000000000 --- a/appengine/flexible_python37_and_earlier/metadata/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -Flask==3.0.3; python_version > '3.6' -Flask==2.3.3; python_version < '3.7' -gunicorn==23.0.0 -requests[security]==2.31.0 -Werkzeug==3.0.3 diff --git a/appengine/flexible_python37_and_earlier/multiple_services/README.md b/appengine/flexible_python37_and_earlier/multiple_services/README.md deleted file mode 100644 index 1e300dd8e00..00000000000 --- a/appengine/flexible_python37_and_earlier/multiple_services/README.md +++ /dev/null @@ -1,63 +0,0 @@ -# Python Google Cloud Microservices Example - API Gateway - -[![Open in Cloud Shell][shell_img]][shell_link] - -[shell_img]: http://gstatic.com/cloudssh/images/open-btn.png -[shell_link]: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&page=editor&open_in_editor=appengine/flexible_python37_and_earlier/multiple_services/README.md - -This example demonstrates how to deploy multiple python services to [App Engine flexible environment](https://cloud.google.com/appengine/docs/flexible/) - -## To Run Locally - -Open a terminal and start the first service: - -```Bash -$ cd gateway-service -$ # follow https://cloud.google.com/python/docs/setup to set up a Python -development environment -$ pip install -r requirements.txt -$ python main.py -``` - -In a separate terminal, start the second service: - -```Bash -$ cd static-service -$ # follow https://cloud.google.com/python/docs/setup to set up a Python -$ pip install -r requirements.txt -$ python main.py -``` - -## To Deploy to App Engine - -### YAML Files - -Each directory contains an `app.yaml` file. These files all describe a -separate App Engine service within the same project. - -For the gateway: - -[Gateway service ](gateway/app.yaml) - -This is the `default` service. There must be one (and not more). The deployed -url will be `https://.appspot.com` - -For the static file server: - -[Static file service ](static/app.yaml) - -The deployed url will be `https://-dot-.appspot.com` - -### Deployment - -To deploy a service cd into its directory and run: -```Bash -$ gcloud app deploy app.yaml -``` -and enter `Y` when prompted. Or to skip the check add `-q`. - -To deploy multiple services simultaneously just add the path to each `app.yaml` -file as an argument to `gcloud app deploy `: -```Bash -$ gcloud app deploy gateway-service/app.yaml static-service/app.yaml -``` diff --git a/appengine/flexible_python37_and_earlier/multiple_services/gateway-service/app.yaml b/appengine/flexible_python37_and_earlier/multiple_services/gateway-service/app.yaml deleted file mode 100644 index fde45fada1c..00000000000 --- a/appengine/flexible_python37_and_earlier/multiple_services/gateway-service/app.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2021 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. - -service: default -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - python_version: 3 - -manual_scaling: - instances: 1 diff --git a/appengine/flexible_python37_and_earlier/multiple_services/gateway-service/main.py b/appengine/flexible_python37_and_earlier/multiple_services/gateway-service/main.py deleted file mode 100644 index f963995ae0b..00000000000 --- a/appengine/flexible_python37_and_earlier/multiple_services/gateway-service/main.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright 2016 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 flask import Flask -import requests - -import services_config - -app = Flask(__name__) -services_config.init_app(app) - - -@app.route("/") -def root(): - """Gets index.html from the static file server""" - res = requests.get(app.config["SERVICE_MAP"]["static"]) - return res.content - - -@app.route("/hello/") -def say_hello(service): - """Recieves requests from buttons on the front end and resopnds - or sends request to the static file server""" - # If 'gateway' is specified return immediate - if service == "gateway": - return "Gateway says hello" - - # Otherwise send request to service indicated by URL param - responses = [] - url = app.config["SERVICE_MAP"][service] - res = requests.get(url + "/hello") - responses.append(res.content) - return b"\n".join(responses) - - -@app.route("/") -def static_file(path): - """Gets static files required by index.html to static file server""" - url = app.config["SERVICE_MAP"]["static"] - res = requests.get(url + "/" + path) - return res.content, 200, {"Content-Type": res.headers["Content-Type"]} - - -if __name__ == "__main__": - # This is used when running locally. Gunicorn is used to run the - # application on Google App Engine. See entrypoint in app.yaml. - app.run(host="127.0.0.1", port=8000, debug=True) diff --git a/appengine/flexible_python37_and_earlier/multiple_services/gateway-service/requirements-test.txt b/appengine/flexible_python37_and_earlier/multiple_services/gateway-service/requirements-test.txt deleted file mode 100644 index 15d066af319..00000000000 --- a/appengine/flexible_python37_and_earlier/multiple_services/gateway-service/requirements-test.txt +++ /dev/null @@ -1 +0,0 @@ -pytest==8.2.0 diff --git a/appengine/flexible_python37_and_earlier/multiple_services/gateway-service/requirements.txt b/appengine/flexible_python37_and_earlier/multiple_services/gateway-service/requirements.txt deleted file mode 100644 index 052021ed812..00000000000 --- a/appengine/flexible_python37_and_earlier/multiple_services/gateway-service/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -Flask==3.0.3; python_version > '3.6' -Flask==2.3.3; python_version < '3.7' -gunicorn==23.0.0 -requests==2.31.0 -Werkzeug==3.0.3 diff --git a/appengine/flexible_python37_and_earlier/multiple_services/gateway-service/services_config.py b/appengine/flexible_python37_and_earlier/multiple_services/gateway-service/services_config.py deleted file mode 100644 index 429ed402e03..00000000000 --- a/appengine/flexible_python37_and_earlier/multiple_services/gateway-service/services_config.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright 2016 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 os - -# To add services insert key value pair of the name of the service and -# the port you want it to run on when running locally -SERVICES = {"default": 8000, "static": 8001} - - -def init_app(app): - # The GAE_INSTANCE environment variable will be set when deployed to GAE. - gae_instance = os.environ.get("GAE_INSTANCE", os.environ.get("GAE_MODULE_INSTANCE")) - environment = "production" if gae_instance is not None else "development" - app.config["SERVICE_MAP"] = map_services(environment) - - -def map_services(environment): - """Generates a map of services to correct urls for running locally - or when deployed.""" - url_map = {} - for service, local_port in SERVICES.items(): - if environment == "production": - url_map[service] = production_url(service) - if environment == "development": - url_map[service] = local_url(local_port) - return url_map - - -def production_url(service_name): - """Generates url for a service when deployed to App Engine.""" - project_id = os.getenv("GOOGLE_CLOUD_PROJECT") - project_url = f"{project_id}.appspot.com" - if service_name == "default": - return f"https://{project_url}" - else: - return f"https://{service_name}-dot-{project_url}" - - -def local_url(port): - """Generates url for a service when running locally""" - return f"http://localhost:{str(port)}" diff --git a/appengine/flexible_python37_and_earlier/multiple_services/noxfile_config.py b/appengine/flexible_python37_and_earlier/multiple_services/noxfile_config.py deleted file mode 100644 index 1665dd736f8..00000000000 --- a/appengine/flexible_python37_and_earlier/multiple_services/noxfile_config.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2023 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. - -# Default TEST_CONFIG_OVERRIDE for python repos. - -# You can copy this file into your directory, then it will be imported from -# the noxfile.py. - -# The source of truth: -# https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/noxfile_config.py - -TEST_CONFIG_OVERRIDE = { - # You can opt out from the test for specific Python versions. - # Skipping for Python 3.9 due to pyarrow compilation failure. - "ignored_versions": ["2.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"], - # Old samples are opted out of enforcing Python type hints - # All new samples should feature them - "enforce_type_hints": False, - # An envvar key for determining the project id to use. Change it - # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a - # build specific Cloud project. You can also use your own string - # to use your own Cloud project. - "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", - # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', - # A dictionary you want to inject into your test. Don't put any - # secrets here. These values will override predefined values. - "envs": {}, -} diff --git a/appengine/flexible_python37_and_earlier/multiple_services/static-service/app.yaml b/appengine/flexible_python37_and_earlier/multiple_services/static-service/app.yaml deleted file mode 100644 index 0583df96c7e..00000000000 --- a/appengine/flexible_python37_and_earlier/multiple_services/static-service/app.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2021 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. - -service: static -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - python_version: 3 - -manual_scaling: - instances: 1 diff --git a/appengine/flexible_python37_and_earlier/multiple_services/static-service/main.py b/appengine/flexible_python37_and_earlier/multiple_services/static-service/main.py deleted file mode 100644 index c4b3a8d4799..00000000000 --- a/appengine/flexible_python37_and_earlier/multiple_services/static-service/main.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright 2016 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 flask import Flask - -app = Flask(__name__) - - -@app.route("/hello") -def say_hello(): - """responds to request from frontend via gateway""" - return "Static File Server says hello!" - - -@app.route("/") -def root(): - """serves index.html""" - return app.send_static_file("index.html") - - -@app.route("/") -def static_file(path): - """serves static files required by index.html""" - mimetype = "" - if "." in path and path.split(".")[1] == "css": - mimetype = "text/css" - if "." in path and path.split(".")[1] == "js": - mimetype = "application/javascript" - return app.send_static_file(path), 200, {"Content-Type": mimetype} - - -if __name__ == "__main__": - # This is used when running locally. Gunicorn is used to run the - # application on Google App Engine. See entrypoint in app.yaml. - app.run(host="127.0.0.1", port=8001, debug=True) diff --git a/appengine/flexible_python37_and_earlier/multiple_services/static-service/requirements-test.txt b/appengine/flexible_python37_and_earlier/multiple_services/static-service/requirements-test.txt deleted file mode 100644 index 15d066af319..00000000000 --- a/appengine/flexible_python37_and_earlier/multiple_services/static-service/requirements-test.txt +++ /dev/null @@ -1 +0,0 @@ -pytest==8.2.0 diff --git a/appengine/flexible_python37_and_earlier/multiple_services/static-service/requirements.txt b/appengine/flexible_python37_and_earlier/multiple_services/static-service/requirements.txt deleted file mode 100644 index 052021ed812..00000000000 --- a/appengine/flexible_python37_and_earlier/multiple_services/static-service/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -Flask==3.0.3; python_version > '3.6' -Flask==2.3.3; python_version < '3.7' -gunicorn==23.0.0 -requests==2.31.0 -Werkzeug==3.0.3 diff --git a/appengine/flexible_python37_and_earlier/multiple_services/static-service/static/index.html b/appengine/flexible_python37_and_earlier/multiple_services/static-service/static/index.html deleted file mode 100644 index 9310b700113..00000000000 --- a/appengine/flexible_python37_and_earlier/multiple_services/static-service/static/index.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - API Gateway on App Engine Flexible Environment - - -

API GATEWAY DEMO

-

Say hi to:

- - -
    - - diff --git a/appengine/flexible_python37_and_earlier/multiple_services/static-service/static/index.js b/appengine/flexible_python37_and_earlier/multiple_services/static-service/static/index.js deleted file mode 100644 index 021f835b9c1..00000000000 --- a/appengine/flexible_python37_and_earlier/multiple_services/static-service/static/index.js +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2016 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 handleResponse(resp){ - const li = document.createElement('li'); - li.innerHTML = resp; - document.querySelector('.responses').appendChild(li) -} - -function handleClick(event){ - $.ajax({ - url: `hello/${event.target.id}`, - type: `GET`, - success(resp){ - handleResponse(resp); - } - }); -} - -document.addEventListener('DOMContentLoaded', () => { - const buttons = document.getElementsByTagName('button') - for (var i = 0; i < buttons.length; i++) { - buttons[i].addEventListener('click', handleClick); - } -}); diff --git a/appengine/flexible_python37_and_earlier/multiple_services/static-service/static/style.css b/appengine/flexible_python37_and_earlier/multiple_services/static-service/static/style.css deleted file mode 100644 index 65074a9ef4d..00000000000 --- a/appengine/flexible_python37_and_earlier/multiple_services/static-service/static/style.css +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright 2021 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. - */ - -h1 { - color: red; -} diff --git a/appengine/flexible_python37_and_earlier/numpy/app.yaml b/appengine/flexible_python37_and_earlier/numpy/app.yaml deleted file mode 100644 index ca76f83fc3b..00000000000 --- a/appengine/flexible_python37_and_earlier/numpy/app.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2021 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. - -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - python_version: 3 diff --git a/appengine/flexible_python37_and_earlier/numpy/main.py b/appengine/flexible_python37_and_earlier/numpy/main.py deleted file mode 100644 index cb14c931d62..00000000000 --- a/appengine/flexible_python37_and_earlier/numpy/main.py +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright 2016 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 logging - -from flask import Flask -import numpy as np - -app = Flask(__name__) - - -@app.route("/") -def calculate(): - """Performs a dot product on predefined arrays. - - Returns: - Returns a formatted message containing the dot product result of - two predefined arrays. - """ - return_str = "" - x = np.array([[1, 2], [3, 4]]) - y = np.array([[5, 6], [7, 8]]) - - return_str += f"x: {str(x)} , y: {str(y)}
    " - - # Multiply matrices - return_str += f"x dot y : {str(np.dot(x, y))}" - return return_str - - -@app.errorhandler(500) -def server_error(e): - """Serves a formatted message on-error. - - Returns: - The error message and a code 500 status. - """ - logging.exception("An error occurred during a request.") - return ( - f"An internal error occurred:
    {e}

    See logs for full stacktrace.", - 500, - ) - - -if __name__ == "__main__": - # This is used when running locally. Gunicorn is used to run the - # application on Google App Engine. See entrypoint in app.yaml. - app.run(host="127.0.0.1", port=8080, debug=True) diff --git a/appengine/flexible_python37_and_earlier/numpy/main_test.py b/appengine/flexible_python37_and_earlier/numpy/main_test.py deleted file mode 100644 index e25c4dfcac3..00000000000 --- a/appengine/flexible_python37_and_earlier/numpy/main_test.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2016 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 main - - -def test_index(): - main.app.testing = True - client = main.app.test_client() - - r = client.get("/") - assert r.status_code == 200 - assert "[[19 22]\n [43 50]]" in r.data.decode("utf-8") diff --git a/appengine/flexible_python37_and_earlier/numpy/noxfile_config.py b/appengine/flexible_python37_and_earlier/numpy/noxfile_config.py deleted file mode 100644 index 1665dd736f8..00000000000 --- a/appengine/flexible_python37_and_earlier/numpy/noxfile_config.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2023 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. - -# Default TEST_CONFIG_OVERRIDE for python repos. - -# You can copy this file into your directory, then it will be imported from -# the noxfile.py. - -# The source of truth: -# https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/noxfile_config.py - -TEST_CONFIG_OVERRIDE = { - # You can opt out from the test for specific Python versions. - # Skipping for Python 3.9 due to pyarrow compilation failure. - "ignored_versions": ["2.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"], - # Old samples are opted out of enforcing Python type hints - # All new samples should feature them - "enforce_type_hints": False, - # An envvar key for determining the project id to use. Change it - # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a - # build specific Cloud project. You can also use your own string - # to use your own Cloud project. - "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", - # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', - # A dictionary you want to inject into your test. Don't put any - # secrets here. These values will override predefined values. - "envs": {}, -} diff --git a/appengine/flexible_python37_and_earlier/numpy/requirements-test.txt b/appengine/flexible_python37_and_earlier/numpy/requirements-test.txt deleted file mode 100644 index 15d066af319..00000000000 --- a/appengine/flexible_python37_and_earlier/numpy/requirements-test.txt +++ /dev/null @@ -1 +0,0 @@ -pytest==8.2.0 diff --git a/appengine/flexible_python37_and_earlier/numpy/requirements.txt b/appengine/flexible_python37_and_earlier/numpy/requirements.txt deleted file mode 100644 index ccd96a3d6d1..00000000000 --- a/appengine/flexible_python37_and_earlier/numpy/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -Flask==3.0.3; python_version > '3.6' -Flask==2.0.3; python_version < '3.7' -gunicorn==23.0.0 -numpy==2.2.4; python_version > '3.9' -numpy==2.2.4; python_version == '3.9' -numpy==2.2.4; python_version == '3.8' -numpy==2.2.4; python_version == '3.7' -Werkzeug==3.0.3 diff --git a/appengine/flexible_python37_and_earlier/pubsub/README.md b/appengine/flexible_python37_and_earlier/pubsub/README.md deleted file mode 100644 index 2e9b0d71918..00000000000 --- a/appengine/flexible_python37_and_earlier/pubsub/README.md +++ /dev/null @@ -1,75 +0,0 @@ -# Python Google Cloud Pub/Sub sample for Google App Engine Flexible Environment - -[![Open in Cloud Shell][shell_img]][shell_link] - -[shell_img]: http://gstatic.com/cloudssh/images/open-btn.png -[shell_link]: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&page=editor&open_in_editor=appengine/flexible_python37_and_earlier/pubsub/README.md - -This demonstrates how to send and receive messages using [Google Cloud Pub/Sub](https://cloud.google.com/pubsub) on [Google App Engine Flexible Environment](https://cloud.google.com/appengine). - -## Setup - -Before you can run or deploy the sample, you will need to do the following: - -1. Enable the Cloud Pub/Sub API in the [Google Developers Console](https://console.developers.google.com/project/_/apiui/apiview/pubsub/overview). - -2. Create a topic and subscription. - - $ gcloud beta pubsub topics create [your-topic-name] - $ gcloud beta pubsub subscriptions create [your-subscription-name] \ - --topic [your-topic-name] \ - --push-endpoint \ - https://[your-app-id].appspot.com/pubsub/push?token=[your-token] \ - --ack-deadline 30 - -3. Update the environment variables in ``app.yaml``. - -## Running locally - -Refer to the [top-level README](../README.md) for instructions on running and deploying. - -When running locally, you can use the [Google Cloud SDK](https://cloud.google.com/sdk) to provide authentication to use Google Cloud APIs: - - $ gcloud init - -Install dependencies, please follow https://cloud.google.com/python/docs/setup -to set up a Python development environment. Then run: - - $ pip install -r requirements.txt - -Then set environment variables before starting your application: - - $ export PUBSUB_VERIFICATION_TOKEN=[your-verification-token] - $ export PUBSUB_TOPIC=[your-topic] - $ python main.py - -### Simulating push notifications - -The application can send messages locally, but it is not able to receive push messages locally. You can, however, simulate a push message by making an HTTP request to the local push notification endpoint. There is an included ``sample_message.json``. You can use -``curl`` or [httpie](https://github.com/jkbrzt/httpie) to POST this: - - $ curl -i --data @sample_message.json ":8080/pubsub/push?token=[your-token]" - -Or - - $ http POST ":8080/pubsub/push?token=[your-token]" < sample_message.json - -Response: - - HTTP/1.0 200 OK - Content-Length: 2 - Content-Type: text/html; charset=utf-8 - Date: Mon, 10 Aug 2015 17:52:03 GMT - Server: Werkzeug/0.10.4 Python/2.7.10 - - OK - -After the request completes, you can refresh ``localhost:8080`` and see the message in the list of received messages. - -## Running on App Engine - -Deploy using `gcloud`: - - gcloud app deploy app.yaml - -You can now access the application at `https://your-app-id.appspot.com`. You can use the form to submit messages, but it's non-deterministic which instance of your application will receive the notification. You can send multiple messages and refresh the page to see the received message. diff --git a/appengine/flexible_python37_and_earlier/pubsub/app.yaml b/appengine/flexible_python37_and_earlier/pubsub/app.yaml deleted file mode 100644 index 5804ac2b266..00000000000 --- a/appengine/flexible_python37_and_earlier/pubsub/app.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2021 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. - -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - python_version: 3 - -# [START gae_flex_pubsub_env] -env_variables: - PUBSUB_TOPIC: your-topic - # This token is used to verify that requests originate from your - # application. It can be any sufficiently random string. - PUBSUB_VERIFICATION_TOKEN: 1234abc -# [END gae_flex_pubsub_env] diff --git a/appengine/flexible_python37_and_earlier/pubsub/main.py b/appengine/flexible_python37_and_earlier/pubsub/main.py deleted file mode 100644 index 5ffc960841c..00000000000 --- a/appengine/flexible_python37_and_earlier/pubsub/main.py +++ /dev/null @@ -1,98 +0,0 @@ -# Copyright 2015 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 base64 -import json -import logging -import os - -from flask import current_app, Flask, render_template, request -from google.cloud import pubsub_v1 - - -app = Flask(__name__) - -# Configure the following environment variables via app.yaml -# This is used in the push request handler to verify that the request came from -# pubsub and originated from a trusted source. -app.config["PUBSUB_VERIFICATION_TOKEN"] = os.environ["PUBSUB_VERIFICATION_TOKEN"] -app.config["PUBSUB_TOPIC"] = os.environ["PUBSUB_TOPIC"] -app.config["PROJECT"] = os.environ["GOOGLE_CLOUD_PROJECT"] - - -# Global list to storage messages received by this instance. -MESSAGES = [] - -# Initialize the publisher client once to avoid memory leak -# and reduce publish latency. -publisher = pubsub_v1.PublisherClient() - - -# [START gae_flex_pubsub_index] -@app.route("/", methods=["GET", "POST"]) -def index(): - if request.method == "GET": - return render_template("index.html", messages=MESSAGES) - - data = request.form.get("payload", "Example payload").encode("utf-8") - - # publisher = pubsub_v1.PublisherClient() - topic_path = publisher.topic_path( - current_app.config["PROJECT"], current_app.config["PUBSUB_TOPIC"] - ) - - publisher.publish(topic_path, data=data) - - return "OK", 200 - - -# [END gae_flex_pubsub_index] - - -# [START gae_flex_pubsub_push] -@app.route("/pubsub/push", methods=["POST"]) -def pubsub_push(): - if request.args.get("token", "") != current_app.config["PUBSUB_VERIFICATION_TOKEN"]: - return "Invalid request", 400 - - envelope = json.loads(request.data.decode("utf-8")) - payload = base64.b64decode(envelope["message"]["data"]) - - MESSAGES.append(payload) - - # Returning any 2xx status indicates successful receipt of the message. - return "OK", 200 - - -# [END gae_flex_pubsub_push] - - -@app.errorhandler(500) -def server_error(e): - logging.exception("An error occurred during a request.") - return ( - """ - An internal error occurred:
    {}
    - See logs for full stacktrace. - """.format( - e - ), - 500, - ) - - -if __name__ == "__main__": - # This is used when running locally. Gunicorn is used to run the - # application on Google App Engine. See entrypoint in app.yaml. - app.run(host="127.0.0.1", port=8080, debug=True) diff --git a/appengine/flexible_python37_and_earlier/pubsub/main_test.py b/appengine/flexible_python37_and_earlier/pubsub/main_test.py deleted file mode 100644 index 37abb0d6240..00000000000 --- a/appengine/flexible_python37_and_earlier/pubsub/main_test.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright 2015 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 base64 -import json -import os - -import pytest - -import main - - -@pytest.fixture -def client(): - main.app.testing = True - return main.app.test_client() - - -def test_index(client): - r = client.get("/") - assert r.status_code == 200 - - -def test_post_index(client): - r = client.post("/", data={"payload": "Test payload"}) - assert r.status_code == 200 - - -def test_push_endpoint(client): - url = "/pubsub/push?token=" + os.environ["PUBSUB_VERIFICATION_TOKEN"] - - r = client.post( - url, - data=json.dumps( - {"message": {"data": base64.b64encode(b"Test message").decode("utf-8")}} - ), - ) - - assert r.status_code == 200 - - # Make sure the message is visible on the home page. - r = client.get("/") - assert r.status_code == 200 - assert "Test message" in r.data.decode("utf-8") - - -def test_push_endpoint_errors(client): - # no token - r = client.post("/pubsub/push") - assert r.status_code == 400 - - # invalid token - r = client.post("/pubsub/push?token=bad") - assert r.status_code == 400 diff --git a/appengine/flexible_python37_and_earlier/pubsub/noxfile_config.py b/appengine/flexible_python37_and_earlier/pubsub/noxfile_config.py deleted file mode 100644 index 1665dd736f8..00000000000 --- a/appengine/flexible_python37_and_earlier/pubsub/noxfile_config.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2023 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. - -# Default TEST_CONFIG_OVERRIDE for python repos. - -# You can copy this file into your directory, then it will be imported from -# the noxfile.py. - -# The source of truth: -# https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/noxfile_config.py - -TEST_CONFIG_OVERRIDE = { - # You can opt out from the test for specific Python versions. - # Skipping for Python 3.9 due to pyarrow compilation failure. - "ignored_versions": ["2.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"], - # Old samples are opted out of enforcing Python type hints - # All new samples should feature them - "enforce_type_hints": False, - # An envvar key for determining the project id to use. Change it - # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a - # build specific Cloud project. You can also use your own string - # to use your own Cloud project. - "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", - # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', - # A dictionary you want to inject into your test. Don't put any - # secrets here. These values will override predefined values. - "envs": {}, -} diff --git a/appengine/flexible_python37_and_earlier/pubsub/requirements-test.txt b/appengine/flexible_python37_and_earlier/pubsub/requirements-test.txt deleted file mode 100644 index 15d066af319..00000000000 --- a/appengine/flexible_python37_and_earlier/pubsub/requirements-test.txt +++ /dev/null @@ -1 +0,0 @@ -pytest==8.2.0 diff --git a/appengine/flexible_python37_and_earlier/pubsub/requirements.txt b/appengine/flexible_python37_and_earlier/pubsub/requirements.txt deleted file mode 100644 index d5b7ce68695..00000000000 --- a/appengine/flexible_python37_and_earlier/pubsub/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -Flask==3.0.3; python_version > '3.6' -Flask==2.3.3; python_version < '3.7' -google-cloud-pubsub==2.28.0 -gunicorn==23.0.0 -Werkzeug==3.0.3 diff --git a/appengine/flexible_python37_and_earlier/pubsub/sample_message.json b/appengine/flexible_python37_and_earlier/pubsub/sample_message.json deleted file mode 100644 index 8fe62d23fb9..00000000000 --- a/appengine/flexible_python37_and_earlier/pubsub/sample_message.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "message": { - "data": "SGVsbG8sIFdvcmxkIQ==" - } -} diff --git a/appengine/flexible_python37_and_earlier/pubsub/templates/index.html b/appengine/flexible_python37_and_earlier/pubsub/templates/index.html deleted file mode 100644 index 28449216c37..00000000000 --- a/appengine/flexible_python37_and_earlier/pubsub/templates/index.html +++ /dev/null @@ -1,36 +0,0 @@ -{# -# Copyright 2015 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. -#} - - - - Pub/Sub Python on Google App Engine Flexible Environment - - -
    -

    Messages received by this instance:

    -
      - {% for message in messages: %} -
    • {{message}}
    • - {% endfor %} -
    -

    Note: because your application is likely running multiple instances, each instance will have a different list of messages.

    -
    -
    - - -
    - - diff --git a/appengine/flexible_python37_and_earlier/scipy/.gitignore b/appengine/flexible_python37_and_earlier/scipy/.gitignore deleted file mode 100644 index de724cf6213..00000000000 --- a/appengine/flexible_python37_and_earlier/scipy/.gitignore +++ /dev/null @@ -1 +0,0 @@ -assets/resized_google_logo.jpg diff --git a/appengine/flexible_python37_and_earlier/scipy/README.md b/appengine/flexible_python37_and_earlier/scipy/README.md deleted file mode 100644 index f1fe346a338..00000000000 --- a/appengine/flexible_python37_and_earlier/scipy/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# SciPy on App Engine Flexible - -[![Open in Cloud Shell][shell_img]][shell_link] - -[shell_img]: http://gstatic.com/cloudssh/images/open-btn.png -[shell_link]: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&page=editor&open_in_editor=appengine/flexible_python37_and_earlier/scipy/README.md - -This sample demonstrates how to use SciPy to resize an image on App Engine Flexible. - diff --git a/appengine/flexible_python37_and_earlier/scipy/app.yaml b/appengine/flexible_python37_and_earlier/scipy/app.yaml deleted file mode 100644 index ca76f83fc3b..00000000000 --- a/appengine/flexible_python37_and_earlier/scipy/app.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2021 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. - -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - python_version: 3 diff --git a/appengine/flexible_python37_and_earlier/scipy/assets/google_logo.jpg b/appengine/flexible_python37_and_earlier/scipy/assets/google_logo.jpg deleted file mode 100644 index 5538eaed2bd..00000000000 Binary files a/appengine/flexible_python37_and_earlier/scipy/assets/google_logo.jpg and /dev/null differ diff --git a/appengine/flexible_python37_and_earlier/scipy/main.py b/appengine/flexible_python37_and_earlier/scipy/main.py deleted file mode 100644 index 992aa59d32d..00000000000 --- a/appengine/flexible_python37_and_earlier/scipy/main.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright 2016 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 logging -import os - -from flask import Flask -from flask import request -import imageio -from PIL import Image - -app = Flask(__name__) - - -@app.route("/") -def resize(): - """Demonstrates using Pillow to resize an image. - - This takes a predefined image, resizes it to 300x300 pixesls, and writes it on disk. - - Returns: - A message stating that the image has been resized. - """ - app_path = os.path.dirname(os.path.realpath(__file__)) - image_path = os.path.join(app_path, "assets/google_logo.jpg") - img = Image.fromarray(imageio.imread(image_path)) - img_tinted = img.resize((300, 300)) - - output_image_path = request.args.get("output_image_path") - # Write the tinted image back to disk - imageio.imwrite(output_image_path, img_tinted) - return "Image resized." - - -@app.errorhandler(500) -def server_error(e): - """Serves a formatted message on-error. - - Returns: - The error message and a code 500 status. - """ - logging.exception("An error occurred during a request.") - return ( - f"An internal error occurred:
    {e}

    See logs for full stacktrace.", - 500, - ) - - -if __name__ == "__main__": - # This is used when running locally. Gunicorn is used to run the - # application on Google App Engine. See entrypoint in app.yaml. - app.run(host="127.0.0.1", port=8080, debug=True) diff --git a/appengine/flexible_python37_and_earlier/scipy/main_test.py b/appengine/flexible_python37_and_earlier/scipy/main_test.py deleted file mode 100644 index d3124ffbb0a..00000000000 --- a/appengine/flexible_python37_and_earlier/scipy/main_test.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2016 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 os -import tempfile - -import main - - -def test_index(): - main.app.testing = True - client = main.app.test_client() - with tempfile.TemporaryDirectory() as test_dir: - output_image_path = os.path.join(test_dir, "resized_google_logo.jpg") - r = client.get("/", query_string={"output_image_path": output_image_path}) - - assert os.path.isfile(output_image_path) - assert r.status_code == 200 diff --git a/appengine/flexible_python37_and_earlier/scipy/requirements-test.txt b/appengine/flexible_python37_and_earlier/scipy/requirements-test.txt deleted file mode 100644 index 15d066af319..00000000000 --- a/appengine/flexible_python37_and_earlier/scipy/requirements-test.txt +++ /dev/null @@ -1 +0,0 @@ -pytest==8.2.0 diff --git a/appengine/flexible_python37_and_earlier/scipy/requirements.txt b/appengine/flexible_python37_and_earlier/scipy/requirements.txt deleted file mode 100644 index a67d9f49c61..00000000000 --- a/appengine/flexible_python37_and_earlier/scipy/requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -Flask==3.0.3; python_version > '3.6' -Flask==2.0.3; python_version < '3.7' -gunicorn==23.0.0 -imageio==2.36.1 -numpy==2.2.4; python_version > '3.9' -numpy==2.2.4; python_version == '3.9' -numpy==2.2.4; python_version == '3.8' -numpy==2.2.4; python_version == '3.7' -pillow==10.4.0 -scipy==1.14.1 -Werkzeug==3.0.3 diff --git a/appengine/flexible_python37_and_earlier/static_files/README.md b/appengine/flexible_python37_and_earlier/static_files/README.md deleted file mode 100644 index 024a0abfbd9..00000000000 --- a/appengine/flexible_python37_and_earlier/static_files/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# Python / Flask static files sample for Google App Engine Flexible Environment - -[![Open in Cloud Shell][shell_img]][shell_link] - -[shell_img]: http://gstatic.com/cloudssh/images/open-btn.png -[shell_link]: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&page=editor&open_in_editor=appengine/flexible_python37_and_earlier/static_files/README.md - -This demonstrates how to use [Flask](http://flask.pocoo.org/) to serve static files in your application. - -Flask automatically makes anything in the ``static`` directory available via the ``/static`` URL. If you plan on using a different framework, it may have different conventions for serving static files. - -Refer to the [top-level README](../README.md) for instructions on running and deploying. diff --git a/appengine/flexible_python37_and_earlier/static_files/app.yaml b/appengine/flexible_python37_and_earlier/static_files/app.yaml deleted file mode 100644 index ca76f83fc3b..00000000000 --- a/appengine/flexible_python37_and_earlier/static_files/app.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2021 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. - -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - python_version: 3 diff --git a/appengine/flexible_python37_and_earlier/static_files/main.py b/appengine/flexible_python37_and_earlier/static_files/main.py deleted file mode 100644 index d77eca69f44..00000000000 --- a/appengine/flexible_python37_and_earlier/static_files/main.py +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright 2015 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. - -# [START gae_flex_python_static_files] -import logging - -from flask import Flask, render_template - - -app = Flask(__name__) - - -@app.route("/") -def hello(): - """Renders and serves a static HTML template page. - - Returns: - A string containing the rendered HTML page. - """ - return render_template("index.html") - - -@app.errorhandler(500) -def server_error(e): - """Serves a formatted message on-error. - - Returns: - The error message and a code 500 status. - """ - logging.exception("An error occurred during a request.") - return ( - f"An internal error occurred:
    {e}

    See logs for full stacktrace.", - 500, - ) - - -if __name__ == "__main__": - # This is used when running locally. Gunicorn is used to run the - # application on Google App Engine. See entrypoint in app.yaml. - app.run(host="127.0.0.1", port=8080, debug=True) -# [END gae_flex_python_static_files] diff --git a/appengine/flexible_python37_and_earlier/static_files/main_test.py b/appengine/flexible_python37_and_earlier/static_files/main_test.py deleted file mode 100644 index 2662db44201..00000000000 --- a/appengine/flexible_python37_and_earlier/static_files/main_test.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2015 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 main - - -def test_index(): - main.app.testing = True - client = main.app.test_client() - - r = client.get("/") - assert r.status_code == 200 - - r = client.get("/static/main.css") - assert r.status_code == 200 diff --git a/appengine/flexible_python37_and_earlier/static_files/noxfile_config.py b/appengine/flexible_python37_and_earlier/static_files/noxfile_config.py deleted file mode 100644 index 1665dd736f8..00000000000 --- a/appengine/flexible_python37_and_earlier/static_files/noxfile_config.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2023 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. - -# Default TEST_CONFIG_OVERRIDE for python repos. - -# You can copy this file into your directory, then it will be imported from -# the noxfile.py. - -# The source of truth: -# https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/noxfile_config.py - -TEST_CONFIG_OVERRIDE = { - # You can opt out from the test for specific Python versions. - # Skipping for Python 3.9 due to pyarrow compilation failure. - "ignored_versions": ["2.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"], - # Old samples are opted out of enforcing Python type hints - # All new samples should feature them - "enforce_type_hints": False, - # An envvar key for determining the project id to use. Change it - # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a - # build specific Cloud project. You can also use your own string - # to use your own Cloud project. - "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", - # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', - # A dictionary you want to inject into your test. Don't put any - # secrets here. These values will override predefined values. - "envs": {}, -} diff --git a/appengine/flexible_python37_and_earlier/static_files/requirements-test.txt b/appengine/flexible_python37_and_earlier/static_files/requirements-test.txt deleted file mode 100644 index 15d066af319..00000000000 --- a/appengine/flexible_python37_and_earlier/static_files/requirements-test.txt +++ /dev/null @@ -1 +0,0 @@ -pytest==8.2.0 diff --git a/appengine/flexible_python37_and_earlier/static_files/requirements.txt b/appengine/flexible_python37_and_earlier/static_files/requirements.txt deleted file mode 100644 index 70ecce34b5b..00000000000 --- a/appengine/flexible_python37_and_earlier/static_files/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -Flask==3.0.3; python_version > '3.6' -Flask==2.0.3; python_version < '3.7' -gunicorn==23.0.0 -Werkzeug==3.0.3 diff --git a/appengine/flexible_python37_and_earlier/static_files/static/main.css b/appengine/flexible_python37_and_earlier/static_files/static/main.css deleted file mode 100644 index f906044f4e4..00000000000 --- a/appengine/flexible_python37_and_earlier/static_files/static/main.css +++ /dev/null @@ -1,21 +0,0 @@ -/* -Copyright 2015 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. -*/ -/* [START gae_flex_python_css] */ -body { - font-family: Verdana, Helvetica, sans-serif; - background-color: #CCCCFF; -} -/* [END gae_flex_python_css] */ diff --git a/appengine/flexible_python37_and_earlier/static_files/templates/index.html b/appengine/flexible_python37_and_earlier/static_files/templates/index.html deleted file mode 100644 index 13b2ebe61af..00000000000 --- a/appengine/flexible_python37_and_earlier/static_files/templates/index.html +++ /dev/null @@ -1,29 +0,0 @@ -{# -# Copyright 2015 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. -#} - - - - Static Files - - - - -

    This is a static file serving example.

    - - diff --git a/appengine/flexible_python37_and_earlier/storage/README.md b/appengine/flexible_python37_and_earlier/storage/README.md deleted file mode 100644 index a2af4d60741..00000000000 --- a/appengine/flexible_python37_and_earlier/storage/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# Python Google Cloud Storage sample for Google App Engine Flexible Environment - -[![Open in Cloud Shell][shell_img]][shell_link] - -[shell_img]: http://gstatic.com/cloudssh/images/open-btn.png -[shell_link]: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&page=editor&open_in_editor=appengine/flexible_python37_and_earlier/storage/README.md - -This sample demonstrates how to use [Google Cloud Storage](https://cloud.google.com/storage/) on [Google App Engine Flexible Environment](https://cloud.google.com/appengine). - -## Setup - -Before you can run or deploy the sample, you will need to do the following: - -1. Enable the Cloud Storage API in the [Google Developers Console](https://console.developers.google.com/project/_/apiui/apiview/storage/overview). - -2. Create a Cloud Storage Bucket. You can do this with the [Google Cloud SDK](https://cloud.google.com/sdk) with the following command: - - $ gsutil mb gs://[your-bucket-name] - -3. Set the default ACL on your bucket to public read in order to serve files directly from Cloud Storage. You can do this with the [Google Cloud SDK](https://cloud.google.com/sdk) with the following command: - - $ gsutil defacl set public-read gs://[your-bucket-name] - -4. Update the environment variables in ``app.yaml``. - -## Running locally - -Refer to the [top-level README](../README.md) for instructions on running and deploying. - -When running locally, you can use the [Google Cloud SDK](https://cloud.google.com/sdk) to provide authentication to use Google Cloud APIs: - - $ gcloud init - -Then set environment variables before starting your application: - - $ export CLOUD_STORAGE_BUCKET=[your-bucket-name] - $ python main.py diff --git a/appengine/flexible_python37_and_earlier/storage/app.yaml b/appengine/flexible_python37_and_earlier/storage/app.yaml deleted file mode 100644 index e21a4c0ae91..00000000000 --- a/appengine/flexible_python37_and_earlier/storage/app.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2021 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. - -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - python_version: 3 - -#[START gae_flex_storage_yaml] -env_variables: - CLOUD_STORAGE_BUCKET: your-bucket-name -#[END gae_flex_storage_yaml] diff --git a/appengine/flexible_python37_and_earlier/storage/main.py b/appengine/flexible_python37_and_earlier/storage/main.py deleted file mode 100644 index dc06fd2ae8e..00000000000 --- a/appengine/flexible_python37_and_earlier/storage/main.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright 2015 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. - -# [START gae_flex_storage_app] -from __future__ import annotations - -import logging -import os - -from flask import Flask, request -from google.cloud import storage - -app = Flask(__name__) - -# Configure this environment variable via app.yaml -CLOUD_STORAGE_BUCKET = os.environ["CLOUD_STORAGE_BUCKET"] - - -@app.route("/") -def index() -> str: - return """ -
    - - -
    -""" - - -@app.route("/upload", methods=["POST"]) -def upload() -> str: - """Process the uploaded file and upload it to Google Cloud Storage.""" - uploaded_file = request.files.get("file") - - if not uploaded_file: - return "No file uploaded.", 400 - - # Create a Cloud Storage client. - gcs = storage.Client() - - # Get the bucket that the file will be uploaded to. - bucket = gcs.get_bucket(CLOUD_STORAGE_BUCKET) - - # Create a new blob and upload the file's content. - blob = bucket.blob(uploaded_file.filename) - - blob.upload_from_string( - uploaded_file.read(), content_type=uploaded_file.content_type - ) - - # Make the blob public. This is not necessary if the - # entire bucket is public. - # See https://cloud.google.com/storage/docs/access-control/making-data-public. - blob.make_public() - - # The public URL can be used to directly access the uploaded file via HTTP. - return blob.public_url - - -@app.errorhandler(500) -def server_error(e: Exception | int) -> str: - logging.exception("An error occurred during a request.") - return ( - """ - An internal error occurred:
    {}
    - See logs for full stacktrace. - """.format( - e - ), - 500, - ) - - -if __name__ == "__main__": - # This is used when running locally. Gunicorn is used to run the - # application on Google App Engine. See entrypoint in app.yaml. - app.run(host="127.0.0.1", port=8080, debug=True) -# [END gae_flex_storage_app] diff --git a/appengine/flexible_python37_and_earlier/storage/main_test.py b/appengine/flexible_python37_and_earlier/storage/main_test.py deleted file mode 100644 index ceb979d1ba6..00000000000 --- a/appengine/flexible_python37_and_earlier/storage/main_test.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright 2015 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 BytesIO -import os -import uuid - -import flask -import flask.testing -from google.cloud import storage -import pytest -import requests - -import main - - -@pytest.fixture -def client() -> flask.testing.FlaskClient: - main.app.testing = True - return main.app.test_client() - - -def test_index(client: flask.testing.FlaskClient) -> None: - r = client.get("/") - assert r.status_code == 200 - - -@pytest.fixture(scope="module") -def blob_name() -> str: - name = f"gae-flex-storage-{uuid.uuid4()}" - yield name - - bucket = storage.Client().bucket(os.environ["CLOUD_STORAGE_BUCKET"]) - blob = bucket.blob(name) - blob.delete() - - -def test_upload(client: flask.testing.FlaskClient, blob_name: str) -> None: - # Upload a simple file - file_content = b"This is some test content." - - r = client.post("/upload", data={"file": (BytesIO(file_content), blob_name)}) - - assert r.status_code == 200 - - # The app should return the public cloud storage URL for the uploaded - # file. Download and verify it. - cloud_storage_url = r.data.decode("utf-8") - r = requests.get(cloud_storage_url) - assert r.text.encode("utf-8") == file_content diff --git a/appengine/flexible_python37_and_earlier/storage/requirements-test.txt b/appengine/flexible_python37_and_earlier/storage/requirements-test.txt deleted file mode 100644 index f27726d7455..00000000000 --- a/appengine/flexible_python37_and_earlier/storage/requirements-test.txt +++ /dev/null @@ -1,2 +0,0 @@ -pytest==8.2.0 -google-cloud-storage==2.9.0 diff --git a/appengine/flexible_python37_and_earlier/storage/requirements.txt b/appengine/flexible_python37_and_earlier/storage/requirements.txt deleted file mode 100644 index 994d3201309..00000000000 --- a/appengine/flexible_python37_and_earlier/storage/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -Flask==3.0.3; python_version > '3.6' -Flask==2.0.3; python_version < '3.7' -werkzeug==3.0.3; python_version > '3.7' -werkzeug==2.3.8; python_version <= '3.7' -google-cloud-storage==2.9.0 -gunicorn==23.0.0 diff --git a/appengine/flexible_python37_and_earlier/tasks/Dockerfile b/appengine/flexible_python37_and_earlier/tasks/Dockerfile deleted file mode 100644 index 5aaeb51144d..00000000000 --- a/appengine/flexible_python37_and_earlier/tasks/Dockerfile +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2021 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. - -# Use the official Python image. -# https://hub.docker.com/_/python -FROM python:3.11 - -# Copy local code to the container image. -ENV APP_HOME /app -WORKDIR $APP_HOME -COPY . . - -# Install production dependencies. -RUN pip install Flask gunicorn - -# Run the web service on container startup. Here we use the gunicorn -# webserver, with one worker process and 8 threads. -# For environments with multiple CPU cores, increase the number of workers -# to be equal to the cores available. -CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 main:app diff --git a/appengine/flexible_python37_and_earlier/tasks/README.md b/appengine/flexible_python37_and_earlier/tasks/README.md deleted file mode 100644 index 5eb60d5fa45..00000000000 --- a/appengine/flexible_python37_and_earlier/tasks/README.md +++ /dev/null @@ -1,122 +0,0 @@ -# Google Cloud Tasks Samples - -[![Open in Cloud Shell][shell_img]][shell_link] - -[shell_img]: http://gstatic.com/cloudssh/images/open-btn.png -[shell_link]: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&page=editor&open_in_editor=appengine/flexible_python37_and_earlier/tasks/README.md - -Sample command-line programs for interacting with the Cloud Tasks API -. - -App Engine queues push tasks to an App Engine HTTP target. This directory -contains both the App Engine app to deploy, as well as the snippets to run -locally to push tasks to it, which could also be called on App Engine. - -`create_app_engine_queue_task.py` is a simple command-line program to create -tasks to be pushed to the App Engine app. - -`main.py` is the main App Engine app. This app serves as an endpoint to receive -App Engine task attempts. - -`app.yaml` configures the App Engine app. - - -## Prerequisites to run locally: - -Please refer to [Setting Up a Python Development Environment](https://cloud.google.com/python/setup). - -### Authentication - -To set up authentication, please refer to our -[authentication getting started guide](https://cloud.google.com/docs/authentication/getting-started). - -### Install Dependencies - -To install the dependencies for this sample, use the following command: - -```sh -pip install -r requirements.txt -``` - -This sample uses the common protos in the [googleapis](https://github.com/googleapis/googleapis) -repository. For more info, see -[Protocol Buffer Basics](https://developers.google.com/protocol-buffers/docs/pythontutorial). - -## Deploying the App Engine App - -Deploy the App Engine app with gcloud: - -* To deploy to the Standard environment: - ```sh - gcloud app deploy app.yaml - ``` -* To deploy to the Flexible environment: - ```sh - gcloud app deploy app.flexible.yaml - ``` - -Verify the index page is serving: - -```sh -gcloud app browse -``` - -The App Engine app serves as a target for the push requests. It has an -endpoint `/example_task_handler` that reads the payload (i.e., the request body) -of the HTTP POST request and logs it. The log output can be viewed with: - -```sh -gcloud app logs read -``` - -## Creating a queue - -To create a queue using the Cloud SDK, use the following gcloud command: - -```sh -gcloud tasks queues create my-appengine-queue -``` - -Note: A newly created queue will route to the default App Engine service and -version unless configured to do otherwise. - -## Run the Sample Using the Command Line - -Set environment variables: - -First, your project ID: - -```sh -export PROJECT_ID=my-project-id -``` - -Then the queue ID, as specified at queue creation time. Queue IDs already -created can be listed with `gcloud tasks queues list`. - -```sh -export QUEUE_ID=my-appengine-queue -``` - -And finally the location ID, which can be discovered with -`gcloud tasks queues describe $QUEUE_ID`, with the location embedded in -the "name" value (for instance, if the name is -"projects/my-project/locations/us-central1/queues/my-appengine-queue", then the -location is "us-central1"). - -```sh -export LOCATION_ID=us-central1 -``` - -### Using App Engine Queues - -Running the sample will create a task, targeted at the `/example_task_handler` -endpoint, with a payload specified: - -> **Note** -> Please update -> [create_app_engine_queue_task.py](./create_app_engine_queue_task.py) before running the following -> command. - -```sh -python create_app_engine_queue_task.py --project=$PROJECT_ID --queue=$QUEUE_ID --location=$LOCATION_ID --payload=hello -``` diff --git a/appengine/flexible_python37_and_earlier/tasks/app.flexible.yaml b/appengine/flexible_python37_and_earlier/tasks/app.flexible.yaml deleted file mode 100644 index 5b3b333fda6..00000000000 --- a/appengine/flexible_python37_and_earlier/tasks/app.flexible.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2019 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. - -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT --threads=4 main:app - -runtime_config: - python_version: 3 diff --git a/appengine/flexible_python37_and_earlier/tasks/create_app_engine_queue_task.py b/appengine/flexible_python37_and_earlier/tasks/create_app_engine_queue_task.py deleted file mode 100644 index 7ddb6fb5a69..00000000000 --- a/appengine/flexible_python37_and_earlier/tasks/create_app_engine_queue_task.py +++ /dev/null @@ -1,121 +0,0 @@ -# Copyright 2019 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 argparse - - -def create_task(project, queue, location, payload=None, in_seconds=None): - # [START cloud_tasks_appengine_create_task] - """Create a task for a given queue with an arbitrary payload.""" - - from google.cloud import tasks_v2 - from google.protobuf import timestamp_pb2 - import datetime - import json - - # Create a client. - client = tasks_v2.CloudTasksClient() - - # TODO(developer): Uncomment these lines and replace with your values. - # project = 'my-project-id' - # queue = 'my-appengine-queue' - # location = 'us-central1' - # payload = 'hello' or {'param': 'value'} for application/json - # in_seconds = None - - # Construct the fully qualified queue name. - parent = client.queue_path(project, location, queue) - - # Construct the request body. - task = { - "app_engine_http_request": { # Specify the type of request. - "http_method": tasks_v2.HttpMethod.POST, - "relative_uri": "/example_task_handler", - } - } - if payload is not None: - if isinstance(payload, dict): - # Convert dict to JSON string - payload = json.dumps(payload) - # specify http content-type to application/json - task["app_engine_http_request"]["headers"] = { - "Content-type": "application/json" - } - # The API expects a payload of type bytes. - converted_payload = payload.encode() - - # Add the payload to the request. - task["app_engine_http_request"]["body"] = converted_payload - - if in_seconds is not None: - # Convert "seconds from now" into an rfc3339 datetime string. - d = datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta( - seconds=in_seconds - ) - - # Create Timestamp protobuf. - timestamp = timestamp_pb2.Timestamp() - timestamp.FromDatetime(d) - - # Add the timestamp to the tasks. - task["schedule_time"] = timestamp - - # Use the client to build and send the task. - response = client.create_task(parent=parent, task=task) - - print(f"Created task {response.name}") - return response - - -# [END cloud_tasks_appengine_create_task] - - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - description=create_task.__doc__, - formatter_class=argparse.RawDescriptionHelpFormatter, - ) - - parser.add_argument( - "--project", - help="Project of the queue to add the task to.", - required=True, - ) - - parser.add_argument( - "--queue", - help="ID (short name) of the queue to add the task to.", - required=True, - ) - - parser.add_argument( - "--location", - help="Location of the queue to add the task to.", - required=True, - ) - - parser.add_argument( - "--payload", help="Optional payload to attach to the push queue." - ) - - parser.add_argument( - "--in_seconds", - type=int, - help="The number of seconds from now to schedule task attempt.", - ) - - args = parser.parse_args() - - create_task(args.project, args.queue, args.location, args.payload, args.in_seconds) diff --git a/appengine/flexible_python37_and_earlier/tasks/create_app_engine_queue_task_test.py b/appengine/flexible_python37_and_earlier/tasks/create_app_engine_queue_task_test.py deleted file mode 100644 index 3bacaed03ac..00000000000 --- a/appengine/flexible_python37_and_earlier/tasks/create_app_engine_queue_task_test.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2019 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 os - -import create_app_engine_queue_task - -TEST_PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") -TEST_LOCATION = os.getenv("TEST_QUEUE_LOCATION", "us-central1") -TEST_QUEUE_NAME = os.getenv("TEST_QUEUE_NAME", "my-appengine-queue") - - -def test_create_task(): - result = create_app_engine_queue_task.create_task( - TEST_PROJECT_ID, TEST_QUEUE_NAME, TEST_LOCATION - ) - assert TEST_QUEUE_NAME in result.name diff --git a/appengine/flexible_python37_and_earlier/tasks/main.py b/appengine/flexible_python37_and_earlier/tasks/main.py deleted file mode 100644 index 4cb9b84a0b6..00000000000 --- a/appengine/flexible_python37_and_earlier/tasks/main.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright 2019 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. - -"""App Engine app to serve as an endpoint for App Engine queue samples.""" - -# [START cloud_tasks_appengine_quickstart] -from flask import Flask, render_template, request - -app = Flask(__name__) - - -@app.route("/example_task_handler", methods=["POST"]) -def example_task_handler(): - """Log the request payload.""" - payload = request.get_data(as_text=True) or "(empty payload)" - print(f"Received task with payload: {payload}") - return render_template("index.html", payload=payload) - - -# [END cloud_tasks_appengine_quickstart] - - -@app.route("/") -def hello(): - """Basic index to verify app is serving.""" - return "Hello World!" - - -if __name__ == "__main__": - # This is used when running locally. Gunicorn is used to run the - # application on Google App Engine. See entrypoint in app.yaml. - app.run(host="127.0.0.1", port=8080, debug=True) diff --git a/appengine/flexible_python37_and_earlier/tasks/main_test.py b/appengine/flexible_python37_and_earlier/tasks/main_test.py deleted file mode 100644 index 42b96402dd0..00000000000 --- a/appengine/flexible_python37_and_earlier/tasks/main_test.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright 2019 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 pytest - - -@pytest.fixture -def app(): - import main - - main.app.testing = True - return main.app.test_client() - - -def test_index(app): - r = app.get("/") - assert r.status_code == 200 - - -def test_log_payload(capsys, app): - payload = "test_payload" - - r = app.post("/example_task_handler", data=payload) - assert r.status_code == 200 - - out, _ = capsys.readouterr() - assert payload in out - - -def test_empty_payload(capsys, app): - r = app.post("/example_task_handler") - assert r.status_code == 200 - - out, _ = capsys.readouterr() - assert "empty payload" in out diff --git a/appengine/flexible_python37_and_earlier/tasks/noxfile_config.py b/appengine/flexible_python37_and_earlier/tasks/noxfile_config.py deleted file mode 100644 index 1665dd736f8..00000000000 --- a/appengine/flexible_python37_and_earlier/tasks/noxfile_config.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2023 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. - -# Default TEST_CONFIG_OVERRIDE for python repos. - -# You can copy this file into your directory, then it will be imported from -# the noxfile.py. - -# The source of truth: -# https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/noxfile_config.py - -TEST_CONFIG_OVERRIDE = { - # You can opt out from the test for specific Python versions. - # Skipping for Python 3.9 due to pyarrow compilation failure. - "ignored_versions": ["2.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"], - # Old samples are opted out of enforcing Python type hints - # All new samples should feature them - "enforce_type_hints": False, - # An envvar key for determining the project id to use. Change it - # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a - # build specific Cloud project. You can also use your own string - # to use your own Cloud project. - "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", - # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', - # A dictionary you want to inject into your test. Don't put any - # secrets here. These values will override predefined values. - "envs": {}, -} diff --git a/appengine/flexible_python37_and_earlier/tasks/requirements-test.txt b/appengine/flexible_python37_and_earlier/tasks/requirements-test.txt deleted file mode 100644 index 15d066af319..00000000000 --- a/appengine/flexible_python37_and_earlier/tasks/requirements-test.txt +++ /dev/null @@ -1 +0,0 @@ -pytest==8.2.0 diff --git a/appengine/flexible_python37_and_earlier/tasks/requirements.txt b/appengine/flexible_python37_and_earlier/tasks/requirements.txt deleted file mode 100644 index 93643e9fb2a..00000000000 --- a/appengine/flexible_python37_and_earlier/tasks/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -Flask==3.0.3; python_version > '3.6' -Flask==2.0.3; python_version < '3.7' -gunicorn==23.0.0 -google-cloud-tasks==2.18.0 -Werkzeug==3.0.3 diff --git a/appengine/flexible_python37_and_earlier/tasks/snippets_test.py b/appengine/flexible_python37_and_earlier/tasks/snippets_test.py deleted file mode 100644 index d0483389fc8..00000000000 --- a/appengine/flexible_python37_and_earlier/tasks/snippets_test.py +++ /dev/null @@ -1,130 +0,0 @@ -# Copyright 2019 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 os -import uuid - -import pytest - -import snippets - -TEST_PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") -TEST_LOCATION = os.getenv("TEST_QUEUE_LOCATION", "us-central1") -QUEUE_NAME_1 = f"queue-{uuid.uuid4()}" -QUEUE_NAME_2 = f"queue-{uuid.uuid4()}" - - -@pytest.mark.order1 -def test_create_queue(): - name = "projects/{}/locations/{}/queues/{}".format( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_2 - ) - result = snippets.create_queue( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1, QUEUE_NAME_2 - ) - assert name in result.name - - -@pytest.mark.order2 -def test_update_queue(): - name = "projects/{}/locations/{}/queues/{}".format( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1 - ) - result = snippets.update_queue(TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) - assert name in result.name - - -@pytest.mark.order3 -def test_create_task(): - name = "projects/{}/locations/{}/queues/{}".format( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1 - ) - result = snippets.create_task(TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) - assert name in result.name - - -@pytest.mark.order4 -def test_create_task_with_data(): - name = "projects/{}/locations/{}/queues/{}".format( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1 - ) - result = snippets.create_tasks_with_data( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1 - ) - assert name in result.name - - -@pytest.mark.order5 -def test_create_task_with_name(): - name = "projects/{}/locations/{}/queues/{}".format( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1 - ) - result = snippets.create_task_with_name( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1, "foo" - ) - assert name in result.name - - -@pytest.mark.order6 -def test_delete_task(): - result = snippets.delete_task(TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) - assert result is None - - -@pytest.mark.order7 -def test_purge_queue(): - name = "projects/{}/locations/{}/queues/{}".format( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1 - ) - result = snippets.purge_queue(TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) - assert name in result.name - - -@pytest.mark.order8 -def test_pause_queue(): - name = "projects/{}/locations/{}/queues/{}".format( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1 - ) - result = snippets.pause_queue(TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) - assert name in result.name - - -@pytest.mark.order9 -def test_delete_queue(): - result = snippets.delete_queue(TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) - assert result is None - - result = snippets.delete_queue(TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_2) - assert result is None - - -@pytest.mark.order10 -def test_retry_task(): - QUEUE_SIZE = 3 - QUEUE_NAME = [] - for i in range(QUEUE_SIZE): - QUEUE_NAME.append(f"queue-{uuid.uuid4()}") - - name = "projects/{}/locations/{}/queues/{}".format( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME[2] - ) - result = snippets.retry_task( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME[0], QUEUE_NAME[1], QUEUE_NAME[2] - ) - assert name in result.name - - for i in range(QUEUE_SIZE): - snippets.delete_queue( - project=TEST_PROJECT_ID, location=TEST_LOCATION, queue=QUEUE_NAME[i] - ) diff --git a/appengine/flexible_python37_and_earlier/tasks/templates/index.html b/appengine/flexible_python37_and_earlier/tasks/templates/index.html deleted file mode 100644 index 7e4efc7b336..00000000000 --- a/appengine/flexible_python37_and_earlier/tasks/templates/index.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - Tasks Sample - - -

    Printed task payload: {{ payload }}

    - - \ No newline at end of file diff --git a/appengine/flexible_python37_and_earlier/twilio/README.md b/appengine/flexible_python37_and_earlier/twilio/README.md deleted file mode 100644 index 9a62b8400b5..00000000000 --- a/appengine/flexible_python37_and_earlier/twilio/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Python Twilio voice and SMS sample for Google App Engine Flexible Environment - -[![Open in Cloud Shell][shell_img]][shell_link] - -[shell_img]: http://gstatic.com/cloudssh/images/open-btn.png -[shell_link]: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&page=editor&open_in_editor=appengine/flexible_python37_and_earlier/twilio/README.md - -This sample demonstrates how to use [Twilio](https://www.twilio.com) on [Google App Engine Flexible Environment](https://cloud.google.com/appengine). - -For more information about Twilio, see their [Python quickstart tutorials](https://www.twilio.com/docs/quickstart/python). - -## Setup - -Before you can run or deploy the sample, you will need to do the following: - -1. [Create a Twilio Account](http://ahoy.twilio.com/googlecloudplatform). Google App Engine -customers receive a complimentary credit for SMS messages and inbound messages. - -2. Create a number on twilio, and configure the voice request URL to be ``https://your-app-id.appspot.com/call/receive`` -and the SMS request URL to be ``https://your-app-id.appspot.com/sms/receive``. - -3. Configure your Twilio settings in the environment variables section in ``app.yaml``. - -## Running locally - -Refer to the [top-level README](../README.md) for instructions on running and deploying. - -You can run the application locally to test the callbacks and SMS sending. You -will need to set environment variables before starting your application: - - $ export TWILIO_ACCOUNT_SID=[your-twilio-account-sid] - $ export TWILIO_AUTH_TOKEN=[your-twilio-auth-token] - $ export TWILIO_NUMBER=[your-twilio-number] - $ python main.py diff --git a/appengine/flexible_python37_and_earlier/twilio/app.yaml b/appengine/flexible_python37_and_earlier/twilio/app.yaml deleted file mode 100644 index 0e7de97eb19..00000000000 --- a/appengine/flexible_python37_and_earlier/twilio/app.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2021 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. - -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - python_version: 3 - -# [START gae_flex_twilio_env] -env_variables: - TWILIO_ACCOUNT_SID: your-account-sid - TWILIO_AUTH_TOKEN: your-auth-token - TWILIO_NUMBER: your-twilio-number -# [END gae_flex_twilio_env] diff --git a/appengine/flexible_python37_and_earlier/twilio/main.py b/appengine/flexible_python37_and_earlier/twilio/main.py deleted file mode 100644 index 6f2a3a6830f..00000000000 --- a/appengine/flexible_python37_and_earlier/twilio/main.py +++ /dev/null @@ -1,96 +0,0 @@ -# Copyright 2015 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 logging -import os - -from flask import Flask, request -from twilio import rest -from twilio.twiml import messaging_response, voice_response - - -TWILIO_ACCOUNT_SID = os.environ["TWILIO_ACCOUNT_SID"] -TWILIO_AUTH_TOKEN = os.environ["TWILIO_AUTH_TOKEN"] -TWILIO_NUMBER = os.environ["TWILIO_NUMBER"] - - -app = Flask(__name__) - - -# [START gae_flex_twilio_receive_call] -@app.route("/call/receive", methods=["POST"]) -def receive_call(): - """Answers a call and replies with a simple greeting.""" - response = voice_response.VoiceResponse() - response.say("Hello from Twilio!") - return str(response), 200, {"Content-Type": "application/xml"} - - -# [END gae_flex_twilio_receive_call] - - -# [START gae_flex_twilio_send_sms] -@app.route("/sms/send") -def send_sms(): - """Sends a simple SMS message.""" - to = request.args.get("to") - if not to: - return ( - 'Please provide the number to message in the "to" query string' - " parameter." - ), 400 - - client = rest.Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN) - rv = client.messages.create(to=to, from_=TWILIO_NUMBER, body="Hello from Twilio!") - return str(rv) - - -# [END gae_flex_twilio_send_sms] - - -# [START gae_flex_twilio_receive_sms] -@app.route("/sms/receive", methods=["POST"]) -def receive_sms(): - """Receives an SMS message and replies with a simple greeting.""" - sender = request.values.get("From") - body = request.values.get("Body") - - message = f"Hello, {sender}, you said: {body}" - - response = messaging_response.MessagingResponse() - response.message(message) - return str(response), 200, {"Content-Type": "application/xml"} - - -# [END gae_flex_twilio_receive_sms] - - -@app.errorhandler(500) -def server_error(e): - logging.exception("An error occurred during a request.") - return ( - """ - An internal error occurred:
    {}
    - See logs for full stacktrace. - """.format( - e - ), - 500, - ) - - -if __name__ == "__main__": - # This is used when running locally. Gunicorn is used to run the - # application on Google App Engine. See entrypoint in app.yaml. - app.run(host="127.0.0.1", port=8080, debug=True) diff --git a/appengine/flexible_python37_and_earlier/twilio/main_test.py b/appengine/flexible_python37_and_earlier/twilio/main_test.py deleted file mode 100644 index 4878384f65a..00000000000 --- a/appengine/flexible_python37_and_earlier/twilio/main_test.py +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright 2016 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 re - -import pytest -import responses - - -@pytest.fixture -def app(monkeypatch): - monkeypatch.setenv("TWILIO_ACCOUNT_SID", "sid123") - monkeypatch.setenv("TWILIO_AUTH_TOKEN", "auth123") - monkeypatch.setenv("TWILIO_NUMBER", "0123456789") - - import main - - main.app.testing = True - return main.app.test_client() - - -def test_receive_call(app): - r = app.post("/call/receive") - assert "Hello from Twilio!" in r.data.decode("utf-8") - - -@responses.activate -def test_send_sms(app, monkeypatch): - sample_response = { - "sid": "sid", - "date_created": "Wed, 20 Dec 2017 19:32:14 +0000", - "date_updated": "Wed, 20 Dec 2017 19:32:14 +0000", - "date_sent": None, - "account_sid": "account_sid", - "to": "+1234567890", - "from": "+9876543210", - "messaging_service_sid": None, - "body": "Hello from Twilio!", - "status": "queued", - "num_segments": "1", - "num_media": "0", - "direction": "outbound-api", - "api_version": "2010-04-01", - "price": None, - "price_unit": "USD", - "error_code": None, - "error_message": None, - "uri": "/2010-04-01/Accounts/sample.json", - "subresource_uris": {"media": "/2010-04-01/Accounts/sample/Media.json"}, - } - responses.add(responses.POST, re.compile(".*"), json=sample_response, status=200) - - r = app.get("/sms/send") - assert r.status_code == 400 - - r = app.get("/sms/send?to=5558675309") - assert r.status_code == 200 - - -def test_receive_sms(app): - r = app.post( - "/sms/receive", data={"From": "5558675309", "Body": "Jenny, I got your number."} - ) - assert r.status_code == 200 - assert "Jenny, I got your number" in r.data.decode("utf-8") diff --git a/appengine/flexible_python37_and_earlier/twilio/noxfile_config.py b/appengine/flexible_python37_and_earlier/twilio/noxfile_config.py deleted file mode 100644 index 1665dd736f8..00000000000 --- a/appengine/flexible_python37_and_earlier/twilio/noxfile_config.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2023 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. - -# Default TEST_CONFIG_OVERRIDE for python repos. - -# You can copy this file into your directory, then it will be imported from -# the noxfile.py. - -# The source of truth: -# https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/noxfile_config.py - -TEST_CONFIG_OVERRIDE = { - # You can opt out from the test for specific Python versions. - # Skipping for Python 3.9 due to pyarrow compilation failure. - "ignored_versions": ["2.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"], - # Old samples are opted out of enforcing Python type hints - # All new samples should feature them - "enforce_type_hints": False, - # An envvar key for determining the project id to use. Change it - # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a - # build specific Cloud project. You can also use your own string - # to use your own Cloud project. - "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", - # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', - # A dictionary you want to inject into your test. Don't put any - # secrets here. These values will override predefined values. - "envs": {}, -} diff --git a/appengine/flexible_python37_and_earlier/twilio/requirements-test.txt b/appengine/flexible_python37_and_earlier/twilio/requirements-test.txt deleted file mode 100644 index e89f6031ad7..00000000000 --- a/appengine/flexible_python37_and_earlier/twilio/requirements-test.txt +++ /dev/null @@ -1,3 +0,0 @@ -pytest==8.2.0 -responses==0.17.0; python_version < '3.7' -responses==0.23.1; python_version > '3.6' diff --git a/appengine/flexible_python37_and_earlier/twilio/requirements.txt b/appengine/flexible_python37_and_earlier/twilio/requirements.txt deleted file mode 100644 index cfa80d12edf..00000000000 --- a/appengine/flexible_python37_and_earlier/twilio/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -Flask==3.0.3; python_version > '3.6' -Flask==2.0.3; python_version < '3.7' -gunicorn==23.0.0 -twilio==9.0.3 -Werkzeug==3.0.3; python_version >= '3.7' -Werkzeug==2.3.8; python_version < '3.7' diff --git a/appengine/flexible_python37_and_earlier/websockets/README.md b/appengine/flexible_python37_and_earlier/websockets/README.md deleted file mode 100644 index fabd0995a40..00000000000 --- a/appengine/flexible_python37_and_earlier/websockets/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Python websockets sample for Google App Engine Flexible Environment - -This sample demonstrates how to use websockets on [Google App Engine Flexible Environment](https://cloud.google.com/appengine). - -## Running locally - -Refer to the [top-level README](../README.md) for instructions on running and deploying. - -To run locally, you need to use gunicorn with the ``flask_socket`` worker: - - $ gunicorn -b 127.0.0.1:8080 -k flask_sockets.worker main:app diff --git a/appengine/flexible_python37_and_earlier/websockets/app.yaml b/appengine/flexible_python37_and_earlier/websockets/app.yaml deleted file mode 100644 index 8a323ffe30f..00000000000 --- a/appengine/flexible_python37_and_earlier/websockets/app.yaml +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2021 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. - -runtime: python -env: flex - -# Use a special gunicorn worker class to support websockets. -entrypoint: gunicorn -b :$PORT -k flask_sockets.worker main:app - -runtime_config: - python_version: 3 - -# Use only a single instance, so that this local-memory-only chat app will work -# consistently with multiple users. To work across multiple instances, an -# extra-instance messaging system or data store would be needed. -manual_scaling: - instances: 1 - - -# For applications which can take advantage of session affinity -# (where the load balancer will attempt to route multiple connections from -# the same user to the same App Engine instance), uncomment the folowing: - -# network: -# session_affinity: true diff --git a/appengine/flexible_python37_and_earlier/websockets/main.py b/appengine/flexible_python37_and_earlier/websockets/main.py deleted file mode 100644 index 132160d9ab5..00000000000 --- a/appengine/flexible_python37_and_earlier/websockets/main.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright 2018 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. - - -# [START gae_flex_websockets_app] -from flask import Flask, render_template -from flask_sockets import Sockets - - -app = Flask(__name__) -sockets = Sockets(app) - - -@sockets.route("/chat") -def chat_socket(ws): - while not ws.closed: - message = ws.receive() - if message is None: # message is "None" if the client has closed. - continue - # Send the message to all clients connected to this webserver - # process. (To support multiple processes or instances, an - # extra-instance storage or messaging system would be required.) - clients = ws.handler.server.clients.values() - for client in clients: - client.ws.send(message) - - -# [END gae_flex_websockets_app] - - -@app.route("/") -def index(): - return render_template("index.html") - - -if __name__ == "__main__": - print( - """ -This can not be run directly because the Flask development server does not -support web sockets. Instead, use gunicorn: - -gunicorn -b 127.0.0.1:8080 -k flask_sockets.worker main:app - -""" - ) diff --git a/appengine/flexible_python37_and_earlier/websockets/main_test.py b/appengine/flexible_python37_and_earlier/websockets/main_test.py deleted file mode 100644 index 597f2416d1c..00000000000 --- a/appengine/flexible_python37_and_earlier/websockets/main_test.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright 2018 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 socket -import subprocess - -import pytest -import requests -from retrying import retry -import websocket - - -@pytest.fixture(scope="module") -def server(): - """Provides the address of a test HTTP/websocket server. - The test server is automatically created before - a test and destroyed at the end. - """ - # Ask the OS to allocate a port. - sock = socket.socket() - sock.bind(("127.0.0.1", 0)) - port = sock.getsockname()[1] - - # Free the port and pass it to a subprocess. - sock.close() - - bind_to = f"127.0.0.1:{port}" - server = subprocess.Popen( - ["gunicorn", "-b", bind_to, "-k" "flask_sockets.worker", "main:app"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - - # With btlr, there can be many processes are spawned and the - # server might be in a tight memory situation, so let's wait for 2 - # mins. - # Wait until the server responds before proceeding. - @retry(wait_fixed=50, stop_max_delay=120000) - def check_server(url): - requests.get(url) - - check_server(f"http://{bind_to}/") - - yield bind_to - - server.kill() - - # Dump the logs for debugging - out, err = server.communicate() - print(f"gunicorn stdout: {out}") - print(f"gunicorn stderr: {err}") - - -def test_http(server): - result = requests.get(f"http://{server}/") - assert "Python Websockets Chat" in result.text - - -def test_websocket(server): - url = f"ws://{server}/chat" - ws_one = websocket.WebSocket() - ws_one.connect(url) - - ws_two = websocket.WebSocket() - ws_two.connect(url) - - message = "Hello, World" - ws_one.send(message) - - assert ws_one.recv() == message - assert ws_two.recv() == message diff --git a/appengine/flexible_python37_and_earlier/websockets/noxfile_config.py b/appengine/flexible_python37_and_earlier/websockets/noxfile_config.py deleted file mode 100644 index 1665dd736f8..00000000000 --- a/appengine/flexible_python37_and_earlier/websockets/noxfile_config.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2023 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. - -# Default TEST_CONFIG_OVERRIDE for python repos. - -# You can copy this file into your directory, then it will be imported from -# the noxfile.py. - -# The source of truth: -# https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/noxfile_config.py - -TEST_CONFIG_OVERRIDE = { - # You can opt out from the test for specific Python versions. - # Skipping for Python 3.9 due to pyarrow compilation failure. - "ignored_versions": ["2.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"], - # Old samples are opted out of enforcing Python type hints - # All new samples should feature them - "enforce_type_hints": False, - # An envvar key for determining the project id to use. Change it - # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a - # build specific Cloud project. You can also use your own string - # to use your own Cloud project. - "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", - # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', - # A dictionary you want to inject into your test. Don't put any - # secrets here. These values will override predefined values. - "envs": {}, -} diff --git a/appengine/flexible_python37_and_earlier/websockets/requirements-test.txt b/appengine/flexible_python37_and_earlier/websockets/requirements-test.txt deleted file mode 100644 index 92b9194cf63..00000000000 --- a/appengine/flexible_python37_and_earlier/websockets/requirements-test.txt +++ /dev/null @@ -1,3 +0,0 @@ -pytest==8.2.0 -retrying==1.3.4 -websocket-client==1.7.0 diff --git a/appengine/flexible_python37_and_earlier/websockets/requirements.txt b/appengine/flexible_python37_and_earlier/websockets/requirements.txt deleted file mode 100644 index c1525d36077..00000000000 --- a/appengine/flexible_python37_and_earlier/websockets/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -Flask==1.1.4 # it seems like Flask-sockets doesn't play well with 2.0+ -Flask-Sockets==0.2.1 -gunicorn==23.0.0 -requests==2.31.0 -markupsafe==2.0.1 -Werkzeug==1.0.1; diff --git a/appengine/flexible_python37_and_earlier/websockets/templates/index.html b/appengine/flexible_python37_and_earlier/websockets/templates/index.html deleted file mode 100644 index af6d791f148..00000000000 --- a/appengine/flexible_python37_and_earlier/websockets/templates/index.html +++ /dev/null @@ -1,96 +0,0 @@ -{# -# Copyright 2018 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. -#} - - - - Google App Engine Flexible Environment - Python Websockets Chat - - - - - -

    Chat demo

    -
    - - -
    - -
    -

    Messages:

    -
      -
      - -
      -

      Status:

      -
        -
        - - - - - - diff --git a/appengine/standard/noxfile-template.py b/appengine/standard/noxfile-template.py deleted file mode 100644 index f96f3288d70..00000000000 --- a/appengine/standard/noxfile-template.py +++ /dev/null @@ -1,246 +0,0 @@ -# Copyright 2019 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 __future__ import print_function - -import os -from pathlib import Path -import sys - -import nox -import tempfile - - -# WARNING - WARNING - WARNING - WARNING - WARNING -# WARNING - WARNING - WARNING - WARNING - WARNING -# DO NOT EDIT THIS FILE EVER! -# WARNING - WARNING - WARNING - WARNING - WARNING -# WARNING - WARNING - WARNING - WARNING - WARNING - -# Copy `noxfile_config.py` to your directory and modify it instead. - -# `TEST_CONFIG` dict is a configuration hook that allows users to -# modify the test configurations. The values here should be in sync -# with `noxfile_config.py`. Users will copy `noxfile_config.py` into -# their directory and modify it. - -TEST_CONFIG = { - # You can opt out from the test for specific Python versions. - "ignored_versions": ["2.7", "3.7", "3.8", "3.10", "3.12", "3.13"], - # An envvar key for determining the project id to use. Change it - # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a - # build specific Cloud project. You can also use your own string - # to use your own Cloud project. - "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", - # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', - # A dictionary you want to inject into your test. Don't put any - # secrets here. These values will override predefined values. - "envs": {}, -} - - -try: - # Ensure we can import noxfile_config in the project's directory. - sys.path.append(".") - from noxfile_config import TEST_CONFIG_OVERRIDE -except ImportError as e: - print("No user noxfile_config found: detail: {}".format(e)) - TEST_CONFIG_OVERRIDE = {} - -# Update the TEST_CONFIG with the user supplied values. -TEST_CONFIG.update(TEST_CONFIG_OVERRIDE) - - -def get_pytest_env_vars(): - """Returns a dict for pytest invocation.""" - ret = {} - - # Override the GCLOUD_PROJECT and the alias. - env_key = TEST_CONFIG["gcloud_project_env"] - # This should error out if not set. - ret["GOOGLE_CLOUD_PROJECT"] = os.environ[env_key] - ret["GCLOUD_PROJECT"] = os.environ[env_key] # deprecated - - # Apply user supplied envs. - ret.update(TEST_CONFIG["envs"]) - return ret - - -# DO NOT EDIT - automatically generated. -# All versions used to tested samples. -ALL_VERSIONS = ["2.7", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] - -# Any default versions that should be ignored. -IGNORED_VERSIONS = ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] - -TESTED_VERSIONS = sorted([v for v in ALL_VERSIONS if v not in IGNORED_VERSIONS]) - -INSTALL_LIBRARY_FROM_SOURCE = bool(os.environ.get("INSTALL_LIBRARY_FROM_SOURCE", False)) -# -# Style Checks -# - - -# Ignore I202 "Additional newline in a section of imports." to accommodate -# region tags in import blocks. Since we specify an explicit ignore, we also -# have to explicitly ignore the list of default ignores: -# `E121,E123,E126,E226,E24,E704,W503,W504` as shown by `flake8 --help`. -def _determine_local_import_names(start_dir): - """Determines all import names that should be considered "local". - - This is used when running the linter to insure that import order is - properly checked. - """ - file_ext_pairs = [os.path.splitext(path) for path in os.listdir(start_dir)] - return [ - basename - for basename, extension in file_ext_pairs - if extension == ".py" - or os.path.isdir(os.path.join(start_dir, basename)) - and basename not in ("__pycache__") - ] - - -FLAKE8_COMMON_ARGS = [ - "--show-source", - "--builtin=gettext", - "--max-complexity=20", - "--import-order-style=google", - "--exclude=.nox,.cache,env,lib,generated_pb2,*_pb2.py,*_pb2_grpc.py", - "--ignore=E121,E123,E126,E203,E226,E24,E266,E501,E704,W503,W504,I100,I201,I202", - "--max-line-length=88", -] - - -@nox.session -def lint(session): - session.install("flake8", "flake8-import-order") - - local_names = _determine_local_import_names(".") - args = FLAKE8_COMMON_ARGS + [ - "--application-import-names", - ",".join(local_names), - ".", - ] - session.run("flake8", *args) - - -# -# Sample Tests -# - - -PYTEST_COMMON_ARGS = ["--junitxml=sponge_log.xml"] - - -def _session_tests(session, post_install=None): - """Runs py.test for a particular project.""" - if os.path.exists("requirements.txt"): - session.install("-r", "requirements.txt") - - if os.path.exists("requirements-test.txt"): - session.install("-r", "requirements-test.txt") - - if post_install: - post_install(session) - - session.run( - "pytest", - *(PYTEST_COMMON_ARGS + session.posargs), - # Pytest will return 5 when no tests are collected. This can happen - # on travis where slow and flaky tests are excluded. - # See http://doc.pytest.org/en/latest/_modules/_pytest/main.html - success_codes=[0, 5], - env=get_pytest_env_vars() - ) - - -_GAE_ROOT = os.environ.get("GAE_ROOT") -if _GAE_ROOT is None: - _GAE_ROOT = tempfile.mkdtemp() - - -def find_download_appengine_sdk_py(filename): - """Find a file with the given name upwards.""" - d = os.getcwd() - while d != "/": - fullpath = os.path.join(d, filename) - if os.path.isfile(fullpath): - return fullpath - d = os.path.abspath(d + "/../") - - -def _setup_appengine_sdk(session): - """Installs the App Engine SDK, if needed.""" - session.env["GAE_SDK_PATH"] = os.path.join(_GAE_ROOT, "google_appengine") - download_appengine_sdk_py = find_download_appengine_sdk_py( - "download-appengine-sdk.py" - ) - session.install("requests") - session.run("python", download_appengine_sdk_py, _GAE_ROOT) - - -@nox.session(python=ALL_VERSIONS) -def py(session): - """Runs py.test for a sample using the specified version of Python.""" - if session.python in TESTED_VERSIONS: - # Create a lib directory if needed, - # otherwise the App Engine vendor library will complain. - if not os.path.isdir("lib"): - os.mkdir("lib") - - # mailjet_rest has an issue with requests being required pre install - # https://github.com/mailjet/mailjet-apiv3-python/issues/38 - if "appengine/standard/mailjet" in os.getcwd(): - session.install("requests") - - _session_tests(session, post_install=_setup_appengine_sdk) - else: - print("SKIPPED: {} tests are disabled for this sample.".format(session.python)) - - -# -# Readmegen -# - - -def _get_repo_root(): - """Returns the root folder of the project.""" - # Get root of this repository. Assume we don't have directories nested deeper than 10 items. - p = Path(os.getcwd()) - for i in range(10): - if p is None: - break - if Path(p / ".git").exists(): - return str(p) - p = p.parent - raise Exception("Unable to detect repository root.") - - -GENERATED_READMES = sorted([x for x in Path(".").rglob("*.rst.in")]) - - -@nox.session -@nox.parametrize("path", GENERATED_READMES) -def readmegen(session, path): - """(Re-)generates the readme for a sample.""" - session.install("jinja2", "pyyaml") - - if os.path.exists(os.path.join(path, "requirements.txt")): - session.install("-r", os.path.join(path, "requirements.txt")) - - in_file = os.path.join(path, "README.rst.in") - session.run( - "python", _get_repo_root() + "/scripts/readme-gen/readme_gen.py", in_file - ) diff --git a/appengine/standard/noxfile_config.py b/appengine/standard/noxfile_config.py index 9d81eb86207..f39811085fa 100644 --- a/appengine/standard/noxfile_config.py +++ b/appengine/standard/noxfile_config.py @@ -24,7 +24,7 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - "ignored_versions": ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"], + "ignored_versions": ["3.6", "3.7", "3.8", "3.10", "3.11", "3.12", "3.13"], # Old samples are opted out of enforcing Python type hints # All new samples should feature them "enforce_type_hints": False, diff --git a/appengine/standard_python3/building-an-app/building-an-app-1/app.yaml b/appengine/standard_python3/building-an-app/building-an-app-1/app.yaml index 100d540982b..2ecf42a0f4f 100644 --- a/appengine/standard_python3/building-an-app/building-an-app-1/app.yaml +++ b/appengine/standard_python3/building-an-app/building-an-app-1/app.yaml @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -runtime: python313 +runtime: python314 handlers: # This configures Google App Engine to serve the files in the app's static diff --git a/appengine/standard_python3/building-an-app/building-an-app-1/requirements-test.txt b/appengine/standard_python3/building-an-app/building-an-app-1/requirements-test.txt index c2845bffbe8..c987bcfee7e 100644 --- a/appengine/standard_python3/building-an-app/building-an-app-1/requirements-test.txt +++ b/appengine/standard_python3/building-an-app/building-an-app-1/requirements-test.txt @@ -1 +1,2 @@ -pytest==7.0.1 +pytest==7.0.1; python_version == '3.9' +pytest==9.0.2; python_version >= '3.10' diff --git a/appengine/standard_python3/django/requirements.txt b/appengine/standard_python3/django/requirements.txt index cdd4b54cf3e..60b4408e6b4 100644 --- a/appengine/standard_python3/django/requirements.txt +++ b/appengine/standard_python3/django/requirements.txt @@ -1,4 +1,4 @@ -Django==5.1.8; python_version >= "3.10" +Django==5.1.15; python_version >= "3.10" Django==4.2.17; python_version >= "3.8" and python_version < "3.10" Django==3.2.25; python_version < "3.8" django-environ==0.10.0 diff --git a/auth/cloud-client-temp/authenticate_explicit_with_adc.py b/auth/cloud-client-temp/authenticate_explicit_with_adc.py new file mode 100644 index 00000000000..c9ce2f02af3 --- /dev/null +++ b/auth/cloud-client-temp/authenticate_explicit_with_adc.py @@ -0,0 +1,55 @@ +# Copyright 2022 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. + +# [START auth_cloud_explicit_adc] + + +import google.auth +from google.cloud import storage +import google.oauth2.credentials + + +def authenticate_explicit_with_adc() -> None: + """ + List storage buckets by authenticating with ADC. + + // TODO(Developer): + // 1. Before running this sample, + // set up ADC as described in https://cloud.google.com/docs/authentication/external/set-up-adc + // 2. Replace the project variable. + // 3. Make sure you have the necessary permission to list storage buckets: "storage.buckets.list" + """ + + # Construct the Google credentials object which obtains the default configuration from your + # working environment. + # google.auth.default() will give you ComputeEngineCredentials + # if you are on a GCE (or other metadata server supported environments). + credentials, project_id = google.auth.default() + # If you are authenticating to a Cloud API, you can let the library include the default scope, + # https://www.googleapis.com/auth/cloud-platform, because IAM is used to provide fine-grained + # permissions for Cloud. + # If you need to provide a scope, specify it as follows: + # credentials = google.auth.default(scopes=scope) + # For more information on scopes to use, + # see: https://developers.google.com/identity/protocols/oauth2/scopes + + # Construct the Storage client. + storage_client = storage.Client(credentials=credentials, project=project_id) + buckets = storage_client.list_buckets() + print("Buckets:") + for bucket in buckets: + print(bucket.name) + print("Listed all storage buckets.") + +# [END auth_cloud_explicit_adc] diff --git a/auth/cloud-client-temp/authenticate_implicit_with_adc.py b/auth/cloud-client-temp/authenticate_implicit_with_adc.py new file mode 100644 index 00000000000..ed967ab880a --- /dev/null +++ b/auth/cloud-client-temp/authenticate_implicit_with_adc.py @@ -0,0 +1,46 @@ +# Copyright 2022 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. + +# [START auth_cloud_implicit_adc] + +from google.cloud import storage + + +def authenticate_implicit_with_adc(project_id: str = "your-google-cloud-project-id") -> None: + """ + When interacting with Google Cloud Client libraries, the library can auto-detect the + credentials to use. + + // TODO(Developer): + // 1. Before running this sample, + // set up ADC as described in https://cloud.google.com/docs/authentication/external/set-up-adc + // 2. Replace the project variable. + // 3. Make sure that the user account or service account that you are using + // has the required permissions. For this sample, you must have "storage.buckets.list". + Args: + project_id: The project id of your Google Cloud project. + """ + + # This snippet demonstrates how to list buckets. + # *NOTE*: Replace the client created below with the client required for your application. + # Note that the credentials are not specified when constructing the client. + # Hence, the client library will look for credentials using ADC. + storage_client = storage.Client(project=project_id) + buckets = storage_client.list_buckets() + print("Buckets:") + for bucket in buckets: + print(bucket.name) + print("Listed all storage buckets.") + +# [END auth_cloud_implicit_adc] diff --git a/auth/cloud-client-temp/custom_aws_supplier.py b/auth/cloud-client-temp/custom_aws_supplier.py new file mode 100644 index 00000000000..abe858eb5b5 --- /dev/null +++ b/auth/cloud-client-temp/custom_aws_supplier.py @@ -0,0 +1,119 @@ +# 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 json +import os +import sys + +import boto3 +from dotenv import load_dotenv +from google.auth.aws import AwsSecurityCredentials, AwsSecurityCredentialsSupplier +from google.auth.aws import Credentials as AwsCredentials +from google.auth.exceptions import GoogleAuthError +from google.auth.transport.requests import AuthorizedSession + +load_dotenv() + + +class CustomAwsSupplier(AwsSecurityCredentialsSupplier): + """Custom AWS Security Credentials Supplier.""" + + def __init__(self) -> None: + """Initializes the Boto3 session, prioritizing environment variables for region.""" + # Explicitly read the region from the environment first. This ensures that + # a value from a .env file is picked up reliably for local testing. + region = os.getenv("AWS_REGION") or os.getenv("AWS_DEFAULT_REGION") + + # If region is None, Boto3's discovery chain will be used when needed. + self.session = boto3.Session(region_name=region) + self._cached_region = None + print(f"[INFO] CustomAwsSupplier initialized. Region from env: {region}") + + def get_aws_region(self, context: object, request: object) -> str: + """Returns the AWS region using Boto3's default provider chain.""" + if self._cached_region: + return self._cached_region + + # Accessing region_name will use the value from the constructor if provided, + # otherwise it triggers Boto3's lazy-loading discovery (e.g., metadata service). + self._cached_region = self.session.region_name + + if not self._cached_region: + print("[ERROR] Boto3 was unable to resolve an AWS region.", file=sys.stderr) + raise GoogleAuthError("Boto3 was unable to resolve an AWS region.") + + print(f"[INFO] Boto3 resolved AWS Region: {self._cached_region}") + return self._cached_region + + def get_aws_security_credentials(self, context: object, request: object = None) -> AwsSecurityCredentials: + """Retrieves AWS security credentials using Boto3's default provider chain.""" + aws_credentials = self.session.get_credentials() + if not aws_credentials: + print("[ERROR] Unable to resolve AWS credentials.", file=sys.stderr) + raise GoogleAuthError("Unable to resolve AWS credentials from the provider chain.") + + # Instead of printing the whole key, mask everything but the last 4 characters + masked_access_key = f"{'*' * 16}{aws_credentials.access_key[-4:]}" + print(f"[INFO] Resolved AWS Access Key ID: {masked_access_key}") + + return AwsSecurityCredentials( + access_key_id=aws_credentials.access_key, + secret_access_key=aws_credentials.secret_key, + session_token=aws_credentials.token, + ) + + +def main() -> None: + """Main function to demonstrate the custom AWS supplier.""" + print("--- Starting Script ---") + + gcp_audience = os.getenv("GCP_WORKLOAD_AUDIENCE") + sa_impersonation_url = os.getenv("GCP_SERVICE_ACCOUNT_IMPERSONATION_URL") + gcs_bucket_name = os.getenv("GCS_BUCKET_NAME") + + print(f"GCP_WORKLOAD_AUDIENCE: {gcp_audience}") + print(f"GCS_BUCKET_NAME: {gcs_bucket_name}") + + if not all([gcp_audience, sa_impersonation_url, gcs_bucket_name]): + print("[ERROR] Missing required environment variables.", file=sys.stderr) + raise GoogleAuthError("Missing required environment variables.") + + custom_supplier = CustomAwsSupplier() + + credentials = AwsCredentials( + audience=gcp_audience, + subject_token_type="urn:ietf:params:aws:token-type:aws4_request", + service_account_impersonation_url=sa_impersonation_url, + aws_security_credentials_supplier=custom_supplier, + scopes=['https://www.googleapis.com/auth/devstorage.read_write'], + ) + + bucket_url = f"https://storage.googleapis.com/storage/v1/b/{gcs_bucket_name}" + print(f"Request URL: {bucket_url}") + + authed_session = AuthorizedSession(credentials) + try: + print("Attempting to make authenticated request to Google Cloud Storage...") + res = authed_session.get(bucket_url) + res.raise_for_status() + print("\n--- SUCCESS! ---") + print("Successfully authenticated and retrieved bucket data:") + print(json.dumps(res.json(), indent=2)) + except Exception as e: + print("--- FAILED --- ", file=sys.stderr) + print(e, file=sys.stderr) + exit(1) + + +if __name__ == "__main__": + main() diff --git a/auth/cloud-client-temp/custom_okta_supplier.py b/auth/cloud-client-temp/custom_okta_supplier.py new file mode 100644 index 00000000000..c2b35fd406f --- /dev/null +++ b/auth/cloud-client-temp/custom_okta_supplier.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 json +import os +import time +import urllib.parse + +from dotenv import load_dotenv +from google.auth.exceptions import GoogleAuthError +from google.auth.identity_pool import Credentials as IdentityPoolClient +from google.auth.transport.requests import AuthorizedSession +import requests + +load_dotenv() + +# Workload Identity Pool Configuration +GCP_WORKLOAD_AUDIENCE = os.getenv("GCP_WORKLOAD_AUDIENCE") +SERVICE_ACCOUNT_IMPERSONATION_URL = os.getenv("GCP_SERVICE_ACCOUNT_IMPERSONATION_URL") +GCS_BUCKET_NAME = os.getenv("GCS_BUCKET_NAME") + +# Okta Configuration +OKTA_DOMAIN = os.getenv("OKTA_DOMAIN") +OKTA_CLIENT_ID = os.getenv("OKTA_CLIENT_ID") +OKTA_CLIENT_SECRET = os.getenv("OKTA_CLIENT_SECRET") + +# Constants +TOKEN_URL = "https://sts.googleapis.com/v1/token" +SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:jwt" + + +class OktaClientCredentialsSupplier: + """A custom SubjectTokenSupplier that authenticates with Okta. + + This supplier uses the Client Credentials grant flow for machine-to-machine + (M2M) authentication with Okta. + """ + + def __init__(self, domain: str, client_id: str, client_secret: str) -> None: + self.okta_token_url = f"{domain}/oauth2/default/v1/token" + self.client_id = client_id + self.client_secret = client_secret + self.access_token = None + self.expiry_time = 0 + print("OktaClientCredentialsSupplier initialized.") + + def get_subject_token(self, context: object, request: object = None) -> str: + """Fetches a new token if the current one is expired or missing. + + Args: + context: The context object, not used in this implementation. + + Returns: + The Okta Access token. + """ + # Check if the current token is still valid (with a 60-second buffer). + is_token_valid = self.access_token and time.time() < self.expiry_time - 60 + + if is_token_valid: + print("[Supplier] Returning cached Okta Access token.") + return self.access_token + + print( + "[Supplier] Token is missing or expired. Fetching new Okta Access token..." + ) + self._fetch_okta_access_token() + return self.access_token + + def _fetch_okta_access_token(self) -> None: + """Performs the Client Credentials grant flow with Okta.""" + headers = { + "Content-Type": "application/x-www-form-urlencoded", + "Accept": "application/json", + } + data = { + "grant_type": "client_credentials", + "scope": "gcp.test.read", + } + encoded_data = urllib.parse.urlencode(data) + + try: + response = requests.post( + self.okta_token_url, + headers=headers, + data=encoded_data, + auth=(self.client_id, self.client_secret), + ) + response.raise_for_status() + token_data = response.json() + + if "access_token" in token_data and "expires_in" in token_data: + self.access_token = token_data["access_token"] + self.expiry_time = time.time() + token_data["expires_in"] + print( + f"[Supplier] Successfully received Access Token from Okta. " + f"Expires in {token_data['expires_in']} seconds." + ) + else: + raise GoogleAuthError( + "Access token or expires_in not found in Okta response." + ) + except requests.exceptions.RequestException as e: + print(f"[Supplier] Error fetching token from Okta: {e}") + if e.response: + print(f"[Supplier] Okta response: {e.response.text}") + raise GoogleAuthError( + "Failed to authenticate with Okta using Client Credentials grant." + ) from e + + +def main() -> None: + """Main function to demonstrate the custom Okta supplier. + + TODO(Developer): + 1. Before running this sample, set up your environment variables. You can do + this by creating a .env file in the same directory as this script and + populating it with the following variables: + - GCP_WORKLOAD_AUDIENCE: The audience for the GCP workload identity pool. + - GCP_SERVICE_ACCOUNT_IMPERSONATION_URL: The URL for service account impersonation (optional). + - GCS_BUCKET_NAME: The name of the GCS bucket to access. + - OKTA_DOMAIN: Your Okta domain (e.g., https://dev-12345.okta.com). + - OKTA_CLIENT_ID: The Client ID of your Okta M2M application. + - OKTA_CLIENT_SECRET: The Client Secret of your Okta M2M application. + """ + if not all( + [ + GCP_WORKLOAD_AUDIENCE, + GCS_BUCKET_NAME, + OKTA_DOMAIN, + OKTA_CLIENT_ID, + OKTA_CLIENT_SECRET, + ] + ): + raise GoogleAuthError( + "Missing required environment variables. Please check your .env file." + ) + + # 1. Instantiate the custom supplier with Okta credentials. + okta_supplier = OktaClientCredentialsSupplier( + OKTA_DOMAIN, OKTA_CLIENT_ID, OKTA_CLIENT_SECRET + ) + + # 2. Instantiate an IdentityPoolClient. + client = IdentityPoolClient( + audience=GCP_WORKLOAD_AUDIENCE, + subject_token_type=SUBJECT_TOKEN_TYPE, + token_url=TOKEN_URL, + subject_token_supplier=okta_supplier, + # If you choose to provide explicit scopes: use the `scopes` parameter. + default_scopes=['https://www.googleapis.com/auth/cloud-platform'], + service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, + ) + + # 3. Construct the URL for the Cloud Storage JSON API. + bucket_url = f"https://storage.googleapis.com/storage/v1/b/{GCS_BUCKET_NAME}" + print(f"[Test] Getting metadata for bucket: {GCS_BUCKET_NAME}...") + print(f"[Test] Request URL: {bucket_url}") + + # 4. Use the client to make an authenticated request. + authed_session = AuthorizedSession(client) + try: + res = authed_session.get(bucket_url) + res.raise_for_status() + print("\n--- SUCCESS! ---") + print("Successfully authenticated and retrieved bucket data:") + print(json.dumps(res.json(), indent=2)) + except requests.exceptions.RequestException as e: + print("\n--- FAILED ---") + print(f"Request failed: {e}") + if e.response: + print(f"Response: {e.response.text}") + exit(1) + except GoogleAuthError as e: + print("\n--- FAILED ---") + print(f"Authentication or request failed: {e}") + exit(1) + + +if __name__ == "__main__": + main() diff --git a/auth/cloud-client-temp/idtoken_from_impersonated_credentials.py b/auth/cloud-client-temp/idtoken_from_impersonated_credentials.py new file mode 100644 index 00000000000..7819072d927 --- /dev/null +++ b/auth/cloud-client-temp/idtoken_from_impersonated_credentials.py @@ -0,0 +1,75 @@ +# Copyright 2022 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. + +# [auth_cloud_idtoken_impersonated_credentials] + +import google +from google.auth import impersonated_credentials +import google.auth.transport.requests + + +def idtoken_from_impersonated_credentials( + impersonated_service_account: str, scope: str, target_audience: str) -> None: + """ + Use a service account (SA1) to impersonate as another service account (SA2) and obtain id token + for the impersonated account. + To obtain token for SA2, SA1 should have the "roles/iam.serviceAccountTokenCreator" permission + on SA2. + + Args: + impersonated_service_account: The name of the privilege-bearing service account for whom the credential is created. + Examples: name@project.service.gserviceaccount.com + + scope: Provide the scopes that you might need to request to access Google APIs, + depending on the level of access you need. + For this example, we use the cloud-wide scope and use IAM to narrow the permissions. + https://cloud.google.com/docs/authentication#authorization_for_services + For more information, see: https://developers.google.com/identity/protocols/oauth2/scopes + + target_audience: The service name for which the id token is requested. Service name refers to the + logical identifier of an API service, such as "iap.googleapis.com". + Examples: iap.googleapis.com + """ + + # Construct the GoogleCredentials object which obtains the default configuration from your + # working environment. + credentials, project_id = google.auth.default() + + # Create the impersonated credential. + target_credentials = impersonated_credentials.Credentials( + source_credentials=credentials, + target_principal=impersonated_service_account, + # delegates: The chained list of delegates required to grant the final accessToken. + # For more information, see: + # https://cloud.google.com/iam/docs/create-short-lived-credentials-direct#sa-credentials-permissions + # Delegate is NOT USED here. + delegates=[], + target_scopes=[scope], + lifetime=300) + + # Set the impersonated credential, target audience and token options. + id_creds = impersonated_credentials.IDTokenCredentials( + target_credentials, + target_audience=target_audience, + include_email=True) + + # Get the ID token. + # Once you've obtained the ID token, use it to make an authenticated call + # to the target audience. + request = google.auth.transport.requests.Request() + id_creds.refresh(request) + # token = id_creds.token + print("Generated ID token.") + +# [auth_cloud_idtoken_impersonated_credentials] diff --git a/auth/cloud-client-temp/idtoken_from_metadata_server.py b/auth/cloud-client-temp/idtoken_from_metadata_server.py new file mode 100644 index 00000000000..7c9277f349e --- /dev/null +++ b/auth/cloud-client-temp/idtoken_from_metadata_server.py @@ -0,0 +1,50 @@ +# Copyright 2022 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. + +# [START auth_cloud_idtoken_metadata_server] + +import google +from google.auth import compute_engine +import google.auth.transport.requests +import google.oauth2.credentials + + +def idtoken_from_metadata_server(url: str) -> None: + """ + Use the Google Cloud metadata server in the Cloud Run (or AppEngine or Kubernetes etc.,) + environment to create an identity token and add it to the HTTP request as part of an + Authorization header. + + Args: + url: The url or target audience to obtain the ID token for. + Examples: http://www.example.com + """ + + request = google.auth.transport.requests.Request() + # Set the target audience. + # Setting "use_metadata_identity_endpoint" to "True" will make the request use the default application + # credentials. Optionally, you can also specify a specific service account to use by mentioning + # the service_account_email. + credentials = compute_engine.IDTokenCredentials( + request=request, target_audience=url, use_metadata_identity_endpoint=True + ) + + # Get the ID token. + # Once you've obtained the ID token, use it to make an authenticated call + # to the target audience. + credentials.refresh(request) + # print(credentials.token) + print("Generated ID token.") + +# [END auth_cloud_idtoken_metadata_server] diff --git a/auth/cloud-client-temp/idtoken_from_service_account.py b/auth/cloud-client-temp/idtoken_from_service_account.py new file mode 100644 index 00000000000..d96a4862a8b --- /dev/null +++ b/auth/cloud-client-temp/idtoken_from_service_account.py @@ -0,0 +1,50 @@ +# Copyright 2022 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. + +# [START auth_cloud_idtoken_service_account] + +import google.auth +import google.auth.transport.requests + +from google.oauth2 import service_account + + +def get_idToken_from_serviceaccount(json_credential_path: str, target_audience: str) -> None: + """ + TODO(Developer): Replace the below variables before running the code. + + *NOTE*: + Using service account keys introduces risk; they are long-lived, and can be used by anyone + that obtains the key. Proper rotation and storage reduce this risk but do not eliminate it. + For these reasons, you should consider an alternative approach that + does not use a service account key. Several alternatives to service account keys + are described here: + https://cloud.google.com/docs/authentication/external/set-up-adc + + Args: + json_credential_path: Path to the service account json credential file. + target_audience: The url or target audience to obtain the ID token for. + Examples: http://www.abc.com + """ + + # Obtain the id token by providing the json file path and target audience. + credentials = service_account.IDTokenCredentials.from_service_account_file( + filename=json_credential_path, + target_audience=target_audience) + + request = google.auth.transport.requests.Request() + credentials.refresh(request) + print("Generated ID token.") + +# [END auth_cloud_idtoken_service_account] diff --git a/auth/cloud-client-temp/noxfile.py b/auth/cloud-client-temp/noxfile.py new file mode 100644 index 00000000000..3cdf3cf3bdb --- /dev/null +++ b/auth/cloud-client-temp/noxfile.py @@ -0,0 +1,85 @@ +# Copyright 2019 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 nox + +CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute() + +# https://github.com/psf/black/issues/2964, pin click version to 8.0.4 to +# avoid incompatiblity with black. +CLICK_VERSION = "click==8.0.4" +BLACK_VERSION = "black==19.3b0" +BLACK_PATHS = [ + "google", + "tests", + "tests_async", + "noxfile.py", + "setup.py", + "docs/conf.py", +] + + +# Error if a python version is missing +nox.options.error_on_missing_interpreters = True + +# +# Style Checks +# + + +# Linting with flake8. +# +# We ignore the following rules: +# E203: whitespace before ‘:’ +# E266: too many leading ‘#’ for block comment +# E501: line too long +# I202: Additional newline in a section of imports +# +# We also need to specify the rules which are ignored by default: +# ['E226', 'W504', 'E126', 'E123', 'W503', 'E24', 'E704', 'E121'] +FLAKE8_COMMON_ARGS = [ + "--show-source", + "--builtin=gettext", + "--max-complexity=20", + "--exclude=.nox,.cache,env,lib,generated_pb2,*_pb2.py,*_pb2_grpc.py", + "--ignore=E121,E123,E126,E203,E226,E24,E266,E501,E704,W503,W504,I202", + "--max-line-length=88", +] + + +@nox.session(python=["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]) +def unit(session): + # constraints_path = str( + # CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" + # ) + session.install("-r", "requirements.txt") + # session.install("-e", ".") + session.run( + "pytest", + f"--junitxml=unit_{session.python}_sponge_log.xml", + "snippets_test.py", + # "tests_async", + ) + + +@nox.session +def lint(session: nox.sessions.Session) -> None: + session.install("flake8") + + args = FLAKE8_COMMON_ARGS + [ + ".", + ] + session.run("flake8", *args) diff --git a/appengine/flexible_python37_and_earlier/scipy/noxfile_config.py b/auth/cloud-client-temp/noxfile_config.py similarity index 80% rename from appengine/flexible_python37_and_earlier/scipy/noxfile_config.py rename to auth/cloud-client-temp/noxfile_config.py index 887244766fd..e892b338fce 100644 --- a/appengine/flexible_python37_and_earlier/scipy/noxfile_config.py +++ b/auth/cloud-client-temp/noxfile_config.py @@ -14,25 +14,24 @@ # Default TEST_CONFIG_OVERRIDE for python repos. -# You can copy this file into your directory, then it will be imported from +# You can copy this file into your directory, then it will be inported from # the noxfile.py. # The source of truth: -# https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/noxfile_config.py +# https://github.com/GoogleCloudPlatform/python-docs-samples/blob/master/noxfile_config.py TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - # Skipping for Python 3.9 due to pyarrow compilation failure. - "ignored_versions": ["2.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"], + "ignored_versions": ["2.7"], # Old samples are opted out of enforcing Python type hints # All new samples should feature them - "enforce_type_hints": False, + "enforce_type_hints": True, # An envvar key for determining the project id to use. Change it # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a # build specific Cloud project. You can also use your own string # to use your own Cloud project. + # "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", - # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', # A dictionary you want to inject into your test. Don't put any # secrets here. These values will override predefined values. "envs": {}, diff --git a/auth/cloud-client-temp/requirements.txt b/auth/cloud-client-temp/requirements.txt new file mode 100644 index 00000000000..8dafe853ea0 --- /dev/null +++ b/auth/cloud-client-temp/requirements.txt @@ -0,0 +1,8 @@ +google-cloud-compute==1.42.0 +google-cloud-storage==3.8.0 +google-auth==2.47.0 +pytest===8.4.2; python_version == '3.9' +pytest==9.0.2; python_version > '3.9' +boto3>=1.26.0 +requests==2.32.5 +python-dotenv==1.2.1 diff --git a/auth/cloud-client-temp/snippets_test.py b/auth/cloud-client-temp/snippets_test.py new file mode 100644 index 00000000000..940f27e553c --- /dev/null +++ b/auth/cloud-client-temp/snippets_test.py @@ -0,0 +1,76 @@ +# Copyright 2022 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 os +import re + +from _pytest.capture import CaptureFixture +import google +import google.auth.transport.requests +from google.oauth2 import service_account + +import authenticate_explicit_with_adc +import authenticate_implicit_with_adc +import idtoken_from_metadata_server +import idtoken_from_service_account +# from system_tests.noxfile import SERVICE_ACCOUNT_FILE +import verify_google_idtoken + +CREDENTIALS, PROJECT = google.auth.default() +SERVICE_ACCOUNT_FILE = os.getenv("GOOGLE_APPLICATION_CREDENTIALS") + + +def test_authenticate_explicit_with_adc(capsys: CaptureFixture) -> None: + authenticate_explicit_with_adc.authenticate_explicit_with_adc() + out, err = capsys.readouterr() + assert re.search("Listed all storage buckets.", out) + + +def test_authenticate_implicit_with_adc(capsys: CaptureFixture) -> None: + authenticate_implicit_with_adc.authenticate_implicit_with_adc(PROJECT) + out, err = capsys.readouterr() + assert re.search("Listed all storage buckets.", out) + + +def test_idtoken_from_metadata_server(capsys: CaptureFixture) -> None: + idtoken_from_metadata_server.idtoken_from_metadata_server("https://www.google.com") + out, err = capsys.readouterr() + assert re.search("Generated ID token.", out) + + +def test_idtoken_from_service_account(capsys: CaptureFixture) -> None: + idtoken_from_service_account.get_idToken_from_serviceaccount( + SERVICE_ACCOUNT_FILE, + "iap.googleapis.com") + out, err = capsys.readouterr() + assert re.search("Generated ID token.", out) + + +def test_verify_google_idtoken() -> None: + idtoken = get_idtoken_from_service_account(SERVICE_ACCOUNT_FILE, "iap.googleapis.com") + + verify_google_idtoken.verify_google_idtoken( + idtoken, + "iap.googleapis.com", + "https://www.googleapis.com/oauth2/v3/certs" + ) + + +def get_idtoken_from_service_account(json_credential_path: str, target_audience: str) -> str: + credentials = service_account.IDTokenCredentials.from_service_account_file( + filename=json_credential_path, + target_audience=target_audience) + + request = google.auth.transport.requests.Request() + credentials.refresh(request) + return credentials.token diff --git a/auth/cloud-client-temp/verify_google_idtoken.py b/auth/cloud-client-temp/verify_google_idtoken.py new file mode 100644 index 00000000000..8bb4c075fd7 --- /dev/null +++ b/auth/cloud-client-temp/verify_google_idtoken.py @@ -0,0 +1,62 @@ +# Copyright 2022 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. + +# [START auth_cloud_verify_google_idtoken] + +import google +import google.auth.transport.requests +from google.oauth2 import id_token + + +def verify_google_idtoken(idtoken: str, audience: str = "iap.googleapis.com", + jwk_url: str = "https://www.googleapis.com/oauth2/v3/certs") -> None: + """ + Verifies the obtained Google id token. This is done at the receiving end of the OIDC endpoint. + The most common use case for verifying the ID token is when you are protecting + your own APIs with IAP. Google services already verify credentials as a platform, + so verifying ID tokens before making Google API calls is usually unnecessary. + + Args: + idtoken: The Google ID token to verify. + + audience: The service name for which the id token is requested. Service name refers to the + logical identifier of an API service, such as "iap.googleapis.com". + + jwk_url: To verify id tokens, get the Json Web Key endpoint (jwk). + OpenID Connect allows the use of a "Discovery document," a JSON document found at a + well-known location containing key-value pairs which provide details about the + OpenID Connect provider's configuration. + For more information on validating the jwt, see: + https://developers.google.com/identity/protocols/oauth2/openid-connect#validatinganidtoken + + Here, we validate Google's token using Google's OpenID Connect service (jwkUrl). + For more information on jwk,see: + https://auth0.com/docs/secure/tokens/json-web-tokens/json-web-key-sets + """ + + request = google.auth.transport.requests.Request() + # Set the parameters and verify the token. + # Setting "certs_url" is optional. When verifying a Google ID token, this is set by default. + result = id_token.verify_token(idtoken, request, audience, clock_skew_in_seconds=10) + + # Verify that the token contains subject and email claims. + # Get the User id. + if not result["sub"] is None: + print(f"User id: {result['sub']}") + # Optionally, if "INCLUDE_EMAIL" was set in the token options, check if the + # email was verified. + if result.get('email_verified'): + print(f"Email verified {result['email']}") + +# [END auth_cloud_verify_google_idtoken] diff --git a/auth/custom-credentials/aws/Dockerfile b/auth/custom-credentials/aws/Dockerfile new file mode 100644 index 00000000000..d90d88aa0a8 --- /dev/null +++ b/auth/custom-credentials/aws/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.13-slim + +RUN useradd -m appuser + +WORKDIR /app + +COPY --chown=appuser:appuser requirements.txt . + +USER appuser +RUN pip install --no-cache-dir -r requirements.txt + +COPY --chown=appuser:appuser snippets.py . + + +CMD ["python3", "snippets.py"] diff --git a/auth/custom-credentials/aws/README.md b/auth/custom-credentials/aws/README.md new file mode 100644 index 00000000000..551c95ef691 --- /dev/null +++ b/auth/custom-credentials/aws/README.md @@ -0,0 +1,127 @@ +# Running the Custom AWS Credential Supplier Sample + +This sample demonstrates how to use a custom AWS security credential supplier to authenticate with Google Cloud using AWS as an external identity provider. It uses Boto3 (the AWS SDK for Python) to fetch credentials from sources like Amazon Elastic Kubernetes Service (EKS) with IAM Roles for Service Accounts(IRSA), Elastic Container Service (ECS), or Fargate. + +## Prerequisites + +* An AWS account. +* A Google Cloud project with the IAM API enabled. +* A GCS bucket. +* Python 3.10 or later installed. + +If you want to use AWS security credentials that cannot be retrieved using methods supported natively by the [google-auth](https://github.com/googleapis/google-auth-library-python) library, a custom `AwsSecurityCredentialsSupplier` implementation may be specified. The supplier must return valid, unexpired AWS security credentials when called by the Google Cloud Auth library. + + +## Running Locally + +For local development, you can provide credentials and configuration in a JSON file. + +### Install Dependencies + +Ensure you have Python installed, then install the required libraries: + +```bash +pip install -r requirements.txt +``` + +### Configure Credentials for Local Development + +1. Copy the example secrets file to a new file named `custom-credentials-aws-secrets.json`: + ```bash + cp custom-credentials-aws-secrets.json.example custom-credentials-aws-secrets.json + ``` +2. Open `custom-credentials-aws-secrets.json` and fill in the required values for your AWS and Google Cloud configuration. Do not check your `custom-credentials-aws-secrets.json` file into version control. + +**Note:** This file is only used for local development and is not needed when running in a containerized environment like EKS with IRSA. + + +### Run the Script + +```bash +python3 snippets.py +``` + +When run locally, the script will detect the `custom-credentials-aws-secrets.json` file and use it to configure the necessary environment variables for the Boto3 client. + +## Running in a Containerized Environment (EKS) + +This section provides a brief overview of how to run the sample in an Amazon EKS cluster. + +### EKS Cluster Setup + +First, you need an EKS cluster. You can create one using `eksctl` or the AWS Management Console. For detailed instructions, refer to the [Amazon EKS documentation](https://docs.aws.amazon.com/eks/latest/userguide/create-cluster.html). + +### Configure IAM Roles for Service Accounts (IRSA) + +IRSA enables you to associate an IAM role with a Kubernetes service account. This provides a secure way for your pods to access AWS services without hardcoding long-lived credentials. + +Run the following command to create the IAM role and bind it to a Kubernetes Service Account: + +```bash +eksctl create iamserviceaccount \ + --name your-k8s-service-account \ + --namespace default \ + --cluster your-cluster-name \ + --region your-aws-region \ + --role-name your-role-name \ + --attach-policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess \ + --approve +``` + +> **Note**: The `--attach-policy-arn` flag is used here to demonstrate attaching permissions. Update this with the specific AWS policy ARN your application requires. + +For a deep dive into how this works without using `eksctl`, refer to the [IAM Roles for Service Accounts](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html) documentation. + +### Configure Google Cloud to Trust the AWS Role + +To allow your AWS role to authenticate as a Google Cloud service account, you need to configure Workload Identity Federation. This process involves these key steps: + +1. **Create a Workload Identity Pool and an AWS Provider:** The pool holds the configuration, and the provider is set up to trust your AWS account. + +2. **Create or select a Google Cloud Service Account:** This service account will be impersonated by your AWS role. + +3. **Bind the AWS Role to the Google Cloud Service Account:** Create an IAM policy binding that gives your AWS role the `Workload Identity User` (`roles/iam.workloadIdentityUser`) role on the Google Cloud service account. + +For more detailed information, see the documentation on [Configuring Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds). + +**Alternative: Direct Access** + +> For supported resources, you can grant roles directly to the AWS identity, bypassing service account impersonation. To do this, grant a role (like `roles/storage.objectViewer`) to the workload identity principal (`principalSet://...`) directly on the resource's IAM policy. + +For more detailed information, see the documentation on [Configuring Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds). + +### Containerize and Package the Application + +Create a `Dockerfile` for the Python application and push the image to a container registry (for example Amazon ECR) that your EKS cluster can access. + +**Note:** The provided [`Dockerfile`](Dockerfile) is an example and may need to be modified for your specific needs. + +Build and push the image: +```bash +docker build -t your-container-image:latest . +docker push your-container-image:latest +``` + +### Deploy to EKS + +Create a Kubernetes deployment manifest to deploy your application to the EKS cluster. See the [`pod.yaml`](pod.yaml) file for an example. + +**Note:** The provided [`pod.yaml`](pod.yaml) is an example and may need to be modified for your specific needs. + +Deploy the pod: + +```bash +kubectl apply -f pod.yaml +``` + +### Clean Up + +To clean up the resources, delete the EKS cluster and any other AWS and Google Cloud resources you created. + +```bash +eksctl delete cluster --name your-cluster-name +``` + +## Testing + +This sample is not continuously tested. It is provided for instructional purposes and may require modifications to work in your environment. diff --git a/auth/custom-credentials/aws/custom-credentials-aws-secrets.json.example b/auth/custom-credentials/aws/custom-credentials-aws-secrets.json.example new file mode 100644 index 00000000000..300dc70c138 --- /dev/null +++ b/auth/custom-credentials/aws/custom-credentials-aws-secrets.json.example @@ -0,0 +1,8 @@ +{ + "aws_access_key_id": "YOUR_AWS_ACCESS_KEY_ID", + "aws_secret_access_key": "YOUR_AWS_SECRET_ACCESS_KEY", + "aws_region": "YOUR_AWS_REGION", + "gcp_workload_audience": "YOUR_GCP_WORKLOAD_AUDIENCE", + "gcs_bucket_name": "YOUR_GCS_BUCKET_NAME", + "gcp_service_account_impersonation_url": "YOUR_GCP_SERVICE_ACCOUNT_IMPERSONATION_URL" +} diff --git a/appengine/flexible_python37_and_earlier/hello_world_django/app.yaml b/auth/custom-credentials/aws/noxfile_config.py similarity index 79% rename from appengine/flexible_python37_and_earlier/hello_world_django/app.yaml rename to auth/custom-credentials/aws/noxfile_config.py index 62b74a9c27e..0ed973689f7 100644 --- a/appengine/flexible_python37_and_earlier/hello_world_django/app.yaml +++ b/auth/custom-credentials/aws/noxfile_config.py @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# 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. @@ -12,9 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT project_name.wsgi - -runtime_config: - python_version: 3 +TEST_CONFIG_OVERRIDE = { + "ignored_versions": ["2.7", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"], +} diff --git a/auth/custom-credentials/aws/pod.yaml b/auth/custom-credentials/aws/pod.yaml new file mode 100644 index 00000000000..70b94bf25e2 --- /dev/null +++ b/auth/custom-credentials/aws/pod.yaml @@ -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. + +apiVersion: v1 +kind: Pod +metadata: + name: custom-credential-pod +spec: + # The Kubernetes Service Account that is annotated with the corresponding + # AWS IAM role ARN. See the README for instructions on setting up IAM + # Roles for Service Accounts (IRSA). + serviceAccountName: your-k8s-service-account + containers: + - name: gcp-auth-sample + # The container image pushed to the container registry + # For example, Amazon Elastic Container Registry + image: your-container-image:latest + env: + # REQUIRED: The AWS region. Boto3 requires this to be set explicitly + # in containers. + - name: AWS_REGION + value: "your-aws-region" + # REQUIRED: The full identifier of the Workload Identity Pool provider + - name: GCP_WORKLOAD_AUDIENCE + value: "your-gcp-workload-audience" + # OPTIONAL: Enable Google Cloud service account impersonation + # - name: GCP_SERVICE_ACCOUNT_IMPERSONATION_URL + # value: "your-gcp-service-account-impersonation-url" + - name: GCS_BUCKET_NAME + value: "your-gcs-bucket-name" diff --git a/auth/custom-credentials/aws/requirements-test.txt b/auth/custom-credentials/aws/requirements-test.txt new file mode 100644 index 00000000000..43b24059d3e --- /dev/null +++ b/auth/custom-credentials/aws/requirements-test.txt @@ -0,0 +1,2 @@ +-r requirements.txt +pytest==8.2.0 diff --git a/auth/custom-credentials/aws/requirements.txt b/auth/custom-credentials/aws/requirements.txt new file mode 100644 index 00000000000..2c302888ed7 --- /dev/null +++ b/auth/custom-credentials/aws/requirements.txt @@ -0,0 +1,5 @@ +boto3==1.40.53 +google-auth==2.43.0 +google-cloud-storage==2.19.0 +python-dotenv==1.1.1 +requests==2.32.3 diff --git a/auth/custom-credentials/aws/snippets.py b/auth/custom-credentials/aws/snippets.py new file mode 100644 index 00000000000..2d77a123015 --- /dev/null +++ b/auth/custom-credentials/aws/snippets.py @@ -0,0 +1,153 @@ +# 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. + +# [START auth_custom_credential_supplier_aws] +import json +import os +import sys + +import boto3 +from google.auth import aws +from google.auth import exceptions +from google.cloud import storage + + +class CustomAwsSupplier(aws.AwsSecurityCredentialsSupplier): + """Custom AWS Security Credentials Supplier using Boto3.""" + + def __init__(self): + """Initializes the Boto3 session, prioritizing environment variables for region.""" + # Explicitly read the region from the environment first. + region = os.getenv("AWS_REGION") or os.getenv("AWS_DEFAULT_REGION") + + # If region is None, Boto3's discovery chain will be used when needed. + self.session = boto3.Session(region_name=region) + self._cached_region = None + + def get_aws_region(self, context, request) -> str: + """Returns the AWS region using Boto3's default provider chain.""" + if self._cached_region: + return self._cached_region + + self._cached_region = self.session.region_name + + if not self._cached_region: + raise exceptions.GoogleAuthError( + "Boto3 was unable to resolve an AWS region." + ) + + return self._cached_region + + def get_aws_security_credentials( + self, context, request=None + ) -> aws.AwsSecurityCredentials: + """Retrieves AWS security credentials using Boto3's default provider chain.""" + creds = self.session.get_credentials() + if not creds: + raise exceptions.GoogleAuthError( + "Unable to resolve AWS credentials from Boto3." + ) + + return aws.AwsSecurityCredentials( + access_key_id=creds.access_key, + secret_access_key=creds.secret_key, + session_token=creds.token, + ) + + +def authenticate_with_aws_credentials(bucket_name, audience, impersonation_url=None): + """Authenticates using the custom AWS supplier and gets bucket metadata. + + Returns: + dict: The bucket metadata response from the Google Cloud Storage API. + """ + + custom_supplier = CustomAwsSupplier() + + credentials = aws.Credentials( + audience=audience, + subject_token_type="urn:ietf:params:aws:token-type:aws4_request", + service_account_impersonation_url=impersonation_url, + aws_security_credentials_supplier=custom_supplier, + scopes=["https://www.googleapis.com/auth/devstorage.read_only"], + ) + + storage_client = storage.Client(credentials=credentials) + + bucket = storage_client.get_bucket(bucket_name) + + return bucket._properties + + +# [END auth_custom_credential_supplier_aws] + + +def _load_config_from_file(): + """ + If a local secrets file is present, load it into the environment. + This is a "just-in-time" configuration for local development. These + variables are only set for the current process and are not exposed to the + shell. + """ + secrets_file = "custom-credentials-aws-secrets.json" + if os.path.exists(secrets_file): + with open(secrets_file, "r") as f: + try: + secrets = json.load(f) + except json.JSONDecodeError: + print(f"Error: '{secrets_file}' is not valid JSON.", file=sys.stderr) + return + + os.environ["AWS_ACCESS_KEY_ID"] = secrets.get("aws_access_key_id", "") + os.environ["AWS_SECRET_ACCESS_KEY"] = secrets.get("aws_secret_access_key", "") + os.environ["AWS_REGION"] = secrets.get("aws_region", "") + os.environ["GCP_WORKLOAD_AUDIENCE"] = secrets.get("gcp_workload_audience", "") + os.environ["GCS_BUCKET_NAME"] = secrets.get("gcs_bucket_name", "") + os.environ["GCP_SERVICE_ACCOUNT_IMPERSONATION_URL"] = secrets.get( + "gcp_service_account_impersonation_url", "" + ) + + +def main(): + + # Reads the custom-credentials-aws-secrets.json if running locally. + _load_config_from_file() + + # Now, read the configuration from the environment. In a local run, these + # will be the values we just set. In a containerized run, they will be + # the values provided by the environment. + gcp_audience = os.getenv("GCP_WORKLOAD_AUDIENCE") + sa_impersonation_url = os.getenv("GCP_SERVICE_ACCOUNT_IMPERSONATION_URL") + gcs_bucket_name = os.getenv("GCS_BUCKET_NAME") + + if not all([gcp_audience, gcs_bucket_name]): + print( + "Required configuration missing. Please provide it in a " + "custom-credentials-aws-secrets.json file or as environment variables: " + "GCP_WORKLOAD_AUDIENCE, GCS_BUCKET_NAME" + ) + return + + try: + print(f"Retrieving metadata for bucket: {gcs_bucket_name}...") + metadata = authenticate_with_aws_credentials( + gcs_bucket_name, gcp_audience, sa_impersonation_url + ) + print("--- SUCCESS! ---") + print(json.dumps(metadata, indent=2)) + except Exception as e: + print(f"Authentication or Request failed: {e}") + + +if __name__ == "__main__": + main() diff --git a/auth/custom-credentials/aws/snippets_test.py b/auth/custom-credentials/aws/snippets_test.py new file mode 100644 index 00000000000..e0382cfc6f5 --- /dev/null +++ b/auth/custom-credentials/aws/snippets_test.py @@ -0,0 +1,130 @@ +# 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 json +import os +from unittest import mock + +import pytest + +import snippets + +# --- Unit Tests --- + + +@mock.patch.dict(os.environ, {"AWS_REGION": "us-west-2"}) +@mock.patch("boto3.Session") +def test_init_priority_env_var(mock_boto_session): + """Test that AWS_REGION env var takes priority during init.""" + snippets.CustomAwsSupplier() + mock_boto_session.assert_called_with(region_name="us-west-2") + + +@mock.patch.dict(os.environ, {}, clear=True) +@mock.patch("boto3.Session") +def test_get_aws_region_caching(mock_boto_session): + """Test that get_aws_region caches the result from Boto3.""" + mock_session_instance = mock_boto_session.return_value + mock_session_instance.region_name = "us-east-1" + + supplier = snippets.CustomAwsSupplier() + + # First call should hit the session + region = supplier.get_aws_region(None, None) + assert region == "us-east-1" + + # Change the mock to ensure we aren't calling it again + mock_session_instance.region_name = "us-west-2" + + # Second call should return the cached value + region2 = supplier.get_aws_region(None, None) + assert region2 == "us-east-1" + + +@mock.patch("boto3.Session") +def test_get_aws_security_credentials_success(mock_boto_session): + """Test successful retrieval of AWS credentials.""" + mock_session_instance = mock_boto_session.return_value + + mock_creds = mock.MagicMock() + mock_creds.access_key = "test-key" + mock_creds.secret_key = "test-secret" + mock_creds.token = "test-token" + mock_session_instance.get_credentials.return_value = mock_creds + + supplier = snippets.CustomAwsSupplier() + creds = supplier.get_aws_security_credentials(None) + + assert creds.access_key_id == "test-key" + assert creds.secret_access_key == "test-secret" + assert creds.session_token == "test-token" + + +@mock.patch("snippets.auth_requests.AuthorizedSession") +@mock.patch("snippets.aws.Credentials") +@mock.patch("snippets.CustomAwsSupplier") +def test_authenticate_unit_success(MockSupplier, MockAwsCreds, MockSession): + """Unit test for the main flow using mocks.""" + mock_response = mock.MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"name": "my-bucket"} + + mock_session_instance = MockSession.return_value + mock_session_instance.get.return_value = mock_response + + result = snippets.authenticate_with_aws_credentials( + bucket_name="my-bucket", + audience="//iam.googleapis.com/...", + impersonation_url=None, + ) + + assert result == {"name": "my-bucket"} + MockSupplier.assert_called_once() + MockAwsCreds.assert_called_once() + + +# --- System Test (Integration) --- + + +def test_authenticate_system(): + """ + System test that runs against the real API. + Skips automatically if custom-credentials-aws-secrets.json is missing or incomplete. + """ + if not os.path.exists("custom-credentials-aws-secrets.json"): + pytest.skip( + "Skipping system test: custom-credentials-aws-secrets.json not found." + ) + + with open("custom-credentials-aws-secrets.json", "r") as f: + secrets = json.load(f) + + required_keys = [ + "gcp_workload_audience", + "gcs_bucket_name", + "aws_access_key_id", + "aws_secret_access_key", + "aws_region", + ] + if not all(key in secrets and secrets[key] for key in required_keys): + pytest.skip( + "Skipping system test: custom-credentials-aws-secrets.json is missing or has empty required keys." + ) + + metadata = snippets.main() + + # Verify that the returned metadata is a dictionary with expected keys. + assert isinstance(metadata, dict) + assert "name" in metadata + assert metadata["name"] == secrets["gcs_bucket_name"] diff --git a/auth/custom-credentials/okta/README.md b/auth/custom-credentials/okta/README.md new file mode 100644 index 00000000000..96d444e85a4 --- /dev/null +++ b/auth/custom-credentials/okta/README.md @@ -0,0 +1,83 @@ +# Running the Custom Okta Credential Supplier Sample + +This sample demonstrates how to use a custom subject token supplier to authenticate with Google Cloud using Okta as an external identity provider. It uses the Client Credentials flow for machine-to-machine (M2M) authentication. + +## Prerequisites + +* An Okta developer account. +* A Google Cloud project with the IAM API enabled. +* A Google Cloud Storage bucket. Ensure that the authenticated user has access to this bucket. +* Python 3.10 or later installed. +* +## Okta Configuration + +Before running the sample, you need to configure an Okta application for Machine-to-Machine (M2M) communication. + +### Create an M2M Application in Okta + +1. Log in to your Okta developer console. +2. Navigate to **Applications** > **Applications** and click **Create App Integration**. +3. Select **API Services** as the sign-on method and click **Next**. +4. Give your application a name and click **Save**. + +### Obtain Okta Credentials + +Once the application is created, you will find the following information in the **General** tab: + +* **Okta Domain**: Your Okta developer domain (e.g., `https://dev-123456.okta.com`). +* **Client ID**: The client ID for your application. +* **Client Secret**: The client secret for your application. + +You will need these values to configure the sample. + +## Google Cloud Configuration + +You need to configure a Workload Identity Pool in Google Cloud to trust the Okta application. + +### Set up Workload Identity Federation + +1. In the Google Cloud Console, navigate to **IAM & Admin** > **Workload Identity Federation**. +2. Click **Create Pool** to create a new Workload Identity Pool. +3. Add a new **OIDC provider** to the pool. +4. Configure the provider with your Okta domain as the issuer URL. +5. Map the Okta `sub` (subject) assertion to a GCP principal. + +For detailed instructions, refer to the [Workload Identity Federation documentation](https://cloud.google.com/iam/docs/workload-identity-federation). + +## 3. Running the Script + +To run the sample on your local system, you need to install the dependencies and configure your credentials. + +### Install Dependencies + +```bash +pip install -r requirements.txt +``` + +### Configure Credentials + +1. Copy the example secrets file to a new file named `custom-credentials-okta-secrets.json`: + ```bash + cp custom-credentials-okta-secrets.json.example custom-credentials-okta-secrets.json + ``` +2. Open `custom-credentials-okta-secrets.json` and fill in the following values: + + * `okta_domain`: Your Okta developer domain (for example `https://dev-123456.okta.com`). + * `okta_client_id`: The client ID for your application. + * `okta_client_secret`: The client secret for your application. + * `gcp_workload_audience`: The audience for the Google Cloud Workload Identity Pool. This is the full identifier of the Workload Identity Pool provider. + * `gcs_bucket_name`: The name of the Google Cloud Storage bucket to access. + * `gcp_service_account_impersonation_url`: (Optional) The URL for service account impersonation. + + +### Run the Application + +```bash +python3 snippets.py +``` + +The script authenticates with Okta to get an OIDC token, exchanges that token for a Google Cloud federated token, and uses it to list metadata for the specified Google Cloud Storage bucket. + +## Testing + +This sample is not continuously tested. It is provided for instructional purposes and may require modifications to work in your environment. diff --git a/auth/custom-credentials/okta/custom-credentials-okta-secrets.json.example b/auth/custom-credentials/okta/custom-credentials-okta-secrets.json.example new file mode 100644 index 00000000000..fa04fda7cb2 --- /dev/null +++ b/auth/custom-credentials/okta/custom-credentials-okta-secrets.json.example @@ -0,0 +1,8 @@ +{ + "okta_domain": "https://your-okta-domain.okta.com", + "okta_client_id": "your-okta-client-id", + "okta_client_secret": "your-okta-client-secret", + "gcp_workload_audience": "//iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider", + "gcs_bucket_name": "your-gcs-bucket-name", + "gcp_service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/my-service-account@my-project.iam.gserviceaccount.com:generateAccessToken" +} diff --git a/appengine/flexible_python37_and_earlier/metadata/app.yaml b/auth/custom-credentials/okta/noxfile_config.py similarity index 79% rename from appengine/flexible_python37_and_earlier/metadata/app.yaml rename to auth/custom-credentials/okta/noxfile_config.py index ca76f83fc3b..0ed973689f7 100644 --- a/appengine/flexible_python37_and_earlier/metadata/app.yaml +++ b/auth/custom-credentials/okta/noxfile_config.py @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# 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. @@ -12,9 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - python_version: 3 +TEST_CONFIG_OVERRIDE = { + "ignored_versions": ["2.7", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"], +} diff --git a/auth/custom-credentials/okta/requirements-test.txt b/auth/custom-credentials/okta/requirements-test.txt new file mode 100644 index 00000000000..f47609d2651 --- /dev/null +++ b/auth/custom-credentials/okta/requirements-test.txt @@ -0,0 +1,2 @@ +-r requirements.txt +pytest==7.1.2 diff --git a/auth/custom-credentials/okta/requirements.txt b/auth/custom-credentials/okta/requirements.txt new file mode 100644 index 00000000000..d9669ebee9f --- /dev/null +++ b/auth/custom-credentials/okta/requirements.txt @@ -0,0 +1,4 @@ +requests==2.32.3 +google-cloud-storage==2.19.0 +google-auth==2.43.0 +python-dotenv==1.1.1 diff --git a/auth/custom-credentials/okta/snippets.py b/auth/custom-credentials/okta/snippets.py new file mode 100644 index 00000000000..02af2dadc93 --- /dev/null +++ b/auth/custom-credentials/okta/snippets.py @@ -0,0 +1,138 @@ +# 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. + +# [START auth_custom_credential_supplier_okta] +import json +import time +import urllib.parse + +from google.auth import identity_pool +from google.cloud import storage +import requests + + +class OktaClientCredentialsSupplier: + """A custom SubjectTokenSupplier that authenticates with Okta. + + This supplier uses the Client Credentials grant flow for machine-to-machine + (M2M) authentication with Okta. + """ + + def __init__(self, domain, client_id, client_secret): + self.okta_token_url = f"{domain.rstrip('/')}/oauth2/default/v1/token" + self.client_id = client_id + self.client_secret = client_secret + self.access_token = None + self.expiry_time = 0 + + def get_subject_token(self, context, request=None) -> str: + """Fetches a new token if the current one is expired or missing.""" + if self.access_token and time.time() < self.expiry_time - 60: + return self.access_token + self._fetch_okta_access_token() + return self.access_token + + def _fetch_okta_access_token(self): + """Performs the Client Credentials grant flow with Okta.""" + headers = { + "Content-Type": "application/x-www-form-urlencoded", + "Accept": "application/json", + } + data = { + "grant_type": "client_credentials", + "scope": "gcp.test.read", # Set scope as per Okta app config. + } + + response = requests.post( + self.okta_token_url, + headers=headers, + data=urllib.parse.urlencode(data), + auth=(self.client_id, self.client_secret), + ) + response.raise_for_status() + + token_data = response.json() + self.access_token = token_data["access_token"] + self.expiry_time = time.time() + token_data["expires_in"] + + +def authenticate_with_okta_credentials( + bucket_name, audience, domain, client_id, client_secret, impersonation_url=None +): + """Authenticates using the custom Okta supplier and gets bucket metadata. + + Returns: + dict: The bucket metadata response from the Google Cloud Storage API. + """ + + okta_supplier = OktaClientCredentialsSupplier(domain, client_id, client_secret) + + credentials = identity_pool.Credentials( + audience=audience, + subject_token_type="urn:ietf:params:oauth:token-type:jwt", + token_url="https://sts.googleapis.com/v1/token", + subject_token_supplier=okta_supplier, + default_scopes=["https://www.googleapis.com/auth/devstorage.read_only"], + service_account_impersonation_url=impersonation_url, + ) + + storage_client = storage.Client(credentials=credentials) + + bucket = storage_client.get_bucket(bucket_name) + + return bucket._properties + + +# [END auth_custom_credential_supplier_okta] + + +def main(): + try: + with open("custom-credentials-okta-secrets.json") as f: + secrets = json.load(f) + except FileNotFoundError: + print("Could not find custom-credentials-okta-secrets.json.") + return + + gcp_audience = secrets.get("gcp_workload_audience") + gcs_bucket_name = secrets.get("gcs_bucket_name") + sa_impersonation_url = secrets.get("gcp_service_account_impersonation_url") + + okta_domain = secrets.get("okta_domain") + okta_client_id = secrets.get("okta_client_id") + okta_client_secret = secrets.get("okta_client_secret") + + if not all( + [gcp_audience, gcs_bucket_name, okta_domain, okta_client_id, okta_client_secret] + ): + print("Missing required values in secrets.json.") + return + + try: + print(f"Retrieving metadata for bucket: {gcs_bucket_name}...") + metadata = authenticate_with_okta_credentials( + bucket_name=gcs_bucket_name, + audience=gcp_audience, + domain=okta_domain, + client_id=okta_client_id, + client_secret=okta_client_secret, + impersonation_url=sa_impersonation_url, + ) + print("--- SUCCESS! ---") + print(json.dumps(metadata, indent=2)) + except Exception as e: + print(f"Authentication or Request failed: {e}") + + +if __name__ == "__main__": + main() diff --git a/auth/custom-credentials/okta/snippets_test.py b/auth/custom-credentials/okta/snippets_test.py new file mode 100644 index 00000000000..1f05c4ad7bf --- /dev/null +++ b/auth/custom-credentials/okta/snippets_test.py @@ -0,0 +1,134 @@ +# 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 json +import os +import time +from unittest import mock +import urllib.parse + +import pytest + +import snippets + +# --- Unit Tests --- + + +def test_init_url_cleaning(): + """Test that the token URL strips trailing slashes.""" + s1 = snippets.OktaClientCredentialsSupplier("https://okta.com/", "id", "sec") + assert s1.okta_token_url == "https://okta.com/oauth2/default/v1/token" + + s2 = snippets.OktaClientCredentialsSupplier("https://okta.com", "id", "sec") + assert s2.okta_token_url == "https://okta.com/oauth2/default/v1/token" + + +@mock.patch("requests.post") +def test_get_subject_token_fetch(mock_post): + """Test fetching a new token from Okta.""" + supplier = snippets.OktaClientCredentialsSupplier("https://okta.com", "id", "sec") + + mock_response = mock.MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"access_token": "new-token", "expires_in": 3600} + mock_post.return_value = mock_response + + token = supplier.get_subject_token(None, None) + + assert token == "new-token" + mock_post.assert_called_once() + + # Verify args + _, kwargs = mock_post.call_args + assert kwargs["auth"] == ("id", "sec") + + sent_data = urllib.parse.parse_qs(kwargs["data"]) + assert sent_data["grant_type"][0] == "client_credentials" + + +@mock.patch("requests.post") +def test_get_subject_token_cached(mock_post): + """Test that cached token is returned if valid.""" + supplier = snippets.OktaClientCredentialsSupplier("https://okta.com", "id", "sec") + supplier.access_token = "cached-token" + supplier.expiry_time = time.time() + 3600 + + token = supplier.get_subject_token(None, None) + + assert token == "cached-token" + mock_post.assert_not_called() + + +@mock.patch("snippets.auth_requests.AuthorizedSession") +@mock.patch("snippets.identity_pool.Credentials") +@mock.patch("snippets.OktaClientCredentialsSupplier") +def test_authenticate_unit_success(MockSupplier, MockCreds, MockSession): + """Unit test for the main Okta auth flow.""" + mock_response = mock.MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"name": "test-bucket"} + + mock_session_instance = MockSession.return_value + mock_session_instance.get.return_value = mock_response + + metadata = snippets.authenticate_with_okta_credentials( + bucket_name="test-bucket", + audience="test-aud", + domain="https://okta.com", + client_id="id", + client_secret="sec", + impersonation_url=None, + ) + + assert metadata == {"name": "test-bucket"} + MockSupplier.assert_called_once() + MockCreds.assert_called_once() + + +# --- System Test --- + + +def test_authenticate_system(): + """ + System test that runs against the real API. + Skips automatically if custom-credentials-okta-secrets.json is missing or incomplete. + """ + if not os.path.exists("custom-credentials-okta-secrets.json"): + pytest.skip( + "Skipping system test: custom-credentials-okta-secrets.json not found." + ) + + with open("custom-credentials-okta-secrets.json", "r") as f: + secrets = json.load(f) + + required_keys = [ + "gcp_workload_audience", + "gcs_bucket_name", + "okta_domain", + "okta_client_id", + "okta_client_secret", + ] + if not all(key in secrets for key in required_keys): + pytest.skip( + "Skipping system test: custom-credentials-okta-secrets.json is missing required keys." + ) + + # The main() function handles the auth flow and printing. + # We mock the print function to verify the output. + with mock.patch("builtins.print") as mock_print: + snippets.main() + + # Check for the success message in the print output. + output = "\n".join([call.args[0] for call in mock_print.call_args_list]) + assert "--- SUCCESS! ---" in output diff --git a/bigquery-datatransfer/snippets/conftest.py b/bigquery-datatransfer/snippets/conftest.py index 1248a9407f7..30dd52f3ce6 100644 --- a/bigquery-datatransfer/snippets/conftest.py +++ b/bigquery-datatransfer/snippets/conftest.py @@ -123,7 +123,7 @@ def transfer_client(default_credentials, project_id): @pytest.fixture(scope="session") def transfer_config_name(transfer_client, project_id, dataset_id, service_account_name): - from . import manage_transfer_configs, scheduled_query + from . import scheduled_query # Use the transfer_client fixture so we know quota is attributed to the # correct project. @@ -140,9 +140,10 @@ def transfer_config_name(transfer_client, project_id, dataset_id, service_accoun } ) yield transfer_config.name - manage_transfer_configs.delete_config( - {"transfer_config_name": transfer_config.name} - ) + try: + transfer_client.delete_transfer_config(name=transfer_config.name) + except google.api_core.exceptions.NotFound: + pass @pytest.fixture diff --git a/bigquery-datatransfer/snippets/manage_transfer_configs.py b/bigquery-datatransfer/snippets/manage_transfer_configs.py index cd865455c10..726b4caf8f2 100644 --- a/bigquery-datatransfer/snippets/manage_transfer_configs.py +++ b/bigquery-datatransfer/snippets/manage_transfer_configs.py @@ -13,62 +13,6 @@ # limitations under the License. -def list_configs(override_values={}): - # [START bigquerydatatransfer_list_configs] - from google.cloud import bigquery_datatransfer - - transfer_client = bigquery_datatransfer.DataTransferServiceClient() - - project_id = "my-project" - # [END bigquerydatatransfer_list_configs] - # To facilitate testing, we replace values with alternatives - # provided by the testing harness. - project_id = override_values.get("project_id", project_id) - # [START bigquerydatatransfer_list_configs] - parent = transfer_client.common_project_path(project_id) - - configs = transfer_client.list_transfer_configs(parent=parent) - print("Got the following configs:") - for config in configs: - print(f"\tID: {config.name}, Schedule: {config.schedule}") - # [END bigquerydatatransfer_list_configs] - - -def update_config(override_values={}): - # [START bigquerydatatransfer_update_config] - from google.cloud import bigquery_datatransfer - from google.protobuf import field_mask_pb2 - - transfer_client = bigquery_datatransfer.DataTransferServiceClient() - - transfer_config_name = "projects/1234/locations/us/transferConfigs/abcd" - new_display_name = "My Transfer Config" - # [END bigquerydatatransfer_update_config] - # To facilitate testing, we replace values with alternatives - # provided by the testing harness. - new_display_name = override_values.get("new_display_name", new_display_name) - transfer_config_name = override_values.get( - "transfer_config_name", transfer_config_name - ) - # [START bigquerydatatransfer_update_config] - - transfer_config = bigquery_datatransfer.TransferConfig(name=transfer_config_name) - transfer_config.display_name = new_display_name - - transfer_config = transfer_client.update_transfer_config( - { - "transfer_config": transfer_config, - "update_mask": field_mask_pb2.FieldMask(paths=["display_name"]), - } - ) - - print(f"Updated config: '{transfer_config.name}'") - print(f"New display name: '{transfer_config.display_name}'") - # [END bigquerydatatransfer_update_config] - # Return the config name for testing purposes, so that it can be deleted. - return transfer_config - - def update_credentials_with_service_account(override_values={}): # [START bigquerydatatransfer_update_credentials] from google.cloud import bigquery_datatransfer @@ -159,27 +103,3 @@ def schedule_backfill_manual_transfer(override_values={}): print(f"backfill: {run.run_time} run: {run.name}") # [END bigquerydatatransfer_schedule_backfill] return response.runs - - -def delete_config(override_values={}): - # [START bigquerydatatransfer_delete_transfer] - import google.api_core.exceptions - from google.cloud import bigquery_datatransfer - - transfer_client = bigquery_datatransfer.DataTransferServiceClient() - - transfer_config_name = "projects/1234/locations/us/transferConfigs/abcd" - # [END bigquerydatatransfer_delete_transfer] - # To facilitate testing, we replace values with alternatives - # provided by the testing harness. - transfer_config_name = override_values.get( - "transfer_config_name", transfer_config_name - ) - # [START bigquerydatatransfer_delete_transfer] - try: - transfer_client.delete_transfer_config(name=transfer_config_name) - except google.api_core.exceptions.NotFound: - print("Transfer config not found.") - else: - print(f"Deleted transfer config: {transfer_config_name}") - # [END bigquerydatatransfer_delete_transfer] diff --git a/bigquery-datatransfer/snippets/manage_transfer_configs_test.py b/bigquery-datatransfer/snippets/manage_transfer_configs_test.py index 5504f19cbf9..505c61d269c 100644 --- a/bigquery-datatransfer/snippets/manage_transfer_configs_test.py +++ b/bigquery-datatransfer/snippets/manage_transfer_configs_test.py @@ -15,26 +15,6 @@ from . import manage_transfer_configs -def test_list_configs(capsys, project_id, transfer_config_name): - manage_transfer_configs.list_configs({"project_id": project_id}) - out, _ = capsys.readouterr() - assert "Got the following configs:" in out - assert transfer_config_name in out - - -def test_update_config(capsys, transfer_config_name): - manage_transfer_configs.update_config( - { - "new_display_name": "name from test_update_config", - "transfer_config_name": transfer_config_name, - } - ) - out, _ = capsys.readouterr() - assert "Updated config:" in out - assert transfer_config_name in out - assert "name from test_update_config" in out - - def test_update_credentials_with_service_account( capsys, project_id, service_account_name, transfer_config_name ): @@ -60,9 +40,3 @@ def test_schedule_backfill_manual_transfer(capsys, transfer_config_name): assert transfer_config_name in out # Check that there are three runs for between 2 and 5 days ago. assert len(runs) == 3 - - -def test_delete_config(capsys, transfer_config_name): - # transfer_config_name fixture in conftest.py calls the delete config - # sample. To conserve limited BQ-DTS quota we only make basic checks. - assert len(transfer_config_name) != 0 diff --git a/appengine/flexible_python37_and_earlier/tasks/app.yaml b/bigquery/python-db-dtypes-pandas/__init__.py similarity index 91% rename from appengine/flexible_python37_and_earlier/tasks/app.yaml rename to bigquery/python-db-dtypes-pandas/__init__.py index 15ac0d97205..7e1ec16ec8c 100644 --- a/appengine/flexible_python37_and_earlier/tasks/app.yaml +++ b/bigquery/python-db-dtypes-pandas/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019 Google LLC. +# Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,5 +11,3 @@ # 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. - -runtime: python37 diff --git a/appengine/flexible_python37_and_earlier/django_cloudsql/mysite/__init__.py b/bigquery/python-db-dtypes-pandas/pytest.ini similarity index 100% rename from appengine/flexible_python37_and_earlier/django_cloudsql/mysite/__init__.py rename to bigquery/python-db-dtypes-pandas/pytest.ini diff --git a/appengine/flexible_python37_and_earlier/django_cloudsql/polls/apps.py b/bigquery/python-db-dtypes-pandas/snippets/__init__.py similarity index 82% rename from appengine/flexible_python37_and_earlier/django_cloudsql/polls/apps.py rename to bigquery/python-db-dtypes-pandas/snippets/__init__.py index 88bdacda7c7..7e1ec16ec8c 100644 --- a/appengine/flexible_python37_and_earlier/django_cloudsql/polls/apps.py +++ b/bigquery/python-db-dtypes-pandas/snippets/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2015 Google LLC. +# Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,9 +11,3 @@ # 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 django.apps import AppConfig - - -class PollsConfig(AppConfig): - name = "polls" diff --git a/appengine/flexible_python37_and_earlier/storage/noxfile_config.py b/bigquery/python-db-dtypes-pandas/snippets/noxconfig.py similarity index 88% rename from appengine/flexible_python37_and_earlier/storage/noxfile_config.py rename to bigquery/python-db-dtypes-pandas/snippets/noxconfig.py index 6c2c81fa22b..b9d835eefee 100644 --- a/appengine/flexible_python37_and_earlier/storage/noxfile_config.py +++ b/bigquery/python-db-dtypes-pandas/snippets/noxconfig.py @@ -22,8 +22,7 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - # Skipping for Python 3.9 due to pyarrow compilation failure. - "ignored_versions": ["2.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"], + "ignored_versions": ["2.7", "3.7", "3.8"], # Old samples are opted out of enforcing Python type hints # All new samples should feature them "enforce_type_hints": True, @@ -39,5 +38,5 @@ "pip_version_override": None, # A dictionary you want to inject into your test. Don't put any # secrets here. These values will override predefined values. - "envs": {"CLOUD_STORAGE_BUCKET": "python-docs-samples-tests-public"}, + "envs": {}, } diff --git a/bigquery/python-db-dtypes-pandas/snippets/pandas_date_and_time.py b/bigquery/python-db-dtypes-pandas/snippets/pandas_date_and_time.py new file mode 100644 index 00000000000..b6e55813064 --- /dev/null +++ b/bigquery/python-db-dtypes-pandas/snippets/pandas_date_and_time.py @@ -0,0 +1,79 @@ +# Copyright 2021 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. + + +def pandas_date_and_time(): + # [START bigquery_pandas_date_create] + + import datetime + + import pandas as pd + + import db_dtypes # noqa import to register dtypes + + dates = pd.Series([datetime.date(2021, 9, 17), "2021-9-18"], dtype="dbdate") + + # [END bigquery_pandas_date_create] + # [START bigquery_pandas_date_as_datetime] + + datetimes = dates.astype("datetime64") + + # [END bigquery_pandas_date_as_datetime] + # [START bigquery_pandas_date_sub] + + dates2 = pd.Series(["2021-1-1", "2021-1-2"], dtype="dbdate") + diffs = dates - dates2 + + # [END bigquery_pandas_date_sub] + # [START bigquery_pandas_date_add_offset] + + do = pd.DateOffset(days=1) + after = dates + do + before = dates - do + + # [END bigquery_pandas_date_add_offset] + # [START bigquery_pandas_time_create] + + times = pd.Series([datetime.time(1, 2, 3, 456789), "12:00:00.6"], dtype="dbtime") + + # [END bigquery_pandas_time_create] + # [START bigquery_pandas_time_as_timedelta] + + timedeltas = times.astype("timedelta64") + + # [END bigquery_pandas_time_as_timedelta] + + # Combine datetime64 and timedelta64 to confirm adding dates and times are + # equivalent. + combined0 = datetimes + timedeltas + + # [START bigquery_pandas_combine_date_time] + + combined = dates + times + + # [END bigquery_pandas_combine_date_time] + + return ( + dates, + datetimes, + dates2, + diffs, + do, + after, + before, + times, + timedeltas, + combined, + combined0, + ) diff --git a/bigquery/python-db-dtypes-pandas/snippets/pandas_date_and_time_test.py b/bigquery/python-db-dtypes-pandas/snippets/pandas_date_and_time_test.py new file mode 100644 index 00000000000..56641765c30 --- /dev/null +++ b/bigquery/python-db-dtypes-pandas/snippets/pandas_date_and_time_test.py @@ -0,0 +1,60 @@ +# Copyright 2021 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 datetime + +import numpy as np +from pandas import Timestamp + + +def test_pandas_date_and_time(): + from .pandas_date_and_time import pandas_date_and_time + + ( + dates, + _, + dates2, + diffs, + do, + after, + before, + times, + _, + combined, + combined0, + ) = pandas_date_and_time() + + assert str(dates.dtype) == "dbdate" + assert list(dates) == [datetime.date(2021, 9, 17), datetime.date(2021, 9, 18)] + + assert np.array_equal( + diffs, + dates.astype("datetime64") - dates2.astype("datetime64"), + ) + + assert np.array_equal(after, dates.astype("object") + do) + assert np.array_equal(before, dates.astype("object") - do) + + assert str(times.dtype) == "dbtime" + assert list(times) == [ + datetime.time(1, 2, 3, 456789), + datetime.time(12, 0, 0, 600000), + ] + + for c in combined0, combined: + assert str(c.dtype) == "datetime64[ns]" + assert list(c) == [ + Timestamp("2021-09-17 01:02:03.456789"), + Timestamp("2021-09-18 12:00:00.600000"), + ] diff --git a/bigquery/python-db-dtypes-pandas/snippets/requirements-test.txt b/bigquery/python-db-dtypes-pandas/snippets/requirements-test.txt new file mode 100644 index 00000000000..9471b3d92fb --- /dev/null +++ b/bigquery/python-db-dtypes-pandas/snippets/requirements-test.txt @@ -0,0 +1 @@ +pytest==8.4.2 diff --git a/bigquery/python-db-dtypes-pandas/snippets/requirements.txt b/bigquery/python-db-dtypes-pandas/snippets/requirements.txt new file mode 100644 index 00000000000..5a18bf31224 --- /dev/null +++ b/bigquery/python-db-dtypes-pandas/snippets/requirements.txt @@ -0,0 +1,4 @@ +db-dtypes +numpy +pandas +pyarrow diff --git a/cloud-sql/mysql/sqlalchemy/requirements.txt b/cloud-sql/mysql/sqlalchemy/requirements.txt index 397f59c2759..a5e6f819085 100644 --- a/cloud-sql/mysql/sqlalchemy/requirements.txt +++ b/cloud-sql/mysql/sqlalchemy/requirements.txt @@ -2,6 +2,6 @@ Flask==2.2.2 SQLAlchemy==2.0.40 PyMySQL==1.1.1 gunicorn==23.0.0 -cloud-sql-python-connector==1.18.4 +cloud-sql-python-connector==1.20.0 functions-framework==3.9.2 Werkzeug==2.3.8 diff --git a/cloud-sql/postgres/sqlalchemy/requirements.txt b/cloud-sql/postgres/sqlalchemy/requirements.txt index d3a74b1c5ef..ba738cc1669 100644 --- a/cloud-sql/postgres/sqlalchemy/requirements.txt +++ b/cloud-sql/postgres/sqlalchemy/requirements.txt @@ -1,7 +1,7 @@ Flask==2.2.2 pg8000==1.31.5 SQLAlchemy==2.0.40 -cloud-sql-python-connector==1.18.4 +cloud-sql-python-connector==1.20.0 gunicorn==23.0.0 functions-framework==3.9.2 Werkzeug==2.3.8 diff --git a/cloud-sql/sql-server/sqlalchemy/requirements.txt b/cloud-sql/sql-server/sqlalchemy/requirements.txt index 3302326ab42..a2aae8784d1 100644 --- a/cloud-sql/sql-server/sqlalchemy/requirements.txt +++ b/cloud-sql/sql-server/sqlalchemy/requirements.txt @@ -3,7 +3,7 @@ gunicorn==23.0.0 python-tds==1.16.0 pyopenssl==25.0.0 SQLAlchemy==2.0.40 -cloud-sql-python-connector==1.18.4 +cloud-sql-python-connector==1.20.0 sqlalchemy-pytds==1.0.2 functions-framework==3.9.2 Werkzeug==2.3.8 diff --git a/cloud_scheduler/snippets/requirements.txt b/cloud_scheduler/snippets/requirements.txt index e95a2ef8c50..af65635c6c9 100644 --- a/cloud_scheduler/snippets/requirements.txt +++ b/cloud_scheduler/snippets/requirements.txt @@ -1,4 +1,4 @@ Flask==3.0.3 gunicorn==23.0.0 google-cloud-scheduler==2.14.1 -Werkzeug==3.0.6 +Werkzeug==3.1.5 diff --git a/composer/2022_airflow_summit/data_analytics_process_expansion_test.py b/composer/2022_airflow_summit/data_analytics_process_expansion_test.py index 466a546391d..ffd4b46b7c5 100644 --- a/composer/2022_airflow_summit/data_analytics_process_expansion_test.py +++ b/composer/2022_airflow_summit/data_analytics_process_expansion_test.py @@ -214,7 +214,7 @@ def bq_dataset(test_bucket): print(f"Ignoring NotFound on cleanup, details: {e}") -@backoff.on_exception(backoff.expo, AssertionError, max_tries=3) +@backoff.on_exception(backoff.expo, AssertionError, max_tries=5) def test_process(test_dataproc_batch): print(test_dataproc_batch) diff --git a/composer/airflow_1_samples/gke_operator.py b/composer/airflow_1_samples/gke_operator.py index b3638655b20..082d3333f9a 100644 --- a/composer/airflow_1_samples/gke_operator.py +++ b/composer/airflow_1_samples/gke_operator.py @@ -92,7 +92,7 @@ # project-id as the gcr.io images and the service account that Composer # uses has permission to access the Google Container Registry # (the default service account has permission) - image="gcr.io/gcp-runtimes/ubuntu_18_0_4", + image="marketplace.gcr.io/google/ubuntu2204", ) # [END composer_gkeoperator_minconfig_airflow_1] diff --git a/composer/airflow_1_samples/kubernetes_pod_operator.py b/composer/airflow_1_samples/kubernetes_pod_operator.py index 11abdb6b1ec..2799f467ec9 100644 --- a/composer/airflow_1_samples/kubernetes_pod_operator.py +++ b/composer/airflow_1_samples/kubernetes_pod_operator.py @@ -93,7 +93,7 @@ # project-id as the gcr.io images and the service account that Composer # uses has permission to access the Google Container Registry # (the default service account has permission) - image="gcr.io/gcp-runtimes/ubuntu_18_0_4", + image="marketplace.gcr.io/google/ubuntu2204", ) # [END composer_kubernetespodoperator_minconfig_airflow_1] # [START composer_kubernetespodoperator_templateconfig_airflow_1] diff --git a/composer/airflow_1_samples/noxfile_config.py b/composer/airflow_1_samples/noxfile_config.py index 7185f415100..21ea6aca21a 100644 --- a/composer/airflow_1_samples/noxfile_config.py +++ b/composer/airflow_1_samples/noxfile_config.py @@ -32,7 +32,7 @@ # You can opt out from the test for specific Python versions. # Skipping for Python 3.9 due to numpy compilation failure. # Skipping 3.6 and 3.7, they are more out of date - "ignored_versions": ["2.7", "3.6", "3.7", "3.9", "3.10", "3.11", "3.12", "3.13"], + "ignored_versions": ["2.7", "3.6", "3.7", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"], # Old samples are opted out of enforcing Python type hints # All new samples should feature them "enforce_type_hints": False, diff --git a/composer/tools/composer_migrate.py b/composer/tools/composer_migrate.py index ecbbb97dae8..c4ef2fbb5f9 100644 --- a/composer/tools/composer_migrate.py +++ b/composer/tools/composer_migrate.py @@ -108,7 +108,7 @@ def unpause_dag( dag_id: str, environment_name: str, ) -> Any: - """Unpauses all DAGs in a Composer environment.""" + """Unpauses a DAG in a Composer environment.""" command = ( f"CLOUDSDK_API_ENDPOINT_OVERRIDES_COMPOSER={self.sdk_endpoint} gcloud" " composer environments run" @@ -363,7 +363,7 @@ def main( pprint.pformat(target_environment), ) logger.warning( - "Composer 3 environnment workloads config may be different from the" + "Composer 3 environment workloads config may be different from the" " source environment." ) logger.warning( @@ -413,7 +413,7 @@ def main( client.load_snapshot(target_environment_name, snapshot_path) logger.info("Snapshot loaded.") - # 6. Unpase DAGs in the new environment + # 6. Unpause DAGs in the new environment logger.info("STEP 6: Unpausing DAGs in the new environment...") all_dags_present = False # Wait until all DAGs from source environment are visible. diff --git a/composer/workflows/gke_operator.py b/composer/workflows/gke_operator.py index 2f1eaa62c8a..31536ba55e7 100644 --- a/composer/workflows/gke_operator.py +++ b/composer/workflows/gke_operator.py @@ -29,7 +29,7 @@ with models.DAG( "example_gcp_gke", - schedule_interval=None, # Override to match your needs + schedule=None, # Override to match your needs start_date=days_ago(1), tags=["example"], ) as dag: @@ -86,7 +86,7 @@ # project-id as the gcr.io images and the service account that Composer # uses has permission to access the Google Container Registry # (the default service account has permission) - image="gcr.io/gcp-runtimes/ubuntu_18_0_4", + image="marketplace.gcr.io/google/ubuntu2204", ) # [END composer_gkeoperator_minconfig] diff --git a/composer/workflows/kubernetes_pod_operator.py b/composer/workflows/kubernetes_pod_operator.py index f679ead81d7..26dcb9d5173 100644 --- a/composer/workflows/kubernetes_pod_operator.py +++ b/composer/workflows/kubernetes_pod_operator.py @@ -96,7 +96,7 @@ # project-id as the gcr.io images and the service account that Composer # uses has permission to access the Google Container Registry # (the default service account has permission) - image="gcr.io/gcp-runtimes/ubuntu_18_0_4", + image="marketplace.gcr.io/google/ubuntu2204", ) # [END composer_kubernetespodoperator_minconfig] # [START composer_kubernetespodoperator_templateconfig] diff --git a/composer/workflows/kubernetes_pod_operator_c2.py b/composer/workflows/kubernetes_pod_operator_c2.py index 65e43289695..0a227058d77 100644 --- a/composer/workflows/kubernetes_pod_operator_c2.py +++ b/composer/workflows/kubernetes_pod_operator_c2.py @@ -17,10 +17,11 @@ import datetime from airflow import models -from airflow.kubernetes.secret import Secret + from airflow.providers.cncf.kubernetes.operators.pod import ( KubernetesPodOperator, ) +from airflow.providers.cncf.kubernetes.secret import Secret from kubernetes.client import models as k8s_models # A Secret is an object that contains a small amount of sensitive data such as @@ -60,7 +61,7 @@ # required to debug. with models.DAG( dag_id="composer_sample_kubernetes_pod", - schedule_interval=datetime.timedelta(days=1), + schedule=datetime.timedelta(days=1), start_date=YESTERDAY, ) as dag: # Only name, image, and task_id are required to create a @@ -88,7 +89,7 @@ # project-id as the gcr.io images and the service account that Composer # uses has permission to access the Google Container Registry # (the default service account has permission) - image="gcr.io/gcp-runtimes/ubuntu_20_0_4", + image="marketplace.gcr.io/google/ubuntu2204", # Specifies path to kubernetes config. The config_file is templated. config_file="/home/airflow/composer_kube_config", # Identifier of connection that should be used @@ -130,7 +131,7 @@ task_id="ex-kube-secrets", name="ex-kube-secrets", namespace="composer-user-workloads", - image="gcr.io/gcp-runtimes/ubuntu_20_0_4", + image="marketplace.gcr.io/google/ubuntu2204", startup_timeout_seconds=300, # The secrets to pass to Pod, the Pod will fail to create if the # secrets you specify in a Secret object do not exist in Kubernetes. diff --git a/composer/workflows/noxfile_config.py b/composer/workflows/noxfile_config.py index 7eeb5bb5817..1dbb9beffd2 100644 --- a/composer/workflows/noxfile_config.py +++ b/composer/workflows/noxfile_config.py @@ -39,6 +39,7 @@ "3.10", "3.12", "3.13", + "3.14", ], # Old samples are opted out of enforcing Python type hints # All new samples should feature them diff --git a/dataflow/flex-templates/pipeline_with_dependencies/requirements.txt b/dataflow/flex-templates/pipeline_with_dependencies/requirements.txt index eeed8f6f3ce..bef166bb943 100644 --- a/dataflow/flex-templates/pipeline_with_dependencies/requirements.txt +++ b/dataflow/flex-templates/pipeline_with_dependencies/requirements.txt @@ -305,7 +305,7 @@ typing-extensions==4.10.0 # via apache-beam tzlocal==5.2 # via js2py -urllib3==2.5.0 +urllib3==2.6.0 # via requests wrapt==1.16.0 # via deprecated diff --git a/datastore/samples/snippets/requirements-test.txt b/datastore/samples/snippets/requirements-test.txt new file mode 100644 index 00000000000..2a21e952015 --- /dev/null +++ b/datastore/samples/snippets/requirements-test.txt @@ -0,0 +1,7 @@ +backoff===1.11.1; python_version < "3.7" +backoff==2.2.1; python_version >= "3.7" +pytest===7.4.3; python_version == '3.7' +pytest===8.3.5; python_version == '3.8' +pytest===8.4.2; python_version == '3.9' +pytest==9.0.2; python_version >= '3.10' +flaky==3.8.1 diff --git a/datastore/samples/snippets/requirements.txt b/datastore/samples/snippets/requirements.txt new file mode 100644 index 00000000000..7852f23b24e --- /dev/null +++ b/datastore/samples/snippets/requirements.txt @@ -0,0 +1 @@ +google-cloud-datastore==2.23.0 \ No newline at end of file diff --git a/datastore/samples/snippets/schedule-export/README.md b/datastore/samples/snippets/schedule-export/README.md new file mode 100644 index 00000000000..a8501cddc34 --- /dev/null +++ b/datastore/samples/snippets/schedule-export/README.md @@ -0,0 +1,5 @@ +# Scheduling Datastore exports with Cloud Functions and Cloud Scheduler + +This sample application demonstrates how to schedule exports of your Datastore entities. To deploy this sample, see: + +[Scheduling exports](https://cloud.google.com/datastore/docs/schedule-export) diff --git a/datastore/samples/snippets/schedule-export/main.py b/datastore/samples/snippets/schedule-export/main.py new file mode 100644 index 00000000000..f91b1466913 --- /dev/null +++ b/datastore/samples/snippets/schedule-export/main.py @@ -0,0 +1,57 @@ +# Copyright 2021 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 base64 +import json +import os + +from google.cloud import datastore_admin_v1 + +project_id = os.environ.get("GCP_PROJECT") +client = datastore_admin_v1.DatastoreAdminClient() + + +def datastore_export(event, context): + """Triggers a Datastore export from a Cloud Scheduler job. + + Args: + event (dict): event[data] must contain a json object encoded in + base-64. Cloud Scheduler encodes payloads in base-64 by default. + Object must include a 'bucket' value and can include 'kinds' + and 'namespaceIds' values. + context (google.cloud.functions.Context): The Cloud Functions event + metadata. + """ + if "data" in event: + # Triggered via Cloud Scheduler, decode the inner data field of the json payload. + json_data = json.loads(base64.b64decode(event["data"]).decode("utf-8")) + else: + # Otherwise, for instance if triggered via the Cloud Console on a Cloud Function, the event is the data. + json_data = event + + bucket = json_data["bucket"] + entity_filter = datastore_admin_v1.EntityFilter() + + if "kinds" in json_data: + entity_filter.kinds = json_data["kinds"] + + if "namespaceIds" in json_data: + entity_filter.namespace_ids = json_data["namespaceIds"] + + export_request = datastore_admin_v1.ExportEntitiesRequest( + project_id=project_id, output_url_prefix=bucket, entity_filter=entity_filter + ) + operation = client.export_entities(request=export_request) + response = operation.result() + print(response) diff --git a/datastore/samples/snippets/schedule-export/requirements-test.txt b/datastore/samples/snippets/schedule-export/requirements-test.txt new file mode 100644 index 00000000000..cb982446b31 --- /dev/null +++ b/datastore/samples/snippets/schedule-export/requirements-test.txt @@ -0,0 +1,2 @@ +pytest===8.4.2; python_version == '3.9' +pytest==9.0.2; python_version >= '3.10' diff --git a/datastore/samples/snippets/schedule-export/requirements.txt b/datastore/samples/snippets/schedule-export/requirements.txt new file mode 100644 index 00000000000..fa16c1e95ab --- /dev/null +++ b/datastore/samples/snippets/schedule-export/requirements.txt @@ -0,0 +1 @@ +google-cloud-datastore==2.23.0 diff --git a/datastore/samples/snippets/schedule-export/schedule_export_test.py b/datastore/samples/snippets/schedule-export/schedule_export_test.py new file mode 100644 index 00000000000..48d9147c923 --- /dev/null +++ b/datastore/samples/snippets/schedule-export/schedule_export_test.py @@ -0,0 +1,73 @@ +# Copyright 2019 Google LLC All Rights Reserved. +# +# 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 base64 +from unittest.mock import Mock + +import main + +mock_context = Mock() +mock_context.event_id = "617187464135194" +mock_context.timestamp = "2020-04-15T22:09:03.761Z" + + +def test_datastore_export(capsys): + # Test an export without an entity filter + bucket = "gs://my-bucket" + json_string = '{{ "bucket": "{bucket}" }}'.format(bucket=bucket) + + # Encode data like Cloud Scheduler + data = bytes(json_string, "utf-8") + data_encoded = base64.b64encode(data) + event = {"data": data_encoded} + + # Mock the Datastore service + mockDatastore = Mock() + main.client = mockDatastore + + # Call tested function + main.datastore_export(event, mock_context) + out, err = capsys.readouterr() + export_args = mockDatastore.export_entities.call_args[1] + # Assert request includes test values + assert export_args["request"].output_url_prefix == bucket + + +def test_datastore_export_entity_filter(capsys): + # Test an export with an entity filter + bucket = "gs://my-bucket" + kinds = "Users,Tasks" + namespaceIds = "Customer831,Customer157" + json_string = '{{ "bucket": "{bucket}", "kinds": "{kinds}", "namespaceIds": "{namespaceIds}" }}'.format( + bucket=bucket, kinds=kinds, namespaceIds=namespaceIds + ) + + # Encode data like Cloud Scheduler + data = bytes(json_string, "utf-8") + data_encoded = base64.b64encode(data) + event = {"data": data_encoded} + + # Mock the Datastore service + mockDatastore = Mock() + main.client = mockDatastore + + # Call tested function + main.datastore_export(event, mock_context) + out, err = capsys.readouterr() + export_args = mockDatastore.export_entities.call_args[1] + # Assert request includes test values + + assert export_args["request"].output_url_prefix == bucket + assert export_args["request"].entity_filter.kinds == kinds + assert export_args["request"].entity_filter.namespace_ids == namespaceIds diff --git a/datastore/samples/snippets/snippets.py b/datastore/samples/snippets/snippets.py new file mode 100644 index 00000000000..1b86ba8b0cd --- /dev/null +++ b/datastore/samples/snippets/snippets.py @@ -0,0 +1,513 @@ +# Copyright 2022 Google, Inc. +# 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 argparse +from datetime import datetime, timedelta, timezone +from pprint import pprint +import time + +from google.cloud import datastore # noqa: I100 + + +def _preamble(): + # [START datastore_size_coloration_query] + from google.cloud import datastore + + # For help authenticating your client, visit + # https://cloud.google.com/docs/authentication/getting-started + client = datastore.Client() + + # [END datastore_size_coloration_query] + assert client is not None + + +def in_query(client): + # [START datastore_in_query] + query = client.query(kind="Task") + query.add_filter("tag", "IN", ["learn", "study"]) + # [END datastore_in_query] + + return list(query.fetch()) + + +def not_equals_query(client): + # [START datastore_not_equals_query] + query = client.query(kind="Task") + query.add_filter("category", "!=", "work") + # [END datastore_not_equals_query] + + return list(query.fetch()) + + +def not_in_query(client): + # [START datastore_not_in_query] + query = client.query(kind="Task") + query.add_filter("category", "NOT_IN", ["work", "chores", "school"]) + # [END datastore_not_in_query] + + return list(query.fetch()) + + +def query_with_readtime(client): + # [START datastore_stale_read] + # Create a read time of 15 seconds in the past + read_time = datetime.now(timezone.utc) - timedelta(seconds=15) + + # Fetch an entity with read_time + task_key = client.key("Task", "sampletask") + entity = client.get(task_key, read_time=read_time) + + # Query Task entities with read_time + query = client.query(kind="Task") + tasks = query.fetch(read_time=read_time, limit=10) + # [END datastore_stale_read] + + results = list(tasks) + results.append(entity) + + return results + + +def count_query_in_transaction(client): + # [START datastore_count_in_transaction] + task1 = datastore.Entity(client.key("Task", "task1")) + task2 = datastore.Entity(client.key("Task", "task2")) + + task1["owner"] = "john" + task2["owner"] = "john" + + tasks = [task1, task2] + client.put_multi(tasks) + + with client.transaction() as transaction: + + tasks_of_john = client.query(kind="Task") + tasks_of_john.add_filter("owner", "=", "john") + total_tasks_query = client.aggregation_query(tasks_of_john) + + query_result = total_tasks_query.count(alias="tasks_count").fetch() + for task_result in query_result: + tasks_count = task_result[0] + if tasks_count.value < 2: + task3 = datastore.Entity(client.key("Task", "task3")) + task3["owner"] = "john" + transaction.put(task3) + tasks.append(task3) + else: + print(f"Found existing {tasks_count.value} tasks, rolling back") + client.entities_to_delete.extend(tasks) + raise ValueError("User 'John' cannot have more than 2 tasks") + # [END datastore_count_in_transaction] + + +def count_query_on_kind(client): + # [START datastore_count_on_kind] + task1 = datastore.Entity(client.key("Task", "task1")) + task2 = datastore.Entity(client.key("Task", "task2")) + + tasks = [task1, task2] + client.put_multi(tasks) + all_tasks_query = client.query(kind="Task") + all_tasks_count_query = client.aggregation_query(all_tasks_query).count() + query_result = all_tasks_count_query.fetch() + for aggregation_results in query_result: + for aggregation in aggregation_results: + print(f"Total tasks (accessible from default alias) is {aggregation.value}") + # [END datastore_count_on_kind] + return tasks + + +def count_query_with_limit(client): + # [START datastore_count_with_limit] + task1 = datastore.Entity(client.key("Task", "task1")) + task2 = datastore.Entity(client.key("Task", "task2")) + task3 = datastore.Entity(client.key("Task", "task3")) + + tasks = [task1, task2, task3] + client.put_multi(tasks) + all_tasks_query = client.query(kind="Task") + all_tasks_count_query = client.aggregation_query(all_tasks_query).count() + query_result = all_tasks_count_query.fetch(limit=2) + for aggregation_results in query_result: + for aggregation in aggregation_results: + print(f"We have at least {aggregation.value} tasks") + # [END datastore_count_with_limit] + return tasks + + +def count_query_property_filter(client): + # [START datastore_count_with_property_filter] + task1 = datastore.Entity(client.key("Task", "task1")) + task2 = datastore.Entity(client.key("Task", "task2")) + task3 = datastore.Entity(client.key("Task", "task3")) + + task1["done"] = True + task2["done"] = False + task3["done"] = True + + tasks = [task1, task2, task3] + client.put_multi(tasks) + completed_tasks = client.query(kind="Task").add_filter("done", "=", True) + remaining_tasks = client.query(kind="Task").add_filter("done", "=", False) + + completed_tasks_query = client.aggregation_query(query=completed_tasks).count( + alias="total_completed_count" + ) + remaining_tasks_query = client.aggregation_query(query=remaining_tasks).count( + alias="total_remaining_count" + ) + + completed_query_result = completed_tasks_query.fetch() + for aggregation_results in completed_query_result: + for aggregation_result in aggregation_results: + if aggregation_result.alias == "total_completed_count": + print(f"Total completed tasks count is {aggregation_result.value}") + + remaining_query_result = remaining_tasks_query.fetch() + for aggregation_results in remaining_query_result: + for aggregation_result in aggregation_results: + if aggregation_result.alias == "total_remaining_count": + print(f"Total remaining tasks count is {aggregation_result.value}") + # [END datastore_count_with_property_filter] + return tasks + + +def count_query_with_stale_read(client): + + tasks = [task for task in client.query(kind="Task").fetch()] + client.delete_multi(tasks) # ensure the database is empty before starting + + # [START datastore_count_query_with_stale_read] + task1 = datastore.Entity(client.key("Task", "task1")) + task2 = datastore.Entity(client.key("Task", "task2")) + + # Saving two tasks + task1["done"] = True + task2["done"] = False + client.put_multi([task1, task2]) + time.sleep(10) + + past_timestamp = datetime.now( + timezone.utc + ) # we have two tasks in database at this time. + time.sleep(10) + + # Saving third task + task3 = datastore.Entity(client.key("Task", "task3")) + task3["done"] = False + client.put(task3) + + all_tasks = client.query(kind="Task") + all_tasks_count = client.aggregation_query( + query=all_tasks, + ).count(alias="all_tasks_count") + + # Executing aggregation query + query_result = all_tasks_count.fetch() + for aggregation_results in query_result: + for aggregation_result in aggregation_results: + print(f"Latest tasks count is {aggregation_result.value}") + + # Executing aggregation query with past timestamp + tasks_in_past = client.aggregation_query(query=all_tasks).count( + alias="tasks_in_past" + ) + tasks_in_the_past_query_result = tasks_in_past.fetch(read_time=past_timestamp) + for aggregation_results in tasks_in_the_past_query_result: + for aggregation_result in aggregation_results: + print(f"Stale tasks count is {aggregation_result.value}") + # [END datastore_count_query_with_stale_read] + return [task1, task2, task3] + + +def sum_query_on_kind(client): + # [START datastore_sum_aggregation_query_on_kind] + # Set up sample entities + # Use incomplete key to auto-generate ID + task1 = datastore.Entity(client.key("Task")) + task2 = datastore.Entity(client.key("Task")) + task3 = datastore.Entity(client.key("Task")) + + task1["hours"] = 5 + task2["hours"] = 3 + task3["hours"] = 1 + + tasks = [task1, task2, task3] + client.put_multi(tasks) + + # Execute sum aggregation query + all_tasks_query = client.query(kind="Task") + all_tasks_sum_query = client.aggregation_query(all_tasks_query).sum("hours") + query_result = all_tasks_sum_query.fetch() + for aggregation_results in query_result: + for aggregation in aggregation_results: + print(f"Total sum of hours in tasks is {aggregation.value}") + # [END datastore_sum_aggregation_query_on_kind] + return tasks + + +def sum_query_property_filter(client): + # [START datastore_sum_aggregation_query_with_filters] + # Set up sample entities + # Use incomplete key to auto-generate ID + task1 = datastore.Entity(client.key("Task")) + task2 = datastore.Entity(client.key("Task")) + task3 = datastore.Entity(client.key("Task")) + + task1["hours"] = 5 + task2["hours"] = 3 + task3["hours"] = 1 + + task1["done"] = True + task2["done"] = True + task3["done"] = False + + tasks = [task1, task2, task3] + client.put_multi(tasks) + + # Execute sum aggregation query with filters + completed_tasks = client.query(kind="Task").add_filter("done", "=", True) + completed_tasks_query = client.aggregation_query(query=completed_tasks).sum( + property_ref="hours", alias="total_completed_sum_hours" + ) + + completed_query_result = completed_tasks_query.fetch() + for aggregation_results in completed_query_result: + for aggregation_result in aggregation_results: + if aggregation_result.alias == "total_completed_sum_hours": + print( + f"Total sum of hours in completed tasks is {aggregation_result.value}" + ) + # [END datastore_sum_aggregation_query_with_filters] + return tasks + + +def avg_query_on_kind(client): + # [START datastore_avg_aggregation_query_on_kind] + # Set up sample entities + # Use incomplete key to auto-generate ID + task1 = datastore.Entity(client.key("Task")) + task2 = datastore.Entity(client.key("Task")) + task3 = datastore.Entity(client.key("Task")) + + task1["hours"] = 5 + task2["hours"] = 3 + task3["hours"] = 1 + + tasks = [task1, task2, task3] + client.put_multi(tasks) + + # Execute average aggregation query + all_tasks_query = client.query(kind="Task") + all_tasks_avg_query = client.aggregation_query(all_tasks_query).avg("hours") + query_result = all_tasks_avg_query.fetch() + for aggregation_results in query_result: + for aggregation in aggregation_results: + print(f"Total average of hours in tasks is {aggregation.value}") + # [END datastore_avg_aggregation_query_on_kind] + return tasks + + +def avg_query_property_filter(client): + # [START datastore_avg_aggregation_query_with_filters] + # Set up sample entities + # Use incomplete key to auto-generate ID + task1 = datastore.Entity(client.key("Task")) + task2 = datastore.Entity(client.key("Task")) + task3 = datastore.Entity(client.key("Task")) + + task1["hours"] = 5 + task2["hours"] = 3 + task3["hours"] = 1 + + task1["done"] = True + task2["done"] = True + task3["done"] = False + + tasks = [task1, task2, task3] + client.put_multi(tasks) + + # Execute average aggregation query with filters + completed_tasks = client.query(kind="Task").add_filter("done", "=", True) + completed_tasks_query = client.aggregation_query(query=completed_tasks).avg( + property_ref="hours", alias="total_completed_avg_hours" + ) + + completed_query_result = completed_tasks_query.fetch() + for aggregation_results in completed_query_result: + for aggregation_result in aggregation_results: + if aggregation_result.alias == "total_completed_avg_hours": + print( + f"Total average of hours in completed tasks is {aggregation_result.value}" + ) + # [END datastore_avg_aggregation_query_with_filters] + return tasks + + +def multiple_aggregations_query(client): + # [START datastore_multiple_aggregation_in_structured_query] + # Set up sample entities + # Use incomplete key to auto-generate ID + task1 = datastore.Entity(client.key("Task")) + task2 = datastore.Entity(client.key("Task")) + task3 = datastore.Entity(client.key("Task")) + + task1["hours"] = 5 + task2["hours"] = 3 + task3["hours"] = 1 + + tasks = [task1, task2, task3] + client.put_multi(tasks) + + # Execute query with multiple aggregations + all_tasks_query = client.query(kind="Task") + aggregation_query = client.aggregation_query(all_tasks_query) + # Add aggregations + aggregation_query.add_aggregations( + [ + datastore.aggregation.CountAggregation(alias="count_aggregation"), + datastore.aggregation.SumAggregation( + property_ref="hours", alias="sum_aggregation" + ), + datastore.aggregation.AvgAggregation( + property_ref="hours", alias="avg_aggregation" + ), + ] + ) + + query_result = aggregation_query.fetch() + for aggregation_results in query_result: + for aggregation in aggregation_results: + print(f"{aggregation.alias} value is {aggregation.value}") + # [END datastore_multiple_aggregation_in_structured_query] + return tasks + + +def explain_analyze_entity(client): + # [START datastore_query_explain_analyze_entity] + # Build the query with explain_options + # analzye = true to get back the query stats, plan info, and query results + query = client.query( + kind="Task", explain_options=datastore.ExplainOptions(analyze=True) + ) + + # initiate the query + iterator = query.fetch() + + # explain_metrics is only available after query is completed + for task_result in iterator: + print(task_result) + + # get the plan summary + plan_summary = iterator.explain_metrics.plan_summary + print(f"Indexes used: {plan_summary.indexes_used}") + + # get the execution stats + execution_stats = iterator.explain_metrics.execution_stats + print(f"Results returned: {execution_stats.results_returned}") + print(f"Execution duration: {execution_stats.execution_duration}") + print(f"Read operations: {execution_stats.read_operations}") + print(f"Debug stats: {execution_stats.debug_stats}") + # [END datastore_query_explain_analyze_entity] + + +def explain_entity(client): + # [START datastore_query_explain_entity] + # Build the query with explain_options + # by default (analyze = false), only plan_summary property is available + query = client.query(kind="Task", explain_options=datastore.ExplainOptions()) + + # initiate the query + iterator = query.fetch() + + # get the plan summary + plan_summary = iterator.explain_metrics.plan_summary + print(f"Indexes used: {plan_summary.indexes_used}") + # [END datastore_query_explain_entity] + + +def explain_analyze_aggregation(client): + # [START datastore_query_explain_analyze_aggregation] + # Build the aggregation query with explain_options + # analzye = true to get back the query stats, plan info, and query results + all_tasks_query = client.query(kind="Task") + count_query = client.aggregation_query( + all_tasks_query, explain_options=datastore.ExplainOptions(analyze=True) + ).count() + + # initiate the query + iterator = count_query.fetch() + + # explain_metrics is only available after query is completed + for task_result in iterator: + print(task_result) + + # get the plan summary + plan_summary = iterator.explain_metrics.plan_summary + print(f"Indexes used: {plan_summary.indexes_used}") + + # get the execution stats + execution_stats = iterator.explain_metrics.execution_stats + print(f"Results returned: {execution_stats.results_returned}") + print(f"Execution duration: {execution_stats.execution_duration}") + print(f"Read operations: {execution_stats.read_operations}") + print(f"Debug stats: {execution_stats.debug_stats}") + # [END datastore_query_explain_analyze_aggregation] + + +def explain_aggregation(client): + # [START datastore_query_explain_aggregation] + # Build the aggregation query with explain_options + # by default (analyze = false), only plan_summary property is available + all_tasks_query = client.query(kind="Task") + count_query = client.aggregation_query( + all_tasks_query, explain_options=datastore.ExplainOptions() + ).count() + + # initiate the query + iterator = count_query.fetch() + + # get the plan summary + plan_summary = iterator.explain_metrics.plan_summary + print(f"Indexes used: {plan_summary.indexes_used}") + # [END datastore_query_explain_aggregation] + + +def main(project_id): + client = datastore.Client(project_id) + + for name, function in globals().items(): + if name in ( + "main", + "_preamble", + "defaultdict", + "datetime", + "timezone", + "timedelta", + ) or not callable(function): + continue + + print(name) + pprint(function(client)) + print("\n-----------------\n") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Demonstrates datastore API operations." + ) + parser.add_argument("project_id", help="Your cloud project ID.") + + args = parser.parse_args() + + main(args.project_id) diff --git a/datastore/samples/snippets/snippets_test.py b/datastore/samples/snippets/snippets_test.py new file mode 100644 index 00000000000..ae3b2948b34 --- /dev/null +++ b/datastore/samples/snippets/snippets_test.py @@ -0,0 +1,249 @@ +# Copyright 2022 Google, Inc. +# 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 os + +import backoff +import google.api_core.exceptions +from google.cloud import datastore +from google.cloud import datastore_admin_v1 +import pytest + +import snippets + +PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] + + +class CleanupClient(datastore.Client): + def __init__(self, *args, **kwargs): + super(CleanupClient, self).__init__(*args, **kwargs) + self.entities_to_delete = [] + self.keys_to_delete = [] + + def cleanup(self): + with self.batch(): + self.delete_multi( + list(set([x.key for x in self.entities_to_delete if x])) + + list(set(self.keys_to_delete)) + ) + + +@pytest.fixture +def client(): + client = CleanupClient(PROJECT) + yield client + client.cleanup() + + +@pytest.fixture(scope="session", autouse=True) +def setup_indexes(request): + # Set up required indexes + admin_client = datastore_admin_v1.DatastoreAdminClient() + + indexes = [] + done_property_index = datastore_admin_v1.Index.IndexedProperty( + name="done", direction=datastore_admin_v1.Index.Direction.ASCENDING + ) + hour_property_index = datastore_admin_v1.Index.IndexedProperty( + name="hours", direction=datastore_admin_v1.Index.Direction.ASCENDING + ) + done_hour_index = datastore_admin_v1.Index( + kind="Task", + ancestor=datastore_admin_v1.Index.AncestorMode.NONE, + properties=[done_property_index, hour_property_index], + ) + indexes.append(done_hour_index) + + for index in indexes: + request = datastore_admin_v1.CreateIndexRequest(project_id=PROJECT, index=index) + # Create the required index + # Dependant tests will fail until the index is ready + try: + admin_client.create_index(request) + # Pass if the index already exists + except (google.api_core.exceptions.AlreadyExists): + pass + + +@pytest.mark.flaky +class TestDatastoreSnippets: + # These tests mostly just test the absence of exceptions. + + @backoff.on_exception(backoff.expo, AssertionError, max_time=240) + def test_in_query(self, client): + tasks = snippets.in_query(client) + client.entities_to_delete.extend(tasks) + assert tasks is not None + + @backoff.on_exception(backoff.expo, AssertionError, max_time=240) + def test_not_equals_query(self, client): + tasks = snippets.not_equals_query(client) + client.entities_to_delete.extend(tasks) + assert tasks is not None + + @backoff.on_exception(backoff.expo, AssertionError, max_time=240) + def test_not_in_query(self, client): + tasks = snippets.not_in_query(client) + client.entities_to_delete.extend(tasks) + assert tasks is not None + + @backoff.on_exception(backoff.expo, AssertionError, max_time=240) + def test_query_with_readtime(self, client): + tasks = snippets.query_with_readtime(client) + client.entities_to_delete.extend(tasks) + assert tasks is not None + + @backoff.on_exception(backoff.expo, AssertionError, max_time=240) + def test_count_query_in_transaction(self, client): + with pytest.raises(ValueError) as excinfo: + snippets.count_query_in_transaction(client) + assert "User 'John' cannot have more than 2 tasks" in str(excinfo.value) + + @backoff.on_exception(backoff.expo, AssertionError, max_time=240) + def test_count_query_on_kind(self, capsys, client): + tasks = snippets.count_query_on_kind(client) + captured = capsys.readouterr() + assert ( + captured.out.strip() == "Total tasks (accessible from default alias) is 2" + ) + assert captured.err == "" + + client.entities_to_delete.extend(tasks) + + @backoff.on_exception(backoff.expo, AssertionError, max_time=240) + def test_count_query_with_limit(self, capsys, client): + tasks = snippets.count_query_with_limit(client) + captured = capsys.readouterr() + assert captured.out.strip() == "We have at least 2 tasks" + assert captured.err == "" + + client.entities_to_delete.extend(tasks) + + @backoff.on_exception(backoff.expo, AssertionError, max_time=240) + def test_count_query_property_filter(self, capsys, client): + tasks = snippets.count_query_property_filter(client) + captured = capsys.readouterr() + + assert "Total completed tasks count is 2" in captured.out + assert "Total remaining tasks count is 1" in captured.out + assert captured.err == "" + + client.entities_to_delete.extend(tasks) + + @backoff.on_exception(backoff.expo, AssertionError, max_time=240) + def test_count_query_with_stale_read(self, capsys, client): + tasks = snippets.count_query_with_stale_read(client) + captured = capsys.readouterr() + + assert "Latest tasks count is 3" in captured.out + assert "Stale tasks count is 2" in captured.out + assert captured.err == "" + + client.entities_to_delete.extend(tasks) + + @backoff.on_exception(backoff.expo, AssertionError, max_time=240) + def test_sum_query_on_kind(self, capsys, client): + tasks = snippets.sum_query_on_kind(client) + captured = capsys.readouterr() + assert captured.out.strip() == "Total sum of hours in tasks is 9" + assert captured.err == "" + + client.entities_to_delete.extend(tasks) + + @backoff.on_exception(backoff.expo, AssertionError, max_time=240) + def test_sum_query_property_filter(self, capsys, client): + tasks = snippets.sum_query_property_filter(client) + captured = capsys.readouterr() + assert captured.out.strip() == "Total sum of hours in completed tasks is 8" + assert captured.err == "" + + client.entities_to_delete.extend(tasks) + + @backoff.on_exception(backoff.expo, AssertionError, max_time=240) + def test_avg_query_on_kind(self, capsys, client): + tasks = snippets.avg_query_on_kind(client) + captured = capsys.readouterr() + assert captured.out.strip() == "Total average of hours in tasks is 3.0" + assert captured.err == "" + + client.entities_to_delete.extend(tasks) + + @backoff.on_exception(backoff.expo, AssertionError, max_time=240) + def test_avg_query_property_filter(self, capsys, client): + tasks = snippets.avg_query_property_filter(client) + captured = capsys.readouterr() + assert ( + captured.out.strip() == "Total average of hours in completed tasks is 4.0" + ) + assert captured.err == "" + + client.entities_to_delete.extend(tasks) + + @backoff.on_exception(backoff.expo, AssertionError, max_time=240) + def test_multiple_aggregations_query(self, capsys, client): + tasks = snippets.multiple_aggregations_query(client) + captured = capsys.readouterr() + assert "avg_aggregation value is 3.0" in captured.out + assert "count_aggregation value is 3" in captured.out + assert "sum_aggregation value is 9" in captured.out + assert captured.err == "" + + client.entities_to_delete.extend(tasks) + + @backoff.on_exception(backoff.expo, AssertionError, max_time=240) + def test_explain_analyze_entity(self, capsys, client): + snippets.explain_analyze_entity(client) + captured = capsys.readouterr() + assert ( + "Indexes used: [{'properties': '(__name__ ASC)', 'query_scope': 'Collection group'}]" + in captured.out + ) + assert "Results returned: 0" in captured.out + assert "Execution duration: 0:00" in captured.out + assert "Read operations: 0" in captured.out + assert "Debug stats: {" in captured.out + assert captured.err == "" + + @backoff.on_exception(backoff.expo, AssertionError, max_time=240) + def test_explain_entity(self, capsys, client): + snippets.explain_entity(client) + captured = capsys.readouterr() + assert ( + "Indexes used: [{'properties': '(__name__ ASC)', 'query_scope': 'Collection group'}]" + in captured.out + ) + assert captured.err == "" + + @backoff.on_exception(backoff.expo, AssertionError, max_time=240) + def test_explain_analyze_aggregation(self, capsys, client): + snippets.explain_analyze_aggregation(client) + captured = capsys.readouterr() + assert ( + "Indexes used: [{'properties': '(__name__ ASC)', 'query_scope': 'Collection group'}]" + in captured.out + ) + assert "Results returned: 1" in captured.out + assert "Execution duration: 0:00" in captured.out + assert "Read operations: 1" in captured.out + assert "Debug stats: {" in captured.out + assert captured.err == "" + + @backoff.on_exception(backoff.expo, AssertionError, max_time=240) + def test_explain_aggregation(self, capsys, client): + snippets.explain_aggregation(client) + captured = capsys.readouterr() + assert ( + "Indexes used: [{'properties': '(__name__ ASC)', 'query_scope': 'Collection group'}]" + in captured.out + ) + assert captured.err == "" diff --git a/dialogflow-cx/noxfile_config.py b/dialogflow-cx/noxfile_config.py index 462f6d428f7..cc8143940ee 100644 --- a/dialogflow-cx/noxfile_config.py +++ b/dialogflow-cx/noxfile_config.py @@ -22,7 +22,7 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - "ignored_versions": ["2.7", "3.7", "3.9", "3.10", "3.11", "3.12", "3.13"], + "ignored_versions": ["2.7", "3.7", "3.8", "3.9", "3.11", "3.12", "3.13"], # An envvar key for determining the project id to use. Change it # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a # build specific Cloud project. You can also use your own string diff --git a/dialogflow-cx/requirements.txt b/dialogflow-cx/requirements.txt index c162d7a7e98..5c29bf4a7bf 100644 --- a/dialogflow-cx/requirements.txt +++ b/dialogflow-cx/requirements.txt @@ -2,7 +2,7 @@ google-cloud-dialogflow-cx==2.0.0 Flask==3.0.3 python-dateutil==2.9.0.post0 functions-framework==3.9.2 -Werkzeug==3.0.6 +Werkzeug==3.1.5 termcolor==3.0.0; python_version >= "3.9" termcolor==2.4.0; python_version == "3.8" pyaudio==0.2.14 \ No newline at end of file diff --git a/discoveryengine/answer_query_sample.py b/discoveryengine/answer_query_sample.py index 80e02e0c7c5..fcb47bff6b8 100644 --- a/discoveryengine/answer_query_sample.py +++ b/discoveryengine/answer_query_sample.py @@ -69,7 +69,7 @@ def answer_query_sample( ignore_non_answer_seeking_query=False, # Optional: Ignore non-answer seeking query ignore_low_relevant_content=False, # Optional: Return fallback answer when content is not relevant model_spec=discoveryengine.AnswerQueryRequest.AnswerGenerationSpec.ModelSpec( - model_version="gemini-2.0-flash-001/answer_gen/v1", # Optional: Model to use for answer generation + model_version="gemini-2.5-flash/answer_gen/v1", # Optional: Model to use for answer generation ), prompt_spec=discoveryengine.AnswerQueryRequest.AnswerGenerationSpec.PromptSpec( preamble="Give a detailed answer.", # Optional: Natural language instructions for customizing the answer. diff --git a/firestore/cloud-async-client/snippets.py b/firestore/cloud-async-client/snippets.py index b0a97962cc5..3a7b9476941 100644 --- a/firestore/cloud-async-client/snippets.py +++ b/firestore/cloud-async-client/snippets.py @@ -693,24 +693,16 @@ async def delete_full_collection(): db = firestore.AsyncClient() # [START firestore_data_delete_collection_async] - async def delete_collection(coll_ref, batch_size): - docs = coll_ref.limit(batch_size).stream() - deleted = 0 + async def delete_collection(coll_ref): - async for doc in docs: - print(f"Deleting doc {doc.id} => {doc.to_dict()}") - await doc.reference.delete() - deleted = deleted + 1 - - if deleted >= batch_size: - return delete_collection(coll_ref, batch_size) + await db.recursive_delete(coll_ref) # [END firestore_data_delete_collection_async] - await delete_collection(db.collection("cities"), 10) - await delete_collection(db.collection("data"), 10) - await delete_collection(db.collection("objects"), 10) - await delete_collection(db.collection("users"), 10) + await delete_collection(db.collection("cities")) + await delete_collection(db.collection("data")) + await delete_collection(db.collection("objects")) + await delete_collection(db.collection("users")) async def collection_group_query(db): diff --git a/firestore/cloud-client/snippets.py b/firestore/cloud-client/snippets.py index 09dff308a50..9bc64d1c383 100644 --- a/firestore/cloud-client/snippets.py +++ b/firestore/cloud-client/snippets.py @@ -839,28 +839,17 @@ def delete_full_collection(): db = firestore.Client() # [START firestore_data_delete_collection] - def delete_collection(coll_ref, batch_size): - if batch_size == 0: - return + def delete_collection(coll_ref): - docs = coll_ref.list_documents(page_size=batch_size) - deleted = 0 - - for doc in docs: - print(f"Deleting doc {doc.id} => {doc.get().to_dict()}") - doc.delete() - deleted = deleted + 1 - - if deleted >= batch_size: - return delete_collection(coll_ref, batch_size) + print(f"Recursively deleting collection: {coll_ref}") + db.recursive_delete(coll_ref) # [END firestore_data_delete_collection] - delete_collection(db.collection("cities"), 10) - delete_collection(db.collection("data"), 10) - delete_collection(db.collection("objects"), 10) - delete_collection(db.collection("users"), 10) - delete_collection(db.collection("users"), 0) + delete_collection(db.collection("cities")) + delete_collection(db.collection("data")) + delete_collection(db.collection("objects")) + delete_collection(db.collection("users")) def collection_group_query(db): diff --git a/genai/code_execution/codeexecution_annotateimage_with_txt_gcsimg.py b/genai/code_execution/codeexecution_annotateimage_with_txt_gcsimg.py new file mode 100644 index 00000000000..a81f62c8491 --- /dev/null +++ b/genai/code_execution/codeexecution_annotateimage_with_txt_gcsimg.py @@ -0,0 +1,150 @@ +# Copyright 2026 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 +# +# https://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. + + +def generate_content() -> bool: + # [START googlegenaisdk_codeexecution_annotateimage_with_txt_gcsimg] + import io + from PIL import Image + from google import genai + from google.genai import types + + client = genai.Client() + + response = client.models.generate_content( + model="gemini-3-flash-preview", + contents=[ + types.Part.from_uri( + file_uri="https://storage.googleapis.com/cloud-samples-data/generative-ai/image/robotic.jpeg", + mime_type="image/png", + ), + "Annotate on the image with arrows of different colors, which object should go into which bin.", + ], + config=types.GenerateContentConfig(tools=[types.Tool(code_execution=types.ToolCodeExecution)]), + ) + + img_count = 0 + for part in response.candidates[0].content.parts: + if part.text is not None: + print(part.text) + if part.executable_code is not None: + print("####################### 1. Generate Python Code #######################") + print(part.executable_code.code) + if part.code_execution_result is not None: + print("####################### 2. Executing Python Code #######################") + print(part.code_execution_result.output) + # For local executions, save the output to a local filename + if part.as_image() is not None: + print("####################### 3. Save Output #######################") + img_count += 1 + output_location = f"robotic-annotate-output-{img_count}.jpg" + image_data = part.as_image().image_bytes + image = Image.open(io.BytesIO(image_data)) + image = image.convert("RGB") + image.save(output_location) + print(f"Output is saved to {output_location}") + # Example response: + # ####################### 1. Generate Python Code ####################### + # import PIL.Image + # import PIL.ImageDraw + # + # # Load the image to get dimensions + # img = PIL.Image.open('f_https___storage.googleapis.com_cloud_samples_data_generative_ai_image_robotic.jpeg') + # width, height = img.size + # + # # Define objects and bins with normalized coordinates [ymin, xmin, ymax, xmax] + # bins = { + # 'light_blue': [118, 308, 338, 436], + # 'green': [248, 678, 458, 831], + # 'black': [645, 407, 898, 578] + # } + # + # objects = [ + # {'name': 'green pepper', 'box': [256, 482, 296, 546], 'target': 'green'}, + # {'name': 'red pepper', 'box': [317, 478, 349, 544], 'target': 'green'}, + # {'name': 'grapes', 'box': [584, 555, 664, 593], 'target': 'green'}, + # {'name': 'cherries', 'box': [463, 671, 511, 718], 'target': 'green'}, + # {'name': 'soda can', 'box': [397, 524, 489, 605], 'target': 'light_blue'}, + # {'name': 'brown snack', 'box': [397, 422, 475, 503], 'target': 'black'}, + # {'name': 'welch snack', 'box': [520, 466, 600, 543], 'target': 'black'}, + # {'name': 'paper towel', 'box': [179, 564, 250, 607], 'target': 'black'}, + # {'name': 'plastic cup', 'box': [271, 587, 346, 643], 'target': 'black'}, + # ] + # + # # Helper to get center of a normalized box + # def get_center(box): + # ymin, xmin, ymax, xmax = box + # return ((xmin + xmax) / 2000 * width, (ymin + ymax) / 2000 * height) + # + # draw = PIL.ImageDraw.Draw(img) + # + # # Define arrow colors based on target bin + # colors = { + # 'green': 'green', + # 'light_blue': 'blue', + # 'black': 'red' + # } + # + # for obj in objects: + # start_point = get_center(obj['box']) + # end_point = get_center(bins[obj['target']]) + # color = colors[obj['target']] + # # Drawing a line with an arrow head (simulated with a few extra lines) + # draw.line([start_point, end_point], fill=color, width=5) + # # Simple arrowhead + # import math + # angle = math.atan2(end_point[1] - start_point[1], end_point[0] - start_point[0]) + # arrow_len = 20 + # p1 = (end_point[0] - arrow_len * math.cos(angle - math.pi / 6), + # end_point[1] - arrow_len * math.sin(angle - math.pi / 6)) + # p2 = (end_point[0] - arrow_len * math.cos(angle + math.pi / 6), + # end_point[1] - arrow_len * math.sin(angle + math.pi / 6)) + # draw.line([end_point, p1], fill=color, width=5) + # draw.line([end_point, p2], fill=color, width=5) + # + # img.save('annotated_robotic.jpeg') + # + # # Also list detections for confirmation + # # [ + # # {"box_2d": [118, 308, 338, 436], "label": "light blue bin"}, + # # {"box_2d": [248, 678, 458, 831], "label": "green bin"}, + # # {"box_2d": [645, 407, 898, 578], "label": "black bin"}, + # # {"box_2d": [256, 482, 296, 546], "label": "green pepper"}, + # # {"box_2d": [317, 478, 349, 544], "label": "red pepper"}, + # # {"box_2d": [584, 555, 664, 593], "label": "grapes"}, + # # {"box_2d": [463, 671, 511, 718], "label": "cherries"}, + # # {"box_2d": [397, 524, 489, 605], "label": "soda can"}, + # # {"box_2d": [397, 422, 475, 503], "label": "brown snack"}, + # # {"box_2d": [520, 466, 600, 543], "label": "welch snack"}, + # # {"box_2d": [179, 564, 250, 607], "label": "paper towel"}, + # # {"box_2d": [271, 587, 346, 643], "label": "plastic cup"} + # # ] + # + # ####################### 2. Executing Python Code ####################### + # None + # ####################### 3. Save Output ####################### + # Output is saved to output-annotate-image-1.jpg + # The image has been annotated with arrows indicating the appropriate bin for each object based on standard waste sorting practices: + # + # - **Green Arrows (Compost):** Organic items such as the green pepper, red pepper, grapes, and cherries are directed to the **green bin**. + # - **Blue Arrow (Recycling):** The crushed soda can is directed to the **light blue bin**. + # - **Red Arrows (Trash/Landfill):** Non-recyclable or contaminated items like the snack wrappers (brown and Welch's), the white paper towel, and the small plastic cup are directed to the **black bin**. + # + # These categorizations follow common sorting rules where green is for organics, blue for recyclables, and black for general waste. + # [END googlegenaisdk_codeexecution_annotateimage_with_txt_gcsimg] + return True + + +if __name__ == "__main__": + generate_content() diff --git a/genai/code_execution/codeexecution_barplot_with_txt_img.py b/genai/code_execution/codeexecution_barplot_with_txt_img.py new file mode 100644 index 00000000000..7542282e4be --- /dev/null +++ b/genai/code_execution/codeexecution_barplot_with_txt_img.py @@ -0,0 +1,156 @@ +# Copyright 2026 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 +# +# https://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. + + +def generate_content() -> bool: + # [START googlegenaisdk_codeexecution_barplot_with_txt_img] + import io + from PIL import Image + from google import genai + from google.genai import types + + # Use to the benchmark image in Cloud Storage + image = types.Part.from_uri( + file_uri="https://storage.googleapis.com/cloud-samples-data/generative-ai/image/benchmark.jpeg", + mime_type="image/jpeg", + ) + + client = genai.Client() + + response = client.models.generate_content( + model="gemini-3-flash-preview", + contents=[ + image, + "Make a bar chart of per-category performance, normalize prior SOTA as 1.0 for each task," + "then take average per-category. Plot using matplotlib with nice style.", + ], + config=types.GenerateContentConfig(tools=[types.Tool(code_execution=types.ToolCodeExecution)]), + ) + + img_count = 0 + for part in response.candidates[0].content.parts: + if part.text is not None: + print(part.text) + if part.executable_code is not None: + print("####################### 1. Generate Python Code #######################") + print(part.executable_code.code) + if part.code_execution_result is not None: + print("####################### 2. Executing Python Code #######################") + print(part.code_execution_result.output) + # For local executions, save the output to a local filename + if part.as_image() is not None: + print("####################### 3. Save Output #######################") + img_count += 1 + output_location = f"output-barplot-{img_count}.jpg" + image_data = part.as_image().image_bytes + image = Image.open(io.BytesIO(image_data)) + image = image.convert("RGB") + image.save(output_location) + print(f"Output is saved to {output_location}") + # Example response: + # ####################### 1. Generate Python Code ####################### + # import matplotlib.pyplot as plt + # import numpy as np + # + # data = [ + # # Category, Benchmark, G3P, G2.5P, C4.5, GPT5.1, lower_is_better + # ("Visual Reasoning", "MMMU Pro", 81.0, 68.0, 72.0, 76.0, False), + # ("Visual Reasoning", "VLMsAreBiased", 50.6, 24.3, 32.7, 21.7, False), + # ("Document", "CharXiv Reasoning", 81.4, 69.6, 67.2, 69.5, False), + # ("Document", "OmniDocBench1.5*", 0.115, 0.145, 0.120, 0.147, True), + # ("Spatial", "ERQA", 70.5, 56.0, 51.3, 60.0, False), + # ("Spatial", "Point-Bench", 85.5, 62.7, 38.5, 41.8, False), + # ("Spatial", "RefSpatial", 65.5, 33.6, 19.5, 28.2, False), + # ("Spatial", "CV-Bench", 92.0, 85.9, 83.8, 84.6, False), + # ("Spatial", "MindCube", 77.7, 57.5, 58.5, 61.7, False), + # ("Screen", "ScreenSpot Pro", 72.7, 11.4, 49.9, 3.50, False), + # ("Screen", "Gui-World QA", 68.0, 42.8, 44.9, 38.7, False), + # ("Video", "Video-MMMU", 87.6, 83.6, 84.4, 80.4, False), + # ("Video", "Video-MME", 88.4, 86.9, 84.1, 86.3, False), + # ("Video", "1H-VideoQA", 81.8, 79.4, 52.0, 61.5, False), + # ("Video", "Perception Test", 80.0, 78.4, 74.1, 77.8, False), + # ("Video", "YouCook2", 222.7, 188.3, 145.8, 132.4, False), + # ("Video", "Vatex", 77.4, 71.3, 60.1, 62.9, False), + # ("Video", "Motion Bench", 70.3, 66.3, 65.9, 61.1, False), + # ("Education", "Math Kangaroo", 84.4, 77.4, 68.9, 79.9, False), + # ("Biomedical", "MedXpertQA-MM", 77.8, 65.9, 62.2, 65.5, False), + # ("Biomedical", "VQA-RAD", 81.9, 71.4, 76.0, 72.2, False), + # ("Biomedical", "MicroVQA", 68.8, 63.5, 61.4, 61.5, False), + # ] + # + # normalized_scores = [] + # for cat, bench, g3p, g25p, c45, gpt, lib in data: + # others = [g25p, c45, gpt] + # if lib: + # sota = min(others) + # norm_score = sota / g3p + # else: + # sota = max(others) + # norm_score = g3p / sota + # normalized_scores.append((cat, norm_score)) + # + # categories = {} + # for cat, score in normalized_scores: + # if cat not in categories: + # categories[cat] = [] + # categories[cat].append(score) + # + # avg_per_category = {cat: np.mean(scores) for cat, scores in categories.items()} + # + # # Plotting + # cats = list(avg_per_category.keys()) + # values = [avg_per_category[c] for c in cats] + # + # # Sort categories for better visualization if needed, or keep order from data + # plt.figure(figsize=(10, 6)) + # plt.style.use('ggplot') + # bars = plt.bar(cats, values, color='skyblue', edgecolor='navy') + # + # plt.axhline(y=1.0, color='red', linestyle='--', label='Prior SOTA (1.0)') + # plt.ylabel('Normalized Performance (SOTA = 1.0)') + # plt.title('Gemini 3 Pro Performance relative to Prior SOTA (Normalized)', fontsize=14) + # plt.xticks(rotation=45, ha='right') + # plt.ylim(0, max(values) * 1.2) + # + # for bar in bars: + # yval = bar.get_height() + # plt.text(bar.get_x() + bar.get_width()/2, yval + 0.02, f'{yval:.2f}x', ha='center', va='bottom') + # + # plt.legend() + # plt.tight_layout() + # plt.savefig('performance_chart.png') + # plt.show() + # + # print(avg_per_category) + # + # ####################### 2. Executing Python Code ####################### + # {'Visual Reasoning': np.float64(1.3065950426525028), 'Document': np.float64(1.1065092453773113), 'Spatial': np.float64(1.3636746436001959), 'Screen': np.float64(1.4856952211773211), 'Video': np.float64(1.0620548283943443), 'Education': np.float64(1.0563204005006257), 'Biomedical': np.float64(1.1138909257119955)} + # + # ####################### 3. Save Output ####################### + # Output is saved to output-barplot-1.jpg + # ####################### 3. Save Output ####################### + # Output is saved to output-barplot-2.jpg + # Based on the data provided in the table, I have calculated the per-category performance of Gemini 3 Pro normalized against the prior state-of-the-art (SOTA), which is defined as the best performance among Gemini 2.5 Pro, Claude Opus 4.5, and GPT-5.1 for each benchmark. + # + # For benchmarks where lower values are better (indicated by an asterisk, e.g., OmniDocBench1.5*), the normalization was calculated as $\text{Prior SOTA} / \text{Gemini 3 Pro Score}$. For all other benchmarks, it was calculated as $\text{Gemini 3 Pro Score} / \text{Prior SOTA}$. The values were then averaged within each category. + # + # The resulting bar chart below shows that Gemini 3 Pro outperforms the prior SOTA across all categories, with the most significant gains in **Screen** (1.49x), **Spatial** (1.36x), and **Visual Reasoning** (1.31x) benchmarks. + # + # ![Gemini 3 Pro Performance Chart](performance_chart.png) + # [END googlegenaisdk_codeexecution_barplot_with_txt_img] + return True + + +if __name__ == "__main__": + generate_content() diff --git a/genai/code_execution/codeexecution_cropimage_with_txt_img.py b/genai/code_execution/codeexecution_cropimage_with_txt_img.py new file mode 100644 index 00000000000..9acfae1f93f --- /dev/null +++ b/genai/code_execution/codeexecution_cropimage_with_txt_img.py @@ -0,0 +1,95 @@ +# Copyright 2026 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 +# +# https://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. + + +def generate_content() -> bool: + # [START googlegenaisdk_codeexecution_cropimage_with_txt_img] + import io + import requests + from PIL import Image + from google import genai + from google.genai import types + + # Download the input image + image_path = "https://storage.googleapis.com/cloud-samples-data/generative-ai/image/chips.jpeg" + image_bytes = requests.get(image_path).content + image = types.Part.from_bytes(data=image_bytes, mime_type="image/jpeg") + + client = genai.Client() + + response = client.models.generate_content( + model="gemini-3-flash-preview", + contents=[ + image, + "Locate the ESMT chip. What are the numbers on the chip?", + ], + config=types.GenerateContentConfig(tools=[types.Tool(code_execution=types.ToolCodeExecution)]), + ) + + for part in response.candidates[0].content.parts: + if part.text is not None: + print(part.text) + if part.executable_code is not None: + print("####################### 1. Generate Python Code #######################") + print(part.executable_code.code) + if part.code_execution_result is not None: + print("####################### 2. Executing Python Code #######################") + print(part.code_execution_result.output) + # For local executions, save the output to a local filename + if part.as_image() is not None: + print("####################### 3. Save Output #######################") + image_data = part.as_image().image_bytes + image = Image.open(io.BytesIO(image_data)) + output_location = "ESMT-chip-output.jpg" + image.save(output_location) + print(f"Output is saved to {output_location}") + # Example response: + # ####################### 1. Generate Python Code ####################### + # import PIL.Image + # import PIL.ImageDraw + # + # # Load the image to get dimensions + # img = PIL.Image.open('input_file_0.jpeg') + # width, height = img.size + # + # # Define the region for expression pedals + # # They are roughly in the center + # # Normalized coordinates roughly: [ymin, xmin, ymax, xmax] + # expression_pedals_box = [460, 465, 615, 615] + # + # # Convert normalized to pixel coordinates + # def norm_to_pixel(norm_box, w, h): + # ymin, xmin, ymax, xmax = norm_box + # return [int(ymin * h / 1000), int(xmin * w / 1000), int(ymax * h / 1000), int(xmax * w / 1000)] + # + # pedals_pixel_box = norm_to_pixel(expression_pedals_box, width, height) + # + # # Crop and save + # pedals_crop = img.crop((pedals_pixel_box[1], pedals_pixel_box[0], pedals_pixel_box[3], pedals_pixel_box[2])) + # pedals_crop.save('expression_pedals_zoom.png') + # + # # Output objects for verification (optional but helpful for internal tracking) + # # [{box_2d: [460, 465, 615, 615], label: "expression pedals"}] + # + # ####################### 2. Executing Python Code ####################### + # None + # ####################### 3. Save Output ####################### + # Output is saved to instrument-img-output.jpg + # Based on the zoomed-in image, there are 4 expression pedals located in the center of the organ console, above the pedalboard. + # [END googlegenaisdk_codeexecution_cropimage_with_txt_img] + return True + + +if __name__ == "__main__": + generate_content() diff --git a/appengine/flexible_python37_and_earlier/hello_world_django/noxfile_config.py b/genai/code_execution/noxfile_config.py similarity index 81% rename from appengine/flexible_python37_and_earlier/hello_world_django/noxfile_config.py rename to genai/code_execution/noxfile_config.py index 1665dd736f8..29d9e7911eb 100644 --- a/appengine/flexible_python37_and_earlier/hello_world_django/noxfile_config.py +++ b/genai/code_execution/noxfile_config.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,17 +22,20 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - # Skipping for Python 3.9 due to pyarrow compilation failure. - "ignored_versions": ["2.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"], + "ignored_versions": ["2.7", "3.7", "3.8", "3.9", "3.10", "3.11", "3.13", "3.14"], # Old samples are opted out of enforcing Python type hints # All new samples should feature them - "enforce_type_hints": False, + "enforce_type_hints": True, # An envvar key for determining the project id to use. Change it # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a # build specific Cloud project. You can also use your own string # to use your own Cloud project. "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', + # If you need to use a specific version of pip, + # change pip_version_override to the string representation + # of the version number, for example, "20.2.4" + "pip_version_override": None, # A dictionary you want to inject into your test. Don't put any # secrets here. These values will override predefined values. "envs": {}, diff --git a/genai/code_execution/requirements-test.txt b/genai/code_execution/requirements-test.txt new file mode 100644 index 00000000000..8d10ef87035 --- /dev/null +++ b/genai/code_execution/requirements-test.txt @@ -0,0 +1,4 @@ +backoff==2.2.1 +google-api-core==2.29.0 +pytest==9.0.2 +pytest-asyncio==1.3.0 diff --git a/genai/code_execution/requirements.txt b/genai/code_execution/requirements.txt new file mode 100644 index 00000000000..7365e0b937d --- /dev/null +++ b/genai/code_execution/requirements.txt @@ -0,0 +1,2 @@ +google-genai==1.60.0 +pillow==11.1.0 diff --git a/genai/code_execution/test_codeexecution.py b/genai/code_execution/test_codeexecution.py new file mode 100644 index 00000000000..e3a8bfb7944 --- /dev/null +++ b/genai/code_execution/test_codeexecution.py @@ -0,0 +1,35 @@ +# Copyright 2026 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 +# +# https://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 os + +import codeexecution_annotateimage_with_txt_gcsimg +import codeexecution_barplot_with_txt_img +import codeexecution_cropimage_with_txt_img + +os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True" +os.environ["GOOGLE_CLOUD_LOCATION"] = "global" # "us-central1" +# The project name is included in the CICD pipeline +# os.environ['GOOGLE_CLOUD_PROJECT'] = "add-your-project-name" + + +def test_codeexecution_annotateimage_with_txt_gcsimg() -> None: + assert codeexecution_annotateimage_with_txt_gcsimg.generate_content() + + +def test_codeexecution_barplot_with_txt_img() -> None: + assert codeexecution_barplot_with_txt_img.generate_content() + + +def test_codeexecution_cropimage_with_txt_img() -> None: + assert codeexecution_cropimage_with_txt_img.generate_content() diff --git a/genai/image_generation/imggen_mmflash_edit_img_with_txt_img.py b/genai/image_generation/imggen_mmflash_edit_img_with_txt_img.py index b0a7bb2a94c..e2d9888a027 100644 --- a/genai/image_generation/imggen_mmflash_edit_img_with_txt_img.py +++ b/genai/image_generation/imggen_mmflash_edit_img_with_txt_img.py @@ -26,7 +26,7 @@ def generate_content() -> str: image = Image.open("test_resources/example-image-eiffel-tower.png") response = client.models.generate_content( - model="gemini-2.5-flash-image", + model="gemini-3-pro-image-preview", contents=[image, "Edit this image to make it look like a cartoon."], config=GenerateContentConfig(response_modalities=[Modality.TEXT, Modality.IMAGE]), ) @@ -36,12 +36,7 @@ def generate_content() -> str: elif part.inline_data: image = Image.open(BytesIO((part.inline_data.data))) image.save("output_folder/bw-example-image.png") - # Example response: - # Here's the cartoon-style edit of the image: - # Cartoon-style edit: - # - Simplified the Eiffel Tower with bolder lines and slightly exaggerated proportions. - # - Brightened and saturated the colors of the sky, fireworks, and foliage for a more vibrant, cartoonish look. - # .... + # [END googlegenaisdk_imggen_mmflash_edit_img_with_txt_img] return "output_folder/bw-example-image.png" diff --git a/genai/image_generation/imggen_mmflash_txt_and_img_with_txt.py b/genai/image_generation/imggen_mmflash_txt_and_img_with_txt.py index 9e54d7b895e..7a9d11103a7 100644 --- a/genai/image_generation/imggen_mmflash_txt_and_img_with_txt.py +++ b/genai/image_generation/imggen_mmflash_txt_and_img_with_txt.py @@ -23,7 +23,7 @@ def generate_content() -> int: client = genai.Client() response = client.models.generate_content( - model="gemini-2.5-flash-image", + model="gemini-3-pro-image-preview", contents=( "Generate an illustrated recipe for a paella." "Create images to go alongside the text as you generate the recipe" @@ -38,9 +38,7 @@ def generate_content() -> int: image = Image.open(BytesIO((part.inline_data.data))) image.save(f"output_folder/example-image-{i+1}.png") fp.write(f"![image](example-image-{i+1}.png)") - # Example response: - # A markdown page for a Paella recipe(`paella-recipe.md`) has been generated. - # It includes detailed steps and several images illustrating the cooking process. + # [END googlegenaisdk_imggen_mmflash_txt_and_img_with_txt] return True diff --git a/genai/image_generation/imggen_mmflash_with_txt.py b/genai/image_generation/imggen_mmflash_with_txt.py index ed0b6d416bf..cd6c458a757 100644 --- a/genai/image_generation/imggen_mmflash_with_txt.py +++ b/genai/image_generation/imggen_mmflash_with_txt.py @@ -15,24 +15,20 @@ def generate_content() -> str: # [START googlegenaisdk_imggen_mmflash_with_txt] + import os + from io import BytesIO + from google import genai from google.genai.types import GenerateContentConfig, Modality from PIL import Image - from io import BytesIO client = genai.Client() response = client.models.generate_content( - model="gemini-2.5-flash-image", + model="gemini-3-pro-image-preview", contents=("Generate an image of the Eiffel tower with fireworks in the background."), config=GenerateContentConfig( response_modalities=[Modality.TEXT, Modality.IMAGE], - candidate_count=1, - safety_settings=[ - {"method": "PROBABILITY"}, - {"category": "HARM_CATEGORY_DANGEROUS_CONTENT"}, - {"threshold": "BLOCK_MEDIUM_AND_ABOVE"}, - ], ), ) for part in response.candidates[0].content.parts: @@ -40,12 +36,11 @@ def generate_content() -> str: print(part.text) elif part.inline_data: image = Image.open(BytesIO((part.inline_data.data))) - image.save("output_folder/example-image-eiffel-tower.png") - # Example response: - # I will generate an image of the Eiffel Tower at night, with a vibrant display of - # colorful fireworks exploding in the dark sky behind it. The tower will be - # illuminated, standing tall as the focal point of the scene, with the bursts of - # light from the fireworks creating a festive atmosphere. + # Ensure the output directory exists + output_dir = "output_folder" + os.makedirs(output_dir, exist_ok=True) + image.save(os.path.join(output_dir, "example-image-eiffel-tower.png")) + # [END googlegenaisdk_imggen_mmflash_with_txt] return True diff --git a/genai/image_generation/imggen_virtual_try_on_with_txt_img.py b/genai/image_generation/imggen_virtual_try_on_with_txt_img.py index 98d0c17c76e..f1e6b6cc5cd 100644 --- a/genai/image_generation/imggen_virtual_try_on_with_txt_img.py +++ b/genai/image_generation/imggen_virtual_try_on_with_txt_img.py @@ -26,7 +26,7 @@ def virtual_try_on(output_file: str) -> Image: # output_file = "output-image.png" image = client.models.recontext_image( - model="virtual-try-on-preview-08-04", + model="virtual-try-on-001", source=RecontextImageSource( person_image=Image.from_file(location="test_resources/man.png"), product_images=[ diff --git a/genai/live/live_audio_with_txt.py b/genai/live/live_audio_with_txt.py index 5d4e82cef85..3860b9f0128 100644 --- a/genai/live/live_audio_with_txt.py +++ b/genai/live/live_audio_with_txt.py @@ -37,7 +37,7 @@ def play_audio(audio_array: np.ndarray, sample_rate: int = 24000) -> None: client = genai.Client() voice_name = "Aoede" - model = "gemini-2.0-flash-live-preview-04-09" + model = "gemini-live-2.5-flash-native-audio" config = LiveConnectConfig( response_modalities=[Modality.AUDIO], diff --git a/genai/live/live_audiogen_with_txt.py b/genai/live/live_audiogen_with_txt.py index a6fc09f2e2a..29e20e8d661 100644 --- a/genai/live/live_audiogen_with_txt.py +++ b/genai/live/live_audiogen_with_txt.py @@ -29,7 +29,7 @@ async def generate_content() -> None: VoiceConfig) client = genai.Client() - model = "gemini-2.0-flash-live-preview-04-09" + model = "gemini-live-2.5-flash-native-audio" # For more Voice options, check https://cloud.google.com/vertex-ai/generative-ai/docs/models/gemini/2-5-flash#live-api-native-audio voice_name = "Aoede" diff --git a/genai/live/live_conversation_audio_with_audio.py b/genai/live/live_conversation_audio_with_audio.py index fb39dc36615..5d5b5a05445 100644 --- a/genai/live/live_conversation_audio_with_audio.py +++ b/genai/live/live_conversation_audio_with_audio.py @@ -32,7 +32,7 @@ # The number of audio frames to send in each chunk. CHUNK = 4200 CHANNELS = 1 -MODEL = "gemini-live-2.5-flash-preview-native-audio-09-2025" +MODEL = "gemini-live-2.5-flash-native-audio" # The audio sample rate expected by the model. INPUT_RATE = 16000 @@ -118,7 +118,7 @@ async def receive() -> None: receive_task = asyncio.create_task(receive()) await asyncio.gather(send_task, receive_task) # Example response: - # gemini-2.0-flash-live-preview-04-09 + # gemini-live-2.5-flash-native-audio # {'input_transcription': {'text': 'Hello.'}} # {'output_transcription': {}} # {'output_transcription': {'text': 'Hi'}} diff --git a/genai/live/live_structured_output_with_txt.py b/genai/live/live_structured_output_with_txt.py index b743c87f064..2727fbcb08e 100644 --- a/genai/live/live_structured_output_with_txt.py +++ b/genai/live/live_structured_output_with_txt.py @@ -59,7 +59,7 @@ def generate_content() -> CalendarEvent: ) completion = client.beta.chat.completions.parse( - model="google/gemini-2.0-flash-001", + model="google/gemini-2.5-flash", messages=[ ChatCompletionSystemMessageParam( role="system", content="Extract the event information." diff --git a/genai/live/live_websocket_audiogen_with_txt.py b/genai/live/live_websocket_audiogen_with_txt.py index 5fdeee44299..d81c685cf0e 100644 --- a/genai/live/live_websocket_audiogen_with_txt.py +++ b/genai/live/live_websocket_audiogen_with_txt.py @@ -47,7 +47,7 @@ async def generate_content() -> str: # Configuration Constants PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") LOCATION = "us-central1" - GEMINI_MODEL_NAME = "gemini-2.0-flash-live-preview-04-09" + GEMINI_MODEL_NAME = "gemini-live-2.5-flash-native-audio" # To generate a bearer token in CLI, use: # $ gcloud auth application-default print-access-token # It's recommended to fetch this token dynamically rather than hardcoding. diff --git a/genai/live/live_websocket_audiotranscript_with_txt.py b/genai/live/live_websocket_audiotranscript_with_txt.py index 0ed03b8638d..8b6ce59fb79 100644 --- a/genai/live/live_websocket_audiotranscript_with_txt.py +++ b/genai/live/live_websocket_audiotranscript_with_txt.py @@ -47,7 +47,7 @@ async def generate_content() -> str: # Configuration Constants PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") LOCATION = "us-central1" - GEMINI_MODEL_NAME = "gemini-2.0-flash-live-preview-04-09" + GEMINI_MODEL_NAME = "gemini-live-2.5-flash-native-audio" # To generate a bearer token in CLI, use: # $ gcloud auth application-default print-access-token # It's recommended to fetch this token dynamically rather than hardcoding. diff --git a/genai/tools/requirements.txt b/genai/tools/requirements.txt index 95d3e9bc0f0..9f6fafbe8ec 100644 --- a/genai/tools/requirements.txt +++ b/genai/tools/requirements.txt @@ -1,3 +1,3 @@ -google-genai==1.42.0 +google-genai==1.45.0 # PIl is required for tools_code_execution_with_txt_img.py pillow==11.1.0 diff --git a/genai/tools/tools_google_search_with_txt.py b/genai/tools/tools_google_search_with_txt.py index 2f650b01df9..4069071d0c3 100644 --- a/genai/tools/tools_google_search_with_txt.py +++ b/genai/tools/tools_google_search_with_txt.py @@ -31,7 +31,12 @@ def generate_content() -> str: config=GenerateContentConfig( tools=[ # Use Google Search Tool - Tool(google_search=GoogleSearch()) + Tool( + google_search=GoogleSearch( + # Optional: Domains to exclude from results + exclude_domains=["domain.com", "domain2.com"] + ) + ) ], ), ) diff --git a/genai/tuning/preference_tuning_job_create.py b/genai/tuning/preference_tuning_job_create.py new file mode 100644 index 00000000000..13fa05d61d0 --- /dev/null +++ b/genai/tuning/preference_tuning_job_create.py @@ -0,0 +1,74 @@ +# 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 +# +# https://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. + + +def create_tuning_job() -> str: + # [START googlegenaisdk_preference_tuning_job_create] + import time + + from google import genai + from google.genai.types import HttpOptions, CreateTuningJobConfig, TuningDataset + + client = genai.Client(http_options=HttpOptions(api_version="v1")) + + training_dataset = TuningDataset( + gcs_uri="gs://mybucket/preference_tuning/data/train_data.jsonl", + ) + validation_dataset = TuningDataset( + gcs_uri="gs://mybucket/preference_tuning/data/validation_data.jsonl", + ) + + # Refer to https://docs.cloud.google.com/vertex-ai/generative-ai/docs/models/gemini-use-continuous-tuning#google-gen-ai-sdk + # for example to continuous tune from SFT tuned model. + tuning_job = client.tunings.tune( + base_model="gemini-2.5-flash", + training_dataset=training_dataset, + config=CreateTuningJobConfig( + tuned_model_display_name="Example tuning job", + method="PREFERENCE_TUNING", + validation_dataset=validation_dataset, + ), + ) + + running_states = set([ + "JOB_STATE_PENDING", + "JOB_STATE_RUNNING", + ]) + + while tuning_job.state in running_states: + print(tuning_job.state) + tuning_job = client.tunings.get(name=tuning_job.name) + time.sleep(60) + + print(tuning_job.tuned_model.model) + print(tuning_job.tuned_model.endpoint) + print(tuning_job.experiment) + # Example response: + # projects/123456789012/locations/us-central1/models/1234567890@1 + # projects/123456789012/locations/us-central1/endpoints/123456789012345 + # projects/123456789012/locations/us-central1/metadataStores/default/contexts/tuning-experiment-2025010112345678 + + if tuning_job.tuned_model.checkpoints: + for i, checkpoint in enumerate(tuning_job.tuned_model.checkpoints): + print(f"Checkpoint {i + 1}: ", checkpoint) + # Example response: + # Checkpoint 1: checkpoint_id='1' epoch=1 step=10 endpoint='projects/123456789012/locations/us-central1/endpoints/123456789000000' + # Checkpoint 2: checkpoint_id='2' epoch=2 step=20 endpoint='projects/123456789012/locations/us-central1/endpoints/123456789012345' + + # [END googlegenaisdk_preference_tuning_job_create] + return tuning_job.name + + +if __name__ == "__main__": + create_tuning_job() diff --git a/genai/tuning/requirements.txt b/genai/tuning/requirements.txt index 1efe7b29dbc..e5fdb322ca4 100644 --- a/genai/tuning/requirements.txt +++ b/genai/tuning/requirements.txt @@ -1 +1 @@ -google-genai==1.42.0 +google-genai==1.47.0 diff --git a/genai/tuning/test_tuning_examples.py b/genai/tuning/test_tuning_examples.py index c0e6ec2864d..25b46402622 100644 --- a/genai/tuning/test_tuning_examples.py +++ b/genai/tuning/test_tuning_examples.py @@ -20,6 +20,7 @@ from google.genai import types import pytest +import preference_tuning_job_create import tuning_job_create import tuning_job_get import tuning_job_list @@ -327,3 +328,23 @@ def test_tuning_with_pretuned_model(mock_genai_client: MagicMock) -> None: mock_genai_client.assert_called_once_with(http_options=types.HttpOptions(api_version="v1beta1")) mock_genai_client.return_value.tunings.tune.assert_called_once() assert response == "test-tuning-job" + + +@patch("google.genai.Client") +def test_preference_tuning_job_create(mock_genai_client: MagicMock) -> None: + # Mock the API response + mock_tuning_job = types.TuningJob( + name="test-tuning-job", + experiment="test-experiment", + tuned_model=types.TunedModel( + model="test-model", + endpoint="test-endpoint" + ) + ) + mock_genai_client.return_value.tunings.tune.return_value = mock_tuning_job + + response = preference_tuning_job_create.create_tuning_job() + + mock_genai_client.assert_called_once_with(http_options=types.HttpOptions(api_version="v1")) + mock_genai_client.return_value.tunings.tune.assert_called_once() + assert response == "test-tuning-job" diff --git a/genai/video_generation/videogen_with_reference.py b/genai/video_generation/videogen_with_reference.py index 74f03afa68b..6543530ff9d 100644 --- a/genai/video_generation/videogen_with_reference.py +++ b/genai/video_generation/videogen_with_reference.py @@ -26,18 +26,18 @@ def generate_videos_from_reference(output_gcs_uri: str) -> str: operation = client.models.generate_videos( model="veo-3.1-generate-preview", - prompt="slowly rotate this coffee mug in a 360 degree circle", + prompt="A person walks in carrying a vase full of flowers and places the vase on a kitchen table.", config=GenerateVideosConfig( reference_images=[ VideoGenerationReferenceImage( image=Image( - gcs_uri="gs://cloud-samples-data/generative-ai/image/mug.png", + gcs_uri="gs://cloud-samples-data/generative-ai/image/vase.png", mime_type="image/png", ), reference_type="asset", ), ], - aspect_ratio="16:9", + aspect_ratio="9:16", output_gcs_uri=output_gcs_uri, ), ) diff --git a/genai/video_generation/videogen_with_vid.py b/genai/video_generation/videogen_with_vid.py index b28fa3b73aa..efcd63bcb4b 100644 --- a/genai/video_generation/videogen_with_vid.py +++ b/genai/video_generation/videogen_with_vid.py @@ -25,14 +25,13 @@ def generate_videos_from_video(output_gcs_uri: str) -> str: # output_gcs_uri = "gs://your-bucket/your-prefix" operation = client.models.generate_videos( - model="veo-2.0-generate-001", + model="veo-3.1-generate-preview", prompt="a butterfly flies in and lands on the flower", video=Video( uri="gs://cloud-samples-data/generative-ai/video/flower.mp4", mime_type="video/mp4", ), config=GenerateVideosConfig( - aspect_ratio="16:9", output_gcs_uri=output_gcs_uri, ), ) diff --git a/generative_ai/labels/requirements.txt b/generative_ai/labels/requirements.txt index 913473b5ef0..44964bbf7b1 100644 --- a/generative_ai/labels/requirements.txt +++ b/generative_ai/labels/requirements.txt @@ -1 +1 @@ -google-cloud-aiplatform==1.74.0 +google-cloud-aiplatform==1.133.0 diff --git a/iap/app_engine_app/requirements.txt b/iap/app_engine_app/requirements.txt index f306f93a9ca..3954d17e732 100644 --- a/iap/app_engine_app/requirements.txt +++ b/iap/app_engine_app/requirements.txt @@ -1,2 +1,2 @@ -Flask==3.0.3 -Werkzeug==3.0.3 +Flask==3.1.3 +Werkzeug==3.1.4 diff --git a/iap/requirements.txt b/iap/requirements.txt index 3c2961ba6a2..c0d103f39e4 100644 --- a/iap/requirements.txt +++ b/iap/requirements.txt @@ -1,9 +1,9 @@ cryptography==45.0.1 -Flask==3.0.3 +Flask==3.1.3 google-auth==2.38.0 gunicorn==23.0.0 requests==2.32.4 requests-toolbelt==1.0.0 -Werkzeug==3.0.6 +Werkzeug==3.1.4 google-cloud-iam~=2.17.0 PyJWT~=2.10.1 \ No newline at end of file diff --git a/kms/snippets/delete_key.py b/kms/snippets/delete_key.py new file mode 100644 index 00000000000..512e3df6a42 --- /dev/null +++ b/kms/snippets/delete_key.py @@ -0,0 +1,54 @@ +# Copyright 2026 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. + +# [START kms_delete_key] +from google.cloud import kms + + +def delete_key( + project_id: str, location_id: str, key_ring_id: str, key_id: str +) -> None: + """ + Delete the given key. This action is permanent and cannot be undone. Once the + key is deleted, it will no longer exist. + + Args: + project_id (str): Google Cloud project ID (e.g. 'my-project'). + location_id (str): Cloud KMS location (e.g. 'us-east1'). + key_ring_id (str): ID of the Cloud KMS key ring (e.g. 'my-key-ring'). + key_id (str): ID of the key to use (e.g. 'my-key'). + + Returns: + None + + """ + + # Create the client. + client = kms.KeyManagementServiceClient() + + # Build the key name. + key_name = client.crypto_key_path(project_id, location_id, key_ring_id, key_id) + + # Call the API. + # Note: delete_crypto_key returns a long-running operation. + # Warning: This operation is permanent and cannot be undone. + operation = client.delete_crypto_key(request={"name": key_name}) + + # Wait for the operation to complete. + operation.result() + + print(f"Deleted key: {key_name}") + + +# [END kms_delete_key] diff --git a/kms/snippets/delete_key_version.py b/kms/snippets/delete_key_version.py new file mode 100644 index 00000000000..669de9afbd6 --- /dev/null +++ b/kms/snippets/delete_key_version.py @@ -0,0 +1,57 @@ +# Copyright 2026 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. + +# [START kms_delete_key_version] +from google.cloud import kms + + +def delete_key_version( + project_id: str, location_id: str, key_ring_id: str, key_id: str, version_id: str +) -> None: + """ + Delete the given key version. This action is permanent and cannot be undone. + Once the key version is deleted, it will no longer exist. + + Args: + project_id (str): Google Cloud project ID (e.g. 'my-project'). + location_id (str): Cloud KMS location (e.g. 'us-east1'). + key_ring_id (str): ID of the Cloud KMS key ring (e.g. 'my-key-ring'). + key_id (str): ID of the key to use (e.g. 'my-key'). + version_id (str): ID of the key version to delete (e.g. '1'). + + Returns: + None + + """ + + # Create the client. + client = kms.KeyManagementServiceClient() + + # Build the key version name. + key_version_name = client.crypto_key_version_path( + project_id, location_id, key_ring_id, key_id, version_id + ) + + # Call the API. + # Note: delete_crypto_key_version returns a long-running operation. + # Warning: This operation is permanent and cannot be undone. + operation = client.delete_crypto_key_version(request={"name": key_version_name}) + + # Wait for the operation to complete. + operation.result() + + print(f"Deleted key version: {key_version_name}") + + +# [END kms_delete_key_version] diff --git a/kms/snippets/get_retired_resource.py b/kms/snippets/get_retired_resource.py new file mode 100644 index 00000000000..48042d7fa9f --- /dev/null +++ b/kms/snippets/get_retired_resource.py @@ -0,0 +1,50 @@ +# Copyright 2026 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. + +# [START kms_get_retired_resource] +from google.cloud import kms + + +def get_retired_resource( + project_id: str, location_id: str, retired_resource_id: str +) -> kms.RetiredResource: + """ + Get the details of a retired resource. + + Args: + project_id (str): Google Cloud project ID (e.g. 'my-project'). + location_id (str): Cloud KMS location (e.g. 'us-east1'). + resource_id (str): ID of the retired resource to get. + + Returns: + kms.RetiredResource: The requested retired resource. + + """ + + # Create the client. + client = kms.KeyManagementServiceClient() + + # Build the retired resource name. + # Note: Retired resources are tied to a Location, not a KeyRing. + # The name is like projects/{project}/locations/{location}/retiredResources/{id} + name = client.retired_resource_path(project_id, location_id, retired_resource_id) + + # Call the API. + response = client.get_retired_resource(request={"name": name}) + + print(f"Got retired resource: {response.name}") + return response + + +# [END kms_get_retired_resource] diff --git a/kms/snippets/list_retired_resources.py b/kms/snippets/list_retired_resources.py new file mode 100644 index 00000000000..9393b34de1c --- /dev/null +++ b/kms/snippets/list_retired_resources.py @@ -0,0 +1,50 @@ +# Copyright 2026 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. + +# [START kms_list_retired_resources] +from typing import List + +from google.cloud import kms + + +def list_retired_resources(project_id: str, location_id: str) -> List[kms.RetiredResource]: + """ + List the retired resources in a location. + + Args: + project_id (str): Google Cloud project ID (e.g. 'my-project'). + location_id (str): Cloud KMS location (e.g. 'us-east1'). + + Returns: + list[kms.RetiredResource]: The list of retired resources. + """ + + # Create the client. + client = kms.KeyManagementServiceClient() + + # Build the parent location name. + parent = client.common_location_path(project_id, location_id) + + # Call the API. + # The API paginates, but the Python client library handles that for us. + resources_list = list(client.list_retired_resources(request={"parent": parent})) + + # Iterate over the resources and print them. + for resource in resources_list: + print(f"Retired resource: {resource.name}") + + return resources_list + + +# [END kms_list_retired_resources] diff --git a/kms/snippets/requirements.txt b/kms/snippets/requirements.txt index 6e15391cfd6..167c2a25011 100644 --- a/kms/snippets/requirements.txt +++ b/kms/snippets/requirements.txt @@ -1,4 +1,4 @@ -google-cloud-kms==3.2.1 +google-cloud-kms==3.11.0 cryptography==45.0.1 crcmod==1.7 jwcrypto==1.5.6 \ No newline at end of file diff --git a/kms/snippets/snippets_test.py b/kms/snippets/snippets_test.py index 970cf13dfe6..002ab499269 100644 --- a/kms/snippets/snippets_test.py +++ b/kms/snippets/snippets_test.py @@ -52,6 +52,7 @@ from create_key_version import create_key_version from decrypt_asymmetric import decrypt_asymmetric from decrypt_symmetric import decrypt_symmetric +from delete_key import delete_key from destroy_key_version import destroy_key_version from disable_key_version import disable_key_version from enable_key_version import enable_key_version @@ -62,10 +63,12 @@ from get_key_version_attestation import get_key_version_attestation from get_public_key import get_public_key from get_public_key_jwk import get_public_key_jwk +from get_retired_resource import get_retired_resource from iam_add_member import iam_add_member from iam_get_policy import iam_get_policy from iam_remove_member import iam_remove_member from import_manually_wrapped_key import import_manually_wrapped_key +from list_retired_resources import list_retired_resources from quickstart import quickstart from restore_key_version import restore_key_version from sign_asymmetric import sign_asymmetric @@ -886,3 +889,41 @@ def test_verify_mac( def test_quickstart(project_id: str, location_id: str) -> None: key_rings = quickstart(project_id, location_id) assert key_rings + + +def test_delete_key_and_retired_resources( + client: kms.KeyManagementServiceClient, + project_id: str, + location_id: str, + key_ring_id: str, +) -> None: + # We can test key deletion and retired resources by first creating a key. + key_id = f"delete-key-{uuid.uuid4()}" + key_ring_name = client.key_ring_path(project_id, location_id, key_ring_id) + key = client.create_crypto_key( + request={ + "parent": key_ring_name, + "crypto_key_id": key_id, + "crypto_key": { + "purpose": kms.CryptoKey.CryptoKeyPurpose.ENCRYPT_DECRYPT, + }, + "skip_initial_version_creation": True, + } + ) + + # Delete the key. + delete_key(project_id, location_id, key_ring_id, key_id) + + # List retired resources and filter to just our deleted key. + all_retired = list_retired_resources(project_id, location_id) + filtered_retired = [r for r in all_retired if r.original_resource == key.name] + + # Make sure the len is 1 + assert len(filtered_retired) == 1 + + # Get the retired resource + resource_id = filtered_retired[0].name.split("/")[-1] + retrieved = get_retired_resource(project_id, location_id, resource_id) + + # See if the result is the same as retired resource list[0] + assert retrieved.name == filtered_retired[0].name diff --git a/kubernetes_engine/django_tutorial/requirements.txt b/kubernetes_engine/django_tutorial/requirements.txt index 0c01249d943..df3b50126a0 100644 --- a/kubernetes_engine/django_tutorial/requirements.txt +++ b/kubernetes_engine/django_tutorial/requirements.txt @@ -1,5 +1,4 @@ -Django==5.2.5; python_version >= "3.10" -Django==4.2.24; python_version >= "3.8" and python_version < "3.10" +Django==6.0.1; python_version >= "3.12" # Uncomment the mysqlclient requirement if you are using MySQL rather than # PostgreSQL. You must also have a MySQL client installed in that case. #mysqlclient==1.4.1 @@ -7,4 +6,4 @@ wheel==0.40.0 gunicorn==23.0.0; python_version > '3.0' gunicorn==23.0.0; python_version < '3.0' # psycopg2==2.8.4 # uncomment if you prefer to build from source -psycopg2-binary==2.9.10 +psycopg2-binary==2.9.11 diff --git a/language/snippets/generated-samples/v1/language_sentiment_text.py b/language/snippets/generated-samples/v1/language_sentiment_text.py deleted file mode 100644 index 81b738f1395..00000000000 --- a/language/snippets/generated-samples/v1/language_sentiment_text.py +++ /dev/null @@ -1,55 +0,0 @@ -# -# Copyright 2018 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 -# -# https://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. - -# DO NOT EDIT! This is a generated sample ("Request", "analyze_sentiment") - -# To install the latest published package dependency, execute the following: -# pip install google-cloud-language - -import sys - -# isort: split -# [START language_sentiment_text] - -from google.cloud import language_v1 - - -def sample_analyze_sentiment(content): - client = language_v1.LanguageServiceClient() - - # content = 'Your text to analyze, e.g. Hello, world!' - - if isinstance(content, bytes): - content = content.decode("utf-8") - - type_ = language_v1.Document.Type.PLAIN_TEXT - document = {"type_": type_, "content": content} - - response = client.analyze_sentiment(request={"document": document}) - sentiment = response.document_sentiment - print(f"Score: {sentiment.score}") - print(f"Magnitude: {sentiment.magnitude}") - - -# [END language_sentiment_text] - - -def main(): - # FIXME: Convert argv from strings to the correct types. - sample_analyze_sentiment(*sys.argv[1:]) - - -if __name__ == "__main__": - main() diff --git a/language/snippets/generated-samples/v1/language_sentiment_text_test.py b/language/snippets/generated-samples/v1/language_sentiment_text_test.py deleted file mode 100644 index cfa7199a1df..00000000000 --- a/language/snippets/generated-samples/v1/language_sentiment_text_test.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2018 Google, Inc. -# -# 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 language_sentiment_text - - -def test_analyze_sentiment_text_positive(capsys): - language_sentiment_text.sample_analyze_sentiment("Happy Happy Joy Joy") - out, _ = capsys.readouterr() - assert "Score: 0." in out - - -def test_analyze_sentiment_text_negative(capsys): - language_sentiment_text.sample_analyze_sentiment("Angry Angry Sad Sad") - out, _ = capsys.readouterr() - assert "Score: -0." in out diff --git a/language/snippets/generated-samples/v1/requirements-test.txt b/language/snippets/generated-samples/v1/requirements-test.txt deleted file mode 100644 index 15d066af319..00000000000 --- a/language/snippets/generated-samples/v1/requirements-test.txt +++ /dev/null @@ -1 +0,0 @@ -pytest==8.2.0 diff --git a/language/snippets/generated-samples/v1/requirements.txt b/language/snippets/generated-samples/v1/requirements.txt deleted file mode 100644 index b432a6e4238..00000000000 --- a/language/snippets/generated-samples/v1/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -google-cloud-language==2.15.1 diff --git a/logging/cloud-client/README.rst b/logging/cloud-client/README.rst deleted file mode 100644 index 4ddc91a754f..00000000000 --- a/logging/cloud-client/README.rst +++ /dev/null @@ -1,3 +0,0 @@ -These samples have been moved. - -https://github.com/googleapis/python-logging/tree/main/samples diff --git a/logging/samples/AUTHORING_GUIDE.md b/logging/samples/AUTHORING_GUIDE.md new file mode 100644 index 00000000000..8249522ffc2 --- /dev/null +++ b/logging/samples/AUTHORING_GUIDE.md @@ -0,0 +1 @@ +See https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/AUTHORING_GUIDE.md \ No newline at end of file diff --git a/logging/samples/CONTRIBUTING.md b/logging/samples/CONTRIBUTING.md new file mode 100644 index 00000000000..f5fe2e6baf1 --- /dev/null +++ b/logging/samples/CONTRIBUTING.md @@ -0,0 +1 @@ +See https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/CONTRIBUTING.md \ No newline at end of file diff --git a/logging/samples/snippets/README.rst.in b/logging/samples/snippets/README.rst.in new file mode 100644 index 00000000000..ff243c1ce81 --- /dev/null +++ b/logging/samples/snippets/README.rst.in @@ -0,0 +1,28 @@ +# This file is used to generate README.rst + +product: + name: Cloud Logging + short_name: Cloud Logging + url: https://cloud.google.com/logging/docs + description: > + `Cloud Logging`_ allows you to store, search, analyze, monitor, + and alert on log data and events from Google Cloud Platform and Amazon + Web Services. + +setup: +- auth +- install_deps + +samples: +- name: Quickstart + file: quickstart.py +- name: Snippets + file: snippets.py + show_help: true +- name: Export + file: export.py + show_help: true + +cloud_client_library: true + +folder: logging/cloud-client \ No newline at end of file diff --git a/logging/samples/snippets/export.py b/logging/samples/snippets/export.py new file mode 100644 index 00000000000..9a0673ee72d --- /dev/null +++ b/logging/samples/snippets/export.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python + +# Copyright 2016 Google Inc. All Rights Reserved. +# +# 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 argparse + +from google.cloud import logging + + +# [START logging_list_sinks] +def list_sinks(): + """Lists all sinks.""" + logging_client = logging.Client() + + sinks = list(logging_client.list_sinks()) + + if not sinks: + print("No sinks.") + + for sink in sinks: + print("{}: {} -> {}".format(sink.name, sink.filter_, sink.destination)) + + +# [END logging_list_sinks] + + +# [START logging_create_sink] +def create_sink(sink_name, destination_bucket, filter_): + """Creates a sink to export logs to the given Cloud Storage bucket. + + The filter determines which logs this sink matches and will be exported + to the destination. For example a filter of 'severity>=INFO' will send + all logs that have a severity of INFO or greater to the destination. + See https://cloud.google.com/logging/docs/view/advanced_filters for more + filter information. + """ + logging_client = logging.Client() + + # The destination can be a Cloud Storage bucket, a Cloud Pub/Sub topic, + # or a BigQuery dataset. In this case, it is a Cloud Storage Bucket. + # See https://cloud.google.com/logging/docs/api/tasks/exporting-logs for + # information on the destination format. + destination = "storage.googleapis.com/{bucket}".format(bucket=destination_bucket) + + sink = logging_client.sink(sink_name, filter_=filter_, destination=destination) + + if sink.exists(): + print("Sink {} already exists.".format(sink.name)) + return + + sink.create() + print("Created sink {}".format(sink.name)) + + +# [END logging_create_sink] + + +# [START logging_update_sink] +def update_sink(sink_name, filter_): + """Changes a sink's filter. + + The filter determines which logs this sink matches and will be exported + to the destination. For example a filter of 'severity>=INFO' will send + all logs that have a severity of INFO or greater to the destination. + See https://cloud.google.com/logging/docs/view/advanced_filters for more + filter information. + """ + logging_client = logging.Client() + sink = logging_client.sink(sink_name) + + sink.reload() + + sink.filter_ = filter_ + print("Updated sink {}".format(sink.name)) + sink.update() + + +# [END logging_update_sink] + + +# [START logging_delete_sink] +def delete_sink(sink_name): + """Deletes a sink.""" + logging_client = logging.Client() + sink = logging_client.sink(sink_name) + + sink.delete() + + print("Deleted sink {}".format(sink.name)) + + +# [END logging_delete_sink] + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter + ) + + subparsers = parser.add_subparsers(dest="command") + subparsers.add_parser("list", help=list_sinks.__doc__) + + create_parser = subparsers.add_parser("create", help=list_sinks.__doc__) + create_parser.add_argument("sink_name", help="Name of the log export sink.") + create_parser.add_argument( + "destination_bucket", help="Cloud Storage bucket where logs will be exported." + ) + create_parser.add_argument("filter", help="The filter used to match logs.") + + update_parser = subparsers.add_parser("update", help=update_sink.__doc__) + update_parser.add_argument("sink_name", help="Name of the log export sink.") + update_parser.add_argument("filter", help="The filter used to match logs.") + + delete_parser = subparsers.add_parser("delete", help=delete_sink.__doc__) + delete_parser.add_argument("sink_name", help="Name of the log export sink.") + + args = parser.parse_args() + + if args.command == "list": + list_sinks() + elif args.command == "create": + create_sink(args.sink_name, args.destination_bucket, args.filter) + elif args.command == "update": + update_sink(args.sink_name, args.filter) + elif args.command == "delete": + delete_sink(args.sink_name) diff --git a/logging/samples/snippets/export_test.py b/logging/samples/snippets/export_test.py new file mode 100644 index 00000000000..e7dacd49ee4 --- /dev/null +++ b/logging/samples/snippets/export_test.py @@ -0,0 +1,135 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# 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 os +import random +import re +import string +import time + +import backoff +from google.cloud import logging, storage +import pytest + +import export + + +BUCKET = os.environ["CLOUD_STORAGE_BUCKET"] +TEST_SINK_NAME_TMPL = "example_sink_{}_{}" +TEST_SINK_FILTER = "severity>=CRITICAL" +TIMESTAMP = int(time.time()) + +# Threshold beyond which the cleanup_old_sinks fixture will delete +# old sink, in seconds +CLEANUP_THRESHOLD = 7200 # 2 hours + +# Max buckets to delete at a time, to mitigate operation timeout +# issues. To turn off in the future, set to None. +MAX_BUCKETS = 1500 + + +def _random_id(): + return "".join( + random.choice(string.ascii_uppercase + string.digits) for _ in range(6) + ) + + +def _create_sink_name(): + return TEST_SINK_NAME_TMPL.format(TIMESTAMP, _random_id()) + + +@backoff.on_exception(backoff.expo, Exception, max_time=60, raise_on_giveup=False) +def _delete_object(obj, **kwargs): + obj.delete(**kwargs) + + +# Runs once for entire test suite +@pytest.fixture(scope="module") +def cleanup_old_sinks(): + client = logging.Client() + test_sink_name_regex = ( + r"^" + TEST_SINK_NAME_TMPL.format(r"(\d+)", r"[A-Z0-9]{6}") + r"$" + ) + for sink in client.list_sinks(): + match = re.match(test_sink_name_regex, sink.name) + if match: + sink_timestamp = int(match.group(1)) + if TIMESTAMP - sink_timestamp > CLEANUP_THRESHOLD: + _delete_object(sink) + + storage_client = storage.Client() + + # See _sink_storage_setup in usage_guide.py for details about how + # sinks are named. + test_bucket_name_regex = r"^sink\-storage\-(\d+)$" + for bucket in storage_client.list_buckets(max_results=MAX_BUCKETS): + match = re.match(test_bucket_name_regex, bucket.name) + if match: + # Bucket timestamp is int(time.time() * 1000) + bucket_timestamp = int(match.group(1)) + if TIMESTAMP - bucket_timestamp // 1000 > CLEANUP_THRESHOLD: + _delete_object(bucket, force=True) + + +@pytest.fixture +def example_sink(cleanup_old_sinks): + client = logging.Client() + + sink = client.sink( + _create_sink_name(), + filter_=TEST_SINK_FILTER, + destination="storage.googleapis.com/{bucket}".format(bucket=BUCKET), + ) + + sink.create() + + yield sink + + _delete_object(sink) + + +def test_list(example_sink, capsys): + @backoff.on_exception(backoff.expo, AssertionError, max_time=60) + def eventually_consistent_test(): + export.list_sinks() + out, _ = capsys.readouterr() + assert example_sink.name in out + + eventually_consistent_test() + + +def test_create(capsys): + sink_name = _create_sink_name() + + try: + export.create_sink(sink_name, BUCKET, TEST_SINK_FILTER) + # Clean-up the temporary sink. + finally: + _delete_object(logging.Client().sink(sink_name)) + + out, _ = capsys.readouterr() + assert sink_name in out + + +def test_update(example_sink, capsys): + updated_filter = "severity>=INFO" + export.update_sink(example_sink.name, updated_filter) + + example_sink.reload() + assert example_sink.filter_ == updated_filter + + +def test_delete(example_sink, capsys): + export.delete_sink(example_sink.name) + assert not example_sink.exists() diff --git a/logging/samples/snippets/handler.py b/logging/samples/snippets/handler.py new file mode 100644 index 00000000000..49d2578984f --- /dev/null +++ b/logging/samples/snippets/handler.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python + +# Copyright 2016 Google Inc. All Rights Reserved. +# +# 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. + + +def use_logging_handler(): + # [START logging_stdlogging] + # [START logging_handler_setup] + # Imports the Cloud Logging client library + import google.cloud.logging + + # Instantiates a client + client = google.cloud.logging.Client() + + # Retrieves a Cloud Logging handler based on the environment + # you're running in and integrates the handler with the + # Python logging module. By default this captures all logs + # at INFO level and higher + client.setup_logging() + # [END logging_handler_setup] + + # [START logging_handler_usage] + # Imports Python standard library logging + import logging + + # The data to log + text = "Hello, world!" + + # Emits the data using the standard logging module + logging.warning(text) + # [END logging_handler_usage] + + print("Logged: {}".format(text)) + # [END logging_stdlogging] + + +if __name__ == "__main__": + use_logging_handler() diff --git a/appengine/flexible_python37_and_earlier/django_cloudsql/polls/admin.py b/logging/samples/snippets/handler_test.py similarity index 74% rename from appengine/flexible_python37_and_earlier/django_cloudsql/polls/admin.py rename to logging/samples/snippets/handler_test.py index 5fc6d71455b..9d635806ae1 100644 --- a/appengine/flexible_python37_and_earlier/django_cloudsql/polls/admin.py +++ b/logging/samples/snippets/handler_test.py @@ -1,4 +1,4 @@ -# Copyright 2015 Google LLC. +# Copyright 2016 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,8 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from django.contrib import admin -from .models import Question +import handler -admin.site.register(Question) + +def test_handler(capsys): + handler.use_logging_handler() + out, _ = capsys.readouterr() + assert "Logged" in out diff --git a/appengine/flexible_python37_and_earlier/django_cloudsql/manage.py b/logging/samples/snippets/quickstart.py old mode 100755 new mode 100644 similarity index 50% rename from appengine/flexible_python37_and_earlier/django_cloudsql/manage.py rename to logging/samples/snippets/quickstart.py index 89fb5ae5607..7c38ea6fa82 --- a/appengine/flexible_python37_and_earlier/django_cloudsql/manage.py +++ b/logging/samples/snippets/quickstart.py @@ -1,5 +1,6 @@ #!/usr/bin/env python -# Copyright 2015 Google LLC. + +# Copyright 2016 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,12 +14,29 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os -import sys -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") +def run_quickstart(): + # [START logging_quickstart] + # Imports the Google Cloud client library + from google.cloud import logging + + # Instantiates a client + logging_client = logging.Client() + + # The name of the log to write to + log_name = "my-log" + # Selects the log to write to + logger = logging_client.logger(log_name) - from django.core.management import execute_from_command_line + # The data to log + text = "Hello, world!" - execute_from_command_line(sys.argv) + # Writes the log entry + logger.log_text(text) + + print("Logged: {}".format(text)) + # [END logging_quickstart] + + +if __name__ == "__main__": + run_quickstart() diff --git a/appengine/flexible_python37_and_earlier/django_cloudsql/polls/urls.py b/logging/samples/snippets/quickstart_test.py similarity index 74% rename from appengine/flexible_python37_and_earlier/django_cloudsql/polls/urls.py rename to logging/samples/snippets/quickstart_test.py index ca52d749043..d8ace2cbcf3 100644 --- a/appengine/flexible_python37_and_earlier/django_cloudsql/polls/urls.py +++ b/logging/samples/snippets/quickstart_test.py @@ -1,4 +1,4 @@ -# Copyright 2015 Google LLC. +# Copyright 2016 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,10 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from django.urls import path -from . import views +import quickstart -urlpatterns = [ - path("", views.index, name="index"), -] + +def test_quickstart(capsys): + quickstart.run_quickstart() + out, _ = capsys.readouterr() + assert "Logged" in out diff --git a/logging/samples/snippets/requirements-test.txt b/logging/samples/snippets/requirements-test.txt new file mode 100644 index 00000000000..37eb1f9aa7a --- /dev/null +++ b/logging/samples/snippets/requirements-test.txt @@ -0,0 +1,3 @@ +backoff==2.2.1 +pytest===7.4.4; python_version == '3.7' +pytest==8.2.2; python_version >= '3.8' diff --git a/logging/samples/snippets/requirements.txt b/logging/samples/snippets/requirements.txt new file mode 100644 index 00000000000..65b84840d38 --- /dev/null +++ b/logging/samples/snippets/requirements.txt @@ -0,0 +1,4 @@ +google-cloud-logging==3.13.0 +google-cloud-bigquery==3.40.1 +google-cloud-storage==3.7.0 +google-cloud-pubsub==2.35.0 diff --git a/logging/samples/snippets/snippets.py b/logging/samples/snippets/snippets.py new file mode 100644 index 00000000000..f6c16d17e38 --- /dev/null +++ b/logging/samples/snippets/snippets.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python + +# Copyright 2016 Google Inc. All Rights Reserved. +# +# 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. + +"""This application demonstrates how to perform basic operations on logs and +log entries with Cloud Logging. + +For more information, see the README.md under /logging and the +documentation at https://cloud.google.com/logging/docs. +""" + +import argparse + +from google.cloud import logging + + +# [START logging_write_log_entry] +def write_entry(logger_name): + """Writes log entries to the given logger.""" + logging_client = logging.Client() + + # This log can be found in the Cloud Logging console under 'Custom Logs'. + logger = logging_client.logger(logger_name) + + # Make a simple text log + logger.log_text("Hello, world!") + + # Simple text log with severity. + logger.log_text("Goodbye, world!", severity="WARNING") + + # Struct log. The struct can be any JSON-serializable dictionary. + logger.log_struct( + { + "name": "King Arthur", + "quest": "Find the Holy Grail", + "favorite_color": "Blue", + }, + severity="INFO", + ) + + print("Wrote logs to {}.".format(logger.name)) + + +# [END logging_write_log_entry] + + +# [START logging_list_log_entries] +def list_entries(logger_name): + """Lists the most recent entries for a given logger.""" + logging_client = logging.Client() + logger = logging_client.logger(logger_name) + + print("Listing entries for logger {}:".format(logger.name)) + + for entry in logger.list_entries(): + timestamp = entry.timestamp.isoformat() + print("* {}: {}".format(timestamp, entry.payload)) + + +# [END logging_list_log_entries] + + +# [START logging_delete_log] +def delete_logger(logger_name): + """Deletes a logger and all its entries. + + Note that a deletion can take several minutes to take effect. + """ + logging_client = logging.Client() + logger = logging_client.logger(logger_name) + + logger.delete() + + print("Deleted all logging entries for {}".format(logger.name)) + + +# [END logging_delete_log] + + +if __name__ == "__main__": + + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument("logger_name", help="Logger name", default="example_log") + subparsers = parser.add_subparsers(dest="command") + subparsers.add_parser("list", help=list_entries.__doc__) + subparsers.add_parser("write", help=write_entry.__doc__) + subparsers.add_parser("delete", help=delete_logger.__doc__) + + args = parser.parse_args() + + if args.command == "list": + list_entries(args.logger_name) + elif args.command == "write": + write_entry(args.logger_name) + elif args.command == "delete": + delete_logger(args.logger_name) diff --git a/logging/samples/snippets/snippets_test.py b/logging/samples/snippets/snippets_test.py new file mode 100644 index 00000000000..5cddc92d313 --- /dev/null +++ b/logging/samples/snippets/snippets_test.py @@ -0,0 +1,68 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# 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 uuid + +import backoff +from google.api_core.exceptions import NotFound +from google.cloud import logging +import pytest + +import snippets + + +TEST_LOGGER_NAME = "example_log_{}".format(uuid.uuid4().hex) +TEST_TEXT = "Hello, world." + + +@pytest.fixture +def example_log(): + client = logging.Client() + logger = client.logger(TEST_LOGGER_NAME) + text = "Hello, world." + logger.log_text(text) + return text + + +def test_list(example_log, capsys): + @backoff.on_exception(backoff.expo, AssertionError, max_time=120) + def eventually_consistent_test(): + snippets.list_entries(TEST_LOGGER_NAME) + out, _ = capsys.readouterr() + assert example_log in out + + eventually_consistent_test() + + +def test_write(capsys): + + snippets.write_entry(TEST_LOGGER_NAME) + + @backoff.on_exception(backoff.expo, AssertionError, max_time=120) + def eventually_consistent_test(): + snippets.list_entries(TEST_LOGGER_NAME) + out, _ = capsys.readouterr() + assert TEST_TEXT in out + + eventually_consistent_test() + + +def test_delete(example_log, capsys): + @backoff.on_exception(backoff.expo, NotFound, max_time=120) + def eventually_consistent_test(): + snippets.delete_logger(TEST_LOGGER_NAME) + out, _ = capsys.readouterr() + assert TEST_LOGGER_NAME in out + + eventually_consistent_test() diff --git a/noxfile-template.py b/noxfile-template.py index 93b0186aedd..09bd81c1b77 100644 --- a/noxfile-template.py +++ b/noxfile-template.py @@ -88,7 +88,7 @@ def get_pytest_env_vars() -> dict[str, str]: # All versions used to tested samples. -ALL_VERSIONS = ["2.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] +ALL_VERSIONS = ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] # Any default versions that should be ignored. IGNORED_VERSIONS = TEST_CONFIG["ignored_versions"] diff --git a/people-and-planet-ai/conftest.py b/people-and-planet-ai/conftest.py index fed54feb9b9..1bf49d26a00 100644 --- a/people-and-planet-ai/conftest.py +++ b/people-and-planet-ai/conftest.py @@ -84,7 +84,7 @@ def bucket_name(test_name: str, location: str, unique_id: str) -> Iterable[str]: # Try to remove all files before deleting the bucket. # Deleting a bucket with too many files results in an error. try: - run_cmd("gsutil", "-m", "rm", "-rf", f"gs://{bucket_name}/*") + run_cmd("gcloud", "storage", "rm", "--recursive", f"gs://{bucket_name}/**") except RuntimeError: # If no files were found and it fails, ignore the error. pass diff --git a/people-and-planet-ai/weather-forecasting/serving/weather-model/pyproject.toml b/people-and-planet-ai/weather-forecasting/serving/weather-model/pyproject.toml index 6f6c66d33a9..43c03683ccd 100644 --- a/people-and-planet-ai/weather-forecasting/serving/weather-model/pyproject.toml +++ b/people-and-planet-ai/weather-forecasting/serving/weather-model/pyproject.toml @@ -19,7 +19,7 @@ version = "1.0.0" dependencies = [ "datasets==4.0.0", "torch==2.8.0", # make sure this matches the `container_uri` in `notebooks/3-training.ipynb` - "transformers==4.48.0", + "transformers==5.0.0", ] [project.scripts] diff --git a/pubsublite/spark-connector/README.md b/pubsublite/spark-connector/README.md index c133fd66f64..cdef86589f7 100644 --- a/pubsublite/spark-connector/README.md +++ b/pubsublite/spark-connector/README.md @@ -54,7 +54,7 @@ Get the connector's uber jar from this [public Cloud Storage location]. Alternat ```bash export BUCKET_ID=your-gcs-bucket-id - gsutil mb gs://$BUCKET_ID + gcloud storage buckets create gs://$BUCKET_ID ``` ## Python setup diff --git a/run/django/cloudmigrate.yaml b/run/django/cloudmigrate.yaml index a054e0fc93c..8a6fc01b92b 100644 --- a/run/django/cloudmigrate.yaml +++ b/run/django/cloudmigrate.yaml @@ -15,7 +15,7 @@ # [START cloudrun_django_cloudmigrate_yaml_python] steps: - id: "Build Container Image" - name: buildpacksio/pack + name: gcr.io/k8s-skaffold/pack args: ["build", "${_IMAGE_NAME}", "--builder=gcr.io/buildpacks/builder"] - id: "Push Container Image" diff --git a/run/django/e2e_test_setup.yaml b/run/django/e2e_test_setup.yaml index 6ee69ce3f9f..dac968f03f6 100644 --- a/run/django/e2e_test_setup.yaml +++ b/run/django/e2e_test_setup.yaml @@ -69,7 +69,7 @@ steps: --project ${PROJECT_ID}" - id: "Build Container Image" - name: buildpacksio/pack + name: gcr.io/k8s-skaffold/pack args: ["build", "${_IMAGE_NAME}", "--builder=gcr.io/buildpacks/builder", "--env", "GOOGLE_PYTHON_VERSION=${_PYTHON_VERSION}"] diff --git a/run/helloworld/requirements.txt b/run/helloworld/requirements.txt index 664e38630a1..64cd68f0f5f 100644 --- a/run/helloworld/requirements.txt +++ b/run/helloworld/requirements.txt @@ -1,3 +1,3 @@ Flask==3.0.3 gunicorn==23.0.0 -Werkzeug==3.0.3 +Werkzeug==3.1.6 diff --git a/run/mcp-server/pyproject.toml b/run/mcp-server/pyproject.toml index 397102c991c..32fdf3743bb 100644 --- a/run/mcp-server/pyproject.toml +++ b/run/mcp-server/pyproject.toml @@ -5,5 +5,5 @@ description = "Example of deploying an MCP server on Cloud Run" readme = "README.md" requires-python = ">=3.10" dependencies = [ - "fastmcp==2.13.0", + "fastmcp==3.0.0", ] diff --git a/run/mcp-server/uv.lock b/run/mcp-server/uv.lock index c2f5c2de885..517177da22d 100644 --- a/run/mcp-server/uv.lock +++ b/run/mcp-server/uv.lock @@ -2,6 +2,18 @@ version = 1 revision = 3 requires-python = ">=3.10" +[[package]] +name = "aiofile" +version = "3.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "caio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/e2/d7cb819de8df6b5c1968a2756c3cb4122d4fa2b8fc768b53b7c9e5edb646/aiofile-3.9.0.tar.gz", hash = "sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b", size = 17943, upload-time = "2024-10-08T10:39:35.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/25/da1f0b4dd970e52bf5a36c204c107e11a0c6d3ed195eba0bfbc664c312b2/aiofile-3.9.0-py3-none-any.whl", hash = "sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa", size = 19539, upload-time = "2024-10-08T10:39:32.955Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -37,14 +49,14 @@ wheels = [ [[package]] name = "authlib" -version = "1.6.0" +version = "1.6.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a2/9d/b1e08d36899c12c8b894a44a5583ee157789f26fc4b176f8e4b6217b56e1/authlib-1.6.0.tar.gz", hash = "sha256:4367d32031b7af175ad3a323d571dc7257b7099d55978087ceae4a0d88cd3210", size = 158371, upload-time = "2025-05-23T00:21:45.011Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/6c/c88eac87468c607f88bc24df1f3b31445ee6fc9ba123b09e666adf687cd9/authlib-1.6.8.tar.gz", hash = "sha256:41ae180a17cf672bc784e4a518e5c82687f1fe1e98b0cafaeda80c8e4ab2d1cb", size = 165074, upload-time = "2026-02-14T04:02:17.941Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/29/587c189bbab1ccc8c86a03a5d0e13873df916380ef1be461ebe6acebf48d/authlib-1.6.0-py2.py3-none-any.whl", hash = "sha256:91685589498f79e8655e8a8947431ad6288831d643f11c55c2143ffcc738048d", size = 239981, upload-time = "2025-05-23T00:21:43.075Z" }, + { url = "https://files.pythonhosted.org/packages/9b/73/f7084bf12755113cd535ae586782ff3a6e710bfbe6a0d13d1c2f81ffbbfa/authlib-1.6.8-py2.py3-none-any.whl", hash = "sha256:97286fd7a15e6cfefc32771c8ef9c54f0ed58028f1322de6a2a7c969c3817888", size = 244116, upload-time = "2026-02-14T04:02:15.579Z" }, ] [[package]] @@ -74,6 +86,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/96/c5/1e741d26306c42e2bf6ab740b2202872727e0f606033c9dd713f8b93f5a8/cachetools-6.2.1-py3-none-any.whl", hash = "sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701", size = 11280, upload-time = "2025-10-12T14:55:28.382Z" }, ] +[[package]] +name = "caio" +version = "0.9.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/88/b8527e1b00c1811db339a1df8bd1ae49d146fcea9d6a5c40e3a80aaeb38d/caio-0.9.25.tar.gz", hash = "sha256:16498e7f81d1d0f5a4c0ad3f2540e65fe25691376e0a5bd367f558067113ed10", size = 26781, upload-time = "2025-12-26T15:21:36.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/80/ea4ead0c5d52a9828692e7df20f0eafe8d26e671ce4883a0a146bb91049e/caio-0.9.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ca6c8ecda611478b6016cb94d23fd3eb7124852b985bdec7ecaad9f3116b9619", size = 36836, upload-time = "2025-12-26T15:22:04.662Z" }, + { url = "https://files.pythonhosted.org/packages/17/b9/36715c97c873649d1029001578f901b50250916295e3dddf20c865438865/caio-0.9.25-cp310-cp310-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db9b5681e4af8176159f0d6598e73b2279bb661e718c7ac23342c550bd78c241", size = 79695, upload-time = "2025-12-26T15:22:18.818Z" }, + { url = "https://files.pythonhosted.org/packages/ec/90/543f556fcfcfa270713eef906b6352ab048e1e557afec12925c991dc93c2/caio-0.9.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d6956d9e4a27021c8bd6c9677f3a59eb1d820cc32d0343cea7961a03b1371965", size = 36839, upload-time = "2025-12-26T15:21:40.267Z" }, + { url = "https://files.pythonhosted.org/packages/51/3b/36f3e8ec38dafe8de4831decd2e44c69303d2a3892d16ceda42afed44e1b/caio-0.9.25-cp311-cp311-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bf84bfa039f25ad91f4f52944452a5f6f405e8afab4d445450978cd6241d1478", size = 80255, upload-time = "2025-12-26T15:22:20.271Z" }, + { url = "https://files.pythonhosted.org/packages/d3/25/79c98ebe12df31548ba4eaf44db11b7cad6b3e7b4203718335620939083c/caio-0.9.25-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fb7ff95af4c31ad3f03179149aab61097a71fd85e05f89b4786de0359dffd044", size = 36983, upload-time = "2025-12-26T15:21:36.075Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2b/21288691f16d479945968a0a4f2856818c1c5be56881d51d4dac9b255d26/caio-0.9.25-cp312-cp312-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:97084e4e30dfa598449d874c4d8e0c8d5ea17d2f752ef5e48e150ff9d240cd64", size = 82012, upload-time = "2025-12-26T15:22:20.983Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/5e6ff127e6f62c9f15d989560435c642144aa4210882f9494204bc892305/caio-0.9.25-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6c2a3411af97762a2b03840c3cec2f7f728921ff8adda53d7ea2315a8563451", size = 36979, upload-time = "2025-12-26T15:21:35.484Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9f/f21af50e72117eb528c422d4276cbac11fb941b1b812b182e0a9c70d19c5/caio-0.9.25-cp313-cp313-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0998210a4d5cd5cb565b32ccfe4e53d67303f868a76f212e002a8554692870e6", size = 81900, upload-time = "2025-12-26T15:22:21.919Z" }, + { url = "https://files.pythonhosted.org/packages/69/ca/a08fdc7efdcc24e6a6131a93c85be1f204d41c58f474c42b0670af8c016b/caio-0.9.25-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fab6078b9348e883c80a5e14b382e6ad6aabbc4429ca034e76e730cf464269db", size = 36978, upload-time = "2025-12-26T15:21:41.055Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6c/d4d24f65e690213c097174d26eda6831f45f4734d9d036d81790a27e7b78/caio-0.9.25-cp314-cp314-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44a6b58e52d488c75cfaa5ecaa404b2b41cc965e6c417e03251e868ecd5b6d77", size = 81832, upload-time = "2025-12-26T15:22:22.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" }, +] + [[package]] name = "certifi" version = "2025.4.26" @@ -314,15 +345,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/63/8e5ab2a38281432f568f6b981ad4bf46093c3adbbedb979bc5b6e589e2d1/cyclopts-4.1.0-py3-none-any.whl", hash = "sha256:6468e7e7467af4b6378bf17d0aaf204b713ddc5df383d9ffa7cae6e285da1329", size = 182506, upload-time = "2025-10-28T18:23:15.007Z" }, ] -[[package]] -name = "diskcache" -version = "5.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, -] - [[package]] name = "dnspython" version = "2.8.0" @@ -377,27 +399,33 @@ wheels = [ [[package]] name = "fastmcp" -version = "2.13.0" +version = "3.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "authlib" }, { name = "cyclopts" }, { name = "exceptiongroup" }, { name = "httpx" }, + { name = "jsonref" }, + { name = "jsonschema-path" }, { name = "mcp" }, - { name = "openapi-core" }, { name = "openapi-pydantic" }, + { name = "opentelemetry-api" }, + { name = "packaging" }, { name = "platformdirs" }, - { name = "py-key-value-aio", extra = ["disk", "keyring", "memory"] }, + { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] }, { name = "pydantic", extra = ["email"] }, { name = "pyperclip" }, { name = "python-dotenv" }, + { name = "pyyaml" }, { name = "rich" }, + { name = "uvicorn" }, + { name = "watchfiles" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bc/3b/c30af894db2c3ec439d0e4168ba7ce705474cabdd0a599033ad9a19ad977/fastmcp-2.13.0.tar.gz", hash = "sha256:57f7b7503363e1babc0d1a13af18252b80366a409e1de85f1256cce66a4bee35", size = 7767346, upload-time = "2025-10-25T12:54:10.957Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/be/beb5d3e485983b9dd122f3f74772bcceeb085ca824e11c52c14ba71cf21a/fastmcp-3.0.0.tar.gz", hash = "sha256:f3b0cfa012f6b2b50b877da181431c6f9a551197f466b0bb7de7f39ceae159a1", size = 16093079, upload-time = "2026-02-18T21:25:34.461Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/7f/09942135f506953fc61bb81b9e5eaf50a8eea923b83d9135bd959168ef2d/fastmcp-2.13.0-py3-none-any.whl", hash = "sha256:bdff1399d3b7ebb79286edfd43eb660182432514a5ab8e4cbfb45f1d841d2aa0", size = 367134, upload-time = "2025-10-25T12:54:09.284Z" }, + { url = "https://files.pythonhosted.org/packages/12/14/05bebaf3764ea71ce6fa9d3fcf870610bbc8b1e7be2505e870d709375316/fastmcp-3.0.0-py3-none-any.whl", hash = "sha256:561d537cb789f995174c5591f1b54f758ce3f82da3cd951ffe51ce18609569e9", size = 603327, upload-time = "2026-02-18T21:25:36.701Z" }, ] [[package]] @@ -467,15 +495,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, ] -[[package]] -name = "isodate" -version = "0.7.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, -] - [[package]] name = "jaraco-classes" version = "3.4.0" @@ -521,6 +540,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, ] +[[package]] +name = "jsonref" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", size = 8814, upload-time = "2023-01-16T16:10:04.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425, upload-time = "2023-01-16T16:10:02.255Z" }, +] + [[package]] name = "jsonschema" version = "4.25.1" @@ -581,51 +609,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085, upload-time = "2024-12-25T15:26:44.377Z" }, ] -[[package]] -name = "lazy-object-proxy" -version = "1.12.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/08/a2/69df9c6ba6d316cfd81fe2381e464db3e6de5db45f8c43c6a23504abf8cb/lazy_object_proxy-1.12.0.tar.gz", hash = "sha256:1f5a462d92fd0cfb82f1fab28b51bfb209fabbe6aabf7f0d51472c0c124c0c61", size = 43681, upload-time = "2025-08-22T13:50:06.783Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/2b/d5e8915038acbd6c6a9fcb8aaf923dc184222405d3710285a1fec6e262bc/lazy_object_proxy-1.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61d5e3310a4aa5792c2b599a7a78ccf8687292c8eb09cf187cca8f09cf6a7519", size = 26658, upload-time = "2025-08-22T13:42:23.373Z" }, - { url = "https://files.pythonhosted.org/packages/da/8f/91fc00eeea46ee88b9df67f7c5388e60993341d2a406243d620b2fdfde57/lazy_object_proxy-1.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ca33565f698ac1aece152a10f432415d1a2aa9a42dfe23e5ba2bc255ab91f6", size = 68412, upload-time = "2025-08-22T13:42:24.727Z" }, - { url = "https://files.pythonhosted.org/packages/07/d2/b7189a0e095caedfea4d42e6b6949d2685c354263bdf18e19b21ca9b3cd6/lazy_object_proxy-1.12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01c7819a410f7c255b20799b65d36b414379a30c6f1684c7bd7eb6777338c1b", size = 67559, upload-time = "2025-08-22T13:42:25.875Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ad/b013840cc43971582ff1ceaf784d35d3a579650eb6cc348e5e6ed7e34d28/lazy_object_proxy-1.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:029d2b355076710505c9545aef5ab3f750d89779310e26ddf2b7b23f6ea03cd8", size = 66651, upload-time = "2025-08-22T13:42:27.427Z" }, - { url = "https://files.pythonhosted.org/packages/7e/6f/b7368d301c15612fcc4cd00412b5d6ba55548bde09bdae71930e1a81f2ab/lazy_object_proxy-1.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc6e3614eca88b1c8a625fc0a47d0d745e7c3255b21dac0e30b3037c5e3deeb8", size = 66901, upload-time = "2025-08-22T13:42:28.585Z" }, - { url = "https://files.pythonhosted.org/packages/61/1b/c6b1865445576b2fc5fa0fbcfce1c05fee77d8979fd1aa653dd0f179aefc/lazy_object_proxy-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:be5fe974e39ceb0d6c9db0663c0464669cf866b2851c73971409b9566e880eab", size = 26536, upload-time = "2025-08-22T13:42:29.636Z" }, - { url = "https://files.pythonhosted.org/packages/01/b3/4684b1e128a87821e485f5a901b179790e6b5bc02f89b7ee19c23be36ef3/lazy_object_proxy-1.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1cf69cd1a6c7fe2dbcc3edaa017cf010f4192e53796538cc7d5e1fedbfa4bcff", size = 26656, upload-time = "2025-08-22T13:42:30.605Z" }, - { url = "https://files.pythonhosted.org/packages/3a/03/1bdc21d9a6df9ff72d70b2ff17d8609321bea4b0d3cffd2cea92fb2ef738/lazy_object_proxy-1.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efff4375a8c52f55a145dc8487a2108c2140f0bec4151ab4e1843e52eb9987ad", size = 68832, upload-time = "2025-08-22T13:42:31.675Z" }, - { url = "https://files.pythonhosted.org/packages/3d/4b/5788e5e8bd01d19af71e50077ab020bc5cce67e935066cd65e1215a09ff9/lazy_object_proxy-1.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1192e8c2f1031a6ff453ee40213afa01ba765b3dc861302cd91dbdb2e2660b00", size = 69148, upload-time = "2025-08-22T13:42:32.876Z" }, - { url = "https://files.pythonhosted.org/packages/79/0e/090bf070f7a0de44c61659cb7f74c2fe02309a77ca8c4b43adfe0b695f66/lazy_object_proxy-1.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3605b632e82a1cbc32a1e5034278a64db555b3496e0795723ee697006b980508", size = 67800, upload-time = "2025-08-22T13:42:34.054Z" }, - { url = "https://files.pythonhosted.org/packages/cf/d2/b320325adbb2d119156f7c506a5fbfa37fcab15c26d13cf789a90a6de04e/lazy_object_proxy-1.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a61095f5d9d1a743e1e20ec6d6db6c2ca511961777257ebd9b288951b23b44fa", size = 68085, upload-time = "2025-08-22T13:42:35.197Z" }, - { url = "https://files.pythonhosted.org/packages/6a/48/4b718c937004bf71cd82af3713874656bcb8d0cc78600bf33bb9619adc6c/lazy_object_proxy-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:997b1d6e10ecc6fb6fe0f2c959791ae59599f41da61d652f6c903d1ee58b7370", size = 26535, upload-time = "2025-08-22T13:42:36.521Z" }, - { url = "https://files.pythonhosted.org/packages/0d/1b/b5f5bd6bda26f1e15cd3232b223892e4498e34ec70a7f4f11c401ac969f1/lazy_object_proxy-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ee0d6027b760a11cc18281e702c0309dd92da458a74b4c15025d7fc490deede", size = 26746, upload-time = "2025-08-22T13:42:37.572Z" }, - { url = "https://files.pythonhosted.org/packages/55/64/314889b618075c2bfc19293ffa9153ce880ac6153aacfd0a52fcabf21a66/lazy_object_proxy-1.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4ab2c584e3cc8be0dfca422e05ad30a9abe3555ce63e9ab7a559f62f8dbc6ff9", size = 71457, upload-time = "2025-08-22T13:42:38.743Z" }, - { url = "https://files.pythonhosted.org/packages/11/53/857fc2827fc1e13fbdfc0ba2629a7d2579645a06192d5461809540b78913/lazy_object_proxy-1.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14e348185adbd03ec17d051e169ec45686dcd840a3779c9d4c10aabe2ca6e1c0", size = 71036, upload-time = "2025-08-22T13:42:40.184Z" }, - { url = "https://files.pythonhosted.org/packages/2b/24/e581ffed864cd33c1b445b5763d617448ebb880f48675fc9de0471a95cbc/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4fcbe74fb85df8ba7825fa05eddca764138da752904b378f0ae5ab33a36c308", size = 69329, upload-time = "2025-08-22T13:42:41.311Z" }, - { url = "https://files.pythonhosted.org/packages/78/be/15f8f5a0b0b2e668e756a152257d26370132c97f2f1943329b08f057eff0/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:563d2ec8e4d4b68ee7848c5ab4d6057a6d703cb7963b342968bb8758dda33a23", size = 70690, upload-time = "2025-08-22T13:42:42.51Z" }, - { url = "https://files.pythonhosted.org/packages/5d/aa/f02be9bbfb270e13ee608c2b28b8771f20a5f64356c6d9317b20043c6129/lazy_object_proxy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:53c7fd99eb156bbb82cbc5d5188891d8fdd805ba6c1e3b92b90092da2a837073", size = 26563, upload-time = "2025-08-22T13:42:43.685Z" }, - { url = "https://files.pythonhosted.org/packages/f4/26/b74c791008841f8ad896c7f293415136c66cc27e7c7577de4ee68040c110/lazy_object_proxy-1.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:86fd61cb2ba249b9f436d789d1356deae69ad3231dc3c0f17293ac535162672e", size = 26745, upload-time = "2025-08-22T13:42:44.982Z" }, - { url = "https://files.pythonhosted.org/packages/9b/52/641870d309e5d1fb1ea7d462a818ca727e43bfa431d8c34b173eb090348c/lazy_object_proxy-1.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81d1852fb30fab81696f93db1b1e55a5d1ff7940838191062f5f56987d5fcc3e", size = 71537, upload-time = "2025-08-22T13:42:46.141Z" }, - { url = "https://files.pythonhosted.org/packages/47/b6/919118e99d51c5e76e8bf5a27df406884921c0acf2c7b8a3b38d847ab3e9/lazy_object_proxy-1.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9045646d83f6c2664c1330904b245ae2371b5c57a3195e4028aedc9f999655", size = 71141, upload-time = "2025-08-22T13:42:47.375Z" }, - { url = "https://files.pythonhosted.org/packages/e5/47/1d20e626567b41de085cf4d4fb3661a56c159feaa73c825917b3b4d4f806/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:67f07ab742f1adfb3966c40f630baaa7902be4222a17941f3d85fd1dae5565ff", size = 69449, upload-time = "2025-08-22T13:42:48.49Z" }, - { url = "https://files.pythonhosted.org/packages/58/8d/25c20ff1a1a8426d9af2d0b6f29f6388005fc8cd10d6ee71f48bff86fdd0/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ba769017b944fcacbf6a80c18b2761a1795b03f8899acdad1f1c39db4409be", size = 70744, upload-time = "2025-08-22T13:42:49.608Z" }, - { url = "https://files.pythonhosted.org/packages/c0/67/8ec9abe15c4f8a4bcc6e65160a2c667240d025cbb6591b879bea55625263/lazy_object_proxy-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:7b22c2bbfb155706b928ac4d74c1a63ac8552a55ba7fff4445155523ea4067e1", size = 26568, upload-time = "2025-08-22T13:42:57.719Z" }, - { url = "https://files.pythonhosted.org/packages/23/12/cd2235463f3469fd6c62d41d92b7f120e8134f76e52421413a0ad16d493e/lazy_object_proxy-1.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4a79b909aa16bde8ae606f06e6bbc9d3219d2e57fb3e0076e17879072b742c65", size = 27391, upload-time = "2025-08-22T13:42:50.62Z" }, - { url = "https://files.pythonhosted.org/packages/60/9e/f1c53e39bbebad2e8609c67d0830cc275f694d0ea23d78e8f6db526c12d3/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:338ab2f132276203e404951205fe80c3fd59429b3a724e7b662b2eb539bb1be9", size = 80552, upload-time = "2025-08-22T13:42:51.731Z" }, - { url = "https://files.pythonhosted.org/packages/4c/b6/6c513693448dcb317d9d8c91d91f47addc09553613379e504435b4cc8b3e/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c40b3c9faee2e32bfce0df4ae63f4e73529766893258eca78548bac801c8f66", size = 82857, upload-time = "2025-08-22T13:42:53.225Z" }, - { url = "https://files.pythonhosted.org/packages/12/1c/d9c4aaa4c75da11eb7c22c43d7c90a53b4fca0e27784a5ab207768debea7/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:717484c309df78cedf48396e420fa57fc8a2b1f06ea889df7248fdd156e58847", size = 80833, upload-time = "2025-08-22T13:42:54.391Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ae/29117275aac7d7d78ae4f5a4787f36ff33262499d486ac0bf3e0b97889f6/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a6b7ea5ea1ffe15059eb44bcbcb258f97bcb40e139b88152c40d07b1a1dfc9ac", size = 79516, upload-time = "2025-08-22T13:42:55.812Z" }, - { url = "https://files.pythonhosted.org/packages/19/40/b4e48b2c38c69392ae702ae7afa7b6551e0ca5d38263198b7c79de8b3bdf/lazy_object_proxy-1.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:08c465fb5cd23527512f9bd7b4c7ba6cec33e28aad36fbbe46bf7b858f9f3f7f", size = 27656, upload-time = "2025-08-22T13:42:56.793Z" }, - { url = "https://files.pythonhosted.org/packages/ef/3a/277857b51ae419a1574557c0b12e0d06bf327b758ba94cafc664cb1e2f66/lazy_object_proxy-1.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c9defba70ab943f1df98a656247966d7729da2fe9c2d5d85346464bf320820a3", size = 26582, upload-time = "2025-08-22T13:49:49.366Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b6/c5e0fa43535bb9c87880e0ba037cdb1c50e01850b0831e80eb4f4762f270/lazy_object_proxy-1.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6763941dbf97eea6b90f5b06eb4da9418cc088fce0e3883f5816090f9afcde4a", size = 71059, upload-time = "2025-08-22T13:49:50.488Z" }, - { url = "https://files.pythonhosted.org/packages/06/8a/7dcad19c685963c652624702f1a968ff10220b16bfcc442257038216bf55/lazy_object_proxy-1.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fdc70d81235fc586b9e3d1aeef7d1553259b62ecaae9db2167a5d2550dcc391a", size = 71034, upload-time = "2025-08-22T13:49:54.224Z" }, - { url = "https://files.pythonhosted.org/packages/12/ac/34cbfb433a10e28c7fd830f91c5a348462ba748413cbb950c7f259e67aa7/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0a83c6f7a6b2bfc11ef3ed67f8cbe99f8ff500b05655d8e7df9aab993a6abc95", size = 69529, upload-time = "2025-08-22T13:49:55.29Z" }, - { url = "https://files.pythonhosted.org/packages/6f/6a/11ad7e349307c3ca4c0175db7a77d60ce42a41c60bcb11800aabd6a8acb8/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:256262384ebd2a77b023ad02fbcc9326282bcfd16484d5531154b02bc304f4c5", size = 70391, upload-time = "2025-08-22T13:49:56.35Z" }, - { url = "https://files.pythonhosted.org/packages/59/97/9b410ed8fbc6e79c1ee8b13f8777a80137d4bc189caf2c6202358e66192c/lazy_object_proxy-1.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7601ec171c7e8584f8ff3f4e440aa2eebf93e854f04639263875b8c2971f819f", size = 26988, upload-time = "2025-08-22T13:49:57.302Z" }, - { url = "https://files.pythonhosted.org/packages/41/a0/b91504515c1f9a299fc157967ffbd2f0321bce0516a3d5b89f6f4cad0355/lazy_object_proxy-1.12.0-pp39.pp310.pp311.graalpy311-none-any.whl", hash = "sha256:c3b2e0af1f7f77c4263759c4824316ce458fabe0fceadcd24ef8ca08b2d1e402", size = 15072, upload-time = "2025-08-22T13:50:05.498Z" }, -] - [[package]] name = "markdown-it-py" version = "3.0.0" @@ -638,94 +621,9 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, ] -[[package]] -name = "markupsafe" -version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, - { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, - { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, - { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, - { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, - { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, - { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, - { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, - { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, - { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, - { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, - { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, - { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, - { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, - { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, - { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, - { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, - { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, - { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, - { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, - { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, - { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, - { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, - { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, - { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, - { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, - { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, - { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, - { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, - { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, - { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, - { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, - { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, - { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, - { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, - { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, - { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, - { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, - { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, - { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, - { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, -] - [[package]] name = "mcp" -version = "1.19.0" +version = "1.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -734,15 +632,18 @@ dependencies = [ { name = "jsonschema" }, { name = "pydantic" }, { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, { name = "python-multipart" }, { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "sse-starlette" }, { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/2b/916852a5668f45d8787378461eaa1244876d77575ffef024483c94c0649c/mcp-1.19.0.tar.gz", hash = "sha256:213de0d3cd63f71bc08ffe9cc8d4409cc87acffd383f6195d2ce0457c021b5c1", size = 444163, upload-time = "2025-10-24T01:11:15.839Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/a3/3e71a875a08b6a830b88c40bc413bff01f1650f1efe8a054b5e90a9d4f56/mcp-1.19.0-py3-none-any.whl", hash = "sha256:f5907fe1c0167255f916718f376d05f09a830a215327a3ccdd5ec8a519f2e572", size = 170105, upload-time = "2025-10-24T01:11:14.151Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, ] [[package]] @@ -754,7 +655,7 @@ dependencies = [ ] [package.metadata] -requires-dist = [{ name = "fastmcp", specifier = "==2.13.0" }] +requires-dist = [{ name = "fastmcp", specifier = "==3.0.0" }] [[package]] name = "mdurl" @@ -774,26 +675,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, ] -[[package]] -name = "openapi-core" -version = "0.19.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "isodate" }, - { name = "jsonschema" }, - { name = "jsonschema-path" }, - { name = "more-itertools" }, - { name = "openapi-schema-validator" }, - { name = "openapi-spec-validator" }, - { name = "parse" }, - { name = "typing-extensions" }, - { name = "werkzeug" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/35/1acaa5f2fcc6e54eded34a2ec74b479439c4e469fc4e8d0e803fda0234db/openapi_core-0.19.5.tar.gz", hash = "sha256:421e753da56c391704454e66afe4803a290108590ac8fa6f4a4487f4ec11f2d3", size = 103264, upload-time = "2025-03-20T20:17:28.193Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/6f/83ead0e2e30a90445ee4fc0135f43741aebc30cca5b43f20968b603e30b6/openapi_core-0.19.5-py3-none-any.whl", hash = "sha256:ef7210e83a59394f46ce282639d8d26ad6fc8094aa904c9c16eb1bac8908911f", size = 106595, upload-time = "2025-03-20T20:17:26.77Z" }, -] - [[package]] name = "openapi-pydantic" version = "0.5.1" @@ -807,41 +688,25 @@ wheels = [ ] [[package]] -name = "openapi-schema-validator" -version = "0.6.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jsonschema" }, - { name = "jsonschema-specifications" }, - { name = "rfc3339-validator" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8b/f3/5507ad3325169347cd8ced61c232ff3df70e2b250c49f0fe140edb4973c6/openapi_schema_validator-0.6.3.tar.gz", hash = "sha256:f37bace4fc2a5d96692f4f8b31dc0f8d7400fd04f3a937798eaf880d425de6ee", size = 11550, upload-time = "2025-01-10T18:08:22.268Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/21/c6/ad0fba32775ae749016829dace42ed80f4407b171da41313d1a3a5f102e4/openapi_schema_validator-0.6.3-py3-none-any.whl", hash = "sha256:f3b9870f4e556b5a62a1c39da72a6b4b16f3ad9c73dc80084b1b11e74ba148a3", size = 8755, upload-time = "2025-01-10T18:08:19.758Z" }, -] - -[[package]] -name = "openapi-spec-validator" -version = "0.7.2" +name = "opentelemetry-api" +version = "1.39.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jsonschema" }, - { name = "jsonschema-path" }, - { name = "lazy-object-proxy" }, - { name = "openapi-schema-validator" }, + { name = "importlib-metadata" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/82/af/fe2d7618d6eae6fb3a82766a44ed87cd8d6d82b4564ed1c7cfb0f6378e91/openapi_spec_validator-0.7.2.tar.gz", hash = "sha256:cc029309b5c5dbc7859df0372d55e9d1ff43e96d678b9ba087f7c56fc586f734", size = 36855, upload-time = "2025-06-07T14:48:56.299Z" } +sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/dd/b3fd642260cb17532f66cc1e8250f3507d1e580483e209dc1e9d13bd980d/openapi_spec_validator-0.7.2-py3-none-any.whl", hash = "sha256:4bbdc0894ec85f1d1bea1d6d9c8b2c3c8d7ccaa13577ef40da9c006c9fd0eb60", size = 39713, upload-time = "2025-06-07T14:48:54.077Z" }, + { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, ] [[package]] -name = "parse" -version = "1.20.2" +name = "packaging" +version = "26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4f/78/d9b09ba24bb36ef8b83b71be547e118d46214735b6dfb39e4bfde0e9b9dd/parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce", size = 29391, upload-time = "2024-06-11T04:41:57.34Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/31/ba45bf0b2aa7898d81cbbfac0e88c267befb59ad91a19e36e1bc5578ddb1/parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558", size = 20126, upload-time = "2024-06-11T04:41:55.057Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] @@ -853,15 +718,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592, upload-time = "2025-01-10T18:43:11.88Z" }, ] -[[package]] -name = "pathvalidate" -version = "3.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fa/2a/52a8da6fe965dea6192eb716b357558e103aea0a1e9a8352ad575a8406ca/pathvalidate-3.3.1.tar.gz", hash = "sha256:b18c07212bfead624345bb8e1d6141cdcf15a39736994ea0b94035ad2b1ba177", size = 63262, upload-time = "2025-06-15T09:07:20.736Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/70/875f4a23bfc4731703a5835487d0d2fb999031bd415e7d17c0ae615c18b7/pathvalidate-3.3.1-py3-none-any.whl", hash = "sha256:5263baab691f8e1af96092fa5137ee17df5bdfbd6cff1fcac4d6ef4bc2e1735f", size = 24305, upload-time = "2025-06-15T09:07:19.117Z" }, -] - [[package]] name = "platformdirs" version = "4.5.0" @@ -873,21 +729,21 @@ wheels = [ [[package]] name = "py-key-value-aio" -version = "0.2.8" +version = "0.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beartype" }, - { name = "py-key-value-shared" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/35/65310a4818acec0f87a46e5565e341c5a96fc062a9a03495ad28828ff4d7/py_key_value_aio-0.2.8.tar.gz", hash = "sha256:c0cfbb0bd4e962a3fa1a9fa6db9ba9df812899bd9312fa6368aaea7b26008b36", size = 32853, upload-time = "2025-10-24T13:31:04.688Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/3c/0397c072a38d4bc580994b42e0c90c5f44f679303489e4376289534735e5/py_key_value_aio-0.4.4.tar.gz", hash = "sha256:e3012e6243ed7cc09bb05457bd4d03b1ba5c2b1ca8700096b3927db79ffbbe55", size = 92300, upload-time = "2026-02-16T21:21:43.245Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/5a/e56747d87a97ad2aff0f3700d77f186f0704c90c2da03bfed9e113dae284/py_key_value_aio-0.2.8-py3-none-any.whl", hash = "sha256:561565547ce8162128fd2bd0b9d70ce04a5f4586da8500cce79a54dfac78c46a", size = 69200, upload-time = "2025-10-24T13:31:03.81Z" }, + { url = "https://files.pythonhosted.org/packages/32/69/f1b537ee70b7def42d63124a539ed3026a11a3ffc3086947a1ca6e861868/py_key_value_aio-0.4.4-py3-none-any.whl", hash = "sha256:18e17564ecae61b987f909fc2cd41ee2012c84b4b1dcb8c055cf8b4bc1bf3f5d", size = 152291, upload-time = "2026-02-16T21:21:44.241Z" }, ] [package.optional-dependencies] -disk = [ - { name = "diskcache" }, - { name = "pathvalidate" }, +filetree = [ + { name = "aiofile" }, + { name = "anyio" }, ] keyring = [ { name = "keyring" }, @@ -896,19 +752,6 @@ memory = [ { name = "cachetools" }, ] -[[package]] -name = "py-key-value-shared" -version = "0.2.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "beartype" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/26/79/05a1f9280cfa0709479319cbfd2b1c5beb23d5034624f548c83fb65b0b61/py_key_value_shared-0.2.8.tar.gz", hash = "sha256:703b4d3c61af124f0d528ba85995c3c8d78f8bd3d2b217377bd3278598070cc1", size = 8216, upload-time = "2025-10-24T13:31:03.601Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/7a/1726ceaa3343874f322dd83c9ec376ad81f533df8422b8b1e1233a59f8ce/py_key_value_shared-0.2.8-py3-none-any.whl", hash = "sha256:aff1bbfd46d065b2d67897d298642e80e5349eae588c6d11b48452b46b8d46ba", size = 14586, upload-time = "2025-10-24T13:31:02.838Z" }, -] - [[package]] name = "pycparser" version = "2.22" @@ -1075,6 +918,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, ] +[[package]] +name = "pyjwt" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + [[package]] name = "pyperclip" version = "1.11.0" @@ -1226,18 +1083,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] -[[package]] -name = "rfc3339-validator" -version = "0.1.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, -] - [[package]] name = "rich" version = "14.0.0" @@ -1400,15 +1245,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/91/ff/2e2eed29e02c14a5cb6c57f09b2d5b40e65d6cc71f45b52e0be295ccbc2f/secretstorage-3.4.0-py3-none-any.whl", hash = "sha256:0e3b6265c2c63509fb7415717607e4b2c9ab767b7f344a57473b779ca13bd02e", size = 15272, upload-time = "2025-09-09T16:42:12.744Z" }, ] -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - [[package]] name = "sniffio" version = "1.3.1" @@ -1523,16 +1359,119 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.34.3" +version = "0.41.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/ad/713be230bcda622eaa35c28f0d328c3675c371238470abdea52417f17a8e/uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a", size = 76631, upload-time = "2025-06-01T07:48:17.531Z" } +sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/0d/8adfeaa62945f90d19ddc461c55f4a50c258af7662d34b6a3d5d1f8646f6/uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885", size = 62431, upload-time = "2025-06-01T07:48:15.664Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, + { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, + { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, ] [[package]] @@ -1594,18 +1533,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, ] -[[package]] -name = "werkzeug" -version = "3.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/32/af/d4502dc713b4ccea7175d764718d5183caf8d0867a4f0190d5d4a45cea49/werkzeug-3.1.1.tar.gz", hash = "sha256:8cd39dfbdfc1e051965f156163e2974e52c210f130810e9ad36858f0fd3edad4", size = 806453, upload-time = "2024-11-01T16:40:45.462Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/ea/c67e1dee1ba208ed22c06d1d547ae5e293374bfc43e0eb0ef5e262b68561/werkzeug-3.1.1-py3-none-any.whl", hash = "sha256:a71124d1ef06008baafa3d266c02f56e1836a5984afd6dd6c9230669d60d9fb5", size = 224371, upload-time = "2024-11-01T16:40:43.994Z" }, -] - [[package]] name = "zipp" version = "3.23.0" diff --git a/videointelligence/samples/analyze/resources/README.md b/videointelligence/samples/analyze/resources/README.md index 1acbef1484a..74f763d8f94 100644 --- a/videointelligence/samples/analyze/resources/README.md +++ b/videointelligence/samples/analyze/resources/README.md @@ -9,7 +9,7 @@ Copy from Google Cloud Storage to this folder for testing video analysis of local files. For `cat.mp4` used in the usage example, run the following `gcloud` command. - gsutil cp gs://cloud-samples-data/video/cat.mp4 . + gcloud storage cp gs://cloud-samples-data/video/cat.mp4 . Now, when you run the following command, the video used for label detection will be passed from here: diff --git a/vision/snippets/detect/detect.py b/vision/snippets/detect/detect.py index 1cfa698d747..271ad1b9a53 100644 --- a/vision/snippets/detect/detect.py +++ b/vision/snippets/detect/detect.py @@ -266,7 +266,12 @@ def detect_logos(path): image = vision.Image(content=content) - response = client.logo_detection(image=image) + request = { + "image": image, + "features": [{"type_": vision.Feature.Type.LOGO_DETECTION}], + } + + response = client.annotate_image(request=request) logos = response.logo_annotations print("Logos:") @@ -293,7 +298,13 @@ def detect_logos_uri(uri): image = vision.Image() image.source.image_uri = uri - response = client.logo_detection(image=image) + request = { + "image": image, + "features": [{"type_": vision.Feature.Type.LOGO_DETECTION}], + } + + response = client.annotate_image(request=request) + logos = response.logo_annotations print("Logos:")